@invinite-org/chartlang-adapter-kit 1.4.0 → 1.6.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 (71) hide show
  1. package/CHANGELOG.md +158 -0
  2. package/dist/base/bufferingAdapter.d.ts +1 -0
  3. package/dist/base/bufferingAdapter.d.ts.map +1 -1
  4. package/dist/base/bufferingAdapter.js +1 -0
  5. package/dist/base/bufferingAdapter.js.map +1 -1
  6. package/dist/base/passThroughAdapter.d.ts +1 -0
  7. package/dist/base/passThroughAdapter.d.ts.map +1 -1
  8. package/dist/base/passThroughAdapter.js +1 -0
  9. package/dist/base/passThroughAdapter.js.map +1 -1
  10. package/dist/canvas/glyphs.d.ts +209 -0
  11. package/dist/canvas/glyphs.d.ts.map +1 -0
  12. package/dist/canvas/glyphs.js +219 -0
  13. package/dist/canvas/glyphs.js.map +1 -0
  14. package/dist/canvas/index.d.ts +2 -0
  15. package/dist/canvas/index.d.ts.map +1 -1
  16. package/dist/canvas/index.js +1 -0
  17. package/dist/canvas/index.js.map +1 -1
  18. package/dist/canvas/mockContext.d.ts +42 -0
  19. package/dist/canvas/mockContext.d.ts.map +1 -1
  20. package/dist/canvas/mockContext.js +50 -0
  21. package/dist/canvas/mockContext.js.map +1 -1
  22. package/dist/canvas/renderCtx.d.ts +6 -0
  23. package/dist/canvas/renderCtx.d.ts.map +1 -1
  24. package/dist/canvas/renderCtx.js.map +1 -1
  25. package/dist/capabilities/capabilities.d.ts +16 -0
  26. package/dist/capabilities/capabilities.d.ts.map +1 -1
  27. package/dist/capabilities/capabilities.js +16 -0
  28. package/dist/capabilities/capabilities.js.map +1 -1
  29. package/dist/defineAdapter.d.ts +2 -0
  30. package/dist/defineAdapter.d.ts.map +1 -1
  31. package/dist/defineAdapter.js +1 -0
  32. package/dist/defineAdapter.js.map +1 -1
  33. package/dist/geometry/index.d.ts +3 -0
  34. package/dist/geometry/index.d.ts.map +1 -1
  35. package/dist/geometry/index.js +2 -0
  36. package/dist/geometry/index.js.map +1 -1
  37. package/dist/geometry/renderOrder.d.ts +52 -0
  38. package/dist/geometry/renderOrder.d.ts.map +1 -0
  39. package/dist/geometry/renderOrder.js +35 -0
  40. package/dist/geometry/renderOrder.js.map +1 -0
  41. package/dist/geometry/shift.d.ts +117 -0
  42. package/dist/geometry/shift.d.ts.map +1 -0
  43. package/dist/geometry/shift.js +141 -0
  44. package/dist/geometry/shift.js.map +1 -0
  45. package/dist/index.d.ts +5 -2
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +3 -1
  48. package/dist/index.js.map +1 -1
  49. package/dist/interaction/domWiring.d.ts +90 -0
  50. package/dist/interaction/domWiring.d.ts.map +1 -0
  51. package/dist/interaction/domWiring.js +113 -0
  52. package/dist/interaction/domWiring.js.map +1 -0
  53. package/dist/interaction/index.d.ts +5 -0
  54. package/dist/interaction/index.d.ts.map +1 -0
  55. package/dist/interaction/index.js +5 -0
  56. package/dist/interaction/index.js.map +1 -0
  57. package/dist/interaction/viewController.d.ts +132 -0
  58. package/dist/interaction/viewController.d.ts.map +1 -0
  59. package/dist/interaction/viewController.js +133 -0
  60. package/dist/interaction/viewController.js.map +1 -0
  61. package/dist/mocks/mockCandleSource.d.ts +12 -0
  62. package/dist/mocks/mockCandleSource.d.ts.map +1 -1
  63. package/dist/mocks/mockCandleSource.js +13 -4
  64. package/dist/mocks/mockCandleSource.js.map +1 -1
  65. package/dist/types.d.ts +91 -8
  66. package/dist/types.d.ts.map +1 -1
  67. package/dist/types.js.map +1 -1
  68. package/dist/validation/validateEmission.d.ts.map +1 -1
  69. package/dist/validation/validateEmission.js +10 -0
  70. package/dist/validation/validateEmission.js.map +1 -1
  71. package/package.json +2 -2
@@ -0,0 +1,141 @@
1
+ // Copyright (c) 2026 Invinite. Licensed under the MIT License.
2
+ // See the LICENSE file in the repo root for full license text.
3
+ import { timeToX } from "./project.js";
4
+ // The bar-shift projection contract shared by every adapter. A
5
+ // `PlotEmission.xShift` (signed integer bars; `+n` right / future, `−n`
6
+ // left / past) displaces WHERE a series point draws, not its value. The
7
+ // canvas2d reference adapter originated this math; it is promoted here so
8
+ // the self-scaled (canvas2d, konva), category/index (echarts), and
9
+ // aligned/native-time (uplot, lightweight-charts) adapters all resolve a
10
+ // shifted point identically instead of hand-porting four divergent copies.
11
+ /**
12
+ * Median adjacent-bar time delta of a run, used to extrapolate the time
13
+ * of a bar that lies past the data edge (a future `+k` shift) or before
14
+ * the first bar (a far-past `−k` shift). Returns `0` for a run of fewer
15
+ * than two bars — a degenerate, zero-spacing run that
16
+ * {@link shiftedBarTime} maps to the anchor bar's own time (no NaN).
17
+ *
18
+ * @since 1.7
19
+ * @stable
20
+ * @example
21
+ * const s = medianBarSpacing([{ time: 0 }, { time: 10 }, { time: 30 }]);
22
+ * // s === 15 (median of deltas [10, 20])
23
+ * void s;
24
+ */
25
+ export function medianBarSpacing(bars) {
26
+ if (bars.length < 2)
27
+ return 0;
28
+ const deltas = [];
29
+ for (let i = 1; i < bars.length; i++) {
30
+ deltas.push(bars[i].time - bars[i - 1].time);
31
+ }
32
+ deltas.sort((a, b) => a - b);
33
+ const mid = deltas.length >> 1;
34
+ return deltas.length % 2 === 1 ? deltas[mid] : (deltas[mid - 1] + deltas[mid]) / 2;
35
+ }
36
+ /**
37
+ * Resolve the world time a series point computed at bar index `bar`
38
+ * should render at when displaced by `xShift` bars. The target index
39
+ * `j = bar + xShift`:
40
+ *
41
+ * - in range (`0 ≤ j ≤ last`) ⇒ that bar's exact `time`;
42
+ * - past the last bar (`j > last`) ⇒ extrapolated from the last bar's
43
+ * time and `spacing` (`+k` future projection);
44
+ * - before the first bar (`j < 0`) ⇒ extrapolated from the first bar's
45
+ * time and `spacing` (far-past `−k` projection).
46
+ *
47
+ * `xShift` omitted / `0` with an in-range `bar` returns the bar's own
48
+ * time, so the drawn x is byte-identical to the pre-shift
49
+ * `timeToX(point.time)`. An empty bar run returns `0`.
50
+ *
51
+ * @since 1.7
52
+ * @stable
53
+ * @example
54
+ * const bars = [{ time: 0 }, { time: 10 }, { time: 20 }];
55
+ * const t = shiftedBarTime({ bars, bar: 1, xShift: -1, spacing: 10 });
56
+ * // t === 0 (bar 1 shifted one left → bar 0's time)
57
+ * void t;
58
+ */
59
+ export function shiftedBarTime(args) {
60
+ const { bars, bar, spacing } = args;
61
+ const last = bars.length - 1;
62
+ if (last < 0)
63
+ return 0;
64
+ const j = bar + (args.xShift ?? 0);
65
+ if (j >= 0 && j <= last)
66
+ return bars[j].time;
67
+ if (j > last)
68
+ return bars[last].time + (j - last) * spacing;
69
+ return bars[0].time + j * spacing;
70
+ }
71
+ /**
72
+ * Map a series point's `bar` + `xShift` to an x pixel coordinate:
73
+ * {@link shiftedBarTime} resolves the displaced world time, then
74
+ * {@link timeToX} projects it into the viewport. The single projection
75
+ * funnel every shifted-series render path (line, step-line, histogram,
76
+ * shape / character / arrow glyphs) routes through so the bar-offset
77
+ * math is defined once. Pure on world inputs only — no `ctx`.
78
+ *
79
+ * @since 1.7
80
+ * @stable
81
+ * @example
82
+ * const bars = [{ time: 0 }, { time: 10 }, { time: 20 }];
83
+ * const vp = { xMin: 0, xMax: 20, yMin: 0, yMax: 1, pxWidth: 200, pxHeight: 1 };
84
+ * const x = projectShiftedX({ bars, bar: 0, xShift: 1, spacing: 10 }, vp);
85
+ * // x === 100 (bar 0 shifted one right → bar 1's x)
86
+ * void x;
87
+ */
88
+ export function projectShiftedX(args, viewport) {
89
+ return timeToX(shiftedBarTime(args), viewport);
90
+ }
91
+ /**
92
+ * The largest world time any of `points` reaches once its POSITIVE
93
+ * `xShift` is applied, never below the `xMax` seed. A self-scaled adapter
94
+ * widens its pane's data `xMax` by this value so a `+k` future-projected
95
+ * point stays inside the viewport instead of being clipped past the data
96
+ * edge. Only `xShift > 0` targets extend the edge — a far-past (`−k`)
97
+ * point is canvas-clipped at negative x and never widens the window, and
98
+ * an omitted / `0` shift contributes nothing. Pass the pane's current
99
+ * `xMax` (e.g. the last bar's time) as the seed; an empty bar run leaves
100
+ * the seed unchanged.
101
+ *
102
+ * @since 1.7
103
+ * @stable
104
+ * @example
105
+ * const bars = [{ time: 0 }, { time: 10 }, { time: 20 }];
106
+ * const pts = [{ bar: 2, xShift: 2 }, { bar: 0, xShift: -1 }];
107
+ * const xMax = maxShiftedTime(pts, bars, 10, 20);
108
+ * // xMax === 40 (bar 2 shifted +2 → time 40; the −1 point is ignored)
109
+ * void xMax;
110
+ */
111
+ export function maxShiftedTime(points, bars, spacing, xMax) {
112
+ let extended = xMax;
113
+ for (const point of points) {
114
+ const xShift = point.xShift;
115
+ if (xShift === undefined || xShift <= 0)
116
+ continue;
117
+ const t = shiftedBarTime({ bars, bar: point.bar, xShift, spacing });
118
+ if (t > extended)
119
+ extended = t;
120
+ }
121
+ return extended;
122
+ }
123
+ /**
124
+ * The (possibly out-of-range) category index a point computed at `bar`
125
+ * occupies when displaced by `xShift`: `bar + (xShift ?? 0)`. A
126
+ * category/index adapter (ECharts) writes the value at this column,
127
+ * extending its category axis by `max(0, index − lastIndex)` synthetic
128
+ * future slots and clipping a negative `index` (no negative category) —
129
+ * the index analogue of {@link shiftedBarTime}'s time extrapolation.
130
+ *
131
+ * @since 1.7
132
+ * @stable
133
+ * @example
134
+ * const j = shiftedBarIndex(3, 5);
135
+ * // j === 8
136
+ * void j;
137
+ */
138
+ export function shiftedBarIndex(bar, xShift) {
139
+ return bar + (xShift ?? 0);
140
+ }
141
+ //# sourceMappingURL=shift.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shift.js","sourceRoot":"","sources":["../../src/geometry/shift.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright (c) 2026 Invinite. Licensed under the MIT License.\n// See the LICENSE file in the repo root for full license text.\n\nimport { timeToX } from \"./project.js\";\nimport type { Viewport } from \"./types.js\";\n\n// The bar-shift projection contract shared by every adapter. A\n// `PlotEmission.xShift` (signed integer bars; `+n` right / future, `−n`\n// left / past) displaces WHERE a series point draws, not its value. The\n// canvas2d reference adapter originated this math; it is promoted here so\n// the self-scaled (canvas2d, konva), category/index (echarts), and\n// aligned/native-time (uplot, lightweight-charts) adapters all resolve a\n// shifted point identically instead of hand-porting four divergent copies.\n\n/**\n * Median adjacent-bar time delta of a run, used to extrapolate the time\n * of a bar that lies past the data edge (a future `+k` shift) or before\n * the first bar (a far-past `−k` shift). Returns `0` for a run of fewer\n * than two bars — a degenerate, zero-spacing run that\n * {@link shiftedBarTime} maps to the anchor bar's own time (no NaN).\n *\n * @since 1.7\n * @stable\n * @example\n * const s = medianBarSpacing([{ time: 0 }, { time: 10 }, { time: 30 }]);\n * // s === 15 (median of deltas [10, 20])\n * void s;\n */\nexport function medianBarSpacing(bars: ReadonlyArray<{ readonly time: number }>): number {\n if (bars.length < 2) return 0;\n const deltas: number[] = [];\n for (let i = 1; i < bars.length; i++) {\n deltas.push(bars[i].time - bars[i - 1].time);\n }\n deltas.sort((a, b) => a - b);\n const mid = deltas.length >> 1;\n return deltas.length % 2 === 1 ? deltas[mid] : (deltas[mid - 1] + deltas[mid]) / 2;\n}\n\n/**\n * Resolve the world time a series point computed at bar index `bar`\n * should render at when displaced by `xShift` bars. The target index\n * `j = bar + xShift`:\n *\n * - in range (`0 ≤ j ≤ last`) ⇒ that bar's exact `time`;\n * - past the last bar (`j > last`) ⇒ extrapolated from the last bar's\n * time and `spacing` (`+k` future projection);\n * - before the first bar (`j < 0`) ⇒ extrapolated from the first bar's\n * time and `spacing` (far-past `−k` projection).\n *\n * `xShift` omitted / `0` with an in-range `bar` returns the bar's own\n * time, so the drawn x is byte-identical to the pre-shift\n * `timeToX(point.time)`. An empty bar run returns `0`.\n *\n * @since 1.7\n * @stable\n * @example\n * const bars = [{ time: 0 }, { time: 10 }, { time: 20 }];\n * const t = shiftedBarTime({ bars, bar: 1, xShift: -1, spacing: 10 });\n * // t === 0 (bar 1 shifted one left → bar 0's time)\n * void t;\n */\nexport function shiftedBarTime(args: {\n readonly bars: ReadonlyArray<{ readonly time: number }>;\n readonly bar: number;\n readonly xShift: number | undefined;\n readonly spacing: number;\n}): number {\n const { bars, bar, spacing } = args;\n const last = bars.length - 1;\n if (last < 0) return 0;\n const j = bar + (args.xShift ?? 0);\n if (j >= 0 && j <= last) return bars[j].time;\n if (j > last) return bars[last].time + (j - last) * spacing;\n return bars[0].time + j * spacing;\n}\n\n/**\n * Map a series point's `bar` + `xShift` to an x pixel coordinate:\n * {@link shiftedBarTime} resolves the displaced world time, then\n * {@link timeToX} projects it into the viewport. The single projection\n * funnel every shifted-series render path (line, step-line, histogram,\n * shape / character / arrow glyphs) routes through so the bar-offset\n * math is defined once. Pure on world inputs only — no `ctx`.\n *\n * @since 1.7\n * @stable\n * @example\n * const bars = [{ time: 0 }, { time: 10 }, { time: 20 }];\n * const vp = { xMin: 0, xMax: 20, yMin: 0, yMax: 1, pxWidth: 200, pxHeight: 1 };\n * const x = projectShiftedX({ bars, bar: 0, xShift: 1, spacing: 10 }, vp);\n * // x === 100 (bar 0 shifted one right → bar 1's x)\n * void x;\n */\nexport function projectShiftedX(\n args: {\n readonly bars: ReadonlyArray<{ readonly time: number }>;\n readonly bar: number;\n readonly xShift: number | undefined;\n readonly spacing: number;\n },\n viewport: Viewport,\n): number {\n return timeToX(shiftedBarTime(args), viewport);\n}\n\n/**\n * The largest world time any of `points` reaches once its POSITIVE\n * `xShift` is applied, never below the `xMax` seed. A self-scaled adapter\n * widens its pane's data `xMax` by this value so a `+k` future-projected\n * point stays inside the viewport instead of being clipped past the data\n * edge. Only `xShift > 0` targets extend the edge — a far-past (`−k`)\n * point is canvas-clipped at negative x and never widens the window, and\n * an omitted / `0` shift contributes nothing. Pass the pane's current\n * `xMax` (e.g. the last bar's time) as the seed; an empty bar run leaves\n * the seed unchanged.\n *\n * @since 1.7\n * @stable\n * @example\n * const bars = [{ time: 0 }, { time: 10 }, { time: 20 }];\n * const pts = [{ bar: 2, xShift: 2 }, { bar: 0, xShift: -1 }];\n * const xMax = maxShiftedTime(pts, bars, 10, 20);\n * // xMax === 40 (bar 2 shifted +2 → time 40; the −1 point is ignored)\n * void xMax;\n */\nexport function maxShiftedTime(\n points: ReadonlyArray<{ readonly bar: number; readonly xShift?: number }>,\n bars: ReadonlyArray<{ readonly time: number }>,\n spacing: number,\n xMax: number,\n): number {\n let extended = xMax;\n for (const point of points) {\n const xShift = point.xShift;\n if (xShift === undefined || xShift <= 0) continue;\n const t = shiftedBarTime({ bars, bar: point.bar, xShift, spacing });\n if (t > extended) extended = t;\n }\n return extended;\n}\n\n/**\n * The (possibly out-of-range) category index a point computed at `bar`\n * occupies when displaced by `xShift`: `bar + (xShift ?? 0)`. A\n * category/index adapter (ECharts) writes the value at this column,\n * extending its category axis by `max(0, index − lastIndex)` synthetic\n * future slots and clipping a negative `index` (no negative category) —\n * the index analogue of {@link shiftedBarTime}'s time extrapolation.\n *\n * @since 1.7\n * @stable\n * @example\n * const j = shiftedBarIndex(3, 5);\n * // j === 8\n * void j;\n */\nexport function shiftedBarIndex(bar: number, xShift: number | undefined): number {\n return bar + (xShift ?? 0);\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { KIND_BUCKET, bucketFor } from "@invinite-org/chartlang-core";
1
+ export { KIND_BUCKET, bucketFor, feedKey } from "@invinite-org/chartlang-core";
2
2
  export type { DrawingBucket, DrawingState } from "@invinite-org/chartlang-core";
3
3
  export { defineAdapter } from "./defineAdapter.js";
4
4
  export type { DefineAdapterOpts } from "./defineAdapter.js";
@@ -9,6 +9,9 @@ export { mockCandleSource } from "./mocks/index.js";
9
9
  export type { MockCandleSourceMode, MockCandleSourceOpts } from "./mocks/index.js";
10
10
  export { BufferingAdapter, PassThroughAdapter } from "./base/index.js";
11
11
  export { decomposeDrawing, priceToY, timeToX, worldPointToPixel } from "./geometry/index.js";
12
- export type { DrawPrimitive, FillStyle, Point2, StrokeStyle, Viewport, } from "./geometry/index.js";
12
+ export { maxShiftedTime, medianBarSpacing, projectShiftedX, RENDER_BAND, shiftedBarIndex, shiftedBarTime, sortByRenderOrder, } from "./geometry/index.js";
13
+ export type { DrawPrimitive, FillStyle, Point2, RenderOrderKey, StrokeStyle, Viewport, } from "./geometry/index.js";
14
+ export { attachInteraction, createViewController, onDblCore, onDragCore, onWheelCore, yRangeInWindow, } from "./interaction/index.js";
15
+ export type { InteractionHandlers, ViewController, ViewControllerOpts, WindowYInput, XWindow, } from "./interaction/index.js";
13
16
  export type { Adapter, AdapterSymInfo, AlertChannel, AlertConditionEmission, AlertEmission, Capabilities, CandleEvent, DiagnosticCode, DrawingCounts, DrawingEmission, DrawingKind, InputKind, LogEmission, PlotEmission, PlotKind, PlotOverride, PlotSlotDescriptor, PlotStyle, RunnerEmissions, RuntimeDiagnostic, SymInfoField, } from "./types.js";
14
17
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AACtE,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAChF,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,YAAY,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC3E,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACxE,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAC5F,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,YAAY,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AACnF,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AACvE,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC7F,YAAY,EACR,aAAa,EACb,SAAS,EACT,MAAM,EACN,WAAW,EACX,QAAQ,GACX,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EACR,OAAO,EACP,cAAc,EACd,YAAY,EACZ,sBAAsB,EACtB,aAAa,EACb,YAAY,EACZ,WAAW,EACX,cAAc,EACd,aAAa,EACb,eAAe,EACf,WAAW,EACX,SAAS,EACT,WAAW,EACX,YAAY,EACZ,QAAQ,EACR,YAAY,EACZ,kBAAkB,EAClB,SAAS,EACT,eAAe,EACf,iBAAiB,EACjB,YAAY,GACf,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,8BAA8B,CAAC;AAC/E,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAChF,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,YAAY,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC3E,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACxE,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAC5F,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,YAAY,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AACnF,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AACvE,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC7F,OAAO,EACH,cAAc,EACd,gBAAgB,EAChB,eAAe,EACf,WAAW,EACX,eAAe,EACf,cAAc,EACd,iBAAiB,GACpB,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EACR,aAAa,EACb,SAAS,EACT,MAAM,EACN,cAAc,EACd,WAAW,EACX,QAAQ,GACX,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACH,iBAAiB,EACjB,oBAAoB,EACpB,SAAS,EACT,UAAU,EACV,WAAW,EACX,cAAc,GACjB,MAAM,wBAAwB,CAAC;AAChC,YAAY,EACR,mBAAmB,EACnB,cAAc,EACd,kBAAkB,EAClB,YAAY,EACZ,OAAO,GACV,MAAM,wBAAwB,CAAC;AAChC,YAAY,EACR,OAAO,EACP,cAAc,EACd,YAAY,EACZ,sBAAsB,EACtB,aAAa,EACb,YAAY,EACZ,WAAW,EACX,cAAc,EACd,aAAa,EACb,eAAe,EACf,WAAW,EACX,SAAS,EACT,WAAW,EACX,YAAY,EACZ,QAAQ,EACR,YAAY,EACZ,kBAAkB,EAClB,SAAS,EACT,eAAe,EACf,iBAAiB,EACjB,YAAY,GACf,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -1,10 +1,12 @@
1
1
  // Copyright (c) 2026 Invinite. Licensed under the MIT License.
2
2
  // See the LICENSE file in the repo root for full license text.
3
- export { KIND_BUCKET, bucketFor } from "@invinite-org/chartlang-core";
3
+ export { KIND_BUCKET, bucketFor, feedKey } from "@invinite-org/chartlang-core";
4
4
  export { defineAdapter } from "./defineAdapter.js";
5
5
  export { PHASE_5_PLOT_KINDS, capabilities } from "./capabilities/index.js";
6
6
  export { decodeDrawing, validateEmission } from "./validation/index.js";
7
7
  export { mockCandleSource } from "./mocks/index.js";
8
8
  export { BufferingAdapter, PassThroughAdapter } from "./base/index.js";
9
9
  export { decomposeDrawing, priceToY, timeToX, worldPointToPixel } from "./geometry/index.js";
10
+ export { maxShiftedTime, medianBarSpacing, projectShiftedX, RENDER_BAND, shiftedBarIndex, shiftedBarTime, sortByRenderOrder, } from "./geometry/index.js";
11
+ export { attachInteraction, createViewController, onDblCore, onDragCore, onWheelCore, yRangeInWindow, } from "./interaction/index.js";
10
12
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,+DAA+D;AAE/D,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AAEtE,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEnD,OAAO,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC3E,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAExE,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEpD,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AACvE,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC","sourcesContent":["// Copyright (c) 2026 Invinite. Licensed under the MIT License.\n// See the LICENSE file in the repo root for full license text.\n\nexport { KIND_BUCKET, bucketFor } from \"@invinite-org/chartlang-core\";\nexport type { DrawingBucket, DrawingState } from \"@invinite-org/chartlang-core\";\nexport { defineAdapter } from \"./defineAdapter.js\";\nexport type { DefineAdapterOpts } from \"./defineAdapter.js\";\nexport { PHASE_5_PLOT_KINDS, capabilities } from \"./capabilities/index.js\";\nexport { decodeDrawing, validateEmission } from \"./validation/index.js\";\nexport type { ValidationFail, ValidationOk, ValidationResult } from \"./validation/index.js\";\nexport { mockCandleSource } from \"./mocks/index.js\";\nexport type { MockCandleSourceMode, MockCandleSourceOpts } from \"./mocks/index.js\";\nexport { BufferingAdapter, PassThroughAdapter } from \"./base/index.js\";\nexport { decomposeDrawing, priceToY, timeToX, worldPointToPixel } from \"./geometry/index.js\";\nexport type {\n DrawPrimitive,\n FillStyle,\n Point2,\n StrokeStyle,\n Viewport,\n} from \"./geometry/index.js\";\nexport type {\n Adapter,\n AdapterSymInfo,\n AlertChannel,\n AlertConditionEmission,\n AlertEmission,\n Capabilities,\n CandleEvent,\n DiagnosticCode,\n DrawingCounts,\n DrawingEmission,\n DrawingKind,\n InputKind,\n LogEmission,\n PlotEmission,\n PlotKind,\n PlotOverride,\n PlotSlotDescriptor,\n PlotStyle,\n RunnerEmissions,\n RuntimeDiagnostic,\n SymInfoField,\n} from \"./types.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,+DAA+D;AAE/D,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,8BAA8B,CAAC;AAE/E,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEnD,OAAO,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC3E,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAExE,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEpD,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AACvE,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC7F,OAAO,EACH,cAAc,EACd,gBAAgB,EAChB,eAAe,EACf,WAAW,EACX,eAAe,EACf,cAAc,EACd,iBAAiB,GACpB,MAAM,qBAAqB,CAAC;AAS7B,OAAO,EACH,iBAAiB,EACjB,oBAAoB,EACpB,SAAS,EACT,UAAU,EACV,WAAW,EACX,cAAc,GACjB,MAAM,wBAAwB,CAAC","sourcesContent":["// Copyright (c) 2026 Invinite. Licensed under the MIT License.\n// See the LICENSE file in the repo root for full license text.\n\nexport { KIND_BUCKET, bucketFor, feedKey } from \"@invinite-org/chartlang-core\";\nexport type { DrawingBucket, DrawingState } from \"@invinite-org/chartlang-core\";\nexport { defineAdapter } from \"./defineAdapter.js\";\nexport type { DefineAdapterOpts } from \"./defineAdapter.js\";\nexport { PHASE_5_PLOT_KINDS, capabilities } from \"./capabilities/index.js\";\nexport { decodeDrawing, validateEmission } from \"./validation/index.js\";\nexport type { ValidationFail, ValidationOk, ValidationResult } from \"./validation/index.js\";\nexport { mockCandleSource } from \"./mocks/index.js\";\nexport type { MockCandleSourceMode, MockCandleSourceOpts } from \"./mocks/index.js\";\nexport { BufferingAdapter, PassThroughAdapter } from \"./base/index.js\";\nexport { decomposeDrawing, priceToY, timeToX, worldPointToPixel } from \"./geometry/index.js\";\nexport {\n maxShiftedTime,\n medianBarSpacing,\n projectShiftedX,\n RENDER_BAND,\n shiftedBarIndex,\n shiftedBarTime,\n sortByRenderOrder,\n} from \"./geometry/index.js\";\nexport type {\n DrawPrimitive,\n FillStyle,\n Point2,\n RenderOrderKey,\n StrokeStyle,\n Viewport,\n} from \"./geometry/index.js\";\nexport {\n attachInteraction,\n createViewController,\n onDblCore,\n onDragCore,\n onWheelCore,\n yRangeInWindow,\n} from \"./interaction/index.js\";\nexport type {\n InteractionHandlers,\n ViewController,\n ViewControllerOpts,\n WindowYInput,\n XWindow,\n} from \"./interaction/index.js\";\nexport type {\n Adapter,\n AdapterSymInfo,\n AlertChannel,\n AlertConditionEmission,\n AlertEmission,\n Capabilities,\n CandleEvent,\n DiagnosticCode,\n DrawingCounts,\n DrawingEmission,\n DrawingKind,\n InputKind,\n LogEmission,\n PlotEmission,\n PlotKind,\n PlotOverride,\n PlotSlotDescriptor,\n PlotStyle,\n RunnerEmissions,\n RuntimeDiagnostic,\n SymInfoField,\n} from \"./types.js\";\n"]}
@@ -0,0 +1,90 @@
1
+ import type { ViewController } from "./viewController.js";
2
+ /**
3
+ * The per-frame projection + data context {@link attachInteraction} needs
4
+ * to translate pixel gestures into world-x transforms. The adapter supplies
5
+ * these closing over its latest overlay `Viewport`, so the math always uses
6
+ * the current frame's scale.
7
+ *
8
+ * @since 1.6
9
+ * @stable
10
+ * @example
11
+ * declare const view: import("./viewController.js").ViewController;
12
+ * const h: InteractionHandlers = {
13
+ * controller: view,
14
+ * pxToWorldX: (px) => px,
15
+ * worldXPerPx: () => 1,
16
+ * dataBounds: () => ({ xMin: 0, xMax: 100 }),
17
+ * requestRender: () => {},
18
+ * };
19
+ * void h;
20
+ */
21
+ export type InteractionHandlers = {
22
+ /** The controller whose window the gestures mutate. */
23
+ readonly controller: ViewController;
24
+ /** Map a plot-area pixel x to a world x for the current frame. */
25
+ readonly pxToWorldX: (px: number) => number;
26
+ /** World-x units per pixel for the current frame (pan delta scale). */
27
+ readonly worldXPerPx: () => number;
28
+ /** The current data x extent (grows as bars stream in). */
29
+ readonly dataBounds: () => {
30
+ readonly xMin: number;
31
+ readonly xMax: number;
32
+ };
33
+ /** Re-render the adapter after a gesture mutates the window. */
34
+ readonly requestRender: () => void;
35
+ /** Override the wheel zoom sensitivity (default {@link DEFAULT_ZOOM_STEP}). */
36
+ readonly zoomStep?: number;
37
+ };
38
+ /**
39
+ * Apply one wheel notch: zoom the controller about the world-x under the
40
+ * cursor, then request a render. Pure on `(offsetX, deltaY)` so it is unit
41
+ * tested without a DOM event.
42
+ *
43
+ * @since 1.6
44
+ * @stable
45
+ * @example
46
+ * declare const h: InteractionHandlers;
47
+ * onWheelCore(h, 50, -120); // zoom in about pixel x=50
48
+ */
49
+ export declare function onWheelCore(h: InteractionHandlers, offsetX: number, deltaY: number): void;
50
+ /**
51
+ * Apply one drag step: pan the controller by the pixel delta converted to
52
+ * world x (dragging right reveals earlier data), then request a render.
53
+ * Pure on `dxPx`.
54
+ *
55
+ * @since 1.6
56
+ * @stable
57
+ * @example
58
+ * declare const h: InteractionHandlers;
59
+ * onDragCore(h, 12); // pan left by 12 px
60
+ */
61
+ export declare function onDragCore(h: InteractionHandlers, dxPx: number): void;
62
+ /**
63
+ * Apply a double-click: reset the controller to auto-follow, then request a
64
+ * render.
65
+ *
66
+ * @since 1.6
67
+ * @stable
68
+ * @example
69
+ * declare const h: InteractionHandlers;
70
+ * onDblCore(h);
71
+ */
72
+ export declare function onDblCore(h: InteractionHandlers): void;
73
+ /**
74
+ * Wire wheel-zoom, drag-pan, and double-click-reset on `el`, driving the
75
+ * supplied {@link InteractionHandlers}. Returns a detach function that
76
+ * removes every listener (call it from the adapter's `dispose`). The
77
+ * `addEventListener` plumbing is the only DOM-bound part; the decision
78
+ * logic lives in {@link onWheelCore} / {@link onDragCore} / {@link
79
+ * onDblCore}, which are unit tested directly.
80
+ *
81
+ * @since 1.6
82
+ * @stable
83
+ * @example
84
+ * declare const el: HTMLElement;
85
+ * declare const h: InteractionHandlers;
86
+ * const detach = attachInteraction(el, h);
87
+ * detach();
88
+ */
89
+ export declare function attachInteraction(el: HTMLElement, h: InteractionHandlers): () => void;
90
+ //# sourceMappingURL=domWiring.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"domWiring.d.ts","sourceRoot":"","sources":["../../src/interaction/domWiring.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAQ1D;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAC9B,uDAAuD;IACvD,QAAQ,CAAC,UAAU,EAAE,cAAc,CAAC;IACpC,kEAAkE;IAClE,QAAQ,CAAC,UAAU,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,CAAC;IAC5C,uEAAuE;IACvE,QAAQ,CAAC,WAAW,EAAE,MAAM,MAAM,CAAC;IACnC,2DAA2D;IAC3D,QAAQ,CAAC,UAAU,EAAE,MAAM;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5E,gEAAgE;IAChE,QAAQ,CAAC,aAAa,EAAE,MAAM,IAAI,CAAC;IACnC,+EAA+E;IAC/E,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC9B,CAAC;AAEF;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,mBAAmB,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAKzF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,mBAAmB,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAIrE;AAED;;;;;;;;;GASG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,mBAAmB,GAAG,IAAI,CAGtD;AAED;;;;;;;;;;;;;;;GAeG;AAEH,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,WAAW,EAAE,CAAC,EAAE,mBAAmB,GAAG,MAAM,IAAI,CAwCrF"}
@@ -0,0 +1,113 @@
1
+ // Copyright (c) 2026 Invinite. Licensed under the MIT License.
2
+ // See the LICENSE file in the repo root for full license text.
3
+ // Wheel notch → zoom factor sensitivity. A positive `deltaY` (scroll down)
4
+ // yields a factor > 1 (zoom out); negative (scroll up) yields < 1 (zoom in).
5
+ // `Math.exp` keeps both directions symmetric, so zoom-out always exists —
6
+ // the bug the native-default drag-zoom-in lacked.
7
+ const DEFAULT_ZOOM_STEP = 0.0015;
8
+ /**
9
+ * Apply one wheel notch: zoom the controller about the world-x under the
10
+ * cursor, then request a render. Pure on `(offsetX, deltaY)` so it is unit
11
+ * tested without a DOM event.
12
+ *
13
+ * @since 1.6
14
+ * @stable
15
+ * @example
16
+ * declare const h: InteractionHandlers;
17
+ * onWheelCore(h, 50, -120); // zoom in about pixel x=50
18
+ */
19
+ export function onWheelCore(h, offsetX, deltaY) {
20
+ const factor = Math.exp(deltaY * (h.zoomStep ?? DEFAULT_ZOOM_STEP));
21
+ const { xMin, xMax } = h.dataBounds();
22
+ h.controller.zoomAt(h.pxToWorldX(offsetX), factor, xMin, xMax);
23
+ h.requestRender();
24
+ }
25
+ /**
26
+ * Apply one drag step: pan the controller by the pixel delta converted to
27
+ * world x (dragging right reveals earlier data), then request a render.
28
+ * Pure on `dxPx`.
29
+ *
30
+ * @since 1.6
31
+ * @stable
32
+ * @example
33
+ * declare const h: InteractionHandlers;
34
+ * onDragCore(h, 12); // pan left by 12 px
35
+ */
36
+ export function onDragCore(h, dxPx) {
37
+ const { xMin, xMax } = h.dataBounds();
38
+ h.controller.panBy(-dxPx * h.worldXPerPx(), xMin, xMax);
39
+ h.requestRender();
40
+ }
41
+ /**
42
+ * Apply a double-click: reset the controller to auto-follow, then request a
43
+ * render.
44
+ *
45
+ * @since 1.6
46
+ * @stable
47
+ * @example
48
+ * declare const h: InteractionHandlers;
49
+ * onDblCore(h);
50
+ */
51
+ export function onDblCore(h) {
52
+ h.controller.reset();
53
+ h.requestRender();
54
+ }
55
+ /**
56
+ * Wire wheel-zoom, drag-pan, and double-click-reset on `el`, driving the
57
+ * supplied {@link InteractionHandlers}. Returns a detach function that
58
+ * removes every listener (call it from the adapter's `dispose`). The
59
+ * `addEventListener` plumbing is the only DOM-bound part; the decision
60
+ * logic lives in {@link onWheelCore} / {@link onDragCore} / {@link
61
+ * onDblCore}, which are unit tested directly.
62
+ *
63
+ * @since 1.6
64
+ * @stable
65
+ * @example
66
+ * declare const el: HTMLElement;
67
+ * declare const h: InteractionHandlers;
68
+ * const detach = attachInteraction(el, h);
69
+ * detach();
70
+ */
71
+ /* v8 ignore start -- DOM event plumbing; the pure cores above carry the coverage */
72
+ export function attachInteraction(el, h) {
73
+ let dragging = false;
74
+ let lastX = 0;
75
+ const onWheel = (e) => {
76
+ e.preventDefault();
77
+ onWheelCore(h, e.offsetX, e.deltaY);
78
+ };
79
+ const onPointerDown = (e) => {
80
+ dragging = true;
81
+ lastX = e.clientX;
82
+ el.setPointerCapture(e.pointerId);
83
+ };
84
+ const onPointerMove = (e) => {
85
+ if (!dragging)
86
+ return;
87
+ const dx = e.clientX - lastX;
88
+ lastX = e.clientX;
89
+ onDragCore(h, dx);
90
+ };
91
+ const onPointerUp = (e) => {
92
+ dragging = false;
93
+ if (el.hasPointerCapture(e.pointerId))
94
+ el.releasePointerCapture(e.pointerId);
95
+ };
96
+ const onDblClick = () => {
97
+ onDblCore(h);
98
+ };
99
+ el.addEventListener("wheel", onWheel, { passive: false });
100
+ el.addEventListener("pointerdown", onPointerDown);
101
+ el.addEventListener("pointermove", onPointerMove);
102
+ el.addEventListener("pointerup", onPointerUp);
103
+ el.addEventListener("dblclick", onDblClick);
104
+ return () => {
105
+ el.removeEventListener("wheel", onWheel);
106
+ el.removeEventListener("pointerdown", onPointerDown);
107
+ el.removeEventListener("pointermove", onPointerMove);
108
+ el.removeEventListener("pointerup", onPointerUp);
109
+ el.removeEventListener("dblclick", onDblClick);
110
+ };
111
+ }
112
+ /* v8 ignore stop */
113
+ //# sourceMappingURL=domWiring.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"domWiring.js","sourceRoot":"","sources":["../../src/interaction/domWiring.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright (c) 2026 Invinite. Licensed under the MIT License.\n// See the LICENSE file in the repo root for full license text.\n\nimport type { ViewController } from \"./viewController.js\";\n\n// Wheel notch → zoom factor sensitivity. A positive `deltaY` (scroll down)\n// yields a factor > 1 (zoom out); negative (scroll up) yields < 1 (zoom in).\n// `Math.exp` keeps both directions symmetric, so zoom-out always exists —\n// the bug the native-default drag-zoom-in lacked.\nconst DEFAULT_ZOOM_STEP = 0.0015;\n\n/**\n * The per-frame projection + data context {@link attachInteraction} needs\n * to translate pixel gestures into world-x transforms. The adapter supplies\n * these closing over its latest overlay `Viewport`, so the math always uses\n * the current frame's scale.\n *\n * @since 1.6\n * @stable\n * @example\n * declare const view: import(\"./viewController.js\").ViewController;\n * const h: InteractionHandlers = {\n * controller: view,\n * pxToWorldX: (px) => px,\n * worldXPerPx: () => 1,\n * dataBounds: () => ({ xMin: 0, xMax: 100 }),\n * requestRender: () => {},\n * };\n * void h;\n */\nexport type InteractionHandlers = {\n /** The controller whose window the gestures mutate. */\n readonly controller: ViewController;\n /** Map a plot-area pixel x to a world x for the current frame. */\n readonly pxToWorldX: (px: number) => number;\n /** World-x units per pixel for the current frame (pan delta scale). */\n readonly worldXPerPx: () => number;\n /** The current data x extent (grows as bars stream in). */\n readonly dataBounds: () => { readonly xMin: number; readonly xMax: number };\n /** Re-render the adapter after a gesture mutates the window. */\n readonly requestRender: () => void;\n /** Override the wheel zoom sensitivity (default {@link DEFAULT_ZOOM_STEP}). */\n readonly zoomStep?: number;\n};\n\n/**\n * Apply one wheel notch: zoom the controller about the world-x under the\n * cursor, then request a render. Pure on `(offsetX, deltaY)` so it is unit\n * tested without a DOM event.\n *\n * @since 1.6\n * @stable\n * @example\n * declare const h: InteractionHandlers;\n * onWheelCore(h, 50, -120); // zoom in about pixel x=50\n */\nexport function onWheelCore(h: InteractionHandlers, offsetX: number, deltaY: number): void {\n const factor = Math.exp(deltaY * (h.zoomStep ?? DEFAULT_ZOOM_STEP));\n const { xMin, xMax } = h.dataBounds();\n h.controller.zoomAt(h.pxToWorldX(offsetX), factor, xMin, xMax);\n h.requestRender();\n}\n\n/**\n * Apply one drag step: pan the controller by the pixel delta converted to\n * world x (dragging right reveals earlier data), then request a render.\n * Pure on `dxPx`.\n *\n * @since 1.6\n * @stable\n * @example\n * declare const h: InteractionHandlers;\n * onDragCore(h, 12); // pan left by 12 px\n */\nexport function onDragCore(h: InteractionHandlers, dxPx: number): void {\n const { xMin, xMax } = h.dataBounds();\n h.controller.panBy(-dxPx * h.worldXPerPx(), xMin, xMax);\n h.requestRender();\n}\n\n/**\n * Apply a double-click: reset the controller to auto-follow, then request a\n * render.\n *\n * @since 1.6\n * @stable\n * @example\n * declare const h: InteractionHandlers;\n * onDblCore(h);\n */\nexport function onDblCore(h: InteractionHandlers): void {\n h.controller.reset();\n h.requestRender();\n}\n\n/**\n * Wire wheel-zoom, drag-pan, and double-click-reset on `el`, driving the\n * supplied {@link InteractionHandlers}. Returns a detach function that\n * removes every listener (call it from the adapter's `dispose`). The\n * `addEventListener` plumbing is the only DOM-bound part; the decision\n * logic lives in {@link onWheelCore} / {@link onDragCore} / {@link\n * onDblCore}, which are unit tested directly.\n *\n * @since 1.6\n * @stable\n * @example\n * declare const el: HTMLElement;\n * declare const h: InteractionHandlers;\n * const detach = attachInteraction(el, h);\n * detach();\n */\n/* v8 ignore start -- DOM event plumbing; the pure cores above carry the coverage */\nexport function attachInteraction(el: HTMLElement, h: InteractionHandlers): () => void {\n let dragging = false;\n let lastX = 0;\n\n const onWheel = (e: WheelEvent): void => {\n e.preventDefault();\n onWheelCore(h, e.offsetX, e.deltaY);\n };\n const onPointerDown = (e: PointerEvent): void => {\n dragging = true;\n lastX = e.clientX;\n el.setPointerCapture(e.pointerId);\n };\n const onPointerMove = (e: PointerEvent): void => {\n if (!dragging) return;\n const dx = e.clientX - lastX;\n lastX = e.clientX;\n onDragCore(h, dx);\n };\n const onPointerUp = (e: PointerEvent): void => {\n dragging = false;\n if (el.hasPointerCapture(e.pointerId)) el.releasePointerCapture(e.pointerId);\n };\n const onDblClick = (): void => {\n onDblCore(h);\n };\n\n el.addEventListener(\"wheel\", onWheel, { passive: false });\n el.addEventListener(\"pointerdown\", onPointerDown);\n el.addEventListener(\"pointermove\", onPointerMove);\n el.addEventListener(\"pointerup\", onPointerUp);\n el.addEventListener(\"dblclick\", onDblClick);\n\n return (): void => {\n el.removeEventListener(\"wheel\", onWheel);\n el.removeEventListener(\"pointerdown\", onPointerDown);\n el.removeEventListener(\"pointermove\", onPointerMove);\n el.removeEventListener(\"pointerup\", onPointerUp);\n el.removeEventListener(\"dblclick\", onDblClick);\n };\n}\n/* v8 ignore stop */\n"]}
@@ -0,0 +1,5 @@
1
+ export { createViewController, yRangeInWindow } from "./viewController.js";
2
+ export type { ViewController, ViewControllerOpts, WindowYInput, XWindow, } from "./viewController.js";
3
+ export { attachInteraction, onDblCore, onDragCore, onWheelCore } from "./domWiring.js";
4
+ export type { InteractionHandlers } from "./domWiring.js";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/interaction/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC3E,YAAY,EACR,cAAc,EACd,kBAAkB,EAClB,YAAY,EACZ,OAAO,GACV,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,iBAAiB,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AACvF,YAAY,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC"}
@@ -0,0 +1,5 @@
1
+ // Copyright (c) 2026 Invinite. Licensed under the MIT License.
2
+ // See the LICENSE file in the repo root for full license text.
3
+ export { createViewController, yRangeInWindow } from "./viewController.js";
4
+ export { attachInteraction, onDblCore, onDragCore, onWheelCore } from "./domWiring.js";
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/interaction/index.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,+DAA+D;AAE/D,OAAO,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAO3E,OAAO,EAAE,iBAAiB,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC","sourcesContent":["// Copyright (c) 2026 Invinite. Licensed under the MIT License.\n// See the LICENSE file in the repo root for full license text.\n\nexport { createViewController, yRangeInWindow } from \"./viewController.js\";\nexport type {\n ViewController,\n ViewControllerOpts,\n WindowYInput,\n XWindow,\n} from \"./viewController.js\";\nexport { attachInteraction, onDblCore, onDragCore, onWheelCore } from \"./domWiring.js\";\nexport type { InteractionHandlers } from \"./domWiring.js\";\n"]}
@@ -0,0 +1,132 @@
1
+ /**
2
+ * A visible window into the world x axis (bar times in UTC ms). The two
3
+ * self-scaled adapters (canvas2d, konva) resolve one per frame from a
4
+ * {@link ViewController} and feed `xMin`/`xMax` into their `Viewport`.
5
+ *
6
+ * @since 1.6
7
+ * @stable
8
+ * @example
9
+ * const win: XWindow = { xMin: 0, xMax: 100 };
10
+ * void win;
11
+ */
12
+ export type XWindow = {
13
+ readonly xMin: number;
14
+ readonly xMax: number;
15
+ };
16
+ /**
17
+ * Construction options for {@link createViewController}. `minSpan` is the
18
+ * smallest world-x span a zoom-in can reach (so the window never collapses
19
+ * to a point); `maxSpanFactor` is the largest span a zoom-out can reach as
20
+ * a multiple of the current data span (default `1` ⇒ cannot zoom out past
21
+ * "all data visible").
22
+ *
23
+ * @since 1.6
24
+ * @stable
25
+ * @example
26
+ * const opts: ViewControllerOpts = { minSpan: 1, maxSpanFactor: 1 };
27
+ * void opts;
28
+ */
29
+ export type ViewControllerOpts = {
30
+ readonly minSpan?: number;
31
+ readonly maxSpanFactor?: number;
32
+ };
33
+ /**
34
+ * Stateful, library-agnostic pan/zoom controller for an adapter that
35
+ * computes its own x scale every frame. It holds a user x-window plus a
36
+ * `userInteracted` flag: until the user wheels or drags, {@link
37
+ * ViewController.resolveXWindow} returns the auto-follow window (the full
38
+ * data range, or the framed `autoFollowXMin` window); after the first
39
+ * interaction it returns the held window (re-clamped as data grows), so
40
+ * live frames stop snapping the view back. The first {@link
41
+ * ViewController.zoomAt}/{@link ViewController.panBy} seeds the held window
42
+ * from the window last returned by {@link ViewController.resolveXWindow}
43
+ * (what the user is currently looking at), so leaving auto-follow keeps the
44
+ * framed view rather than jumping to the full data range.
45
+ *
46
+ * All transforms are pure functions of the current state + the supplied
47
+ * data bounds — there is no DOM or library coupling. The example adapters
48
+ * wire DOM events to {@link ViewController.zoomAt} / {@link
49
+ * ViewController.panBy} / {@link ViewController.reset} via {@link
50
+ * attachInteraction}.
51
+ *
52
+ * @since 1.6
53
+ * @stable
54
+ * @example
55
+ * const view = createViewController();
56
+ * view.resolveXWindow(0, 100); // { xMin: 0, xMax: 100 } (auto-follow)
57
+ * view.panBy(10, 0, 100);
58
+ * view.userInteracted; // true
59
+ */
60
+ export type ViewController = {
61
+ /** `true` once the user has zoomed or panned (auto-follow is paused). */
62
+ readonly userInteracted: boolean;
63
+ /**
64
+ * The x-window to render this frame. While not interacted (auto-follow),
65
+ * returns `[autoFollowXMin ?? dataXMin, dataXMax]` — pass `autoFollowXMin`
66
+ * (e.g. the time of the Nth-from-last bar) to frame only the most recent
67
+ * bars by default while keeping the full history scrollable; omit it to
68
+ * fit all data. After the first interaction it returns the held window
69
+ * clamped into the current data bounds (`autoFollowXMin` is then ignored).
70
+ */
71
+ resolveXWindow(dataXMin: number, dataXMax: number, autoFollowXMin?: number): XWindow;
72
+ /**
73
+ * Zoom by `factor` (`<1` in, `>1` out) about a world-x pivot. The first
74
+ * call seeds the held window from the last resolved window (the current
75
+ * view), falling back to the full data range if nothing has rendered yet.
76
+ */
77
+ zoomAt(pivotX: number, factor: number, dataXMin: number, dataXMax: number): void;
78
+ /** Pan the window by a signed world-x delta. */
79
+ panBy(deltaWorldX: number, dataXMin: number, dataXMax: number): void;
80
+ /** Clear the held window + flag so the view auto-follows again. */
81
+ reset(): void;
82
+ };
83
+ /**
84
+ * Build a {@link ViewController}. Pass `opts` to tune the zoom-in floor
85
+ * (`minSpan`) and zoom-out ceiling (`maxSpanFactor`); both default to a
86
+ * "1 ms floor, all-data ceiling" policy.
87
+ *
88
+ * @since 1.6
89
+ * @stable
90
+ * @example
91
+ * const view = createViewController({ minSpan: 2, maxSpanFactor: 1 });
92
+ * view.zoomAt(50, 0.5, 0, 100); // zoom in 2× about x=50
93
+ * void view.resolveXWindow(0, 100);
94
+ */
95
+ export declare function createViewController(opts?: ViewControllerOpts): ViewController;
96
+ /**
97
+ * One candidate row for {@link yRangeInWindow}: a world `x` (bar time) plus
98
+ * the low / high values to fold into the y range when `x` is inside the
99
+ * window. Bars pass `{ x: time, lo: low, hi: high }`; a scalar series point
100
+ * passes `lo === hi === value`.
101
+ *
102
+ * @since 1.6
103
+ * @stable
104
+ * @example
105
+ * const c: WindowYInput = { x: 10, lo: 99, hi: 101 };
106
+ * void c;
107
+ */
108
+ export type WindowYInput = {
109
+ readonly x: number;
110
+ readonly lo: number;
111
+ readonly hi: number;
112
+ };
113
+ /**
114
+ * Fold the y range of every candidate whose `x` falls inside `win` — the
115
+ * shared "auto-fit the price scale to the visible window" helper (matching
116
+ * lightweight-charts' auto price scale). Non-finite `lo`/`hi` rows are
117
+ * skipped. Returns `undefined` when no finite in-window candidate is seen,
118
+ * so the caller keeps its own degenerate-range fallback. Horizontal lines
119
+ * (no `x`) are folded in by the caller, not here.
120
+ *
121
+ * @since 1.6
122
+ * @stable
123
+ * @example
124
+ * const r = yRangeInWindow([{ x: 5, lo: 1, hi: 3 }], { xMin: 0, xMax: 10 });
125
+ * // r === { yMin: 1, yMax: 3 }
126
+ * void r;
127
+ */
128
+ export declare function yRangeInWindow(candidates: Iterable<WindowYInput>, win: XWindow): {
129
+ readonly yMin: number;
130
+ readonly yMax: number;
131
+ } | undefined;
132
+ //# sourceMappingURL=viewController.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"viewController.d.ts","sourceRoot":"","sources":["../../src/interaction/viewController.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;GAUG;AACH,MAAM,MAAM,OAAO,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvE;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,kBAAkB,GAAG;IAC7B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;CACnC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,MAAM,cAAc,GAAG;IACzB,yEAAyE;IACzE,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC;;;;;;;OAOG;IACH,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,cAAc,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACrF;;;;OAIG;IACH,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjF,gDAAgD;IAChD,KAAK,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACrE,mEAAmE;IACnE,KAAK,IAAI,IAAI,CAAC;CACjB,CAAC;AAsCF;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,CAAC,EAAE,kBAAkB,GAAG,cAAc,CAsE9E;AAED;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,YAAY,GAAG;IAAE,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;AAE5F;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAC1B,UAAU,EAAE,QAAQ,CAAC,YAAY,CAAC,EAClC,GAAG,EAAE,OAAO,GACb;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAW9D"}