@zuttora/charts 0.0.0-stage → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +62 -2
- package/dist/chart.d.ts +8 -0
- package/dist/chart.js +238 -0
- package/dist/chartState.d.ts +47 -0
- package/dist/chartState.js +64 -0
- package/dist/core/drawings.d.ts +21 -0
- package/dist/core/drawings.js +46 -0
- package/dist/core/emitter.d.ts +8 -0
- package/dist/core/emitter.js +26 -0
- package/dist/core/format.d.ts +9 -0
- package/dist/core/format.js +35 -0
- package/dist/core/heikinAshi.d.ts +2 -0
- package/dist/core/heikinAshi.js +14 -0
- package/dist/core/indicatorSet.d.ts +20 -0
- package/dist/core/indicatorSet.js +66 -0
- package/dist/core/indicators.d.ts +53 -0
- package/dist/core/indicators.js +187 -0
- package/dist/core/priceScale.d.ts +22 -0
- package/dist/core/priceScale.js +92 -0
- package/dist/core/series.d.ts +41 -0
- package/dist/core/series.js +221 -0
- package/dist/core/time.d.ts +25 -0
- package/dist/core/time.js +44 -0
- package/dist/core/timeLabels.d.ts +11 -0
- package/dist/core/timeLabels.js +52 -0
- package/dist/core/types.d.ts +134 -0
- package/dist/core/types.js +1 -0
- package/dist/core/viewport.d.ts +41 -0
- package/dist/core/viewport.js +116 -0
- package/dist/core/warn.d.ts +1 -0
- package/dist/core/warn.js +3 -0
- package/dist/createChart.d.ts +2 -0
- package/dist/createChart.js +7 -0
- package/dist/dom/environment.d.ts +11 -0
- package/dist/dom/environment.js +49 -0
- package/dist/dom/indicatorReadings.d.ts +7 -0
- package/dist/dom/indicatorReadings.js +26 -0
- package/dist/dom/legend.d.ts +19 -0
- package/dist/dom/legend.js +62 -0
- package/dist/dom/legendSync.d.ts +5 -0
- package/dist/dom/legendSync.js +30 -0
- package/dist/dom/root.d.ts +9 -0
- package/dist/dom/root.js +19 -0
- package/dist/draw/controller.d.ts +36 -0
- package/dist/draw/controller.js +151 -0
- package/dist/draw/geometry.d.ts +44 -0
- package/dist/draw/geometry.js +118 -0
- package/dist/draw/mapper.d.ts +30 -0
- package/dist/draw/mapper.js +32 -0
- package/dist/draw/pointer.d.ts +17 -0
- package/dist/draw/pointer.js +144 -0
- package/dist/draw/state.d.ts +61 -0
- package/dist/draw/state.js +8 -0
- package/dist/drawApi.d.ts +20 -0
- package/dist/drawApi.js +59 -0
- package/dist/host.d.ts +35 -0
- package/dist/host.js +63 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +5 -0
- package/dist/interact/attach.d.ts +2 -0
- package/dist/interact/attach.js +142 -0
- package/dist/interact/gestures.d.ts +47 -0
- package/dist/interact/gestures.js +179 -0
- package/dist/interact/keyboard.d.ts +19 -0
- package/dist/interact/keyboard.js +34 -0
- package/dist/interact/wheel.d.ts +15 -0
- package/dist/interact/wheel.js +18 -0
- package/dist/render/bars.d.ts +17 -0
- package/dist/render/bars.js +55 -0
- package/dist/render/canvas.d.ts +2 -0
- package/dist/render/canvas.js +13 -0
- package/dist/render/drawAxes.d.ts +2 -0
- package/dist/render/drawAxes.js +34 -0
- package/dist/render/drawCandles.d.ts +3 -0
- package/dist/render/drawCandles.js +68 -0
- package/dist/render/drawCrosshair.d.ts +7 -0
- package/dist/render/drawCrosshair.js +42 -0
- package/dist/render/drawDrawings.d.ts +10 -0
- package/dist/render/drawDrawings.js +146 -0
- package/dist/render/drawGrid.d.ts +2 -0
- package/dist/render/drawGrid.js +22 -0
- package/dist/render/drawIndicators.d.ts +4 -0
- package/dist/render/drawIndicators.js +67 -0
- package/dist/render/drawLastPrice.d.ts +10 -0
- package/dist/render/drawLastPrice.js +53 -0
- package/dist/render/drawLine.d.ts +3 -0
- package/dist/render/drawLine.js +43 -0
- package/dist/render/drawMain.d.ts +2 -0
- package/dist/render/drawMain.js +43 -0
- package/dist/render/drawMarkers.d.ts +2 -0
- package/dist/render/drawMarkers.js +39 -0
- package/dist/render/drawPriceLines.d.ts +4 -0
- package/dist/render/drawPriceLines.js +60 -0
- package/dist/render/drawSubPanes.d.ts +2 -0
- package/dist/render/drawSubPanes.js +104 -0
- package/dist/render/frame.d.ts +57 -0
- package/dist/render/frame.js +5 -0
- package/dist/render/frameBuilder.d.ts +12 -0
- package/dist/render/frameBuilder.js +129 -0
- package/dist/render/indicatorFrame.d.ts +17 -0
- package/dist/render/indicatorFrame.js +55 -0
- package/dist/render/layout.d.ts +4 -0
- package/dist/render/layout.js +26 -0
- package/dist/render/paintFrame.d.ts +11 -0
- package/dist/render/paintFrame.js +33 -0
- package/dist/render/scheduler.d.ts +17 -0
- package/dist/render/scheduler.js +38 -0
- package/dist/render/tween.d.ts +12 -0
- package/dist/render/tween.js +38 -0
- package/dist/theme.d.ts +2 -0
- package/dist/theme.js +16 -0
- package/package.json +37 -4
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
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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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,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
|
+
}
|