@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.
Files changed (113) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +62 -2
  3. package/dist/chart.d.ts +8 -0
  4. package/dist/chart.js +238 -0
  5. package/dist/chartState.d.ts +47 -0
  6. package/dist/chartState.js +64 -0
  7. package/dist/core/drawings.d.ts +21 -0
  8. package/dist/core/drawings.js +46 -0
  9. package/dist/core/emitter.d.ts +8 -0
  10. package/dist/core/emitter.js +26 -0
  11. package/dist/core/format.d.ts +9 -0
  12. package/dist/core/format.js +35 -0
  13. package/dist/core/heikinAshi.d.ts +2 -0
  14. package/dist/core/heikinAshi.js +14 -0
  15. package/dist/core/indicatorSet.d.ts +20 -0
  16. package/dist/core/indicatorSet.js +66 -0
  17. package/dist/core/indicators.d.ts +53 -0
  18. package/dist/core/indicators.js +187 -0
  19. package/dist/core/priceScale.d.ts +22 -0
  20. package/dist/core/priceScale.js +92 -0
  21. package/dist/core/series.d.ts +41 -0
  22. package/dist/core/series.js +221 -0
  23. package/dist/core/time.d.ts +25 -0
  24. package/dist/core/time.js +44 -0
  25. package/dist/core/timeLabels.d.ts +11 -0
  26. package/dist/core/timeLabels.js +52 -0
  27. package/dist/core/types.d.ts +134 -0
  28. package/dist/core/types.js +1 -0
  29. package/dist/core/viewport.d.ts +41 -0
  30. package/dist/core/viewport.js +116 -0
  31. package/dist/core/warn.d.ts +1 -0
  32. package/dist/core/warn.js +3 -0
  33. package/dist/createChart.d.ts +2 -0
  34. package/dist/createChart.js +7 -0
  35. package/dist/dom/environment.d.ts +11 -0
  36. package/dist/dom/environment.js +49 -0
  37. package/dist/dom/indicatorReadings.d.ts +7 -0
  38. package/dist/dom/indicatorReadings.js +26 -0
  39. package/dist/dom/legend.d.ts +19 -0
  40. package/dist/dom/legend.js +62 -0
  41. package/dist/dom/legendSync.d.ts +5 -0
  42. package/dist/dom/legendSync.js +30 -0
  43. package/dist/dom/root.d.ts +9 -0
  44. package/dist/dom/root.js +19 -0
  45. package/dist/draw/controller.d.ts +36 -0
  46. package/dist/draw/controller.js +151 -0
  47. package/dist/draw/geometry.d.ts +44 -0
  48. package/dist/draw/geometry.js +118 -0
  49. package/dist/draw/mapper.d.ts +30 -0
  50. package/dist/draw/mapper.js +32 -0
  51. package/dist/draw/pointer.d.ts +17 -0
  52. package/dist/draw/pointer.js +144 -0
  53. package/dist/draw/state.d.ts +61 -0
  54. package/dist/draw/state.js +8 -0
  55. package/dist/drawApi.d.ts +20 -0
  56. package/dist/drawApi.js +59 -0
  57. package/dist/host.d.ts +35 -0
  58. package/dist/host.js +63 -0
  59. package/dist/index.d.ts +7 -0
  60. package/dist/index.js +5 -0
  61. package/dist/interact/attach.d.ts +2 -0
  62. package/dist/interact/attach.js +142 -0
  63. package/dist/interact/gestures.d.ts +47 -0
  64. package/dist/interact/gestures.js +179 -0
  65. package/dist/interact/keyboard.d.ts +19 -0
  66. package/dist/interact/keyboard.js +34 -0
  67. package/dist/interact/wheel.d.ts +15 -0
  68. package/dist/interact/wheel.js +18 -0
  69. package/dist/render/bars.d.ts +17 -0
  70. package/dist/render/bars.js +55 -0
  71. package/dist/render/canvas.d.ts +2 -0
  72. package/dist/render/canvas.js +13 -0
  73. package/dist/render/drawAxes.d.ts +2 -0
  74. package/dist/render/drawAxes.js +34 -0
  75. package/dist/render/drawCandles.d.ts +3 -0
  76. package/dist/render/drawCandles.js +68 -0
  77. package/dist/render/drawCrosshair.d.ts +7 -0
  78. package/dist/render/drawCrosshair.js +42 -0
  79. package/dist/render/drawDrawings.d.ts +10 -0
  80. package/dist/render/drawDrawings.js +146 -0
  81. package/dist/render/drawGrid.d.ts +2 -0
  82. package/dist/render/drawGrid.js +22 -0
  83. package/dist/render/drawIndicators.d.ts +4 -0
  84. package/dist/render/drawIndicators.js +67 -0
  85. package/dist/render/drawLastPrice.d.ts +10 -0
  86. package/dist/render/drawLastPrice.js +53 -0
  87. package/dist/render/drawLine.d.ts +3 -0
  88. package/dist/render/drawLine.js +43 -0
  89. package/dist/render/drawMain.d.ts +2 -0
  90. package/dist/render/drawMain.js +43 -0
  91. package/dist/render/drawMarkers.d.ts +2 -0
  92. package/dist/render/drawMarkers.js +39 -0
  93. package/dist/render/drawPriceLines.d.ts +4 -0
  94. package/dist/render/drawPriceLines.js +60 -0
  95. package/dist/render/drawSubPanes.d.ts +2 -0
  96. package/dist/render/drawSubPanes.js +104 -0
  97. package/dist/render/frame.d.ts +57 -0
  98. package/dist/render/frame.js +5 -0
  99. package/dist/render/frameBuilder.d.ts +12 -0
  100. package/dist/render/frameBuilder.js +129 -0
  101. package/dist/render/indicatorFrame.d.ts +17 -0
  102. package/dist/render/indicatorFrame.js +55 -0
  103. package/dist/render/layout.d.ts +4 -0
  104. package/dist/render/layout.js +26 -0
  105. package/dist/render/paintFrame.d.ts +11 -0
  106. package/dist/render/paintFrame.js +33 -0
  107. package/dist/render/scheduler.d.ts +17 -0
  108. package/dist/render/scheduler.js +38 -0
  109. package/dist/render/tween.d.ts +12 -0
  110. package/dist/render/tween.js +38 -0
  111. package/dist/theme.d.ts +2 -0
  112. package/dist/theme.js +16 -0
  113. package/package.json +37 -4
@@ -0,0 +1,39 @@
1
+ const bar = { o: 0, h: 0, l: 0, c: 0 };
2
+ export function drawMarkers(ctx, f) {
3
+ if (f.markers.length === 0)
4
+ return;
5
+ const r = f.vp.visibleRange();
6
+ if (!r)
7
+ return;
8
+ const size = Math.min(14, Math.max(8, Math.round(f.vp.barSpacing * 1.1)));
9
+ const stacks = new Map();
10
+ ctx.textAlign = 'center';
11
+ ctx.textBaseline = 'middle';
12
+ for (const m of f.markers) {
13
+ const idx = f.series.indexAtTime(m.t);
14
+ if (idx < r.from || idx > r.to)
15
+ continue;
16
+ f.source.at(idx, bar);
17
+ const key = `${idx}${m.kind}`;
18
+ const n = stacks.get(key) ?? 0;
19
+ stacks.set(key, n + 1);
20
+ const x = f.vp.x(idx);
21
+ const up = m.kind === 'buy';
22
+ const gap = 4 + n * (size + 2);
23
+ const apex = up ? f.scale.y(bar.l) + gap : f.scale.y(bar.h) - gap;
24
+ const base = up ? apex + size : apex - size;
25
+ ctx.beginPath();
26
+ ctx.moveTo(x, apex);
27
+ ctx.lineTo(x - size / 2, base);
28
+ ctx.lineTo(x + size / 2, base);
29
+ ctx.closePath();
30
+ ctx.fillStyle = up ? f.theme.up : f.theme.down;
31
+ ctx.fill();
32
+ if (m.label && size >= 11) {
33
+ ctx.font = `700 ${size - 4}px ${f.theme.fontFamily}`;
34
+ ctx.fillStyle = f.theme.background;
35
+ ctx.fillText(m.label.slice(0, 2), x, up ? base - size * 0.3 : base + size * 0.3);
36
+ }
37
+ }
38
+ ctx.textAlign = 'left';
39
+ }
@@ -0,0 +1,4 @@
1
+ import type { Ctx, Frame } from './frame.ts';
2
+ export declare function spreadTags(ys: readonly number[], gap: number, top: number, bottom: number): number[];
3
+ export declare function drawPriceLines(ctx: Ctx, f: Frame): void;
4
+ export declare function drawPriceLineTags(ctx: Ctx, f: Frame): void;
@@ -0,0 +1,60 @@
1
+ import { TAG_HEIGHT, TAG_PAD_X } from "./frame.js";
2
+ import { pinSide } from "../core/priceScale.js";
3
+ const DASH = { solid: [], dashed: [6, 4], dotted: [2, 3] };
4
+ export function spreadTags(ys, gap, top, bottom) {
5
+ const order = ys.map((_, i) => i).sort((a, b) => ys[a] - ys[b]);
6
+ const out = order.map((i) => Math.min(bottom, Math.max(top, ys[i])));
7
+ for (let k = 1; k < out.length; k++)
8
+ out[k] = Math.max(out[k], out[k - 1] + gap);
9
+ if (out.length > 0 && out[out.length - 1] > bottom) {
10
+ out[out.length - 1] = bottom;
11
+ for (let k = out.length - 2; k >= 0; k--)
12
+ out[k] = Math.min(out[k], out[k + 1] - gap);
13
+ }
14
+ const result = new Array(ys.length);
15
+ order.forEach((orig, k) => {
16
+ result[orig] = out[k];
17
+ });
18
+ return result;
19
+ }
20
+ export function drawPriceLines(ctx, f) {
21
+ ctx.lineWidth = 1;
22
+ for (const line of f.lines) {
23
+ if (pinSide(line.price, f.range))
24
+ continue;
25
+ const y = Math.round(f.scale.y(line.price)) + 0.5;
26
+ ctx.beginPath();
27
+ ctx.setLineDash(DASH[line.style ?? 'dashed']);
28
+ ctx.moveTo(f.layout.plotLeft, y);
29
+ ctx.lineTo(f.layout.plotRight, y);
30
+ ctx.strokeStyle = line.color;
31
+ ctx.stroke();
32
+ }
33
+ ctx.setLineDash([]);
34
+ }
35
+ export function drawPriceLineTags(ctx, f) {
36
+ if (f.lines.length === 0)
37
+ return;
38
+ const { layout } = f;
39
+ const half = TAG_HEIGHT / 2;
40
+ const top = layout.mainTop + half;
41
+ const bottom = layout.mainTop + layout.mainHeight - half;
42
+ const ys = f.lines.map((line) => {
43
+ const side = pinSide(line.price, f.range);
44
+ return side === 'above' ? top : side === 'below' ? bottom : f.scale.y(line.price);
45
+ });
46
+ const placed = spreadTags(ys, TAG_HEIGHT + 2, top, bottom);
47
+ ctx.font = f.font;
48
+ ctx.textBaseline = 'middle';
49
+ ctx.textAlign = 'left';
50
+ f.lines.forEach((line, i) => {
51
+ const side = pinSide(line.price, f.range);
52
+ const text = `${side === 'above' ? '↑ ' : side === 'below' ? '↓ ' : ''}${line.label}`;
53
+ const w = Math.max(layout.axisWidth, Math.ceil(ctx.measureText(text).width) + TAG_PAD_X * 2);
54
+ const y = Math.round(placed[i]);
55
+ ctx.fillStyle = line.color;
56
+ ctx.fillRect(layout.width - w, y - half, w, TAG_HEIGHT);
57
+ ctx.fillStyle = f.theme.background;
58
+ ctx.fillText(text, layout.width - w + TAG_PAD_X, y);
59
+ });
60
+ }
@@ -0,0 +1,2 @@
1
+ import type { Ctx, Frame } from './frame.ts';
2
+ export declare function drawSubPanes(ctx: Ctx, f: Frame): void;
@@ -0,0 +1,104 @@
1
+ import { priceFormatter } from "../core/format.js";
2
+ import { indicatorTitle } from "../core/indicators.js";
3
+ import { bodyWidth } from "./drawCandles.js";
4
+ import { tracePath } from "./drawIndicators.js";
5
+ import { TAG_PAD_X } from "./frame.js";
6
+ import { RSI_GUIDES, subTicks, subY } from "./indicatorFrame.js";
7
+ const HIST_ALPHA = 0.55;
8
+ function drawRsi(ctx, f, p, from, to) {
9
+ const { layout } = f;
10
+ ctx.beginPath();
11
+ ctx.lineWidth = 1;
12
+ ctx.setLineDash([3, 3]);
13
+ for (const g of RSI_GUIDES) {
14
+ const y = Math.round(subY(p, g)) + 0.5;
15
+ ctx.moveTo(layout.plotLeft, y);
16
+ ctx.lineTo(layout.plotRight, y);
17
+ }
18
+ ctx.strokeStyle = f.theme.textMuted;
19
+ ctx.globalAlpha = 0.5;
20
+ ctx.stroke();
21
+ ctx.globalAlpha = 1;
22
+ ctx.setLineDash([]);
23
+ ctx.lineWidth = 1.5;
24
+ ctx.strokeStyle = p.entry.spec.color;
25
+ tracePath(ctx, f, p.entry.calc.lines.rsi, from, to, (v) => subY(p, v));
26
+ ctx.stroke();
27
+ }
28
+ function drawMacd(ctx, f, p, from, to) {
29
+ const spec = p.entry.spec;
30
+ if (spec.type !== 'macd')
31
+ return;
32
+ const l = p.entry.calc.lines;
33
+ const zero = Math.round(subY(p, 0));
34
+ const bw = bodyWidth(f.vp.barSpacing);
35
+ const half = (bw - 1) / 2;
36
+ ctx.globalAlpha = HIST_ALPHA;
37
+ for (const up of [true, false]) {
38
+ ctx.beginPath();
39
+ for (let i = from; i <= to; i++) {
40
+ const h = l.hist[i];
41
+ if (h === null || h === undefined || h >= 0 !== up)
42
+ continue;
43
+ const y = Math.round(subY(p, h));
44
+ ctx.rect(Math.round(f.vp.x(i)) - half, Math.min(y, zero), bw, Math.max(1, Math.abs(y - zero)));
45
+ }
46
+ ctx.fillStyle = up ? f.theme.up : f.theme.down;
47
+ ctx.fill();
48
+ }
49
+ ctx.globalAlpha = 1;
50
+ ctx.beginPath();
51
+ ctx.lineWidth = 1;
52
+ ctx.moveTo(f.layout.plotLeft, zero + 0.5);
53
+ ctx.lineTo(f.layout.plotRight, zero + 0.5);
54
+ ctx.strokeStyle = f.theme.grid;
55
+ ctx.stroke();
56
+ ctx.lineWidth = 1.5;
57
+ for (const [values, color] of [
58
+ [l.macd, spec.color],
59
+ [l.signal, spec.signalColor],
60
+ ]) {
61
+ ctx.strokeStyle = color;
62
+ tracePath(ctx, f, values, from, to, (v) => subY(p, v));
63
+ ctx.stroke();
64
+ }
65
+ }
66
+ export function drawSubPanes(ctx, f) {
67
+ const r = f.vp.visibleRange();
68
+ const { layout, theme } = f;
69
+ for (const p of f.subs) {
70
+ ctx.beginPath();
71
+ ctx.lineWidth = 1;
72
+ ctx.moveTo(0, p.top + 0.5);
73
+ ctx.lineTo(layout.width, p.top + 0.5);
74
+ ctx.strokeStyle = theme.grid;
75
+ ctx.stroke();
76
+ if (r) {
77
+ ctx.save();
78
+ ctx.beginPath();
79
+ ctx.rect(layout.plotLeft, p.top + 1, layout.plotRight - layout.plotLeft, p.height - 1);
80
+ ctx.clip();
81
+ const from = Math.max(0, r.from - 1);
82
+ const to = Math.min(f.series.lastIndex(), r.to + 1);
83
+ if (p.entry.spec.type === 'rsi')
84
+ drawRsi(ctx, f, p, from, to);
85
+ else
86
+ drawMacd(ctx, f, p, from, to);
87
+ ctx.restore();
88
+ }
89
+ ctx.font = f.font;
90
+ ctx.fillStyle = theme.textMuted;
91
+ ctx.textBaseline = 'top';
92
+ ctx.textAlign = 'left';
93
+ ctx.fillText(indicatorTitle(p.entry.spec), 8, p.top + 4);
94
+ ctx.fillStyle = theme.text;
95
+ ctx.textBaseline = 'middle';
96
+ const fmt = p.entry.spec.type === 'rsi' ? priceFormatter(0, 'en-IN') : f.fmt;
97
+ for (const v of subTicks(p, 2)) {
98
+ const y = subY(p, v);
99
+ if (y < p.top + 8 || y > p.top + p.height - 8)
100
+ continue;
101
+ ctx.fillText(fmt(v), layout.plotRight + TAG_PAD_X, Math.round(y));
102
+ }
103
+ }
104
+ }
@@ -0,0 +1,57 @@
1
+ import type { CandleSeries } from '../core/series.ts';
2
+ import type { PriceRange, PriceScale } from '../core/priceScale.ts';
3
+ import type { TimeLabel } from '../core/timeLabels.ts';
4
+ import type { CandleStyle, ChartTheme, ChartType, Marker, PriceLine } from '../core/types.ts';
5
+ import type { Viewport } from '../core/viewport.ts';
6
+ import type { IndicatorEntry } from '../core/indicatorSet.ts';
7
+ import type { SubPane } from './indicatorFrame.ts';
8
+ export type Ctx = CanvasRenderingContext2D;
9
+ export interface Ohlc {
10
+ o: number;
11
+ h: number;
12
+ l: number;
13
+ c: number;
14
+ }
15
+ export interface BarSource {
16
+ at(i: number, out: Ohlc): void;
17
+ }
18
+ export interface Layout {
19
+ width: number;
20
+ height: number;
21
+ plotLeft: number;
22
+ plotRight: number;
23
+ mainTop: number;
24
+ mainHeight: number;
25
+ axisWidth: number;
26
+ timeTop: number;
27
+ timeHeight: number;
28
+ subs: {
29
+ top: number;
30
+ height: number;
31
+ }[];
32
+ }
33
+ export interface Frame {
34
+ theme: ChartTheme;
35
+ layout: Layout;
36
+ vp: Viewport;
37
+ scale: PriceScale;
38
+ series: CandleSeries;
39
+ source: BarSource;
40
+ type: ChartType;
41
+ candleStyle: CandleStyle;
42
+ range: PriceRange;
43
+ ticks: number[];
44
+ timeLabels: TimeLabel[];
45
+ lines: readonly PriceLine[];
46
+ markers: readonly Marker[];
47
+ overlays: readonly IndicatorEntry[];
48
+ subs: readonly SubPane[];
49
+ fmt: (v: number) => string;
50
+ font: string;
51
+ noDataText: string;
52
+ intervalMs: number;
53
+ tzOffsetMs: number;
54
+ }
55
+ export declare const TAG_PAD_X = 6;
56
+ export declare const TAG_HEIGHT = 18;
57
+ export declare function fontOf(theme: ChartTheme): string;
@@ -0,0 +1,5 @@
1
+ export const TAG_PAD_X = 6;
2
+ export const TAG_HEIGHT = 18;
3
+ export function fontOf(theme) {
4
+ return `500 ${theme.fontSize}px ${theme.fontFamily}`;
5
+ }
@@ -0,0 +1,12 @@
1
+ import type { ChartState } from '../chartState.ts';
2
+ import type { BarSource, Ctx, Frame } from './frame.ts';
3
+ export declare const RANGE_TWEEN_MS = 150;
4
+ export declare const CLOSE_TWEEN_MS = 180;
5
+ export declare const SCROLL_TWEEN_MS = 250;
6
+ export declare function sourceOf(s: ChartState): BarSource;
7
+ export interface Built {
8
+ frame: Frame;
9
+ animating: boolean;
10
+ relayout: boolean;
11
+ }
12
+ export declare function buildFrame(s: ChartState, ctx: Ctx, animNow: number): Built;
@@ -0,0 +1,129 @@
1
+ import { autoRange, axisTicks } from "../core/priceScale.js";
2
+ import { priceFormatter } from "../core/format.js";
3
+ import { INTERVAL_MS } from "../core/time.js";
4
+ import { pickTimeLabels } from "../core/timeLabels.js";
5
+ import { fontOf } from "./frame.js";
6
+ import { buildSubPanes, mainExtremes } from "./indicatorFrame.js";
7
+ import { computeLayout } from "./layout.js";
8
+ export const RANGE_TWEEN_MS = 150;
9
+ export const CLOSE_TWEEN_MS = 180;
10
+ export const SCROLL_TWEEN_MS = 250;
11
+ const bar = { o: 0, h: 0, l: 0, c: 0 };
12
+ export function sourceOf(s) {
13
+ return s.options.type === 'heikinAshi' ? s.haBars : s.plainBars;
14
+ }
15
+ function extremes(src, from, to) {
16
+ let min = Infinity;
17
+ let max = -Infinity;
18
+ for (let i = from; i <= to; i++) {
19
+ src.at(i, bar);
20
+ if (bar.l < min)
21
+ min = bar.l;
22
+ if (bar.h > max)
23
+ max = bar.h;
24
+ }
25
+ return { min, max };
26
+ }
27
+ function textWidth(s, ctx, text) {
28
+ let w = s.textWidths.get(text);
29
+ if (w === undefined) {
30
+ w = ctx.measureText(text).width;
31
+ if (s.textWidths.size > 500)
32
+ s.textWidths.clear();
33
+ s.textWidths.set(text, w);
34
+ }
35
+ return w;
36
+ }
37
+ export function buildFrame(s, ctx, animNow) {
38
+ const o = s.options;
39
+ const theme = o.theme;
40
+ const reduced = o.reducedMotion;
41
+ const font = fontOf(theme);
42
+ const layout = computeLayout(s.width, s.height, s.axisWidth, s.indicators.subCount);
43
+ s.vp.setPlot(layout.plotLeft, layout.plotRight);
44
+ s.scale.setPane(layout.mainTop, layout.mainHeight);
45
+ let animating = false;
46
+ if (s.rightAnimating) {
47
+ s.vp.setRightIndex(s.rightTween.value(animNow));
48
+ if (s.rightTween.active(animNow))
49
+ animating = true;
50
+ else
51
+ s.rightAnimating = false;
52
+ }
53
+ const src = sourceOf(s);
54
+ s.plainBars.drawnClose = s.series.forming ? s.closeTween.value(animNow) : null;
55
+ if (s.closeTween.active(animNow))
56
+ animating = true;
57
+ const vis = s.vp.visibleRange();
58
+ const overlays = s.indicators.main();
59
+ if (s.scale.manual) {
60
+ s.rangeMin.jump(s.scale.min);
61
+ s.rangeMax.jump(s.scale.max);
62
+ }
63
+ else if (vis) {
64
+ const bars = extremes(src, vis.from, vis.to);
65
+ const ind = mainExtremes(overlays, vis.from, vis.to);
66
+ if (ind) {
67
+ bars.min = Math.min(bars.min, ind.min);
68
+ bars.max = Math.max(bars.max, ind.max);
69
+ }
70
+ const target = autoRange(bars, [], s.lines, o.minMove);
71
+ if (!s.rangeReady || reduced) {
72
+ s.rangeMin.jump(target.min);
73
+ s.rangeMax.jump(target.max);
74
+ s.rangeReady = true;
75
+ }
76
+ else {
77
+ s.rangeMin.set(target.min, animNow, RANGE_TWEEN_MS);
78
+ s.rangeMax.set(target.max, animNow, RANGE_TWEEN_MS);
79
+ }
80
+ }
81
+ if (s.rangeMin.active(animNow) || s.rangeMax.active(animNow))
82
+ animating = true;
83
+ const range = { min: s.rangeMin.value(animNow), max: s.rangeMax.value(animNow) };
84
+ s.scale.setRange(range);
85
+ const ticks = axisTicks(range, layout.mainHeight, o.minMove);
86
+ const fmt = priceFormatter(o.priceDecimals, o.locale);
87
+ ctx.font = font;
88
+ let widest = 0;
89
+ for (const p of ticks)
90
+ widest = Math.max(widest, textWidth(s, ctx, fmt(p)));
91
+ if (s.series.forming) {
92
+ widest = Math.max(widest, textWidth(s, ctx, fmt(s.series.forming.c)));
93
+ widest = Math.max(widest, textWidth(s, ctx, o.intervalMs >= INTERVAL_MS['1h'] ? '00:00:00' : '0:00'));
94
+ }
95
+ const wanted = Math.ceil(widest) + 12;
96
+ const relayout = wanted > s.axisWidth || wanted < s.axisWidth - 8;
97
+ if (relayout)
98
+ s.axisWidth = wanted;
99
+ const labelBars = [];
100
+ if (vis) {
101
+ for (let i = vis.from; i <= vis.to; i++)
102
+ labelBars.push({ t: s.series.timeAt(i), x: s.vp.x(i) });
103
+ }
104
+ const prevT = vis && vis.from > 0 ? s.series.timeAt(vis.from - 1) : null;
105
+ const timeLabels = pickTimeLabels(labelBars, prevT, o.intervalMs, o.tzOffsetMs);
106
+ const frame = {
107
+ theme,
108
+ layout,
109
+ vp: s.vp,
110
+ scale: s.scale,
111
+ series: s.series,
112
+ source: src,
113
+ type: o.type,
114
+ candleStyle: o.candleStyle,
115
+ range,
116
+ ticks,
117
+ timeLabels,
118
+ lines: s.lines,
119
+ markers: s.markers,
120
+ overlays,
121
+ subs: vis ? buildSubPanes(s.indicators.subs(), layout, vis.from, vis.to) : [],
122
+ fmt,
123
+ font,
124
+ noDataText: o.labels.noData,
125
+ intervalMs: o.intervalMs,
126
+ tzOffsetMs: o.tzOffsetMs,
127
+ };
128
+ return { frame, animating, relayout };
129
+ }
@@ -0,0 +1,17 @@
1
+ import type { PriceRange } from '../core/priceScale.ts';
2
+ import type { IndicatorEntry } from '../core/indicatorSet.ts';
3
+ import type { Layout } from './frame.ts';
4
+ export declare const SUB_PAD = 6;
5
+ export declare const RSI_GUIDES: number[];
6
+ export interface SubPane {
7
+ entry: IndicatorEntry;
8
+ top: number;
9
+ height: number;
10
+ min: number;
11
+ max: number;
12
+ }
13
+ export declare function subY(p: SubPane, v: number): number;
14
+ export declare function subValueAt(p: SubPane, y: number): number;
15
+ export declare function mainExtremes(entries: readonly IndicatorEntry[], from: number, to: number): PriceRange | null;
16
+ export declare function buildSubPanes(entries: readonly IndicatorEntry[], layout: Layout, from: number, to: number): SubPane[];
17
+ export declare function subTicks(p: SubPane, decimals: number): number[];
@@ -0,0 +1,55 @@
1
+ import { axisTicks } from "../core/priceScale.js";
2
+ export const SUB_PAD = 6;
3
+ export const RSI_GUIDES = [30, 70];
4
+ export function subY(p, v) {
5
+ return p.top + SUB_PAD + ((p.max - v) / (p.max - p.min)) * Math.max(1, p.height - 2 * SUB_PAD);
6
+ }
7
+ export function subValueAt(p, y) {
8
+ return p.max - ((y - p.top - SUB_PAD) / Math.max(1, p.height - 2 * SUB_PAD)) * (p.max - p.min);
9
+ }
10
+ function scan(lists, from, to, into) {
11
+ for (const values of lists) {
12
+ for (let i = from; i <= to; i++) {
13
+ const v = values[i];
14
+ if (v === null || v === undefined)
15
+ continue;
16
+ if (v < into.min)
17
+ into.min = v;
18
+ if (v > into.max)
19
+ into.max = v;
20
+ }
21
+ }
22
+ }
23
+ export function mainExtremes(entries, from, to) {
24
+ const out = { min: Infinity, max: -Infinity };
25
+ for (const e of entries) {
26
+ const l = e.calc.lines;
27
+ scan(e.spec.type === 'bollinger' ? [l.upper, l.lower] : [l.main], from, to, out);
28
+ }
29
+ return out.min <= out.max ? out : null;
30
+ }
31
+ export function buildSubPanes(entries, layout, from, to) {
32
+ const panes = [];
33
+ entries.forEach((entry, i) => {
34
+ const slot = layout.subs[i];
35
+ if (!slot)
36
+ return;
37
+ let min = 0;
38
+ let max = 100;
39
+ if (entry.spec.type === 'macd') {
40
+ const l = entry.calc.lines;
41
+ const r = { min: 0, max: 0 };
42
+ scan([l.macd, l.signal, l.hist], from, to, r);
43
+ const pad = (r.max - r.min || 1) * 0.08;
44
+ min = r.min - pad;
45
+ max = r.max + pad;
46
+ }
47
+ panes.push({ entry, top: slot.top, height: slot.height, min, max });
48
+ });
49
+ return panes;
50
+ }
51
+ export function subTicks(p, decimals) {
52
+ if (p.entry.spec.type === 'rsi')
53
+ return RSI_GUIDES;
54
+ return axisTicks({ min: p.min, max: p.max }, p.height, 10 ** -(decimals + 2)).map((v) => v + 0);
55
+ }
@@ -0,0 +1,4 @@
1
+ import type { Layout } from './frame.ts';
2
+ export declare const TIME_AXIS_HEIGHT = 22;
3
+ export declare const MIN_AXIS_WIDTH = 56;
4
+ export declare function computeLayout(width: number, height: number, axisWidth: number, subCount: number): Layout;
@@ -0,0 +1,26 @@
1
+ export const TIME_AXIS_HEIGHT = 22;
2
+ export const MIN_AXIS_WIDTH = 56;
3
+ export function computeLayout(width, height, axisWidth, subCount) {
4
+ const axisW = Math.max(MIN_AXIS_WIDTH, Math.ceil(axisWidth));
5
+ const avail = Math.max(0, height - TIME_AXIS_HEIGHT);
6
+ const share = width < 360 ? 0.18 : 0.22;
7
+ let subH = subCount > 0 ? Math.max(64, Math.round(avail * share)) : 0;
8
+ if (subCount * subH > avail * 0.7)
9
+ subH = Math.floor((avail * 0.7) / subCount);
10
+ const mainH = avail - subCount * subH;
11
+ const subs = [];
12
+ for (let i = 0; i < subCount; i++)
13
+ subs.push({ top: mainH + i * subH, height: subH });
14
+ return {
15
+ width,
16
+ height,
17
+ plotLeft: 0,
18
+ plotRight: Math.max(0, width - axisW),
19
+ mainTop: 0,
20
+ mainHeight: mainH,
21
+ axisWidth: axisW,
22
+ timeTop: avail,
23
+ timeHeight: TIME_AXIS_HEIGHT,
24
+ subs,
25
+ };
26
+ }
@@ -0,0 +1,11 @@
1
+ import type { ChartState } from '../chartState.ts';
2
+ import type { Frame } from './frame.ts';
3
+ import type { Flags } from './scheduler.ts';
4
+ export interface PaintTarget {
5
+ mainCanvas: HTMLCanvasElement;
6
+ overlayCanvas: HTMLCanvasElement;
7
+ dpr(): number;
8
+ frameBuilt(frame: Frame): void;
9
+ afterDraw(): void;
10
+ }
11
+ export declare function paintFrame(s: ChartState, t: PaintTarget, flags: Flags, now: number): boolean;
@@ -0,0 +1,33 @@
1
+ import { fitCanvas } from "./canvas.js";
2
+ import { drawCrosshair } from "./drawCrosshair.js";
3
+ import { drawDrawings } from "./drawDrawings.js";
4
+ import { drawLastPrice, PULSE_MS } from "./drawLastPrice.js";
5
+ import { drawMain } from "./drawMain.js";
6
+ import { buildFrame } from "./frameBuilder.js";
7
+ export function paintFrame(s, t, flags, now) {
8
+ if (s.width <= 0 || s.height <= 0)
9
+ return false;
10
+ const mainCtx = fitCanvas(t.mainCanvas, s.width, s.height, t.dpr());
11
+ const built = buildFrame(s, mainCtx, now);
12
+ t.frameBuilt(built.frame);
13
+ if (flags.main || built.animating || built.relayout)
14
+ drawMain(mainCtx, built.frame);
15
+ const overlayCtx = fitCanvas(t.overlayCanvas, s.width, s.height, t.dpr());
16
+ overlayCtx.clearRect(0, 0, s.width, s.height);
17
+ s.pulses = s.pulses.filter((p) => now - p < PULSE_MS);
18
+ if (s.series.count > 0)
19
+ drawDrawings(overlayCtx, built.frame, { drawings: s.drawings, state: s.draw });
20
+ if (s.series.forming) {
21
+ drawLastPrice(overlayCtx, built.frame, {
22
+ close: s.closeTween.value(now),
23
+ wallNow: s.options.now(),
24
+ animNow: now,
25
+ pulses: s.pulses,
26
+ reducedMotion: s.options.reducedMotion,
27
+ });
28
+ }
29
+ if (s.crosshair)
30
+ drawCrosshair(overlayCtx, built.frame, s.crosshair);
31
+ t.afterDraw();
32
+ return built.animating || built.relayout || s.pulses.length > 0;
33
+ }
@@ -0,0 +1,17 @@
1
+ export type Layer = 'main' | 'overlay';
2
+ export interface Flags {
3
+ main: boolean;
4
+ overlay: boolean;
5
+ }
6
+ export declare class Scheduler {
7
+ private flags;
8
+ private handle;
9
+ private draw;
10
+ private raf;
11
+ private caf;
12
+ private stopped;
13
+ constructor(draw: (flags: Flags, now: number) => boolean, raf?: (cb: (now: number) => void) => number, caf?: (handle: number) => void);
14
+ invalidate(layer: Layer): void;
15
+ stop(): void;
16
+ private frame;
17
+ }
@@ -0,0 +1,38 @@
1
+ export class Scheduler {
2
+ constructor(draw, raf = (cb) => requestAnimationFrame(cb), caf = (h) => cancelAnimationFrame(h)) {
3
+ this.flags = { main: false, overlay: false };
4
+ this.handle = null;
5
+ this.stopped = false;
6
+ this.frame = (now) => {
7
+ this.handle = null;
8
+ if (this.stopped)
9
+ return;
10
+ const flags = { main: this.flags.main, overlay: this.flags.overlay };
11
+ this.flags.main = false;
12
+ this.flags.overlay = false;
13
+ const again = this.draw(flags, now);
14
+ if (again) {
15
+ this.flags.main = true;
16
+ this.flags.overlay = true;
17
+ if (this.handle === null)
18
+ this.handle = this.raf(this.frame);
19
+ }
20
+ };
21
+ this.draw = draw;
22
+ this.raf = raf;
23
+ this.caf = caf;
24
+ }
25
+ invalidate(layer) {
26
+ if (this.stopped)
27
+ return;
28
+ this.flags[layer] = true;
29
+ if (this.handle === null)
30
+ this.handle = this.raf(this.frame);
31
+ }
32
+ stop() {
33
+ this.stopped = true;
34
+ if (this.handle !== null)
35
+ this.caf(this.handle);
36
+ this.handle = null;
37
+ }
38
+ }
@@ -0,0 +1,12 @@
1
+ export declare class Tween {
2
+ private from;
3
+ private to;
4
+ private start;
5
+ private dur;
6
+ constructor(value: number);
7
+ get target(): number;
8
+ set(target: number, now: number, duration: number): void;
9
+ jump(value: number): void;
10
+ value(now: number): number;
11
+ active(now: number): boolean;
12
+ }