@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.
Files changed (113) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +62 -2
  3. package/dist/chart.d.ts +8 -0
  4. package/dist/chart.js +238 -0
  5. package/dist/chartState.d.ts +47 -0
  6. package/dist/chartState.js +64 -0
  7. package/dist/core/drawings.d.ts +21 -0
  8. package/dist/core/drawings.js +46 -0
  9. package/dist/core/emitter.d.ts +8 -0
  10. package/dist/core/emitter.js +26 -0
  11. package/dist/core/format.d.ts +9 -0
  12. package/dist/core/format.js +35 -0
  13. package/dist/core/heikinAshi.d.ts +2 -0
  14. package/dist/core/heikinAshi.js +14 -0
  15. package/dist/core/indicatorSet.d.ts +20 -0
  16. package/dist/core/indicatorSet.js +66 -0
  17. package/dist/core/indicators.d.ts +53 -0
  18. package/dist/core/indicators.js +187 -0
  19. package/dist/core/priceScale.d.ts +22 -0
  20. package/dist/core/priceScale.js +92 -0
  21. package/dist/core/series.d.ts +41 -0
  22. package/dist/core/series.js +221 -0
  23. package/dist/core/time.d.ts +25 -0
  24. package/dist/core/time.js +44 -0
  25. package/dist/core/timeLabels.d.ts +11 -0
  26. package/dist/core/timeLabels.js +52 -0
  27. package/dist/core/types.d.ts +134 -0
  28. package/dist/core/types.js +1 -0
  29. package/dist/core/viewport.d.ts +41 -0
  30. package/dist/core/viewport.js +116 -0
  31. package/dist/core/warn.d.ts +1 -0
  32. package/dist/core/warn.js +3 -0
  33. package/dist/createChart.d.ts +2 -0
  34. package/dist/createChart.js +7 -0
  35. package/dist/dom/environment.d.ts +11 -0
  36. package/dist/dom/environment.js +49 -0
  37. package/dist/dom/indicatorReadings.d.ts +7 -0
  38. package/dist/dom/indicatorReadings.js +26 -0
  39. package/dist/dom/legend.d.ts +19 -0
  40. package/dist/dom/legend.js +62 -0
  41. package/dist/dom/legendSync.d.ts +5 -0
  42. package/dist/dom/legendSync.js +30 -0
  43. package/dist/dom/root.d.ts +9 -0
  44. package/dist/dom/root.js +19 -0
  45. package/dist/draw/controller.d.ts +36 -0
  46. package/dist/draw/controller.js +151 -0
  47. package/dist/draw/geometry.d.ts +44 -0
  48. package/dist/draw/geometry.js +118 -0
  49. package/dist/draw/mapper.d.ts +30 -0
  50. package/dist/draw/mapper.js +32 -0
  51. package/dist/draw/pointer.d.ts +17 -0
  52. package/dist/draw/pointer.js +144 -0
  53. package/dist/draw/state.d.ts +61 -0
  54. package/dist/draw/state.js +8 -0
  55. package/dist/drawApi.d.ts +20 -0
  56. package/dist/drawApi.js +59 -0
  57. package/dist/host.d.ts +35 -0
  58. package/dist/host.js +63 -0
  59. package/dist/index.d.ts +7 -0
  60. package/dist/index.js +5 -0
  61. package/dist/interact/attach.d.ts +2 -0
  62. package/dist/interact/attach.js +142 -0
  63. package/dist/interact/gestures.d.ts +47 -0
  64. package/dist/interact/gestures.js +179 -0
  65. package/dist/interact/keyboard.d.ts +19 -0
  66. package/dist/interact/keyboard.js +34 -0
  67. package/dist/interact/wheel.d.ts +15 -0
  68. package/dist/interact/wheel.js +18 -0
  69. package/dist/render/bars.d.ts +17 -0
  70. package/dist/render/bars.js +55 -0
  71. package/dist/render/canvas.d.ts +2 -0
  72. package/dist/render/canvas.js +13 -0
  73. package/dist/render/drawAxes.d.ts +2 -0
  74. package/dist/render/drawAxes.js +34 -0
  75. package/dist/render/drawCandles.d.ts +3 -0
  76. package/dist/render/drawCandles.js +68 -0
  77. package/dist/render/drawCrosshair.d.ts +7 -0
  78. package/dist/render/drawCrosshair.js +42 -0
  79. package/dist/render/drawDrawings.d.ts +10 -0
  80. package/dist/render/drawDrawings.js +146 -0
  81. package/dist/render/drawGrid.d.ts +2 -0
  82. package/dist/render/drawGrid.js +22 -0
  83. package/dist/render/drawIndicators.d.ts +4 -0
  84. package/dist/render/drawIndicators.js +67 -0
  85. package/dist/render/drawLastPrice.d.ts +10 -0
  86. package/dist/render/drawLastPrice.js +53 -0
  87. package/dist/render/drawLine.d.ts +3 -0
  88. package/dist/render/drawLine.js +43 -0
  89. package/dist/render/drawMain.d.ts +2 -0
  90. package/dist/render/drawMain.js +43 -0
  91. package/dist/render/drawMarkers.d.ts +2 -0
  92. package/dist/render/drawMarkers.js +39 -0
  93. package/dist/render/drawPriceLines.d.ts +4 -0
  94. package/dist/render/drawPriceLines.js +60 -0
  95. package/dist/render/drawSubPanes.d.ts +2 -0
  96. package/dist/render/drawSubPanes.js +104 -0
  97. package/dist/render/frame.d.ts +57 -0
  98. package/dist/render/frame.js +5 -0
  99. package/dist/render/frameBuilder.d.ts +12 -0
  100. package/dist/render/frameBuilder.js +129 -0
  101. package/dist/render/indicatorFrame.d.ts +17 -0
  102. package/dist/render/indicatorFrame.js +55 -0
  103. package/dist/render/layout.d.ts +4 -0
  104. package/dist/render/layout.js +26 -0
  105. package/dist/render/paintFrame.d.ts +11 -0
  106. package/dist/render/paintFrame.js +33 -0
  107. package/dist/render/scheduler.d.ts +17 -0
  108. package/dist/render/scheduler.js +38 -0
  109. package/dist/render/tween.d.ts +12 -0
  110. package/dist/render/tween.js +38 -0
  111. package/dist/theme.d.ts +2 -0
  112. package/dist/theme.js +16 -0
  113. package/package.json +37 -4
@@ -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 ADDED
@@ -0,0 +1,35 @@
1
+ import type { ChartEvents } from './core/types.ts';
2
+ import type { Emitter } from './core/emitter.ts';
3
+ import type { ChartState } from './chartState.ts';
4
+ import type { DrawController } from './draw/controller.ts';
5
+ import type { Frame, Layout } from './render/frame.ts';
6
+ export interface ChartHost {
7
+ root: HTMLElement;
8
+ state: ChartState;
9
+ draw: DrawController;
10
+ setCrosshair(pos: {
11
+ x: number;
12
+ y: number;
13
+ } | null): void;
14
+ viewChanged(): void;
15
+ cancelAnimation(): void;
16
+ invalidate(layer: 'main' | 'overlay'): void;
17
+ scrollToLive(): void;
18
+ layout(): Layout | null;
19
+ addDisposer(fn: () => void): void;
20
+ }
21
+ export interface HostDeps {
22
+ root: HTMLElement;
23
+ state: ChartState;
24
+ draw: DrawController;
25
+ events: Emitter<ChartEvents>;
26
+ invalidate(layer: 'main' | 'overlay'): void;
27
+ invalidateAll(): void;
28
+ updateLegend(): void;
29
+ isDestroyed(): boolean;
30
+ lastFrame(): Frame | null;
31
+ now(): number;
32
+ }
33
+ export declare function createHost(d: HostDeps): ChartHost & {
34
+ dispose(): void;
35
+ };
package/dist/host.js ADDED
@@ -0,0 +1,63 @@
1
+ import { crosshairCandle } from "./dom/legendSync.js";
2
+ import { SCROLL_TWEEN_MS } from "./render/frameBuilder.js";
3
+ export function createHost(d) {
4
+ const s = d.state;
5
+ const disposers = [];
6
+ return {
7
+ root: d.root,
8
+ state: s,
9
+ draw: d.draw,
10
+ setCrosshair(pos) {
11
+ if (d.isDestroyed())
12
+ return;
13
+ const frame = d.lastFrame();
14
+ let next = null;
15
+ if (pos && s.series.count > 0 && frame) {
16
+ const idx = Math.round(s.vp.indexAt(pos.x));
17
+ const inPlot = pos.x >= 0 && pos.x <= frame.layout.plotRight && idx >= 0 && idx <= s.series.lastIndex();
18
+ if (inPlot)
19
+ next = { x: pos.x, y: pos.y, index: idx };
20
+ }
21
+ s.crosshair = next;
22
+ const candle = crosshairCandle(s);
23
+ const key = candle ? candle.t : null;
24
+ if (key !== s.lastCrosshairTime) {
25
+ s.lastCrosshairTime = key;
26
+ d.events.emit('crosshair', candle);
27
+ }
28
+ d.updateLegend();
29
+ d.invalidate('overlay');
30
+ },
31
+ viewChanged() {
32
+ d.invalidateAll();
33
+ },
34
+ cancelAnimation() {
35
+ s.rightAnimating = false;
36
+ },
37
+ invalidate: d.invalidate,
38
+ scrollToLive() {
39
+ if (d.isDestroyed())
40
+ return;
41
+ if (s.options.reducedMotion) {
42
+ s.vp.goLive();
43
+ }
44
+ else {
45
+ s.rightTween.jump(s.vp.rightIndex);
46
+ s.rightTween.set(s.vp.liveRightIndex(), d.now(), SCROLL_TWEEN_MS);
47
+ s.rightAnimating = true;
48
+ }
49
+ d.invalidateAll();
50
+ },
51
+ layout() {
52
+ return d.lastFrame()?.layout ?? null;
53
+ },
54
+ addDisposer(fn) {
55
+ disposers.push(fn);
56
+ },
57
+ dispose() {
58
+ for (const fn of disposers)
59
+ fn();
60
+ disposers.length = 0;
61
+ },
62
+ };
63
+ }
@@ -0,0 +1,7 @@
1
+ export type { Candle, CandleStyle, Chart, Drawing, DrawingKind, DrawingTool, ChartEvents, ChartLabels, ChartOptions, ChartTheme, ChartType, IndicatorSpec, Marker, Point, PriceLine, Time, } from './core/types.ts';
2
+ export { createChart } from './createChart.ts';
3
+ export { bucketStart, IST_OFFSET_MS, INTERVAL_MS } from './core/time.ts';
4
+ export type { IntervalId } from './core/time.ts';
5
+ export { heikinAshi } from './core/heikinAshi.ts';
6
+ export { zuttoraTheme } from './theme.ts';
7
+ export declare const VERSION = "zuttora-charts@0.3.0";
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { createChart } from "./createChart.js";
2
+ export { bucketStart, IST_OFFSET_MS, INTERVAL_MS } from "./core/time.js";
3
+ export { heikinAshi } from "./core/heikinAshi.js";
4
+ export { zuttoraTheme } from "./theme.js";
5
+ export const VERSION = 'zuttora-charts@0.3.0';
@@ -0,0 +1,2 @@
1
+ import type { ChartHost } from '../host.ts';
2
+ export declare function attachInteraction(host: ChartHost): void;
@@ -0,0 +1,142 @@
1
+ import { Gestures } from "./gestures.js";
2
+ import { handleKey } from "./keyboard.js";
3
+ import { handleWheel } from "./wheel.js";
4
+ export function attachInteraction(host) {
5
+ const root = host.root;
6
+ const s = host.state;
7
+ const changed = () => {
8
+ host.cancelAnimation();
9
+ host.viewChanged();
10
+ };
11
+ const target = {
12
+ region(x, y) {
13
+ const l = host.layout();
14
+ if (!l || x < 0 || y < 0 || x > l.width || y > l.height)
15
+ return 'outside';
16
+ if (y >= l.timeTop)
17
+ return 'time';
18
+ if (x >= l.plotRight)
19
+ return y < l.mainTop + l.mainHeight ? 'axis' : 'outside';
20
+ return 'plot';
21
+ },
22
+ panBy(dx) {
23
+ s.vp.panBy(dx);
24
+ changed();
25
+ },
26
+ zoomAt(x, factor) {
27
+ s.vp.zoomAt(x, factor);
28
+ changed();
29
+ },
30
+ scaleBy(dy) {
31
+ s.scale.scaleBy(dy);
32
+ changed();
33
+ },
34
+ resetAuto() {
35
+ s.scale.resetAuto();
36
+ changed();
37
+ },
38
+ resetView() {
39
+ s.vp.reset();
40
+ s.scale.resetAuto();
41
+ changed();
42
+ },
43
+ showCrosshair: (x, y) => host.setCrosshair({ x, y }),
44
+ hideCrosshair: () => host.setCrosshair(null),
45
+ now: () => (typeof performance !== 'undefined' ? performance.now() : Date.now()),
46
+ setTimer: (fn, ms) => setTimeout(fn, ms),
47
+ clearTimer: (id) => clearTimeout(id),
48
+ };
49
+ const gestures = new Gestures(target);
50
+ const local = (e) => {
51
+ const r = root.getBoundingClientRect();
52
+ return { id: e.pointerId, type: e.pointerType, x: e.clientX - r.left, y: e.clientY - r.top, button: e.button };
53
+ };
54
+ const onDown = (e) => {
55
+ const input = local(e);
56
+ if (host.draw.down(input) || gestures.down(input)) {
57
+ try {
58
+ root.setPointerCapture(e.pointerId);
59
+ }
60
+ catch {
61
+ }
62
+ root.focus({ preventScroll: true });
63
+ }
64
+ };
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
+ };
72
+ const onUp = (e) => {
73
+ const input = local(e);
74
+ if (!host.draw.up(input))
75
+ gestures.up(input);
76
+ try {
77
+ root.releasePointerCapture(e.pointerId);
78
+ }
79
+ catch {
80
+ }
81
+ };
82
+ const onCancel = (e) => {
83
+ host.draw.cancel(local(e));
84
+ gestures.cancel(local(e));
85
+ };
86
+ const onLeave = (e) => {
87
+ if (e.pointerType !== 'touch')
88
+ gestures.leave();
89
+ };
90
+ const onWheel = (e) => {
91
+ const r = root.getBoundingClientRect();
92
+ const used = handleWheel({ x: e.clientX - r.left, y: e.clientY - r.top, deltaX: e.deltaX, deltaY: e.deltaY, deltaMode: e.deltaMode, shiftKey: e.shiftKey }, {
93
+ inPlot: (x, y) => target.region(x, y) === 'plot',
94
+ panBy: target.panBy,
95
+ zoomAt: target.zoomAt,
96
+ });
97
+ if (used)
98
+ e.preventDefault();
99
+ };
100
+ const onKey = (e) => {
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
+ });
115
+ if (used) {
116
+ e.preventDefault();
117
+ e.stopPropagation();
118
+ }
119
+ };
120
+ const onFocus = () => root.style.setProperty('outline', `2px solid ${s.options.theme.accent}`);
121
+ const onBlur = () => root.style.setProperty('outline', 'none');
122
+ root.addEventListener('pointerdown', onDown);
123
+ root.addEventListener('pointermove', onMove);
124
+ root.addEventListener('pointerup', onUp);
125
+ root.addEventListener('pointercancel', onCancel);
126
+ root.addEventListener('pointerleave', onLeave);
127
+ root.addEventListener('wheel', onWheel, { passive: false });
128
+ root.addEventListener('keydown', onKey);
129
+ root.addEventListener('focus', onFocus);
130
+ root.addEventListener('blur', onBlur);
131
+ host.addDisposer(() => {
132
+ root.removeEventListener('pointerdown', onDown);
133
+ root.removeEventListener('pointermove', onMove);
134
+ root.removeEventListener('pointerup', onUp);
135
+ root.removeEventListener('pointercancel', onCancel);
136
+ root.removeEventListener('pointerleave', onLeave);
137
+ root.removeEventListener('wheel', onWheel);
138
+ root.removeEventListener('keydown', onKey);
139
+ root.removeEventListener('focus', onFocus);
140
+ root.removeEventListener('blur', onBlur);
141
+ });
142
+ }
@@ -0,0 +1,47 @@
1
+ export type Region = 'plot' | 'axis' | 'time' | 'outside';
2
+ export interface GestureTarget {
3
+ region(x: number, y: number): Region;
4
+ panBy(dxPx: number): void;
5
+ zoomAt(x: number, factor: number): void;
6
+ scaleBy(dyPx: number): void;
7
+ resetAuto(): void;
8
+ resetView(): void;
9
+ showCrosshair(x: number, y: number): void;
10
+ hideCrosshair(): void;
11
+ now(): number;
12
+ setTimer(fn: () => void, ms: number): number;
13
+ clearTimer(id: number): void;
14
+ }
15
+ export interface PointerInput {
16
+ id: number;
17
+ type: string;
18
+ x: number;
19
+ y: number;
20
+ button: number;
21
+ }
22
+ export declare const HOLD_MS = 300;
23
+ export declare const HOLD_SLOP = 6;
24
+ export declare const MOUSE_SLOP = 3;
25
+ export declare const DOUBLE_MS = 300;
26
+ export declare const DOUBLE_DIST = 24;
27
+ export declare class Gestures {
28
+ private target;
29
+ private pointers;
30
+ private mode;
31
+ private holdTimer;
32
+ private lastTap;
33
+ private pinchDist;
34
+ private pinchMidX;
35
+ private pinchMidY;
36
+ constructor(target: GestureTarget);
37
+ down(p: PointerInput): boolean;
38
+ move(p: PointerInput): void;
39
+ up(p: PointerInput): void;
40
+ cancel(p: PointerInput): void;
41
+ leave(): void;
42
+ private hold;
43
+ private tap;
44
+ private clearHold;
45
+ private measurePinch;
46
+ private pinchStep;
47
+ }
@@ -0,0 +1,179 @@
1
+ export const HOLD_MS = 300;
2
+ export const HOLD_SLOP = 6;
3
+ export const MOUSE_SLOP = 3;
4
+ export const DOUBLE_MS = 300;
5
+ export const DOUBLE_DIST = 24;
6
+ const TAP_MS = 400;
7
+ export class Gestures {
8
+ constructor(target) {
9
+ this.pointers = new Map();
10
+ this.mode = 'idle';
11
+ this.holdTimer = null;
12
+ this.lastTap = null;
13
+ this.pinchDist = 0;
14
+ this.pinchMidX = 0;
15
+ this.pinchMidY = 0;
16
+ this.target = target;
17
+ }
18
+ down(p) {
19
+ if (p.type === 'mouse' && p.button !== 0)
20
+ return false;
21
+ const region = this.target.region(p.x, p.y);
22
+ if (region === 'outside' || region === 'time')
23
+ return false;
24
+ this.pointers.set(p.id, {
25
+ type: p.type, x: p.x, y: p.y, startX: p.x, startY: p.y, startT: this.target.now(), region, dragged: false,
26
+ });
27
+ if (this.pointers.size >= 2) {
28
+ this.clearHold();
29
+ this.target.hideCrosshair();
30
+ this.mode = 'pinch';
31
+ this.measurePinch();
32
+ return true;
33
+ }
34
+ if (region === 'axis') {
35
+ this.mode = 'axis';
36
+ }
37
+ else if (p.type === 'touch') {
38
+ this.mode = 'press';
39
+ this.holdTimer = this.target.setTimer(() => this.hold(p.id), HOLD_MS);
40
+ }
41
+ else {
42
+ this.mode = 'pan';
43
+ }
44
+ return true;
45
+ }
46
+ move(p) {
47
+ const t = this.pointers.get(p.id);
48
+ if (!t) {
49
+ if (p.type !== 'touch') {
50
+ if (this.target.region(p.x, p.y) === 'plot')
51
+ this.target.showCrosshair(p.x, p.y);
52
+ else
53
+ this.target.hideCrosshair();
54
+ }
55
+ return;
56
+ }
57
+ const dx = p.x - t.x;
58
+ const dy = p.y - t.y;
59
+ t.x = p.x;
60
+ t.y = p.y;
61
+ switch (this.mode) {
62
+ case 'press':
63
+ if (Math.hypot(p.x - t.startX, p.y - t.startY) > HOLD_SLOP) {
64
+ this.clearHold();
65
+ this.mode = 'pan';
66
+ t.dragged = true;
67
+ this.target.panBy(p.x - t.startX);
68
+ }
69
+ break;
70
+ case 'pan':
71
+ if (t.type === 'mouse' && Math.hypot(p.x - t.startX, p.y - t.startY) <= MOUSE_SLOP && !t.dragged)
72
+ break;
73
+ t.dragged = true;
74
+ this.target.panBy(dx);
75
+ if (t.type !== 'touch' && this.target.region(p.x, p.y) === 'plot')
76
+ this.target.showCrosshair(p.x, p.y);
77
+ break;
78
+ case 'axis':
79
+ t.dragged = true;
80
+ this.target.scaleBy(dy);
81
+ break;
82
+ case 'crosshair':
83
+ this.target.showCrosshair(p.x, p.y);
84
+ break;
85
+ case 'pinch':
86
+ this.pinchStep();
87
+ break;
88
+ default:
89
+ break;
90
+ }
91
+ }
92
+ up(p) {
93
+ const t = this.pointers.get(p.id);
94
+ if (!t)
95
+ return;
96
+ this.pointers.delete(p.id);
97
+ this.clearHold();
98
+ const wasMode = this.mode;
99
+ if (this.pointers.size >= 1 && wasMode === 'pinch') {
100
+ this.mode = 'idle';
101
+ }
102
+ else if (this.pointers.size === 0) {
103
+ this.mode = 'idle';
104
+ }
105
+ if (wasMode === 'crosshair') {
106
+ this.target.hideCrosshair();
107
+ return;
108
+ }
109
+ if (wasMode === 'pinch')
110
+ return;
111
+ const quick = this.target.now() - t.startT <= TAP_MS;
112
+ const still = Math.hypot(t.x - t.startX, t.y - t.startY) <= HOLD_SLOP && !t.dragged;
113
+ if (quick && still)
114
+ this.tap(t);
115
+ if (t.type === 'touch')
116
+ this.target.hideCrosshair();
117
+ }
118
+ cancel(p) {
119
+ const t = this.pointers.get(p.id);
120
+ if (!t)
121
+ return;
122
+ this.pointers.delete(p.id);
123
+ this.clearHold();
124
+ if (this.pointers.size === 0)
125
+ this.mode = 'idle';
126
+ if (t.type === 'touch')
127
+ this.target.hideCrosshair();
128
+ }
129
+ leave() {
130
+ if (this.pointers.size === 0)
131
+ this.target.hideCrosshair();
132
+ }
133
+ hold(id) {
134
+ this.holdTimer = null;
135
+ const t = this.pointers.get(id);
136
+ if (!t || this.mode !== 'press')
137
+ return;
138
+ this.mode = 'crosshair';
139
+ this.target.showCrosshair(t.x, t.y);
140
+ }
141
+ tap(t) {
142
+ const now = this.target.now();
143
+ const last = this.lastTap;
144
+ if (last && now - last.t <= DOUBLE_MS && Math.hypot(t.x - last.x, t.y - last.y) <= DOUBLE_DIST) {
145
+ this.lastTap = null;
146
+ if (t.region === 'axis')
147
+ this.target.resetAuto();
148
+ else
149
+ this.target.resetView();
150
+ return;
151
+ }
152
+ this.lastTap = { t: now, x: t.x, y: t.y };
153
+ }
154
+ clearHold() {
155
+ if (this.holdTimer !== null)
156
+ this.target.clearTimer(this.holdTimer);
157
+ this.holdTimer = null;
158
+ }
159
+ measurePinch() {
160
+ const [a, b] = [...this.pointers.values()];
161
+ if (!a || !b)
162
+ return;
163
+ this.pinchDist = Math.max(1, Math.hypot(a.x - b.x, a.y - b.y));
164
+ this.pinchMidX = (a.x + b.x) / 2;
165
+ this.pinchMidY = (a.y + b.y) / 2;
166
+ }
167
+ pinchStep() {
168
+ const [a, b] = [...this.pointers.values()];
169
+ if (!a || !b)
170
+ return;
171
+ const dist = Math.max(1, Math.hypot(a.x - b.x, a.y - b.y));
172
+ const midX = (a.x + b.x) / 2;
173
+ this.target.zoomAt(midX, dist / this.pinchDist);
174
+ this.target.panBy(midX - this.pinchMidX);
175
+ this.pinchDist = dist;
176
+ this.pinchMidX = midX;
177
+ this.pinchMidY = (a.y + b.y) / 2;
178
+ }
179
+ }
@@ -0,0 +1,19 @@
1
+ export interface KeyInput {
2
+ key: string;
3
+ shiftKey: boolean;
4
+ ctrlKey: boolean;
5
+ metaKey: boolean;
6
+ altKey: boolean;
7
+ }
8
+ export interface KeyTarget {
9
+ barSpacing(): number;
10
+ plotCentre(): number;
11
+ panBy(dxPx: number): void;
12
+ zoomAt(x: number, factor: number): void;
13
+ scrollToLive(): void;
14
+ goToOldest(): void;
15
+ hideCrosshair(): void;
16
+ crosshairVisible(): boolean;
17
+ }
18
+ export declare const KEY_ZOOM = 1.25;
19
+ export declare function handleKey(k: KeyInput, t: KeyTarget): boolean;
@@ -0,0 +1,34 @@
1
+ export const KEY_ZOOM = 1.25;
2
+ export function handleKey(k, t) {
3
+ if (k.ctrlKey || k.metaKey || k.altKey)
4
+ return false;
5
+ switch (k.key) {
6
+ case 'ArrowLeft':
7
+ t.panBy(t.barSpacing() * (k.shiftKey ? 10 : 1));
8
+ return true;
9
+ case 'ArrowRight':
10
+ t.panBy(-t.barSpacing() * (k.shiftKey ? 10 : 1));
11
+ return true;
12
+ case '+':
13
+ case '=':
14
+ t.zoomAt(t.plotCentre(), KEY_ZOOM);
15
+ return true;
16
+ case '-':
17
+ case '_':
18
+ t.zoomAt(t.plotCentre(), 1 / KEY_ZOOM);
19
+ return true;
20
+ case 'End':
21
+ t.scrollToLive();
22
+ return true;
23
+ case 'Home':
24
+ t.goToOldest();
25
+ return true;
26
+ case 'Escape':
27
+ if (!t.crosshairVisible())
28
+ return false;
29
+ t.hideCrosshair();
30
+ return true;
31
+ default:
32
+ return false;
33
+ }
34
+ }
@@ -0,0 +1,15 @@
1
+ export interface WheelInput {
2
+ x: number;
3
+ deltaX: number;
4
+ deltaY: number;
5
+ deltaMode: number;
6
+ shiftKey: boolean;
7
+ }
8
+ export interface WheelTarget {
9
+ inPlot(x: number, y: number): boolean;
10
+ panBy(dxPx: number): void;
11
+ zoomAt(x: number, factor: number): void;
12
+ }
13
+ export declare function handleWheel(w: WheelInput & {
14
+ y: number;
15
+ }, t: WheelTarget): boolean;
@@ -0,0 +1,18 @@
1
+ const toPixels = (d, mode) => (mode === 1 ? d * 16 : mode === 2 ? d * 100 : d);
2
+ export function handleWheel(w, t) {
3
+ if (!t.inPlot(w.x, w.y))
4
+ return false;
5
+ const dx = toPixels(w.deltaX, w.deltaMode);
6
+ const dy = toPixels(w.deltaY, w.deltaMode);
7
+ if (w.shiftKey || Math.abs(dx) > Math.abs(dy)) {
8
+ const d = w.shiftKey && dx === 0 ? dy : dx;
9
+ if (d === 0)
10
+ return false;
11
+ t.panBy(-d);
12
+ return true;
13
+ }
14
+ if (dy === 0)
15
+ return false;
16
+ t.zoomAt(w.x, Math.exp(-dy * 0.002));
17
+ return true;
18
+ }
@@ -0,0 +1,17 @@
1
+ import type { CandleSeries } from '../core/series.ts';
2
+ import type { BarSource, Ohlc } from './frame.ts';
3
+ export declare class SeriesBars implements BarSource {
4
+ series: CandleSeries;
5
+ drawnClose: number | null;
6
+ constructor(series: CandleSeries);
7
+ at(i: number, out: Ohlc): void;
8
+ }
9
+ export declare class HeikinBars implements BarSource {
10
+ series: CandleSeries;
11
+ private ha;
12
+ private formingHa;
13
+ constructor(series: CandleSeries);
14
+ rebuild(): void;
15
+ refreshForming(): void;
16
+ at(i: number, out: Ohlc): void;
17
+ }