Quint timeline and convergence

Shared-time-axis lanes for operations data, crossfilter, incidents and deployment markers. The labelled convergence lens expands time around an incident without changing values; loading, interaction, describe() and export.

View .md Agent setup

quintTimeline and quintConvergence put up to five kinds of lanes on one shared time axis: request columns coloured by error rate, heatmap strips, error-budget bands, lines and stacked areas. They are meant for an operations day: what happened, where, and what changed right before.

  • Quint timeline: the lanes with one shared crosshair, incident bands across all lanes, deploy and pipeline markers, and a crossfilter: click a service, cluster or time slot to pin it, Esc clears.
  • Quint convergence: the same lanes plus a visibly labelled focus lens that magnifies the time axis around the incident, and one callout per lane with its value at the incident. The lens changes only the time axis, never a value.

Both types come from one premium type module, west-coast-charts/layers/quint (about 14 kB gzip). It is loaded only by pages that use it and is not part of the premium bundle.

Loading

js
import WestCoastCharts from 'west-coast-charts'
import premium from 'west-coast-charts/premium'
import quint from 'west-coast-charts/layers/quint'
WestCoastCharts.use([premium, quint]) // any order; one use() registers both types

With script tags, load dist/layers/quint.min.js after the core and the premium script; it installs itself.

Without an explicit import the module is loaded on demand, like the radial and network layer modules: premium knows the names quintTimeline and quintConvergence, draws a placeholder ("needs west-coast-charts/layers/quint"), loads the module (bundlers: import() of layers/quint.esm.js next to premium, a lazy chunk; CommonJS: require; script tags: a <script> for layers/quint.min.js from premium's directory or premium.layerBase) and redraws the chart. One load registers both types and the incident layer. With premium.autoload = false (strict CSP, offline bundles) the placeholder stays and the console says once: "quintTimeline" needs west-coast-charts/layers/quint (autoload is off: use() it); premium.setLayerLoader(name => import(...)) replaces the loader. Without premium at all, the core error says chart.type "quintTimeline" is a premium type and how to load premium.

Licensing is as for every premium type: without a key the chart renders fully and shows the small "unlicensed" line on public hosts (Premium and licensing).

Data model

The time axis is a sequence of equal slots: plotOptions.quint.time = { start, step } (ISO string or ms, slot length in ms). Every series is a value per slot and names its lane in lane (not type). keys say what a series is (service, cluster, region); a click on anything with keys lights everything that shares one, in every lane.

js
import WestCoastCharts from 'west-coast-charts'
import premium from 'west-coast-charts/premium'
import quint from 'west-coast-charts/layers/quint'
WestCoastCharts.use([premium, quint])

// 24 hourly slots of an invented service
const hours = Array.from({ length: 24 }, (_, i) => i)
const wave = (base, amp) => hours.map((h) => Math.round(base + amp * Math.sin(((h - 6) / 24) * 2 * Math.PI)))

await new WestCoastCharts(document.querySelector('#chart'), {
  chart: { type: 'quintConvergence', height: 'auto' },
  series: [
    { name: 'Requests', lane: 'req', data: wave(52000, 20000),
      errorRate: hours.map((h) => (h === 14 ? 6.2 : h === 15 ? 2.4 : 0.3)) },
    { name: 'api-1', lane: 'load', keys: ['api-1'], data: wave(55, 20) },
    { name: 'api-2', lane: 'load', keys: ['api-2'], data: hours.map((h, i) => (h === 14 ? 97 : wave(48, 18)[i])) },
    { name: 'Orders', lane: 'p95', keys: ['Orders', 'api-2'], data: hours.map((h) => (h === 14 ? 910 : 160)) },
  ],
  plotOptions: {
    quint: {
      time: { start: '2026-03-02T00:00:00Z', step: 3600000 },
      lanes: [
        { id: 'req', kind: 'events', title: 'Requests', unit: 'requests / h' },
        { id: 'load', kind: 'heatmap', title: 'CPU load', unit: '%' },
        { id: 'p95', kind: 'lines', title: 'p95 latency', unit: 'ms' },
      ],
      markers: [{ time: '2026-03-02T13:40:00Z', kind: 'deploy', label: 'orders v7.2', keys: ['Orders'] }],
      incidents: [{ from: '2026-03-02T14:00:00Z', to: '2026-03-02T15:00:00Z', label: 'Orders timeouts', keys: ['Orders', 'api-2'] }],
    },
  },
}).render()

Lanes

plotOptions.quint.lanes[], top to bottom: { id, kind, title, unit, aggregate, color, max, height }.

kind Draws Series fields Default aggregate
events one column per slot (a count per slot); colour = error rate in three classes; a dashed step line for the typical day; ◆ deploys and ● pipeline runs on top data, errorRate (% per slot), typical sum
heatmap one strip per series, one cell per slot, with a scale key data mean
budget one band per series: error budget left in %, coloured ≥ 50 / ≥ 20 / below; the row label reads "availability · p95 latency" data (% left), availability (%), p95 (ms) last
lines one line per series data mean
stacked stacked areas data mean

The unit belongs to the lane: it is shown in the lane title ("Requests · requests / 10 min"), in every tooltip row, in the CSV header and in describe(). Give a rate its period ('requests / 10 min', '€ / h', 'requests / s') so that a reader never has to guess. aggregate says how a lane adds up over the day in describe() and the keyboard items: a count per slot sums (and the sum is reported without the "per slot" part: "total 2,831,640 requests"), levels and rates average, a budget is read at the end. The error rate of an events lane is always averaged weighted by requests.

Without height, lanes share the free height (events 1, lines 1.25, stacked 1); heatmap and budget rows have a fixed pitch. chart.height: 'auto' gives the natural height for the lanes.

Markers [{ time, kind: 'deploy' | 'pipeline', label, keys, emphasis }] sit on the events lane. The deploy within 2 h before the first incident that shares one of its keys is drawn in the warning colour and labelled, and describe() names it ("checkout v2.41 at 18:30 right before the first incident"). Incidents [{ from, to, label, keys }] are bands across all lanes, with a chip in the header.

The focus lens

plotOptions.quint.lens = { enabled, factor, from, to, pad }, on by default for quintConvergence.

  • Between from and to (default: the first incident ± pad, default max(3 slots, 30 min)) the time axis runs factor times faster than on the rest of the axis (1.5–12, default 4). The warp is piecewise linear and monotone: order and adjacency stay, every slot keeps its own column.
  • Values are never warped. Column heights, line values, heatmap colours and budget levels are the same with the lens on or off (the unit tests compare them).
  • The lens is always labelled: a header chip ("Lens ×4 · 18:10–19:50", a toggle button), dashed edges with a bracket and bold edge times on the axis, a tooltip footer inside it ("inside the focus lens: time ×4, values unchanged") and a sentence in describe(), for example "Focus lens: time axis magnified ×4 between 18:10 and 19:50, relative to the rest of the axis (against a linear axis: lens ×3.31, rest ×0.83); values are not changed."
  • Click the chip or press L (when the lens is enabled) to switch to a linear axis ("Focus lens off: linear time axis"); premium.api(chart).lens(true | false) does the same from code.

quintConvergence is quintTimeline with lens.enabled and focus.enabled on by default. Either type can switch either option, so a timeline can get a lens and a convergence chart can go linear.

Interaction and keyboard

  • One crosshair across all lanes; its tooltip lists every lane at that slot, with units.
  • Click a row label, line, band, strip or marker: lights everything that shares its keys in every lane (the rest fades). Click a time slot elsewhere: pins it. Esc (anywhere while the chart is the last one used) or a click on empty space clears.
  • Keyboard: Tab to the chart, arrow keys move through the lane rows, markers and incidents, Enter selects, Esc clears, L toggles the lens.
  • Events: quintSelect ({ keys }), quintPin ({ time, slot }), quintLens ({ enabled }).

describe(), export and accessibility

describe() (and chart.describe() of the agent module, used by the CLI and MCP tools) gives one sentence per lane with unit and aggregation, peaks with times, the request-weighted error rate, markers, incidents, the lens sentence and, in convergence mode, the callouts. Times carry their zone: "from 2026-10-24 00:00 UTC to 2026-10-25 00:00 UTC in 10 min slots (times are slot starts)".

The CSV export and the accessible table are wide: one row per slot, one column per lane row, the unit in every header ("Cost per cluster · eu-k8s-2 (€ / h)").

Below 560 px the layout turns compact: shorter row names, the budget detail under the name, fewer time labels (never two for the same instant), and the legend wraps. Light and dark follow the theme.

Incident wedge on a radial column

The same module adds a radialColumn layer type, incident, for the clock view of this data (the "Quint semantics on a clock" showcase card). Like the types it is autoloaded when a chart asks for it (the other rings draw first, then the chart redraws); use() the module, or load dist/layers/quint.min.js on the page, to have it in the first frame:

js
layers: [
  // ... inner rings, innermost first
  { type: 'incident', name: 'Incident', gap: 0, data: [{ from: '18:40', to: '19:10', label: 'EU checkout errors', keys: ['Checkout', 'eu-k8s-2'] }] },
]
  • from / to are category names or slot indices; to is inclusive, so '19:10' in 10-min slots ends the window at 19:20. The label reads "Incident 18:40–19:20 · EU checkout errors" (on phones "Incident / 18:40–19:20").
  • The wedge runs from the centre to the rim across every ring and takes no ring space of its own. Hovering inside it picks the slot under the pointer, so the linked bars, heatmap cells and segments light up together; the wedge is emphasised while a slot of the window is hovered or pinned (click, Esc clears).
  • ringTitles (default true) names the heatmap rings with their full layer name, unit included, as curved text in the free gap outside each ring at 12 o'clock, so no cell is covered. Give each heatmap layer a gap of about 12–14 px for it; a ring without a gap of at least 10 px gets no title. It also adds each heatmap ring's colour scale (name, ramp, min / ticks / max; 1-2-5 ticks on a log scale) to the key strip under the ring.
  • A heatmap layer with scale: 'log' spaces its colour ramp by ratio, so a few slow services or a spike do not leave every other cell at the dark end.
  • describe(), the data table and info() list each window with its slot count. Values are never changed.

API

premium.api(chart) of a quint chart adds lens(on?), pin(time | null), select(keys | null), clear(), selection() and ticks(); see the premium options reference.

WestCoast Charts 1.1.5 · generated from the library guide at commit 95f093ba. Code examples in the guide are executed by the library's docs check.