# Radial column charts

> WestCoast Charts 1.1.5, release, public Core release, on npm. HTML: https://westcoast-charts.com/docs/radial-column/ · Index for agents: https://westcoast-charts.com/llms.txt · Source: library guide, commit 95f093ba

A radial column chart arranges one slim bar per category around a circle or an arc. Inside the ring
you can stack any number of **layers** (pies, donuts, sunbursts, goal rings, a live gauge, a chord
diagram and more), and you can **link** what the layers mean to the bars outside. One chart then answers
"how much, when, of what kind, and against which goal" at once. A typical use is an operations room
showing 24 hours of requests around the rim, the services inside, and error budgets as rings.

`radialColumn` is part of `west-coast-charts/premium` (see [Premium and licensing](https://westcoast-charts.com/docs/premium/index.md)). Every
option below is typed in `west-coast-charts/premium` and listed in the
[premium options reference](https://westcoast-charts.com/docs/reference/premium-options/index.md).

- [A first ring](#a-first-ring)
- [Shape: angles, radii, bars](#shape-angles-radii-bars)
- [Layers](#layers)
- [Linking layers to the bars](#linking-layers-to-the-bars)
- [Semantic keys, ring labels and rim markers](#semantic-keys-ring-labels-and-rim-markers)
- [Encoding extra metrics (colour, delta)](#encoding-extra-metrics-colour-delta)
- [Goal rings and the live gauge](#goal-rings-and-the-live-gauge)
- [Chord layer](#chord-layer)
- [Readable labels](#readable-labels)
- [Runtime API and events](#runtime-api-and-events)
- [Accessibility](#accessibility)
- [Performance tips](#performance-tips)

All examples assume this setup:

```js
import WestCoastCharts from 'west-coast-charts'
import premium from 'west-coast-charts/premium'

WestCoastCharts.use(premium)
```

## A first ring

The column ring takes the same data shape as a bar chart: `series` plus `xaxis.categories`, or
`{ x, y }` points.

```js
import WestCoastCharts from 'west-coast-charts'
import premium from 'west-coast-charts/premium'
WestCoastCharts.use(premium)

const hours = Array.from({ length: 24 }, (_, h) => `${String(h).padStart(2, '0')}:00`)
const requests = hours.map((_, h) => Math.round(300 + 250 * Math.sin(((h - 6) / 24) * 2 * Math.PI) + (h % 3) * 20))

const chart = new WestCoastCharts(document.querySelector('#chart'), {
  chart: { type: 'radialColumn', height: 420 },
  series: [{ name: 'Requests / h', data: requests }],
  xaxis: { categories: hours },
  plotOptions: {
    radialColumn: {
      valueAxis: { tickAmount: 4 },
      center: { label: 'Requests today' },
    },
  },
})
await chart.render()
```

What you get without further options: a spectrum fill by angle for a single series, a value scale with
grid rings, category labels that thin out to a readable step, the total in the centre (it shows the
hovered slot while you hover), a tooltip, keyboard navigation and a screen-reader summary.

## Readable density on small screens

Start with the outer ring and at most two inner layers. A 144-slot heat ring with
several nested layers can be useful in a large operational view and unreadable on
a phone. Keep a phone version explicit: for example show 24 hourly slots and one
service donut; retain the complete data in a separate table or detailed view.
Reducing a view must never silently drop source measurements or change their totals.

The renderer emits one `WCC_RADIAL_READABILITY` warning per chart when a layer ring
is thinner than 8 px or heat cells have less than 2 px of arc per slot. The warning
names the affected layers. It leaves every layer and value intact.
`plotOptions.radialColumn.readabilityWarnings: false` disables this developer
diagnostic when the density is a deliberate choice; it does not improve legibility.

Before: 144 time slots plus five inner layers at 390 px. Recommended compact view:
24 explicitly aggregated hourly slots plus one inner layer, with the aggregation
named in its label and the unaggregated data available separately.

### Compact layouts in the operational gallery

The dense gallery examples use the existing top-level `responsive` option at
600 px. These are explicit layout alternatives, not a switch that disables the
readability diagnostic. Their default compact configurations retain their source
time slots, rows and values:

- `quint-radial` and `orient-quint` share space by actual ring count instead of
  assigning the six budget rings a small fixed percentage. On a phone the centre
  and gaps shrink and the full legend moves below the dial. All 144 ten-minute
  slots, six budget values and both named three-row heatmap selections remain.
  Those three-row story selections were already explicit in the desktop example;
  they are not a phone aggregate of all six service/cluster rows. The optional
  **All 6 rows (wide view)** control requests every row and a taller wide view;
  it does not silently substitute the three-row selection on narrow screens.
- `matrix-mesh` retains all 48 service/window latency observations and the full
  eight-service calls matrix. In the compact layout the eight current budget
  percentages become one service-aligned heat band. Each current percentage is
  repeated unchanged over that service's six windows; this is a current snapshot,
  not six historical budget measurements. Percentages are never added together.
  Short outer service labels use the same identities as the full matrix headers
  and tooltip names.
- `goals-team` and `orient-goals` retain all seven teams and thirty days;
  `heat-week` retains all seven days × 24 hours and all five channels;
  `saas-ops-quad` retains all request slots, six budgets, six cluster costs and
  both traffic hierarchy levels. `chord-mesh` retains every service, call and
  ownership node, with a deliberate readable chord-arc width.

The compact layouts do not aggregate or discard source measurements. If an
application instead chooses hourly aggregation, it must define the statistic for
each metric (sum for counts, a deliberate maximum or weighted measure for rates),
rebuild links/markers for the new slots and label that choice. A label saying
"hourly" cannot make a subsampled ten-minute series an hourly total.

## Shape: angles, radii, bars

| Option (`plotOptions.radialColumn.*`) | Default | What it does |
| --- | --- | --- |
| `startAngle`, `endAngle` | `0`, `360` | Degrees, 0 = 12 o'clock, clockwise. A partial range such as `-120 … 120` makes a gauge-like arc; the chart is fitted to the arc. |
| `innerRadius` | `'auto'` | Where the bars start: px, `'40%'` or a 0–1 fraction of the outer radius. |
| `barPadding` | `'auto'` | Gap between slots (fraction of a slot). |
| `groupPadding` | `0.14` | Gap between bars of one slot when several series are grouped. |
| `stacked` | `chart.stacked` | Stack series outward (negative values grow inward). |
| `rounded` | `true` | Rounded bar ends. |
| `fill` | `{ type: 'auto' }` | `'spectrum'` (by angle), `'series'`, `'value'` (by magnitude) with your `stops`; `fade` darkens bars towards their base. |
| `track` | `{ show: false }` | A faint full-height slot behind each bar. |
| `valueAxis` | | `min`, `max`, `tickAmount`, `nice`, `rings`, `labels` (see [Readable labels](#readable-labels)). |
| `categoryLabels` | | `orientation` (`'auto'`, `'horizontal'`, `'radial'`), `step`, `maxCount`, `formatter`, `maxWidth`, `offset`. A formatter that returns `''` hides that label and frees its space. |
| `center` | | `show` (`'auto'` hides it in small holes), `label`, `value`, `formatter`, `hover`. |
| `legend` | | `show: 'auto'`, `position: 'auto' \| 'right' \| 'bottom'`. |

A half-ring with two stacked series and a value-coloured fill:

```js
import WestCoastCharts from 'west-coast-charts'
import premium from 'west-coast-charts/premium'
WestCoastCharts.use(premium)

const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']

await new WestCoastCharts(document.querySelector('#chart'), {
  chart: { type: 'radialColumn', height: 320, stacked: true },
  series: [
    { name: 'Successful builds', data: [42, 51, 47, 55, 60, 12, 9] },
    { name: 'Failed builds', data: [3, 6, 2, 4, 7, 1, 0] },
  ],
  xaxis: { categories: days },
  plotOptions: {
    radialColumn: {
      startAngle: -90,
      endAngle: 90,
      innerRadius: 0.45,
      categoryLabels: { orientation: 'horizontal' },
      center: { label: 'Builds this week' },
    },
  },
}).render()
```

## Layers

`plotOptions.radialColumn.layers` lists the inner rings, **innermost first**. Every layer can carry its
own data or borrow a series by name (`series: 'Mix'`); a borrowed series leaves the column ring.

| `type` | Data | Notes |
| --- | --- | --- |
| `pie` | numbers, `[name, value]` or `{ x, y, color }` | Fills to the centre when it is the first layer. |
| `donut` | as pie | One ring. |
| `nestedDonut` | `[{ name, data }]` or `number[][]` | One ring per entry; a category keeps its colour on every ring. |
| `sunburst` | `{ x, y, children }` nodes | One ring per depth. Children take tones of their parent. |
| `arc`, `radialBar` | `{ x, y, max }` | One progress ring per value over a track (`min`/`max` per layer, default 0–100). |
| `polarArea` | as pie | Equal angles, radius by value. |
| `heatmap` | `[{ name, data }]` or `number[][]`, `null` = no value | Rings of cells aligned with the bars; a colour ramp per layer (`scale: 'log'` spaces it by ratio). |
| `goals` | `{ x, y, target, color, label }` | Activity-style rings that lap past 100 %. See [Goal rings](#goal-rings-and-the-live-gauge). |
| `gauge` | `value`, `data: [n]` or `follow: '<series>'` | One arc with bands and the value in the centre. |
| `chord` | `[from, to, value]` flows, or `matrix` | Ribbons between groups. See [Chord layer](#chord-layer). |
| `incident` | `[{ from, to, label, keys }]` | Needs `west-coast-charts/layers/quint` loaded. A wedge across every ring for an incident window; see [Quint](https://westcoast-charts.com/docs/quint/index.md#incident-wedge-on-a-radial-column). |

Common layer options: `name`, `labels`, `thickness` (px, `'%'` or fraction; rings without one share the
rest), `gap`, `ringGap`, `padding`, `corner`, `colors`, `colorBy: 'spectrum'`, `legend`, `legendTitle`,
`ringLabels`, `trackColor`, `startAngle`, `endAngle`, `min`, `max`, `link`. Between the outermost layer
and the bars sits `layerGap` (10 px); the empty centre is `hole` (`'auto'`, px, % or fraction).

Colours continue through one palette across the column series and all layers, so two layers never
repeat a colour, and long legends stay distinguishable.

```js
import WestCoastCharts from 'west-coast-charts'
import premium from 'west-coast-charts/premium'
WestCoastCharts.use(premium)

const hours = Array.from({ length: 24 }, (_, h) => `${h}:00`)

await new WestCoastCharts(document.querySelector('#chart'), {
  chart: { type: 'radialColumn', height: 460 },
  series: [{ name: 'Orders / h', data: hours.map((_, h) => 40 + ((h * 37) % 90)) }],
  xaxis: { categories: hours },
  plotOptions: {
    radialColumn: {
      layers: [
        // innermost: share of orders per sales channel
        { type: 'donut', name: 'Channel', data: [['Web', 46], ['App', 38], ['Marketplace', 16]], legendTitle: 'Channels' },
        // around it: last week vs this week per region
        {
          type: 'nestedDonut',
          name: 'Regions',
          labels: ['DACH', 'Nordics', 'Benelux'],
          data: [
            { name: 'Last week', data: [52, 21, 27] },
            { name: 'This week', data: [49, 26, 25] },
          ],
          legendTitle: 'Regions',
        },
      ],
      legend: { position: 'right' },
    },
  },
}).render()
```

## Linking layers to the bars

Layers and bars can mean independent things (a channel mix inside, hourly load outside) or connected
things (day parts inside, the hours they cover outside). `plotOptions.radialColumn.link` declares the
relation for all layers; a layer's own `link` overrides it, `link: false` switches it off.

| `mode` | A segment is linked to … |
| --- | --- |
| `'none'` | nothing |
| `'category'` | the categories with the same name as the segment or one of its children |
| `'angle'` | the bars whose slot centre lies inside the segment's angle |
| `'map'` | the categories you name: `map: { segment: [categories] }`, a function, or the datum's own `link: [...]` (indices or names) |
| `'auto'` (default) | datum `link`, else category names, else angle |

More link options: `interaction: 'highlight' | 'filter' | 'both'` (filter fades everything unlinked
on click), `bidirectional` (default `true`: hovering a bar lights the segments that link to it),
`align` (each segment spans exactly the angle of its linked bars), `indicator` (default `true`: a bracket
on the rim over each run of linked bars) and `unit` (how a category is called in announcements, for
example `'hours'`).

Hovering a segment lights it and its bars and dims the rest. Clicking makes the selection sticky; click
again, click empty space or press Escape to clear it. A segment with `values: [one per category]`
replaces the bars with its own values while selected, which is handy for "show me this channel's day".

```js
import WestCoastCharts from 'west-coast-charts'
import premium from 'west-coast-charts/premium'
WestCoastCharts.use(premium)

const hours = Array.from({ length: 24 }, (_, h) => `${h}:00`)
const range = (a, b) => Array.from({ length: b - a }, (_, i) => a + i)

const chart = new WestCoastCharts(document.querySelector('#chart'), {
  chart: { type: 'radialColumn', height: 440 },
  series: [{ name: 'Support tickets', data: hours.map((_, h) => (h >= 8 && h < 18 ? 20 + (h % 5) * 6 : 4 + (h % 3))) }],
  xaxis: { categories: hours },
  plotOptions: {
    radialColumn: {
      link: { mode: 'map', interaction: 'both', align: true, unit: 'hours' },
      layers: [{
        type: 'donut',
        name: 'Shift',
        data: [
          { x: 'Night shift', y: 8, link: [...range(0, 6), ...range(22, 24)] },
          { x: 'Day shift', y: 10, link: range(6, 16) },
          { x: 'Late shift', y: 6, link: range(16, 22) },
        ],
      }],
    },
  },
})
await chart.render()

// select a segment from code, read what it links to, clear again
const api = premium.api(chart)
api.select('Day shift')
console.log(api.selected(), api.links('Day shift'))
api.clearSelection()
```

## Semantic keys, ring labels and rim markers

- **`keys`** name what a mark *is* (`{ x: 'checkout', y: 12, keys: ['checkout'] }`). Hovering a mark with
  keys lights the marks in other layers that share a key, rather than everything in the same angle. A
  sunburst parent carries the union of its children's keys. `link` still connects to the bars.
- **`ringLabels: true`** on a layer writes curved labels along each segment, turned to read upright in
  the lower half, with ink chosen against the segment colour; labels that do not fit are dropped.
  Goal rings (labels on by default) too thin for a label inside, e.g. on a phone, get their names
  outside the stack instead: curved on a track in the free gap (at least 13 px), each at its ring's
  end with a leader line, pushed along where two would touch; a name that would move more than about
  57° is left to the legend.
- **`legendTitle`** groups a right-hand legend under one small header per layer.
- **`markers`** put a glyph just outside the bars at a slot: `[{ at, label, kind, color, shape }]` with
  `at` = slot index or category name and `shape` `'dot'` or `'diamond'`. The slot tooltip lists the
  markers; useful for deploys, incidents or releases.

```js
import WestCoastCharts from 'west-coast-charts'
import premium from 'west-coast-charts/premium'
WestCoastCharts.use(premium)

const slots = Array.from({ length: 48 }, (_, i) => `${String(Math.floor(i / 2)).padStart(2, '0')}:${i % 2 ? '30' : '00'}`)

await new WestCoastCharts(document.querySelector('#chart'), {
  chart: { type: 'radialColumn', height: 480 },
  series: [{ name: 'Requests', data: slots.map((_, i) => 200 + ((i * 53) % 180)) }],
  xaxis: { categories: slots },
  plotOptions: {
    radialColumn: {
      categoryLabels: { formatter: (c, { index }) => (index % 6 === 0 ? c : '') }, // every 3 hours
      markers: [
        { at: 19, label: 'Deploy api v2.14', kind: 'Deploy', shape: 'diamond' },
        { at: '15:30', label: 'Incident INC-311', kind: 'Incident', color: '#e11d48' },
      ],
      layers: [{
        type: 'sunburst',
        name: 'Services',
        ringLabels: true,
        legendTitle: 'Services',
        data: [
          { x: 'EU', children: [
            { x: 'checkout', y: 34, keys: ['checkout'] },
            { x: 'search', y: 22, keys: ['search'] },
          ] },
          { x: 'US', children: [
            { x: 'checkout', y: 28, keys: ['checkout'] },
            { x: 'search', y: 16, keys: ['search'] },
          ] },
        ],
      }, {
        type: 'donut',
        name: 'Cost',
        legendTitle: 'Cost per service',
        data: [
          { x: 'checkout', y: 61, keys: ['checkout'] },
          { x: 'search', y: 39, keys: ['search'] },
        ],
      }],
    },
  },
}).render()
```

Hovering "checkout" in the sunburst now also lights "checkout" in the cost ring, because both carry the
key `checkout`.

## Encoding extra metrics (colour, delta)

Besides the bar length, `plotOptions.radialColumn.encode` can bind more visual channels to their own
metrics. All of them are off until you configure them. Values come from the column datum (plain fields
or `meta`) or from `values: [one per category]`.

| Channel | Option | Shows |
| --- | --- | --- |
| Bar length | the series value, or `encode.length: '<field>'` | the main quantity |
| Bar colour | `encode.color: '<field>'` or `{ field, name, unit, scale: 'sequential' \| 'diverging', domain, mid, stops, reverse, legend }` | a second metric such as an error rate |
| Outer delta | `encode.delta: { field, name, unit, style: 'band' \| 'ticks' \| 'bars', colors: { up, down }, max }` | change against a previous period |
| Angle | `encode.angle: '<name>'` | names the dimension for the reading guide |
| Guide | `encode.guide: true` or `{ open: true \| 'always' }` | a `?` button that explains every channel |

A key under the ring shows the colour scale and the largest delta (`legend: false` per channel hides
it). Tooltips, keyboard labels, announcements and the data table list every channel by name.

```js
import WestCoastCharts from 'west-coast-charts'
import premium from 'west-coast-charts/premium'
WestCoastCharts.use(premium)

const hours = Array.from({ length: 24 }, (_, h) => `${h}:00`)
const data = hours.map((x, h) => ({
  x,
  y: 300 + ((h * 71) % 260),             // requests (bar length)
  errorRate: +(0.4 + ((h * 13) % 30) / 10).toFixed(1), // % (bar colour)
  vsLastWeek: ((h * 29) % 41) - 20,      // % change (outer band)
}))

const chart = new WestCoastCharts(document.querySelector('#chart'), {
  chart: { type: 'radialColumn', height: 460 },
  series: [{ name: 'Requests', data }],
  plotOptions: {
    radialColumn: {
      encode: {
        angle: 'Hour of day',
        color: { field: 'errorRate', name: 'Error rate', unit: '%', scale: 'sequential' },
        delta: { field: 'vsLastWeek', name: 'vs last week', unit: '%', style: 'band' },
        guide: true,
      },
    },
  },
})
await chart.render()

const api = premium.api(chart)
console.log(api.describe('13:00')) // the slot as one line: hour, requests, error rate, change
console.log(api.channels())        // [channel, meaning] pairs, as listed in the guide
```

## Goal rings and the live gauge

**Goals** draw one ring per datum, innermost first, in the style of activity rings. A value past its
`target` (default 100) laps over itself in a shifted tone (`maxLaps`, default 2). Each ring may set its
own `color`, `trackColor` and `label`; labels are drawn along the ring, so keep them short.

**Gauge** shows one value on an arc and in the centre. With `follow: '<series name>'` it always shows the
newest value of that column series, marks the newest slot with a "now" tick (`now: false` hides it), and
the bars keep the history. `bands` colour the arc by value, `formatter` formats the centre.

```js
import WestCoastCharts from 'west-coast-charts'
import premium from 'west-coast-charts/premium'
WestCoastCharts.use(premium)

const minutes = Array.from({ length: 30 }, (_, i) => `-${29 - i} min`)
let load = minutes.map((_, i) => 35 + ((i * 17) % 40))

const chart = new WestCoastCharts(document.querySelector('#chart'), {
  chart: { type: 'radialColumn', height: 420 },
  series: [{ name: 'CPU load', data: load }],
  xaxis: { categories: minutes },
  plotOptions: {
    radialColumn: {
      startAngle: -140,
      endAngle: 140,
      valueAxis: { min: 0, max: 100 },
      layers: [
        {
          type: 'goals',
          name: 'SLOs',
          data: [
            { x: 'Availability', y: 99.2, target: 99.9, label: 'Avail 99.2%' },
            { x: 'Latency', y: 112, target: 100, label: 'p95 ok' },
          ],
        },
        {
          type: 'gauge',
          name: 'CPU now',
          follow: 'CPU load',
          formatter: (v) => `${Math.round(v)} %`,
          bands: [{ to: 60, color: '#16a34a' }, { to: 80, color: '#d97706' }, { to: 100, color: '#dc2626' }],
        },
      ],
    },
  },
})
await chart.render()

// streaming: shift the window and update; bars and gauge animate together
load = [...load.slice(1), 71]
await chart.updateSeries([{ name: 'CPU load', data: load }])
```

## Chord layer

A `chord` layer draws group arcs on a ring with ribbons between them, using the same layout as
`chart.type: 'chord'`. Put it innermost: layers inside a chord are drawn over its ribbons.

- Data: flows `[from, to, value]` or `{ from, to, value }`, or `matrix` (rows = from) with `labels`.
- `groups: [{ id, name, color, link }]` fixes order, names, colours and the linked bars; `sort: 'asc' | 'desc'`
  orders by size; `directed: true` gives each end of a flow its own share.
- `ringWidth` sets the arc thickness; `ribbon: { colorMode: 'source' | 'target' | 'gradient', opacity, padding }`.
- With `align: true` (in the link options) every group spans exactly its linked bars.
- Hovering a ribbon lights both groups and the bars of both ends; the tooltip shows both directions.

```js
import WestCoastCharts from 'west-coast-charts'
import premium from 'west-coast-charts/premium'
WestCoastCharts.use(premium)

const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
const categories = ['EU', 'US', 'APAC'].flatMap((r) => months.map((m) => `${r} ${m}`))

await new WestCoastCharts(document.querySelector('#chart'), {
  chart: { type: 'radialColumn', height: 480 },
  series: [{ name: 'Data transfer (TB)', data: categories.map((_, i) => 10 + ((i * 19) % 24)) }],
  xaxis: { categories },
  plotOptions: {
    radialColumn: {
      categoryLabels: { formatter: (c) => (c.endsWith('Jan') ? c.split(' ')[0] : '') },
      link: { mode: 'map', align: true },
      layers: [{
        type: 'chord',
        name: 'Replication',
        ringWidth: 12,
        groups: [
          { id: 'eu', name: 'EU', link: categories.filter((c) => c.startsWith('EU')) },
          { id: 'us', name: 'US', link: categories.filter((c) => c.startsWith('US')) },
          { id: 'apac', name: 'APAC', link: categories.filter((c) => c.startsWith('APAC')) },
        ],
        data: [['eu', 'us', 42], ['us', 'eu', 31], ['us', 'apac', 18], ['apac', 'eu', 12]],
        directed: true,
        ribbon: { colorMode: 'gradient', opacity: 0.55 },
      }],
    },
  },
}).render()
```

## Readable labels

Value-scale labels sit on top of the bars, often over several colours at once.
`valueAxis.labels.readability` decides how they stay legible; it defaults to `'auto'`.

| Setting | Premium alone | With `west-coast-charts/features/readability` |
| --- | --- | --- |
| `'auto'` (default) | a surface-coloured pill behind labels that sit on bars | picks per label: untouched off the bars, contrasting ink over one fill, a pill over mixed fills |
| `'pill'` | pill | pill with all options (`pill: { fill, opacity, radius, padding, stroke, offset }`) |
| `'hide'` | hides labels that sit on bars | same |
| `'halo'`, `shadow`, `contrast`, `minRatio`, `each` | not available (ignored) | available |
| `'none'` / `false` | off | off |

Two more ways to keep the scale clear:

- `valueAxis.labels.angle`: put the scale at a fixed angle (degrees, 0 = 12 o'clock) or `'auto'`, the
  slot boundary with the shortest bars on both sides. `pinAngle` (default `true`) keeps the auto angle
  across updates so the scale does not wander.
- `center.readability`: the same settings for the centre value and label (default `'none'`).

```js
import WestCoastCharts from 'west-coast-charts'
import premium from 'west-coast-charts/premium'
import readability from 'west-coast-charts/features/readability'
WestCoastCharts.use(premium).use(readability)

const hours = Array.from({ length: 24 }, (_, h) => `${h}:00`)

await new WestCoastCharts(document.querySelector('#chart'), {
  chart: { type: 'radialColumn', height: 400 },
  series: [{ name: 'Throughput', data: hours.map((_, h) => 120 + ((h * 41) % 200)) }],
  xaxis: { categories: hours },
  plotOptions: {
    radialColumn: {
      valueAxis: {
        labels: {
          angle: 'auto',
          readability: { mode: 'auto', contrast: true, minRatio: 4.5 },
        },
      },
      center: { label: 'Throughput', readability: 'halo' },
    },
  },
}).render()
```

## Runtime API and events

`premium.api(chart)` returns the chart's runtime API (with script tags: `WestCoastChartsPremium.api(chart)`):

| Method | Does |
| --- | --- |
| `describe(category?)` | without an argument: the chart summary sentence; with a category: one line with every [encoded channel](#encoding-extra-metrics-colour-delta) of that slot (`null` when no `encode` is configured) |
| `dataTable()` | the rows of the accessible data table |
| `highlightCategory(name)` / `clearHighlight()` | light one slot from code |
| `toggle(id)` | hide or show a series (`'s:<name>'`) or a segment (`'c:<name>'`) |
| `layers()` | `[{ index, type, name, rings, radii, link }]` |
| `links(segment)` | the categories a segment links to |
| `select(nameOrPath)` / `selected()` / `clearSelection()` | sticky selection from code |
| `channels()` | the bound encoding channels as `[channel, meaning]` pairs |

Events (`chart.events`): `dataPointSelection(e, chart, info)` receives
`{ seriesIndex, dataPointIndex, category, values }` for a slot, and `{ layerIndex, layer, segment }` for a
layer segment, plus `linked` and `selected` when links are active. `legendClick(e, chart, { seriesName, hidden })`
fires for legend toggles.

```js
import WestCoastCharts from 'west-coast-charts'
import premium from 'west-coast-charts/premium'
WestCoastCharts.use(premium)

const chart = new WestCoastCharts(document.querySelector('#chart'), {
  chart: {
    type: 'radialColumn',
    height: 360,
    events: {
      dataPointSelection: (e, ctx, info) => console.log('selected', info.category ?? info.segment?.name),
    },
  },
  series: [{ name: 'Alerts', data: [3, 7, 2, 9, 4, 6, 1, 5] }],
  xaxis: { categories: ['db', 'api', 'web', 'queue', 'cache', 'auth', 'cdn', 'search'] },
  plotOptions: { radialColumn: { layers: [{ type: 'pie', name: 'Severity', data: [['critical', 4], ['warning', 21], ['info', 12]], link: false }] } },
})
await chart.render()

const api = premium.api(chart)
console.log(api.describe())          // summary sentence
console.log(api.layers().map((l) => `${l.type}:${l.name}`))
api.highlightCategory('api')
api.clearHighlight()
```

## Accessibility

- Keyboard: Tab focuses the chart; arrow keys move through the slots of the column ring and then the
  segments of each layer; PageDown and PageUp jump between rings; Enter selects; Escape clears.
- Each slot reads as one sentence including every encoded channel, for example
  "13:00 · 412 Requests · 3.4% Error rate · +12% vs last week".
- Linked segments announce what they cover ("18 of 72 time slots, peak 13:20").
- The hidden data table lists the categories with one column per series and channel, followed by one
  row per layer segment.
- Reduced-motion preferences skip the animations.

## Performance tips

- Hit testing is geometric, so hundreds of thin bars need no per-element listeners. Above 150 bars the
  CSS highlight transitions are switched off automatically.
- Prefer `updateSeries` / `updateOptions` over destroying and re-creating: updates morph from what is on
  screen, even mid-animation.
- For streaming, shift a fixed-length window as in the gauge example rather than growing the data.

The complete layout of every option is in the [premium options reference](https://westcoast-charts.com/docs/reference/premium-options/index.md).
