Skip to content

Commit

Permalink
feat: Add ui.table press event listener support (#346)
Browse files Browse the repository at this point in the history
- Depends on my web PR:
deephaven/web-client-ui#1857
- Passes through the on_*_press events to IrisGrid
- Tested with the snippet from the examples, ensured all events were
being printed with the correct information:
```python
import deephaven.ui as ui
import deephaven.plot.express as dx

te = (
    ui.table(dx.data.stocks())
    .on_row_press(lambda row, data: print(f"Row Press: {row}, {data}"))
    .on_row_double_press(lambda row, data: print(f"Row Double Press: {row}, {data}"))
    .on_cell_press(
        lambda cell_index, data: print(f"Cell Press: {cell_index}, {data}")
    )
    .on_cell_double_press(
        lambda cell_index, data: print(f"Cell Double Press: {cell_index}, {data}")
    )
    .on_column_press(lambda column: print(f"Column Press: {column}"))
    .on_column_double_press(
        lambda column: print(f"Column Double Press: {column}")
    )
)
```
  • Loading branch information
mofojed authored Mar 8, 2024
1 parent 5bef1a4 commit b805683
Show file tree
Hide file tree
Showing 13 changed files with 967 additions and 494 deletions.
824 changes: 467 additions & 357 deletions package-lock.json

Large diffs are not rendered by default.

148 changes: 80 additions & 68 deletions plugins/ui/DESIGN.md

Large diffs are not rendered by default.

23 changes: 23 additions & 0 deletions plugins/ui/examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -769,6 +769,29 @@ def stock_table_input(source, default_sym="", default_exchange=""):
sti = stock_table_input(stocks, "CAT", "TPET")
```

### ui.table Events

The `ui.table` component has a few events that you can listen to. You can listen to different kinds of press events that include the data about the region pressed.

```py
import deephaven.ui as ui
import deephaven.plot.express as dx

te = ui.table(
dx.data.stocks(),
on_row_press=lambda row, data: print(f"Row Press: {row}, {data}"),
on_row_double_press=lambda row, data: print(f"Row Double Press: {row}, {data}"),
on_cell_press=lambda cell_index, data: print(f"Cell Press: {cell_index}, {data}"),
on_cell_double_press=lambda cell_index, data: print(
f"Cell Double Press: {cell_index}, {data}"
),
on_column_press=lambda column: print(f"Column Press: {column}"),
on_column_double_press=lambda column: print(f"Column Double Press: {column}"),
)
```

![Table events](table_events.png)

## Re-using components

In a previous example, we created a text_filter_table component. We can re-use that component, and display two tables with an input filter side-by-side:
Expand Down
Binary file added plugins/ui/examples/assets/table_events.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions plugins/ui/setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ install_requires =
deephaven-core>=0.31.0
deephaven-plugin>=0.6.0
json-rpc
typing_extensions
include_package_data = True

[options.packages.find]
Expand Down
43 changes: 40 additions & 3 deletions plugins/ui/src/deephaven/ui/components/table.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,46 @@
from __future__ import annotations

from deephaven.table import Table
from ..elements import UITable
from ..types import (
CellPressCallback,
ColumnPressCallback,
RowPressCallback,
)


def table(table: Table) -> UITable:
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,
) -> UITable:
"""
Add some extra methods to the Table class for giving hints to displaying a table
Customization to how a table is displayed, how it behaves, and listen to UI events.
Args:
table: The table to wrap
on_row_press: The callback function to run when a row is clicked.
The first parameter is the row index, and the second is the row data provided in a dictionary where the
column names are the keys.
on_row_double_press: The callback function to run when a row is double clicked.
The first parameter is the row index, and the second is the row data provided in a dictionary where the
column names are the keys.
on_cell_press: The callback function to run when a cell is clicked.
The first parameter is the cell index, and the second is the row data provided in a dictionary where the
column names are the keys.
on_cell_double_press: The callback function to run when a cell is double clicked.
The first parameter is the cell index, and the second is the row data provided in a dictionary where the
column names are the keys.
on_column_press: The callback function to run when a column is clicked.
The first parameter is the column name.
on_column_double_press: The callback function to run when a column is double clicked.
The first parameter is the column name.
"""
return UITable(table)
props = locals()
del props["table"]
return UITable(table, **props)
130 changes: 92 additions & 38 deletions plugins/ui/src/deephaven/ui/elements/UITable.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
from __future__ import annotations

import collections
import logging
import sys
from typing import Callable, Literal, Sequence, Any, cast
from warnings import warn

if sys.version_info < (3, 11):
from typing_extensions import TypedDict, NotRequired
else:
from typing import TypedDict, NotRequired

from deephaven.table import Table
from deephaven import SortDirection
from .Element import Element
Expand All @@ -14,6 +21,8 @@
Color,
ContextMenuAction,
CellIndex,
CellPressCallback,
ColumnPressCallback,
RowData,
ContextMenuMode,
DataBarAxis,
Expand Down Expand Up @@ -48,33 +57,83 @@ def remap_sort_direction(direction: TableSortDirection | str) -> Literal["ASC",
raise ValueError(f"Invalid table sort direction: {direction}")


class UITable(Element):
class UITableProps(TypedDict):
can_search: NotRequired[bool]
"""
Wrap a Table with some extra props for giving hints to displaying a table
Whether the search bar is accessible or not. Use the system default if no value set.
"""

on_row_press: NotRequired[RowPressCallback]
"""
Callback function to run when a row is clicked.
The first parameter is the row index, and the second is the row data provided in a dictionary where the
column names are the keys.
"""

on_row_double_press: NotRequired[RowPressCallback]
"""
The callback function to run when a row is double clicked.
The first parameter is the row index, and the second is the row data provided in a dictionary where the
column names are the keys.
"""

_table: Table
on_cell_press: NotRequired[CellPressCallback]
"""
The table that is wrapped with some extra props
The callback function to run when a cell is clicked.
The first parameter is the cell index, and the second is the row data provided in a dictionary where the
column names are the keys.
"""

_props: dict[str, Any]
on_cell_double_press: NotRequired[CellPressCallback]
"""
The extra props that are added by each method
The callback function to run when a cell is double clicked.
The first parameter is the cell index, and the second is the row data provided in a dictionary where the
column names are the keys.
"""

def __init__(self, table: Table, props: dict[str, Any] = {}):
on_column_press: NotRequired[ColumnPressCallback]
"""
The callback function to run when a column is clicked.
The first parameter is the column name.
"""

on_column_double_press: NotRequired[ColumnPressCallback]
"""
The callback function to run when a column is double clicked.
The first parameter is the column name.
"""

table: Table
"""
The table to wrap
"""


class UITable(Element):
"""
Wrap a Table with some extra props for giving hints to displaying a table
"""

_props: UITableProps
"""
The props that are passed to the frontend
"""

def __init__(
self,
table: Table,
**props: Any,
):
"""
Create a UITable from the passed in table. UITable provides an [immutable fluent interface](https://en.wikipedia.org/wiki/Fluent_interface#Immutability) for adding UI hints to a table.
Args:
table: The table to wrap
props: UITableProps props to pass to the frontend.
"""
self._table = table

# Store the extra props that are added by each method
# This is a shallow copy of the props so that we don't mutate the passed in props dict
self._props = {**props}
# Store all the props that were passed in
self._props = UITableProps(**props, table=table)

@property
def name(self):
Expand All @@ -92,7 +151,7 @@ def _with_prop(self, key: str, value: Any) -> "UITable":
A new UITable with the passed in prop added to the existing props
"""
logger.debug("_with_prop(%s, %s)", key, value)
return UITable(self._table, {**self._props, key: value})
return UITable(**{**self._props, key: value})

def _with_appendable_prop(self, key: str, value: Any) -> "UITable":
"""
Expand All @@ -114,9 +173,9 @@ def _with_appendable_prop(self, key: str, value: Any) -> "UITable":

value = value if isinstance(value, list) else [value]

return UITable(self._table, {**self._props, key: existing + value})
return UITable(**{**self._props, key: existing + value})

def _with_dict_prop(self, prop_name: str, value: dict) -> "UITable":
def _with_dict_prop(self, key: str, value: dict[str, Any]) -> "UITable":
"""
Create a new UITable with the passed in prop in a dictionary.
This will override any existing prop with the same key within
Expand All @@ -130,14 +189,13 @@ def _with_dict_prop(self, prop_name: str, value: dict) -> "UITable":
Returns:
A new UITable with the passed in prop added to the existing props
"""
logger.debug("_with_dict_prop(%s, %s)", prop_name, value)
existing = self._props.get(prop_name, {})
new = {**existing, **value}
return UITable(self._table, {**self._props, prop_name: new})
logger.debug("_with_dict_prop(%s, %s)", key, value)
existing = self._props.get(key, {})
return UITable(**{**self._props, key: {**existing, **value}}) # type: ignore

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

def aggregations(
self,
Expand Down Expand Up @@ -278,9 +336,13 @@ def color_row(

def context_menu(
self,
items: ContextMenuAction
| list[ContextMenuAction]
| Callable[[CellIndex, RowData], ContextMenuAction | list[ContextMenuAction]],
items: (
ContextMenuAction
| list[ContextMenuAction]
| Callable[
[CellIndex, RowData], ContextMenuAction | list[ContextMenuAction]
]
),
mode: ContextMenuMode = "CELL",
) -> "UITable":
"""
Expand Down Expand Up @@ -395,23 +457,10 @@ def hide_columns(self, columns: str | list[str]) -> "UITable":
"""
raise NotImplementedError()

def on_row_press(self, callback: RowPressCallback) -> "UITable":
"""
Add a callback for when a press on a row is released (e.g. a row is clicked).
Args:
callback: The callback function to run when a row is clicked.
The first parameter is the row index, and the second is the row data provided in a dictionary where the
column names are the keys.
Returns:
A new UITable
"""
raise NotImplementedError()

def on_row_double_press(self, callback: RowPressCallback) -> "UITable":
"""
Add a callback for when a row is double clicked.
*Deprecated: Use the on_row_double_press keyword arg instead.
Args:
callback: The callback function to run when a row is double clicked.
Expand All @@ -421,6 +470,11 @@ def on_row_double_press(self, callback: RowPressCallback) -> "UITable":
Returns:
A new UITable
"""
warn(
"on_row_double_press function is deprecated. Use the on_row_double_press keyword arg instead.",
DeprecationWarning,
stacklevel=2,
)
return self._with_prop("on_row_double_press", callback)

def quick_filter(
Expand Down Expand Up @@ -481,7 +535,7 @@ def sort(
remap_sort_direction(direction) for direction in direction_list_unmapped
]

by_list = by if isinstance(by, Sequence) else [by]
by_list = [by] if isinstance(by, str) else by

if direction and len(direction_list) != len(by_list):
raise ValueError("by and direction must be the same length")
Expand Down
Loading

0 comments on commit b805683

Please sign in to comment.