diff --git a/.changeset/few-items-study.md b/.changeset/few-items-study.md new file mode 100644 index 0000000000000..6fd32b5dfa446 --- /dev/null +++ b/.changeset/few-items-study.md @@ -0,0 +1,7 @@ +--- +"@gradio/core": patch +"@gradio/dataframe": patch +"gradio": patch +--- + +feat:Support column/row deletion in `gr.DataFrame` diff --git a/client/python/test/test_client.py b/client/python/test/test_client.py index 266eddcf03324..6ac5b1bbe34d9 100644 --- a/client/python/test/test_client.py +++ b/client/python/test/test_client.py @@ -778,6 +778,7 @@ def __call__(self, *args, **kwargs): assert all(s in messages for s in statuses) + @pytest.mark.flaky @patch("gradio_client.client.Endpoint.make_end_to_end_fn") def test_messages_correct_two_concurrent( self, mock_make_end_to_end_fn, calculator_demo diff --git a/gradio/components/dataframe.py b/gradio/components/dataframe.py index 7e11be8037fbd..7b8d4ee679686 100644 --- a/gradio/components/dataframe.py +++ b/gradio/components/dataframe.py @@ -100,8 +100,8 @@ def __init__( Parameters: value: Default value to display in the DataFrame. If a Styler is provided, it will be used to set the displayed value in the DataFrame (e.g. to set precision of numbers) if the `interactive` is False. If a Callable function is provided, the function will be called whenever the app loads to set the initial value of the component. headers: List of str header names. If None, no headers are shown. - row_count: Limit number of rows for input and decide whether user can create new rows. The first element of the tuple is an `int`, the row count; the second should be 'fixed' or 'dynamic', the new row behaviour. If an `int` is passed the rows default to 'dynamic' - col_count: Limit number of columns for input and decide whether user can create new columns. The first element of the tuple is an `int`, the number of columns; the second should be 'fixed' or 'dynamic', the new column behaviour. If an `int` is passed the columns default to 'dynamic' + row_count: Limit number of rows for input and decide whether user can create new rows or delete existing rows. The first element of the tuple is an `int`, the row count; the second should be 'fixed' or 'dynamic', the new row behaviour. If an `int` is passed the rows default to 'dynamic' + col_count: Limit number of columns for input and decide whether user can create new columns or delete existing columns. The first element of the tuple is an `int`, the number of columns; the second should be 'fixed' or 'dynamic', the new column behaviour. If an `int` is passed the columns default to 'dynamic' datatype: Datatype of values in sheet. Can be provided per column as a list of strings, or for the entire sheet as a single string. Valid datatypes are "str", "number", "bool", "date", and "markdown". type: Type of value to be returned by component. "pandas" for pandas dataframe, "numpy" for numpy array, "polars" for polars dataframe, or "array" for a Python list of lists. label: the label for this component. Appears above the component and is also used as the header if there are a table of examples for this component. If None and used in a `gr.Interface`, the label will be the name of the parameter this component is assigned to. diff --git a/js/core/src/lang/en.json b/js/core/src/lang/en.json index 65fb919c2cde1..230198c17d3b9 100644 --- a/js/core/src/lang/en.json +++ b/js/core/src/lang/en.json @@ -64,6 +64,8 @@ "new_row": "New row", "add_row_above": "Add row above", "add_row_below": "Add row below", + "delete_row": "Delete row", + "delete_column": "Delete column", "add_column_left": "Add column to the left", "add_column_right": "Add column to the right" }, diff --git a/js/dataframe/shared/Arrow.svelte b/js/dataframe/shared/Arrow.svelte deleted file mode 100644 index 52000514b5a61..0000000000000 --- a/js/dataframe/shared/Arrow.svelte +++ /dev/null @@ -1,10 +0,0 @@ -<script lang="ts"> - export let transform: string; -</script> - -<svg viewBox="0 0 24 24" width="16" height="16"> - <path - d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z" - {transform} - /> -</svg> diff --git a/js/dataframe/shared/CellMenu.svelte b/js/dataframe/shared/CellMenu.svelte index 53f9fdea9fa0e..085b7b24e9f07 100644 --- a/js/dataframe/shared/CellMenu.svelte +++ b/js/dataframe/shared/CellMenu.svelte @@ -1,6 +1,6 @@ <script lang="ts"> import { onMount } from "svelte"; - import Arrow from "./Arrow.svelte"; + import CellMenuIcons from "./CellMenuIcons.svelte"; import type { I18nFormatter } from "js/utils/src"; export let x: number; @@ -12,6 +12,10 @@ export let row: number; export let col_count: [number, "fixed" | "dynamic"]; export let row_count: [number, "fixed" | "dynamic"]; + export let on_delete_row: () => void; + export let on_delete_col: () => void; + export let can_delete_rows: boolean; + export let can_delete_cols: boolean; export let i18n: I18nFormatter; let menu_element: HTMLDivElement; @@ -50,23 +54,35 @@ <div bind:this={menu_element} class="cell-menu"> {#if !is_header && can_add_rows} <button on:click={() => on_add_row_above()}> - <Arrow transform="rotate(-90 12 12)" /> + <CellMenuIcons icon="add-row-above" /> {i18n("dataframe.add_row_above")} </button> <button on:click={() => on_add_row_below()}> - <Arrow transform="rotate(90 12 12)" /> + <CellMenuIcons icon="add-row-below" /> {i18n("dataframe.add_row_below")} </button> + {#if can_delete_rows} + <button on:click={on_delete_row} class="delete"> + <CellMenuIcons icon="delete-row" /> + {i18n("dataframe.delete_row")} + </button> + {/if} {/if} {#if can_add_columns} <button on:click={() => on_add_column_left()}> - <Arrow transform="rotate(180 12 12)" /> + <CellMenuIcons icon="add-column-left" /> {i18n("dataframe.add_column_left")} </button> <button on:click={() => on_add_column_right()}> - <Arrow transform="rotate(0 12 12)" /> + <CellMenuIcons icon="add-column-right" /> {i18n("dataframe.add_column_right")} </button> + {#if can_delete_cols} + <button on:click={on_delete_col} class="delete"> + <CellMenuIcons icon="delete-column" /> + {i18n("dataframe.delete_column")} + </button> + {/if} {/if} </div> @@ -110,8 +126,4 @@ fill: currentColor; transition: fill 0.2s; } - - .cell-menu button:hover :global(svg) { - fill: var(--color-accent); - } </style> diff --git a/js/dataframe/shared/CellMenuIcons.svelte b/js/dataframe/shared/CellMenuIcons.svelte new file mode 100644 index 0000000000000..930423698af9d --- /dev/null +++ b/js/dataframe/shared/CellMenuIcons.svelte @@ -0,0 +1,113 @@ +<script lang="ts"> + export let icon: string; +</script> + +{#if icon == "add-column-right"} + <svg viewBox="0 0 24 24" width="16" height="16"> + <rect + x="4" + y="6" + width="4" + height="12" + stroke="currentColor" + stroke-width="2" + fill="none" + /> + <path + d="M12 12H19M16 8L19 12L16 16" + stroke="currentColor" + stroke-width="2" + fill="none" + stroke-linecap="round" + /> + </svg> +{:else if icon == "add-column-left"} + <svg viewBox="0 0 24 24" width="16" height="16"> + <rect + x="16" + y="6" + width="4" + height="12" + stroke="currentColor" + stroke-width="2" + fill="none" + /> + <path + d="M12 12H5M8 8L5 12L8 16" + stroke="currentColor" + stroke-width="2" + fill="none" + stroke-linecap="round" + /> + </svg> +{:else if icon == "add-row-above"} + <svg viewBox="0 0 24 24" width="16" height="16"> + <rect + x="6" + y="16" + width="12" + height="4" + stroke="currentColor" + stroke-width="2" + /> + <path + d="M12 12V5M8 8L12 5L16 8" + stroke="currentColor" + stroke-width="2" + fill="none" + stroke-linecap="round" + /> + </svg> +{:else if icon == "add-row-below"} + <svg viewBox="0 0 24 24" width="16" height="16"> + <rect + x="6" + y="4" + width="12" + height="4" + stroke="currentColor" + stroke-width="2" + /> + <path + d="M12 12V19M8 16L12 19L16 16" + stroke="currentColor" + stroke-width="2" + fill="none" + stroke-linecap="round" + /> + </svg> +{:else if icon == "delete-row"} + <svg viewBox="0 0 24 24" width="16" height="16"> + <rect + x="5" + y="10" + width="14" + height="4" + stroke="currentColor" + stroke-width="2" + /> + <path + d="M8 7L16 17M16 7L8 17" + stroke="currentColor" + stroke-width="2" + stroke-linecap="round" + /> + </svg> +{:else if icon == "delete-column"} + <svg viewBox="0 0 24 24" width="16" height="16"> + <rect + x="10" + y="5" + width="4" + height="14" + stroke="currentColor" + stroke-width="2" + /> + <path + d="M7 8L17 16M17 8L7 16" + stroke="currentColor" + stroke-width="2" + stroke-linecap="round" + /> + </svg> +{/if} diff --git a/js/dataframe/shared/Table.svelte b/js/dataframe/shared/Table.svelte index 2598673a20b52..9695d1d6c27dc 100644 --- a/js/dataframe/shared/Table.svelte +++ b/js/dataframe/shared/Table.svelte @@ -120,13 +120,7 @@ id: string; }[][] { const data_row_length = _values.length; - return Array( - row_count[1] === "fixed" - ? row_count[0] - : data_row_length < row_count[0] - ? row_count[0] - : data_row_length - ) + return Array(row_count[1] === "fixed" ? row_count[0] : data_row_length) .fill(0) .map((_, i) => Array( @@ -791,6 +785,42 @@ afterUpdate(() => { value_is_output = false; }); + + async function delete_row(index: number): Promise<void> { + parent.focus(); + if (row_count[1] !== "dynamic") return; + if (data.length <= 1) return; + data.splice(index, 1); + data = data; + selected = false; + } + + async function delete_col(index: number): Promise<void> { + parent.focus(); + if (col_count[1] !== "dynamic") return; + if (data[0].length <= 1) return; + + _headers.splice(index, 1); + _headers = _headers; + + data.forEach((row) => { + row.splice(index, 1); + }); + data = data; + selected = false; + } + + function delete_row_at(index: number): void { + delete_row(index); + active_cell_menu = null; + active_header_menu = null; + } + + function delete_col_at(index: number): void { + delete_col(index); + active_cell_menu = null; + active_header_menu = null; + } </script> <svelte:window on:resize={() => set_cell_widths()} /> @@ -1050,18 +1080,22 @@ </div> </div> -{#if active_cell_menu !== null} +{#if active_cell_menu} <CellMenu - {i18n} x={active_cell_menu.x} y={active_cell_menu.y} - row={active_cell_menu?.row ?? -1} + row={active_cell_menu.row} {col_count} {row_count} - on_add_row_above={() => add_row_at(active_cell_menu?.row ?? -1, "above")} - on_add_row_below={() => add_row_at(active_cell_menu?.row ?? -1, "below")} - on_add_column_left={() => add_col_at(active_cell_menu?.col ?? -1, "left")} - on_add_column_right={() => add_col_at(active_cell_menu?.col ?? -1, "right")} + on_add_row_above={() => add_row_at(active_cell_menu?.row || 0, "above")} + on_add_row_below={() => add_row_at(active_cell_menu?.row || 0, "below")} + on_add_column_left={() => add_col_at(active_cell_menu?.col || 0, "left")} + on_add_column_right={() => add_col_at(active_cell_menu?.col || 0, "right")} + on_delete_row={() => delete_row_at(active_cell_menu?.row || 0)} + on_delete_col={() => delete_col_at(active_cell_menu?.col || 0)} + can_delete_rows={data.length > 1} + can_delete_cols={data[0].length > 1} + {i18n} /> {/if} @@ -1078,6 +1112,10 @@ on_add_column_left={() => add_col_at(active_header_menu?.col ?? -1, "left")} on_add_column_right={() => add_col_at(active_header_menu?.col ?? -1, "right")} + on_delete_row={() => delete_row_at(active_cell_menu?.row ?? -1)} + on_delete_col={() => delete_col_at(active_header_menu?.col ?? -1)} + can_delete_rows={false} + can_delete_cols={data[0].length > 1} /> {/if}