Views
Views
Every view reads the same row model and the same .base file; type: picks one. 17 of 17 are built; the rest are planned, each with its stock library already chosen and installed (docs/VIEWS.md). A planned type in a .base file draws the table with a one-line notice.
Which views are built
| View | .base type | Status | Stock part | Priority |
|---|---|---|---|---|
| Table Decided by the table lane: headless engine, Notion-like surface ours, 36 KB gz, 22/22 checks. | table (Bases) | built | TanStack Table v9 + Virtual@tanstack/table-core@9.3.1, @tanstack/virtual-core@3.19.7 | now |
| Pivot A transform before the table: same menus and drill-down, no new library. | pivot | built | grammar.pivot() + TanStack@tanstack/table-core@9.3.1 | now |
| List Same virtualiser as the table, another row template; 0 KB extra. | list (Bases) | built | TanStack Virtual@tanstack/virtual-core@3.19.7 | later |
| Cards / gallery `lanes` give grid now and masonry later in one virtualiser; 0 KB extra. Live in the report. | cards (Bases) | built | TanStack Virtual (lanes) + CSS@tanstack/virtual-core@3.19.7 | now |
| Image / media grid Cards plus a lightbox; the mature MIT one, 12 CSS variables. lightGallery is GPL, Fancybox paid. | cards + lightbox: | built | PhotoSwipephotoswipe@5.4.4 | soon |
| Chart The `chart:` block is Vega-Lite: the grammar agents and Altair already write; every chart type asked. 283 KB gz, lazy. Live in the report. | chart | built | Vega-Lite + vega-embedvega@6.4.0, vega-lite@6.5.0, vega-embed@7.3.0 | now |
| KPI card / stat The survey's kit as the SPEC renamed it; a 60-line SVG sparkline, no chart engine. Tremor is React + Tailwind and stale. | kpi | built | <data-kpi> + <data-value> + <data-sparkline>none (ours) | soon |
| Kanban / board Bases' own type since 1.14. Headless drag (7 KB) that never moves a node, so edits are proposals and columns virtualise. Live in the report. | kanban (Bases) | built | Pragmatic drag and drop (+ hitbox)@atlaskit/pragmatic-drag-and-drop@4.0.0, @atlaskit/pragmatic-drag-and-drop-hitbox@3.0.0, @tanstack/virtual-core@3.19.7 | soon |
| Calendar Framework-free, MIT, resource and timeline views free, 29 CSS variables. FullCalendar and Schedule-X paywall what we'd use. | calendar | built | Event Calendar (vkurko)@event-calendar/build@5.18.1 | later |
| Timeline / Gantt Lanes of runs on a zoomable axis is its whole job; dual Apache/MIT. Frappe Gantt only if dependencies are ever needed. | timeline | built | vis-timelinevis-timeline@8.5.5 | later |
| Map The engine Obsidian's own Maps plugin uses, with its ten keys as-is; basemap self-hosted (PMTiles), never a tile CDN. | map (Bases) | built | MapLibre GL + PMTilesmaplibre-gl@6.16.0, pmtiles@4.5.0 | later |
| Tree / hierarchy TanStack sub-rows on the table and list views; 0 KB extra. Not a new type. | table / list + tree: | built | TanStack Table sub-rows@tanstack/table-core@9.3.1 | soon |
| Graph / network Zach's call; also his Block View chassis. A React island: 128 KB gz with React, 61 KB where React is already loaded. `layout: {engine: mermaid}` draws the same nodes and edges as a static Mermaid flowchart, its own chunks, fetched only when such a view opens. | graph | built | React Flow (@xyflow/react)@xyflow/react@12.12.0, react@19.3.1, react-dom@19.3.1, @dagrejs/dagre@3.1.3, mermaid@12.2.0 | soon |
| Record / peek / detail page The lane's peek, generalised into the one record surface every view opens. | record | built | The lane's peek, generalisednone (ours) | now |
| Form / editor No framework-free stock form fits; native inputs over the dataset's own property schema, submits as proposals. | record + edit: true | built | Native inputs over the property schemanone (ours) | later |
| Compare (side by side) No maintained compare component exists; it is the table on transposed rows, plus a 4 KB image slider. | compare | built | TanStack Table on transposed rows@tanstack/table-core@9.3.1, img-comparison-slider@8.0.7 | soon |
| Raw source Zach, 2026-10-26: "almost a plain Excel chart of what the data set is behind it". The same virtualiser as the table and list, over the rows and the columns; every key of every row, each value as stored, the record as it sits in its file (CONTRACT §24). 0 KB extra. | source | built | TanStack Virtual@tanstack/virtual-core@3.19.7 | now |
Every view, live
Each block is one entry of an example's .base file. Preview runs the real kit on the example's real data (it starts when you scroll to it); Code is the entry that draws it, cut from the file as written.
Table type: table
LiveRows under a sticky header: two group levels with fold and summaries, multi-sort, a display per column, units in the header or inline.
The live view starts when it scrolls into sight.
- type: table
name: By brand
order: [model, form, peak_torque, rated_torque, max_speed, mass, gear_ratio, voltage, od, length, buses, price]
groupBy: { property: brand, direction: ASC }
sort:
- { property: peak_torque, direction: DESC }
summaries: { peak_torque: Max, mass: Min, price: Min }From the example robotparts actuators · two group levels, summaries, bars in cells · stock part @tanstack/table-core@9.3.1
Pivot type: pivot
LiveGroups x split columns, one summarised value per cell; a cell opens the records behind it.
The live view starts when it scrolls into sight.
- type: pivot
name: Benchmarks
order: [variant]
groupBy: { property: family }
splitBy: [suite, test]
value: { property: q_rms_mrad, summary: Median, display: dist }From the example Models × benches · stock part @tanstack/table-core@9.3.1
List and tree type: list
LiveOne line per row: the title, then its properties inline or on their own lines, under group headings; with tree: an outline that folds.
The live view starts when it scrolls into sight.
- type: list
name: Spec tree
tree: { parent: parent }
order: [title, state, tests, status]
markers: none
separator: " "From the example Vibe Tracks feature tree · with tree:, an outline that folds · stock part @tanstack/virtual-core@3.19.7 · also covers Tree / hierarchy
Cards and media grid type: cards
LiveBases' cards: an image, the title and the view's properties per card, group headings; lightbox: true opens each image full screen with its caption and source.
The live view starts when it scrolls into sight.
- type: cards
name: Actuators
filters: 'category == "Actuator"'
order: [model, logo, brand, peak_torque, mass]
image: photo
imageFit: contain
imageAspectRatio: 0.8
cardSize: 190
lightbox: true
caption: [brand, peak_torque]
groupBy: { property: brand, direction: ASC }
sort:
- { property: peak_torque, direction: DESC }From the example robotparts photos · lightbox: true opens each photo full screen · stock part @tanstack/virtual-core@3.19.7 · also covers Image / media grid
Kanban type: kanban
LiveColumns are groupBy's values in groupOrder, each with its count; dragging a card proposes a data edit (derived properties refuse), dragging a column writes groupOrder; both work from the keyboard.
The live view starts when it scrolls into sight.
- type: kanban
name: By status
order: [title, id, priority, areas]
groupBy: { property: status }
groupOrder: [backlog, ready, running, waiting, review, frontier, done, archived]
columnWidth: 260
sort:
- { property: id, direction: ASC }From the example Vibe Tracks feature board · a drag proposes an edit; the host decides · stock part @atlaskit/pragmatic-drag-and-drop@4.0.0
Chart type: chart
LiveAny Vega-Lite chart of the view's rows: presets per .base, an Encode menu, a hover card with the photo, Ashby envelopes and index lines, the same rows as a table one click away.
The live view starts when it scrolls into sight.
- type: chart
name: Ashby · torque vs mass
chart:
mark: { type: point, filled: true, size: 22 }
encoding:
x: { field: mass, scale: { type: log } }
y: { field: peak_torque, scale: { type: log } }
color: { field: drivetrain }
guides:
- { envelope: drivetrain, shape: hull }
- { indexLine: { slope: 1, through: median }, label: "constant N·m per g (median)" }
hover: { image: photo, title: name, fields: [peak_torque, mass, torque_density, drivetrain, brand] }From the example Actuator charts · inside chart: the vocabulary is Vega-Lite's · stock part vega@6.4.0
KPI type: kpi
LiveOne stat card, or one per group: the value with its unit and basis, a delta coloured by which way is better, an SVG sparkline, "computed from N runs"; the headline card draws its history as a chart.
The live view starts when it scrolls into sight.
- type: kpi
name: KPIs
groupBy: { property: label }
groupOrder: [Successful picks per minute (SPPM), Computation time, Grasp success, Computation time p95, Bench score]
sort:
- { property: t, direction: ASC }
kpi:
value: { property: value }
compare: { against: previous, better: better, neutral: 0.01 }
spark: { x: t, y: value }From the example Vibe Tracks KPIs · the delta is coloured by which way is better · no library: ours
Calendar type: calendar
LiveRows with a date (or a start and end) on month, week, day and list grids, coloured by group; an event opens its record; drag one to propose a new date.
The live view starts when it scrolls into sight.
- type: calendar
name: Month
start: date
title: title
calendarView: month
groupBy: { property: kind, direction: ASC }From the example Releases and daily notes · stock part @event-calendar/build@5.18.1
Timeline type: timeline
LiveRows as bars (start to end) or points on a zoomable time axis, in lanes from groupBy, nested from subGroupBy, coloured by a property; a bar opens its record.
The live view starts when it scrolls into sight.
- type: timeline
name: Lanes by kind
start: start
end: end
title: title
color: release
groupBy: { property: kind, direction: ASC }
groupOrder: [release, integration, contract, main, lane]
zoom: { min: PT10M, max: P14D }From the example robotparts releases and lanes · stock part vis-timeline@8.5.5
Map type: map
LiveRows with coordinates as markers on a bundled Natural Earth world map (no tile server); rows at one point count together; marker colour from a property; a marker opens its records.
The live view starts when it scrolls into sight.
- type: map
name: Where sellers ship
filters: 'relation == "ships to"'
coordinates: coordinates
markerColor: sellerFrom the example robotparts sellers by country · a bundled world map, no tile server · stock part maplibre-gl@6.16.0
Graph type: graph
LiveRows as nodes, edges from a property listing other rows' ids. Engine flow (default): React Flow laid out by dagre; group by draws subflows; pan, zoom, minimap. Engine mermaid: the same graph as a static Mermaid flowchart. A node opens its record.
The live view starts when it scrolls into sight.
- type: graph
name: Wiring
edges: { property: feeds, label: wire }
layout: { algorithm: dagre, direction: LR }
groupBy: { property: role, direction: ASC }
groupOrder: [Power, CAN, Boards, Motors, Sensing]
order: [model, v_load, current]From the example 1-DOF rack wiring · stock part @xyflow/react@12.12.0
Record and form type: record
LiveOne row as a property sheet in sections: values with unit, basis and source, every value behind it; with edit: true, a form that proposes edits.
The live view starts when it scrolls into sight.
- type: record
name: Part page
record:
sections:
- { name: Torque and speed, properties: [peak_torque, rated_torque, torque_density, max_speed, rated_speed, rated_power] }
- { name: Size, properties: [mass, od, length, gear_ratio, backlash] }
- { name: Electrical, properties: [voltage, buses] }
- { name: In the catalog, properties: [level, cited, cad, sim, measured, price, sellers] }From the example Actuators side by side · with edit: true the record is a form · no library: ours · also covers Form / editor
Compare type: compare
Live2 to 6 rows side by side, one line per property, the better value marked, differences only, a photo pair as a slider.
The live view starts when it scrolls into sight.
- type: compare
name: Side by side
order: [brand, level, drivetrain, buses, peak_torque, rated_torque, torque_density, max_speed, rated_speed, mass, gear_ratio, voltage, backlash, od, length, rated_power, price, sellers, measured, cited, cad, sim]
compare:
rows: [aifitlab-8541438771336, aifitlab-8542347853960, aifitlab-8682672160904, aifitlab-8649843966088]From the example Actuators side by side · stock part @tanstack/table-core@9.3.1
Time series
Dates are grouped in the core, so every view gets the same buckets: a group level takes by: day | week | month | quarter | year, and a chart's date channel takes the same by:. The date is the one written: a date-only value is never shifted, and a datetime is bucketed by the calendar date in its own offset, never the viewer's time zone. Weeks start on Monday. More in Time groups and peek trail.
by: | Bucket key (sorts in time order) | Label |
|---|---|---|
day | 2026-10-20 | Tue 20 Oct 2026 |
week | 2026-10-19 | Week of 19 Oct 2026 |
month | 2026-10 | Oct 2026 |
quarter | 2026-Q4 | Q4 2026 |
year | 2026 | 2026 |
Rows grouped by week groupBy: {property, by: week}
LiveA table whose group level is a date bucket: one group per week, oldest first; the rows inside a week keep the view's sort. gaps: true would keep the empty weeks too.
The live view starts when it scrolls into sight.
- type: table
name: Runs by week
order: [run, started, model, env, attempts, successes, success_rate]
groupBy: { property: started, by: week }
sort:
- { property: started, direction: ASC }
display: { success_rate: bar }From the example ML experiment tracking
A time axis x: {field, by: week}
LiveA chart whose x is a week: one band per bucket, labelled as the table labels it, every week from the first to the last, so an empty week is an empty band and the axis holds still while you filter. WHY not Vega-Lite's own timeUnit: its weeks start on Sunday and it reads dates in the viewer's zone.
The live view starts when it scrolls into sight.
- type: chart
name: Runs per week
chart:
mark: bar
encoding:
x: { field: started, by: week, title: Week }
y: { aggregate: count, title: Runs }
color: { field: model }
hover: { title: run, fields: [started, model, env, success_rate] }From the example ML experiment tracking
Timeline type: timeline
LiveBars from start to end (or points), in lanes from groupBy, nested by subGroupBy; scroll to zoom within zoom: {min, max}.
The live view starts when it scrolls into sight.
- type: timeline
name: By release
start: start
end: end
title: title
color: kind
groupBy: { property: release, direction: ASC }
subGroupBy: { property: kind, direction: ASC }
zoom: { min: PT10M, max: P14D }From the example robotparts releases and lanes
Calendar calendarView: week
LiveThe same dates on a week grid; month, day and list are the other grids. Dragging an event proposes a new date to the host.
The live view starts when it scrolls into sight.
- type: calendar
name: Release week
filters: 'kind == "release"'
start: date
title: title
calendarView: weekFrom the example Releases and daily notes
KPI sparklines and gates kpi: {spark, gate}
LiveOne card per group, each with its history as a sparkline (spark: {x, y}) and a gate line; with compare: a card also shows its delta, coloured by which way is better.
The live view starts when it scrolls into sight.
- type: kpi
name: Pooled success by model
groupBy: { property: model, direction: ASC }
sort:
- { property: round, direction: ASC }
kpi:
value: { property: successes, summary: Ratio, per: attempts }
spark: { x: round, y: success_rate }
gate: { min: 0.55 }From the example ML experiment tracking
Explain the data
A view can open with its answer and make its numbers readable: intro: puts markdown and KPI cards above the rows, rollup: collapses a dimension, columnGroups: adds a header over related columns, and the stats, range, value, dist and markdown displays draw a number with its spread. More in Explain the data.
An answer first, rolled up intro: {text, kpi} · rollup:
LiveMarkdown and KPI cards above the rows, then every model ranked with the actuators rolled away (each cell a median with its spread). Roll up in the toolbar of the full case brings them back.
The live view starts when it scrolls into sight.
- type: table
name: Leaderboard
intro:
text: |
**Which model wins on each actuator, and is it twin-grade yet?** A card is an actuator's best model on the
Oct 23 held-out tapes (× its floor), the delta its lead over the runner-up, the gate twin-grade's 1.5 ×, the
spark its models worst → best. Below, every model ranked, **rolled up across actuators** (a cell's dots are the
actuators it ran on); un-roll in **Roll up**, click a row for the actuators behind it.
kpi:
filters:
and:
- 'kind == "model"'
- '!headline_x.isEmpty()'
groupBy: { property: leader }
kpi:
value: { property: headline_x }
compare: { against: runner_up_x, better: lower, neutral: 0.02 }
spark: { x: rank_desc, y: headline_x }
gate: { max: 1.5 }
filters:
and:
- 'kind == "model"'
- '!headline_x.isEmpty()'
order: [variant, rung, headline_x, actuator, dataset_x, worst_class_x, floor_tape_x, twin_grade, n_runs]
sort:
- { property: headline_x, direction: ASC }
display: { headline_x: dist }
rollup: { keep: [variant], dims: [actuator, variant, model, ladder_step, rung, family] }
record:
sections:
- { name: Model, properties: [variant, actuator, rung, family, description] }
- { name: Error, properties: [headline_x, dataset_x, worst_class_x, floor_tape_x, twin_grade, benches] }
- { name: Code, properties: [code_commit, code_hosted, code_path, readme] }From the example Actuator models · benches, leaderboard, parameters
Column groups columnGroups:
LiveA header over related columns (Friction, Capture, Drivetrain, Driver); display: value puts the basis mark beside each number and its 95 % interval in the hover.
The live view starts when it scrolls into sight.
- type: table
name: Parameters
intro:
text: |
**What does each model run on?** One row per model and actuator with the terms it was fitted with, grouped
under Friction, Capture, Drivetrain and Driver. The mark after a value is its basis (● measured, ○ an estimate
whose fit the model card refused, C chosen); hover a value for its 95 % CI and source. "—" is a term the model
does not run, with the reason on hover. The cards are each actuator's Coulomb friction in its best model.
kpi:
filters:
and:
- 'kind == "model"'
- 'best == true'
groupBy: { property: actuator }
kpi:
value: { property: coulomb }
filters: 'kind == "model"'
order: [variant, actuator, coulomb, viscous, breakaway, stribeck_velocity, weakening, capture_fraction, capture_speed, capture_dwell, ripple, backlash, gear_stiffness, inertia, kt, current_loop, torque_scale]
groupBy: { property: rung }
groupOrder: [M2, M3, M4, M5, M1, M0]
columnGroups:
Friction: [coulomb, viscous, breakaway, stribeck_velocity, weakening]
Capture: [capture_fraction, capture_speed, capture_dwell]
Drivetrain: [ripple, backlash, gear_stiffness, inertia]
Driver: [kt, current_loop, torque_scale]
display: { coulomb: value, viscous: value, weakening: value, ripple: value, backlash: value, inertia: value, kt: value }
record:
sections:
- { name: Model, properties: [variant, actuator, rung, description] }
- { name: Terms, properties: [coulomb, viscous, weakening, ripple, backlash, inertia, kt, current_loop] }
- { name: Code, properties: [code_commit, code_hosted, code_path, readme] }From the example Actuator models · benches, leaderboard, parameters
Sub-columns for a value and its interval display: stats
LiveOne property split into value, low, high and basis, each in its own column under the property's label, instead of the range squeezed into one cell.
The live view starts when it scrolls into sight.
- type: table
name: Parameters · with CI
intro:
text: |
The friction terms **split into sub-columns**: value, the 95 % CI's low and high, and the basis, each its own
column under its term, instead of the range squeezed into the cell.
filters:
and:
- 'kind == "model"'
- '!coulomb.isEmpty()'
order: [variant, actuator, coulomb, viscous, weakening]
groupBy: { property: rung, direction: ASC }
display: { coulomb: stats, viscous: stats, weakening: stats }
stats:
coulomb: { parts: [value, low, high, basis], labels: { low: 95% CI low, high: high } }
viscous: { parts: [value, low, high, basis], labels: { low: 95% CI low, high: high } }
weakening: { parts: [value, low, high, basis], labels: { low: 95% CI low, high: high } }From the example Actuator models · benches, leaderboard, parameters
Distributions on one scale display: dist
LiveEvery run as a dot, their median a tall tick, on one scale down the column with its ends drawn in the header, so rows compare at a glance.
The live view starts when it scrolls into sight.
- type: table
name: What each rung adds
intro:
text: |
**What does each term buy?** The full twin's ablation: switch one term off and replay the held-out tapes; the
error added is how much worse the twin gets (positive = the term helps). The strip shows every run's change on
one scale per column (dots runs, tall tick their median). The cards: the median added per rung, across actuators.
kpi:
filters: 'kind == "term"'
groupBy: { property: rung }
kpi:
value: { property: delta_x, summary: Median }
filters: 'kind == "term"'
order: [term, actuator, delta_mrad, delta_x, term_note]
groupBy: { property: rung }
groupOrder: [M2, M3, M4, driver]
sort:
- { property: delta_x, direction: DESC }
summaries: { delta_x: Median }
display: { delta_mrad: dist }From the example Actuator models · benches, leaderboard, parameters