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
53 changes: 52 additions & 1 deletion plugins/ui/docs/components/table.md
Original file line number Diff line number Diff line change
Expand Up @@ -800,9 +800,54 @@ t2 = ui.table( # Filters applied when table is opened on the client

![Example of quick filters](../_assets/table_quick_filter.png)

## Sort

You can set the initial sort state of a `ui.table` with the `sorts` prop. This controls table UI state (similar to `quick_filters` and `reverse`) without changing the underlying table definition.

The `sorts` prop accepts:

- A single column name string (ascending sort)
- A `ui.TableSort` object
- A list mixing column names and `ui.TableSort` objects

When you pass `sorts`, those values seed the initial client sort state. If the user changes the sort, that client state is persisted and restored on reload.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll let Don and Margaret review the docs, but this wording is weird to me and inconsistent with how we describe features (like with Quick filters).


```python order=data,t_mixed_sort_list,t_string_sort
from deephaven import ui, new_table
from deephaven.column import int_col, string_col

data = new_table(
[
string_col("Name", ["R01", "R02", "R03", "R04", "R05", "R06"]),
string_col("Category", ["B", "A", "B", "A", "C", "C"]),
int_col("SepalLength", [51, -149, 64, 58, -32, 58]),
]
)

t_mixed_sort_list = ui.table(
data,
sorts=[
"Category",
ui.TableSort(column="Name", direction="DESC"),
ui.TableSort(column="SepalLength", direction="ASC", is_abs=True),
],
)

t_string_sort = ui.table(
data,
sorts="Name",
)
```

`ui.TableSort` supports:

- `column`: The column name to sort by.
- `direction`: `"ASC"` or `"DESC"`.
- `is_abs`: If `True`, sort by absolute value.

## Reverse

The table can be displayed in reverse order using the `reverse` prop. Using the reverse prop visually indicates to the user that the table is reversed via a colored bar under the column headers. Users can disable the reverse with the column header context menu or via a shortcut. The reverse is applied on the server via request from the client.
The table can be displayed in reverse order using the `reverse` prop. Using the reverse prop visually indicates to the user that the table is reversed via a colored bar under the column headers. Users can disable the reverse with the column header context menu or via a shortcut. The reverse is applied on the server via request from the client. If both `sorts` and `reverse` are set, reverse is applied after sorting.

```python
from deephaven import ui
Expand All @@ -825,6 +870,12 @@ t = ui.table(dx.data.stocks(), reverse=True)
.. dhautofunction:: deephaven.ui.TableAgg
```

### TableSort

```{eval-rst}
.. dhautofunction:: deephaven.ui.TableSort
```

### TableFormat

```{eval-rst}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"file":"components/table.md","objects":{"data":{"type":"Table","data":{"columns":[{"name":"Name","type":"java.lang.String"},{"name":"Category","type":"java.lang.String"},{"name":"SepalLength","type":"int"}],"rows":[[{"value":"R01"},{"value":"B"},{"value":"51"}],[{"value":"R02"},{"value":"A"},{"value":"-149"}],[{"value":"R03"},{"value":"B"},{"value":"64"}],[{"value":"R04"},{"value":"A"},{"value":"58"}],[{"value":"R05"},{"value":"C"},{"value":"-32"}],[{"value":"R06"},{"value":"C"},{"value":"58"}]]}},"t_mixed_sort_list":{"type":"deephaven.ui.Element","data":{"document":{"__dhElemName":"deephaven.ui.elements.UITable","props":{"table":{"__dhObid":0},"sorts":[{"column":"Category","direction":"ASC","isAbs":false},{"column":"Name","direction":"DESC","isAbs":false},{"column":"SepalLength","direction":"ASC","isAbs":true}],"showQuickFilters":false,"showGroupingColumn":true,"showSearch":false,"reverse":false}},"state":"{}"}},"t_string_sort":{"type":"deephaven.ui.Element","data":{"document":{"__dhElemName":"deephaven.ui.elements.UITable","props":{"table":{"__dhObid":0},"sorts":[{"column":"Name","direction":"ASC","isAbs":false}],"showQuickFilters":false,"showGroupingColumn":true,"showSearch":false,"reverse":false}},"state":"{}"}}}}
10 changes: 9 additions & 1 deletion plugins/ui/src/deephaven/ui/components/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,14 @@
from .tab_list import tab_list
from .tab_panels import tab_panels
from .tab import tab
from .table import table, TableAgg, TableDatabar, TableFormat, TableHeatmap
from .table import (
table,
TableAgg,
TableDatabar,
TableFormat,
TableHeatmap,
TableSort,
)
from .tabs import tabs
from .tag_group import tag_group
from .text import text
Expand Down Expand Up @@ -166,6 +173,7 @@
"TableDatabar",
"TableFormat",
"TableHeatmap",
"TableSort",
"tab_list",
"tab_panels",
"tabs",
Expand Down
65 changes: 64 additions & 1 deletion plugins/ui/src/deephaven/ui/components/table.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Literal, Any, Optional
from typing import Literal, Any, Union
import logging
from deephaven.table import RollupTable, TreeTable
from ..elements import Element, resolve
Expand Down Expand Up @@ -39,6 +39,8 @@
"Var",
]

SortDirection = Literal["ASC", "DESC"]


@dataclass
class TableAgg:
Expand All @@ -58,6 +60,27 @@ class TableAgg:
ignore_cols: ColumnName | list[ColumnName] | None = None


@dataclass
class TableSort:
"""
A sort configuration for a table.

Args:
column: The column to sort.
direction: The sort direction. One of "ASC" or "DESC".
is_abs: Whether to sort by absolute value.
Returns:
The TableSort configuration.
"""

column: ColumnName
direction: SortDirection = "ASC"
is_abs: bool = False


TableSortLike = Union[ColumnName, TableSort]


@dataclass
class TableFormat:
"""
Expand Down Expand Up @@ -206,6 +229,38 @@ def _validate_table_format(
raise ValueError("TableHeatmap gradient must have at least 2 colors.")


def _normalize_table_sorts(
sorts: TableSortLike | list[TableSortLike],
) -> list[dict[str, Any]]:
"""Normalize table sorts into the dehydrated sort shape used by iris-grid."""

sort_list = sorts if isinstance(sorts, list) else [sorts]
normalized: list[dict[str, Any]] = []
for sort in sort_list:
if isinstance(sort, str):
sort = TableSort(column=sort)
elif not isinstance(sort, TableSort):
raise ValueError(
"Table sorts must be a column name, TableSort, or list of column "
f"names and TableSort instances. Received {type(sort).__name__}."
)

direction = sort.direction.upper()
if direction not in ("ASC", "DESC"):
raise ValueError(
f"Invalid sort direction: {sort.direction}. Expected 'ASC' or 'DESC'."
)
normalized.append(
{
"column": sort.column,
"direction": direction,
"isAbs": sort.is_abs,
}
)

return normalized


class table(Element):
"""
Customization to how a table is displayed, how it behaves, and listen to UI events.
Expand All @@ -232,6 +287,10 @@ class table(Element):
always_fetch_columns: The columns to always fetch from the server regardless of if they are in the viewport.
If True, all columns will always be fetched. This may make tables with many columns slow.
quick_filters: The quick filters to apply to the table. Dictionary of column name to filter value.
sorts: The sorts to apply to the table.
These are UI-controlled sorts (similar to reverse) rather than engine-transformed table data.
User changes to the sort state are persisted and restored on reload.
Accepts a column name, TableSort, or list containing column names and TableSort instances.
show_quick_filters: Whether to show the quick filter bar by default.
aggregations: An aggregation or list of aggregations to apply to the table. These will be shown as a floating row at the bottom of the table by default.
aggregations_position: The position to show the aggregations. One of "top" or "bottom". "bottom" by default.
Expand Down Expand Up @@ -317,6 +376,7 @@ def __init__(
on_selection_change: SelectionChangeCallback | None = None,
always_fetch_columns: ColumnName | list[ColumnName] | bool | None = None,
quick_filters: dict[ColumnName, QuickFilterExpression] | None = None,
sorts: TableSortLike | list[TableSortLike] | None = None,
show_quick_filters: bool = False,
aggregations: TableAgg | list[TableAgg] | None = None,
aggregations_position: Literal["top", "bottom"] | None = None,
Expand Down Expand Up @@ -384,6 +444,9 @@ def __init__(
if format_ is not None:
_validate_table_format(format_, table)

if sorts is not None:
props["sorts"] = _normalize_table_sorts(sorts)

props["table"] = resolve(table) if isinstance(table, str) else table
del props["self"]
self._props = props
Expand Down
80 changes: 49 additions & 31 deletions plugins/ui/src/js/src/elements/UITable/UITable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -374,23 +374,43 @@ export function UITable({
[memoizedStateFn, model, setDehydratedState]
);

const initialHydratedState = useMemo(() => {
if (model && utils && initialState.current != null) {
return {
...utils.hydrateIrisGridState(model, initialState.current),
...IrisGridUtils.hydrateGridState(model, initialState.current),
};
}
}, [model, utils]);

const hydratedSorts = useMemo(() => {
if (utils && sorts !== undefined && columns !== undefined) {
log.debug('Hydrating sorts', sorts);

return utils.hydrateSort(columns, sorts);
// Initial sorts are captured once at mount so later re-renders never push
// a new `sorts` reference into IrisGrid (which would call updateSorts and
// clobber the user's interactive sort changes).
const initialSortsRef = useRef(sorts);

// Lock the initial hydrated state to a stable value the first time model+utils
// are available. Recomputing it would change the `sorts` (and other) prop
// identities and cause IrisGrid to overwrite user changes on every re-render.
const lockedInitialHydratedStateRef = useRef<
Partial<IrisGridProps> | undefined
>(undefined);
const initialHydratedStateComputedRef = useRef(false);
if (
!initialHydratedStateComputedRef.current &&
model != null &&
utils != null
) {
initialHydratedStateComputedRef.current = true;
const persisted =
initialState.current != null
? {
...utils.hydrateIrisGridState(model, initialState.current),
...IrisGridUtils.hydrateGridState(model, initialState.current),
}
: undefined;
const initialSorts = initialSortsRef.current;
const seededSorts =
persisted == null && initialSorts !== undefined && columns !== undefined
? utils.hydrateSort(columns, initialSorts)
: undefined;
if (persisted != null) {
lockedInitialHydratedStateRef.current = persisted;
} else if (seededSorts !== undefined) {
lockedInitialHydratedStateRef.current = { sorts: seededSorts };
}
return undefined;
}, [columns, utils, sorts]);
}
const initialHydratedState = lockedInitialHydratedStateRef.current;

const hydratedQuickFilters = useMemo(() => {
if (
Expand Down Expand Up @@ -536,7 +556,6 @@ export function UITable({
mouseHandlers,
alwaysFetchColumns,
showSearchBar,
sorts: hydratedSorts,
quickFilters: hydratedQuickFilters,
isFilterBarShown: showQuickFilters,
reverse,
Expand Down Expand Up @@ -586,7 +605,6 @@ export function UITable({
alwaysFetchColumns,
showSearchBar,
showQuickFilters,
hydratedSorts,
hydratedQuickFilters,
reverse,
density,
Expand Down Expand Up @@ -629,19 +647,19 @@ export function UITable({
* Otherwise, we have received changes from the server and we should use those over client state.
* In the future we may want to do a smarter merge of these.
*/
const mergedIrisGridProps = useMemo(() => {
if (initialIrisGridServerProps.current === irisGridServerProps) {
return {
...irisGridServerProps,
...initialHydratedState,
};
}

return {
...initialHydratedState,
...irisGridServerProps,
};
}, [irisGridServerProps, initialHydratedState]);
const mergedIrisGridProps = useMemo(
() =>
initialIrisGridServerProps.current === irisGridServerProps
? {
...irisGridServerProps,
...(initialHydratedState ?? {}),
}
: {
...(initialHydratedState ?? {}),
...irisGridServerProps,
},
[irisGridServerProps, initialHydratedState]
);

const inputFilters = useDashboardColumnFilters(
model?.columns ?? null,
Expand Down
Loading
Loading