@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
@@ -0,0 +1,34 @@
1
+ export const KEY_ZOOM = 1.25;
2
+ export function handleKey(k, t) {
3
+ if (k.ctrlKey || k.metaKey || k.altKey)
4
+ return false;
5
+ switch (k.key) {
6
+ case 'ArrowLeft':
7
+ t.panBy(t.barSpacing() * (k.shiftKey ? 10 : 1));
8
+ return true;
9
+ case 'ArrowRight':
10
+ t.panBy(-t.barSpacing() * (k.shiftKey ? 10 : 1));
11
+ return true;
12
+ case '+':
13
+ case '=':
14
+ t.zoomAt(t.plotCentre(), KEY_ZOOM);
15
+ return true;
16
+ case '-':
17
+ case '_':
18
+ t.zoomAt(t.plotCentre(), 1 / KEY_ZOOM);
19
+ return true;
20
+ case 'End':
21
+ t.scrollToLive();
22
+ return true;
23
+ case 'Home':
24
+ t.goToOldest();
25
+ return true;
26
+ case 'Escape':
27
+ if (!t.crosshairVisible())
28
+ return false;
29
+ t.hideCrosshair();
30
+ return true;
31
+ default:
32
+ return false;
33
+ }
34
+ }
@@ -0,0 +1,15 @@
1
+ export interface WheelInput {
2
+ x: number;
3
+ deltaX: number;
4
+ deltaY: number;
5
+ deltaMode: number;
6
+ shiftKey: boolean;
7
+ }
8
+ export interface WheelTarget {
9
+ inPlot(x: number, y: number): boolean;
10
+ panBy(dxPx: number): void;
11
+ zoomAt(x: number, factor: number): void;
12
+ }
13
+ export declare function handleWheel(w: WheelInput & {
14
+ y: number;
15
+ }, t: WheelTarget): boolean;
@@ -0,0 +1,18 @@
1
+ const toPixels = (d, mode) => (mode === 1 ? d * 16 : mode === 2 ? d * 100 : d);
2
+ export function handleWheel(w, t) {
3
+ if (!t.inPlot(w.x, w.y))
4
+ return false;
5
+ const dx = toPixels(w.deltaX, w.deltaMode);
6
+ const dy = toPixels(w.deltaY, w.deltaMode);
7
+ if (w.shiftKey || Math.abs(dx) > Math.abs(dy)) {
8
+ const d = w.shiftKey && dx === 0 ? dy : dx;
9
+ if (d === 0)
10
+ return false;
11
+ t.panBy(-d);
12
+ return true;
13
+ }
14
+ if (dy === 0)
15
+ return false;
16
+ t.zoomAt(w.x, Math.exp(-dy * 0.002));
17
+ return true;
18
+ }
@@ -0,0 +1,17 @@
1
+ import type { CandleSeries } from '../core/series.ts';
2
+ import type { BarSource, Ohlc } from './frame.ts';
3
+ export declare class SeriesBars implements BarSource {
4
+ series: CandleSeries;
5
+ drawnClose: number | null;
6
+ constructor(series: CandleSeries);
7
+ at(i: number, out: Ohlc): void;
8
+ }
9
+ export declare class HeikinBars implements BarSource {
10
+ series: CandleSeries;
11
+ private ha;
12
+ private formingHa;
13
+ constructor(series: CandleSeries);
14
+ rebuild(): void;
15
+ refreshForming(): void;
16
+ at(i: number, out: Ohlc): void;
17
+ }
@@ -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,39 @@
1
+ import { formatSlotTime } from "../core/time.js";
2
+ import { TAG_HEIGHT, TAG_PAD_X } from "./frame.js";
3
+ export function drawCrosshair(ctx, f, c) {
4
+ if (c.index < 0 || c.index > f.series.lastIndex())
5
+ return;
6
+ const { layout, theme } = f;
7
+ const x = Math.round(f.vp.x(c.index)) + 0.5;
8
+ const inMain = c.y >= layout.mainTop && c.y <= layout.mainTop + layout.mainHeight;
9
+ const y = Math.round(c.y) + 0.5;
10
+ ctx.beginPath();
11
+ ctx.lineWidth = 1;
12
+ ctx.setLineDash([4, 4]);
13
+ ctx.strokeStyle = theme.crosshair;
14
+ ctx.moveTo(x, layout.mainTop);
15
+ ctx.lineTo(x, layout.timeTop);
16
+ if (inMain) {
17
+ ctx.moveTo(layout.plotLeft, y);
18
+ ctx.lineTo(layout.plotRight, y);
19
+ }
20
+ ctx.stroke();
21
+ ctx.setLineDash([]);
22
+ ctx.font = f.font;
23
+ ctx.textBaseline = 'middle';
24
+ ctx.fillStyle = theme.accent;
25
+ if (inMain) {
26
+ ctx.fillRect(layout.plotRight + 1, Math.round(c.y - TAG_HEIGHT / 2), layout.axisWidth - 1, TAG_HEIGHT);
27
+ ctx.fillStyle = theme.accentText;
28
+ ctx.textAlign = 'left';
29
+ ctx.fillText(f.fmt(f.scale.priceAt(c.y)), layout.plotRight + TAG_PAD_X, Math.round(c.y));
30
+ }
31
+ const text = formatSlotTime(f.series.timeAt(c.index), f.intervalMs, f.tzOffsetMs);
32
+ const w = Math.ceil(ctx.measureText(text).width) + TAG_PAD_X * 2;
33
+ const left = Math.min(layout.width - w, Math.max(0, Math.round(x - w / 2)));
34
+ ctx.fillStyle = theme.accent;
35
+ ctx.fillRect(left, layout.timeTop + 1, w, layout.timeHeight - 1);
36
+ ctx.fillStyle = theme.accentText;
37
+ ctx.textAlign = 'left';
38
+ ctx.fillText(text, left + TAG_PAD_X, layout.timeTop + layout.timeHeight / 2 + 1);
39
+ }
@@ -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,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,30 @@
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 { drawMarkers } from "./drawMarkers.js";
6
+ import { drawPriceLines, drawPriceLineTags } from "./drawPriceLines.js";
7
+ export function drawMain(ctx, f) {
8
+ const { layout, theme } = f;
9
+ ctx.fillStyle = theme.background;
10
+ ctx.fillRect(0, 0, layout.width, layout.height);
11
+ drawGrid(ctx, f);
12
+ if (f.series.count > 0) {
13
+ if (f.type === 'line' || f.type === 'area')
14
+ drawLine(ctx, f);
15
+ else
16
+ drawCandles(ctx, f);
17
+ drawPriceLines(ctx, f);
18
+ drawMarkers(ctx, f);
19
+ }
20
+ drawAxes(ctx, f);
21
+ drawPriceLineTags(ctx, f);
22
+ if (f.series.count === 0) {
23
+ ctx.font = f.font;
24
+ ctx.fillStyle = theme.textMuted;
25
+ ctx.textAlign = 'center';
26
+ ctx.textBaseline = 'middle';
27
+ ctx.fillText(f.noDataText, layout.plotRight / 2, layout.mainHeight / 2);
28
+ ctx.textAlign = 'left';
29
+ }
30
+ }
@@ -0,0 +1,2 @@
1
+ import type { Ctx, Frame } from './frame.ts';
2
+ export declare function drawMarkers(ctx: Ctx, f: Frame): void;
@@ -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,53 @@
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
+ export type Ctx = CanvasRenderingContext2D;
7
+ export interface Ohlc {
8
+ o: number;
9
+ h: number;
10
+ l: number;
11
+ c: number;
12
+ }
13
+ export interface BarSource {
14
+ at(i: number, out: Ohlc): void;
15
+ }
16
+ export interface Layout {
17
+ width: number;
18
+ height: number;
19
+ plotLeft: number;
20
+ plotRight: number;
21
+ mainTop: number;
22
+ mainHeight: number;
23
+ axisWidth: number;
24
+ timeTop: number;
25
+ timeHeight: number;
26
+ subs: {
27
+ top: number;
28
+ height: number;
29
+ }[];
30
+ }
31
+ export interface Frame {
32
+ theme: ChartTheme;
33
+ layout: Layout;
34
+ vp: Viewport;
35
+ scale: PriceScale;
36
+ series: CandleSeries;
37
+ source: BarSource;
38
+ type: ChartType;
39
+ candleStyle: CandleStyle;
40
+ range: PriceRange;
41
+ ticks: number[];
42
+ timeLabels: TimeLabel[];
43
+ lines: readonly PriceLine[];
44
+ markers: readonly Marker[];
45
+ fmt: (v: number) => string;
46
+ font: string;
47
+ noDataText: string;
48
+ intervalMs: number;
49
+ tzOffsetMs: number;
50
+ }
51
+ export declare const TAG_PAD_X = 6;
52
+ export declare const TAG_HEIGHT = 18;
53
+ 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
+ }