@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.
Files changed (47) hide show
  1. package/README.md +16 -2
  2. package/dist/chart.js +24 -0
  3. package/dist/chartState.d.ts +6 -0
  4. package/dist/chartState.js +5 -0
  5. package/dist/core/drawings.d.ts +21 -0
  6. package/dist/core/drawings.js +46 -0
  7. package/dist/core/indicatorSet.d.ts +20 -0
  8. package/dist/core/indicatorSet.js +66 -0
  9. package/dist/core/indicators.d.ts +53 -0
  10. package/dist/core/indicators.js +187 -0
  11. package/dist/core/types.d.ts +21 -0
  12. package/dist/dom/indicatorReadings.d.ts +7 -0
  13. package/dist/dom/indicatorReadings.js +26 -0
  14. package/dist/dom/legend.d.ts +5 -0
  15. package/dist/dom/legend.js +10 -1
  16. package/dist/dom/legendSync.js +8 -1
  17. package/dist/draw/controller.d.ts +36 -0
  18. package/dist/draw/controller.js +151 -0
  19. package/dist/draw/geometry.d.ts +44 -0
  20. package/dist/draw/geometry.js +118 -0
  21. package/dist/draw/mapper.d.ts +30 -0
  22. package/dist/draw/mapper.js +32 -0
  23. package/dist/draw/pointer.d.ts +17 -0
  24. package/dist/draw/pointer.js +144 -0
  25. package/dist/draw/state.d.ts +61 -0
  26. package/dist/draw/state.js +8 -0
  27. package/dist/drawApi.d.ts +20 -0
  28. package/dist/drawApi.js +59 -0
  29. package/dist/host.d.ts +3 -0
  30. package/dist/host.js +1 -0
  31. package/dist/index.d.ts +2 -2
  32. package/dist/index.js +1 -1
  33. package/dist/interact/attach.js +33 -18
  34. package/dist/render/drawCrosshair.js +6 -3
  35. package/dist/render/drawDrawings.d.ts +10 -0
  36. package/dist/render/drawDrawings.js +146 -0
  37. package/dist/render/drawIndicators.d.ts +4 -0
  38. package/dist/render/drawIndicators.js +67 -0
  39. package/dist/render/drawMain.js +13 -0
  40. package/dist/render/drawSubPanes.d.ts +2 -0
  41. package/dist/render/drawSubPanes.js +104 -0
  42. package/dist/render/frame.d.ts +4 -0
  43. package/dist/render/frameBuilder.js +12 -2
  44. package/dist/render/indicatorFrame.d.ts +17 -0
  45. package/dist/render/indicatorFrame.js +55 -0
  46. package/dist/render/paintFrame.js +3 -0
  47. package/package.json +1 -1
@@ -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
@@ -6,6 +6,7 @@ export function createHost(d) {
6
6
  return {
7
7
  root: d.root,
8
8
  state: s,
9
+ draw: d.draw,
9
10
  setCrosshair(pos) {
10
11
  if (d.isDestroyed())
11
12
  return;
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.1.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.1.0';
5
+ export const VERSION = 'zuttora-charts@0.3.0';
@@ -15,7 +15,9 @@ export function attachInteraction(host) {
15
15
  return 'outside';
16
16
  if (y >= l.timeTop)
17
17
  return 'time';
18
- return x < l.plotRight ? 'plot' : 'axis';
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
- if (gestures.down(local(e))) {
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) => gestures.move(local(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
- gestures.up(local(e));
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) => gestures.cancel(local(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 = handleKey(e, {
88
- barSpacing: () => s.vp.barSpacing,
89
- plotCentre: () => (s.vp.plotLeft + s.vp.plotRight) / 2,
90
- panBy: target.panBy,
91
- zoomAt: target.zoomAt,
92
- scrollToLive: () => host.scrollToLive(),
93
- goToOldest: () => {
94
- s.vp.setRightIndex(s.vp.plotWidth / s.vp.barSpacing);
95
- changed();
96
- },
97
- hideCrosshair: target.hideCrosshair,
98
- crosshairVisible: () => s.crosshair !== null,
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 (inMain) {
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 (inMain) {
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
+ }
@@ -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,2 @@
1
+ import type { Ctx, Frame } from './frame.ts';
2
+ export declare function drawSubPanes(ctx: Ctx, f: Frame): void;
@@ -0,0 +1,104 @@
1
+ import { priceFormatter } from "../core/format.js";
2
+ import { indicatorTitle } from "../core/indicators.js";
3
+ import { bodyWidth } from "./drawCandles.js";
4
+ import { tracePath } from "./drawIndicators.js";
5
+ import { TAG_PAD_X } from "./frame.js";
6
+ import { RSI_GUIDES, subTicks, subY } from "./indicatorFrame.js";
7
+ const HIST_ALPHA = 0.55;
8
+ function drawRsi(ctx, f, p, from, to) {
9
+ const { layout } = f;
10
+ ctx.beginPath();
11
+ ctx.lineWidth = 1;
12
+ ctx.setLineDash([3, 3]);
13
+ for (const g of RSI_GUIDES) {
14
+ const y = Math.round(subY(p, g)) + 0.5;
15
+ ctx.moveTo(layout.plotLeft, y);
16
+ ctx.lineTo(layout.plotRight, y);
17
+ }
18
+ ctx.strokeStyle = f.theme.textMuted;
19
+ ctx.globalAlpha = 0.5;
20
+ ctx.stroke();
21
+ ctx.globalAlpha = 1;
22
+ ctx.setLineDash([]);
23
+ ctx.lineWidth = 1.5;
24
+ ctx.strokeStyle = p.entry.spec.color;
25
+ tracePath(ctx, f, p.entry.calc.lines.rsi, from, to, (v) => subY(p, v));
26
+ ctx.stroke();
27
+ }
28
+ function drawMacd(ctx, f, p, from, to) {
29
+ const spec = p.entry.spec;
30
+ if (spec.type !== 'macd')
31
+ return;
32
+ const l = p.entry.calc.lines;
33
+ const zero = Math.round(subY(p, 0));
34
+ const bw = bodyWidth(f.vp.barSpacing);
35
+ const half = (bw - 1) / 2;
36
+ ctx.globalAlpha = HIST_ALPHA;
37
+ for (const up of [true, false]) {
38
+ ctx.beginPath();
39
+ for (let i = from; i <= to; i++) {
40
+ const h = l.hist[i];
41
+ if (h === null || h === undefined || h >= 0 !== up)
42
+ continue;
43
+ const y = Math.round(subY(p, h));
44
+ ctx.rect(Math.round(f.vp.x(i)) - half, Math.min(y, zero), bw, Math.max(1, Math.abs(y - zero)));
45
+ }
46
+ ctx.fillStyle = up ? f.theme.up : f.theme.down;
47
+ ctx.fill();
48
+ }
49
+ ctx.globalAlpha = 1;
50
+ ctx.beginPath();
51
+ ctx.lineWidth = 1;
52
+ ctx.moveTo(f.layout.plotLeft, zero + 0.5);
53
+ ctx.lineTo(f.layout.plotRight, zero + 0.5);
54
+ ctx.strokeStyle = f.theme.grid;
55
+ ctx.stroke();
56
+ ctx.lineWidth = 1.5;
57
+ for (const [values, color] of [
58
+ [l.macd, spec.color],
59
+ [l.signal, spec.signalColor],
60
+ ]) {
61
+ ctx.strokeStyle = color;
62
+ tracePath(ctx, f, values, from, to, (v) => subY(p, v));
63
+ ctx.stroke();
64
+ }
65
+ }
66
+ export function drawSubPanes(ctx, f) {
67
+ const r = f.vp.visibleRange();
68
+ const { layout, theme } = f;
69
+ for (const p of f.subs) {
70
+ ctx.beginPath();
71
+ ctx.lineWidth = 1;
72
+ ctx.moveTo(0, p.top + 0.5);
73
+ ctx.lineTo(layout.width, p.top + 0.5);
74
+ ctx.strokeStyle = theme.grid;
75
+ ctx.stroke();
76
+ if (r) {
77
+ ctx.save();
78
+ ctx.beginPath();
79
+ ctx.rect(layout.plotLeft, p.top + 1, layout.plotRight - layout.plotLeft, p.height - 1);
80
+ ctx.clip();
81
+ const from = Math.max(0, r.from - 1);
82
+ const to = Math.min(f.series.lastIndex(), r.to + 1);
83
+ if (p.entry.spec.type === 'rsi')
84
+ drawRsi(ctx, f, p, from, to);
85
+ else
86
+ drawMacd(ctx, f, p, from, to);
87
+ ctx.restore();
88
+ }
89
+ ctx.font = f.font;
90
+ ctx.fillStyle = theme.textMuted;
91
+ ctx.textBaseline = 'top';
92
+ ctx.textAlign = 'left';
93
+ ctx.fillText(indicatorTitle(p.entry.spec), 8, p.top + 4);
94
+ ctx.fillStyle = theme.text;
95
+ ctx.textBaseline = 'middle';
96
+ const fmt = p.entry.spec.type === 'rsi' ? priceFormatter(0, 'en-IN') : f.fmt;
97
+ for (const v of subTicks(p, 2)) {
98
+ const y = subY(p, v);
99
+ if (y < p.top + 8 || y > p.top + p.height - 8)
100
+ continue;
101
+ ctx.fillText(fmt(v), layout.plotRight + TAG_PAD_X, Math.round(y));
102
+ }
103
+ }
104
+ }
@@ -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, 0);
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 target = autoRange(extremes(src, vis.from, vis.to), [], s.lines, o.minMove);
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,