Skip to content

Commit 81dcf18

Browse files
committed
hover cell, proof of concept
1 parent c792f03 commit 81dcf18

File tree

6 files changed

+147
-1
lines changed

6 files changed

+147
-1
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/* Copyright 2024 Marimo. All rights reserved. */
2+
"use no memo";
3+
4+
import type {
5+
Cell,
6+
Column,
7+
InitialTableState,
8+
Row,
9+
RowData,
10+
Table,
11+
TableFeature,
12+
} from "@tanstack/react-table";
13+
import { INDEX_COLUMN_NAME } from "../types";
14+
import type { CellHoverTextState, CellHoverTextTableState } from "./types";
15+
16+
function getRowId<TData>(row: Row<TData>): string {
17+
if (row && typeof row === "object" && INDEX_COLUMN_NAME in row) {
18+
return String(row[INDEX_COLUMN_NAME]);
19+
}
20+
return row.id;
21+
}
22+
23+
export const CellHoverTextFeature: TableFeature = {
24+
getInitialState: (state?: InitialTableState): CellHoverTextTableState => {
25+
return {
26+
...state,
27+
cellHoverText: {} as CellHoverTextState,
28+
};
29+
},
30+
31+
createCell: <TData extends RowData>(
32+
cell: Cell<TData, unknown>,
33+
column: Column<TData>,
34+
row: Row<TData>,
35+
table: Table<TData>,
36+
) => {
37+
cell.getUserHoverText = () => {
38+
const state = table.getState().cellHoverText;
39+
const rowId = getRowId(row);
40+
return state?.[rowId]?.[column.id] || undefined;
41+
};
42+
},
43+
};
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/* Copyright 2024 Marimo. All rights reserved. */
2+
/* eslint-disable @typescript-eslint/no-empty-interface */
3+
import type { RowData } from "@tanstack/react-table";
4+
5+
export type CellHoverTextState = Record<
6+
string,
7+
Record<string, string | null | undefined>
8+
>;
9+
10+
export interface CellHoverTextTableState {
11+
cellHoverText: CellHoverTextState | null;
12+
}
13+
14+
export interface CellHoverTextCell {
15+
/**
16+
* Returns a hover text for the cell.
17+
*/
18+
getUserHoverText?: () => string | undefined;
19+
}
20+
21+
// Use declaration merging to add our new feature APIs
22+
declare module "@tanstack/react-table" {
23+
interface TableState extends CellHoverTextTableState {}
24+
25+
interface Cell<TData extends RowData, TValue> extends CellHoverTextCell {}
26+
}

frontend/src/components/data-table/data-table.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import { Table } from "@/components/ui/table";
2424
import type { GetRowIds } from "@/plugins/impl/DataTablePlugin";
2525
import { cn } from "@/utils/cn";
2626
import type { PanelType } from "../editor/chrome/panels/context-aware-panel/context-aware-panel";
27+
import { CellHoverTextFeature } from "./cell-hover-text/feature";
28+
import type { CellHoverTextState } from "./cell-hover-text/types";
2729
import { CellSelectionFeature } from "./cell-selection/feature";
2830
import type { CellSelectionState } from "./cell-selection/types";
2931
import { CellStylingFeature } from "./cell-styling/feature";
@@ -63,6 +65,7 @@ interface DataTableProps<TData> extends Partial<DownloadActionProps> {
6365
rowSelection?: RowSelectionState;
6466
cellSelection?: CellSelectionState;
6567
cellStyling?: CellStyleState | null;
68+
cellHoverText?: CellHoverTextState | null;
6669
onRowSelectionChange?: OnChangeFn<RowSelectionState>;
6770
onCellSelectionChange?: OnChangeFn<CellSelectionState>;
6871
getRowIds?: GetRowIds;
@@ -103,6 +106,7 @@ const DataTableInternal = <TData,>({
103106
rowSelection,
104107
cellSelection,
105108
cellStyling,
109+
cellHoverText,
106110
paginationState,
107111
setPaginationState,
108112
downloadAs,
@@ -176,6 +180,7 @@ const DataTableInternal = <TData,>({
176180
ColumnFormattingFeature,
177181
CellSelectionFeature,
178182
CellStylingFeature,
183+
CellHoverTextFeature,
179184
CopyColumnFeature,
180185
FocusRowFeature,
181186
],
@@ -236,6 +241,7 @@ const DataTableInternal = <TData,>({
236241
rowSelection,
237242
cellSelection,
238243
cellStyling,
244+
cellHoverText,
239245
columnPinning: columnPinning,
240246
},
241247
});

frontend/src/components/data-table/renderers.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ export const DataTableBody = <TData,>({
101101
cell.getUserStyling?.() || {},
102102
pinningstyle,
103103
);
104+
const hoverText = cell.getUserHoverText?.();
104105
return (
105106
<TableCell
106107
tabIndex={0}
@@ -117,6 +118,7 @@ export const DataTableBody = <TData,>({
117118
onMouseDown={(e) => handleCellMouseDown(e, cell)}
118119
onMouseUp={handleCellMouseUp}
119120
onMouseOver={(e) => handleCellMouseOver(e, cell)}
121+
title={hoverText}
120122
>
121123
<CellRangeSelectionIndicator cellId={cell.id} />
122124
<div className="relative">

frontend/src/plugins/impl/DataTablePlugin.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import React, {
2222
} from "react";
2323
import useEvent from "react-use-event-hook";
2424
import { z } from "zod";
25+
import type { CellHoverTextState } from "@/components/data-table/cell-hover-text/types";
2526
import type { CellSelectionState } from "@/components/data-table/cell-selection/types";
2627
import type { CellStyleState } from "@/components/data-table/cell-styling/types";
2728
import { TablePanel } from "@/components/data-table/charts/charts";
@@ -208,6 +209,7 @@ type DataTableFunctions = {
208209
data: TableData<T>;
209210
total_rows: number | TooManyRows;
210211
cell_styles?: CellStyleState | null;
212+
cell_hover_texts?: CellHoverTextState | null;
211213
}>;
212214
get_data_url?: GetDataUrl;
213215
get_row_ids?: GetRowIds;
@@ -254,6 +256,7 @@ export const DataTablePlugin = createPlugin<S>("marimo-table")
254256
maxColumns: z.union([z.number(), z.literal("all")]).default("all"),
255257
hasStableRowId: z.boolean().default(false),
256258
cellStyles: z.record(z.record(z.object({}).passthrough())).optional(),
259+
cellHoverTexts: z.record(z.record(z.string().nullish())).optional(),
257260
// Whether to load the data lazily.
258261
lazy: z.boolean().default(false),
259262
// If lazy, this will preload the first page of data
@@ -298,6 +301,9 @@ export const DataTablePlugin = createPlugin<S>("marimo-table")
298301
cell_styles: z
299302
.record(z.record(z.object({}).passthrough()))
300303
.nullable(),
304+
cell_hover_texts: z
305+
.record(z.record(z.string().nullable().optional()))
306+
.nullable(),
301307
}),
302308
),
303309
get_row_ids: rpc.input(z.object({}).passthrough()).output(
@@ -385,6 +391,7 @@ interface DataTableProps<T> extends Data<T>, DataTableFunctions {
385391
// Filters
386392
enableFilters?: boolean;
387393
cellStyles?: CellStyleState | null;
394+
cellHoverTexts?: CellHoverTextState | null;
388395
toggleDisplayHeader?: () => void;
389396
host: HTMLElement;
390397
cellId?: CellId | null;
@@ -453,16 +460,23 @@ export const LoadingDataTableComponent = memo(
453460
rows: T[];
454461
totalRows: number | TooManyRows;
455462
cellStyles: CellStyleState | undefined | null;
463+
cellHoverTexts: CellHoverTextState | undefined | null;
456464
}>(async () => {
457465
// If there is no data, return an empty array
458466
if (props.totalRows === 0) {
459-
return { rows: Arrays.EMPTY, totalRows: 0, cellStyles: {} };
467+
return {
468+
rows: Arrays.EMPTY,
469+
totalRows: 0,
470+
cellStyles: {},
471+
cellHoverTexts: {},
472+
};
460473
}
461474

462475
// Table data is a url string or an array of objects
463476
let tableData = props.data;
464477
let totalRows = props.totalRows;
465478
let cellStyles = props.cellStyles;
479+
let cellHoverTexts = props.cellHoverTexts;
466480

467481
const pageSizeChanged = paginationState.pageSize !== props.pageSize;
468482

@@ -513,12 +527,14 @@ export const LoadingDataTableComponent = memo(
513527
tableData = searchResults.data;
514528
totalRows = searchResults.total_rows;
515529
cellStyles = searchResults.cell_styles || {};
530+
cellHoverTexts = searchResults.cell_hover_texts || {};
516531
}
517532
tableData = await loadTableData(tableData);
518533
return {
519534
rows: tableData,
520535
totalRows: totalRows,
521536
cellStyles,
537+
cellHoverTexts,
522538
};
523539
}, [
524540
sorting,
@@ -644,6 +660,7 @@ export const LoadingDataTableComponent = memo(
644660
paginationState={paginationState}
645661
setPaginationState={setPaginationState}
646662
cellStyles={data?.cellStyles ?? props.cellStyles}
663+
cellHoverTexts={data?.cellHoverTexts ?? props.cellHoverTexts}
647664
toggleDisplayHeader={toggleDisplayHeader}
648665
getRow={getRow}
649666
cellId={cellId}
@@ -707,6 +724,7 @@ const DataTableComponent = ({
707724
totalColumns,
708725
get_row_ids,
709726
cellStyles,
727+
cellHoverTexts,
710728
toggleDisplayHeader,
711729
calculate_top_k_rows,
712730
preview_column,
@@ -904,6 +922,7 @@ const DataTableComponent = ({
904922
rowSelection={rowSelection}
905923
cellSelection={cellSelection}
906924
cellStyling={cellStyles}
925+
cellHoverText={cellHoverTexts}
907926
downloadAs={showDownload ? downloadAs : undefined}
908927
enableSearch={enableSearch}
909928
searchQuery={searchQuery}

marimo/_plugins/ui/_impl/table.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,13 +126,15 @@ class SearchTableArgs:
126126

127127

128128
CellStyles = dict[RowId, dict[ColumnName, dict[str, Any]]]
129+
CellHoverTexts = dict[RowId, dict[ColumnName, str | None]]
129130

130131

131132
@dataclass(frozen=True)
132133
class SearchTableResponse:
133134
data: str
134135
total_rows: Union[int, Literal["too_many"]]
135136
cell_styles: Optional[CellStyles] = None
137+
cell_hover_texts: Optional[CellHoverTexts] = None
136138

137139

138140
@dataclass(frozen=True)
@@ -439,6 +441,7 @@ def __init__(
439441
]
440442
] = None,
441443
style_cell: Optional[Callable[[str, str, Any], dict[str, Any]]] = None,
444+
hover_cell: Optional[Callable[[str, str, Any], str]] = None,
442445
# The _internal_* arguments are for overriding and unit tests
443446
# table should take the value unconditionally
444447
_internal_column_charts_row_limit: Optional[int] = None,
@@ -603,8 +606,10 @@ def __init__(
603606
pagination = False
604607

605608
self._style_cell = style_cell
609+
self._hover_cell = hover_cell
606610

607611
search_result_styles: Optional[CellStyles] = None
612+
search_result_hover_texts: Optional[CellHoverTexts] = None
608613
search_result_data: JSONType = []
609614
field_types: Optional[FieldTypes] = None
610615
num_columns = 0
@@ -621,6 +626,7 @@ def __init__(
621626
)
622627
)
623628
search_result_styles = search_result.cell_styles
629+
search_result_hover_texts = search_result.cell_hover_texts
624630
search_result_data = search_result.data
625631

626632
# Validate column configurations
@@ -666,6 +672,7 @@ def __init__(
666672
"wrapped-columns": wrapped_columns,
667673
"has-stable-row-id": self._has_stable_row_id,
668674
"cell-styles": search_result_styles,
675+
"cell-hover-texts": search_result_hover_texts,
669676
"lazy": _internal_lazy,
670677
"preload": _internal_preload,
671678
},
@@ -1171,6 +1178,42 @@ def do_style_cell(row: str, col: str) -> dict[str, Any]:
11711178
for row in row_ids
11721179
}
11731180

1181+
def _hover_cells(
1182+
self, skip: int, take: int, total_rows: Union[int, Literal["too_many"]]
1183+
) -> Optional[CellHoverTexts]:
1184+
"""Calculate the hover text of the cells in the table."""
1185+
if self._hover_cell is None:
1186+
return None
1187+
1188+
def get_hover_text_of_cell(row: str, col: str) -> str | None:
1189+
selected_cells = self._searched_manager.select_cells(
1190+
[TableCoordinate(row_id=row, column_name=col)]
1191+
)
1192+
if not selected_cells or self._hover_cell is None:
1193+
return None
1194+
return self._hover_cell(row, col, selected_cells[0].value)
1195+
1196+
columns = self._searched_manager.get_column_names()
1197+
response = self._get_row_ids(EmptyArgs())
1198+
1199+
# Clamp the take to the total number of rows
1200+
if total_rows != "too_many" and skip + take > total_rows:
1201+
take = total_rows - skip
1202+
1203+
# Determine row range
1204+
row_ids: Union[list[int], range]
1205+
if response.all_rows or response.error:
1206+
row_ids = range(skip, skip + take)
1207+
else:
1208+
row_ids = response.row_ids[skip : skip + take]
1209+
1210+
return {
1211+
str(row): {
1212+
col: get_hover_text_of_cell(str(row), col) for col in columns
1213+
}
1214+
for row in row_ids
1215+
}
1216+
11741217
def _search(self, args: SearchTableArgs) -> SearchTableResponse:
11751218
"""Search and filter the table data.
11761219
@@ -1193,6 +1236,7 @@ def _search(self, args: SearchTableArgs) -> SearchTableResponse:
11931236
- data: Filtered and formatted table data for the requested page
11941237
- total_rows: Total number of rows after applying filters
11951238
- cell_styles: User defined styling information for each cell in the page
1239+
- cell_hover_texts: User defined hover information for each cell in the page
11961240
"""
11971241
offset = args.page_number * args.page_size
11981242
max_columns = args.max_columns
@@ -1231,6 +1275,9 @@ def clamp_rows_and_columns(manager: TableManager[Any]) -> str:
12311275
cell_styles=self._style_cells(
12321276
offset, args.page_size, total_rows
12331277
),
1278+
cell_hover_texts=self._hover_cells(
1279+
offset, args.page_size, total_rows
1280+
),
12341281
)
12351282

12361283
filter_function = (
@@ -1261,6 +1308,9 @@ def clamp_rows_and_columns(manager: TableManager[Any]) -> str:
12611308
data=clamp_rows_and_columns(result),
12621309
total_rows=total_rows,
12631310
cell_styles=self._style_cells(offset, args.page_size, total_rows),
1311+
cell_hover_texts=self._hover_cells(
1312+
offset, args.page_size, total_rows
1313+
),
12641314
)
12651315

12661316
def _get_row_ids(self, args: EmptyArgs) -> GetRowIdsResponse:

0 commit comments

Comments
 (0)