diff --git a/plugins/ui/docs/README.md b/plugins/ui/docs/README.md index 7863b74c5..11adb6407 100644 --- a/plugins/ui/docs/README.md +++ b/plugins/ui/docs/README.md @@ -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. @@ -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. diff --git a/plugins/ui/docs/_assets/change_monitor.png b/plugins/ui/docs/_assets/change_monitor.png deleted file mode 100644 index c8391e318..000000000 Binary files a/plugins/ui/docs/_assets/change_monitor.png and /dev/null differ diff --git a/plugins/ui/docs/components/toast.md b/plugins/ui/docs/components/toast.md index 37b68fd8c..588fee120 100644 --- a/plugins/ui/docs/components/toast.md +++ b/plugins/ui/docs/components/toast.md @@ -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 @@ -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 @@ -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() diff --git a/plugins/ui/docs/hooks/overview.md b/plugins/ui/docs/hooks/overview.md index 39d302f18..8a817686d 100644 --- a/plugins/ui/docs/hooks/overview.md +++ b/plugins/ui/docs/hooks/overview.md @@ -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 diff --git a/plugins/ui/docs/hooks/use_cell_data.md b/plugins/ui/docs/hooks/use_cell_data.md index 12195d688..727e0122a 100644 --- a/plugins/ui/docs/hooks/use_cell_data.md +++ b/plugins/ui/docs/hooks/use_cell_data.md @@ -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 diff --git a/plugins/ui/docs/hooks/use_column_data.md b/plugins/ui/docs/hooks/use_column_data.md index b1f43d817..f42528292 100644 --- a/plugins/ui/docs/hooks/use_column_data.md +++ b/plugins/ui/docs/hooks/use_column_data.md @@ -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` @@ -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} diff --git a/plugins/ui/docs/hooks/use_liveness_scope.md b/plugins/ui/docs/hooks/use_liveness_scope.md new file mode 100644 index 000000000..b806dd9af --- /dev/null +++ b/plugins/ui/docs/hooks/use_liveness_scope.md @@ -0,0 +1,64 @@ +# use_liveness_scope + +`use_liveness_scope` allows you to interact with the [liveness scope](/core/docs/conceptual/liveness-scope-concept/) to manage live objects within a component. Some functions that 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 is to create tables outside the component's own function, and pass them as state for the component's next update. If the table is not kept alive by the component, it will be garbage collected and the component will not be able to update with the new data. + +## Example + +This example shows how to use `use_liveness_scope` to manage a live table object. The table is created outside the component's own function and set in the [state](use_state.md) of the component. The `handle_press` function is used to update the table with new data. + +```python +from deephaven import ui, time_table + + +@ui.component +def ui_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, + ] + + +resetable_table = ui_resetable_table() +``` + +## UI recommendations + +1. **Avoid using `use_liveness_scope` unless necessary**: This is an advanced feature that should only be used when you need to manage the liveness of objects outside of the component's own function. Instead, derive a live component based on state rather than setting a live component within state. +2. **Use `use_liveness_scope` to manage live objects**: If you need to manage the liveness of objects created outside of the component's own function, use `use_liveness_scope` to ensure they are kept alive. 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/). + +## Refactor to avoid liveness scope + +In the above example, we could refactor the component to avoid using `use_liveness_scope` by deriving the table from state instead of setting it directly. If you can avoid using `use_liveness_scope`, it is recommended to do so: + +```python +from deephaven import ui, time_table + + +@ui.component +def ui_resetable_table(): + iteration, set_iteration = ui.use_state(0) + table = ui.use_memo(lambda: time_table("PT1s"), [iteration]) + return [ + ui.action_button( + "Reset", + on_press=lambda: set_iteration(iteration + 1), + ), + table, + ] + + +resetable_table = ui_resetable_table() +``` + +## API Reference + +```{eval-rst} +.. dhautofunction:: deephaven.ui.use_liveness_scope +``` diff --git a/plugins/ui/docs/hooks/use_render_queue.md b/plugins/ui/docs/hooks/use_render_queue.md new file mode 100644 index 000000000..45bbdcf3a --- /dev/null +++ b/plugins/ui/docs/hooks/use_render_queue.md @@ -0,0 +1,148 @@ +# use_render_queue + +`use_render_queue` lets you use the render queue in your component. Whenever work is done in a component, it must be performed on the render thread. If you create a new thread to perform some work on the background and then want to update a component, you should queue that work on the render thread. Some actions (like [toasts](../components/toast.md)) will raise an error if they are not triggered from the render thread. + +## Example + +This example listens to table updates and displays a toast message when the table updates. The [`toast` function](../components/toast.md) must be triggered on the render thread, whereas the listener is not fired on the render thread. Therefore, you must use the render queue to trigger the toast. + +```python +from deephaven import time_table +from deephaven import ui + +_source = time_table("PT5S").update("X = i").tail(5) + + +@ui.component +def toast_table(t): + render_queue = ui.use_render_queue() + + def listener_function(update, is_replay): + data_added = update.added()["X"][0] + render_queue(lambda: ui.toast(f"added {data_added}", timeout=5000)) + + ui.use_table_listener(t, listener_function, []) + return t + + +my_toast_table = toast_table(_source) +``` + +## UI recommendations + +1. **Use the render queue to trigger toasts**: When you need to trigger a toast from a background thread, use the render queue to ensure the toast is triggered on the render thread. Otherwise, an exception will be raised. +2. **Use the render queue to batch UI updates from a background thread**: By default, setter functions from the [`use_state`](./use_state.md) hook are already fired on the render thread. However, if you have multiple updates to make to the UI from a background thread, you can use the render queue to batch them together. + +## Batch updates + +Setter functions from the [`use_state`](./use_state.md) hook are always fired on the render thread, so if you call a series of updates from a callback on the render thread, they will be batched together. Consider the following, which will increment states `a` and `b` in the callback from pressing on "Update values": + +```python +from deephaven import ui +import time + + +@ui.component +def ui_batch_example(): + a, set_a = ui.use_state(0) + b, set_b = ui.use_state(0) + + ui.toast( + f"Values are {a} and {b}", + variant="negative" if a != b else "neutral", + timeout=5000, + ) + + def do_work(): + set_a(lambda new_a: new_a + 1) + # Introduce a bit of delay between updates + time.sleep(0.1) + set_b(lambda new_b: new_b + 1) + + return ui.button("Update values", on_press=do_work) + + +batch_example = ui_batch_example() +``` + +Because `do_work` is called from the render thread (in response to the `on_press` ), `set_a` and `set_b` will queue their updates on the render thread and they will be batched together. This means that the toast will only show once, with the updated values of `a` and `b` and they will always be the same value when the component re-renders. + +If we instead put `do_work` in a background thread, the updates are not guaranteed to be batched together: + +```python +from deephaven import ui +import threading +import time + + +@ui.component +def ui_batch_example(): + a, set_a = ui.use_state(0) + b, set_b = ui.use_state(0) + + ui.toast( + f"Values are {a} and {b}", + variant="negative" if a != b else "neutral", + timeout=5000, + ) + + def do_work(): + set_a(lambda new_a: new_a + 1) + # Introduce a bit of delay between updates + time.sleep(0.1) + set_b(lambda new_b: new_b + 1) + + def start_background_thread(): + threading.Thread(target=do_work).start() + + return ui.button("Update values", on_press=start_background_thread) + + +batch_example = ui_batch_example() +``` + +When running the above example, _two_ toasts appear with each button press: a red one where `a != b` (as `a` gets updated first), then a neutral one where `a == b` (as `b` gets updated second). Use the `use_render_queue` hook to ensure the updates are always batched together when working with a background thread: + +```python +from deephaven import ui +import threading +import time + + +@ui.component +def ui_batch_example(): + render_queue = ui.use_render_queue() + a, set_a = ui.use_state(0) + b, set_b = ui.use_state(0) + + ui.toast( + f"Values are {a} and {b}", + variant="negative" if a != b else "neutral", + timeout=5000, + ) + + def do_work(): + def update_state(): + set_a(lambda new_a: new_a + 1) + # Introduce a bit of delay between updates + time.sleep(0.1) + set_b(lambda new_b: new_b + 1) + + render_queue(update_state) + + def start_background_thread(): + threading.Thread(target=do_work).start() + + return ui.button("Update values", on_press=start_background_thread) + + +batch_example = ui_batch_example() +``` + +Now when we run this example and press the button, we'll see only one toast with the updated values of `a` and `b`, and they will always be the same value when the component re-renders (since the updates are batched together on the render thread). + +## API Reference + +```{eval-rst} +.. dhautofunction:: deephaven.ui.use_render_queue +``` diff --git a/plugins/ui/docs/hooks/use_row_data.md b/plugins/ui/docs/hooks/use_row_data.md new file mode 100644 index 000000000..f4cdab1bd --- /dev/null +++ b/plugins/ui/docs/hooks/use_row_data.md @@ -0,0 +1,102 @@ +# use_row_data + +`use_row_data` lets you use the data of the first row of a table as a dictionary. This is useful when you want to listen to an updating table and use the data in your component. + +## Example + +```python +from deephaven import time_table, ui + + +@ui.component +def ui_table_row(table): + row_data = ui.use_row_data(table) + if row_data == (): + return ui.heading("No data yet.") + return ui.heading(f"Row data is {row_data}. Value of X is {row_data['x']}") + + +table_row = ui_table_row(time_table("PT1s").update("x=i").reverse()) +``` + +In the above example, `ui_table_row` is a component that listens to a table and displays the first row of data. The `row_data` variable is updated every time the table updates. + +## Recommendations + +1. **Use `use_row_data` for listening to table updates**: If you need to listen to a table for one row of data, use `use_row_data`. +2. **Use table operations to filter to one row**: 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 row you want to listen to. `use_row_data` always uses the first row of the table. +3. **Pass a Sentinel value to `use_row_data`**: If you want to use a default value when the table is empty, pass a sentinel value to `use_row_data`. The default sentinel value is `None`, which is returned when the table is empty. + +## Empty tables + +If the table is empty, the value of `row_data` will return the value of `None`. + +```python +from deephaven import time_table, ui +import datetime as dt + + +@ui.component +def ui_table_row(table): + row_data = ui.use_row_data(table) + if row_data is None: + return ui.heading("No data yet.") + return ui.heading(f"Row data: {row_data}.") + + +start_time = dt.datetime.now() + dt.timedelta(seconds=2) +table_row = ui_table_row( + time_table("PT1s", start_time=start_time).update("x=i").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_row(table): + row_data = ui.use_row_data(table, sentinel={"Timestamp": "No data yet."}) + return ui.heading(f"Row data: {row_data}. Value of 'x' is {row_data['x']}") + + +start_time = dt.datetime.now() + dt.timedelta(seconds=2) +table_row = ui_table_row( + time_table("PT1s", start_time=start_time).update("x=i").tail(1) +) +``` + +## Null values + +If the table has a `null` value in the first row, the value for that cell will be `pandas.NA`. + +```python +from deephaven import time_table, ui +import datetime as dt +import pandas as pd + + +@ui.component +def ui_table_row(table): + row_data = ui.use_row_data(table) + if row_data is None: + return ui.heading("No data yet.") + if pd.isna(row_data["x"]): + return ui.heading("Value of 'x' is null.") + return ui.heading(f"Row data: {row_data}. Value of 'x' is {row_data['x']}") + + +start_time = dt.datetime.now() + dt.timedelta(seconds=2) +table_row = ui_table_row( + time_table("PT1s", start_time=start_time).update("x=i%2==0?null:i").tail(1) +) +``` + +## API reference + +```{eval-rst} +.. dhautofunction:: deephaven.ui.use_row_data +``` diff --git a/plugins/ui/docs/hooks/use_row_list.md b/plugins/ui/docs/hooks/use_row_list.md new file mode 100644 index 000000000..ef6539ead --- /dev/null +++ b/plugins/ui/docs/hooks/use_row_list.md @@ -0,0 +1,102 @@ +# use_row_list + +`use_row_list` lets you use the data of the first row of a table as a list. This is useful when you want to listen to an updating table and use the data in your component. + +## Example + +```python +from deephaven import time_table, ui + + +@ui.component +def ui_table_row_list(table): + row_list = ui.use_row_list(table) + if row_list == (): + return ui.heading("No data yet.") + return ui.heading(f"The row list is {row_list}. Value of X is {row_list[1]}.") + + +table_row_list = ui_table_row_list(time_table("PT1s").update("x=i").reverse()) +``` + +In the above example, `ui_table_row_list` is a component that listens to a table and displays the first row of data as a list. The `row_list` variable is updated every time the table updates. + +## Recommendations + +1. **Use `use_row_list` for listening to table updates**: If you need to listen to a table for one row of data as a list, use `use_row_list`. +2. **Use table operations to filter to one row**: 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 row you want to listen to. `use_row_list` always uses the first row of the table. +3. **Pass a Sentinel value to `use_row_list`**: If you want to use a default value when the table is empty, pass a sentinel value to `use_row_list`. The default sentinel value is `None`, which is returned when the table is empty. + +## Empty tables + +If the table is empty, the value of `row_list` will return the value of `None`. + +```python +from deephaven import time_table, ui +import datetime as dt + + +@ui.component +def ui_table_row_list(table): + row_list = ui.use_row_list(table) + if row_list is None: + return ui.heading("No data yet.") + return ui.heading(f"Row list: {row_list}") + + +start_time = dt.datetime.now() + dt.timedelta(seconds=2) +table_row_list = ui_table_row_list( + time_table("PT1s", start_time=start_time).update("x=i").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_row_list(table): + row_list = ui.use_row_list(table, sentinel="No data yet.") + return ui.heading(f"Row list: {row_list}") + + +start_time = dt.datetime.now() + dt.timedelta(seconds=2) +table_row_list = ui_table_row_list( + time_table("PT1s", start_time=start_time).update("x=i").tail(1) +) +``` + +## Null values + +If the table has a `null` value in the first row, the value for that cell will be `pandas.NA`. + +```python +from deephaven import time_table, ui +import datetime as dt +import pandas as pd + + +@ui.component +def ui_table_row_list(table): + row_list = ui.use_row_list(table) + if row_list is None: + return ui.heading("No data yet.") + if pd.isna(row_list[1]): + return ui.heading("x is null value.") + return ui.heading(f"Row list: {row_list}. Value of X is {row_list[1]}") + + +start_time = dt.datetime.now() + dt.timedelta(seconds=2) +table_row_list = ui_table_row_list( + time_table("PT1s", start_time=start_time).update("x=i%2==0?null:i").tail(1) +) +``` + +## API reference + +```{eval-rst} +.. dhaufunction:: deephaven.ui.use_row_list +``` diff --git a/plugins/ui/docs/hooks/use_table_data.md b/plugins/ui/docs/hooks/use_table_data.md index 80fa7edc0..d77ef3fdc 100644 --- a/plugins/ui/docs/hooks/use_table_data.md +++ b/plugins/ui/docs/hooks/use_table_data.md @@ -22,7 +22,75 @@ In the above example, `ui_table_data` is a component that listens to the last 5 ## Recommendations 1. **Use `use_table_data` for listening to table updates**: If you need to listen to a table for all the data, use `use_table_data`. -2. **Use table operations to filter to the data you want**: If your table has multiple rows and columns, use table operations such as `.where` and `.select` to filter to the data you want to listen to. +2. **Use table operations to filter to the data you want**: 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 data you want to listen to. +3. **Pass a Sentinel value to `use_table_data`**: If you want to use a default value when the table is empty, pass a sentinel value to `use_table_data`. The default sentinel value is `None`, which is returned when the table is empty. + +## Empty tables + +If the table is empty, the value of `table_data` will return the value of `None`. + +```python +from deephaven import time_table, ui +import datetime as dt + + +@ui.component +def ui_table_data(table): + table_data = ui.use_table_data(table) + if table_data is None: + return ui.heading("No data yet.") + return ui.heading(f"Table data: {table_data}") + + +start_time = dt.datetime.now() + dt.timedelta(seconds=2) +table_data = ui_table_data( + time_table("PT1s", start_time=start_time).update("x=i").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_data(table): + table_data = ui.use_table_data(table, sentinel="No data yet.") + return ui.heading(f"Table data: {table_data}") + + +start_time = dt.datetime.now() + dt.timedelta(seconds=2) +table_data = ui_table_data( + time_table("PT1s", start_time=start_time).update("x=i").tail(5) +) +``` + +## Null values + +If the table has null values, they will be represented in the data with `pandas.NA`. + +```python +from deephaven import time_table, ui +import datetime as dt + + +@ui.component +def ui_table_data(table): + table_data = ui.use_table_data(table) + if table_data is None: + return ui.heading("No data yet.") + if pd.isna(table_data["x"][0]): + return ui.heading("First value of 'x' is null.") + return ui.heading(f"Table data: {table_data}") + + +start_time = dt.datetime.now() + dt.timedelta(seconds=2) +table_data = ui_table_data( + time_table("PT1s", start_time=start_time).update("x=i%2==0?null:i").tail(3) +) +``` ## API Reference diff --git a/plugins/ui/docs/hooks/use_table_listener.md b/plugins/ui/docs/hooks/use_table_listener.md new file mode 100644 index 000000000..b41e8147c --- /dev/null +++ b/plugins/ui/docs/hooks/use_table_listener.md @@ -0,0 +1,80 @@ +# use_table_listener + +`use_table_listener` lets you listen to a table for raw updates. This is an advanced feature requiring an understanding of how [table listeners](https://deephaven.io/core/docs/how-to-guides/table-listeners-python/) work and the limitations of running code while the Update Graph is running. Most usages of this are more appropriate to implement with [table data hooks](./overview.md#data-hooks). This is useful when you want to listen to the raw updates from a table and perform a custom action when the table updates. + +## Example + +```python +from deephaven import time_table, ui +from deephaven.table import Table + +_source = time_table("PT1s").update("X = i") + + +@ui.component +def ui_table_monitor(t: Table): + def listener_function(update, is_replay): + print(f"Table updated: {update}, is_replay: {is_replay}") + + ui.use_table_listener(t, listener_function, []) + return t + + +table_monitor = ui_table_monitor(_source) +``` + +## Display the last updated row + +Here's an example that listens to table updates and will display the last update as a header above the table. This is a simple example to demonstrate how to use `use_table_listener` to listen to table updates and update state in your component. + +```python +from deephaven import time_table, ui +from deephaven.table import Table + + +@ui.component +def ui_show_last_changed(t: Table): + last_change, set_last_change = ui.use_state("No changes yet.") + + def listener_function(update, is_replay): + set_last_change(f"{update.added()['X'][0]} was added") + + ui.use_table_listener(t, listener_function, []) + return [ui.heading(f"Last change: {last_change}"), t] + + +_source = time_table("PT5s").update("X = i") +show_last_changed = ui_show_last_changed(_source) +``` + +## Display a toast + +Here is a simple example that listens to table updates and displays a toast message when the table updates. Note you must use a [render queue](./use_render_queue.md) to trigger the toast, as the listener is not fired on the render thread. + +```python +from deephaven import time_table +from deephaven import ui + +_source = time_table("PT5S").update("X = i").tail(5) + + +@ui.component +def toast_table(t): + render_queue = ui.use_render_queue() + + def listener_function(update, is_replay): + 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]) + return t + + +my_toast_table = toast_table(_source) +``` + +## API Reference + +```{eval-rst} +.. dhautofunction:: deephaven.ui.use_table_listener +``` diff --git a/plugins/ui/docs/sidebar.json b/plugins/ui/docs/sidebar.json index d0f924735..4d8676eca 100644 --- a/plugins/ui/docs/sidebar.json +++ b/plugins/ui/docs/sidebar.json @@ -295,6 +295,10 @@ "label": "use_effect", "path": "hooks/use_effect.md" }, + { + "label": "use_liveness_scope", + "path": "hooks/use_liveness_scope.md" + }, { "label": "use_memo", "path": "hooks/use_memo.md" @@ -303,6 +307,18 @@ "label": "use_ref", "path": "hooks/use_ref.md" }, + { + "label": "use_render_queue", + "path": "hooks/use_render_queue.md" + }, + { + "label": "use_row_data", + "path": "hooks/use_row_data.md" + }, + { + "label": "use_row_list", + "path": "hooks/use_row_list.md" + }, { "label": "use_state", "path": "hooks/use_state.md" @@ -310,6 +326,10 @@ { "label": "use_table_data", "path": "hooks/use_table_data.md" + }, + { + "label": "use_table_listener", + "path": "hooks/use_table_listener.md" } ] } diff --git a/plugins/ui/src/deephaven/ui/_internal/EventContext.py b/plugins/ui/src/deephaven/ui/_internal/EventContext.py index 6fdad81aa..12b081d93 100644 --- a/plugins/ui/src/deephaven/ui/_internal/EventContext.py +++ b/plugins/ui/src/deephaven/ui/_internal/EventContext.py @@ -28,8 +28,8 @@ def get_event_context() -> EventContext: """ try: return _local_data.event_context - except AttributeError: - raise NoContextException("No context set") + except AttributeError as e: + raise NoContextException("No context set") from e def _set_event_context(context: Optional[EventContext]): diff --git a/plugins/ui/src/deephaven/ui/components/toast.py b/plugins/ui/src/deephaven/ui/components/toast.py index 72ecfc289..d14b99c3a 100644 --- a/plugins/ui/src/deephaven/ui/components/toast.py +++ b/plugins/ui/src/deephaven/ui/components/toast.py @@ -4,11 +4,16 @@ from typing import Callable from .._internal.utils import dict_to_react_props +from .._internal.EventContext import NoContextException from ..types import ToastVariant _TOAST_EVENT = "toast.event" +class ToastException(NoContextException): + pass + + def toast( message: str, *, @@ -37,5 +42,10 @@ def toast( None """ params = dict_to_react_props(locals()) - send_event = use_send_event() + try: + send_event = use_send_event() + except NoContextException as e: + raise ToastException( + "Toasts must be triggered from the render thread. Use the hook `use_render_queue` to queue a function on the render thread." + ) from e send_event(_TOAST_EVENT, params) diff --git a/plugins/ui/src/deephaven/ui/hooks/use_cell_data.py b/plugins/ui/src/deephaven/ui/hooks/use_cell_data.py index 4f68c76bf..a57986f7a 100644 --- a/plugins/ui/src/deephaven/ui/hooks/use_cell_data.py +++ b/plugins/ui/src/deephaven/ui/hooks/use_cell_data.py @@ -29,13 +29,13 @@ def _cell_data( raise IndexError("Cannot get row list from an empty table") -def use_cell_data(table: Table | None, sentinel: Sentinel = ()) -> Any | Sentinel: +def use_cell_data(table: Table | None, sentinel: Sentinel = None) -> Any | Sentinel: """ Return the first cell of the table. The table should already be filtered to only have a single cell. Args: table: The table to extract the cell from. - sentinel: The sentinel value to return if the table is ticking but empty. Defaults to (). + sentinel: The sentinel value to return if the table is ticking but empty. Defaults to None. Returns: Any: The first cell of the table. diff --git a/plugins/ui/src/deephaven/ui/hooks/use_column_data.py b/plugins/ui/src/deephaven/ui/hooks/use_column_data.py index 418ed8372..ba02c2e78 100644 --- a/plugins/ui/src/deephaven/ui/hooks/use_column_data.py +++ b/plugins/ui/src/deephaven/ui/hooks/use_column_data.py @@ -29,14 +29,14 @@ def _column_data( def use_column_data( - table: Table | None, sentinel: Sentinel = () + table: Table | None, sentinel: Sentinel = None ) -> ColumnData | Sentinel | None: """ Return the first column of the table as a list. The table should already be filtered to only have a single column. Args: table: The table to extract the column from. - sentinel: The sentinel value to return if the table is ticking but empty. Defaults to (). + sentinel: The sentinel value to return if the table is ticking but empty. Defaults to None. Returns: The first column of the table as a list or the sentinel value. diff --git a/plugins/ui/src/deephaven/ui/hooks/use_row_data.py b/plugins/ui/src/deephaven/ui/hooks/use_row_data.py index 4545726f9..7c1c8de5f 100644 --- a/plugins/ui/src/deephaven/ui/hooks/use_row_data.py +++ b/plugins/ui/src/deephaven/ui/hooks/use_row_data.py @@ -29,14 +29,14 @@ def _row_data( def use_row_data( - table: Table | None, sentinel: Sentinel = () + table: Table | None, sentinel: Sentinel = None ) -> RowData | Sentinel | None: """ Return the first row of the table as a dictionary. The table should already be filtered to only have a single row. Args: table: The table to extract the row from. - sentinel: The sentinel value to return if the table is ticking but empty. Defaults to (). + sentinel: The sentinel value to return if the table is ticking but empty. Defaults to None. Returns: The first row of the table as a dictionary or the sentinel value. diff --git a/plugins/ui/src/deephaven/ui/hooks/use_row_list.py b/plugins/ui/src/deephaven/ui/hooks/use_row_list.py index 46fc57f79..6d4459364 100644 --- a/plugins/ui/src/deephaven/ui/hooks/use_row_list.py +++ b/plugins/ui/src/deephaven/ui/hooks/use_row_list.py @@ -30,14 +30,14 @@ def _row_list( def use_row_list( - table: Table | None, sentinel: Sentinel = () + table: Table | None, sentinel: Sentinel = None ) -> list[Any] | Sentinel | None: """ Return the first row of the table as a list. The table should already be filtered to only have a single row. Args: table: The table to extract the row from. - sentinel: The sentinel value to return if the table is ticking but empty. Defaults to (). + sentinel: The sentinel value to return if the table is ticking but empty. Defaults to None. Returns: The first row of the table as a list or the sentinel value. diff --git a/plugins/ui/src/deephaven/ui/hooks/use_table_data.py b/plugins/ui/src/deephaven/ui/hooks/use_table_data.py index df839f2b0..557187c1d 100644 --- a/plugins/ui/src/deephaven/ui/hooks/use_table_data.py +++ b/plugins/ui/src/deephaven/ui/hooks/use_table_data.py @@ -118,11 +118,11 @@ def _table_data( def use_table_data( table: Table | None, - sentinel: Sentinel = (), - transform: Callable[ - [pd.DataFrame | Sentinel | None, bool], TransformedData | Sentinel - ] - | None = None, + sentinel: Sentinel = None, + transform: ( + Callable[[pd.DataFrame | Sentinel | None, bool], TransformedData | Sentinel] + | None + ) = None, ) -> TableData | Sentinel | TransformedData: """ Returns a dictionary with the contents of the table. Component will redraw if the table @@ -130,7 +130,7 @@ def use_table_data( Args: table: The table to listen to. If None, None will be returned, not the sentinel value. - sentinel: The sentinel value to return if the table is ticking but empty. Defaults to an empty tuple. + sentinel: The sentinel value to return if the table is ticking but empty. Defaults to None. transform: A function to transform the table data and is_sentinel values. Defaults to None, which will return the data as TableData. diff --git a/plugins/ui/src/deephaven/ui/hooks/use_table_listener.py b/plugins/ui/src/deephaven/ui/hooks/use_table_listener.py index 4d1d87027..cae15cf08 100644 --- a/plugins/ui/src/deephaven/ui/hooks/use_table_listener.py +++ b/plugins/ui/src/deephaven/ui/hooks/use_table_listener.py @@ -25,6 +25,9 @@ def listener_with_ctx( listener: The listener to call. update: The update to pass to the listener. is_replay: Whether the update is a replay. + + Returns: + None """ with exec_ctx: listener(update, is_replay) @@ -84,6 +87,9 @@ def use_table_listener( description: An optional description for the UpdatePerformanceTracker to append to the listener’s entry description, default is None. do_replay: Whether to replay the initial snapshot of the table, default is False. + + Returns: + None """ def start_listener() -> Callable[[], None]: @@ -107,5 +113,5 @@ def start_listener() -> Callable[[], None]: use_effect( start_listener, - [table, listener, description, do_replay] + list(dependencies), + [table, description, do_replay] + list(dependencies), ) diff --git a/plugins/ui/test/deephaven/ui/hooks/test_table.py b/plugins/ui/test/deephaven/ui/hooks/test_table.py index dc09aa643..ffa6373ec 100644 --- a/plugins/ui/test/deephaven/ui/hooks/test_table.py +++ b/plugins/ui/test/deephaven/ui/hooks/test_table.py @@ -5,6 +5,20 @@ from typing import Any, Callable, Union from ..BaseTest import BaseTestCase from .render_utils import render_hook +from deephaven.ui.hooks import ( + use_cell_data, + use_column_data, + use_row_data, + use_row_list, + use_table_data, + use_table_listener, +) +from deephaven import new_table +from deephaven.column import int_col +from deephaven import DynamicTableWriter +from deephaven.table_listener import TableUpdate +import deephaven.dtypes as dht +import pandas as pd LISTENER_TIMEOUT = 2.0 QUEUE_TIMEOUT = 1.0 @@ -55,9 +69,6 @@ def unregister_notify(self) -> None: class UseTableTestCase(BaseTestCase): def verify_table_updated(self, table_writer, table, update): - from deephaven.ui.hooks import use_table_listener - from deephaven.table_listener import TableUpdate - event = threading.Event() def listener(update: TableUpdate, is_replay: bool) -> None: @@ -75,9 +86,6 @@ def _test_table_listener(replayed_table_val=table, listener_val=listener): assert False, "listener was not called" def verify_table_replayed(self, table): - from deephaven.ui.hooks import use_table_listener - from deephaven.table_listener import TableUpdate - event = threading.Event() def listener(update: TableUpdate, is_replay: bool) -> None: @@ -93,9 +101,6 @@ def _test_table_listener(replay_table=table, listener_val=listener): assert False, "listener was not called" def test_table_listener(self): - from deephaven import DynamicTableWriter - import deephaven.dtypes as dht - column_definitions = {"Numbers": dht.int32, "Words": dht.string} table_writer = DynamicTableWriter(column_definitions) @@ -104,10 +109,6 @@ def test_table_listener(self): self.verify_table_updated(table_writer, table, (1, "Testing")) def test_table_data(self): - from deephaven.ui.hooks import use_table_data - from deephaven import new_table - from deephaven.column import int_col - table = new_table( [ int_col("X", [1, 2, 3]), @@ -127,9 +128,6 @@ def _test_table_data(t=table): self.assertEqual(result, expected) def test_empty_table_data(self): - from deephaven.ui.hooks import use_table_data - from deephaven import new_table - empty = new_table([]) def _test_table_data(t=empty): @@ -144,10 +142,6 @@ def _test_table_data(t=empty): self.assertEqual(result, expected) def test_ticking_table_data(self): - from deephaven.ui.hooks import use_table_data - from deephaven import DynamicTableWriter - import deephaven.dtypes as dht - column_definitions = {"Numbers": dht.int32, "Words": dht.string} table_writer = DynamicTableWriter(column_definitions) @@ -161,7 +155,7 @@ def _test_table_data(t=table): result, _ = itemgetter("result", "rerender")(render_result) - self.assertEqual(result, ()) + self.assertEqual(result, None) def _test_table_data(t=table): return use_table_data(t, sentinel="sentinel") @@ -212,12 +206,6 @@ def check_size(q): queue.unregister_notify() def test_swapping_table_data(self): - from deephaven.ui.hooks import use_table_data - from deephaven import new_table - from deephaven.column import int_col - from deephaven import DynamicTableWriter - import deephaven.dtypes as dht - table = new_table( [ int_col("X", [1, 2, 3]), @@ -262,8 +250,6 @@ def _test_table_data(t=table): self.assertEqual(result, expected) def test_none_table_data(self): - from deephaven.ui.hooks import use_table_data - def _test_table_data(t=None): return use_table_data(t) @@ -276,10 +262,6 @@ def _test_table_data(t=None): self.assertEqual(result, expected) def test_column_data(self): - from deephaven.ui.hooks import use_column_data - from deephaven import new_table - from deephaven.column import int_col - table = new_table( [ int_col("X", [1, 2, 3]), @@ -298,9 +280,6 @@ def _test_column_data(t=table): self.assertEqual(result, expected) def test_empty_column_data(self): - from deephaven.ui.hooks import use_column_data - from deephaven import new_table - empty = new_table([]) def _test_column_data(t=empty): @@ -309,10 +288,6 @@ def _test_column_data(t=empty): self.assertRaises(IndexError, render_hook, _test_column_data) def test_ticking_column_data(self): - from deephaven.ui.hooks import use_column_data - from deephaven import DynamicTableWriter - import deephaven.dtypes as dht - column_definitions = {"Words": dht.string} table_writer = DynamicTableWriter(column_definitions) @@ -326,7 +301,7 @@ def _test_column_data(t=table): result, _ = itemgetter("result", "rerender")(render_result) - self.assertEqual(result, ()) + self.assertEqual(result, None) def _test_column_data(t=table): return use_column_data(t, sentinel="sentinel") @@ -349,8 +324,6 @@ def _test_column_data(t=table): self.assertEqual(result, expected) def test_none_column_data(self): - from deephaven.ui.hooks import use_column_data - def _test_column_data(t=None): return use_column_data(t) @@ -361,10 +334,6 @@ def _test_column_data(t=None): self.assertEqual(result, None) def test_row_data(self): - from deephaven.ui.hooks import use_row_data - from deephaven import new_table - from deephaven.column import int_col - table = new_table( [ int_col("X", [1]), @@ -384,9 +353,6 @@ def _test_row_data(t=table): self.assertEqual(result, expected) def test_empty_row_data(self): - from deephaven.ui.hooks import use_row_data - from deephaven import new_table - empty = new_table([]) def _test_row_data(t=empty): @@ -395,10 +361,6 @@ def _test_row_data(t=empty): self.assertRaises(IndexError, render_hook, _test_row_data) def test_ticking_row_data(self): - from deephaven.ui.hooks import use_row_data - from deephaven import DynamicTableWriter - import deephaven.dtypes as dht - column_definitions = {"Numbers": dht.int32, "Words": dht.string} table_writer = DynamicTableWriter(column_definitions) @@ -412,7 +374,7 @@ def _test_row_data(t=table): result, _ = itemgetter("result", "rerender")(render_result) - self.assertEqual(result, ()) + self.assertEqual(result, None) def _test_row_data(t=table): return use_row_data(t, sentinel="sentinel") @@ -435,8 +397,6 @@ def _test_row_data(t=table): self.assertEqual(result, expected) def test_none_row_data(self): - from deephaven.ui.hooks import use_row_data - def _test_row_data(t=None): return use_row_data(t) @@ -447,10 +407,6 @@ def _test_row_data(t=None): self.assertEqual(result, None) def test_row_list(self): - from deephaven.ui.hooks import use_row_list - from deephaven import new_table - from deephaven.column import int_col - table = new_table( [ int_col("X", [1]), @@ -470,9 +426,6 @@ def _use_row_list(t=table): self.assertEqual(result, expected) def test_empty_row_list(self): - from deephaven.ui.hooks import use_row_list - from deephaven import new_table - empty = new_table([]) def _test_row_list(t=empty): @@ -481,10 +434,6 @@ def _test_row_list(t=empty): self.assertRaises(IndexError, render_hook, _test_row_list) def test_ticking_row_list(self): - from deephaven.ui.hooks import use_row_list - from deephaven import DynamicTableWriter - import deephaven.dtypes as dht - column_definitions = {"Numbers": dht.int32, "Words": dht.string} table_writer = DynamicTableWriter(column_definitions) @@ -498,7 +447,7 @@ def _test_row_list(t=table): result, _ = itemgetter("result", "rerender")(render_result) - self.assertEqual(result, ()) + self.assertEqual(result, None) def _test_row_list(t=table): return use_row_list(t, sentinel="sentinel") @@ -521,8 +470,6 @@ def _test_row_list(t=table): self.assertEqual(result, expected) def test_none_row_list(self): - from deephaven.ui.hooks import use_row_list - def _use_row_list(t=None): return use_row_list(t) @@ -533,10 +480,6 @@ def _use_row_list(t=None): self.assertEqual(result, None) def test_cell_data(self): - from deephaven.ui.hooks import use_cell_data - from deephaven import new_table - from deephaven.column import int_col - table = new_table( [ int_col("X", [1]), @@ -555,9 +498,6 @@ def _test_cell_data(t=table): self.assertEqual(result, expected) def test_empty_cell_data(self): - from deephaven.ui.hooks import use_cell_data - from deephaven import new_table - empty = new_table([]) def _use_cell_data(t=empty): @@ -566,10 +506,6 @@ def _use_cell_data(t=empty): self.assertRaises(IndexError, render_hook, _use_cell_data) def test_ticking_cell_data(self): - from deephaven.ui.hooks import use_cell_data - from deephaven import DynamicTableWriter - import deephaven.dtypes as dht - column_definitions = {"Words": dht.string} table_writer = DynamicTableWriter(column_definitions) @@ -583,7 +519,7 @@ def _test_cell_data(t=table): result, _ = itemgetter("result", "rerender")(render_result) - self.assertEqual(result, ()) + self.assertEqual(result, None) def _test_cell_data(t=table): return use_cell_data(t, sentinel="sentinel") @@ -606,8 +542,6 @@ def _test_cell_data(t=table): self.assertEqual(result, expected) def test_none_cell_data(self): - from deephaven.ui.hooks import use_cell_data - def _test_cell_data(t=None): return use_cell_data(t) @@ -616,3 +550,29 @@ def _test_cell_data(t=None): result, rerender = itemgetter("result", "rerender")(render_result) self.assertEqual(result, None) + + def test_ticking_cell_data_with_none(self): + + column_definitions = {"Words": dht.string} + + table_writer = DynamicTableWriter(column_definitions) + table = table_writer.table + + # a ticking table with no data should return the sentinel value + def _test_cell_data(t=table): + return use_cell_data(t, sentinel="sentinel") + + render_result = render_hook(_test_cell_data) + + result, rerender = itemgetter("result", "rerender")(render_result) + + # the initial render should return the sentinel value + self.assertEqual(result, "sentinel") + + self.verify_table_updated(table_writer, table, (None,)) + + render_result = render_hook(_test_cell_data) + + result, rerender = itemgetter("result", "rerender")(render_result) + + self.assertTrue(pd.isna(result))