wickchart 1.5.0 → 1.6.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
@@ -707,6 +707,193 @@ writer wins, remote updates never touch the local undo stack. Peer
707
707
  dependency: wickchart ≥ 1.4. See the live playground in the docs (Drawings
708
708
  section — it shares a room, so open it twice and draw on either chart).
709
709
 
710
+ ### Sessions — the `wickchart-sessions` plugin
711
+
712
+ Market session shading as opt-in bytes (~6 KB gz, own CI budget): Asia /
713
+ London / New York and other sessions drawn as translucent bands, with labels,
714
+ closed-weekend shading for equities/futures, and crosshair hover events.
715
+ Presets for crypto & forex use the common UTC convention; equity/futures
716
+ presets use IANA timezones, so 09:30 is the real 09:30 across DST changes.
717
+ Custom defs (`{ name, start, end, tz?, days?, color?, alpha? }`) cover
718
+ midnight-crossing sessions and weekday filters.
719
+
720
+ ```js
721
+ npm install wickchart wickchart-sessions // sessions are a separate opt-in package
722
+
723
+ import { attachSessions } from 'wickchart-sessions';
724
+
725
+ const sessions = attachSessions(chart, { preset: 'crypto' });
726
+ sessions.setPreset('nyse'); // 'crypto' | 'forex' | 'nyse' | 'cme' | null
727
+ sessions.setSessions([...]); // custom defs (validated; getSessions() → JSON)
728
+ sessions.setWeekends(true); // shade closed Sat+Sun (default for nyse/cme)
729
+ chart.addEventListener('wick:sessions', (e) => status.textContent = e.detail.hover || '');
730
+ ```
731
+
732
+ The hover bridge listens to the chart's own crosshair events, so shading
733
+ never claims a pointer gesture — pan/zoom/measure work untouched. Peer
734
+ dependency: wickchart ≥ 1.4.
735
+
736
+ ### Replay — the `wickchart-replay` plugin
737
+
738
+ Bar replay as opt-in bytes (~3 KB gz, own CI budget): play history forward
739
+ bar-by-bar or at speed while the future stays hidden. The whole engine runs
740
+ on the public data API — a `setData` slice hides the future, `update()`
741
+ appends one bar per step — so the core stays replay-free. A badge layer shows
742
+ the mode and position at a glance.
743
+
744
+ ```js
745
+ npm install wickchart wickchart-replay // replay is a separate opt-in package
746
+
747
+ import { attachReplay } from 'wickchart-replay';
748
+
749
+ const replay = attachReplay(chart);
750
+ replay.start(); // head at ~70% of the data (or pass a time/index)
751
+ replay.play(); // 4 bars/sec — play(15) for faster, pause() stops
752
+ replay.step(); // reveal one bar
753
+ replay.seek('2026-03-06'); // jump the head
754
+ replay.setLoop(true); // wrap to the anchor at the end
755
+ replay.stop(); // exit — the full dataset is restored
756
+ chart.addEventListener('wick:replay', (e) => progress.textContent =
757
+ e.detail.active ? `${e.detail.index + 1}/${e.detail.total}` : '');
758
+ ```
759
+
760
+ Anchors accept bar indices, timestamps (ms/s) or date strings; every change
761
+ fires `wick:replay` with the full state. Pause live feeds while replaying —
762
+ an external `update()`/`setData()` aborts replay instead of corrupting the
763
+ chart (the demo pauses its feed automatically). Paper trading and an equity
764
+ curve are the planned 0.2 follow-up. Peer dependency: wickchart ≥ 1.4.
765
+
766
+ ### Compare — the `wickchart-compare` plugin
767
+
768
+ Normalized multi-asset overlays as opt-in bytes (~4 KB gz, own CI budget):
769
+ percent-rebased compare lines (ETH against BTC, TradingView-style) plus
770
+ derived **ratio** and **diff** lines (`BTC/ETH`, `BTC−ETH`), drawn over the
771
+ main pane against their own invisible scale so the price axis is untouched.
772
+ A legend chip row shows each series with its live value.
773
+
774
+ ```js
775
+ npm install wickchart wickchart-compare // compare is a separate opt-in package
776
+
777
+ import { attachCompare } from 'wickchart-compare';
778
+
779
+ const cmp = attachCompare(chart);
780
+ cmp.setSeries([
781
+ { label: 'ETH', data: ethBars }, // OHLC or {time, value}
782
+ { label: 'BTC/ETH', op: 'ratio', a: btcBars, b: ethBars }, // derived
783
+ ]);
784
+ cmp.setRebase('visible'); // 0% at the window edge, re-normalized while
785
+ // panning; 'first' or an epoch anchor also work
786
+ cmp.clear(); cmp.detach();
787
+ ```
788
+
789
+ Series are sampled onto the main chart's bar times, so timeframes can mix
790
+ and gaps break the line instead of bridging. Rebased values share one
791
+ invisible scale inset 8% from the pane edges; the price scale is never
792
+ distorted. Validated, capped at 6 series, invalid entries dropped. Peer
793
+ dependency: wickchart ≥ 1.4.
794
+
795
+ ### Navigator — the `wickchart-navigator` plugin
796
+
797
+ The most-missed TradingView affordance: a silhouette of the whole dataset
798
+ docked below the chart with a draggable viewport window (~3 KB gz, own CI
799
+ budget). Drag the window to pan, grab an edge to resize, click outside it to
800
+ jump — pan/zoom and the window stay in sync live, both directions.
801
+
802
+ ```js
803
+ npm install wickchart wickchart-navigator // navigator is a separate opt-in package
804
+
805
+ import { attachNavigator } from 'wickchart-navigator';
806
+ const nav = attachNavigator(chart, { height: 46 }); // strip height, 24..120
807
+ nav.detach(); // remove the strip again
808
+ ```
809
+
810
+ The strip needs bottom space, so this plugin pairs with a small core hook:
811
+ a layer may declare `insetBottom` (px) — the largest declared inset reserves
812
+ a docked strip at the bottom of the canvas, panes and the time axis shrink
813
+ above it, and layers draw it as `api.layout.dock`. On charts without the
814
+ hook the navigator degrades silently. The silhouette is O(n) once per
815
+ (dataset, width) and cached. Peer dependency: wickchart ≥ 1.6.
816
+
817
+ ### Alerts+ — the `wickchart-alerts-plus` plugin
818
+
819
+ The "pro" alert tier (~3 KB gz, own CI budget). Core alerts are runtime-only
820
+ by design; this adds what a trading tool actually needs, without the core
821
+ growing any of it: **persistence** (the alert list mirrors into
822
+ localStorage and re-arms on reload), **desktop notifications + a WebAudio
823
+ beep** while the tab is hidden, and an optional **webhook** that receives
824
+ every fire as `POST { id, price, when, time, bar, key }`.
825
+
826
+ ```js
827
+ npm install wickchart wickchart-alerts-plus // alerts-plus is a separate opt-in package
828
+
829
+ import { attachAlertsPlus } from 'wickchart-alerts-plus';
830
+ const ap = attachAlertsPlus(chart, {
831
+ key: 'BTC:1h', // one storage key per symbol+timeframe
832
+ notify: true, sound: true, // hidden-tab surfacing
833
+ webhook: 'https://example.com/hook', // optional
834
+ });
835
+ await ap.requestNotify(); // ask for the notification permission
836
+ ap.add({ price: 100, direction: 'above' }); // persisted, re-armed on reload
837
+ ap.add({ when: 'rsi(close,14) < 30' }); // scripted alerts persist too
838
+ ap.list(); ap.remove(id); ap.clear(); ap.sync(); ap.detach();
839
+ ```
840
+
841
+ Once-fired alerts drop out of storage automatically; alerts added directly
842
+ on the chart are captured at the next save point; storage/fetch are
843
+ injectable and every storage failure degrades to memory-only, never
844
+ throwing. Peer dependency: wickchart ≥ 1.4.
845
+
846
+ ### Layouts — the `wickchart-layouts` plugin
847
+
848
+ Named workspace persistence (~3 KB gz, own CI budget): save and restore
849
+ whole chart setups by name — type, theme, log scale, toggles, indicators,
850
+ view range, positions, alerts — plus the drawing list when wickchart-draw
851
+ is attached. Everything rides the core's public `getState()`/`setState()`.
852
+
853
+ ```js
854
+ npm install wickchart wickchart-layouts // layouts is a separate opt-in package
855
+
856
+ import { attachLayouts } from 'wickchart-layouts';
857
+ const layouts = attachLayouts(chart, {
858
+ key: 'my-desk', // storage key (default 'wickchart-layouts')
859
+ drawings: draw, // optional wickchart-draw handle — include drawings
860
+ });
861
+ layouts.save('swing'); // capture the current setup under a name
862
+ layouts.load('swing'); // apply it back
863
+ layouts.list(); // → [{ name, at, drawingCount }] newest first
864
+ layouts.export(); // → JSON string — share it, store it anywhere
865
+ layouts.import(json); // merge layouts back (replaces same names)
866
+ chart.addEventListener('wick:layouts', (e) => console.log(e.detail.action, e.detail.name));
867
+ ```
868
+
869
+ Entries are capped (oldest evicted), `storage` is injectable, storage
870
+ failures degrade to an in-memory store for the session and never throw.
871
+ Pair a `load` with `wickchart-alerts-plus`'s `sync()` if you also persist
872
+ alerts, since a layout load replaces the chart's alert list. Peer
873
+ dependency: wickchart ≥ 1.4.
874
+
875
+ ### Signals — the `wickchart-signals` plugin
876
+
877
+ Candlestick pattern badges (~4 KB gz, own CI budget): bullish/bearish
878
+ **engulfing**, **pin bars** (hammer / shooting star) and **inside bars**
879
+ drawn as direction-colored letter chips above/below the bar. Hover a badged
880
+ bar and the plugin draws the explanation ("Bullish engulfing") and fires
881
+ `wick:signals` — the same passive crosshair bridge as wickchart-sessions,
882
+ so badges never claim a pointer gesture.
883
+
884
+ ```js
885
+ npm install wickchart wickchart-signals // signals is a separate opt-in package
886
+
887
+ import { attachSignals } from 'wickchart-signals';
888
+ const signals = attachSignals(chart);
889
+ signals.setKinds(['engulfing', 'pinbar']); // subset (default: all three)
890
+ signals.setLabels(false); // hover explanations off
891
+ chart.addEventListener('wick:signals', (e) => status.textContent = e.detail?.label || '');
892
+ ```
893
+
894
+ Detection is O(n), cached per dataset and kind subset — pan/zoom are pure
895
+ repaints. Peer dependency: wickchart ≥ 1.4.
896
+
710
897
  ## Methods
711
898
 
712
899
  | Method | Description |
@@ -721,7 +908,7 @@ section — it shares a room, so open it twice and draw on either chart).
721
908
  | `getDataWindow()` | → AI-ready summary of the visible window (see below) |
722
909
  | `getState()` | → serializable snapshot (type, indicators, view, positions, alerts) |
723
910
  | `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) |
911
+ | `addLayer(layer)` / `removeLayer(idOrHandle)` | Register/detach a plugin layer (draw hook + optional pointer claim + optional `insetBottom` dock strip) |
725
912
  | `requestDraw()` | Repaint on the next frame (interactive layers) |
726
913
  | `timeToX(t)` / `xToTime(x)` | Bar time ⇄ x-pixel; extrapolates into future space |
727
914
  | `priceToY(p)` / `yToPrice(y)` | Price ⇄ y-pixel in the main pane (log-aware) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wickchart",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
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.",
5
5
  "type": "module",
6
6
  "main": "src/wick-chart.js",
@@ -42,10 +42,10 @@
42
42
  },
43
43
  "scripts": {
44
44
  "dev": "npx --yes serve . -l 5173",
45
- "test": "node --test \"tests/*.test.mjs\" \"plugins/draw/tests/*.test.mjs\"",
45
+ "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\"",
46
46
  "build:types": "node -e \"require('fs').rmSync('types', { recursive: true, force: true });\" && tsc -p tsconfig.json",
47
47
  "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"
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 && 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"
49
49
  },
50
50
  "keywords": [
51
51
  "chart",
package/src/wick-chart.js CHANGED
@@ -1598,8 +1598,9 @@ class WickChart extends HTMLElementBase {
1598
1598
  );
1599
1599
  }
1600
1600
  const timeH = 26;
1601
+ const dock = this._dockInset();
1601
1602
  const plotRight = Math.max(30, W - priceW);
1602
- const plotBottom = H - timeH;
1603
+ const plotBottom = H - timeH - dock;
1603
1604
  const paneList = this._ind.panes;
1604
1605
  const paneArea = paneList.length
1605
1606
  ? Math.min(
@@ -1622,6 +1623,7 @@ class WickChart extends HTMLElementBase {
1622
1623
  plotBottom,
1623
1624
  main: { y0: 0, y1: mainH, h: mainH },
1624
1625
  panes,
1626
+ dock: dock > 0 ? { y0: H - dock, h: dock } : null,
1625
1627
  });
1626
1628
 
1627
1629
  /* background */
@@ -2942,7 +2944,13 @@ class WickChart extends HTMLElementBase {
2942
2944
  * layer then receives that pointer's move/up/cancel events (plus a
2943
2945
  * 'cancel' on Escape) and the chart suppresses its own pan/measure/brush
2944
2946
  * for the duration.
2945
- * @param {{id?: string, draw: Function, onPointer?: Function}} layer
2947
+ *
2948
+ * A layer may also declare `insetBottom` (px, 0..160): the largest
2949
+ * declared inset reserves a docked strip at the very bottom of the
2950
+ * canvas — all chart content (panes + time axis) shrinks above it and
2951
+ * the strip is handed to layers as `api.layout.dock = { y0, h }`
2952
+ * (used by the wickchart-navigator plugin).
2953
+ * @param {{id?: string, draw: Function, onPointer?: Function, insetBottom?: number}} layer
2946
2954
  * @returns {object|null} the normalized layer handle (with `id`), or null
2947
2955
  * if the layer was rejected (no draw fn, or 16 layers already added)
2948
2956
  */
@@ -2956,6 +2964,10 @@ class WickChart extends HTMLElementBase {
2956
2964
  const entry = {
2957
2965
  id,
2958
2966
  draw: layer.draw,
2967
+ insetBottom:
2968
+ typeof layer.insetBottom === 'number' && Number.isFinite(layer.insetBottom)
2969
+ ? Math.max(0, Math.min(160, Math.round(layer.insetBottom)))
2970
+ : 0,
2959
2971
  onPointer: typeof layer.onPointer === 'function' ? layer.onPointer : null,
2960
2972
  };
2961
2973
  const at = this._layers.findIndex((l) => l.id === id);
@@ -3007,6 +3019,18 @@ class WickChart extends HTMLElementBase {
3007
3019
  }
3008
3020
  }
3009
3021
 
3022
+ /**
3023
+ * Bottom space reserved by plugin layers: the largest declared
3024
+ * `insetBottom` (px, clamped 0..160 at addLayer time), or 0.
3025
+ */
3026
+ _dockInset() {
3027
+ let dock = 0;
3028
+ for (const l of this._layers) {
3029
+ if (l.insetBottom > dock) dock = l.insetBottom;
3030
+ }
3031
+ return dock;
3032
+ }
3033
+
3010
3034
  /** Ask layers, in order, whether one claims this pointerdown. */
3011
3035
  _layerHit(e, pt) {
3012
3036
  for (const layer of this._layers) {
@@ -46,6 +46,10 @@ declare class WickChart extends HTMLElementBase {
46
46
  y1: number;
47
47
  h: number;
48
48
  }[];
49
+ dock: {
50
+ y0: number;
51
+ h: number;
52
+ };
49
53
  };
50
54
  _cache: {
51
55
  v: number;
@@ -791,7 +795,13 @@ declare class WickChart extends HTMLElementBase {
791
795
  * layer then receives that pointer's move/up/cancel events (plus a
792
796
  * 'cancel' on Escape) and the chart suppresses its own pan/measure/brush
793
797
  * for the duration.
794
- * @param {{id?: string, draw: Function, onPointer?: Function}} layer
798
+ *
799
+ * A layer may also declare `insetBottom` (px, 0..160): the largest
800
+ * declared inset reserves a docked strip at the very bottom of the
801
+ * canvas — all chart content (panes + time axis) shrinks above it and
802
+ * the strip is handed to layers as `api.layout.dock = { y0, h }`
803
+ * (used by the wickchart-navigator plugin).
804
+ * @param {{id?: string, draw: Function, onPointer?: Function, insetBottom?: number}} layer
795
805
  * @returns {object|null} the normalized layer handle (with `id`), or null
796
806
  * if the layer was rejected (no draw fn, or 16 layers already added)
797
807
  */
@@ -799,6 +809,7 @@ declare class WickChart extends HTMLElementBase {
799
809
  id?: string;
800
810
  draw: Function;
801
811
  onPointer?: Function;
812
+ insetBottom?: number;
802
813
  }): object | null;
803
814
  /**
804
815
  * Remove a layer added via addLayer (pass the returned handle or its id).
@@ -810,6 +821,11 @@ declare class WickChart extends HTMLElementBase {
810
821
  requestDraw(): void;
811
822
  /** Paint every registered layer. Called from _render with live state. */
812
823
  _drawLayers(ctx: any, pal: any, ly: any, d: any): void;
824
+ /**
825
+ * Bottom space reserved by plugin layers: the largest declared
826
+ * `insetBottom` (px, clamped 0..160 at addLayer time), or 0.
827
+ */
828
+ _dockInset(): number;
813
829
  /** Ask layers, in order, whether one claims this pointerdown. */
814
830
  _layerHit(e: any, pt: any): any;
815
831
  /** Deliver a pointer event to a claiming layer; never throws outward. */