@obitrain/charts 0.10.4 → 0.12.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 (132) hide show
  1. package/README.md +6 -4
  2. package/lib/commonjs/Charts/BottomAxis/AxisLine.js +1 -1
  3. package/lib/commonjs/Charts/BottomAxis/BottomAxis.js +54 -14
  4. package/lib/commonjs/Charts/BottomAxis/BottomAxis.js.map +1 -1
  5. package/lib/commonjs/Charts/BottomAxis/Tick.js +29 -13
  6. package/lib/commonjs/Charts/BottomAxis/Tick.js.map +1 -1
  7. package/lib/commonjs/Charts/BottomAxis/index.js.map +1 -1
  8. package/lib/commonjs/Charts/Cursor.js +33 -13
  9. package/lib/commonjs/Charts/Cursor.js.map +1 -1
  10. package/lib/commonjs/Charts/Dot.js +51 -0
  11. package/lib/commonjs/Charts/Dot.js.map +1 -0
  12. package/lib/commonjs/Charts/Dots.js +74 -0
  13. package/lib/commonjs/Charts/Dots.js.map +1 -0
  14. package/lib/commonjs/Charts/Linechart/index.js +1 -1
  15. package/lib/commonjs/Charts/YAxis.js +77 -0
  16. package/lib/commonjs/Charts/YAxis.js.map +1 -0
  17. package/lib/commonjs/Charts/ZoomableLinechart/ScalablePath.js +5 -4
  18. package/lib/commonjs/Charts/ZoomableLinechart/ScalablePath.js.map +1 -1
  19. package/lib/commonjs/Charts/ZoomableLinechart/index.js +1 -1
  20. package/lib/commonjs/Charts/gesture.js +90 -38
  21. package/lib/commonjs/Charts/gesture.js.map +1 -1
  22. package/lib/commonjs/Charts/graphUtils.js +69 -16
  23. package/lib/commonjs/Charts/graphUtils.js.map +1 -1
  24. package/lib/commonjs/Charts/index.js +39 -0
  25. package/lib/commonjs/Charts/index.js.map +1 -1
  26. package/lib/commonjs/Charts/maths.js +107 -16
  27. package/lib/commonjs/Charts/maths.js.map +1 -1
  28. package/lib/module/Charts/BottomAxis/AxisLine.js +1 -1
  29. package/lib/module/Charts/BottomAxis/AxisLine.js.map +1 -1
  30. package/lib/module/Charts/BottomAxis/BottomAxis.js +56 -15
  31. package/lib/module/Charts/BottomAxis/BottomAxis.js.map +1 -1
  32. package/lib/module/Charts/BottomAxis/Tick.js +29 -11
  33. package/lib/module/Charts/BottomAxis/Tick.js.map +1 -1
  34. package/lib/module/Charts/BottomAxis/index.js.map +1 -1
  35. package/lib/module/Charts/Cursor.js +35 -15
  36. package/lib/module/Charts/Cursor.js.map +1 -1
  37. package/lib/module/Charts/Dot.js +47 -0
  38. package/lib/module/Charts/Dot.js.map +1 -0
  39. package/lib/module/Charts/Dots.js +70 -0
  40. package/lib/module/Charts/Dots.js.map +1 -0
  41. package/lib/module/Charts/Linechart/index.js +1 -1
  42. package/lib/module/Charts/Linechart/index.js.map +1 -1
  43. package/lib/module/Charts/YAxis.js +73 -0
  44. package/lib/module/Charts/YAxis.js.map +1 -0
  45. package/lib/module/Charts/ZoomableLinechart/ScalablePath.js +5 -4
  46. package/lib/module/Charts/ZoomableLinechart/ScalablePath.js.map +1 -1
  47. package/lib/module/Charts/ZoomableLinechart/index.js +1 -1
  48. package/lib/module/Charts/ZoomableLinechart/index.js.map +1 -1
  49. package/lib/module/Charts/gesture.js +90 -39
  50. package/lib/module/Charts/gesture.js.map +1 -1
  51. package/lib/module/Charts/graphUtils.js +71 -18
  52. package/lib/module/Charts/graphUtils.js.map +1 -1
  53. package/lib/module/Charts/index.js +5 -2
  54. package/lib/module/Charts/index.js.map +1 -1
  55. package/lib/module/Charts/maths.js +101 -14
  56. package/lib/module/Charts/maths.js.map +1 -1
  57. package/lib/typescript/commonjs/src/Charts/BottomAxis/AxisLine.d.ts +1 -1
  58. package/lib/typescript/commonjs/src/Charts/BottomAxis/AxisLine.d.ts.map +1 -1
  59. package/lib/typescript/commonjs/src/Charts/BottomAxis/BottomAxis.d.ts +26 -3
  60. package/lib/typescript/commonjs/src/Charts/BottomAxis/BottomAxis.d.ts.map +1 -1
  61. package/lib/typescript/commonjs/src/Charts/BottomAxis/Tick.d.ts +16 -3
  62. package/lib/typescript/commonjs/src/Charts/BottomAxis/Tick.d.ts.map +1 -1
  63. package/lib/typescript/commonjs/src/Charts/BottomAxis/index.d.ts +1 -1
  64. package/lib/typescript/commonjs/src/Charts/BottomAxis/index.d.ts.map +1 -1
  65. package/lib/typescript/commonjs/src/Charts/Cursor.d.ts +5 -2
  66. package/lib/typescript/commonjs/src/Charts/Cursor.d.ts.map +1 -1
  67. package/lib/typescript/commonjs/src/Charts/Dot.d.ts +22 -0
  68. package/lib/typescript/commonjs/src/Charts/Dot.d.ts.map +1 -0
  69. package/lib/typescript/commonjs/src/Charts/Dots.d.ts +28 -0
  70. package/lib/typescript/commonjs/src/Charts/Dots.d.ts.map +1 -0
  71. package/lib/typescript/commonjs/src/Charts/Linechart/index.d.ts +1 -1
  72. package/lib/typescript/commonjs/src/Charts/Linechart/index.d.ts.map +1 -1
  73. package/lib/typescript/commonjs/src/Charts/YAxis.d.ts +21 -0
  74. package/lib/typescript/commonjs/src/Charts/YAxis.d.ts.map +1 -0
  75. package/lib/typescript/commonjs/src/Charts/ZoomableLinechart/ScalablePath.d.ts +2 -2
  76. package/lib/typescript/commonjs/src/Charts/ZoomableLinechart/ScalablePath.d.ts.map +1 -1
  77. package/lib/typescript/commonjs/src/Charts/ZoomableLinechart/index.d.ts +1 -1
  78. package/lib/typescript/commonjs/src/Charts/ZoomableLinechart/index.d.ts.map +1 -1
  79. package/lib/typescript/commonjs/src/Charts/gesture.d.ts +6 -0
  80. package/lib/typescript/commonjs/src/Charts/gesture.d.ts.map +1 -1
  81. package/lib/typescript/commonjs/src/Charts/graphUtils.d.ts +1 -1
  82. package/lib/typescript/commonjs/src/Charts/graphUtils.d.ts.map +1 -1
  83. package/lib/typescript/commonjs/src/Charts/index.d.ts +9 -3
  84. package/lib/typescript/commonjs/src/Charts/index.d.ts.map +1 -1
  85. package/lib/typescript/commonjs/src/Charts/maths.d.ts +15 -1
  86. package/lib/typescript/commonjs/src/Charts/maths.d.ts.map +1 -1
  87. package/lib/typescript/module/src/Charts/BottomAxis/AxisLine.d.ts +1 -1
  88. package/lib/typescript/module/src/Charts/BottomAxis/AxisLine.d.ts.map +1 -1
  89. package/lib/typescript/module/src/Charts/BottomAxis/BottomAxis.d.ts +26 -3
  90. package/lib/typescript/module/src/Charts/BottomAxis/BottomAxis.d.ts.map +1 -1
  91. package/lib/typescript/module/src/Charts/BottomAxis/Tick.d.ts +16 -3
  92. package/lib/typescript/module/src/Charts/BottomAxis/Tick.d.ts.map +1 -1
  93. package/lib/typescript/module/src/Charts/BottomAxis/index.d.ts +1 -1
  94. package/lib/typescript/module/src/Charts/BottomAxis/index.d.ts.map +1 -1
  95. package/lib/typescript/module/src/Charts/Cursor.d.ts +5 -2
  96. package/lib/typescript/module/src/Charts/Cursor.d.ts.map +1 -1
  97. package/lib/typescript/module/src/Charts/Dot.d.ts +22 -0
  98. package/lib/typescript/module/src/Charts/Dot.d.ts.map +1 -0
  99. package/lib/typescript/module/src/Charts/Dots.d.ts +28 -0
  100. package/lib/typescript/module/src/Charts/Dots.d.ts.map +1 -0
  101. package/lib/typescript/module/src/Charts/Linechart/index.d.ts +1 -1
  102. package/lib/typescript/module/src/Charts/Linechart/index.d.ts.map +1 -1
  103. package/lib/typescript/module/src/Charts/YAxis.d.ts +21 -0
  104. package/lib/typescript/module/src/Charts/YAxis.d.ts.map +1 -0
  105. package/lib/typescript/module/src/Charts/ZoomableLinechart/ScalablePath.d.ts +2 -2
  106. package/lib/typescript/module/src/Charts/ZoomableLinechart/ScalablePath.d.ts.map +1 -1
  107. package/lib/typescript/module/src/Charts/ZoomableLinechart/index.d.ts +1 -1
  108. package/lib/typescript/module/src/Charts/ZoomableLinechart/index.d.ts.map +1 -1
  109. package/lib/typescript/module/src/Charts/gesture.d.ts +6 -0
  110. package/lib/typescript/module/src/Charts/gesture.d.ts.map +1 -1
  111. package/lib/typescript/module/src/Charts/graphUtils.d.ts +1 -1
  112. package/lib/typescript/module/src/Charts/graphUtils.d.ts.map +1 -1
  113. package/lib/typescript/module/src/Charts/index.d.ts +9 -3
  114. package/lib/typescript/module/src/Charts/index.d.ts.map +1 -1
  115. package/lib/typescript/module/src/Charts/maths.d.ts +15 -1
  116. package/lib/typescript/module/src/Charts/maths.d.ts.map +1 -1
  117. package/package.json +5 -5
  118. package/src/Charts/BottomAxis/AxisLine.tsx +1 -1
  119. package/src/Charts/BottomAxis/BottomAxis.tsx +100 -19
  120. package/src/Charts/BottomAxis/Tick.tsx +61 -15
  121. package/src/Charts/BottomAxis/index.tsx +5 -1
  122. package/src/Charts/Cursor.tsx +38 -10
  123. package/src/Charts/Dot.tsx +64 -0
  124. package/src/Charts/Dots.tsx +96 -0
  125. package/src/Charts/Linechart/index.tsx +1 -1
  126. package/src/Charts/YAxis.tsx +103 -0
  127. package/src/Charts/ZoomableLinechart/ScalablePath.tsx +8 -9
  128. package/src/Charts/ZoomableLinechart/index.tsx +1 -1
  129. package/src/Charts/gesture.ts +132 -71
  130. package/src/Charts/graphUtils.ts +78 -16
  131. package/src/Charts/index.tsx +9 -0
  132. package/src/Charts/maths.ts +107 -16
@@ -1,11 +1,13 @@
1
1
  import {
2
+ DashPathEffect,
2
3
  Group,
3
4
  Line,
4
5
  Text,
5
6
  vec,
7
+ type Color,
6
8
  type SkFont,
7
- } from '@shopify/react-native-skia';
8
- import React from 'react';
9
+ } from 'react-native-skia';
10
+ import { memo, useMemo } from 'react';
9
11
  import { useDerivedValue, type SharedValue } from 'react-native-reanimated';
10
12
  import { getPositionWl } from '../gesture';
11
13
 
@@ -17,10 +19,37 @@ export type TickProps = {
17
19
  offsetX: SharedValue<number>;
18
20
  offsetY?: number;
19
21
  font: SkFont;
22
+ color?: Color;
23
+ labelColor?: Color;
24
+ /** Length of the tick line. Use a negative value (e.g. -chartHeight) to draw a vertical gridline above the axis. */
25
+ tickLength?: number;
26
+ strokeWidth?: number;
27
+ /** Dash/gap lengths for a dashed tick line. */
28
+ dash?: [number, number];
29
+ showLabel?: boolean;
30
+ /** `center` puts the label under the tick, `left` starts it right after the tick. */
31
+ labelAlign?: 'center' | 'left';
32
+ /** Gap between the tick and a left-aligned label. */
33
+ labelGap?: number;
20
34
  };
21
35
 
22
- export const Tick: React.FC<TickProps> = (props) => {
23
- const { label, scale, focalX, offsetX, initPosition, font } = props;
36
+ const TickComponent = function (props: TickProps) {
37
+ const {
38
+ label,
39
+ scale,
40
+ focalX,
41
+ offsetX,
42
+ initPosition,
43
+ font,
44
+ color = 'black',
45
+ labelColor,
46
+ tickLength = 10,
47
+ strokeWidth,
48
+ dash,
49
+ showLabel = true,
50
+ labelAlign = 'center',
51
+ labelGap = 6,
52
+ } = props;
24
53
  const offsetY = props.offsetY ?? 0;
25
54
  const transform = useDerivedValue(() => [
26
55
  {
@@ -32,20 +61,37 @@ export const Tick: React.FC<TickProps> = (props) => {
32
61
  ),
33
62
  },
34
63
  ]);
35
- const width = font
36
- .getGlyphWidths(font.getGlyphIDs(label))
37
- .reduce((a, b) => a + b, 0);
64
+ const width = useMemo(
65
+ () =>
66
+ font.getGlyphWidths(font.getGlyphIDs(label)).reduce((a, b) => a + b, 0),
67
+ [font, label]
68
+ );
69
+ const labelX = labelAlign === 'left' ? labelGap : -width / 2;
38
70
 
39
71
  return (
40
72
  <Group transform={transform}>
41
- <Line color="black" p1={vec(0, offsetY)} p2={vec(0, offsetY + 10)} />
42
- <Text
43
- text={label}
44
- color="black"
45
- x={-width / 2}
46
- y={offsetY + 23}
47
- font={font}
48
- />
73
+ <Line
74
+ color={color}
75
+ strokeWidth={strokeWidth}
76
+ p1={vec(0, offsetY)}
77
+ p2={vec(0, offsetY + tickLength)}
78
+ >
79
+ {dash !== undefined ? <DashPathEffect intervals={dash} /> : null}
80
+ </Line>
81
+ {showLabel ? (
82
+ <Text
83
+ text={label}
84
+ color={labelColor ?? color}
85
+ x={labelX}
86
+ y={offsetY + 23}
87
+ font={font}
88
+ />
89
+ ) : null}
49
90
  </Group>
50
91
  );
51
92
  };
93
+
94
+ // Memoized: zoom/pan flows through shared values, so a Tick only needs to
95
+ // re-render when its label or base position changes
96
+ export const Tick = memo(TickComponent);
97
+ Tick.displayName = 'Tick';
@@ -1,3 +1,7 @@
1
1
  export { AxisLine, type AxisLineProps } from './AxisLine';
2
- export { BottomAxis, type BottomAxisProps } from './BottomAxis';
2
+ export {
3
+ BottomAxis,
4
+ type AxisTick,
5
+ type BottomAxisProps,
6
+ } from './BottomAxis';
3
7
  export { Tick, type TickProps } from './Tick';
@@ -1,7 +1,7 @@
1
- import { Circle, type PathCommand } from '@shopify/react-native-skia';
2
- import type { FC } from 'react';
1
+ import { Circle, type PathCommand } from 'react-native-skia';
2
+ import { useEffect, type FC } from 'react';
3
3
  import { useDerivedValue, type SharedValue } from 'react-native-reanimated';
4
- import { getYForX } from './maths';
4
+ import { commandsToBezier, getYForXOnBeziers } from './maths';
5
5
 
6
6
  const CURSOR_SIZE = 10;
7
7
 
@@ -10,6 +10,9 @@ export type CursorProps = {
10
10
  positionX: SharedValue<number>;
11
11
  size?: number;
12
12
  color?: string;
13
+ /** When set, draws a ring of this color around the cursor. */
14
+ strokeColor?: string;
15
+ strokeWidth?: number;
13
16
  currentValue?: SharedValue<number> | SharedValue<number | undefined>;
14
17
  translateY?: SharedValue<number | undefined>;
15
18
  };
@@ -18,20 +21,30 @@ const Cursor: FC<CursorProps> = function ({
18
21
  commands,
19
22
  positionX,
20
23
  color,
24
+ strokeColor,
25
+ strokeWidth = 2,
21
26
  currentValue,
22
27
  translateY,
23
28
  size = CURSOR_SIZE,
24
29
  }) {
25
- if (currentValue !== undefined && translateY !== undefined) {
26
- console.warn('currentValue has no effect when translateY is set');
27
- }
30
+ const conflictingProps = currentValue !== undefined && translateY !== undefined;
31
+ useEffect(() => {
32
+ if (conflictingProps)
33
+ console.warn('currentValue has no effect when translateY is set');
34
+ }, [conflictingProps]);
28
35
 
29
- const derivedTranslateY = useDerivedValue(() => {
36
+ // Normalized once per path change, not on every cursor move
37
+ const beziers = useDerivedValue(() => {
30
38
  const _commands = commands?.value;
31
- if (_commands === undefined) {
39
+ return _commands === undefined ? undefined : commandsToBezier(_commands);
40
+ });
41
+
42
+ const derivedTranslateY = useDerivedValue(() => {
43
+ const _beziers = beziers.value;
44
+ if (_beziers === undefined) {
32
45
  return 0;
33
46
  }
34
- const _value = getYForX(_commands, positionX.value) ?? 0;
47
+ const _value = getYForXOnBeziers(_beziers, positionX.value) ?? 0;
35
48
  if (translateY === undefined && currentValue !== undefined) {
36
49
  currentValue.value = _value;
37
50
  }
@@ -44,7 +57,22 @@ const Cursor: FC<CursorProps> = function ({
44
57
  { translateX: positionX.value },
45
58
  { translateY: _translateY.value ?? 0 },
46
59
  ]);
47
- return <Circle transform={transform} cx={0} cy={0} r={size} color={color} />;
60
+ return (
61
+ <>
62
+ <Circle transform={transform} cx={0} cy={0} r={size} color={color} />
63
+ {strokeColor !== undefined ? (
64
+ <Circle
65
+ transform={transform}
66
+ cx={0}
67
+ cy={0}
68
+ r={size}
69
+ color={strokeColor}
70
+ style="stroke"
71
+ strokeWidth={strokeWidth}
72
+ />
73
+ ) : null}
74
+ </>
75
+ );
48
76
  };
49
77
 
50
78
  export { Cursor };
@@ -0,0 +1,64 @@
1
+ import { Circle, Group, type Color } from 'react-native-skia';
2
+ import { memo } from 'react';
3
+ import { useDerivedValue, type SharedValue } from 'react-native-reanimated';
4
+ import { getPositionWl } from './gesture';
5
+
6
+ export type DotProps = {
7
+ x: SharedValue<number> | number;
8
+ y: SharedValue<number> | number;
9
+ opacity?: SharedValue<number> | number;
10
+ r?: number;
11
+ color?: Color;
12
+ /**
13
+ * When set, the dot is drawn as a ring: `fillColor` inside, `color` as the
14
+ * stroke. Use the chart background color to get a "hollow" marker.
15
+ */
16
+ fillColor?: Color;
17
+ strokeWidth?: number;
18
+ // Optional zoom/pan awareness (see useScalableGesture)
19
+ scale?: SharedValue<number>;
20
+ focalX?: SharedValue<number>;
21
+ offsetX?: SharedValue<number>;
22
+ };
23
+
24
+ function DotComponent(props: DotProps) {
25
+ const {
26
+ x,
27
+ y,
28
+ opacity = 1,
29
+ r = 4,
30
+ color = 'black',
31
+ fillColor,
32
+ strokeWidth = 2,
33
+ scale,
34
+ focalX,
35
+ offsetX,
36
+ } = props;
37
+
38
+ const cx = useDerivedValue(() => {
39
+ const _x = typeof x === 'number' ? x : x.value;
40
+ if (scale === undefined || focalX === undefined || offsetX === undefined)
41
+ return _x;
42
+ return getPositionWl(_x, focalX.value, scale.value, offsetX.value);
43
+ });
44
+
45
+ return (
46
+ <Group opacity={opacity}>
47
+ <Circle cx={cx} cy={y} r={r} color={fillColor ?? color} />
48
+ {fillColor !== undefined ? (
49
+ <Circle
50
+ cx={cx}
51
+ cy={y}
52
+ r={r}
53
+ style="stroke"
54
+ color={color}
55
+ strokeWidth={strokeWidth}
56
+ />
57
+ ) : null}
58
+ </Group>
59
+ );
60
+ }
61
+
62
+ // Memoized: charts render many dots and the position updates flow through
63
+ // shared values, so parent re-renders don't need to reach them
64
+ export const Dot = memo(DotComponent);
@@ -0,0 +1,96 @@
1
+ import { Path, Skia, type Color } from 'react-native-skia';
2
+ import { type FC } from 'react';
3
+ import { useDerivedValue, type SharedValue } from 'react-native-reanimated';
4
+ import { getPositionWl } from './gesture';
5
+ import type { AnimatedDot } from './types';
6
+
7
+ export type DotShape = 'circle' | 'diamond';
8
+
9
+ export type DotsProps = {
10
+ dots: AnimatedDot[];
11
+ r?: number;
12
+ shape?: DotShape;
13
+ color?: Color;
14
+ /** When set, dots are drawn as rings: `fillColor` inside, `color` as the stroke. */
15
+ fillColor?: Color;
16
+ strokeWidth?: number;
17
+ /** Viewport width used to cull off-screen dots. */
18
+ width?: number;
19
+ // Optional zoom/pan awareness (see useScalableGesture)
20
+ scale?: SharedValue<number>;
21
+ focalX?: SharedValue<number>;
22
+ offsetX?: SharedValue<number>;
23
+ };
24
+
25
+ /**
26
+ * Draw a whole series of dots as a single Skia path (two with `fillColor`).
27
+ * Unlike mapping over {@link Dot}, this runs one worklet per frame instead of
28
+ * one per dot, which keeps large series smooth during zoom/pan gestures.
29
+ * Per-dot opacity is binary here: dots below 0.5 are hidden.
30
+ */
31
+ const Dots: FC<DotsProps> = function (props) {
32
+ const {
33
+ dots,
34
+ r = 4,
35
+ shape = 'circle',
36
+ color = 'black',
37
+ fillColor,
38
+ strokeWidth = 2,
39
+ width,
40
+ scale,
41
+ focalX,
42
+ offsetX,
43
+ } = props;
44
+
45
+ const path = useDerivedValue(() => {
46
+ const p = Skia.PathBuilder.Make();
47
+ const margin = r + strokeWidth;
48
+ for (const dot of dots) {
49
+ if (dot.opacity.value < 0.5) continue;
50
+ const x =
51
+ scale === undefined || focalX === undefined || offsetX === undefined
52
+ ? dot.x.value
53
+ : getPositionWl(
54
+ dot.x.value,
55
+ focalX.value,
56
+ scale.value,
57
+ offsetX.value
58
+ );
59
+ if (width !== undefined && (x < -margin || x > width + margin)) continue;
60
+ const y = dot.y.value;
61
+ if (shape === 'diamond') {
62
+ // One addPoly beats four moveTo/lineTo calls: each is a JSI hop, and
63
+ // this worklet rebuilds every dot on every frame of a gesture
64
+ p.addPoly(
65
+ [
66
+ { x, y: y - r },
67
+ { x: x + r, y },
68
+ { x, y: y + r },
69
+ { x: x - r, y },
70
+ ],
71
+ true
72
+ );
73
+ } else {
74
+ p.addCircle(x, y, r);
75
+ }
76
+ }
77
+ return p.build();
78
+ });
79
+
80
+ return (
81
+ <>
82
+ <Path path={path} color={fillColor ?? color} />
83
+ {fillColor !== undefined ? (
84
+ <Path
85
+ path={path}
86
+ style="stroke"
87
+ color={color}
88
+ strokeWidth={strokeWidth}
89
+ strokeJoin="round"
90
+ />
91
+ ) : null}
92
+ </>
93
+ );
94
+ };
95
+
96
+ export { Dots };
@@ -1,4 +1,4 @@
1
- import { Canvas, Group, Path, type SkPath } from '@shopify/react-native-skia';
1
+ import { Canvas, Group, Path, type SkPath } from 'react-native-skia';
2
2
  import type { FC } from 'react';
3
3
  import React from 'react';
4
4
  import type { StyleProp, ViewStyle } from 'react-native';
@@ -0,0 +1,103 @@
1
+ import {
2
+ Group,
3
+ Line,
4
+ Text,
5
+ vec,
6
+ type Color,
7
+ type SkFont,
8
+ } from 'react-native-skia';
9
+ import { memo, useMemo, type FC } from 'react';
10
+ import { StyleSheet } from 'react-native';
11
+
12
+ export type YAxisProps = {
13
+ width: number;
14
+ height: number;
15
+ font: SkFont;
16
+ minY: number;
17
+ maxY: number;
18
+ /** Values to draw a gridline at. Defaults to `nbTicks` evenly spaced values. */
19
+ values?: number[];
20
+ nbTicks?: number;
21
+ color?: Color;
22
+ labelColor?: Color;
23
+ strokeWidth?: number;
24
+ showLabels?: boolean;
25
+ formatLabel?: (value: number) => string;
26
+ };
27
+
28
+ const defaultFormatLabel = (value: number) => `${Math.round(value)}`;
29
+
30
+ /** Evenly spaced integer ticks from 0 to a padded maximum, e.g. 0, 26, 52, 78, 104. */
31
+ export const getPaddedTicks = function (
32
+ maxValue: number,
33
+ nbTicks = 4,
34
+ padding = 1.15
35
+ ): number[] {
36
+ const step = Math.max(1, Math.ceil((maxValue * padding) / nbTicks));
37
+ return Array.from({ length: nbTicks + 1 }, (_, i) => i * step);
38
+ };
39
+
40
+ const YAxis: FC<YAxisProps> = function (props) {
41
+ const {
42
+ width,
43
+ height,
44
+ font,
45
+ minY,
46
+ maxY,
47
+ nbTicks = 4,
48
+ color = 'black',
49
+ labelColor,
50
+ strokeWidth = StyleSheet.hairlineWidth,
51
+ showLabels = true,
52
+ formatLabel = defaultFormatLabel,
53
+ } = props;
54
+
55
+ // Measured once per input change: parents re-render while panning
56
+ const rows = useMemo(() => {
57
+ // A flat range would divide by zero
58
+ const span = maxY - minY || 1;
59
+ const values =
60
+ props.values ??
61
+ Array.from(
62
+ { length: nbTicks },
63
+ (_, i) => minY + ((i + 1) * (maxY - minY)) / nbTicks
64
+ );
65
+ return values.map((value) => {
66
+ const label = formatLabel(value);
67
+ const labelWidth = font
68
+ .getGlyphWidths(font.getGlyphIDs(label))
69
+ .reduce((a, b) => a + b, 0);
70
+ return {
71
+ label,
72
+ labelWidth,
73
+ transform: [{ translateY: height - ((value - minY) * height) / span }],
74
+ p1: vec(0, 0),
75
+ p2: vec(showLabels ? width - 10 - labelWidth : width, 0),
76
+ };
77
+ });
78
+ }, [props.values, nbTicks, minY, maxY, height, width, font, showLabels, formatLabel]);
79
+
80
+ return (
81
+ <Group>
82
+ {rows.map((row, i) => (
83
+ <Group key={`YTick-${i}`} transform={row.transform}>
84
+ <Line p1={row.p1} p2={row.p2} color={color} strokeWidth={strokeWidth} />
85
+ {showLabels ? (
86
+ <Text
87
+ text={row.label}
88
+ x={width - row.labelWidth}
89
+ y={font.getSize() / 2.5}
90
+ font={font}
91
+ color={labelColor ?? color}
92
+ />
93
+ ) : null}
94
+ </Group>
95
+ ))}
96
+ </Group>
97
+ );
98
+ };
99
+
100
+ const MemoYAxis = memo(YAxis);
101
+ MemoYAxis.displayName = 'YAxis';
102
+
103
+ export { MemoYAxis as YAxis };
@@ -1,4 +1,4 @@
1
- import { Path, Skia, type SkPath } from '@shopify/react-native-skia';
1
+ import { Path, Skia, type SkPath } from 'react-native-skia';
2
2
  import { type FC } from 'react';
3
3
  import { useDerivedValue, type SharedValue } from 'react-native-reanimated';
4
4
 
@@ -8,7 +8,7 @@ export type ScalablePathProps = {
8
8
  focalX: SharedValue<number>;
9
9
  offsetX: SharedValue<number>;
10
10
  color?: string;
11
- pathProps?: Omit<React.ComponentProps<typeof Path>, 'color'>;
11
+ pathProps?: Omit<React.ComponentProps<typeof Path>, 'color' | 'path'>;
12
12
  };
13
13
 
14
14
  const ScalablePath: FC<ScalablePathProps> = function (props) {
@@ -16,13 +16,12 @@ const ScalablePath: FC<ScalablePathProps> = function (props) {
16
16
 
17
17
  const animatedPath = useDerivedValue(() => {
18
18
  // Affine equivalent of getPositionWl: x' = scale * x + focalX * (1 - scale) + offsetX
19
- const _path = path.value.copy();
20
- _path.transform(
21
- Skia.Matrix()
22
- .translate(focalX.value * (1 - scale.value) + offsetX.value, 0)
23
- .scale(scale.value, 1)
24
- );
25
- return _path;
19
+ // Plain 3x3 matrix: a Skia.Matrix() would allocate a native object per frame
20
+ const s = scale.value;
21
+ const tx = focalX.value * (1 - s) + offsetX.value;
22
+ return Skia.PathBuilder.MakeFromPath(path.value)
23
+ .transform([s, 0, tx, 0, 1, 0, 0, 0, 1])
24
+ .build();
26
25
  });
27
26
 
28
27
  return (
@@ -1,4 +1,4 @@
1
- import { Canvas, Group, type SkPath } from '@shopify/react-native-skia';
1
+ import { Canvas, Group, type SkPath } from 'react-native-skia';
2
2
  import type { FC } from 'react';
3
3
  import React from 'react';
4
4
  import type { StyleProp, ViewStyle } from 'react-native';