@gradio/dataframe 0.17.17 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/Dataframe.stories.svelte +1 -2
  3. package/Index.svelte +3 -13
  4. package/dist/Index.svelte +3 -10
  5. package/dist/Index.svelte.d.ts +1 -5
  6. package/dist/shared/CellMenu.svelte +37 -0
  7. package/dist/shared/CellMenu.svelte.d.ts +4 -1
  8. package/dist/shared/CellMenuIcons.svelte +48 -0
  9. package/dist/shared/EditableCell.svelte +16 -20
  10. package/dist/shared/EditableCell.svelte.d.ts +1 -2
  11. package/dist/shared/FilterMenu.svelte +235 -0
  12. package/dist/shared/FilterMenu.svelte.d.ts +19 -0
  13. package/dist/shared/Table.svelte +59 -1
  14. package/dist/shared/TableCell.svelte.d.ts +1 -1
  15. package/dist/shared/TableHeader.svelte +26 -0
  16. package/dist/shared/TableHeader.svelte.d.ts +8 -1
  17. package/dist/shared/context/dataframe_context.d.ts +20 -1
  18. package/dist/shared/context/dataframe_context.js +46 -6
  19. package/dist/shared/types.d.ts +1 -1
  20. package/dist/shared/utils/data_processing.d.ts +2 -2
  21. package/dist/shared/utils/filter_utils.d.ts +28 -0
  22. package/dist/shared/utils/filter_utils.js +123 -0
  23. package/dist/shared/utils/keyboard_utils.js +5 -1
  24. package/dist/shared/utils/table_utils.d.ts +7 -0
  25. package/dist/shared/utils/table_utils.js +29 -0
  26. package/package.json +10 -10
  27. package/shared/CellMenu.svelte +45 -1
  28. package/shared/CellMenuIcons.svelte +48 -0
  29. package/shared/EditableCell.svelte +18 -25
  30. package/shared/FilterMenu.svelte +248 -0
  31. package/shared/Table.svelte +79 -5
  32. package/shared/TableCell.svelte +1 -1
  33. package/shared/TableHeader.svelte +32 -1
  34. package/shared/Toolbar.svelte +1 -1
  35. package/shared/context/dataframe_context.ts +81 -18
  36. package/shared/types.ts +1 -1
  37. package/shared/utils/data_processing.ts +2 -2
  38. package/shared/utils/filter_utils.ts +207 -0
  39. package/shared/utils/keyboard_utils.ts +5 -2
  40. package/shared/utils/table_utils.ts +52 -0
@@ -7,6 +7,8 @@
7
7
  import SortArrowUp from "./icons/SortArrowUp.svelte";
8
8
  import SortArrowDown from "./icons/SortArrowDown.svelte";
9
9
  import type { SortDirection } from "./context/dataframe_context";
10
+ import CellMenuIcons from "./CellMenuIcons.svelte";
11
+ import type { FilterDatatype } from "./context/dataframe_context";
10
12
  export let value: string;
11
13
  export let i: number;
12
14
  export let actual_pinned_columns: number;
@@ -18,6 +20,12 @@
18
20
  export let toggle_header_menu: (event: MouseEvent, col: number) => void;
19
21
  export let end_header_edit: (event: CustomEvent<KeyboardEvent>) => void;
20
22
  export let sort_columns: { col: number; direction: SortDirection }[] = [];
23
+ export let filter_columns: {
24
+ col: number;
25
+ datatype: FilterDatatype;
26
+ filter: string;
27
+ value: string;
28
+ }[] = [];
21
29
 
22
30
  export let latex_delimiters: {
23
31
  left: string;
@@ -28,12 +36,13 @@
28
36
  export let max_chars: number | undefined;
29
37
  export let editable: boolean;
30
38
  export let i18n: I18nFormatter;
31
- export let el: HTMLInputElement | null;
39
+ export let el: HTMLTextAreaElement | null;
32
40
  export let is_static: boolean;
33
41
  export let col_count: [number, "fixed" | "dynamic"];
34
42
 
35
43
  $: can_add_columns = col_count && col_count[1] === "dynamic";
36
44
  $: sort_index = sort_columns.findIndex((item) => item.col === i);
45
+ $: filter_index = filter_columns.findIndex((item) => item.col === i);
37
46
  $: sort_priority = sort_index !== -1 ? sort_index + 1 : null;
38
47
  $: current_direction =
39
48
  sort_index !== -1 ? sort_columns[sort_index].direction : null;
@@ -63,6 +72,7 @@
63
72
  class:last-pinned={i === actual_pinned_columns - 1}
64
73
  class:focus={header_edit === i || selected_header === i}
65
74
  class:sorted={sort_index !== -1}
75
+ class:filtered={filter_index !== -1}
66
76
  aria-sort={get_sort_status(value, sort_columns, headers) === "none"
67
77
  ? "none"
68
78
  : get_sort_status(value, sort_columns, headers) === "asc"
@@ -125,6 +135,13 @@
125
135
  {/if}
126
136
  </div>
127
137
  {/if}
138
+ {#if filter_index !== -1}
139
+ <div class="filter-indicators">
140
+ <span class="filter-icon">
141
+ <CellMenuIcons icon="filter" />
142
+ </span>
143
+ </div>
144
+ {/if}
128
145
  </button>
129
146
  {#if is_static}
130
147
  <Padlock />
@@ -243,6 +260,20 @@
243
260
  padding: var(--size-1-5);
244
261
  }
245
262
 
263
+ .filter-indicators {
264
+ display: flex;
265
+ align-items: center;
266
+ margin-left: var(--size-1);
267
+ gap: var(--size-1);
268
+ }
269
+
270
+ .filter-icon {
271
+ display: flex;
272
+ align-items: center;
273
+ justify-content: center;
274
+ color: var(--body-text-color);
275
+ }
276
+
246
277
  .pinned-column {
247
278
  position: sticky;
248
279
  z-index: 5;
@@ -21,7 +21,7 @@
21
21
  let input_value = "";
22
22
 
23
23
  function handle_search_input(e: Event): void {
24
- const target = e.target as HTMLInputElement;
24
+ const target = e.target as HTMLTextAreaElement;
25
25
  input_value = target.value;
26
26
  const new_query = input_value || null;
27
27
  if (current_search_query !== new_query) {
@@ -13,6 +13,7 @@ import {
13
13
  export const DATAFRAME_KEY = Symbol("dataframe");
14
14
 
15
15
  export type SortDirection = "asc" | "desc";
16
+ export type FilterDatatype = "string" | "number";
16
17
  export type CellCoordinate = [number, number];
17
18
 
18
19
  interface DataFrameState {
@@ -40,6 +41,19 @@ interface DataFrameState {
40
41
  styling: string[][] | null;
41
42
  } | null;
42
43
  };
44
+ filter_state: {
45
+ filter_columns: {
46
+ col: number;
47
+ datatype: FilterDatatype;
48
+ filter: string;
49
+ value: string;
50
+ }[];
51
+ initial_data: {
52
+ data: { id: string; value: string | number }[][];
53
+ display_value: string[][] | null;
54
+ styling: string[][] | null;
55
+ } | null;
56
+ };
43
57
  ui_state: {
44
58
  active_cell_menu: { row: number; col: number; x: number; y: number } | null;
45
59
  active_header_menu: { col: number; x: number; y: number } | null;
@@ -60,6 +74,12 @@ interface DataFrameState {
60
74
  interface DataFrameActions {
61
75
  handle_search: (query: string | null) => void;
62
76
  handle_sort: (col: number, direction: SortDirection) => void;
77
+ handle_filter: (
78
+ col: number,
79
+ datatype: FilterDatatype,
80
+ filter: string,
81
+ value: string
82
+ ) => void;
63
83
  get_sort_status: (name: string, headers: string[]) => "none" | "asc" | "desc";
64
84
  sort_data: (
65
85
  data: any[][],
@@ -109,6 +129,7 @@ interface DataFrameActions {
109
129
  dispatch: (e: "change" | "input", detail?: any) => void
110
130
  ) => Promise<void>;
111
131
  reset_sort_state: () => void;
132
+ reset_filter_state: () => void;
112
133
  set_active_cell_menu: (
113
134
  menu: { row: number; col: number; x: number; y: number } | null
114
135
  ) => void;
@@ -158,7 +179,7 @@ export interface DataFrameContext {
158
179
  styling?: string[][] | null;
159
180
  els?: Record<
160
181
  string,
161
- { cell: HTMLTableCellElement | null; input: HTMLInputElement | null }
182
+ { cell: HTMLTableCellElement | null; input: HTMLTextAreaElement | null }
162
183
  >;
163
184
  parent_element?: HTMLElement;
164
185
  get_data_at?: (row: number, col: number) => string | number;
@@ -209,6 +230,15 @@ function create_actions(
209
230
  return { data: new_data, headers: new_headers };
210
231
  };
211
232
 
233
+ const update_array = (
234
+ source: { id: string; value: string | number }[][] | string[][] | null,
235
+ target: any[] | null | undefined
236
+ ): void => {
237
+ if (source && target) {
238
+ target.splice(0, target.length, ...JSON.parse(JSON.stringify(source)));
239
+ }
240
+ };
241
+
212
242
  return {
213
243
  handle_search: (query) =>
214
244
  update_state((s) => ({ current_search_query: query })),
@@ -247,6 +277,39 @@ function create_actions(
247
277
  }
248
278
  };
249
279
  }),
280
+ handle_filter: (col, datatype, filter, value) =>
281
+ update_state((s) => {
282
+ const filter_cols = s.filter_state.filter_columns.some(
283
+ (c) => c.col === col
284
+ )
285
+ ? s.filter_state.filter_columns.filter((c) => c.col !== col)
286
+ : [
287
+ ...s.filter_state.filter_columns,
288
+ { col, datatype, filter, value }
289
+ ];
290
+
291
+ const initial_data =
292
+ s.filter_state.initial_data ||
293
+ (context.data && filter_cols.length > 0
294
+ ? {
295
+ data: JSON.parse(JSON.stringify(context.data)),
296
+ display_value: context.display_value
297
+ ? JSON.parse(JSON.stringify(context.display_value))
298
+ : null,
299
+ styling: context.styling
300
+ ? JSON.parse(JSON.stringify(context.styling))
301
+ : null
302
+ }
303
+ : null);
304
+
305
+ return {
306
+ filter_state: {
307
+ ...s.filter_state,
308
+ filter_columns: filter_cols,
309
+ initial_data: initial_data
310
+ }
311
+ };
312
+ }),
250
313
  get_sort_status: (name, headers) => {
251
314
  const s = get(state);
252
315
  const sort_item = s.sort_state.sort_columns.find(
@@ -345,7 +408,8 @@ function create_actions(
345
408
  ) {
346
409
  if (!dequal(current_headers, previous_headers)) {
347
410
  update_state((s) => ({
348
- sort_state: { sort_columns: [], row_order: [], initial_data: null }
411
+ sort_state: { sort_columns: [], row_order: [], initial_data: null },
412
+ filter_state: { filter_columns: [], initial_data: null }
349
413
  }));
350
414
  }
351
415
  dispatch("change", {
@@ -361,22 +425,6 @@ function create_actions(
361
425
  if (s.sort_state.initial_data && context.data) {
362
426
  const original = s.sort_state.initial_data;
363
427
 
364
- const update_array = (
365
- source:
366
- | { id: string; value: string | number }[][]
367
- | string[][]
368
- | null,
369
- target: any[] | null | undefined
370
- ): void => {
371
- if (source && target) {
372
- target.splice(
373
- 0,
374
- target.length,
375
- ...JSON.parse(JSON.stringify(source))
376
- );
377
- }
378
- };
379
-
380
428
  update_array(original.data, context.data);
381
429
  update_array(original.display_value, context.display_value);
382
430
  update_array(original.styling, context.styling);
@@ -386,6 +434,20 @@ function create_actions(
386
434
  sort_state: { sort_columns: [], row_order: [], initial_data: null }
387
435
  };
388
436
  }),
437
+ reset_filter_state: () =>
438
+ update_state((s) => {
439
+ if (s.filter_state.initial_data && context.data) {
440
+ const original = s.filter_state.initial_data;
441
+
442
+ update_array(original.data, context.data);
443
+ update_array(original.display_value, context.display_value);
444
+ update_array(original.styling, context.styling);
445
+ }
446
+
447
+ return {
448
+ filter_state: { filter_columns: [], initial_data: null }
449
+ };
450
+ }),
389
451
  set_active_cell_menu: (menu) =>
390
452
  update_state((s) => ({
391
453
  ui_state: { ...s.ui_state, active_cell_menu: menu }
@@ -599,6 +661,7 @@ export function create_dataframe_context(
599
661
  config,
600
662
  current_search_query: null,
601
663
  sort_state: { sort_columns: [], row_order: [], initial_data: null },
664
+ filter_state: { filter_columns: [], initial_data: null },
602
665
  ui_state: {
603
666
  active_cell_menu: null,
604
667
  active_header_menu: null,
package/shared/types.ts CHANGED
@@ -22,7 +22,7 @@ export type ElementRefs = Record<
22
22
  string,
23
23
  {
24
24
  cell: null | HTMLTableCellElement;
25
- input: null | HTMLInputElement;
25
+ input: null | HTMLTextAreaElement;
26
26
  }
27
27
  >;
28
28
 
@@ -5,7 +5,7 @@ export function make_headers(
5
5
  col_count: [number, "fixed" | "dynamic"],
6
6
  els: Record<
7
7
  string,
8
- { cell: null | HTMLTableCellElement; input: null | HTMLInputElement }
8
+ { cell: null | HTMLTableCellElement; input: null | HTMLTextAreaElement }
9
9
  >,
10
10
  make_id: () => string
11
11
  ): HeadersWithIDs {
@@ -38,7 +38,7 @@ export function process_data(
38
38
  values: (string | number)[][],
39
39
  els: Record<
40
40
  string,
41
- { cell: null | HTMLTableCellElement; input: null | HTMLInputElement }
41
+ { cell: null | HTMLTableCellElement; input: null | HTMLTextAreaElement }
42
42
  >,
43
43
  data_binding: Record<string, any>,
44
44
  make_id: () => string,
@@ -0,0 +1,207 @@
1
+ import { filter_table_data } from "./table_utils";
2
+
3
+ export type FilterDatatype = "string" | "number";
4
+
5
+ export function filter_data(
6
+ data: { id: string; value: string | number }[][],
7
+ filter_columns: {
8
+ col: number;
9
+ datatype: FilterDatatype;
10
+ filter: string;
11
+ value: string;
12
+ }[]
13
+ ): number[] {
14
+ if (!data || !data.length || !data[0]) {
15
+ return [];
16
+ }
17
+ let row_indices = [...Array(data.length)].map((_, i) => i);
18
+
19
+ if (filter_columns.length > 0) {
20
+ filter_columns.forEach((column) => {
21
+ if (column.datatype === "string") {
22
+ switch (column.filter) {
23
+ case "Contains":
24
+ row_indices = row_indices.filter((i) =>
25
+ data[i][column.col]?.value.toString().includes(column.value)
26
+ );
27
+ break;
28
+ case "Does not contain":
29
+ row_indices = row_indices.filter(
30
+ (i) =>
31
+ !data[i][column.col]?.value.toString().includes(column.value)
32
+ );
33
+ break;
34
+ case "Starts with":
35
+ row_indices = row_indices.filter((i) =>
36
+ data[i][column.col]?.value.toString().startsWith(column.value)
37
+ );
38
+ break;
39
+ case "Ends with":
40
+ row_indices = row_indices.filter((i) =>
41
+ data[i][column.col]?.value.toString().endsWith(column.value)
42
+ );
43
+ break;
44
+ case "Is":
45
+ row_indices = row_indices.filter(
46
+ (i) => data[i][column.col]?.value.toString() === column.value
47
+ );
48
+ break;
49
+ case "Is not":
50
+ row_indices = row_indices.filter(
51
+ (i) => !(data[i][column.col]?.value.toString() === column.value)
52
+ );
53
+ break;
54
+ case "Is empty":
55
+ row_indices = row_indices.filter(
56
+ (i) => data[i][column.col]?.value.toString() === ""
57
+ );
58
+ break;
59
+ case "Is not empty":
60
+ row_indices = row_indices.filter(
61
+ (i) => !(data[i][column.col]?.value.toString() === "")
62
+ );
63
+ break;
64
+ }
65
+ } else if (column.datatype === "number") {
66
+ switch (column.filter) {
67
+ case "=":
68
+ row_indices = row_indices.filter((i) => {
69
+ if (
70
+ !isNaN(Number(data[i][column.col]?.value)) &&
71
+ !isNaN(Number(column.value))
72
+ ) {
73
+ return (
74
+ Number(data[i][column.col]?.value) === Number(column.value)
75
+ );
76
+ }
77
+ return false;
78
+ });
79
+ break;
80
+ case "≠":
81
+ row_indices = row_indices.filter((i) => {
82
+ if (
83
+ !isNaN(Number(data[i][column.col]?.value)) &&
84
+ !isNaN(Number(column.value))
85
+ ) {
86
+ return !(
87
+ Number(data[i][column.col]?.value) === Number(column.value)
88
+ );
89
+ }
90
+ return false;
91
+ });
92
+ break;
93
+ case ">":
94
+ row_indices = row_indices.filter((i) => {
95
+ if (
96
+ !isNaN(Number(data[i][column.col]?.value)) &&
97
+ !isNaN(Number(column.value))
98
+ ) {
99
+ return (
100
+ Number(data[i][column.col]?.value) > Number(column.value)
101
+ );
102
+ }
103
+ return false;
104
+ });
105
+ break;
106
+ case "<":
107
+ row_indices = row_indices.filter((i) => {
108
+ if (
109
+ !isNaN(Number(data[i][column.col]?.value)) &&
110
+ !isNaN(Number(column.value))
111
+ ) {
112
+ return (
113
+ Number(data[i][column.col]?.value) < Number(column.value)
114
+ );
115
+ }
116
+ return false;
117
+ });
118
+ break;
119
+ case "≥":
120
+ row_indices = row_indices.filter((i) => {
121
+ if (
122
+ !isNaN(Number(data[i][column.col]?.value)) &&
123
+ !isNaN(Number(column.value))
124
+ ) {
125
+ return (
126
+ Number(data[i][column.col]?.value) >= Number(column.value)
127
+ );
128
+ }
129
+ return false;
130
+ });
131
+ break;
132
+ case "≤":
133
+ row_indices = row_indices.filter((i) => {
134
+ if (
135
+ !isNaN(Number(data[i][column.col]?.value)) &&
136
+ !isNaN(Number(column.value))
137
+ ) {
138
+ return (
139
+ Number(data[i][column.col]?.value) <= Number(column.value)
140
+ );
141
+ }
142
+ return false;
143
+ });
144
+ break;
145
+ case "Is empty":
146
+ row_indices = row_indices.filter(
147
+ (i) => data[i][column.col]?.value.toString() === ""
148
+ );
149
+ break;
150
+ case "Is not empty":
151
+ row_indices = row_indices.filter((i) => {
152
+ if (!isNaN(Number(data[i][column.col]?.value))) {
153
+ return !(data[i][column.col]?.value.toString() === "");
154
+ }
155
+ return false;
156
+ });
157
+ break;
158
+ }
159
+ }
160
+ });
161
+ return row_indices;
162
+ }
163
+ return [...Array(data.length)].map((_, i) => i);
164
+ }
165
+
166
+ export function filter_data_and_preserve_selection(
167
+ data: { id: string; value: string | number }[][],
168
+ display_value: string[][] | null,
169
+ styling: string[][] | null,
170
+ filter_columns: {
171
+ col: number;
172
+ datatype: FilterDatatype;
173
+ filter: string;
174
+ value: string;
175
+ }[],
176
+ selected: [number, number] | false,
177
+ get_current_indices: (
178
+ id: string,
179
+ data: { id: string; value: string | number }[][]
180
+ ) => [number, number],
181
+ original_data?: { id: string; value: string | number }[][],
182
+ original_display_value?: string[][] | null,
183
+ original_styling?: string[][] | null
184
+ ): { data: typeof data; selected: [number, number] | false } {
185
+ let id = null;
186
+ if (selected && selected[0] in data && selected[1] in data[selected[0]]) {
187
+ id = data[selected[0]][selected[1]].id;
188
+ }
189
+
190
+ filter_table_data(
191
+ data,
192
+ display_value,
193
+ styling,
194
+ filter_columns,
195
+ original_data,
196
+ original_display_value,
197
+ original_styling
198
+ );
199
+
200
+ let new_selected = selected;
201
+ if (id) {
202
+ const [i, j] = get_current_indices(id, data);
203
+ new_selected = [i, j];
204
+ }
205
+
206
+ return { data, selected: new_selected };
207
+ }
@@ -180,9 +180,10 @@ async function handle_enter_key(
180
180
  const state = get(ctx.state);
181
181
  if (!state.config.editable) return false;
182
182
 
183
- event.preventDefault();
184
-
185
183
  const editing = state.ui_state.editing;
184
+ if (editing && event.shiftKey) return false;
185
+
186
+ event.preventDefault();
186
187
 
187
188
  if (editing && dequal(editing, [i, j])) {
188
189
  const cell_id = ctx.data[i][j].id;
@@ -191,6 +192,8 @@ async function handle_enter_key(
191
192
  await save_cell_value(input_el.value, ctx, i, j);
192
193
  }
193
194
  ctx.actions.set_editing(false);
195
+ await tick();
196
+ ctx.parent_element?.focus();
194
197
  } else {
195
198
  ctx.actions.set_editing([i, j]);
196
199
  }
@@ -1,6 +1,8 @@
1
1
  import type { Headers, HeadersWithIDs, TableCell, TableData } from "../types";
2
2
  import { sort_data } from "./sort_utils";
3
+ import { filter_data } from "./filter_utils";
3
4
  import type { SortDirection } from "./sort_utils";
5
+ import type { FilterDatatype } from "./filter_utils";
4
6
  import { dsvFormat } from "d3-dsv";
5
7
 
6
8
  export function make_cell_id(row: number, col: number): string {
@@ -49,6 +51,56 @@ export function sort_table_data(
49
51
  }
50
52
  }
51
53
 
54
+ export function filter_table_data(
55
+ data: TableData,
56
+ display_value: string[][] | null,
57
+ styling: string[][] | null,
58
+ filter_columns: {
59
+ col: number;
60
+ datatype: FilterDatatype;
61
+ filter: string;
62
+ value: string;
63
+ }[],
64
+ original_data?: TableData,
65
+ original_display_value?: string[][] | null,
66
+ original_styling?: string[][] | null
67
+ ): void {
68
+ const base_data = original_data ?? data;
69
+ const base_display_value = original_display_value ?? display_value;
70
+ const base_styling = original_styling ?? styling;
71
+
72
+ if (!filter_columns.length) {
73
+ data.splice(0, data.length, ...base_data.map((row) => [...row]));
74
+ if (display_value && base_display_value) {
75
+ display_value.splice(
76
+ 0,
77
+ display_value.length,
78
+ ...base_display_value.map((row) => [...row])
79
+ );
80
+ }
81
+ if (styling && base_styling) {
82
+ styling.splice(0, styling.length, ...base_styling.map((row) => [...row]));
83
+ }
84
+ return;
85
+ }
86
+ if (!data || !data.length) return;
87
+
88
+ const indices = filter_data(base_data, filter_columns);
89
+
90
+ const new_data = indices.map((i: number) => base_data[i]);
91
+ data.splice(0, data.length, ...new_data);
92
+
93
+ if (display_value && base_display_value) {
94
+ const new_display = indices.map((i: number) => base_display_value[i]);
95
+ display_value.splice(0, display_value.length, ...new_display);
96
+ }
97
+
98
+ if (styling && base_styling) {
99
+ const new_styling = indices.map((i: number) => base_styling[i]);
100
+ styling.splice(0, styling.length, ...new_styling);
101
+ }
102
+ }
103
+
52
104
  export async function copy_table_data(
53
105
  data: TableData,
54
106
  selected_cells: [number, number][] | null