Two things every real dashboard needs, and neither costs you a line of CSS or JavaScript: a chart over your data, and a page that looks right.
chart is an element like table — valid inside views, it takes a
frame and named arguments naming the columns to draw, and renders as SVG
on the server:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
use web, ui, db, frame
conn = db.connect['sqlite://bars.db']
view render[]
bars = query[conn, 'SELECT ts, o, hi, lo, c, vol FROM bars ORDER BY ts']
return div[
h1['AAPL — 1 minute'],
chart[bars, candles = ['o', 'hi', 'lo', 'c'], bars_y = 'vol',
tz = 'America/New_York', height = 240],
class = 'card'
]
page['/', render]
serve[3000]The named arguments are the whole vocabulary:
| Argument | Default | Draws |
|---|---|---|
x | 'ts' | the column of moments on the x axis |
candles | [] | open, high, low, close columns as candlesticks |
lines | [] | columns as line series (lines = ['vwap', 'ema']) |
bars_y | '' | a column as a volume pane |
marker | 0 | a moment marked with a vertical play-head |
tz | 'UTC' | the timezone the axis ticks are labeled in |
height | 320 | height in pixels |
panes | [] | lists of columns, each drawn in its own strip with its own scale ([['rsi'], ['cvd']]) |
levels | 0 | dashed reference lines keyed by column (map[rsi = [30, 70], cvd = [0]]) |
points | 0 | a frame of markers: ts, y, and optionally label and kind ('buy', 'sell') |
seek | 0 | a handler that receives the moment of whichever bucket is clicked |
Three properties follow from "a chart is a view over a frame":
serve[3000, refresh = 5] from the live-loop section keeps it moving.GROUP BY in the query or head[frame, n] — because a chart
that repaints ten thousand points every refresh would freeze the live
loop the last section warned about. It never downsamples silently.The colors come from the style tokens below, so a chart is theme-correct in light and dark mode without any color plumbing.
A chart draws columns, so an indicator is a column. use chart brings
the usual ones — sma, ema, rsi, macd, vwap, rolling_sum,
cvd — as functions over series that return a series of the same length,
with NaN where the value is not defined yet, so the line starts where
the math does instead of guessing:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
use chart, frame
bars = frame.new[]
bars = with[bars, 'ts', [0.0, 60.0, 120.0, 180.0, 240.0, 300.0]]
bars = with[bars, 'c', [10.0, 11.0, 10.5, 11.5, 12.0, 11.0]]
bars = with[bars, 'vol', [100.0, 200.0, 150.0, 300.0, 250.0, 100.0]]
bars = with[bars, 'sma3', sma[bars.c, 3]]
bars = with[bars, 'ema3', ema[bars.c, 3]]
bars = with[bars, 'rsi3', rsi[bars.c, 3]]
bars = with[bars, 'vwap', vwap[bars.c, bars.vol]]
bars = with[bars, 'vol_2m', rolling_sum[bars.ts, bars.vol, 120.0]]
bars = with[bars, 'cvd', cvd[bars.c, bars.vol]]
m = macd[bars.c, 2, 4, 2]
log[bars.sma3[5]]
log[bars.ema3[5]]
log[bars.rsi3[5]]
log[bars.vwap[1]]
log[bars.vol_2m[5]]
log[bars.cvd[5]]
log[m.columns]expected output — from the book, verified by crow test --book
11.5
11.25
50
10.666666666666666
650
500
['line', 'signal', 'hist']The same function computed the same numbers in both execution modes —
the indicators live in the kernel, next to the chart geometry. Two forms
take moments: rsi[ts, values, n, secs] computes on clock-aligned
buckets of secs seconds (a 14-period RSI on 3-minute candles, drawn
over 1-second bars, whose history never changes as the tape advances),
and vwap[ts, price, vol, secs] is the trailing-window VWAP.
In the view, each indicator column names a pane, a level, or a line:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
use web, ui, db, frame, chart
conn = db.connect['sqlite://bars.db']
clock = 0.0
fn jump[t]
clock = t
view render[]
bars = query[conn, 'SELECT ts, o, hi, lo, c, vol FROM bars ORDER BY ts']
bars = with[bars, 'rsi', rsi[bars.c, 14]]
bars = with[bars, 'cvd', cvd[bars.c, bars.vol]]
fills = query[conn, 'SELECT ts, px AS y, side AS kind FROM fills']
return chart[bars, candles = ['o', 'hi', 'lo', 'c'], bars_y = 'vol',
panes = [['rsi'], ['cvd']],
levels = map[rsi = [30, 70], cvd = [0]],
points = fills, seek = jump,
marker = clock, tz = 'America/New_York', height = 400]
page['/', render]
serve[3000]panes stacks strips under the volume, each with its own scale and a
legend; levels draws dashed lines in whichever pane holds the column;
points marks trades on the price pane (kind = 'buy' is an up
triangle in the up color, 'sell' a down triangle); seek = jump makes
every bucket a click target that sends its moment to the handler — a
replay's scrubber in one line, with the play-head as reactive state the
way the live-loop section described.
A page is composed from five containers — stack, row, grid,
overlay, scroll — and a fixed set of layout attributes that any element
accepts: spacing from a scale (gap = 'm', pad = 'l'), sizes
(width = '240px', '50%', 'fill'), type (size = 'xl', weight = 'bold'), surfaces (surface = 'card', radius, shadow), colours as
tokens (color = 'up'), and responsive rules at named widths
(below = 'phone', columns_then = 1). There is no CSS to write, and a
wrong value is a compile error that names the scale.
Because the vocabulary is closed, Crow ships a layout engine — written in
Crow — that computes where everything goes. Tests read it through
box[], style[], and fits[]:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
use web, ui
min_qty = 0
fn reset[]
min_qty = 0
view stat_card[label, value]
return stack[h3[label], p['`value`', size = 'xl', weight = 'bold', font = 'mono'], surface = 'card']
view render[]
return stack[
h1['Trading Dashboard'],
grid[stat_card['P&L', '1,116.10'], stat_card['Trades', '4'],
columns = 2, below = 'phone', columns_then = 1],
row[field['Min qty', min_qty], button['Reset', click = reset], gap = 'm'],
overlay[scroll[p['the tape'], height = '120px'], span['LIVE', color = 'up', anchor = 'top_right']],
gap = 'l', pad = 'l'
]
test layout[]
s = app[render, width = 1280]
cards = box[s, 'grid .card']
assert[len[cards] eq 2]
assert[cards[0].y eq cards[1].y] --- side by side on a desktop
controls = box[s, 'row'][0]
assert[controls.y gt cards[0].y + cards[0].height]
assert[fits[s, 1280]]
assert[style[s, '.card'][0].background eq '--crow-surface']
assert[style[s, '.card p'][0].size eq 'xl']
phone = app[render, width = 390]
stacked = box[phone, 'grid .card']
assert[stacked[1].y gte stacked[0].y + stacked[0].height] --- one under the other
assert[stacked[0].width eq stacked[1].width]
assert[fits[phone, 390]]
page['/', render]
log['dashboard defined']expected output — from the book, verified by crow test --book
dashboard definedRead the test block as claims about the page at two widths: two cards
side by side at 1280 pixels and stacked at 390, the controls under them,
the card on the surface colour, the big number at the xl step, and
nothing overflowing at either width. Every claim is checked by the same
engine that determined the page's layout, and the browser lays out the
emitted CSS by the same rules — see docs/ui-engine-design.md for how
that agreement is kept.
Every attribute is a literal, and crow check rejects a value the table
does not know.
| On | Attribute | Values |
|---|---|---|
| any container | gap, pad, margin | 'xs' 's' 'm' 'l' 'xl' (4, 8, 16, 24, 32px); pad = 'm l' is vertical then horizontal |
row | align, justify, wrap | 'start' 'center' 'end' 'stretch'; 'start' 'center' 'end' 'between' 'around'; wrap = 'true' |
grid | columns | a count; cells share the width equally |
scroll | height | a size such as '96px' |
overlay children | anchor | 'top_left' 'top_right' 'bottom_left' 'bottom_right' 'center' |
| any element | width, min_width, max_width, fill | '240px', '50%', 'fill'; fill = 'true' takes the remaining space in a row |
| any element | size, weight, lines | 'xs' 's' 'm' 'l' 'xl' 'xxl'; 'normal' 'bold'; a line clamp |
| any element | surface, radius, border, shadow | surface = 'card'; radius = 's'; border = 'true'; shadow = 's' 'm' |
| any element | color, background | a token name: 'text' 'muted' 'accent' 'up' 'down' 'surface' 'bg' |
| any container | below + becomes, below + columns_then, hidden_below | below = 'phone' (under 600px) or 'tablet' (under 1024px); becomes = 'stack' turns a row into a stack; columns_then = 1 reflows a grid; hidden_below hides the element |
Anything else (class, href, title, placeholder) passes through as
an ordinary HTML attribute.
The full version of that dashboard — three cards, an alert badge in an
overlay, a control row that turns into a stack on phones, a
scrolling tape — lives in the repository as
examples/layout_dashboard.co.
Three commands, in the order you'd use them:
crow test examples/layout_dashboard.co # the layout truths, no browser
crow run examples/layout_dashboard.co # serve it on http://localhost:3000
crow stop # from the same directory
crow test runs three blocks: structure clicks and changes state,
layout_desktop checks that the cards share a line at 1280px and that
the P&L is green and the drawdown red, layout_phone checks that the
same cards stack one per row at 390px and that nothing pokes out. Break
a rule — drop becomes = 'stack' from the header row — and the phone
block fails before you've opened a browser, because at 390px a heading
and a badge no longer fit side by side.
crow run compiles the same file and serves it; the page you see is laid
out by the same rules the test just checked, emitted as CSS. Change a
number in a test block, or an attribute in the view, and run again.
On this page, the shorter example above has the same buttons: with
crow book running, press ▶ Run for the interpreter, ⚙ Compile
for the native build, ✓ Test for its test layout[] block — the file
is served and tested from the book without leaving it.
Every page ships with a default stylesheet — cards, tables, buttons, forms, light and dark following the system theme. The entire styling vocabulary you write in Crow is a handful of class names, passed as an ordinary attribute:
classes card stats controls primary up down stale
tokens --crow-bg --crow-surface --crow-text --crow-muted
--crow-border --crow-accent --crow-up --crow-down
You've used card, stats, and controls in the dashboard already.
up/down color a value by sign, stale dims data past its freshness
window, primary marks the one button that matters.
When the defaults aren't enough, write plain CSS in css/app.css in the
project — it's inlined after the defaults on every page. Override a
token there and every element and chart that uses it follows:
:root { --crow-accent: #c60; }
crow check flags a misspelled --crow-* token with a did-you-mean.
CSS never appears inside a .co file: the view says what, the
stylesheet says how it looks, and the boundary is the file extension.
That's all of it. Next: the final project puts the whole stack together.