HypenHypen
Guide

Charts

Draw data with Chart and its marks — lines, bars, points, axes, markers — in data units, with touch and click events that carry the datum

Charts

Chart gives a subtree a coordinate space. Everything inside it is a mark positioned in data units — a Line through [x, y] points, Bars from rows, an Axis, a Marker that pins ordinary Hypen components to a data point. Nothing in the DSL ever names a pixel: the chart resolves the domains, scales the marks, and re-lays them out when state, children, or the viewport change.

Chart(y: [0, 100]) {
    Axis(x)
    Axis(y, ticks: 4, grid: true)
    Bars(data: @state.units, x: "month", y: "count")
        .fill("#3b82f6")
        .onClick(@actions.selectMonth)
    Rule(y: 75).stroke("#f59e0b")
}
.height(240)

Charts ship on all five renderers: web DOM, web canvas, Android, iOS and desktop. The mark set and the event payload below are the contract each of them meets, so a chart and its handlers behave the same everywhere. Glow is a true blur on web, Android and iOS; the desktop renderer approximates it with layered translucent strokes.

Data

Every data mark takes its rows from points: / data: (or the positional argument) and accepts three shapes:

ShapeExampleMeaning
NumbersLine(points: [3, 5, 2])x is the index
TuplesLine(points: [[1, 3], [2, 5]])[x, y] pairs
ObjectsBars(data: @state.rows, x: "month", y: "count")field names from x: / y:

A string x anywhere in the chart ("Jan", "Mon") switches the x axis to categories: bars get equal bands, lines and points snap to the band centre, Axis(x) labels every category.

Data almost always comes from state: compute the rows in your module and bind them. A chart re-lays out whenever the bound array changes.

export default app
  .defineState<{ units: Array<{ month: string; count: number }> }>({ units: [] })
  .onCreated(async (state) => {
    state.units = await fetchMonthly();
  })
  .build();

Domains

By default the chart's domain is the union of its marks' data, rounded out to tick-friendly bounds. Bars always include zero so bar heights stay honest. Pin either axis explicitly when you want a stable frame:

Chart(x: [0, 12], y: [0, 100]) { ... }

Room for axis labels is reserved automatically. A chart with no Axis children is drawn edge to edge — that is a sparkline:

Row {
    Text("Revenue")
    Chart { Line(points: @state.history).stroke("#10b981") }
        .width(120)
        .height(32)
}

Marks

MarkPropsDraws
Linepoints/data, x, y, smootha polyline, or a smooth curve with smooth: true
Areapoints/data, x, y, smooththe region between the line and zero
Barsdata, x, y (or label, value), highlight, barWidth, radiusone bar per row
Pointspoints/data, x, y, radius, highlightone dot per row
Axisx or y (positional), ticks, label, gridaxis line, ticks, labels, optional grid
Rulex: or y:a dashed reference line across the plot
Markerx, y, anchorpins its children to a data point
Pathdan SVG path whose coordinates are in data units

highlight on Bars / Points takes an index or a list of indices; the rest are dimmed. Bind it to state to reflect a selection:

Bars(data: @state.units, x: "month", y: "count", highlight: @state.selected)

Marker is the bridge back to normal UI. Its children are ordinary components with ordinary applicators, positioned at a data coordinate. anchor is one of top (default, content sits above the point), bottom, left, right, center.

Marker(x: 7, y: 82) {
    Badge("Launch")
}

Path is the escape hatch: any shape, in data units, computed in your module.

Path(d: @state.outline).stroke("#8b5cf6")

Styling

Marks are styled with the usual applicators. stroke, fill, strokeWidth, fillOpacity, strokeDasharray and opacity apply to the mark's geometry; color on the Chart (or any ancestor) sets the default for axes, lines and fills, so a chart follows your text colour unless a mark says otherwise.

Chart {
    Axis(y)
    Area(points: @state.revenue).fill("#10b981").fillOpacity(0.2)
    Line(points: @state.revenue, smooth: true).stroke("#10b981").strokeWidth(3)
}
.color("#64748b")

Any applicator that resolves to CSS applies to a mark, because the mark's <g> is a normal styling target. What differs from a Box is which properties SVG geometry honours:

WantUseNotes
Colour, thicknessstroke, fill, strokeWidth, strokeDasharray, strokeLinecapinherit into every shape in the mark
Translucencyopacity, fillOpacity, strokeOpacityunitless
Glowglow(color), glow(radius: 10), glow(color: "#f59e0b", radius: 12)a drop-shadow filter that follows the stroke
Shadowshadow(...), boxShadow(...), elevation(n), dropShadow(...)on SVG these all become drop-shadow; a box shadow would be invisible
Filtersblur, saturate, brightness, hueRotate, filter(...)compose with each other and with glow
BlendmixBlendModeoverlapping series
Animatetransition("d 300ms ease-out"), transition("stroke 200ms")CSS transitions on d animate a Line between datasets (Chromium); stroke, opacity and filter transition everywhere
Cursorcursor("pointer")on interactive marks

Layout applicators (padding, margin, width on a mark) have nothing to lay out inside an <svg> and are ignored; size the Chart instead.

Interaction

Marks respond to touch and pointer input through the same event applicators as everything else. What differs is the payload: every event on a mark carries the datum it refers to, in data units, never pixels. Handlers therefore work identically on every renderer.

Bars(data: @state.units, x: "month", y: "count", highlight: @state.selected)
    .onClick(@actions.selectMonth)
    .onLongPress(@actions.showDetails)
Line(points: @state.revenue)
    .onMove(@actions.hoverPoint)
    .onMouseLeave(@actions.clearHover)

The payload:

{ "series": "bars", "index": 3, "x": "Apr", "y": 62, "datum": { "month": "Apr", "count": 62 } }
FieldMeaning
seriesthe mark's series: / name: prop, else its kind (bars, line, …)
indexposition of the datum in the bound array
x, ythe datum's values
datumthe original row, untouched

Static arguments travel alongside: .onClick(@actions.pick, tag: "targets") yields { tag, series, index, x, y, datum }.

Tapping a bar, a point, or a line vertex resolves that datum directly. A hit anywhere else on a line resolves the datum nearest the pointer along x, so a finger landing between two vertices still gets a useful answer. Touch targets around vertices and points are larger than what is drawn.

ApplicatorFires
onClick / onPresstap or click
onLongPresspress held for 500 ms
onHoverpointer enters the mark (once)
onMovepointer moves across the mark, throttled to about a frame — mouse, touch and pen
onMouseLeavepointer leaves the mark

Events on the Chart itself carry the pointer position in data units ({ x, y }), for "add a point where I tapped" interactions.

Selection and tooltips go through state

A mark holds no selection of its own. The handler writes to state, and the chart reflects it — highlight for the selected bar, a Marker for the tooltip.

Chart {
    Axis(x)
    Axis(y)
    Line(points: @state.revenue, smooth: true)
        .onMove(@actions.hoverPoint)
        .onMouseLeave(@actions.clearHover)
    Points(points: @state.hoverPoints).radius(5)
    Marker(x: @state.hover.x, y: @state.hover.y) {
        Card { Text("@{state.hover.label}") }.padding(6)
    }
}
.onAction<{ x: number; y: number }>("hoverPoint", ({ state, action }) => {
    const { x, y } = action.payload!;
    state.hover = { x, y, label: `Week ${x}: ${y}k` };
    state.hoverPoints = [[x, y]];
})
.onAction("clearHover", ({ state }) => {
    state.hover = null;
    state.hoverPoints = [];
})

A Marker whose coordinates are missing (here, state.hover is null) is hidden, so no conditional is needed around it.

Composition

A dashboard is just layout. Charts size to their container like any block, so put them in Rows, Grids and Cards, and give them a height.

Row {
    Card {
        Heading("Units")
        Chart { Axis(x) Bars(data: @state.units, x: "month", y: "count") }.height(180)
    }
    Card {
        Heading("Revenue")
        Chart { Axis(y) Line(points: @state.revenue, smooth: true) }.height(180)
    }
}
.gap(16)

Next Steps

  • State — computing rows in a module and binding them
  • Inputs — the full event applicator reference
  • Styling — applicators for colour, size and spacing