From 4c4aaa42432e73733eb98a295ec9eac23c526a5c Mon Sep 17 00:00:00 2001 From: Sven Augustin Date: Sat, 2 May 2026 13:46:36 +0200 Subject: [PATCH] first prototype --- aggridx.py | 141 ++++++++++++++++++++++++++++++++++++++++++++++++++++ registry.py | 18 +++++++ stand.py | 110 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 269 insertions(+) create mode 100644 aggridx.py create mode 100644 registry.py create mode 100644 stand.py diff --git a/aggridx.py b/aggridx.py new file mode 100644 index 0000000..a410523 --- /dev/null +++ b/aggridx.py @@ -0,0 +1,141 @@ +from nicegui import ui + + +class aggridx(ui.aggrid): + +# #TODO: this should be done in the constructor via a keyword argument switch +# #TODO: this would need the from_pandas/from_polars classmethods to pass through that switch +# def setup_sync_edits(self): +# """ +# automatically sync data to the server after edits +# this operation maintains the current view on the data +# """ +# self.on("cellValueChanged", self.set_cell_server) + + + def set_cell_server(self, evt): + row_index = evt.args["rowIndex"] + col_id = evt.args["colId"] + new_val = evt.args["newValue"] + with self.props.suspend_updates(): + self.options["rowData"][row_index][col_id] = new_val + + def set_cell_client(self, evt): + row_id = evt.args["rowId"] + col_id = evt.args["colId"] + new_val = evt.args["newValue"] + self.run_row_method(row_id, "setDataValue", col_id, new_val) + + +# def set_row_server_from_event(self, evt): +# index = evt.args["rowIndex"] +# data = evt.args["data"] +# self.set_row_server(index, data) + +# def set_row_server(self, index, data): +# """ +# overwrite row at index with data +# this operation maintains the current view on the data +# """ +# with self.props.suspend_updates(): +# self.options["rowData"][index] = data + + + def append(self, row): + """ + append row to the bottom of the table and + append any missing columns on the right side of the table + this operation maintains the current view on the data + """ + self.ensure_column_defs(row) + self.add_row_data([row]) + +# def insert(self, index, row): +# """ +# insert row at the provided index +# append any missing columns on the right side of the table +# this operation maintains the current view on the data +# """ +# self.ensure_column_defs(row) +# self.add_row_data([row], index=index) + +# def extend(self, rows): +# """ +# append rows to the bottom of the table and +# append any missing columns on the right side of the table +# this operation maintains the current view on the data +# """ +# columns = dict.fromkeys(k for d in rows for k in d) # dict.fromkeys acts as ordered set +# self.ensure_column_defs(columns) +# self.add_row_data(rows) + + + def ensure_column_defs(self, columns): + """ + append any missing columns on the right side of the table + this operation maintains the current view on the data + """ + current_column_defs = self.options["columnDefs"] + current_fields = [i["field"] for i in current_column_defs] + added_fields = [i for i in columns if i not in current_fields] + if not added_fields: + return + added_column_defs = [{"field": n} for n in added_fields] + new_column_defs = current_column_defs + added_column_defs + # update server without re-draw + with self.props.suspend_updates(): + self.options["columnDefs"] = new_column_defs + # update client + self.run_grid_method("setGridOption", "columnDefs", new_column_defs) + + def add_row_data(self, rows, index=-1): + """ + insert rows at the provided index, + with the default being appending at the bottom of the table + this operation maintains the current view on the data + """ + # update server without re-draw + with self.props.suspend_updates(): + self.options["rowData"].extend(rows) + # update client + self.run_grid_method("applyTransaction", {"add": rows, "addIndex": index}) + + +# def scroll_to_row_index(self, index): +# """ +# scroll the view on the data such that the row with the given index is visible +# negative indices count from the back +# """ +# nrows = len(self.options["rowData"]) +# index %= nrows # move index into [0, nrows) +# self.run_grid_method("ensureIndexVisible", index) + +# def scroll_to_row_key(self, key): +# """ +# scroll the view on the data such that the row with the given key is visible +# consistent with tables generated from dataframes, the column called index is used to match the key +# if the key is not found, the view is not scrolled +# """ +# indices = [i.get("index") for i in self.options["rowData"]] +# try: +# index = indices.index(key) +# except ValueError: +# return +# self.scroll_to_row_index(index) + +# def scroll_to_column_index(self, index): +# """ +# scroll the view on the data such that the column with the given index is visible +# negative indices count from the back +# """ +# key = self.options["columnDefs"][index]["field"] +# self.scroll_to_column_key(key) + +# def scroll_to_column_key(self, key): +# """ +# scroll the view on the data such that the column with the given key is visible +# """ +# self.run_grid_method("ensureColumnVisible", key) + + + diff --git a/registry.py b/registry.py new file mode 100644 index 0000000..eddb7fb --- /dev/null +++ b/registry.py @@ -0,0 +1,18 @@ +from nicegui import ui + + +class Registry: + + def __init__(self): + self.data = {} + + def __iter__(self): + return iter(self.data.values()) + + def add(self, widget): + client = ui.context.client + self.data[client.id] = widget + client.on_disconnect(lambda: self.data.pop(client.id, None)) + + + diff --git a/stand.py b/stand.py new file mode 100644 index 0000000..4e0bd9b --- /dev/null +++ b/stand.py @@ -0,0 +1,110 @@ +from datetime import datetime +from typing import Any + +import arcticdb as adb +import pandas as pd +from fastapi import APIRouter +from nicegui import app, ui + +from aggridx import aggridx +from registry import Registry + + +uri = "lmdb://adb" +ac = adb.Arctic(uri) + +lib = ac.get_library( + "stand", + create_if_missing=True, + library_options=adb.LibraryOptions(dynamic_schema=True) +) + + +router = APIRouter() +grids = Registry() + + +options = { + "defaultColDef": { + "filter": True, + "editable": True, + "sortable": True, + "resizable": True + }, + "pagination": True, + "paginationAutoPageSize": True, + "theme": "balham" +} + + +def update_adb(evt): + if evt.args.get("source") != "edit": + # ignore event if it was no direct edit + return + + index = evt.args["data"]["index"] + index = datetime.fromisoformat(index) # nicegui converts datetime to str + df = lib.read("p12345", date_range=[index]).data + + col_id = evt.args["colId"] + new_val = evt.args["newValue"] + + df.at[index, col_id] = new_val + lib.update("p12345", df) + + +def update_grids(evt): + sender = evt.sender + for grid in grids: + grid.set_cell_server(evt) + if grid == sender: + # the sender is already up-to-date + continue + grid.set_cell_client(evt) + + +@ui.page("/") +def page(): +# with ui.left_drawer() as ld: +# ld.hide() +# dark = ui.dark_mode() +# ui.switch("dark mode").bind_value(dark) + + try: + df = lib.read("p12345").data + except adb.exceptions.NoSuchVersionException: + df = pd.DataFrame() + + df = df.reset_index() + + grid = aggridx.from_pandas(df, options=options) + grid.classes("h-[calc(100vh-2rem)]") # full height minus padding + grid.on("cellValueChanged", update_adb) + grid.on("cellValueChanged", update_grids) + grids.add(grid) + + +@router.post("/append/") +def append(row: dict[str, Any]): + now = datetime.now() + + df = pd.DataFrame(row, index=[now]) + lib.append("p12345", df) + + now = str(now) # nicegui converts datetime to str + row = {"index": now, **row} # setdefault would not force index to be the first column + + res = [] + for grid in grids: + grid.append(row) + res.append(grid.options) + + return res + + +app.include_router(router) + +ui.run(title="stand", favicon="icon.png", fastapi_docs=True, dark=True) + + +