wickchart 1.5.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # WickChart
2
2
 
3
+ [![npm](https://img.shields.io/npm/v/wickchart)](https://www.npmjs.com/package/wickchart)
4
+
3
5
  **`<wick-chart>` — a modern, simpler, more useful charting web component.**
4
6
 
5
7
  A TradingView-style financial chart as a single framework-agnostic Web Component.
@@ -62,6 +64,7 @@ with **zero JavaScript written**:
62
64
  | `tf` | timeframe: `1m 3m 5m 15m 30m 1h 2h 4h 6h 12h 1d 3d 1w` |
63
65
  | `limit` | initial bars (default 500) |
64
66
  | `live` | `live="false"` loads history without streaming |
67
+ | `aggregate` | information-based bars from a trade stream (see below) |
65
68
 
66
69
  The element reflects its state in the `status` attribute (`loading`, `live`,
67
70
  `polling`, `fallback`, `loaded`, `waiting`, `idle`) and emits
@@ -70,6 +73,78 @@ The element reflects its state in the `status` attribute (`loading`, `live`,
70
73
  synthetic stream bridged from the last real price, so the chart never goes
71
74
  blank. It also wires `chart.onloadmore` for infinite backfill automatically.
72
75
 
76
+ ### Information-based bars (advanced bars)
77
+
78
+ Add `aggregate` to any feed and bars close on *information*, not the clock:
79
+
80
+ ```html
81
+ <wick-feed for="c" binance="BTCUSDT" aggregate="dollar:50000"></wick-feed>
82
+ <wick-chart id="c" indicators="volume"></wick-chart>
83
+ ```
84
+
85
+ `tick:200` closes a bar every 200 prints, `volume:50` every 50 base units,
86
+ `dollar:50000` every $50k traded — the quant-grade alternative to time
87
+ candles, built client-side from the raw trade tape (Binance aggTrade
88
+ WebSocket + paginated REST backfill; offline synthetic prints with `demo=`;
89
+ your own JSON trades endpoint with `url=` + `poll=`). The value you pass *is*
90
+ the bar size — tune it per instrument. The machinery is exported too:
91
+ `import { TickBarAggregator, aggregateTrades } from 'wickchart/feed'` to pipe
92
+ any trade stream through the same aggregator.
93
+
94
+ ## Web Worker compute path (1M-bar histories)
95
+
96
+ One extra import, one attribute — and the built-in indicators compute in a
97
+ Web Worker, built for million-bar histories:
98
+
99
+ ```js
100
+ import 'wickchart/worker'; // once — wires a shared worker pool into the chart
101
+
102
+ <wick-chart worker indicators="sma:20 bb:20 rsi:14"></wick-chart>
103
+ ```
104
+
105
+ The dataset crosses once per bulk load as six transferable `Float64Array`s
106
+ (~25 ms per million bars; a structured clone of bar objects would cost ~1 s),
107
+ and indicator tasks reference it worker-side. First paint of every indicator
108
+ line happens off the main thread; `wick:worker` fires as results land.
109
+ Engages at 50k+ bars with built-in indicators (custom/scripted defs are
110
+ closures and stay sync, as does everything below the threshold); results are
111
+ cached per data epoch, so streamed ticks stop recomputing the full series per
112
+ bar. No worker available? Everything silently stays synchronous — the
113
+ attribute is an optimization, never a dependency. Live demo with freeze
114
+ numbers: **[demo/worker.html](./demo/worker.html)**.
115
+
116
+ **Incremental tick updates** (automatic, worker or not): a streamed tick —
117
+ appending a bar or replacing the forming one — patches every online-capable
118
+ series by recomputing a bounded tail with the *same* batch definition
119
+ (O(warm-up) ≈ 0.1 ms, not O(full history)) and only writing the last
120
+ `period` values, so history is never degraded by tail warm-up error. This
121
+ works on worker-computed bases too: the forming bar's indicator value stays
122
+ fresh instead of waiting for the next bulk load. Cumulative indicators
123
+ (`obv`, `vwap`) and the stateful `supertrend` are excluded and keep the
124
+ full-recompute behavior.
125
+
126
+ ## Report export (branded snapshots)
127
+
128
+ One shareable PNG — chart, visible-range stats, watermark — composed from
129
+ public surfaces only, as an opt-in entry:
130
+
131
+ ```js
132
+ import { exportReport, downloadReport } from 'wickchart/report';
133
+
134
+ const url = await exportReport(chart); // PNG data URL
135
+ const blob = await exportReport(chart, { as: 'blob' });
136
+ await downloadReport(chart, 'btc-1h.png', { source: 'binance: BTCUSDT' });
137
+ ```
138
+
139
+ The header carries the title (the chart's `label` by default), the visible
140
+ range and the brand; the chart keeps its full DPR resolution with a corner
141
+ watermark; the stats grid covers the visible window (return, annualized
142
+ vol, max drawdown, bars, up/down, average volume, high, low); the footer
143
+ credits your `source` and a timestamp. Theme follows the chart's own
144
+ `--wick-*` CSS variables (or `theme: 'dark' | 'light'`), scale 1–4.
145
+ `reportModel(chart, opts)` is exported too — plain data, if you want your
146
+ own layout.
147
+
73
148
  ---
74
149
 
75
150
  ## Why another chart library?
@@ -78,7 +153,8 @@ TradingView's charting library is powerful but heavy and enterprise-licensed;
78
153
  most wrappers add build steps and framework lock-in. WickChart takes the opposite
79
154
  bet:
80
155
 
81
- - **Zero dependencies, single file** (~40 KB unminified, no build step required)
156
+ - **Zero dependencies, no build step required** (~67 KB gzipped for the whole
157
+ component — `core.js` + `wick-chart.js`, held to a 68 KB CI budget)
82
158
  - **One tag, sane defaults** — drop it in and it renders; everything optional
83
159
  - **Built-in usefulness** — crosshair + OHLC legend, last-price line, wheel zoom,
84
160
  drag pan, pinch, keyboard navigation, live streaming, PNG export
@@ -97,7 +173,10 @@ chart, the full interactive demo, the zero-JavaScript declarative page, and a
97
173
  [benyblack.github.io/wickchart/docs.html](./docs.html)** — every attribute,
98
174
  method, event, the WickScript reference, overlays (with a live JSON
99
175
  playground), feeds, theming and framework bindings, each with runnable
100
- examples. This README covers the same ground in plain markdown.
176
+ examples. The **[Plugins hub](./plugins.html)** documents every opt-in
177
+ package — draw, sessions, replay, compare, navigator, alerts+, layouts,
178
+ signals, tape, grid, paper — each with its own live playground. This README covers
179
+ the same ground in plain markdown.
101
180
 
102
181
  ## Run the demo locally
103
182
 
@@ -707,6 +786,217 @@ writer wins, remote updates never touch the local undo stack. Peer
707
786
  dependency: wickchart ≥ 1.4. See the live playground in the docs (Drawings
708
787
  section — it shares a room, so open it twice and draw on either chart).
709
788
 
789
+ ### Sessions — the `wickchart-sessions` plugin
790
+
791
+ Market session shading as opt-in bytes (~6 KB gz, own CI budget): Asia /
792
+ London / New York and other sessions drawn as translucent bands, with labels,
793
+ closed-weekend shading for equities/futures, and crosshair hover events.
794
+ Presets for crypto & forex use the common UTC convention; equity/futures
795
+ presets use IANA timezones, so 09:30 is the real 09:30 across DST changes.
796
+ Custom defs (`{ name, start, end, tz?, days?, color?, alpha? }`) cover
797
+ midnight-crossing sessions and weekday filters.
798
+
799
+ ```js
800
+ npm install wickchart wickchart-sessions // sessions are a separate opt-in package
801
+
802
+ import { attachSessions } from 'wickchart-sessions';
803
+
804
+ const sessions = attachSessions(chart, { preset: 'crypto' });
805
+ sessions.setPreset('nyse'); // 'crypto' | 'forex' | 'nyse' | 'cme' | null
806
+ sessions.setSessions([...]); // custom defs (validated; getSessions() → JSON)
807
+ sessions.setWeekends(true); // shade closed Sat+Sun (default for nyse/cme)
808
+ chart.addEventListener('wick:sessions', (e) => status.textContent = e.detail.hover || '');
809
+ ```
810
+
811
+ The hover bridge listens to the chart's own crosshair events, so shading
812
+ never claims a pointer gesture — pan/zoom/measure work untouched. Peer
813
+ dependency: wickchart ≥ 1.4.
814
+
815
+ ### Replay — the `wickchart-replay` plugin
816
+
817
+ Bar replay as opt-in bytes (~3 KB gz, own CI budget): play history forward
818
+ bar-by-bar or at speed while the future stays hidden. The whole engine runs
819
+ on the public data API — a `setData` slice hides the future, `update()`
820
+ appends one bar per step — so the core stays replay-free. A badge layer shows
821
+ the mode and position at a glance.
822
+
823
+ ```js
824
+ npm install wickchart wickchart-replay // replay is a separate opt-in package
825
+
826
+ import { attachReplay } from 'wickchart-replay';
827
+
828
+ const replay = attachReplay(chart);
829
+ replay.start(); // head at ~70% of the data (or pass a time/index)
830
+ replay.play(); // 4 bars/sec — play(15) for faster, pause() stops
831
+ replay.step(); // reveal one bar
832
+ replay.seek('2026-03-06'); // jump the head
833
+ replay.setLoop(true); // wrap to the anchor at the end
834
+ replay.stop(); // exit — the full dataset is restored
835
+ chart.addEventListener('wick:replay', (e) => progress.textContent =
836
+ e.detail.active ? `${e.detail.index + 1}/${e.detail.total}` : '');
837
+ ```
838
+
839
+ Anchors accept bar indices, timestamps (ms/s) or date strings; every change
840
+ fires `wick:replay` with the full state. Pause live feeds while replaying —
841
+ an external `update()`/`setData()` aborts replay instead of corrupting the
842
+ chart (the demo pauses its feed automatically). Paper trading and an equity
843
+ curve are the planned 0.2 follow-up. Peer dependency: wickchart ≥ 1.4.
844
+
845
+ ### Compare — the `wickchart-compare` plugin
846
+
847
+ Normalized multi-asset overlays as opt-in bytes (~4 KB gz, own CI budget):
848
+ percent-rebased compare lines (ETH against BTC, TradingView-style) plus
849
+ derived **ratio** and **diff** lines (`BTC/ETH`, `BTC−ETH`), drawn over the
850
+ main pane against their own invisible scale so the price axis is untouched.
851
+ A legend chip row shows each series with its live value.
852
+
853
+ ```js
854
+ npm install wickchart wickchart-compare // compare is a separate opt-in package
855
+
856
+ import { attachCompare } from 'wickchart-compare';
857
+
858
+ const cmp = attachCompare(chart);
859
+ cmp.setSeries([
860
+ { label: 'ETH', data: ethBars }, // OHLC or {time, value}
861
+ { label: 'BTC/ETH', op: 'ratio', a: btcBars, b: ethBars }, // derived
862
+ ]);
863
+ cmp.setRebase('visible'); // 0% at the window edge, re-normalized while
864
+ // panning; 'first' or an epoch anchor also work
865
+ cmp.clear(); cmp.detach();
866
+ ```
867
+
868
+ Series are sampled onto the main chart's bar times, so timeframes can mix
869
+ and gaps break the line instead of bridging. Rebased values share one
870
+ invisible scale inset 8% from the pane edges; the price scale is never
871
+ distorted. Validated, capped at 6 series, invalid entries dropped. Peer
872
+ dependency: wickchart ≥ 1.4.
873
+
874
+ ### Navigator — the `wickchart-navigator` plugin
875
+
876
+ The most-missed TradingView affordance: a silhouette of the whole dataset
877
+ docked below the chart with a draggable viewport window (~3 KB gz, own CI
878
+ budget). Drag the window to pan, grab an edge to resize, click outside it to
879
+ jump — pan/zoom and the window stay in sync live, both directions.
880
+
881
+ ```js
882
+ npm install wickchart wickchart-navigator // navigator is a separate opt-in package
883
+
884
+ import { attachNavigator } from 'wickchart-navigator';
885
+ const nav = attachNavigator(chart, { height: 46 }); // strip height, 24..120
886
+ nav.detach(); // remove the strip again
887
+ ```
888
+
889
+ The strip needs bottom space, so this plugin pairs with a small core hook:
890
+ a layer may declare `insetBottom` (px) — the largest declared inset reserves
891
+ a docked strip at the bottom of the canvas, panes and the time axis shrink
892
+ above it, and layers draw it as `api.layout.dock`. On charts without the
893
+ hook the navigator degrades silently. The silhouette is O(n) once per
894
+ (dataset, width) and cached. Peer dependency: wickchart ≥ 1.6.
895
+
896
+ ### Alerts+ — the `wickchart-alerts-plus` plugin
897
+
898
+ The "pro" alert tier (~3 KB gz, own CI budget). Core alerts are runtime-only
899
+ by design; this adds what a trading tool actually needs, without the core
900
+ growing any of it: **persistence** (the alert list mirrors into
901
+ localStorage and re-arms on reload), **desktop notifications + a WebAudio
902
+ beep** while the tab is hidden, and an optional **webhook** that receives
903
+ every fire as `POST { id, price, when, time, bar, key }`.
904
+
905
+ ```js
906
+ npm install wickchart wickchart-alerts-plus // alerts-plus is a separate opt-in package
907
+
908
+ import { attachAlertsPlus } from 'wickchart-alerts-plus';
909
+ const ap = attachAlertsPlus(chart, {
910
+ key: 'BTC:1h', // one storage key per symbol+timeframe
911
+ notify: true, sound: true, // hidden-tab surfacing
912
+ webhook: 'https://example.com/hook', // optional
913
+ });
914
+ await ap.requestNotify(); // ask for the notification permission
915
+ ap.add({ price: 100, direction: 'above' }); // persisted, re-armed on reload
916
+ ap.add({ when: 'rsi(close,14) < 30' }); // scripted alerts persist too
917
+ ap.list(); ap.remove(id); ap.clear(); ap.sync(); ap.detach();
918
+ ```
919
+
920
+ Once-fired alerts drop out of storage automatically; alerts added directly
921
+ on the chart are captured at the next save point; storage/fetch are
922
+ injectable and every storage failure degrades to memory-only, never
923
+ throwing. Peer dependency: wickchart ≥ 1.4.
924
+
925
+ ### Layouts — the `wickchart-layouts` plugin
926
+
927
+ Named workspace persistence (~3 KB gz, own CI budget): save and restore
928
+ whole chart setups by name — type, theme, log scale, toggles, indicators,
929
+ view range, positions, alerts — plus the drawing list when wickchart-draw
930
+ is attached. Everything rides the core's public `getState()`/`setState()`.
931
+
932
+ ```js
933
+ npm install wickchart wickchart-layouts // layouts is a separate opt-in package
934
+
935
+ import { attachLayouts } from 'wickchart-layouts';
936
+ const layouts = attachLayouts(chart, {
937
+ key: 'my-desk', // storage key (default 'wickchart-layouts')
938
+ drawings: draw, // optional wickchart-draw handle — include drawings
939
+ });
940
+ layouts.save('swing'); // capture the current setup under a name
941
+ layouts.load('swing'); // apply it back
942
+ layouts.list(); // → [{ name, at, drawingCount }] newest first
943
+ layouts.export(); // → JSON string — share it, store it anywhere
944
+ layouts.import(json); // merge layouts back (replaces same names)
945
+ chart.addEventListener('wick:layouts', (e) => console.log(e.detail.action, e.detail.name));
946
+ ```
947
+
948
+ Entries are capped (oldest evicted), `storage` is injectable, storage
949
+ failures degrade to an in-memory store for the session and never throw.
950
+ Pair a `load` with `wickchart-alerts-plus`'s `sync()` if you also persist
951
+ alerts, since a layout load replaces the chart's alert list. Peer
952
+ dependency: wickchart ≥ 1.4.
953
+
954
+ ### Signals — the `wickchart-signals` plugin
955
+
956
+ Candlestick pattern badges (~4 KB gz, own CI budget): bullish/bearish
957
+ **engulfing**, **pin bars** (hammer / shooting star) and **inside bars**
958
+ drawn as direction-colored letter chips above/below the bar. Hover a badged
959
+ bar and the plugin draws the explanation ("Bullish engulfing") and fires
960
+ `wick:signals` — the same passive crosshair bridge as wickchart-sessions,
961
+ so badges never claim a pointer gesture.
962
+
963
+ ```js
964
+ npm install wickchart wickchart-signals // signals is a separate opt-in package
965
+
966
+ import { attachSignals } from 'wickchart-signals';
967
+ const signals = attachSignals(chart);
968
+ signals.setKinds(['engulfing', 'pinbar']); // subset (default: all three; [] = off)
969
+ signals.setLabels(false); // hover explanations off
970
+ chart.addEventListener('wick:signals', (e) => status.textContent = e.detail?.label || '');
971
+ ```
972
+
973
+ Detection is O(n), cached per dataset and kind subset — pan/zoom are pure
974
+ repaints. Peer dependency: wickchart ≥ 1.4.
975
+
976
+ ### Tape — the `wickchart-tape` plugin
977
+
978
+ Time & sales (~4.6 KB gz, own CI budget): a live trade-print strip docked at
979
+ the bottom of the canvas through the `insetBottom` hook — `time · price ·
980
+ size` rows colored by side with proportional size bars, oversized prints
981
+ highlighted. Display-only: it never claims a pointer gesture. Prints carry
982
+ an optional side; without one the plugin applies the classic **tick rule**
983
+ (uptick → buy, downtick → sell), carried continuously across pushes. The
984
+ same stream drives the chart: `chart.setData(tape.toBars(60000))`.
985
+
986
+ ```js
987
+ npm install wickchart wickchart-tape // tape is a separate opt-in package
988
+
989
+ import { attachTape } from 'wickchart-tape';
990
+ const tape = attachTape(chart, { rows: 7, bigSize: 50 });
991
+ socket.onmessage = (m) => tape.push(m.trades); // single print or batch
992
+ tape.setRows(4); tape.hide(); tape.detach(); // rows 3–8; hide frees the dock
993
+ chart.addEventListener('wick:tape', (e) => status.textContent = e.detail.total + ' prints');
994
+ ```
995
+
996
+ Keeps the newest 500 prints. Peer dependency: wickchart ≥ 1.6 (the dock
997
+ hook); shares the bottom strip with wickchart-navigator, so attach one or
998
+ the other.
999
+
710
1000
  ## Methods
711
1001
 
712
1002
  | Method | Description |
@@ -721,7 +1011,7 @@ section — it shares a room, so open it twice and draw on either chart).
721
1011
  | `getDataWindow()` | → AI-ready summary of the visible window (see below) |
722
1012
  | `getState()` | → serializable snapshot (type, indicators, view, positions, alerts) |
723
1013
  | `setState(state)` | Apply a snapshot; a pending view applies after the next `setData()` |
724
- | `addLayer(layer)` / `removeLayer(idOrHandle)` | Register/detach a plugin layer (draw hook + optional pointer claim) |
1014
+ | `addLayer(layer)` / `removeLayer(idOrHandle)` | Register/detach a plugin layer (draw hook + optional pointer claim + optional `insetBottom` dock strip) |
725
1015
  | `requestDraw()` | Repaint on the next frame (interactive layers) |
726
1016
  | `timeToX(t)` / `xToTime(x)` | Bar time ⇄ x-pixel; extrapolates into future space |
727
1017
  | `priceToY(p)` / `yToPrice(y)` | Price ⇄ y-pixel in the main pane (log-aware) |
@@ -760,6 +1050,9 @@ chart.addEventListener('wick:alert', (e) => {
760
1050
  // scripted alerts — any WickScript predicate, fired on its false→true edge
761
1051
  chart.addAlert({ when: 'crossup(rsi(close,14), 30)' });
762
1052
  chart.addAlert({ when: 'volume > sma(volume,20) * 3', once: false }); // re-arms
1053
+
1054
+ // evaluate only on final candles, so the signal cannot repaint
1055
+ chart.addAlert({ when: 'crossup(rsi(close,14), 30)', evaluate: 'close' });
763
1056
  ```
764
1057
 
765
1058
  The P&L chip recalculates on every streamed bar. Alerts are edge-triggered
@@ -768,6 +1061,43 @@ Scripted alerts are evaluated locally on every streamed bar — the event
768
1061
  carries the triggering close as `price` plus the `when` source; an invalid
769
1062
  predicate is rejected (`addAlert` returns `null`), never thrown.
770
1063
 
1064
+ **Live vs closed-candle evaluation.** Alerts evaluate on every update by
1065
+ default, the still-forming candle included — so a technical signal can
1066
+ repaint (RSI crosses 30 mid-candle, price reverses, the candle closes back
1067
+ above 30). Pass `evaluate: 'close'` to fire only on final candles, or set
1068
+ `<wick-chart alert-evaluate="close">` as the chart-wide default (per-alert
1069
+ `evaluate` still wins). A candle is final once a newer bar arrives, or as
1070
+ soon as the feed says so via `closed: true` on `update()` — `<wick-feed>`
1071
+ forwards Binance's `k.x` flag, so the signal lands at the close rather than
1072
+ one candle later. Historical corrections and backfilled candles never fire
1073
+ live alerts in either mode.
1074
+
1075
+ ### Timezone & VWAP sessions
1076
+
1077
+ Axis labels and the crosshair readout use the viewer's timezone by default.
1078
+ Pin them with `timezone` — `local`, `utc`, or any IANA zone, DST included:
1079
+
1080
+ ```html
1081
+ <wick-chart timezone="Europe/Stockholm"></wick-chart>
1082
+ <wick-chart timezone="America/New_York"></wick-chart>
1083
+ ```
1084
+
1085
+ Day dividers and month/year ticks follow the chosen zone, so a "1 Feb" tick
1086
+ is 1 February *there*. An unrecognised zone falls back to UTC and warns once.
1087
+
1088
+ VWAP's session boundary is deliberately **separate** from the display zone —
1089
+ changing the axis to Stockholm shouldn't silently re-anchor a BTC chart. It
1090
+ defaults to the UTC day (the crypto convention) and moves only when asked:
1091
+
1092
+ ```html
1093
+ <wick-chart indicators="vwap" vwap-anchor="America/New_York"></wick-chart>
1094
+ ```
1095
+
1096
+ `vwap-anchor` takes `utc` (default), `local`, an IANA zone, or a fixed offset
1097
+ in milliseconds. Equities, futures and FX rarely open at UTC midnight, so the
1098
+ default is right for crypto and wrong for most other markets — set it
1099
+ deliberately. `calcVWAP(bars, anchor)` takes the same values directly.
1100
+
771
1101
  ### Stats & measure
772
1102
 
773
1103
  `<wick-chart stats>` shows live statistics of the visible range — return %,
@@ -835,7 +1165,9 @@ wick-chart {
835
1165
  | Trackpad horizontal scroll | Pan |
836
1166
  | Drag | Pan (auto-follow re-arms at the right edge) |
837
1167
  | Pinch (touch) | Zoom |
838
- | Double-click | Reset view |
1168
+ | Long press (touch) | Open the crosshair, then drag to scrub across bars |
1169
+ | Vertical swipe (touch) | Scrolls the page, not the chart |
1170
+ | Double-click / double-tap | Reset view |
839
1171
  | `←` `→` (`+Shift` ×10) | Move crosshair |
840
1172
  | `+` / `−` | Zoom in / out |
841
1173
  | `Home` / `End` | Jump to oldest / newest |
@@ -872,13 +1204,55 @@ column, and an offscreen layer so hover only repaints the crosshair.
872
1204
  ## Architecture notes
873
1205
 
874
1206
  - Single ES module, Custom Element + Shadow DOM, Canvas 2D with
875
- devicePixelRatio scaling and rAF-batched invalidation
1207
+ devicePixelRatio scaling and rAF-batched invalidation. The ratio is watched
1208
+ with a `resolution` media query, so moving a window between monitors
1209
+ re-renders at the new resolution rather than staying soft
876
1210
  - Only visible bars are drawn; indicator series are computed lazily and cached
877
1211
  per data version (prefix-sum SMA, Wilder RSI)
878
1212
  - Time axis picks tick steps from bar interval (minutes → months) and labels
879
1213
  day/month boundaries like a pro terminal
880
1214
  - No dependencies, no build step required — but it bundles/tree-shakes fine
881
1215
 
1216
+ ## Tests
1217
+
1218
+ Two suites, and they answer different questions.
1219
+
1220
+ ```bash
1221
+ npm test # Node: pure functions, indicator maths, parsing, plugins
1222
+ npm run test:e2e # Playwright: the chart in a real browser
1223
+ ```
1224
+
1225
+ `npm test` is the fast one and covers the bulk of the library. What it cannot
1226
+ reach is anything that only exists once a browser is involved: custom-element
1227
+ upgrade, a real canvas, wheel/pointer/touch input, `devicePixelRatio`,
1228
+ `ResizeObserver`, and React re-renders against a live DOM node. Bugs have
1229
+ shipped in exactly that gap — a React parent re-render used to silently reset
1230
+ the user's zoom, and a chart moved to a monitor with a different pixel ratio
1231
+ kept rendering at the old resolution. Both are covered in `e2e/` now.
1232
+
1233
+ The browser suite serves the repository over a small dependency-free static
1234
+ server (`e2e/server.mjs`) and loads the library from source, so it tests the
1235
+ files that ship rather than a build artifact. The React fixture pulls React
1236
+ from esm.sh, the same way `demo/react.html` does.
1237
+
1238
+ **Running it locally.** `npm run test:e2e` downloads Playwright's bundled
1239
+ Chromium the first time. If that CDN is blocked on your machine, point the
1240
+ suite at a browser you already have:
1241
+
1242
+ ```bash
1243
+ WICK_E2E_CHANNEL=chrome npm run test:e2e # or msedge
1244
+ ```
1245
+
1246
+ **Visual regression** is opt-in. Canvas output is not pixel-identical across
1247
+ operating systems, so a committed baseline from one machine red-lights
1248
+ everyone else; the rest of the suite compares the chart against *itself*
1249
+ instead (repaint X, assert only what should have moved did). To gate on real
1250
+ screenshots, generate baselines on the platform that will run them:
1251
+
1252
+ ```bash
1253
+ WICK_E2E_VISUAL=1 npm run test:e2e -- --update-snapshots
1254
+ ```
1255
+
882
1256
  ## Roadmap ideas
883
1257
 
884
1258
  - More overlays (Bollinger, VWAP), MACD pane, drawing tools
@@ -907,6 +1281,12 @@ Two behavioral notes: custom indicators registered via
907
1281
  alias (one registry), and cross-tab co-view channels are now prefixed
908
1282
  `wick-co-view:` (a 0.x tab and a 1.x tab won't pair — refresh both).
909
1283
 
1284
+ ## Releases
1285
+
1286
+ Versioned per [semver](./CHANGELOG.md#how-this-project-versions); every
1287
+ release is a tagged GitHub Release with the changelog — see
1288
+ **[CHANGELOG.md](./CHANGELOG.md)**.
1289
+
910
1290
  ## License
911
1291
 
912
1292
  MIT
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "wickchart",
3
- "version": "1.5.0",
4
- "description": "<wick-chart> — a modern, dependency-free financial charting web component. Candles, line & area charts, crosshair, zoom/pan, indicators (incl. a safe expression mini-language), live streaming via <wick-feed>, theming.",
3
+ "version": "1.7.0",
4
+ "description": "<wick-chart> — a modern, dependency-free financial charting web component. Candles, line & area charts, crosshair, zoom/pan, indicators (incl. a safe expression mini-language), live streaming via <wick-feed> (incl. tick/volume/dollar bars), a worker compute path for 1M-bar histories, report export, theming.",
5
5
  "type": "module",
6
6
  "main": "src/wick-chart.js",
7
7
  "module": "src/wick-chart.js",
@@ -22,6 +22,14 @@
22
22
  "./react": {
23
23
  "types": "./types/react.d.ts",
24
24
  "default": "./src/react.js"
25
+ },
26
+ "./worker": {
27
+ "types": "./types/worker.d.ts",
28
+ "default": "./src/worker.js"
29
+ },
30
+ "./report": {
31
+ "types": "./types/report.d.ts",
32
+ "default": "./src/report.js"
25
33
  }
26
34
  },
27
35
  "files": [
@@ -42,10 +50,11 @@
42
50
  },
43
51
  "scripts": {
44
52
  "dev": "npx --yes serve . -l 5173",
45
- "test": "node --test \"tests/*.test.mjs\" \"plugins/draw/tests/*.test.mjs\"",
53
+ "test": "node --test \"tests/*.test.mjs\" \"plugins/draw/tests/*.test.mjs\" \"plugins/sessions/tests/*.test.mjs\" \"plugins/replay/tests/*.test.mjs\" \"plugins/compare/tests/*.test.mjs\" \"plugins/navigator/tests/*.test.mjs\" \"plugins/alerts-plus/tests/*.test.mjs\" \"plugins/layouts/tests/*.test.mjs\" \"plugins/signals/tests/*.test.mjs\" \"plugins/tape/tests/*.test.mjs\" \"plugins/grid/tests/*.test.mjs\" \"plugins/paper/tests/*.test.mjs\"",
54
+ "test:e2e": "playwright test",
46
55
  "build:types": "node -e \"require('fs').rmSync('types', { recursive: true, force: true });\" && tsc -p tsconfig.json",
47
56
  "prepack": "npm run build:types",
48
- "ci": "npm run build:types && npm test && node --check src/wick-chart.js && node --check src/wick-feed.js && node --check src/core.js && node --check src/react.js && node --check src/react-core.js && node --check demo/app.js && node --check plugins/draw/core.mjs && node --check plugins/draw/draw.mjs"
57
+ "ci": "npm run build:types && npm test && node --check src/wick-chart.js && node --check src/wick-feed.js && node --check src/core.js && node --check src/react.js && node --check src/react-core.js && node --check src/worker.js && node --check src/worker-core.js && node --check src/report.js && node --check demo/app.js && node --check plugins/draw/core.mjs && node --check plugins/draw/draw.mjs && node --check plugins/sessions/core.mjs && node --check plugins/sessions/sessions.mjs && node --check plugins/replay/replay.mjs && node --check plugins/compare/core.mjs && node --check plugins/compare/compare.mjs && node --check plugins/navigator/core.mjs && node --check plugins/navigator/navigator.mjs && node --check plugins/alerts-plus/alerts-plus.mjs && node --check plugins/layouts/layouts.mjs && node --check plugins/signals/core.mjs && node --check plugins/signals/signals.mjs && node --check plugins/tape/core.mjs && node --check plugins/tape/tape.mjs && node --check plugins/grid/core.mjs && node --check plugins/grid/grid.mjs && node --check plugins/paper/core.mjs && node --check plugins/paper/paper.mjs"
49
58
  },
50
59
  "keywords": [
51
60
  "chart",
@@ -56,6 +65,9 @@
56
65
  "web-component",
57
66
  "custom-element",
58
67
  "canvas",
68
+ "paper-trading",
69
+ "equity-curve",
70
+ "web-worker",
59
71
  "zero-dependency",
60
72
  "tradingview",
61
73
  "vwap",
@@ -72,6 +84,7 @@
72
84
  "url": "git+https://github.com/benyblack/wickchart.git"
73
85
  },
74
86
  "devDependencies": {
87
+ "@playwright/test": "^1.63.0",
75
88
  "@types/react": "^19.1.0",
76
89
  "jsdom": "^26.1.0",
77
90
  "react": "^19.1.0",