# 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@1;
  model "model/**/*.osy";
}
```

> **It is a PACKAGE**, fetched from `github.com/osysharp/charts` — the first of the platform's own kits to move out of
> the binary, which is why the `use` carries a version. `osy lock` pins it by content address; see [package.osy](https://osysharp.com/reference/project/package/).

⭐ **It ships NO BUNDLE.** `Osysharp.MarkdownEditor` 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.

An enum member is written QUALIFIED — `side: AxisSide.Left` — exactly as in C#. A bare `Left` is refused, with a
message that says so: an enum member, a theme token and a style keyword are spelled alike, so the language always
qualifies them.

A mark may equally be written **inside a `foreach` or an `if`** — see [[#from-data]]. That is how a chart whose series
are data is written.

## 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 |
| **signed values** | a negative grows from the zero line — down on a column, left on a bar — and the axis opens below zero to make room |
| **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** | led by the app's accent, then validated hues in fixed order — any too close to the accent skipped ([[ui-palette#series]]) — never cycled |
| **theme** | card, ink, gridlines, readout and type are the app's theme tokens (`Colors.Surface`, `Colors.CardLine`, `Colors.Muted`, …), so a chart follows any theme or look with no line written for it |
| **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 |
| `Marker(category, label)` | an ANNOTATION — an event: a line across the plot at one category | which category |
| `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`, `Note` and `Marker` 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.

### Marks from data — `foreach` and `if`   {#from-data}

When the series are DATA — one per region, one per row a query returned — write the loop where the marks go. A mark
inside a `foreach` or an `if` reaches the chart exactly as a mark written directly does, and marks are drawn in the
order they are **written**: an axis before the loop, then every iteration's marks in iteration order, then whatever
follows the loop.

```osy title="the loop goes where the marks go" syntax
Chart(title: "Revenue by region", labels: months) {
  Axis(side: AxisSide.Left);
  foreach (var r in regions) {
    Column(label: r.Name, values: r.Revenue);   // one series per region, reading the loop variable
  }
  if (showTarget) { Rule(value: 60000.0, label: "Target"); }
}
```

- **An `if` with no `else`** adds its marks only while the condition holds — and when the condition is state that
  changes, the chart follows it.
- **A loop over an empty list** draws a chart with no marks in it. It is not an error and draws no default series.
- **Loops and `if`s nest** either way, and one loop body may write several marks, or a mark and an annotation together;
  each goes to its own place (`Column` to the series, `Rule` to the annotations).

⚠ **Two things cannot sit in a block that writes marks**, because that block is collected as one expression: a `var`
binding (write the value inline in the mark's arguments instead) and a NAMED pattern, `if (x is T t)` (test `x is T`
and cast where you read it). Both are refused with a message that names them.

Building the array yourself and passing it — `marks: regions.Select(r => new Mark { … }).ToArray()` — still works and is
the same value; the loop is simply the spelling that keeps the kit's own vocabulary (`Column(label:, values:)`).

### 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: AxisSide.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.

**A `Marker` is an EVENT, where a `Rule` is a VALUE** — "we released on the 9th", against "the target is 60k". It
draws a dashed line straight across the plot at one category, named at the top, so a change in the marks can be
read against what happened that day:

```osy title="releases as lines on a daily chart" syntax
Chart(title: "Downtime per day", labels: days) {
  Column(label: "Minutes down", values: downMinutes);
  foreach (var r in releases) { Marker(category: r.Day, label: r.Version); }
  Axis(side: AxisSide.Left);
}
```

It crosses the CATEGORY axis, so it is vertical on a column or line chart and horizontal on a `Bar` chart — the
opposite of a `Rule`. Like a `Note` it names its category by name, and a name the chart does not have is said on the
chart. ⚑ **Two events on one category are ONE marker whose label names both**: two lines drawn on the same spot are one
line with two labels printed over each other. Neighbouring markers' labels alternate between two rows so they do not
collide.

### A negative grows from the zero line   {#negatives}

A signed quantity — profit and loss, variance, net flow — is drawn from the **zero line**, not from the bottom of the
plot: positives grow up (right, on a bar chart) and negatives grow down (left), the axis opens below zero to fit the
data's minimum, and the zero line falls on a gridline like any other tick. A stack with values on both sides splits at
that line, and its value label stays the NET total, because that is what the stack says and what the axis measures.

```osy title="a signed series needs nothing said — it just draws" syntax
Chart(title: "Net flow", labels: months, showValues: true) {
  Column(label: "Net flow", values: netFlow);      // e.g. [12000, -8000, 24000, -3000]
  Axis(side: AxisSide.Left, format: "N0", prefix: "£");
}
```

⚠ **The sign leads the prefix**: a negative on a `prefix: "£"` axis reads `-£8,000`, as C# writes it — not `£-8,000`.
One formatter feeds the tick labels, the hover readout, the table cells, the on-mark values and every mark's
accessible name, so they cannot disagree about it.

⛔ **A bar's baseline stays zero.** `Axis(min:)` can extend the axis below zero but never lift it above, because a bar
is read by its length from zero and cropping that length changes what the bar says. Zooming is honest for a line,
scatter or candle — a price between 118 and 137 drawn from zero is a band in the top eighth — and those honour a
declared `min` in full.

### 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;

[Route("/")]
[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);
    }
  }
}
```

The series come from a list, so the loop writes one mark per region — and the target is an `if`:

```osy title="series from data, with a conditional target" test app=ui-chart-kit-data
app ChartFromData {
  use Osysharp.Charts;
  model "model/**/*.osy";
}

using Osysharp.Charts;

class Region { public string Name; public double[] Revenue; }

[Route("/")]
[Render(CSR)]
[AllowAnonymous]
component Regions() {
  string[] months = ["Jan", "Feb", "Mar"];
  var regions = new List<Region> {
    new Region { Name = "North", Revenue = [42000.0, 55000.0, 48000.0] },
    new Region { Name = "South", Revenue = [38000.0, 41000.0, 52000.0] }
  };
  bool showTarget = true;

  render {
    Chart(title: "Revenue by region", labels: months) {
      Axis(side: AxisSide.Left);
      foreach (var r in regions) {
        Column(label: r.Name, values: r.Revenue);
      }
      if (showTarget) { Rule(value: 50000.0, label: "Target"); }
    }
  }
}
```

`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
