@gradio/dataframe 0.17.17 → 0.18.0

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.
@@ -0,0 +1,248 @@
1
+ <script lang="ts">
2
+ import { onMount } from "svelte";
3
+ import { Check } from "@gradio/icons";
4
+ import DropdownArrow from "../../icons/src/DropdownArrow.svelte";
5
+ import type { FilterDatatype } from "./context/dataframe_context";
6
+
7
+ export let on_filter: (
8
+ datatype: FilterDatatype,
9
+ selected_filter: string,
10
+ value: string
11
+ ) => void = () => {};
12
+
13
+ let menu_element: HTMLDivElement;
14
+ let datatype: "string" | "number" = "string";
15
+ let current_filter = "Contains";
16
+ let filter_dropdown_open = false;
17
+ let filter_input_value = "";
18
+
19
+ const filter_options = {
20
+ string: [
21
+ "Contains",
22
+ "Does not contain",
23
+ "Starts with",
24
+ "Ends with",
25
+ "Is",
26
+ "Is not",
27
+ "Is empty",
28
+ "Is not empty"
29
+ ],
30
+ number: ["=", "≠", ">", "<", "≥", "≤", "Is empty", "Is not empty"]
31
+ };
32
+
33
+ onMount(() => {
34
+ position_menu();
35
+ });
36
+
37
+ function position_menu(): void {
38
+ if (!menu_element) return;
39
+
40
+ const viewport_width = window.innerWidth;
41
+ const viewport_height = window.innerHeight;
42
+ const menu_rect = menu_element.getBoundingClientRect();
43
+
44
+ const x = (viewport_width - menu_rect.width) / 2;
45
+ const y = (viewport_height - menu_rect.height) / 2;
46
+
47
+ menu_element.style.left = `${x}px`;
48
+ menu_element.style.top = `${y}px`;
49
+ }
50
+
51
+ function handle_filter_input(e: Event): void {
52
+ const target = e.target as HTMLInputElement;
53
+ filter_input_value = target.value;
54
+ }
55
+ </script>
56
+
57
+ <div>
58
+ <div class="background"></div>
59
+ <div bind:this={menu_element} class="filter-menu">
60
+ <div class="filter-datatype-container">
61
+ <span>Filter as</span>
62
+ <button
63
+ on:click|stopPropagation={() => {
64
+ datatype = datatype === "string" ? "number" : "string";
65
+ current_filter = filter_options[datatype][0];
66
+ }}
67
+ aria-label={`Change filter type. Filtering ${datatype}s`}
68
+ >
69
+ {datatype}
70
+ </button>
71
+ </div>
72
+
73
+ <div class="input-container">
74
+ <div class="filter-dropdown">
75
+ <button
76
+ on:click|stopPropagation={() =>
77
+ (filter_dropdown_open = !filter_dropdown_open)}
78
+ aria-label={`Change filter. Using '${current_filter}'`}
79
+ >
80
+ {current_filter}
81
+ <DropdownArrow />
82
+ </button>
83
+
84
+ {#if filter_dropdown_open}
85
+ <div class="dropdown-filter-options">
86
+ {#each filter_options[datatype] as opt}
87
+ <button
88
+ on:click|stopPropagation={() => {
89
+ current_filter = opt;
90
+ filter_dropdown_open = !filter_dropdown_open;
91
+ }}
92
+ class="filter-option"
93
+ >
94
+ {opt}
95
+ </button>
96
+ {/each}
97
+ </div>
98
+ {/if}
99
+ </div>
100
+
101
+ <input
102
+ type="text"
103
+ value={filter_input_value}
104
+ on:click|stopPropagation
105
+ on:input={handle_filter_input}
106
+ placeholder="Type a value"
107
+ class="filter-input"
108
+ />
109
+ </div>
110
+
111
+ <button
112
+ class="check-button"
113
+ on:click={() => on_filter(datatype, current_filter, filter_input_value)}
114
+ >
115
+ <Check />
116
+ </button>
117
+ </div>
118
+ </div>
119
+
120
+ <style>
121
+ .background {
122
+ position: fixed;
123
+ top: 0;
124
+ left: 0;
125
+ width: 100vw;
126
+ height: 100vh;
127
+ background-color: rgba(0, 0, 0, 0.4);
128
+ z-index: 20;
129
+ }
130
+
131
+ .filter-menu {
132
+ position: fixed;
133
+ background: var(--background-fill-primary);
134
+ border: 1px solid var(--border-color-primary);
135
+ border-radius: var(--radius-sm);
136
+ padding: var(--size-2);
137
+ display: flex;
138
+ flex-direction: column;
139
+ gap: var(--size-2);
140
+ box-shadow: var(--shadow-drop-lg);
141
+ width: 300px;
142
+ z-index: 21;
143
+ }
144
+
145
+ .filter-datatype-container {
146
+ display: flex;
147
+ gap: var(--size-2);
148
+ align-items: center;
149
+ }
150
+
151
+ .filter-menu span {
152
+ font-size: var(--text-sm);
153
+ color: var(--body-text-color);
154
+ }
155
+
156
+ .filter-menu button {
157
+ height: var(--size-6);
158
+ background: none;
159
+ border: 1px solid var(--border-color-primary);
160
+ border-radius: var(--radius-sm);
161
+ padding: var(--size-1) var(--size-2);
162
+ color: var(--body-text-color);
163
+ font-size: var(--text-sm);
164
+ cursor: pointer;
165
+ display: flex;
166
+ align-items: center;
167
+ justify-content: center;
168
+ gap: var(--size-2);
169
+ }
170
+
171
+ .filter-menu button:hover {
172
+ background-color: var(--background-fill-secondary);
173
+ }
174
+
175
+ .filter-input {
176
+ width: var(--size-full);
177
+ height: var(--size-6);
178
+ padding: var(--size-2);
179
+ padding-right: var(--size-8);
180
+ border: 1px solid var(--border-color-primary);
181
+ border-radius: var(--table-radius);
182
+ font-size: var(--text-sm);
183
+ color: var(--body-text-color);
184
+ background: var(--background-fill-secondary);
185
+ transition: all 0.2s ease;
186
+ }
187
+
188
+ .filter-input:hover {
189
+ border-color: var(--border-color-secondary);
190
+ background: var(--background-fill-primary);
191
+ }
192
+
193
+ .filter-input:focus {
194
+ outline: none;
195
+ border-color: var(--color-accent);
196
+ background: var(--background-fill-primary);
197
+ box-shadow: 0 0 0 1px var(--color-accent);
198
+ }
199
+
200
+ .dropdown-filter-options {
201
+ display: flex;
202
+ flex-direction: column;
203
+ background: var(--background-fill-primary);
204
+ border: 1px solid var(--border-color-primary);
205
+ border-radius: var(--radius-sm);
206
+ box-shadow: var(--shadow-drop-md);
207
+ position: absolute;
208
+ z-index: var(--layer-1);
209
+ }
210
+
211
+ .dropdown-filter-options .filter-option {
212
+ border: none;
213
+ justify-content: flex-start;
214
+ }
215
+
216
+ .input-container {
217
+ display: flex;
218
+ gap: var(--size-2);
219
+ align-items: center;
220
+ }
221
+
222
+ .input-container button {
223
+ width: 130px;
224
+ }
225
+
226
+ :global(svg.dropdown-arrow) {
227
+ width: var(--size-4);
228
+ height: var(--size-4);
229
+ margin-left: auto;
230
+ }
231
+
232
+ .filter-menu .check-button {
233
+ background: var(--color-accent);
234
+ color: white;
235
+ border: none;
236
+ width: var(--size-full);
237
+ height: var(--size-6);
238
+ border-radius: var(--radius-sm);
239
+ display: flex;
240
+ align-items: center;
241
+ justify-content: center;
242
+ padding: var(--size-1);
243
+ }
244
+
245
+ .check-button:hover {
246
+ background: var(--color-accent-soft);
247
+ }
248
+ </style>
@@ -1,7 +1,8 @@
1
1
  <script lang="ts" context="module">
2
2
  import {
3
3
  create_dataframe_context,
4
- type SortDirection
4
+ type SortDirection,
5
+ type FilterDatatype
5
6
  } from "./context/dataframe_context";
6
7
  </script>
7
8
 
@@ -43,6 +44,7 @@
43
44
  type DragHandlers
44
45
  } from "./utils/drag_utils";
45
46
  import { sort_data_and_preserve_selection } from "./utils/sort_utils";
47
+ import { filter_data_and_preserve_selection } from "./utils/filter_utils";
46
48
 
47
49
  export let datatype: Datatype | Datatype[];
48
50
  export let label: string | null = null;
@@ -261,6 +263,12 @@
261
263
  df_actions.reset_sort_state();
262
264
  }
263
265
 
266
+ if ($df_state.filter_state.filter_columns.length > 0) {
267
+ filter_data(data, display_value, styling);
268
+ } else {
269
+ df_actions.reset_filter_state();
270
+ }
271
+
264
272
  if ($df_state.current_search_query) {
265
273
  df_actions.handle_search(null);
266
274
  }
@@ -340,11 +348,33 @@
340
348
 
341
349
  function clear_sort(): void {
342
350
  df_actions.reset_sort_state();
351
+ sort_data(data, display_value, styling);
343
352
  }
344
353
 
345
- $: if ($df_state.sort_state.sort_columns.length > 0) {
346
- sort_data(data, display_value, styling);
347
- df_actions.update_row_order(data);
354
+ $: {
355
+ if ($df_state.filter_state.filter_columns.length > 0) {
356
+ filter_data(data, display_value, styling);
357
+ }
358
+
359
+ if ($df_state.sort_state.sort_columns.length > 0) {
360
+ sort_data(data, display_value, styling);
361
+ df_actions.update_row_order(data);
362
+ }
363
+ }
364
+
365
+ function handle_filter(
366
+ col: number,
367
+ datatype: FilterDatatype,
368
+ filter: string,
369
+ value: string
370
+ ): void {
371
+ df_actions.handle_filter(col, datatype, filter, value);
372
+ filter_data(data, display_value, styling);
373
+ }
374
+
375
+ function clear_filter(): void {
376
+ df_actions.reset_filter_state();
377
+ filter_data(data, display_value, styling);
348
378
  }
349
379
 
350
380
  async function edit_header(i: number, _select = false): Promise<void> {
@@ -449,6 +479,9 @@
449
479
 
450
480
  function set_cell_widths(): void {
451
481
  const column_count = data[0]?.length || 0;
482
+ if ($df_state.filter_state.filter_columns.length > 0) {
483
+ return;
484
+ }
452
485
  if (
453
486
  last_width_data_length === data.length &&
454
487
  last_width_column_count === column_count &&
@@ -513,6 +546,26 @@
513
546
  selected = result.selected;
514
547
  }
515
548
 
549
+ function filter_data(
550
+ _data: typeof data,
551
+ _display_value: string[][] | null,
552
+ _styling: string[][] | null
553
+ ): void {
554
+ const result = filter_data_and_preserve_selection(
555
+ _data,
556
+ _display_value,
557
+ _styling,
558
+ $df_state.filter_state.filter_columns,
559
+ selected,
560
+ get_current_indices,
561
+ $df_state.filter_state.initial_data?.data,
562
+ $df_state.filter_state.initial_data?.display_value,
563
+ $df_state.filter_state.initial_data?.styling
564
+ );
565
+ data = result.data;
566
+ selected = result.selected;
567
+ }
568
+
516
569
  $: selected_index = !!selected && selected[0];
517
570
 
518
571
  let is_visible = false;
@@ -802,6 +855,7 @@
802
855
  {toggle_header_menu}
803
856
  {end_header_edit}
804
857
  sort_columns={$df_state.sort_state.sort_columns}
858
+ filter_columns={$df_state.filter_state.filter_columns}
805
859
  {latex_delimiters}
806
860
  {line_breaks}
807
861
  {max_chars}
@@ -906,6 +960,7 @@
906
960
  {toggle_header_menu}
907
961
  {end_header_edit}
908
962
  sort_columns={$df_state.sort_state.sort_columns}
963
+ filter_columns={$df_state.filter_state.filter_columns}
909
964
  {latex_delimiters}
910
965
  {line_breaks}
911
966
  {max_chars}
@@ -1022,6 +1077,25 @@
1022
1077
  (item) => item.col === (active_header_menu?.col ?? -1)
1023
1078
  ) + 1 || null
1024
1079
  : null}
1080
+ on_filter={active_header_menu
1081
+ ? (datatype, filter, value) => {
1082
+ if (active_header_menu) {
1083
+ handle_filter(active_header_menu.col, datatype, filter, value);
1084
+ df_actions.set_active_header_menu(null);
1085
+ }
1086
+ }
1087
+ : undefined}
1088
+ on_clear_filter={active_header_menu
1089
+ ? () => {
1090
+ clear_filter();
1091
+ df_actions.set_active_header_menu(null);
1092
+ }
1093
+ : undefined}
1094
+ filter_active={active_header_menu
1095
+ ? $df_state.filter_state.filter_columns.some(
1096
+ (c) => c.col === (active_header_menu?.col ?? -1)
1097
+ )
1098
+ : null}
1025
1099
  />
1026
1100
  {/if}
1027
1101
 
@@ -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;
@@ -34,6 +42,7 @@
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;
@@ -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;
@@ -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,