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 wickchartimport '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>
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" };
Series types #
Six render modes via the type attribute (or chart.setAttribute('type', …)):
| Value | Rendering |
|---|---|
candles | Solid candlesticks (default) |
line | Close-price line |
area | Line + gradient fill |
bars | Classic OHLC bars |
hollow | Hollow up-candles / solid down-candles |
heikin | Heikin-Ashi transform |
Attributes #
Everything is an attribute first — the component is usable from pure HTML. Remove an attribute (or set "false") to turn a toggle off.
| Attribute | Default | Description |
|---|---|---|
theme | dark | dark, light, or any registerTheme() name |
type | candles | Series type — see Series types |
indicators | volume* | 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") |
log | off | Logarithmic price scale |
auto | on | Follow the right edge while streaming |
precision | auto | Forced decimal places (0–12) |
stats | off | Visible-range statistics chip |
profile | off | Volume profile (POC + value area) |
annotations | off | Smart annotations (spikes, gaps, pivots, divergences) |
volshading | off | Volatility-regime background — details |
overlays | – | JSON array of zones & levels — details |
co-view | – | BroadcastChannel name: sync crosshair/markings across tabs |
sonify | off | Arrow keys play the chart as pitch (a11y) |
alert-evaluate | live | Default alert evaluation: live (every tick) or close (final candles only) — details |
timezone | local | Display zone for axis labels & the crosshair: local, utc, or an IANA name — details |
vwap-anchor | utc | Session boundary VWAP resets on — details |
lang | en | UI 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]
| Name | Params | Draws |
|---|---|---|
sma / ema | :20 period | Overlay line |
bb | :20 period (±2σ fixed) | Bollinger band (3 lines) |
vwap | – | VWAP overlay, resets each UTC day |
supertrend | :10/3 period/mult | Trend line that flips with ATR bands |
donchian | :20 period | High/low channel (3 lines) |
keltner | :20/2 period/mult | EMA ± ATR channel (3 lines) |
ichimoku | :9/26/52/26 tenkan/kijun/senkouB/disp | 5 lines + shaded kumo; senkou spans displaced ahead |
rsi | :14 | Own pane, 30/70 guides |
macd | :12/26/9 | Own pane: histogram + 2 lines |
stoch | :14/3 period/smooth | Own pane: %K + %D, 20/80 guides |
atr | :14 | Own pane: Wilder ATR |
obv | – | Own pane: on-balance volume |
cci | :20 | Own pane, ±100 guides |
wr | :14 | Own 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>
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@colorsuffix.- 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
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 -->
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
| Field | Type | Notes |
|---|---|---|
type | 'zone' | required |
from / to | timestamp · null | ms or seconds, snapped to bars. from null → left edge; to null → right edge, into the future |
priceFrom / priceTo | number | required (any order — auto-sorted) |
color | string | hex / rgb() / CSS name, or palette key up|down|accent |
alpha | number | fill opacity, clamped 0.02–0.8 (default 0.22) |
border | boolean | 1px border in the same color (default true) |
label / id | string | label drawn inside the zone / stable id for upserts |
Level
| Field | Type | Notes |
|---|---|---|
type | 'level' | required |
price | number | required — horizontal line |
from / to | timestamp · null | null → chart edge (default: full width) |
width / dash | number / boolean | line width 1–4 (default 1) / dashed (default solid) |
color / label / id | string | label 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>
overlays as a prop (fresh array → re-apply).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.
Stats, profile & annotations #
| Toggle | What you get |
|---|---|
stats | Visible-range analytics: return %, annualized vol, max drawdown, up/down bars, avg volume |
profile | Volume profile with POC + value area (VAH/VAL) over the visible range |
annotations | Auto-badged volume spikes, gaps, pivot highs/lows, RSI divergences — hover for a one-line insight |
| shift-drag | Measure tool: Δprice, ±%, Δtime, bar count (fires wick:measure) |
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
wick:select); wheel/keyboard still pan and zoom. Replacing the dataset clears a committed selection — indices are data-bound.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
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>
| Attribute | Meaning |
|---|---|
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 / limit | Timeframe (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. |
for | Chart 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.
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.
Events #
Custom events carry their payload in event.detail. (0.x hab:* names still fire as deprecated aliases.)
| Event | detail | Fires 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:annotations | annotation list | smart annotations recompute |
wick:measure | measure result | shift-drag measurement completes |
wick:worker | { key, epoch } | a worker-computed indicator lands (worker path) |
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):
| Variable | Controls |
|---|---|
--wick-bg | Chart background |
--wick-text / --wick-text-strong | Axis / legend text |
--wick-grid / --wick-border | Grid lines / borders |
--wick-up / --wick-down | Up / down candles |
--wick-accent | Crosshair, highlights, overlay fallback |
--wick-overlay / --wick-overlay-N | Indicator line palette (cycle or per-index) |
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 #
| Input | Action |
|---|---|
| scroll / pinch | Zoom around the cursor |
| drag | Pan (auto-follow releases when you pan away from the right edge) |
| shift-drag | Measure tool (Δprice, ±%, Δtime, bars) |
| double-click | Reset view / re-fit |
| click bar | wick:select |
← → + sonify | Walk 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):
| api | what it gives you |
|---|---|
ctx | The 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 } |
palette | Resolved theme colors (up/down/accent/text/border…) |
data, view | Normalized bars + { rightIndex, spacing } |
timeToX ⇄ xToTime | Bar time (ms or s) ⇄ x-pixel — extrapolates past the last bar into future space, so a trendline can point at tomorrow |
priceToY ⇄ yToPrice | Price ⇄ y-pixel in the main pane (log-aware, unclamped) |
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));
| Gesture | What happens |
|---|---|
| tool armed + drag | Creates the drawing (anchors magnet-snap to bar times & OHLC); click-only tools: level, text |
| text note | Placing 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 drawing | Selects it — handles appear, Delete removes it, Esc cancels a gesture |
| drag body / handle | Moves the whole drawing / re-anchors one point (undoable) |
| click empty space | Deselects — the gesture falls through to the chart (pan, brush, measure all keep working) |
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.
| Package | What it gives you |
|---|---|
| wickchart-draw | Drawing tools — trendline/ray/level/rect/fib/text with magnet snap, undo, JSON (de)serialization, optional cross-tab sharing |
| wickchart-sessions | Market-session shading — crypto/forex/NYSE/CME presets or custom windows, DST-correct, weekend tint, crosshair hover bridge |
| wickchart-replay | Bar replay — step/seek/play from any anchor, adjustable speed, loop, badge overlay, aborts cleanly when the dataset moves |
| wickchart-compare | Symbol overlays — up to 6 rebased series, ratio & diff derived series, legend chips with live values |
| wickchart-navigator | Range navigator — full-dataset silhouette docked below the chart, drag/resize/click-jump viewport (peer: wickchart ≥ 1.6) |
| wickchart-alerts-plus | Alert persistence + delivery — localStorage/any storage, WebAudio beep, hidden-tab Notification, webhook POST |
| wickchart-layouts | Named workspace snapshots — save/load/rename/delete/export/import of full chart state (+ drawings) |
| wickchart-signals | Candlestick signals — engulfing, pin bar, inside bar chips with crosshair explanations (setKinds([]) = off) |
| wickchart-tape | Time & 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-grid | Multi-chart layout + sync — shared visible ranges and ghost crosshairs, echo-suppressed fan-out |
| wickchart-paper | Paper trading on top of replay — honest fills (next-bar opens, gap-aware limits), equity curve, position mirroring |
| wickchart-narrator | Guided playback — the narrate() timeline, walk player, sonification and story tours (the narrate/story/sonify family) |
| wickchart-coview | Cross-tab co-viewing — presence bands, shared crosshair ghosts, the public BroadcastChannel protocol (the co-view family) |
| wickchart-scenario | Planning — σ-cone projections and R-multiple risk plans (the setScenario/setRiskPlan family) |
| wickchart-ai | The 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
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.x | 1.x |
|---|---|
<hab-chart> / <hab-feed> | <wick-chart> / <wick-feed> |
hab:* events | wick:* events |
--hab-* CSS vars | --wick-* (falls back automatically) |
HabChart / HabFeed classes | WickChart / WickFeed |
hab-co-view: channel | wick-co-view: |