Skip to content

Dashboards and interaction graphs

Prefer DashboardWorkspace for validated filters, one request-bound loader, and named render-only panels (phase 0.60). Phase 0.17 DashboardBinding / InteractionGraph remain available as Advanced linked-interaction primitives.

Capability readiness is Supported in the published 1.1.2 release; API compatibility remains beta. Require hedron>=1.0.0.

Start here

Need Where
Progressive dashboard facade This page (DashboardWorkspace)
What shipped in 0.60 What's new in 0.60
What shipped in 0.17 What's new in 0.17
Dash callback mapping Dash migration
NiceGUI refreshable mapping NiceGUI migration
Generated signatures Autodoc — Dashboards

Golden path — DashboardWorkspace

import os

from pydantic import BaseModel, Field

from hedron import DashboardWorkspace, DesignSystem, Hedron, Text

design = DesignSystem.brand("sales", accent="#0f766e")

app = Hedron(
    title="Sales dashboard",
    security="standard",
    explorer="off",
    theme=design,
    session_secret=os.environ.get("HEDRON_SESSION_SECRET", "replace-in-production"),
)


class Filters(BaseModel):
    region: str = "all"
    limit: int = Field(default=5, ge=1, le=50)


class DashData(BaseModel):
    region: str
    total: int


def load_dashboard(filters: Filters) -> DashData:
    # Synthetic loader — replace with authorized IO and caching policy in production.
    base = 42 if filters.region == "all" else 7
    return DashData(region=filters.region, total=base * filters.limit)


def summary_panel(data: DashData) -> object:
    return Text(f"{data.region}: {data.total}")


dashboard = DashboardWorkspace(
    name="sales",
    path="/sales",
    title="Sales",
    filters=Filters,
    load=load_dashboard,
    panels={"summary": summary_panel},
)
app.include(dashboard)


@app.page("/", title="Home")
def home():
    return Text("Open /sales for the DashboardWorkspace. Replace loader/auth for production.")

Or scaffold with hedron new NAME --template dashboard.

Mental model

  • Prefer DashboardWorkspace for ordinary filter → load → panel composition.
  • Prefer DashboardBinding / InteractionGraph / TriggerContext (Advanced) for multi-writer linked interactions — graphs are page-local and fail closed on cycles / duplicate writers.
  • PropertyPatch / CollectionPatch provide versioned incremental updates with full-fragment fallback when a patch cannot apply.
  • Cross-filter and recorder/replay compose chart/grid/map viewport triggers; do not rely on sleep-based races in tests. On 0.39, prefer compose_chartlink_039 so table selection consumes Published hedron-chart events without a parallel renderer (DATA.md).
  • Live SSE/WebSocket transports remain experimental — prefer HTMX polling / fragment refresh for Supported production paths (live interaction).

Try a cross-filter (simulated)

This compact example shows the user-facing result of one page-local filter writing to one declared table region. The production graph still owns dependency ordering, duplicate-writer checks, authorization, and the action that supplies the rows.

Use page-local controls to cross-filter one declared table region. Docs simulation.

All people
IDNameRole
1Adaadmin
2Gracemember
3Katherineadmin
4Margaretmember

Filter swaps the declared table region — same pattern as DataTable HTMX.

Minimal runnable app.py that reproduces this demo (real Hedron, not the docs simulator):

app.py
import os

from hedron import Hedron, Page, Stack, html, swap

app = Hedron(
    title="People",
    security="standard",
    explorer="off",
    session_secret=os.environ.get("HEDRON_SESSION_SECRET", "dev-only"),
)

table = app.region("people-table", description="People table")

ROWS = (
    ("1", "Ada", "admin"),
    ("2", "Grace", "member"),
    ("3", "Katherine", "admin"),
    ("4", "Margaret", "member"),
)


def table_panel(filter_role: str | None = None):
    filtered = [r for r in ROWS if filter_role is None or r[2] == filter_role]
    label = "All people" if filter_role is None else f"Role: {filter_role}"
    return html.div(
        html.strong(label),
        html.table(
            html.thead(html.tr(html.th("ID"), html.th("Name"), html.th("Role"))),
            html.tbody(*[html.tr(html.td(a), html.td(b), html.td(c)) for a, b, c in filtered]),
        ),
        id=table.id,
    )


@app.page("/")
def home() -> Page:
    return Page(
        Stack(
            table_panel(),
            html.button(
                "All",
                type="button",
                **{"hx-get": "/rows", "hx-target": table.selector, "hx-swap": "outerHTML"},
            ),
            html.button(
                "Admins",
                type="button",
                **{"hx-get": "/rows/admin", "hx-target": table.selector, "hx-swap": "outerHTML"},
            ),
            html.button(
                "Members",
                type="button",
                **{"hx-get": "/rows/member", "hx-target": table.selector, "hx-swap": "outerHTML"},
            ),
        ),
        title="People",
    )


@app.view("/rows", fragment_regions=(table,))
def all_rows():
    return swap(table_panel())


@app.view("/rows/admin", fragment_regions=(table,))
def admin_rows():
    return swap(table_panel("admin"))


@app.view("/rows/member", fragment_regions=(table,))
def member_rows():
    return swap(table_panel("member"))

Advanced — interaction graph

Register inputs and bindings before serving. Empty targets, duplicate ids, missing dependencies, and cycles raise DashboardGraphError (HED-GRAPH-0001 … 0005).

from hedron import Hedron, Text
from hedron_core.dashboard import DashboardBinding, InteractionGraph

app = Hedron(
    title="Dashboard sketch",
    security="standard",
    explorer="off",
    session_secret="replace-in-production",
)

graph = InteractionGraph()
graph.declare_inputs("chart.select", "grid.select")
graph.register(
    DashboardBinding(
        id="filter-panel",
        triggers=("chart.select", "grid.select"),
        snapshot_inputs=(),
        targets=("main-panel",),
        action_id="apply_filters",
        debounce_ms=50,
    )
)


@app.page("/", title="Home")
def home():
    order = ", ".join(graph.topological_order())
    return Text(f"Bindings in order: {order}")

Wire action_id to your own @app.action / fragment handlers and declared FragmentRegions — see the dashboard-0.17 stub for AppShell + InteractionResult wiring.

Errors

Condition Behavior
Empty / duplicate binding id DashboardGraphError (HED-GRAPH-0005)
Empty targets HED-GRAPH-0004
Cycle / missing dependency / duplicate writers Fail closed at register (HED-GRAPH-0001–0003)

Not Dash / Streamlit

There is no global callback DAG, no automatic JS conversion, and no notebook-style full rerun model. Map concepts via the migration guides above, then keep authorization and tenant isolation in your host app (multi-tenant).

Next