@zuttora/charts 0.1.0 → 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.
Files changed (47) hide show
  1. package/README.md +16 -2
  2. package/dist/chart.js +24 -0
  3. package/dist/chartState.d.ts +6 -0
  4. package/dist/chartState.js +5 -0
  5. package/dist/core/drawings.d.ts +21 -0
  6. package/dist/core/drawings.js +46 -0
  7. package/dist/core/indicatorSet.d.ts +20 -0
  8. package/dist/core/indicatorSet.js +66 -0
  9. package/dist/core/indicators.d.ts +53 -0
  10. package/dist/core/indicators.js +187 -0
  11. package/dist/core/types.d.ts +21 -0
  12. package/dist/dom/indicatorReadings.d.ts +7 -0
  13. package/dist/dom/indicatorReadings.js +26 -0
  14. package/dist/dom/legend.d.ts +5 -0
  15. package/dist/dom/legend.js +10 -1
  16. package/dist/dom/legendSync.js +8 -1
  17. package/dist/draw/controller.d.ts +36 -0
  18. package/dist/draw/controller.js +151 -0
  19. package/dist/draw/geometry.d.ts +44 -0
  20. package/dist/draw/geometry.js +118 -0
  21. package/dist/draw/mapper.d.ts +30 -0
  22. package/dist/draw/mapper.js +32 -0
  23. package/dist/draw/pointer.d.ts +17 -0
  24. package/dist/draw/pointer.js +144 -0
  25. package/dist/draw/state.d.ts +61 -0
  26. package/dist/draw/state.js +8 -0
  27. package/dist/drawApi.d.ts +20 -0
  28. package/dist/drawApi.js +59 -0
  29. package/dist/host.d.ts +3 -0
  30. package/dist/host.js +1 -0
  31. package/dist/index.d.ts +2 -2
  32. package/dist/index.js +1 -1
  33. package/dist/interact/attach.js +33 -18
  34. package/dist/render/drawCrosshair.js +6 -3
  35. package/dist/render/drawDrawings.d.ts +10 -0
  36. package/dist/render/drawDrawings.js +146 -0
  37. package/dist/render/drawIndicators.d.ts +4 -0
  38. package/dist/render/drawIndicators.js +67 -0
  39. package/dist/render/drawMain.js +13 -0
  40. package/dist/render/drawSubPanes.d.ts +2 -0
  41. package/dist/render/drawSubPanes.js +104 -0
  42. package/dist/render/frame.d.ts +4 -0
  43. package/dist/render/frameBuilder.js +12 -2
  44. package/dist/render/indicatorFrame.d.ts +17 -0
  45. package/dist/render/indicatorFrame.js +55 -0
  46. package/dist/render/paintFrame.js +3 -0
  47. package/package.json +1 -1
package/README.md CHANGED
@@ -20,7 +20,20 @@ chart.on('needMore', (oldest) => { /* load older candles, then chart.prependData
20
20
  chart.on('liveChange', (atLive) => { /* show or hide a "Go to live" button */ });
21
21
  ```
22
22
 
23
- Also: `replaceClosed`, `setOptions`, `setPriceLines`, `setMarkers`, `scrollToLive`, `fitAll`, `destroy`.
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
+ ```
24
37
 
25
38
  Gestures: drag to pan, wheel or pinch to zoom, press and hold (touch) or hover (mouse) for the
26
39
  crosshair, drag the price axis to scale it, double-click or double-tap to reset, arrow keys, `+`/`-`,
@@ -31,7 +44,8 @@ crosshair, drag the price axis to scale it, double-click or double-tap to reset,
31
44
  | | |
32
45
  |---|---|
33
46
  | Core, rendering, interaction (M3 to M5) | done |
34
- | Indicators (M8) | `0.2.0` |
47
+ | Indicators (M8) | done, `0.2.0` |
48
+ | Drawing tools (M9) | done, `0.3.0` |
35
49
  | Published to npm | after review (needs the `NPM_TOKEN` secret) |
36
50
 
37
51
  ## Develop
package/dist/chart.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { createDrawApi } from "./drawApi.js";
1
2
  import { Emitter } from "./core/emitter.js";
2
3
  import { buildDom } from "./dom/root.js";
3
4
  import { watchEnvironment } from "./dom/environment.js";
@@ -69,6 +70,15 @@ export function createChartCore(container, options) {
69
70
  visible: invalidateAll,
70
71
  });
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
+ });
72
82
  const chart = {
73
83
  setData(closed, forming, hasMore) {
74
84
  if (destroyed)
@@ -80,6 +90,7 @@ export function createChartCore(container, options) {
80
90
  const keepT = hadData && !wasLive ? s.series.timeAt(Math.min(edge, s.series.lastIndex())) : null;
81
91
  s.series.setData(closed, forming, hasMore);
82
92
  s.haBars.rebuild();
93
+ s.indicators.rebuild(s.series);
83
94
  s.vp.setLastIndex(s.series.lastIndex());
84
95
  s.moreGate.clear();
85
96
  s.closeTween.jump(forming ? forming.c : 0);
@@ -105,6 +116,7 @@ export function createChartCore(container, options) {
105
116
  if (added > 0) {
106
117
  s.vp.onPrepend(added);
107
118
  s.haBars.rebuild();
119
+ s.indicators.rebuild(s.series);
108
120
  }
109
121
  s.moreGate.clear();
110
122
  updateLegend();
@@ -115,6 +127,7 @@ export function createChartCore(container, options) {
115
127
  return;
116
128
  if (s.series.replaceClosed(candles) > 0) {
117
129
  s.haBars.rebuild();
130
+ s.indicators.rebuild(s.series);
118
131
  updateLegend();
119
132
  invalidateAll();
120
133
  }
@@ -125,6 +138,7 @@ export function createChartCore(container, options) {
125
138
  const r = s.series.applyTick(price, time, s.options.intervalMs, s.options.tzOffsetMs);
126
139
  if (r.kind === 'ignored')
127
140
  return;
141
+ s.indicators.onTick(s.series, r);
128
142
  const t = animNow();
129
143
  if (r.kind === 'updated') {
130
144
  s.closeTween.set(price, t, s.options.reducedMotion ? 0 : CLOSE_TWEEN_MS);
@@ -175,6 +189,15 @@ export function createChartCore(container, options) {
175
189
  updateLegend();
176
190
  invalidate('main');
177
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,
178
201
  scrollToLive() {
179
202
  host.scrollToLive();
180
203
  },
@@ -202,6 +225,7 @@ export function createChartCore(container, options) {
202
225
  const host = createHost({
203
226
  root,
204
227
  state: s,
228
+ draw: drawApi.controller,
205
229
  events,
206
230
  invalidate,
207
231
  invalidateAll,
@@ -1,5 +1,8 @@
1
1
  import { CandleSeries } from './core/series.ts';
2
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';
3
6
  import { MoreGate, Viewport } from './core/viewport.ts';
4
7
  import type { ChartLabels, ChartOptions, Marker, PriceLine } from './core/types.ts';
5
8
  import { HeikinBars, SeriesBars } from './render/bars.ts';
@@ -23,6 +26,9 @@ export interface ChartState {
23
26
  moreGate: MoreGate;
24
27
  lines: readonly PriceLine[];
25
28
  markers: readonly Marker[];
29
+ indicators: IndicatorSet;
30
+ drawings: Drawing[];
31
+ draw: DrawState;
26
32
  rangeMin: Tween;
27
33
  rangeMax: Tween;
28
34
  rangeReady: boolean;
@@ -1,5 +1,7 @@
1
1
  import { CandleSeries } from "./core/series.js";
2
2
  import { PriceScale } from "./core/priceScale.js";
3
+ import { IndicatorSet } from "./core/indicatorSet.js";
4
+ import { createDrawState } from "./draw/controller.js";
3
5
  import { MoreGate, Viewport } from "./core/viewport.js";
4
6
  import { HeikinBars, SeriesBars } from "./render/bars.js";
5
7
  import { Tween } from "./render/tween.js";
@@ -41,6 +43,9 @@ export function createState(options) {
41
43
  moreGate: new MoreGate(),
42
44
  lines: [],
43
45
  markers: [],
46
+ indicators: new IndicatorSet(),
47
+ drawings: [],
48
+ draw: createDrawState(options.theme.accent),
44
49
  rangeMin: new Tween(0),
45
50
  rangeMax: new Tween(1),
46
51
  rangeReady: false,
@@ -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,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
+ }
@@ -0,0 +1,66 @@
1
+ import { isSubPane, makeCalc } from "./indicators.js";
2
+ export const MAX_INDICATORS = 8;
3
+ const keyOf = (spec) => JSON.stringify({ ...spec, color: undefined, signalColor: undefined });
4
+ export class IndicatorSet {
5
+ constructor() {
6
+ this.entries = [];
7
+ this.closes = [];
8
+ }
9
+ get subCount() {
10
+ let n = 0;
11
+ for (const e of this.entries)
12
+ if (isSubPane(e.spec))
13
+ n++;
14
+ return n;
15
+ }
16
+ main() {
17
+ return this.entries.filter((e) => !isSubPane(e.spec));
18
+ }
19
+ subs() {
20
+ return this.entries.filter((e) => isSubPane(e.spec));
21
+ }
22
+ setSpecs(specs, series) {
23
+ const old = new Map(this.entries.map((e) => [e.spec.id, e]));
24
+ const next = [];
25
+ const seen = new Set();
26
+ for (const spec of specs) {
27
+ if (seen.has(spec.id) || next.length >= MAX_INDICATORS)
28
+ continue;
29
+ seen.add(spec.id);
30
+ const key = keyOf(spec);
31
+ const kept = old.get(spec.id);
32
+ next.push(kept && kept.key === key ? { ...kept, spec } : { spec, key, calc: makeCalc(spec) });
33
+ }
34
+ const fresh = next.filter((e) => !this.entries.some((o) => o.calc === e.calc));
35
+ this.entries = next;
36
+ if (fresh.length > 0) {
37
+ this.loadCloses(series);
38
+ for (const e of fresh)
39
+ e.calc.computeFrom(this.closes, 0);
40
+ }
41
+ }
42
+ rebuild(series) {
43
+ if (this.entries.length === 0)
44
+ return;
45
+ this.loadCloses(series);
46
+ for (const e of this.entries)
47
+ e.calc.computeFrom(this.closes, 0);
48
+ }
49
+ onTick(series, result) {
50
+ if (this.entries.length === 0 || result.kind === 'ignored')
51
+ return;
52
+ const n = series.count;
53
+ if ((result.kind === 'closed' && result.shifted > 0) || this.closes.length < n - 1 || this.closes.length > n) {
54
+ this.rebuild(series);
55
+ return;
56
+ }
57
+ this.closes[n - 1] = series.closeAt(n - 1);
58
+ for (const e of this.entries)
59
+ e.calc.computeFrom(this.closes, n - 1);
60
+ }
61
+ loadCloses(series) {
62
+ this.closes = [];
63
+ for (let i = 0; i < series.count; i++)
64
+ this.closes.push(series.closeAt(i));
65
+ }
66
+ }
@@ -0,0 +1,53 @@
1
+ import type { IndicatorSpec } from './types.ts';
2
+ export type Values = (number | null)[];
3
+ export interface Calc {
4
+ computeFrom(closes: readonly number[], from: number): void;
5
+ lines: Record<string, Values>;
6
+ }
7
+ export declare class Sma implements Calc {
8
+ readonly values: Values;
9
+ readonly lines: Record<string, Values>;
10
+ private period;
11
+ constructor(period: number);
12
+ computeFrom(closes: readonly number[], from: number): void;
13
+ }
14
+ export declare class Ema implements Calc {
15
+ readonly values: Values;
16
+ readonly lines: Record<string, Values>;
17
+ private period;
18
+ private k;
19
+ constructor(period: number);
20
+ computeFrom(input: readonly (number | null)[], from: number): void;
21
+ }
22
+ export declare class Bollinger implements Calc {
23
+ readonly mid: Values;
24
+ readonly upper: Values;
25
+ readonly lower: Values;
26
+ readonly lines: Record<string, Values>;
27
+ private period;
28
+ private width;
29
+ constructor(period: number, stdDev: number);
30
+ computeFrom(closes: readonly number[], from: number): void;
31
+ }
32
+ export declare class Rsi implements Calc {
33
+ readonly values: Values;
34
+ readonly lines: Record<string, Values>;
35
+ private gain;
36
+ private loss;
37
+ private period;
38
+ constructor(period: number);
39
+ computeFrom(closes: readonly number[], from: number): void;
40
+ }
41
+ export declare class Macd implements Calc {
42
+ readonly macd: Values;
43
+ readonly hist: Values;
44
+ readonly lines: Record<string, Values>;
45
+ private fast;
46
+ private slow;
47
+ private signal;
48
+ constructor(fast: number, slow: number, signal: number);
49
+ computeFrom(closes: readonly number[], from: number): void;
50
+ }
51
+ export declare function makeCalc(spec: IndicatorSpec): Calc;
52
+ export declare function isSubPane(spec: IndicatorSpec): boolean;
53
+ export declare function indicatorTitle(spec: IndicatorSpec): string;
@@ -0,0 +1,187 @@
1
+ function cut(values, n) {
2
+ if (values.length > n)
3
+ values.length = n;
4
+ }
5
+ function whole(n) {
6
+ return Math.max(1, Math.floor(Number.isFinite(n) ? n : 1));
7
+ }
8
+ function windowMean(x, end, period) {
9
+ let sum = 0;
10
+ for (let j = end - period + 1; j <= end; j++)
11
+ sum += x[j];
12
+ return sum / period;
13
+ }
14
+ function firstValue(x) {
15
+ for (let i = 0; i < x.length; i++)
16
+ if (x[i] !== null)
17
+ return i;
18
+ return -1;
19
+ }
20
+ export class Sma {
21
+ constructor(period) {
22
+ this.values = [];
23
+ this.lines = { main: this.values };
24
+ this.period = whole(period);
25
+ }
26
+ computeFrom(closes, from) {
27
+ cut(this.values, from);
28
+ for (let i = this.values.length; i < closes.length; i++) {
29
+ this.values.push(i < this.period - 1 ? null : windowMean(closes, i, this.period));
30
+ }
31
+ }
32
+ }
33
+ export class Ema {
34
+ constructor(period) {
35
+ this.values = [];
36
+ this.lines = { main: this.values };
37
+ this.period = whole(period);
38
+ this.k = 2 / (this.period + 1);
39
+ }
40
+ computeFrom(input, from) {
41
+ cut(this.values, from);
42
+ const start = firstValue(input);
43
+ for (let i = this.values.length; i < input.length; i++) {
44
+ if (start < 0 || i < start + this.period - 1)
45
+ this.values.push(null);
46
+ else if (i === start + this.period - 1)
47
+ this.values.push(windowMean(input, i, this.period));
48
+ else {
49
+ const prev = this.values[i - 1];
50
+ this.values.push(prev + this.k * (input[i] - prev));
51
+ }
52
+ }
53
+ }
54
+ }
55
+ export class Bollinger {
56
+ constructor(period, stdDev) {
57
+ this.mid = [];
58
+ this.upper = [];
59
+ this.lower = [];
60
+ this.lines = { mid: this.mid, upper: this.upper, lower: this.lower };
61
+ this.period = whole(period);
62
+ this.width = Number.isFinite(stdDev) ? stdDev : 2;
63
+ }
64
+ computeFrom(closes, from) {
65
+ for (const v of [this.mid, this.upper, this.lower])
66
+ cut(v, from);
67
+ for (let i = this.mid.length; i < closes.length; i++) {
68
+ if (i < this.period - 1) {
69
+ this.mid.push(null);
70
+ this.upper.push(null);
71
+ this.lower.push(null);
72
+ continue;
73
+ }
74
+ const mean = windowMean(closes, i, this.period);
75
+ let sq = 0;
76
+ for (let j = i - this.period + 1; j <= i; j++)
77
+ sq += (closes[j] - mean) ** 2;
78
+ const band = this.width * Math.sqrt(sq / this.period);
79
+ this.mid.push(mean);
80
+ this.upper.push(mean + band);
81
+ this.lower.push(mean - band);
82
+ }
83
+ }
84
+ }
85
+ export class Rsi {
86
+ constructor(period) {
87
+ this.values = [];
88
+ this.lines = { rsi: this.values };
89
+ this.gain = [];
90
+ this.loss = [];
91
+ this.period = whole(period);
92
+ }
93
+ computeFrom(closes, from) {
94
+ for (const v of [this.values, this.gain, this.loss])
95
+ cut(v, from);
96
+ const p = this.period;
97
+ for (let i = this.values.length; i < closes.length; i++) {
98
+ if (i < p) {
99
+ this.values.push(null);
100
+ this.gain.push(null);
101
+ this.loss.push(null);
102
+ continue;
103
+ }
104
+ let g;
105
+ let l;
106
+ if (i === p) {
107
+ g = 0;
108
+ l = 0;
109
+ for (let j = 1; j <= p; j++) {
110
+ const d = closes[j] - closes[j - 1];
111
+ if (d > 0)
112
+ g += d;
113
+ else
114
+ l -= d;
115
+ }
116
+ g /= p;
117
+ l /= p;
118
+ }
119
+ else {
120
+ const d = closes[i] - closes[i - 1];
121
+ g = (this.gain[i - 1] * (p - 1) + (d > 0 ? d : 0)) / p;
122
+ l = (this.loss[i - 1] * (p - 1) + (d < 0 ? -d : 0)) / p;
123
+ }
124
+ this.gain.push(g);
125
+ this.loss.push(l);
126
+ this.values.push(l === 0 ? (g === 0 ? 50 : 100) : 100 - 100 / (1 + g / l));
127
+ }
128
+ }
129
+ }
130
+ export class Macd {
131
+ constructor(fast, slow, signal) {
132
+ this.macd = [];
133
+ this.hist = [];
134
+ this.fast = new Ema(fast);
135
+ this.slow = new Ema(slow);
136
+ this.signal = new Ema(signal);
137
+ this.lines = { macd: this.macd, signal: this.signal.values, hist: this.hist };
138
+ }
139
+ computeFrom(closes, from) {
140
+ this.fast.computeFrom(closes, from);
141
+ this.slow.computeFrom(closes, from);
142
+ cut(this.macd, from);
143
+ for (let i = this.macd.length; i < closes.length; i++) {
144
+ const f = this.fast.values[i];
145
+ const s = this.slow.values[i];
146
+ this.macd.push(f !== null && f !== undefined && s !== null && s !== undefined ? f - s : null);
147
+ }
148
+ this.signal.computeFrom(this.macd, from);
149
+ cut(this.hist, from);
150
+ for (let i = this.hist.length; i < closes.length; i++) {
151
+ const m = this.macd[i];
152
+ const g = this.signal.values[i];
153
+ this.hist.push(m !== null && m !== undefined && g !== null && g !== undefined ? m - g : null);
154
+ }
155
+ }
156
+ }
157
+ export function makeCalc(spec) {
158
+ switch (spec.type) {
159
+ case 'sma':
160
+ return new Sma(spec.period);
161
+ case 'ema':
162
+ return new Ema(spec.period);
163
+ case 'bollinger':
164
+ return new Bollinger(spec.period, spec.stdDev);
165
+ case 'rsi':
166
+ return new Rsi(spec.period);
167
+ case 'macd':
168
+ return new Macd(spec.fast, spec.slow, spec.signal);
169
+ }
170
+ }
171
+ export function isSubPane(spec) {
172
+ return spec.type === 'rsi' || spec.type === 'macd';
173
+ }
174
+ export function indicatorTitle(spec) {
175
+ switch (spec.type) {
176
+ case 'sma':
177
+ return `SMA ${spec.period}`;
178
+ case 'ema':
179
+ return `EMA ${spec.period}`;
180
+ case 'bollinger':
181
+ return `BB ${spec.period} ${spec.stdDev}`;
182
+ case 'rsi':
183
+ return `RSI ${spec.period}`;
184
+ case 'macd':
185
+ return `MACD ${spec.fast} ${spec.slow} ${spec.signal}`;
186
+ }
187
+ }
@@ -62,6 +62,8 @@ export interface Marker {
62
62
  label?: string;
63
63
  tooltip?: string;
64
64
  }
65
+ import type { Drawing, DrawingTool } from './drawings.ts';
66
+ export type { Drawing, DrawingKind, DrawingTool, Point } from './drawings.ts';
65
67
  export type IndicatorSpec = {
66
68
  id: string;
67
69
  type: 'sma';
@@ -97,6 +99,14 @@ export interface ChartEvents {
97
99
  candleClosed: (candle: Candle) => void;
98
100
  liveChange: (atLive: boolean) => void;
99
101
  crosshair: (candle: Candle | null) => void;
102
+ drawingsChange: (drawings: readonly Drawing[]) => void;
103
+ selectionChange: (id: string | null) => void;
104
+ drawProgress: (info: {
105
+ tool: DrawingTool;
106
+ placed: number;
107
+ needed: number;
108
+ } | null) => void;
109
+ drawingEdit: (drawing: Drawing) => void;
100
110
  }
101
111
  export interface Chart {
102
112
  setData(closed: readonly Candle[], forming: Candle | null, hasMore: boolean): void;
@@ -106,6 +116,17 @@ export interface Chart {
106
116
  setOptions(patch: Partial<ChartOptions>): void;
107
117
  setPriceLines(lines: readonly PriceLine[]): void;
108
118
  setMarkers(markers: readonly Marker[]): void;
119
+ setIndicators(specs: readonly IndicatorSpec[]): void;
120
+ setDrawingTool(tool: DrawingTool | null): void;
121
+ setDrawings(drawings: readonly Drawing[]): void;
122
+ getDrawings(): Drawing[];
123
+ updateDrawing(id: string, patch: {
124
+ text?: string;
125
+ color?: string;
126
+ }): void;
127
+ deleteSelected(): void;
128
+ clearDrawings(): void;
129
+ setDrawingColor(color: string): void;
109
130
  scrollToLive(): void;
110
131
  fitAll(): void;
111
132
  on<E extends keyof ChartEvents>(event: E, listener: ChartEvents[E]): () => void;
@@ -0,0 +1,7 @@
1
+ import type { IndicatorEntry } from '../core/indicatorSet.ts';
2
+ type Reading = {
3
+ text: string;
4
+ color: string;
5
+ };
6
+ export declare function indicatorReadings(entries: readonly IndicatorEntry[], i: number, fmt: (v: number) => string): Reading[];
7
+ export {};
@@ -0,0 +1,26 @@
1
+ import { indicatorTitle } from "../core/indicators.js";
2
+ export function indicatorReadings(entries, i, fmt) {
3
+ const out = [];
4
+ const at = (values) => {
5
+ const v = values?.[i];
6
+ return v === null || v === undefined ? '–' : fmt(v);
7
+ };
8
+ for (const e of entries) {
9
+ const l = e.calc.lines;
10
+ const name = indicatorTitle(e.spec);
11
+ switch (e.spec.type) {
12
+ case 'bollinger':
13
+ out.push({ text: `${name} ${at(l.lower)} ${at(l.mid)} ${at(l.upper)}`, color: e.spec.color });
14
+ break;
15
+ case 'macd':
16
+ out.push({ text: `MACD ${at(l.macd)} sig ${at(l.signal)}`, color: e.spec.color });
17
+ break;
18
+ case 'rsi':
19
+ out.push({ text: `${name} ${at(l.rsi) === '–' ? '–' : l.rsi[i].toFixed(1)}`, color: e.spec.color });
20
+ break;
21
+ default:
22
+ out.push({ text: `${name} ${at(l.main)}`, color: e.spec.color });
23
+ }
24
+ }
25
+ return out;
26
+ }
@@ -3,11 +3,16 @@ export interface LegendInput {
3
3
  candle: Candle | null;
4
4
  notes: readonly string[];
5
5
  caption: string | null;
6
+ indicators: readonly {
7
+ text: string;
8
+ color: string;
9
+ }[];
6
10
  }
7
11
  export declare class Legend {
8
12
  readonly el: HTMLDivElement;
9
13
  private line;
10
14
  private extra;
15
+ private readings;
11
16
  constructor(doc: Document, theme: ChartTheme);
12
17
  applyTheme(theme: ChartTheme): void;
13
18
  update(input: LegendInput, labels: ChartLabels, theme: ChartTheme, decimals: number, fmt: (v: number) => string, locale: string): void;
@@ -4,8 +4,10 @@ export class Legend {
4
4
  this.el = doc.createElement('div');
5
5
  this.line = doc.createElement('div');
6
6
  this.extra = doc.createElement('div');
7
+ this.readings = doc.createElement('div');
7
8
  this.line.style.cssText = 'display:flex;flex-wrap:wrap;column-gap:12px';
8
- this.el.append(this.line, this.extra);
9
+ this.readings.style.cssText = 'display:flex;flex-wrap:wrap;column-gap:12px';
10
+ this.el.append(this.line, this.readings, this.extra);
9
11
  this.el.setAttribute('aria-live', 'off');
10
12
  this.applyTheme(theme);
11
13
  }
@@ -27,6 +29,7 @@ export class Legend {
27
29
  const k = input.candle;
28
30
  this.line.textContent = '';
29
31
  this.extra.textContent = '';
32
+ this.readings.textContent = '';
30
33
  if (!k)
31
34
  return;
32
35
  const parts = [
@@ -46,6 +49,12 @@ export class Legend {
46
49
  change.textContent = `${ch.arrow} ${ch.amount} (${ch.percent})`.trim();
47
50
  change.style.color = ch.direction > 0 ? theme.up : ch.direction < 0 ? theme.down : theme.textMuted;
48
51
  this.line.append(change);
52
+ for (const r of input.indicators) {
53
+ const span = doc.createElement('span');
54
+ span.textContent = r.text;
55
+ span.style.color = r.color;
56
+ this.readings.append(span);
57
+ }
49
58
  const lines = input.caption ? [input.caption, ...input.notes] : [...input.notes];
50
59
  this.extra.textContent = lines.join('\n');
51
60
  this.extra.style.color = theme.textMuted;