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.
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
- Data model
- Lanes
- The focus lens
- Interaction and keyboard
- describe(), export and accessibility
- API
Loading
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 typesWith 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.
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
fromandto(default: the first incident ±pad, default max(3 slots, 30 min)) the time axis runsfactortimes 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:
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/toare category names or slot indices;tois 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 layername, 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 agapof 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.