Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 6 additions & 1 deletion examples/ui/table.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import marimo

__generated_with = "0.15.5"
__generated_with = "0.16.0"
app = marimo.App()


Expand All @@ -22,6 +22,11 @@ def _(mo):
],
# Show full name on hover for each row using column placeholders
hover_template="{{first_name}} {{last_name}}",
# Add header tooltip for column headers (shown via info icon + title)
header_tooltip={
"first_name": "Employee's first name",
"last_name": "Employee's last name",
},
)
table
return (table,)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,42 @@ describe("DataTable", () => {
expect(rows[1]).toHaveAttribute("title", "Michael Scott");
expect(rows[2]).toHaveAttribute("title", "Jim Halpert");
});

it("applies headerHoverText mapping to column header titles", () => {
interface RowData {
id: number;
first: string;
last: string;
}

const testData: RowData[] = [
{ id: 1, first: "Michael", last: "Scott" },
{ id: 2, first: "Jim", last: "Halpert" },
];

const columns: Array<ColumnDef<RowData>> = [
{ accessorKey: "first", header: "First" },
{ accessorKey: "last", header: "Last" },
];

render(
<TooltipProvider>
<DataTable
data={testData}
columns={columns}
selection={null}
totalRows={2}
totalColumns={2}
pagination={false}
headerTooltip={{ first: "Given name", last: "Family name" }}
/>
</TooltipProvider>,
);

// Header row is role="row" as well; get all headers by role="columnheader"
const headers = screen.getAllByRole("columnheader");
// Order should correspond to columns array
expect(headers[0]).toHaveAttribute("title", "Given name");
expect(headers[1]).toHaveAttribute("title", "Family name");
});
});
23 changes: 20 additions & 3 deletions frontend/src/components/data-table/column-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import type { Column } from "@tanstack/react-table";
import { capitalize } from "lodash-es";
import { FilterIcon, MinusIcon, TextIcon, XIcon } from "lucide-react";
import { FilterIcon, InfoIcon, MinusIcon, TextIcon, XIcon } from "lucide-react";
import { useMemo, useRef, useState } from "react";
import { useLocale } from "react-aria";
import {
Expand Down Expand Up @@ -67,12 +67,14 @@ interface DataTableColumnHeaderProps<TData, TValue>
extends React.HTMLAttributes<HTMLDivElement> {
column: Column<TData, TValue>;
header: React.ReactNode;
headerTitle?: string;
calculateTopKRows?: CalculateTopKRows;
}

export const DataTableColumnHeader = <TData, TValue>({
column,
header,
headerTitle,
className,
calculateTopKRows,
}: DataTableColumnHeaderProps<TData, TValue>) => {
Expand All @@ -86,7 +88,16 @@ export const DataTableColumnHeader = <TData, TValue>({

// No sorting or filtering
if (!column.getCanSort() && !column.getCanFilter()) {
return <div className={cn(className)}>{header}</div>;
return (
<div className={cn(className)} title={headerTitle}>
<span className="flex items-center gap-1">
{header}
{headerTitle && (
<InfoIcon className="h-3 w-3 text-muted-foreground" />
)}
</span>
</div>
);
}

const hasFilter = column.getFilterValue() !== undefined;
Expand All @@ -102,8 +113,14 @@ export const DataTableColumnHeader = <TData, TValue>({
className,
)}
data-testid="data-table-sort-button"
title={headerTitle}
>
<span className="flex-1">{header}</span>
<span className="flex-1 flex items-center gap-1">
{header}
{headerTitle && (
<InfoIcon className="h-3 w-3 text-muted-foreground" />
)}
</span>
<span
className={cn(
"h-5 py-1 px-1",
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/components/data-table/columns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export function generateColumns<T>({
chartSpecModel,
textJustifyColumns,
wrappedColumns,
headerTooltip,
showDataTypes,
calculateTopKRows,
}: {
Expand All @@ -112,6 +113,7 @@ export function generateColumns<T>({
chartSpecModel?: ColumnChartSpecModel<unknown>;
textJustifyColumns?: Record<string, "left" | "center" | "right">;
wrappedColumns?: string[];
headerTooltip?: Record<string, string>;
showDataTypes?: boolean;
calculateTopKRows?: CalculateTopKRows;
}): Array<ColumnDef<T>> {
Expand Down Expand Up @@ -165,6 +167,7 @@ export function generateColumns<T>({
header: ({ column }) => {
const stats = chartSpecModel?.getColumnStats(key);
const dtype = column.columnDef.meta?.dtype;
const headerTitle = headerTooltip?.[key];
const dtypeHeader =
showDataTypes && dtype ? (
<div className="flex flex-row gap-1">
Expand All @@ -188,6 +191,7 @@ export function generateColumns<T>({
<DataTableColumnHeader
header={headerWithType}
column={column}
headerTitle={headerTitle}
calculateTopKRows={calculateTopKRows}
/>
);
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/components/data-table/data-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ interface DataTableProps<TData> extends Partial<DownloadActionProps> {
cellSelection?: CellSelectionState;
cellStyling?: CellStyleState | null;
hoverTemplate?: string | null;
headerTooltip?: Record<string, string> | undefined;
onRowSelectionChange?: OnChangeFn<RowSelectionState>;
onCellSelectionChange?: OnChangeFn<CellSelectionState>;
getRowIds?: GetRowIds;
Expand Down Expand Up @@ -107,6 +108,7 @@ const DataTableInternal = <TData,>({
cellSelection,
cellStyling,
hoverTemplate,
headerTooltip,
paginationState,
setPaginationState,
downloadAs,
Expand Down Expand Up @@ -267,7 +269,7 @@ const DataTableInternal = <TData,>({
{showLoadingBar && (
<div className="absolute top-0 left-0 h-[3px] w-1/2 bg-primary animate-slide" />
)}
{renderTableHeader(table)}
{renderTableHeader(table, headerTooltip)}
<CellSelectionProvider>
<DataTableBody
table={table}
Expand Down
34 changes: 26 additions & 8 deletions frontend/src/components/data-table/renderers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"use no memo";

import {
type AccessorKeyColumnDefBase,
type Cell,
type Column,
type ColumnDef,
Expand All @@ -26,6 +27,7 @@ import { useScrollIntoViewOnFocus } from "./range-focus/use-scroll-into-view";

export function renderTableHeader<TData>(
table: Table<TData>,
headerTooltip?: Record<string, string>,
): JSX.Element | null {
if (!table.getRowModel().rows?.length) {
return null;
Expand All @@ -35,6 +37,12 @@ export function renderTableHeader<TData>(
return headerGroups.map((headerGroup) =>
headerGroup.headers.map((header) => {
const { className, style } = getPinningStyles(header.column);
const accessorKey: string = (
header.column.columnDef as AccessorKeyColumnDefBase<TData>
)?.accessorKey as string;
const lookupKey: string = accessorKey ?? header.column.id;
const headerHoverTitle: string | undefined =
headerTooltip?.[lookupKey] || undefined;
return (
<TableHead
key={header.id}
Expand All @@ -43,13 +51,19 @@ export function renderTableHeader<TData>(
className,
)}
style={style}
title={headerHoverTitle}
ref={(thead) => {
columnSizingHandler(thead, table, header.column);
}}
>
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
{header.isPlaceholder ? null : (
<div title={headerHoverTitle} className="contents">
{flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</div>
)}
</TableHead>
);
}),
Expand Down Expand Up @@ -106,9 +120,9 @@ export const DataTableBody = <TData,>({
const s = renderUnknownValue({ value: v, nullAsEmptyString: true });
idToValue.set(c.column.id, s);
}
return template.replace(variableRegex, (_substr, varName: string) => {
return template.replaceAll(variableRegex, (_substr, varName: string) => {
const val = idToValue.get(varName);
return val !== undefined ? val : `{{${varName}}}`;
return val === undefined ? `{{${varName}}}` : val;
});
}

Expand Down Expand Up @@ -165,11 +179,15 @@ export const DataTableBody = <TData,>({
const isRowViewedInPanel =
rowViewerPanelOpen && viewedRowIdx === rowIndex;

// Compute hover title once per row using this row's cells (visible or hidden)
// Compute hover title once per row using all visible cells
const hoverTemplate = table.getState().cellHoverTemplate || null;
const rowCells = row.getAllCells();
const visibleCells = row.getVisibleCells?.() ?? [
...row.getLeftVisibleCells(),
...row.getCenterVisibleCells(),
...row.getRightVisibleCells(),
];
const rowTitle = hoverTemplate
? applyHoverTemplate(hoverTemplate, rowCells)
? applyHoverTemplate(hoverTemplate, visibleCells)
: undefined;

return (
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/plugins/impl/DataTablePlugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ interface Data<T> {
freezeColumnsRight?: string[];
textJustifyColumns?: Record<string, "left" | "center" | "right">;
wrappedColumns?: string[];
headerTooltip?: Record<string, string>;
totalColumns: number;
maxColumns: number | "all";
hasStableRowId: boolean;
Expand Down Expand Up @@ -249,6 +250,7 @@ export const DataTablePlugin = createPlugin<S>("marimo-table")
.record(z.enum(["left", "center", "right"]))
.optional(),
wrappedColumns: z.array(z.string()).optional(),
headerTooltip: z.record(z.string()).optional(),
fieldTypes: columnToFieldTypesSchema.nullish(),
totalColumns: z.number(),
maxColumns: z.union([z.number(), z.literal("all")]).default("all"),
Expand Down Expand Up @@ -346,6 +348,7 @@ export const DataTablePlugin = createPlugin<S>("marimo-table")
data={props.data.data}
value={props.value}
setValue={props.setValue}
headerInfo={props.data.headerTooltip}
/>
</LazyDataTableComponent>
</TableProviders>
Expand Down Expand Up @@ -387,6 +390,7 @@ interface DataTableProps<T> extends Data<T>, DataTableFunctions {
enableFilters?: boolean;
cellStyles?: CellStyleState | null;
hoverTemplate?: string | null;
headerInfo?: Record<string, string> | undefined;
toggleDisplayHeader?: () => void;
host: HTMLElement;
cellId?: CellId | null;
Expand Down Expand Up @@ -706,6 +710,7 @@ const DataTableComponent = ({
freezeColumnsRight,
textJustifyColumns,
wrappedColumns,
headerTooltip,
totalColumns,
get_row_ids,
cellStyles,
Expand Down Expand Up @@ -779,6 +784,7 @@ const DataTableComponent = ({
fieldTypes: memoizedClampedFieldTypes,
textJustifyColumns: memoizedTextJustifyColumns,
wrappedColumns: memoizedWrappedColumns,
headerTooltip: headerTooltip,
// Only show data types if they are explicitly set
showDataTypes: showDataTypes,
calculateTopKRows: calculate_top_k_rows,
Expand All @@ -791,6 +797,7 @@ const DataTableComponent = ({
memoizedClampedFieldTypes,
memoizedTextJustifyColumns,
memoizedWrappedColumns,
headerTooltip,
calculate_top_k_rows,
],
);
Expand Down Expand Up @@ -908,6 +915,7 @@ const DataTableComponent = ({
cellSelection={cellSelection}
cellStyling={cellStyles}
hoverTemplate={hoverTemplate}
headerTooltip={headerTooltip}
downloadAs={showDownload ? downloadAs : undefined}
enableSearch={enableSearch}
searchQuery={searchQuery}
Expand Down
20 changes: 20 additions & 0 deletions marimo/_plugins/ui/_impl/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@ def style_cell(_rowId, _columnName, value):
text_justify_columns (Dict[str, Literal["left", "center", "right"]], optional):
Dictionary of column names to text justification options: left, center, right.
wrapped_columns (List[str], optional): List of column names to wrap.
header_tooltip (Dict[str, str], optional): Mapping from column names to tooltip text on the column header.
label (str, optional): Markdown label for the element. Defaults to "".
on_change (Callable[[Union[List[JSONType], Dict[str, List[JSONType]], IntoDataFrame, List[TableCell]]], None], optional):
Optional callback to run when this element's value changes.
Expand Down Expand Up @@ -422,6 +423,7 @@ def __init__(
dict[str, Literal["left", "center", "right"]]
] = None,
wrapped_columns: Optional[list[str]] = None,
header_tooltip: Optional[dict[str, str]] = None,
show_download: bool = True,
max_columns: MaxColumnsType = MAX_COLUMNS_NOT_PROVIDED,
*,
Expand Down Expand Up @@ -634,6 +636,7 @@ def __init__(
_validate_column_formatting(
text_justify_columns, wrapped_columns, column_names_set
)
_validate_header_tooltip(header_tooltip, column_names_set)

field_types = self._manager.get_field_types()

Expand Down Expand Up @@ -666,6 +669,7 @@ def __init__(
"freeze-columns-right": freeze_columns_right,
"text-justify-columns": text_justify_columns,
"wrapped-columns": wrapped_columns,
"header-tooltip": header_tooltip,
"has-stable-row-id": self._has_stable_row_id,
"cell-styles": search_result_styles,
"hover-template": hover_template,
Expand Down Expand Up @@ -1413,3 +1417,19 @@ def _validate_column_formatting(
raise ValueError(
f"Column '{next(iter(invalid))}' not found in table."
)


def _validate_header_tooltip(
header_tooltip: Optional[dict[str, str]],
column_names_set: set[str],
) -> None:
"""Validate header tooltip mapping.

Ensures all specified columns exist in the table.
"""
if header_tooltip:
invalid = set(header_tooltip.keys()) - column_names_set
if invalid:
raise ValueError(
f"Column '{next(iter(invalid))}' not found in table."
)
Loading
Loading