@zuttora/charts 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -2
- package/dist/chart.js +24 -0
- package/dist/chartState.d.ts +6 -0
- package/dist/chartState.js +5 -0
- package/dist/core/drawings.d.ts +21 -0
- package/dist/core/drawings.js +46 -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/types.d.ts +21 -0
- package/dist/dom/indicatorReadings.d.ts +7 -0
- package/dist/dom/indicatorReadings.js +26 -0
- package/dist/dom/legend.d.ts +5 -0
- package/dist/dom/legend.js +10 -1
- package/dist/dom/legendSync.js +8 -1
- 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 +3 -0
- package/dist/host.js +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/interact/attach.js +33 -18
- package/dist/render/drawCrosshair.js +6 -3
- package/dist/render/drawDrawings.d.ts +10 -0
- package/dist/render/drawDrawings.js +146 -0
- package/dist/render/drawIndicators.d.ts +4 -0
- package/dist/render/drawIndicators.js +67 -0
- package/dist/render/drawMain.js +13 -0
- package/dist/render/drawSubPanes.d.ts +2 -0
- package/dist/render/drawSubPanes.js +104 -0
- package/dist/render/frame.d.ts +4 -0
- package/dist/render/frameBuilder.js +12 -2
- package/dist/render/indicatorFrame.d.ts +17 -0
- package/dist/render/indicatorFrame.js +55 -0
- package/dist/render/paintFrame.js +3 -0
- package/package.json +1 -1
package/dist/drawApi.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { DrawController } from "./draw/controller.js";
|
|
2
|
+
import { mapperOf } from "./render/drawDrawings.js";
|
|
3
|
+
export function createDrawApi(d) {
|
|
4
|
+
const { s } = d;
|
|
5
|
+
const syncTouch = () => d.root.style.setProperty('touch-action', s.draw.tool || s.draw.selectedId ? 'none' : 'pan-y');
|
|
6
|
+
const controller = new DrawController({
|
|
7
|
+
drawings: () => s.drawings,
|
|
8
|
+
store: (next) => {
|
|
9
|
+
s.drawings = next;
|
|
10
|
+
},
|
|
11
|
+
mapper: () => {
|
|
12
|
+
const f = d.lastFrame();
|
|
13
|
+
return f ? mapperOf(f) : null;
|
|
14
|
+
},
|
|
15
|
+
fontSize: () => s.options.theme.fontSize,
|
|
16
|
+
now: d.now,
|
|
17
|
+
redraw: () => d.invalidate('overlay'),
|
|
18
|
+
changed: (list) => d.events.emit('drawingsChange', list),
|
|
19
|
+
selection: (id) => {
|
|
20
|
+
syncTouch();
|
|
21
|
+
d.events.emit('selectionChange', id);
|
|
22
|
+
},
|
|
23
|
+
progress: (info) => {
|
|
24
|
+
syncTouch();
|
|
25
|
+
d.events.emit('drawProgress', info);
|
|
26
|
+
},
|
|
27
|
+
edit: (drawing) => d.events.emit('drawingEdit', drawing),
|
|
28
|
+
}, s.draw);
|
|
29
|
+
const methods = {
|
|
30
|
+
setDrawingTool(tool) {
|
|
31
|
+
if (!d.isDestroyed())
|
|
32
|
+
controller.setTool(tool);
|
|
33
|
+
},
|
|
34
|
+
setDrawings(list) {
|
|
35
|
+
if (!d.isDestroyed())
|
|
36
|
+
controller.replace(list);
|
|
37
|
+
},
|
|
38
|
+
getDrawings() {
|
|
39
|
+
return s.drawings.map((x) => ({ ...x, points: x.points.map((p) => ({ ...p })) }));
|
|
40
|
+
},
|
|
41
|
+
updateDrawing(id, patch) {
|
|
42
|
+
if (!d.isDestroyed())
|
|
43
|
+
controller.update(id, patch);
|
|
44
|
+
},
|
|
45
|
+
deleteSelected() {
|
|
46
|
+
if (!d.isDestroyed())
|
|
47
|
+
controller.deleteSelected();
|
|
48
|
+
},
|
|
49
|
+
clearDrawings() {
|
|
50
|
+
if (!d.isDestroyed())
|
|
51
|
+
controller.clear();
|
|
52
|
+
},
|
|
53
|
+
setDrawingColor(color) {
|
|
54
|
+
if (!d.isDestroyed())
|
|
55
|
+
controller.setColor(color);
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
return { controller, methods };
|
|
59
|
+
}
|
package/dist/host.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { ChartEvents } from './core/types.ts';
|
|
2
2
|
import type { Emitter } from './core/emitter.ts';
|
|
3
3
|
import type { ChartState } from './chartState.ts';
|
|
4
|
+
import type { DrawController } from './draw/controller.ts';
|
|
4
5
|
import type { Frame, Layout } from './render/frame.ts';
|
|
5
6
|
export interface ChartHost {
|
|
6
7
|
root: HTMLElement;
|
|
7
8
|
state: ChartState;
|
|
9
|
+
draw: DrawController;
|
|
8
10
|
setCrosshair(pos: {
|
|
9
11
|
x: number;
|
|
10
12
|
y: number;
|
|
@@ -19,6 +21,7 @@ export interface ChartHost {
|
|
|
19
21
|
export interface HostDeps {
|
|
20
22
|
root: HTMLElement;
|
|
21
23
|
state: ChartState;
|
|
24
|
+
draw: DrawController;
|
|
22
25
|
events: Emitter<ChartEvents>;
|
|
23
26
|
invalidate(layer: 'main' | 'overlay'): void;
|
|
24
27
|
invalidateAll(): void;
|
package/dist/host.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export type { Candle, CandleStyle, Chart, ChartEvents, ChartLabels, ChartOptions, ChartTheme, ChartType, Marker, PriceLine, Time, } from './core/types.ts';
|
|
1
|
+
export type { Candle, CandleStyle, Chart, Drawing, DrawingKind, DrawingTool, ChartEvents, ChartLabels, ChartOptions, ChartTheme, ChartType, IndicatorSpec, Marker, Point, PriceLine, Time, } from './core/types.ts';
|
|
2
2
|
export { createChart } from './createChart.ts';
|
|
3
3
|
export { bucketStart, IST_OFFSET_MS, INTERVAL_MS } from './core/time.ts';
|
|
4
4
|
export type { IntervalId } from './core/time.ts';
|
|
5
5
|
export { heikinAshi } from './core/heikinAshi.ts';
|
|
6
6
|
export { zuttoraTheme } from './theme.ts';
|
|
7
|
-
export declare const VERSION = "zuttora-charts@0.
|
|
7
|
+
export declare const VERSION = "zuttora-charts@0.3.0";
|
package/dist/index.js
CHANGED
|
@@ -2,4 +2,4 @@ export { createChart } from "./createChart.js";
|
|
|
2
2
|
export { bucketStart, IST_OFFSET_MS, INTERVAL_MS } from "./core/time.js";
|
|
3
3
|
export { heikinAshi } from "./core/heikinAshi.js";
|
|
4
4
|
export { zuttoraTheme } from "./theme.js";
|
|
5
|
-
export const VERSION = 'zuttora-charts@0.
|
|
5
|
+
export const VERSION = 'zuttora-charts@0.3.0';
|
package/dist/interact/attach.js
CHANGED
|
@@ -15,7 +15,9 @@ export function attachInteraction(host) {
|
|
|
15
15
|
return 'outside';
|
|
16
16
|
if (y >= l.timeTop)
|
|
17
17
|
return 'time';
|
|
18
|
-
|
|
18
|
+
if (x >= l.plotRight)
|
|
19
|
+
return y < l.mainTop + l.mainHeight ? 'axis' : 'outside';
|
|
20
|
+
return 'plot';
|
|
19
21
|
},
|
|
20
22
|
panBy(dx) {
|
|
21
23
|
s.vp.panBy(dx);
|
|
@@ -50,7 +52,8 @@ export function attachInteraction(host) {
|
|
|
50
52
|
return { id: e.pointerId, type: e.pointerType, x: e.clientX - r.left, y: e.clientY - r.top, button: e.button };
|
|
51
53
|
};
|
|
52
54
|
const onDown = (e) => {
|
|
53
|
-
|
|
55
|
+
const input = local(e);
|
|
56
|
+
if (host.draw.down(input) || gestures.down(input)) {
|
|
54
57
|
try {
|
|
55
58
|
root.setPointerCapture(e.pointerId);
|
|
56
59
|
}
|
|
@@ -59,16 +62,27 @@ export function attachInteraction(host) {
|
|
|
59
62
|
root.focus({ preventScroll: true });
|
|
60
63
|
}
|
|
61
64
|
};
|
|
62
|
-
const onMove = (e) =>
|
|
65
|
+
const onMove = (e) => {
|
|
66
|
+
const input = local(e);
|
|
67
|
+
if (host.draw.move(input))
|
|
68
|
+
return;
|
|
69
|
+
root.style.cursor = host.draw.cursor(input.x, input.y);
|
|
70
|
+
gestures.move(input);
|
|
71
|
+
};
|
|
63
72
|
const onUp = (e) => {
|
|
64
|
-
|
|
73
|
+
const input = local(e);
|
|
74
|
+
if (!host.draw.up(input))
|
|
75
|
+
gestures.up(input);
|
|
65
76
|
try {
|
|
66
77
|
root.releasePointerCapture(e.pointerId);
|
|
67
78
|
}
|
|
68
79
|
catch {
|
|
69
80
|
}
|
|
70
81
|
};
|
|
71
|
-
const onCancel = (e) =>
|
|
82
|
+
const onCancel = (e) => {
|
|
83
|
+
host.draw.cancel(local(e));
|
|
84
|
+
gestures.cancel(local(e));
|
|
85
|
+
};
|
|
72
86
|
const onLeave = (e) => {
|
|
73
87
|
if (e.pointerType !== 'touch')
|
|
74
88
|
gestures.leave();
|
|
@@ -84,19 +98,20 @@ export function attachInteraction(host) {
|
|
|
84
98
|
e.preventDefault();
|
|
85
99
|
};
|
|
86
100
|
const onKey = (e) => {
|
|
87
|
-
const used =
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
101
|
+
const used = host.draw.key(e.key) ||
|
|
102
|
+
handleKey(e, {
|
|
103
|
+
barSpacing: () => s.vp.barSpacing,
|
|
104
|
+
plotCentre: () => (s.vp.plotLeft + s.vp.plotRight) / 2,
|
|
105
|
+
panBy: target.panBy,
|
|
106
|
+
zoomAt: target.zoomAt,
|
|
107
|
+
scrollToLive: () => host.scrollToLive(),
|
|
108
|
+
goToOldest: () => {
|
|
109
|
+
s.vp.setRightIndex(s.vp.plotWidth / s.vp.barSpacing);
|
|
110
|
+
changed();
|
|
111
|
+
},
|
|
112
|
+
hideCrosshair: target.hideCrosshair,
|
|
113
|
+
crosshairVisible: () => s.crosshair !== null,
|
|
114
|
+
});
|
|
100
115
|
if (used) {
|
|
101
116
|
e.preventDefault();
|
|
102
117
|
e.stopPropagation();
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { formatSlotTime } from "../core/time.js";
|
|
2
2
|
import { TAG_HEIGHT, TAG_PAD_X } from "./frame.js";
|
|
3
|
+
import { subValueAt } from "./indicatorFrame.js";
|
|
3
4
|
export function drawCrosshair(ctx, f, c) {
|
|
4
5
|
if (c.index < 0 || c.index > f.series.lastIndex())
|
|
5
6
|
return;
|
|
6
7
|
const { layout, theme } = f;
|
|
7
8
|
const x = Math.round(f.vp.x(c.index)) + 0.5;
|
|
8
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;
|
|
9
12
|
const y = Math.round(c.y) + 0.5;
|
|
10
13
|
ctx.beginPath();
|
|
11
14
|
ctx.lineWidth = 1;
|
|
@@ -13,7 +16,7 @@ export function drawCrosshair(ctx, f, c) {
|
|
|
13
16
|
ctx.strokeStyle = theme.crosshair;
|
|
14
17
|
ctx.moveTo(x, layout.mainTop);
|
|
15
18
|
ctx.lineTo(x, layout.timeTop);
|
|
16
|
-
if (
|
|
19
|
+
if (guide) {
|
|
17
20
|
ctx.moveTo(layout.plotLeft, y);
|
|
18
21
|
ctx.lineTo(layout.plotRight, y);
|
|
19
22
|
}
|
|
@@ -22,11 +25,11 @@ export function drawCrosshair(ctx, f, c) {
|
|
|
22
25
|
ctx.font = f.font;
|
|
23
26
|
ctx.textBaseline = 'middle';
|
|
24
27
|
ctx.fillStyle = theme.accent;
|
|
25
|
-
if (
|
|
28
|
+
if (guide) {
|
|
26
29
|
ctx.fillRect(layout.plotRight + 1, Math.round(c.y - TAG_HEIGHT / 2), layout.axisWidth - 1, TAG_HEIGHT);
|
|
27
30
|
ctx.fillStyle = theme.accentText;
|
|
28
31
|
ctx.textAlign = 'left';
|
|
29
|
-
ctx.fillText(f.fmt(f.scale.priceAt(c.y)), layout.plotRight + TAG_PAD_X, Math.round(c.y));
|
|
32
|
+
ctx.fillText(f.fmt(sub ? subValueAt(sub, c.y) : f.scale.priceAt(c.y)), layout.plotRight + TAG_PAD_X, Math.round(c.y));
|
|
30
33
|
}
|
|
31
34
|
const text = formatSlotTime(f.series.timeAt(c.index), f.intervalMs, f.tzOffsetMs);
|
|
32
35
|
const w = Math.ceil(ctx.measureText(text).width) + TAG_PAD_X * 2;
|
|
@@ -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,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
|
+
}
|
package/dist/render/drawMain.js
CHANGED
|
@@ -2,22 +2,35 @@ import { drawAxes } from "./drawAxes.js";
|
|
|
2
2
|
import { drawCandles } from "./drawCandles.js";
|
|
3
3
|
import { drawGrid } from "./drawGrid.js";
|
|
4
4
|
import { drawLine } from "./drawLine.js";
|
|
5
|
+
import { drawOverlays } from "./drawIndicators.js";
|
|
5
6
|
import { drawMarkers } from "./drawMarkers.js";
|
|
6
7
|
import { drawPriceLines, drawPriceLineTags } from "./drawPriceLines.js";
|
|
8
|
+
import { drawSubPanes } from "./drawSubPanes.js";
|
|
7
9
|
export function drawMain(ctx, f) {
|
|
8
10
|
const { layout, theme } = f;
|
|
9
11
|
ctx.fillStyle = theme.background;
|
|
10
12
|
ctx.fillRect(0, 0, layout.width, layout.height);
|
|
11
13
|
drawGrid(ctx, f);
|
|
12
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
|
+
}
|
|
13
22
|
if (f.type === 'line' || f.type === 'area')
|
|
14
23
|
drawLine(ctx, f);
|
|
15
24
|
else
|
|
16
25
|
drawCandles(ctx, f);
|
|
26
|
+
drawOverlays(ctx, f);
|
|
17
27
|
drawPriceLines(ctx, f);
|
|
18
28
|
drawMarkers(ctx, f);
|
|
29
|
+
if (clip)
|
|
30
|
+
ctx.restore();
|
|
19
31
|
}
|
|
20
32
|
drawAxes(ctx, f);
|
|
33
|
+
drawSubPanes(ctx, f);
|
|
21
34
|
drawPriceLineTags(ctx, f);
|
|
22
35
|
if (f.series.count === 0) {
|
|
23
36
|
ctx.font = f.font;
|
|
@@ -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
|
+
}
|
package/dist/render/frame.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ import type { PriceRange, PriceScale } from '../core/priceScale.ts';
|
|
|
3
3
|
import type { TimeLabel } from '../core/timeLabels.ts';
|
|
4
4
|
import type { CandleStyle, ChartTheme, ChartType, Marker, PriceLine } from '../core/types.ts';
|
|
5
5
|
import type { Viewport } from '../core/viewport.ts';
|
|
6
|
+
import type { IndicatorEntry } from '../core/indicatorSet.ts';
|
|
7
|
+
import type { SubPane } from './indicatorFrame.ts';
|
|
6
8
|
export type Ctx = CanvasRenderingContext2D;
|
|
7
9
|
export interface Ohlc {
|
|
8
10
|
o: number;
|
|
@@ -42,6 +44,8 @@ export interface Frame {
|
|
|
42
44
|
timeLabels: TimeLabel[];
|
|
43
45
|
lines: readonly PriceLine[];
|
|
44
46
|
markers: readonly Marker[];
|
|
47
|
+
overlays: readonly IndicatorEntry[];
|
|
48
|
+
subs: readonly SubPane[];
|
|
45
49
|
fmt: (v: number) => string;
|
|
46
50
|
font: string;
|
|
47
51
|
noDataText: string;
|
|
@@ -3,6 +3,7 @@ import { priceFormatter } from "../core/format.js";
|
|
|
3
3
|
import { INTERVAL_MS } from "../core/time.js";
|
|
4
4
|
import { pickTimeLabels } from "../core/timeLabels.js";
|
|
5
5
|
import { fontOf } from "./frame.js";
|
|
6
|
+
import { buildSubPanes, mainExtremes } from "./indicatorFrame.js";
|
|
6
7
|
import { computeLayout } from "./layout.js";
|
|
7
8
|
export const RANGE_TWEEN_MS = 150;
|
|
8
9
|
export const CLOSE_TWEEN_MS = 180;
|
|
@@ -38,7 +39,7 @@ export function buildFrame(s, ctx, animNow) {
|
|
|
38
39
|
const theme = o.theme;
|
|
39
40
|
const reduced = o.reducedMotion;
|
|
40
41
|
const font = fontOf(theme);
|
|
41
|
-
const layout = computeLayout(s.width, s.height, s.axisWidth,
|
|
42
|
+
const layout = computeLayout(s.width, s.height, s.axisWidth, s.indicators.subCount);
|
|
42
43
|
s.vp.setPlot(layout.plotLeft, layout.plotRight);
|
|
43
44
|
s.scale.setPane(layout.mainTop, layout.mainHeight);
|
|
44
45
|
let animating = false;
|
|
@@ -54,12 +55,19 @@ export function buildFrame(s, ctx, animNow) {
|
|
|
54
55
|
if (s.closeTween.active(animNow))
|
|
55
56
|
animating = true;
|
|
56
57
|
const vis = s.vp.visibleRange();
|
|
58
|
+
const overlays = s.indicators.main();
|
|
57
59
|
if (s.scale.manual) {
|
|
58
60
|
s.rangeMin.jump(s.scale.min);
|
|
59
61
|
s.rangeMax.jump(s.scale.max);
|
|
60
62
|
}
|
|
61
63
|
else if (vis) {
|
|
62
|
-
const
|
|
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);
|
|
63
71
|
if (!s.rangeReady || reduced) {
|
|
64
72
|
s.rangeMin.jump(target.min);
|
|
65
73
|
s.rangeMax.jump(target.max);
|
|
@@ -109,6 +117,8 @@ export function buildFrame(s, ctx, animNow) {
|
|
|
109
117
|
timeLabels,
|
|
110
118
|
lines: s.lines,
|
|
111
119
|
markers: s.markers,
|
|
120
|
+
overlays,
|
|
121
|
+
subs: vis ? buildSubPanes(s.indicators.subs(), layout, vis.from, vis.to) : [],
|
|
112
122
|
fmt,
|
|
113
123
|
font,
|
|
114
124
|
noDataText: o.labels.noData,
|