@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,55 @@
1
+ import { heikinAshi } from "../core/heikinAshi.js";
2
+ export class SeriesBars {
3
+ constructor(series) {
4
+ this.drawnClose = null;
5
+ this.series = series;
6
+ }
7
+ at(i, out) {
8
+ const s = this.series;
9
+ if (i === s.t.length && s.forming) {
10
+ const f = s.forming;
11
+ out.o = f.o;
12
+ out.h = f.h;
13
+ out.l = f.l;
14
+ out.c = this.drawnClose ?? f.c;
15
+ return;
16
+ }
17
+ out.o = s.o[i];
18
+ out.h = s.h[i];
19
+ out.l = s.l[i];
20
+ out.c = s.c[i];
21
+ }
22
+ }
23
+ export class HeikinBars {
24
+ constructor(series) {
25
+ this.ha = [];
26
+ this.formingHa = null;
27
+ this.series = series;
28
+ }
29
+ rebuild() {
30
+ const s = this.series;
31
+ const all = [];
32
+ for (let i = 0; i < s.t.length; i++)
33
+ all.push({ t: s.t[i], o: s.o[i], h: s.h[i], l: s.l[i], c: s.c[i] });
34
+ this.ha = heikinAshi(all);
35
+ this.refreshForming();
36
+ }
37
+ refreshForming() {
38
+ const f = this.series.forming;
39
+ if (!f) {
40
+ this.formingHa = null;
41
+ return;
42
+ }
43
+ const prev = this.ha[this.ha.length - 1];
44
+ const c = (f.o + f.h + f.l + f.c) / 4;
45
+ const o = prev ? (prev.o + prev.c) / 2 : (f.o + f.c) / 2;
46
+ this.formingHa = { t: f.t, o, h: Math.max(f.h, o, c), l: Math.min(f.l, o, c), c };
47
+ }
48
+ at(i, out) {
49
+ const k = i === this.ha.length && this.formingHa ? this.formingHa : this.ha[i];
50
+ out.o = k.o;
51
+ out.h = k.h;
52
+ out.l = k.l;
53
+ out.c = k.c;
54
+ }
55
+ }
@@ -0,0 +1,2 @@
1
+ import type { Ctx } from './frame.ts';
2
+ export declare function fitCanvas(canvas: HTMLCanvasElement, cssWidth: number, cssHeight: number, dpr: number): Ctx;
@@ -0,0 +1,13 @@
1
+ export function fitCanvas(canvas, cssWidth, cssHeight, dpr) {
2
+ const w = Math.max(1, Math.round(cssWidth * dpr));
3
+ const h = Math.max(1, Math.round(cssHeight * dpr));
4
+ if (canvas.width !== w)
5
+ canvas.width = w;
6
+ if (canvas.height !== h)
7
+ canvas.height = h;
8
+ canvas.style.width = `${cssWidth}px`;
9
+ canvas.style.height = `${cssHeight}px`;
10
+ const ctx = canvas.getContext('2d');
11
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
12
+ return ctx;
13
+ }
@@ -0,0 +1,2 @@
1
+ import type { Ctx, Frame } from './frame.ts';
2
+ export declare function drawAxes(ctx: Ctx, f: Frame): void;
@@ -0,0 +1,34 @@
1
+ import { TAG_PAD_X } from "./frame.js";
2
+ export function drawAxes(ctx, f) {
3
+ const { layout, theme } = f;
4
+ ctx.fillStyle = theme.background;
5
+ ctx.fillRect(layout.plotRight, 0, layout.axisWidth, layout.height);
6
+ ctx.fillRect(0, layout.timeTop, layout.width, layout.timeHeight);
7
+ ctx.beginPath();
8
+ ctx.lineWidth = 1;
9
+ ctx.moveTo(layout.plotRight + 0.5, 0);
10
+ ctx.lineTo(layout.plotRight + 0.5, layout.timeTop);
11
+ ctx.moveTo(0, layout.timeTop + 0.5);
12
+ ctx.lineTo(layout.width, layout.timeTop + 0.5);
13
+ ctx.strokeStyle = theme.grid;
14
+ ctx.stroke();
15
+ ctx.font = f.font;
16
+ ctx.fillStyle = theme.text;
17
+ ctx.textBaseline = 'middle';
18
+ ctx.textAlign = 'left';
19
+ const bottom = layout.mainTop + layout.mainHeight;
20
+ for (const p of f.ticks) {
21
+ const y = f.scale.y(p);
22
+ if (y < layout.mainTop + 6 || y > bottom - 6)
23
+ continue;
24
+ ctx.fillText(f.fmt(p), layout.plotRight + TAG_PAD_X, Math.round(y));
25
+ }
26
+ ctx.textAlign = 'center';
27
+ const ty = layout.timeTop + layout.timeHeight / 2 + 1;
28
+ for (const label of f.timeLabels) {
29
+ if (label.x < 0 || label.x > layout.plotRight)
30
+ continue;
31
+ ctx.fillText(label.text, Math.round(label.x), ty);
32
+ }
33
+ ctx.textAlign = 'left';
34
+ }
@@ -0,0 +1,3 @@
1
+ import type { Ctx, Frame } from './frame.ts';
2
+ export declare function bodyWidth(barSpacing: number): number;
3
+ export declare function drawCandles(ctx: Ctx, f: Frame): void;
@@ -0,0 +1,68 @@
1
+ const bar = { o: 0, h: 0, l: 0, c: 0 };
2
+ export function bodyWidth(barSpacing) {
3
+ const w = Math.max(1, Math.floor(barSpacing * 0.7));
4
+ return w % 2 === 0 ? w - 1 || 1 : w;
5
+ }
6
+ export function drawCandles(ctx, f) {
7
+ const r = f.vp.visibleRange();
8
+ if (!r)
9
+ return;
10
+ const wickOnly = f.vp.barSpacing < 3;
11
+ const bw = bodyWidth(f.vp.barSpacing);
12
+ const half = (bw - 1) / 2;
13
+ ctx.lineWidth = 1;
14
+ const wicks = (up) => {
15
+ ctx.beginPath();
16
+ for (let i = r.from; i <= r.to; i++) {
17
+ f.source.at(i, bar);
18
+ if (bar.c >= bar.o !== up)
19
+ continue;
20
+ const x = Math.round(f.vp.x(i)) + 0.5;
21
+ const yh = Math.round(f.scale.y(bar.h));
22
+ const yl = Math.round(f.scale.y(bar.l));
23
+ if (wickOnly) {
24
+ ctx.moveTo(x, yh);
25
+ ctx.lineTo(x, yl);
26
+ continue;
27
+ }
28
+ const top = Math.round(f.scale.y(Math.max(bar.o, bar.c)));
29
+ const bottom = Math.max(top + 1, Math.round(f.scale.y(Math.min(bar.o, bar.c))));
30
+ ctx.moveTo(x, yh);
31
+ ctx.lineTo(x, top);
32
+ ctx.moveTo(x, bottom);
33
+ ctx.lineTo(x, yl);
34
+ }
35
+ ctx.strokeStyle = up ? f.theme.up : f.theme.down;
36
+ ctx.stroke();
37
+ };
38
+ const bodies = (up) => {
39
+ const hollow = up && f.candleStyle === 'hollowUp';
40
+ ctx.beginPath();
41
+ for (let i = r.from; i <= r.to; i++) {
42
+ f.source.at(i, bar);
43
+ if (bar.c >= bar.o !== up)
44
+ continue;
45
+ const x0 = Math.round(f.vp.x(i)) - half;
46
+ const top = Math.round(f.scale.y(Math.max(bar.o, bar.c)));
47
+ const h = Math.max(1, Math.round(f.scale.y(Math.min(bar.o, bar.c))) - top);
48
+ if (hollow)
49
+ ctx.rect(x0 + 0.5, top + 0.5, bw - 1, h - 1);
50
+ else
51
+ ctx.rect(x0, top, bw, h);
52
+ }
53
+ if (hollow) {
54
+ ctx.strokeStyle = f.theme.up;
55
+ ctx.stroke();
56
+ }
57
+ else {
58
+ ctx.fillStyle = up ? f.theme.up : f.theme.down;
59
+ ctx.fill();
60
+ }
61
+ };
62
+ wicks(true);
63
+ wicks(false);
64
+ if (!wickOnly) {
65
+ bodies(true);
66
+ bodies(false);
67
+ }
68
+ }
@@ -0,0 +1,7 @@
1
+ import type { Ctx, Frame } from './frame.ts';
2
+ export interface CrosshairInput {
3
+ x: number;
4
+ y: number;
5
+ index: number;
6
+ }
7
+ export declare function drawCrosshair(ctx: Ctx, f: Frame, c: CrosshairInput): void;
@@ -0,0 +1,42 @@
1
+ import { formatSlotTime } from "../core/time.js";
2
+ import { TAG_HEIGHT, TAG_PAD_X } from "./frame.js";
3
+ import { subValueAt } from "./indicatorFrame.js";
4
+ export function drawCrosshair(ctx, f, c) {
5
+ if (c.index < 0 || c.index > f.series.lastIndex())
6
+ return;
7
+ const { layout, theme } = f;
8
+ const x = Math.round(f.vp.x(c.index)) + 0.5;
9
+ const inMain = c.y >= layout.mainTop && c.y <= layout.mainTop + layout.mainHeight;
10
+ const sub = f.subs.find((p) => c.y >= p.top && c.y <= p.top + p.height);
11
+ const guide = inMain || sub !== undefined;
12
+ const y = Math.round(c.y) + 0.5;
13
+ ctx.beginPath();
14
+ ctx.lineWidth = 1;
15
+ ctx.setLineDash([4, 4]);
16
+ ctx.strokeStyle = theme.crosshair;
17
+ ctx.moveTo(x, layout.mainTop);
18
+ ctx.lineTo(x, layout.timeTop);
19
+ if (guide) {
20
+ ctx.moveTo(layout.plotLeft, y);
21
+ ctx.lineTo(layout.plotRight, y);
22
+ }
23
+ ctx.stroke();
24
+ ctx.setLineDash([]);
25
+ ctx.font = f.font;
26
+ ctx.textBaseline = 'middle';
27
+ ctx.fillStyle = theme.accent;
28
+ if (guide) {
29
+ ctx.fillRect(layout.plotRight + 1, Math.round(c.y - TAG_HEIGHT / 2), layout.axisWidth - 1, TAG_HEIGHT);
30
+ ctx.fillStyle = theme.accentText;
31
+ ctx.textAlign = 'left';
32
+ ctx.fillText(f.fmt(sub ? subValueAt(sub, c.y) : f.scale.priceAt(c.y)), layout.plotRight + TAG_PAD_X, Math.round(c.y));
33
+ }
34
+ const text = formatSlotTime(f.series.timeAt(c.index), f.intervalMs, f.tzOffsetMs);
35
+ const w = Math.ceil(ctx.measureText(text).width) + TAG_PAD_X * 2;
36
+ const left = Math.min(layout.width - w, Math.max(0, Math.round(x - w / 2)));
37
+ ctx.fillStyle = theme.accent;
38
+ ctx.fillRect(left, layout.timeTop + 1, w, layout.timeHeight - 1);
39
+ ctx.fillStyle = theme.accentText;
40
+ ctx.textAlign = 'left';
41
+ ctx.fillText(text, left + TAG_PAD_X, layout.timeTop + layout.timeHeight / 2 + 1);
42
+ }
@@ -0,0 +1,10 @@
1
+ import type { Drawing } from '../core/drawings.ts';
2
+ import type { DrawState } from '../draw/controller.ts';
3
+ import type { Mapper } from '../draw/mapper.ts';
4
+ import type { Ctx, Frame } from './frame.ts';
5
+ export interface DrawingsView {
6
+ drawings: readonly Drawing[];
7
+ state: DrawState;
8
+ }
9
+ export declare function mapperOf(f: Frame): Mapper;
10
+ export declare function drawDrawings(ctx: Ctx, f: Frame, view: DrawingsView): void;
@@ -0,0 +1,146 @@
1
+ import { measureBox, shapeOf } from "../draw/geometry.js";
2
+ import { makeMapper } from "../draw/mapper.js";
3
+ import { TAG_HEIGHT, TAG_PAD_X } from "./frame.js";
4
+ const HANDLE = 9;
5
+ const PREVIEW_ALPHA = 0.75;
6
+ const ZONE_ALPHA = 0.1;
7
+ export function mapperOf(f) {
8
+ const l = f.layout;
9
+ return makeMapper({
10
+ series: f.series,
11
+ vp: f.vp,
12
+ scale: f.scale,
13
+ intervalMs: f.intervalMs,
14
+ bounds: { left: l.plotLeft, right: l.plotRight, top: l.mainTop, bottom: l.mainTop + l.mainHeight },
15
+ });
16
+ }
17
+ function paintDrawing(ctx, f, m, d, selected) {
18
+ const shape = shapeOf(d, m, f.theme.fontSize);
19
+ ctx.strokeStyle = d.color;
20
+ ctx.fillStyle = d.color;
21
+ ctx.lineWidth = selected ? 2 : 1.5;
22
+ if (shape.box) {
23
+ const base = ctx.globalAlpha || 1;
24
+ ctx.globalAlpha = base * ZONE_ALPHA;
25
+ ctx.fillRect(shape.box.x, shape.box.y, shape.box.w, shape.box.h);
26
+ ctx.globalAlpha = base;
27
+ }
28
+ if (shape.segments.length > 0) {
29
+ ctx.beginPath();
30
+ for (const s of shape.segments) {
31
+ ctx.moveTo(s.a.x, s.a.y);
32
+ ctx.lineTo(s.b.x, s.b.y);
33
+ }
34
+ ctx.stroke();
35
+ }
36
+ ctx.font = f.font;
37
+ ctx.textBaseline = 'bottom';
38
+ ctx.textAlign = 'left';
39
+ for (const line of shape.fib) {
40
+ ctx.lineWidth = 1;
41
+ ctx.setLineDash(line.level === 0 || line.level === 1 ? [] : [4, 4]);
42
+ ctx.beginPath();
43
+ ctx.moveTo(line.seg.a.x, line.seg.a.y + 0.5);
44
+ ctx.lineTo(line.seg.b.x, line.seg.b.y + 0.5);
45
+ ctx.stroke();
46
+ ctx.setLineDash([]);
47
+ ctx.fillText(`${line.level} ${f.fmt(line.price)}`, line.seg.a.x + 4, line.seg.a.y - 2);
48
+ }
49
+ if (shape.note) {
50
+ const { box, pin } = shape.note;
51
+ ctx.lineWidth = 1;
52
+ ctx.beginPath();
53
+ ctx.moveTo(pin.x, pin.y);
54
+ ctx.lineTo(pin.x < box.x ? box.x : box.x + box.w, pin.y < box.y ? box.y : box.y + box.h);
55
+ ctx.stroke();
56
+ ctx.fillRect(pin.x - 3, pin.y - 3, 6, 6);
57
+ ctx.fillStyle = f.theme.background;
58
+ ctx.fillRect(box.x, box.y, box.w, box.h);
59
+ ctx.strokeRect(box.x + 0.5, box.y + 0.5, box.w - 1, box.h - 1);
60
+ ctx.fillStyle = f.theme.text;
61
+ ctx.textBaseline = 'middle';
62
+ ctx.fillText(d.text ?? '', box.x + 6, box.y + box.h / 2 + 1);
63
+ }
64
+ if (selected)
65
+ paintHandles(ctx, f, shape.handles, d.color);
66
+ }
67
+ function paintHandles(ctx, f, handles, color) {
68
+ ctx.lineWidth = 1.5;
69
+ ctx.strokeStyle = color;
70
+ ctx.fillStyle = f.theme.background;
71
+ for (const h of handles) {
72
+ ctx.fillRect(Math.round(h.x - HANDLE / 2), Math.round(h.y - HANDLE / 2), HANDLE, HANDLE);
73
+ ctx.strokeRect(Math.round(h.x - HANDLE / 2) + 0.5, Math.round(h.y - HANDLE / 2) + 0.5, HANDLE - 1, HANDLE - 1);
74
+ }
75
+ }
76
+ function paintMeasure(ctx, f, m, a, b) {
77
+ const pa = m.px(a);
78
+ const pb = m.px(b);
79
+ const box = measureBox(pa, pb);
80
+ const up = b.price >= a.price;
81
+ const color = up ? f.theme.up : f.theme.down;
82
+ ctx.fillStyle = color;
83
+ ctx.globalAlpha = ZONE_ALPHA;
84
+ ctx.fillRect(box.x, box.y, box.w, box.h);
85
+ ctx.globalAlpha = 1;
86
+ ctx.strokeStyle = color;
87
+ ctx.lineWidth = 1;
88
+ ctx.setLineDash([4, 4]);
89
+ ctx.strokeRect(box.x + 0.5, box.y + 0.5, box.w, box.h);
90
+ ctx.setLineDash([]);
91
+ const diff = b.price - a.price;
92
+ const sign = diff >= 0 ? '+' : '−';
93
+ const pct = a.price !== 0 ? (Math.abs(diff) / a.price) * 100 : 0;
94
+ const candles = Math.round(Math.abs(m.indexOfTime(b.t) - m.indexOfTime(a.t)));
95
+ const text = `${sign}${f.fmt(Math.abs(diff))} (${sign}${pct.toFixed(2)}%) · ${candles} candle${candles === 1 ? '' : 's'}`;
96
+ ctx.font = f.font;
97
+ const w = Math.ceil(ctx.measureText(text).width) + TAG_PAD_X * 2;
98
+ const { left, right, top, bottom } = m.bounds;
99
+ const x = Math.min(right - w, Math.max(left, Math.round(box.x + box.w / 2 - w / 2)));
100
+ const y = Math.min(bottom - TAG_HEIGHT, Math.max(top, Math.round(box.y + box.h / 2 - TAG_HEIGHT / 2)));
101
+ ctx.fillStyle = color;
102
+ ctx.fillRect(x, y, w, TAG_HEIGHT);
103
+ ctx.fillStyle = f.theme.background;
104
+ ctx.textAlign = 'left';
105
+ ctx.textBaseline = 'middle';
106
+ ctx.fillText(text, x + TAG_PAD_X, y + TAG_HEIGHT / 2 + 1);
107
+ }
108
+ export function drawDrawings(ctx, f, view) {
109
+ const { drawings, state } = view;
110
+ const l = f.layout;
111
+ if (drawings.length === 0 && state.pending.length === 0 && !state.measure)
112
+ return;
113
+ const m = mapperOf(f);
114
+ ctx.save();
115
+ ctx.beginPath();
116
+ ctx.rect(l.plotLeft, l.mainTop, l.plotRight - l.plotLeft, l.mainHeight);
117
+ ctx.clip();
118
+ for (const d of drawings)
119
+ paintDrawing(ctx, f, m, d, d.id === state.selectedId);
120
+ const first = state.pending[0];
121
+ if (first && state.preview && state.tool) {
122
+ ctx.globalAlpha = PREVIEW_ALPHA;
123
+ if (state.tool === 'measure')
124
+ paintMeasure(ctx, f, m, first, state.preview);
125
+ else
126
+ paintDrawing(ctx, f, m, { type: state.tool, points: [first, state.preview], color: state.color }, false);
127
+ ctx.globalAlpha = 1;
128
+ }
129
+ if (state.measure)
130
+ paintMeasure(ctx, f, m, state.measure.a, state.measure.b);
131
+ ctx.restore();
132
+ ctx.font = f.font;
133
+ ctx.textBaseline = 'middle';
134
+ ctx.textAlign = 'left';
135
+ for (const d of drawings) {
136
+ if (d.type !== 'hline')
137
+ continue;
138
+ const y = Math.round(f.scale.y(d.points[0].price));
139
+ if (y < l.mainTop + TAG_HEIGHT / 2 || y > l.mainTop + l.mainHeight - TAG_HEIGHT / 2)
140
+ continue;
141
+ ctx.fillStyle = d.color;
142
+ ctx.fillRect(l.plotRight + 1, y - TAG_HEIGHT / 2, l.axisWidth - 1, TAG_HEIGHT);
143
+ ctx.fillStyle = f.theme.background;
144
+ ctx.fillText(f.fmt(d.points[0].price), l.plotRight + TAG_PAD_X, y + 1);
145
+ }
146
+ }
@@ -0,0 +1,2 @@
1
+ import type { Ctx, Frame } from './frame.ts';
2
+ export declare function drawGrid(ctx: Ctx, f: Frame): void;
@@ -0,0 +1,22 @@
1
+ export function drawGrid(ctx, f) {
2
+ const { layout } = f;
3
+ const bottom = layout.mainTop + layout.mainHeight;
4
+ ctx.beginPath();
5
+ ctx.lineWidth = 1;
6
+ for (const p of f.ticks) {
7
+ const y = Math.round(f.scale.y(p)) + 0.5;
8
+ if (y < layout.mainTop || y > bottom)
9
+ continue;
10
+ ctx.moveTo(layout.plotLeft, y);
11
+ ctx.lineTo(layout.plotRight, y);
12
+ }
13
+ for (const label of f.timeLabels) {
14
+ const x = Math.round(label.x) + 0.5;
15
+ if (x < layout.plotLeft || x > layout.plotRight)
16
+ continue;
17
+ ctx.moveTo(x, layout.mainTop);
18
+ ctx.lineTo(x, layout.timeTop);
19
+ }
20
+ ctx.strokeStyle = f.theme.grid;
21
+ ctx.stroke();
22
+ }
@@ -0,0 +1,4 @@
1
+ import type { Values } from '../core/indicators.ts';
2
+ import type { Ctx, Frame } from './frame.ts';
3
+ export declare function tracePath(ctx: Ctx, f: Frame, values: Values, from: number, to: number, y: (v: number) => number): void;
4
+ export declare function drawOverlays(ctx: Ctx, f: Frame): void;
@@ -0,0 +1,67 @@
1
+ export function tracePath(ctx, f, values, from, to, y) {
2
+ ctx.beginPath();
3
+ let pen = false;
4
+ for (let i = from; i <= to; i++) {
5
+ const v = values[i];
6
+ if (v === null || v === undefined) {
7
+ pen = false;
8
+ continue;
9
+ }
10
+ if (pen)
11
+ ctx.lineTo(f.vp.x(i), y(v));
12
+ else
13
+ ctx.moveTo(f.vp.x(i), y(v));
14
+ pen = true;
15
+ }
16
+ }
17
+ export function drawOverlays(ctx, f) {
18
+ const r = f.vp.visibleRange();
19
+ if (!r || f.overlays.length === 0)
20
+ return;
21
+ const from = Math.max(0, r.from - 1);
22
+ const to = Math.min(f.series.lastIndex(), r.to + 1);
23
+ const y = (v) => f.scale.y(v);
24
+ ctx.lineJoin = 'miter';
25
+ for (const e of f.overlays) {
26
+ const color = e.spec.color;
27
+ const l = e.calc.lines;
28
+ if (e.spec.type === 'bollinger') {
29
+ const upper = l.upper;
30
+ const lower = l.lower;
31
+ let first = -1;
32
+ for (let i = from; i <= to; i++) {
33
+ if (upper[i] !== null && upper[i] !== undefined) {
34
+ first = i;
35
+ break;
36
+ }
37
+ }
38
+ if (first >= 0) {
39
+ ctx.beginPath();
40
+ for (let i = first; i <= to; i++)
41
+ ctx[i === first ? 'moveTo' : 'lineTo'](f.vp.x(i), y(upper[i]));
42
+ for (let i = to; i >= first; i--)
43
+ ctx.lineTo(f.vp.x(i), y(lower[i]));
44
+ ctx.closePath();
45
+ ctx.globalAlpha = 0.08;
46
+ ctx.fillStyle = color;
47
+ ctx.fill();
48
+ ctx.globalAlpha = 1;
49
+ }
50
+ ctx.strokeStyle = color;
51
+ ctx.lineWidth = 1;
52
+ for (const v of [upper, lower]) {
53
+ tracePath(ctx, f, v, from, to, y);
54
+ ctx.stroke();
55
+ }
56
+ ctx.lineWidth = 1.5;
57
+ tracePath(ctx, f, l.mid, from, to, y);
58
+ ctx.stroke();
59
+ }
60
+ else {
61
+ ctx.strokeStyle = color;
62
+ ctx.lineWidth = 1.5;
63
+ tracePath(ctx, f, l.main, from, to, y);
64
+ ctx.stroke();
65
+ }
66
+ }
67
+ }
@@ -0,0 +1,10 @@
1
+ import type { Ctx, Frame } from './frame.ts';
2
+ export declare const PULSE_MS = 600;
3
+ export interface LastPriceInput {
4
+ close: number;
5
+ wallNow: number;
6
+ animNow: number;
7
+ pulses: readonly number[];
8
+ reducedMotion: boolean;
9
+ }
10
+ export declare function drawLastPrice(ctx: Ctx, f: Frame, input: LastPriceInput): void;
@@ -0,0 +1,53 @@
1
+ import { slotEnd } from "../core/time.js";
2
+ import { formatCountdown } from "../core/format.js";
3
+ import { TAG_HEIGHT, TAG_PAD_X } from "./frame.js";
4
+ import { LIVE_POINT } from "./drawLine.js";
5
+ export const PULSE_MS = 600;
6
+ const PULSE_GROW = 16;
7
+ export function drawLastPrice(ctx, f, input) {
8
+ const forming = f.series.forming;
9
+ if (!forming)
10
+ return;
11
+ const { layout, theme } = f;
12
+ const half = TAG_HEIGHT;
13
+ const top = layout.mainTop + half;
14
+ const bottom = layout.mainTop + layout.mainHeight - half;
15
+ const rawY = f.scale.y(input.close);
16
+ const y = Math.round(Math.min(bottom, Math.max(top, rawY)));
17
+ const colour = forming.c >= forming.o ? theme.up : theme.down;
18
+ if (rawY >= layout.mainTop && rawY <= layout.mainTop + layout.mainHeight) {
19
+ ctx.beginPath();
20
+ ctx.lineWidth = 1;
21
+ ctx.setLineDash([4, 4]);
22
+ ctx.moveTo(layout.plotLeft, y + 0.5);
23
+ ctx.lineTo(layout.plotRight, y + 0.5);
24
+ ctx.strokeStyle = colour;
25
+ ctx.stroke();
26
+ ctx.setLineDash([]);
27
+ }
28
+ ctx.font = f.font;
29
+ ctx.textBaseline = 'middle';
30
+ ctx.textAlign = 'left';
31
+ ctx.fillStyle = colour;
32
+ ctx.fillRect(layout.plotRight + 1, y - TAG_HEIGHT, layout.axisWidth - 1, TAG_HEIGHT * 2);
33
+ ctx.fillStyle = theme.background;
34
+ ctx.fillText(f.fmt(input.close), layout.plotRight + TAG_PAD_X, y - TAG_HEIGHT / 2);
35
+ const left = formatCountdown(slotEnd(forming.t, f.intervalMs) - input.wallNow);
36
+ ctx.fillText(left, layout.plotRight + TAG_PAD_X, y + TAG_HEIGHT / 2);
37
+ if ((f.type === 'line' || f.type === 'area') && !input.reducedMotion) {
38
+ const x = f.vp.x(f.series.lastIndex());
39
+ if (x < layout.plotLeft || x > layout.plotRight)
40
+ return;
41
+ ctx.lineWidth = 1;
42
+ for (const start of input.pulses) {
43
+ const p = (input.animNow - start) / PULSE_MS;
44
+ if (p < 0 || p >= 1)
45
+ continue;
46
+ const s = LIVE_POINT + p * PULSE_GROW;
47
+ ctx.globalAlpha = 1 - p;
48
+ ctx.strokeStyle = theme.line;
49
+ ctx.strokeRect(Math.round(x - s / 2) + 0.5, Math.round(rawY - s / 2) + 0.5, Math.round(s), Math.round(s));
50
+ }
51
+ ctx.globalAlpha = 1;
52
+ }
53
+ }
@@ -0,0 +1,3 @@
1
+ import type { Ctx, Frame } from './frame.ts';
2
+ export declare const LIVE_POINT = 5;
3
+ export declare function drawLine(ctx: Ctx, f: Frame): void;
@@ -0,0 +1,43 @@
1
+ const bar = { o: 0, h: 0, l: 0, c: 0 };
2
+ export const LIVE_POINT = 5;
3
+ export function drawLine(ctx, f) {
4
+ const r = f.vp.visibleRange();
5
+ if (!r)
6
+ return;
7
+ const bottom = f.layout.mainTop + f.layout.mainHeight;
8
+ const trace = () => {
9
+ for (let i = r.from; i <= r.to; i++) {
10
+ f.source.at(i, bar);
11
+ const x = f.vp.x(i);
12
+ const y = f.scale.y(bar.c);
13
+ if (i === r.from)
14
+ ctx.moveTo(x, y);
15
+ else
16
+ ctx.lineTo(x, y);
17
+ }
18
+ };
19
+ if (f.type === 'area') {
20
+ ctx.beginPath();
21
+ trace();
22
+ ctx.lineTo(f.vp.x(r.to), bottom);
23
+ ctx.lineTo(f.vp.x(r.from), bottom);
24
+ ctx.closePath();
25
+ const g = ctx.createLinearGradient(0, f.layout.mainTop, 0, bottom);
26
+ g.addColorStop(0, f.theme.areaTop);
27
+ g.addColorStop(1, f.theme.areaBottom);
28
+ ctx.fillStyle = g;
29
+ ctx.fill();
30
+ }
31
+ ctx.beginPath();
32
+ trace();
33
+ ctx.lineWidth = 1.5;
34
+ ctx.lineJoin = 'miter';
35
+ ctx.strokeStyle = f.theme.line;
36
+ ctx.stroke();
37
+ if (f.series.forming && r.to === f.series.lastIndex()) {
38
+ f.source.at(r.to, bar);
39
+ const s = LIVE_POINT;
40
+ ctx.fillStyle = f.theme.line;
41
+ ctx.fillRect(Math.round(f.vp.x(r.to) - s / 2), Math.round(f.scale.y(bar.c) - s / 2), s, s);
42
+ }
43
+ }
@@ -0,0 +1,2 @@
1
+ import type { Ctx, Frame } from './frame.ts';
2
+ export declare function drawMain(ctx: Ctx, f: Frame): void;
@@ -0,0 +1,43 @@
1
+ import { drawAxes } from "./drawAxes.js";
2
+ import { drawCandles } from "./drawCandles.js";
3
+ import { drawGrid } from "./drawGrid.js";
4
+ import { drawLine } from "./drawLine.js";
5
+ import { drawOverlays } from "./drawIndicators.js";
6
+ import { drawMarkers } from "./drawMarkers.js";
7
+ import { drawPriceLines, drawPriceLineTags } from "./drawPriceLines.js";
8
+ import { drawSubPanes } from "./drawSubPanes.js";
9
+ export function drawMain(ctx, f) {
10
+ const { layout, theme } = f;
11
+ ctx.fillStyle = theme.background;
12
+ ctx.fillRect(0, 0, layout.width, layout.height);
13
+ drawGrid(ctx, f);
14
+ if (f.series.count > 0) {
15
+ const clip = f.subs.length > 0;
16
+ if (clip) {
17
+ ctx.save();
18
+ ctx.beginPath();
19
+ ctx.rect(0, layout.mainTop, layout.plotRight, layout.mainHeight);
20
+ ctx.clip();
21
+ }
22
+ if (f.type === 'line' || f.type === 'area')
23
+ drawLine(ctx, f);
24
+ else
25
+ drawCandles(ctx, f);
26
+ drawOverlays(ctx, f);
27
+ drawPriceLines(ctx, f);
28
+ drawMarkers(ctx, f);
29
+ if (clip)
30
+ ctx.restore();
31
+ }
32
+ drawAxes(ctx, f);
33
+ drawSubPanes(ctx, f);
34
+ drawPriceLineTags(ctx, f);
35
+ if (f.series.count === 0) {
36
+ ctx.font = f.font;
37
+ ctx.fillStyle = theme.textMuted;
38
+ ctx.textAlign = 'center';
39
+ ctx.textBaseline = 'middle';
40
+ ctx.fillText(f.noDataText, layout.plotRight / 2, layout.mainHeight / 2);
41
+ ctx.textAlign = 'left';
42
+ }
43
+ }
@@ -0,0 +1,2 @@
1
+ import type { Ctx, Frame } from './frame.ts';
2
+ export declare function drawMarkers(ctx: Ctx, f: Frame): void;