The Crow Programming Language

Charts and Styling

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: a view over a frame

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:

ArgumentDefaultDraws
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
marker0a moment marked with a vertical play-head
tz'UTC'the timezone the axis ticks are labeled in
height320height in pixels
panes[]lists of columns, each drawn in its own strip with its own scale ([['rsi'], ['cvd']])
levels0dashed reference lines keyed by column (map[rsi = [30, 70], cvd = [0]])
points0a frame of markers: ts, y, and optionally label and kind ('buy', 'sell')
seek0a handler that receives the moment of whichever bucket is clicked

Three properties follow from "a chart is a view over a frame":

  • It re-renders like everything else. Query at render time, or read a reactive variable, and the chart tracks the data — serve[3000, refresh = 5] from the live-loop section keeps it moving.
  • Empty is not an error. A frame with no rows renders a placeholder, so a chart before the first tick doesn't blank the page.
  • Too much is an error. A frame over 1000 points refuses with an error that names the fix — aggregate to what a screen can show, with a coarser 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.

Indicators and panes

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.

Layout: compose boxes, don't write CSS

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 defined

Read 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.

The whole vocabulary

Every attribute is a literal, and crow check rejects a value the table does not know.

OnAttributeValues
any containergap, pad, margin'xs' 's' 'm' 'l' 'xl' (4, 8, 16, 24, 32px); pad = 'm l' is vertical then horizontal
rowalign, justify, wrap'start' 'center' 'end' 'stretch'; 'start' 'center' 'end' 'between' 'around'; wrap = 'true'
gridcolumnsa count; cells share the width equally
scrollheighta size such as '96px'
overlay childrenanchor'top_left' 'top_right' 'bottom_left' 'bottom_right' 'center'
any elementwidth, min_width, max_width, fill'240px', '50%', 'fill'; fill = 'true' takes the remaining space in a row
any elementsize, weight, lines'xs' 's' 'm' 'l' 'xl' 'xxl'; 'normal' 'bold'; a line clamp
any elementsurface, radius, border, shadowsurface = 'card'; radius = 's'; border = 'true'; shadow = 's' 'm'
any elementcolor, backgrounda token name: 'text' 'muted' 'accent' 'up' 'down' 'surface' 'bg'
any containerbelow + becomes, below + columns_then, hidden_belowbelow = '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.

Run the layout example yourself

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.

Styling: seven classes, eight tokens

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.