@react-native-ohos/victory-native-xl 41.17.5-rc.1 → 41.18.0-beta.1

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.
@@ -1,709 +1,795 @@
1
- import * as React from "react";
2
- import { type LayoutChangeEvent, Button, View } from "react-native";
3
- import { Canvas, Group, rect, Path } from "@shopify/react-native-skia";
4
- import { useSharedValue } from "react-native-reanimated";
5
- import {
6
- type ComposedGesture,
7
- Gesture,
8
- GestureDetector,
9
- GestureHandlerRootView,
10
- type TouchData,
11
- } from "react-native-gesture-handler";
12
- import { type MutableRefObject } from "react";
13
- import type { ScaleLinear } from "d3-scale";
14
- import type {
15
- AxisProps,
16
- CartesianChartRenderArg,
17
- InputFields,
18
- NumericalFields,
19
- SidedNumber,
20
- TransformedData,
21
- ChartBounds,
22
- Viewport,
23
- ChartPressPanConfig,
24
- YAxisInputProps,
25
- FrameInputProps,
26
- XAxisInputProps,
27
- GestureHandlerConfig
28
- } from "victory-native/src/types";
29
- import { transformInputData } from "victory-native/src/cartesian/utils/transformInputData";
30
- import { findClosestPoint } from "victory-native/src/utils/findClosestPoint";
31
- import { valueFromSidedNumber } from "victory-native/src/utils/valueFromSidedNumber";
32
- import {
33
- CartesianAxis,
34
- CartesianAxisDefaultProps,
35
- } from "victory-native/src/cartesian/components/CartesianAxis";
36
- import { asNumber } from "victory-native/src/utils/asNumber";
37
- import type { ChartPressState,
38
- ChartPressStateInit, } from "victory-native/src/cartesian/hooks/useChartPressState";
39
- import { useFunctionRef } from "victory-native/src/hooks/useFunctionRef";
40
- import { CartesianChartProvider } from "victory-native/src/cartesian/contexts/CartesianChartContext";
41
- import { XAxis } from "victory-native/src/cartesian/components/XAxis";
42
- import { YAxis } from "victory-native/src/cartesian/components/YAxis";
43
- import { Frame } from "victory-native/src/cartesian/components/Frame";
44
- import { useBuildChartAxis } from "victory-native/src/cartesian/hooks/useBuildChartAxis";
45
- import { type ChartTransformState } from "victory-native/src/cartesian/hooks/useChartTransformState";
46
- import {
47
- panTransformGesture,
48
- type PanTransformGestureConfig,
49
- pinchTransformGesture,
50
- type PinchTransformGestureConfig,
51
- } from "victory-native/src/cartesian/utils/transformGestures";
52
- import {
53
- CartesianTransformProvider,
54
- useCartesianTransformContext,
55
- } from "victory-native/src/cartesian/contexts/CartesianTransformContext";
56
- import { downsampleTicks } from "victory-native/src/utils/tickHelpers";
57
- import { GestureHandler } from "victory-native/src/shared/GestureHandler";
58
- import { boundsToClip } from "victory-native/src/utils/boundsToClip";
59
- import { normalizeYAxisTicks } from "victory-native/src/utils/normalizeYAxisTicks";
60
- import { createFallbackChartState } from "victory-native/src/cartesian/utils/createFallbackChartState";
61
- import { ZoomTransform } from "d3-zoom";
62
- import isEqual from "react-fast-compare";
63
- export type CartesianActionsHandle<T = undefined> =
64
- T extends ChartPressState<infer S>
65
- ? S extends ChartPressStateInit
66
- ? {
67
- handleTouch: (v: T, x: number, y: number) => void;
68
- }
69
- : never
70
- : never;
71
-
72
- type CartesianChartProps<
73
- RawData extends Record<string, unknown>,
74
- XK extends keyof InputFields<RawData>,
75
- YK extends keyof NumericalFields<RawData>,
76
- > = {
77
- data: RawData[];
78
- xKey: XK;
79
- yKeys: YK[];
80
- padding?: SidedNumber;
81
- domainPadding?: SidedNumber;
82
- domain?: { x?: [number] | [number, number]; y?: [number] | [number, number] };
83
- viewport?: Viewport;
84
- chartPressState?:
85
- | ChartPressState<{ x: InputFields<RawData>[XK]; y: Record<YK, number> }>
86
- | ChartPressState<{ x: InputFields<RawData>[XK]; y: Record<YK, number> }>[];
87
- chartPressConfig?: {
88
- pan?: ChartPressPanConfig;
89
- };
90
- gestureHandlerConfig?: GestureHandlerConfig;
91
- children: (args: CartesianChartRenderArg<RawData, YK>) => React.ReactNode;
92
- renderOutside?: (
93
- args: CartesianChartRenderArg<RawData, YK>,
94
- ) => React.ReactNode;
95
- axisOptions?: Partial<Omit<AxisProps<RawData, XK, YK>, "xScale" | "yScale">>;
96
-
97
- onChartBoundsChange?: (bounds: ChartBounds) => void;
98
- onScaleChange?: (
99
- xScale: ScaleLinear<number, number>,
100
- yScale: ScaleLinear<number, number>,
101
- ) => void;
102
- /**
103
- * @deprecated This prop will eventually be replaced by the new `chartPressConfig`. For now it's being kept around for backwards compatibility sake.
104
- */
105
- gestureLongPressDelay?: number;
106
- xAxis?: XAxisInputProps<RawData, XK>;
107
- yAxis?: YAxisInputProps<RawData, YK>[];
108
- frame?: FrameInputProps;
109
- transformState?: ChartTransformState;
110
- transformConfig?: {
111
- pan?: PanTransformGestureConfig;
112
- pinch?: PinchTransformGestureConfig;
113
- };
114
- customGestures?: ComposedGesture;
115
- actionsRef?: MutableRefObject<CartesianActionsHandle<
116
- | ChartPressState<{
117
- x: InputFields<RawData>[XK];
118
- y: Record<YK, number>;
119
- }>
120
- | undefined
121
- > | null>;
122
- };
123
-
124
- export function CartesianChart<
125
- RawData extends Record<string, unknown>,
126
- XK extends keyof InputFields<RawData>,
127
- YK extends keyof NumericalFields<RawData>,
128
- >({ transformState, children, ...rest }: CartesianChartProps<RawData, XK, YK>) {
129
- return (
130
- <CartesianTransformProvider transformState={transformState}>
131
- <CartesianChartContent {...{ ...rest, transformState }}>
132
- {children}
133
- </CartesianChartContent>
134
- </CartesianTransformProvider>
135
- );
136
- }
137
-
138
- function CartesianChartContent<
139
- RawData extends Record<string, unknown>,
140
- XK extends keyof InputFields<RawData>,
141
- YK extends keyof NumericalFields<RawData>,
142
- >({
143
- data,
144
- xKey,
145
- yKeys,
146
- padding,
147
- domainPadding,
148
- children,
149
- renderOutside = () => null,
150
- axisOptions,
151
- domain,
152
- chartPressState,
153
- chartPressConfig,
154
- gestureHandlerConfig,
155
- onChartBoundsChange,
156
- onScaleChange,
157
- gestureLongPressDelay = 100,
158
- xAxis,
159
- yAxis,
160
- frame,
161
- transformState,
162
- transformConfig,
163
- customGestures,
164
- actionsRef,
165
- viewport,
166
- }: CartesianChartProps<RawData, XK, YK>) {
167
- const [size, setSize] = React.useState({ width: 0, height: 0 });
168
- const chartBoundsRef = React.useRef<ChartBounds | undefined>(undefined);
169
- const xScaleRef = React.useRef<ScaleLinear<number, number> | undefined>(
170
- undefined,
171
- );
172
- const yScaleRef = React.useRef<ScaleLinear<number, number> | undefined>(
173
- undefined,
174
- );
175
- const [hasMeasuredLayoutSize, setHasMeasuredLayoutSize] =
176
- React.useState(false);
177
- const onLayout = React.useCallback(
178
- ({ nativeEvent: { layout } }: LayoutChangeEvent) => {
179
- setHasMeasuredLayoutSize(true);
180
- setSize(layout);
181
- },
182
- [],
183
- );
184
- const normalizedAxisProps = useBuildChartAxis({
185
- xAxis,
186
- yAxis,
187
- frame,
188
- yKeys,
189
- axisOptions,
190
- });
191
-
192
- // create a d3-zoom transform object based on the current transform state. This
193
- // is used for rescaling the X and Y axes.
194
- const transform = useCartesianTransformContext();
195
- const zoomX = React.useMemo(
196
- () => new ZoomTransform(transform.k, transform.tx, transform.ty),
197
- [transform.k, transform.tx, transform.ty],
198
- );
199
- const zoomY = React.useMemo(
200
- () => new ZoomTransform(transform.ky, transform.tx, transform.ty),
201
- [transform.ky, transform.tx, transform.ty],
202
- );
203
-
204
- const tData = useSharedValue<TransformedData<RawData, XK, YK>>({
205
- ix: [],
206
- ox: [],
207
- y: yKeys.reduce(
208
- (acc, key) => {
209
- acc[key] = { i: [], o: [] };
210
- return acc;
211
- },
212
- {} as TransformedData<RawData, XK, YK>["y"],
213
- ),
214
- });
215
-
216
- const {
217
- yAxes,
218
- xScale,
219
- chartBounds,
220
- isNumericalData,
221
- xTicksNormalized,
222
- _tData,
223
- } = React.useMemo(() => {
224
- if (!data.length) {
225
- return createFallbackChartState<RawData, XK, YK>(yKeys);
226
- }
227
- const { xScale, yAxes, isNumericalData, xTicksNormalized, ..._tData } =
228
- transformInputData({
229
- data,
230
- xKey,
231
- yKeys,
232
- outputWindow: {
233
- xMin: valueFromSidedNumber(padding, "left"),
234
- xMax: size.width - valueFromSidedNumber(padding, "right"),
235
- yMin: valueFromSidedNumber(padding, "top"),
236
- yMax: size.height - valueFromSidedNumber(padding, "bottom"),
237
- },
238
- domain,
239
- domainPadding,
240
- xAxis: normalizedAxisProps.xAxis,
241
- yAxes: normalizedAxisProps.yAxes,
242
- viewport,
243
- labelRotate: normalizedAxisProps.xAxis.labelRotate,
244
- });
245
-
246
- const primaryYAxis = yAxes[0];
247
- const primaryYScale = primaryYAxis.yScale;
248
- const chartBounds = {
249
- left: xScale(viewport?.x?.[0] ?? xScale.domain().at(0) ?? 0),
250
- right: xScale(viewport?.x?.[1] ?? xScale.domain().at(-1) ?? 0),
251
- top: primaryYScale(
252
- viewport?.y?.[1] ?? (primaryYScale.domain().at(0) || 0),
253
- ),
254
- bottom: primaryYScale(
255
- viewport?.y?.[0] ?? (primaryYScale.domain().at(-1) || 0),
256
- ),
257
- };
258
-
259
- return {
260
- xTicksNormalized,
261
- yAxes,
262
- xScale,
263
- chartBounds,
264
- isNumericalData,
265
- _tData,
266
- };
267
- }, [
268
- data,
269
- xKey,
270
- yKeys,
271
- padding,
272
- size.width,
273
- size.height,
274
- domain,
275
- domainPadding,
276
- normalizedAxisProps,
277
- viewport,
278
- ]);
279
-
280
- React.useEffect(() => {
281
- tData.value = _tData;
282
- }, [_tData, tData]);
283
-
284
- const primaryYAxis = yAxes[0];
285
- const primaryYScale = primaryYAxis.yScale;
286
-
287
- // stacked bar values
288
- const chartHeight = chartBounds.bottom;
289
- const yScaleTop = primaryYAxis.yScale.domain().at(0);
290
- const yScaleBottom = primaryYAxis.yScale.domain().at(-1);
291
- // end stacked bar values
292
-
293
- /**
294
- * Pan gesture handling
295
- */
296
- const lastIdx = useSharedValue(null as null | number);
297
- /**
298
- * Take a "press value" and an x-value and update the shared values accordingly.
299
- */
300
- const handleTouch = (
301
- v: ChartPressState<{ x: InputFields<RawData>[XK]; y: Record<YK, number> }>,
302
- x: number,
303
- y: number,
304
- ) => {
305
- "worklet";
306
- const idx = findClosestPoint(tData.value.ox, x);
307
-
308
- if (typeof idx !== "number") return;
309
-
310
- const isInYs = (yk: string): yk is YK & string => yKeys.includes(yk as YK);
311
-
312
- // begin stacked bar handling:
313
- // store the heights of each bar segment
314
- const barHeights: number[] = [];
315
- for (const yk in v.y) {
316
- if (isInYs(yk)) {
317
- const height = asNumber(tData.value.y[yk].i[idx]);
318
- barHeights.push(height);
319
- }
320
- }
321
-
322
- const chartYPressed = chartHeight - y; // Invert y-coordinate, since RNGH gives us the absolute Y, and we want to know where in the chart they clicked
323
- // Calculate the actual yValue of the touch within the domain of the yScale
324
- const yDomainValue =
325
- (chartYPressed / chartHeight) * (yScaleTop! - yScaleBottom!);
326
-
327
- // track the cumulative height and the y-index of the touched segment
328
- let cumulativeHeight = 0;
329
- let yIndex = -1;
330
-
331
- // loop through the bar heights to find which bar was touched
332
- for (let i = 0; i < barHeights.length; i++) {
333
- // Accumulate the height as we go along
334
- cumulativeHeight += barHeights[i]!;
335
- // Check if the y-value touched falls within the current segment
336
- if (yDomainValue <= cumulativeHeight) {
337
- // If it does, set yIndex to the current segment index and break
338
- yIndex = i;
339
- break;
340
- }
341
- }
342
-
343
- // Update the yIndex value in the state or context
344
- v.yIndex.value = yIndex;
345
- // end stacked bar handling
346
-
347
- if (v) {
348
- try {
349
- v.matchedIndex.value = idx;
350
- v.x.value.value = tData.value.ix[idx]!;
351
- v.x.position.value = asNumber(tData.value.ox[idx]);
352
- for (const yk in v.y) {
353
- if (isInYs(yk)) {
354
- v.y[yk].value.value = asNumber(tData.value.y[yk].i[idx]);
355
- v.y[yk].position.value = asNumber(tData.value.y[yk].o[idx]);
356
- }
357
- }
358
- } catch (err) {
359
- // no-op
360
- }
361
- }
362
-
363
- lastIdx.value = idx;
364
- };
365
-
366
- if (actionsRef) {
367
- actionsRef.current = {
368
- handleTouch,
369
- };
370
- }
371
-
372
- /**
373
- * Touch gesture is a modified Pan gesture handler that allows for multiple presses:
374
- * - Using Pan Gesture handler effectively _just_ for the .activateAfterLongPress functionality.
375
- * - Tracking the finger is handled with .onTouchesMove instead of .onUpdate, because
376
- * .onTouchesMove gives us access to each individual finger.
377
- * - The activation gets a bit complicated because we want to wait til "start" state before updating Press Value
378
- * which gives time for the gesture to get cancelled before we start updating the shared values.
379
- * Therefore we use gestureState.bootstrap to store some "bootstrap" information if gesture isn't active when finger goes down.
380
- */
381
- // touch ID -> value index mapping to keep track of which finger updates which value
382
- const touchMap = useSharedValue({} as Record<number, number | undefined>);
383
- const activePressSharedValues = Array.isArray(chartPressState)
384
- ? chartPressState
385
- : [chartPressState];
386
- const gestureState = useSharedValue({
387
- isGestureActive: false,
388
- bootstrap: [] as [
389
- ChartPressState<{ x: InputFields<RawData>[XK]; y: Record<YK, number> }>,
390
- TouchData,
391
- ][],
392
- });
393
-
394
- const panGesture = Gesture.Pan()
395
- /**
396
- * When a finger goes down, either update the state or store in the bootstrap array.
397
- */
398
- .onTouchesDown((e) => {
399
- const vals = activePressSharedValues || [];
400
- if (!vals.length || e.numberOfTouches === 0) return;
401
-
402
- for (let i = 0; i < Math.min(e.allTouches.length, vals.length); i++) {
403
- const touch = e.allTouches[i];
404
- const v = vals[i];
405
- if (!v || !touch) continue;
406
-
407
- if (gestureState.value.isGestureActive) {
408
- // Update the mapping
409
- if (typeof touchMap.value[touch.id] !== "number")
410
- touchMap.value[touch.id] = i;
411
-
412
- v.isActive.value = true;
413
- handleTouch(v, touch.x, touch.y);
414
- } else {
415
- gestureState.value.bootstrap.push([v, touch]);
416
- }
417
- }
418
- })
419
- /**
420
- * On start, check if we have any bootstrapped updates we need to apply.
421
- */
422
- .onStart(() => {
423
- gestureState.value.isGestureActive = true;
424
-
425
- for (let i = 0; i < gestureState.value.bootstrap.length; i++) {
426
- const [v, touch] = gestureState.value.bootstrap[i]!;
427
- // Update the mapping
428
- if (typeof touchMap.value[touch.id] !== "number")
429
- touchMap.value[touch.id] = i;
430
-
431
- v.isActive.value = true;
432
- handleTouch(v, touch.x, touch.y);
433
- }
434
- })
435
- /**
436
- * Clear gesture state on gesture end.
437
- */
438
- .onFinalize(() => {
439
- gestureState.value.isGestureActive = false;
440
- gestureState.value.bootstrap = [];
441
- })
442
- /**
443
- * As fingers move, update the shared values accordingly.
444
- */
445
- .onTouchesMove((e) => {
446
- const vals = activePressSharedValues || [];
447
- if (!vals.length || e.numberOfTouches === 0) return;
448
-
449
- for (let i = 0; i < Math.min(e.allTouches.length, vals.length); i++) {
450
- const touch = e.allTouches[i];
451
- const touchId = touch?.id;
452
- const idx = typeof touchId === "number" && touchMap.value[touchId];
453
- const v = typeof idx === "number" && vals?.[idx];
454
-
455
- if (!v || !touch) continue;
456
- if (!v.isActive.value) v.isActive.value = true;
457
- handleTouch(v, touch.x, touch.y);
458
- }
459
- })
460
- /**
461
- * On each finger up, start to update values and "free up" the touch map.
462
- */
463
- .onTouchesUp((e) => {
464
- for (const touch of e.changedTouches) {
465
- const vals = activePressSharedValues || [];
466
-
467
- // Set active state to false
468
- const touchId = touch?.id;
469
- const idx = typeof touchId === "number" && touchMap.value[touchId];
470
- const val = typeof idx === "number" && vals[idx];
471
- if (val) {
472
- val.isActive.value = false;
473
- }
474
-
475
- // Free up touch map for this touch
476
- touchMap.value[touch.id] = undefined;
477
- }
478
- })
479
- /**
480
- * Once the gesture ends, ensure all active values are falsified.
481
- */
482
- .onEnd(() => {
483
- const vals = activePressSharedValues || [];
484
- // Set active state to false for all vals
485
- for (const val of vals) {
486
- if (val) {
487
- val.isActive.value = false;
488
- }
489
- }
490
- });
491
-
492
- if (!chartPressConfig?.pan) {
493
- /**
494
- * Activate after a long press, which helps with preventing all touch hijacking.
495
- * This is important if this chart is inside of some sort of scrollable container.
496
- */
497
- panGesture.activateAfterLongPress(gestureLongPressDelay);
498
- }
499
-
500
- if (chartPressConfig?.pan?.activateAfterLongPress) {
501
- panGesture.activateAfterLongPress(
502
- chartPressConfig.pan?.activateAfterLongPress,
503
- );
504
- }
505
- if (chartPressConfig?.pan?.activeOffsetX) {
506
- panGesture.activeOffsetX(chartPressConfig.pan.activeOffsetX);
507
- }
508
- if (chartPressConfig?.pan?.activeOffsetY) {
509
- panGesture.activeOffsetX(chartPressConfig.pan.activeOffsetY);
510
- }
511
- if (chartPressConfig?.pan?.failOffsetX) {
512
- panGesture.failOffsetX(chartPressConfig.pan.failOffsetX);
513
- }
514
- if (chartPressConfig?.pan?.failOffsetY) {
515
- panGesture.failOffsetX(chartPressConfig.pan.failOffsetY);
516
- }
517
-
518
- /**
519
- * Allow end-user to request "raw-ish" data for a given yKey.
520
- * Generate this on demand using a proxy.
521
- */
522
- type PointsArg = CartesianChartRenderArg<RawData, YK>["points"];
523
- const points = React.useMemo<PointsArg>(() => {
524
- const cache = {} as Record<YK, PointsArg[keyof PointsArg]>;
525
- return new Proxy(
526
- {},
527
- {
528
- get(_, property: string): PointsArg[keyof PointsArg] | undefined {
529
- const key = property as YK;
530
- if (!yKeys.includes(key)) return undefined;
531
- if (cache[key]) return cache[key];
532
-
533
- cache[key] = _tData.ix.map((x, i) => ({
534
- x: asNumber(_tData.ox[i]),
535
- xValue: x,
536
- y: _tData.y[key].o[i],
537
- yValue: _tData.y[key].i[i],
538
- }));
539
-
540
- return cache[key];
541
- },
542
- },
543
- ) as PointsArg;
544
- }, [_tData, yKeys]);
545
-
546
- // On bounds change, emit
547
- const onChartBoundsRef = useFunctionRef(onChartBoundsChange);
548
- React.useEffect(() => {
549
- if (!isEqual(chartBounds, chartBoundsRef.current)) {
550
- chartBoundsRef.current = chartBounds;
551
- onChartBoundsRef.current?.(chartBounds);
552
- }
553
- }, [chartBounds, onChartBoundsRef]);
554
-
555
- const onScaleRef = useFunctionRef(onScaleChange);
556
- React.useEffect(() => {
557
- const rescaledX = zoomX.rescaleX(xScale);
558
- const rescaledY = zoomY.rescaleY(primaryYScale);
559
- if (
560
- !isEqual(xScaleRef.current?.domain(), rescaledX.domain()) ||
561
- !isEqual(yScaleRef.current?.domain(), rescaledY.domain()) ||
562
- !isEqual(xScaleRef.current?.range(), rescaledX.range()) ||
563
- !isEqual(yScaleRef.current?.range(), rescaledY.range())
564
- ) {
565
- xScaleRef.current = xScale;
566
- yScaleRef.current = primaryYScale;
567
- onScaleRef.current?.(rescaledX, rescaledY);
568
- }
569
- }, [onScaleChange, onScaleRef, xScale, zoomX, zoomY, primaryYScale]);
570
-
571
- const renderArg: CartesianChartRenderArg<RawData, YK> = {
572
- xScale,
573
- xTicks: xTicksNormalized,
574
- yScale: primaryYScale,
575
- yTicks: primaryYAxis.yTicksNormalized,
576
- chartBounds,
577
- canvasSize: size,
578
- points,
579
- };
580
-
581
- const clipRect = boundsToClip(chartBounds);
582
- const YAxisComponents =
583
- hasMeasuredLayoutSize && (axisOptions || yAxes)
584
- ? normalizedAxisProps.yAxes?.map((axis, index) => {
585
- const yAxis = yAxes[index];
586
-
587
- if (!yAxis) return null;
588
-
589
- const primaryAxisProps = normalizedAxisProps.yAxes[0]!;
590
- const primaryRescaled = zoomY.rescaleY(primaryYScale);
591
- const rescaled = zoomY.rescaleY(yAxis.yScale);
592
-
593
- const rescaledTicks = axis.tickValues
594
- ? downsampleTicks(axis.tickValues, axis.tickCount)
595
- : axis.enableRescaling
596
- ? rescaled.ticks(axis.tickCount)
597
- : yAxis.yScale.ticks(axis.tickCount);
598
-
599
- const primaryTicksRescaled = primaryAxisProps.tickValues
600
- ? downsampleTicks(
601
- primaryAxisProps.tickValues,
602
- primaryAxisProps.tickCount,
603
- )
604
- : primaryAxisProps.enableRescaling
605
- ? primaryRescaled.ticks(primaryAxisProps.tickCount)
606
- : primaryYScale.ticks(primaryAxisProps.tickCount);
607
-
608
- return (
609
- <YAxis
610
- key={index}
611
- {...axis}
612
- xScale={zoomX.rescaleX(xScale)}
613
- yScale={rescaled}
614
- yTicksNormalized={
615
- index > 0 && !axis.tickValues
616
- ? normalizeYAxisTicks(
617
- primaryTicksRescaled,
618
- primaryRescaled,
619
- rescaled,
620
- )
621
- : rescaledTicks
622
- }
623
- chartBounds={chartBounds}
624
- />
625
- );
626
- })
627
- : null;
628
-
629
- const XAxisComponents =
630
- hasMeasuredLayoutSize && (axisOptions || xAxis) ? (
631
- <XAxis
632
- {...normalizedAxisProps.xAxis}
633
- xScale={xScale}
634
- yScale={zoomY.rescaleY(primaryYScale)}
635
- ix={_tData.ix}
636
- isNumericalData={isNumericalData}
637
- chartBounds={chartBounds}
638
- zoom={zoomX}
639
- />
640
- ) : null;
641
-
642
- const FrameComponent =
643
- hasMeasuredLayoutSize && (axisOptions || frame) ? (
644
- <Frame
645
- {...normalizedAxisProps.frame}
646
- xScale={xScale}
647
- yScale={primaryYScale}
648
- />
649
- ) : null;
650
-
651
- // Body of the chart.
652
- const body = (
653
- <Canvas style={{ flex: 1 }} onLayout={onLayout}>
654
- {YAxisComponents}
655
- {XAxisComponents}
656
- {FrameComponent}
657
- <CartesianChartProvider yScale={primaryYScale} xScale={xScale}>
658
- <Group clip={clipRect}>
659
- <Group matrix={transformState?.matrix}>
660
- {hasMeasuredLayoutSize && children(renderArg)}
661
- </Group>
662
- </Group>
663
- </CartesianChartProvider>
664
- {hasMeasuredLayoutSize && renderOutside?.(renderArg)}
665
- </Canvas>
666
- );
667
-
668
- let composed = customGestures ?? Gesture.Race();
669
- if (transformState) {
670
- let gestures = Gesture.Simultaneous();
671
-
672
- if (transformConfig?.pinch?.enabled ?? true) {
673
- gestures = Gesture.Simultaneous(
674
- gestures,
675
- pinchTransformGesture(transformState, transformConfig?.pinch),
676
- );
677
- }
678
-
679
- if (transformConfig?.pan?.enabled ?? true) {
680
- gestures = Gesture.Simultaneous(
681
- gestures,
682
- panTransformGesture(transformState, transformConfig?.pan),
683
- );
684
- }
685
-
686
- composed = Gesture.Race(composed, Gesture.Simultaneous(gestures));
687
- }
688
- if (chartPressState) {
689
- composed = Gesture.Race(composed, panGesture);
690
- }
691
-
692
- return (
693
- <GestureHandlerRootView style={{ flex: 1, overflow: "hidden" }}>
694
- {body}
695
- <GestureHandler
696
- config={gestureHandlerConfig}
697
- gesture={composed}
698
- transformState={transformState}
699
- dimensions={{
700
- x: Math.min(xScale.range()[0]!, 0),
701
- y: Math.min(primaryYScale.range()[0]!, 0),
702
- width: xScale.range()[1]! - Math.min(xScale.range()[0]!, 0),
703
- height:
704
- primaryYScale.range()[1]! - Math.min(primaryYScale.range()[0]!, 0),
705
- }}
706
- />
707
- </GestureHandlerRootView>
708
- );
709
- }
1
+ import * as React from "react";
2
+ import { View, type LayoutChangeEvent } from "react-native";
3
+ import { Canvas, Group, type CanvasRef } from "@shopify/react-native-skia";
4
+ import { useSharedValue, useAnimatedReaction, runOnJS } from "react-native-reanimated";
5
+ import {
6
+ type ComposedGesture,
7
+ Gesture,
8
+ GestureHandlerRootView,
9
+ type TouchData,
10
+ } from "react-native-gesture-handler";
11
+ import { type MutableRefObject } from "react";
12
+ import { ZoomTransform } from "d3-zoom";
13
+ import type { ScaleLinear, ScaleLogarithmic } from "d3-scale";
14
+ import isEqual from "react-fast-compare";
15
+ import type {
16
+ AxisProps,
17
+ CartesianChartRenderArg,
18
+ InputFields,
19
+ NumericalFields,
20
+ SidedNumber,
21
+ TransformedData,
22
+ ChartBounds,
23
+ YAxisInputProps,
24
+ XAxisInputProps,
25
+ FrameInputProps,
26
+ ChartPressPanConfig,
27
+ Viewport,
28
+ GestureHandlerConfig,
29
+ } from "victory-native/src/types";
30
+ import { transformInputData } from "victory-native/src/cartesian/utils/transformInputData";
31
+ import { findClosestPoint } from "victory-native/src/utils/findClosestPoint";
32
+ import { valueFromSidedNumber } from "victory-native/src/utils/valueFromSidedNumber";
33
+ import { asNumber } from "victory-native/src/utils/asNumber";
34
+ import type {
35
+ ChartPressState,
36
+ ChartPressStateInit,
37
+ } from "victory-native/src/cartesian/hooks/useChartPressState";
38
+ import { useFunctionRef } from "victory-native/src/hooks/useFunctionRef";
39
+ import { CartesianChartProvider } from "victory-native/src/cartesian/contexts/CartesianChartContext";
40
+ import { XAxis } from "victory-native/src/cartesian/components/XAxis";
41
+ import { YAxis } from "victory-native/src/cartesian/components/YAxis";
42
+ import { Frame } from "victory-native/src/cartesian/components/Frame";
43
+ import { useBuildChartAxis } from "victory-native/src/cartesian/hooks/useBuildChartAxis";
44
+ import { type ChartTransformState } from "victory-native/src/cartesian/hooks/useChartTransformState";
45
+ import {
46
+ panTransformGesture,
47
+ type PanTransformGestureConfig,
48
+ pinchTransformGesture,
49
+ type PinchTransformGestureConfig,
50
+ } from "victory-native/src/cartesian/utils/transformGestures";
51
+ import {
52
+ CartesianTransformProvider,
53
+ useCartesianTransformContext,
54
+ } from "victory-native/src/cartesian/contexts/CartesianTransformContext";
55
+ import { downsampleTicks } from "victory-native/src/utils/tickHelpers";
56
+ import { GestureHandler } from "../shared/GestureHandler";
57
+ import { boundsToClip } from "victory-native/src/utils/boundsToClip";
58
+ import { normalizeYAxisTicks } from "victory-native/src/utils/normalizeYAxisTicks";
59
+ import { createFallbackChartState } from "victory-native/src/cartesian/utils/createFallbackChartState";
60
+
61
+ export type CartesianActionsHandle<T = undefined> =
62
+ T extends ChartPressState<infer S>
63
+ ? S extends ChartPressStateInit
64
+ ? {
65
+ handleTouch: (v: T, x: number, y: number) => void;
66
+ }
67
+ : never
68
+ : never;
69
+
70
+ export type CartesianChartRef<T = undefined> = {
71
+ canvas: CanvasRef | null;
72
+ actions: CartesianActionsHandle<T>;
73
+ };
74
+
75
+ type CartesianChartProps<
76
+ RawData extends Record<string, unknown>,
77
+ XK extends keyof InputFields<RawData>,
78
+ YK extends keyof NumericalFields<RawData>,
79
+ > = {
80
+ data: RawData[];
81
+ xKey: XK;
82
+ yKeys: YK[];
83
+ padding?: SidedNumber;
84
+ domainPadding?: SidedNumber;
85
+ domain?: { x?: [number] | [number, number]; y?: [number] | [number, number] };
86
+ viewport?: Viewport;
87
+ chartPressState?:
88
+ | ChartPressState<{ x: InputFields<RawData>[XK]; y: Record<YK, number> }>
89
+ | ChartPressState<{ x: InputFields<RawData>[XK]; y: Record<YK, number> }>[];
90
+ chartPressConfig?: {
91
+ pan?: ChartPressPanConfig;
92
+ };
93
+ gestureHandlerConfig?: GestureHandlerConfig;
94
+ children: (args: CartesianChartRenderArg<RawData, YK>) => React.ReactNode;
95
+ renderOutside?: (
96
+ args: CartesianChartRenderArg<RawData, YK>,
97
+ ) => React.ReactNode;
98
+ axisOptions?: Partial<Omit<AxisProps<RawData, XK, YK>, "xScale" | "yScale">>;
99
+ onChartBoundsChange?: (bounds: ChartBounds) => void;
100
+ onScaleChange?: (
101
+ xScale: ScaleLinear<number, number>,
102
+ yScale: ScaleLinear<number, number>,
103
+ ) => void;
104
+ /**
105
+ * @deprecated This prop will eventually be replaced by the new `chartPressConfig`. For now it's being kept around for backwards compatibility sake.
106
+ */
107
+ gestureLongPressDelay?: number;
108
+ xAxis?: XAxisInputProps<RawData, XK>;
109
+ yAxis?: YAxisInputProps<RawData, YK>[];
110
+ frame?: FrameInputProps;
111
+ transformState?: ChartTransformState;
112
+ transformConfig?: {
113
+ pan?: PanTransformGestureConfig;
114
+ pinch?: PinchTransformGestureConfig;
115
+ };
116
+ /** Optional hook for ScrollView coordination (e.g. disable scroll on pan/pinch). */
117
+ onTransformInteractionChange?: (active: boolean) => void;
118
+ customGestures?: ComposedGesture;
119
+ actionsRef?: MutableRefObject<CartesianActionsHandle<
120
+ | ChartPressState<{
121
+ x: InputFields<RawData>[XK];
122
+ y: Record<YK, number>;
123
+ }>
124
+ | undefined
125
+ > | null>;
126
+ ref?: React.Ref<
127
+ CartesianChartRef<
128
+ | ChartPressState<{
129
+ x: InputFields<RawData>[XK];
130
+ y: Record<YK, number>;
131
+ }>
132
+ | undefined
133
+ >
134
+ >;
135
+ };
136
+
137
+ export function CartesianChart<
138
+ RawData extends Record<string, unknown>,
139
+ XK extends keyof InputFields<RawData>,
140
+ YK extends keyof NumericalFields<RawData>,
141
+ >({
142
+ transformState,
143
+ children,
144
+ ref,
145
+ ...rest
146
+ }: CartesianChartProps<RawData, XK, YK>) {
147
+ return (
148
+ <CartesianTransformProvider transformState={transformState}>
149
+ <CartesianChartContent {...{ ...rest, transformState }} ref={ref}>
150
+ {children}
151
+ </CartesianChartContent>
152
+ </CartesianTransformProvider>
153
+ );
154
+ }
155
+
156
+ function CartesianChartContent<
157
+ RawData extends Record<string, unknown>,
158
+ XK extends keyof InputFields<RawData>,
159
+ YK extends keyof NumericalFields<RawData>,
160
+ >({
161
+ data,
162
+ xKey,
163
+ yKeys,
164
+ padding,
165
+ domainPadding,
166
+ children,
167
+ renderOutside = () => null,
168
+ axisOptions,
169
+ domain,
170
+ chartPressState,
171
+ chartPressConfig,
172
+ gestureHandlerConfig,
173
+ onChartBoundsChange,
174
+ onScaleChange,
175
+ gestureLongPressDelay = 100,
176
+ xAxis,
177
+ yAxis,
178
+ frame,
179
+ transformState,
180
+ transformConfig,
181
+ onTransformInteractionChange,
182
+ customGestures,
183
+ actionsRef,
184
+ viewport,
185
+ ref,
186
+ }: CartesianChartProps<RawData, XK, YK>) {
187
+ const onTransformInteractionChangeRef = useFunctionRef(
188
+ onTransformInteractionChange,
189
+ );
190
+
191
+ useAnimatedReaction(
192
+ () =>
193
+ transformState
194
+ ? transformState.panActive.value || transformState.zoomActive.value
195
+ : false,
196
+ (active, prev) => {
197
+ if (
198
+ !onTransformInteractionChangeRef.current ||
199
+ prev === null ||
200
+ active === prev
201
+ ) {
202
+ return;
203
+ }
204
+ runOnJS(onTransformInteractionChangeRef.current)(active);
205
+ },
206
+ );
207
+
208
+ const [size, setSize] = React.useState({ width: 0, height: 0 });
209
+ const chartBoundsRef = React.useRef<ChartBounds | undefined>(undefined);
210
+ const xScaleRef = React.useRef<
211
+ ScaleLogarithmic<number, number> | ScaleLinear<number, number> | undefined
212
+ >(undefined);
213
+
214
+ const yScaleRef = React.useRef<ScaleLinear<number, number> | undefined>(
215
+ undefined,
216
+ );
217
+ const canvasRef = React.useRef<CanvasRef | null>(null);
218
+ const [hasMeasuredLayoutSize, setHasMeasuredLayoutSize] =
219
+ React.useState(false);
220
+ const onLayout = React.useCallback(
221
+ ({ nativeEvent: { layout } }: LayoutChangeEvent) => {
222
+ setHasMeasuredLayoutSize(true);
223
+ setSize(layout);
224
+ },
225
+ [],
226
+ );
227
+ const normalizedAxisProps = useBuildChartAxis({
228
+ xAxis,
229
+ yAxis,
230
+ frame,
231
+ yKeys,
232
+ axisOptions,
233
+ });
234
+
235
+ // create a d3-zoom transform object based on the current transform state. This
236
+ // is used for rescaling the X and Y axes.
237
+ const transform = useCartesianTransformContext();
238
+ const zoomX = React.useMemo(
239
+ () => new ZoomTransform(transform.k, transform.tx, transform.ty),
240
+ [transform.k, transform.tx, transform.ty],
241
+ );
242
+ const zoomY = React.useMemo(
243
+ () => new ZoomTransform(transform.ky, transform.tx, transform.ty),
244
+ [transform.ky, transform.tx, transform.ty],
245
+ );
246
+
247
+ const tData = useSharedValue<TransformedData<RawData, XK, YK>>({
248
+ ix: [],
249
+ ox: [],
250
+ y: yKeys.reduce(
251
+ (acc, key) => {
252
+ acc[key] = { i: [], o: [] };
253
+ return acc;
254
+ },
255
+ {} as TransformedData<RawData, XK, YK>["y"],
256
+ ),
257
+ });
258
+
259
+ const {
260
+ yAxes,
261
+ xScale,
262
+ chartBounds,
263
+ isNumericalData,
264
+ xTicksNormalized,
265
+ _tData,
266
+ } = React.useMemo(() => {
267
+ if (!data.length) {
268
+ return createFallbackChartState<RawData, XK, YK>(yKeys);
269
+ }
270
+ const { xScale, yAxes, isNumericalData, xTicksNormalized, ..._tData } =
271
+ transformInputData({
272
+ data,
273
+ xKey,
274
+ yKeys,
275
+ outputWindow: {
276
+ xMin: valueFromSidedNumber(padding, "left"),
277
+ xMax: size.width - valueFromSidedNumber(padding, "right"),
278
+ yMin: valueFromSidedNumber(padding, "top"),
279
+ yMax: size.height - valueFromSidedNumber(padding, "bottom"),
280
+ },
281
+ domain,
282
+ domainPadding,
283
+ xAxis: normalizedAxisProps.xAxis,
284
+ yAxes: normalizedAxisProps.yAxes,
285
+ viewport,
286
+ labelRotate: normalizedAxisProps.xAxis.labelRotate,
287
+ axisScales: axisOptions?.axisScales,
288
+ });
289
+
290
+ const primaryYAxis = yAxes[0];
291
+ const primaryYScale = primaryYAxis.yScale;
292
+ const chartBounds = {
293
+ left: xScale(viewport?.x?.[0] ?? xScale.domain().at(0) ?? 0),
294
+ right: xScale(viewport?.x?.[1] ?? xScale.domain().at(-1) ?? 0),
295
+ top: primaryYScale(
296
+ viewport?.y?.[1] ?? (primaryYScale.domain().at(0) || 0),
297
+ ),
298
+ bottom: primaryYScale(
299
+ viewport?.y?.[0] ?? (primaryYScale.domain().at(-1) || 0),
300
+ ),
301
+ };
302
+
303
+ return {
304
+ xTicksNormalized,
305
+ yAxes,
306
+ xScale,
307
+ chartBounds,
308
+ isNumericalData,
309
+ _tData,
310
+ };
311
+ }, [
312
+ data,
313
+ xKey,
314
+ yKeys,
315
+ padding,
316
+ size.width,
317
+ size.height,
318
+ domain,
319
+ domainPadding,
320
+ normalizedAxisProps,
321
+ viewport,
322
+ ]);
323
+
324
+ React.useEffect(() => {
325
+ tData.value = _tData;
326
+ }, [_tData, tData]);
327
+
328
+ const primaryYAxis = yAxes[0];
329
+ const primaryYScale = primaryYAxis.yScale;
330
+
331
+ // stacked bar values
332
+ const chartHeight = chartBounds.bottom;
333
+ const yScaleTop = primaryYAxis.yScale.domain().at(0);
334
+ const yScaleBottom = primaryYAxis.yScale.domain().at(-1);
335
+ // end stacked bar values
336
+
337
+ /**
338
+ * Pan gesture handling
339
+ */
340
+ const lastIdx = useSharedValue(null as null | number);
341
+ /**
342
+ * Take a "press value" and an x-value and update the shared values accordingly.
343
+ */
344
+ const handleTouch = (
345
+ v: ChartPressState<{
346
+ x: InputFields<RawData>[XK];
347
+ y: Record<YK, number>;
348
+ }>,
349
+ x: number,
350
+ y: number,
351
+ ) => {
352
+ "worklet";
353
+ const idx = findClosestPoint(tData.value.ox, x);
354
+
355
+ if (typeof idx !== "number") return;
356
+
357
+ const isInYs = (yk: string): yk is YK & string => yKeys.includes(yk as YK);
358
+ // begin stacked bar handling:
359
+ // store the heights of each bar segment
360
+ const barHeights: number[] = [];
361
+ for (const yk in v.y) {
362
+ if (isInYs(yk)) {
363
+ const height = asNumber(tData.value.y[yk].i[idx]);
364
+ barHeights.push(height);
365
+ }
366
+ }
367
+
368
+ const chartYPressed = chartHeight - y; // Invert y-coordinate, since RNGH gives us the absolute Y, and we want to know where in the chart they clicked
369
+ // Calculate the actual yValue of the touch within the domain of the yScale
370
+ const yDomainValue =
371
+ (chartYPressed / chartHeight) * (yScaleTop! - yScaleBottom!);
372
+
373
+ // track the cumulative height and the y-index of the touched segment
374
+ let cumulativeHeight = 0;
375
+ let yIndex = -1;
376
+
377
+ // loop through the bar heights to find which bar was touched
378
+ for (let i = 0; i < barHeights.length; i++) {
379
+ // Accumulate the height as we go along
380
+ cumulativeHeight += barHeights[i]!;
381
+ // Check if the y-value touched falls within the current segment
382
+ if (yDomainValue <= cumulativeHeight) {
383
+ // If it does, set yIndex to the current segment index and break
384
+ yIndex = i;
385
+ break;
386
+ }
387
+ }
388
+
389
+ // Update the yIndex value in the state or context
390
+ v.yIndex.value = yIndex;
391
+ // end stacked bar handling
392
+
393
+ if (v) {
394
+ try {
395
+ v.matchedIndex.value = idx;
396
+ v.x.value.value = tData.value.ix[idx]!;
397
+ v.x.position.value = asNumber(tData.value.ox[idx]);
398
+ for (const yk in v.y) {
399
+ if (isInYs(yk)) {
400
+ v.y[yk].value.value = asNumber(tData.value.y[yk].i[idx]);
401
+ v.y[yk].position.value = asNumber(tData.value.y[yk].o[idx]);
402
+ }
403
+ }
404
+ } catch (err) {
405
+ // no-op
406
+ }
407
+ }
408
+
409
+ lastIdx.value = idx;
410
+ };
411
+
412
+ React.useImperativeHandle(
413
+ ref,
414
+ () => ({
415
+ canvas: canvasRef.current,
416
+ actions: {
417
+ handleTouch,
418
+ },
419
+ }),
420
+ // eslint-disable-next-line react-hooks/exhaustive-deps
421
+ [canvasRef],
422
+ );
423
+
424
+ if (actionsRef) {
425
+ actionsRef.current = {
426
+ handleTouch,
427
+ };
428
+ }
429
+
430
+ /**
431
+ * Touch gesture is a modified Pan gesture handler that allows for multiple presses:
432
+ * - Using Pan Gesture handler effectively _just_ for the .activateAfterLongPress functionality.
433
+ * - Tracking the finger is handled with .onTouchesMove instead of .onUpdate, because
434
+ * .onTouchesMove gives us access to each individual finger.
435
+ * - The activation gets a bit complicated because we want to wait til "start" state before updating Press Value
436
+ * which gives time for the gesture to get cancelled before we start updating the shared values.
437
+ * Therefore we use gestureState.bootstrap to store some "bootstrap" information if gesture isn't active when finger goes down.
438
+ */
439
+ // touch ID -> value index mapping to keep track of which finger updates which value
440
+ const touchMap = useSharedValue({} as Record<number, number | undefined>);
441
+ const activePressSharedValues = Array.isArray(chartPressState)
442
+ ? chartPressState
443
+ : [chartPressState];
444
+ const gestureState = useSharedValue({
445
+ isGestureActive: false,
446
+ bootstrap: [] as [
447
+ ChartPressState<{ x: InputFields<RawData>[XK]; y: Record<YK, number> }>,
448
+ TouchData,
449
+ ][],
450
+ });
451
+
452
+ const panGesture = Gesture.Pan()
453
+ /**
454
+ * When a finger goes down, either update the state or store in the bootstrap array.
455
+ */
456
+ .onTouchesDown((e) => {
457
+ const vals = activePressSharedValues || [];
458
+ if (!vals.length || e.numberOfTouches === 0) return;
459
+
460
+ for (let i = 0; i < Math.min(e.allTouches.length, vals.length); i++) {
461
+ const touch = e.allTouches[i];
462
+ const v = vals[i];
463
+ if (!v || !touch) continue;
464
+
465
+ if (gestureState.value.isGestureActive) {
466
+ // Update the mapping
467
+ if (typeof touchMap.value[touch.id] !== "number")
468
+ touchMap.value[touch.id] = i;
469
+
470
+ v.isActive.value = true;
471
+
472
+ // [3] and [7] are the X and Y translation components of the 4x4 transformation matrix, respectively.
473
+ const scrolledX = transformState?.matrix.value?.[3] || 0;
474
+ const scrolledY = transformState?.matrix.value?.[7] || 0;
475
+
476
+ handleTouch(
477
+ v,
478
+ touch.absoluteX - scrolledX,
479
+ touch.absoluteY - scrolledY,
480
+ );
481
+ } else {
482
+ gestureState.value.bootstrap.push([v, touch]);
483
+ }
484
+ }
485
+ })
486
+ /**
487
+ * On start, check if we have any bootstrapped updates we need to apply.
488
+ */
489
+ .onStart(() => {
490
+ gestureState.value.isGestureActive = true;
491
+
492
+ for (let i = 0; i < gestureState.value.bootstrap.length; i++) {
493
+ const [v, touch] = gestureState.value.bootstrap[i]!;
494
+ // Update the mapping
495
+ if (typeof touchMap.value[touch.id] !== "number")
496
+ touchMap.value[touch.id] = i;
497
+
498
+ v.isActive.value = true;
499
+
500
+ const scrolledX = transformState?.matrix.value?.[3] || 0;
501
+ const scrolledY = transformState?.matrix.value?.[7] || 0;
502
+
503
+ handleTouch(
504
+ v,
505
+ touch.absoluteX - scrolledX,
506
+ touch.absoluteY - scrolledY,
507
+ );
508
+ }
509
+ })
510
+ /**
511
+ * Clear gesture state on gesture end.
512
+ */
513
+ .onFinalize(() => {
514
+ gestureState.value.isGestureActive = false;
515
+ gestureState.value.bootstrap = [];
516
+ })
517
+ /**
518
+ * As fingers move, update the shared values accordingly.
519
+ */
520
+ .onTouchesMove((e) => {
521
+ const vals = activePressSharedValues || [];
522
+ if (!vals.length || e.numberOfTouches === 0) return;
523
+
524
+ for (let i = 0; i < Math.min(e.allTouches.length, vals.length); i++) {
525
+ const touch = e.allTouches[i];
526
+ const touchId = touch?.id;
527
+ const idx = typeof touchId === "number" && touchMap.value[touchId];
528
+ const v = typeof idx === "number" && vals?.[idx];
529
+
530
+ if (!v || !touch) continue;
531
+ if (!v.isActive.value) v.isActive.value = true;
532
+
533
+ const scrolledX = transformState?.matrix.value?.[3] || 0;
534
+ const scrolledY = transformState?.matrix.value?.[7] || 0;
535
+
536
+ handleTouch(
537
+ v,
538
+ touch.absoluteX - scrolledX,
539
+ touch.absoluteY - scrolledY,
540
+ );
541
+ }
542
+ })
543
+ /**
544
+ * On each finger up, start to update values and "free up" the touch map.
545
+ */
546
+ .onTouchesUp((e) => {
547
+ for (const touch of e.changedTouches) {
548
+ const vals = activePressSharedValues || [];
549
+
550
+ // Set active state to false
551
+ const touchId = touch?.id;
552
+ const idx = typeof touchId === "number" && touchMap.value[touchId];
553
+ const val = typeof idx === "number" && vals[idx];
554
+ if (val) {
555
+ val.isActive.value = false;
556
+ }
557
+
558
+ // Free up touch map for this touch
559
+ touchMap.value[touch.id] = undefined;
560
+ }
561
+ })
562
+ /**
563
+ * Once the gesture ends, ensure all active values are falsified.
564
+ */
565
+ .onEnd(() => {
566
+ const vals = activePressSharedValues || [];
567
+ // Set active state to false for all vals
568
+ for (const val of vals) {
569
+ if (val) {
570
+ val.isActive.value = false;
571
+ }
572
+ }
573
+ });
574
+
575
+ if (!chartPressConfig?.pan) {
576
+ /**
577
+ * Activate after a long press, which helps with preventing all touch hijacking.
578
+ * This is important if this chart is inside of some sort of scrollable container.
579
+ */
580
+ panGesture.activateAfterLongPress(gestureLongPressDelay);
581
+ }
582
+
583
+ if (chartPressConfig?.pan?.activateAfterLongPress) {
584
+ panGesture.activateAfterLongPress(
585
+ chartPressConfig.pan?.activateAfterLongPress,
586
+ );
587
+ }
588
+ if (chartPressConfig?.pan?.activeOffsetX) {
589
+ panGesture.activeOffsetX(chartPressConfig.pan.activeOffsetX);
590
+ }
591
+ if (chartPressConfig?.pan?.activeOffsetY) {
592
+ panGesture.activeOffsetX(chartPressConfig.pan.activeOffsetY);
593
+ }
594
+ if (chartPressConfig?.pan?.failOffsetX) {
595
+ panGesture.failOffsetX(chartPressConfig.pan.failOffsetX);
596
+ }
597
+ if (chartPressConfig?.pan?.failOffsetY) {
598
+ panGesture.failOffsetX(chartPressConfig.pan.failOffsetY);
599
+ }
600
+
601
+ /**
602
+ * Allow end-user to request "raw-ish" data for a given yKey.
603
+ * Generate this on demand using a proxy.
604
+ */
605
+ type PointsArg = CartesianChartRenderArg<RawData, YK>["points"];
606
+ const points = React.useMemo<PointsArg>(() => {
607
+ const cache = {} as Record<YK, PointsArg[keyof PointsArg]>;
608
+ return new Proxy(
609
+ {},
610
+ {
611
+ get(_, property: string): PointsArg[keyof PointsArg] | undefined {
612
+ const key = property as YK;
613
+ if (!yKeys.includes(key)) return undefined;
614
+ if (cache[key]) return cache[key];
615
+
616
+ cache[key] = _tData.ix.map((x, i) => ({
617
+ x: asNumber(_tData.ox[i]),
618
+ xValue: x,
619
+ y: _tData.y[key].o[i],
620
+ yValue: _tData.y[key].i[i],
621
+ }));
622
+
623
+ return cache[key];
624
+ },
625
+ },
626
+ ) as PointsArg;
627
+ }, [_tData, yKeys]);
628
+
629
+ // On bounds change, emit
630
+ const onChartBoundsRef = useFunctionRef(onChartBoundsChange);
631
+ React.useEffect(() => {
632
+ if (!isEqual(chartBounds, chartBoundsRef.current)) {
633
+ chartBoundsRef.current = chartBounds;
634
+ onChartBoundsRef.current?.(chartBounds);
635
+ }
636
+ }, [chartBounds, onChartBoundsRef]);
637
+
638
+ const onScaleRef = useFunctionRef(onScaleChange);
639
+ React.useEffect(() => {
640
+ const rescaledX = zoomX.rescaleX(xScale);
641
+ const rescaledY = zoomY.rescaleY(primaryYScale);
642
+ if (
643
+ !isEqual(xScaleRef.current?.domain(), rescaledX.domain()) ||
644
+ !isEqual(yScaleRef.current?.domain(), rescaledY.domain()) ||
645
+ !isEqual(xScaleRef.current?.range(), rescaledX.range()) ||
646
+ !isEqual(yScaleRef.current?.range(), rescaledY.range())
647
+ ) {
648
+ xScaleRef.current = xScale;
649
+ yScaleRef.current = primaryYScale;
650
+ onScaleRef.current?.(rescaledX, rescaledY);
651
+ }
652
+ }, [onScaleChange, onScaleRef, xScale, zoomX, zoomY, primaryYScale]);
653
+
654
+ const renderArg: CartesianChartRenderArg<RawData, YK> = {
655
+ xScale,
656
+ xTicks: xTicksNormalized,
657
+ yScale: primaryYScale,
658
+ yTicks: primaryYAxis.yTicksNormalized,
659
+ chartBounds,
660
+ canvasSize: size,
661
+ points,
662
+ };
663
+
664
+ const clipRect = boundsToClip(chartBounds);
665
+ const YAxisComponents =
666
+ hasMeasuredLayoutSize && (axisOptions || yAxes)
667
+ ? normalizedAxisProps.yAxes?.map((axis, index) => {
668
+ const yAxis = yAxes[index];
669
+
670
+ if (!yAxis) return null;
671
+
672
+ const primaryAxisProps = normalizedAxisProps.yAxes[0]!;
673
+ const primaryRescaled = zoomY.rescaleY(primaryYScale);
674
+ const rescaled = zoomY.rescaleY(yAxis.yScale);
675
+
676
+ const rescaledTicks = axis.tickValues
677
+ ? downsampleTicks(axis.tickValues, axis.tickCount)
678
+ : axis.enableRescaling
679
+ ? rescaled.ticks(axis.tickCount)
680
+ : yAxis.yScale.ticks(axis.tickCount);
681
+
682
+ const primaryTicksRescaled = primaryAxisProps.tickValues
683
+ ? downsampleTicks(
684
+ primaryAxisProps.tickValues,
685
+ primaryAxisProps.tickCount,
686
+ )
687
+ : primaryAxisProps.enableRescaling
688
+ ? primaryRescaled.ticks(primaryAxisProps.tickCount)
689
+ : primaryYScale.ticks(primaryAxisProps.tickCount);
690
+
691
+ return (
692
+ <YAxis
693
+ key={index}
694
+ {...axis}
695
+ xScale={zoomX.rescaleX(xScale)}
696
+ yScale={rescaled}
697
+ yTicksNormalized={
698
+ index > 0 && !axis.tickValues
699
+ ? normalizeYAxisTicks(
700
+ primaryTicksRescaled,
701
+ primaryRescaled,
702
+ rescaled,
703
+ )
704
+ : rescaledTicks
705
+ }
706
+ chartBounds={chartBounds}
707
+ />
708
+ );
709
+ })
710
+ : null;
711
+
712
+ const XAxisComponents =
713
+ hasMeasuredLayoutSize && (axisOptions || xAxis) ? (
714
+ <XAxis
715
+ {...normalizedAxisProps.xAxis}
716
+ xScale={xScale}
717
+ yScale={zoomY.rescaleY(primaryYScale)}
718
+ ix={_tData.ix}
719
+ isNumericalData={isNumericalData}
720
+ chartBounds={chartBounds}
721
+ zoom={zoomX}
722
+ />
723
+ ) : null;
724
+
725
+ const FrameComponent =
726
+ hasMeasuredLayoutSize && (axisOptions || frame) ? (
727
+ <Frame
728
+ {...normalizedAxisProps.frame}
729
+ xScale={xScale}
730
+ yScale={primaryYScale}
731
+ />
732
+ ) : null;
733
+
734
+ // Body of the chart.
735
+ const body = (
736
+ <Canvas ref={canvasRef} style={{ flex: 1 }}>
737
+ {YAxisComponents}
738
+ {XAxisComponents}
739
+ {FrameComponent}
740
+ <CartesianChartProvider yScale={primaryYScale} xScale={xScale}>
741
+ <Group clip={clipRect}>
742
+ <Group matrix={transformState?.matrix}>
743
+ {hasMeasuredLayoutSize && children(renderArg)}
744
+ </Group>
745
+ </Group>
746
+ </CartesianChartProvider>
747
+ {hasMeasuredLayoutSize && renderOutside?.(renderArg)}
748
+ </Canvas>
749
+ );
750
+
751
+ let composed = customGestures ?? Gesture.Race();
752
+ if (transformState) {
753
+ let gestures = Gesture.Simultaneous();
754
+
755
+ if (transformConfig?.pinch?.enabled ?? true) {
756
+ gestures = Gesture.Simultaneous(
757
+ gestures,
758
+ pinchTransformGesture(transformState, transformConfig?.pinch),
759
+ );
760
+ }
761
+
762
+ if (transformConfig?.pan?.enabled ?? true) {
763
+ gestures = Gesture.Simultaneous(
764
+ gestures,
765
+ panTransformGesture(transformState, transformConfig?.pan),
766
+ );
767
+ }
768
+
769
+ composed = Gesture.Race(composed, Gesture.Simultaneous(gestures));
770
+ }
771
+ if (chartPressState) {
772
+ composed = Gesture.Race(composed, panGesture);
773
+ }
774
+
775
+ return (
776
+ <GestureHandlerRootView style={{ flex: 1}}>
777
+ <View style={{ flex: 1, overflow: "hidden" }} onLayout={onLayout}>
778
+ {body}
779
+ <GestureHandler
780
+ config={gestureHandlerConfig}
781
+ gesture={composed}
782
+ transformState={transformState}
783
+ dimensions={{
784
+ x: Math.min(xScale.range()[0]!, 0),
785
+ y: Math.min(primaryYScale.range()[0]!, 0),
786
+ width: xScale.range()[1]! - Math.min(xScale.range()[0]!, 0),
787
+ height:
788
+ primaryYScale.range()[1]! -
789
+ Math.min(primaryYScale.range()[0]!, 0),
790
+ }}
791
+ />
792
+ </View>
793
+ </GestureHandlerRootView>
794
+ );
795
+ }