Expand and collapse dataframe cells (#10463)

* - truncate long cell values
- add expand and collapse logic

* add changeset

* tweak

* add max_chars and single click expanding

* - fix test
- add story

* Update gradio/components/dataframe.py

Co-authored-by: Abubakar Abid <abubakar@huggingface.co>

---------

Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com>
Co-authored-by: Abubakar Abid <abubakar@huggingface.co>
This commit is contained in:
Hannah 2025-02-03 22:37:34 +00:00 committed by GitHub
parent ff5f976bbb
commit ed7a0919ab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 109 additions and 14 deletions

View File

@ -0,0 +1,6 @@
---
"@gradio/dataframe": minor
"gradio": minor
---
feat:Expand and collapse dataframe cells

View File

@ -99,6 +99,7 @@ class Dataframe(Component):
show_fullscreen_button: bool = False, show_fullscreen_button: bool = False,
show_copy_button: bool = False, show_copy_button: bool = False,
show_row_numbers: bool = False, show_row_numbers: bool = False,
max_chars: int | None = None,
): ):
""" """
Parameters: Parameters:
@ -129,6 +130,7 @@ class Dataframe(Component):
show_fullscreen_button: If True, will show a button to view the values in the table in fullscreen mode. show_fullscreen_button: If True, will show a button to view the values in the table in fullscreen mode.
show_copy_button: If True, will show a button to copy the table data to the clipboard. show_copy_button: If True, will show a button to copy the table data to the clipboard.
show_row_numbers: If True, will display row numbers in a separate column. show_row_numbers: If True, will display row numbers in a separate column.
max_chars: Maximum number of characters to display in each cell before truncating (single-clicking a cell value will still reveal the full content). If None, no truncation is applied.
""" """
self.wrap = wrap self.wrap = wrap
self.row_count = self.__process_counts(row_count) self.row_count = self.__process_counts(row_count)
@ -165,6 +167,7 @@ class Dataframe(Component):
self.show_fullscreen_button = show_fullscreen_button self.show_fullscreen_button = show_fullscreen_button
self.show_copy_button = show_copy_button self.show_copy_button = show_copy_button
self.show_row_numbers = show_row_numbers self.show_row_numbers = show_row_numbers
self.max_chars = max_chars
super().__init__( super().__init__(
label=label, label=label,
every=every, every=every,

View File

@ -606,6 +606,7 @@ class Numpy(components.Dataframe):
column_widths: list[str | int] | None = None, column_widths: list[str | int] | None = None,
show_row_numbers: bool = False, show_row_numbers: bool = False,
show_fullscreen_button: bool = False, show_fullscreen_button: bool = False,
max_chars: int | None = None,
show_copy_button: bool = False, show_copy_button: bool = False,
): ):
super().__init__( super().__init__(
@ -634,6 +635,7 @@ class Numpy(components.Dataframe):
min_width=min_width, min_width=min_width,
show_row_numbers=show_row_numbers, show_row_numbers=show_row_numbers,
show_fullscreen_button=show_fullscreen_button, show_fullscreen_button=show_fullscreen_button,
max_chars=max_chars,
show_copy_button=show_copy_button, show_copy_button=show_copy_button,
) )
@ -679,6 +681,7 @@ class Matrix(components.Dataframe):
column_widths: list[str | int] | None = None, column_widths: list[str | int] | None = None,
show_row_numbers: bool = False, show_row_numbers: bool = False,
show_fullscreen_button: bool = True, show_fullscreen_button: bool = True,
max_chars: int | None = None,
show_copy_button: bool = False, show_copy_button: bool = False,
): ):
super().__init__( super().__init__(
@ -707,6 +710,7 @@ class Matrix(components.Dataframe):
min_width=min_width, min_width=min_width,
show_row_numbers=show_row_numbers, show_row_numbers=show_row_numbers,
show_fullscreen_button=show_fullscreen_button, show_fullscreen_button=show_fullscreen_button,
max_chars=max_chars,
show_copy_button=show_copy_button, show_copy_button=show_copy_button,
) )
@ -752,6 +756,7 @@ class List(components.Dataframe):
column_widths: list[str | int] | None = None, column_widths: list[str | int] | None = None,
show_row_numbers: bool = False, show_row_numbers: bool = False,
show_fullscreen_button: bool = True, show_fullscreen_button: bool = True,
max_chars: int | None = None,
show_copy_button: bool = False, show_copy_button: bool = False,
): ):
super().__init__( super().__init__(
@ -780,6 +785,7 @@ class List(components.Dataframe):
min_width=min_width, min_width=min_width,
show_row_numbers=show_row_numbers, show_row_numbers=show_row_numbers,
show_fullscreen_button=show_fullscreen_button, show_fullscreen_button=show_fullscreen_button,
max_chars=max_chars,
show_copy_button=show_copy_button, show_copy_button=show_copy_button,
) )

View File

@ -299,6 +299,34 @@
}} }}
/> />
<Story
name="Dataframe with truncated text"
args={{
values: [
[
"This is a very long text that should be truncated",
"Short text",
"Another very long text that needs truncation"
],
[
"Short",
"This text is also quite long and should be truncated as well",
"Medium length text here"
],
[
"Medium text",
"Brief",
"This is the longest text in the entire table and it should definitely be truncated"
]
],
headers: ["Column A", "Column B", "Column C"],
label: "Truncated Text Example",
max_chars: 20,
col_count: [3, "dynamic"],
row_count: [3, "dynamic"]
}}
/>
<Story <Story
name="Dataframe with multiline headers" name="Dataframe with multiline headers"
args={{ args={{

View File

@ -49,6 +49,7 @@
export let loading_status: LoadingStatus; export let loading_status: LoadingStatus;
export let interactive: boolean; export let interactive: boolean;
export let show_fullscreen_button = false; export let show_fullscreen_button = false;
export let max_chars: number | undefined = undefined;
export let show_copy_button = false; export let show_copy_button = false;
$: _headers = [...(value.headers || headers)]; $: _headers = [...(value.headers || headers)];
@ -106,6 +107,7 @@
stream_handler={(...args) => gradio.client.stream(...args)} stream_handler={(...args) => gradio.client.stream(...args)}
bind:value_is_output bind:value_is_output
{show_fullscreen_button} {show_fullscreen_button}
{max_chars}
{show_copy_button} {show_copy_button}
/> />
</Block> </Block>

View File

@ -23,12 +23,27 @@
export let line_breaks = true; export let line_breaks = true;
export let editable = true; export let editable = true;
export let root: string; export let root: string;
export let max_chars: number | null = null;
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
let is_expanded = false;
export let el: HTMLInputElement | null; export let el: HTMLInputElement | null;
$: _value = value; $: _value = value;
function truncate_text(
text: string | number,
max_length: number | null = null
): string {
const str = String(text);
if (!max_length || str.length <= max_length) return str;
return str.slice(0, max_length) + "...";
}
$: display_text = is_expanded
? value
: truncate_text(display_value || value, max_chars);
function use_focus(node: HTMLInputElement): any { function use_focus(node: HTMLInputElement): any {
if (clear_on_focus) { if (clear_on_focus) {
_value = ""; _value = "";
@ -52,11 +67,21 @@
function handle_keydown(event: KeyboardEvent): void { function handle_keydown(event: KeyboardEvent): void {
if (event.key === "Enter") { if (event.key === "Enter") {
value = _value; if (edit) {
dispatch("blur"); value = _value;
dispatch("blur");
} else if (!header) {
is_expanded = !is_expanded;
}
} }
dispatch("keydown", event); dispatch("keydown", event);
} }
function handle_click(): void {
if (!edit && !header) {
is_expanded = !is_expanded;
}
}
</script> </script>
{#if edit} {#if edit}
@ -76,28 +101,31 @@
{/if} {/if}
<span <span
on:dblclick on:click={handle_click}
tabindex="-1" on:keydown={handle_keydown}
tabindex="0"
role="button" role="button"
class:edit class:edit
class:expanded={is_expanded}
class:multiline={header} class:multiline={header}
on:focus|preventDefault on:focus|preventDefault
style={styling} style={styling}
class="table-cell-text" class="table-cell-text"
data-editable={editable}
placeholder=" " placeholder=" "
> >
{#if datatype === "html"} {#if datatype === "html"}
{@html value} {@html display_text}
{:else if datatype === "markdown"} {:else if datatype === "markdown"}
<MarkdownCode <MarkdownCode
message={value.toLocaleString()} message={display_text.toLocaleString()}
{latex_delimiters} {latex_delimiters}
{line_breaks} {line_breaks}
chatbot={false} chatbot={false}
{root} {root}
/> />
{:else} {:else}
{editable ? value : display_value || value} {editable ? display_text : display_value || display_text}
{/if} {/if}
</span> </span>
@ -118,6 +146,8 @@
span { span {
flex: 1 1 0%; flex: 1 1 0%;
position: relative;
display: inline-block;
outline: none; outline: none;
padding: var(--size-2); padding: var(--size-2);
-webkit-user-select: text; -webkit-user-select: text;
@ -125,6 +155,19 @@
-ms-user-select: text; -ms-user-select: text;
user-select: text; user-select: text;
cursor: text; cursor: text;
width: 100%;
height: 100%;
}
input:where(:not(.header), [data-editable="true"]) {
width: calc(100% - var(--size-10));
}
span.expanded {
height: auto;
min-height: 100%;
white-space: pre-wrap;
word-break: break-word;
white-space: normal; white-space: normal;
} }
@ -135,6 +178,8 @@
.header { .header {
transform: translateX(0); transform: translateX(0);
font-weight: var(--weight-bold); font-weight: var(--weight-bold);
white-space: normal;
word-break: break-word;
} }
.edit { .edit {

View File

@ -57,6 +57,7 @@
export let show_fullscreen_button = false; export let show_fullscreen_button = false;
export let show_copy_button = false; export let show_copy_button = false;
export let value_is_output = false; export let value_is_output = false;
export let max_chars: number | undefined = undefined;
let selected_cells: CellCoordinate[] = []; let selected_cells: CellCoordinate[] = [];
$: selected_cells = [...selected_cells]; $: selected_cells = [...selected_cells];
@ -912,6 +913,7 @@
<div class="cell-wrap"> <div class="cell-wrap">
<div class="header-content"> <div class="header-content">
<EditableCell <EditableCell
{max_chars}
bind:value={_headers[i].value} bind:value={_headers[i].value}
bind:el={els[id].input} bind:el={els[id].input}
{latex_delimiters} {latex_delimiters}
@ -1012,6 +1014,7 @@
}} }}
{clear_on_focus} {clear_on_focus}
{root} {root}
{max_chars}
/> />
{#if editable && should_show_cell_menu([index, j], selected_cells, editable)} {#if editable && should_show_cell_menu([index, j], selected_cells, editable)}
<button <button
@ -1216,11 +1219,11 @@
.cell-wrap { .cell-wrap {
display: flex; display: flex;
align-items: center; align-items: flex-start;
outline: none; outline: none;
height: var(--size-full);
min-height: var(--size-9); min-height: var(--size-9);
overflow: hidden; position: relative;
height: auto;
} }
.header-content { .header-content {
@ -1254,10 +1257,10 @@
padding: 0; padding: 0;
margin-right: var(--spacing-sm); margin-right: var(--spacing-sm);
z-index: var(--layer-1); z-index: var(--layer-1);
} position: absolute;
right: var(--size-1);
.cell-menu-button:hover { top: 50%;
background-color: var(--color-bg-hover); transform: translateY(-50%);
} }
.cell-selected .cell-menu-button { .cell-selected .cell-menu-button {

View File

@ -57,6 +57,7 @@ class TestDataframe:
"column_widths": [], "column_widths": [],
"show_fullscreen_button": False, "show_fullscreen_button": False,
"show_copy_button": False, "show_copy_button": False,
"max_chars": None,
} }
dataframe_input = gr.Dataframe() dataframe_input = gr.Dataframe()
output = dataframe_input.preprocess(DataframeData(**x_data)) output = dataframe_input.preprocess(DataframeData(**x_data))
@ -103,6 +104,7 @@ class TestDataframe:
"line_breaks": True, "line_breaks": True,
"column_widths": [], "column_widths": [],
"show_fullscreen_button": False, "show_fullscreen_button": False,
"max_chars": None,
"show_copy_button": False, "show_copy_button": False,
} }