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

docs: use_render_queue, use_liveness_scope, use_table_listener docs #1044

Merged
merged 20 commits into from
Jan 8, 2025
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
165 changes: 0 additions & 165 deletions plugins/ui/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1448,104 +1448,6 @@ st = stock_table(stocks)

![Stock Rollup](_assets/stock_rollup.png)

## Listening to Table Updates

You can use the `use_table_listener` hook to listen to changes to a table. In this example, we use the `use_table_listener` hook to listen to changes to the table then display the last changes.

This is an advanced feature, requiring understanding of how the [table listeners](https://deephaven.io/core/docs/how-to-guides/table-listeners-python/) work, and limitations of running code while the Update Graph is running. Most usages of this are more appropriate to implement with [the table data hooks](#using-table-data-hooks).

```python
from deephaven import ui
from deephaven.table import Table
from deephaven import time_table, empty_table, merge
from deephaven import pandas as dhpd
import pandas as pd


def to_table(update):
return dhpd.to_table(pd.DataFrame.from_dict(update))


def add_as_op(ls, t, op):
t = t.update(f"type=`{op}`")
ls.append(t)


@ui.component
def monitor_changed_data(source: Table):

changed, set_changed = ui.use_state(empty_table(0))

show_added, set_show_added = ui.use_state(True)
show_removed, set_show_removed = ui.use_state(True)

def listener(update, is_replay):

to_merge = []

if (added_dict := update.added()) and show_added:
added = to_table(added_dict)
add_as_op(to_merge, added, "added")

if (removed_dict := update.removed()) and show_removed:
removed = to_table(removed_dict)
add_as_op(to_merge, removed, "removed")

if to_merge:
set_changed(merge(to_merge))
else:
set_changed(empty_table(0))

ui.use_table_listener(source, listener, [])

added_check = ui.checkbox(
"Show Added", isSelected=show_added, on_change=set_show_added
)

removed_check = ui.checkbox(
"Show Removed", isSelected=show_removed, on_change=set_show_removed
)

return [added_check, removed_check, changed]


t = time_table("PT1S").update(formulas=["X=i"]).tail(5)

monitor = monitor_changed_data(t)
```

## Handling liveness in functions

Some functions which interact with a component will create live objects that need to be managed by the component to ensure they are kept active.

The primary use case for this is when creating tables outside the component's own function, and passing them as state for the component's next update:

```python
from deephaven import ui, time_table


@ui.component
def resetable_table():
table, set_table = ui.use_state(lambda: time_table("PT1s"))
handle_press = ui.use_liveness_scope(lambda _: set_table(time_table("PT1s")), [])
return [
ui.action_button(
"Reset",
on_press=handle_press,
),
table,
]


f = resetable_table()
```

Without the `use_liveness_scope` wrapping the lamdba, the newly created live tables it creates go out of scope before the component can make use of it.

For more information on liveness scopes and why they are needed, see the [liveness scope documentation](https://deephaven.io/core/docs/conceptual/liveness-scope-concept/).

![Change Monitor](_assets/change_monitor.png)

## Tabs using ui.tab

You can add [Tabs](https://react-spectrum.adobe.com/react-spectrum/Tabs.html) within a panel by using the `ui.tabs` method. In this example, we create a panel with two tabs by passing in two instances of `ui.tab` as children.
Expand Down Expand Up @@ -1607,73 +1509,6 @@ def ui_tabs(source):
my_tabs = ui_tabs(stocks)
```

## Using Table Data Hooks

There are five different hooks that can be used to get data from a table:

1. `use_table_data`: Returns a dictionary of rows and columns from the table.
2. `use_row_data`: Returns a single row from the table as a dictionary
3. `use_row_list`: Returns a single row from the table as a list
4. `use_column_data`: Returns a single column from the table as a list
5. `use_cell_data`: Returns a single cell from the table

In this example, the hooks are used to display various pieces of information about LIZARD trades.

```python
from deephaven import ui
from deephaven.table import Table
from deephaven import time_table, agg
import deephaven.plot.express as dx

stocks = dx.data.stocks()


@ui.component
def watch_lizards(source: Table):

sold_lizards = source.where(["Side in `sell`", "Sym in `LIZARD`"])
exchange_count_table = sold_lizards.view(["Exchange"]).count_by(
"Count", by=["Exchange"]
)
last_sell_table = sold_lizards.tail(1)
max_size_and_price_table = sold_lizards.agg_by([agg.max_(cols=["Size", "Price"])])
last_ten_sizes_table = sold_lizards.view("Size").tail(10)
average_sell_table = (
sold_lizards.view(["Size", "Dollars"])
.tail(100)
.sum_by()
.view("Average = Dollars/Size")
)

exchange_count = ui.use_table_data(exchange_count_table)
last_sell = ui.use_row_data(last_sell_table)
max_size_and_price = ui.use_row_list(max_size_and_price_table)
last_ten_sizes = ui.use_column_data(last_ten_sizes_table)
average_sell = ui.use_cell_data(average_sell_table)

exchange_count_view = ui.view(f"Exchange counts {exchange_count}")
last_sell_view = ui.view(f"Last Sold LIZARD: {last_sell}")
max_size_and_price_view = ui.view(f"Max size and max price: {max_size_and_price}")
last_ten_sizes_view = ui.view(f"Last Ten Sizes: {last_ten_sizes}")
average_sell_view = ui.view(f"Average LIZARD price: {average_sell}")

return ui.flex(
exchange_count_view,
last_sell_view,
max_size_and_price_view,
last_ten_sizes_view,
average_sell_view,
margin=10,
gap=10,
direction="column",
)


watch = watch_lizards(stocks)
```

![Table Hooks](_assets/table_hooks.png)

## Multi-threading

State updates must be called from the render thread. All callbacks are automatically called from the render thread, but sometimes you will need to do some long-running operations asynchronously. You can use the `use_render_queue` hook to run a callback on the render thread. In this example, we create a form that takes a URL as input, and loads the CSV file from another thread before updating the state on the current thread.
Expand Down
Binary file removed plugins/ui/docs/_assets/change_monitor.png
Binary file not shown.
7 changes: 4 additions & 3 deletions plugins/ui/docs/components/toast.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ my_mount_example = ui_toast_on_mount()

## Toast from table example

This example shows how to create a toast from the latest update of a ticking table. It is recommended to auto dismiss these toasts with a `timeout` and to avoid ticking faster than the value of the `timeout`.
This example shows how to create a toast from the latest update of a ticking table. It is recommended to auto dismiss these toasts with a `timeout` and to avoid ticking faster than the value of the `timeout`. Note that the toast must be triggered on the render thread, whereas the table listener may be fired from another thread. Therefore you must use the render queue to trigger the toast.

```python
from deephaven import time_table
Expand All @@ -123,7 +123,7 @@ def toast_table(t):
data_added = update.added()["X"][0]
render_queue(lambda: ui.toast(f"added {data_added}", timeout=5000))

ui.use_table_listener(t, listener_function, [t])
ui.use_table_listener(t, listener_function, [])
return t


Expand All @@ -141,7 +141,8 @@ from deephaven import read_csv, ui

@ui.component
def csv_loader():
# The render_queue we fetch using the `use_render_queue` hook at the top of the component
# The render_queue we fetch using the `use_render_queue` hook at the top of the component.
# The toast must be triggered from the render queue.
render_queue = ui.use_render_queue()
table, set_table = ui.use_state()
error, set_error = ui.use_state()
Expand Down
7 changes: 5 additions & 2 deletions plugins/ui/docs/hooks/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,12 @@ _Performance_ hooks let you optimize components for performance. They allow you

_Data_ hooks let you use data from within a Deephaven table in your component.

- [`use_table_data`](use_table_data.md) lets you use the full table contents.
- [`use_column_data`](use_column_data.md) lets you use the data of one column.
- [`use_table_data`](use_table_data.md) lets you use the full table contents as a dictionary of rows and columns.
- [`use_column_data`](use_column_data.md) lets you use the data of one column as a list.
- [`use_row_data`](use_row_data.md) lets you use the data of one row as a dictionary.
- [`use_row_list`](use_row_list.md) lets you use the data of one row as a list.
- [`use_cell_data`](use_cell_data.md) lets you use the data of one cell.
- [`use_table_listener`](use_table_listener.md) lets you listen to a table for updates.

## Create custom hooks

Expand Down
73 changes: 71 additions & 2 deletions plugins/ui/docs/hooks/use_cell_data.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,78 @@ In the above example, `ui_table_first_cell` is a component that listens to the l
## Recommendations

1. **Use `use_cell_data` for listening to table updates**: If you need to listen to a table for one cell of data, use `use_cell_data`.
2. **Use table operations to filter to one cell**: Because `use_cell_data` always uses the top-left cell of the table, you can filter your table to determine what cell to listen to. If your table has multiple rows and columns, use table operations such as `.where` and `.select` to filter to the desired cell.
2. **Use table operations to filter to one cell**: Because `use_cell_data` always uses the top-left cell of the table, you can filter your table to determine what cell to listen to. If your table has multiple rows and columns, use table operations such as [`.where`](/core/docs/reference/table-operations/filter/where/), [`.select`](/core/docs/reference/table-operations/select/) and [`.reverse`](/core/docs/reference/table-operations/sort/reverse/) to filter to the desired cell.

## API Reference
## Empty tables

If the table is empty, the value of `cell_value` will return the value of `None`.

```python
from deephaven import time_table, ui
import datetime as dt


@ui.component
def ui_table_first_cell(table):
cell_value = ui.use_cell_data(table)
if cell_value is None:
return ui.heading("No data yet.")
return ui.heading(f"The first cell value is {cell_value}")


start_time = dt.datetime.now() + dt.timedelta(seconds=2)
table_first_cell = ui_table_first_cell(
time_table("PT1s", start_time=start_time).tail(1)
)
```

You can optionally provide a `sentinel` value to return when the table is empty instead.

```python
from deephaven import time_table, ui
import datetime as dt


@ui.component
def ui_table_first_cell(table):
cell_value = ui.use_cell_data(table, sentinel="No data yet.")
return ui.heading(f"Cell value: {cell_value}")


start_time = dt.datetime.now() + dt.timedelta(seconds=2)
table_first_cell = ui_table_first_cell(
time_table("PT1s", start_time=start_time).tail(1)
)
```

## Null values

If the table cell is a `null` value, the value of `cell_value` will be `pandas.NA`. You can check for `null` values using the `pandas.isna` function.

```python
from deephaven import time_table, ui
import datetime as dt
import pandas as pd


@ui.component
def ui_table_first_cell(table):
cell_value = ui.use_cell_data(table)
if cell_value is None:
return ui.heading("No data yet.")
if pd.isna(cell_value):
return ui.heading("Cell value is null.")
return ui.heading(f"Cell value: {cell_value}")


start_time = dt.datetime.now() + dt.timedelta(seconds=2)
table_first_cell = ui_table_first_cell(
time_table("PT1s", start_time=start_time)
.update("x=i%2==0?null:i")
.select("x")
.tail(1)
)
```

```{eval-rst}
.. dhautofunction:: deephaven.ui.use_cell_data
Expand Down
69 changes: 68 additions & 1 deletion plugins/ui/docs/hooks/use_column_data.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ In the above example, `ui_table_column` is a component that listens to the last
## Recommendations

1. **Use `use_column_data` for listening to table updates**: If you need to listen to a table for one column of data, use `use_column_data`.
2. **Use table operations to filter to one column**: If your table has multiple rows and columns, use table operations such as `.where` and `.select` to filter to the column you want to listen to. `use_column_data` always uses the first column of the table.
2. **Use table operations to filter to one column**: If your table has multiple rows and columns, use table operations such as [`.where`](/core/docs/reference/table-operations/filter/where/), [`.select`](/core/docs/reference/table-operations/select/) and [`.reverse`](/core/docs/reference/table-operations/sort/reverse/) to filter to the column you want to listen to. `use_column_data` always uses the first column of the table.
3. **Do not use `use_column_data` with [`list_view`](../components/list_view.md) or [`picker`](../components/picker.md)**: Some components are optimized to work with large tables of data, and will take a table passed in directly as their data source, only pulling in the options currently visible to the user. In those cases, pass the table directly to the component, otherwise you will fetch the entire column of data unnecessarily.
4. **Pass a Sentinel value to `use_column_data`**: If you want to use a default value when the table is empty, pass a sentinel value to `use_column_data`. The default sentinel value is `None`, which is returned when the table is empty.

## Tab switcher with `use_column_data`

Expand Down Expand Up @@ -54,6 +55,72 @@ _stocks = dx.data.stocks()
table_tabs = ui_table_tabs(_stocks, "Exchange")
```

## Empty tables

If the table is empty, the value of `column_data` will return the value of `None`.

```python
from deephaven import time_table, ui
import datetime as dt


@ui.component
def ui_table_column(table):
column_data = ui.use_column_data(table)
if column_data is None:
return ui.heading("No data yet.")
return ui.heading(f"Column data: {column_data}")


start_time = dt.datetime.now() + dt.timedelta(seconds=2)
table_column = ui_table_column(time_table("PT1s", start_time=start_time).tail(5))
```

You can optionally provide a `sentinel` value to return when the table is empty instead.

```python
from deephaven import time_table, ui
import datetime as dt


@ui.component
def ui_table_column(table):
column_data = ui.use_column_data(table, sentinel="No data yet.")
return ui.heading(f"Column data: {column_data}")


start_time = dt.datetime.now() + dt.timedelta(seconds=2)
table_column = ui_table_column(time_table("PT1s", start_time=start_time).tail(5))
```

## Null values

If the table has a `null` value in the first column, the value for that cell will be `pandas.NA`.

```python
from deephaven import time_table, ui
import datetime as dt


@ui.component
def ui_table_column(table):
column_data = ui.use_column_data(table)
if column_data is None:
return ui.heading("No data yet.")
if pd.isna(column_data[0]):
return ui.heading("Value of first cell is null.")
return ui.heading(f"Column data: {column_data}")


start_time = dt.datetime.now() + dt.timedelta(seconds=2)
table_column = ui_table_column(
time_table("PT1s", start_time=start_time)
.update("x=i%2==0?null:i")
.select("x")
.tail(4)
)
```

## API Reference

```{eval-rst}
Expand Down
Loading
Loading