Dashboards
Dashboards
A dashboard is a .base file with a layout:. Every panel is an ordinary view of that file, placed on a 12-column grid; datasets: names more data files, links: says how their rows relate, one rail filters every panel, and a click in one panel can narrow the others. <data-dashboard src="x.base"> draws it.
On main now: <data-dashboard>, layout:, datasets:, links:, select: and rollup: (dist/dashboard.js, CONTRACT §17). Every block below is a real dashboard from the examples, live. The card catalog and the synced clock are still on lane/dashboard-cards: Lands with dashboard cards
The dashboard file
layout: sits next to views:. Panels are in reading order; w is the number of columns (of 12) and h the height in row units (110 px each). A panel is {view}, {views: [...]} (several views as tabs inside the panel) or {text} (markdown). Under 900 px the grid is one column. Obsidian opens the same file and shows the panels as tabs.
A dashboard, live layout:
LiveVibe Tracks' track overview: a text panel, a strip of KPI cards, the track tree beside a bar chart, and one panel holding two views as tabs. Code is its layout:; every panel names a view of the same file.
The live dashboard starts when it scrolls into sight.
layout:
panels:
- text: |
## Every track, computed from its runs
Headlines are each producer's own newest number, never recomputed. **Passing** and **Pass rate** are
rolled up: the newest run of each suite, summed through every child track (a parent shows its children's
checks). Click a card, a bar or a track to narrow the whole page; click a run for its details.
w: 12
- { view: Headlines, w: 12, h: 5 }
- { view: Tracks, w: 7, h: 5 }
- { view: Runs by track, w: 5, h: 5 }
- { views: [Latest runs, Pass rate by kind], title: Runs, w: 12, h: 5 }<script type="module" src="/vendor/data-ui/data-ui.js"></script>
<script type="module" src="/vendor/data-ui/dashboard.js"></script>
<data-dashboard src="views.base"></data-dashboard>From the example Vibe Tracks · track overview dashboard · a page without a dashboard never loads dashboard.js
Several datasets, linked, and roll-ups
datasets: names each data file; a view picks one with from: (default: the file's own data:). links: says which property of one dataset holds the ids of another's rows. A link filters, it never adds a column: "show the runs of the tracks that pass the rail" reaches the runs panel as an ordinary filter on track, visible in that panel's Filter menu. Links work both ways and chain (kpis → tracks → runs).
Two datasets, one link, a roll-up datasets: · links: · rollup:
LiveVibe Tracks' run explorer. The runs are the file's own data:; tracks is a second dataset, linked by runs.track → tracks.id. The Leaderboard panel reads tracks' roll-up: each track's newest run per suite, summed through its child tracks. Hover a pass rate there: the cell says it is derived and from how many rows. Tick an area in the rail (a tracks facet) and the runs follow through the link.
The live dashboard starts when it scrolls into sight.
datasets:
runs: ../vibetracks-dashboard/runs.json
tracks:
data: ../vibetracks-dashboard/tracks.json
tree: { parent: parent }
facets: [area]
rollup:
from: runs
latest: { by: t, per: suite }
values:
checks_passed: { property: passed, summary: Sum }
checks_run: { property: checks, summary: Sum }
pass_rate: { property: passed, summary: Ratio, per: checks }
links:
- { from: runs.track, to: tracks.id }From the example Vibe Tracks · run explorer
- A roll-up is a computed column on a dataset with a
tree::rollup: {from, latest: {by, per}, values: {<column>: {property, summary, per}}}. It reads one direct link tofrom, keeps the newest row perperwhenlatest:says so, and sums through the tree, so a parent shows its children's numbers. summaryis a Bases summary name, orRatio: Σproperty/ Σper, a ratio of sums, never a mean of ratios.- Its cells say what they are:
{value, basis: derived, n, note}, drawn hollow like an estimate. A producer's own headline number is never recomputed. - A roll-up reads only its source dataset's own filters, so ticking a track in the rail does not shrink the totals it is ranked by.
One rail, click to filter
- One rail. Each dataset's
facets:is its part of the rail. Its counts include every other restriction, so a count says what a tick would leave. The rail's edge drags, like every side panel. - What a panel shows: its dataset's rail filter, every selection other panels made on it, and a semi-join from every other restricted dataset that has a link path to it. Its own
filters:still apply. - Click to filter.
select: <property>on a view: a click on a bar, a card or a row selects that property's values for the whole dashboard; clicking again clears it. The panel you clicked ignores its own selection, so you can click another bar (Superset's and Mosaic's rule). Chips above the grid show each selection. - Drill down. A click on a panel without
select:opens one side peek for the whole dashboard, resizable like every side panel. - The URL holds it all:
?health=green&runs.kind=testsfor the rails,#s.<panel>=track:a,bfor selections,#p.<panel>=<view>for a panel's tab. Reload and Back restore it. (The demos here areno-url, so they leave this page's address alone.)
Cross-filtering with select: select:
Livepidtuna's four-rung ladder on nine simulated robots. Click a KPI card or a point: select: robot_id narrows every panel over the gains to that robot, and a chip above the grid says so (click the chip to clear it). Click a bar in Error by rung to select a rung as well. The rack's data at the bottom has no link to the robots, so their rail and selections leave its two panels alone.
The live dashboard starts when it scrolls into sight.
views:
- type: kpi
name: Tuned against the oracle
from: robots
select: robot_id
groupBy: { property: robot }
kpi:
value: { property: tuned_tracking_rms }
compare: { against: oracle_tracking_rms, better: lower, neutral: 0.05 }
- type: chart
name: Gain vs error
select: robot_id
chart:
mark: { type: point, size: 70 }
encoding:
x: { field: kp, scale: { type: log }, title: Kp }
y: { field: tracking_rms, scale: { type: log }, title: Tracking RMS (mrad; mm for belt axes) }
color: { field: rung, title: Rung, sort: [factory, analytic, tuned, oracle] }
shape: { field: robot, title: Robot }
hover: { title: joint, fields: [robot, rung, kp, kd, tracking_rms, stop_p90, feedforward] }
- type: chart
name: Error by rung
select: rung
chart:
mark: bar
encoding:
x: { field: rung, sort: [factory, analytic, tuned, oracle], title: null }
y: { field: tracking_rms, aggregate: median, scale: { type: log }, title: Median tracking RMS }
color: { field: rung, sort: [factory, analytic, tuned, oracle], legend: null }From the example PID tuning results · simulated robots with illustrative parameters, and said so on the dashboard
The card catalog
A card is a view: every card is a type: in views:, placed by layout: like a table or a chart. Only its own keys are new.
Every card type
Lands with dashboard cardstype: | Draws | Stock part | Follows the clock |
|---|---|---|---|
kpi | a stat with its delta, coloured by which way is better, and a sparkline | ours (exists today) | |
gauge | a bar or semicircle against a target or gate | ours (SVG/CSS) | |
bullet | a value bar, a target tick and qualitative bands | ours (SVG) | |
barlist | a ranked bar list, a leaderboard | ours (CSS) | |
status | one cell per row, coloured by a state (a tracker grid) | ours (CSS) | |
timeseries | lines or areas over t: bands, rules, a log axis, brush-zoom, a shared crosshair, the playhead | Vega-Lite | yes |
chart | bar, histogram, scatter, heatmap: any Vega-Lite | Vega-Lite (exists today) | |
trajectory | a 2D path against its target, equal aspect, coloured by error, a tolerance tube | Vega-Lite | yes |
scene3d | 3D paths, a moving marker, an optional stick robot | three.js | yes |
video | one video that seeks to the clock | the browser | yes |
video-compare | two videos: wipe, onion skin, difference or side by side | the browser + CSS | yes |
image | an image, or a strip of them from rows | the browser | |
playback | the clock's control: play, scrub, speed, loop, step a frame, a range | ours | is the clock |
views:
- type: playback
name: Playback
playback: { speeds: [0.25, 0.5, 1, 2], range: true, readout: true }
- type: timeseries
name: Error over time
filters: 'joint == "elbow_joint" && situation != "target"'
timeseries:
x: t
y: err_mrad
series: situation
unit: mrad
rules: [{ y: 5, label: "gate 5 mrad", tone: bad }, { y: -5, tone: bad }]
bands: [{ y: [-5, 5], label: tolerance, tone: good }]
- type: video-compare
name: Overlay
from: media
overlay: { by: situation, a: tuned, b: factory, mode: wipe, split: 0.5 }
- type: trajectory
name: Tool path (XZ)
from: tool
trajectory: { x: px, y: pz, series: situation, target: target, t: t, trail: true, tolerance: 0.005 }Planned syntax, from lane/dashboard-cards @ 4ceda4d (docs/peps/0005-dashboard-cards.md, gallery/examples/dashboard-cards). Not on main yet: this block becomes live when that lane lands.
One clock, synced
Time is one more piece of dashboard state. One clock per dashboard sets t; every time-aware card follows it: a video seeks, a time series draws its playhead, a trajectory and a 3D scene move their marker. The playback card is its control. #t=3.20 in the URL lands a shared link on the same instant.
The clock and the dashboard's look
Lands with dashboard cardsPlaying, a primary video drives the clock and the other videos follow it (a jump when more than 0.1 s off, a ±5 % nudge when close). Paused, a seek places every video, playhead and marker in the same task. Keys: Space, ← / → a frame, Shift+← / → a second, J / K / L, [ and ] for the range. style: sets about ten dashboard-wide knobs (surface, radius, gaps, titles, chart chrome, number format, a colour per series name); a panel may override any of them.
style:
chrome: minimal
numbers: { digits: 3, compact: true }
palette: { target: "#8b9099", factory: "#e8743b", tuned: "#2a78d6", oracle: "#19a979" }
clock:
data: samples
time: t
loop: true
fps: 30Planned syntax, from lane/dashboard-cards @ 4ceda4d (docs/peps/0005-dashboard-cards.md, gallery/examples/dashboard-cards). Not on main yet: this block becomes live when that lane lands.