How to stream data and change layouts¶
Open, close, and save channels¶
Configure channels by table name and enable the optional toolbar:
panel = PerspectivePanel(
id="workspace",
toolbar=True,
config={
"ws_url": "/perspective",
"channels": {
"trades": {"title": "Trades", "plugin": "Datagrid", "columns": ["symbol", "price"]},
"orders": {"title": "Orders", "plugin": "Datagrid"},
},
"single_tab": True,
"layout": {
"layout": {"type": "tab-layout", "tabs": ["trades"]},
"panels": {"trades": {"table": "trades", "plugin": "Datagrid"}},
},
},
)
Host both tables on the Perspective server. Selecting Orders replaces the selected detail tab;
reopening Trades restores its columns, filters, sorting, and title. Omit single_tab to keep
multiple tabs. To use your own design-kit buttons, omit toolbar and attach these actions:
from spaday import Invoke, Sequence, SetStorage, by_id, field
open_orders = Invoke(by_id("workspace"), "openChannel", "orders")
close_orders = Invoke(by_id("workspace"), "closeChannel", "orders")
save_layout = Sequence(
Invoke(by_id("workspace"), "saveClean", result="saved_layout"),
SetStorage("workspace-layout", field("saved_layout")),
)
Assign a saved or imported token to config.layout to restore it. Keep its closed_channels field:
saveClean() includes configurations for channels closed through either the wrapper or native tabs.
Use the same result with Spaday’s Download action when exporting a layout.
Saved panel themes follow the current application theme when restored. For a filter sidebar that
must use its own theme, set config.master_theme and list its panel ID in the layout’s masters.
Embed a grid without internal tabs¶
Set hide_tabs=True when your application already supplies navigation:
panel = PerspectivePanel(
hide_tabs=True,
config={
"ws_url": "/perspective",
"layout": {
"panels": {"trades": {"table": "trades", "plugin": "Datagrid"}},
"layout": {"type": "tab-layout", "tabs": ["trades"]},
},
},
).style(height="24rem", width="100%", min_width="0")
Give the grid a definite height, including inside a WebAwesome tab panel. height="100%" needs a
parent with a definite height; otherwise the grid can collapse. A sized grid observes its container
when the tab becomes visible and when the container resizes. No tab-show callback is required.
hide_tabs changes presentation only. For several channels, retain toolbar=True or supply
buttons calling openChannel as shown above. The example uses the wrapper toolbar with hidden
internal tabs.
Export rows without action columns¶
Attach this action to your own button to download the configured view as CSV:
from spaday import Download, Invoke, Sequence, by_id, field
export_trades = Sequence(
Invoke(by_id("workspace"), "exportData", {"panel": "trades", "format": "csv"}, result="exported_trades"),
Download("trades.csv", field("exported_trades"), "text/csv"),
)
Use format="json" in the options mapping for JSON text, or omit panel to export the active panel.
Filters and sorting still apply. This exports the configured view, not the selected cell block.
Stream rows from Python¶
Keep the table returned by client.table(), then update it whenever application data changes:
trades = client.table(schema, limit=1_000, name="trades")
trades.update(
[
{
"symbol": "NVDA",
"quantity": 50,
"price": 182.40,
}
]
)
Connected viewers receive the update through Perspective’s websocket. Do not copy bulk rows into a spaday store.
Switch between saved layouts¶
Build two workspace layouts and compute config from a small reactive field:
from spaday import SetField, cond, element, eq, field, obj
from spaday_perspective import PerspectivePanel
panel = PerspectivePanel().compute(
"config",
obj(
{
"ws_url": "/perspective",
"tables": ["trades"],
"layout": cond(
eq(field("view"), "grouped"),
grouped_layout,
blotter_layout,
),
}
),
)
controls = element(
"nav",
element("button").text("Blotter").on("click", SetField("view", "blotter")),
element("button").text("By symbol").on("click", SetField("view", "grouped")),
)
Pass store={"view": "blotter"} to serve(). Changing the field restores only the new layout; it does
not reconnect the table websocket.
Map a page theme¶
Perspective uses its own theme names. Compute the wrapper property explicitly:
from spaday import cond, field
panel.compute("theme", cond(field("dark"), "dark", "light"))
The aliases map to Pro Dark and Pro Light (spaday_perspective.THEMES). A full Perspective
theme name is also accepted, so to use themes of your own, put their CSS on the page and compute
their names instead:
panel.compute("theme", cond(field("dark"), "Acme Dark", "Acme Light"))
Configure selection and find a value¶
Enable region selection, cell-aligned scrolling, and Find from Python:
from spaday_perspective import EditMode, PerspectivePanel, Theme
panel = PerspectivePanel(
theme=Theme.DARK,
toolbar=True,
find=True,
config={
"ws_url": "/perspective",
"layout": {
"layout": {"type": "tab-layout", "tabs": ["trades"]},
"panels": {
"trades": {
"table": "trades",
"plugin": "Datagrid",
"plugin_config": {"edit_mode": EditMode.SELECT_REGION, "scroll_lock": True},
},
},
},
},
)
Select cells and press Ctrl+C / Cmd+C to copy. Press Ctrl+F / Cmd+F while the grid is focused, or click Find, then choose a column and enter an exact value. Apply filter keeps existing filters. Use Perspective Settings to change or remove them. Cancel leaves the data view unchanged.
The main Python example and browser preview include these controls. To use a custom Find button,
call Invoke(by_id("your-panel-id"), "openFind") from its click action; keep find=True on the panel.
Persist user-edited layouts¶
perspective-config-update re-dispatches from the panel whenever the user edits the workspace —
drags a panel, changes a view, filters. Bind it to an endpoint to persist the edit:
from spaday import CallEndpoint, event_value
panel.on("perspective-config-update", CallEndpoint("POST", "/api/layout", event_value()))
Serve the stored config back through config["layout"] on the next load. panel.save() (JS)
returns the same whole-element shape on demand.
Mirror a table into a local worker¶
For tables that are read-heavy in the browser, mirror them client-side with a per-table architecture — reads (scrolls, sorts, filters) stop round-tripping to Python:
panel = PerspectivePanel(
config={
"ws_url": "/perspective",
"tables": [
{"name": "trades", "architecture": "client-server", "index": "symbol"},
"orders", # plain entries stay on the server
],
"layout": layout,
}
)
index and limit apply to the local copy. default_architecture flips the default for every
plain entry.
Mirrors resnapshot after source changes so deletions and replacements propagate correctly. Keep large, high-frequency tables on the default server architecture to avoid full-table transfer on refresh.