Skip to content

Commit 484db5a

Browse files
committed
Merge branch 'data_view_meta' into df_input_cell_selection
* data_view_meta: Add demo apps for `update_sort()` and `update_filter()` Add `update_sort()` and `update_filter()` to DF bug(output transformer): fix transformer auto-registration (#1394) fix: Add wait till pulse animation has started (#1393) bug(test-deploy): Add retries for deploy tests (#1392) Add busy indicator tests (#1391) Yield to give "synchronous" writes a chance to complete (#1388) chore(busy indicator): Update busy indicator css files (#1389) `ColumnFilter` and `ColumnSort` should use `col: num` and not `id: str` for consistency Lints Have `.data_view()` use `.data_view_info()` information for consistent subsetting feat(cli): Add `shiny --version` (#1387) fix(selectize): Accept jsonifiable values in `options` dictionary (#1382) bug(data frame): Use `<ID>_data_view_rows` (#1386) test(data frame): Verify that data frame's outputs are reset before moving forward (#1383) Send busy/idle at the right times (#1380) feat(data frame): Restore `input.<ID>_selected_rows()`. Rename `input.<ID>_data_view_indices` to `input.<ID>_data_view_rows` (#1377) Apply suggestions from code review
2 parents 9727f1b + 86b5e42 commit 484db5a

File tree

48 files changed

+878
-140
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+878
-140
lines changed

CHANGELOG.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [UNRELEASED]
99

10-
### Breaking Changes
10+
### `input` key changes
11+
12+
* Restored `@render.data_frame`'s (prematurely removed in v0.9.0) input value `input.<ID>_selected_rows()`. Please use `<ID>.input_cell_selection()["rows"]` and consider `input.<ID>_selected_rows()` deprecated. (#1345, #1377)
13+
14+
* `@render.data_frame`'s input value `input.<ID>_data_view_indices` has been renamed to `input.<ID>_data_view_rows` for consistent naming. Please use `input.<ID>_data_view_rows` and consider `input.<ID>_data_view_indices` deprecated. (#1377)
1115

1216
* `@render.data_frame`'s input value `input.<ID>_data_view_indices` has been renamed to `input.<ID>_data_view_rows` for consistent naming. Please use `input.<ID>_data_view_rows` and consider `input.<ID>_data_view_indices` deprecated. (#1374)
1317

@@ -29,7 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2933

3034
* Fixed an issue that prevented Shiny from serving the `font.css` file referenced in Shiny's Bootstrap CSS file. (#1342)
3135

32-
* Removed temporary state where a data frame renderer would try to subset to selected rows that did not exist. (#1351)
36+
* Removed temporary state where a data frame renderer would try to subset to selected rows that did not exist. (#1351, #1377)
3337

3438
### Other changes
3539

js/data-frame/filter-numeric.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ interface FilterNumericImplProps {
5252
const FilterNumericImpl: React.FC<FilterNumericImplProps> = (props) => {
5353
const [min, max] = props.value;
5454
const { editing, onFocus } = props;
55+
const [rangeMin, rangeMax] = props.range();
5556

5657
const minInputRef = useRef<HTMLInputElement>(null);
5758
const maxInputRef = useRef<HTMLInputElement>(null);
@@ -77,11 +78,14 @@ const FilterNumericImpl: React.FC<FilterNumericImplProps> = (props) => {
7778
}`}
7879
style={{ flex: "1 1 0", width: "0" }}
7980
type="number"
80-
placeholder={createPlaceholder(editing, "Min", props.range()[0])}
81+
placeholder={createPlaceholder(editing, "Min", rangeMin)}
8182
defaultValue={min}
83+
// min={rangeMin}
84+
// max={rangeMax}
8285
step="any"
8386
onChange={(e) => {
8487
const value = coerceToNum(e.target.value);
88+
if (!minInputRef.current) return;
8589
minInputRef.current.classList.toggle(
8690
"is-invalid",
8791
!e.target.checkValidity()
@@ -96,11 +100,14 @@ const FilterNumericImpl: React.FC<FilterNumericImplProps> = (props) => {
96100
}`}
97101
style={{ flex: "1 1 0", width: "0" }}
98102
type="number"
99-
placeholder={createPlaceholder(editing, "Max", props.range()[1])}
103+
placeholder={createPlaceholder(editing, "Max", rangeMax)}
100104
defaultValue={max}
105+
// min={rangeMin}
106+
// max={rangeMax}
101107
step="any"
102108
onChange={(e) => {
103109
const value = coerceToNum(e.target.value);
110+
if (!maxInputRef.current) return;
104111
maxInputRef.current.classList.toggle(
105112
"is-invalid",
106113
!e.target.checkValidity()
@@ -118,7 +125,7 @@ function createPlaceholder(
118125
value: number | undefined
119126
) {
120127
if (!editing) {
121-
return null;
128+
return undefined;
122129
} else if (typeof value === "undefined") {
123130
return label;
124131
} else {

js/data-frame/filter.tsx

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@ import React, {
1818
} from "react";
1919
import { FilterNumeric } from "./filter-numeric";
2020

21+
type FilterValueString = string;
22+
type FilterValueNumeric =
23+
| [number, number]
24+
| [number | undefined, number]
25+
| [number, number | undefined];
26+
type FilterValue = FilterValueString | FilterValueNumeric;
27+
28+
export type { ColumnFiltersState, FilterValue };
29+
2130
export function useFilters<TData>(enabled: boolean | undefined): {
2231
columnFilters: ColumnFiltersState;
2332
setColumnFilters: React.Dispatch<React.SetStateAction<ColumnFiltersState>>;
@@ -59,13 +68,16 @@ export interface FilterProps
5968
export const Filter: FC<FilterProps> = ({ header, className, ...props }) => {
6069
const typeHint = header.column.columnDef.meta?.typeHint;
6170

62-
if (typeHint.type === "html") {
63-
// Do not filter on html types
64-
return null;
65-
}
71+
// Do not filter on unknown types
72+
if (!typeHint) return null;
73+
74+
// Do not filter on html types
75+
if (typeHint.type === "html") return null;
76+
6677
if (typeHint.type === "numeric") {
6778
const [from, to] = (header.column.getFilterValue() as
68-
| [number | undefined, number | undefined]
79+
| FilterValueNumeric
80+
| [undefined, undefined]
6981
| undefined) ?? [undefined, undefined];
7082

7183
const range = () => {
@@ -83,6 +95,7 @@ export const Filter: FC<FilterProps> = ({ header, className, ...props }) => {
8395
return (
8496
<input
8597
{...props}
98+
value={header.column.getFilterValue() as string}
8699
className={`form-control form-control-sm ${className}`}
87100
type="text"
88101
onChange={(e) => header.column.setFilterValue(e.target.value)}

js/data-frame/index.tsx

Lines changed: 109 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import {
44
ColumnDef,
55
RowData,
66
RowModel,
7-
SortingState,
87
TableOptions,
98
flexRender,
109
getCoreRowModel,
@@ -29,14 +28,14 @@ import { useImmer } from "use-immer";
2928
import { TableBodyCell } from "./cell";
3029
import { getCellEditMapObj, useCellEditMap } from "./cell-edit-map";
3130
import { findFirstItemInView, getStyle } from "./dom-utils";
32-
import { Filter, useFilters } from "./filter";
31+
import { ColumnFiltersState, Filter, FilterValue, useFilters } from "./filter";
3332
import type { CellSelection, SelectionModesProp } from "./selection";
3433
import {
3534
SelectionModes,
3635
initRowSelectionModes,
3736
useSelection,
3837
} from "./selection";
39-
import { useSort } from "./sort";
38+
import { SortingState, useSort } from "./sort";
4039
import { SortArrow } from "./sort-arrows";
4140
import css from "./styles.scss";
4241
import { useTabindexGroup } from "./tabindex-group";
@@ -175,10 +174,14 @@ const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = ({
175174
const dataOriginal = useMemo(() => rowData, [rowData]);
176175
const [dataState, setData] = useImmer(rowData);
177176

178-
const { sorting, sortState, sortingTableOptions } = useSort();
177+
const { sorting, sortState, sortingTableOptions, setSorting } = useSort();
179178

180-
const { columnFilters, columnFiltersState, filtersTableOptions } =
181-
useFilters<unknown[]>(withFilters);
179+
const {
180+
columnFilters,
181+
columnFiltersState,
182+
filtersTableOptions,
183+
setColumnFilters,
184+
} = useFilters<unknown[]>(withFilters);
182185

183186
const options: TableOptions<unknown[]> = {
184187
data: dataState,
@@ -278,7 +281,7 @@ const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = ({
278281
);
279282

280283
useEffect(() => {
281-
const handleMessage = (
284+
const handleCellSelection = (
282285
event: CustomEvent<{ cellSelection: CellSelection }>
283286
) => {
284287
// We convert "None" to an empty tuple on the python side
@@ -307,17 +310,85 @@ const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = ({
307310

308311
element.addEventListener(
309312
"updateCellSelection",
310-
handleMessage as EventListener
313+
handleCellSelection as EventListener
311314
);
312315

313316
return () => {
314317
element.removeEventListener(
315318
"updateCellSelection",
316-
handleMessage as EventListener
319+
handleCellSelection as EventListener
317320
);
318321
};
319322
}, [id, rowSelection, rowData]);
320323

324+
useEffect(() => {
325+
const handleColumnSort = (
326+
event: CustomEvent<{ sort: { col: number; desc: boolean }[] }>
327+
) => {
328+
const shinySorting = event.detail.sort;
329+
const columnSorting: SortingState = [];
330+
331+
shinySorting.map((sort) => {
332+
columnSorting.push({
333+
id: columns[sort.col],
334+
desc: sort.desc,
335+
});
336+
});
337+
setSorting(columnSorting);
338+
};
339+
340+
if (!id) return;
341+
342+
const element = document.getElementById(id);
343+
if (!element) return;
344+
345+
element.addEventListener(
346+
"updateColumnSort",
347+
handleColumnSort as EventListener
348+
);
349+
350+
return () => {
351+
element.removeEventListener(
352+
"updateColumnSort",
353+
handleColumnSort as EventListener
354+
);
355+
};
356+
}, [columns, id, setSorting]);
357+
358+
useEffect(() => {
359+
const handleColumnFilter = (
360+
event: CustomEvent<{ filter: { col: number; value: FilterValue }[] }>
361+
) => {
362+
const shinyFilters = event.detail.filter;
363+
364+
const columnFilters: ColumnFiltersState = [];
365+
shinyFilters.map((filter) => {
366+
columnFilters.push({
367+
id: columns[filter.col],
368+
value: filter.value,
369+
});
370+
});
371+
setColumnFilters(columnFilters);
372+
};
373+
374+
if (!id) return;
375+
376+
const element = document.getElementById(id);
377+
if (!element) return;
378+
379+
element.addEventListener(
380+
"updateColumnFilter",
381+
handleColumnFilter as EventListener
382+
);
383+
384+
return () => {
385+
element.removeEventListener(
386+
"updateColumnFilter",
387+
handleColumnFilter as EventListener
388+
);
389+
};
390+
}, [columns, id, setColumnFilters]);
391+
321392
useEffect(() => {
322393
if (!id) return;
323394
let shinyValue: CellSelection | null = null;
@@ -338,22 +409,42 @@ const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = ({
338409

339410
useEffect(() => {
340411
if (!id) return;
341-
Shiny.setInputValue!(`${id}_column_sort`, sorting);
342-
}, [id, sorting]);
412+
const shinySort: { col: number; desc: boolean }[] = [];
413+
sorting.map((sortObj) => {
414+
const columnNum = columns.indexOf(sortObj.id);
415+
shinySort.push({
416+
col: columnNum,
417+
desc: sortObj.desc,
418+
});
419+
});
420+
Shiny.setInputValue!(`${id}_column_sort`, shinySort);
421+
}, [columns, id, sorting]);
343422
useEffect(() => {
344423
if (!id) return;
345-
Shiny.setInputValue!(`${id}_column_filter`, columnFilters);
346-
}, [id, columnFilters]);
424+
const shinyFilter: {
425+
col: number;
426+
value: FilterValue;
427+
}[] = [];
428+
columnFilters.map((filterObj) => {
429+
const columnNum = columns.indexOf(filterObj.id);
430+
shinyFilter.push({
431+
col: columnNum,
432+
value: filterObj.value as FilterValue,
433+
});
434+
});
435+
Shiny.setInputValue!(`${id}_column_filter`, shinyFilter);
436+
}, [id, columnFilters, columns]);
347437
useEffect(() => {
348438
if (!id) return;
349-
// Already prefiltered rows!
350-
const shinyValue: RowModel<unknown[]> = table.getSortedRowModel();
351439

352-
const rowIndices = table.getSortedRowModel().rows.map((row) => row.index);
353-
Shiny.setInputValue!(`${id}_data_view_rows`, rowIndices);
440+
const shinyRows: number[] = table
441+
// Already prefiltered rows!
442+
.getSortedRowModel()
443+
.rows.map((row) => row.index);
444+
Shiny.setInputValue!(`${id}_data_view_rows`, shinyRows);
354445

355446
// Legacy value as of 2024-05-13
356-
Shiny.setInputValue!(`${id}_data_view_indices`, rowIndices);
447+
Shiny.setInputValue!(`${id}_data_view_indices`, shinyRows);
357448
}, [
358449
id,
359450
table,

js/data-frame/sort.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import {
55
} from "@tanstack/react-table";
66
import React, { useState } from "react";
77

8+
import type { ColumnSort } from "@tanstack/react-table";
9+
10+
export type { ColumnSort, SortingState };
11+
812
export function useSort<TData>(): {
913
sorting: SortingState;
1014
setSorting: React.Dispatch<React.SetStateAction<SortingState>>;

setup.cfg

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ test =
6565
pytest>=6.2.4
6666
pytest-asyncio>=0.17.2
6767
pytest-playwright>=0.3.0
68+
playwright>=1.43.0
6869
pytest-xdist
6970
pytest-timeout
7071
pytest-rerunfailures
@@ -93,6 +94,7 @@ test =
9394
folium
9495
palmerpenguins
9596
faicons
97+
ridgeplot
9698
dev =
9799
black>=24.0
98100
flake8>=6.0.0

shiny/_main.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,15 @@
1818

1919
import shiny
2020

21-
from . import _autoreload, _hostenv, _static, _utils
21+
from . import __version__, _autoreload, _hostenv, _static, _utils
2222
from ._docstring import no_example
2323
from ._typing_extensions import NotRequired, TypedDict
2424
from .express import is_express_app
2525
from .express._utils import escape_to_var_name
2626

2727

2828
@click.group("main")
29+
@click.version_option(__version__)
2930
def main() -> None:
3031
pass
3132

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from shared import mtcars
2+
3+
from shiny import App, reactive, render, ui
4+
5+
app_ui = ui.page_fillable(
6+
ui.card(
7+
ui.layout_column_wrap(
8+
ui.input_action_button("btn", "Filter on columns 0, 1, and 3"),
9+
ui.input_action_button("reset", "Reset column filters"),
10+
fill=False,
11+
),
12+
ui.output_data_frame("df"),
13+
),
14+
)
15+
16+
17+
def server(input, output, session):
18+
data = reactive.value(mtcars.iloc[:, range(4)])
19+
20+
@render.data_frame
21+
def df():
22+
return render.DataGrid(data(), filters=True)
23+
24+
@reactive.effect
25+
@reactive.event(input.reset)
26+
async def _():
27+
await df.update_filter(None)
28+
29+
@reactive.effect
30+
@reactive.event(input.btn)
31+
async def _():
32+
await df.update_filter(
33+
[
34+
{"col": 0, "value": [19, 25]},
35+
{"col": 1, "value": [None, 6]},
36+
{"col": 3, "value": [100, None]},
37+
]
38+
)
39+
40+
41+
app = App(app_ui, server, debug=True)

0 commit comments

Comments
 (0)