@zuttora/charts 0.0.0-stage → 0.1.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.
Files changed (85) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +48 -2
  3. package/dist/chart.d.ts +8 -0
  4. package/dist/chart.js +214 -0
  5. package/dist/chartState.d.ts +41 -0
  6. package/dist/chartState.js +59 -0
  7. package/dist/core/emitter.d.ts +8 -0
  8. package/dist/core/emitter.js +26 -0
  9. package/dist/core/format.d.ts +9 -0
  10. package/dist/core/format.js +35 -0
  11. package/dist/core/heikinAshi.d.ts +2 -0
  12. package/dist/core/heikinAshi.js +14 -0
  13. package/dist/core/priceScale.d.ts +22 -0
  14. package/dist/core/priceScale.js +92 -0
  15. package/dist/core/series.d.ts +41 -0
  16. package/dist/core/series.js +221 -0
  17. package/dist/core/time.d.ts +25 -0
  18. package/dist/core/time.js +44 -0
  19. package/dist/core/timeLabels.d.ts +11 -0
  20. package/dist/core/timeLabels.js +52 -0
  21. package/dist/core/types.d.ts +113 -0
  22. package/dist/core/types.js +1 -0
  23. package/dist/core/viewport.d.ts +41 -0
  24. package/dist/core/viewport.js +116 -0
  25. package/dist/core/warn.d.ts +1 -0
  26. package/dist/core/warn.js +3 -0
  27. package/dist/createChart.d.ts +2 -0
  28. package/dist/createChart.js +7 -0
  29. package/dist/dom/environment.d.ts +11 -0
  30. package/dist/dom/environment.js +49 -0
  31. package/dist/dom/legend.d.ts +14 -0
  32. package/dist/dom/legend.js +53 -0
  33. package/dist/dom/legendSync.d.ts +5 -0
  34. package/dist/dom/legendSync.js +23 -0
  35. package/dist/dom/root.d.ts +9 -0
  36. package/dist/dom/root.js +19 -0
  37. package/dist/host.d.ts +32 -0
  38. package/dist/host.js +62 -0
  39. package/dist/index.d.ts +7 -0
  40. package/dist/index.js +5 -0
  41. package/dist/interact/attach.d.ts +2 -0
  42. package/dist/interact/attach.js +127 -0
  43. package/dist/interact/gestures.d.ts +47 -0
  44. package/dist/interact/gestures.js +179 -0
  45. package/dist/interact/keyboard.d.ts +19 -0
  46. package/dist/interact/keyboard.js +34 -0
  47. package/dist/interact/wheel.d.ts +15 -0
  48. package/dist/interact/wheel.js +18 -0
  49. package/dist/render/bars.d.ts +17 -0
  50. package/dist/render/bars.js +55 -0
  51. package/dist/render/canvas.d.ts +2 -0
  52. package/dist/render/canvas.js +13 -0
  53. package/dist/render/drawAxes.d.ts +2 -0
  54. package/dist/render/drawAxes.js +34 -0
  55. package/dist/render/drawCandles.d.ts +3 -0
  56. package/dist/render/drawCandles.js +68 -0
  57. package/dist/render/drawCrosshair.d.ts +7 -0
  58. package/dist/render/drawCrosshair.js +39 -0
  59. package/dist/render/drawGrid.d.ts +2 -0
  60. package/dist/render/drawGrid.js +22 -0
  61. package/dist/render/drawLastPrice.d.ts +10 -0
  62. package/dist/render/drawLastPrice.js +53 -0
  63. package/dist/render/drawLine.d.ts +3 -0
  64. package/dist/render/drawLine.js +43 -0
  65. package/dist/render/drawMain.d.ts +2 -0
  66. package/dist/render/drawMain.js +30 -0
  67. package/dist/render/drawMarkers.d.ts +2 -0
  68. package/dist/render/drawMarkers.js +39 -0
  69. package/dist/render/drawPriceLines.d.ts +4 -0
  70. package/dist/render/drawPriceLines.js +60 -0
  71. package/dist/render/frame.d.ts +53 -0
  72. package/dist/render/frame.js +5 -0
  73. package/dist/render/frameBuilder.d.ts +12 -0
  74. package/dist/render/frameBuilder.js +119 -0
  75. package/dist/render/layout.d.ts +4 -0
  76. package/dist/render/layout.js +26 -0
  77. package/dist/render/paintFrame.d.ts +11 -0
  78. package/dist/render/paintFrame.js +30 -0
  79. package/dist/render/scheduler.d.ts +17 -0
  80. package/dist/render/scheduler.js +38 -0
  81. package/dist/render/tween.d.ts +12 -0
  82. package/dist/render/tween.js +38 -0
  83. package/dist/theme.d.ts +2 -0
  84. package/dist/theme.js +16 -0
  85. 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,49 @@
1
- # Temporary Holding Version
1
+ # chartZ (`@zuttora/charts`)
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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`, `scrollToLive`, `fitAll`, `destroy`.
24
+
25
+ Gestures: drag to pan, wheel or pinch to zoom, press and hold (touch) or hover (mouse) for the
26
+ crosshair, drag the price axis to scale it, double-click or double-tap to reset, arrow keys, `+`/`-`,
27
+ `Home`, `End`.
28
+
29
+ ## Status
30
+
31
+ | | |
32
+ |---|---|
33
+ | Core, rendering, interaction (M3 to M5) | done |
34
+ | Indicators (M8) | `0.2.0` |
35
+ | Published to npm | after review (needs the `NPM_TOKEN` secret) |
36
+
37
+ ## Develop
38
+
39
+ ```
40
+ yarn install
41
+ yarn typecheck && yarn lint && yarn test && yarn build && yarn size
42
+ python3 -m http.server # then open http://localhost:8000/demo/ (after yarn build)
43
+ ```
44
+
45
+ Demo URL options: `?type=candles|line|area|heikinAshi&interval=1m|5m|10m|1h|3h|6h|1D&tick=200` (ms between fake ticks).
46
+
47
+ Rules: no runtime dependencies, TypeScript that Node can run by stripping types (no `enum`,
48
+ `namespace` or constructor parameter properties), relative imports with the `.ts` extension, no
49
+ rounded corners, no `console` outside `src/core/warn.ts`.
@@ -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,214 @@
1
+ import { Emitter } from "./core/emitter.js";
2
+ import { buildDom } from "./dom/root.js";
3
+ import { watchEnvironment } from "./dom/environment.js";
4
+ import { syncLegend } from "./dom/legendSync.js";
5
+ import { createHost } from "./host.js";
6
+ import { createState, resolveOptions } from "./chartState.js";
7
+ import { CLOSE_TWEEN_MS } from "./render/frameBuilder.js";
8
+ import { paintFrame } from "./render/paintFrame.js";
9
+ import { Scheduler } from "./render/scheduler.js";
10
+ const perf = () => (typeof performance !== 'undefined' ? performance.now() : Date.now());
11
+ export function createChartCore(container, options) {
12
+ const doc = container.ownerDocument;
13
+ const s = createState(resolveOptions(options));
14
+ const events = new Emitter();
15
+ let destroyed = false;
16
+ let lastFrame = null;
17
+ const { root, mainCanvas, overlayCanvas, legend } = buildDom(doc, s.options.theme);
18
+ container.appendChild(root);
19
+ const applyAria = () => root.setAttribute('aria-label', s.options.ariaLabel);
20
+ const applyFocusRing = () => {
21
+ root.style.setProperty('--zc-focus', s.options.theme.accent);
22
+ };
23
+ applyAria();
24
+ applyFocusRing();
25
+ const updateLegend = () => syncLegend(s, legend);
26
+ const checkEvents = () => {
27
+ const live = s.vp.atLive();
28
+ if (live !== s.lastLive) {
29
+ s.lastLive = live;
30
+ events.emit('liveChange', live);
31
+ }
32
+ if (s.series.count > 0 && s.moreGate.check(s.vp.leftIndex(), s.series.hasMore)) {
33
+ events.emit('needMore', s.series.timeAt(0));
34
+ }
35
+ };
36
+ const draw = (flags, now) => {
37
+ if (destroyed)
38
+ return false;
39
+ return paintFrame(s, {
40
+ mainCanvas,
41
+ overlayCanvas,
42
+ dpr: () => env.dpr(),
43
+ frameBuilt: (f) => {
44
+ lastFrame = f;
45
+ },
46
+ afterDraw: checkEvents,
47
+ }, flags, now);
48
+ };
49
+ const scheduler = new Scheduler(draw);
50
+ const invalidate = (layer) => scheduler.invalidate(layer);
51
+ const invalidateAll = () => {
52
+ invalidate('main');
53
+ invalidate('overlay');
54
+ };
55
+ const resize = () => {
56
+ const r = container.getBoundingClientRect();
57
+ s.width = Math.max(0, Math.floor(r.width));
58
+ s.height = Math.max(0, Math.floor(r.height));
59
+ invalidateAll();
60
+ };
61
+ resize();
62
+ const env = watchEnvironment(container, doc, {
63
+ resize,
64
+ densityChanged: invalidateAll,
65
+ second: () => {
66
+ if (s.series.forming)
67
+ invalidate('overlay');
68
+ },
69
+ visible: invalidateAll,
70
+ });
71
+ const animNow = perf;
72
+ const chart = {
73
+ setData(closed, forming, hasMore) {
74
+ if (destroyed)
75
+ return;
76
+ const hadData = s.series.count > 0;
77
+ const wasLive = !hadData || s.vp.atLive();
78
+ const edge = Math.round(s.vp.rightIndex);
79
+ const frac = s.vp.rightIndex - edge;
80
+ const keepT = hadData && !wasLive ? s.series.timeAt(Math.min(edge, s.series.lastIndex())) : null;
81
+ s.series.setData(closed, forming, hasMore);
82
+ s.haBars.rebuild();
83
+ s.vp.setLastIndex(s.series.lastIndex());
84
+ s.moreGate.clear();
85
+ s.closeTween.jump(forming ? forming.c : 0);
86
+ s.rightAnimating = false;
87
+ if (keepT !== null) {
88
+ const i = s.series.indexAtTime(keepT);
89
+ if (i >= 0 && s.series.timeAt(i) === keepT)
90
+ s.vp.setRightIndex(i + frac);
91
+ else
92
+ s.vp.goLive();
93
+ }
94
+ else {
95
+ s.vp.goLive();
96
+ }
97
+ s.rangeReady = false;
98
+ updateLegend();
99
+ invalidateAll();
100
+ },
101
+ prependData(older, hasMore) {
102
+ if (destroyed)
103
+ return;
104
+ const added = s.series.prependData(older, hasMore);
105
+ if (added > 0) {
106
+ s.vp.onPrepend(added);
107
+ s.haBars.rebuild();
108
+ }
109
+ s.moreGate.clear();
110
+ updateLegend();
111
+ invalidateAll();
112
+ },
113
+ replaceClosed(candles) {
114
+ if (destroyed)
115
+ return;
116
+ if (s.series.replaceClosed(candles) > 0) {
117
+ s.haBars.rebuild();
118
+ updateLegend();
119
+ invalidateAll();
120
+ }
121
+ },
122
+ applyTick(price, time) {
123
+ if (destroyed)
124
+ return;
125
+ const r = s.series.applyTick(price, time, s.options.intervalMs, s.options.tzOffsetMs);
126
+ if (r.kind === 'ignored')
127
+ return;
128
+ const t = animNow();
129
+ if (r.kind === 'updated') {
130
+ s.closeTween.set(price, t, s.options.reducedMotion ? 0 : CLOSE_TWEEN_MS);
131
+ }
132
+ else {
133
+ if (r.kind === 'closed' && r.shifted > 0)
134
+ s.vp.onShift(r.shifted);
135
+ s.vp.onAppend(s.series.lastIndex());
136
+ s.closeTween.jump(price);
137
+ s.haBars.rebuild();
138
+ }
139
+ s.haBars.refreshForming();
140
+ if (!s.options.reducedMotion && (s.options.type === 'line' || s.options.type === 'area')) {
141
+ s.pulses.push(t);
142
+ if (s.pulses.length > 8)
143
+ s.pulses.shift();
144
+ }
145
+ if (r.kind === 'closed')
146
+ events.emit('candleClosed', r.closed);
147
+ if (!s.crosshair)
148
+ updateLegend();
149
+ invalidateAll();
150
+ },
151
+ setOptions(patch) {
152
+ if (destroyed)
153
+ return;
154
+ const next = resolveOptions({ ...s.options, ...patch, labels: { ...s.options.labels, ...patch.labels } });
155
+ s.options = next;
156
+ s.haBars.rebuild();
157
+ legend.applyTheme(next.theme);
158
+ applyAria();
159
+ applyFocusRing();
160
+ s.textWidths.clear();
161
+ s.rangeReady = false;
162
+ updateLegend();
163
+ invalidateAll();
164
+ },
165
+ setPriceLines(lines) {
166
+ if (destroyed)
167
+ return;
168
+ s.lines = lines.slice();
169
+ invalidate('main');
170
+ },
171
+ setMarkers(markers) {
172
+ if (destroyed)
173
+ return;
174
+ s.markers = markers.slice();
175
+ updateLegend();
176
+ invalidate('main');
177
+ },
178
+ scrollToLive() {
179
+ host.scrollToLive();
180
+ },
181
+ fitAll() {
182
+ if (destroyed)
183
+ return;
184
+ s.vp.fitAll();
185
+ s.rightAnimating = false;
186
+ invalidateAll();
187
+ },
188
+ on(event, listener) {
189
+ return events.on(event, listener);
190
+ },
191
+ destroy() {
192
+ if (destroyed)
193
+ return;
194
+ destroyed = true;
195
+ scheduler.stop();
196
+ env.dispose();
197
+ events.clear();
198
+ host.dispose();
199
+ root.remove();
200
+ },
201
+ };
202
+ const host = createHost({
203
+ root,
204
+ state: s,
205
+ events,
206
+ invalidate,
207
+ invalidateAll,
208
+ updateLegend,
209
+ isDestroyed: () => destroyed,
210
+ lastFrame: () => lastFrame,
211
+ now: animNow,
212
+ });
213
+ return { chart, host };
214
+ }
@@ -0,0 +1,41 @@
1
+ import { CandleSeries } from './core/series.ts';
2
+ import { PriceScale } from './core/priceScale.ts';
3
+ import { MoreGate, Viewport } from './core/viewport.ts';
4
+ import type { ChartLabels, ChartOptions, Marker, PriceLine } from './core/types.ts';
5
+ import { HeikinBars, SeriesBars } from './render/bars.ts';
6
+ import type { CrosshairInput } from './render/drawCrosshair.ts';
7
+ import { Tween } from './render/tween.ts';
8
+ export declare const DEFAULT_LABELS: ChartLabels;
9
+ export interface ResolvedOptions extends Required<Omit<ChartOptions, 'labels' | 'ariaLabel' | 'now' | 'reducedMotion'>> {
10
+ labels: ChartLabels;
11
+ ariaLabel: string;
12
+ now: () => number;
13
+ reducedMotion: boolean;
14
+ }
15
+ export declare function resolveOptions(o: ChartOptions): ResolvedOptions;
16
+ export interface ChartState {
17
+ options: ResolvedOptions;
18
+ series: CandleSeries;
19
+ vp: Viewport;
20
+ scale: PriceScale;
21
+ plainBars: SeriesBars;
22
+ haBars: HeikinBars;
23
+ moreGate: MoreGate;
24
+ lines: readonly PriceLine[];
25
+ markers: readonly Marker[];
26
+ rangeMin: Tween;
27
+ rangeMax: Tween;
28
+ rangeReady: boolean;
29
+ closeTween: Tween;
30
+ rightTween: Tween;
31
+ rightAnimating: boolean;
32
+ axisWidth: number;
33
+ textWidths: Map<string, number>;
34
+ width: number;
35
+ height: number;
36
+ crosshair: CrosshairInput | null;
37
+ pulses: number[];
38
+ lastLive: boolean;
39
+ lastCrosshairTime: number | null;
40
+ }
41
+ export declare function createState(options: ResolvedOptions): ChartState;
@@ -0,0 +1,59 @@
1
+ import { CandleSeries } from "./core/series.js";
2
+ import { PriceScale } from "./core/priceScale.js";
3
+ import { MoreGate, Viewport } from "./core/viewport.js";
4
+ import { HeikinBars, SeriesBars } from "./render/bars.js";
5
+ import { Tween } from "./render/tween.js";
6
+ export const DEFAULT_LABELS = {
7
+ open: 'O',
8
+ high: 'H',
9
+ low: 'L',
10
+ close: 'C',
11
+ change: 'Change',
12
+ noData: 'No data yet',
13
+ heikinAshi: 'Heikin-Ashi (smoothed)',
14
+ };
15
+ export function resolveOptions(o) {
16
+ return {
17
+ intervalMs: o.intervalMs,
18
+ tzOffsetMs: o.tzOffsetMs,
19
+ priceDecimals: o.priceDecimals,
20
+ minMove: o.minMove ?? 10 ** -o.priceDecimals,
21
+ type: o.type ?? 'candles',
22
+ candleStyle: o.candleStyle ?? 'hollowUp',
23
+ theme: o.theme,
24
+ labels: { ...DEFAULT_LABELS, ...o.labels },
25
+ locale: o.locale ?? 'en-IN',
26
+ now: o.now ?? (() => Date.now()),
27
+ reducedMotion: o.reducedMotion ??
28
+ (typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches),
29
+ ariaLabel: o.ariaLabel ?? 'Price chart',
30
+ };
31
+ }
32
+ export function createState(options) {
33
+ const series = new CandleSeries();
34
+ return {
35
+ options,
36
+ series,
37
+ vp: new Viewport(),
38
+ scale: new PriceScale(),
39
+ plainBars: new SeriesBars(series),
40
+ haBars: new HeikinBars(series),
41
+ moreGate: new MoreGate(),
42
+ lines: [],
43
+ markers: [],
44
+ rangeMin: new Tween(0),
45
+ rangeMax: new Tween(1),
46
+ rangeReady: false,
47
+ closeTween: new Tween(0),
48
+ rightTween: new Tween(0),
49
+ rightAnimating: false,
50
+ axisWidth: 0,
51
+ textWidths: new Map(),
52
+ width: 0,
53
+ height: 0,
54
+ crosshair: null,
55
+ pulses: [],
56
+ lastLive: true,
57
+ lastCrosshairTime: null,
58
+ };
59
+ }
@@ -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,2 @@
1
+ import type { Candle } from './types.ts';
2
+ export declare function heikinAshi(candles: readonly Candle[]): Candle[];
@@ -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,22 @@
1
+ import type { PriceLine } from './types.ts';
2
+ export declare function niceStep(raw: number, minMove: number): number;
3
+ export interface PriceRange {
4
+ min: number;
5
+ max: number;
6
+ }
7
+ export declare function autoRange(bars: PriceRange, extraValues: readonly number[], lines: readonly PriceLine[], minMove: number): PriceRange;
8
+ export declare function pinSide(price: number, range: PriceRange): 'above' | 'below' | null;
9
+ export declare function axisTicks(range: PriceRange, paneHeight: number, minMove: number): number[];
10
+ export declare class PriceScale {
11
+ top: number;
12
+ height: number;
13
+ min: number;
14
+ max: number;
15
+ manual: boolean;
16
+ setPane(top: number, height: number): void;
17
+ setRange(range: PriceRange): void;
18
+ y(price: number): number;
19
+ priceAt(y: number): number;
20
+ scaleBy(dyPx: number): void;
21
+ resetAuto(): void;
22
+ }
@@ -0,0 +1,92 @@
1
+ export function niceStep(raw, minMove) {
2
+ const exp = Math.floor(Math.log10(raw));
3
+ const base = 10 ** exp;
4
+ const m = raw / base;
5
+ const nice = m <= 1 ? 1 : m <= 2 ? 2 : m <= 2.5 ? 2.5 : m <= 5 ? 5 : 10;
6
+ return Math.max(nice * base, minMove);
7
+ }
8
+ export function autoRange(bars, extraValues, lines, minMove) {
9
+ let min = bars.min;
10
+ let max = bars.max;
11
+ for (const v of extraValues) {
12
+ if (Number.isFinite(v)) {
13
+ if (v < min)
14
+ min = v;
15
+ if (v > max)
16
+ max = v;
17
+ }
18
+ }
19
+ const span = max - min;
20
+ const lo = min - 0.5 * span;
21
+ const hi = max + 0.5 * span;
22
+ for (const line of lines) {
23
+ if (line.price >= lo && line.price <= hi) {
24
+ if (line.price < min)
25
+ min = line.price;
26
+ if (line.price > max)
27
+ max = line.price;
28
+ }
29
+ }
30
+ if (max === min) {
31
+ const pad = Math.max(Math.abs(max) * 0.001, 10 * minMove);
32
+ return { min: min - pad, max: max + pad };
33
+ }
34
+ const pad = (max - min) * 0.08;
35
+ return { min: min - pad, max: max + pad };
36
+ }
37
+ export function pinSide(price, range) {
38
+ if (price > range.max)
39
+ return 'above';
40
+ if (price < range.min)
41
+ return 'below';
42
+ return null;
43
+ }
44
+ export function axisTicks(range, paneHeight, minMove) {
45
+ const target = Math.max(2, Math.floor(paneHeight / 40));
46
+ const span = range.max - range.min;
47
+ if (!(span > 0))
48
+ return [];
49
+ const step = niceStep(span / target, minMove);
50
+ const out = [];
51
+ const first = Math.ceil(range.min / step - 1e-9);
52
+ for (let k = first; k * step <= range.max + step * 1e-9; k++) {
53
+ out.push(Math.round(k * step * 1e8) / 1e8);
54
+ if (out.length > 200)
55
+ break;
56
+ }
57
+ return out;
58
+ }
59
+ export class PriceScale {
60
+ constructor() {
61
+ this.top = 0;
62
+ this.height = 1;
63
+ this.min = 0;
64
+ this.max = 1;
65
+ this.manual = false;
66
+ }
67
+ setPane(top, height) {
68
+ this.top = top;
69
+ this.height = Math.max(1, height);
70
+ }
71
+ setRange(range) {
72
+ this.min = range.min;
73
+ this.max = range.max;
74
+ }
75
+ y(price) {
76
+ return this.top + ((this.max - price) / (this.max - this.min)) * this.height;
77
+ }
78
+ priceAt(y) {
79
+ return this.max - ((y - this.top) / this.height) * (this.max - this.min);
80
+ }
81
+ scaleBy(dyPx) {
82
+ const factor = Math.exp(dyPx * 0.005);
83
+ const centre = (this.max + this.min) / 2;
84
+ const half = ((this.max - this.min) / 2) * factor;
85
+ this.min = centre - half;
86
+ this.max = centre + half;
87
+ this.manual = true;
88
+ }
89
+ resetAuto() {
90
+ this.manual = false;
91
+ }
92
+ }
@@ -0,0 +1,41 @@
1
+ import type { Candle } from './types.ts';
2
+ export declare const MAX_CLOSED = 5000;
3
+ export type TickResult = {
4
+ kind: 'ignored';
5
+ } | {
6
+ kind: 'updated';
7
+ } | {
8
+ kind: 'started';
9
+ } | {
10
+ kind: 'closed';
11
+ closed: Candle;
12
+ shifted: number;
13
+ };
14
+ export declare class CandleSeries {
15
+ t: number[];
16
+ o: number[];
17
+ h: number[];
18
+ l: number[];
19
+ c: number[];
20
+ forming: Candle | null;
21
+ hasMore: boolean;
22
+ get closedCount(): number;
23
+ get count(): number;
24
+ lastIndex(): number;
25
+ candleAt(i: number): Candle;
26
+ timeAt(i: number): number;
27
+ closeAt(i: number): number;
28
+ toArray(): Candle[];
29
+ setData(closed: readonly Candle[], forming: Candle | null, hasMore: boolean): void;
30
+ prependData(older: readonly Candle[], hasMore: boolean): number;
31
+ replaceClosed(candles: readonly Candle[]): number;
32
+ applyTick(price: number, time: number, intervalMs: number, offsetMs: number): TickResult;
33
+ visibleExtremes(from: number, to: number): {
34
+ min: number;
35
+ max: number;
36
+ } | null;
37
+ indexAtTime(time: number): number;
38
+ private closedIndexOf;
39
+ private push;
40
+ private dropOldest;
41
+ }