Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: UI table formatting #950

Merged
merged 23 commits into from
Nov 5, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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
14 changes: 14 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

57 changes: 57 additions & 0 deletions plugins/ui/docs/components/table.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,55 @@ t = ui.table(_t)
2. Use a UI table to show properties like filters as if the user had created them in the UI. Users can change the default values provided by the UI table, such as filters.
3. UI tables handle ticking tables automatically, so you can pass any Deephaven table to a UI table.

## Formatting

You can format the table using the `formatting` prop. This prop takes a list of `ui.TableFormat` objects. `ui.TableFormat` is a dataclass that encapsulates the formatting options for a table. The full list of formatting options can be found in the [API Reference](#tableformat).

### Formatting Rows and Columns
mattrunyon marked this conversation as resolved.
Show resolved Hide resolved

Every formatting rule may optionally specify `cols` and `where` properties. The `cols` property is a column name or list of column names to apply the formatting rule to. If `cols` is omitted, then the rule will be applied to the entire row. The `where` property is a Deephaven formula to apply the formatting rule to. The `where` property _must_ evaluate to a boolean. If `where` is omitted, then the rule will be applied to every row. These may be combined to apply formatting to specific columns only when a condition is met.

> [!NOTE]
> The `where` property is a Deephaven formula evaluated in the engine. You can think of it like adding a new boolean column using [`update_view`](https://deephaven.io/core/docs/reference/table-operations/select/update-view/)

Copy link
Contributor

Choose a reason for hiding this comment

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

Can we add a sections for each formatter? Helps both SEO and shows a functional example of how to accomplish each task

#### Formatting table background colors

#### Formatting table decimals

#### Formatting table dates
...

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think it would make more sense to do Formatting Colors and Formatting Values unless you want each of those sections to be 1 or 2 sentences. That feels like overkill to me to show someone what "changing the background color to red" does or "changing text alignment to left".

I think formatting colors would explain what kind of color values you can use and what values can take colors.

Formatting values would link out to appropriate docs about value format strings (I think they're Java formatting strings, but need to verify)

Copy link
Contributor

Choose a reason for hiding this comment

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

Formatting colors and Formatting values sound reasonable in terms of avoiding super short sections; however, if a user is likely to search "Formatting table dates" and similar, it might be worth it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I ended up adding Formatting Colors as an h3 to describe you can (and should) use theme colors. Then text/background color as h4.

For values I added an h3 for numeric and datetime

The following example shows how to format the `Sym` and `Exchange` columns with a red background and white text when the `Sym` is `DOG`.

```python
from deephaven import ui
import deephaven.plot.express as dx

t = ui.table(
dx.data.stocks(),
formatting=[
ui.TableFormat(
cols=["Sym", "Exchange"],
where="Sym = `DOG`",
background_color="red",
color="white",
mattrunyon marked this conversation as resolved.
Show resolved Hide resolved
)
],
)
```

### Formatting Rule Priority
mattrunyon marked this conversation as resolved.
Show resolved Hide resolved

The last matching formatting rule for each property will be applied. This means the lowest priority rules should be first in the list with higher priority rules at the end.

In the following example, the `Sym` column will have a red background with white text, and the rest of the table will have a blue background with white text.

```python
from deephaven import ui
import deephaven.plot.express as dx

t = ui.table(
dx.data.stocks(),
formatting=[
ui.TableFormat(background_color="blue", color="white"),
ui.TableFormat(cols="Sym", background_color="red"),
],
)
```

## Events

You can listen for different user events on a `ui.table`. There is both a `press` and `double_press` event for `row`, `cell`, and `column`. These events typically correspond to a click or double click on the table. The event payloads include table data related to the event. For `row` and `column` events, the corresponding data within the viewport will be sent to the event handler. The viewport is typically the visible area ± a window equal to the visible area (e.g., if rows 5-10 are visible, rows 0-15 will be in the viewport).
Expand Down Expand Up @@ -265,6 +314,14 @@ t = ui.table(

## API Reference

### Table

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

### TableFormat

```{eval-rst}
.. dhautofunction:: deephaven.ui.TableFormat
```
4 changes: 3 additions & 1 deletion plugins/ui/src/deephaven/ui/components/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
from .tab_list import tab_list
from .tab_panels import tab_panels
from .tab import tab
from .table import table
from .table import table, TableDatabar, TableFormat
from .tabs import tabs
from .text import text
from .text_area import text_area
Expand Down Expand Up @@ -108,6 +108,8 @@
"stack",
"switch",
"table",
"TableDatabar",
"TableFormat",
"tab_list",
"tab_panels",
"tabs",
Expand Down
238 changes: 166 additions & 72 deletions plugins/ui/src/deephaven/ui/components/table.py
Original file line number Diff line number Diff line change
@@ -1,91 +1,99 @@
from __future__ import annotations
from typing import Literal

from dataclasses import dataclass
from typing import Literal, Any, Optional
import logging
from deephaven.table import Table
from ..elements import UITable
from ..elements import Element
from .types import AlignSelf, DimensionValue, JustifySelf, LayoutFlex, Position
from ..types import (
CellPressCallback,
ColumnGroup,
ColumnName,
ColumnPressCallback,
DatabarConfig,
QuickFilterExpression,
RowPressCallback,
ResolvableContextMenuItem,
)
from .._internal import dict_to_camel_case, RenderContext

logger = logging.getLogger(__name__)


@dataclass()
class TableFormat:
"""
A formatting rule for a table.

Args:
cols: The columns to format. If None, the format will apply to the entire row.
where: Query string to filter which rows should be formatted.
color: The font color.
background_color: The cell background color.
alignment: The cell text alignment.
value: Format string for the cell value.
E.g. "0.00%" to format as a percentage with two decimal places.
mode: The cell rendering mode.
Currently only databar is supported as an alternate rendering mode.
Returns:
The TableFormat.
"""

cols: ColumnName | list[ColumnName] | None = None
where: str | None = None
color: str | None = None
background_color: str | None = None
alignment: Literal["left", "center", "right"] | None = None
value: str | None = None
mode: TableDatabar | None = None

def table(
table: Table,
*,
on_row_press: RowPressCallback | None = None,
on_row_double_press: RowPressCallback | None = None,
on_cell_press: CellPressCallback | None = None,
on_cell_double_press: CellPressCallback | None = None,
on_column_press: ColumnPressCallback | None = None,
on_column_double_press: ColumnPressCallback | None = None,
always_fetch_columns: ColumnName | list[ColumnName] | bool | None = None,
quick_filters: dict[ColumnName, QuickFilterExpression] | None = None,
show_quick_filters: bool = False,
show_grouping_column: bool = True,
show_search: bool = False,
reverse: bool = False,
front_columns: list[ColumnName] | None = None,
back_columns: list[ColumnName] | None = None,
frozen_columns: list[ColumnName] | None = None,
hidden_columns: list[ColumnName] | None = None,
column_groups: list[ColumnGroup] | None = None,
density: Literal["compact", "regular", "spacious"] | None = None,
context_menu: (
ResolvableContextMenuItem | list[ResolvableContextMenuItem] | None
) = None,
context_header_menu: (
ResolvableContextMenuItem | list[ResolvableContextMenuItem] | None
) = None,
databars: list[DatabarConfig] | None = None,
key: str | None = None,
flex: LayoutFlex | None = None,
flex_grow: float | None = None,
flex_shrink: float | None = None,
flex_basis: DimensionValue | None = None,
align_self: AlignSelf | None = None,
justify_self: JustifySelf | None = None,
order: int | None = None,
grid_area: str | None = None,
grid_row: str | None = None,
grid_row_start: str | None = None,
grid_row_end: str | None = None,
grid_column: str | None = None,
grid_column_start: str | None = None,
grid_column_end: str | None = None,
margin: DimensionValue | None = None,
margin_top: DimensionValue | None = None,
margin_bottom: DimensionValue | None = None,
margin_start: DimensionValue | None = None,
margin_end: DimensionValue | None = None,
margin_x: DimensionValue | None = None,
margin_y: DimensionValue | None = None,
width: DimensionValue | None = None,
height: DimensionValue | None = None,
min_width: DimensionValue | None = None,
min_height: DimensionValue | None = None,
max_width: DimensionValue | None = None,
max_height: DimensionValue | None = None,
position: Position | None = None,
top: DimensionValue | None = None,
bottom: DimensionValue | None = None,
start: DimensionValue | None = None,
end: DimensionValue | None = None,
left: DimensionValue | None = None,
right: DimensionValue | None = None,
z_index: int | None = None,
) -> UITable:

@dataclass()
class TableDatabar:
"""
A databar configuration for a table.

Args:
column: Name of the column to display as a databar.
value_column: Name of the column to use as the value for the databar.
If not provided, the databar will use the column value.

This can be useful if you want to display a databar with
a log scale, but display the actual value in the cell.
In this case, the value_column would be the log of the actual value.
min: Minimum value for the databar. Defaults to the minimum value in the column.

If a column name is provided, the minimum value will be the value in that column.
If a constant is providded, the minimum value will be that constant.
max: Maximum value for the databar. Defaults to the maximum value in the column.

If a column name is provided, the maximum value will be the value in that column.
If a constant is providded, the maximum value will be that constant.
axis: Whether the databar 0 value should be proportional to the min and max values,
in the middle of the cell, or on one side of the databar based on direction.
direction: The direction of the databar.
value_placement: Placement of the value relative to the databar.
color: The color of the databar.
opacity: The opacity of the databar.
"""

column: ColumnName
value_column: ColumnName | None = None
min: ColumnName | float | None = None
max: ColumnName | float | None = None
axis: Literal["proportional", "middle", "directional"] | None = None
direction: Literal["LTR", "RTL"] | None = None
value_placement: Literal["beside", "overlap", "hide"] | None = None
color: str | None = None
opacity: float | None = None


class table(Element):
"""
Customization to how a table is displayed, how it behaves, and listen to UI events.

Args:
table: The table to wrap
formatting: A list of formatting rules for the table.
on_row_press: The callback function to run when a row is clicked.
The callback is invoked with the visible row data provided in a dictionary where the
column names are the keys.
Expand Down Expand Up @@ -165,6 +173,92 @@ def table(
Returns:
The rendered Table.
"""
props = locals()
del props["table"]
return UITable(table, **props)

_props: dict[str, Any]
"""
The props that are passed to the frontend
"""

def __init__(
self,
table: Table,
*,
formatting: list[TableFormat] | None = None,
mattrunyon marked this conversation as resolved.
Show resolved Hide resolved
on_row_press: RowPressCallback | None = None,
on_row_double_press: RowPressCallback | None = None,
on_cell_press: CellPressCallback | None = None,
on_cell_double_press: CellPressCallback | None = None,
on_column_press: ColumnPressCallback | None = None,
on_column_double_press: ColumnPressCallback | None = None,
always_fetch_columns: ColumnName | list[ColumnName] | bool | None = None,
quick_filters: dict[ColumnName, QuickFilterExpression] | None = None,
show_quick_filters: bool = False,
show_grouping_column: bool = True,
show_search: bool = False,
reverse: bool = False,
front_columns: list[ColumnName] | None = None,
back_columns: list[ColumnName] | None = None,
frozen_columns: list[ColumnName] | None = None,
hidden_columns: list[ColumnName] | None = None,
column_groups: list[ColumnGroup] | None = None,
density: Literal["compact", "regular", "spacious"] | None = None,
context_menu: (
ResolvableContextMenuItem | list[ResolvableContextMenuItem] | None
) = None,
context_header_menu: (
ResolvableContextMenuItem | list[ResolvableContextMenuItem] | None
) = None,
databars: list[TableDatabar] | None = None,
key: str | None = None,
flex: LayoutFlex | None = None,
flex_grow: float | None = None,
flex_shrink: float | None = None,
flex_basis: DimensionValue | None = None,
align_self: AlignSelf | None = None,
justify_self: JustifySelf | None = None,
order: int | None = None,
grid_area: str | None = None,
grid_row: str | None = None,
grid_row_start: str | None = None,
grid_row_end: str | None = None,
grid_column: str | None = None,
grid_column_start: str | None = None,
grid_column_end: str | None = None,
margin: DimensionValue | None = None,
margin_top: DimensionValue | None = None,
margin_bottom: DimensionValue | None = None,
margin_start: DimensionValue | None = None,
margin_end: DimensionValue | None = None,
margin_x: DimensionValue | None = None,
margin_y: DimensionValue | None = None,
width: DimensionValue | None = None,
height: DimensionValue | None = None,
min_width: DimensionValue | None = None,
min_height: DimensionValue | None = None,
max_width: DimensionValue | None = None,
max_height: DimensionValue | None = None,
position: Position | None = None,
top: DimensionValue | None = None,
bottom: DimensionValue | None = None,
start: DimensionValue | None = None,
end: DimensionValue | None = None,
left: DimensionValue | None = None,
right: DimensionValue | None = None,
z_index: int | None = None,
) -> None:
props = locals()
del props["self"]
self._props = props
self._key = props.get("key")

@property
def name(self):
return "deephaven.ui.elements.UITable"

@property
def key(self) -> str | None:
return self._key

def render(self, context: RenderContext) -> dict[str, Any]:
logger.debug("Returning props %s", self._props)
return dict_to_camel_case(self._props)
Loading
Loading