@zuttora/charts 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +62 -2
- package/dist/chart.d.ts +8 -0
- package/dist/chart.js +238 -0
- package/dist/chartState.d.ts +47 -0
- package/dist/chartState.js +64 -0
- package/dist/core/drawings.d.ts +21 -0
- package/dist/core/drawings.js +46 -0
- package/dist/core/emitter.d.ts +8 -0
- package/dist/core/emitter.js +26 -0
- package/dist/core/format.d.ts +9 -0
- package/dist/core/format.js +35 -0
- package/dist/core/heikinAshi.d.ts +2 -0
- package/dist/core/heikinAshi.js +14 -0
- package/dist/core/indicatorSet.d.ts +20 -0
- package/dist/core/indicatorSet.js +66 -0
- package/dist/core/indicators.d.ts +53 -0
- package/dist/core/indicators.js +187 -0
- package/dist/core/priceScale.d.ts +22 -0
- package/dist/core/priceScale.js +92 -0
- package/dist/core/series.d.ts +41 -0
- package/dist/core/series.js +221 -0
- package/dist/core/time.d.ts +25 -0
- package/dist/core/time.js +44 -0
- package/dist/core/timeLabels.d.ts +11 -0
- package/dist/core/timeLabels.js +52 -0
- package/dist/core/types.d.ts +134 -0
- package/dist/core/types.js +1 -0
- package/dist/core/viewport.d.ts +41 -0
- package/dist/core/viewport.js +116 -0
- package/dist/core/warn.d.ts +1 -0
- package/dist/core/warn.js +3 -0
- package/dist/createChart.d.ts +2 -0
- package/dist/createChart.js +7 -0
- package/dist/dom/environment.d.ts +11 -0
- package/dist/dom/environment.js +49 -0
- package/dist/dom/indicatorReadings.d.ts +7 -0
- package/dist/dom/indicatorReadings.js +26 -0
- package/dist/dom/legend.d.ts +19 -0
- package/dist/dom/legend.js +62 -0
- package/dist/dom/legendSync.d.ts +5 -0
- package/dist/dom/legendSync.js +30 -0
- package/dist/dom/root.d.ts +9 -0
- package/dist/dom/root.js +19 -0
- package/dist/draw/controller.d.ts +36 -0
- package/dist/draw/controller.js +151 -0
- package/dist/draw/geometry.d.ts +44 -0
- package/dist/draw/geometry.js +118 -0
- package/dist/draw/mapper.d.ts +30 -0
- package/dist/draw/mapper.js +32 -0
- package/dist/draw/pointer.d.ts +17 -0
- package/dist/draw/pointer.js +144 -0
- package/dist/draw/state.d.ts +61 -0
- package/dist/draw/state.js +8 -0
- package/dist/drawApi.d.ts +20 -0
- package/dist/drawApi.js +59 -0
- package/dist/host.d.ts +35 -0
- package/dist/host.js +63 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +5 -0
- package/dist/interact/attach.d.ts +2 -0
- package/dist/interact/attach.js +142 -0
- package/dist/interact/gestures.d.ts +47 -0
- package/dist/interact/gestures.js +179 -0
- package/dist/interact/keyboard.d.ts +19 -0
- package/dist/interact/keyboard.js +34 -0
- package/dist/interact/wheel.d.ts +15 -0
- package/dist/interact/wheel.js +18 -0
- package/dist/render/bars.d.ts +17 -0
- package/dist/render/bars.js +55 -0
- package/dist/render/canvas.d.ts +2 -0
- package/dist/render/canvas.js +13 -0
- package/dist/render/drawAxes.d.ts +2 -0
- package/dist/render/drawAxes.js +34 -0
- package/dist/render/drawCandles.d.ts +3 -0
- package/dist/render/drawCandles.js +68 -0
- package/dist/render/drawCrosshair.d.ts +7 -0
- package/dist/render/drawCrosshair.js +42 -0
- package/dist/render/drawDrawings.d.ts +10 -0
- package/dist/render/drawDrawings.js +146 -0
- package/dist/render/drawGrid.d.ts +2 -0
- package/dist/render/drawGrid.js +22 -0
- package/dist/render/drawIndicators.d.ts +4 -0
- package/dist/render/drawIndicators.js +67 -0
- package/dist/render/drawLastPrice.d.ts +10 -0
- package/dist/render/drawLastPrice.js +53 -0
- package/dist/render/drawLine.d.ts +3 -0
- package/dist/render/drawLine.js +43 -0
- package/dist/render/drawMain.d.ts +2 -0
- package/dist/render/drawMain.js +43 -0
- package/dist/render/drawMarkers.d.ts +2 -0
- package/dist/render/drawMarkers.js +39 -0
- package/dist/render/drawPriceLines.d.ts +4 -0
- package/dist/render/drawPriceLines.js +60 -0
- package/dist/render/drawSubPanes.d.ts +2 -0
- package/dist/render/drawSubPanes.js +104 -0
- package/dist/render/frame.d.ts +57 -0
- package/dist/render/frame.js +5 -0
- package/dist/render/frameBuilder.d.ts +12 -0
- package/dist/render/frameBuilder.js +129 -0
- package/dist/render/indicatorFrame.d.ts +17 -0
- package/dist/render/indicatorFrame.js +55 -0
- package/dist/render/layout.d.ts +4 -0
- package/dist/render/layout.js +26 -0
- package/dist/render/paintFrame.d.ts +11 -0
- package/dist/render/paintFrame.js +33 -0
- package/dist/render/scheduler.d.ts +17 -0
- package/dist/render/scheduler.js +38 -0
- package/dist/render/tween.d.ts +12 -0
- package/dist/render/tween.js +38 -0
- package/dist/theme.d.ts +2 -0
- package/dist/theme.js +16 -0
- package/package.json +37 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nishit
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,63 @@
|
|
|
1
|
-
#
|
|
1
|
+
# chartZ (`@zuttora/charts`)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Zuttora's own charting package: candlesticks on an HTML canvas, square corners, no runtime
|
|
4
|
+
dependencies. The full design is in Zuttora's `docs/charting.md` (section 6).
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
import { createChart, zuttoraTheme, INTERVAL_MS, IST_OFFSET_MS } from '@zuttora/charts';
|
|
8
|
+
|
|
9
|
+
const chart = createChart(container, { // the container must have a height
|
|
10
|
+
intervalMs: INTERVAL_MS['5m'],
|
|
11
|
+
tzOffsetMs: IST_OFFSET_MS,
|
|
12
|
+
priceDecimals: 2,
|
|
13
|
+
minMove: 0.05,
|
|
14
|
+
theme: { ...zuttoraTheme, fontFamily: resolvedFontFamily },
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
chart.setData(closedCandles, formingCandle, hasMore); // Candle = { t, o, h, l, c }, t = slot start in epoch ms
|
|
18
|
+
chart.applyTick(price, serverTime); // live prices: the last candle moves, slots roll over
|
|
19
|
+
chart.on('needMore', (oldest) => { /* load older candles, then chart.prependData(older, hasMore) */ });
|
|
20
|
+
chart.on('liveChange', (atLive) => { /* show or hide a "Go to live" button */ });
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Also: `replaceClosed`, `setOptions`, `setPriceLines`, `setMarkers`, `setIndicators`, `setDrawingTool`, `setDrawings`, `getDrawings`, `updateDrawing`, `deleteSelected`, `clearDrawings`, `setDrawingColor`, `scrollToLive`, `fitAll`, `destroy`.
|
|
24
|
+
|
|
25
|
+
Drawing tools (`setDrawingTool(tool)`): `hline`, `trend`, `ray`, `rect`, `fib`, `note` and `measure` (a ruler that is never saved). Tap or click to place each point, or press and drag. A finished drawing is picked; drag it to move it, drag a square to move one point, `Delete` removes it, `Escape` backs out. Drawings sit at a time and a price, so they stay in place when the candle size changes.
|
|
26
|
+
The host owns saving: listen to `drawingsChange(list)`, store it, and restore with `setDrawings(list)`. `drawingEdit(drawing)` fires when a note needs its words (`updateDrawing(id, { text })`); `drawProgress` tells a hint line how far along the trader is; `setDrawingColor(color)` colours the next drawing and the picked one.
|
|
27
|
+
|
|
28
|
+
Indicators (`setIndicators(specs)`, diffed by `id`): `sma`, `ema` and `bollinger` are drawn over the candles; `rsi` and `macd` get their own pane under the price.
|
|
29
|
+
They are calculated from every loaded close, the forming candle included, so they move live.
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
chart.setIndicators([
|
|
33
|
+
{ id: 'sma20', type: 'sma', period: 20, color: '#FFD34D' },
|
|
34
|
+
{ id: 'rsi', type: 'rsi', period: 14, color: '#C792EA' },
|
|
35
|
+
]);
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Gestures: drag to pan, wheel or pinch to zoom, press and hold (touch) or hover (mouse) for the
|
|
39
|
+
crosshair, drag the price axis to scale it, double-click or double-tap to reset, arrow keys, `+`/`-`,
|
|
40
|
+
`Home`, `End`.
|
|
41
|
+
|
|
42
|
+
## Status
|
|
43
|
+
|
|
44
|
+
| | |
|
|
45
|
+
|---|---|
|
|
46
|
+
| Core, rendering, interaction (M3 to M5) | done |
|
|
47
|
+
| Indicators (M8) | done, `0.2.0` |
|
|
48
|
+
| Drawing tools (M9) | done, `0.3.0` |
|
|
49
|
+
| Published to npm | after review (needs the `NPM_TOKEN` secret) |
|
|
50
|
+
|
|
51
|
+
## Develop
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
yarn install
|
|
55
|
+
yarn typecheck && yarn lint && yarn test && yarn build && yarn size
|
|
56
|
+
python3 -m http.server # then open http://localhost:8000/demo/ (after yarn build)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Demo URL options: `?type=candles|line|area|heikinAshi&interval=1m|5m|10m|1h|3h|6h|1D&tick=200` (ms between fake ticks).
|
|
60
|
+
|
|
61
|
+
Rules: no runtime dependencies, TypeScript that Node can run by stripping types (no `enum`,
|
|
62
|
+
`namespace` or constructor parameter properties), relative imports with the `.ts` extension, no
|
|
63
|
+
rounded corners, no `console` outside `src/core/warn.ts`.
|
package/dist/chart.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Chart, ChartOptions } from './core/types.ts';
|
|
2
|
+
import type { ChartHost } from './host.ts';
|
|
3
|
+
export type { ChartHost } from './host.ts';
|
|
4
|
+
export interface ChartCore {
|
|
5
|
+
chart: Chart;
|
|
6
|
+
host: ChartHost;
|
|
7
|
+
}
|
|
8
|
+
export declare function createChartCore(container: HTMLElement, options: ChartOptions): ChartCore;
|
package/dist/chart.js
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { createDrawApi } from "./drawApi.js";
|
|
2
|
+
import { Emitter } from "./core/emitter.js";
|
|
3
|
+
import { buildDom } from "./dom/root.js";
|
|
4
|
+
import { watchEnvironment } from "./dom/environment.js";
|
|
5
|
+
import { syncLegend } from "./dom/legendSync.js";
|
|
6
|
+
import { createHost } from "./host.js";
|
|
7
|
+
import { createState, resolveOptions } from "./chartState.js";
|
|
8
|
+
import { CLOSE_TWEEN_MS } from "./render/frameBuilder.js";
|
|
9
|
+
import { paintFrame } from "./render/paintFrame.js";
|
|
10
|
+
import { Scheduler } from "./render/scheduler.js";
|
|
11
|
+
const perf = () => (typeof performance !== 'undefined' ? performance.now() : Date.now());
|
|
12
|
+
export function createChartCore(container, options) {
|
|
13
|
+
const doc = container.ownerDocument;
|
|
14
|
+
const s = createState(resolveOptions(options));
|
|
15
|
+
const events = new Emitter();
|
|
16
|
+
let destroyed = false;
|
|
17
|
+
let lastFrame = null;
|
|
18
|
+
const { root, mainCanvas, overlayCanvas, legend } = buildDom(doc, s.options.theme);
|
|
19
|
+
container.appendChild(root);
|
|
20
|
+
const applyAria = () => root.setAttribute('aria-label', s.options.ariaLabel);
|
|
21
|
+
const applyFocusRing = () => {
|
|
22
|
+
root.style.setProperty('--zc-focus', s.options.theme.accent);
|
|
23
|
+
};
|
|
24
|
+
applyAria();
|
|
25
|
+
applyFocusRing();
|
|
26
|
+
const updateLegend = () => syncLegend(s, legend);
|
|
27
|
+
const checkEvents = () => {
|
|
28
|
+
const live = s.vp.atLive();
|
|
29
|
+
if (live !== s.lastLive) {
|
|
30
|
+
s.lastLive = live;
|
|
31
|
+
events.emit('liveChange', live);
|
|
32
|
+
}
|
|
33
|
+
if (s.series.count > 0 && s.moreGate.check(s.vp.leftIndex(), s.series.hasMore)) {
|
|
34
|
+
events.emit('needMore', s.series.timeAt(0));
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const draw = (flags, now) => {
|
|
38
|
+
if (destroyed)
|
|
39
|
+
return false;
|
|
40
|
+
return paintFrame(s, {
|
|
41
|
+
mainCanvas,
|
|
42
|
+
overlayCanvas,
|
|
43
|
+
dpr: () => env.dpr(),
|
|
44
|
+
frameBuilt: (f) => {
|
|
45
|
+
lastFrame = f;
|
|
46
|
+
},
|
|
47
|
+
afterDraw: checkEvents,
|
|
48
|
+
}, flags, now);
|
|
49
|
+
};
|
|
50
|
+
const scheduler = new Scheduler(draw);
|
|
51
|
+
const invalidate = (layer) => scheduler.invalidate(layer);
|
|
52
|
+
const invalidateAll = () => {
|
|
53
|
+
invalidate('main');
|
|
54
|
+
invalidate('overlay');
|
|
55
|
+
};
|
|
56
|
+
const resize = () => {
|
|
57
|
+
const r = container.getBoundingClientRect();
|
|
58
|
+
s.width = Math.max(0, Math.floor(r.width));
|
|
59
|
+
s.height = Math.max(0, Math.floor(r.height));
|
|
60
|
+
invalidateAll();
|
|
61
|
+
};
|
|
62
|
+
resize();
|
|
63
|
+
const env = watchEnvironment(container, doc, {
|
|
64
|
+
resize,
|
|
65
|
+
densityChanged: invalidateAll,
|
|
66
|
+
second: () => {
|
|
67
|
+
if (s.series.forming)
|
|
68
|
+
invalidate('overlay');
|
|
69
|
+
},
|
|
70
|
+
visible: invalidateAll,
|
|
71
|
+
});
|
|
72
|
+
const animNow = perf;
|
|
73
|
+
const drawApi = createDrawApi({
|
|
74
|
+
s,
|
|
75
|
+
root,
|
|
76
|
+
events,
|
|
77
|
+
invalidate,
|
|
78
|
+
lastFrame: () => lastFrame,
|
|
79
|
+
now: animNow,
|
|
80
|
+
isDestroyed: () => destroyed,
|
|
81
|
+
});
|
|
82
|
+
const chart = {
|
|
83
|
+
setData(closed, forming, hasMore) {
|
|
84
|
+
if (destroyed)
|
|
85
|
+
return;
|
|
86
|
+
const hadData = s.series.count > 0;
|
|
87
|
+
const wasLive = !hadData || s.vp.atLive();
|
|
88
|
+
const edge = Math.round(s.vp.rightIndex);
|
|
89
|
+
const frac = s.vp.rightIndex - edge;
|
|
90
|
+
const keepT = hadData && !wasLive ? s.series.timeAt(Math.min(edge, s.series.lastIndex())) : null;
|
|
91
|
+
s.series.setData(closed, forming, hasMore);
|
|
92
|
+
s.haBars.rebuild();
|
|
93
|
+
s.indicators.rebuild(s.series);
|
|
94
|
+
s.vp.setLastIndex(s.series.lastIndex());
|
|
95
|
+
s.moreGate.clear();
|
|
96
|
+
s.closeTween.jump(forming ? forming.c : 0);
|
|
97
|
+
s.rightAnimating = false;
|
|
98
|
+
if (keepT !== null) {
|
|
99
|
+
const i = s.series.indexAtTime(keepT);
|
|
100
|
+
if (i >= 0 && s.series.timeAt(i) === keepT)
|
|
101
|
+
s.vp.setRightIndex(i + frac);
|
|
102
|
+
else
|
|
103
|
+
s.vp.goLive();
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
s.vp.goLive();
|
|
107
|
+
}
|
|
108
|
+
s.rangeReady = false;
|
|
109
|
+
updateLegend();
|
|
110
|
+
invalidateAll();
|
|
111
|
+
},
|
|
112
|
+
prependData(older, hasMore) {
|
|
113
|
+
if (destroyed)
|
|
114
|
+
return;
|
|
115
|
+
const added = s.series.prependData(older, hasMore);
|
|
116
|
+
if (added > 0) {
|
|
117
|
+
s.vp.onPrepend(added);
|
|
118
|
+
s.haBars.rebuild();
|
|
119
|
+
s.indicators.rebuild(s.series);
|
|
120
|
+
}
|
|
121
|
+
s.moreGate.clear();
|
|
122
|
+
updateLegend();
|
|
123
|
+
invalidateAll();
|
|
124
|
+
},
|
|
125
|
+
replaceClosed(candles) {
|
|
126
|
+
if (destroyed)
|
|
127
|
+
return;
|
|
128
|
+
if (s.series.replaceClosed(candles) > 0) {
|
|
129
|
+
s.haBars.rebuild();
|
|
130
|
+
s.indicators.rebuild(s.series);
|
|
131
|
+
updateLegend();
|
|
132
|
+
invalidateAll();
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
applyTick(price, time) {
|
|
136
|
+
if (destroyed)
|
|
137
|
+
return;
|
|
138
|
+
const r = s.series.applyTick(price, time, s.options.intervalMs, s.options.tzOffsetMs);
|
|
139
|
+
if (r.kind === 'ignored')
|
|
140
|
+
return;
|
|
141
|
+
s.indicators.onTick(s.series, r);
|
|
142
|
+
const t = animNow();
|
|
143
|
+
if (r.kind === 'updated') {
|
|
144
|
+
s.closeTween.set(price, t, s.options.reducedMotion ? 0 : CLOSE_TWEEN_MS);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
if (r.kind === 'closed' && r.shifted > 0)
|
|
148
|
+
s.vp.onShift(r.shifted);
|
|
149
|
+
s.vp.onAppend(s.series.lastIndex());
|
|
150
|
+
s.closeTween.jump(price);
|
|
151
|
+
s.haBars.rebuild();
|
|
152
|
+
}
|
|
153
|
+
s.haBars.refreshForming();
|
|
154
|
+
if (!s.options.reducedMotion && (s.options.type === 'line' || s.options.type === 'area')) {
|
|
155
|
+
s.pulses.push(t);
|
|
156
|
+
if (s.pulses.length > 8)
|
|
157
|
+
s.pulses.shift();
|
|
158
|
+
}
|
|
159
|
+
if (r.kind === 'closed')
|
|
160
|
+
events.emit('candleClosed', r.closed);
|
|
161
|
+
if (!s.crosshair)
|
|
162
|
+
updateLegend();
|
|
163
|
+
invalidateAll();
|
|
164
|
+
},
|
|
165
|
+
setOptions(patch) {
|
|
166
|
+
if (destroyed)
|
|
167
|
+
return;
|
|
168
|
+
const next = resolveOptions({ ...s.options, ...patch, labels: { ...s.options.labels, ...patch.labels } });
|
|
169
|
+
s.options = next;
|
|
170
|
+
s.haBars.rebuild();
|
|
171
|
+
legend.applyTheme(next.theme);
|
|
172
|
+
applyAria();
|
|
173
|
+
applyFocusRing();
|
|
174
|
+
s.textWidths.clear();
|
|
175
|
+
s.rangeReady = false;
|
|
176
|
+
updateLegend();
|
|
177
|
+
invalidateAll();
|
|
178
|
+
},
|
|
179
|
+
setPriceLines(lines) {
|
|
180
|
+
if (destroyed)
|
|
181
|
+
return;
|
|
182
|
+
s.lines = lines.slice();
|
|
183
|
+
invalidate('main');
|
|
184
|
+
},
|
|
185
|
+
setMarkers(markers) {
|
|
186
|
+
if (destroyed)
|
|
187
|
+
return;
|
|
188
|
+
s.markers = markers.slice();
|
|
189
|
+
updateLegend();
|
|
190
|
+
invalidate('main');
|
|
191
|
+
},
|
|
192
|
+
setIndicators(specs) {
|
|
193
|
+
if (destroyed)
|
|
194
|
+
return;
|
|
195
|
+
s.indicators.setSpecs(specs, s.series);
|
|
196
|
+
s.rangeReady = false;
|
|
197
|
+
updateLegend();
|
|
198
|
+
invalidateAll();
|
|
199
|
+
},
|
|
200
|
+
...drawApi.methods,
|
|
201
|
+
scrollToLive() {
|
|
202
|
+
host.scrollToLive();
|
|
203
|
+
},
|
|
204
|
+
fitAll() {
|
|
205
|
+
if (destroyed)
|
|
206
|
+
return;
|
|
207
|
+
s.vp.fitAll();
|
|
208
|
+
s.rightAnimating = false;
|
|
209
|
+
invalidateAll();
|
|
210
|
+
},
|
|
211
|
+
on(event, listener) {
|
|
212
|
+
return events.on(event, listener);
|
|
213
|
+
},
|
|
214
|
+
destroy() {
|
|
215
|
+
if (destroyed)
|
|
216
|
+
return;
|
|
217
|
+
destroyed = true;
|
|
218
|
+
scheduler.stop();
|
|
219
|
+
env.dispose();
|
|
220
|
+
events.clear();
|
|
221
|
+
host.dispose();
|
|
222
|
+
root.remove();
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
const host = createHost({
|
|
226
|
+
root,
|
|
227
|
+
state: s,
|
|
228
|
+
draw: drawApi.controller,
|
|
229
|
+
events,
|
|
230
|
+
invalidate,
|
|
231
|
+
invalidateAll,
|
|
232
|
+
updateLegend,
|
|
233
|
+
isDestroyed: () => destroyed,
|
|
234
|
+
lastFrame: () => lastFrame,
|
|
235
|
+
now: animNow,
|
|
236
|
+
});
|
|
237
|
+
return { chart, host };
|
|
238
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { CandleSeries } from './core/series.ts';
|
|
2
|
+
import { PriceScale } from './core/priceScale.ts';
|
|
3
|
+
import type { Drawing } from './core/drawings.ts';
|
|
4
|
+
import { IndicatorSet } from './core/indicatorSet.ts';
|
|
5
|
+
import type { DrawState } from './draw/controller.ts';
|
|
6
|
+
import { MoreGate, Viewport } from './core/viewport.ts';
|
|
7
|
+
import type { ChartLabels, ChartOptions, Marker, PriceLine } from './core/types.ts';
|
|
8
|
+
import { HeikinBars, SeriesBars } from './render/bars.ts';
|
|
9
|
+
import type { CrosshairInput } from './render/drawCrosshair.ts';
|
|
10
|
+
import { Tween } from './render/tween.ts';
|
|
11
|
+
export declare const DEFAULT_LABELS: ChartLabels;
|
|
12
|
+
export interface ResolvedOptions extends Required<Omit<ChartOptions, 'labels' | 'ariaLabel' | 'now' | 'reducedMotion'>> {
|
|
13
|
+
labels: ChartLabels;
|
|
14
|
+
ariaLabel: string;
|
|
15
|
+
now: () => number;
|
|
16
|
+
reducedMotion: boolean;
|
|
17
|
+
}
|
|
18
|
+
export declare function resolveOptions(o: ChartOptions): ResolvedOptions;
|
|
19
|
+
export interface ChartState {
|
|
20
|
+
options: ResolvedOptions;
|
|
21
|
+
series: CandleSeries;
|
|
22
|
+
vp: Viewport;
|
|
23
|
+
scale: PriceScale;
|
|
24
|
+
plainBars: SeriesBars;
|
|
25
|
+
haBars: HeikinBars;
|
|
26
|
+
moreGate: MoreGate;
|
|
27
|
+
lines: readonly PriceLine[];
|
|
28
|
+
markers: readonly Marker[];
|
|
29
|
+
indicators: IndicatorSet;
|
|
30
|
+
drawings: Drawing[];
|
|
31
|
+
draw: DrawState;
|
|
32
|
+
rangeMin: Tween;
|
|
33
|
+
rangeMax: Tween;
|
|
34
|
+
rangeReady: boolean;
|
|
35
|
+
closeTween: Tween;
|
|
36
|
+
rightTween: Tween;
|
|
37
|
+
rightAnimating: boolean;
|
|
38
|
+
axisWidth: number;
|
|
39
|
+
textWidths: Map<string, number>;
|
|
40
|
+
width: number;
|
|
41
|
+
height: number;
|
|
42
|
+
crosshair: CrosshairInput | null;
|
|
43
|
+
pulses: number[];
|
|
44
|
+
lastLive: boolean;
|
|
45
|
+
lastCrosshairTime: number | null;
|
|
46
|
+
}
|
|
47
|
+
export declare function createState(options: ResolvedOptions): ChartState;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { CandleSeries } from "./core/series.js";
|
|
2
|
+
import { PriceScale } from "./core/priceScale.js";
|
|
3
|
+
import { IndicatorSet } from "./core/indicatorSet.js";
|
|
4
|
+
import { createDrawState } from "./draw/controller.js";
|
|
5
|
+
import { MoreGate, Viewport } from "./core/viewport.js";
|
|
6
|
+
import { HeikinBars, SeriesBars } from "./render/bars.js";
|
|
7
|
+
import { Tween } from "./render/tween.js";
|
|
8
|
+
export const DEFAULT_LABELS = {
|
|
9
|
+
open: 'O',
|
|
10
|
+
high: 'H',
|
|
11
|
+
low: 'L',
|
|
12
|
+
close: 'C',
|
|
13
|
+
change: 'Change',
|
|
14
|
+
noData: 'No data yet',
|
|
15
|
+
heikinAshi: 'Heikin-Ashi (smoothed)',
|
|
16
|
+
};
|
|
17
|
+
export function resolveOptions(o) {
|
|
18
|
+
return {
|
|
19
|
+
intervalMs: o.intervalMs,
|
|
20
|
+
tzOffsetMs: o.tzOffsetMs,
|
|
21
|
+
priceDecimals: o.priceDecimals,
|
|
22
|
+
minMove: o.minMove ?? 10 ** -o.priceDecimals,
|
|
23
|
+
type: o.type ?? 'candles',
|
|
24
|
+
candleStyle: o.candleStyle ?? 'hollowUp',
|
|
25
|
+
theme: o.theme,
|
|
26
|
+
labels: { ...DEFAULT_LABELS, ...o.labels },
|
|
27
|
+
locale: o.locale ?? 'en-IN',
|
|
28
|
+
now: o.now ?? (() => Date.now()),
|
|
29
|
+
reducedMotion: o.reducedMotion ??
|
|
30
|
+
(typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches),
|
|
31
|
+
ariaLabel: o.ariaLabel ?? 'Price chart',
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function createState(options) {
|
|
35
|
+
const series = new CandleSeries();
|
|
36
|
+
return {
|
|
37
|
+
options,
|
|
38
|
+
series,
|
|
39
|
+
vp: new Viewport(),
|
|
40
|
+
scale: new PriceScale(),
|
|
41
|
+
plainBars: new SeriesBars(series),
|
|
42
|
+
haBars: new HeikinBars(series),
|
|
43
|
+
moreGate: new MoreGate(),
|
|
44
|
+
lines: [],
|
|
45
|
+
markers: [],
|
|
46
|
+
indicators: new IndicatorSet(),
|
|
47
|
+
drawings: [],
|
|
48
|
+
draw: createDrawState(options.theme.accent),
|
|
49
|
+
rangeMin: new Tween(0),
|
|
50
|
+
rangeMax: new Tween(1),
|
|
51
|
+
rangeReady: false,
|
|
52
|
+
closeTween: new Tween(0),
|
|
53
|
+
rightTween: new Tween(0),
|
|
54
|
+
rightAnimating: false,
|
|
55
|
+
axisWidth: 0,
|
|
56
|
+
textWidths: new Map(),
|
|
57
|
+
width: 0,
|
|
58
|
+
height: 0,
|
|
59
|
+
crosshair: null,
|
|
60
|
+
pulses: [],
|
|
61
|
+
lastLive: true,
|
|
62
|
+
lastCrosshairTime: null,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Time } from './types.ts';
|
|
2
|
+
export type DrawingTool = 'hline' | 'trend' | 'ray' | 'rect' | 'fib' | 'note' | 'measure';
|
|
3
|
+
export type DrawingKind = Exclude<DrawingTool, 'measure'>;
|
|
4
|
+
export interface Point {
|
|
5
|
+
t: Time;
|
|
6
|
+
price: number;
|
|
7
|
+
}
|
|
8
|
+
export interface Drawing {
|
|
9
|
+
id: string;
|
|
10
|
+
type: DrawingKind;
|
|
11
|
+
points: Point[];
|
|
12
|
+
color: string;
|
|
13
|
+
text?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare const MAX_DRAWINGS = 100;
|
|
16
|
+
export declare const MAX_NOTE_LENGTH = 60;
|
|
17
|
+
export declare const FIB_LEVELS: readonly number[];
|
|
18
|
+
export declare function pointsNeeded(tool: DrawingTool): number;
|
|
19
|
+
export declare function isKind(tool: string): tool is DrawingKind;
|
|
20
|
+
export declare function newDrawingId(): string;
|
|
21
|
+
export declare function sanitizeDrawings(list: readonly unknown[]): Drawing[];
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export const MAX_DRAWINGS = 100;
|
|
2
|
+
export const MAX_NOTE_LENGTH = 60;
|
|
3
|
+
export const FIB_LEVELS = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1];
|
|
4
|
+
const KINDS = ['hline', 'trend', 'ray', 'rect', 'fib', 'note'];
|
|
5
|
+
export function pointsNeeded(tool) {
|
|
6
|
+
return tool === 'hline' || tool === 'note' ? 1 : 2;
|
|
7
|
+
}
|
|
8
|
+
export function isKind(tool) {
|
|
9
|
+
return KINDS.includes(tool);
|
|
10
|
+
}
|
|
11
|
+
let counter = 0;
|
|
12
|
+
export function newDrawingId() {
|
|
13
|
+
counter += 1;
|
|
14
|
+
return `d${Date.now().toString(36)}${counter.toString(36)}`;
|
|
15
|
+
}
|
|
16
|
+
const finite = (n) => typeof n === 'number' && Number.isFinite(n);
|
|
17
|
+
export function sanitizeDrawings(list) {
|
|
18
|
+
const out = [];
|
|
19
|
+
const ids = new Set();
|
|
20
|
+
for (const raw of list) {
|
|
21
|
+
if (out.length >= MAX_DRAWINGS)
|
|
22
|
+
break;
|
|
23
|
+
if (typeof raw !== 'object' || raw === null)
|
|
24
|
+
continue;
|
|
25
|
+
const d = raw;
|
|
26
|
+
if (typeof d.id !== 'string' || ids.has(d.id) || typeof d.type !== 'string' || !isKind(d.type))
|
|
27
|
+
continue;
|
|
28
|
+
if (typeof d.color !== 'string' || !Array.isArray(d.points))
|
|
29
|
+
continue;
|
|
30
|
+
const need = pointsNeeded(d.type);
|
|
31
|
+
const points = [];
|
|
32
|
+
for (const p of d.points.slice(0, need)) {
|
|
33
|
+
const q = p;
|
|
34
|
+
if (q && finite(q.t) && finite(q.price))
|
|
35
|
+
points.push({ t: q.t, price: q.price });
|
|
36
|
+
}
|
|
37
|
+
if (points.length !== need)
|
|
38
|
+
continue;
|
|
39
|
+
const drawing = { id: d.id, type: d.type, points, color: d.color };
|
|
40
|
+
if (d.type === 'note')
|
|
41
|
+
drawing.text = typeof d.text === 'string' ? d.text.slice(0, MAX_NOTE_LENGTH) : '';
|
|
42
|
+
ids.add(d.id);
|
|
43
|
+
out.push(drawing);
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare class Emitter<Events extends {
|
|
2
|
+
[K in keyof Events]: (...args: never[]) => void;
|
|
3
|
+
}> {
|
|
4
|
+
private map;
|
|
5
|
+
on<E extends keyof Events>(event: E, listener: Events[E]): () => void;
|
|
6
|
+
emit<E extends keyof Events>(event: E, ...args: Parameters<Events[E]>): void;
|
|
7
|
+
clear(): void;
|
|
8
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export class Emitter {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.map = new Map();
|
|
4
|
+
}
|
|
5
|
+
on(event, listener) {
|
|
6
|
+
let set = this.map.get(event);
|
|
7
|
+
if (!set) {
|
|
8
|
+
set = new Set();
|
|
9
|
+
this.map.set(event, set);
|
|
10
|
+
}
|
|
11
|
+
set.add(listener);
|
|
12
|
+
return () => {
|
|
13
|
+
set.delete(listener);
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
emit(event, ...args) {
|
|
17
|
+
const set = this.map.get(event);
|
|
18
|
+
if (!set)
|
|
19
|
+
return;
|
|
20
|
+
for (const fn of [...set])
|
|
21
|
+
fn(...args);
|
|
22
|
+
}
|
|
23
|
+
clear() {
|
|
24
|
+
this.map.clear();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare function priceFormatter(decimals: number, locale?: string): (value: number) => string;
|
|
2
|
+
export declare function formatCountdown(msLeft: number): string;
|
|
3
|
+
export interface ChangeParts {
|
|
4
|
+
direction: 1 | -1 | 0;
|
|
5
|
+
arrow: '▲' | '▼' | '';
|
|
6
|
+
amount: string;
|
|
7
|
+
percent: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function changeParts(o: number, c: number, decimals: number, locale?: string): ChangeParts;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const formatters = new Map();
|
|
2
|
+
export function priceFormatter(decimals, locale = 'en-IN') {
|
|
3
|
+
const key = `${locale}|${decimals}`;
|
|
4
|
+
let f = formatters.get(key);
|
|
5
|
+
if (!f) {
|
|
6
|
+
f = new Intl.NumberFormat(locale, { minimumFractionDigits: decimals, maximumFractionDigits: decimals });
|
|
7
|
+
formatters.set(key, f);
|
|
8
|
+
}
|
|
9
|
+
const fmt = f;
|
|
10
|
+
return (value) => fmt.format(value);
|
|
11
|
+
}
|
|
12
|
+
export function formatCountdown(msLeft) {
|
|
13
|
+
const total = Math.max(0, Math.ceil(msLeft / 1000));
|
|
14
|
+
const h = Math.floor(total / 3600);
|
|
15
|
+
const m = Math.floor((total % 3600) / 60);
|
|
16
|
+
const s = total % 60;
|
|
17
|
+
const ss = s < 10 ? `0${s}` : String(s);
|
|
18
|
+
if (h > 0)
|
|
19
|
+
return `${h}:${m < 10 ? '0' : ''}${m}:${ss}`;
|
|
20
|
+
return `${m}:${ss}`;
|
|
21
|
+
}
|
|
22
|
+
export function changeParts(o, c, decimals, locale = 'en-IN') {
|
|
23
|
+
const fmt = priceFormatter(decimals, locale);
|
|
24
|
+
const pctFmt = priceFormatter(2, locale);
|
|
25
|
+
const diff = c - o;
|
|
26
|
+
const direction = diff > 0 ? 1 : diff < 0 ? -1 : 0;
|
|
27
|
+
const pct = o !== 0 ? (diff / o) * 100 : 0;
|
|
28
|
+
const sign = direction > 0 ? '+' : direction < 0 ? '−' : '';
|
|
29
|
+
return {
|
|
30
|
+
direction,
|
|
31
|
+
arrow: direction > 0 ? '▲' : direction < 0 ? '▼' : '',
|
|
32
|
+
amount: `${sign}${fmt(Math.abs(diff))}`,
|
|
33
|
+
percent: `${sign}${pctFmt(Math.abs(pct))}%`,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function heikinAshi(candles) {
|
|
2
|
+
const out = [];
|
|
3
|
+
let prevO = 0;
|
|
4
|
+
let prevC = 0;
|
|
5
|
+
for (let i = 0; i < candles.length; i++) {
|
|
6
|
+
const k = candles[i];
|
|
7
|
+
const c = (k.o + k.h + k.l + k.c) / 4;
|
|
8
|
+
const o = i === 0 ? (k.o + k.c) / 2 : (prevO + prevC) / 2;
|
|
9
|
+
out.push({ t: k.t, o, h: Math.max(k.h, o, c), l: Math.min(k.l, o, c), c });
|
|
10
|
+
prevO = o;
|
|
11
|
+
prevC = c;
|
|
12
|
+
}
|
|
13
|
+
return out;
|
|
14
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { CandleSeries, TickResult } from './series.ts';
|
|
2
|
+
import type { Calc } from './indicators.ts';
|
|
3
|
+
import type { IndicatorSpec } from './types.ts';
|
|
4
|
+
export declare const MAX_INDICATORS = 8;
|
|
5
|
+
export interface IndicatorEntry {
|
|
6
|
+
spec: IndicatorSpec;
|
|
7
|
+
calc: Calc;
|
|
8
|
+
key: string;
|
|
9
|
+
}
|
|
10
|
+
export declare class IndicatorSet {
|
|
11
|
+
entries: IndicatorEntry[];
|
|
12
|
+
private closes;
|
|
13
|
+
get subCount(): number;
|
|
14
|
+
main(): IndicatorEntry[];
|
|
15
|
+
subs(): IndicatorEntry[];
|
|
16
|
+
setSpecs(specs: readonly IndicatorSpec[], series: CandleSeries): void;
|
|
17
|
+
rebuild(series: CandleSeries): void;
|
|
18
|
+
onTick(series: CandleSeries, result: TickResult): void;
|
|
19
|
+
private loadCloses;
|
|
20
|
+
}
|