@ledgerhq/lumen-ui-rnative-visualization 0.1.12 → 0.1.14

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 (51) hide show
  1. package/dist/module/lib/Components/CartesianChart/CartesianChart.js.map +1 -1
  2. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/ChartTooltipItem.js +52 -0
  3. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/ChartTooltipItem.js.map +1 -0
  4. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/DefaultScrubberTooltip.js +81 -0
  5. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/DefaultScrubberTooltip.js.map +1 -0
  6. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/DefaultScrubberTooltip.test.js +340 -0
  7. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/DefaultScrubberTooltip.test.js.map +1 -0
  8. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/constants.js +12 -0
  9. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/constants.js.map +1 -0
  10. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/utils.js +145 -0
  11. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/utils.js.map +1 -0
  12. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/utils.test.js +253 -0
  13. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/utils.test.js.map +1 -0
  14. package/dist/module/lib/Components/Scrubber/Scrubber.js +40 -21
  15. package/dist/module/lib/Components/Scrubber/Scrubber.js.map +1 -1
  16. package/dist/module/lib/Components/Scrubber/Scrubber.stories.js +80 -30
  17. package/dist/module/lib/Components/Scrubber/Scrubber.stories.js.map +1 -1
  18. package/dist/module/lib/Components/Scrubber/Scrubber.test.js +10 -5
  19. package/dist/module/lib/Components/Scrubber/Scrubber.test.js.map +1 -1
  20. package/dist/module/lib/Components/Scrubber/ScrubberProvider.js +51 -27
  21. package/dist/module/lib/Components/Scrubber/ScrubberProvider.js.map +1 -1
  22. package/dist/typescript/src/lib/Components/CartesianChart/CartesianChart.d.ts.map +1 -1
  23. package/dist/typescript/src/lib/Components/Scrubber/DefaultScrubberTooltip/ChartTooltipItem.d.ts +9 -0
  24. package/dist/typescript/src/lib/Components/Scrubber/DefaultScrubberTooltip/ChartTooltipItem.d.ts.map +1 -0
  25. package/dist/typescript/src/lib/Components/Scrubber/DefaultScrubberTooltip/DefaultScrubberTooltip.d.ts +12 -0
  26. package/dist/typescript/src/lib/Components/Scrubber/DefaultScrubberTooltip/DefaultScrubberTooltip.d.ts.map +1 -0
  27. package/dist/typescript/src/lib/Components/Scrubber/DefaultScrubberTooltip/constants.d.ts +10 -0
  28. package/dist/typescript/src/lib/Components/Scrubber/DefaultScrubberTooltip/constants.d.ts.map +1 -0
  29. package/dist/typescript/src/lib/Components/Scrubber/DefaultScrubberTooltip/utils.d.ts +65 -0
  30. package/dist/typescript/src/lib/Components/Scrubber/DefaultScrubberTooltip/utils.d.ts.map +1 -0
  31. package/dist/typescript/src/lib/Components/Scrubber/Scrubber.d.ts +14 -2
  32. package/dist/typescript/src/lib/Components/Scrubber/Scrubber.d.ts.map +1 -1
  33. package/dist/typescript/src/lib/Components/Scrubber/ScrubberProvider.d.ts.map +1 -1
  34. package/dist/typescript/src/lib/Components/Scrubber/index.d.ts +1 -1
  35. package/dist/typescript/src/lib/Components/Scrubber/index.d.ts.map +1 -1
  36. package/dist/typescript/src/lib/Components/Scrubber/types.d.ts +103 -4
  37. package/dist/typescript/src/lib/Components/Scrubber/types.d.ts.map +1 -1
  38. package/package.json +4 -4
  39. package/src/lib/Components/CartesianChart/CartesianChart.tsx +0 -1
  40. package/src/lib/Components/Scrubber/DefaultScrubberTooltip/ChartTooltipItem.tsx +54 -0
  41. package/src/lib/Components/Scrubber/DefaultScrubberTooltip/DefaultScrubberTooltip.test.tsx +259 -0
  42. package/src/lib/Components/Scrubber/DefaultScrubberTooltip/DefaultScrubberTooltip.tsx +105 -0
  43. package/src/lib/Components/Scrubber/DefaultScrubberTooltip/constants.ts +9 -0
  44. package/src/lib/Components/Scrubber/DefaultScrubberTooltip/utils.test.ts +264 -0
  45. package/src/lib/Components/Scrubber/DefaultScrubberTooltip/utils.ts +229 -0
  46. package/src/lib/Components/Scrubber/Scrubber.stories.tsx +104 -42
  47. package/src/lib/Components/Scrubber/Scrubber.test.tsx +7 -5
  48. package/src/lib/Components/Scrubber/Scrubber.tsx +47 -24
  49. package/src/lib/Components/Scrubber/ScrubberProvider.tsx +56 -28
  50. package/src/lib/Components/Scrubber/index.ts +10 -1
  51. package/src/lib/Components/Scrubber/types.ts +109 -4
@@ -8,26 +8,26 @@ import {
8
8
  LinearGradient,
9
9
  Rect,
10
10
  Stop,
11
- Text as SvgText,
12
11
  } from 'react-native-svg';
13
12
 
14
13
  import { useCartesianChartContext } from '../CartesianChart/context';
15
14
  import {
16
15
  BEACON_RADIUS,
17
16
  BEACON_STROKE_WIDTH,
18
- LABEL_OFFSET_Y,
19
17
  LINE_GRADIENT_EDGE_OPACITY,
20
18
  OVERLAY_LINE_INSET,
21
19
  OVERLAY_OFFSET,
22
20
  OVERLAY_OPACITY,
23
21
  } from './constants';
24
22
  import { useScrubberContext } from './context';
23
+ import { DefaultScrubberTooltip } from './DefaultScrubberTooltip/DefaultScrubberTooltip';
25
24
  import type { ScrubberProps } from './types';
26
25
  import { resolvePixelX, resolvePixelY } from './utils';
27
26
 
28
27
  /**
29
28
  * Renders the scrubber visuals: vertical reference line, future-data overlay
30
- * rect, per-series beacon dots, and an optional formatted label above the line.
29
+ * rect, per-series beacon dots, optional label above the line, and an optional
30
+ * tooltip when {@link ScrubberProps.tooltip} is set, using {@link DefaultScrubberTooltip}.
31
31
  *
32
32
  * Must be used as a child of `LineChart` (or `CartesianChart`) with
33
33
  * `enableScrubbing` enabled. Renders nothing when no scrubber position is active.
@@ -38,12 +38,23 @@ import { resolvePixelX, resolvePixelY } from './utils';
38
38
  * <Scrubber label={(i) => data[i].date} />
39
39
  * </LineChart>
40
40
  * ```
41
+ *
42
+ * @example Tooltip
43
+ * ```tsx
44
+ * <Scrubber
45
+ * tooltip={(i) => ({
46
+ * title: `${counts[i]} Transactions`,
47
+ * items: [{ label: 'Index', value: String(i) }],
48
+ * minWidth: 160,
49
+ * })}
50
+ * />
51
+ * ```
41
52
  */
42
53
  export function Scrubber({
43
- label,
44
54
  hideLine = false,
45
55
  hideOverlay = false,
46
56
  showBeacons = false,
57
+ tooltip,
47
58
  }: Readonly<ScrubberProps>) {
48
59
  const lineGradientId = useId();
49
60
  const { theme } = useTheme();
@@ -76,10 +87,27 @@ export function Scrubber({
76
87
  );
77
88
  }, [scrubberPosition, showBeacons, series, seriesMap, getYScale]);
78
89
 
79
- const resolvedLabel = useMemo(() => {
80
- if (scrubberPosition === undefined || !label) return undefined;
81
- return label(scrubberPosition);
82
- }, [scrubberPosition, label]);
90
+ const tooltipPayload = useMemo(() => {
91
+ if (scrubberPosition === undefined || !tooltip) {
92
+ return undefined;
93
+ }
94
+
95
+ const content = tooltip(scrubberPosition);
96
+
97
+ if (content.items.length === 0) return undefined;
98
+
99
+ const resolvedTitle =
100
+ typeof content.title === 'function'
101
+ ? content.title(scrubberPosition)
102
+ : content.title;
103
+
104
+ return {
105
+ items: content.items,
106
+ resolvedTitle,
107
+ offset: content.offset,
108
+ minWidth: content.minWidth,
109
+ };
110
+ }, [scrubberPosition, tooltip]);
83
111
 
84
112
  if (scrubberPosition === undefined || pixelX === undefined) {
85
113
  return null;
@@ -102,7 +130,6 @@ export function Scrubber({
102
130
 
103
131
  const borderMutedColor = theme.colors.border.base;
104
132
  const backgroundBaseColor = theme.colors.bg.base;
105
- const textBaseColor = theme.colors.text.base;
106
133
  const bgCanvasColor = theme.colors.bg.canvas;
107
134
 
108
135
  return (
@@ -156,21 +183,6 @@ export function Scrubber({
156
183
  />
157
184
  )}
158
185
 
159
- {resolvedLabel !== undefined && (
160
- <SvgText
161
- testID='scrubber-label'
162
- x={pixelX}
163
- y={drawY - LABEL_OFFSET_Y}
164
- textAnchor='middle'
165
- fill={textBaseColor}
166
- fontSize={theme.typographies.body4.fontSize}
167
- fontWeight={theme.typographies.body4.fontWeight}
168
- fontFamily={theme.fontFamilies.sans}
169
- >
170
- {resolvedLabel}
171
- </SvgText>
172
- )}
173
-
174
186
  {showBeacons &&
175
187
  beacons.map((beacon) => (
176
188
  <Circle
@@ -184,6 +196,17 @@ export function Scrubber({
184
196
  strokeWidth={BEACON_STROKE_WIDTH}
185
197
  />
186
198
  ))}
199
+
200
+ {tooltipPayload !== undefined && (
201
+ <DefaultScrubberTooltip
202
+ pixelX={pixelX}
203
+ drawingArea={drawingArea}
204
+ title={tooltipPayload.resolvedTitle}
205
+ items={tooltipPayload.items}
206
+ offset={tooltipPayload.offset}
207
+ minWidth={tooltipPayload.minWidth}
208
+ />
209
+ )}
187
210
  </G>
188
211
  );
189
212
  }
@@ -9,6 +9,8 @@ import { ScrubberContextProvider } from './context';
9
9
  import type { ScrubberProviderProps } from './types';
10
10
  import { getDataIndexFromPosition } from './utils';
11
11
 
12
+ const LONG_PRESS_MIN_DURATION_MS = 50;
13
+
12
14
  /**
13
15
  * Provides scrubbing interaction for a chart.
14
16
  *
@@ -32,24 +34,39 @@ export function ScrubberProvider({
32
34
  const [scrubberPosition, setScrubberPosition] = useState<
33
35
  number | undefined
34
36
  >();
35
- const lastPositionRef = useRef<number | undefined>(undefined);
36
37
 
37
38
  const { getXScale, getXAxisConfig, dataLength } = useCartesianChartContext();
38
39
 
39
- const isScrubbing = useSharedValue(false);
40
+ // All values touched by the gesture's JS-thread callback live in a single
41
+ // ref. Reading via `latest.current` keeps the callbacks reference-stable
42
+ // across data, scale, and prop updates, which in turn keeps `composed` and
43
+ // the gesture surface JSX stable so re-rendering the provider on each
44
+ // index change never tears down the GestureDetector or the View tree.
45
+ const latest = useRef({
46
+ getXScale,
47
+ getXAxisConfig,
48
+ dataLength,
49
+ onChange: onScrubberPositionChange,
50
+ lastIndex: undefined as number | undefined,
51
+ });
52
+ latest.current.getXScale = getXScale;
53
+ latest.current.getXAxisConfig = getXAxisConfig;
54
+ latest.current.dataLength = dataLength;
55
+ latest.current.onChange = onScrubberPositionChange;
40
56
 
41
57
  const setScrubberPositionAndNotify = useCallback(
42
58
  (index: number | undefined) => {
59
+ const ref = latest.current;
43
60
  const clamped =
44
- index === undefined || dataLength <= 0
61
+ index === undefined || ref.dataLength <= 0
45
62
  ? undefined
46
- : Math.max(0, Math.min(index, dataLength - 1));
47
- if (clamped === lastPositionRef.current) return;
48
- lastPositionRef.current = clamped;
63
+ : Math.max(0, Math.min(index, ref.dataLength - 1));
64
+ if (clamped === ref.lastIndex) return;
65
+ ref.lastIndex = clamped;
49
66
  setScrubberPosition(clamped);
50
- onScrubberPositionChange?.(clamped);
67
+ ref.onChange?.(clamped);
51
68
  },
52
- [dataLength, onScrubberPositionChange],
69
+ [],
53
70
  );
54
71
 
55
72
  const handlePositionChange = useCallback(
@@ -58,22 +75,22 @@ export function ScrubberProvider({
58
75
  setScrubberPositionAndNotify(undefined);
59
76
  return;
60
77
  }
61
-
62
- const scale = getXScale();
63
- if (!scale || dataLength <= 0) return;
64
-
65
- const axisConfig = getXAxisConfig();
78
+ const ref = latest.current;
79
+ const scale = ref.getXScale();
80
+ if (!scale || ref.dataLength <= 0) return;
66
81
  const index = getDataIndexFromPosition(
67
82
  pixelX,
68
83
  scale,
69
- axisConfig,
70
- dataLength,
84
+ ref.getXAxisConfig(),
85
+ ref.dataLength,
71
86
  );
72
87
  setScrubberPositionAndNotify(index);
73
88
  },
74
- [getXScale, getXAxisConfig, dataLength, setScrubberPositionAndNotify],
89
+ [setScrubberPositionAndNotify],
75
90
  );
76
91
 
92
+ const isScrubbing = useSharedValue(false);
93
+
77
94
  const resetScrubber = useCallback((): void => {
78
95
  'worklet';
79
96
  isScrubbing.value = false;
@@ -82,7 +99,7 @@ export function ScrubberProvider({
82
99
 
83
100
  const composed = useMemo(() => {
84
101
  const longPress = Gesture.LongPress()
85
- .minDuration(50)
102
+ .minDuration(LONG_PRESS_MIN_DURATION_MS)
86
103
  .onStart((e) => {
87
104
  'worklet';
88
105
  isScrubbing.value = true;
@@ -105,17 +122,12 @@ export function ScrubberProvider({
105
122
  return Gesture.Simultaneous(longPress, pan);
106
123
  }, [isScrubbing, handlePositionChange, resetScrubber]);
107
124
 
108
- const contextValue = useMemo(
109
- () => ({
110
- enableScrubbing,
111
- scrubberPosition,
112
- onScrubberPositionChange: setScrubberPositionAndNotify,
113
- }),
114
- [enableScrubbing, scrubberPosition, setScrubberPositionAndNotify],
115
- );
116
-
117
- return (
118
- <ScrubberContextProvider value={contextValue}>
125
+ // Memoizing the surface lets React's element-identity bailout skip
126
+ // reconciling the View / GestureDetector / chart `children` subtree when
127
+ // only `scrubberPosition` changes. The Scrubber consumer still updates via
128
+ // context as expected.
129
+ const surface = useMemo(
130
+ () => (
119
131
  <View style={{ width, height }}>
120
132
  {children}
121
133
  {enableScrubbing && (
@@ -127,6 +139,22 @@ export function ScrubberProvider({
127
139
  </GestureDetector>
128
140
  )}
129
141
  </View>
142
+ ),
143
+ [width, height, children, enableScrubbing, composed],
144
+ );
145
+
146
+ const contextValue = useMemo(
147
+ () => ({
148
+ enableScrubbing,
149
+ scrubberPosition,
150
+ onScrubberPositionChange: setScrubberPositionAndNotify,
151
+ }),
152
+ [enableScrubbing, scrubberPosition, setScrubberPositionAndNotify],
153
+ );
154
+
155
+ return (
156
+ <ScrubberContextProvider value={contextValue}>
157
+ {surface}
130
158
  </ScrubberContextProvider>
131
159
  );
132
160
  }
@@ -1,3 +1,12 @@
1
1
  export { Scrubber } from './Scrubber';
2
2
  export { useScrubberContext } from './context';
3
- export type { ScrubberProps, ScrubberContextValue } from './types';
3
+
4
+ export type {
5
+ ChartTooltipItemData,
6
+ ChartTooltipItemProps,
7
+ ScrubberContextValue,
8
+ ScrubberProps,
9
+ ScrubberTooltipContent,
10
+ ScrubberTooltipProps,
11
+ SvgBBoxElement,
12
+ } from './types';
@@ -1,5 +1,7 @@
1
1
  import type { ReactNode } from 'react';
2
2
 
3
+ import type { DrawingArea } from '../../utils/types';
4
+
3
5
  export type ScrubberContextValue = {
4
6
  /**
5
7
  * Whether scrubbing interactions are enabled.
@@ -35,12 +37,109 @@ export type ScrubberProviderProps = {
35
37
  onScrubberPositionChange?: (index: number | undefined) => void;
36
38
  };
37
39
 
38
- export type ScrubberProps = {
40
+ export type ChartTooltipItemData = {
41
+ /**
42
+ * Label displayed on the left side of the row.
43
+ */
44
+ label: string | number;
45
+ /**
46
+ * Value displayed on the right side of the row.
47
+ */
48
+ value: string | number;
49
+ };
50
+
51
+ export type ChartTooltipItemProps = ChartTooltipItemData & {
52
+ /**
53
+ * X position in SVG coordinate space.
54
+ * @default 0
55
+ */
56
+ x?: number;
57
+ /**
58
+ * Y midline position in SVG coordinate space.
59
+ * @default 0
60
+ */
61
+ y?: number;
62
+ /**
63
+ * Width allocated to this row, used to right-align the value text.
64
+ */
65
+ width: number;
66
+ /**
67
+ * Optional callback ref forwarded to the label `<Text>` element.
68
+ * Useful to measure the label's natural width via `getBBox` for auto-fit layouts.
69
+ */
70
+ labelRef?: (el: SvgBBoxElement | null) => void;
71
+ /**
72
+ * Optional callback ref forwarded to the value `<Text>` element.
73
+ * Useful to measure the value's natural width via `getBBox` for auto-fit layouts.
74
+ */
75
+ valueRef?: (el: SvgBBoxElement | null) => void;
76
+ };
77
+
78
+ /**
79
+ * Minimal interface for an SVG element that supports `getBBox`.
80
+ * Handles both react-native-svg (returns `Rect | undefined`) and web DOM SVG
81
+ * (returns `DOMRect` synchronously or a Promise on native bridge).
82
+ */
83
+ export type SvgBBoxElement = {
84
+ getBBox: () =>
85
+ | { width: number }
86
+ | undefined
87
+ | Promise<{ width: number } | undefined>;
88
+ };
89
+
90
+ export type ScrubberTooltipLayoutProps = {
39
91
  /**
40
- * Formats a label string shown above the reference line for a given data index.
41
- * When omitted, no label is rendered.
92
+ * Horizontal gap in pixels between the scrubber line and the tooltip box.
93
+ * @default 10
42
94
  */
43
- label?: (dataIndex: number) => string;
95
+ offset?: number;
96
+ /**
97
+ * Minimum width in pixels. The tooltip auto-fits to the rendered content
98
+ * but never collapses below this floor; raise it to avoid jitter when
99
+ * value length changes between indices.
100
+ * @default 80
101
+ */
102
+ minWidth?: number;
103
+ };
104
+
105
+ export type ScrubberTooltipProps = ScrubberTooltipLayoutProps & {
106
+ /**
107
+ * Horizontal pixel position of the scrubber line within the SVG coordinate
108
+ * space. Used to place the tooltip left or right of the line.
109
+ */
110
+ pixelX: number;
111
+ /**
112
+ * Bounding box of the chart's drawing area (x, y, width, height). Used to
113
+ * constrain the tooltip so it never overflows the chart boundaries.
114
+ */
115
+ drawingArea: DrawingArea;
116
+ /**
117
+ * Optional title displayed at the top of the tooltip. Omit to render
118
+ * the tooltip without a title row.
119
+ */
120
+ title?: string | number;
121
+ /**
122
+ * List of label/value pairs rendered as rows inside the tooltip body.
123
+ */
124
+ items: ChartTooltipItemData[];
125
+ };
126
+
127
+ /**
128
+ * Return value of the `tooltip` callback on {@link ScrubberProps}.
129
+ */
130
+ export type ScrubberTooltipContent = ScrubberTooltipLayoutProps & {
131
+ /**
132
+ * Optional header. Static value or callback per data index.
133
+ * A callback may return `undefined` to suppress the title.
134
+ */
135
+ title?: string | number | ((index: number) => string | number | undefined);
136
+ /**
137
+ * Tooltip rows for this index. Return an empty array to hide the tooltip.
138
+ */
139
+ items: ChartTooltipItemData[];
140
+ };
141
+
142
+ export type ScrubberProps = {
44
143
  /**
45
144
  * Hides the vertical reference line.
46
145
  * @default false
@@ -56,4 +155,10 @@ export type ScrubberProps = {
56
155
  * @default false
57
156
  */
58
157
  showBeacons?: boolean;
158
+ /**
159
+ * Produces tooltip content for the active data index. When set, {@link DefaultScrubberTooltip}
160
+ * is rendered. Optional `offset` and `minWidth` on the returned object tune layout.
161
+ * Return `{ items: [] }` to hide the tooltip at an index.
162
+ */
163
+ tooltip?: (dataIndex: number) => ScrubberTooltipContent;
59
164
  };