mirror of
https://github.com/gradio-app/gradio.git
synced 2025-04-12 12:40:29 +08:00
Add static_columns
param for interactive dataframes (#10734)
* 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 * add static_cols param and test * add changeset * tweak * fix tests * test tweaks * add row story * tweaks * fix test * tweak * redesign selection buttons * fix test * fix test * fix test * fix test * edit demo with static cols * change to list[int] * cursor tweak * fix test * nb * set col count to fixed if static_cols are set * lint * tweak demo * fix col count logic * add padlock icon * fix py test * lint * fix test --------- Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com>
This commit is contained in:
parent
0ce7bfe1bd
commit
c44b8f47b9
6
.changeset/long-olives-glow.md
Normal file
6
.changeset/long-olives-glow.md
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
"@gradio/dataframe": minor
|
||||
"gradio": minor
|
||||
---
|
||||
|
||||
feat:Add `static_columns` param for interactive dataframes
|
File diff suppressed because one or more lines are too long
@ -38,6 +38,7 @@ with gr.Blocks() as demo:
|
||||
show_search="filter",
|
||||
show_copy_button=True,
|
||||
show_row_numbers=True,
|
||||
static_columns=[4]
|
||||
)
|
||||
|
||||
with gr.Column(scale=1):
|
||||
|
@ -102,6 +102,7 @@ class Dataframe(Component):
|
||||
max_chars: int | None = None,
|
||||
show_search: Literal["none", "search", "filter"] = "none",
|
||||
pinned_columns: int | None = None,
|
||||
static_columns: list[int] | None = None,
|
||||
):
|
||||
"""
|
||||
Parameters:
|
||||
@ -135,12 +136,19 @@ class Dataframe(Component):
|
||||
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.
|
||||
show_search: Show a search input in the toolbar. If "search", a search input is shown. If "filter", a search input and filter buttons are shown. If "none", no search input is shown.
|
||||
pinned_columns: If provided, will pin the specified number of columns from the left.
|
||||
static_columns: List of column indices (int) that should not be editable. Only applies when interactive=True. When specified, col_count is automatically set to "fixed" and columns cannot be inserted or deleted.
|
||||
"""
|
||||
self.wrap = wrap
|
||||
self.row_count = self.__process_counts(row_count)
|
||||
self.static_columns = static_columns or []
|
||||
|
||||
self.col_count = self.__process_counts(
|
||||
col_count, len(headers) if headers else 3
|
||||
)
|
||||
|
||||
if self.static_columns and isinstance(self.col_count, tuple):
|
||||
self.col_count = (self.col_count[0], "fixed")
|
||||
|
||||
self.__validate_headers(headers, self.col_count[0])
|
||||
|
||||
self.headers = (
|
||||
|
@ -606,6 +606,7 @@ class Numpy(components.Dataframe):
|
||||
column_widths: list[str | int] | None = None,
|
||||
show_row_numbers: bool = False,
|
||||
show_search: Literal["none", "search", "filter"] = "none",
|
||||
static_columns: list[int] | None = None,
|
||||
pinned_columns: int | None = None,
|
||||
show_fullscreen_button: bool = False,
|
||||
max_chars: int | None = None,
|
||||
@ -641,6 +642,7 @@ class Numpy(components.Dataframe):
|
||||
show_fullscreen_button=show_fullscreen_button,
|
||||
max_chars=max_chars,
|
||||
show_copy_button=show_copy_button,
|
||||
static_columns=static_columns,
|
||||
)
|
||||
|
||||
|
||||
@ -689,6 +691,7 @@ class Matrix(components.Dataframe):
|
||||
show_fullscreen_button: bool = False,
|
||||
max_chars: int | None = None,
|
||||
show_copy_button: bool = False,
|
||||
static_columns: list[int] | None = None,
|
||||
):
|
||||
super().__init__(
|
||||
value=value,
|
||||
@ -720,6 +723,7 @@ class Matrix(components.Dataframe):
|
||||
show_fullscreen_button=show_fullscreen_button,
|
||||
max_chars=max_chars,
|
||||
show_copy_button=show_copy_button,
|
||||
static_columns=static_columns,
|
||||
)
|
||||
|
||||
|
||||
@ -768,6 +772,7 @@ class List(components.Dataframe):
|
||||
show_fullscreen_button: bool = False,
|
||||
max_chars: int | None = None,
|
||||
show_copy_button: bool = False,
|
||||
static_columns: list[int] | None = None,
|
||||
):
|
||||
super().__init__(
|
||||
value=value,
|
||||
@ -795,6 +800,7 @@ class List(components.Dataframe):
|
||||
min_width=min_width,
|
||||
show_row_numbers=show_row_numbers,
|
||||
show_search=show_search,
|
||||
static_columns=static_columns,
|
||||
pinned_columns=pinned_columns,
|
||||
show_fullscreen_button=show_fullscreen_button,
|
||||
max_chars=max_chars,
|
||||
|
@ -57,6 +57,7 @@
|
||||
export let show_row_numbers = false;
|
||||
export let show_search: "none" | "search" | "filter" = "none";
|
||||
export let pinned_columns = 0;
|
||||
export let static_columns: (string | number)[] = [];
|
||||
|
||||
$: _headers = [...(value.headers || headers)];
|
||||
$: display_value = value?.metadata?.display_value
|
||||
@ -119,5 +120,6 @@
|
||||
{show_search}
|
||||
{pinned_columns}
|
||||
components={{ image: Image }}
|
||||
{static_columns}
|
||||
/>
|
||||
</Block>
|
||||
|
@ -24,6 +24,7 @@
|
||||
export let clear_on_focus = false;
|
||||
export let line_breaks = true;
|
||||
export let editable = true;
|
||||
export let is_static = false;
|
||||
export let root: string;
|
||||
export let max_chars: number | null = null;
|
||||
export let components: Record<string, any> = {};
|
||||
@ -102,8 +103,11 @@
|
||||
|
||||
{#if edit}
|
||||
<input
|
||||
disabled={is_static}
|
||||
aria-disabled={is_static}
|
||||
class:static={is_static}
|
||||
role="textbox"
|
||||
aria-label="Edit cell"
|
||||
aria-label={is_static ? "Cell is read-only" : "Edit cell"}
|
||||
bind:this={el}
|
||||
bind:value={_value}
|
||||
class:header
|
||||
@ -125,6 +129,7 @@
|
||||
class:edit
|
||||
class:expanded={is_expanded}
|
||||
class:multiline={header}
|
||||
class:static={!editable}
|
||||
on:focus|preventDefault
|
||||
style={styling}
|
||||
data-editable={editable}
|
||||
@ -232,4 +237,8 @@
|
||||
width: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
input:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
|
@ -75,6 +75,7 @@
|
||||
export let max_chars: number | undefined = undefined;
|
||||
export let show_search: "none" | "search" | "filter" = "none";
|
||||
export let pinned_columns = 0;
|
||||
export let static_columns: (string | number)[] = [];
|
||||
|
||||
$: actual_pinned_columns =
|
||||
pinned_columns && data?.[0]?.length
|
||||
@ -699,8 +700,10 @@
|
||||
{max_chars}
|
||||
{root}
|
||||
{editable}
|
||||
is_static={static_columns.includes(i)}
|
||||
{i18n}
|
||||
bind:el={els[id].input}
|
||||
{col_count}
|
||||
/>
|
||||
{/each}
|
||||
</tr>
|
||||
@ -804,8 +807,10 @@
|
||||
{max_chars}
|
||||
{root}
|
||||
{editable}
|
||||
is_static={static_columns.includes(i)}
|
||||
{i18n}
|
||||
bind:el={els[id].input}
|
||||
{col_count}
|
||||
/>
|
||||
{/each}
|
||||
</tr>
|
||||
@ -836,6 +841,7 @@
|
||||
{max_chars}
|
||||
{root}
|
||||
{editable}
|
||||
is_static={static_columns.includes(j)}
|
||||
{i18n}
|
||||
{components}
|
||||
{handle_select_column}
|
||||
|
@ -50,6 +50,7 @@
|
||||
export let max_chars: number | undefined;
|
||||
export let root: string;
|
||||
export let editable: boolean;
|
||||
export let is_static = false;
|
||||
export let i18n: I18nFormatter;
|
||||
export let components: Record<string, any> = {};
|
||||
export let el: {
|
||||
@ -130,6 +131,7 @@
|
||||
{latex_delimiters}
|
||||
{line_breaks}
|
||||
{editable}
|
||||
{is_static}
|
||||
edit={editing && editing[0] === index && editing[1] === j}
|
||||
{datatype}
|
||||
on:blur={() => {
|
||||
|
@ -4,6 +4,7 @@
|
||||
import CellMenuButton from "./CellMenuButton.svelte";
|
||||
import type { I18nFormatter } from "js/core/src/gradio_helper";
|
||||
import type { SortDirection } from "./context/table_context";
|
||||
import Padlock from "./icons/Padlock.svelte";
|
||||
|
||||
export let value: string;
|
||||
export let i: number;
|
||||
@ -33,6 +34,10 @@
|
||||
export let editable: boolean;
|
||||
export let i18n: I18nFormatter;
|
||||
export let el: HTMLInputElement | null;
|
||||
export let is_static: boolean;
|
||||
export let col_count: [number, "fixed" | "dynamic"];
|
||||
|
||||
$: can_add_columns = col_count && col_count[1] === "dynamic";
|
||||
|
||||
function get_header_position(col_index: number): string {
|
||||
if (col_index >= actual_pinned_columns) {
|
||||
@ -100,11 +105,15 @@
|
||||
header
|
||||
{root}
|
||||
{editable}
|
||||
{is_static}
|
||||
{i18n}
|
||||
/>
|
||||
</button>
|
||||
{#if is_static}
|
||||
<Padlock />
|
||||
{/if}
|
||||
</div>
|
||||
{#if editable}
|
||||
{#if editable && can_add_columns}
|
||||
<CellMenuButton on_click={(event) => toggle_header_menu(event, i)} />
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { getContext, setContext } from "svelte";
|
||||
import { writable, type Writable, get } from "svelte/store";
|
||||
import { writable, get } from "svelte/store";
|
||||
import type { Writable } from "svelte/store";
|
||||
import { sort_table_data } from "../utils/table_utils";
|
||||
import { dequal } from "dequal/lite";
|
||||
import type { DataframeValue } from "../utils";
|
||||
|
24
js/dataframe/shared/icons/Padlock.svelte
Normal file
24
js/dataframe/shared/icons/Padlock.svelte
Normal file
@ -0,0 +1,24 @@
|
||||
<div class="wrapper" aria-label="Static column">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="13"
|
||||
height="13"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
|
||||
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
@ -134,7 +134,11 @@ async function handle_enter_key(
|
||||
const cell_id = ctx.data[i][j].id;
|
||||
const input_el = ctx.els[cell_id].input;
|
||||
if (input_el) {
|
||||
const old_value = ctx.data[i][j].value;
|
||||
ctx.data[i][j].value = input_el.value;
|
||||
if (old_value !== input_el.value) {
|
||||
ctx.dispatch("input");
|
||||
}
|
||||
}
|
||||
ctx.df_actions.set_editing(false);
|
||||
await tick();
|
||||
|
@ -239,7 +239,7 @@ test("Dataframe shift+click selection works", async ({ page }) => {
|
||||
navigator.clipboard.readText()
|
||||
);
|
||||
|
||||
expect(clipboard_value).toBe("0,6\n0,6");
|
||||
expect(clipboard_value).toBe("0,6\n0,0");
|
||||
});
|
||||
|
||||
test("Dataframe cmd + click selection works", async ({ page }) => {
|
||||
@ -268,10 +268,32 @@ test("Dataframe cmd + click selection works", async ({ page }) => {
|
||||
navigator.clipboard.readText()
|
||||
);
|
||||
|
||||
expect(clipboard_value).toBe("6\n8");
|
||||
expect(clipboard_value).toBe("6\n0");
|
||||
});
|
||||
|
||||
test.only("Dataframe search functionality works correctly after data update", async ({
|
||||
test("Static columns cannot be edited", async ({ page }) => {
|
||||
const static_df = page.locator("#dataframe");
|
||||
|
||||
const static_column_cell = get_cell(static_df, 0, 4);
|
||||
await static_column_cell.click();
|
||||
await page.waitForTimeout(100);
|
||||
|
||||
const is_disabled =
|
||||
(await static_column_cell.locator("input").getAttribute("disabled")) !==
|
||||
null;
|
||||
expect(is_disabled).toBe(true);
|
||||
|
||||
const editable_cell = get_cell(static_df, 2, 3);
|
||||
await editable_cell.click();
|
||||
await page.waitForTimeout(100);
|
||||
|
||||
const is_not_disabled = await editable_cell
|
||||
.locator("input")
|
||||
.getAttribute("aria-disabled");
|
||||
expect(is_not_disabled).toEqual("false");
|
||||
});
|
||||
|
||||
test("Dataframe search functionality works correctly after data update", async ({
|
||||
page
|
||||
}) => {
|
||||
const df = page.locator("#non-interactive-dataframe");
|
||||
|
@ -49,6 +49,7 @@ class TestDataframe:
|
||||
"elem_classes": [],
|
||||
"show_row_numbers": False,
|
||||
"show_search": "none",
|
||||
"static_columns": [],
|
||||
"pinned_columns": None,
|
||||
"wrap": False,
|
||||
"proxy_url": None,
|
||||
@ -93,6 +94,7 @@ class TestDataframe:
|
||||
"show_label": True,
|
||||
"show_row_numbers": False,
|
||||
"show_search": "none",
|
||||
"static_columns": [],
|
||||
"pinned_columns": None,
|
||||
"scale": None,
|
||||
"min_width": 160,
|
||||
@ -402,3 +404,24 @@ class TestDataframe:
|
||||
styled_df = test_df.style
|
||||
styled_df.hide(axis=1, subset=["col2"])
|
||||
assert df.get_cell_data(styled_df) == [[1], [3]]
|
||||
|
||||
def test_static_columns(self):
|
||||
# when static_columns is specified, col_count should be fixed
|
||||
dataframe = gr.Dataframe(static_columns=[0, 1])
|
||||
assert dataframe.col_count[1] == "fixed"
|
||||
|
||||
# when static_columns is specified with dynamic col_count, it should be converted to fixed
|
||||
dataframe = gr.Dataframe(col_count=(4, "dynamic"), static_columns=[0, 1])
|
||||
assert dataframe.col_count[1] == "fixed"
|
||||
|
||||
# when static_columns is empty, col_count should remain as specified
|
||||
dataframe = gr.Dataframe(col_count=(4, "dynamic"), static_columns=[])
|
||||
assert dataframe.col_count[1] == "dynamic"
|
||||
|
||||
# when static_columns is None, col_count should remain as specified
|
||||
dataframe = gr.Dataframe(col_count=(4, "dynamic"), static_columns=None)
|
||||
assert dataframe.col_count[1] == "dynamic"
|
||||
|
||||
# when static_columns is not specified at all, col_count should remain as specified
|
||||
dataframe = gr.Dataframe(col_count=(4, "dynamic"))
|
||||
assert dataframe.col_count[1] == "dynamic"
|
||||
|
Loading…
x
Reference in New Issue
Block a user