wickchart 0.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 ADDED
@@ -0,0 +1,404 @@
1
+ # HabView
2
+
3
+ **`<hab-chart>` — a modern, simpler, more useful charting web component.**
4
+
5
+ A TradingView-style financial chart as a single framework-agnostic Web Component.
6
+ One file, zero dependencies, one HTML tag. Canvas-rendered, fast, themeable, and
7
+ streaming-ready.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install wickchart
13
+ ```
14
+
15
+ ```js
16
+ // any bundler / framework — TypeScript types included
17
+ import 'wickchart'; // registers <hab-chart>
18
+ import HabChart from 'wickchart'; // for HabChart.registerIndicator(...)
19
+ import { encodeStateQuery } from 'wickchart/core'; // pure helpers
20
+ ```
21
+
22
+ Or straight from a CDN — no install, no build:
23
+
24
+ ```html
25
+ <script type="module" src="https://unpkg.com/wickchart"></script>
26
+
27
+ <hab-chart label="BTC · 1h" type="candles" indicators="sma:20 volume"></hab-chart>
28
+
29
+ <script type="module">
30
+ const chart = document.querySelector('hab-chart');
31
+ chart.setData(bars); // [{ time, open, high, low, close, volume }]
32
+ chart.update(bar); // stream live updates
33
+ </script>
34
+ ```
35
+
36
+ Works in plain HTML, React, Vue, Svelte, Angular — anywhere a `<div>` works.
37
+ TypeScript declarations ship inside the package (generated at pack time from
38
+ the JSDoc-annotated source — the repo itself stays 100% dependency-free JS).
39
+
40
+ ## Declarative live charts with `<hab-feed>`
41
+
42
+ One more script tag and your chart is fully live — data, backfill, streaming —
43
+ with **zero JavaScript written**:
44
+
45
+ ```html
46
+ <script type="module" src="https://unpkg.com/wickchart/feed"></script>
47
+
48
+ <hab-feed for="chart" binance="BTCUSDT" tf="1h"></hab-feed>
49
+ <hab-chart id="chart" indicators="sma:20 volume" profile></hab-chart>
50
+ ```
51
+
52
+ | Attribute | Meaning |
53
+ | ----------- | ----------------------------------------------------------------------- |
54
+ | `for` | target `<hab-chart>` id (auto-pairs with the first chart when omitted) |
55
+ | `binance` | Binance symbol (`BTCUSDT`) — REST load + WebSocket live + backfill |
56
+ | `demo` | deterministic offline synthetic feed (`demo="ETH"` picks a base price) |
57
+ | `url` | generic REST endpoint returning a JSON array of bars (+ `poll="10"` sec) |
58
+ | `tf` | timeframe: `1m 3m 5m 15m 30m 1h 2h 4h 6h 12h 1d 3d 1w` |
59
+ | `limit` | initial bars (default 500) |
60
+ | `live` | `live="false"` loads history without streaming |
61
+
62
+ The element reflects its state in the `status` attribute (`loading`, `live`,
63
+ `polling`, `fallback`, `loaded`, `waiting`, `idle`) and emits
64
+ `hab-feed:status` / `hab-feed:fallback` events. When Binance is unreachable
65
+ (geo-blocked, offline), it degrades gracefully: WebSocket → REST polling → a
66
+ synthetic stream bridged from the last real price, so the chart never goes
67
+ blank. It also wires `chart.onloadmore` for infinite backfill automatically.
68
+
69
+ ---
70
+
71
+ ## Why another chart library?
72
+
73
+ TradingView's charting library is powerful but heavy and enterprise-licensed;
74
+ most wrappers add build steps and framework lock-in. HabView takes the opposite
75
+ bet:
76
+
77
+ - **Zero dependencies, single file** (~40 KB unminified, no build step required)
78
+ - **One tag, sane defaults** — drop it in and it renders; everything optional
79
+ - **Built-in usefulness** — crosshair + OHLC legend, last-price line, wheel zoom,
80
+ drag pan, pinch, keyboard navigation, live streaming, PNG export
81
+ - **Themeable with CSS variables** — two built-in themes, full control from
82
+ outside the component (Shadow DOM friendly)
83
+ - **Accessible** — focusable, arrow-key crosshair, ARIA summary of the data
84
+
85
+ ## Run the demo
86
+
87
+ ```bash
88
+ npm run dev # serves on http://localhost:5173
89
+ # or: npx serve . -l 5173
90
+ # or: python -m http.server 5173
91
+ ```
92
+
93
+ Then open **http://localhost:5173/demo/**.
94
+
95
+ The demo ships with an offline synthetic feed (random walk with volatility
96
+ regimes + live ticking), and optionally loads **real Binance data** (REST +
97
+ WebSocket) for BTC/ETH/SOL when the API is reachable from your network —
98
+ with graceful fallback to synthetic data if it isn't.
99
+
100
+ ---
101
+
102
+ ## Data format
103
+
104
+ Bars are plain objects; `time` accepts **milliseconds or seconds** (auto-detected).
105
+ For line-style data you can pass `{ time, value }` instead of full OHLCV.
106
+
107
+ ```js
108
+ chart.setData([
109
+ { time: 1694000000000, open: 100.5, high: 101.2, low: 99.8, close: 100.9, volume: 1200 },
110
+ // ...
111
+ ]);
112
+ ```
113
+
114
+ ## Attributes
115
+
116
+ | Attribute | Default | Description |
117
+ | ------------- | ---------- | ------------------------------------------------------------------ |
118
+ | `theme` | `dark` | `dark` or `light` |
119
+ | `type` | `candles` | `candles`, `line`, `area`, `bars` (OHLC), `hollow` (hollow up-candles), `heikin` (Heikin-Ashi) |
120
+ | `indicators` | `volume`* | Space/comma-separated: `sma:20`, `ema:50`, `bb:20`, `rsi:14`, `macd:12/26/9`, `volume`, or any registered indicator |
121
+ | `label` | – | Text shown in the legend (e.g. `"BTC · 1h"`) |
122
+ | `log` | off | Logarithmic price scale |
123
+ | `auto` | on | Keep the right edge pinned to the latest bar while streaming |
124
+ | `precision` | auto | Forced decimal places for prices (auto-detected from magnitude) |
125
+ | `stats` | off | Live statistics chip for the visible range |
126
+ | `profile` | off | Volume profile overlay (POC + 70% value area) |
127
+ | `annotations` | off | Smart annotations (volume spikes, gaps, pivots, RSI divergences) |
128
+
129
+ \* `indicators=""` disables everything, including volume. Token syntax:
130
+ `name[:param[/param…]][@color]` — e.g. `sma:20@#ff0000`, `macd:12/26/9`.
131
+
132
+ ### Built-in indicators
133
+
134
+ | Name | Kind | Params | Notes |
135
+ |---|---|---|---|
136
+ | `sma` | overlay | `period` (20) | |
137
+ | `ema` | overlay | `period` (50) | |
138
+ | `bb` | overlay | `period`, `mult` (20, 2) | Bollinger bands (3 lines) |
139
+ | `rsi` | pane | `period` (14) | fixed 0–100 scale, 30/70 guides |
140
+ | `macd` | pane | `fast/slow/signal` (12/26/9) | 2 lines + histogram |
141
+ | `volume` | overlay | – | histogram at the bottom of the price pane |
142
+
143
+ ### Custom indicators
144
+
145
+ Register your own — anything from a one-liner moving average to a multi-line
146
+ pane:
147
+
148
+ ```js
149
+ HabChart.registerIndicator('vwap', {
150
+ kind: 'overlay', // or 'pane'
151
+ params: { period: 20 }, // defaults; set via indicators="vwap:30"
152
+ compute(bars, params) { // bars: normalized {time,open,high,low,close,volume}
153
+ const out = new Array(bars.length).fill(null);
154
+ let pv = 0, vv = 0;
155
+ for (let i = 0; i < bars.length; i++) {
156
+ pv += bars[i].close * bars[i].volume;
157
+ vv += bars[i].volume;
158
+ out[i] = vv ? pv / vv : null;
159
+ }
160
+ return out; // single series — or { lines:[{name,values}], histogram }
161
+ },
162
+ // pane-only extras: guides:[30,70], range:[0,100], fmt:'price'|'fixed1'
163
+ });
164
+ chart.indicators = 'vwap:20';
165
+ ```
166
+
167
+ `import HabChart from 'wickchart'` gives you the class for
168
+ `HabChart.registerIndicator(...)` (the element is registered as a side effect
169
+ of importing the package).
170
+
171
+ ### Sonification — the chart by ear
172
+
173
+ `<hab-chart sonify>` maps price to pitch (180–880 Hz across the visible
174
+ scale, log-aware): moving the crosshair with the mouse or **arrow keys** plays
175
+ a short tone per bar, so trend and shape are audible — a rare accessibility
176
+ win for screen-reader users. `chart.playRange()` sweeps the whole visible
177
+ range as a ~4-second pitch sequence, riding the crosshair along for sighted
178
+ users. Audio starts lazily within the enabling user gesture (autoplay-policy
179
+ safe).
180
+
181
+ ### Cross-tab co-view
182
+
183
+ Tag charts with the same channel and they share pointers — across browser
184
+ tabs, or between multiple charts on one page:
185
+
186
+ ```html
187
+ <hab-chart co-view="btc-room"></hab-chart>
188
+ ```
189
+
190
+ Hovering in one tab draws a ghost crosshair (accent, dotted, with the time
191
+ pill) in every peer. Positions are synced by bar **time**, so peers with
192
+ different history depths still line up. Ghosts fade ~2.5 s after the peer
193
+ stops moving. Same-origin only (BroadcastChannel); the connection follows the
194
+ `co-view` attribute and closes with the element.
195
+
196
+ ### Smart annotations
197
+
198
+ `<hab-chart annotations>` marks notable events on the visible range — volume
199
+ spikes (>3× average), price gaps, 41-bar pivot highs/lows, and RSI
200
+ divergences — with lettered badges (V/G/H/L/D). Hover a badged bar and the
201
+ legend shows a one-line insight ("Volume 4.2× average", "Bearish RSI
202
+ divergence"). The current set is emitted on every recompute via the
203
+ `hab:annotations` event, so hosts can build their own UI from it. Badges are
204
+ hidden at extreme zoom-out, where bars collapse into columns.
205
+
206
+ ### Example: VWAP via the registry
207
+
208
+ VWAP ships in the demo but *not* as a builtin — it's the reference for writing
209
+ your own (session-anchored, resets each trading day):
210
+
211
+ ```js
212
+ import HabChart from 'wickchart';
213
+
214
+ HabChart.registerIndicator('vwap', {
215
+ kind: 'overlay',
216
+ params: {},
217
+ compute(bars) {
218
+ const out = new Array(bars.length).fill(null);
219
+ let pv = 0, vv = 0, day = -1;
220
+ for (let i = 0; i < bars.length; i++) {
221
+ const b = bars[i];
222
+ const d = new Date(b.time).setHours(0, 0, 0, 0);
223
+ if (d !== day) { day = d; pv = 0; vv = 0; }
224
+ const tp = (b.high + b.low + b.close) / 3;
225
+ pv += tp * b.volume;
226
+ vv += b.volume;
227
+ out[i] = vv ? pv / vv : null;
228
+ }
229
+ return out;
230
+ },
231
+ });
232
+ chart.indicators = 'vwap';
233
+ ```
234
+
235
+ ## Methods
236
+
237
+ | Method | Description |
238
+ | ------------------------------- | ------------------------------------------------ |
239
+ | `setData(bars)` | Replace the dataset (sorted automatically) |
240
+ | `update(bar)` | Stream: replaces last bar or appends a new one |
241
+ | `clearData()` | Empty the chart |
242
+ | `fit()` | Reset zoom to the default view (~150 bars) |
243
+ | `getVisibleRange()` | → `{ from, to }` (ms timestamps) |
244
+ | `setVisibleRange({from, to})` | Jump to a time window |
245
+ | `exportPNG()` | → PNG data URL of the current canvas |
246
+ | `getState()` | → serializable snapshot (type, indicators, view, positions, alerts) |
247
+ | `setState(state)` | Apply a snapshot; a pending view applies after the next `setData()` |
248
+
249
+ ### Infinite history (`loadMore`)
250
+
251
+ Assign a callback and the chart fetches older bars whenever the user scrolls
252
+ toward the left edge — the view stays anchored while data is prepended:
253
+
254
+ ```js
255
+ chart.onloadmore = async (fromTime) => {
256
+ const res = await fetch(`/api/bars?before=${fromTime}&limit=500`);
257
+ return res.json(); // [{ time, open, high, low, close, volume }, …]
258
+ };
259
+ ```
260
+
261
+ Return an empty array (or throw) when history is exhausted and the chart stops
262
+ asking. Data gaps (weekends, session breaks) are marked with subtle dashed
263
+ dividers on the time axis.
264
+
265
+ ### Positions & alerts
266
+
267
+ Visualize trades directly on the chart — entry/stop/target zones, a live P&L
268
+ chip, and price alerts that fire during streaming updates:
269
+
270
+ ```js
271
+ chart.addPosition({ side: 'long', entry: 64200, stop: 62900, target: 66800, qty: 0.5 });
272
+ chart.addPosition({ id: 'x1', side: 'short', entry: 66000, qty: 1 });
273
+ chart.removePosition('x1');
274
+
275
+ chart.addAlert({ price: 65000, direction: 'above' }); // 'above' | 'below' | 'cross'
276
+ chart.addEventListener('hab:alert', (e) => {
277
+ console.log('crossed!', e.detail.id, e.detail.price);
278
+ });
279
+ ```
280
+
281
+ The P&L chip recalculates on every streamed bar. Alerts are edge-triggered
282
+ (fire once per crossing) and one-shot by default (`once: false` to re-arm).
283
+
284
+ ### Stats & measure
285
+
286
+ `<hab-chart stats>` shows live statistics of the visible range — return %,
287
+ max drawdown, annualized volatility, up/down bar counts, average volume —
288
+ recalculated as you pan and zoom.
289
+
290
+ Hold **Shift and drag** across the chart to measure a move: an overlay shows
291
+ Δprice, Δ%, bar count and elapsed time, and a `hab:measure` event fires on
292
+ release (`detail.from` / `detail.to` carry index, time and price). Click or
293
+ press `Esc` to clear.
294
+
295
+ ### Shareable URLs
296
+
297
+ `getState()` / `setState()` serialize everything about the chart, and
298
+ `encodeStateQuery` / `decodeStateQuery` (exported from `src/core.js`) turn a
299
+ state into a compact query string — the demo maps it to the page hash, so any
300
+ chart configuration is one link away:
301
+
302
+ ```js
303
+ import { encodeStateQuery, decodeStateQuery } from 'wickchart/core';
304
+
305
+ const link = `${location.origin}#${encodeStateQuery(chart.getState())}`;
306
+ history.replaceState(null, '', link);
307
+ // later, on load:
308
+ chart.setState(decodeStateQuery(location.hash.slice(1)));
309
+ ```
310
+
311
+ Reflected properties (`chart.type = 'line'`) work for `theme`, `type`, `label`,
312
+ `indicators`.
313
+
314
+ ## Events
315
+
316
+ | Event | Detail |
317
+ | --------------- | ---------------------------------------------------------- |
318
+ | `hab:crosshair` | `{ index, bar, x, y, price }` on hover / arrows, `null` on leave |
319
+ | `hab:range` | `{ from, to }` after zoom / pan / jump |
320
+ | `hab:select` | `{ index, bar, price }` on click/tap (e.g. open an order form at that price) |
321
+
322
+ ## Theming
323
+
324
+ All colors are CSS custom properties settable on the element (they pierce the
325
+ Shadow DOM):
326
+
327
+ ```css
328
+ hab-chart {
329
+ --hab-bg: #0d1117; /* transparent works too */
330
+ --hab-up: #16c784;
331
+ --hab-down: #ea3943;
332
+ --hab-accent: #4c8dff; /* line & area color */
333
+ --hab-text: #8b949e; /* axis text */
334
+ --hab-text-strong: #e6edf3; /* legend values */
335
+ --hab-grid: rgba(230,237,243,.05);
336
+ --hab-border: rgba(230,237,243,.09);
337
+ --hab-crosshair: rgba(230,237,243,.42);
338
+ --hab-rsi: #a78bfa;
339
+ --hab-overlay-0: #f0b429; /* SMA color, …-1, -2, … for more overlays */
340
+ }
341
+ ```
342
+
343
+ ## Interactions
344
+
345
+ | Gesture | Action |
346
+ | -------------------------- | ----------------------------------- |
347
+ | Mouse wheel / trackpad ⌘+scroll | Zoom, anchored at the cursor |
348
+ | Trackpad horizontal scroll | Pan |
349
+ | Drag | Pan (auto-follow re-arms at the right edge) |
350
+ | Pinch (touch) | Zoom |
351
+ | Double-click | Reset view |
352
+ | `←` `→` (`+Shift` ×10) | Move crosshair |
353
+ | `+` / `−` | Zoom in / out |
354
+ | `Home` / `End` | Jump to oldest / newest |
355
+ | `Esc` | Clear crosshair |
356
+
357
+ ## Performance
358
+
359
+ Canvas 2D with a rAF-batched, visible-range-only render pipeline. Measured on a
360
+ desktop (Chromium, 1100×760, all indicators on: SMA + EMA + RSI + volume):
361
+
362
+ | Scenario | Per full render |
363
+ | ----------------------------------------- | --------------- |
364
+ | 600–50,000 bars, default view (~150 visible) | **~0.2 ms** |
365
+ | 5,000 bars, max zoom-out (~2,900 visible) | ~6 ms |
366
+ | 50,000 bars, max zoom-out (~3,100 visible) | ~16 ms |
367
+ | Streaming tick (update + full re-render) | 0.5–19 ms |
368
+
369
+ A 60 fps frame budget is 16.7 ms, so the default view uses ~1% of a frame.
370
+ Hot paths are deliberately allocation-light: date labels are built lazily only
371
+ for actual axis ticks (with cached `Intl.DateTimeFormat`s), and candles/volume
372
+ are drawn in two batched passes by direction instead of one draw call per bar.
373
+
374
+ **Deep zoom-outs are columnar**: when more bars are visible than ~1.5× the
375
+ pixel width, bars aggregate into per-pixel min/max columns (first open / max
376
+ high / min low / last close / summed volume), so rendering any history at any
377
+ zoom costs O(screen width), not O(bars). The minimum zoom level adapts to the
378
+ dataset — every chart can be zoomed out until the entire history fits.
379
+
380
+ If you ever push past this (100k+ simultaneously visible bars, dozens of
381
+ series, high-frequency ticks), the scaling levers are: incremental indicator
382
+ updates (SMA/EMA/RSI are O(1) online), min/max columnar downsampling per pixel
383
+ column, and an offscreen layer so hover only repaints the crosshair.
384
+
385
+ ## Architecture notes
386
+
387
+ - Single ES module, Custom Element + Shadow DOM, Canvas 2D with
388
+ devicePixelRatio scaling and rAF-batched invalidation
389
+ - Only visible bars are drawn; indicator series are computed lazily and cached
390
+ per data version (prefix-sum SMA, Wilder RSI)
391
+ - Time axis picks tick steps from bar interval (minutes → months) and labels
392
+ day/month boundaries like a pro terminal
393
+ - No dependencies, no build step required — but it bundles/tree-shakes fine
394
+
395
+ ## Roadmap ideas
396
+
397
+ - More overlays (Bollinger, VWAP), MACD pane, drawing tools
398
+ - Data callbacks (`loadMore` for infinite history)
399
+ - Incremental (O(1)) indicator updates for high-frequency streaming
400
+ - Min/max downsampling and/or an offscreen hover layer if profiling ever demands
401
+
402
+ ## License
403
+
404
+ MIT
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "wickchart",
3
+ "version": "0.3.0",
4
+ "description": "<hab-chart> — a modern, dependency-free financial charting web component. Candles, line & area charts, crosshair, zoom/pan, indicators, live streaming, theming.",
5
+ "type": "module",
6
+ "main": "src/hab-chart.js",
7
+ "module": "src/hab-chart.js",
8
+ "types": "types/hab-chart.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./types/hab-chart.d.ts",
12
+ "default": "./src/hab-chart.js"
13
+ },
14
+ "./core": {
15
+ "types": "./types/core.d.ts",
16
+ "default": "./src/core.js"
17
+ },
18
+ "./feed": {
19
+ "types": "./types/hab-feed.d.ts",
20
+ "default": "./src/hab-feed.js"
21
+ }
22
+ },
23
+ "files": [
24
+ "src",
25
+ "types"
26
+ ],
27
+ "sideEffects": [
28
+ "src/hab-chart.js"
29
+ ],
30
+ "scripts": {
31
+ "dev": "npx --yes serve . -l 5173",
32
+ "test": "node --test \"tests/*.test.mjs\"",
33
+ "build:types": "tsc -p tsconfig.json",
34
+ "prepack": "npm run build:types",
35
+ "ci": "npm run build:types && npm test && node --check src/hab-chart.js && node --check src/core.js && node --check demo/app.js"
36
+ },
37
+ "keywords": [
38
+ "chart",
39
+ "charting",
40
+ "candlestick",
41
+ "trading",
42
+ "finance",
43
+ "web-component",
44
+ "custom-element",
45
+ "canvas",
46
+ "zero-dependency",
47
+ "tradingview"
48
+ ],
49
+ "license": "MIT",
50
+ "repository": {
51
+ "type": "git",
52
+ "url": "git+https://github.com/benyblack/wickchart.git"
53
+ },
54
+ "devDependencies": {
55
+ "typescript": "^7.0.2"
56
+ }
57
+ }