@tranding-custom-chart/renderer-canvas 0.0.0-stage → 2.0.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 (104) hide show
  1. package/README.md +40 -2
  2. package/dist/canvas-engine.d.ts +82 -0
  3. package/dist/canvas-engine.d.ts.map +1 -0
  4. package/dist/canvas-engine.js +594 -0
  5. package/dist/dpi.d.ts +31 -0
  6. package/dist/dpi.d.ts.map +1 -0
  7. package/dist/dpi.js +65 -0
  8. package/dist/index.d.ts +38 -0
  9. package/dist/index.d.ts.map +1 -0
  10. package/dist/index.js +37 -0
  11. package/dist/renderers/bar-renderer.d.ts +26 -0
  12. package/dist/renderers/bar-renderer.d.ts.map +1 -0
  13. package/dist/renderers/bar-renderer.js +45 -0
  14. package/dist/renderers/baseline-renderer.d.ts +20 -0
  15. package/dist/renderers/baseline-renderer.d.ts.map +1 -0
  16. package/dist/renderers/baseline-renderer.js +101 -0
  17. package/dist/renderers/box-plot-renderer.d.ts +14 -0
  18. package/dist/renderers/box-plot-renderer.d.ts.map +1 -0
  19. package/dist/renderers/box-plot-renderer.js +76 -0
  20. package/dist/renderers/bubble-renderer.d.ts +14 -0
  21. package/dist/renderers/bubble-renderer.d.ts.map +1 -0
  22. package/dist/renderers/bubble-renderer.js +37 -0
  23. package/dist/renderers/candlestick-renderer.d.ts +18 -0
  24. package/dist/renderers/candlestick-renderer.d.ts.map +1 -0
  25. package/dist/renderers/candlestick-renderer.js +87 -0
  26. package/dist/renderers/columns-renderer.d.ts +21 -0
  27. package/dist/renderers/columns-renderer.d.ts.map +1 -0
  28. package/dist/renderers/columns-renderer.js +35 -0
  29. package/dist/renderers/crosshair-renderer.d.ts +19 -0
  30. package/dist/renderers/crosshair-renderer.d.ts.map +1 -0
  31. package/dist/renderers/crosshair-renderer.js +71 -0
  32. package/dist/renderers/curved-line-renderer.d.ts +17 -0
  33. package/dist/renderers/curved-line-renderer.d.ts.map +1 -0
  34. package/dist/renderers/curved-line-renderer.js +46 -0
  35. package/dist/renderers/drawing-renderer.d.ts +19 -0
  36. package/dist/renderers/drawing-renderer.d.ts.map +1 -0
  37. package/dist/renderers/drawing-renderer.js +487 -0
  38. package/dist/renderers/grid-renderer.d.ts +26 -0
  39. package/dist/renderers/grid-renderer.d.ts.map +1 -0
  40. package/dist/renderers/grid-renderer.js +103 -0
  41. package/dist/renderers/heatmap-renderer.d.ts +16 -0
  42. package/dist/renderers/heatmap-renderer.d.ts.map +1 -0
  43. package/dist/renderers/heatmap-renderer.js +80 -0
  44. package/dist/renderers/high-low-renderer.d.ts +19 -0
  45. package/dist/renderers/high-low-renderer.d.ts.map +1 -0
  46. package/dist/renderers/high-low-renderer.js +28 -0
  47. package/dist/renderers/hlc-area-renderer.d.ts +22 -0
  48. package/dist/renderers/hlc-area-renderer.d.ts.map +1 -0
  49. package/dist/renderers/hlc-area-renderer.js +72 -0
  50. package/dist/renderers/hollow-candlestick-renderer.d.ts +26 -0
  51. package/dist/renderers/hollow-candlestick-renderer.d.ts.map +1 -0
  52. package/dist/renderers/hollow-candlestick-renderer.js +71 -0
  53. package/dist/renderers/horizontal-bar-renderer.d.ts +25 -0
  54. package/dist/renderers/horizontal-bar-renderer.d.ts.map +1 -0
  55. package/dist/renderers/horizontal-bar-renderer.js +52 -0
  56. package/dist/renderers/last-price-renderer.d.ts +17 -0
  57. package/dist/renderers/last-price-renderer.d.ts.map +1 -0
  58. package/dist/renderers/last-price-renderer.js +46 -0
  59. package/dist/renderers/line-renderer.d.ts +23 -0
  60. package/dist/renderers/line-renderer.d.ts.map +1 -0
  61. package/dist/renderers/line-renderer.js +80 -0
  62. package/dist/renderers/line-with-markers-renderer.d.ts +13 -0
  63. package/dist/renderers/line-with-markers-renderer.d.ts.map +1 -0
  64. package/dist/renderers/line-with-markers-renderer.js +35 -0
  65. package/dist/renderers/marker-renderer.d.ts +16 -0
  66. package/dist/renderers/marker-renderer.d.ts.map +1 -0
  67. package/dist/renderers/marker-renderer.js +91 -0
  68. package/dist/renderers/mixed-renderer.d.ts +18 -0
  69. package/dist/renderers/mixed-renderer.d.ts.map +1 -0
  70. package/dist/renderers/mixed-renderer.js +123 -0
  71. package/dist/renderers/ohlc-bar-renderer.d.ts +21 -0
  72. package/dist/renderers/ohlc-bar-renderer.d.ts.map +1 -0
  73. package/dist/renderers/ohlc-bar-renderer.js +73 -0
  74. package/dist/renderers/pie-renderer.d.ts +26 -0
  75. package/dist/renderers/pie-renderer.d.ts.map +1 -0
  76. package/dist/renderers/pie-renderer.js +71 -0
  77. package/dist/renderers/polar-area-renderer.d.ts +13 -0
  78. package/dist/renderers/polar-area-renderer.d.ts.map +1 -0
  79. package/dist/renderers/polar-area-renderer.js +44 -0
  80. package/dist/renderers/radar-renderer.d.ts +16 -0
  81. package/dist/renderers/radar-renderer.d.ts.map +1 -0
  82. package/dist/renderers/radar-renderer.js +99 -0
  83. package/dist/renderers/scatter-renderer.d.ts +14 -0
  84. package/dist/renderers/scatter-renderer.d.ts.map +1 -0
  85. package/dist/renderers/scatter-renderer.js +42 -0
  86. package/dist/renderers/stacked-bar-renderer.d.ts +14 -0
  87. package/dist/renderers/stacked-bar-renderer.d.ts.map +1 -0
  88. package/dist/renderers/stacked-bar-renderer.js +52 -0
  89. package/dist/renderers/step-line-renderer.d.ts +19 -0
  90. package/dist/renderers/step-line-renderer.d.ts.map +1 -0
  91. package/dist/renderers/step-line-renderer.js +52 -0
  92. package/dist/renderers/supertrend-renderer.d.ts +18 -0
  93. package/dist/renderers/supertrend-renderer.d.ts.map +1 -0
  94. package/dist/renderers/supertrend-renderer.js +80 -0
  95. package/dist/renderers/volume-profile-renderer.d.ts +25 -0
  96. package/dist/renderers/volume-profile-renderer.d.ts.map +1 -0
  97. package/dist/renderers/volume-profile-renderer.js +83 -0
  98. package/dist/renderers/watermark-renderer.d.ts +15 -0
  99. package/dist/renderers/watermark-renderer.d.ts.map +1 -0
  100. package/dist/renderers/watermark-renderer.js +26 -0
  101. package/dist/scheduler.d.ts +25 -0
  102. package/dist/scheduler.d.ts.map +1 -0
  103. package/dist/scheduler.js +77 -0
  104. package/package.json +33 -4
@@ -0,0 +1,594 @@
1
+ /**
2
+ * @file packages/chart-renderer-canvas/src/canvas-engine.ts
3
+ * High-performance 2D Canvas Chart Engine.
4
+ * Manages layer creation, High-DPI backing store, rAF scheduling, and disposal.
5
+ */
6
+ import { DEFAULT_DARK_THEME, PluginManager, resolveTheme, ChartEventEmitter, } from '@tranding-custom-chart/core';
7
+ import { setupCanvasDpi, getDevicePixelRatio } from './dpi.js';
8
+ import { RenderScheduler } from './scheduler.js';
9
+ export class CanvasChartEngine {
10
+ _container;
11
+ _bgCanvas;
12
+ _overlayCanvas;
13
+ _bgCtx;
14
+ _overlayCtx;
15
+ _scheduler;
16
+ _pluginManager = new PluginManager();
17
+ _emitter = new ChartEventEmitter();
18
+ _chartId = 'trend-chart-' + Math.random().toString(36).slice(2, 9);
19
+ _hitTestResolver = null;
20
+ _isPointerDown = false;
21
+ _hasDragged = false;
22
+ _dragStartPos = null;
23
+ _lastHoverTarget = null;
24
+ _width;
25
+ _height;
26
+ _padding;
27
+ _theme;
28
+ _dpr = 1;
29
+ _disposed = false;
30
+ _xScale = null;
31
+ _yScale = null;
32
+ _onHover;
33
+ _drawDataCallback;
34
+ _drawOverlayCallback;
35
+ constructor(config) {
36
+ this._container = config.container;
37
+ this._width = config.width ?? Math.max(100, config.container.clientWidth || 600);
38
+ this._height = config.height ?? Math.max(100, config.container.clientHeight || 400);
39
+ this._padding = config.padding ?? { top: 20, right: 65, bottom: 30, left: 15 };
40
+ this._theme = config.theme ?? DEFAULT_DARK_THEME;
41
+ this._onHover = config.onHover;
42
+ // Create Background / Data Canvas
43
+ this._bgCanvas = document.createElement('canvas');
44
+ this._bgCanvas.style.position = 'absolute';
45
+ this._bgCanvas.style.top = '0';
46
+ this._bgCanvas.style.left = '0';
47
+ this._bgCanvas.style.pointerEvents = 'none';
48
+ // Create Interactive Overlay Canvas
49
+ this._overlayCanvas = document.createElement('canvas');
50
+ this._overlayCanvas.style.position = 'absolute';
51
+ this._overlayCanvas.style.top = '0';
52
+ this._overlayCanvas.style.left = '0';
53
+ this._overlayCanvas.style.touchAction = 'none';
54
+ const createFallbackContext = () => {
55
+ const noop = () => { };
56
+ return {
57
+ setTransform: noop,
58
+ clearRect: noop,
59
+ fillRect: noop,
60
+ beginPath: noop,
61
+ moveTo: noop,
62
+ lineTo: noop,
63
+ stroke: noop,
64
+ fill: noop,
65
+ save: noop,
66
+ restore: noop,
67
+ fillText: noop,
68
+ measureText: () => ({ width: 0 }),
69
+ setLineDash: noop,
70
+ createLinearGradient: () => ({ addColorStop: noop }),
71
+ roundRect: noop,
72
+ };
73
+ };
74
+ const bgContext = this._bgCanvas.getContext('2d');
75
+ const overlayContext = this._overlayCanvas.getContext('2d');
76
+ this._bgCtx = bgContext ?? createFallbackContext();
77
+ this._overlayCtx = overlayContext ?? createFallbackContext();
78
+ // Style container
79
+ this._container.style.position = 'relative';
80
+ if (!this._container.style.width || this._container.style.width === 'auto') {
81
+ this._container.style.width = '100%';
82
+ }
83
+ this._container.style.height = `${this._height}px`;
84
+ this._container.appendChild(this._bgCanvas);
85
+ this._container.appendChild(this._overlayCanvas);
86
+ // Register initial plugins if provided
87
+ if (config.plugins) {
88
+ for (const p of config.plugins) {
89
+ this._pluginManager.register(p);
90
+ }
91
+ }
92
+ this._pluginManager.notifyBeforeInit(this);
93
+ this._scheduler = new RenderScheduler(() => this._paint());
94
+ this._attachEventListeners();
95
+ this.resize(this._width, this._height);
96
+ this._pluginManager.notifyAfterInit(this);
97
+ }
98
+ registerPlugin(plugin, options) {
99
+ this._pluginManager.register(plugin, options);
100
+ this.requestRender();
101
+ }
102
+ unregisterPlugin(pluginId) {
103
+ const res = this._pluginManager.unregister(pluginId);
104
+ this.requestRender();
105
+ return res;
106
+ }
107
+ getPluginManager() {
108
+ return this._pluginManager;
109
+ }
110
+ getChartId() {
111
+ return this._chartId;
112
+ }
113
+ setChartId(id) {
114
+ this._chartId = id;
115
+ }
116
+ getEmitter() {
117
+ return this._emitter;
118
+ }
119
+ subscribe(eventType, handler) {
120
+ return this._emitter.subscribe(eventType, handler);
121
+ }
122
+ on(eventType, handler) {
123
+ return this._emitter.on(eventType, handler);
124
+ }
125
+ off(eventType, handler) {
126
+ this._emitter.off(eventType, handler);
127
+ }
128
+ emit(event) {
129
+ this._emitter.emit(event);
130
+ }
131
+ setHitTestResolver(resolver) {
132
+ this._hitTestResolver = resolver;
133
+ }
134
+ setScales(xScale, yScale) {
135
+ this._xScale = xScale;
136
+ this._yScale = yScale;
137
+ }
138
+ setDrawCallbacks(drawData, drawOverlay) {
139
+ this._drawDataCallback = drawData;
140
+ this._drawOverlayCallback = drawOverlay;
141
+ this.requestRender();
142
+ }
143
+ resize(width, height) {
144
+ if (this._disposed || width <= 0 || height <= 0)
145
+ return;
146
+ this._width = width;
147
+ this._height = height;
148
+ this._dpr = getDevicePixelRatio();
149
+ if (this._container.style.width !== '100%') {
150
+ this._container.style.width = `${width}px`;
151
+ }
152
+ this._container.style.height = `${height}px`;
153
+ setupCanvasDpi(this._bgCanvas, this._bgCtx, width, height, this._dpr);
154
+ setupCanvasDpi(this._overlayCanvas, this._overlayCtx, width, height, this._dpr);
155
+ this.requestRender();
156
+ this._emitter.emit({
157
+ eventType: 'chartResize',
158
+ chartId: this._chartId,
159
+ chartType: 'generic',
160
+ timestamp: Date.now(),
161
+ source: 'programmatic',
162
+ viewport: {
163
+ x: { start: 0, end: width },
164
+ y: { start: 0, end: height },
165
+ },
166
+ metadata: { width, height },
167
+ });
168
+ }
169
+ requestRender() {
170
+ this._scheduler.requestRender();
171
+ }
172
+ renderOverlayOnly() {
173
+ if (this._disposed)
174
+ return;
175
+ this._overlayCtx.clearRect(0, 0, this._width, this._height);
176
+ if (this._drawOverlayCallback) {
177
+ this._drawOverlayCallback(this._overlayCtx, { width: this._width, height: this._height }, this._padding, this._dpr);
178
+ }
179
+ }
180
+ applyOptions(options) {
181
+ if (this._disposed || !options)
182
+ return;
183
+ if (options.theme) {
184
+ this._theme = resolveTheme(options.theme);
185
+ }
186
+ if (options.layout?.padding) {
187
+ this._padding = {
188
+ ...this._padding,
189
+ ...options.layout.padding,
190
+ };
191
+ }
192
+ if (options.layout?.background) {
193
+ this._theme = {
194
+ ...this._theme,
195
+ background: options.layout.background,
196
+ };
197
+ }
198
+ if (options.layout?.textPrimary) {
199
+ this._theme = {
200
+ ...this._theme,
201
+ textPrimary: options.layout.textPrimary,
202
+ };
203
+ }
204
+ if (options.layout?.textSecondary) {
205
+ this._theme = {
206
+ ...this._theme,
207
+ textSecondary: options.layout.textSecondary,
208
+ };
209
+ }
210
+ if (options.grid?.horzLines?.color || options.grid?.vertLines?.color) {
211
+ const color = options.grid.horzLines?.color ?? options.grid.vertLines?.color;
212
+ if (color) {
213
+ this._theme = {
214
+ ...this._theme,
215
+ gridLine: color,
216
+ };
217
+ }
218
+ }
219
+ if (options.crosshair?.color) {
220
+ this._theme = {
221
+ ...this._theme,
222
+ crosshairLine: options.crosshair.color,
223
+ };
224
+ }
225
+ if (options.series?.upColor) {
226
+ this._theme = {
227
+ ...this._theme,
228
+ upCandle: options.series.upColor,
229
+ volumeUp: options.series.upColor,
230
+ };
231
+ }
232
+ if (options.series?.downColor) {
233
+ this._theme = {
234
+ ...this._theme,
235
+ downCandle: options.series.downColor,
236
+ volumeDown: options.series.downColor,
237
+ };
238
+ }
239
+ this.requestRender();
240
+ this._emitter.emit({
241
+ eventType: 'configChange',
242
+ chartId: this._chartId,
243
+ chartType: 'generic',
244
+ timestamp: Date.now(),
245
+ source: 'api',
246
+ currentState: options,
247
+ });
248
+ }
249
+ getTheme() {
250
+ return this._theme;
251
+ }
252
+ getWidth() {
253
+ return this._width;
254
+ }
255
+ getHeight() {
256
+ return this._height;
257
+ }
258
+ getPadding() {
259
+ return this._padding;
260
+ }
261
+ getDpr() {
262
+ return this._dpr;
263
+ }
264
+ dispose() {
265
+ if (this._disposed)
266
+ return;
267
+ this._disposed = true;
268
+ this._emitter.emit({
269
+ eventType: 'chartDestroy',
270
+ chartId: this._chartId,
271
+ chartType: 'generic',
272
+ timestamp: Date.now(),
273
+ source: 'programmatic',
274
+ });
275
+ this._detachEventListeners();
276
+ this._scheduler.dispose();
277
+ this._pluginManager.notifyDestroy(this);
278
+ this._emitter.dispose();
279
+ if (this._bgCanvas.parentElement === this._container) {
280
+ this._container.removeChild(this._bgCanvas);
281
+ }
282
+ if (this._overlayCanvas.parentElement === this._container) {
283
+ this._container.removeChild(this._overlayCanvas);
284
+ }
285
+ }
286
+ _paint() {
287
+ if (this._disposed)
288
+ return;
289
+ const bounds = { width: this._width, height: this._height };
290
+ const drawContext = {
291
+ ctx: this._bgCtx,
292
+ bounds,
293
+ padding: this._padding,
294
+ dpr: this._dpr,
295
+ xScale: this._xScale,
296
+ yScale: this._yScale,
297
+ };
298
+ this._pluginManager.notifyBeforeRender(drawContext);
299
+ // Clear background unless cancelled by a plugin
300
+ const shouldDrawBg = this._pluginManager.notifyBeforeDraw(drawContext);
301
+ if (shouldDrawBg !== false) {
302
+ this._bgCtx.fillStyle = this._theme.background;
303
+ this._bgCtx.fillRect(0, 0, this._width, this._height);
304
+ }
305
+ // Draw main data & axes unless cancelled
306
+ const shouldDrawDatasets = this._pluginManager.notifyBeforeDatasetsDraw(drawContext);
307
+ if (shouldDrawDatasets !== false && this._drawDataCallback) {
308
+ this._drawDataCallback(this._bgCtx, bounds, this._padding, this._dpr);
309
+ }
310
+ this._pluginManager.notifyAfterDatasetsDraw(drawContext);
311
+ // Draw overlay
312
+ this.renderOverlayOnly();
313
+ this._pluginManager.notifyAfterDraw(drawContext);
314
+ this._pluginManager.notifyAfterRender(drawContext);
315
+ }
316
+ _handlePointerDown = (e) => {
317
+ if (this._disposed)
318
+ return;
319
+ const rect = this._overlayCanvas.getBoundingClientRect();
320
+ const chartX = e.clientX - rect.left;
321
+ const chartY = e.clientY - rect.top;
322
+ this._isPointerDown = true;
323
+ this._hasDragged = false;
324
+ this._dragStartPos = { clientX: e.clientX, clientY: e.clientY, chartX, chartY };
325
+ const target = this._hitTestResolver?.(chartX, chartY) ?? { elementType: 'background' };
326
+ this._emitter.emit({
327
+ eventType: 'pointerDown',
328
+ chartId: this._chartId,
329
+ chartType: 'generic',
330
+ timestamp: Date.now(),
331
+ source: 'pointer',
332
+ pointer: {
333
+ clientX: e.clientX,
334
+ clientY: e.clientY,
335
+ chartX,
336
+ chartY,
337
+ button: e.button,
338
+ pointerId: e.pointerId,
339
+ },
340
+ target,
341
+ });
342
+ };
343
+ _handlePointerMove = (e) => {
344
+ if (this._disposed)
345
+ return;
346
+ const rect = this._overlayCanvas.getBoundingClientRect();
347
+ const chartX = e.clientX - rect.left;
348
+ const chartY = e.clientY - rect.top;
349
+ if (this._onHover) {
350
+ this._onHover({ x: chartX, y: chartY });
351
+ }
352
+ const pointerPos = {
353
+ clientX: e.clientX,
354
+ clientY: e.clientY,
355
+ chartX,
356
+ chartY,
357
+ button: e.button,
358
+ pointerId: e.pointerId,
359
+ };
360
+ const target = this._hitTestResolver?.(chartX, chartY) ?? { elementType: 'background' };
361
+ if (this._isPointerDown && this._dragStartPos) {
362
+ const dist = Math.hypot(chartX - this._dragStartPos.chartX, chartY - this._dragStartPos.chartY);
363
+ if (!this._hasDragged && dist > 3) {
364
+ this._hasDragged = true;
365
+ this._emitter.emit({
366
+ eventType: 'dragStart',
367
+ chartId: this._chartId,
368
+ chartType: 'generic',
369
+ timestamp: Date.now(),
370
+ source: 'pointer',
371
+ pointer: pointerPos,
372
+ target,
373
+ viewport: {
374
+ x: { start: this._dragStartPos.chartX, end: chartX },
375
+ y: { start: this._dragStartPos.chartY, end: chartY },
376
+ },
377
+ });
378
+ }
379
+ if (this._hasDragged) {
380
+ this._emitter.emit({
381
+ eventType: 'drag',
382
+ chartId: this._chartId,
383
+ chartType: 'generic',
384
+ timestamp: Date.now(),
385
+ source: 'pointer',
386
+ pointer: pointerPos,
387
+ viewport: {
388
+ x: { start: this._dragStartPos.chartX, end: chartX },
389
+ y: { start: this._dragStartPos.chartY, end: chartY },
390
+ },
391
+ });
392
+ }
393
+ }
394
+ const isTargetChanged = !this._lastHoverTarget ||
395
+ this._lastHoverTarget.elementType !== target.elementType ||
396
+ this._lastHoverTarget.dataIndex !== target.dataIndex ||
397
+ this._lastHoverTarget.elementId !== target.elementId;
398
+ if (isTargetChanged) {
399
+ this._emitter.emit({
400
+ eventType: 'hoverChange',
401
+ chartId: this._chartId,
402
+ chartType: 'generic',
403
+ timestamp: Date.now(),
404
+ source: 'pointer',
405
+ pointer: pointerPos,
406
+ previousState: this._lastHoverTarget,
407
+ currentState: target,
408
+ target,
409
+ });
410
+ this._lastHoverTarget = target;
411
+ }
412
+ this._emitter.emit({
413
+ eventType: 'pointerMove',
414
+ chartId: this._chartId,
415
+ chartType: 'generic',
416
+ timestamp: Date.now(),
417
+ source: 'pointer',
418
+ pointer: pointerPos,
419
+ target,
420
+ });
421
+ this._emitter.emit({
422
+ eventType: 'hover',
423
+ chartId: this._chartId,
424
+ chartType: 'generic',
425
+ timestamp: Date.now(),
426
+ source: 'pointer',
427
+ pointer: pointerPos,
428
+ target,
429
+ });
430
+ this.renderOverlayOnly();
431
+ };
432
+ _handlePointerUp = (e) => {
433
+ if (this._disposed || !this._isPointerDown)
434
+ return;
435
+ const rect = this._overlayCanvas.getBoundingClientRect();
436
+ const chartX = e.clientX - rect.left;
437
+ const chartY = e.clientY - rect.top;
438
+ const pointerPos = {
439
+ clientX: e.clientX,
440
+ clientY: e.clientY,
441
+ chartX,
442
+ chartY,
443
+ button: e.button,
444
+ pointerId: e.pointerId,
445
+ };
446
+ const target = this._hitTestResolver?.(chartX, chartY) ?? { elementType: 'background' };
447
+ if (this._hasDragged && this._dragStartPos) {
448
+ this._emitter.emit({
449
+ eventType: 'dragEnd',
450
+ chartId: this._chartId,
451
+ chartType: 'generic',
452
+ timestamp: Date.now(),
453
+ source: 'pointer',
454
+ pointer: pointerPos,
455
+ viewport: {
456
+ x: { start: this._dragStartPos.chartX, end: chartX },
457
+ y: { start: this._dragStartPos.chartY, end: chartY },
458
+ },
459
+ });
460
+ this._emitter.emit({
461
+ eventType: 'rangeSelect',
462
+ chartId: this._chartId,
463
+ chartType: 'generic',
464
+ timestamp: Date.now(),
465
+ source: 'pointer',
466
+ pointer: pointerPos,
467
+ domain: {
468
+ x: {
469
+ start: Math.min(this._dragStartPos.chartX, chartX),
470
+ end: Math.max(this._dragStartPos.chartX, chartX),
471
+ },
472
+ y: {
473
+ start: Math.min(this._dragStartPos.chartY, chartY),
474
+ end: Math.max(this._dragStartPos.chartY, chartY),
475
+ },
476
+ },
477
+ });
478
+ }
479
+ else {
480
+ this._emitter.emit({
481
+ eventType: 'click',
482
+ chartId: this._chartId,
483
+ chartType: 'generic',
484
+ timestamp: Date.now(),
485
+ source: 'pointer',
486
+ pointer: pointerPos,
487
+ target,
488
+ });
489
+ if (target.elementType !== 'background') {
490
+ if (target.elementType === 'series') {
491
+ this._emitter.emit({
492
+ eventType: 'seriesClick',
493
+ chartId: this._chartId,
494
+ chartType: 'generic',
495
+ timestamp: Date.now(),
496
+ source: 'pointer',
497
+ pointer: pointerPos,
498
+ target,
499
+ });
500
+ }
501
+ else if (target.elementType === 'legend') {
502
+ this._emitter.emit({
503
+ eventType: 'legendClick',
504
+ chartId: this._chartId,
505
+ chartType: 'generic',
506
+ timestamp: Date.now(),
507
+ source: 'pointer',
508
+ pointer: pointerPos,
509
+ target,
510
+ });
511
+ }
512
+ else {
513
+ this._emitter.emit({
514
+ eventType: 'dataPointClick',
515
+ chartId: this._chartId,
516
+ chartType: 'generic',
517
+ timestamp: Date.now(),
518
+ source: 'pointer',
519
+ pointer: pointerPos,
520
+ target,
521
+ });
522
+ }
523
+ }
524
+ }
525
+ this._emitter.emit({
526
+ eventType: 'pointerUp',
527
+ chartId: this._chartId,
528
+ chartType: 'generic',
529
+ timestamp: Date.now(),
530
+ source: 'pointer',
531
+ pointer: pointerPos,
532
+ target,
533
+ });
534
+ this._isPointerDown = false;
535
+ this._hasDragged = false;
536
+ this._dragStartPos = null;
537
+ };
538
+ _handleDoubleClick = (e) => {
539
+ if (this._disposed)
540
+ return;
541
+ const rect = this._overlayCanvas.getBoundingClientRect();
542
+ const chartX = e.clientX - rect.left;
543
+ const chartY = e.clientY - rect.top;
544
+ const target = this._hitTestResolver?.(chartX, chartY) ?? { elementType: 'background' };
545
+ this._emitter.emit({
546
+ eventType: 'doubleClick',
547
+ chartId: this._chartId,
548
+ chartType: 'generic',
549
+ timestamp: Date.now(),
550
+ source: 'pointer',
551
+ pointer: {
552
+ clientX: e.clientX,
553
+ clientY: e.clientY,
554
+ chartX,
555
+ chartY,
556
+ button: e.button,
557
+ },
558
+ target,
559
+ });
560
+ };
561
+ _handlePointerLeave = () => {
562
+ if (this._disposed)
563
+ return;
564
+ if (this._onHover) {
565
+ this._onHover(null);
566
+ }
567
+ this._overlayCtx.clearRect(0, 0, this._width, this._height);
568
+ if (this._lastHoverTarget) {
569
+ this._emitter.emit({
570
+ eventType: 'elementLeave',
571
+ chartId: this._chartId,
572
+ chartType: 'generic',
573
+ timestamp: Date.now(),
574
+ source: 'pointer',
575
+ target: this._lastHoverTarget,
576
+ });
577
+ this._lastHoverTarget = null;
578
+ }
579
+ };
580
+ _attachEventListeners() {
581
+ this._overlayCanvas.addEventListener('pointerdown', this._handlePointerDown);
582
+ this._overlayCanvas.addEventListener('pointermove', this._handlePointerMove);
583
+ this._overlayCanvas.addEventListener('pointerup', this._handlePointerUp);
584
+ this._overlayCanvas.addEventListener('dblclick', this._handleDoubleClick);
585
+ this._overlayCanvas.addEventListener('pointerleave', this._handlePointerLeave);
586
+ }
587
+ _detachEventListeners() {
588
+ this._overlayCanvas.removeEventListener('pointerdown', this._handlePointerDown);
589
+ this._overlayCanvas.removeEventListener('pointermove', this._handlePointerMove);
590
+ this._overlayCanvas.removeEventListener('pointerup', this._handlePointerUp);
591
+ this._overlayCanvas.removeEventListener('dblclick', this._handleDoubleClick);
592
+ this._overlayCanvas.removeEventListener('pointerleave', this._handlePointerLeave);
593
+ }
594
+ }
package/dist/dpi.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ /**
2
+ * @file packages/chart-renderer-canvas/src/dpi.ts
3
+ * High-DPI canvas backing store scaling and sub-pixel snapping routines.
4
+ */
5
+ export declare const MAX_CANVAS_PIXELS = 16777216;
6
+ export declare const MAX_DIMENSION = 8192;
7
+ /**
8
+ * Returns the effective devicePixelRatio clamped to safe bounds [1, 4].
9
+ */
10
+ export declare function getDevicePixelRatio(customWindow?: Window): number;
11
+ /**
12
+ * Snaps a 1px stroke coordinate to 0.5 physical pixel offset.
13
+ * Prevents fuzzy anti-aliasing on high-DPI displays.
14
+ */
15
+ export declare function snapLine(coord: number, dpr?: number): number;
16
+ /**
17
+ * Snaps a filled rectangle boundary to integer physical pixels.
18
+ */
19
+ export declare function snapRect(coord: number, dpr?: number): number;
20
+ export interface CanvasDimensions {
21
+ readonly cssWidth: number;
22
+ readonly cssHeight: number;
23
+ readonly physicalWidth: number;
24
+ readonly physicalHeight: number;
25
+ readonly dpr: number;
26
+ }
27
+ /**
28
+ * Configures canvas backing store dimensions and scale transform with DoS bounds.
29
+ */
30
+ export declare function setupCanvasDpi(canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D, width: number, height: number, dprInput?: number): CanvasDimensions;
31
+ //# sourceMappingURL=dpi.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dpi.d.ts","sourceRoot":"","sources":["../src/dpi.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,eAAO,MAAM,iBAAiB,WAAa,CAAC;AAC5C,eAAO,MAAM,aAAa,OAAO,CAAC;AAElC;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,CAMjE;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,SAAI,GAAG,MAAM,CAGvD;AAED;;GAEG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,SAAI,GAAG,MAAM,CAGvD;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,iBAAiB,EACzB,GAAG,EAAE,wBAAwB,EAC7B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,MAAM,GAChB,gBAAgB,CAkClB"}
package/dist/dpi.js ADDED
@@ -0,0 +1,65 @@
1
+ /**
2
+ * @file packages/chart-renderer-canvas/src/dpi.ts
3
+ * High-DPI canvas backing store scaling and sub-pixel snapping routines.
4
+ */
5
+ export const MAX_CANVAS_PIXELS = 16_777_216; // 16M physical pixels (~64MB RGBA buffer)
6
+ export const MAX_DIMENSION = 8192; // Max width or height in physical pixels
7
+ /**
8
+ * Returns the effective devicePixelRatio clamped to safe bounds [1, 4].
9
+ */
10
+ export function getDevicePixelRatio(customWindow) {
11
+ const win = customWindow ?? (typeof window !== 'undefined' ? window : undefined);
12
+ if (!win || !win.devicePixelRatio || !Number.isFinite(win.devicePixelRatio)) {
13
+ return 1;
14
+ }
15
+ return Math.max(1, Math.min(4, win.devicePixelRatio));
16
+ }
17
+ /**
18
+ * Snaps a 1px stroke coordinate to 0.5 physical pixel offset.
19
+ * Prevents fuzzy anti-aliasing on high-DPI displays.
20
+ */
21
+ export function snapLine(coord, dpr = 1) {
22
+ if (dpr <= 0 || !Number.isFinite(coord))
23
+ return coord;
24
+ return (Math.floor(coord * dpr) + 0.5) / dpr;
25
+ }
26
+ /**
27
+ * Snaps a filled rectangle boundary to integer physical pixels.
28
+ */
29
+ export function snapRect(coord, dpr = 1) {
30
+ if (dpr <= 0 || !Number.isFinite(coord))
31
+ return coord;
32
+ return Math.floor(coord * dpr) / dpr;
33
+ }
34
+ /**
35
+ * Configures canvas backing store dimensions and scale transform with DoS bounds.
36
+ */
37
+ export function setupCanvasDpi(canvas, ctx, width, height, dprInput) {
38
+ const safeCssWidth = Math.max(1, Math.min(MAX_DIMENSION, Math.floor(width)));
39
+ const safeCssHeight = Math.max(1, Math.min(MAX_DIMENSION, Math.floor(height)));
40
+ const dpr = dprInput ?? getDevicePixelRatio();
41
+ let physicalWidth = Math.floor(safeCssWidth * dpr);
42
+ let physicalHeight = Math.floor(safeCssHeight * dpr);
43
+ // Guard against GPU memory exhaustion on ultra-high resolution displays
44
+ if (physicalWidth * physicalHeight > MAX_CANVAS_PIXELS) {
45
+ const scaleFactor = Math.sqrt(MAX_CANVAS_PIXELS / (physicalWidth * physicalHeight));
46
+ physicalWidth = Math.floor(physicalWidth * scaleFactor);
47
+ physicalHeight = Math.floor(physicalHeight * scaleFactor);
48
+ }
49
+ if (canvas.width !== physicalWidth || canvas.height !== physicalHeight) {
50
+ canvas.width = physicalWidth;
51
+ canvas.height = physicalHeight;
52
+ }
53
+ canvas.style.width = `${safeCssWidth}px`;
54
+ canvas.style.height = `${safeCssHeight}px`;
55
+ const scaleX = physicalWidth / safeCssWidth;
56
+ const scaleY = physicalHeight / safeCssHeight;
57
+ ctx.setTransform(scaleX, 0, 0, scaleY, 0, 0);
58
+ return {
59
+ cssWidth: safeCssWidth,
60
+ cssHeight: safeCssHeight,
61
+ physicalWidth,
62
+ physicalHeight,
63
+ dpr: (scaleX + scaleY) / 2,
64
+ };
65
+ }