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: Table hooks #168

Merged
merged 8 commits into from
Dec 22, 2023
Merged
Show file tree
Hide file tree
Changes from 2 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
11 changes: 11 additions & 0 deletions plugins/ui/src/deephaven/ui/hooks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
from .use_state import use_state
from .use_ref import use_ref
from .use_table_listener import use_table_listener
from .use_table_data import use_table_data
from .use_column_data import use_column_data
from .use_row_data import use_row_data
from .use_row_list import use_row_list
from .use_cell_data import use_cell_data


__all__ = [
"use_callback",
Expand All @@ -12,4 +18,9 @@
"use_state",
"use_ref",
"use_table_listener",
"use_table_data",
"use_column_data",
"use_row_data",
"use_row_list",
"use_cell_data",
]
40 changes: 40 additions & 0 deletions plugins/ui/src/deephaven/ui/hooks/use_cell_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from __future__ import annotations

from typing import Any
import pandas as pd

from deephaven.table import Table
from .use_table_data import _use_table_data, Sentinel


def _cell_data(data: pd.DataFrame) -> None:
"""
Return the first cell of the table.

Args:
data: pd.DataFrame: The table to extract the cell from.

Returns:
Any: The first cell of the table.
"""
try:
return data.iloc[0, 0]
except IndexError:
# if there is a static table with no rows, we will get an IndexError
raise IndexError("Cannot get row list from an empty table")


def use_cell_data(table: Table, sentinel: Sentinel = None) -> Any:
"""
Return the first cell of the table. The table should already be filtered to only have a single cell.

Args:
table: Table: The table to extract the cell from.
sentinel: Sentinel: The sentinel value to return if the table is empty. Defaults to None.

Returns:
Any: The first cell of the table.
"""
data, is_sentinel = _use_table_data(table, sentinel)

return data if is_sentinel else _cell_data(data)
40 changes: 40 additions & 0 deletions plugins/ui/src/deephaven/ui/hooks/use_column_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from __future__ import annotations

import pandas as pd

from deephaven.table import Table
from .use_table_data import _use_table_data, Sentinel, ColumnData


def _column_data(data: pd.DataFrame) -> ColumnData:
"""
Return the first column of the table as a list.

Args:
data: pd.DataFrame: The table to extract the column from.

Returns:
ColumnData: The first column of the table as a list.
"""
try:
return data.iloc[:, 0].tolist()
except IndexError:
# if there is a static table with no columns, we will get an IndexError
raise IndexError("Cannot get column data from an empty table")


def use_column_data(table: Table, sentinel: Sentinel = None) -> ColumnData | Sentinel:
"""
Return the first column of the table as a list. The table should already be filtered to only have a single column.

Args:
table: Table: The table to extract the column from.
sentinel: Sentinel: The sentinel value to return if the table is empty. Defaults to None.

Returns:
ColumnData | Sentinel: The first column of the table as a list or the
sentinel value.
"""
data, is_sentinel = _use_table_data(table, sentinel)

return data if is_sentinel else _column_data(data)
40 changes: 40 additions & 0 deletions plugins/ui/src/deephaven/ui/hooks/use_row_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from __future__ import annotations

import pandas as pd

from deephaven.table import Table

from .use_table_data import _use_table_data, RowData, Sentinel


def _row_data(data: pd.DataFrame) -> RowData:
"""
Return the first row of the table as a dictionary.

Args:
data: pd.DataFrame: The dataframe to extract the row from or the sentinel value.

Returns:
RowData: The first row of the table as a dictionary.
"""
try:
return data.iloc[0].to_dict()
except IndexError:
# if there is a static table with no rows, we will get an IndexError
raise IndexError("Cannot get row data from an empty table")


def use_row_data(table: Table, sentinel: Sentinel = None) -> RowData | Sentinel:
"""
Return the first row of the table as a dictionary. The table should already be filtered to only have a single row.

Args:
table: Table: The table to extract the row from.
sentinel: Sentinel: The sentinel value to return if the table is empty. Defaults to None.

Returns:
RowData | Sentinel: The first row of the table as a dictionary or the sentinel value.
"""
data, is_sentinel = _use_table_data(table, sentinel)

return data if is_sentinel else _row_data(data)
40 changes: 40 additions & 0 deletions plugins/ui/src/deephaven/ui/hooks/use_row_list.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from __future__ import annotations

from typing import Any
import pandas as pd

from deephaven.table import Table
from .use_table_data import _use_table_data, Sentinel


def _row_list(data: pd.DataFrame) -> None:
"""
Return the first row of the table as a list.

Args:
data: pd.DataFrame | Sentinel: The dataframe to extract the row from or the sentinel value.

Returns:
list[Any]: The first row of the table as a list.
"""
try:
return data.iloc[0].values.tolist()
except IndexError:
# if there is a static table with no rows, we will get an IndexError
raise IndexError("Cannot get row list from an empty table")


def use_row_list(table: Table, sentinel: Sentinel = None) -> list[Any] | Sentinel:
"""
Return the first row of the table as a list. The table should already be filtered to only have a single row.

Args:
table: Table: The table to extract the row from.
sentinel: Sentinel: The sentinel value to return if the table is empty. Defaults to None.

Returns:
list[Any] | Sentinel: The first row of the table as a list or the sentinel value.
"""
data, is_sentinel = _use_table_data(table, sentinel)

return data if is_sentinel else _row_list(data)
169 changes: 169 additions & 0 deletions plugins/ui/src/deephaven/ui/hooks/use_table_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
from __future__ import annotations

import deephaven.ui as ui

from deephaven.table import Table
from deephaven.table_listener import TableListener, listen, TableUpdate
from deephaven.pandas import to_pandas
from deephaven.execution_context import ExecutionContext, get_exec_ctx
from deephaven.server.executors import submit_task
from deephaven.update_graph import has_exclusive_lock

from concurrent.futures import ThreadPoolExecutor
from functools import partial
from typing import Callable, Any
from typing import List, Dict

import pandas as pd

_executor = ThreadPoolExecutor(max_workers=1)
jnumainville marked this conversation as resolved.
Show resolved Hide resolved

Sentinel = Any
ColumnName = str
ColumnData = List[Any]
RowData = Dict[ColumnName, Any]
TableData = Dict[ColumnName, ColumnData]


def _deferred_update(ctx: ExecutionContext, func: Callable[[], None]) -> None:
"""
Call the function within an execution context.

Args:
ctx: ExecutionContext: The execution context to use.
func: Callable[[], None]: The function to call.
"""
with ctx:
func()


def _on_update(
ctx: ExecutionContext,
func: Callable[[], None],
executor_name: str,
update: TableUpdate,
is_replay: bool,
) -> None:
"""
Call the function within an execution context, deferring the call to a thread pool.

Args:
ctx: ExecutionContext: The execution context to use.
func: Callable[[], None]: The function to call.
executor_name: str: The name of the executor to use.
update: TableUpdate: The update to pass to the function.
is_replay: True if the update is a replay, False otherwise.
"""
submit_task(executor_name, partial(_deferred_update, ctx, func))
Copy link
Member

Choose a reason for hiding this comment

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

I'm not too familiar with this submit_task functionality. Will need to take a closer look...



def _get_data_values(table: Table, sentinel: Sentinel):
"""
Called to get the new data and is_sentinel values when the table updates.

Args:
table: Table: The table that updated.
sentinel: Sentinel: The sentinel value to return if the table is empty and refreshing.

Returns:
tuple[pd.DataFrame | Sentinel, bool]: The table data and whether the sentinel value was
returned.
"""
data = to_pandas(table)
if table.is_refreshing:
if data.empty:
return sentinel, True
else:
return data, False
else:
return data, False


def _set_new_data(
table: Table,
sentinel: Sentinel,
set_data: Callable[[pd.DataFrame | Sentinel], None],
set_is_sentinel: Callable[[bool], None],
) -> None:
"""
Called to set the new data and is_sentinel values when the table updates.

Args:
table: Table: The table that updated.
sentinel: Sentinel: The sentinel value to return if the table is empty.
set_data: Callable[[pd.DataFrame | Sentinel], None]: The function to call to set the new data.
set_is_sentinel: Callable[[bool], None]: The function to call to set the is_sentinel value.
"""
new_data, new_is_sentinel = _get_data_values(table, sentinel)
set_data(new_data)
set_is_sentinel(new_is_sentinel)


def _use_table_data(
table: Table, sentinel: Sentinel = None
) -> tuple[pd.DataFrame | Sentinel, bool]:
jnumainville marked this conversation as resolved.
Show resolved Hide resolved
"""
Internal hook for all table data hooks. This hook will listen to the table and return the
table data as a pandas dataframe. The hook will also return a boolean indicating whether
the sentinel value was returned. This is useful as a sentinel could be a pandas dataframe.

Args:
table: Table: The table to listen to.
sentinel: Sentinel: The sentinel value to return if the table is empty. Defaults to None.

Returns:
tuple[pd.DataFrame | Sentinel, bool]: The table data and whether the sentinel value was
returned.
"""
initial_data, initial_is_sentinel = _get_data_values(table, sentinel)
data, set_data = ui.use_state(initial_data)
is_sentinel, set_is_sentinel = ui.use_state(initial_is_sentinel)

if table.is_refreshing:
ctx = get_exec_ctx()

# Decide which executor to submit callbacks to now, while we hold any locks from the caller
if has_exclusive_lock(ctx.update_graph):
executor_name = "serial"
else:
executor_name = "concurrent"

table_updated = lambda: _set_new_data(
table, sentinel, set_data, set_is_sentinel
)
ui.use_table_listener(
table, partial(_on_update, ctx, table_updated, executor_name)
)
jnumainville marked this conversation as resolved.
Show resolved Hide resolved

return data, is_sentinel


def _table_data(data: pd.DataFrame) -> TableData:
"""
Returns the table as a dictionary.

Args:
data: pd.DataFrame | Sentinel: The dataframe to extract the table data from or the
sentinel value.

Returns:
TableData: The table data.
"""
return data.to_dict(orient="list")


def use_table_data(table: Table, sentinel: Sentinel = None) -> TableData | Sentinel:
"""
Returns a dictionary with the contents of the table. Component will redraw if the table
changes, resulting in an updated frame.

Args:
table: Table: The table to listen to.
sentinel: Sentinel: The sentinel value to return if the table is empty. Defaults to None.

Returns:
pd.DataFrame | Sentinel: The table data or the sentinel value.
"""
data, is_sentinel = _use_table_data(table, sentinel)

return data if is_sentinel else _table_data(data)
Loading
Loading