Skip to content

Commit

Permalink
Merge branch 'data_view_meta' into df_input_cell_selection
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
schloerke committed May 20, 2024
2 parents 9727f1b + 86b5e42 commit 484db5a
Show file tree
Hide file tree
Showing 48 changed files with 878 additions and 140 deletions.
8 changes: 6 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [UNRELEASED]

### Breaking Changes
### `input` key changes

* 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)

* `@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)

* `@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)

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

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

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

### Other changes

Expand Down
13 changes: 10 additions & 3 deletions js/data-frame/filter-numeric.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ interface FilterNumericImplProps {
const FilterNumericImpl: React.FC<FilterNumericImplProps> = (props) => {
const [min, max] = props.value;
const { editing, onFocus } = props;
const [rangeMin, rangeMax] = props.range();

const minInputRef = useRef<HTMLInputElement>(null);
const maxInputRef = useRef<HTMLInputElement>(null);
Expand All @@ -77,11 +78,14 @@ const FilterNumericImpl: React.FC<FilterNumericImplProps> = (props) => {
}`}
style={{ flex: "1 1 0", width: "0" }}
type="number"
placeholder={createPlaceholder(editing, "Min", props.range()[0])}
placeholder={createPlaceholder(editing, "Min", rangeMin)}
defaultValue={min}
// min={rangeMin}
// max={rangeMax}
step="any"
onChange={(e) => {
const value = coerceToNum(e.target.value);
if (!minInputRef.current) return;
minInputRef.current.classList.toggle(
"is-invalid",
!e.target.checkValidity()
Expand All @@ -96,11 +100,14 @@ const FilterNumericImpl: React.FC<FilterNumericImplProps> = (props) => {
}`}
style={{ flex: "1 1 0", width: "0" }}
type="number"
placeholder={createPlaceholder(editing, "Max", props.range()[1])}
placeholder={createPlaceholder(editing, "Max", rangeMax)}
defaultValue={max}
// min={rangeMin}
// max={rangeMax}
step="any"
onChange={(e) => {
const value = coerceToNum(e.target.value);
if (!maxInputRef.current) return;
maxInputRef.current.classList.toggle(
"is-invalid",
!e.target.checkValidity()
Expand All @@ -118,7 +125,7 @@ function createPlaceholder(
value: number | undefined
) {
if (!editing) {
return null;
return undefined;
} else if (typeof value === "undefined") {
return label;
} else {
Expand Down
23 changes: 18 additions & 5 deletions js/data-frame/filter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ import React, {
} from "react";
import { FilterNumeric } from "./filter-numeric";

type FilterValueString = string;
type FilterValueNumeric =
| [number, number]
| [number | undefined, number]
| [number, number | undefined];
type FilterValue = FilterValueString | FilterValueNumeric;

export type { ColumnFiltersState, FilterValue };

export function useFilters<TData>(enabled: boolean | undefined): {
columnFilters: ColumnFiltersState;
setColumnFilters: React.Dispatch<React.SetStateAction<ColumnFiltersState>>;
Expand Down Expand Up @@ -59,13 +68,16 @@ export interface FilterProps
export const Filter: FC<FilterProps> = ({ header, className, ...props }) => {
const typeHint = header.column.columnDef.meta?.typeHint;

if (typeHint.type === "html") {
// Do not filter on html types
return null;
}
// Do not filter on unknown types
if (!typeHint) return null;

// Do not filter on html types
if (typeHint.type === "html") return null;

if (typeHint.type === "numeric") {
const [from, to] = (header.column.getFilterValue() as
| [number | undefined, number | undefined]
| FilterValueNumeric
| [undefined, undefined]
| undefined) ?? [undefined, undefined];

const range = () => {
Expand All @@ -83,6 +95,7 @@ export const Filter: FC<FilterProps> = ({ header, className, ...props }) => {
return (
<input
{...props}
value={header.column.getFilterValue() as string}
className={`form-control form-control-sm ${className}`}
type="text"
onChange={(e) => header.column.setFilterValue(e.target.value)}
Expand Down
127 changes: 109 additions & 18 deletions js/data-frame/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
ColumnDef,
RowData,
RowModel,
SortingState,
TableOptions,
flexRender,
getCoreRowModel,
Expand All @@ -29,14 +28,14 @@ import { useImmer } from "use-immer";
import { TableBodyCell } from "./cell";
import { getCellEditMapObj, useCellEditMap } from "./cell-edit-map";
import { findFirstItemInView, getStyle } from "./dom-utils";
import { Filter, useFilters } from "./filter";
import { ColumnFiltersState, Filter, FilterValue, useFilters } from "./filter";
import type { CellSelection, SelectionModesProp } from "./selection";
import {
SelectionModes,
initRowSelectionModes,
useSelection,
} from "./selection";
import { useSort } from "./sort";
import { SortingState, useSort } from "./sort";
import { SortArrow } from "./sort-arrows";
import css from "./styles.scss";
import { useTabindexGroup } from "./tabindex-group";
Expand Down Expand Up @@ -175,10 +174,14 @@ const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = ({
const dataOriginal = useMemo(() => rowData, [rowData]);
const [dataState, setData] = useImmer(rowData);

const { sorting, sortState, sortingTableOptions } = useSort();
const { sorting, sortState, sortingTableOptions, setSorting } = useSort();

const { columnFilters, columnFiltersState, filtersTableOptions } =
useFilters<unknown[]>(withFilters);
const {
columnFilters,
columnFiltersState,
filtersTableOptions,
setColumnFilters,
} = useFilters<unknown[]>(withFilters);

const options: TableOptions<unknown[]> = {
data: dataState,
Expand Down Expand Up @@ -278,7 +281,7 @@ const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = ({
);

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

element.addEventListener(
"updateCellSelection",
handleMessage as EventListener
handleCellSelection as EventListener
);

return () => {
element.removeEventListener(
"updateCellSelection",
handleMessage as EventListener
handleCellSelection as EventListener
);
};
}, [id, rowSelection, rowData]);

useEffect(() => {
const handleColumnSort = (
event: CustomEvent<{ sort: { col: number; desc: boolean }[] }>
) => {
const shinySorting = event.detail.sort;
const columnSorting: SortingState = [];

shinySorting.map((sort) => {
columnSorting.push({
id: columns[sort.col],
desc: sort.desc,
});
});
setSorting(columnSorting);
};

if (!id) return;

const element = document.getElementById(id);
if (!element) return;

element.addEventListener(
"updateColumnSort",
handleColumnSort as EventListener
);

return () => {
element.removeEventListener(
"updateColumnSort",
handleColumnSort as EventListener
);
};
}, [columns, id, setSorting]);

useEffect(() => {
const handleColumnFilter = (
event: CustomEvent<{ filter: { col: number; value: FilterValue }[] }>
) => {
const shinyFilters = event.detail.filter;

const columnFilters: ColumnFiltersState = [];
shinyFilters.map((filter) => {
columnFilters.push({
id: columns[filter.col],
value: filter.value,
});
});
setColumnFilters(columnFilters);
};

if (!id) return;

const element = document.getElementById(id);
if (!element) return;

element.addEventListener(
"updateColumnFilter",
handleColumnFilter as EventListener
);

return () => {
element.removeEventListener(
"updateColumnFilter",
handleColumnFilter as EventListener
);
};
}, [columns, id, setColumnFilters]);

useEffect(() => {
if (!id) return;
let shinyValue: CellSelection | null = null;
Expand All @@ -338,22 +409,42 @@ const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = ({

useEffect(() => {
if (!id) return;
Shiny.setInputValue!(`${id}_column_sort`, sorting);
}, [id, sorting]);
const shinySort: { col: number; desc: boolean }[] = [];
sorting.map((sortObj) => {
const columnNum = columns.indexOf(sortObj.id);
shinySort.push({
col: columnNum,
desc: sortObj.desc,
});
});
Shiny.setInputValue!(`${id}_column_sort`, shinySort);
}, [columns, id, sorting]);
useEffect(() => {
if (!id) return;
Shiny.setInputValue!(`${id}_column_filter`, columnFilters);
}, [id, columnFilters]);
const shinyFilter: {
col: number;
value: FilterValue;
}[] = [];
columnFilters.map((filterObj) => {
const columnNum = columns.indexOf(filterObj.id);
shinyFilter.push({
col: columnNum,
value: filterObj.value as FilterValue,
});
});
Shiny.setInputValue!(`${id}_column_filter`, shinyFilter);
}, [id, columnFilters, columns]);
useEffect(() => {
if (!id) return;
// Already prefiltered rows!
const shinyValue: RowModel<unknown[]> = table.getSortedRowModel();

const rowIndices = table.getSortedRowModel().rows.map((row) => row.index);
Shiny.setInputValue!(`${id}_data_view_rows`, rowIndices);
const shinyRows: number[] = table
// Already prefiltered rows!
.getSortedRowModel()
.rows.map((row) => row.index);
Shiny.setInputValue!(`${id}_data_view_rows`, shinyRows);

// Legacy value as of 2024-05-13
Shiny.setInputValue!(`${id}_data_view_indices`, rowIndices);
Shiny.setInputValue!(`${id}_data_view_indices`, shinyRows);
}, [
id,
table,
Expand Down
4 changes: 4 additions & 0 deletions js/data-frame/sort.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import {
} from "@tanstack/react-table";
import React, { useState } from "react";

import type { ColumnSort } from "@tanstack/react-table";

export type { ColumnSort, SortingState };

export function useSort<TData>(): {
sorting: SortingState;
setSorting: React.Dispatch<React.SetStateAction<SortingState>>;
Expand Down
2 changes: 2 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ test =
pytest>=6.2.4
pytest-asyncio>=0.17.2
pytest-playwright>=0.3.0
playwright>=1.43.0
pytest-xdist
pytest-timeout
pytest-rerunfailures
Expand Down Expand Up @@ -93,6 +94,7 @@ test =
folium
palmerpenguins
faicons
ridgeplot
dev =
black>=24.0
flake8>=6.0.0
Expand Down
3 changes: 2 additions & 1 deletion shiny/_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@

import shiny

from . import _autoreload, _hostenv, _static, _utils
from . import __version__, _autoreload, _hostenv, _static, _utils
from ._docstring import no_example
from ._typing_extensions import NotRequired, TypedDict
from .express import is_express_app
from .express._utils import escape_to_var_name


@click.group("main")
@click.version_option(__version__)
def main() -> None:
pass

Expand Down
41 changes: 41 additions & 0 deletions shiny/api-examples/data_frame_update_filter/app-core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from shared import mtcars

from shiny import App, reactive, render, ui

app_ui = ui.page_fillable(
ui.card(
ui.layout_column_wrap(
ui.input_action_button("btn", "Filter on columns 0, 1, and 3"),
ui.input_action_button("reset", "Reset column filters"),
fill=False,
),
ui.output_data_frame("df"),
),
)


def server(input, output, session):
data = reactive.value(mtcars.iloc[:, range(4)])

@render.data_frame
def df():
return render.DataGrid(data(), filters=True)

@reactive.effect
@reactive.event(input.reset)
async def _():
await df.update_filter(None)

@reactive.effect
@reactive.event(input.btn)
async def _():
await df.update_filter(
[
{"col": 0, "value": [19, 25]},
{"col": 1, "value": [None, 6]},
{"col": 3, "value": [100, None]},
]
)


app = App(app_ui, server, debug=True)
Loading

0 comments on commit 484db5a

Please sign in to comment.