@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
@@ -1,4 +1,5 @@
1
1
  import { priceFormatter } from "../core/format.js";
2
+ import { indicatorReadings } from "./indicatorReadings.js";
2
3
  export function crosshairCandle(s) {
3
4
  const c = s.crosshair;
4
5
  if (!c || c.index < 0 || c.index > s.series.lastIndex())
@@ -19,5 +20,11 @@ export function syncLegend(s, legend) {
19
20
  if (m.tooltip && s.series.indexAtTime(m.t) === idx)
20
21
  notes.push(m.tooltip);
21
22
  }
22
- legend.update({ candle, notes, caption: o.type === 'heikinAshi' ? o.labels.heikinAshi : null }, o.labels, o.theme, o.priceDecimals, priceFormatter(o.priceDecimals, o.locale), o.locale);
23
+ const fmt = priceFormatter(o.priceDecimals, o.locale);
24
+ legend.update({
25
+ candle,
26
+ notes,
27
+ caption: o.type === 'heikinAshi' ? o.labels.heikinAshi : null,
28
+ indicators: candle ? indicatorReadings(s.indicators.entries, idx, fmt) : [],
29
+ }, o.labels, o.theme, o.priceDecimals, fmt, o.locale);
23
30
  }
@@ -0,0 +1,36 @@
1
+ import type { Drawing, DrawingTool, Point } from '../core/drawings.ts';
2
+ import type { PointerInput } from '../interact/gestures.ts';
3
+ import type { Hit } from './geometry.ts';
4
+ import type { Mapper } from './mapper.ts';
5
+ import type { DrawDeps, DrawState } from './state.ts';
6
+ export { createDrawState } from './state.ts';
7
+ export type { DrawDeps, DrawProgress, DrawState } from './state.ts';
8
+ export declare class DrawController {
9
+ readonly state: DrawState;
10
+ readonly deps: DrawDeps;
11
+ private pointer;
12
+ constructor(deps: DrawDeps, state: DrawState);
13
+ down: (p: PointerInput) => boolean;
14
+ move: (p: PointerInput) => boolean;
15
+ up: (p: PointerInput) => boolean;
16
+ cancel: (p: PointerInput) => void;
17
+ cursor: (x: number, y: number) => string;
18
+ selected(): Drawing | undefined;
19
+ setTool(tool: DrawingTool | null): void;
20
+ setColor(color: string): void;
21
+ replace(list: readonly unknown[]): void;
22
+ update(id: string, patch: {
23
+ text?: string;
24
+ color?: string;
25
+ }): void;
26
+ deleteSelected(): boolean;
27
+ clear(): void;
28
+ key(key: string): boolean;
29
+ finish(last: Point): void;
30
+ select(id: string | null): void;
31
+ edit(id: string, fn: (d: Drawing) => Drawing): void;
32
+ topHit(x: number, y: number, tol: number, m: Mapper): {
33
+ drawing: Drawing;
34
+ hit: Hit;
35
+ } | null;
36
+ }
@@ -0,0 +1,151 @@
1
+ import { MAX_DRAWINGS, newDrawingId, pointsNeeded, sanitizeDrawings } from "../core/drawings.js";
2
+ import { hitTest } from "./geometry.js";
3
+ import { DrawPointer } from "./pointer.js";
4
+ import { DEFAULT_NOTE } from "./state.js";
5
+ export { createDrawState } from "./state.js";
6
+ export class DrawController {
7
+ constructor(deps, state) {
8
+ this.down = (p) => this.pointer.down(p);
9
+ this.move = (p) => this.pointer.move(p);
10
+ this.up = (p) => this.pointer.up(p);
11
+ this.cancel = (p) => this.pointer.cancel(p);
12
+ this.cursor = (x, y) => this.pointer.cursor(x, y);
13
+ this.deps = deps;
14
+ this.state = state;
15
+ this.pointer = new DrawPointer(this);
16
+ }
17
+ selected() {
18
+ return this.deps.drawings().find((d) => d.id === this.state.selectedId);
19
+ }
20
+ setTool(tool) {
21
+ const s = this.state;
22
+ s.tool = tool;
23
+ s.pending = [];
24
+ s.preview = null;
25
+ s.measure = null;
26
+ this.pointer.reset();
27
+ if (tool !== null)
28
+ this.select(null);
29
+ this.deps.progress(tool === null ? null : { tool, placed: 0, needed: pointsNeeded(tool) });
30
+ this.deps.redraw();
31
+ }
32
+ setColor(color) {
33
+ this.state.color = color;
34
+ const d = this.selected();
35
+ if (d)
36
+ this.update(d.id, { color });
37
+ }
38
+ replace(list) {
39
+ this.deps.store(sanitizeDrawings(list));
40
+ this.setTool(null);
41
+ this.select(null);
42
+ }
43
+ update(id, patch) {
44
+ const list = this.deps.drawings();
45
+ if (!list.some((d) => d.id === id))
46
+ return;
47
+ const next = list.map((d) => (d.id === id ? { ...d, ...patch } : d));
48
+ this.deps.store(next);
49
+ this.deps.changed(next);
50
+ this.deps.redraw();
51
+ }
52
+ deleteSelected() {
53
+ const id = this.state.selectedId;
54
+ if (id === null)
55
+ return false;
56
+ const next = this.deps.drawings().filter((d) => d.id !== id);
57
+ this.deps.store(next);
58
+ this.select(null);
59
+ this.deps.changed(next);
60
+ this.deps.redraw();
61
+ return true;
62
+ }
63
+ clear() {
64
+ this.deps.store([]);
65
+ this.select(null);
66
+ this.deps.changed([]);
67
+ this.deps.redraw();
68
+ }
69
+ key(key) {
70
+ const s = this.state;
71
+ if (key === 'Escape') {
72
+ if (s.pending.length > 0 && s.tool) {
73
+ this.setTool(s.tool);
74
+ return true;
75
+ }
76
+ if (s.tool !== null) {
77
+ this.setTool(null);
78
+ return true;
79
+ }
80
+ if (s.measure !== null) {
81
+ s.measure = null;
82
+ this.deps.redraw();
83
+ return true;
84
+ }
85
+ if (s.selectedId !== null) {
86
+ this.select(null);
87
+ this.deps.redraw();
88
+ return true;
89
+ }
90
+ return false;
91
+ }
92
+ if (key === 'Delete' || key === 'Backspace')
93
+ return this.deleteSelected();
94
+ return false;
95
+ }
96
+ finish(last) {
97
+ const s = this.state;
98
+ const tool = s.tool;
99
+ if (!tool)
100
+ return;
101
+ const points = [...s.pending, last];
102
+ s.pending = [];
103
+ s.preview = null;
104
+ if (tool === 'measure') {
105
+ s.measure = { a: points[0], b: points[1] ?? last };
106
+ this.deps.progress({ tool, placed: 0, needed: 2 });
107
+ return;
108
+ }
109
+ const list = this.deps.drawings();
110
+ if (list.length >= MAX_DRAWINGS) {
111
+ this.setTool(null);
112
+ return;
113
+ }
114
+ const drawing = { id: newDrawingId(), type: tool, points, color: s.color };
115
+ if (tool === 'note')
116
+ drawing.text = DEFAULT_NOTE;
117
+ const next = [...list, drawing];
118
+ this.deps.store(next);
119
+ s.tool = null;
120
+ this.deps.progress(null);
121
+ this.select(drawing.id);
122
+ this.deps.changed(next);
123
+ if (tool === 'note')
124
+ this.deps.edit(drawing);
125
+ }
126
+ select(id) {
127
+ if (this.state.selectedId === id)
128
+ return;
129
+ this.state.selectedId = id;
130
+ this.deps.selection(id);
131
+ }
132
+ edit(id, fn) {
133
+ this.deps.store(this.deps.drawings().map((d) => (d.id === id ? fn(d) : d)));
134
+ }
135
+ topHit(x, y, tol, m) {
136
+ const list = this.deps.drawings();
137
+ const fontSize = this.deps.fontSize();
138
+ const sel = list.find((d) => d.id === this.state.selectedId);
139
+ if (sel) {
140
+ const hit = hitTest(sel, x, y, tol, true, m, fontSize);
141
+ if (hit)
142
+ return { drawing: sel, hit };
143
+ }
144
+ for (let i = list.length - 1; i >= 0; i--) {
145
+ const hit = hitTest(list[i], x, y, tol, false, m, fontSize);
146
+ if (hit)
147
+ return { drawing: list[i], hit };
148
+ }
149
+ return null;
150
+ }
151
+ }
@@ -0,0 +1,44 @@
1
+ import type { Drawing } from '../core/drawings.ts';
2
+ import type { Mapper } from './mapper.ts';
3
+ export interface Px {
4
+ x: number;
5
+ y: number;
6
+ }
7
+ export interface Seg {
8
+ a: Px;
9
+ b: Px;
10
+ }
11
+ export interface Box {
12
+ x: number;
13
+ y: number;
14
+ w: number;
15
+ h: number;
16
+ }
17
+ export interface FibLine {
18
+ level: number;
19
+ price: number;
20
+ seg: Seg;
21
+ }
22
+ export interface Shape {
23
+ segments: Seg[];
24
+ box: Box | null;
25
+ fib: FibLine[];
26
+ note: {
27
+ box: Box;
28
+ pin: Px;
29
+ } | null;
30
+ handles: Px[];
31
+ }
32
+ export type Hit = {
33
+ handle: number;
34
+ } | 'body';
35
+ type Shaped = Pick<Drawing, 'type' | 'points' | 'text'>;
36
+ export declare const noteSize: (text: string, fontSize: number) => {
37
+ w: number;
38
+ h: number;
39
+ };
40
+ export declare function distToSegment(p: Px, s: Seg): number;
41
+ export declare function shapeOf(d: Shaped, m: Mapper, fontSize: number): Shape;
42
+ export declare function measureBox(a: Px, b: Px): Box;
43
+ export declare function hitTest(d: Shaped, x: number, y: number, tol: number, withHandles: boolean, m: Mapper, fontSize: number): Hit | null;
44
+ export {};
@@ -0,0 +1,118 @@
1
+ import { FIB_LEVELS } from "../core/drawings.js";
2
+ export const noteSize = (text, fontSize) => ({
3
+ w: Math.max(24, Math.ceil(text.length * fontSize * 0.62) + 12),
4
+ h: fontSize + 10,
5
+ });
6
+ const NOTE_GAP = 8;
7
+ export function distToSegment(p, s) {
8
+ const dx = s.b.x - s.a.x;
9
+ const dy = s.b.y - s.a.y;
10
+ const len2 = dx * dx + dy * dy;
11
+ const u = len2 === 0 ? 0 : Math.max(0, Math.min(1, ((p.x - s.a.x) * dx + (p.y - s.a.y) * dy) / len2));
12
+ return Math.hypot(p.x - (s.a.x + u * dx), p.y - (s.a.y + u * dy));
13
+ }
14
+ function extend(a, b, m) {
15
+ const { left, right, top, bottom } = m.bounds;
16
+ const dx = b.x - a.x;
17
+ const dy = b.y - a.y;
18
+ if (Math.abs(dx) < 1e-6)
19
+ return { a, b: { x: a.x, y: dy >= 0 ? bottom + 1 : top - 1 } };
20
+ const x = dx > 0 ? right + 1 : left - 1;
21
+ return { a, b: { x, y: a.y + (dy / dx) * (x - a.x) } };
22
+ }
23
+ function boxOf(a, b) {
24
+ return { x: Math.min(a.x, b.x), y: Math.min(a.y, b.y), w: Math.abs(b.x - a.x), h: Math.abs(b.y - a.y) };
25
+ }
26
+ function edgesOf(r) {
27
+ const tl = { x: r.x, y: r.y };
28
+ const tr = { x: r.x + r.w, y: r.y };
29
+ const br = { x: r.x + r.w, y: r.y + r.h };
30
+ const bl = { x: r.x, y: r.y + r.h };
31
+ return [
32
+ { a: tl, b: tr },
33
+ { a: tr, b: br },
34
+ { a: br, b: bl },
35
+ { a: bl, b: tl },
36
+ ];
37
+ }
38
+ const empty = () => ({ segments: [], box: null, fib: [], note: null, handles: [] });
39
+ export function shapeOf(d, m, fontSize) {
40
+ const s = empty();
41
+ const px = d.points.map((p) => m.px(p));
42
+ const a = px[0];
43
+ const b = px[1];
44
+ if (!a)
45
+ return s;
46
+ const { left, right } = m.bounds;
47
+ switch (d.type) {
48
+ case 'hline':
49
+ s.segments.push({ a: { x: left, y: a.y }, b: { x: right, y: a.y } });
50
+ s.handles.push({ x: Math.min(right - 12, Math.max(left + 12, a.x)), y: a.y });
51
+ break;
52
+ case 'trend':
53
+ if (b)
54
+ s.segments.push({ a, b });
55
+ s.handles.push(...px);
56
+ break;
57
+ case 'ray':
58
+ if (b)
59
+ s.segments.push(extend(a, b, m));
60
+ s.handles.push(...px);
61
+ break;
62
+ case 'rect':
63
+ if (b) {
64
+ s.box = boxOf(a, b);
65
+ s.segments.push(...edgesOf(s.box));
66
+ }
67
+ s.handles.push(...px);
68
+ break;
69
+ case 'fib':
70
+ if (b) {
71
+ const x1 = Math.min(a.x, b.x);
72
+ for (const level of FIB_LEVELS) {
73
+ const y = b.y + (a.y - b.y) * level;
74
+ s.fib.push({
75
+ level,
76
+ price: d.points[1].price + (d.points[0].price - d.points[1].price) * level,
77
+ seg: { a: { x: x1, y }, b: { x: right, y } },
78
+ });
79
+ }
80
+ s.segments.push({ a, b });
81
+ }
82
+ s.handles.push(...px);
83
+ break;
84
+ case 'note': {
85
+ const size = noteSize(d.text ?? '', fontSize);
86
+ const x = a.x + NOTE_GAP + size.w > right ? a.x - NOTE_GAP - size.w : a.x + NOTE_GAP;
87
+ const y = a.y - NOTE_GAP - size.h < m.bounds.top ? a.y + NOTE_GAP : a.y - NOTE_GAP - size.h;
88
+ const box = { x, y, w: size.w, h: size.h };
89
+ s.note = { box, pin: a };
90
+ s.handles.push(a);
91
+ break;
92
+ }
93
+ }
94
+ return s;
95
+ }
96
+ export function measureBox(a, b) {
97
+ return boxOf(a, b);
98
+ }
99
+ const inside = (p, r, pad) => p.x >= r.x - pad && p.x <= r.x + r.w + pad && p.y >= r.y - pad && p.y <= r.y + r.h + pad;
100
+ export function hitTest(d, x, y, tol, withHandles, m, fontSize) {
101
+ const shape = shapeOf(d, m, fontSize);
102
+ const p = { x, y };
103
+ if (withHandles) {
104
+ for (let i = shape.handles.length - 1; i >= 0; i--) {
105
+ if (Math.hypot(shape.handles[i].x - x, shape.handles[i].y - y) <= tol)
106
+ return { handle: i };
107
+ }
108
+ }
109
+ for (const seg of shape.segments)
110
+ if (distToSegment(p, seg) <= tol)
111
+ return 'body';
112
+ for (const f of shape.fib)
113
+ if (distToSegment(p, f.seg) <= tol)
114
+ return 'body';
115
+ if (shape.note && (inside(p, shape.note.box, tol / 2) || Math.hypot(shape.note.pin.x - x, shape.note.pin.y - y) <= tol))
116
+ return 'body';
117
+ return null;
118
+ }
@@ -0,0 +1,30 @@
1
+ import type { CandleSeries } from '../core/series.ts';
2
+ import type { PriceScale } from '../core/priceScale.ts';
3
+ import type { Viewport } from '../core/viewport.ts';
4
+ import type { Point } from '../core/drawings.ts';
5
+ export interface Bounds {
6
+ left: number;
7
+ right: number;
8
+ top: number;
9
+ bottom: number;
10
+ }
11
+ export interface MapperInput {
12
+ series: CandleSeries;
13
+ vp: Viewport;
14
+ scale: PriceScale;
15
+ intervalMs: number;
16
+ bounds: Bounds;
17
+ }
18
+ export interface Mapper {
19
+ bounds: Bounds;
20
+ indexOfTime(t: number): number;
21
+ timeOfIndex(i: number): number;
22
+ px(p: Point): {
23
+ x: number;
24
+ y: number;
25
+ };
26
+ point(x: number, y: number): Point;
27
+ indexAtX(x: number): number;
28
+ priceAtY(y: number): number;
29
+ }
30
+ export declare function makeMapper(m: MapperInput): Mapper;
@@ -0,0 +1,32 @@
1
+ export function makeMapper(m) {
2
+ const { series, vp, scale, intervalMs, bounds } = m;
3
+ const last = series.count - 1;
4
+ const indexOfTime = (t) => {
5
+ if (last < 0)
6
+ return 0;
7
+ const i = series.indexAtTime(t);
8
+ if (i < 0)
9
+ return (t - series.timeAt(0)) / intervalMs;
10
+ const frac = (t - series.timeAt(i)) / intervalMs;
11
+ return i === last ? last + frac : i + Math.min(1, frac);
12
+ };
13
+ const timeOfIndex = (i) => {
14
+ if (last < 0)
15
+ return Math.round(i) * intervalMs;
16
+ if (i <= 0)
17
+ return series.timeAt(0) + i * intervalMs;
18
+ if (i >= last)
19
+ return series.timeAt(last) + (i - last) * intervalMs;
20
+ const lo = Math.floor(i);
21
+ return series.timeAt(lo) + (i - lo) * intervalMs;
22
+ };
23
+ return {
24
+ bounds,
25
+ indexOfTime,
26
+ timeOfIndex,
27
+ px: (p) => ({ x: vp.x(indexOfTime(p.t)), y: scale.y(p.price) }),
28
+ point: (x, y) => ({ t: timeOfIndex(Math.round(vp.indexAt(x))), price: scale.priceAt(y) }),
29
+ indexAtX: (x) => vp.indexAt(x),
30
+ priceAtY: (y) => scale.priceAt(y),
31
+ };
32
+ }
@@ -0,0 +1,17 @@
1
+ import type { PointerInput } from '../interact/gestures.ts';
2
+ import type { DrawController } from './controller.ts';
3
+ export declare class DrawPointer {
4
+ private c;
5
+ private drag;
6
+ private lastTap;
7
+ constructor(controller: DrawController);
8
+ reset(): void;
9
+ cursor(x: number, y: number): string;
10
+ down(p: PointerInput): boolean;
11
+ move(p: PointerInput): boolean;
12
+ up(p: PointerInput): boolean;
13
+ cancel(p: PointerInput): void;
14
+ private downTool;
15
+ private tapped;
16
+ private inPlot;
17
+ }
@@ -0,0 +1,144 @@
1
+ import { pointsNeeded } from "../core/drawings.js";
2
+ import { DRAG_SLOP, MOUSE_TOLERANCE, NOTE_DOUBLE_MS, TOUCH_TOLERANCE } from "./state.js";
3
+ export class DrawPointer {
4
+ constructor(controller) {
5
+ this.drag = null;
6
+ this.lastTap = null;
7
+ this.c = controller;
8
+ }
9
+ reset() {
10
+ this.drag = null;
11
+ }
12
+ cursor(x, y) {
13
+ if (this.c.state.tool)
14
+ return 'crosshair';
15
+ const m = this.c.deps.mapper();
16
+ return m && this.c.topHit(x, y, TOUCH_TOLERANCE / 2, m) ? 'pointer' : '';
17
+ }
18
+ down(p) {
19
+ if (p.type === 'mouse' && p.button !== 0)
20
+ return false;
21
+ const m = this.c.deps.mapper();
22
+ if (!m || !this.inPlot(p, m))
23
+ return false;
24
+ if (this.c.state.tool)
25
+ return this.downTool(p, m);
26
+ const tol = p.type === 'mouse' ? MOUSE_TOLERANCE : TOUCH_TOLERANCE;
27
+ const found = this.c.topHit(p.x, p.y, tol, m);
28
+ if (!found) {
29
+ if (this.c.state.selectedId !== null)
30
+ this.c.select(null);
31
+ this.c.deps.redraw();
32
+ return false;
33
+ }
34
+ this.c.select(found.drawing.id);
35
+ if (typeof found.hit === 'object') {
36
+ this.drag = { kind: 'handle', id: p.id, drawing: found.drawing.id, index: found.hit.handle, moved: false };
37
+ }
38
+ else {
39
+ this.drag = { kind: 'move', id: p.id, origin: found.drawing, idx: m.indexAtX(p.x), price: m.priceAtY(p.y), moved: false, x: p.x, y: p.y };
40
+ }
41
+ this.c.deps.redraw();
42
+ return true;
43
+ }
44
+ move(p) {
45
+ const m = this.c.deps.mapper();
46
+ if (!m)
47
+ return false;
48
+ const d = this.drag;
49
+ const s = this.c.state;
50
+ if (!d || d.id !== p.id) {
51
+ if (s.pending.length > 0) {
52
+ s.preview = m.point(p.x, p.y);
53
+ this.c.deps.redraw();
54
+ }
55
+ return false;
56
+ }
57
+ switch (d.kind) {
58
+ case 'create':
59
+ if (Math.hypot(p.x - d.x, p.y - d.y) > DRAG_SLOP)
60
+ d.moved = true;
61
+ s.preview = m.point(p.x, p.y);
62
+ break;
63
+ case 'handle':
64
+ d.moved = true;
65
+ this.c.edit(d.drawing, (q) => ({ ...q, points: q.points.map((pt, i) => (i === d.index ? m.point(p.x, p.y) : pt)) }));
66
+ break;
67
+ case 'move': {
68
+ if (Math.hypot(p.x - d.x, p.y - d.y) > 2)
69
+ d.moved = true;
70
+ const di = m.indexAtX(p.x) - d.idx;
71
+ const dp = m.priceAtY(p.y) - d.price;
72
+ const flat = d.origin.type === 'hline';
73
+ this.c.edit(d.origin.id, (q) => ({
74
+ ...q,
75
+ points: d.origin.points.map((pt) => ({ t: flat ? pt.t : m.timeOfIndex(m.indexOfTime(pt.t) + di), price: pt.price + dp })),
76
+ }));
77
+ break;
78
+ }
79
+ default:
80
+ break;
81
+ }
82
+ this.c.deps.redraw();
83
+ return true;
84
+ }
85
+ up(p) {
86
+ const d = this.drag;
87
+ const m = this.c.deps.mapper();
88
+ if (!d || d.id !== p.id)
89
+ return false;
90
+ this.drag = null;
91
+ if (d.kind === 'create') {
92
+ if (d.moved && m)
93
+ this.c.finish(m.point(p.x, p.y));
94
+ this.c.deps.redraw();
95
+ }
96
+ else if (d.kind === 'handle' || d.kind === 'move') {
97
+ if (d.moved)
98
+ this.c.deps.changed(this.c.deps.drawings());
99
+ else
100
+ this.tapped(d.kind === 'move' ? d.origin.id : d.drawing);
101
+ }
102
+ return true;
103
+ }
104
+ cancel(p) {
105
+ if (this.drag && this.drag.id === p.id)
106
+ this.drag = null;
107
+ }
108
+ downTool(p, m) {
109
+ const s = this.c.state;
110
+ const tool = s.tool;
111
+ const pt = m.point(p.x, p.y);
112
+ if (s.pending.length === 0) {
113
+ if (pointsNeeded(tool) === 1) {
114
+ this.c.finish(pt);
115
+ this.drag = { kind: 'consumed', id: p.id };
116
+ return true;
117
+ }
118
+ s.pending = [pt];
119
+ s.measure = null;
120
+ s.preview = pt;
121
+ this.drag = { kind: 'create', id: p.id, x: p.x, y: p.y, moved: false };
122
+ this.c.deps.progress({ tool, placed: 1, needed: 2 });
123
+ this.c.deps.redraw();
124
+ return true;
125
+ }
126
+ this.c.finish(pt);
127
+ this.drag = { kind: 'consumed', id: p.id };
128
+ return true;
129
+ }
130
+ tapped(id) {
131
+ const now = this.c.deps.now();
132
+ const d = this.c.deps.drawings().find((q) => q.id === id);
133
+ if (d && d.type === 'note' && this.lastTap && this.lastTap.id === id && now - this.lastTap.t <= NOTE_DOUBLE_MS) {
134
+ this.lastTap = null;
135
+ this.c.deps.edit(d);
136
+ return;
137
+ }
138
+ this.lastTap = { id, t: now };
139
+ }
140
+ inPlot(p, m) {
141
+ const b = m.bounds;
142
+ return p.x >= b.left && p.x <= b.right && p.y >= b.top && p.y <= b.bottom;
143
+ }
144
+ }
@@ -0,0 +1,61 @@
1
+ import type { Drawing, DrawingTool, Point } from '../core/drawings.ts';
2
+ import type { Mapper } from './mapper.ts';
3
+ export declare const MOUSE_TOLERANCE = 6;
4
+ export declare const TOUCH_TOLERANCE = 14;
5
+ export declare const DRAG_SLOP = 6;
6
+ export declare const NOTE_DOUBLE_MS = 350;
7
+ export declare const DEFAULT_NOTE = "Note";
8
+ export interface DrawState {
9
+ tool: DrawingTool | null;
10
+ pending: Point[];
11
+ preview: Point | null;
12
+ selectedId: string | null;
13
+ measure: {
14
+ a: Point;
15
+ b: Point;
16
+ } | null;
17
+ color: string;
18
+ }
19
+ export interface DrawProgress {
20
+ tool: DrawingTool;
21
+ placed: number;
22
+ needed: number;
23
+ }
24
+ export interface DrawDeps {
25
+ drawings(): Drawing[];
26
+ store(next: Drawing[]): void;
27
+ mapper(): Mapper | null;
28
+ fontSize(): number;
29
+ now(): number;
30
+ redraw(): void;
31
+ changed(list: readonly Drawing[]): void;
32
+ selection(id: string | null): void;
33
+ progress(info: DrawProgress | null): void;
34
+ edit(d: Drawing): void;
35
+ }
36
+ export type Drag = {
37
+ kind: 'create';
38
+ id: number;
39
+ x: number;
40
+ y: number;
41
+ moved: boolean;
42
+ } | {
43
+ kind: 'consumed';
44
+ id: number;
45
+ } | {
46
+ kind: 'handle';
47
+ id: number;
48
+ drawing: string;
49
+ index: number;
50
+ moved: boolean;
51
+ } | {
52
+ kind: 'move';
53
+ id: number;
54
+ origin: Drawing;
55
+ idx: number;
56
+ price: number;
57
+ moved: boolean;
58
+ x: number;
59
+ y: number;
60
+ };
61
+ export declare function createDrawState(color: string): DrawState;
@@ -0,0 +1,8 @@
1
+ export const MOUSE_TOLERANCE = 6;
2
+ export const TOUCH_TOLERANCE = 14;
3
+ export const DRAG_SLOP = 6;
4
+ export const NOTE_DOUBLE_MS = 350;
5
+ export const DEFAULT_NOTE = 'Note';
6
+ export function createDrawState(color) {
7
+ return { tool: null, pending: [], preview: null, selectedId: null, measure: null, color };
8
+ }
@@ -0,0 +1,20 @@
1
+ import type { Emitter } from './core/emitter.ts';
2
+ import type { Chart, ChartEvents } from './core/types.ts';
3
+ import { DrawController } from './draw/controller.ts';
4
+ import type { ChartState } from './chartState.ts';
5
+ import type { Frame } from './render/frame.ts';
6
+ export interface DrawApiDeps {
7
+ s: ChartState;
8
+ root: HTMLElement;
9
+ events: Emitter<ChartEvents>;
10
+ invalidate(layer: 'main' | 'overlay'): void;
11
+ lastFrame(): Frame | null;
12
+ now(): number;
13
+ isDestroyed(): boolean;
14
+ }
15
+ type DrawMethods = Pick<Chart, 'setDrawingTool' | 'setDrawings' | 'getDrawings' | 'updateDrawing' | 'deleteSelected' | 'clearDrawings' | 'setDrawingColor'>;
16
+ export declare function createDrawApi(d: DrawApiDeps): {
17
+ controller: DrawController;
18
+ methods: DrawMethods;
19
+ };
20
+ export {};