Skip to content

Fix value synchronisation issue in gr.Dataframe #10918

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Apr 5, 2025
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/wet-rockets-bow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@gradio/dataframe": patch
"gradio": patch
---

feat:Fix value synchronisation issue in gr.Dataframe
2 changes: 1 addition & 1 deletion demo/dataframe_events/run.ipynb
Original file line number Diff line number Diff line change
@@ -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", " static_columns=[4]\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}
{"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", " return regular_df, wide_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", " return regular_empty_df, wide_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", " static_columns=[4]\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", " tall_df_value = [\n", " [\"DeepSeek Coder\", 79.3],\n", " [\"Llama 3.3\", 68.9],\n", " [\"Qwen 2.5\", 61.9],\n", " [\"Gemma 2\", 59.5],\n", " [\"GPT 2\", 18.3],\n", " ]\n", "\n", " def get_display_value(values):\n", " display_values = []\n", " medals = [\"\ud83e\udd47\", \"\ud83e\udd48\", \"\ud83e\udd49\"]\n", " for i, row in enumerate(values):\n", " if i < 3:\n", " display_values.append([f\"{medals[i]} {row[0]}\", row[1]])\n", " else:\n", " display_values.append([row[0], row[1]])\n", " return display_values\n", "\n", " display_value = get_display_value(tall_df_value)\n", "\n", " tall_df_value = {\n", " \"data\": tall_df_value,\n", " \"headers\": [\"Model\", \"% Correct (LeetCode Hard)\"],\n", " \"metadata\": {\n", " \"display_value\": display_value\n", " }\n", " }\n", "\n", " with gr.Row():\n", " df_tall = gr.Dataframe(\n", " value=tall_df_value,\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])\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}
40 changes: 32 additions & 8 deletions demo/dataframe_events/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,12 @@ def update_dataframe():
[104, 52, 49, 26, 83, 67, 31, 92, 79, 18, 241, 115, 159, 123, 137],
[16, 95, 74, 68, 43, 101, 27, 85, 39, 57, 129, 148, 132, 111, 156]
], columns=pd.Index([f"col_{i}" for i in range(15)]))
tall_df = pd.DataFrame(np.random.randint(1, 10, size=(50, 3)), columns=pd.Index(["A", "B", "C"]))
return regular_df, wide_df, tall_df
return regular_df, wide_df

def clear_dataframes():
regular_empty_df = pd.DataFrame([], columns=pd.Index([str(i) for i in range(5)]))
wide_empty_df = pd.DataFrame([], columns=pd.Index([f"col_{i}" for i in range(15)]))
tall_empty_df = pd.DataFrame([], columns=pd.Index(["A", "B", "C"]))
return regular_empty_df, wide_empty_df, tall_empty_df
return regular_empty_df, wide_empty_df

def increment_select_counter(evt: gr.SelectData, count):
count_val = 1 if count is None else count + 1
Expand Down Expand Up @@ -56,11 +54,37 @@ def increment_select_counter(evt: gr.SelectData, count):
show_fullscreen_button=True,
)

with gr.Row():
initial_tall_df = pd.DataFrame(np.zeros((50, 3), dtype=int), columns=pd.Index(["A", "B", "C"]))
tall_df_value = [
["DeepSeek Coder", 79.3],
["Llama 3.3", 68.9],
["Qwen 2.5", 61.9],
["Gemma 2", 59.5],
["GPT 2", 18.3],
]

def get_display_value(values):
display_values = []
medals = ["🥇", "🥈", "🥉"]
for i, row in enumerate(values):
if i < 3:
display_values.append([f"{medals[i]} {row[0]}", row[1]])
else:
display_values.append([row[0], row[1]])
return display_values

display_value = get_display_value(tall_df_value)

tall_df_value = {
"data": tall_df_value,
"headers": ["Model", "% Correct (LeetCode Hard)"],
"metadata": {
"display_value": display_value
}
}

with gr.Row():
df_tall = gr.Dataframe(
value=initial_tall_df,
value=tall_df_value,
interactive=False,
label="Tall Dataframe (Scroll Vertically)",
show_label=True,
Expand Down Expand Up @@ -92,7 +116,7 @@ def increment_select_counter(evt: gr.SelectData, count):
label="Selected cell value", elem_id="selected_cell_value"
)

update_btn.click(fn=update_dataframe, outputs=[df, df_view, df_tall])
update_btn.click(fn=update_dataframe, outputs=[df, df_view])
clear_btn.click(fn=clear_dataframes, outputs=[df, df_view, df_tall])
df.change(fn=lambda x: x + 1, inputs=[change_events], outputs=[change_events])
df.input(fn=lambda x: x + 1, inputs=[input_events], outputs=[input_events])
Expand Down
45 changes: 45 additions & 0 deletions js/dataframe/Dataframe.stories.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,51 @@
}}
/>

<Story
name="Dataframe with display values"
args={{
values: [
[95, 92, 88],
[89, 90, 85],
[92, 88, 91],
[87, 85, 89],
[91, 93, 90],
[82, 81, 83]
],
headers: ["Model A", "Model B", "Model C"],
display_value: [
["🥇 95", "92", "88"],
["🥈 89", "90", "85"],
["🥉 92", "88", "91"],
["87", "85", "89"],
["91", "93", "90"],
["82", "81", "83"]
],
label: "Model Performance with Medal Indicators",
col_count: [3, "dynamic"],
row_count: [6, "dynamic"],
show_row_numbers: true,
editable: false
}}
play={async ({ canvasElement }) => {
const canvas = within(canvasElement);

const first_row = canvas.getAllByRole("cell")[2];
expect(first_row.textContent).toContain("🥇");

const second_row = canvas.getAllByRole("cell")[5];
expect(second_row.textContent).toContain("🥈");

const third_row = canvas.getAllByRole("cell")[8];
expect(third_row.textContent).toContain("🥉");

const fourth_row = canvas.getAllByRole("cell")[11];
expect(fourth_row.textContent).not.toContain("🥇");
expect(fourth_row.textContent).not.toContain("🥈");
expect(fourth_row.textContent).not.toContain("🥉");
}}
/>

<Story
name="Dataframe with text wrapping, no max chars"
args={{
Expand Down
2 changes: 1 addition & 1 deletion js/dataframe/shared/CellMenu.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { onMount } from "svelte";
import CellMenuIcons from "./CellMenuIcons.svelte";
import type { I18nFormatter } from "js/utils/src";
import type { SortDirection } from "./context/table_context";
import type { SortDirection } from "./context/dataframe_context";

export let x: number;
export let y: number;
Expand Down
29 changes: 9 additions & 20 deletions js/dataframe/shared/EditableCell.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
right: string;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removing clear_on_focus and adding blur event

display: boolean;
}[];
export let clear_on_focus = false;
export let line_breaks = true;
export let editable = true;
export let is_static = false;
Expand All @@ -33,19 +32,18 @@
export let wrap_text = false;

export let show_selection_buttons = false;
export let coords: [number, number] | null = null;
export let coords: [number, number];
export let on_select_column: ((col: number) => void) | null = null;
export let on_select_row: ((row: number) => void) | null = null;

const dispatch = createEventDispatcher<{
blur: void;
blur: { blur_event: FocusEvent; coords: [number, number] };
keydown: KeyboardEvent;
}>();

let is_expanded = false;

export let el: HTMLInputElement | null;
$: _value = value;

function truncate_text(
text: string | number,
Expand Down Expand Up @@ -73,32 +71,23 @@
: display_content;

function use_focus(node: HTMLInputElement): any {
if (clear_on_focus) {
_value = "";
}

requestAnimationFrame(() => {
node.focus();
});

return {};
}

function handle_blur({
currentTarget
}: Event & {
currentTarget: HTMLInputElement;
}): void {
value = currentTarget.value;
dispatch("blur");
function handle_blur(event: FocusEvent): void {
dispatch("blur", {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dispatch blur event with coords of the cell to update the value of that cell

blur_event: event,
coords: coords
});
}

function handle_keydown(event: KeyboardEvent): void {
if (event.key === "Enter") {
if (edit) {
value = _value;
dispatch("blur");
} else if (!header) {
Comment on lines -98 to -101
Copy link
Collaborator Author

@hannahblair hannahblair Apr 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't need this logic, blur is handled in handle_keydown

if (!header) {
is_expanded = !is_expanded;
}
}
Expand All @@ -119,7 +108,7 @@
role="textbox"
aria-label={is_static ? "Cell is read-only" : "Edit cell"}
bind:this={el}
bind:value={_value}
bind:value
class:header
tabindex="-1"
on:blur={handle_blur}
Expand Down
Loading
Loading