Skip to content

Commit b5ca1dc

Browse files
Refactor gr.Dataframe (#10631)
* allow showing image sin dataframe with image component * add scroll to top button * add changeset * fix truncated text issue + prevent truncating non-text data * refactor with state mgmnt * add changeset * formatting * tweaks * tweak * add e2e tests * notebook * more component extraction, move css to components * type fixes * test fixes * fix test * notebook * add changeset * fix tests * fix test * fix test * remove misc.css file * reset sort * fix z-index over progress bar * css tweak * fix search and add search to e2e test * fix keyboard reactivity issue * add cell selection e2e tests * z-index fixes * ensure unique context ids * fix row number bug * frozen -> pinned * pinned col border tweak * pinned col fix when show_row_numbers is true * header tweak * fix pinned columns clash with column_widths * add row test * tweak * fix tests * test tweaks * add row story * tweaks * redesign selection buttons * fix test * fix test * fix seach issue * allow navigation with arrows when not in editing mode --------- Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com>
1 parent db0e09c commit b5ca1dc

31 files changed

+2901
-1321
lines changed

.changeset/crazy-nails-end.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@gradio/dataframe": patch
3+
"gradio": patch
4+
---
5+
6+
fix:Refactor `gr.Dataframe`

demo/dataframe_events/run.ipynb

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: dataframe_events"]}, {"cell_type": "code", "execution_count": null, "id": "272996653310673477252411125948039410165", "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": "288918539441861185822528903084949547379", "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "import pandas as pd\n", "import numpy as np\n", "\n", "def update_dataframe():\n", " regular_df = pd.DataFrame(np.random.randint(1, 10, size=(5, 5)), columns=pd.Index([str(i) for i in range(5)]))\n", " wide_df = pd.DataFrame([\n", " [5, 22, 91, 17, 73, 38, 84, 46, 65, 10, 155, 122, 11, 144, 133],\n", " [81, 42, 13, 97, 33, 77, 59, 100, 29, 61, 213, 195, 142, 118, 127],\n", " [37, 71, 63, 102, 28, 94, 19, 55, 88, 44, 116, 139, 122, 150, 147],\n", " [104, 52, 49, 26, 83, 67, 31, 92, 79, 18, 241, 115, 159, 123, 137],\n", " [16, 95, 74, 68, 43, 101, 27, 85, 39, 57, 129, 148, 132, 111, 156]\n", " ], columns=pd.Index([f\"col_{i}\" for i in range(15)]))\n", " tall_df = pd.DataFrame(np.random.randint(1, 10, size=(50, 3)), columns=pd.Index([\"A\", \"B\", \"C\"]))\n", " return regular_df, wide_df, tall_df\n", "\n", "def clear_dataframes():\n", " regular_empty_df = pd.DataFrame([], columns=pd.Index([str(i) for i in range(5)]))\n", " wide_empty_df = pd.DataFrame([], columns=pd.Index([f\"col_{i}\" for i in range(15)]))\n", " tall_empty_df = pd.DataFrame([], columns=pd.Index([\"A\", \"B\", \"C\"]))\n", " return regular_empty_df, wide_empty_df, tall_empty_df\n", "\n", "def increment_select_counter(evt: gr.SelectData, count):\n", " count_val = 1 if count is None else count + 1\n", " return count_val, evt.index, evt.value\n", "\n", "with gr.Blocks() as demo:\n", " with gr.Row():\n", " with gr.Column(scale=1):\n", " initial_regular_df = pd.DataFrame(np.zeros((5, 5), dtype=int), columns=pd.Index([str(i) for i in range(5)]))\n", "\n", " df = gr.Dataframe(\n", " value=initial_regular_df,\n", " interactive=True,\n", " label=\"Interactive Dataframe\",\n", " show_label=True,\n", " elem_id=\"dataframe\",\n", " show_search=\"filter\",\n", " show_copy_button=True,\n", " show_row_numbers=True,\n", " )\n", "\n", " with gr.Column(scale=1):\n", " initial_wide_df = pd.DataFrame(np.zeros((5, 15), dtype=int), columns=pd.Index([f\"col_{i}\" for i in range(15)]))\n", "\n", " df_view = gr.Dataframe(\n", " value=initial_wide_df,\n", " interactive=False,\n", " label=\"Non-Interactive View (Scroll Horizontally)\",\n", " show_label=True,\n", " show_search=\"search\",\n", " elem_id=\"non-interactive-dataframe\",\n", " show_copy_button=True,\n", " show_row_numbers=True,\n", " show_fullscreen_button=True,\n", " )\n", "\n", " with gr.Row():\n", " initial_tall_df = pd.DataFrame(np.zeros((50, 3), dtype=int), columns=pd.Index([\"A\", \"B\", \"C\"]))\n", "\n", " df_tall = gr.Dataframe(\n", " value=initial_tall_df,\n", " interactive=False,\n", " label=\"Tall Dataframe (Scroll Vertically)\",\n", " show_label=True,\n", " elem_id=\"dataframe_tall\",\n", " show_copy_button=True,\n", " show_row_numbers=True,\n", " max_height=300,\n", " )\n", "\n", " with gr.Row():\n", " with gr.Column():\n", " update_btn = gr.Button(\"Update dataframe\", elem_id=\"update_btn\")\n", " clear_btn = gr.Button(\"Clear dataframe\", elem_id=\"clear_btn\")\n", "\n", " with gr.Row():\n", " change_events = gr.Number(\n", " value=0, label=\"Change events\", elem_id=\"change_events\"\n", " )\n", " input_events = gr.Number(value=0, label=\"Input events\", elem_id=\"input_events\")\n", " select_events = gr.Number(\n", " value=0, label=\"Select events\", elem_id=\"select_events\"\n", " )\n", "\n", " with gr.Row():\n", " selected_cell_index = gr.Textbox(\n", " label=\"Selected cell index\", elem_id=\"selected_cell_index\"\n", " )\n", " selected_cell_value = gr.Textbox(\n", " label=\"Selected cell value\", elem_id=\"selected_cell_value\"\n", " )\n", "\n", " update_btn.click(fn=update_dataframe, outputs=[df, df_view, df_tall])\n", " clear_btn.click(fn=clear_dataframes, outputs=[df, df_view, df_tall])\n", " df.change(fn=lambda x: x + 1, inputs=[change_events], outputs=[change_events])\n", " df.input(fn=lambda x: x + 1, inputs=[input_events], outputs=[input_events])\n", " df.select(\n", " fn=increment_select_counter,\n", " inputs=[select_events],\n", " outputs=[select_events, selected_cell_index, selected_cell_value],\n", " )\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}

demo/dataframe_events/run.py

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import gradio as gr
2+
import pandas as pd
3+
import numpy as np
4+
5+
def update_dataframe():
6+
regular_df = pd.DataFrame(np.random.randint(1, 10, size=(5, 5)), columns=pd.Index([str(i) for i in range(5)]))
7+
wide_df = pd.DataFrame([
8+
[5, 22, 91, 17, 73, 38, 84, 46, 65, 10, 155, 122, 11, 144, 133],
9+
[81, 42, 13, 97, 33, 77, 59, 100, 29, 61, 213, 195, 142, 118, 127],
10+
[37, 71, 63, 102, 28, 94, 19, 55, 88, 44, 116, 139, 122, 150, 147],
11+
[104, 52, 49, 26, 83, 67, 31, 92, 79, 18, 241, 115, 159, 123, 137],
12+
[16, 95, 74, 68, 43, 101, 27, 85, 39, 57, 129, 148, 132, 111, 156]
13+
], columns=pd.Index([f"col_{i}" for i in range(15)]))
14+
tall_df = pd.DataFrame(np.random.randint(1, 10, size=(50, 3)), columns=pd.Index(["A", "B", "C"]))
15+
return regular_df, wide_df, tall_df
16+
17+
def clear_dataframes():
18+
regular_empty_df = pd.DataFrame([], columns=pd.Index([str(i) for i in range(5)]))
19+
wide_empty_df = pd.DataFrame([], columns=pd.Index([f"col_{i}" for i in range(15)]))
20+
tall_empty_df = pd.DataFrame([], columns=pd.Index(["A", "B", "C"]))
21+
return regular_empty_df, wide_empty_df, tall_empty_df
22+
23+
def increment_select_counter(evt: gr.SelectData, count):
24+
count_val = 1 if count is None else count + 1
25+
return count_val, evt.index, evt.value
26+
27+
with gr.Blocks() as demo:
28+
with gr.Row():
29+
with gr.Column(scale=1):
30+
initial_regular_df = pd.DataFrame(np.zeros((5, 5), dtype=int), columns=pd.Index([str(i) for i in range(5)]))
31+
32+
df = gr.Dataframe(
33+
value=initial_regular_df,
34+
interactive=True,
35+
label="Interactive Dataframe",
36+
show_label=True,
37+
elem_id="dataframe",
38+
show_search="filter",
39+
show_copy_button=True,
40+
show_row_numbers=True,
41+
)
42+
43+
with gr.Column(scale=1):
44+
initial_wide_df = pd.DataFrame(np.zeros((5, 15), dtype=int), columns=pd.Index([f"col_{i}" for i in range(15)]))
45+
46+
df_view = gr.Dataframe(
47+
value=initial_wide_df,
48+
interactive=False,
49+
label="Non-Interactive View (Scroll Horizontally)",
50+
show_label=True,
51+
show_search="search",
52+
elem_id="non-interactive-dataframe",
53+
show_copy_button=True,
54+
show_row_numbers=True,
55+
show_fullscreen_button=True,
56+
)
57+
58+
with gr.Row():
59+
initial_tall_df = pd.DataFrame(np.zeros((50, 3), dtype=int), columns=pd.Index(["A", "B", "C"]))
60+
61+
df_tall = gr.Dataframe(
62+
value=initial_tall_df,
63+
interactive=False,
64+
label="Tall Dataframe (Scroll Vertically)",
65+
show_label=True,
66+
elem_id="dataframe_tall",
67+
show_copy_button=True,
68+
show_row_numbers=True,
69+
max_height=300,
70+
)
71+
72+
with gr.Row():
73+
with gr.Column():
74+
update_btn = gr.Button("Update dataframe", elem_id="update_btn")
75+
clear_btn = gr.Button("Clear dataframe", elem_id="clear_btn")
76+
77+
with gr.Row():
78+
change_events = gr.Number(
79+
value=0, label="Change events", elem_id="change_events"
80+
)
81+
input_events = gr.Number(value=0, label="Input events", elem_id="input_events")
82+
select_events = gr.Number(
83+
value=0, label="Select events", elem_id="select_events"
84+
)
85+
86+
with gr.Row():
87+
selected_cell_index = gr.Textbox(
88+
label="Selected cell index", elem_id="selected_cell_index"
89+
)
90+
selected_cell_value = gr.Textbox(
91+
label="Selected cell value", elem_id="selected_cell_value"
92+
)
93+
94+
update_btn.click(fn=update_dataframe, outputs=[df, df_view, df_tall])
95+
clear_btn.click(fn=clear_dataframes, outputs=[df, df_view, df_tall])
96+
df.change(fn=lambda x: x + 1, inputs=[change_events], outputs=[change_events])
97+
df.input(fn=lambda x: x + 1, inputs=[input_events], outputs=[input_events])
98+
df.select(
99+
fn=increment_select_counter,
100+
inputs=[select_events],
101+
outputs=[select_events, selected_cell_index, selected_cell_value],
102+
)
103+
104+
if __name__ == "__main__":
105+
demo.launch()

gradio/components/dataframe.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def __init__(
7474
headers: list[str] | None = None,
7575
row_count: int | tuple[int, str] = (1, "dynamic"),
7676
col_count: int | tuple[int, str] | None = None,
77-
datatype: Literal["str", "number", "bool", "date", "markdown", "html"]
77+
datatype: Literal["str", "number", "bool", "date", "markdown", "html", "image"]
7878
| Sequence[
7979
Literal["str", "number", "bool", "date", "markdown", "html"]
8080
] = "str",

js/dataframe/Dataframe.stories.svelte

+44-6
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import { userEvent } from "@storybook/test";
77
import { get } from "svelte/store";
88
import { format } from "svelte-i18n";
9+
import Image from "@gradio/image";
910
</script>
1011

1112
<Meta
@@ -219,6 +220,18 @@
219220
}}
220221
/>
221222

223+
<Story
224+
name="Interactive dataframe with zero row count"
225+
args={{
226+
values: [],
227+
headers: ["Narrow", "Wide", "Half"],
228+
label: "Test scores",
229+
col_count: [0, "dynamic"],
230+
row_count: [0, "dynamic"],
231+
editable: true
232+
}}
233+
/>
234+
222235
<Story
223236
name="Dataframe with link"
224237
args={{
@@ -417,6 +430,31 @@
417430
}}
418431
/>
419432

433+
<Story
434+
name="Dataframe with custom components"
435+
args={{
436+
values: [
437+
[
438+
"Absol G",
439+
70,
440+
"https://images.pokemontcg.io/pl3/1_hires.png",
441+
"pl3-1",
442+
"Supreme Victors"
443+
]
444+
],
445+
datatype: ["str", "number", "image", "str", "str"],
446+
headers: ["Pokemon", "HP", "Image", "ID", "Set"],
447+
label: "Pokemon Cards",
448+
col_count: [5, "fixed"],
449+
row_count: [1, "dynamic"],
450+
interactive: true,
451+
editable: true,
452+
components: {
453+
image: Image
454+
}
455+
}}
456+
/>
457+
420458
<Story
421459
name="Dataframe with row and column selection"
422460
args={{
@@ -441,16 +479,16 @@
441479
const cells = canvas.getAllByRole("cell");
442480
await user.click(cells[5]); // Click cell with value 6
443481

444-
const row_button = await canvas.findByRole("button", {
482+
const row_button = await canvas.findAllByRole("button", {
445483
name: "Select row"
446-
});
484+
})[0];
447485
await user.click(row_button);
448486

449487
await user.click(cells[6]);
450488

451-
const col_button = await canvas.findByRole("button", {
489+
const col_button = await canvas.findAllByRole("button", {
452490
name: "Select column"
453-
});
491+
})[0];
454492
await user.click(col_button);
455493

456494
await user.keyboard("{Delete}");
@@ -513,7 +551,7 @@
513551
const canvas = within(canvasElement);
514552
const user = userEvent.setup();
515553

516-
const search_input = canvas.getByPlaceholderText("Search...");
554+
const search_input = canvas.getByPlaceholderText("Filter...");
517555
await user.type(search_input, "Pet");
518556

519557
await new Promise((resolve) => setTimeout(resolve, 100));
@@ -528,7 +566,7 @@
528566
/>
529567

530568
<Story
531-
name="Dataframe with frozen columns"
569+
name="Dataframe with pinned columns"
532570
args={{
533571
values: [
534572
["ID", "Name", "Age", "City", "Country", "Score"],

js/dataframe/Index.svelte

+5-13
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
import { StatusTracker } from "@gradio/statustracker";
1313
import type { LoadingStatus } from "@gradio/statustracker";
1414
import type { Headers, Datatype, DataframeValue } from "./shared/utils";
15+
import Image from "@gradio/image";
16+
1517
export let headers: Headers = [];
1618
export let elem_id = "";
1719
export let elem_classes: string[] = [];
@@ -39,6 +41,7 @@
3941
select: SelectData;
4042
input: never;
4143
clear_status: LoadingStatus;
44+
search: string | null;
4245
}>;
4346
export let latex_delimiters: {
4447
left: string;
@@ -53,17 +56,6 @@
5356
export let show_copy_button = false;
5457
export let show_row_numbers = false;
5558
export let show_search: "none" | "search" | "filter" = "none";
56-
57-
let search_query: string | null = null;
58-
$: filtered_cell_values = search_query
59-
? value.data?.filter((row) =>
60-
row.some(
61-
(cell) =>
62-
search_query &&
63-
String(cell).toLowerCase().includes(search_query.toLowerCase())
64-
)
65-
)
66-
: null;
6759
export let pinned_columns = 0;
6860
6961
$: _headers = [...(value.headers || headers)];
@@ -98,7 +90,7 @@
9890
{show_label}
9991
{row_count}
10092
{col_count}
101-
values={filtered_cell_values || value.data}
93+
values={value.data}
10294
{display_value}
10395
{styling}
10496
headers={_headers}
@@ -109,7 +101,6 @@
109101
}}
110102
on:input={(e) => gradio.dispatch("input")}
111103
on:select={(e) => gradio.dispatch("select", e.detail)}
112-
on:search={(e) => (search_query = e.detail)}
113104
{wrap}
114105
{datatype}
115106
{latex_delimiters}
@@ -127,5 +118,6 @@
127118
{show_row_numbers}
128119
{show_search}
129120
{pinned_columns}
121+
components={{ image: Image }}
130122
/>
131123
</Block>

js/dataframe/shared/CellMenu.svelte

+1-1
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@
8989
<style>
9090
.cell-menu {
9191
position: fixed;
92-
z-index: var(--layer-4);
92+
z-index: 9;
9393
background: var(--background-fill-primary);
9494
border: 1px solid var(--border-color-primary);
9595
border-radius: var(--radius-sm);
+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<script lang="ts">
2+
export let on_click: (event: MouseEvent) => void;
3+
</script>
4+
5+
<button
6+
class="cell-menu-button"
7+
on:click={on_click}
8+
on:touchstart={(event) => {
9+
event.preventDefault();
10+
const touch = event.touches[0];
11+
const mouseEvent = new MouseEvent("click", {
12+
clientX: touch.clientX,
13+
clientY: touch.clientY,
14+
bubbles: true,
15+
cancelable: true,
16+
view: window
17+
});
18+
on_click(mouseEvent);
19+
}}
20+
>
21+
&#8942;
22+
</button>
23+
24+
<style>
25+
.cell-menu-button {
26+
flex-shrink: 0;
27+
display: none;
28+
align-items: center;
29+
justify-content: center;
30+
background-color: var(--block-background-fill);
31+
border: 1px solid var(--border-color-primary);
32+
border-radius: var(--block-radius);
33+
width: var(--size-5);
34+
height: var(--size-5);
35+
min-width: var(--size-5);
36+
padding: 0;
37+
margin-right: var(--spacing-sm);
38+
z-index: 2;
39+
position: absolute;
40+
right: var(--size-1);
41+
top: 50%;
42+
transform: translateY(-50%);
43+
}
44+
</style>

0 commit comments

Comments
 (0)