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

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

<!-- id: ui-chart-kit · area: ui · stability: stable · html: https://osysharp.com/reference/ui/chart-kit/ -->

## Summary        {#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 Osysharp.Charts;
  model "model/**/*.osy";
}
```

⭐ **It ships NO BUNDLE.** `Osysharp.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    {#description}

### What it draws   {#marks}

| | |
|---|---|
| **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   {#controls}
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   {#hover}

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   {#annotations}

Things that are true about the chart but are **not in the data**: a target, an acceptable range, the day something
happened.

```osy title="a band, a rule and a note — none of them a series" syntax
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   {#one-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       {#examples}

The commonest dashboard chart there is — magnitude by category, with a target over it:

```osy title="the commonest dashboard chart there is — magnitude by category, with a target over it" test app=ui-chart-kit
app ChartExample {
  use Osysharp.Charts;
  model "model/**/*.osy";
}

using Osysharp.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       {#see-also}

- [The markdown editor kit — a rich editor you opt into](https://osysharp.com/reference/ui/markdown-editor-kit/) — the other shipped kit, and the one that DOES carry a bundle
- [Osysharp.Ui (the UI kit)](https://osysharp.com/reference/ui/kit/) — the 46 bundled controls, which need no `use` at all
- [theme tokens](https://osysharp.com/reference/ui/theming/) — the tokens a chart's surface, ink and grid read
