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_copy_button: bool = False,
show_row_numbers: bool = False,
max_chars: int | None = None,
):
"""
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_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.
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.row_count = self.__process_counts(row_count)
@ -165,6 +167,7 @@ class Dataframe(Component):
self.show_fullscreen_button = show_fullscreen_button
self.show_copy_button = show_copy_button
self.show_row_numbers = show_row_numbers
self.max_chars = max_chars
super().__init__(
label=label,
every=every,

View File

@ -606,6 +606,7 @@ class Numpy(components.Dataframe):
column_widths: list[str | int] | None = None,
show_row_numbers: bool = False,
show_fullscreen_button: bool = False,
max_chars: int | None = None,
show_copy_button: bool = False,
):
super().__init__(
@ -634,6 +635,7 @@ class Numpy(components.Dataframe):
min_width=min_width,
show_row_numbers=show_row_numbers,
show_fullscreen_button=show_fullscreen_button,
max_chars=max_chars,
show_copy_button=show_copy_button,
)
@ -679,6 +681,7 @@ class Matrix(components.Dataframe):
column_widths: list[str | int] | None = None,
show_row_numbers: bool = False,
show_fullscreen_button: bool = True,
max_chars: int | None = None,
show_copy_button: bool = False,
):
super().__init__(
@ -707,6 +710,7 @@ class Matrix(components.Dataframe):
min_width=min_width,
show_row_numbers=show_row_numbers,
show_fullscreen_button=show_fullscreen_button,
max_chars=max_chars,
show_copy_button=show_copy_button,
)
@ -752,6 +756,7 @@ class List(components.Dataframe):
column_widths: list[str | int] | None = None,
show_row_numbers: bool = False,
show_fullscreen_button: bool = True,
max_chars: int | None = None,
show_copy_button: bool = False,
):
super().__init__(
@ -780,6 +785,7 @@ class List(components.Dataframe):
min_width=min_width,
show_row_numbers=show_row_numbers,
show_fullscreen_button=show_fullscreen_button,
max_chars=max_chars,
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
name="Dataframe with multiline headers"
args={{

View File

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

View File

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

View File

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

View File

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