API · Options
Options & series
On this page
Hosted demos for advanced series: step-line · impulse · error-bars · band-range · stacked-mountain · heatmap-spectrogram · candlestick-streaming
Chart configuration. Full types: types.ts.
ChartGPUOptions
theme'dark' | 'light'orThemeConfigDataPointtuple[x, y, size?]or object{ x, y, size? }autoScroll?booleanwhentrue,appendData()keeps visible x-range anchored to newest data (only when data zoom enabled andxAxis.min/maxunset). Demo:live-streaming/. Disables sticky X headroom so FIFO/maxPointssliding windows track the retained data min/max (sticky would freeze historical xMin and compress the waveform into a thin strip on the right).antialias?: boolean(defaulttrue): main + overlay GPU passes use 4× MSAA whentrue, orsampleCount: 1whenfalse(lower fill-rate / memory for multi-chart dashboards). WebGPU portable multisample counts are only 1 or 4. Create-onlytakes effect atChartGPU.create/ coordinator construction; changing viasetOptiondoes not rebuild MSAA pipelines or the texture manager. Dispose and recreate the chart to change.devicePixelRatio?numbercanvas backing-store pixel ratio. Create-time policy (setOptioncannot change it): omitted → everyresize()re-reads livewindow.devicePixelRatio(page zoom); explicit finite> 0→ frozen for buffer + text overlays for the chart lifetime. Set to1on multi-chart dashboards to cap GPU fill rate. Buffer size ismax(1, round(clientWidth|Height × dpr))(layout CSS pixels), notgetBoundingClientRect()(visual size under CSS zoom). Dispose and recreate to change policy.
Annotations
annotations?: ReadonlyArray<AnnotationConfig>: overlays (lineX, lineY, point, text).position.space: 'plot'uses fractions [0,1] for x/y. Full guide: Annotations API. Authoring:annotation-authoring/.
Series Configuration
SeriesType'line' | 'area' | 'bar' | 'scatter' | 'pie' | 'candlestick' | 'ohlc' | 'heatmap' | 'band' | 'errorBar' | 'impulse' | 'pointCloud3d' | 'surface3d'Seetypes.ts.StepMode'before' | 'middle' | 'after'digital / step connection for line and area (not a series type). Booleanstep: true≡'after'.coordinateSystem'cartesian2d'(default) |'cartesian3d'. 3D charts use a separate depth + camera path; onlypointCloud3d/surface3dare valid there. Full 3D API: 3D charts.- Sampling (cartesian)
sampling?: 'none' | 'lttb' | 'average' | 'max' | 'min',samplingThreshold?: number(default 5000). Zoom-aware resampling when data zoom enabled. On the GPU decimation path (lttb/min/max, gap-free lines), effective bucket count is also capped by plot width (targetBuckets = min(samplingThreshold, max(128, 2×plotWidthDevicePx), N)) — intentional screen-space LOD on narrow multi-chart slots; see performance — GPU targetBuckets. visible?booleanhide series from rendering and interaction. Legend toggle updates both.- Data identity / in-place mutationChartGPU treats series
databy reference identity for hot paths (setOptionresolve, content hashing, GPU upload skip, area geometry cache). Mutating point values under a stable array / columns object without replacing the reference may not be detected. Prefer a new array (orappendData(...)) when content changes. Axes-only / presentation-only updates that re-pass the samedatareference are intentionally O(1) on the data path. - Series array identity reuse (axes-only
setOption)When consecutivesetOptioncalls re-pass the same series config object identities (outerseriesarray may be the same or a new array wrapping the same elements) and theme/palette refs are unchanged, ChartGPU may reuse the entire previous resolved series array without re-walking each series. Treat the outerseriesarray and each series config object as immutable for this path: to change data, color, visibility, style, or candlestickpriceLabel, pass a new series element object (or a newseriesarray of new objects). Index assignment (series[i] = {...}) is detected; mutating properties under a stable element object (e.g.series[i].data = newData,series[i].priceLabel = false) is not detected on the full-array path. - Full rewrite (
setOptionwith a newdataarray every frame)When the data reference changes, ChartGPU uses an O(1) content stamp (not a full float hash) for dirty tracking. GPU buffers are reused when capacity fits; pack goes into retained staging. Tooltip off skips ChartGPU hit-test columnar rebuild untilhitTest()or tooltip re-enable (dual-store relief). The coordinator may keep your data array by reference untilappendData— append always copies into owned columns and never mutates caller{ x, y }arrays. - Y-only DataStore rewrite (lines)When length is stable and every x matches the previous pack, only y floats are rewritten in CPU staging; GPU uploads a dense N×4 y channel and a compute pass rewrites y lanes into interleaved storage (not a full N×8
writeBuffer). Length change, any x change, any non-finite x in the series or staging (including stable null gaps), or modular ring layout force a full interleaved rewrite. Unsorted Brownian xy (group 3) never hits this path. - Equal-N y-only scatter (const radius)When x is stable (e.g.
x = i), scatter uses dual x/y instance buffers and uploads only the y channel (N×4). Index-sorted equal-N rewrites withsampling: 'lttb'(matching prior sampling + threshold) underperformance.lod: 'auto're-bind y at the frozen prior LTTB index set in O(k) instead of full O(N) LTTB — approximate hold: newly emerging y extrema between retained indices do not appear until full LTTB resumes (length, x change, sampling/threshold change, orperformance.lod: 'strict'). Understrict, plain'lttb'always full-recomputes on y change. min/max/average always re-sample. Brownian xy updates stay on the full path. - Dense scatter draw (const radius)When points-per-plot-pixel is high and
performance.lod !== 'strict', drawn marker radius may compact toward ~1 device pixel (draw-only; does not changesamplingor uploaded point count). Low-density charts andstrictkeep fullsymbolSize. - Dense line draw (hairline)When a line series has ≥ ~25 000 draw / displayed points (raw stroke length, or GPU-decimation bucket /
pointCountOverrideinstance count) andperformance.lod !== 'strict', ChartGPU switches draw only to a native 1 device-pxline-listhairline (denseHairline). Those segments are drawn in a post-resolve sampleCount:1 pass (not under main 4× MSAA overdraw). SerieslineStyle.widthconfig is unchanged. Mid-N GPU-decimated series (e.g. 50k–500k raw LTTB’d to a few thousand buckets) keep full AA quads + configured width when draw N is low. Multi‑M residency gate: when GPU decimation keeps raw residency ≥ ~1 000 000 points, hairline policy may use that raw count even though draw instance count is the smaller bucket N — multi‑M FIFO / streaming exits 4× MSAA AA-quad fill while still drawing the LTTB sample. Applies to all high-N lines under auto LOD, not only unsorted full rewrites. - Multi-series dense hairline (total-segment budget)When the chart has ≥ 2 visible line series and the approximate total segment count
visibleLineSeriesCount × max(0, pointCount − 1)is ≥ ~500 000 (equal-N approximation using each series' own point count) and lod is auto, those lines also enterdenseHairlineeven if each series is below the 25k per-series threshold. Example: 1000 series × 1000 points hairlines; 500 × 500 stays standard AA. Visual tradeoff: multi-series stress charts draw 1 device-px strokes instead of thick width-2 AA quads. Does not change sampling or uploaded data. - Dense multi-M draw stride (mountain fill + multi-M hairline stroke)When N ≥ ~1 000 000 and drawn segments would exceed
max(8192, 4× plotWidthDevicePx)and lod is auto, ChartGPU caps drawn consecutive-segment instances via an index stride (shareddenseDrawLodpolicy for area fill and dense hairline stroke). GPU residency andsamplingstay unchanged — undersampling: 'none'the full raw series remains resident. Series below 1M (including 250k–500k demos) keep full N−1 geometry under auto. At multi-M this may draw ≪ N−1 segments; useperformance.lod: 'strict'for full geometry. Dense mountain fill may also defer to sampleCount:1 (post-resolve / dense-only direct path when no annotations or pointer overlays). performance.lod'auto'(default) enables dense hairline / scatter radius compaction / mountain fill+stroke draw-stride and approximate equal-N LTTB index freeze;'strict'always honors configured line width and marker size, full N−1 mountain geometry, and full-recomputes LTTB on equal-N y updates. See Performance guide.- Main scene MSAA (library-wide)Main series pass and overlay UI both use 4× MSAA (
MAIN_SCENE_MSAA_SAMPLE_COUNT/ANNOTATION_OVERLAY_MSAA_SAMPLE_COUNT) by default, or 1× whenantialias: falseis passed at create. Dense hairline lines additionally use a legal sampleCount:1 pass after main resolve (see library Internals on GitHub). WebGPU only allows portable multisample counts of 1 or 4 —sampleCount: 2is invalid and will fail validation (Invalid CommandBuffer).antialiasis create-only.devicePixelRatiois a create-time policy (explicit freezes buffer + overlays; omitted tracks live window DPR on resize) — see top of this page.
CandlestickSeriesConfig
DataOHLCDataPoint— tuple[timestamp, open, close, low, high]or object.style?: 'classic' | 'hollow'.sampling?: 'none' | 'ohlc'for bucket aggregation. Body-only hit-testing. Demos:candlestick/,candlestick-streaming/.priceLabel?boolean | CandlestickPriceLabelConfigexchange-style last-price badge (DOM) and optional horizontal price line (GPU) on the series Y-axis rail. v1: one badge + one line per chart — first visible candlestick or ohlc series with resolvedpriceLabel.showwins.Sugartrue/falseforce on/off with field defaults; object form enables unlessshow: false.undefined(default): auto-enables when the chart is finance-primary (series[0].type === 'candlestick'or'ohlc'); off when neither is first. See Migration (P1 default badge) under Axis Configuration.Full options (
CandlestickPriceLabelConfig):Field Default Notes show?truewhen object form; else sugar / autoHard off with falseorpriceLabel: falseshowLine?same as resolved showHorizontal last-close line; always omitted when showis falseintervalMs?— Finite > 0candle period (ms). Required for countdown; omitted → price-only badgeshowCountdown?truewhenintervalMsvalid; else forcedfalseSecondary countdown line under the price nowMs?: () => numberwall clock ( Date.now) at use siteInjectable clock for accelerated / simulated streams formatter?: (close) => stringlibrary formatPriceLabelValueNot axis tickFormatter(which may returnnull/ sparse precision). Output is plain text (textContent), never HTMLoutOfDomain?'clamp''clamp': pin badge to plot edge (dimmed); price line still draws at true Y (scissor may clip).'hide': hide badge and linecolor?#ffffffBadge text color lineColor?candle direction color Line only; badge background is always up/down from last open/close lineWidth?1CSS px - Direction colorlast candle
close >= open→itemStyle.upColor(default green); elseitemStyle.downColor(default red). Flat candles count as up. CountdownDOM-only timer (does not callrequestRender/ GPU). Streaming demos with simulated time must pass a stablenowMsfunction identity acrosssetOptionrewrites (seecandlestick-streaming/).- setOption replace semantics
setOptionreplaces the full options object (not deep-merge). Always re-passpriceLabel(includingintervalMs/nowMs) on every options rewrite that should keep the badge/countdown. Prefer caching a full options object and updating fields, as the streaming example does. - Series element identityunder axes-only
setOptionreuse, changingpriceLabelrequires a new series element object inseries[]. In-place mutation (series[i].priceLabel = falseon a stable element) is not re-resolved. Same contract as other series fields (see Series array identity reuse above). - Public exports
CandlestickPriceLabelConfig,ResolvedCandlestickPriceLabel,resolvePriceLabel,createPriceLabel,PriceLabel,formatPriceLabelValue,isCandlePrimaryChart(package entry /resolveOptions). - Examples:
// Candle-primary: badge + line auto-on (omit priceLabel or leave undefined)
series: [{ type: 'candlestick', data }]
// Opt out (keep right axis defaults but no badge/line)
series: [{ type: 'candlestick', data, priceLabel: false }]
// Countdown + simulated clock (streaming)
series: [{
type: 'candlestick',
data,
priceLabel: {
intervalMs: 60_000,
nowMs: () => simulatedTimeMs, // stable function ref across setOption
},
}]
// Badge only, custom format, hide when zoomed past last close
series: [{
type: 'candlestick',
data,
priceLabel: {
showLine: false,
formatter: (c) => c.toFixed(2),
outOfDomain: 'hide',
},
}]
OhlcSeriesConfig
Thin open / high / low / close bars (not filled candle bodies). Same data contract as candlestick.
high
│
open ────┤
│
├──── close
│
low
DataidenticalOHLCDataPoint/ ECharts tuple order[timestamp, open, close, low, high].Geometryvertical stemlow→highat the bar timestamp; left horizontal tick atopen; right tick atclose. Color:close > open→itemStyle.upColor, elsedownColor(doji uses down; same as candlestick body fill). Stroke-only in v1 —itemStyle.border*fields are accepted on the shared style type but do not change OHLC stroke geometry.barWidth/barMinWidth/barMaxWidth: category slot width (same resolve as candlestick); drives default tick length scale.stemWidth?numberstroke thickness in CSS px (default1). Stem uses domain X conversion; open/close tick thickness uses the same CSS px via domain Y (must not share the X conversion — time-scale widths are huge in price space). Updated via uniforms on zoom without re-packing.tickLength?number | stringopen/close arm length (horizontal) — CSS px number or percent of resolved body width (default'45%'). Not vertical thickness.sampling?'none' | 'ohlc'only (default'ohlc'); other modes fall back like candlestick. Invalid modes are ignored (default applied).priceLabelsame sugar as candlestick; auto when finance-primary (series[0].type === 'ohlc').- Hit-testkind: 'ohlc'; category-width box with Y over
[low, high](stem range) for bothChartGPU.hitTestand tooltip/hover (findCandlestickyHitMode: 'lowHigh'). Candlestick remains body-only (openClose). Tooltip / events still surface timestamp + close at the ChartGPU wrapper (same as candlestick). StreamingappendData/maxPointsFIFO parity with candlestick.Demoexamples/ohlc-bars/— toggle candlestick ↔ ohlc on the same data.
series: [{
type: 'ohlc',
name: 'BTC-USD',
data: ohlcBars, // same OHLCDataPoint[] as candlestick
barWidth: '60%',
stemWidth: 1,
tickLength: '45%',
itemStyle: { upColor: '#22c55e', downColor: '#ef4444' },
sampling: 'ohlc',
// priceLabel auto when series[0]
}]
LineSeriesConfig
lineStyle?: { width?, opacity?, color? }(default width 2).areaStyle?for fill under line. Color precedence:lineStyle.color→series.color→ palette.stack?stringstack group id for mountain fill composition (multi-series stacked area). Non-empty +areaStyle→ this series stacks with peers sharing the same id and the sameyAxis. Stroke-only lines (noareaStyle) ignorestackfor fill (dev warning once). Same string semantics as barstack. Series array order within the stack = bottom → top. Omitted / empty / whitespace → unstacked single-series mountain (suite group 8 path).step?boolean | StepModeconnect samples with stairs instead of diagonals (digital / step connection). Applies to stroke and mountain fill whenareaStyleis set.true≡'after'. Modes:Mode Geometry (consecutive samples (P_i), (P_{i+1})) after(default /true)Hold (y_i) until (x_{i+1}), then vertical to (y_{i+1}) beforeVertical first at (x_i) to (y_{i+1}), then horizontal middleHorizontal to midpoint, vertical, horizontal to next - Omitted /
false→ linear (default). Invalid strings warn once → linear. Notdense hairline (lineDrawPolicy) — whenstepis active, ChartGPU forces standard AA quads.NotGPU line-decimation eligible (CPU expand after sampling on source samples). Prefersampling: 'none'for exact digital edges; LTTB then step is approximate.- Hit-test / tooltip use source samples (not densified stair corners).
- Works with
stack(step on stacked yTop / yBottom). - Library expands owned geometry — callers keep sparse sample arrays.
- High-N hairline topologyUnder
performance.lod: 'auto'(default), at ≥ ~25k draw points or multi-series total-segment budget ≥ ~500k or multi‑M raw residency (≥ ~1M) on the GPU-decimation path, the GPU may draw withline-listhairline topology (1 device px) regardless oflineStyle.width— draw-only LOD; does not change sampling or uploaded point count. Mid-N residency below 1M with low draw N keeps AA quads. Underperformance.lod: 'strict', configured width + AA quads are always honored. See Dense line draw (hairline) /performance.lodabove and Performance guide. Step series never use dense hairline.
Null Gaps (Line Segmentation)
Line and area series support null entries in DataPoint[] arrays to represent gaps (disconnected segments):
series: [{
type: 'line',
data: [[0, 1], [1, 3], null, [3, 5], [4, 7]], // gap between x=1 and x=3
}]
connectNulls?boolean(default:false): whentrue, null entries are stripped and the line/area draws through the gap. Whenfalse, null entries produce visible gaps.- Multi-segment patternconcatenate pre-split data with null separators:
[...segment1, null, ...segment2]. Samplingwhen data contains null gaps and sampling is enabled, ChartGPU bypasses sampling and uses raw data to preserve gap positions. Gap-aware sampling may be added in a future release.- Supported formats
DataPoint[]only.XYArraysDataandInterleavedXYDatado not support null gaps.
AreaSeriesConfig
baseline?number(default: y-axis min).areaStyle?: { opacity?, color? }.stack?stringstack group id (same as line mountain / bar). Non-empty → stacked with peers of the same id andyAxis. When stacked, per-seriesbaselineis ignored for layout (cumulative floor from 0; dev warning once if both set).step?boolean | StepModestepped top edge (and mountain fill) — same modes as linestep(true≡'after'). See LineSeriesConfigstep.
Stacked mountain / area (stack on line mountain or area)
Multi-series composition fill — each layer’s floor is the sum of layers below (stacked mountain / area). Not single-series mountain (already: line + areaStyle / type: 'area' without stack; performance suite group 8 is 1-series only). Not type: 'band' (dual-curve envelope on one series). Not a new type: 'stackedArea'.
series: [
{ type: 'line', stack: 'traffic', data: { x: t, y: organic }, areaStyle: { opacity: 0.85 }, color: '#38bdf8' },
{ type: 'line', stack: 'traffic', data: { x: t, y: paid }, areaStyle: { opacity: 0.85 }, color: '#a78bfa' },
{ type: 'line', stack: 'traffic', data: { x: t, y: referral }, areaStyle: { opacity: 0.85 }, color: '#34d399' },
// Unstacked guide line (no stack / no areaStyle)
{ type: 'line', data: { x: t, y: target }, lineStyle: { width: 2, color: '#f472b6' } },
]
| Topic | Behavior |
|---|---|
| Order | Within a stack id, ascending series[] index = bottom → top (first drawn near 0). |
| Pos / neg | Positive y stack upward from 0; negative stack downward independently (bar parity). |
| X alignment | Equal length + equal x[i] → index align (fast). Sparse / unequal → align by x-value key; missing peer at x contributes 0. No linear interpolation of missing peers in v1. |
| Auto Y | Includes stacked tops/bottoms (composition totals), not raw per-layer max alone. |
| Stroke | Optional line stroke draws at layer top (baseline + y), not raw unstacked y. |
| Tooltip | value[1] = layer contribution; optional stack + stackTotal on TooltipParams. Hit prefers topmost layer under the cursor. |
| Sampling | Prefer sampling: 'none' or identical thresholds for stack peers; independent LTTB per series is imperfect after stack. Non-empty stack with mountain fill (isStackedMountainSeries) → not GPU line-decimation eligible. Stroke-only lines with inert stack (no areaStyle) remain eligible. |
| connectNulls | Stack baselines use the same view as pack (gaps filtered when connectNulls: true) so indices stay aligned. Prefer identical gap patterns across peers. |
| Visibility | Legend-hidden layers (visible: false) do not participate in composition, auto-Y, or hit-test (toggle recompute). |
| Multi-axis | Same stack string on different yAxis ids → independent stacks; hit-test uses per-axis Y scale. |
| Streaming | appendData / equal-N setOption recompute stack baselines for the group (caller data never mutated to cumulative y). Ranged GPU append is disabled for stacked mountain (kind other). |
Example: examples/stacked-mountain/. Pure math: src/data/stackedArea.ts. Stacked fill shader: src/shaders/areaStacked.wgsl.
BarSeriesConfig
Extends the shared series fields with type: 'bar' and bar-specific layout/styling. See types.ts.
barWidth?number | stringbar width in CSS pixels (number) or as a percentage string.- When
barWidthis a percentage string (e.g.'80%'), it is interpreted as a percentage of the maximum per-bar width that still avoids overlap within a category, given the currentclusterCount(derived fromstack) andbarGap.'100%'equals the default “auto” width (the max non-overlap width).- Values are clamped to ([0, 100])%, and the resulting layout guarantees
clusterWidth <= categoryInnerWidth(no intra-category overlap).
- When
barWidthis a number (CSS px), it is clamped to the same maximum non-overlap per-bar width. - When omitted, ChartGPU uses the same “auto” width (max non-overlap) behavior.
- When
barGap?numbergap between bars in the same category/group (ratio in ([0, 1])). Default:0.01(minimal gap). To make grouped bars flush (no gap between bars within a group), setbarGapto0or a value near0. SeecreateBarRenderer.ts.barCategoryGap?numbergap between categories (ratio in ([0, 1])). Default:0.2. SeecreateBarRenderer.ts.stack?stringstack group id (bars with the same id may be stacked).itemStyle?BarItemStyleConfigper-bar styling.- Rendering (current)bar series render as clustered bars per x-category via an instanced draw path. If multiple bar series share the same non-empty
stackid, they render as stacked segments within the same cluster slot (positive values stack upward from the baseline; negative values stack downward). Bars are clipped to the plot grid (scissor) so they do not render into the chart margins. SeecreateBarRenderer.ts, shader sourcebar.wgsl, and coordinator wiring increateRenderCoordinator.ts. For an example, seeexamples/grouped-bar/.- Note: y-axis auto bounds are currently derived from raw series y-values (not stacked totals). If stacked bars clip, set
yAxis.min/yAxis.max.
- Note: y-axis auto bounds are currently derived from raw series y-values (not stacked totals). If stacked bars clip, set
ScatterSeriesConfig
Extends the shared series fields with type: 'scatter', optional symbol?: ScatterSymbol, and optional symbolSize?: number | ((value: ScatterPointTuple) => number). See types.ts.
- Scatter point tuples may include an optional third
sizevalue (readonly [x, y, size?]). - Rendering (current)scatter series render as instanced circles (SDF + alpha blending). Size is treated as a radius in CSS pixels from either the per-point
size(when provided) orseries.symbolSizeas a fallback. See the internal renderercreateScatterRenderer.tsand shaderscatter.wgsl. mode?'points' | 'density'scatter rendering mode. Default:'points'.- When
mode === 'points', ChartGPU draws individual point markers (current behavior). - When
mode === 'density', ChartGPU renders a binned density heatmap in screen space (useful for very large point clouds where markers overplot).
- When
binSize?numberdensity bin size in CSS pixels (used only whenmode === 'density'). Default:2.- DPR behavior:
binSizeis specified in CSS px, but bins are computed in device pixels usinground(binSize * devicePixelRatio)(minimum 1 device pixel). This keeps the visual bin size roughly consistent across displays. SeecreateScatterDensityRenderer.ts.
- DPR behavior:
densityColormap?'viridis' | 'plasma' | 'inferno' | readonly string[]colormap used for density rendering (used only whenmode === 'density'). Default:'viridis'.- Named presets'viridis' | 'plasma' | 'inferno'use built-in “anchor” color stops that are interpolated into a 256-entry lookup table. See
createScatterDensityRenderer.ts. - Custom gradienta
readonly string[]is interpreted as a low→high gradient of CSS color strings (interpolated into a 256-entry lookup table). Invalid color strings fall back to black. Note: the current density shader renders output as fully opaque (alpha = 1.0) regardless of any alpha in your color stops. SeecreateScatterDensityRenderer.tsandscatterDensityColormap.wgsl.
densityNormalization?'linear' | 'sqrt' | 'log'normalization curve used to map per-bin counts to color intensity (used only whenmode === 'density'). Default:'log'.- Normalization is applied relative to the maximum bin count in the current view: linear uses
count / max, sqrt usessqrt(count / max), and log useslog1p(count) / log1p(max). This means color intensity can rescale as you zoom/pan (because the per-view max changes). SeescatterDensityColormap.wgsl.
- Normalization is applied relative to the maximum bin count in the current view: linear uses
Notes (density mode):
- Correctness (important)density mode uses raw (unsampled) points for binning, even when
samplingis configured, to avoid undercounting. - Zoom/pan behaviordensity is recomputed as the view changes. When x-values are monotonic, ChartGPU limits compute to the current visible x-range; otherwise it may process the full series. See the coordinator wiring in
createRenderCoordinator.tsand compute shaderscatterDensityBinning.wgsl. - Performance vs resolution
binSizetrades resolution for performance. Smaller bins increase detail but increase both bin count and compute cost per recompute. Examplefor a working 1M-point density scatter demo (including controls for colormap, normalization, and bin size), seeexamples/scatter-density-1m/.- Not a data-grid heatmapscatter density bins a point cloud in screen space. For spectrograms / correlation matrices / regular fields, use
type: 'heatmap'(HeatmapSeriesConfig).
HeatmapSeriesConfig
Uniform rectangular data-grid heatmap / spectrogram (type: 'heatmap'). Distinct from scatter mode: 'density'.
Data layout (HeatmapData)
{
xStart, xStep, yStart, yStep,
columns, rows,
z: Float32Array | number[], // length === columns * rows, row-major
}
- Cell indices
z[j * columns + i]at columni, rowj. - Row
j = 0is the band starting atyStart(increasingj→ increasingy). - Prefer
Float32Arrayfor upload efficiency.
Options
| Field | Default | Notes |
|---|---|---|
colormap | 'viridis' | 'viridis' | 'plasma' | 'inferno' | 'magma' | 'grayscale' or custom low→high CSS stops |
zMin / zMax | auto from finite z | If equal, expanded by epsilon so colormap t does not NaN |
zScale | 'linear' | 'log' uses positive finite z only; non-positive cells follow nullHandling |
opacity | 1 | Series-wide alpha multiplier |
cellAnchor | 'corner' | 'corner': (xStart,yStart) is min-corner of cell (0,0); 'center': center of cell (0,0) |
nullHandling | 'transparent' | 'transparent' | 'lowest' | 'highest' for NaN/±Inf (and log-invalid) z |
cellGapPx | 0 | Optional UV inset between cells (CSS px) |
Behavior (v1)
- GPU path
r32floatz texture + 256-entry colormap LUT + single data-space quad (main pass 4× MSAA). Shared colormap stops with density viasrc/utils/colormap.ts. PreferFloat32Arrayfor packing efficiency; GPU still pads/copies rows forbytesPerRowalignment (not a zero-copy path). NoLTTB / GPU line decimation / cartesian DataStore XY packing.- Draw orderall heatmaps render before area/bar/candlestick/scatter/line strokes (under overlays). Relative order among heatmaps follows
series[]. Series array order does not interleave heatmaps above lines. - Zoom / panreprojects via uniforms only when z size/ref/content are unchanged (no z re-upload). Opacity / zMin / zMax / colormap changes do not force a z rewrite (LUT rebuilds on colormap only).
Streamingpreferchart.updateHeatmap(seriesIndex, update)(below). Equal-sizesetOptionz replace still works.appendDatais unsupported (console warning points atupdateHeatmap).- Axis auto-boundsgrid extent from
heatmapGridBoundscontributes to X/Y domains (z does not). After scroll, auto X tracks the newxStart/width. Tooltipcell under cursor →TooltipParamswithvalue: [cellCenterX, cellCenterY], optionalz, anddataIndex = j * columns + i. Hit priority (item): pie → candlestick body → nearest cartesian point → heatmap cell (heatmaps are under strokes). Axis mode appends heatmap params alongsidefindPointsAtXmatches, not instead of them. TransparentnullHandling+ non-finite z → miss (no tooltip). Chart-sync tooltips are x-only and do not include heatmap cells (need data-space Y); heatmap tooltips require a local pointer over the plot.Legendseries name + solid palette placeholder color only (not a colormap gradient swatch; gradient legend is a follow-up).- Log axesVS uses the same log projection flags as other continuous series when X/Y are log.
- Negative stepssupported. UV
u=0/v=0always maps to cell(0,0)(signed origin+extent), matching CPUheatmapHitTest/heatmapCellIndex. Axis bounds normalize so min ≤ max.
Streaming (chart.updateHeatmap)
Mirrors 3D updateSurface3D. Discriminated union HeatmapUpdate:
| Mode | Payload layout | Notes |
|---|---|---|
replaceZ | Full field row-major z[j * columns + i] | Optional zMin/zMax; when both set, skip full-field domain recompute |
appendColumns | Column-major strips z[c * rows + r] | scrollX default true: fixed window, drop oldest, xStart += drop * xStep. scrollX: false grows columns (texture recreate) |
appendRows | Row-major blocks z[r * columns + i] | scrollY default true: drop oldest rows, yStart += drop * yStep; scrollY: false grows rows |
// Spectrogram: one new spectrum column per frame (length === series.rows)
chart.updateHeatmap(0, {
mode: 'appendColumns',
columns: 1,
z: spectrumColumn, // Float32Array(rows), column-major
scrollX: true,
});
GPU hot path: single-column appendColumns + scrollX uses a modular texture ring + strip writeTexture (O(rows) GPU z traffic), not a full-grid pack/upload every frame. Multi-column batch / replaceZ / dimension change reset the ring and full-upload. CPU z for hit-test stays a linear logical window (oldest column at index 0).
Stream vs setOption (same policy as surface3d):
| Action | Behavior |
|---|---|
updateHeatmap | Owns a stream override (xStart / yStart / z / dims) |
setOption with same series[i].data object identity | Stream kept (style-only: colormap, opacity, zMin/zMax, …) |
setOption with a new data object | Stream cleared; user data wins |
First seed (prevUser == null) | Stream not cleared spuriously |
Colormap domain on stream (D5):
| Situation | Behavior |
|---|---|
Series has both finite user zMin/zMax (zDomainExplicit) | Keep that range on append (no expand-from-strip drift) |
Style setOption with same data ref and new zMin/zMax | Style domain wins; stream field (xStart/z) kept |
replaceZ with both zMin/zMax in the update | Use those values |
| Auto domain + single-column scroll | Expand min/max from the new strip only |
Auto domain + multi-column / appendRows / replace without pair | Recompute from full field |
Live spectrogram demos should set explicit zMin/zMax (fixed dB range).
Example: examples/heatmap-spectrogram/. Implementation: createHeatmapRenderer.ts, heatmap.wgsl, heatmapStream.ts.
BandSeriesConfig
Band / range series (type: 'band') fills the region between two curves that share the same x — confidence / prediction intervals, sensor min–max envelopes, bid–ask ranges, and threshold fills (constant y vs varying y1).
Not the same as:
type: 'area' | Annotation bandX | type: 'band' | |
|---|---|---|---|
| Input | (x, y)[] | from/to x-span | (x, y, y1)[] |
| Fill | Curve → baseline | Full-height vertical strip | Curve y ↔ curve y1 |
| Use | Mountain under a series | Regime / outage highlight | Confidence / min–max range |
dataone of:ReadonlyArray<BandDataPoint | null>— tuple[x, y, y1]or{ x, y, y1 }{ x, y, y1 }parallel arrays (Xyy-style split channels); length = min of the three (mismatch warns)- Interleaved
ArrayBufferViewstride 3:[x0,y0,y1_0, …]— do not use stride-2InterleavedXYData(would dropy1).appendDatarejects interleaved views whose length is not a multiple of 3.
y/y1: first and second curves; convention in docs is lower/upper but crossing is allowed (ymay exceedy1).areaStyle?{ color?, opacity? }fill between curves. Default opacity 0.25. Color falls back toseries.color→ palette.lineStyle?stroke for theycurve. When omitted, no y stroke (fill-only). When present, width defaults to 1; width 0 / opacity 0 also hides.lineStyleY1?stroke for they1curve. When omitted, no y1 stroke (fill-only is valid).connectNulls?boolean(defaultfalse): null/NaN gaps break fill + both strokes unlesstrue.sampling?'none' | 'lttb' | 'average' | 'max' | 'min'(default like other series:lttb). Noohlc(rejected with warn →lttb). No GPU compute decimation in v1.LTTBruns on midlinem = (y+y1)/2, then carries both y and y1 from chosen indices (aligned).- min / maxaliases in v1 — both keep the full dual-Y envelope per bucket (
y = envMin,y1 = envMaxofmin/max(y,y1)), so axis bounds retain envelope fidelity under downsample. averageaverage each of y and y1 separately per bucket (only over finite samples in that channel).
- Draw layeringbands are type-layered after heatmaps and before area fills / lines (not strict
series[]z-order vs area/line). Put mean lines after bands in the array for readability; strokes of the same band series still draw after its fill. - Axis auto-boundsX from finite
x; Y from both finiteyandy1. Tooltipnearest sample by x;value: [x, y], plus optionaly1,yMid,yRange.appendDatasupported with Xyy payloads ({x,y,y1}, tuples, or interleaved). FIFOmaxPointsworks like cartesian series.GPUprivate storage buffer ofBandPoint { x, y, y1, pad }(16-byte); per-segment trapezoid triangle-list (area topology, dual tops). Zoom/pan rewrites VS uniforms only when data identity is stable.- Log axessame log projection as area/line; non-positive y/y1 on log Y discard that segment endpoint. Log auto-bounds scan both y channels for strictly positive values.
Example: examples/band-range/. Implementation: createBandRenderer.ts, band.wgsl, bandData.ts.
ErrorBarSeriesConfig
Error bars (type: 'errorBar') draw per-point high/low whiskers around a measured center (HLC style) for scientific uncertainty, SEM/CI, and assay plots.
Not the same as:
type: 'band' | type: 'ohlc' | type: 'errorBar' | |
|---|---|---|---|
| Geometry | Continuous fill between two curves | Finance stem + open/close ticks | Discrete stem + whisker caps per sample |
| Data | {x,y,y1} | OHLC open/close/high/low | Center y + absolute high/low (or relative yError) |
| Use | Confidence envelope | Candles / OHLC bars | Per-x measurement uncertainty |
dataone of:- Absolute HLC columns
{ x, y, high, low }(preferred for streaming) - Relative columns
{ x, y, yError }(symmetric) or{ x, y, yErrorHigh, yErrorLow }(asymmetric offsets; abs applied →high = y + |eH|,low = y - |eL|) ReadonlyArrayof:- tuples
[x, y, high, low] - absolute objects
{ x, y, high, low } - relative objects
{ x, y, yError }or{ x, y, yErrorHigh, yErrorLow }(resolved to absolute at resolve time) nullgaps
- tuples
- Length = min of channels; mismatch warns. Relative forms resolve to owned absolute HLC (caller arrays never mutated).
errorMode?: 'both' | 'high' | 'low'(default'both'):bothstem low→high, both capshighstem y→high, high cap onlylowstem low→y, low cap only
direction?'vertical' | 'horizontal'(default'vertical'). Horizontal reinterpretshigh/lowas absolute X extents (stem along X aty; category/sample axis is Y). Remap data when switching direction (centers on Y, whiskers on X).capWidth?number | stringwhisker tip-to-tip length.numberCSS px (converted to domain along the cap axis — X when vertical, Y when horizontal)- percent string (e.g.
'40%'): fraction of category stepmin positive Δx for vertical, min positive Δy for horizontal - Default
'40%'. Pure zoom recomputes domain length into uniforms without re-uploading instances.
itemStyle?{ color?, borderWidth?, opacity? }stem + whisker stroke.borderWidthis CSS px (default 1.5). Color falls back toseries.color→ palette.drawWhiskers?boolean(defaulttrue),drawConnector?: boolean(defaulttrue— the stem).showCenter?boolean(defaultfalse) +symbolSize?: number(default 6): optional center marker. Prefer overlaying a separate scatter/line for the dual-series mean + whiskers pattern.sampling'none'only. Other modes warn and are ignored (error bars are sparse science series; no LTTB / GPU line decimation).- Null / NaNskip sample if
ynon-finite, or if required ends are non-finite for the activeerrorMode(bothneeds both ends;highneeds high;lowneeds low).low > highswaps with a one-shot warn. - Axis auto-boundsvertical — X from finite
x, Y from finitey/high/low. Horizontal — X fromx/high/low, Y fromy. Bounds reuse onsetOptionis direction-keyed (vertical↔horizontal toggles recompute). - Tooltip / hit-teststem + caps (CSS-px pad).
TooltipParams:value: [x, y]plushigh,low, optionalyErrorHigh/yErrorLow. Hit priority: pie → candle/ohlc → errorBar → impulse → nearest cartesian → heatmap. Multi-axis: hit uses the seriesyAxisscale. appendDatasupported with the same HLC / relative / tuple / relative-object shapes. FIFOmaxPointsworks like band.- Draw orderrespects
series[]order among peers; typical pattern placeserrorBarbefore a meanline/scatterso markers sit on top. GPUinstanced stem + caps (+ optional center) viaerrorBar.wgsl. Domain pack + affine uniforms; zoom/pan without re-upload when data identity is stable. Horizontal packshigh/lowrelative to the same packing origin asx. Stem thickness is domain X (vertical) / Y (horizontal); cap thickness uses the opposite axis (OHLC lesson — never reuse domain-X as Y thickness).
// Absolute HLC + companion mean line (recommended dual-series pattern)
series: [
{
type: 'errorBar',
name: 'Assay ±SEM',
data: { x: doses, y: means, high: highs, low: lows },
itemStyle: { color: '#38bdf8', borderWidth: 2 },
capWidth: '40%',
errorMode: 'both',
showCenter: true,
symbolSize: 8,
},
{
type: 'line',
name: 'Mean',
data: { x: doses, y: means },
lineStyle: { color: '#38bdf8', width: 2 },
sampling: 'none',
},
]
// Relative SEM: data: { x, y: means, yError: sems }
Example: examples/error-bars/. Implementation: createErrorBarRenderer.ts, errorBar.wgsl, errorBarData.ts, errorBarGeometry.ts.
ImpulseSeriesConfig
Impulse / stem series (type: 'impulse') draws one vertical stem per sample from baseline → y — for event trains, DSP stems, and lollipop charts.
Not the same as:
type: 'errorBar' | type: 'ohlc' | type: 'impulse' | |
|---|---|---|---|
| Geometry | HLC whiskers around center | Finance stem + open/close ticks | Stem baseline→y only |
| Data | HLC / relative error | OHLC | Cartesian XY |
| Marker | Optional center | N/A | Optional tip marker (showMarker) |
datasame cartesian XY formats as line ({x,y}, tuples, interleaved).baseline?number(default 0): data-space floor of each stem. Non-finite →0+ warn. Auto Y bounds include baseline when outside the data range.lineStyle?{ width?, opacity?, color? }stem stroke. Width default 2 CSS px (clamp ≤0 → 1). Color falls back toseries.color→ palette. Thickness is CSS px → domain X (same lesson as error bars / OHLC — not domain-X units as Y thickness).showMarker?boolean(default true): lollipop head at(x, y). Setfalsefor pure stems.symbolSize?number(default 6): marker size (CSS px, scatter spirit).sampling'none'only. Other modes warn and are ignored (sparse event stems; no LTTB / GPU line decimation).- Null / NaNskip stem for non-finite x/y. Zero-length (
y ≈ baseline) skips the stem body but still draws the marker when enabled. - Tooltip / hit-teststem + marker with CSS-px pad.
TooltipParams:value: [x, y]plus optionalbaseline. Hit priority: pie → candle/ohlc → errorBar → impulse → nearest cartesian → heatmap. appendDatasame XY payloads as line; FIFOmaxPointssupported.GPUinstanced stems (+ optional markers) reusing error-bar instance layout; private pack dirty-gated on data identity.
series: [{
type: 'impulse',
name: 'Events',
data: { x, y },
baseline: 0,
lineStyle: { width: 2, color: '#a78bfa' },
showMarker: true,
symbolSize: 6,
sampling: 'none',
}]
Example: examples/impulse/. Step digital line: examples/step-line/. Implementation: createImpulseRenderer.ts, impulseGeometry.ts, stepGeometry.ts.
Step / impulse cheatsheet
| Pattern | ChartGPU |
|---|---|
| Digital / step line | type: 'line', step: true (≡ 'after') |
| Digital mountain | type: 'line' + areaStyle + step, or type: 'area' + step |
| Impulse / stem | type: 'impulse' |
| Stem tip marker | showMarker + symbolSize |
| Stem floor | baseline (default 0) |
PieSeriesConfig
- Non-cartesian. No x/y bounds or cartesian hit-test. Slice hit-test via
findPieSlice.radius?,center?,startAngle?. Example:pie/.
Axis Configuration
AxisConfigAxisType'value' | 'time' | 'category' | 'log''value' | 'time' | 'category' | 'log'.- Logarithmic axes (
type: 'log')- Available on
xAxisand eachyAxis/yAxes[]independently. logBase?numberlogarithm base (default 10). Must be finite, > 0, and ≠ 1; invalid values fall back to 10 with a dev warning.Projectiondata stays in linear storage (DataStore / GPU buffers). Log is applied in series vertex shaders before the clip affine — toggling log does not force a full re-upload.- Ticks / gridmajor ticks at integer powers of the base (e.g. (10^{-2}, 10^{0}, 10^{3})) for the visible domain (updates on zoom/pan; not locked to full explicit
min/max). When the window has few majors, intermediate ticks densify (e.g. 2×/5× within a decade). GPU grid lines for log axes are co-located with those ticks (not even data-space splits). Labels use a scientific/1eNhybrid by default; customtickFormatterstill receives data-space values (e.g.1000, not3). - Domain must be strictly positiveauto-bounds ignore ≤0 samples; if no positive data exists the domain falls back to
[1, 10]with a warning. Explicitmin/max≤ 0 are clamped using the smallest positive data valuepd: preferpd × 0.5, then floor to a power oflogBase(base ** floor(log_b(pd × 0.5))), with a dev warning. Example (base 10):pd = 9→ half4.5→ floored power1. - Non-positive pointstreated as gaps on line/area (NaN packing path / VS discard) and omitted on scatter under log projection.
- Area / bar baselineclassic “fill to zero” is unsupported on log; baseline uses the positive axis min (or first major ≤ data min). Document that fill-to-zero is not available on log Y.
- Sticky headroomon log axes is applied in log space (decade headroom).
Zoomstart/endpercents remain percent of data-space base domain (not log-percent).- No library UI log toggleconfig only (
setOption({ yAxis: { type: 'log' } })). Examples may include demo chrome. - Showcase
- Multiple Y-Axes
- Instead of a single
yAxisobject, ChartGPU supports an array of Y axes viaaxes.yfor independent scales (e.g. Price vs Volume). - Each axis in
axes.ymay specify anid: string(defaults: first axis"y", then"y1", …). - Series map to a specific axis via
yAxisid in their config. - Axes can be positioned on the right using
position: "right". Default position is"left", except for candle-primary charts (see below). - Dual Y may mix log + linear (e.g. log pressure + linear temperature). Horizontal grid follows the primary (first) Y axis ticks (log and linear value majors).
- Instead of a single
- Finance-primary layout defaults(when
series[0].type === 'candlestick'or'ohlc'):Predicateonly the first series type matters. Overlay lines asseries[0]mean the chart is not candle-primary (setposition: 'right'explicitly if needed). Helper:isCandlePrimaryChart(userOptions).- Y positionthe first Y axis defaults to
position: 'right'when the user leavespositionunset (yAxisoraxes.y[0]). Secondary Y axes still default to'left'. Explicitpositionalways wins. Soft grid gutters (per-key only for most cases — each of
grid.left/grid.rightis set only when that key isundefinedon user options;0is preserved):Scenario grid.leftif unsetgrid.rightif unsetSingle candle, price on right 2080Candle + volume (left Y remains) 6080User set grid.left: 80only80(kept)70User set both margins (right ≥ 80 or 0) unchanged unchanged User set both with small right (e.g. right: 24) and a right Yunchanged floored to 80Non-candle-primary 6020- Right-rail floorwhen candle-primary and any Y is on the right, a positive
grid.rightbelow80is raised to80so left-Y line-chart templates (left: 70, right: 24) do not clip tick labels or the last-price badge. Explicitright: 0(full-bleed) andright ≥ 80are unchanged. - Non-candle-primary charts keep the standard grid defaults and left Y.
- Migration (P1 default badge)candle-primary charts also auto-enable
priceLabel(last-price badge + price line) whenpriceLabelis omitted. This is a default visual change for existing candle consumers. Opt out withpriceLabel: false. To restore a pre-upgrade left-axis look:
{
yAxis: { type: 'value', position: 'left' },
grid: { left: 70, right: 24 },
series: [{ type: 'candlestick', data, priceLabel: false }],
}
- Recommended candle + volume dual-Y (or omit both gutters — dual-Y policy yields left
60/ right80):
{
grid: { left: 60, right: 80 },
axes: {
y: [
{ id: 'price', position: 'right', type: 'value', header: 'USDT' },
{ id: 'vol', position: 'left', type: 'value' },
],
},
series: [
{
type: 'candlestick',
yAxis: 'price',
data,
// Optional: countdown needs intervalMs (and nowMs for simulated clocks)
priceLabel: { intervalMs: 60_000 },
},
{ type: 'bar', yAxis: 'vol', data: volumes },
],
}
- Explicit domains (override auto-bounds)
AxisConfig.min?: number/AxisConfig.max?: numberwhen set, ChartGPU uses these explicit axis bounds and does not auto-derive bounds from data for that axis.Precedenceexplicitmin/maxalways override any auto-bounds behavior.- One-sided explicitif only
minor onlymaxis set, the other end is still data-derived; sticky auto-domain headroom (below) is disabled whenever either end is explicit so growBy padding never extends past a locked edge. Logboth ends must resolve to strictly positive values (see log policy above).
- Sticky auto-domain headroom (streaming / multi-chart)default motion:
- When both ends of an axis are auto (no explicit
min/max), ChartGPU uses a sticky domain: first establish matches the data extrema exactly (static column/mountain charts fill the plot). - Y: ~10% growBy headroom is added only when data breaches that domain (amortizes overlay rebuild under amplitude noise).
- X: headroom is 0 so unbounded
appendDatatracks data max tightly and the series stays full plot width. Non-zero X pad previously left an empty right gutter that filled then re-expanded on every breach (visible as a grow/reset loop in examples like ultimate-benchmark streaming). - If the data min slides upward (FIFO/
maxPointsdrop-oldest), sticky follows the new min instead of freezing the historical origin. Sticky X is off entirely whenautoScroll: true. - Cleared on
setOption(including axes-only rewrites) and when any end becomes explicit. - Does not change sampling contracts (
lttbstayslttb).
- When both ends of an axis are auto (no explicit
- Auto-range motion (
AxisConfig.autoRange)opt-in continuous / animated rolling Y axes:- Y-onlypaint applies
autoRange/growByto Y axes. Values onxAxisare accepted by the option resolver but ignored at paint time (X keeps sticky headroom 0 + autoScroll skip). autoRange?: 'sticky' | 'continuous' | 'animated'(default'sticky'). Applies when both ends are free; any explicitmin/maxstill disables pad past locked edges.stickycurrent headroom hold-until-breach policy (safe multi-chart default).continuousvisible domain tracks data bounds every paint with optional padding (growBy).animatedtracks a target domain (same pad as continuous); paint domain lerps toward the target (time-based) so range motion is smooth.
growBy?number | [minEdge, maxEdge]pad as a fraction of data span for continuous/animated Y only (default ~5% per edge when omitted). Sticky ignoresgrowByand keeps internal ~10% Y / 0 X headroom.- Nice value tickslinear/
valuemajors use a shared 1–2–5 × 10ⁿ ladder clamped to the visible domain. Category X uses equal index splits. Time and log generators stay authoritative for those axis types. - Tick-aligned gridprimary Y majors drive horizontal grid lines; X tick values drive vertical grid (value, time, and log) — labels, GPU tick marks, and grid share one tick list per axis.
- Demo
examples/axis-streaming-smooth/(autoRange: 'continuous', grid on, rising Y). - Example:
{
yAxis: { type: 'value', autoRange: 'continuous', growBy: 0.05 },
// or: autoRange: 'animated'
series: [{ type: 'line', data }],
}
- Y-axis auto-bounds during x-zoom (new default)
yAxis.autoBounds?: 'visible' | 'global'controls how ChartGPU derives the y-axis domain whenyAxis.min/yAxis.maxare not set.'visible'(default): when x-axis data zoom is active, ChartGPU derives y-bounds from the visible (zoomed) x-range (using the same sticky X domain the paint path uses when sticky is active).'global'derive y-bounds from the full dataset (pre-zoom behavior), even while x-zoomed.
- This option is intended for
yAxis(it has no effect onxAxis).
AxisConfig.tickCount?numberhint for nice major count on value axes (clamped ~2–20; generator may adjust).AxisConfig.tickFormatter?(value: number) => string | nullcustom formatter for axis tick labels. When provided, replaces the built-in tick label formatting for that axis.- For
type: 'value'axes,valueis the numeric tick value. - For
type: 'time'axes,valueis a timestamp in milliseconds (epoch-ms, same unit asnew Date(ms)). - For
type: 'log'axes,valueis the data-space tick value (e.g.1000for (10^3)), not the log exponent. - Return a
stringto display as the label, ornullto suppress that specific tick label. - When omitted, ChartGPU uses its built-in formatting:
Intl.NumberFormatfor value axes, adaptive tier-based date formatting for time axes, and scientific/1eNhybrid labels for log axes. - The formatter is also used for label width measurement in the adaptive time x-axis tick count algorithm, ensuring overlap avoidance uses the correct label widths.
- For
Tick Formatter Examples
// Duration formatting (seconds → human-readable)
yAxis: {
tickFormatter: (seconds) => {
const d = Math.floor(seconds / 86400);
const h = Math.floor((seconds % 86400) / 3600);
return d > 0 ? `${d}d ${h}h` : `${h}h`;
}
}
// Percentage formatting (0–1 → 0%–100%)
yAxis: { tickFormatter: (v) => `${(v * 100).toFixed(0)}%` }
// Integer-only ticks (suppress fractional labels)
xAxis: { tickFormatter: (v) => Number.isInteger(v) ? v.toLocaleString() : null }
// Custom time axis formatting
xAxis: {
type: 'time',
tickFormatter: (ms) => new Date(ms).toLocaleDateString('de-DE')
}
// Append units
yAxis: { tickFormatter: (v) => `${v.toFixed(1)} ms` }
xAxis.type'time' (timestamps)whenxAxis.type === 'time', x-values are interpreted as timestamps in milliseconds since Unix epoch (the same unit accepted bynew Date(ms)), including candlesticktimestampvalues. For GPU precision, ChartGPU may internally rebase large time x-values (e.g. epoch-ms domains) before uploading to Float32 vertex buffers; this is automatic and does not change your units. See the runtime axis label/tick logic increateRenderCoordinator.ts.- Time x-axis tick labels (automatic tiers)when
xAxis.type === 'time', x-axis tick labels are formatted based on the current visible x-range (after data zoom):Visible x-range (approx.) Label format < 2 secondsHH:mm:ss.SSS< 5 minutesHH:mm:ss< 1 dayHH:mm1–7 daysMM/DD HH:mm~1–12 weeks(or< ~3 months)MM/DD~3–12 months(≤ ~1 year)MMM DD> ~1 yearYYYY/MMNotes: month/year thresholds are approximate (30d / 365d), and formatting uses the browser's
Datesemantics (local timezone). Sub-5-minute / sub-second tiers keep labels unique when deeply zoomed and update as you pan. Implementation:timeAxisUtils.ts(formatTimeTickValue); wired fromcreateRenderCoordinatorImpl.ts(public shell:createRenderCoordinator.ts). - Nice time tick positions (time x-axis only)for
xAxis.type === 'time', tick values are snapped to a ladder of epoch-aligned nice steps (ms → s → min → h → day → week → ~month/~quarter/~year), not raw equal splits ofdataMin→dataMax. Steps are absolute multiples of each ladder duration from Unix epoch (e.g. whole-hour UTC boundaries for hour steps)—not local midnights, week starts, or true calendar month boundaries. Label text still uses localDateformatting, so wall-clock labels can differ from the absolute grid depending on timezone offset. That still avoids awkward mid-interval times like02:41/08:40on a full-day view in favor of nice hour/day spacing. Non-time (value) axes still use evenly spaced linear ticks. SeetimeAxisUtils.ts. - Adaptive tick count (overlap avoidance, time x-axis only)when
xAxis.type === 'time', ChartGPU may vary tick density per render to avoid DOM label overlap. Density is step-wise, not an exact linear count: it tries denser nice-step sets first (target count in [1, 9]), and when a candidate set's labels overlap it may subsample that set (every 2nd / 3rd tick) before dropping to a coarser ladder target. The densest non-overlapping set is kept (minimum gap 6 CSS px); if measurement isn't available it falls back to a default-density nice tick set. GPU tick marks and DOM tick labels share the same computed tick values (nice time steps for time axes; linear domain splits for value axes). SeetimeAxisUtils.ts(computeAdaptiveTimeXAxisTicks),createRenderCoordinatorImpl.ts, andcreateAxisRenderer.ts. AxisConfig.name?stringrenders an axis title for cartesian charts when provided (and non-empty aftertrim()): x-axis titles are centered below x-axis tick labels, and y-axis titles are rotated (-90°) and placed left of y-axis tick labels (or (+90°) on the right rail); titles can be clipped ifgrid.bottom/grid.left/grid.rightmargins are too small. WhendataZoomincludes a slider (see below), ChartGPU reserves extra bottom space so the x-axis title remains visible above the slider overlay and is centered within the remaining space above the slider track. SeecreateRenderCoordinatorImpl.ts(public shell:createRenderCoordinator.ts) and option resolution inOptionResolver.ts.AxisConfig.header?stringoptional non-rotated unit header at the top of a Y-axis rail (e.g."USDT"for a quote currency). Independent ofname— useheaderfor short exchange-style unit labels and keepnamefor a longer rotated side title if needed. Applies toyAxis/axes.yonly (ignored forxAxis). Place it with enoughgrid.topmargin so it is not clipped. Rendered via the same axis text overlay as tick labels (bold, themetextColor/fontFamily).
// Exchange-style right price axis with unit header
yAxis: { type: 'value', header: 'USDT' } // candle-primary defaults position to 'right'
- Axis title stylingtitles are rendered via the internal DOM text overlay and use the resolved theme's
textColorandfontFamilywith slightly larger, bold text (label elements also setdir='auto').
Grid Lines Configuration
ChartGPUOptions.gridLines?GridLinesConfigoptional configuration for the background grid lines drawn inside the plot area. SeeGridLinesConfiganddefaults.ts.- What grid lines aregrid lines are evenly-spaced lines drawn across the plot grid to aid visual reading of values. They are not aligned to axis ticks — they are distributed uniformly across the horizontal and vertical extent of the plot area. Lines are rendered via a WebGPU
line-listprimitive, so they are always 1 device pixel wide (this is a WebGPU limitation; line width is not configurable). - Default behaviorwhen
gridLinesis omitted, grid lines are shown using the theme'sgridLineColor(dark theme:rgba(255,255,255,0.1); light theme:rgba(0,0,0,0.1)) with 5 horizontal and 6 vertical lines. SeedefaultGridLinesandcreateGridRenderer.ts.
GridLinesConfig
Top-level grid lines configuration. See types.ts.
show?booleanglobal toggle for all grid lines. Whenfalse, no grid lines are drawn. Default:true.color?stringCSS color string for all grid lines. Can be overridden per-direction viahorizontal.colororvertical.color. Falls back totheme.gridLineColorwhen not specified. Expected formats:#rgb,#rrggbb,#rrggbbaa,rgb(r,g,b),rgba(r,g,b,a).opacity?numberglobal opacity multiplier for grid lines (0–1). This multiplies the alpha channel of the resolved color (including per-direction overrides). Default:1.horizontal?boolean | GridLinesDirectionConfighorizontal grid lines (constant-Y, spanning left→right). Accepts a boolean shorthand (true= show with defaults,false= hide) or a detailedGridLinesDirectionConfig. Default:{ show: true, count: 5 }.vertical?boolean | GridLinesDirectionConfigvertical grid lines (constant-X, spanning top→bottom). Accepts a boolean shorthand (true= show with defaults,false= hide) or a detailedGridLinesDirectionConfig. Default:{ show: true, count: 6 }.
GridLinesDirectionConfig
Per-direction (horizontal or vertical) grid line settings. See types.ts.
show?booleanwhether to show grid lines in this direction. Whenfalse, no lines are drawn regardless ofcount. Default:true.count?numbernumber of evenly-spaced grid lines. Default:5(horizontal),6(vertical).color?stringCSS color string for lines in this direction. Overrides the top-levelgridLines.colorandtheme.gridLineColor.
Grid Lines Examples
gridLines: { show: false } // hide all
gridLines: { horizontal: true, vertical: false } // horizontal only
gridLines: { color: 'rgba(100,100,255,0.2)', horizontal: { count: 8 }, vertical: { count: 10 } }
Data Zoom Configuration
ChartGPUOptions.dataZoom?ReadonlyArray<DataZoomConfig>optional data-zoom configuration list. SeeChartGPUOptionsandDataZoomConfig.- Runtime behavior (current)data zoom controls a shared percent-space zoom window
{ start, end }in ([0, 100]) that is applied to the effective x-domain for both rendering and pointer interaction. See the x-domain application increateRenderCoordinator.tsand percent-space semantics increateZoomState.ts.- Span constraints (min/max)ChartGPU clamps the zoom window span using
DataZoomConfig.minSpan/DataZoomConfig.maxSpanat runtime (applies consistently to inside zoom, slider UI, and programmatic APIs), and re-applies constraints when options/data change (e.g. onsetOption(...)and streamingappendData(...)). SeecreateZoomState.tsand coordinator wiring increateRenderCoordinator.ts.- When
minSpan/maxSpanare omitted, ChartGPU uses a dataset-aware default minimum span forxAxis.type: 'value' | 'time', targeting roughly one data interval: approximately (100/(N-1))% where (N) is the largest raw point count across non-pie cartesian series (raw/unsampled data; updates as points are appended). When there is insufficient data to infer an interval ((N < 2)), ChartGPU falls back to 0.5% to keep the UI usable. SeecreateRenderCoordinator.ts. - For
xAxis.type: 'category', no dataset-aware default is currently applied; useminSpanexplicitly if you need deeper zoom for very large category counts.
- When
- Zoom-aware sampling (cartesian)when a cartesian series has sampling enabled, ChartGPU resamples from raw (unsampled) series data over the current visible x-range. Zoom changes schedule an honest re-sample on the next flush/frame (period=1 — not a multi-frame slice of prior full-span samples); a ±10% buffer zone reduces re-work during small pans. GPU-decimation series keep full raw and scope via visible indices. See
createRenderCoordinator.tsand performance — zoom-aware resampling. - Inside zoomwhen
ChartGPUOptions.dataZoomincludes{ type: 'inside' }, ChartGPU enables an internal wheel/drag interaction. SeecreateRenderCoordinator.tsandcreateInsideZoom.ts. - Zoom gesturemouse wheel zoom, centered on the current cursor x-position (only when the pointer is inside the plot grid).
- Pan gestureshift+left-drag or middle-mouse drag pans left/right (only when the pointer is inside the plot grid).
Scopethe zoom window is applied to the x-domain; the y-domain is derived from data unless you set explicityAxis.min/yAxis.max.- Default behaviorduring x-zoom,
yAxis.autoBounds: 'visible'derives y-bounds from the visible x-range. - Opt outset
yAxis.autoBounds: 'global'to keep y-bounds derived from the full dataset, or set explicityAxis.min/yAxis.max.
- Grid-onlyinput is ignored outside the plot grid (respects
gridmargins). - Slider UIwhen
ChartGPUOptions.dataZoomincludes{ type: 'slider' }, ChartGPU mounts a slider-style UI that manipulates the same percent zoom window. ChartGPU also reserves 40 CSS px of additional bottom plot space so x-axis tick labels and the x-axis title remain visible above the slider overlay (you generally should not need to manually “make room” by increasinggrid.bottom). SeeChartGPU.ts, option resolution inOptionResolver.ts, and the internal UI helpercreateDataZoomSlider.ts. Coexistencemultiple data-zoom configs can coexist (e.g. inside + slider) and drive the same x-zoom window.- Config fields
start/endare used as the initial percent window (defaulting to0/100when omitted).minSpan/maxSpanare applied to the runtime clamping behavior (see above).xAxisIndexis currently accepted by the type but onlyxAxisIndex: 0is supported by the runtime zoom path.
DataZoomConfigdata zoom configuration type. SeeDataZoomConfig.type: 'inside' | 'slider'xAxisIndex?: numberstart?: number: start percent in ([0, 100])end?: number: end percent in ([0, 100])minSpan?: numbermaxSpan?: number
Tooltip Configuration
tooltip?TooltipConfigshow,trigger: 'item' | 'axis',formatter. Enabled by default.TooltipParams.value[x, y](cartesian/pie),[timestamp, open, close, low, high](candlestick). Distinguish viavalue.length.Safetytooltip usesinnerHTML— return trusted/sanitized strings only.- Helpers
Animation Configuration
ChartGPUOptions.animation?AnimationConfig | booleanoptional animation configuration.Defaultwhen omitted, animation is enabled with defaults (equivalent totrue). SeeOptionResolver.ts.Disablementset tofalseto disable all animation.Defaultswhen enabled,AnimationConfig.durationdefaults to300ms when omitted.
AnimationConfig- Initial-load intro animationwhen animation is enabled, series marks animate on first render. Axes, grid lines, and labels render immediately (not animated). Per-series effects: line/area series reveal left-to-right via plot scissor; bar series grow upward from baseline; pie slices expand radius; scatter points fade in. The intro animation requests frames internally during the transition. See
createRenderCoordinator.ts. Streaming demos may prefer disabling animation (animation: false). - Data update transition animationwhen animation is enabled, subsequent calls to
ChartGPUInstance.setOption(...)(and the internalRenderCoordinator.setOptions(...)) that change series data can animate transitions after the initial render has occurred. See the internal implementation increateRenderCoordinator.tsand the visual acceptance examples inexamples/data-update-animation/(bar + line + pie) andexamples/multi-series-animation/(area + bar + line + scatter on a single chart, with configurable line width).- When it triggers (high-level)a post-initial-render options update that changes
series[i].data(cartesian and pie), withChartGPUOptions.animationenabled. - What animates (high-level)
- Cartesian seriesy-values interpolate by index while x-values come from the new series (index-aligned). Bars morph via the same y interpolation.
- Pie seriesslice values interpolate by index, producing animated angle changes.
- Derived domains/scaleswhen auto-derived axis domains change (from updated data), the domain values animate to the new extents.
- Constraints / notes (high-level)
- Match-by-indexinterpolation is index-based; length changes and type/shape mismatches may skip interpolation and apply the new series immediately.
- Large-series safeguardvery large series may skip per-point interpolation while still animating derived domains (internal safeguard).
- Mid-flight updatesa new
setOption(...)during an active transition rebases the transition from the current displayed state (avoids a visual jump).
OHLC Data Types
OHLCDataPoint (public export): exported from the public entrypoint src/index.ts and defined in types.ts. Represents a candlestick data point as either a tuple (readonly [timestamp, open, close, low, high], ECharts order) or an object (Readonly<{ timestamp, open, close, low, high }>). Used by CandlestickSeriesConfig. Both candlestick rendering and OHLC sampling are fully functional (see CandlestickSeriesConfig above).
candlestickDefaults (public export): exported from the public entrypoint src/index.ts and defined in defaults.ts. Provides default configuration values for candlestick series. Both candlestick rendering and OHLC sampling are fully functional (see CandlestickSeriesConfig above).
Default Options
Default chart options used as a baseline for resolution.
See defaults.ts for the defaults (including grid, grid lines, palette, and axis defaults).
Behavior notes (essential):
- Default grid
left: 60,right: 20,top: 40,bottom: 40(non-candle-primary). Candle-primary soft gutters: see Candle-primary layout defaults under Axis Configuration (right: 80;left: 20or60depending on left Y axes). - Palette / series colors
ChartGPUOptions.paletteacts as an override for the resolved theme palette (resolvedOptions.theme.colorPalette). Whenseries[i].coloris missing, the default series color comes fromresolvedOptions.theme.colorPalette[i % ...]. For backward compatibility, the resolvedpaletteis the resolved theme palette. SeeresolveOptionsandThemeConfig. - Line series stroke color precedencefor
type: 'line', effective stroke color follows:lineStyle.color→series.color→ theme palette. SeeresolveOptions. - Line series fill color precedencefor
type: 'line'withareaStyle, effective fill color follows:areaStyle.color→ resolved stroke color (from above precedence). SeeresolveOptions. - Area series fill color precedencefor
type: 'area', effective fill color follows:areaStyle.color→series.color→ theme palette. SeeresolveOptions. - Axis ticks
AxisConfig.tickLengthcontrols tick length in CSS pixels (default: 6)
resolveOptions
resolveOptions(userOptions?: ChartGPUOptions)
// Alias:
OptionResolver.resolve(userOptions?: ChartGPUOptions)
Resolves user options against defaults by deep-merging user-provided values with defaults and returning a resolved options object.
See OptionResolver.ts for the resolver API and resolved option types.
Behavior notes (essential):
- Theme input
ChartGPUOptions.themeaccepts'dark' | 'light'or aThemeConfig; the resolvedthemeis always a concreteThemeConfig. SeeChartGPUOptionsandresolveOptions. - Default theme
- Theme name resolutionresolveOptions({ theme: 'light' })resolves
themeto the light preset config (seelightTheme). - Palette overridewhen
ChartGPUOptions.paletteis provided (non-empty), it overrides the resolved theme palette (resolvedOptions.theme.colorPalette). The resolvedpalettemirrors the resolved theme palette for backward compatibility. SeeresolveOptions.
Performance Metrics Types
PerformanceMetricsfps,frameTimeStats(min/max/avg/p50/p95/p99),gpuTiming,memory,frameDrops,totalFrames,elapsedTime. Returnsnullbefore first frame.PerformanceCapabilitiesgpuTimingSupported,highResTimerSupported,performanceMetricsSupported.- Branded types
API reference for the linked @chartgpu/chartgpu package (v0.4.0). Source on GitHub.