@ledgerhq/lumen-ui-rnative-visualization 0.1.10 → 0.1.11

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 (163) hide show
  1. package/dist/module/lib/Components/Axis/XAxis/XAxis.js +1 -1
  2. package/dist/module/lib/Components/Axis/XAxis/XAxis.js.map +1 -1
  3. package/dist/module/lib/Components/Axis/YAxis/YAxis.js +1 -1
  4. package/dist/module/lib/Components/Axis/YAxis/YAxis.js.map +1 -1
  5. package/dist/module/lib/Components/CartesianChart/CartesianChart.js +31 -28
  6. package/dist/module/lib/Components/CartesianChart/CartesianChart.js.map +1 -1
  7. package/dist/module/lib/Components/CartesianChart/RevealClip/RevealClipDefs.js +53 -0
  8. package/dist/module/lib/Components/CartesianChart/RevealClip/RevealClipDefs.js.map +1 -0
  9. package/dist/module/lib/Components/CartesianChart/RevealClip/RevealClipDefs.test.js +144 -0
  10. package/dist/module/lib/Components/CartesianChart/RevealClip/RevealClipDefs.test.js.map +1 -0
  11. package/dist/module/lib/Components/CartesianChart/RevealClip/context.js +8 -0
  12. package/dist/module/lib/Components/CartesianChart/RevealClip/context.js.map +1 -0
  13. package/dist/module/lib/Components/CartesianChart/RevealClip/index.js +5 -0
  14. package/dist/module/lib/Components/CartesianChart/RevealClip/index.js.map +1 -0
  15. package/dist/module/lib/Components/CartesianChart/RevealClip/types.js +4 -0
  16. package/dist/module/lib/Components/CartesianChart/RevealClip/types.js.map +1 -0
  17. package/dist/module/lib/Components/CartesianChart/RevealClip/utils.js +32 -0
  18. package/dist/module/lib/Components/CartesianChart/RevealClip/utils.js.map +1 -0
  19. package/dist/module/lib/Components/CartesianChart/utils.js +4 -4
  20. package/dist/module/lib/Components/Line/Line.js +5 -2
  21. package/dist/module/lib/Components/Line/Line.js.map +1 -1
  22. package/dist/module/lib/Components/LineChart/LineChart.js +7 -1
  23. package/dist/module/lib/Components/LineChart/LineChart.js.map +1 -1
  24. package/dist/module/lib/Components/LineChart/LineChart.test.js +25 -0
  25. package/dist/module/lib/Components/LineChart/LineChart.test.js.map +1 -1
  26. package/dist/module/lib/Components/Point/Point.js +3 -0
  27. package/dist/module/lib/Components/Point/Point.js.map +1 -1
  28. package/dist/module/lib/Components/ReferenceLine/ReferenceLine.js +125 -0
  29. package/dist/module/lib/Components/ReferenceLine/ReferenceLine.js.map +1 -0
  30. package/dist/module/lib/Components/ReferenceLine/ReferenceLine.stories.js +111 -0
  31. package/dist/module/lib/Components/ReferenceLine/ReferenceLine.stories.js.map +1 -0
  32. package/dist/module/lib/Components/ReferenceLine/ReferenceLine.test.js +338 -0
  33. package/dist/module/lib/Components/ReferenceLine/ReferenceLine.test.js.map +1 -0
  34. package/dist/module/lib/Components/ReferenceLine/constants.js +5 -0
  35. package/dist/module/lib/Components/ReferenceLine/constants.js.map +1 -0
  36. package/dist/module/lib/Components/ReferenceLine/index.js +5 -0
  37. package/dist/module/lib/Components/ReferenceLine/index.js.map +1 -0
  38. package/dist/module/lib/Components/ReferenceLine/types.js +2 -0
  39. package/dist/module/lib/Components/ReferenceLine/types.js.map +1 -0
  40. package/dist/module/lib/Components/ReferenceLine/utils.js +121 -0
  41. package/dist/module/lib/Components/ReferenceLine/utils.js.map +1 -0
  42. package/dist/module/lib/Components/ReferenceLine/utils.test.js +297 -0
  43. package/dist/module/lib/Components/ReferenceLine/utils.test.js.map +1 -0
  44. package/dist/module/lib/Components/Scrubber/Scrubber.js +152 -0
  45. package/dist/module/lib/Components/Scrubber/Scrubber.js.map +1 -0
  46. package/dist/module/lib/Components/Scrubber/Scrubber.stories.js +113 -0
  47. package/dist/module/lib/Components/Scrubber/Scrubber.stories.js.map +1 -0
  48. package/dist/module/lib/Components/Scrubber/Scrubber.test.js +132 -0
  49. package/dist/module/lib/Components/Scrubber/Scrubber.test.js.map +1 -0
  50. package/dist/module/lib/Components/Scrubber/ScrubberProvider.js +105 -0
  51. package/dist/module/lib/Components/Scrubber/ScrubberProvider.js.map +1 -0
  52. package/dist/module/lib/Components/Scrubber/ScrubberProvider.test.js +217 -0
  53. package/dist/module/lib/Components/Scrubber/ScrubberProvider.test.js.map +1 -0
  54. package/dist/module/lib/Components/Scrubber/constants.js +10 -0
  55. package/dist/module/lib/Components/Scrubber/constants.js.map +1 -0
  56. package/dist/module/lib/Components/Scrubber/context/index.js +4 -0
  57. package/dist/module/lib/Components/Scrubber/context/index.js.map +1 -0
  58. package/dist/module/lib/Components/Scrubber/context/scrubberContext.js +10 -0
  59. package/dist/module/lib/Components/Scrubber/context/scrubberContext.js.map +1 -0
  60. package/dist/module/lib/Components/Scrubber/index.js +5 -0
  61. package/dist/module/lib/Components/Scrubber/index.js.map +1 -0
  62. package/dist/module/lib/Components/Scrubber/types.js +4 -0
  63. package/dist/module/lib/Components/Scrubber/types.js.map +1 -0
  64. package/dist/module/lib/Components/Scrubber/utils.js +83 -0
  65. package/dist/module/lib/Components/Scrubber/utils.js.map +1 -0
  66. package/dist/module/lib/Components/Scrubber/utils.test.js +192 -0
  67. package/dist/module/lib/Components/Scrubber/utils.test.js.map +1 -0
  68. package/dist/module/lib/Components/index.js +2 -0
  69. package/dist/module/lib/Components/index.js.map +1 -1
  70. package/dist/module/lib/utils/scales/scales.js +16 -8
  71. package/dist/module/lib/utils/scales/scales.js.map +1 -1
  72. package/dist/module/lib/utils/scales/scales.test.js +32 -1
  73. package/dist/module/lib/utils/scales/scales.test.js.map +1 -1
  74. package/dist/typescript/src/lib/Components/CartesianChart/CartesianChart.d.ts +1 -1
  75. package/dist/typescript/src/lib/Components/CartesianChart/CartesianChart.d.ts.map +1 -1
  76. package/dist/typescript/src/lib/Components/CartesianChart/RevealClip/RevealClipDefs.d.ts +3 -0
  77. package/dist/typescript/src/lib/Components/CartesianChart/RevealClip/RevealClipDefs.d.ts.map +1 -0
  78. package/dist/typescript/src/lib/Components/CartesianChart/RevealClip/context.d.ts +7 -0
  79. package/dist/typescript/src/lib/Components/CartesianChart/RevealClip/context.d.ts.map +1 -0
  80. package/dist/typescript/src/lib/Components/CartesianChart/RevealClip/index.d.ts +3 -0
  81. package/dist/typescript/src/lib/Components/CartesianChart/RevealClip/index.d.ts.map +1 -0
  82. package/dist/typescript/src/lib/Components/CartesianChart/RevealClip/types.d.ts +38 -0
  83. package/dist/typescript/src/lib/Components/CartesianChart/RevealClip/types.d.ts.map +1 -0
  84. package/dist/typescript/src/lib/Components/CartesianChart/RevealClip/utils.d.ts +17 -0
  85. package/dist/typescript/src/lib/Components/CartesianChart/RevealClip/utils.d.ts.map +1 -0
  86. package/dist/typescript/src/lib/Components/CartesianChart/types.d.ts +17 -0
  87. package/dist/typescript/src/lib/Components/CartesianChart/types.d.ts.map +1 -1
  88. package/dist/typescript/src/lib/Components/Line/Line.d.ts.map +1 -1
  89. package/dist/typescript/src/lib/Components/LineChart/LineChart.d.ts +1 -1
  90. package/dist/typescript/src/lib/Components/LineChart/LineChart.d.ts.map +1 -1
  91. package/dist/typescript/src/lib/Components/LineChart/types.d.ts +16 -0
  92. package/dist/typescript/src/lib/Components/LineChart/types.d.ts.map +1 -1
  93. package/dist/typescript/src/lib/Components/Point/Point.d.ts.map +1 -1
  94. package/dist/typescript/src/lib/Components/ReferenceLine/ReferenceLine.d.ts +3 -0
  95. package/dist/typescript/src/lib/Components/ReferenceLine/ReferenceLine.d.ts.map +1 -0
  96. package/dist/typescript/src/lib/Components/ReferenceLine/constants.d.ts +3 -0
  97. package/dist/typescript/src/lib/Components/ReferenceLine/constants.d.ts.map +1 -0
  98. package/dist/typescript/src/lib/Components/ReferenceLine/index.d.ts +3 -0
  99. package/dist/typescript/src/lib/Components/ReferenceLine/index.d.ts.map +1 -0
  100. package/dist/typescript/src/lib/Components/ReferenceLine/types.d.ts +74 -0
  101. package/dist/typescript/src/lib/Components/ReferenceLine/types.d.ts.map +1 -0
  102. package/dist/typescript/src/lib/Components/ReferenceLine/utils.d.ts +55 -0
  103. package/dist/typescript/src/lib/Components/ReferenceLine/utils.d.ts.map +1 -0
  104. package/dist/typescript/src/lib/Components/Scrubber/Scrubber.d.ts +17 -0
  105. package/dist/typescript/src/lib/Components/Scrubber/Scrubber.d.ts.map +1 -0
  106. package/dist/typescript/src/lib/Components/Scrubber/ScrubberProvider.d.ts +16 -0
  107. package/dist/typescript/src/lib/Components/Scrubber/ScrubberProvider.d.ts.map +1 -0
  108. package/dist/typescript/src/lib/Components/Scrubber/constants.d.ts +8 -0
  109. package/dist/typescript/src/lib/Components/Scrubber/constants.d.ts.map +1 -0
  110. package/dist/typescript/src/lib/Components/Scrubber/context/index.d.ts +2 -0
  111. package/dist/typescript/src/lib/Components/Scrubber/context/index.d.ts.map +1 -0
  112. package/dist/typescript/src/lib/Components/Scrubber/context/scrubberContext.d.ts +8 -0
  113. package/dist/typescript/src/lib/Components/Scrubber/context/scrubberContext.d.ts.map +1 -0
  114. package/dist/typescript/src/lib/Components/Scrubber/index.d.ts +4 -0
  115. package/dist/typescript/src/lib/Components/Scrubber/index.d.ts.map +1 -0
  116. package/dist/typescript/src/lib/Components/Scrubber/types.d.ts +57 -0
  117. package/dist/typescript/src/lib/Components/Scrubber/types.d.ts.map +1 -0
  118. package/dist/typescript/src/lib/Components/Scrubber/utils.d.ts +22 -0
  119. package/dist/typescript/src/lib/Components/Scrubber/utils.d.ts.map +1 -0
  120. package/dist/typescript/src/lib/Components/index.d.ts +2 -0
  121. package/dist/typescript/src/lib/Components/index.d.ts.map +1 -1
  122. package/dist/typescript/src/lib/utils/scales/scales.d.ts +6 -0
  123. package/dist/typescript/src/lib/utils/scales/scales.d.ts.map +1 -1
  124. package/package.json +6 -3
  125. package/src/lib/Components/Axis/XAxis/XAxis.tsx +1 -1
  126. package/src/lib/Components/Axis/YAxis/YAxis.tsx +1 -1
  127. package/src/lib/Components/CartesianChart/CartesianChart.tsx +32 -32
  128. package/src/lib/Components/CartesianChart/RevealClip/RevealClipDefs.test.tsx +129 -0
  129. package/src/lib/Components/CartesianChart/RevealClip/RevealClipDefs.tsx +59 -0
  130. package/src/lib/Components/CartesianChart/RevealClip/context.ts +13 -0
  131. package/src/lib/Components/CartesianChart/RevealClip/index.ts +2 -0
  132. package/src/lib/Components/CartesianChart/RevealClip/types.ts +38 -0
  133. package/src/lib/Components/CartesianChart/RevealClip/utils.ts +51 -0
  134. package/src/lib/Components/CartesianChart/types.ts +17 -0
  135. package/src/lib/Components/CartesianChart/utils.ts +4 -4
  136. package/src/lib/Components/Line/Line.tsx +5 -3
  137. package/src/lib/Components/LineChart/LineChart.test.tsx +23 -0
  138. package/src/lib/Components/LineChart/LineChart.tsx +6 -0
  139. package/src/lib/Components/LineChart/types.ts +16 -0
  140. package/src/lib/Components/Point/Point.tsx +3 -1
  141. package/src/lib/Components/ReferenceLine/ReferenceLine.stories.tsx +109 -0
  142. package/src/lib/Components/ReferenceLine/ReferenceLine.test.tsx +269 -0
  143. package/src/lib/Components/ReferenceLine/ReferenceLine.tsx +149 -0
  144. package/src/lib/Components/ReferenceLine/constants.ts +2 -0
  145. package/src/lib/Components/ReferenceLine/index.ts +2 -0
  146. package/src/lib/Components/ReferenceLine/types.ts +79 -0
  147. package/src/lib/Components/ReferenceLine/utils.test.ts +332 -0
  148. package/src/lib/Components/ReferenceLine/utils.ts +175 -0
  149. package/src/lib/Components/Scrubber/Scrubber.stories.tsx +122 -0
  150. package/src/lib/Components/Scrubber/Scrubber.test.tsx +110 -0
  151. package/src/lib/Components/Scrubber/Scrubber.tsx +189 -0
  152. package/src/lib/Components/Scrubber/ScrubberProvider.test.tsx +223 -0
  153. package/src/lib/Components/Scrubber/ScrubberProvider.tsx +132 -0
  154. package/src/lib/Components/Scrubber/constants.ts +7 -0
  155. package/src/lib/Components/Scrubber/context/index.ts +1 -0
  156. package/src/lib/Components/Scrubber/context/scrubberContext.ts +14 -0
  157. package/src/lib/Components/Scrubber/index.ts +3 -0
  158. package/src/lib/Components/Scrubber/types.ts +59 -0
  159. package/src/lib/Components/Scrubber/utils.test.ts +172 -0
  160. package/src/lib/Components/Scrubber/utils.ts +126 -0
  161. package/src/lib/Components/index.ts +2 -0
  162. package/src/lib/utils/scales/scales.test.ts +22 -0
  163. package/src/lib/utils/scales/scales.ts +19 -9
@@ -0,0 +1,59 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ export type ScrubberContextValue = {
4
+ /**
5
+ * Whether scrubbing interactions are enabled.
6
+ */
7
+ enableScrubbing: boolean;
8
+ /**
9
+ * The current data index of the scrubber, or undefined when idle.
10
+ */
11
+ scrubberPosition: number | undefined;
12
+ /**
13
+ * Callback to update the scrubber position.
14
+ */
15
+ onScrubberPositionChange: (index: number | undefined) => void;
16
+ };
17
+
18
+ export type ScrubberProviderProps = {
19
+ children: ReactNode;
20
+ /**
21
+ * Width of the chart in pixels. Used to bound gesture detection.
22
+ */
23
+ width: number;
24
+ /**
25
+ * Height of the chart in pixels. Used to bound gesture detection.
26
+ */
27
+ height: number;
28
+ /**
29
+ * Whether scrubbing is enabled.
30
+ */
31
+ enableScrubbing: boolean;
32
+ /**
33
+ * Optional external callback fired whenever the scrubber position changes.
34
+ */
35
+ onScrubberPositionChange?: (index: number | undefined) => void;
36
+ };
37
+
38
+ export type ScrubberProps = {
39
+ /**
40
+ * Formats a label string shown above the reference line for a given data index.
41
+ * When omitted, no label is rendered.
42
+ */
43
+ label?: (dataIndex: number) => string;
44
+ /**
45
+ * Hides the vertical reference line.
46
+ * @default false
47
+ */
48
+ hideLine?: boolean;
49
+ /**
50
+ * Hides the semi-transparent overlay that dims data after the scrubber position.
51
+ * @default false
52
+ */
53
+ hideOverlay?: boolean;
54
+ /**
55
+ * Shows the beacon dots on each series at the scrubbed data index.
56
+ * @default false
57
+ */
58
+ showBeacons?: boolean;
59
+ };
@@ -0,0 +1,172 @@
1
+ import { describe, expect, it } from '@jest/globals';
2
+
3
+ import {
4
+ getCategoricalScale,
5
+ getNumericScale,
6
+ } from '../../utils/scales/scales';
7
+ import {
8
+ getDataIndexFromPosition,
9
+ resolvePixelX,
10
+ resolvePixelY,
11
+ } from './utils';
12
+
13
+ describe('getDataIndexFromPosition', () => {
14
+ describe('with a categorical (band) scale', () => {
15
+ const scale = getCategoricalScale({
16
+ domain: { min: 0, max: 3 },
17
+ range: { min: 0, max: 400 },
18
+ padding: 0,
19
+ });
20
+
21
+ it('returns 0 for a position at the center of the first band', () => {
22
+ const bandwidth = scale.bandwidth();
23
+ const center0 = (scale(0) ?? 0) + bandwidth / 2;
24
+ expect(getDataIndexFromPosition(center0, scale, undefined, 4)).toBe(0);
25
+ });
26
+
27
+ it('returns 1 for a position at the center of the second band', () => {
28
+ const bandwidth = scale.bandwidth();
29
+ const center1 = (scale(1) ?? 0) + bandwidth / 2;
30
+ expect(getDataIndexFromPosition(center1, scale, undefined, 4)).toBe(1);
31
+ });
32
+
33
+ it('returns the nearest band for a position between bands', () => {
34
+ expect(getDataIndexFromPosition(190, scale, undefined, 4)).toBe(1);
35
+ expect(getDataIndexFromPosition(210, scale, undefined, 4)).toBe(2);
36
+ });
37
+
38
+ it('returns 3 (last) for a position beyond the last band', () => {
39
+ expect(getDataIndexFromPosition(500, scale, undefined, 4)).toBe(3);
40
+ });
41
+ });
42
+
43
+ describe('with a numeric (linear) scale and no axisData', () => {
44
+ const scale = getNumericScale({
45
+ scaleType: 'linear',
46
+ domain: { min: 0, max: 4 },
47
+ range: { min: 0, max: 400 },
48
+ });
49
+
50
+ it('returns 0 for pixel 0', () => {
51
+ expect(getDataIndexFromPosition(0, scale, undefined, 5)).toBe(0);
52
+ });
53
+
54
+ it('returns 2 for pixel 200 (midpoint)', () => {
55
+ expect(getDataIndexFromPosition(200, scale, undefined, 5)).toBe(2);
56
+ });
57
+
58
+ it('clamps to 0 for negative positions', () => {
59
+ expect(getDataIndexFromPosition(-50, scale, undefined, 5)).toBe(0);
60
+ });
61
+
62
+ it('clamps to dataLength-1 for positions beyond range', () => {
63
+ expect(getDataIndexFromPosition(600, scale, undefined, 5)).toBe(4);
64
+ });
65
+ });
66
+
67
+ describe('with a numeric scale and numeric axisData', () => {
68
+ const scale = getNumericScale({
69
+ scaleType: 'linear',
70
+ domain: { min: 10, max: 30 },
71
+ range: { min: 0, max: 200 },
72
+ });
73
+ const axisConfig = { data: [10, 20, 30] as number[] };
74
+
75
+ it('returns 0 for pixel closest to first data point', () => {
76
+ expect(getDataIndexFromPosition(10, scale, axisConfig, 3)).toBe(0);
77
+ });
78
+
79
+ it('returns 1 for pixel closest to middle data point', () => {
80
+ expect(getDataIndexFromPosition(100, scale, axisConfig, 3)).toBe(1);
81
+ });
82
+
83
+ it('returns 2 for pixel closest to last data point', () => {
84
+ expect(getDataIndexFromPosition(195, scale, axisConfig, 3)).toBe(2);
85
+ });
86
+ });
87
+ });
88
+
89
+ describe('resolvePixelX', () => {
90
+ it('returns undefined when getXScale returns undefined', () => {
91
+ const getXScale = () => undefined;
92
+ expect(resolvePixelX(0, getXScale)).toBeUndefined();
93
+ });
94
+
95
+ it('returns the correct pixel for a numeric scale', () => {
96
+ const scale = getNumericScale({
97
+ scaleType: 'linear',
98
+ domain: { min: 0, max: 4 },
99
+ range: { min: 0, max: 400 },
100
+ });
101
+ const getXScale = () => scale;
102
+ const result = resolvePixelX(2, getXScale);
103
+ expect(result).toBe(scale(2));
104
+ });
105
+
106
+ it('returns the band center for a categorical scale', () => {
107
+ const scale = getCategoricalScale({
108
+ domain: { min: 0, max: 3 },
109
+ range: { min: 0, max: 400 },
110
+ padding: 0,
111
+ });
112
+ const getXScale = () => scale;
113
+ const expected = (scale(1) ?? 0) + scale.bandwidth() / 2;
114
+ expect(resolvePixelX(1, getXScale)).toBe(expected);
115
+ });
116
+
117
+ it('uses numeric axisConfig.data values instead of the index', () => {
118
+ const scale = getNumericScale({
119
+ scaleType: 'linear',
120
+ domain: { min: 10, max: 30 },
121
+ range: { min: 0, max: 200 },
122
+ });
123
+ const getXScale = () => scale;
124
+ const axisConfig = { data: [10, 20, 30] as number[] };
125
+
126
+ const atIndex = resolvePixelX(1, getXScale);
127
+ const atAxisValue = resolvePixelX(1, getXScale, axisConfig);
128
+
129
+ expect(atAxisValue).not.toBe(atIndex);
130
+ expect(atAxisValue).toBe(scale(20));
131
+ });
132
+
133
+ it('falls back to index when axisConfig.data is strings', () => {
134
+ const scale = getNumericScale({
135
+ scaleType: 'linear',
136
+ domain: { min: 0, max: 4 },
137
+ range: { min: 0, max: 400 },
138
+ });
139
+ const getXScale = () => scale;
140
+ const axisConfig = { data: ['a', 'b', 'c'] as string[] };
141
+
142
+ expect(resolvePixelX(2, getXScale, axisConfig)).toBe(
143
+ resolvePixelX(2, getXScale),
144
+ );
145
+ });
146
+ });
147
+
148
+ describe('resolvePixelY', () => {
149
+ const scale = getNumericScale({
150
+ scaleType: 'linear',
151
+ domain: { min: 0, max: 100 },
152
+ range: { min: 200, max: 0 },
153
+ });
154
+ const getYScale = () => scale;
155
+
156
+ it('returns undefined when getYScale returns undefined', () => {
157
+ expect(resolvePixelY(0, [10, 20], () => undefined)).toBeUndefined();
158
+ });
159
+
160
+ it('returns undefined when seriesData is undefined', () => {
161
+ expect(resolvePixelY(0, undefined, getYScale)).toBeUndefined();
162
+ });
163
+
164
+ it('returns undefined for a null data value', () => {
165
+ expect(resolvePixelY(1, [10, null, 30], getYScale)).toBeUndefined();
166
+ });
167
+
168
+ it('returns the correct pixel y for a valid data point', () => {
169
+ const result = resolvePixelY(0, [50], getYScale);
170
+ expect(result).toBe(scale(50));
171
+ });
172
+ });
@@ -0,0 +1,126 @@
1
+ import {
2
+ getPointOnScale,
3
+ isCategoricalScale,
4
+ isNumericScale,
5
+ } from '../../utils/scales/scales';
6
+ import type {
7
+ AxisConfigProps,
8
+ CartesianChartContextValue,
9
+ ChartScaleFunction,
10
+ } from '../../utils/types';
11
+
12
+ const isNumberArray = (arr: string[] | number[]): arr is number[] =>
13
+ typeof arr[0] === 'number';
14
+
15
+ /**
16
+ * Returns the index of the item whose pixel position is closest to `pixelX`.
17
+ * `getPixelPosition` maps each index to its pixel coordinate (or undefined if
18
+ * the value cannot be projected).
19
+ */
20
+ const findClosestIndex = (
21
+ length: number,
22
+ pixelX: number,
23
+ getPixelPosition: (index: number) => number | undefined,
24
+ ): number => {
25
+ let closestIndex = 0;
26
+ let closestDistance = Infinity;
27
+
28
+ for (let i = 0; i < length; i++) {
29
+ const pos = getPixelPosition(i);
30
+ if (pos === undefined) continue;
31
+
32
+ const distance = Math.abs(pixelX - pos);
33
+ if (distance < closestDistance) {
34
+ closestDistance = distance;
35
+ closestIndex = i;
36
+ }
37
+ }
38
+
39
+ return closestIndex;
40
+ };
41
+
42
+ /**
43
+ * Converts a pixel position along the x-axis into the nearest data index.
44
+ *
45
+ * For band (categorical) scales, finds the band whose center is closest to
46
+ * `pixelX`. For numeric scales, uses `scale.invert()` and rounds to the
47
+ * nearest integer, clamped to the valid index range.
48
+ */
49
+ export const getDataIndexFromPosition = (
50
+ pixelX: number,
51
+ scale: ChartScaleFunction,
52
+ axisConfig: Partial<AxisConfigProps> | undefined,
53
+ dataLength: number,
54
+ ): number => {
55
+ if (dataLength <= 0) return 0;
56
+
57
+ if (isCategoricalScale(scale)) {
58
+ const axisData = axisConfig?.data;
59
+ if (axisData && axisData.length > 0 && isNumberArray(axisData)) {
60
+ return findClosestIndex(dataLength, pixelX, (i) =>
61
+ getPointOnScale(axisData[i], scale),
62
+ );
63
+ }
64
+ const step = scale.step();
65
+ const [rangeStart] = scale.range();
66
+ const offset =
67
+ rangeStart + scale.paddingOuter() * step + scale.bandwidth() / 2;
68
+ const index = Math.round((pixelX - offset) / step);
69
+ return Math.max(0, Math.min(index, dataLength - 1));
70
+ }
71
+
72
+ if (isNumericScale(scale)) {
73
+ const axisData = axisConfig?.data;
74
+
75
+ if (axisData && axisData.length > 0 && isNumberArray(axisData)) {
76
+ return findClosestIndex(axisData.length, pixelX, (i) =>
77
+ scale(axisData[i]),
78
+ );
79
+ }
80
+
81
+ const inverted = scale.invert(pixelX);
82
+ return Math.max(
83
+ 0,
84
+ Math.min(Math.round(inverted as number), dataLength - 1),
85
+ );
86
+ }
87
+
88
+ return 0;
89
+ };
90
+
91
+ /**
92
+ * Resolves the pixel y-coordinate for a given series data point at a data index.
93
+ * Returns undefined when the value is null/missing or the scale is unavailable.
94
+ */
95
+ export const resolvePixelY = (
96
+ dataIndex: number,
97
+ seriesData: (number | null)[] | undefined,
98
+ getYScale: CartesianChartContextValue['getYScale'],
99
+ ): number | undefined => {
100
+ const yScale = getYScale();
101
+ if (!yScale || !isNumericScale(yScale)) return undefined;
102
+ if (!seriesData) return undefined;
103
+
104
+ const value = seriesData[dataIndex];
105
+ if (value === null || value === undefined) return undefined;
106
+
107
+ return yScale(value) as number;
108
+ };
109
+
110
+ /**
111
+ * Resolves the pixel x-coordinate for a given data index using the x-scale.
112
+ * When numeric x-axis data is provided, the corresponding axis value is used;
113
+ * otherwise the data index is used as the x input.
114
+ * Returns undefined only when the scale is unavailable.
115
+ */
116
+ export const resolvePixelX = (
117
+ dataIndex: number,
118
+ getXScale: CartesianChartContextValue['getXScale'],
119
+ axisConfig?: AxisConfigProps,
120
+ ): number | undefined => {
121
+ const scale = getXScale();
122
+ if (!scale) return undefined;
123
+ const axisValue = axisConfig?.data?.[dataIndex];
124
+ const xValue = typeof axisValue === 'number' ? axisValue : dataIndex;
125
+ return getPointOnScale(xValue, scale);
126
+ };
@@ -1,2 +1,4 @@
1
1
  export * from './LineChart';
2
2
  export * from './Point';
3
+ export * from './ReferenceLine';
4
+ export * from './Scrubber';
@@ -3,6 +3,7 @@ import { describe, expect, it } from '@jest/globals';
3
3
  import {
4
4
  getCategoricalScale,
5
5
  getNumericScale,
6
+ getPointOnScale,
6
7
  isBandScaleType,
7
8
  isCategoricalScale,
8
9
  isNumericScale,
@@ -133,6 +134,27 @@ describe('isCategoricalScale / isNumericScale', () => {
133
134
  });
134
135
  });
135
136
 
137
+ describe('getPointOnScale', () => {
138
+ it('returns the center of a band for categorical scales', () => {
139
+ const scale = getCategoricalScale({
140
+ domain: { min: 0, max: 3 },
141
+ range: { min: 0, max: 400 },
142
+ padding: 0,
143
+ });
144
+ const expected = (scale(0) ?? 0) + scale.bandwidth() / 2;
145
+ expect(getPointOnScale(0, scale)).toBe(expected);
146
+ });
147
+
148
+ it('returns the direct scale value for numeric scales', () => {
149
+ const scale = getNumericScale({
150
+ scaleType: 'linear',
151
+ domain: { min: 0, max: 4 },
152
+ range: { min: 0, max: 400 },
153
+ });
154
+ expect(getPointOnScale(2, scale)).toBe(scale(2));
155
+ });
156
+ });
157
+
136
158
  describe('projectPoint', () => {
137
159
  it('should project using two linear scales', () => {
138
160
  const xScale = getNumericScale({
@@ -73,6 +73,21 @@ export const isNumericScale = (
73
73
  return !isCategoricalScale(scale);
74
74
  };
75
75
 
76
+ /**
77
+ * Returns the pixel coordinate at the center of the given data index on the scale.
78
+ * For band (categorical) scales, centers on the bandwidth.
79
+ * For numeric scales, calls the scale directly.
80
+ */
81
+ export const getPointOnScale = (
82
+ dataIndex: number,
83
+ scale: ChartScaleFunction,
84
+ ): number => {
85
+ if (isCategoricalScale(scale)) {
86
+ return (scale(dataIndex) ?? 0) + scale.bandwidth() / 2;
87
+ }
88
+ return scale(dataIndex) as number;
89
+ };
90
+
76
91
  /**
77
92
  * Projects a single data-space coordinate pair to pixel-space
78
93
  * using the provided x and y scale functions.
@@ -84,12 +99,7 @@ export const projectPoint = (
84
99
  dataY: number,
85
100
  xScale: ChartScaleFunction,
86
101
  yScale: ChartScaleFunction,
87
- ): { x: number; y: number } => {
88
- const x = isCategoricalScale(xScale)
89
- ? (xScale(dataX) ?? 0) + xScale.bandwidth() / 2
90
- : xScale(dataX);
91
- const y = isCategoricalScale(yScale)
92
- ? (yScale(dataY) ?? 0) + yScale.bandwidth() / 2
93
- : yScale(dataY);
94
- return { x: x, y };
95
- };
102
+ ): { x: number; y: number } => ({
103
+ x: getPointOnScale(dataX, xScale),
104
+ y: getPointOnScale(dataY, yScale),
105
+ });