@tranding-custom-chart/react 0.0.0-stage → 2.1.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 (116) hide show
  1. package/README.md +351 -2
  2. package/dist/AccessibleChartTable.d.ts +12 -0
  3. package/dist/AccessibleChartTable.d.ts.map +1 -0
  4. package/dist/AccessibleChartTable.js +19 -0
  5. package/dist/BaselineSeries.d.ts +13 -0
  6. package/dist/BaselineSeries.d.ts.map +1 -0
  7. package/dist/BaselineSeries.js +32 -0
  8. package/dist/BoxPlotSeries.d.ts +11 -0
  9. package/dist/BoxPlotSeries.d.ts.map +1 -0
  10. package/dist/BoxPlotSeries.js +37 -0
  11. package/dist/BubbleSeries.d.ts +11 -0
  12. package/dist/BubbleSeries.d.ts.map +1 -0
  13. package/dist/BubbleSeries.js +37 -0
  14. package/dist/CandlestickSeries.d.ts +13 -0
  15. package/dist/CandlestickSeries.d.ts.map +1 -0
  16. package/dist/CandlestickSeries.js +54 -0
  17. package/dist/ChartContainer.d.ts +73 -0
  18. package/dist/ChartContainer.d.ts.map +1 -0
  19. package/dist/ChartContainer.js +631 -0
  20. package/dist/ChartContext.d.ts +36 -0
  21. package/dist/ChartContext.d.ts.map +1 -0
  22. package/dist/ChartContext.js +13 -0
  23. package/dist/ChartSyncGroup.d.ts +14 -0
  24. package/dist/ChartSyncGroup.d.ts.map +1 -0
  25. package/dist/ChartSyncGroup.js +18 -0
  26. package/dist/ColumnsSeries.d.ts +13 -0
  27. package/dist/ColumnsSeries.d.ts.map +1 -0
  28. package/dist/ColumnsSeries.js +54 -0
  29. package/dist/CrosshairOverlay.d.ts +10 -0
  30. package/dist/CrosshairOverlay.d.ts.map +1 -0
  31. package/dist/CrosshairOverlay.js +65 -0
  32. package/dist/DrawingOverlay.d.ts +16 -0
  33. package/dist/DrawingOverlay.d.ts.map +1 -0
  34. package/dist/DrawingOverlay.js +50 -0
  35. package/dist/GridAxes.d.ts +19 -0
  36. package/dist/GridAxes.d.ts.map +1 -0
  37. package/dist/GridAxes.js +50 -0
  38. package/dist/HeatmapSeries.d.ts +11 -0
  39. package/dist/HeatmapSeries.d.ts.map +1 -0
  40. package/dist/HeatmapSeries.js +74 -0
  41. package/dist/HighLowSeries.d.ts +12 -0
  42. package/dist/HighLowSeries.d.ts.map +1 -0
  43. package/dist/HighLowSeries.js +29 -0
  44. package/dist/HlcAreaSeries.d.ts +13 -0
  45. package/dist/HlcAreaSeries.d.ts.map +1 -0
  46. package/dist/HlcAreaSeries.js +28 -0
  47. package/dist/HollowCandlestickSeries.d.ts +13 -0
  48. package/dist/HollowCandlestickSeries.d.ts.map +1 -0
  49. package/dist/HollowCandlestickSeries.js +30 -0
  50. package/dist/HorizontalBarSeries.d.ts +15 -0
  51. package/dist/HorizontalBarSeries.d.ts.map +1 -0
  52. package/dist/HorizontalBarSeries.js +42 -0
  53. package/dist/InteractiveLegend.d.ts +19 -0
  54. package/dist/InteractiveLegend.d.ts.map +1 -0
  55. package/dist/InteractiveLegend.js +39 -0
  56. package/dist/LastPriceLine.d.ts +11 -0
  57. package/dist/LastPriceLine.d.ts.map +1 -0
  58. package/dist/LastPriceLine.js +29 -0
  59. package/dist/LineSeries.d.ts +20 -0
  60. package/dist/LineSeries.d.ts.map +1 -0
  61. package/dist/LineSeries.js +74 -0
  62. package/dist/LineWithMarkersSeries.d.ts +13 -0
  63. package/dist/LineWithMarkersSeries.d.ts.map +1 -0
  64. package/dist/LineWithMarkersSeries.js +30 -0
  65. package/dist/MixedChartSeries.d.ts +14 -0
  66. package/dist/MixedChartSeries.d.ts.map +1 -0
  67. package/dist/MixedChartSeries.js +30 -0
  68. package/dist/OhlcBarSeries.d.ts +14 -0
  69. package/dist/OhlcBarSeries.d.ts.map +1 -0
  70. package/dist/OhlcBarSeries.js +31 -0
  71. package/dist/PieSeries.d.ts +18 -0
  72. package/dist/PieSeries.d.ts.map +1 -0
  73. package/dist/PieSeries.js +85 -0
  74. package/dist/PluginLayer.d.ts +11 -0
  75. package/dist/PluginLayer.d.ts.map +1 -0
  76. package/dist/PluginLayer.js +14 -0
  77. package/dist/PolarAreaSeries.d.ts +11 -0
  78. package/dist/PolarAreaSeries.d.ts.map +1 -0
  79. package/dist/PolarAreaSeries.js +35 -0
  80. package/dist/RadarSeries.d.ts +16 -0
  81. package/dist/RadarSeries.d.ts.map +1 -0
  82. package/dist/RadarSeries.js +47 -0
  83. package/dist/ScatterSeries.d.ts +11 -0
  84. package/dist/ScatterSeries.d.ts.map +1 -0
  85. package/dist/ScatterSeries.js +60 -0
  86. package/dist/SeriesMarkersOverlay.d.ts +10 -0
  87. package/dist/SeriesMarkersOverlay.d.ts.map +1 -0
  88. package/dist/SeriesMarkersOverlay.js +28 -0
  89. package/dist/StackedBarSeries.d.ts +13 -0
  90. package/dist/StackedBarSeries.d.ts.map +1 -0
  91. package/dist/StackedBarSeries.js +26 -0
  92. package/dist/StepLineSeries.d.ts +15 -0
  93. package/dist/StepLineSeries.d.ts.map +1 -0
  94. package/dist/StepLineSeries.js +28 -0
  95. package/dist/SupertrendSeries.d.ts +16 -0
  96. package/dist/SupertrendSeries.d.ts.map +1 -0
  97. package/dist/SupertrendSeries.js +40 -0
  98. package/dist/UniversalSeries.d.ts +21 -0
  99. package/dist/UniversalSeries.d.ts.map +1 -0
  100. package/dist/UniversalSeries.js +222 -0
  101. package/dist/VolumeProfileSeries.d.ts +21 -0
  102. package/dist/VolumeProfileSeries.d.ts.map +1 -0
  103. package/dist/VolumeProfileSeries.js +57 -0
  104. package/dist/VolumeSeries.d.ts +14 -0
  105. package/dist/VolumeSeries.d.ts.map +1 -0
  106. package/dist/VolumeSeries.js +33 -0
  107. package/dist/WatermarkOverlay.d.ts +11 -0
  108. package/dist/WatermarkOverlay.d.ts.map +1 -0
  109. package/dist/WatermarkOverlay.js +27 -0
  110. package/dist/index.d.ts +42 -0
  111. package/dist/index.d.ts.map +1 -0
  112. package/dist/index.js +41 -0
  113. package/dist/useChart.d.ts +16 -0
  114. package/dist/useChart.d.ts.map +1 -0
  115. package/dist/useChart.js +34 -0
  116. package/package.json +93 -4
@@ -0,0 +1,631 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * @file packages/chart-react/src/ChartContainer.tsx
4
+ * High-performance, responsive, accessible React container wrapping CanvasChartEngine.
5
+ */
6
+ import React, { useCallback, useEffect, useMemo, useRef, useState, } from 'react';
7
+ import { DEFAULT_DARK_THEME, resolveTheme, } from '@tranding-custom-chart/core';
8
+ import { CanvasChartEngine } from '@tranding-custom-chart/renderer-canvas';
9
+ import { ChartContext, } from './ChartContext.js';
10
+ import { useChartSync } from './ChartSyncGroup.js';
11
+ export function ChartContainer({ width: initialWidth = 760, height: initialHeight = 400, padding: initialPadding = { top: 20, right: 65, bottom: 30, left: 15 }, theme: initialTheme = DEFAULT_DARK_THEME, options, plugins, chartRef, xScale = null, yScale = null, secondaryYScale = null, ariaLabel, pointCount = 0, onHoverIndexChange, onHoverPosChange, onZoom, onPan, isPanDisabled = false, onDimensionsChange, className, style, children, onClick, onDoubleClick, onPointerDown, onPointerUp, onPointerMove, onHover, onHoverChange, onDataPointClick, onSeriesClick, onLegendClick, onRangeSelect, onRangeChange, onDragStart, onDrag, onDragEnd, onVisibleRangeChange, onConfigChange, onDataChange, onDrawingChange, onDrawingCreate, onDrawingSelect, onDrawingMove, onDrawingResize, onDrawingDelete, onDrawingLockChange, onChartReady, onChartResize, onChartDestroy, onError, }) {
12
+ const containerRef = useRef(null);
13
+ const engineRef = useRef(null);
14
+ const isDraggingRef = useRef(false);
15
+ const lastXRef = useRef(0);
16
+ const padding = useMemo(() => {
17
+ if (options?.layout?.padding) {
18
+ return { ...initialPadding, ...options.layout.padding };
19
+ }
20
+ return initialPadding;
21
+ }, [options?.layout?.padding, initialPadding]);
22
+ const theme = useMemo(() => {
23
+ let resolved = options?.theme
24
+ ? resolveTheme(options.theme)
25
+ : initialTheme;
26
+ if (options) {
27
+ const gridColor = options.grid?.horzLines?.color ?? options.grid?.vertLines?.color;
28
+ resolved = {
29
+ ...resolved,
30
+ ...(options.layout?.background ? { background: options.layout.background } : {}),
31
+ ...(options.layout?.textPrimary ? { textPrimary: options.layout.textPrimary } : {}),
32
+ ...(options.layout?.textSecondary ? { textSecondary: options.layout.textSecondary } : {}),
33
+ ...(gridColor ? { gridLine: gridColor } : {}),
34
+ ...(options.crosshair?.color ? { crosshairLine: options.crosshair.color } : {}),
35
+ ...(options.series?.upColor
36
+ ? { upCandle: options.series.upColor, volumeUp: options.series.upColor }
37
+ : {}),
38
+ ...(options.series?.downColor
39
+ ? { downCandle: options.series.downColor, volumeDown: options.series.downColor }
40
+ : {}),
41
+ };
42
+ }
43
+ return resolved;
44
+ }, [options, initialTheme]);
45
+ const [bounds, setBounds] = useState({
46
+ width: initialWidth,
47
+ height: initialHeight,
48
+ });
49
+ const [hoverIndex, setHoverIndex] = useState(null);
50
+ const [hoverPos, setHoverPos] = useState(null);
51
+ const [screenReaderAnnouncement, setScreenReaderAnnouncement] = useState('');
52
+ const chartIdRef = useRef('chart-' + Math.random().toString(36).slice(2, 9));
53
+ // Universal Event Callback refs
54
+ const callbacksRef = useRef({
55
+ onClick,
56
+ onDoubleClick,
57
+ onPointerDown,
58
+ onPointerUp,
59
+ onPointerMove,
60
+ onHover,
61
+ onHoverChange,
62
+ onDataPointClick,
63
+ onSeriesClick,
64
+ onLegendClick,
65
+ onRangeSelect,
66
+ onRangeChange,
67
+ onDragStart,
68
+ onDrag,
69
+ onDragEnd,
70
+ onVisibleRangeChange,
71
+ onConfigChange,
72
+ onDataChange,
73
+ onDrawingChange,
74
+ onDrawingCreate,
75
+ onDrawingSelect,
76
+ onDrawingMove,
77
+ onDrawingResize,
78
+ onDrawingDelete,
79
+ onDrawingLockChange,
80
+ onChartReady,
81
+ onChartResize,
82
+ onChartDestroy,
83
+ onError,
84
+ });
85
+ callbacksRef.current = {
86
+ onClick,
87
+ onDoubleClick,
88
+ onPointerDown,
89
+ onPointerUp,
90
+ onPointerMove,
91
+ onHover,
92
+ onHoverChange,
93
+ onDataPointClick,
94
+ onSeriesClick,
95
+ onLegendClick,
96
+ onRangeSelect,
97
+ onRangeChange,
98
+ onDragStart,
99
+ onDrag,
100
+ onDragEnd,
101
+ onVisibleRangeChange,
102
+ onConfigChange,
103
+ onDataChange,
104
+ onDrawingChange,
105
+ onDrawingCreate,
106
+ onDrawingSelect,
107
+ onDrawingMove,
108
+ onDrawingResize,
109
+ onDrawingDelete,
110
+ onDrawingLockChange,
111
+ onChartReady,
112
+ onChartResize,
113
+ onChartDestroy,
114
+ onError,
115
+ };
116
+ // Keep latest refs to avoid re-instantiating the Canvas engine
117
+ const xScaleRef = useRef(xScale);
118
+ xScaleRef.current = xScale;
119
+ const onHoverIndexChangeRef = useRef(onHoverIndexChange);
120
+ onHoverIndexChangeRef.current = onHoverIndexChange;
121
+ const onHoverPosChangeRef = useRef(onHoverPosChange);
122
+ onHoverPosChangeRef.current = onHoverPosChange;
123
+ const onZoomRef = useRef(onZoom);
124
+ onZoomRef.current = onZoom;
125
+ const onPanRef = useRef(onPan);
126
+ onPanRef.current = onPan;
127
+ const isPanDisabledRef = useRef(isPanDisabled);
128
+ isPanDisabledRef.current = isPanDisabled;
129
+ const onDimensionsChangeRef = useRef(onDimensionsChange);
130
+ onDimensionsChangeRef.current = onDimensionsChange;
131
+ // Layer registries
132
+ const dataLayers = useRef(new Map());
133
+ const overlayLayers = useRef(new Map());
134
+ const hitTests = useRef(new Map());
135
+ const registerDataLayer = useCallback((id, drawFn) => {
136
+ dataLayers.current.set(id, drawFn);
137
+ engineRef.current?.requestRender();
138
+ return () => {
139
+ dataLayers.current.delete(id);
140
+ engineRef.current?.requestRender();
141
+ };
142
+ }, []);
143
+ const registerOverlayLayer = useCallback((id, drawFn) => {
144
+ overlayLayers.current.set(id, drawFn);
145
+ engineRef.current?.renderOverlayOnly();
146
+ return () => {
147
+ overlayLayers.current.delete(id);
148
+ engineRef.current?.renderOverlayOnly();
149
+ };
150
+ }, []);
151
+ const registerHitTest = useCallback((id, fn) => {
152
+ hitTests.current.set(id, fn);
153
+ return () => {
154
+ hitTests.current.delete(id);
155
+ };
156
+ }, []);
157
+ const registerPlugin = useCallback((plugin, pluginOpts) => {
158
+ engineRef.current?.registerPlugin(plugin, pluginOpts);
159
+ return () => {
160
+ engineRef.current?.unregisterPlugin(plugin.id);
161
+ };
162
+ }, []);
163
+ const requestRender = useCallback(() => {
164
+ engineRef.current?.requestRender();
165
+ }, []);
166
+ const paddingKey = `${padding.top},${padding.right},${padding.bottom},${padding.left}`;
167
+ // Initialize engine once per container/theme/padding lifecycle
168
+ useEffect(() => {
169
+ const container = containerRef.current;
170
+ if (!container)
171
+ return;
172
+ const engine = new CanvasChartEngine({
173
+ container,
174
+ width: bounds.width,
175
+ height: bounds.height,
176
+ padding,
177
+ theme,
178
+ plugins,
179
+ onHover: (pos) => {
180
+ setHoverPos(pos);
181
+ onHoverPosChangeRef.current?.(pos);
182
+ if (pos && xScaleRef.current) {
183
+ const idx = xScaleRef.current.invert(pos.x);
184
+ const validIndex = Math.round(idx);
185
+ setHoverIndex(validIndex);
186
+ onHoverIndexChangeRef.current?.(validIndex);
187
+ }
188
+ else {
189
+ setHoverIndex(null);
190
+ onHoverIndexChangeRef.current?.(null);
191
+ }
192
+ },
193
+ });
194
+ engine.setChartId(chartIdRef.current);
195
+ engine.setHitTestResolver((x, y) => {
196
+ for (const resolver of hitTests.current.values()) {
197
+ const target = resolver(x, y);
198
+ if (target && target.elementType !== 'background') {
199
+ return target;
200
+ }
201
+ }
202
+ return { elementType: 'background' };
203
+ });
204
+ const unsubAll = engine.subscribe('*', (ev) => {
205
+ const c = callbacksRef.current;
206
+ switch (ev.eventType) {
207
+ case 'click':
208
+ c.onClick?.(ev);
209
+ break;
210
+ case 'doubleClick':
211
+ c.onDoubleClick?.(ev);
212
+ break;
213
+ case 'pointerDown':
214
+ c.onPointerDown?.(ev);
215
+ break;
216
+ case 'pointerUp':
217
+ c.onPointerUp?.(ev);
218
+ break;
219
+ case 'pointerMove':
220
+ c.onPointerMove?.(ev);
221
+ break;
222
+ case 'hover':
223
+ c.onHover?.(ev);
224
+ break;
225
+ case 'hoverChange':
226
+ c.onHoverChange?.(ev);
227
+ break;
228
+ case 'dataPointClick':
229
+ c.onDataPointClick?.(ev);
230
+ break;
231
+ case 'seriesClick':
232
+ c.onSeriesClick?.(ev);
233
+ break;
234
+ case 'legendClick':
235
+ c.onLegendClick?.(ev);
236
+ break;
237
+ case 'rangeSelect':
238
+ c.onRangeSelect?.(ev);
239
+ c.onRangeChange?.(ev);
240
+ break;
241
+ case 'rangeChange':
242
+ c.onRangeChange?.(ev);
243
+ break;
244
+ case 'dragStart':
245
+ c.onDragStart?.(ev);
246
+ break;
247
+ case 'drag':
248
+ c.onDrag?.(ev);
249
+ c.onRangeChange?.(ev);
250
+ break;
251
+ case 'dragEnd':
252
+ c.onDragEnd?.(ev);
253
+ break;
254
+ case 'zoom':
255
+ if (typeof c.onZoom === 'function') {
256
+ c.onZoom(ev);
257
+ }
258
+ break;
259
+ case 'visibleRangeChange':
260
+ c.onVisibleRangeChange?.(ev);
261
+ break;
262
+ case 'configChange':
263
+ c.onConfigChange?.(ev);
264
+ break;
265
+ case 'dataChange':
266
+ c.onDataChange?.(ev);
267
+ break;
268
+ case 'drawingChange':
269
+ c.onDrawingChange?.(ev);
270
+ break;
271
+ case 'drawingCreate':
272
+ c.onDrawingCreate?.(ev);
273
+ c.onDrawingChange?.(ev);
274
+ break;
275
+ case 'drawingSelect':
276
+ c.onDrawingSelect?.(ev);
277
+ break;
278
+ case 'drawingMove':
279
+ c.onDrawingMove?.(ev);
280
+ c.onDrawingChange?.(ev);
281
+ break;
282
+ case 'drawingResize':
283
+ c.onDrawingResize?.(ev);
284
+ c.onDrawingChange?.(ev);
285
+ break;
286
+ case 'drawingDelete':
287
+ c.onDrawingDelete?.(ev);
288
+ c.onDrawingChange?.(ev);
289
+ break;
290
+ case 'drawingLockChange':
291
+ c.onDrawingLockChange?.(ev);
292
+ c.onDrawingChange?.(ev);
293
+ break;
294
+ case 'chartReady':
295
+ c.onChartReady?.(ev);
296
+ break;
297
+ case 'chartResize':
298
+ c.onChartResize?.(ev);
299
+ break;
300
+ case 'chartDestroy':
301
+ c.onChartDestroy?.(ev);
302
+ break;
303
+ case 'error':
304
+ c.onError?.(ev);
305
+ break;
306
+ }
307
+ });
308
+ engine.setDrawCallbacks((ctx, b, p, dpr) => {
309
+ dataLayers.current.forEach((fn) => fn(ctx, b, p, dpr));
310
+ }, (ctx, b, p, dpr) => {
311
+ overlayLayers.current.forEach((fn) => fn(ctx, b, p, dpr));
312
+ });
313
+ engineRef.current = engine;
314
+ return () => {
315
+ unsubAll();
316
+ engine.dispose();
317
+ engineRef.current = null;
318
+ };
319
+ }, [theme, paddingKey]);
320
+ // Synchronize scales to engine for plugins
321
+ useEffect(() => {
322
+ engineRef.current?.setScales(xScale, yScale);
323
+ }, [xScale, yScale]);
324
+ // Synchronize bounds changes into the engine without reconstructing
325
+ useEffect(() => {
326
+ if (engineRef.current && bounds.width > 0 && bounds.height > 0) {
327
+ engineRef.current.resize(bounds.width, bounds.height);
328
+ onDimensionsChangeRef.current?.(bounds);
329
+ }
330
+ }, [bounds]);
331
+ // Synchronize dynamic options updates into the engine without reconstructing
332
+ useEffect(() => {
333
+ if (engineRef.current && options) {
334
+ engineRef.current.applyOptions(options);
335
+ }
336
+ }, [options]);
337
+ // Imperative handle binding for chartRef
338
+ useEffect(() => {
339
+ if (!chartRef)
340
+ return;
341
+ const handle = {
342
+ applyOptions: (opt) => engineRef.current?.applyOptions(opt),
343
+ resize: (w, h) => engineRef.current?.resize(w, h),
344
+ requestRender: () => engineRef.current?.requestRender(),
345
+ getEngine: () => engineRef.current,
346
+ subscribe: (evType, handler) => engineRef.current?.subscribe(evType, handler) ?? (() => { }),
347
+ emit: (ev) => engineRef.current?.emit(ev),
348
+ };
349
+ if (typeof chartRef === 'function') {
350
+ chartRef(handle);
351
+ }
352
+ else if ('current' in chartRef) {
353
+ chartRef.current = handle;
354
+ }
355
+ return () => {
356
+ if (typeof chartRef === 'function') {
357
+ chartRef(null);
358
+ }
359
+ else if ('current' in chartRef) {
360
+ chartRef.current = null;
361
+ }
362
+ };
363
+ }, [chartRef]);
364
+ // Update bounds when width prop changes
365
+ useEffect(() => {
366
+ if (typeof initialWidth === 'number' && initialWidth > 0) {
367
+ setBounds((prev) => (prev.width === initialWidth ? prev : { ...prev, width: initialWidth }));
368
+ }
369
+ }, [initialWidth]);
370
+ // Update bounds when height prop changes
371
+ useEffect(() => {
372
+ if (typeof initialHeight === 'number' && initialHeight > 0) {
373
+ setBounds((prev) => (prev.height === initialHeight ? prev : { ...prev, height: initialHeight }));
374
+ }
375
+ }, [initialHeight]);
376
+ // Responsive observation
377
+ useEffect(() => {
378
+ const container = containerRef.current;
379
+ if (!container || typeof ResizeObserver === 'undefined')
380
+ return;
381
+ const observer = new ResizeObserver((entries) => {
382
+ const entry = entries[0];
383
+ if (!entry)
384
+ return;
385
+ const { width: measuredWidth, height: measuredHeight } = entry.contentRect;
386
+ if (measuredWidth > 0) {
387
+ const nextW = Math.floor(measuredWidth);
388
+ const nextH = typeof initialHeight === 'number' && initialHeight > 0
389
+ ? initialHeight
390
+ : (measuredHeight > 0 ? Math.floor(measuredHeight) : bounds.height);
391
+ setBounds((prev) => {
392
+ if (prev.width === nextW && prev.height === nextH)
393
+ return prev;
394
+ return { width: nextW, height: nextH };
395
+ });
396
+ }
397
+ });
398
+ observer.observe(container);
399
+ return () => observer.disconnect();
400
+ }, [initialHeight, bounds.height]);
401
+ // Keyboard navigation for WCAG 2.2 AA
402
+ const handleKeyDown = (e) => {
403
+ if (pointCount <= 0)
404
+ return;
405
+ let nextIndex = null;
406
+ const current = hoverIndex ?? pointCount - 1;
407
+ switch (e.key) {
408
+ case '+':
409
+ case '=':
410
+ if (onZoomRef.current) {
411
+ e.preventDefault();
412
+ onZoomRef.current(1.2, hoverIndex);
413
+ return;
414
+ }
415
+ break;
416
+ case '-':
417
+ case '_':
418
+ if (onZoomRef.current) {
419
+ e.preventDefault();
420
+ onZoomRef.current(0.8, hoverIndex);
421
+ return;
422
+ }
423
+ break;
424
+ case '0':
425
+ if (onZoomRef.current) {
426
+ e.preventDefault();
427
+ onZoomRef.current(0, null); // 0 factor signals reset
428
+ return;
429
+ }
430
+ break;
431
+ case 'ArrowLeft':
432
+ nextIndex = Math.max(0, current - 1);
433
+ break;
434
+ case 'ArrowRight':
435
+ nextIndex = Math.min(pointCount - 1, current + 1);
436
+ break;
437
+ case 'Home':
438
+ nextIndex = 0;
439
+ break;
440
+ case 'End':
441
+ nextIndex = pointCount - 1;
442
+ break;
443
+ case 'PageUp':
444
+ nextIndex = Math.max(0, current - 10);
445
+ break;
446
+ case 'PageDown':
447
+ nextIndex = Math.min(pointCount - 1, current + 10);
448
+ break;
449
+ default:
450
+ return;
451
+ }
452
+ e.preventDefault();
453
+ setHoverIndex(nextIndex);
454
+ onHoverIndexChange?.(nextIndex);
455
+ if (nextIndex !== null) {
456
+ if (xScale) {
457
+ const x = xScale.scale(nextIndex);
458
+ const y = bounds.height / 2;
459
+ setHoverPos({ x, y });
460
+ onHoverPosChange?.({ x, y });
461
+ }
462
+ setScreenReaderAnnouncement(`Selected point ${nextIndex + 1} of ${pointCount}`);
463
+ }
464
+ engineRef.current?.renderOverlayOnly();
465
+ };
466
+ // Non-passive wheel and touch listeners to allow preventDefault without browser intervention
467
+ useEffect(() => {
468
+ const container = containerRef.current;
469
+ if (!container)
470
+ return;
471
+ const handleWheel = (e) => {
472
+ if (!onZoomRef.current)
473
+ return;
474
+ e.preventDefault();
475
+ e.stopPropagation();
476
+ const factor = e.deltaY < 0 ? 1.15 : 0.85;
477
+ let focalIndex = null;
478
+ if (xScaleRef.current) {
479
+ const rect = container.getBoundingClientRect();
480
+ const mouseX = e.clientX - rect.left;
481
+ focalIndex = xScaleRef.current.invert(mouseX);
482
+ }
483
+ onZoomRef.current(factor, focalIndex);
484
+ };
485
+ let initialPinchDist = null;
486
+ let lastTouchX = 0;
487
+ const handleTouchStart = (e) => {
488
+ if (e.touches.length === 2) {
489
+ const t1 = e.touches[0];
490
+ const t2 = e.touches[1];
491
+ initialPinchDist = Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY);
492
+ }
493
+ else if (e.touches.length === 1 && onPanRef.current && !isPanDisabledRef.current) {
494
+ lastTouchX = e.touches[0].clientX;
495
+ }
496
+ };
497
+ const handleTouchMove = (e) => {
498
+ if (e.touches.length === 2 && initialPinchDist !== null && onZoomRef.current) {
499
+ e.preventDefault();
500
+ const t1 = e.touches[0];
501
+ const t2 = e.touches[1];
502
+ const dist = Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY);
503
+ const ratio = dist / initialPinchDist;
504
+ if (Math.abs(ratio - 1) > 0.04) {
505
+ const factor = ratio > 1 ? 1.08 : 0.92;
506
+ const midX = (t1.clientX + t2.clientX) / 2;
507
+ let focalIndex = null;
508
+ if (xScaleRef.current) {
509
+ const rect = container.getBoundingClientRect();
510
+ focalIndex = xScaleRef.current.invert(midX - rect.left);
511
+ }
512
+ onZoomRef.current(factor, focalIndex);
513
+ initialPinchDist = dist;
514
+ }
515
+ }
516
+ else if (e.touches.length === 1 && onPanRef.current && !isPanDisabledRef.current) {
517
+ const currentX = e.touches[0].clientX;
518
+ const delta = currentX - lastTouchX;
519
+ lastTouchX = currentX;
520
+ if (delta !== 0) {
521
+ onPanRef.current(delta);
522
+ }
523
+ }
524
+ };
525
+ const handleTouchEnd = () => {
526
+ initialPinchDist = null;
527
+ };
528
+ container.addEventListener('wheel', handleWheel, { passive: false });
529
+ container.addEventListener('touchstart', handleTouchStart, { passive: true });
530
+ container.addEventListener('touchmove', handleTouchMove, { passive: false });
531
+ container.addEventListener('touchend', handleTouchEnd, { passive: true });
532
+ return () => {
533
+ container.removeEventListener('wheel', handleWheel);
534
+ container.removeEventListener('touchstart', handleTouchStart);
535
+ container.removeEventListener('touchmove', handleTouchMove);
536
+ container.removeEventListener('touchend', handleTouchEnd);
537
+ };
538
+ }, []);
539
+ const handlePointerDown = (e) => {
540
+ containerRef.current?.focus();
541
+ if (!onPanRef.current || isPanDisabledRef.current || e.button !== 0)
542
+ return;
543
+ isDraggingRef.current = true;
544
+ lastXRef.current = e.clientX;
545
+ try {
546
+ e.currentTarget.setPointerCapture(e.pointerId);
547
+ }
548
+ catch {
549
+ // Ignore in environments where setPointerCapture is mocked
550
+ }
551
+ };
552
+ const handlePointerMove = (e) => {
553
+ if (isDraggingRef.current && onPanRef.current) {
554
+ const delta = e.clientX - lastXRef.current;
555
+ lastXRef.current = e.clientX;
556
+ if (delta !== 0) {
557
+ onPanRef.current(delta);
558
+ }
559
+ }
560
+ };
561
+ const handlePointerUp = (e) => {
562
+ if (isDraggingRef.current) {
563
+ isDraggingRef.current = false;
564
+ try {
565
+ e.currentTarget.releasePointerCapture(e.pointerId);
566
+ }
567
+ catch {
568
+ // Ignore
569
+ }
570
+ }
571
+ };
572
+ const sync = useChartSync();
573
+ const syncHoverIndex = sync?.syncHoverIndex ?? null;
574
+ useEffect(() => {
575
+ if (syncHoverIndex !== undefined) {
576
+ engineRef.current?.requestRender();
577
+ }
578
+ }, [syncHoverIndex]);
579
+ const contextValue = useMemo(() => ({
580
+ chartId: engineRef.current?.getChartId() ?? chartIdRef.current,
581
+ bounds,
582
+ padding,
583
+ theme,
584
+ dpr: engineRef.current?.getDpr() ?? 1,
585
+ xScale,
586
+ yScale,
587
+ secondaryYScale,
588
+ hoverIndex,
589
+ syncHoverIndex,
590
+ hoverPos,
591
+ registerDataLayer,
592
+ registerOverlayLayer,
593
+ registerHitTest,
594
+ registerPlugin,
595
+ requestRender,
596
+ getEngine: () => engineRef.current,
597
+ subscribe: (evType, handler) => engineRef.current?.subscribe(evType, handler) ?? (() => { }),
598
+ emit: (ev) => engineRef.current?.emit(ev),
599
+ }), [
600
+ bounds,
601
+ padding,
602
+ theme,
603
+ xScale,
604
+ yScale,
605
+ secondaryYScale,
606
+ hoverIndex,
607
+ syncHoverIndex,
608
+ hoverPos,
609
+ registerDataLayer,
610
+ registerOverlayLayer,
611
+ registerHitTest,
612
+ registerPlugin,
613
+ requestRender,
614
+ ]);
615
+ return (_jsx(ChartContext.Provider, { value: contextValue, children: _jsxs("div", { ref: containerRef, role: "region", "aria-label": ariaLabel, tabIndex: 0, className: className, onKeyDown: handleKeyDown, onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp, style: {
616
+ position: 'relative',
617
+ width: '100%',
618
+ height: `${bounds.height}px`,
619
+ outline: 'none',
620
+ userSelect: 'none',
621
+ cursor: onPan && !isPanDisabled ? (isDraggingRef.current ? 'grabbing' : 'default') : 'default',
622
+ ...style,
623
+ }, children: [children, _jsx("div", { "aria-live": "polite", style: {
624
+ position: 'absolute',
625
+ width: '1px',
626
+ height: '1px',
627
+ margin: '-1px',
628
+ overflow: 'hidden',
629
+ clip: 'rect(0, 0, 0, 0)',
630
+ }, children: screenReaderAnnouncement })] }) }));
631
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * @file packages/chart-react/src/ChartContext.ts
3
+ * React Context providing coordinate scales, dimensions, theme, and layer registry.
4
+ */
5
+ import React from 'react';
6
+ import type { ChartPlugin, ChartTheme, Dimension, Padding, Scale, ChartEvent, ChartEventHandler, ChartEventType, ChartElementTarget, Unsubscribe } from '@tranding-custom-chart/core';
7
+ import type { CanvasChartEngine } from '@tranding-custom-chart/renderer-canvas';
8
+ export type LayerDrawFn = (ctx: CanvasRenderingContext2D, bounds: Dimension, padding: Padding, dpr: number) => void;
9
+ export type HitTestFn = (x: number, y: number) => ChartElementTarget | null;
10
+ export interface ChartContextValue {
11
+ readonly chartId: string;
12
+ readonly bounds: Dimension;
13
+ readonly padding: Padding;
14
+ readonly theme: ChartTheme;
15
+ readonly dpr: number;
16
+ readonly xScale: Scale<number> | null;
17
+ readonly yScale: Scale<number> | null;
18
+ readonly secondaryYScale?: Scale<number> | null | undefined;
19
+ readonly hoverIndex: number | null;
20
+ readonly syncHoverIndex?: number | null | undefined;
21
+ readonly hoverPos: {
22
+ readonly x: number;
23
+ readonly y: number;
24
+ } | null;
25
+ readonly registerDataLayer: (id: string, drawFn: LayerDrawFn) => () => void;
26
+ readonly registerOverlayLayer: (id: string, drawFn: LayerDrawFn) => () => void;
27
+ readonly registerHitTest: (id: string, hitTestFn: HitTestFn) => () => void;
28
+ readonly registerPlugin: <O>(plugin: ChartPlugin<O>, options?: O) => () => void;
29
+ readonly requestRender: () => void;
30
+ readonly getEngine: () => CanvasChartEngine | null;
31
+ readonly subscribe: <T = unknown>(eventType: ChartEventType | '*', handler: ChartEventHandler<T>) => Unsubscribe;
32
+ readonly emit: <T = unknown>(event: ChartEvent<T>) => void;
33
+ }
34
+ export declare const ChartContext: React.Context<ChartContextValue | null>;
35
+ export declare function useChartContext(): ChartContextValue;
36
+ //# sourceMappingURL=ChartContext.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ChartContext.d.ts","sourceRoot":"","sources":["../src/ChartContext.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAoC,MAAM,OAAO,CAAC;AACzD,OAAO,KAAK,EACV,WAAW,EACX,UAAU,EACV,SAAS,EACT,OAAO,EACP,KAAK,EACL,UAAU,EACV,iBAAiB,EACjB,cAAc,EACd,kBAAkB,EAClB,WAAW,EACZ,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wCAAwC,CAAC;AAEhF,MAAM,MAAM,WAAW,GAAG,CACxB,GAAG,EAAE,wBAAwB,EAC7B,MAAM,EAAE,SAAS,EACjB,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,MAAM,KACR,IAAI,CAAC;AAEV,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,kBAAkB,GAAG,IAAI,CAAC;AAE5E,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAC3B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;IACtC,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;IACtC,QAAQ,CAAC,eAAe,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,GAAG,IAAI,GAAG,SAAS,CAAC;IAC5D,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACpD,QAAQ,CAAC,QAAQ,EAAE;QAAE,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACrE,QAAQ,CAAC,iBAAiB,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,KAAK,MAAM,IAAI,CAAC;IAC5E,QAAQ,CAAC,oBAAoB,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,KAAK,MAAM,IAAI,CAAC;IAC/E,QAAQ,CAAC,eAAe,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,KAAK,MAAM,IAAI,CAAC;IAC3E,QAAQ,CAAC,cAAc,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,CAAC,KAAK,MAAM,IAAI,CAAC;IAChF,QAAQ,CAAC,aAAa,EAAE,MAAM,IAAI,CAAC;IACnC,QAAQ,CAAC,SAAS,EAAE,MAAM,iBAAiB,GAAG,IAAI,CAAC;IACnD,QAAQ,CAAC,SAAS,EAAE,CAAC,CAAC,GAAG,OAAO,EAAE,SAAS,EAAE,cAAc,GAAG,GAAG,EAAE,OAAO,EAAE,iBAAiB,CAAC,CAAC,CAAC,KAAK,WAAW,CAAC;IACjH,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,GAAG,OAAO,EAAE,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC;CAC5D;AAED,eAAO,MAAM,YAAY,yCAAgD,CAAC;AAE1E,wBAAgB,eAAe,IAAI,iBAAiB,CAMnD"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @file packages/chart-react/src/ChartContext.ts
3
+ * React Context providing coordinate scales, dimensions, theme, and layer registry.
4
+ */
5
+ import React, { createContext, useContext } from 'react';
6
+ export const ChartContext = createContext(null);
7
+ export function useChartContext() {
8
+ const ctx = useContext(ChartContext);
9
+ if (!ctx) {
10
+ throw new Error('useChartContext must be used within a <ChartContainer>');
11
+ }
12
+ return ctx;
13
+ }