Osy#the first language built for agents
Agents firstAgentic appsWorkflowsDurable Execution — built inSecurityTestingThe editorThe UI modelOne program

Reference / UI

The chart kit — line, column, bar, scatter and candle, with no JavaScript

Chart(title: "Revenue against target", labels: months) { Column(label: "Revenue", values: revenue); Axis(side: Left); }

Line, area, column, bar, scatter and OHLC candle marks over one shared value scale, plus a Pie — an optional KIT you depend on with one line. Marks are written as CHILDREN, so the call site reads as the picture being described. It ships no bundle at all: a charting library is the surface people are most certain needs JavaScript, and this one does not.

stable1 example compiled by CIuichartskitvisualisation

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#

marksline, area, column, horizontal bar (GROUPED for more than one series, or stacked: true), scatter, OHLC candles
axesa value axis with round-number ticks, a category axis, optional gridlines, a title, an explicit min/max
hovera crosshair and a readout naming every series' value in the hovered category
legendautomatic for two or more series; position: on any edge or None, interactive: true to toggle a series off
paletteeight validated hues in fixed order, never cycled
annotationsa reference Rule, a shaded Band, a labelled Notenone of them a series
tableTable() below the chart, or Table(position: Instead) in place of it — a real role: Grid
bare modechrome: 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.

controlit iswhat it takes
Chart(title, subtitle, labels, …)the container for every category chartthe category labels, then marks as children
Pie(title, subtitle, slices, …)the container for a part-of-whole chartSlice children, or a slices: array
Line(label, values)a mark — a series as a lineone double[], one per category
Area(label, values)a mark — a filled lineas Line; stacked: true on the chart to stack them
Column(label, values)a mark — vertical barsas Line
Bar(label, values)a mark — horizontal barsas Line
Scatter(label, values)a mark — pointsas Line
Candle(label, values, opens, highs, …)a mark — OHLC candlesfour arrays, one per category
Slice(label, value)a mark — one wedge of a Piea single number
Axis(side, grid, ticks, …)the value or category axisAxisSide, and whether to draw gridlines
Legend(position, interactive)the series keyLegendPos, and whether clicking toggles a series
Rule(value, label)an ANNOTATION — a reference linethe value to sit at
Band(from, to, label)an ANNOTATION — a shaded rangethe two bounds
Note(category, value, label)an ANNOTATION — a label at one pointwhere to put it
Table(position)the same data as a real role: Gridbelow the chart, or Instead of it

An annotation is not a seriesRule, 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#

Related

control — foreign UI controls (charts, grids, maps)

A `control` block declares the contract of a foreign UI widget — a chart, a data grid, a map — that a small JavaScript…

The markdown editor kit — a rich editor you opt into

A full rich-text markdown editor — sections, partial saves, a block menu, tables, find and replace, maths and diagrams…

Osyrin.Ui (the UI kit)

The bundled UI kit — ready-made styled controls like `Button`, the shared design-system vocabularies (`Tone`, `Size`)…

theme tokens

A `theme` block names your app's design tokens — colors, spacing, radii, and more — as reusable values. A token can…

layout primitives

The built-in layout primitives and how they arrange children. `Stack` stacks children in a column, `Row` lays them in a…

use

Declares a capability your app depends on, written inside the `app { }` manifest block. It provisions the capability…