@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.
- package/LICENSE +21 -0
- package/README.md +62 -2
- package/dist/chart.d.ts +8 -0
- package/dist/chart.js +238 -0
- package/dist/chartState.d.ts +47 -0
- package/dist/chartState.js +64 -0
- package/dist/core/drawings.d.ts +21 -0
- package/dist/core/drawings.js +46 -0
- package/dist/core/emitter.d.ts +8 -0
- package/dist/core/emitter.js +26 -0
- package/dist/core/format.d.ts +9 -0
- package/dist/core/format.js +35 -0
- package/dist/core/heikinAshi.d.ts +2 -0
- package/dist/core/heikinAshi.js +14 -0
- package/dist/core/indicatorSet.d.ts +20 -0
- package/dist/core/indicatorSet.js +66 -0
- package/dist/core/indicators.d.ts +53 -0
- package/dist/core/indicators.js +187 -0
- package/dist/core/priceScale.d.ts +22 -0
- package/dist/core/priceScale.js +92 -0
- package/dist/core/series.d.ts +41 -0
- package/dist/core/series.js +221 -0
- package/dist/core/time.d.ts +25 -0
- package/dist/core/time.js +44 -0
- package/dist/core/timeLabels.d.ts +11 -0
- package/dist/core/timeLabels.js +52 -0
- package/dist/core/types.d.ts +134 -0
- package/dist/core/types.js +1 -0
- package/dist/core/viewport.d.ts +41 -0
- package/dist/core/viewport.js +116 -0
- package/dist/core/warn.d.ts +1 -0
- package/dist/core/warn.js +3 -0
- package/dist/createChart.d.ts +2 -0
- package/dist/createChart.js +7 -0
- package/dist/dom/environment.d.ts +11 -0
- package/dist/dom/environment.js +49 -0
- package/dist/dom/indicatorReadings.d.ts +7 -0
- package/dist/dom/indicatorReadings.js +26 -0
- package/dist/dom/legend.d.ts +19 -0
- package/dist/dom/legend.js +62 -0
- package/dist/dom/legendSync.d.ts +5 -0
- package/dist/dom/legendSync.js +30 -0
- package/dist/dom/root.d.ts +9 -0
- package/dist/dom/root.js +19 -0
- package/dist/draw/controller.d.ts +36 -0
- package/dist/draw/controller.js +151 -0
- package/dist/draw/geometry.d.ts +44 -0
- package/dist/draw/geometry.js +118 -0
- package/dist/draw/mapper.d.ts +30 -0
- package/dist/draw/mapper.js +32 -0
- package/dist/draw/pointer.d.ts +17 -0
- package/dist/draw/pointer.js +144 -0
- package/dist/draw/state.d.ts +61 -0
- package/dist/draw/state.js +8 -0
- package/dist/drawApi.d.ts +20 -0
- package/dist/drawApi.js +59 -0
- package/dist/host.d.ts +35 -0
- package/dist/host.js +63 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +5 -0
- package/dist/interact/attach.d.ts +2 -0
- package/dist/interact/attach.js +142 -0
- package/dist/interact/gestures.d.ts +47 -0
- package/dist/interact/gestures.js +179 -0
- package/dist/interact/keyboard.d.ts +19 -0
- package/dist/interact/keyboard.js +34 -0
- package/dist/interact/wheel.d.ts +15 -0
- package/dist/interact/wheel.js +18 -0
- package/dist/render/bars.d.ts +17 -0
- package/dist/render/bars.js +55 -0
- package/dist/render/canvas.d.ts +2 -0
- package/dist/render/canvas.js +13 -0
- package/dist/render/drawAxes.d.ts +2 -0
- package/dist/render/drawAxes.js +34 -0
- package/dist/render/drawCandles.d.ts +3 -0
- package/dist/render/drawCandles.js +68 -0
- package/dist/render/drawCrosshair.d.ts +7 -0
- package/dist/render/drawCrosshair.js +42 -0
- package/dist/render/drawDrawings.d.ts +10 -0
- package/dist/render/drawDrawings.js +146 -0
- package/dist/render/drawGrid.d.ts +2 -0
- package/dist/render/drawGrid.js +22 -0
- package/dist/render/drawIndicators.d.ts +4 -0
- package/dist/render/drawIndicators.js +67 -0
- package/dist/render/drawLastPrice.d.ts +10 -0
- package/dist/render/drawLastPrice.js +53 -0
- package/dist/render/drawLine.d.ts +3 -0
- package/dist/render/drawLine.js +43 -0
- package/dist/render/drawMain.d.ts +2 -0
- package/dist/render/drawMain.js +43 -0
- package/dist/render/drawMarkers.d.ts +2 -0
- package/dist/render/drawMarkers.js +39 -0
- package/dist/render/drawPriceLines.d.ts +4 -0
- package/dist/render/drawPriceLines.js +60 -0
- package/dist/render/drawSubPanes.d.ts +2 -0
- package/dist/render/drawSubPanes.js +104 -0
- package/dist/render/frame.d.ts +57 -0
- package/dist/render/frame.js +5 -0
- package/dist/render/frameBuilder.d.ts +12 -0
- package/dist/render/frameBuilder.js +129 -0
- package/dist/render/indicatorFrame.d.ts +17 -0
- package/dist/render/indicatorFrame.js +55 -0
- package/dist/render/layout.d.ts +4 -0
- package/dist/render/layout.js +26 -0
- package/dist/render/paintFrame.d.ts +11 -0
- package/dist/render/paintFrame.js +33 -0
- package/dist/render/scheduler.d.ts +17 -0
- package/dist/render/scheduler.js +38 -0
- package/dist/render/tween.d.ts +12 -0
- package/dist/render/tween.js +38 -0
- package/dist/theme.d.ts +2 -0
- package/dist/theme.js +16 -0
- 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,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,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,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,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,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,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,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
|
+
}
|