wickchart 1.0.0 → 1.3.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
@@ -17,6 +17,7 @@ npm install wickchart
17
17
  import 'wickchart'; // registers <wick-chart>
18
18
  import WickChart from 'wickchart'; // for WickChart.registerIndicator(...)
19
19
  import { encodeStateQuery } from 'wickchart/core'; // pure helpers
20
+ import { WickChart } from 'wickchart/react'; // React bindings (optional)
20
21
  ```
21
22
 
22
23
  Or straight from a CDN — no install, no build:
@@ -86,7 +87,14 @@ bet:
86
87
 
87
88
  The demo site is deployed to GitHub Pages:
88
89
  **https://benyblack.github.io/wickchart/** — a landing page with a live hero
89
- chart, the full interactive demo, and the zero-JavaScript declarative page.
90
+ chart, the full interactive demo, the zero-JavaScript declarative page, and a
91
+ [React demo](./demo/react.html) driven entirely by React state.
92
+
93
+ **Full documentation lives at
94
+ [benyblack.github.io/wickchart/docs.html](./docs.html)** — every attribute,
95
+ method, event, the WickScript reference, overlays (with a live JSON
96
+ playground), feeds, theming and framework bindings, each with runnable
97
+ examples. This README covers the same ground in plain markdown.
90
98
 
91
99
  ## Run the demo locally
92
100
 
@@ -105,6 +113,101 @@ with graceful fallback to synthetic data if it isn't.
105
113
 
106
114
  ---
107
115
 
116
+ ## Frameworks
117
+
118
+ `<wick-chart>` is framework-agnostic — attributes, one `data` property,
119
+ standard DOM events. The one opinionated wrapper ships as `wickchart/react`,
120
+ which turns that contract into idiomatic React with proper event
121
+ subscription/cleanup. `react` is an **optional** peer dependency: nothing
122
+ changes if you never import `wickchart/react`.
123
+
124
+ ### React
125
+
126
+ ```bash
127
+ npm install wickchart react
128
+ ```
129
+
130
+ ```jsx
131
+ import { WickChart, useWickChart } from 'wickchart/react';
132
+
133
+ // drop-in component — props map 1:1 onto the element
134
+ export function PriceChart({ bars, onRange }) {
135
+ return (
136
+ <WickChart
137
+ type="candles"
138
+ indicators="sma:20 ema:50 volume"
139
+ volshading
140
+ label="BTC · 1h"
141
+ data={bars} // bars are assigned as a property
142
+ onRange={onRange} // subscribes to wick:range
143
+ onAlert={(e) => toast(`crossed ${e.detail.price}`)}
144
+ style={{ height: 420 }}
145
+ />
146
+ );
147
+ }
148
+
149
+ // or the hook, when you need the imperative API
150
+ function PracticeChart({ bars }) {
151
+ const { ref, chart } = useWickChart({ data: bars, indicators: 'sma:20' });
152
+ // chart.getDataWindow(), chart.addAlert(...), chart.getState() … after mount
153
+ return <wick-chart ref={ref} style={{ height: 420 }} />;
154
+ }
155
+ ```
156
+
157
+ Rules of thumb:
158
+
159
+ - **Pass a fresh array** to `data` when the bars change — the binding compares
160
+ by reference, and reassignment is what triggers a redraw (don't mutate).
161
+ The same rule applies to `overlays` (see
162
+ [Server-side overlays](#server-side-overlays-zones--levels)).
163
+ - **String/number/boolean props become attributes** (`type`, `indicators`,
164
+ `volshading`, …); `className`/`style`/`id` reach React as usual.
165
+ - **`onXxx` subscribes to `wick:xxx`** with cleanup on unmount; an
166
+ `events={{ range: fn }}` object works too.
167
+ - Works the same on React 16.8 → 19 — no custom-element event caveats.
168
+
169
+ No build step? The [React demo](./demo/react.html) runs straight off a CDN
170
+ import map — `react` and `react-dom` from esm.sh, the bindings from the
171
+ package source.
172
+
173
+ ### Vue 3
174
+
175
+ ```vue
176
+ <script setup>
177
+ import { ref, onMounted } from 'vue';
178
+ import 'wickchart';
179
+ const chart = ref(null);
180
+ const bars = ref([]);
181
+ onMounted(async () => {
182
+ bars.value = await loadBars();
183
+ chart.value.data = bars.value;
184
+ chart.value.addEventListener('wick:range', (e) => console.log(e.detail));
185
+ });
186
+ </script>
187
+
188
+ <template>
189
+ <wick-chart ref="chart" type="candles" indicators="sma:20"
190
+ style="height: 420px"></wick-chart>
191
+ </template>
192
+ ```
193
+
194
+ ### Svelte
195
+
196
+ ```svelte
197
+ <script>
198
+ import 'wickchart';
199
+ let el;
200
+ let bars = [];
201
+ $: if (el && bars.length) el.data = bars;
202
+ </script>
203
+
204
+ <wick-chart bind:this={el} type="candles" indicators="sma:20"
205
+ on:wick:alert={(e) => console.log(e.detail)}
206
+ style="height: 420px"></wick-chart>
207
+ ```
208
+
209
+ ---
210
+
108
211
  ## Data format
109
212
 
110
213
  Bars are plain objects; `time` accepts **milliseconds or seconds** (auto-detected).
@@ -132,6 +235,7 @@ chart.setData([
132
235
  | `profile` | off | Volume profile overlay (POC + 70% value area) |
133
236
  | `annotations` | off | Smart annotations (volume spikes, gaps, pivots, RSI divergences) |
134
237
  | `volshading` | off | Volatility-regime background shading (see below) |
238
+ | `overlays` | – | JSON array of server-side zones & levels (see below) |
135
239
 
136
240
  \* `indicators=""` disables everything, including volume. Token syntax:
137
241
  `name[:param[/param…]][@color]` — e.g. `sma:20@#ff0000`, `macd:12/26/9`.
@@ -249,6 +353,151 @@ classifies everything as normal. The pieces are exported from
249
353
  `wickchart/core` (`calcRealizedVol`, `volRegimeBands`, `percentileOfSorted`)
250
354
  if you want to build on them.
251
355
 
356
+ ### Server-side overlays (zones & levels)
357
+
358
+ Draw analysis from your own API straight onto the chart: supply/demand
359
+ **zones** (time × price rectangles) and horizontal **levels**, rendered
360
+ behind the candles. Zones without a `to` extend into future space past the
361
+ last bar, like TradingView drawings.
362
+
363
+ ```js
364
+ const res = await fetch('https://api.example.com/analysis?symbol=BTC');
365
+ chart.setOverlays(await res.json());
366
+ ```
367
+
368
+ ```js
369
+ [
370
+ // zone: from/to are timestamps (ms or s); null → chart edge
371
+ { type: 'zone', from: 1753920000000, priceFrom: 33000, priceTo: 35600,
372
+ color: '#ef5350', alpha: 0.25, label: 'demand' },
373
+ { type: 'zone', from: 1753920000000, // no `to` → extends
374
+ priceFrom: 37700, priceTo: 40900, color: '#26a69a' }, // to the right edge
375
+ // level: horizontal price line, full width by default
376
+ { type: 'level', price: 28700, color: '#3f51b5', label: 'S1' },
377
+ { type: 'level', price: 22800, color: '#3f51b5', dash: true },
378
+ ]
379
+ ```
380
+
381
+ - `addOverlay(o)` upserts one (by `id`), `removeOverlay(id)`,
382
+ `clearOverlays()`, and `chart.overlays` reads them back.
383
+ - Colors accept hex / `rgb()` / CSS names plus the palette keys
384
+ `up` | `down` | `accent`; `alpha` clamps to 0.02–0.8 (default 0.22).
385
+ - Timestamps snap to bars (before the first bar clamps left, after the last
386
+ clamps right); invalid entries are dropped, never thrown — it's API data.
387
+ - Fully declarative, too — the same JSON as an attribute:
388
+
389
+ ```html
390
+ <wick-chart overlays='[{"type":"level","price":28700,"color":"#3f51b5","label":"S1"}]'></wick-chart>
391
+ ```
392
+
393
+ The React binding takes `overlays` as a prop (fresh array → re-apply), and
394
+ `normalizeOverlays` / `barIndexForTime` / `resolveOverlayColor` are exported
395
+ from `wickchart/core`.
396
+
397
+ ### Scenario mode — ghost paths & volatility cones
398
+
399
+ Project what-if into future space: a ghost path of hypothetical prices plus
400
+ a volatility cone (±1σ/±2σ bands widening with √h from realized vol).
401
+
402
+ ```js
403
+ chart.setScenario({
404
+ path: [64000, 65500, 66800, 68000], // prices for future bars 1..N
405
+ cone: true, // σ-bands from realized vol (default)
406
+ label: 'bull case',
407
+ color: 'up', // up | down | accent or safe colors
408
+ });
409
+ chart.setScenario({ horizon: 48 }); // cone-only projection
410
+ chart.clearScenario();
411
+ ```
412
+
413
+ Setting a scenario reserves future space on the right so the cone stays
414
+ visible; the horizon defaults to the path length (1–500) and `levels` are σ
415
+ multipliers (default `[1, 2]`). Like overlays, scenarios are analysis data —
416
+ excluded from shareable state, and the same shape a server-side model could
417
+ push. `calcVolCone` / `normalizeScenario` are exported from `wickchart/core`.
418
+
419
+ ### Risk planner — R-multiple grid
420
+
421
+ Plan the trade on the chart: entry + stop define **1R** (the risk unit) and
422
+ reward lines are drawn at kR beyond the entry, with the risk/reward zones
423
+ shaded. Direction is derived from the stop side.
424
+
425
+ ```js
426
+ chart.setRiskPlan({ entry: 64500, stop: 63800, multiples: [1, 2, 3] });
427
+ chart.setRiskPlan({ entry: 64500, stop: 63800, targets: [65900, 67300] }); // prices → kR
428
+ chart.clearRiskPlan();
429
+ chart.riskPlan; // { entry, stop, risk, direction, levels: [{ k, price }], maxK, label }
430
+ ```
431
+
432
+ Explicit `targets` convert to their R multiple (wrong-side prices drop);
433
+ `multiples` win when both are given. At most 8 levels, each ≤ 20R; invalid
434
+ specs clear the plan, never throw. `normalizeRiskPlan` is exported from
435
+ `wickchart/core`.
436
+
437
+ ### Bar-walk narrator — history as a story
438
+
439
+ `narrate()` builds the timeline of a window (pivot highs/lows, volume
440
+ spikes, gaps, RSI divergences, plus derived legs — the move between
441
+ opposite pivots); `walk()` replays the chart through it while `wick:walk`
442
+ events announce each step, so a caption bar can narrate the replay.
443
+
444
+ ```js
445
+ chart.narrate(); // [{ i, time, type, note, legPct?, legBars? }]
446
+ chart.walk({ from: 0, to: 500, speed: 120, step: 10 });
447
+ chart.addEventListener('wick:walk', (e) => {
448
+ // { phase: 'step' | 'end' | 'stop', index, events: [...], from, to }
449
+ });
450
+ chart.stopWalk(); // any pointer/wheel/key input stops it too
451
+ ```
452
+
453
+ `narrateWindow` (the analyzer) is exported from `wickchart/core`.
454
+
455
+ ### Delta brush — drag-select with stats
456
+
457
+ `<wick-chart brush>` makes a plain drag **select bars** instead of panning:
458
+ a live band follows the pointer with a delta chip (Δ% · bars · high · low ·
459
+ Σvol); on release the selection commits and fires `wick:brush` with the
460
+ range statistics. Esc (or `clearBrush()`) clears it.
461
+
462
+ ```html
463
+ <wick-chart brush></wick-chart>
464
+ ```
465
+
466
+ ```js
467
+ chart.addEventListener('wick:brush', (e) => {
468
+ // { bars, from: {index, time}, to: {index, time}, delta, deltaPct,
469
+ // firstOpen, lastClose, high, low, volume }
470
+ });
471
+ chart.brushSelection; // { i0, i1, stats } | null
472
+ chart.clearBrush();
473
+ ```
474
+
475
+ Brush mode replaces plain-drag panning (shift-drag still measures);
476
+ replacing the dataset clears a committed selection. `brushStats` is
477
+ exported from `wickchart/core`.
478
+
479
+ ### Story mode — guided tours of chart state
480
+
481
+ A **story** is an array of **scenes** (view range, type, indicators,
482
+ overlays, scenario, risk plan + title/note). `playStory()` applies each
483
+ scene, eases the camera to its range, holds for `dwell`, and narrates
484
+ through `wick:story`. Record scenes with `captureScene()` while you
485
+ arrange the chart, or generate them from an analysis.
486
+
487
+ ```js
488
+ const story = [chart.captureScene('Overview', 'the full picture')];
489
+ story.push({ title: 'The breakout', range: { from, to }, indicators: 'sma:20' });
490
+ chart.playStory(story, { dwell: 2200, panMs: 900, loop: false });
491
+ chart.addEventListener('wick:story', (e) => {
492
+ // { phase: 'scene' | 'end' | 'stop', index, total, scene, title, note }
493
+ });
494
+ chart.stopStory(); chart.getStory();
495
+ ```
496
+
497
+ Any user interaction stops the tour. Scenes are plain data — serialize
498
+ or share them. `normalizeScene` / `sceneList` / `easeInOutCubic` are
499
+ exported from `wickchart/core`.
500
+
252
501
  ### AI-ready data window — `getDataWindow()`
253
502
 
254
503
  One call turns whatever is on screen into a compact, LLM-pasteable summary.
@@ -266,6 +515,31 @@ s.volPctile; // 84 → hot regime relative to the window itself
266
515
  s.patterns; // [{ time, note }] — most recent first
267
516
  ```
268
517
 
518
+ ### AI agent interface — the chart as a tool surface
519
+
520
+ The chart can publish its own **tool manifest** and accept validated
521
+ tool-calls, so any LLM can operate it with zero glue code — the chart never
522
+ touches the network; you supply the model call.
523
+
524
+ ```js
525
+ chart.aiTools(); // manifest: get_data_window, set_indicators, set_overlays, add_alert, …
526
+ chart.aiPrompt(); // system prompt demanding JSON [{tool, args}] ops
527
+ chart.aiContext(); // grounding: current state + visible-window summary
528
+ chart.applyAI(ops); // validated dispatcher — per-op {ok, result} / {ok:false, error}
529
+
530
+ const { results } = await chart.ask(
531
+ 'add RSI, mark the demand zone, and alert me on volume spikes',
532
+ { run: async (payload) => (await callMyLLM(payload)).ops }
533
+ );
534
+ ```
535
+
536
+ Every op is whitelisted and its args validated (indicator names checked
537
+ against the registry, overlays through the sanitizer, enums enforced) — LLM
538
+ output is treated as untrusted input, and a bad op returns an error the
539
+ model can self-correct from instead of throwing. The
540
+ [docs page](./docs.html) has a live playground driving `applyAI()` with an
541
+ offline demo agent (no network, no keys).
542
+
269
543
  `text` renders like:
270
544
 
271
545
  ```
@@ -291,13 +565,13 @@ range as a ~4-second pitch sequence, riding the crosshair along for sighted
291
565
  users. Audio starts lazily within the enabling user gesture (autoplay-policy
292
566
  safe).
293
567
 
294
- ### Cross-tab co-view
568
+ ### Cross-tab co-view & presence
295
569
 
296
570
  Tag charts with the same channel and they share pointers — across browser
297
571
  tabs, or between multiple charts on one page:
298
572
 
299
573
  ```html
300
- <wick-chart co-view="btc-room"></wick-chart>
574
+ <wick-chart co-view="btc-room" co-view-name="ben"></wick-chart>
301
575
  ```
302
576
 
303
577
  Hovering in one tab draws a ghost crosshair (accent, dotted, with the time
@@ -306,6 +580,20 @@ different history depths still line up. Ghosts fade ~2.5 s after the peer
306
580
  stops moving. Same-origin only (BroadcastChannel); the connection follows the
307
581
  `co-view` attribute and closes with the element.
308
582
 
583
+ Peers also see **where everyone is looking**: each peer's viewport renders
584
+ as a colored band (with name) along the top of the plot, updated live as
585
+ they pan or zoom and swept away ~12 s after they go quiet.
586
+
587
+ ```js
588
+ chart.getPeers(); // [{ id, name, range: { from, to }, at }]
589
+ chart.addEventListener('wick:peers', (e) => {
590
+ // { peers, joined, left } — membership changes only
591
+ });
592
+ ```
593
+
594
+ `PresenceTracker` (the TTL bookkeeping) is exported from `wickchart/core`
595
+ for apps that sync presence over their own transport instead.
596
+
309
597
  ### Smart annotations
310
598
 
311
599
  `<wick-chart annotations>` marks notable events on the visible range — volume
@@ -390,10 +678,17 @@ chart.addAlert({ price: 65000, direction: 'above' }); // 'above' | 'below' | 'cr
390
678
  chart.addEventListener('wick:alert', (e) => {
391
679
  console.log('crossed!', e.detail.id, e.detail.price);
392
680
  });
681
+
682
+ // scripted alerts — any WickScript predicate, fired on its false→true edge
683
+ chart.addAlert({ when: 'crossup(rsi(close,14), 30)' });
684
+ chart.addAlert({ when: 'volume > sma(volume,20) * 3', once: false }); // re-arms
393
685
  ```
394
686
 
395
687
  The P&L chip recalculates on every streamed bar. Alerts are edge-triggered
396
688
  (fire once per crossing) and one-shot by default (`once: false` to re-arm).
689
+ Scripted alerts are evaluated locally on every streamed bar — the event
690
+ carries the triggering close as `price` plus the `when` source; an invalid
691
+ predicate is rejected (`addAlert` returns `null`), never thrown.
397
692
 
398
693
  ### Stats & measure
399
694
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wickchart",
3
- "version": "1.0.0",
3
+ "version": "1.3.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",
@@ -18,6 +18,10 @@
18
18
  "./feed": {
19
19
  "types": "./types/wick-feed.d.ts",
20
20
  "default": "./src/wick-feed.js"
21
+ },
22
+ "./react": {
23
+ "types": "./types/react.d.ts",
24
+ "default": "./src/react.js"
21
25
  }
22
26
  },
23
27
  "files": [
@@ -25,14 +29,23 @@
25
29
  "types"
26
30
  ],
27
31
  "sideEffects": [
28
- "src/wick-chart.js"
32
+ "src/wick-chart.js",
33
+ "src/react.js"
29
34
  ],
35
+ "peerDependencies": {
36
+ "react": ">=16.8"
37
+ },
38
+ "peerDependenciesMeta": {
39
+ "react": {
40
+ "optional": true
41
+ }
42
+ },
30
43
  "scripts": {
31
44
  "dev": "npx --yes serve . -l 5173",
32
45
  "test": "node --test \"tests/*.test.mjs\"",
33
46
  "build:types": "node -e \"require('fs').rmSync('types', { recursive: true, force: true });\" && tsc -p tsconfig.json",
34
47
  "prepack": "npm run build:types",
35
- "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 demo/app.js"
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"
36
49
  },
37
50
  "keywords": [
38
51
  "chart",
@@ -52,6 +65,10 @@
52
65
  "url": "git+https://github.com/benyblack/wickchart.git"
53
66
  },
54
67
  "devDependencies": {
68
+ "@types/react": "^19.1.0",
69
+ "jsdom": "^26.1.0",
70
+ "react": "^19.1.0",
71
+ "react-dom": "^19.1.0",
55
72
  "typescript": "^7.0.2"
56
73
  }
57
74
  }