Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9999c5e
Separate `mode=` into `selection_mode: Literal["none", "row", "rows"]…
schloerke Mar 26, 2024
5d3cf0c
Update test apps
schloerke Mar 26, 2024
08ba5f3
Restore selected cells in DataTable dataframe
schloerke Mar 26, 2024
1b02b0a
Update hooks for JS code
schloerke Mar 26, 2024
bd28d55
Remove `type: "all"` and `type: "none"` options?
schloerke Mar 26, 2024
002c5ee
Merge branch 'main' into split_df_mode
schloerke Mar 26, 2024
7da88ec
lints
schloerke Mar 26, 2024
673ee75
Update _selection.py
schloerke Mar 26, 2024
e353fd2
Add init file so quartodoc can build
schloerke Mar 26, 2024
74b65d6
Import `TypedDict` from typing extensions
schloerke Mar 26, 2024
6b3a6af
Assert patches shape when received
schloerke Mar 26, 2024
702f1e9
Merge branch 'main' into split_df_mode
schloerke Mar 27, 2024
ac7f2da
Rearrange logic and update comment to say editable and selection_mode…
schloerke Mar 27, 2024
41677b0
Return a tuple, not a Sequence from helper method.
schloerke Mar 27, 2024
6eba7ed
Code suggestions
schloerke Mar 27, 2024
d37d6f9
`selection_mode=` -> `selection_modes=`
schloerke Mar 27, 2024
035b9f1
type lint
schloerke Mar 27, 2024
a8f34ab
Merge branch 'main' into split_df_mode
schloerke Mar 28, 2024
ba768ab
Doc update
schloerke Mar 29, 2024
dc646ba
`summary_modes` -> `summary_mode`; Use cannonical structure for summa…
schloerke Apr 1, 2024
9e1c634
Ignore more things for pyright
schloerke Apr 1, 2024
d89ad1c
Update _selection.py
schloerke Apr 1, 2024
698a805
build assets
schloerke Apr 1, 2024
48c1fa0
Add test for unique columns
schloerke Apr 1, 2024
a12ba02
Update notes and add tests
schloerke Apr 1, 2024
fc33c8a
lints
schloerke Apr 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,9 @@ docs: FORCE ## docs: build docs with quartodoc
docs-preview: FORCE ## docs: preview docs in browser
@echo "-------- Previewing docs in browser --------"
@cd docs && make serve
docs-quartodoc: FORCE
@echo "-------- Making quartodoc docs --------"
@cd docs && make quartodoc


install-npm: FORCE
Expand Down
33 changes: 11 additions & 22 deletions docs/_quartodoc-core.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,31 +118,21 @@ quartodoc:
- title: Update inputs
desc: Programmatically update input values.
contents:
- name: ui.update_select
dynamic: true
- name: ui.update_selectize
dynamic: true
- name: ui.update_slider
dynamic: true
- ui.update_select
- ui.update_selectize
- ui.update_slider
- ui.update_dark_mode
- ui.update_date
- name: ui.update_date_range
dynamic: true
- name: ui.update_checkbox
dynamic: true
- name: ui.update_checkbox_group
dynamic: true
- name: ui.update_switch
dynamic: true
- name: ui.update_radio_buttons
dynamic: true
- name: ui.update_numeric
dynamic: true
- ui.update_date_range
- ui.update_checkbox
- ui.update_checkbox_group
- ui.update_switch
- ui.update_radio_buttons
- ui.update_numeric
- ui.update_text
- name: ui.update_text_area
dynamic: "shiny.ui.update_text"
- name: ui.update_navs
dynamic: true
- ui.update_navs
- ui.update_action_button
- ui.update_action_link
- ui.update_task_button
Expand Down Expand Up @@ -242,8 +232,7 @@ quartodoc:
- session.Session.on_flushed
- session.Session.on_ended
- session.Session.dynamic_route
- name: input_handler.input_handlers
dynamic: true
- input_handler.input_handlers
- kind: page
path: Renderer
flatten: true
Expand Down
12 changes: 6 additions & 6 deletions examples/dataframe/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ def app_ui(req):
"Selection mode",
{
"none": "(None)",
"single_row": "Single",
"multiple_row": "Multiple",
"row": "Single",
"rows": "Multiple",
},
selected="multiple",
),
Expand Down Expand Up @@ -82,15 +82,15 @@ def grid():
width=width,
height=height,
filters=input.filters(),
mode=selection_mode(),
selection_mode=selection_mode(),
)
else:
return render.DataTable(
df(),
width=width,
height=height,
filters=input.filters(),
mode=selection_mode(),
selection_mode=selection_mode(),
)

@reactive.effect
Expand All @@ -103,10 +103,10 @@ def handle_edit():

@render.text
def detail():
selected_rows = grid.input_selected_rows() or ()
selected_rows = (grid.input_cell_selection() or {}).get("rows", ())
if len(selected_rows) > 0:
# "split", "records", "index", "columns", "values", "table"
return df().iloc[list(grid.input_selected_rows())]
return df().iloc[list(selected_rows)]


app = App(app_ui, server)
117 changes: 73 additions & 44 deletions js/dataframe/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@ import { TableBodyCell } from "./cell";
import { useCellEditMap } from "./cell-edit-map";
import { findFirstItemInView, getStyle } from "./dom-utils";
import { Filter, useFilter } from "./filter";
import type { BrowserCellSelection, SelectionModesProp } from "./selection";
import {
SelectionModeEnum,
initRowSelectionMode,
SelectionModes,
initRowSelectionModes,
useSelection,
} from "./selection";
import { SortArrow } from "./sort-arrows";
Expand All @@ -39,12 +40,14 @@ import { useTabindexGroup } from "./tabindex-group";
import { useSummary } from "./table-summary";
import { EditModeEnum, PandasData, PatchInfo, TypeHint } from "./types";

// TODO-barret set selected cell as input! (Might be a followup?)

// TODO-barret; Type support
// export interface PandasData<TIndex> {
// columns: ReadonlyArray<string>;
// // index: ReadonlyArray<TIndex>;
// data: unknown[][];
// type_hints?: ReadonlyArray<TypeHint>;
// typeHints?: ReadonlyArray<TypeHint>;
// options: DataGridOptions;
// }

Expand Down Expand Up @@ -84,21 +87,30 @@ declare module "@tanstack/table-core" {
// TODO: Drag to resize table/grid
// TODO: Row numbers

type ShinyDataGridServerInfo<TIndex> = {
payload: PandasData<TIndex>;
patchInfo: PatchInfo;
selectionModes: SelectionModesProp;
};

interface ShinyDataGridProps<TIndex> {
id: string | null;
data: PandasData<TIndex>;
patchInfo: PatchInfo;
gridInfo: ShinyDataGridServerInfo<TIndex>;
bgcolor?: string;
}

const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = ({
id,
data,
patchInfo,
gridInfo: { payload, patchInfo, selectionModes: selectionModesProp },
bgcolor,
}) => {
const { columns, type_hints: typeHints, data: rowData } = data;
const { width, height, fill, filters: withFilters } = data.options;
const {
columns,
typeHints,
data: rowData,
options: payload_options,
} = payload;
const { width, height, fill, filters: withFilters } = payload_options;

const containerRef = useRef<HTMLDivElement>(null);
const theadRef = useRef<HTMLTableSectionElement>(null);
Expand All @@ -114,7 +126,7 @@ const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = ({
// // console.log("rowModel", rowModel);
// }, [editColumnIndex, editRowIndex]);

const editCellsIsAllowed = data.options["mode"] === EditModeEnum.Edit;
const editCellsIsAllowed = payload_options["editable"] === true;

const [cellEditMap, setCellEditMap] = useCellEditMap();

Expand Down Expand Up @@ -194,7 +206,7 @@ const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = ({
// Reset scroll when dataset changes
useLayoutEffect(() => {
rowVirtualizer.scrollToOffset(0);
}, [data, rowVirtualizer]);
}, [payload, rowVirtualizer]);

const totalSize = rowVirtualizer.getTotalSize();
const virtualRows = rowVirtualizer.getVirtualItems();
Expand All @@ -212,30 +224,28 @@ const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = ({
: 0;

const summary = useSummary(
data.options["summary"],
payload_options["summary"],
containerRef?.current,
virtualRows,
theadRef.current,
rowVirtualizer.options.count
);

const tableStyle = data.options["style"] ?? "grid";
const tableStyle = payload_options["style"] ?? "grid";
const containerClass =
tableStyle === "grid" ? "shiny-data-grid-grid" : "shiny-data-grid-table";
const tableClass = tableStyle === "table" ? "table table-sm" : null;

// ### Row selection ###############################################################
// rowSelectionMode

const rowSelectionMode = initRowSelectionMode(data.options["mode"]);
const rowSelectionModes = initRowSelectionModes(selectionModesProp);

const canSelect = rowSelectionMode !== SelectionModeEnum.None;
const canMultiSelect =
rowSelectionMode === SelectionModeEnum.MultiNative ||
rowSelectionMode === SelectionModeEnum.Multiple;
const canSelect = !rowSelectionModes.is_none();
const canMultiRowSelect =
rowSelectionModes.row !== SelectionModes._rowEnum.NONE;

const rowSelection = useSelection<string, HTMLTableRowElement>(
rowSelectionMode,
rowSelectionModes,
(el) => el.dataset.key!,
(key, offset) => {
const rowModel = table.getSortedRowModel();
Expand All @@ -262,13 +272,25 @@ const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = ({
);

useEffect(() => {
const handleMessage = (event: CustomEvent<{ keys: number[] }>) => {
const handleMessage = (
event: CustomEvent<{ cellSelection: BrowserCellSelection }>
) => {
// We convert "None" to an empty tuple on the python side
// so an empty array indicates that selection should be cleared.
if (!event.detail.keys.length) {

const cellSelection = event.detail.cellSelection;

if (cellSelection.type === "none") {
rowSelection.clear();
return;
// } else if (cellSelection.type === "all") {
// rowSelection.setMultiple(rowData.map((_, i) => String(i)));
// return;
} else if (cellSelection.type === "row") {
rowSelection.setMultiple(cellSelection.rows.map(String));
return;
} else {
rowSelection.setMultiple(event.detail.keys.map(String));
console.error("Unhandled cell selection update:", cellSelection);
}
};

Expand All @@ -278,34 +300,42 @@ const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = ({
if (!element) return;

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

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

useEffect(() => {
if (!id) return;
if (rowSelectionMode === SelectionModeEnum.None) {
Shiny.setInputValue!(`${id}_selected_rows`, null);
const shinyId = `${id}_cell_selection`;
let shinyValue: BrowserCellSelection | null = null;
if (rowSelectionModes.is_none()) {
shinyValue = null;
} else if (rowSelectionModes.row !== SelectionModes._rowEnum.NONE) {
const rowSelectionKeys = rowSelection.keys().toList();
// Do not sent `none` or `all` to the server as it is hard to utilize when we know the selection is row based
// if (rowSelectionKeys.length === 0) {
// shinyValue = { type: "none" };
// } else if (rowSelectionKeys.length === rowData.length) {
// shinyValue = { type: "all" };
// } else {
shinyValue = {
type: "row",
rows: rowSelectionKeys.map((key) => parseInt(key)).sort(),
};
} else {
Shiny.setInputValue!(
`${id}_selected_rows`,
rowSelection
.keys()
.toList()
.map((key) => parseInt(key))
.sort()
);
console.error("Unhandled row selection mode:", rowSelectionModes);
}
Shiny.setInputValue!(shinyId, shinyValue);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id, rowSelectionMode, [...rowSelection.keys()]]);
}, [id, rowSelectionModes, [...rowSelection.keys()]]);

// ### End row selection ############################################################

Expand All @@ -331,6 +361,7 @@ const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = ({
//
const tbodyTabItems = React.useCallback(
() => tbodyRef.current!.querySelectorAll("[tabindex='-1']"),
// eslint-disable-next-line react-hooks/exhaustive-deps
[tbodyRef.current]
);
const tbodyTabGroup = useTabindexGroup(containerRef.current, tbodyTabItems, {
Expand All @@ -345,7 +376,7 @@ const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = ({
rowSelection.clear();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data]);
}, [payload]);

const headerRowCount = table.getHeaderGroups().length;

Expand Down Expand Up @@ -383,7 +414,7 @@ const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = ({
<table
className={tableClass + (withFilters ? " filtering" : "")}
aria-rowcount={dataState.length}
aria-multiselectable={canMultiSelect}
aria-multiselectable={canMultiRowSelect}
style={{
width: width === null || width === "auto" ? undefined : "100%",
}}
Expand Down Expand Up @@ -458,6 +489,7 @@ const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = ({
{...rowSelection.itemHandlers()}
>
{row.getVisibleCells().map((cell) => {
// TODO-barret; Only send in the cell data that is needed;
return (
<TableBodyCell
id={id}
Expand Down Expand Up @@ -651,9 +683,7 @@ export class ShinyDataFrameOutput extends HTMLElement {
}
}

renderValue(
value: null | { patchInfo: PatchInfo; data: PandasData<unknown> }
) {
renderValue(value: ShinyDataGridServerInfo<unknown> | null) {
this.clearError();

if (!value) {
Expand All @@ -665,8 +695,7 @@ export class ShinyDataFrameOutput extends HTMLElement {
<StrictMode>
<ShinyDataGrid
id={this.id}
data={value.data}
patchInfo={value.patchInfo}
gridInfo={value}
bgcolor={getComputedBgColor(this)}
></ShinyDataGrid>
</StrictMode>
Expand Down
Loading