WickChart docs

Getting started #

wickchart is a zero-dependency charting Web Component: one <wick-chart> element renders candles, indicators, live streams and overlays — anywhere a <div> works, no build step required.

npm

npm install wickchart
import 'wickchart';   // registers <wick-chart> (+ types included)

CDN — no install

<script type="module" src="https://unpkg.com/wickchart"></script>

Your first chart

With the declarative feed element this is the whole integration — the example below is live:

<script type="module" src="…/wickchart/feed"></script>

<wick-feed for="c1" demo="BTC" tf="1h"></wick-feed>
<wick-chart id="c1" label="BTC · 1h" indicators="sma:20 volume"></wick-chart>
Livedemo feed
Everything renders into a shadow-DOM canvas at device-pixel resolution with rAF batching — ~100 KB unminified, zero dependencies.

Optional: drawing tools

Drawings are not part of the core — they ship as a separate package, wickchart-draw (~8 KB gzipped, peer: wickchart ≥ 1.4), so you only pay for them if you use them. Same two install paths as the chart:

npm install wickchart-draw

import { attachDrawings } from 'wickchart-draw';
<!-- CDN — a plain module import, no build step -->
<script type="module">
  import { attachDrawings } from 'https://unpkg.com/wickchart-draw';
</script>

Then attachDrawings(chart) — the full tour (tools, magnet snap, sharing across tabs) is in the Drawings section. Drawing is only one of eight opt-in packages now — sessions, replay, compare, navigator, alerts-plus, layouts, signals and tape — see The plugin family.

Data & streaming #

Bars are plain objects. time accepts milliseconds or seconds (auto-detected); line-style data can use { time, value }.

const bars = [
  { time: 1700000000, open: 42, high: 43.5, low: 41.2, close: 43, volume: 1204 },
  // time in ms works too: 1_700_000_000_000
];

chart.setData(bars);   // full history (replaces)
chart.update(bar);      // stream: updates last bar or appends the next
chart.clearData();     // empty the chart
chart.data;             // getter — the live bar array

// cross-symbol spreads: register a second symbol, then reference its
// fields in WickScript as name_close, name_high, … (time-aligned, NaN in gaps)
chart.setSeries('eth', ethBars);
chart.clearSeries('eth');

Spread & ratio panes

A pane expression over a registered series is a spread chart with its own axis:

<wick-chart indicators="pexpr:{close - eth_close}"></wick-chart>
// ratio with a moving-average of the spread, same pane
<wick-chart indicators="pexpr:{close / eth_close}"></wick-chart>

The pane autoscales to its own values, so the spread's units never fight the price axis. Values are NaN where the two symbols' timestamps don't overlap.

Infinite history (backfill)

Assign onloadmore and the chart fetches older bars when the user scrolls past the left edge — the view stays anchored:

chart.onloadmore = async (fromTime) => {
  const older = await fetch(`/api/bars?before=${fromTime}`).then(r => r.json());
  return older;  // return [] to signal "no more"
};
Live
no data yet — press a button

Series types #

Six render modes via the type attribute (or chart.setAttribute('type', …)):

ValueRendering
candlesSolid candlesticks (default)
lineClose-price line
areaLine + gradient fill
barsClassic OHLC bars
hollowHollow up-candles / solid down-candles
heikinHeikin-Ashi transform
Live

Attributes #

Everything is an attribute first — the component is usable from pure HTML. Remove an attribute (or set "false") to turn a toggle off.

AttributeDefaultDescription
themedarkdark, light, or any registerTheme() name
typecandlesSeries type — see Series types
indicatorsvolume*Space/comma list: sma:20 ema:50 bb:20 vwap supertrend:10/3 ichimoku:9/26/52/26 rsi:14 macd:12/26/9 stoch:14/3 atr:14 obv cci:20 wr:14 volume, registered names, or WickScript expressions. "" disables all.
label–Legend text (e.g. "BTC · 1h")
logoffLogarithmic price scale
autoonFollow the right edge while streaming
precisionautoForced decimal places (0–12)
statsoffVisible-range statistics chip
profileoffVolume profile (POC + value area)
annotationsoffSmart annotations (spikes, gaps, pivots, divergences)
volshadingoffVolatility-regime background — details
overlays–JSON array of zones & levels — details
co-view–BroadcastChannel name: sync crosshair/markings across tabs
sonifyoffArrow keys play the chart as pitch (a11y)
alert-evaluateliveDefault alert evaluation: live (every tick) or close (final candles only) — details
timezonelocalDisplay zone for axis labels & the crosshair: local, utc, or an IANA name — details
vwap-anchorutcSession boundary VWAP resets on — details
langenUI string pack for built-in labels (en, de, or a registered pack) — details
preset–Chrome starting point: minimal (series only) or pro (stats + volume) — details

* indicators="" disables everything, including volume.

i18n & presets #

Built-in UI chrome — the empty state, the stats chip labels, the measure readout, the accessibility label — ships in string packs selected by lang (English and German built in; numbers and dates already follow the viewer's locale through Intl). Register your own:

import { WickChart } from 'wickchart';

WickChart.registerStrings('fr', {
  noData: 'Aucune donnée',
  bars: 'bougies',
});

Untranslated keys fall back to English; unknown lang codes fall back to English entirely.

preset is a starting point for the chrome, never an override — explicit attributes keep their word. preset="minimal" hides the legend, stats chip and volume pane (just the series, for embedding); preset="pro" turns the stats chip and volume pane on (the dense trading view).

Indicators #

Built-ins

Token syntax: name[:param[/param…]][@color]

NameParamsDraws
sma / ema:20 periodOverlay line
bb:20 period (±2σ fixed)Bollinger band (3 lines)
vwap–VWAP overlay, resets each UTC day
supertrend:10/3 period/multTrend line that flips with ATR bands
donchian:20 periodHigh/low channel (3 lines)
keltner:20/2 period/multEMA ± ATR channel (3 lines)
ichimoku:9/26/52/26 tenkan/kijun/senkouB/disp5 lines + shaded kumo; senkou spans displaced ahead
rsi:14Own pane, 30/70 guides
macd:12/26/9Own pane: histogram + 2 lines
stoch:14/3 period/smoothOwn pane: %K + %D, 20/80 guides
atr:14Own pane: Wilder ATR
obv–Own pane: on-balance volume
cci:20Own pane, ±100 guides
wr:14Own pane: Williams %R, −80/−20 guides
volume–Volume histogram pane
<wick-chart indicators="sma:20@#f0b429 ema:50 bb:20 rsi:14 volume"></wick-chart>

Custom indicators (registry)

Register once, use by name in the attribute. compute is a pure function of the bars; return an array of named series (pane: true moves it below price):

import WickChart from 'wickchart';

WickChart.registerIndicator('hlmid', {
  pane: false,                    // overlay on price (true = own pane)
  compute(bars, params) {
    const n = params[0] || 2;
    const values = bars.map((b, i) => {
      const win = bars.slice(Math.max(0, i - n + 1), i + 1);
      return win.reduce((s, x) => s + (x.high + x.low) / 2, 0) / win.length;
    });
    return [{ name: 'HL-mid', values }];   // or { lines: [{name, values}] }
  },
});

<wick-chart indicators="hlmid:3 volume"></wick-chart>
Live

WickScript expressions #

Custom indicators as inline expressions — parsed by a hand-written tokenizer + recursive-descent parser, never eval, and URL-safe.

<wick-chart indicators="expr:{(close - sma(close,20)) / sma(close,20) * 100}@#f0b429
               pexpr:{rsi(close,14) - 50}"></wick-chart>
  • expr:{…} draws on the price chart; pexpr:{…} gets its own pane. Optional @color suffix.
  • Series variables: open high low close volume hl2 hlc3 ohlc4
  • Functions: sma ema wma stddev rsi hh ll prev change abs sqrt log min max crossup crossdown vwap obv atr — e.g. hh(high,20), crossup(close, vwap()), close - atr(14)*2
  • Operators: + - * / %, comparisons > < >= <= == != (1/0), parentheses, unary minus
  • Caps: 512 chars, 128 tokens, nesting depth 24; window periods must be whole-number literals
Live

Volatility shading #

Background tint by realized-volatility percentile — calm vs hot markets at a glance, with the hovered regime in the legend.

<wick-chart volshading></wick-chart>                  <!-- 30/70 percentiles, 20-bar window -->
<wick-chart volshading="20/85"></wick-chart>        <!-- custom cutoffs -->
<wick-chart volshading="20/85/50"></wick-chart>     <!-- + 50-bar vol window -->
Live

Server-side overlays # NEW

Draw analysis from your own API straight onto the chart: zones (time × price rectangles) and levels (horizontal price lines), rendered behind the candles. Zones without a to extend into future space past the last bar, like TradingView drawings.

const res = await fetch('https://api.example.com/analysis?symbol=BTC');
chart.setOverlays(await res.json());

Zone

FieldTypeNotes
type'zone'required
from / totimestamp · nullms or seconds, snapped to bars. from null → left edge; to null → right edge, into the future
priceFrom / priceTonumberrequired (any order — auto-sorted)
colorstringhex / rgb() / CSS name, or palette key up|down|accent
alphanumberfill opacity, clamped 0.02–0.8 (default 0.22)
borderboolean1px border in the same color (default true)
label / idstringlabel drawn inside the zone / stable id for upserts

Level

FieldTypeNotes
type'level'required
pricenumberrequired — horizontal line
from / totimestamp · nullnull → chart edge (default: full width)
width / dashnumber / booleanline width 1–4 (default 1) / dashed (default solid)
color / label / idstringlabel drawn at the right edge above the line

API & attribute

chart.setOverlays(list);      // replace all (returns applied ids)
chart.addOverlay(overlay);     // add or replace by id (upsert)
chart.removeOverlay(id);
chart.clearOverlays();
chart.overlays;               // getter — normalized copies
<!-- fully declarative — server-rendered HTML works too -->
<wick-chart overlays='[{"type":"level","price":28700,"color":"#3f51b5","label":"S1"}]'></wick-chart>
Safety: overlays are API data — colors pass a strict validator, invalid entries are dropped (never thrown), timestamps outside history clamp to the first/last bar. In React, pass overlays as a prop (fresh array → re-apply).
Live playground
edit the JSON and re-apply — errors show below the chart

Timezone & sessions #

Axis labels and the crosshair readout use the viewer's own timezone by default. Set timezone to pin them somewhere specific — utc, or any IANA zone, DST included:

<wick-chart timezone="Europe/Stockholm"></wick-chart>
<wick-chart timezone="America/New_York"></wick-chart>
<wick-chart timezone="utc"></wick-chart>

Day dividers, month and year ticks all follow the chosen zone, so a "1 Feb" tick is 1 February there. An unrecognised zone falls back to UTC and warns once in the console.

VWAP session anchor

VWAP resets at a session boundary, and that boundary is not the display timezone — changing the axis to Stockholm should not silently re-anchor a BTC chart. It defaults to the UTC day, which is the crypto convention, and moves only when you say so:

<wick-chart indicators="vwap" vwap-anchor="America/New_York"></wick-chart>

Accepts utc (default), local, an IANA zone, or a fixed offset in milliseconds for an exchange session that doesn't start at local midnight. Equities, futures and FX rarely open at UTC midnight — the default is right for crypto and wrong for most other markets, so set it deliberately.

Positions & alerts #

Positions / orders

Stop/target zones with live P&L readout against the streaming price:

chart.addPosition({ id: 'p1', side: 'long', entry: 43100, stop: 42240, target: 44830, qty: 0.5 });
chart.removePosition('p1');  chart.clearPositions();

Alerts

Price alerts are edge-triggered on streaming closes. Scripted alerts take a WickScript predicate instead of a price — evaluated locally on every bar, firing on its false→true edge:

chart.addAlert({ id: 'a1', price: 43900, direction: 'above' });  // 'above' | 'below' | 'cross'
chart.addAlert({ when: 'crossup(rsi(close,14), 30)' });        // scripted predicate
chart.addAlert({ when: 'crossup(close, vwap())' });            // bar-level funcs work too
chart.addAlert({ when: 'volume > sma(volume,20) * 3', once: false });
chart.removeAlert('a1');  chart.clearAlerts();
chart.addEventListener('wick:alert', (e) => console.log(e.detail.id, e.detail.price));

Scripted events carry the triggering close as price plus the when source; once: false re-arms after the condition drops false again. Invalid predicates return null — never throw.

Live vs closed-candle alerts #

By default alerts evaluate on every update, including the still-forming candle. That is what you want for a price line, but a technical signal can repaint: RSI crosses 30 mid-candle, the alert fires, price reverses, and the candle closes with RSI back above 30 — a signal that, in hindsight, never happened.

Set evaluate: 'close' to evaluate only candles that are final:

chart.addAlert({ when: 'crossup(rsi(close,14), 30)', evaluate: 'close' });

Or set the default for a whole chart, still overridable per alert:

<wick-chart alert-evaluate="close"></wick-chart>

A candle counts as final once a newer bar arrives, or as soon as the feed says so by passing closed: true to update() — which <wick-feed> does automatically from Binance's k.x flag, so the signal lands at the close rather than one candle later. Historical corrections and backfilled candles never fire live alerts in either mode.

Live
positions/alerts fire wick:alert here

Stats, profile & annotations #

ToggleWhat you get
statsVisible-range analytics: return %, annualized vol, max drawdown, up/down bars, avg volume
profileVolume profile with POC + value area (VAH/VAL) over the visible range
annotationsAuto-badged volume spikes, gaps, pivot highs/lows, RSI divergences — hover for a one-line insight
shift-dragMeasure tool: Δprice, ±%, Δtime, bar count (fires wick:measure)
Live

Delta brush # NEW

Turn on brush and a plain drag selects bars instead of panning: a live band follows the pointer with a delta chip (Δ% · bars · high · low · Σvol), and on release the selection stays with the stats committed. The measure tool still works with shift-drag.

<wick-chart brush></wick-chart>

chart.addEventListener('wick:brush', (e) => {
  // e.detail = { bars, from: {index, time}, to: {index, time},
  //   delta, deltaPct, firstOpen, lastClose, high, low, volume }
});
chart.brushSelection;   // getter — { i0, i1, stats } or null
chart.clearBrush();        // Esc does the same
Brush mode replaces plain-drag panning (and plain clicks select a 1-bar range instead of firing wick:select); wheel/keyboard still pan and zoom. Replacing the dataset clears a committed selection — indices are data-bound.
Live playground
drag across the candles — release to commit · Esc clears

AI-ready data window #

getDataWindow() turns the visible range into a compact, paste-anywhere summary — trend (least-squares drift + fit), vol percentile, SMA/RSI snapshot, patterns. All computed locally.

const s = chart.getDataWindow();
s.text;       // markdown — paste into any AI chat
s.trend;      // { label: 'strong uptrend', slopePctPerBar: 0.77, r2: 0.94 }
s.volPctile;  // 84 → hot regime, relative to the window
s.patterns;   // [{ time, note }] — most recent first
Live
The agent interface that drives the chart with validated tool-calls (aiTools(), aiPrompt(), aiContext(), applyAI(), ask()) lives in the wickchart-ai plugin — along with the narrator/story, co-view and scenario/risk-plan families on the plugins hub.

Feeds — <wick-feed> #

A declarative data source that pairs with any chart — a fully live chart with zero JavaScript written.

<script type="module" src="https://unpkg.com/wickchart/feed"></script>

<wick-feed for="c" binance="BTCUSDT" tf="1h"></wick-feed>
<wick-chart id="c"></wick-chart>
AttributeMeaning
binance="SYMBOL"Live Binance (REST klines + WebSocket stream; falls back to REST polling, then a synthetic stream when the network blocks it)
demo="KEY"Deterministic offline synthetic feed (BTC/ETH/SOL base prices; any other key seeds a fresh series)
url="ENDPOINT"Generic REST JSON array of bars; optional poll="SECONDS"
tf / limitTimeframe (1m…1w) / initial bar count (default 500)
aggregate="KIND[:N]"Information-based bars from a trade stream — tick:200 (N prints), volume:50 (N base units) or dollar:50000 ($N traded) per bar. See below.
forChart id to pair with (auto-pairs with the first chart otherwise)
live="false"Disable streaming after the initial load

Status is reflected in the status attribute and via wick-feed:status events (loading / live / polling / fallback / loaded). Feeds also wire onloadmore backfill automatically for Binance sources.

Information-based bars (advanced bars)

Add aggregate to any feed and bars close on information, not the clock — the quant-grade alternative to time candles, in one attribute:

<wick-feed for="c" binance="BTCUSDT" aggregate="dollar:50000"></wick-feed>
<wick-chart id="c" indicators="volume"></wick-chart>

With binance= the feed switches to the raw trade tape (aggTrade WebSocket, paginated aggTrades REST for the seed and onloadmore backfill, REST polling as the degraded path); with demo= it runs on deterministic synthetic prints offline; with url= it polls a JSON trades array ({ time, price, size }; qty/amount/t aliases accepted). The completing print belongs entirely to its bar — a whale trade is never split. Thresholds are per-instrument: there is no universal "one bar" size, and the value you pass is the bar size (default tick:100, volume:10, dollar:25000).

To pipe your own trade stream, import the machinery directly — import { TickBarAggregator, aggregateTrades } from 'wickchart/feed' — feed prints to add(), and stream the returned bars into chart.update() as they close.

Worker compute path #

Built for million-bar histories. One extra import plus one attribute, and the built-in indicators compute in a Web Worker: the main thread ships the dataset once per bulk load as six transferable Float64Arrays (~25 ms per million bars — a structured clone of bar objects would cost ~1 s), then posts indicator tasks that reference it. First paint of every SMA/Bollinger/SuperTrend/… line happens off-thread; wick:worker fires as each result lands.

import 'wickchart/worker';   // once — wires a shared worker pool into the chart class

<wick-chart worker indicators="sma:20 bb:20 rsi:14"></wick-chart>
// chart.setData(millionBars) — indicators land a few frames later, off the main thread

Rules of the road: the path engages at worker + a dataset of 50k+ bars + built-in indicators only (custom registerIndicator and WickScript defs are closures — they cannot cross the worker boundary and stay synchronous, as does everything below the threshold). Results are cached per data epoch — a bulk load — so streamed ticks stop recomputing the full series per bar; the forming bar's indicator value catches up on the next load. If no worker can spawn (old browsers, blocked contexts) every chart silently stays on the synchronous path: the attribute is an optimization, never a dependency. A wick:worker { key, epoch } event fires on the chart when a result lands.

Incremental tick updates come along automatically — with or without the worker: a streamed tick (appending a bar or replacing the forming one) patches every online-capable series by recomputing a bounded tail with the same batch definition — O(warm-up) ≈ 0.1 ms instead of a full-history recompute per indicator per tick — and writes only the last period values, so deeper history keeps its exact full-compute values. On worker-computed bases this means the forming bar's value stays fresh instead of waiting for the next bulk load. Cumulative indicators (obv, vwap) and the stateful supertrend are excluded and keep the full-recompute behavior.

1M-bar demo: demo/worker.html loads a million synthetic bars in worker and sync mode side by side, with live main-thread freeze numbers.

Report export #

The branded snapshot: chart + visible-range stats + watermark, composed into one shareable PNG — from public surfaces only (exportPNG(), getVisibleRange(), the chart's own --wick-* variables for theming), so it's an opt-in entry with zero core changes.

import { exportReport, downloadReport } from 'wickchart/report';

const url  = await exportReport(chart);            // PNG data URL
const blob = await exportReport(chart, { as: 'blob' });
await downloadReport(chart, 'btc-1h.png', {
  source: 'binance: BTCUSDT',   // credited in the footer
  title: 'BTC · 1h',            // default: the chart's label
  theme: 'dark' | 'light' | 'auto', // auto reads the chart's CSS
  scale: 2,                      // 1..4 (default 2)
});

The header band carries the title, the visible range and the brand; the chart keeps its full DPR resolution with a corner watermark; the stats band is a grid over the visible window (return, annualized vol, max drawdown, bars, up/down, average volume, high, low — the same computeStats the stats panel uses); the footer credits source and timestamp. The model (reportModel(chart, opts)) is exported too — plain data, unit-testable, if you want to render your own layout.

Playground
compose a report from the visible range — preview it, or download the PNG

Events #

Custom events carry their payload in event.detail. (0.x hab:* names still fire as deprecated aliases.)

EventdetailFires when
wick:range{ from, to } (ms)visible range changes (pan, zoom, stream)
wick:select{ time, price, bar }a bar is clicked
wick:crosshair{ time, price }crosshair moves
wick:alert{ id, price, bar }a price alert crosses (edge-triggered)
wick:annotationsannotation listsmart annotations recompute
wick:measuremeasure resultshift-drag measurement completes
wick:worker{ key, epoch }a worker-computed indicator lands (worker path)
Live event logpan / zoom / click this chart

State & shareable URLs #

const state = chart.getState();   // { type, theme, log, stats, profile, annotations,
                                  //   volshading, indicators, view }
chart.setState(state);              // restore (partial ok)

import { encodeStateQuery, decodeStateQuery } from 'wickchart/core';
location.hash = encodeStateQuery(chart.getState());   // → URL-safe query string

Query keys: type theme log stats profile ann vsh ind from to. Pattern for shareable links — #type=candles&ind=sma:20,rsi:14&from=…&to=…&vsh=1.

Theming #

theme="dark|light" — or any registered registerTheme() name — for the presets, then override any slot with CSS custom properties on the element (0.x --hab-* names still work as fallbacks):

VariableControls
--wick-bgChart background
--wick-text / --wick-text-strongAxis / legend text
--wick-grid / --wick-borderGrid lines / borders
--wick-up / --wick-downUp / down candles
--wick-accentCrosshair, highlights, overlay fallback
--wick-overlay / --wick-overlay-NIndicator line palette (cycle or per-index)
Live

Named custom themes — registerTheme()

Ship a palette as a theme name instead of per-element CSS variables. registerTheme(name, palette, opts?) — from 'wickchart' or 'wickchart/core' — merges a partial palette over { base: 'dark' | 'light' } (default dark): unknown keys drop, a string overlay expands to the full indicator palette while an array pads with the base colors, volAlpha coerces, re-registering a name overwrites it live (built-in names are replaceable too), and registration is global — every chart on the page resolves the name. Re-registering a built-in name does not change report export colors — the report keeps its own tuned light/dark palettes.

import { registerTheme } from 'wickchart/core';

registerTheme('matrix', {
  bg: '#001100', text: '#00ff66', up: '#00ff66', down: '#ff2d2d',  // partial — the rest comes from the base
});

// the name then works as an attribute:
<wick-chart theme="matrix"></wick-chart>

--wick-* variables still override the registered palette slot by slot; the PNG report export follows the chart's registered theme; saved layouts and shareable presets round-trip the name. Declarative-flow note: with the HTML-attribute + module-script pattern the chart may render once before your script registers the theme — the name resolves at the next render (the setData call), not retroactively.

Interactions & accessibility #

InputAction
scroll / pinchZoom around the cursor
dragPan (auto-follow releases when you pan away from the right edge)
shift-dragMeasure tool (Δprice, ±%, Δtime, bars)
double-clickReset view / re-fit
click barwick:select
← → + sonifyWalk bars as audible pitch — screen-reader-friendly trend reading
+ −Zoom in / out

The canvas carries an aria-label with the latest price, change % and bar count; crosshair + legend keep a text readout for pointer users.

React, Vue, Svelte #

The element is framework-agnostic. For React there is an official binding — react is an optional peer dependency:

npm install wickchart react
import { WickChart } from 'wickchart/react';

<WickChart type="candles" indicators="sma:20" volshading
           data={bars} overlays={zones}
           onRange={fn} onAlert={fn} />

Props map 1:1: primitives → attributes, data/overlays → setData()/setOverlays() (pass a fresh array to update), onXxx → wick:xxx with unmount cleanup. The live React demo runs without a build step.

<!-- Vue 3 -->
<wick-chart ref="chart" type="candles" indicators="sma:20"></wick-chart>
// onMounted: chart.value.data = bars; chart.value.addEventListener('wick:range', fn)

<!-- Svelte -->
<wick-chart bind:this={el} indicators="sma:20" on:wick:alert={fn}></wick-chart>
// $: if (el && bars.length) el.data = bars;

Plugin layers # NEW

A layer is an external draw hook: addLayer() paints into the render pipeline — above chart content, under the crosshair — and can claim pointer gestures so drags reach your code instead of panning the chart. It is the whole extension surface: markers, watermarks, signal badges, or a full drawing toolkit plug in without the core growing a single tool of its own.

const handle = chart.addLayer({
  id: 'my-layer',               // optional; same id replaces
  draw(api) {
    const { ctx, palette: pal, layout } = api;
    const x = api.timeToX(bar.time);   // anchors move with the chart
    const y = api.priceToY(price);
    ctx.strokeStyle = pal.accent;
    // …paint in CSS pixels; ctx is already DPR-scaled
  },
  onPointer(ev) {
    // ev = { type: 'down'|'move'|'up'|'cancel', x, y, pointerId, shiftKey… }
    if (ev.type === 'down' && hitsMyContent(ev)) return true; // claim
  },
});
chart.removeLayer(handle);  // or removeLayer('my-layer')
chart.requestDraw();        // repaint hook for interactive layers

Returning true from a down claims that gesture: the layer receives the pointer's move/up (a cancel on Escape or pointercancel) while the chart suppresses pan, brush and measure for it. Layers are asked in registration order — first claim wins; a throwing layer is isolated with a console warning and never breaks the render.

A layer may also declare insetBottom (px, 0–160): the largest declared inset reserves a docked strip at the very bottom of the canvas — panes and the time axis shrink above it, and the strip is handed to layers as layout.dock = { y0, h }. This is how the navigator's silhouette and the tape's print strip dock; the two share that one strip, so attach one or the other.

The draw api carries live state plus the four coordinate transforms (also public methods on the element, for use in event handlers):

apiwhat it gives you
ctxThe canvas 2D context — DPR-scaled, draw in CSS pixels
layout{ W, H, priceW, timeH, plotRight, plotBottom, main: {y0,y1,h}, panes, dock: {y0,h} | null }
paletteResolved theme colors (up/down/accent/text/border…)
data, viewNormalized bars + { rightIndex, spacing }
timeToX ⇄ xToTimeBar time (ms or s) ⇄ x-pixel — extrapolates past the last bar into future space, so a trendline can point at tomorrow
priceToY ⇄ yToPricePrice ⇄ y-pixel in the main pane (log-aware, unclamped)
Live playground
pick a layer — both are ~25 lines of ordinary JS
This is the surface the wickchart-draw toolkit builds on — trendlines, fibs, rectangles and text as an opt-in package, while the core stays drawing-free. The layer API itself is ~2 KB gzipped and covered by a CI size budget.

Drawings # NEW

wickchart-draw is an opt-in package, not core bytes: trendlines (segment / ray / infinite), horizontal levels, rectangles, fibonacci retracements and text notes — built entirely on the plugin layer API, ~8 KB gzipped under its own CI budget. Drawings are plain { time, price } data: they ride zoom & pan, survive data reloads, extrapolate into future space, and serialize to JSON.

Install it separately from the chart — npm install wickchart-draw, or import … from 'https://unpkg.com/wickchart-draw' straight from the CDN (peer: wickchart ≥ 1.4, already on the page from Getting started):

import { attachDrawings } from 'wickchart-draw';  // npm i wickchart-draw — a separate package

const draw = attachDrawings(chart, { magnet: true });
draw.setTool('trendline');  // arm a tool — dragging draws instead of panning
draw.setTool(null);       // select mode: click to select, drag to move,
                       //   drag the square handles to re-anchor
draw.getDrawings();          // → JSON array — save it
draw.setDrawings(saved);
draw.undo(); draw.clear();

chart.addEventListener('wick:drawings', (e) => save(e.detail.drawings));
GestureWhat happens
tool armed + dragCreates the drawing (anchors magnet-snap to bar times & OHLC); click-only tools: level, text
text notePlacing a note opens an inline editor — type and hit Enter (Esc cancels, empty deletes); click a selected note again to re-edit
select mode + click on a drawingSelects it — handles appear, Delete removes it, Esc cancels a gesture
drag body / handleMoves the whole drawing / re-anchors one point (undoable)
click empty spaceDeselects — the gesture falls through to the chart (pan, brush, measure all keep working)
Live playground
pick a tool and drag on the chart — drawings anchor in time & price
The package ships as wickchart-draw on npm (peer: wickchart ≥ 1.4) with its own gzip budget — the drawing toolkit can never silently bloat the core. Locked/invisible flags, per-drawing color and width ride along in the JSON. Pass share: true (or a room name) to sync drawings across tabs over BroadcastChannel — this playground shares the docs-draw room, so open the page in a second window and draw: every trendline, fib and note appears on both charts. Last writer wins; remote updates fire wick:drawings with action remote and never touch the local undo stack.

The plugin family # NEW

Fifteen opt-in packages now build on the layer API — each one a separate npm package, zero dependencies, wired through public surface only and kept small by its own CI gzip budget. The core chart stays plugin-free; you pay bytes only for what you attach. The four newest (narrator, coview, scenario, ai) are the leading edge of the 2.0 split: they document features that core still carries, and become their only home at 2.0.

PackageWhat it gives you
wickchart-drawDrawing tools — trendline/ray/level/rect/fib/text with magnet snap, undo, JSON (de)serialization, optional cross-tab sharing
wickchart-sessionsMarket-session shading — crypto/forex/NYSE/CME presets or custom windows, DST-correct, weekend tint, crosshair hover bridge
wickchart-replayBar replay — step/seek/play from any anchor, adjustable speed, loop, badge overlay, aborts cleanly when the dataset moves
wickchart-compareSymbol overlays — up to 6 rebased series, ratio & diff derived series, legend chips with live values
wickchart-navigatorRange navigator — full-dataset silhouette docked below the chart, drag/resize/click-jump viewport (peer: wickchart ≥ 1.6)
wickchart-alerts-plusAlert persistence + delivery — localStorage/any storage, WebAudio beep, hidden-tab Notification, webhook POST
wickchart-layoutsNamed workspace snapshots — save/load/rename/delete/export/import of full chart state (+ drawings)
wickchart-signalsCandlestick signals — engulfing, pin bar, inside bar chips with crosshair explanations (setKinds([]) = off)
wickchart-tapeTime & sales — live trade prints docked below the chart, tick-rule side inference, toBars() feeds the chart from a trade stream (peer: wickchart ≥ 1.6)
wickchart-gridMulti-chart layout + sync — shared visible ranges and ghost crosshairs, echo-suppressed fan-out
wickchart-paperPaper trading on top of replay — honest fills (next-bar opens, gap-aware limits), equity curve, position mirroring
wickchart-narratorGuided playback — the narrate() timeline, walk player, sonification and story tours (the narrate/story/sonify family)
wickchart-coviewCross-tab co-viewing — presence bands, shared crosshair ghosts, the public BroadcastChannel protocol (the co-view family)
wickchart-scenarioPlanning — σ-cone projections and R-multiple risk plans (the setScenario/setRiskPlan family)
wickchart-aiThe agent interface — tool manifest, system prompt, grounding context, validated dispatcher (the aiTools/applyAI/ask family)

All of them follow the same shape — attach, configure, detach — and compose freely (the demo toolbar toggles most of them):

import { attachDrawings }  from 'wickchart-draw';
import { attachSessions }  from 'wickchart-sessions';
import { attachSignals }   from 'wickchart-signals';
import { attachNavigator } from 'wickchart-navigator';

const chart = document.querySelector('wick-chart');
const draw  = attachDrawings(chart);
attachSessions(chart, { preset: 'crypto' });
const sig   = attachSignals(chart, { kinds: [] }); // armed by your UI
const nav   = attachNavigator(chart);              // docks the bottom strip

chart.addEventListener('wick:drawings', (e) => save(e.detail.drawings));
chart.addEventListener('wick:signals',  (e) => status.textContent = e.detail?.label ?? '');
nav.detach();  // everything detaches cleanly — the core never knew it was there
Version notes: the navigator and the tape build on the 1.6 insetBottom dock hook (peer: wickchart ≥ 1.6); everything else targets ≥ 1.4. The navigator and the tape share the single bottom dock — attach one or the other. Every one of these packages has its own chapter with a live playground on the Plugins hub, and each is size-budgeted in this repo's CI, so none of them can silently bloat.

Performance & export #

  • ~0.2 ms per frame at default zoom (Canvas 2D, rAF-batched invalidation, DPR-aware)
  • Indicator results cached per data version; O(1) streaming updates via update()
  • History backfill shifts the view anchor — no full refit
  • chart.exportPNG() returns a PNG data-URL of the current render (crosshair excluded)
  • chart.fit() / getVisibleRange() / setVisibleRange({from,to}) for programmatic control

Migrating from 0.x (HabView) #

1.0 renamed the public surface; every 0.x name still works as a deprecated alias (removal planned for 2.0):

0.x1.x
<hab-chart> / <hab-feed><wick-chart> / <wick-feed>
hab:* eventswick:* events
--hab-* CSS vars--wick-* (falls back automatically)
HabChart / HabFeed classesWickChart / WickFeed
hab-co-view: channelwick-co-view: