@ledgerhq/lumen-ui-rnative-visualization 0.1.10 → 0.1.12
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.
- package/dist/module/lib/Components/Axis/XAxis/XAxis.js +1 -1
- package/dist/module/lib/Components/Axis/XAxis/XAxis.js.map +1 -1
- package/dist/module/lib/Components/Axis/YAxis/YAxis.js +1 -1
- package/dist/module/lib/Components/Axis/YAxis/YAxis.js.map +1 -1
- package/dist/module/lib/Components/CartesianChart/CartesianChart.js +31 -28
- package/dist/module/lib/Components/CartesianChart/CartesianChart.js.map +1 -1
- package/dist/module/lib/Components/CartesianChart/RevealClip/RevealClipDefs.js +58 -0
- package/dist/module/lib/Components/CartesianChart/RevealClip/RevealClipDefs.js.map +1 -0
- package/dist/module/lib/Components/CartesianChart/RevealClip/RevealClipDefs.test.js +144 -0
- package/dist/module/lib/Components/CartesianChart/RevealClip/RevealClipDefs.test.js.map +1 -0
- package/dist/module/lib/Components/CartesianChart/RevealClip/context.js +8 -0
- package/dist/module/lib/Components/CartesianChart/RevealClip/context.js.map +1 -0
- package/dist/module/lib/Components/CartesianChart/RevealClip/index.js +5 -0
- package/dist/module/lib/Components/CartesianChart/RevealClip/index.js.map +1 -0
- package/dist/module/lib/Components/CartesianChart/RevealClip/types.js +4 -0
- package/dist/module/lib/Components/CartesianChart/RevealClip/types.js.map +1 -0
- package/dist/module/lib/Components/CartesianChart/RevealClip/utils.js +32 -0
- package/dist/module/lib/Components/CartesianChart/RevealClip/utils.js.map +1 -0
- package/dist/module/lib/Components/CartesianChart/utils.js +4 -4
- package/dist/module/lib/Components/Line/Line.js +5 -2
- package/dist/module/lib/Components/Line/Line.js.map +1 -1
- package/dist/module/lib/Components/LineChart/LineChart.js +7 -1
- package/dist/module/lib/Components/LineChart/LineChart.js.map +1 -1
- package/dist/module/lib/Components/LineChart/LineChart.test.js +25 -0
- package/dist/module/lib/Components/LineChart/LineChart.test.js.map +1 -1
- package/dist/module/lib/Components/Point/Point.js +3 -0
- package/dist/module/lib/Components/Point/Point.js.map +1 -1
- package/dist/module/lib/Components/ReferenceLine/ReferenceLine.js +125 -0
- package/dist/module/lib/Components/ReferenceLine/ReferenceLine.js.map +1 -0
- package/dist/module/lib/Components/ReferenceLine/ReferenceLine.stories.js +111 -0
- package/dist/module/lib/Components/ReferenceLine/ReferenceLine.stories.js.map +1 -0
- package/dist/module/lib/Components/ReferenceLine/ReferenceLine.test.js +338 -0
- package/dist/module/lib/Components/ReferenceLine/ReferenceLine.test.js.map +1 -0
- package/dist/module/lib/Components/ReferenceLine/constants.js +5 -0
- package/dist/module/lib/Components/ReferenceLine/constants.js.map +1 -0
- package/dist/module/lib/Components/ReferenceLine/index.js +5 -0
- package/dist/module/lib/Components/ReferenceLine/index.js.map +1 -0
- package/dist/module/lib/Components/ReferenceLine/types.js +2 -0
- package/dist/module/lib/Components/ReferenceLine/types.js.map +1 -0
- package/dist/module/lib/Components/ReferenceLine/utils.js +121 -0
- package/dist/module/lib/Components/ReferenceLine/utils.js.map +1 -0
- package/dist/module/lib/Components/ReferenceLine/utils.test.js +297 -0
- package/dist/module/lib/Components/ReferenceLine/utils.test.js.map +1 -0
- package/dist/module/lib/Components/Scrubber/Scrubber.js +152 -0
- package/dist/module/lib/Components/Scrubber/Scrubber.js.map +1 -0
- package/dist/module/lib/Components/Scrubber/Scrubber.stories.js +113 -0
- package/dist/module/lib/Components/Scrubber/Scrubber.stories.js.map +1 -0
- package/dist/module/lib/Components/Scrubber/Scrubber.test.js +132 -0
- package/dist/module/lib/Components/Scrubber/Scrubber.test.js.map +1 -0
- package/dist/module/lib/Components/Scrubber/ScrubberProvider.js +105 -0
- package/dist/module/lib/Components/Scrubber/ScrubberProvider.js.map +1 -0
- package/dist/module/lib/Components/Scrubber/ScrubberProvider.test.js +217 -0
- package/dist/module/lib/Components/Scrubber/ScrubberProvider.test.js.map +1 -0
- package/dist/module/lib/Components/Scrubber/constants.js +10 -0
- package/dist/module/lib/Components/Scrubber/constants.js.map +1 -0
- package/dist/module/lib/Components/Scrubber/context/index.js +4 -0
- package/dist/module/lib/Components/Scrubber/context/index.js.map +1 -0
- package/dist/module/lib/Components/Scrubber/context/scrubberContext.js +10 -0
- package/dist/module/lib/Components/Scrubber/context/scrubberContext.js.map +1 -0
- package/dist/module/lib/Components/Scrubber/index.js +5 -0
- package/dist/module/lib/Components/Scrubber/index.js.map +1 -0
- package/dist/module/lib/Components/Scrubber/types.js +4 -0
- package/dist/module/lib/Components/Scrubber/types.js.map +1 -0
- package/dist/module/lib/Components/Scrubber/utils.js +83 -0
- package/dist/module/lib/Components/Scrubber/utils.js.map +1 -0
- package/dist/module/lib/Components/Scrubber/utils.test.js +192 -0
- package/dist/module/lib/Components/Scrubber/utils.test.js.map +1 -0
- package/dist/module/lib/Components/index.js +2 -0
- package/dist/module/lib/Components/index.js.map +1 -1
- package/dist/module/lib/utils/scales/scales.js +16 -8
- package/dist/module/lib/utils/scales/scales.js.map +1 -1
- package/dist/module/lib/utils/scales/scales.test.js +32 -1
- package/dist/module/lib/utils/scales/scales.test.js.map +1 -1
- package/dist/typescript/src/lib/Components/CartesianChart/CartesianChart.d.ts +1 -1
- package/dist/typescript/src/lib/Components/CartesianChart/CartesianChart.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/CartesianChart/RevealClip/RevealClipDefs.d.ts +3 -0
- package/dist/typescript/src/lib/Components/CartesianChart/RevealClip/RevealClipDefs.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/CartesianChart/RevealClip/context.d.ts +7 -0
- package/dist/typescript/src/lib/Components/CartesianChart/RevealClip/context.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/CartesianChart/RevealClip/index.d.ts +3 -0
- package/dist/typescript/src/lib/Components/CartesianChart/RevealClip/index.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/CartesianChart/RevealClip/types.d.ts +38 -0
- package/dist/typescript/src/lib/Components/CartesianChart/RevealClip/types.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/CartesianChart/RevealClip/utils.d.ts +17 -0
- package/dist/typescript/src/lib/Components/CartesianChart/RevealClip/utils.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/CartesianChart/types.d.ts +17 -0
- package/dist/typescript/src/lib/Components/CartesianChart/types.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/Line/Line.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/LineChart/LineChart.d.ts +1 -1
- package/dist/typescript/src/lib/Components/LineChart/LineChart.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/LineChart/types.d.ts +16 -0
- package/dist/typescript/src/lib/Components/LineChart/types.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/Point/Point.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/ReferenceLine/ReferenceLine.d.ts +3 -0
- package/dist/typescript/src/lib/Components/ReferenceLine/ReferenceLine.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/ReferenceLine/constants.d.ts +3 -0
- package/dist/typescript/src/lib/Components/ReferenceLine/constants.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/ReferenceLine/index.d.ts +3 -0
- package/dist/typescript/src/lib/Components/ReferenceLine/index.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/ReferenceLine/types.d.ts +74 -0
- package/dist/typescript/src/lib/Components/ReferenceLine/types.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/ReferenceLine/utils.d.ts +55 -0
- package/dist/typescript/src/lib/Components/ReferenceLine/utils.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/Scrubber/Scrubber.d.ts +17 -0
- package/dist/typescript/src/lib/Components/Scrubber/Scrubber.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/Scrubber/ScrubberProvider.d.ts +16 -0
- package/dist/typescript/src/lib/Components/Scrubber/ScrubberProvider.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/Scrubber/constants.d.ts +8 -0
- package/dist/typescript/src/lib/Components/Scrubber/constants.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/Scrubber/context/index.d.ts +2 -0
- package/dist/typescript/src/lib/Components/Scrubber/context/index.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/Scrubber/context/scrubberContext.d.ts +8 -0
- package/dist/typescript/src/lib/Components/Scrubber/context/scrubberContext.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/Scrubber/index.d.ts +4 -0
- package/dist/typescript/src/lib/Components/Scrubber/index.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/Scrubber/types.d.ts +57 -0
- package/dist/typescript/src/lib/Components/Scrubber/types.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/Scrubber/utils.d.ts +22 -0
- package/dist/typescript/src/lib/Components/Scrubber/utils.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/index.d.ts +2 -0
- package/dist/typescript/src/lib/Components/index.d.ts.map +1 -1
- package/dist/typescript/src/lib/utils/scales/scales.d.ts +6 -0
- package/dist/typescript/src/lib/utils/scales/scales.d.ts.map +1 -1
- package/package.json +6 -3
- package/src/lib/Components/Axis/XAxis/XAxis.tsx +1 -1
- package/src/lib/Components/Axis/YAxis/YAxis.tsx +1 -1
- package/src/lib/Components/CartesianChart/CartesianChart.tsx +32 -32
- package/src/lib/Components/CartesianChart/RevealClip/RevealClipDefs.test.tsx +129 -0
- package/src/lib/Components/CartesianChart/RevealClip/RevealClipDefs.tsx +65 -0
- package/src/lib/Components/CartesianChart/RevealClip/context.ts +13 -0
- package/src/lib/Components/CartesianChart/RevealClip/index.ts +2 -0
- package/src/lib/Components/CartesianChart/RevealClip/types.ts +38 -0
- package/src/lib/Components/CartesianChart/RevealClip/utils.ts +51 -0
- package/src/lib/Components/CartesianChart/types.ts +17 -0
- package/src/lib/Components/CartesianChart/utils.ts +4 -4
- package/src/lib/Components/Line/Line.tsx +5 -3
- package/src/lib/Components/LineChart/LineChart.test.tsx +23 -0
- package/src/lib/Components/LineChart/LineChart.tsx +6 -0
- package/src/lib/Components/LineChart/types.ts +16 -0
- package/src/lib/Components/Point/Point.tsx +3 -1
- package/src/lib/Components/ReferenceLine/ReferenceLine.stories.tsx +109 -0
- package/src/lib/Components/ReferenceLine/ReferenceLine.test.tsx +269 -0
- package/src/lib/Components/ReferenceLine/ReferenceLine.tsx +149 -0
- package/src/lib/Components/ReferenceLine/constants.ts +2 -0
- package/src/lib/Components/ReferenceLine/index.ts +2 -0
- package/src/lib/Components/ReferenceLine/types.ts +79 -0
- package/src/lib/Components/ReferenceLine/utils.test.ts +332 -0
- package/src/lib/Components/ReferenceLine/utils.ts +175 -0
- package/src/lib/Components/Scrubber/Scrubber.stories.tsx +122 -0
- package/src/lib/Components/Scrubber/Scrubber.test.tsx +110 -0
- package/src/lib/Components/Scrubber/Scrubber.tsx +189 -0
- package/src/lib/Components/Scrubber/ScrubberProvider.test.tsx +223 -0
- package/src/lib/Components/Scrubber/ScrubberProvider.tsx +132 -0
- package/src/lib/Components/Scrubber/constants.ts +7 -0
- package/src/lib/Components/Scrubber/context/index.ts +1 -0
- package/src/lib/Components/Scrubber/context/scrubberContext.ts +14 -0
- package/src/lib/Components/Scrubber/index.ts +3 -0
- package/src/lib/Components/Scrubber/types.ts +59 -0
- package/src/lib/Components/Scrubber/utils.test.ts +172 -0
- package/src/lib/Components/Scrubber/utils.ts +126 -0
- package/src/lib/Components/index.ts +2 -0
- package/src/lib/utils/scales/scales.test.ts +22 -0
- package/src/lib/utils/scales/scales.ts +19 -9
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { describe, expect, it } from '@jest/globals';
|
|
2
|
+
|
|
3
|
+
import { getNumericScale } from '../../utils/scales/scales';
|
|
4
|
+
import type { DrawingArea } from '../../utils/types';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
computeHorizontalLabelCoordinates,
|
|
8
|
+
computeVerticalLabelCoordinates,
|
|
9
|
+
dominantBaselineToDy,
|
|
10
|
+
isPixelWithinDrawingArea,
|
|
11
|
+
resolvePixel,
|
|
12
|
+
} from './utils';
|
|
13
|
+
|
|
14
|
+
const drawingArea: DrawingArea = { x: 20, y: 10, width: 360, height: 180 };
|
|
15
|
+
|
|
16
|
+
const linearScale = getNumericScale({
|
|
17
|
+
scaleType: 'linear',
|
|
18
|
+
domain: { min: 0, max: 100 },
|
|
19
|
+
range: { min: drawingArea.x, max: drawingArea.x + drawingArea.width },
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('isPixelWithinDrawingArea', () => {
|
|
23
|
+
describe('x axis', () => {
|
|
24
|
+
it('returns true when pixel is at the left edge', () => {
|
|
25
|
+
expect(isPixelWithinDrawingArea(20, 'x', drawingArea)).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('returns true when pixel is at the right edge', () => {
|
|
29
|
+
expect(isPixelWithinDrawingArea(380, 'x', drawingArea)).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('returns true when pixel is within bounds', () => {
|
|
33
|
+
expect(isPixelWithinDrawingArea(200, 'x', drawingArea)).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('returns false when pixel is before the left edge', () => {
|
|
37
|
+
expect(isPixelWithinDrawingArea(19, 'x', drawingArea)).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('returns false when pixel is past the right edge', () => {
|
|
41
|
+
expect(isPixelWithinDrawingArea(381, 'x', drawingArea)).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('y axis', () => {
|
|
46
|
+
it('returns true when pixel is at the top edge', () => {
|
|
47
|
+
expect(isPixelWithinDrawingArea(10, 'y', drawingArea)).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('returns true when pixel is at the bottom edge', () => {
|
|
51
|
+
expect(isPixelWithinDrawingArea(190, 'y', drawingArea)).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('returns true when pixel is within bounds', () => {
|
|
55
|
+
expect(isPixelWithinDrawingArea(100, 'y', drawingArea)).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('returns false when pixel is above the top edge', () => {
|
|
59
|
+
expect(isPixelWithinDrawingArea(9, 'y', drawingArea)).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('returns false when pixel is below the bottom edge', () => {
|
|
63
|
+
expect(isPixelWithinDrawingArea(191, 'y', drawingArea)).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('computeHorizontalLabelCoordinates', () => {
|
|
69
|
+
const pixel = 100;
|
|
70
|
+
|
|
71
|
+
it('positions label at the end (right edge) with textAnchor start', () => {
|
|
72
|
+
const result = computeHorizontalLabelCoordinates({
|
|
73
|
+
pixel,
|
|
74
|
+
labelPosition: 'end',
|
|
75
|
+
drawingArea,
|
|
76
|
+
});
|
|
77
|
+
expect(result.x).toBe(380);
|
|
78
|
+
expect(result.y).toBe(100);
|
|
79
|
+
expect(result.textAnchor).toBe('start');
|
|
80
|
+
expect(result.dominantBaseline).toBe('hanging');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('positions label at the start (left edge) with textAnchor end', () => {
|
|
84
|
+
const result = computeHorizontalLabelCoordinates({
|
|
85
|
+
pixel,
|
|
86
|
+
labelPosition: 'start',
|
|
87
|
+
drawingArea,
|
|
88
|
+
});
|
|
89
|
+
expect(result.x).toBe(20);
|
|
90
|
+
expect(result.textAnchor).toBe('end');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('positions label at center with textAnchor middle', () => {
|
|
94
|
+
const result = computeHorizontalLabelCoordinates({
|
|
95
|
+
pixel,
|
|
96
|
+
labelPosition: 'center',
|
|
97
|
+
drawingArea,
|
|
98
|
+
});
|
|
99
|
+
expect(result.x).toBe(200);
|
|
100
|
+
expect(result.textAnchor).toBe('middle');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('applies dx offset', () => {
|
|
104
|
+
const result = computeHorizontalLabelCoordinates({
|
|
105
|
+
pixel,
|
|
106
|
+
labelPosition: 'end',
|
|
107
|
+
drawingArea,
|
|
108
|
+
dx: -8,
|
|
109
|
+
});
|
|
110
|
+
expect(result.x).toBe(372);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('applies dy offset', () => {
|
|
114
|
+
const result = computeHorizontalLabelCoordinates({
|
|
115
|
+
pixel,
|
|
116
|
+
labelPosition: 'end',
|
|
117
|
+
drawingArea,
|
|
118
|
+
dy: -4,
|
|
119
|
+
});
|
|
120
|
+
expect(result.y).toBe(96);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('defaults dominantBaseline to hanging (verticalAlignment=end)', () => {
|
|
124
|
+
const result = computeHorizontalLabelCoordinates({
|
|
125
|
+
pixel,
|
|
126
|
+
labelPosition: 'end',
|
|
127
|
+
drawingArea,
|
|
128
|
+
});
|
|
129
|
+
expect(result.dominantBaseline).toBe('hanging');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('overrides dominantBaseline with verticalAlignment=start', () => {
|
|
133
|
+
const result = computeHorizontalLabelCoordinates({
|
|
134
|
+
pixel,
|
|
135
|
+
labelPosition: 'end',
|
|
136
|
+
drawingArea,
|
|
137
|
+
verticalAlignment: 'start',
|
|
138
|
+
});
|
|
139
|
+
expect(result.dominantBaseline).toBe('auto');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('overrides dominantBaseline with verticalAlignment=center', () => {
|
|
143
|
+
const result = computeHorizontalLabelCoordinates({
|
|
144
|
+
pixel,
|
|
145
|
+
labelPosition: 'end',
|
|
146
|
+
drawingArea,
|
|
147
|
+
verticalAlignment: 'center',
|
|
148
|
+
});
|
|
149
|
+
expect(result.dominantBaseline).toBe('central');
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe('computeVerticalLabelCoordinates', () => {
|
|
154
|
+
const pixel = 150;
|
|
155
|
+
|
|
156
|
+
it('positions label at start (top) with dominant-baseline auto', () => {
|
|
157
|
+
const result = computeVerticalLabelCoordinates({
|
|
158
|
+
pixel,
|
|
159
|
+
labelPosition: 'start',
|
|
160
|
+
drawingArea,
|
|
161
|
+
});
|
|
162
|
+
expect(result.x).toBe(150);
|
|
163
|
+
expect(result.y).toBe(10);
|
|
164
|
+
expect(result.textAnchor).toBe('middle');
|
|
165
|
+
expect(result.dominantBaseline).toBe('auto');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('positions label at end (bottom) with dominant-baseline hanging', () => {
|
|
169
|
+
const result = computeVerticalLabelCoordinates({
|
|
170
|
+
pixel,
|
|
171
|
+
labelPosition: 'end',
|
|
172
|
+
drawingArea,
|
|
173
|
+
});
|
|
174
|
+
expect(result.y).toBe(190);
|
|
175
|
+
expect(result.dominantBaseline).toBe('hanging');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('positions label at center with dominant-baseline central', () => {
|
|
179
|
+
const result = computeVerticalLabelCoordinates({
|
|
180
|
+
pixel,
|
|
181
|
+
labelPosition: 'center',
|
|
182
|
+
drawingArea,
|
|
183
|
+
});
|
|
184
|
+
expect(result.y).toBe(100);
|
|
185
|
+
expect(result.dominantBaseline).toBe('central');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('applies dx offset', () => {
|
|
189
|
+
const result = computeVerticalLabelCoordinates({
|
|
190
|
+
pixel,
|
|
191
|
+
labelPosition: 'start',
|
|
192
|
+
drawingArea,
|
|
193
|
+
dx: 4,
|
|
194
|
+
});
|
|
195
|
+
expect(result.x).toBe(154);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('applies dy offset', () => {
|
|
199
|
+
const result = computeVerticalLabelCoordinates({
|
|
200
|
+
pixel,
|
|
201
|
+
labelPosition: 'start',
|
|
202
|
+
drawingArea,
|
|
203
|
+
dy: 6,
|
|
204
|
+
});
|
|
205
|
+
expect(result.y).toBe(16);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('defaults textAnchor to middle (horizontalAlignment=center)', () => {
|
|
209
|
+
const result = computeVerticalLabelCoordinates({
|
|
210
|
+
pixel,
|
|
211
|
+
labelPosition: 'start',
|
|
212
|
+
drawingArea,
|
|
213
|
+
});
|
|
214
|
+
expect(result.textAnchor).toBe('middle');
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('overrides textAnchor with horizontalAlignment=start', () => {
|
|
218
|
+
const result = computeVerticalLabelCoordinates({
|
|
219
|
+
pixel,
|
|
220
|
+
labelPosition: 'start',
|
|
221
|
+
drawingArea,
|
|
222
|
+
horizontalAlignment: 'start',
|
|
223
|
+
});
|
|
224
|
+
expect(result.textAnchor).toBe('end');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('overrides textAnchor with horizontalAlignment=end', () => {
|
|
228
|
+
const result = computeVerticalLabelCoordinates({
|
|
229
|
+
pixel,
|
|
230
|
+
labelPosition: 'start',
|
|
231
|
+
drawingArea,
|
|
232
|
+
horizontalAlignment: 'end',
|
|
233
|
+
});
|
|
234
|
+
expect(result.textAnchor).toBe('start');
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe('resolvePixel', () => {
|
|
239
|
+
it('returns a pixel value for a data value within bounds', () => {
|
|
240
|
+
const result = resolvePixel({
|
|
241
|
+
dataValue: 50,
|
|
242
|
+
scale: linearScale,
|
|
243
|
+
axis: 'x',
|
|
244
|
+
drawingArea,
|
|
245
|
+
});
|
|
246
|
+
expect(typeof result).toBe('number');
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('returns undefined when scale is missing', () => {
|
|
250
|
+
expect(
|
|
251
|
+
resolvePixel({
|
|
252
|
+
dataValue: 50,
|
|
253
|
+
scale: undefined,
|
|
254
|
+
axis: 'x',
|
|
255
|
+
drawingArea,
|
|
256
|
+
}),
|
|
257
|
+
).toBeUndefined();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('returns undefined when pixel falls outside drawing area', () => {
|
|
261
|
+
expect(
|
|
262
|
+
resolvePixel({
|
|
263
|
+
dataValue: 200,
|
|
264
|
+
scale: linearScale,
|
|
265
|
+
axis: 'x',
|
|
266
|
+
drawingArea,
|
|
267
|
+
}),
|
|
268
|
+
).toBeUndefined();
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('resolves data index through numeric axisConfig.data', () => {
|
|
272
|
+
const atIndex = resolvePixel({
|
|
273
|
+
dataValue: 50,
|
|
274
|
+
scale: linearScale,
|
|
275
|
+
axis: 'x',
|
|
276
|
+
drawingArea,
|
|
277
|
+
});
|
|
278
|
+
const throughData = resolvePixel({
|
|
279
|
+
dataValue: 1,
|
|
280
|
+
scale: linearScale,
|
|
281
|
+
axis: 'x',
|
|
282
|
+
drawingArea,
|
|
283
|
+
axisConfig: { data: [0, 50, 100] },
|
|
284
|
+
});
|
|
285
|
+
expect(throughData).toBe(atIndex);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('uses index directly when axisConfig.data contains strings', () => {
|
|
289
|
+
const withoutConfig = resolvePixel({
|
|
290
|
+
dataValue: 2,
|
|
291
|
+
scale: linearScale,
|
|
292
|
+
axis: 'x',
|
|
293
|
+
drawingArea,
|
|
294
|
+
});
|
|
295
|
+
const withStringData = resolvePixel({
|
|
296
|
+
dataValue: 2,
|
|
297
|
+
scale: linearScale,
|
|
298
|
+
axis: 'x',
|
|
299
|
+
drawingArea,
|
|
300
|
+
axisConfig: { data: ['a', 'b', 'c'] },
|
|
301
|
+
});
|
|
302
|
+
expect(withStringData).toBe(withoutConfig);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('returns undefined when data index is out of bounds', () => {
|
|
306
|
+
expect(
|
|
307
|
+
resolvePixel({
|
|
308
|
+
dataValue: 10,
|
|
309
|
+
scale: linearScale,
|
|
310
|
+
axis: 'x',
|
|
311
|
+
drawingArea,
|
|
312
|
+
axisConfig: { data: [1, 2, 3] },
|
|
313
|
+
}),
|
|
314
|
+
).toBeUndefined();
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
describe('dominantBaselineToDy', () => {
|
|
319
|
+
const fontSize = 10;
|
|
320
|
+
|
|
321
|
+
it('returns 0 for auto (text above anchor)', () => {
|
|
322
|
+
expect(dominantBaselineToDy('auto', fontSize)).toBe(0);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('returns fontSize * 0.35 for central (vertically centered)', () => {
|
|
326
|
+
expect(dominantBaselineToDy('central', fontSize)).toBe(3.5);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('returns fontSize * 0.8 for hanging (text below anchor)', () => {
|
|
330
|
+
expect(dominantBaselineToDy('hanging', fontSize)).toBe(8);
|
|
331
|
+
});
|
|
332
|
+
});
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { getPointOnScale, isCategoricalScale } from '../../utils/scales/scales';
|
|
2
|
+
import type {
|
|
3
|
+
AxisConfigProps,
|
|
4
|
+
ChartScaleFunction,
|
|
5
|
+
DrawingArea,
|
|
6
|
+
} from '../../utils/types';
|
|
7
|
+
|
|
8
|
+
import type { LabelAlignment, LabelPosition } from './types';
|
|
9
|
+
|
|
10
|
+
const DEFAULT_H_ALIGN: LabelAlignment = 'center';
|
|
11
|
+
const DEFAULT_V_ALIGN: LabelAlignment = 'end';
|
|
12
|
+
|
|
13
|
+
type LabelCoordinates = {
|
|
14
|
+
x: number;
|
|
15
|
+
y: number;
|
|
16
|
+
textAnchor: 'start' | 'middle' | 'end';
|
|
17
|
+
dominantBaseline: 'hanging' | 'central' | 'auto';
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const DY_RATIO: Record<LabelCoordinates['dominantBaseline'], number> = {
|
|
21
|
+
auto: 0,
|
|
22
|
+
central: 0.35,
|
|
23
|
+
hanging: 0.8,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type ResolvePixelParams = {
|
|
27
|
+
dataValue: number;
|
|
28
|
+
scale: ChartScaleFunction | undefined;
|
|
29
|
+
axis: 'x' | 'y';
|
|
30
|
+
drawingArea: DrawingArea;
|
|
31
|
+
axisConfig?: AxisConfigProps;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type LabelParams = {
|
|
35
|
+
pixel: number;
|
|
36
|
+
labelPosition: LabelPosition;
|
|
37
|
+
drawingArea: DrawingArea;
|
|
38
|
+
dx?: number;
|
|
39
|
+
dy?: number;
|
|
40
|
+
horizontalAlignment?: LabelAlignment;
|
|
41
|
+
verticalAlignment?: LabelAlignment;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const textAnchorMap: Record<LabelAlignment, LabelCoordinates['textAnchor']> = {
|
|
45
|
+
start: 'end',
|
|
46
|
+
center: 'middle',
|
|
47
|
+
end: 'start',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const dominantBaselineMap: Record<
|
|
51
|
+
LabelAlignment,
|
|
52
|
+
LabelCoordinates['dominantBaseline']
|
|
53
|
+
> = {
|
|
54
|
+
start: 'auto',
|
|
55
|
+
center: 'central',
|
|
56
|
+
end: 'hanging',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const positionRatio: Record<LabelPosition, number> = {
|
|
60
|
+
start: 0,
|
|
61
|
+
center: 0.5,
|
|
62
|
+
end: 1,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Converts a `dominantBaseline` value to a `dy` pixel offset.
|
|
67
|
+
* react-native-svg doesn't support `dominantBaseline`, so we emulate it
|
|
68
|
+
* with a font-relative `dy` shift — same approach used by Axis components.
|
|
69
|
+
*/
|
|
70
|
+
export const dominantBaselineToDy = (
|
|
71
|
+
baseline: LabelCoordinates['dominantBaseline'],
|
|
72
|
+
fontSize: number,
|
|
73
|
+
): number => fontSize * DY_RATIO[baseline];
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Checks whether a pixel coordinate falls within the drawing area
|
|
77
|
+
* along a single axis.
|
|
78
|
+
*/
|
|
79
|
+
export const isPixelWithinDrawingArea = (
|
|
80
|
+
pixel: number,
|
|
81
|
+
axis: 'x' | 'y',
|
|
82
|
+
drawingArea: DrawingArea,
|
|
83
|
+
): boolean => {
|
|
84
|
+
if (axis === 'x') {
|
|
85
|
+
return pixel >= drawingArea.x && pixel <= drawingArea.x + drawingArea.width;
|
|
86
|
+
}
|
|
87
|
+
return pixel >= drawingArea.y && pixel <= drawingArea.y + drawingArea.height;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Translates a data index into the value the axis scale expects.
|
|
92
|
+
* When the axis config contains numeric `data`, the index is mapped to the
|
|
93
|
+
* corresponding value — returns `undefined` if the index is out of bounds.
|
|
94
|
+
* When there is no numeric axis data the index is used directly.
|
|
95
|
+
*/
|
|
96
|
+
const resolveDataValue = (
|
|
97
|
+
index: number,
|
|
98
|
+
axisConfig?: AxisConfigProps,
|
|
99
|
+
): number | undefined => {
|
|
100
|
+
const data = axisConfig?.data;
|
|
101
|
+
if (!data || data.length === 0) return index;
|
|
102
|
+
|
|
103
|
+
if (index < 0 || index >= data.length) return undefined;
|
|
104
|
+
|
|
105
|
+
const axisValue = data[index];
|
|
106
|
+
return typeof axisValue === 'number' ? axisValue : index;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Resolves a data-space value to a validated pixel coordinate.
|
|
111
|
+
* When `axisConfig` is provided, the data value is first translated
|
|
112
|
+
* through the axis data array before being projected onto the scale.
|
|
113
|
+
* Returns `undefined` if the value is out of bounds, the scale is missing,
|
|
114
|
+
* the result is not finite, or the pixel falls outside the drawing area.
|
|
115
|
+
*/
|
|
116
|
+
export const resolvePixel = ({
|
|
117
|
+
dataValue,
|
|
118
|
+
scale,
|
|
119
|
+
axis,
|
|
120
|
+
drawingArea,
|
|
121
|
+
axisConfig,
|
|
122
|
+
}: ResolvePixelParams): number | undefined => {
|
|
123
|
+
const resolved = resolveDataValue(dataValue, axisConfig);
|
|
124
|
+
if (resolved === undefined) return undefined;
|
|
125
|
+
|
|
126
|
+
if (!scale) return undefined;
|
|
127
|
+
|
|
128
|
+
if (isCategoricalScale(scale) && scale(resolved) === undefined)
|
|
129
|
+
return undefined;
|
|
130
|
+
|
|
131
|
+
const p = getPointOnScale(resolved, scale);
|
|
132
|
+
|
|
133
|
+
if (!Number.isFinite(p) || !isPixelWithinDrawingArea(p, axis, drawingArea)) {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
return p;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Computes the label pixel coordinates and text alignment attributes
|
|
141
|
+
* for a horizontal reference line.
|
|
142
|
+
*/
|
|
143
|
+
export const computeHorizontalLabelCoordinates = ({
|
|
144
|
+
pixel,
|
|
145
|
+
labelPosition,
|
|
146
|
+
drawingArea,
|
|
147
|
+
dx = 0,
|
|
148
|
+
dy = 0,
|
|
149
|
+
horizontalAlignment,
|
|
150
|
+
verticalAlignment,
|
|
151
|
+
}: LabelParams): LabelCoordinates => ({
|
|
152
|
+
x: drawingArea.x + drawingArea.width * positionRatio[labelPosition] + dx,
|
|
153
|
+
y: pixel + dy,
|
|
154
|
+
textAnchor: textAnchorMap[horizontalAlignment ?? labelPosition],
|
|
155
|
+
dominantBaseline: dominantBaselineMap[verticalAlignment ?? DEFAULT_V_ALIGN],
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Computes the label pixel coordinates and text alignment attributes
|
|
160
|
+
* for a vertical reference line.
|
|
161
|
+
*/
|
|
162
|
+
export const computeVerticalLabelCoordinates = ({
|
|
163
|
+
pixel,
|
|
164
|
+
labelPosition,
|
|
165
|
+
drawingArea,
|
|
166
|
+
dx = 0,
|
|
167
|
+
dy = 0,
|
|
168
|
+
horizontalAlignment,
|
|
169
|
+
verticalAlignment,
|
|
170
|
+
}: LabelParams): LabelCoordinates => ({
|
|
171
|
+
x: pixel + dx,
|
|
172
|
+
y: drawingArea.y + drawingArea.height * positionRatio[labelPosition] + dy,
|
|
173
|
+
textAnchor: textAnchorMap[horizontalAlignment ?? DEFAULT_H_ALIGN],
|
|
174
|
+
dominantBaseline: dominantBaselineMap[verticalAlignment ?? labelPosition],
|
|
175
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-native-web-vite';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Text, View } from 'react-native';
|
|
4
|
+
|
|
5
|
+
import { StoryDecorator } from '../../../../.storybook/StoryDecorator.tsx';
|
|
6
|
+
import { LineChart } from '../LineChart';
|
|
7
|
+
import { Scrubber } from './Scrubber';
|
|
8
|
+
import type { ScrubberProps } from './types';
|
|
9
|
+
|
|
10
|
+
const meta = {
|
|
11
|
+
component: Scrubber,
|
|
12
|
+
title: 'Visualization/Scrubber',
|
|
13
|
+
tags: ['experimental'],
|
|
14
|
+
decorators: [
|
|
15
|
+
(Story, context) => (
|
|
16
|
+
<StoryDecorator context={context}>
|
|
17
|
+
<Story />
|
|
18
|
+
</StoryDecorator>
|
|
19
|
+
),
|
|
20
|
+
],
|
|
21
|
+
} satisfies Meta<typeof Scrubber>;
|
|
22
|
+
|
|
23
|
+
export default meta;
|
|
24
|
+
type Story = StoryObj<typeof Scrubber>;
|
|
25
|
+
|
|
26
|
+
const dates = [
|
|
27
|
+
'Jan',
|
|
28
|
+
'Feb',
|
|
29
|
+
'Mar',
|
|
30
|
+
'Apr',
|
|
31
|
+
'May',
|
|
32
|
+
'Jun',
|
|
33
|
+
'Jul',
|
|
34
|
+
'Aug',
|
|
35
|
+
'Sep',
|
|
36
|
+
'Oct',
|
|
37
|
+
'Nov',
|
|
38
|
+
'Dec',
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const singleSeries = [
|
|
42
|
+
{
|
|
43
|
+
id: 'prices',
|
|
44
|
+
stroke: '#7B61FF',
|
|
45
|
+
data: [10, 22, 29, 45, 98, 45, 22, 52, 21, 4, 68, 20],
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
const multiSeries = [
|
|
50
|
+
{
|
|
51
|
+
id: 'lineA',
|
|
52
|
+
label: 'Line A',
|
|
53
|
+
stroke: '#7B61FF',
|
|
54
|
+
data: [5, 15, 10, 90, 85, 70, 30, 25, 25, 40, 60, 80],
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: 'lineB',
|
|
58
|
+
label: 'Line B',
|
|
59
|
+
stroke: '#44D7B6',
|
|
60
|
+
data: [90, 85, 70, 25, 23, 40, 45, 40, 50, 30, 20, 10],
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
export const Base: Story = {
|
|
65
|
+
render: (args: ScrubberProps) => (
|
|
66
|
+
<LineChart series={singleSeries} height={200} showArea enableScrubbing>
|
|
67
|
+
<Scrubber {...args} />
|
|
68
|
+
</LineChart>
|
|
69
|
+
),
|
|
70
|
+
args: {},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const WithLabel: Story = {
|
|
74
|
+
render: (args: ScrubberProps) => {
|
|
75
|
+
const [activeIndex, setActiveIndex] = useState<number | undefined>();
|
|
76
|
+
const displayIndex = activeIndex ?? singleSeries[0].data.length - 1;
|
|
77
|
+
return (
|
|
78
|
+
<View>
|
|
79
|
+
<Text style={{ marginBottom: 8, fontSize: 14 }}>
|
|
80
|
+
{dates[displayIndex]} — ${singleSeries[0].data[displayIndex]}
|
|
81
|
+
</Text>
|
|
82
|
+
<LineChart
|
|
83
|
+
series={singleSeries}
|
|
84
|
+
height={200}
|
|
85
|
+
showArea
|
|
86
|
+
enableScrubbing
|
|
87
|
+
onScrubberPositionChange={setActiveIndex}
|
|
88
|
+
>
|
|
89
|
+
<Scrubber {...args} label={(i: number) => dates[i] ?? ''} />
|
|
90
|
+
</LineChart>
|
|
91
|
+
</View>
|
|
92
|
+
);
|
|
93
|
+
},
|
|
94
|
+
args: {},
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export const MultiSeries: Story = {
|
|
98
|
+
render: (args: ScrubberProps) => (
|
|
99
|
+
<LineChart series={multiSeries} height={200} enableScrubbing>
|
|
100
|
+
<Scrubber {...args} label={(i: number) => dates[i] ?? ''} showBeacons />
|
|
101
|
+
</LineChart>
|
|
102
|
+
),
|
|
103
|
+
args: {},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const HiddenLine: Story = {
|
|
107
|
+
render: (args: ScrubberProps) => (
|
|
108
|
+
<LineChart series={singleSeries} height={200} showArea enableScrubbing>
|
|
109
|
+
<Scrubber {...args} hideLine />
|
|
110
|
+
</LineChart>
|
|
111
|
+
),
|
|
112
|
+
args: {},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export const HiddenOverlay: Story = {
|
|
116
|
+
render: (args: ScrubberProps) => (
|
|
117
|
+
<LineChart series={singleSeries} height={200} showArea enableScrubbing>
|
|
118
|
+
<Scrubber {...args} hideOverlay />
|
|
119
|
+
</LineChart>
|
|
120
|
+
),
|
|
121
|
+
args: {},
|
|
122
|
+
};
|