Summary#
Charts are a KIT, not a platform feature — you depend on it in one line and nothing is copied into your project.
It draws line, area, column, horizontal bar, scatter and OHLC candle marks over one shared value scale, with a
Pie/donut sibling beside it.
```osy title="one line to depend on it, one using to write it" syntax
app Dashboard {
use Osyrin.Charts;
model "model/**/*.osy";
}
⭐ **It ships NO BUNDLE.** `Osyrin.Markdown` carries a JavaScript shim because it wraps a third-party editor nobody
should rewrite; this kit is `.osy` files and nothing else. A charting library is the surface people are most certain
needs JavaScript — hit-testing a hover against pixel coordinates on a `<canvas>` is most of one — and the hover here
is an ancestor-conditioned style variant that lowers to a descendant CSS rule. No pointer handler, no coordinates,
no measurement.
## Signature {#signature}
```osy title="the shape: a chart, its categories, and its marks as children" syntax
Chart(title: string, subtitle: string, labels: string[], height: int, chrome: ChartChrome) {
Column(label: string, values: double[]); // …or Line, Area, Bar, Dots, Candles
Axis(side: AxisSide, ticks: int, title: string, min: double, max: double);
}The marks are CHILDREN, and that is the point. They are config records, so the call site reads as the picture
being described rather than as an array being assembled. Giving the chart labels is what asks for a category axis
— you rarely declare Axis(side: Bottom) yourself.
A bare enum member is the spelling (side: Left), exactly as in any other component call; AxisSide.Left also
works and is what you would write in C#.
Description#
What it draws#
| marks | line, area, column, horizontal bar (GROUPED for more than one series, or stacked: true), scatter, OHLC candles |
| axes | a value axis with round-number ticks, a category axis, optional gridlines, a title, an explicit min/max |
| hover | a crosshair and a readout naming every series' value in the hovered category |
| legend | automatic for two or more series; position: on any edge or None, interactive: true to toggle a series off |
| palette | eight validated hues in fixed order, never cycled |
| annotations | a reference Rule, a shaded Band, a labelled Note — none of them a series |
| table | Table() below the chart, or Table(position: Instead) in place of it — a real role: Grid |
| bare mode | chrome: Bare — the sparkline: no card, axes, gridlines, legend, heading or hover |
The whole vocabulary, in one place#
Fifteen controls. Chart and Pie are the containers; everything else goes inside one of them.
| control | it is | what it takes |
|---|---|---|
Chart(title, subtitle, labels, …) | the container for every category chart | the category labels, then marks as children |
Pie(title, subtitle, slices, …) | the container for a part-of-whole chart | Slice children, or a slices: array |
Line(label, values) | a mark — a series as a line | one double[], one per category |
Area(label, values) | a mark — a filled line | as Line; stacked: true on the chart to stack them |
Column(label, values) | a mark — vertical bars | as Line |
Bar(label, values) | a mark — horizontal bars | as Line |
Scatter(label, values) | a mark — points | as Line |
Candle(label, values, opens, highs, …) | a mark — OHLC candles | four arrays, one per category |
Slice(label, value) | a mark — one wedge of a Pie | a single number |
Axis(side, grid, ticks, …) | the value or category axis | AxisSide, and whether to draw gridlines |
Legend(position, interactive) | the series key | LegendPos, and whether clicking toggles a series |
Rule(value, label) | an ANNOTATION — a reference line | the value to sit at |
Band(from, to, label) | an ANNOTATION — a shaded range | the two bounds |
Note(category, value, label) | an ANNOTATION — a label at one point | where to put it |
Table(position) | the same data as a real role: Grid | below the chart, or Instead of it |
⚠ An annotation is not a series — Rule, Band and Note carry no data, take no palette colour and never
appear in the legend. See [[#annotations]], which is the half people get wrong.
osy kit <name> prints any of them in full, and osy kit --for "<what you want>" finds one by what it does.
One crosshair rule for every mark kind#
An invisible band of full-height cells sits over the plot, one per category. Hovering a cell reveals a crosshair and a readout of every series at that category — the shared-crosshair behaviour a line chart wants, which happens to be right for columns and dots too. What you learn on a bar chart holds on a line chart.
The readout stays inside the plot: a readout centred on the first or last category would hang past the edge and be
clipped, losing exactly the label you hovered to read. It measures with Layout.TextWidth and shifts only the
cells that would overflow, so a middle category is still centred on its crosshair — a flip, not a re-anchor.
⚑ Measured from the TEXT, not from the box. Asking Layout.Width for the readout's own width from inside the
readout answered 109.0 where the painted box was 114.6: a self-measurement lags its own content by construction,
because the number describes one layout pass and the content may be from another. Layout.TextWidth is a pure
function of the string and the font, so there is nothing to lag.
An annotation is not a series#
Things that are true about the chart but are not in the data: a target, an acceptable range, the day something happened.
Chart(title: "Revenue against target", labels: months) {
Column(label: "Revenue", values: revenue);
Band(from: 55000.0, to: 65000.0, label: "Acceptable");
Rule(value: 60000.0, label: "Target");
Note(category: "May", value: 73000.0, label: "v2 launch");
Axis(side: Left);
}⛔ An annotation gets no legend entry, no palette slot, cannot be toggled off, and DOES NOT MOVE THE SCALE —
because none of those things is true of "the target is 60k". Modelling a target as a flat one-value Line is the
usual shortcut and it costs exactly those four: the legend grows an entry nobody clicks, the palette shifts under
the real series, and a 500k target stretches the axis until every real bar is a stub in the bottom eighth.
An out-of-range annotation is clamped and says so both ways — ↑ on the face of the chart, and "(above the top
of this chart)" in the accessible name. A clamped line that looked exactly like a met target would be its own kind
of lie.
⚠ A Note names its category by NAME. An index would be a lie waiting to happen: insert a month at the front
and every note silently shifts one slot. A name that is not in labels is said out loud.
Why one value scale#
Two y-axes let any two series be made to cross wherever you like. One shared scale is the honest picture, and it is why a target belongs in an annotation rather than in a second axis.
Examples#
The commonest dashboard chart there is — magnitude by category, with a target over it:
app ChartExample {
use Osyrin.Charts;
model "model/**/*.osy";
}
using Osyrin.Charts;
[Page("/")]
[Render(CSR)]
[AllowAnonymous]
[Title("Revenue")]
component Dashboard() {
string[] months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"];
double[] revenue = [42000.0, 55000.0, 48000.0, 61000.0, 73000.0, 69000.0];
double[] target = [40000.0, 50000.0, 55000.0, 60000.0, 65000.0, 70000.0];
render {
Chart(title: "Revenue against target", subtitle: "First half", labels: months, height: 280) {
Column(label: "Revenue", values: revenue);
Line(label: "Target", values: target);
Rule(value: 60000.0, label: "Target");
Axis(side: AxisSide.Left, ticks: 4);
}
}
}demo/chart-demo is the worked gallery: every mark kind at a realistic size (/), a live query feeding a chart
(/live), the palette's eight slots and what happens at nine series (/palette), and candles over sessions
(/time).
See also#
- The markdown editor kit — a rich editor you opt into — the other shipped kit, and the one that DOES carry a bundle
- Osyrin.Ui (the UI kit) — the 46 bundled controls, which need no
useat all - theme tokens — the tokens a chart's surface, ink and grid read