@ledgerhq/lumen-ui-rnative-visualization 0.1.2 → 0.1.4
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/Axis.types.js +2 -0
- package/dist/module/lib/Components/Axis/Axis.types.js.map +1 -0
- package/dist/module/lib/Components/Axis/XAxis/XAxis.js +81 -0
- package/dist/module/lib/Components/Axis/XAxis/XAxis.js.map +1 -0
- package/dist/module/lib/Components/Axis/XAxis/XAxis.test.js +121 -0
- package/dist/module/lib/Components/Axis/XAxis/XAxis.test.js.map +1 -0
- package/dist/module/lib/Components/Axis/XAxis/index.js +4 -0
- package/dist/module/lib/Components/Axis/XAxis/index.js.map +1 -0
- package/dist/module/lib/Components/Axis/XAxis/types.js +4 -0
- package/dist/module/lib/Components/Axis/XAxis/types.js.map +1 -0
- package/dist/module/lib/Components/Axis/YAxis/YAxis.js +81 -0
- package/dist/module/lib/Components/Axis/YAxis/YAxis.js.map +1 -0
- package/dist/module/lib/Components/Axis/YAxis/YAxis.test.js +121 -0
- package/dist/module/lib/Components/Axis/YAxis/YAxis.test.js.map +1 -0
- package/dist/module/lib/Components/Axis/YAxis/index.js +4 -0
- package/dist/module/lib/Components/Axis/YAxis/index.js.map +1 -0
- package/dist/module/lib/Components/Axis/YAxis/types.js +4 -0
- package/dist/module/lib/Components/Axis/YAxis/types.js.map +1 -0
- package/dist/module/lib/Components/CartesianChart/CartesianChart.js +123 -0
- package/dist/module/lib/Components/CartesianChart/CartesianChart.js.map +1 -0
- package/dist/module/lib/Components/CartesianChart/context/cartesianChartContext.js +10 -0
- package/dist/module/lib/Components/CartesianChart/context/cartesianChartContext.js.map +1 -0
- package/dist/module/lib/Components/CartesianChart/context/index.js +5 -0
- package/dist/module/lib/Components/CartesianChart/context/index.js.map +1 -0
- package/dist/module/lib/Components/CartesianChart/context/useBuildChartContext.js +75 -0
- package/dist/module/lib/Components/CartesianChart/context/useBuildChartContext.js.map +1 -0
- package/dist/module/lib/Components/CartesianChart/context/useBuildChartContext.test.js +279 -0
- package/dist/module/lib/Components/CartesianChart/context/useBuildChartContext.test.js.map +1 -0
- package/dist/module/lib/Components/CartesianChart/index.js +5 -0
- package/dist/module/lib/Components/CartesianChart/index.js.map +1 -0
- package/dist/module/lib/Components/CartesianChart/types.js +4 -0
- package/dist/module/lib/Components/CartesianChart/types.js.map +1 -0
- package/dist/module/lib/Components/Line/Line.js +72 -0
- package/dist/module/lib/Components/Line/Line.js.map +1 -0
- package/dist/module/lib/Components/Line/index.js +4 -0
- package/dist/module/lib/Components/Line/index.js.map +1 -0
- package/dist/module/lib/Components/Line/types.js +2 -0
- package/dist/module/lib/Components/Line/types.js.map +1 -0
- package/dist/module/lib/Components/Line/utils.js +38 -0
- package/dist/module/lib/Components/Line/utils.js.map +1 -0
- package/dist/module/lib/Components/LineChart/LineChart.js +70 -31
- package/dist/module/lib/Components/LineChart/LineChart.js.map +1 -1
- package/dist/module/lib/Components/LineChart/LineChart.stories.js +271 -2
- package/dist/module/lib/Components/LineChart/LineChart.stories.js.map +1 -1
- package/dist/module/lib/Components/LineChart/LineChart.test.js +84 -3
- package/dist/module/lib/Components/LineChart/LineChart.test.js.map +1 -1
- package/dist/module/lib/Components/LineChart/types.js +4 -0
- package/dist/module/lib/Components/LineChart/types.js.map +1 -0
- package/dist/module/lib/utils/domain/domain.js +93 -0
- package/dist/module/lib/utils/domain/domain.js.map +1 -0
- package/dist/module/lib/utils/domain/domain.test.js +152 -0
- package/dist/module/lib/utils/domain/domain.test.js.map +1 -0
- package/dist/module/lib/utils/index.js +0 -2
- package/dist/module/lib/utils/index.js.map +1 -1
- package/dist/module/lib/utils/scales/scales.js +45 -0
- package/dist/module/lib/utils/scales/scales.js.map +1 -0
- package/dist/module/lib/utils/scales/scales.test.js +173 -0
- package/dist/module/lib/utils/scales/scales.test.js.map +1 -0
- package/dist/module/lib/utils/ticks/ticks.js +71 -0
- package/dist/module/lib/utils/ticks/ticks.js.map +1 -0
- package/dist/module/lib/utils/ticks/ticks.test.js +121 -0
- package/dist/module/lib/utils/ticks/ticks.test.js.map +1 -0
- package/dist/module/lib/utils/types.js +2 -0
- package/dist/typescript/src/lib/Components/Axis/Axis.types.d.ts +33 -0
- package/dist/typescript/src/lib/Components/Axis/Axis.types.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/Axis/XAxis/XAxis.d.ts +4 -0
- package/dist/typescript/src/lib/Components/Axis/XAxis/XAxis.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/Axis/XAxis/index.d.ts +3 -0
- package/dist/typescript/src/lib/Components/Axis/XAxis/index.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/Axis/XAxis/types.d.ts +9 -0
- package/dist/typescript/src/lib/Components/Axis/XAxis/types.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/Axis/YAxis/YAxis.d.ts +4 -0
- package/dist/typescript/src/lib/Components/Axis/YAxis/YAxis.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/Axis/YAxis/index.d.ts +3 -0
- package/dist/typescript/src/lib/Components/Axis/YAxis/index.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/Axis/YAxis/types.d.ts +14 -0
- package/dist/typescript/src/lib/Components/Axis/YAxis/types.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/CartesianChart/CartesianChart.d.ts +3 -0
- package/dist/typescript/src/lib/Components/CartesianChart/CartesianChart.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/CartesianChart/context/cartesianChartContext.d.ts +8 -0
- package/dist/typescript/src/lib/Components/CartesianChart/context/cartesianChartContext.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/CartesianChart/context/index.d.ts +3 -0
- package/dist/typescript/src/lib/Components/CartesianChart/context/index.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/CartesianChart/context/useBuildChartContext.d.ts +23 -0
- package/dist/typescript/src/lib/Components/CartesianChart/context/useBuildChartContext.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/CartesianChart/index.d.ts +4 -0
- package/dist/typescript/src/lib/Components/CartesianChart/index.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/CartesianChart/types.d.ts +46 -0
- package/dist/typescript/src/lib/Components/CartesianChart/types.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/Line/Line.d.ts +3 -0
- package/dist/typescript/src/lib/Components/Line/Line.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/Line/index.d.ts +3 -0
- package/dist/typescript/src/lib/Components/Line/index.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/Line/types.d.ts +24 -0
- package/dist/typescript/src/lib/Components/Line/types.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/Line/utils.d.ts +19 -0
- package/dist/typescript/src/lib/Components/Line/utils.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/LineChart/LineChart.d.ts +2 -8
- package/dist/typescript/src/lib/Components/LineChart/LineChart.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/LineChart/index.d.ts +1 -0
- package/dist/typescript/src/lib/Components/LineChart/index.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/LineChart/types.d.ts +62 -0
- package/dist/typescript/src/lib/Components/LineChart/types.d.ts.map +1 -0
- package/dist/typescript/src/lib/utils/domain/domain.d.ts +17 -0
- package/dist/typescript/src/lib/utils/domain/domain.d.ts.map +1 -0
- package/dist/typescript/src/lib/utils/index.d.ts +1 -1
- package/dist/typescript/src/lib/utils/index.d.ts.map +1 -1
- package/dist/typescript/src/lib/utils/scales/scales.d.ts +28 -0
- package/dist/typescript/src/lib/utils/scales/scales.d.ts.map +1 -0
- package/dist/typescript/src/lib/utils/ticks/ticks.d.ts +37 -0
- package/dist/typescript/src/lib/utils/ticks/ticks.d.ts.map +1 -0
- package/dist/typescript/src/lib/utils/types.d.ts +93 -6
- package/dist/typescript/src/lib/utils/types.d.ts.map +1 -1
- package/package.json +11 -1
- package/src/lib/Components/Axis/Axis.types.ts +32 -0
- package/src/lib/Components/Axis/XAxis/XAxis.test.tsx +89 -0
- package/src/lib/Components/Axis/XAxis/XAxis.tsx +116 -0
- package/src/lib/Components/Axis/XAxis/index.ts +2 -0
- package/src/lib/Components/Axis/XAxis/types.ts +9 -0
- package/src/lib/Components/Axis/YAxis/YAxis.test.tsx +89 -0
- package/src/lib/Components/Axis/YAxis/YAxis.tsx +115 -0
- package/src/lib/Components/Axis/YAxis/index.ts +2 -0
- package/src/lib/Components/Axis/YAxis/types.ts +14 -0
- package/src/lib/Components/CartesianChart/CartesianChart.tsx +153 -0
- package/src/lib/Components/CartesianChart/context/cartesianChartContext.ts +14 -0
- package/src/lib/Components/CartesianChart/context/index.ts +10 -0
- package/src/lib/Components/CartesianChart/context/useBuildChartContext.test.ts +224 -0
- package/src/lib/Components/CartesianChart/context/useBuildChartContext.ts +115 -0
- package/src/lib/Components/CartesianChart/index.ts +3 -0
- package/src/lib/Components/CartesianChart/types.ts +47 -0
- package/src/lib/Components/Line/Line.tsx +88 -0
- package/src/lib/Components/Line/index.ts +2 -0
- package/src/lib/Components/Line/types.ts +23 -0
- package/src/lib/Components/Line/utils.ts +72 -0
- package/src/lib/Components/LineChart/LineChart.stories.tsx +287 -4
- package/src/lib/Components/LineChart/LineChart.test.tsx +71 -2
- package/src/lib/Components/LineChart/LineChart.tsx +90 -34
- package/src/lib/Components/LineChart/index.ts +1 -0
- package/src/lib/Components/LineChart/types.ts +63 -0
- package/src/lib/utils/domain/domain.test.ts +129 -0
- package/src/lib/utils/domain/domain.ts +101 -0
- package/src/lib/utils/index.ts +11 -1
- package/src/lib/utils/scales/scales.test.ts +133 -0
- package/src/lib/utils/scales/scales.ts +74 -0
- package/src/lib/utils/ticks/ticks.test.ts +136 -0
- package/src/lib/utils/ticks/ticks.ts +108 -0
- package/src/lib/utils/types.ts +100 -6
- package/dist/module/lib/utils/math/index.js +0 -4
- package/dist/module/lib/utils/math/index.js.map +0 -1
- package/dist/typescript/src/lib/utils/math/index.d.ts +0 -1
- package/dist/typescript/src/lib/utils/math/index.d.ts.map +0 -1
- package/src/lib/utils/math/index.ts +0 -2
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { describe, expect, it } from '@jest/globals';
|
|
2
|
+
import { renderHook } from '@testing-library/react-native';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
isCategoricalScale,
|
|
6
|
+
isNumericScale,
|
|
7
|
+
} from '../../../utils/scales/scales';
|
|
8
|
+
import type {
|
|
9
|
+
ChartInset,
|
|
10
|
+
ChartScaleFunction,
|
|
11
|
+
Series,
|
|
12
|
+
} from '../../../utils/types';
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
buildScale,
|
|
16
|
+
computeAxisRange,
|
|
17
|
+
computeDrawingArea,
|
|
18
|
+
useBuildChartContext,
|
|
19
|
+
} from './useBuildChartContext';
|
|
20
|
+
|
|
21
|
+
const makeSeries = (data: (number | null)[]): Series => ({
|
|
22
|
+
id: 'test',
|
|
23
|
+
stroke: '#000',
|
|
24
|
+
data,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const DEFAULT_INSET: ChartInset = { top: 0, right: 0, bottom: 0, left: 0 };
|
|
28
|
+
const ZERO_PADDING: ChartInset = { top: 0, right: 0, bottom: 0, left: 0 };
|
|
29
|
+
|
|
30
|
+
const hookParams = (
|
|
31
|
+
overrides: Partial<Parameters<typeof useBuildChartContext>[0]> = {},
|
|
32
|
+
) => ({
|
|
33
|
+
series: [makeSeries([10, 20, 30])],
|
|
34
|
+
width: 400,
|
|
35
|
+
height: 200,
|
|
36
|
+
inset: DEFAULT_INSET,
|
|
37
|
+
axisPadding: ZERO_PADDING,
|
|
38
|
+
...overrides,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('computeAxisRange', () => {
|
|
42
|
+
it('computes x range from drawing area', () => {
|
|
43
|
+
const { xRange } = computeAxisRange({
|
|
44
|
+
x: 10,
|
|
45
|
+
y: 0,
|
|
46
|
+
width: 100,
|
|
47
|
+
height: 50,
|
|
48
|
+
});
|
|
49
|
+
expect(xRange).toEqual({ min: 10, max: 110 });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('inverts y range for SVG coordinates', () => {
|
|
53
|
+
const { yRange } = computeAxisRange({
|
|
54
|
+
x: 0,
|
|
55
|
+
y: 10,
|
|
56
|
+
width: 100,
|
|
57
|
+
height: 200,
|
|
58
|
+
});
|
|
59
|
+
expect(yRange).toEqual({ min: 210, max: 10 });
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('buildScale', () => {
|
|
64
|
+
const range = { min: 0, max: 400 };
|
|
65
|
+
|
|
66
|
+
it('builds a numeric linear scale by default', () => {
|
|
67
|
+
const scale = buildScale({ min: 0, max: 100 }, range);
|
|
68
|
+
expect(isNumericScale(scale)).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('builds a numeric log scale', () => {
|
|
72
|
+
const scale = buildScale({ min: 1, max: 100 }, range, 'log');
|
|
73
|
+
expect(isNumericScale(scale)).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('builds a categorical band scale', () => {
|
|
77
|
+
const scale = buildScale({ min: 0, max: 2 }, range, 'band');
|
|
78
|
+
expect(isCategoricalScale(scale)).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('computeDrawingArea', () => {
|
|
83
|
+
it('subtracts inset from available space', () => {
|
|
84
|
+
const inset: ChartInset = { top: 10, right: 20, bottom: 30, left: 40 };
|
|
85
|
+
const area = computeDrawingArea(400, 200, inset, ZERO_PADDING);
|
|
86
|
+
expect(area).toEqual({ x: 40, y: 10, width: 340, height: 160 });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('combines inset and axis padding', () => {
|
|
90
|
+
const inset: ChartInset = { top: 5, right: 5, bottom: 5, left: 5 };
|
|
91
|
+
const axisPad: ChartInset = { top: 0, right: 0, bottom: 28, left: 40 };
|
|
92
|
+
const area = computeDrawingArea(400, 200, inset, axisPad);
|
|
93
|
+
expect(area).toEqual({ x: 45, y: 5, width: 350, height: 162 });
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('clamps negative dimensions to zero', () => {
|
|
97
|
+
const huge: ChartInset = { top: 100, right: 100, bottom: 100, left: 100 };
|
|
98
|
+
const area = computeDrawingArea(50, 50, huge, ZERO_PADDING);
|
|
99
|
+
expect(area.width).toBe(0);
|
|
100
|
+
expect(area.height).toBe(0);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// Hook integration tests
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
describe('useBuildChartContext', () => {
|
|
109
|
+
it('returns undefined scales when drawing area is zero', () => {
|
|
110
|
+
const { result } = renderHook(() =>
|
|
111
|
+
useBuildChartContext(hookParams({ width: 0, height: 0 })),
|
|
112
|
+
);
|
|
113
|
+
expect(result.current.getXScale()).toBeUndefined();
|
|
114
|
+
expect(result.current.getYScale()).toBeUndefined();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('creates linear scales by default', () => {
|
|
118
|
+
const { result } = renderHook(() => useBuildChartContext(hookParams()));
|
|
119
|
+
const xScale = result.current.getXScale();
|
|
120
|
+
const yScale = result.current.getYScale();
|
|
121
|
+
expect(xScale).toBeDefined();
|
|
122
|
+
expect(yScale).toBeDefined();
|
|
123
|
+
expect(isNumericScale(xScale as ChartScaleFunction)).toBe(true);
|
|
124
|
+
expect(isNumericScale(yScale as ChartScaleFunction)).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('creates a band scale when scaleType is band', () => {
|
|
128
|
+
const { result } = renderHook(() =>
|
|
129
|
+
useBuildChartContext(hookParams({ xAxis: { scaleType: 'band' } })),
|
|
130
|
+
);
|
|
131
|
+
expect(
|
|
132
|
+
isCategoricalScale(result.current.getXScale() as ChartScaleFunction),
|
|
133
|
+
).toBe(true);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('computes data length from series', () => {
|
|
137
|
+
const { result } = renderHook(() =>
|
|
138
|
+
useBuildChartContext(
|
|
139
|
+
hookParams({ series: [makeSeries([1, 2, 3, 4, 5])] }),
|
|
140
|
+
),
|
|
141
|
+
);
|
|
142
|
+
expect(result.current.dataLength).toBe(5);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('computes data length from axis data when provided', () => {
|
|
146
|
+
const { result } = renderHook(() =>
|
|
147
|
+
useBuildChartContext(
|
|
148
|
+
hookParams({ xAxis: { data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'] } }),
|
|
149
|
+
),
|
|
150
|
+
);
|
|
151
|
+
expect(result.current.dataLength).toBe(5);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('applies domain overrides', () => {
|
|
155
|
+
const { result } = renderHook(() =>
|
|
156
|
+
useBuildChartContext(
|
|
157
|
+
hookParams({ yAxis: { domain: { min: 0, max: 100 } } }),
|
|
158
|
+
),
|
|
159
|
+
);
|
|
160
|
+
const yScale = result.current.getYScale()!;
|
|
161
|
+
expect(isNumericScale(yScale)).toBe(true);
|
|
162
|
+
expect(yScale.domain()).toEqual([0, 100]);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('applies domain function overrides', () => {
|
|
166
|
+
const { result } = renderHook(() =>
|
|
167
|
+
useBuildChartContext(
|
|
168
|
+
hookParams({
|
|
169
|
+
yAxis: {
|
|
170
|
+
domain: (bounds: { min: number; max: number }) => ({
|
|
171
|
+
min: bounds.min - 5,
|
|
172
|
+
max: bounds.max + 5,
|
|
173
|
+
}),
|
|
174
|
+
},
|
|
175
|
+
}),
|
|
176
|
+
),
|
|
177
|
+
);
|
|
178
|
+
const domain = result.current.getYScale()!.domain();
|
|
179
|
+
expect(domain[0]).toBe(0);
|
|
180
|
+
expect(domain[1]).toBe(40);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('inverts Y range for SVG coordinates', () => {
|
|
184
|
+
const { result } = renderHook(() =>
|
|
185
|
+
useBuildChartContext(
|
|
186
|
+
hookParams({
|
|
187
|
+
inset: { top: 10, right: 0, bottom: 0, left: 0 },
|
|
188
|
+
}),
|
|
189
|
+
),
|
|
190
|
+
);
|
|
191
|
+
const yScale = result.current.getYScale()!;
|
|
192
|
+
const range = yScale.range();
|
|
193
|
+
expect(range[0]).toBeGreaterThan(range[1]);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('exposes series and axis configs', () => {
|
|
197
|
+
const series = [makeSeries([1, 2])];
|
|
198
|
+
const xAxis = { scaleType: 'linear' as const };
|
|
199
|
+
const { result } = renderHook(() =>
|
|
200
|
+
useBuildChartContext(hookParams({ series, xAxis })),
|
|
201
|
+
);
|
|
202
|
+
expect(result.current.series).toBe(series);
|
|
203
|
+
expect(result.current.getXAxisConfig()).toBe(xAxis);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('builds a seriesMap for lookups', () => {
|
|
207
|
+
const series = [makeSeries([1, 2])];
|
|
208
|
+
const { result } = renderHook(() =>
|
|
209
|
+
useBuildChartContext(hookParams({ series })),
|
|
210
|
+
);
|
|
211
|
+
expect(result.current.seriesMap.get('test')).toBe(series[0]);
|
|
212
|
+
expect(result.current.seriesMap.size).toBe(1);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('applies static axisPadding to drawingArea', () => {
|
|
216
|
+
const axisPadding: ChartInset = { top: 0, right: 0, bottom: 28, left: 40 };
|
|
217
|
+
const { result } = renderHook(() =>
|
|
218
|
+
useBuildChartContext(hookParams({ axisPadding })),
|
|
219
|
+
);
|
|
220
|
+
expect(result.current.drawingArea.x).toBe(40);
|
|
221
|
+
expect(result.current.drawingArea.width).toBe(400 - 40);
|
|
222
|
+
expect(result.current.drawingArea.height).toBe(200 - 28);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
computeDataLength,
|
|
5
|
+
computeXDomain,
|
|
6
|
+
computeYDomain,
|
|
7
|
+
} from '../../../utils/domain/domain';
|
|
8
|
+
import {
|
|
9
|
+
getCategoricalScale,
|
|
10
|
+
getNumericScale,
|
|
11
|
+
isBandScaleType,
|
|
12
|
+
} from '../../../utils/scales/scales';
|
|
13
|
+
import type {
|
|
14
|
+
AxisBounds,
|
|
15
|
+
AxisConfigProps,
|
|
16
|
+
CartesianChartContextValue,
|
|
17
|
+
ChartInset,
|
|
18
|
+
ChartScaleFunction,
|
|
19
|
+
DrawingArea,
|
|
20
|
+
Series,
|
|
21
|
+
} from '../../../utils/types';
|
|
22
|
+
|
|
23
|
+
type UseBuildChartContextParams = {
|
|
24
|
+
series: Series[];
|
|
25
|
+
xAxis?: Partial<AxisConfigProps>;
|
|
26
|
+
yAxis?: Partial<AxisConfigProps>;
|
|
27
|
+
width: number;
|
|
28
|
+
height: number;
|
|
29
|
+
inset: ChartInset;
|
|
30
|
+
axisPadding: ChartInset;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type AxisRange = { min: number; max: number };
|
|
34
|
+
|
|
35
|
+
export const computeAxisRange = (
|
|
36
|
+
drawingArea: DrawingArea,
|
|
37
|
+
): { xRange: AxisRange; yRange: AxisRange } => ({
|
|
38
|
+
xRange: { min: drawingArea.x, max: drawingArea.x + drawingArea.width },
|
|
39
|
+
yRange: {
|
|
40
|
+
min: drawingArea.y + drawingArea.height,
|
|
41
|
+
max: drawingArea.y,
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export const buildScale = (
|
|
46
|
+
domain: AxisBounds,
|
|
47
|
+
range: AxisRange,
|
|
48
|
+
scaleType: AxisConfigProps['scaleType'] = 'linear',
|
|
49
|
+
): ChartScaleFunction =>
|
|
50
|
+
isBandScaleType(scaleType)
|
|
51
|
+
? getCategoricalScale({ domain, range })
|
|
52
|
+
: getNumericScale({ scaleType, domain, range });
|
|
53
|
+
|
|
54
|
+
export const computeDrawingArea = (
|
|
55
|
+
width: number,
|
|
56
|
+
height: number,
|
|
57
|
+
inset: ChartInset,
|
|
58
|
+
axisPadding: ChartInset,
|
|
59
|
+
): DrawingArea => {
|
|
60
|
+
const totalInset = {
|
|
61
|
+
top: inset.top + axisPadding.top,
|
|
62
|
+
right: inset.right + axisPadding.right,
|
|
63
|
+
bottom: inset.bottom + axisPadding.bottom,
|
|
64
|
+
left: inset.left + axisPadding.left,
|
|
65
|
+
};
|
|
66
|
+
return {
|
|
67
|
+
x: totalInset.left,
|
|
68
|
+
y: totalInset.top,
|
|
69
|
+
width: Math.max(0, width - totalInset.left - totalInset.right),
|
|
70
|
+
height: Math.max(0, height - totalInset.top - totalInset.bottom),
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const useBuildChartContext = ({
|
|
75
|
+
series,
|
|
76
|
+
xAxis,
|
|
77
|
+
yAxis,
|
|
78
|
+
width,
|
|
79
|
+
height,
|
|
80
|
+
inset,
|
|
81
|
+
axisPadding,
|
|
82
|
+
}: UseBuildChartContextParams): CartesianChartContextValue => {
|
|
83
|
+
const drawingArea = useMemo(
|
|
84
|
+
() => computeDrawingArea(width, height, inset, axisPadding),
|
|
85
|
+
[width, height, inset, axisPadding],
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
return useMemo(() => {
|
|
89
|
+
const { xRange, yRange } = computeAxisRange(drawingArea);
|
|
90
|
+
|
|
91
|
+
let xScale: ChartScaleFunction | undefined;
|
|
92
|
+
let yScale: ChartScaleFunction | undefined;
|
|
93
|
+
|
|
94
|
+
if (drawingArea.width > 0 && drawingArea.height > 0) {
|
|
95
|
+
const xDomain = computeXDomain(series, xAxis);
|
|
96
|
+
const yDomain = computeYDomain(series, yAxis);
|
|
97
|
+
xScale = buildScale(xDomain, xRange, xAxis?.scaleType);
|
|
98
|
+
yScale = buildScale(yDomain, yRange, yAxis?.scaleType);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const dataLength = computeDataLength(series, xAxis);
|
|
102
|
+
const seriesMap = new Map(series.map((s) => [s.id, s]));
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
series,
|
|
106
|
+
seriesMap,
|
|
107
|
+
getXScale: (_id?: string) => xScale,
|
|
108
|
+
getYScale: (_id?: string) => yScale,
|
|
109
|
+
getXAxisConfig: (_id?: string) => xAxis,
|
|
110
|
+
getYAxisConfig: (_id?: string) => yAxis,
|
|
111
|
+
drawingArea,
|
|
112
|
+
dataLength,
|
|
113
|
+
};
|
|
114
|
+
}, [series, xAxis, yAxis, drawingArea]);
|
|
115
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
import type { AxisConfigProps, ChartInset, Series } from '../../utils/types';
|
|
4
|
+
|
|
5
|
+
export type CartesianChartProps = {
|
|
6
|
+
/**
|
|
7
|
+
* Data series provided to child components via context.
|
|
8
|
+
*/
|
|
9
|
+
series: Series[];
|
|
10
|
+
/**
|
|
11
|
+
* Scale and domain configuration for the x-axis.
|
|
12
|
+
*/
|
|
13
|
+
xAxis?: Partial<AxisConfigProps>;
|
|
14
|
+
/**
|
|
15
|
+
* Scale and domain configuration for the y-axis.
|
|
16
|
+
*/
|
|
17
|
+
yAxis?: Partial<AxisConfigProps>;
|
|
18
|
+
/**
|
|
19
|
+
* Width of the chart in pixels.
|
|
20
|
+
* When omitted, the component auto-measures via `onLayout`.
|
|
21
|
+
*/
|
|
22
|
+
width?: number;
|
|
23
|
+
/**
|
|
24
|
+
* Height of the chart in pixels.
|
|
25
|
+
* @default 160
|
|
26
|
+
*/
|
|
27
|
+
height?: number;
|
|
28
|
+
/**
|
|
29
|
+
* Padding between the SVG edge and the drawing area.
|
|
30
|
+
* A number applies uniformly; a partial object overrides individual sides.
|
|
31
|
+
*/
|
|
32
|
+
inset?: number | Partial<ChartInset>;
|
|
33
|
+
/**
|
|
34
|
+
* Space reserved for axes around the drawing area.
|
|
35
|
+
* Shrinks the drawing area by the specified amount on each side.
|
|
36
|
+
*/
|
|
37
|
+
axisPadding?: Partial<ChartInset>;
|
|
38
|
+
/**
|
|
39
|
+
* Accessible label for the chart.
|
|
40
|
+
* @default 'Chart'
|
|
41
|
+
*/
|
|
42
|
+
ariaLabel?: string;
|
|
43
|
+
/**
|
|
44
|
+
* SVG content rendered inside the chart's context provider.
|
|
45
|
+
*/
|
|
46
|
+
children?: ReactNode;
|
|
47
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { useId, useMemo } from 'react';
|
|
2
|
+
import { Defs, LinearGradient, Path, Stop } from 'react-native-svg';
|
|
3
|
+
|
|
4
|
+
import { isNumericScale } from '../../utils/scales/scales';
|
|
5
|
+
import { useCartesianChartContext } from '../CartesianChart/context';
|
|
6
|
+
|
|
7
|
+
import type { LineProps } from './types';
|
|
8
|
+
import { buildAreaPath, buildLinePath, toScaledPoints } from './utils';
|
|
9
|
+
|
|
10
|
+
const STROKE_WIDTH = 2;
|
|
11
|
+
const AREA_GRADIENT_OPACITY = 0.2;
|
|
12
|
+
|
|
13
|
+
export const Line = ({
|
|
14
|
+
seriesId,
|
|
15
|
+
stroke,
|
|
16
|
+
showArea = false,
|
|
17
|
+
areaType: _areaType = 'gradient',
|
|
18
|
+
}: LineProps) => {
|
|
19
|
+
const { getXScale, getYScale, getXAxisConfig, drawingArea, seriesMap } =
|
|
20
|
+
useCartesianChartContext();
|
|
21
|
+
|
|
22
|
+
const xScale = getXScale();
|
|
23
|
+
const yScale = getYScale();
|
|
24
|
+
const xAxisConfig = getXAxisConfig();
|
|
25
|
+
|
|
26
|
+
const gradientId = useId();
|
|
27
|
+
const seriesData = seriesMap.get(seriesId);
|
|
28
|
+
const resolvedStroke = stroke ?? seriesData?.stroke;
|
|
29
|
+
|
|
30
|
+
const points = useMemo(
|
|
31
|
+
() =>
|
|
32
|
+
seriesData?.data && xScale && yScale && isNumericScale(yScale)
|
|
33
|
+
? toScaledPoints(seriesData.data, xScale, yScale, xAxisConfig?.data)
|
|
34
|
+
: null,
|
|
35
|
+
[seriesData, xScale, yScale, xAxisConfig],
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const linePath = useMemo(
|
|
39
|
+
() => (points ? buildLinePath(points) : null),
|
|
40
|
+
[points],
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const areaPath = useMemo(
|
|
44
|
+
() =>
|
|
45
|
+
showArea && points && drawingArea
|
|
46
|
+
? buildAreaPath(points, drawingArea)
|
|
47
|
+
: null,
|
|
48
|
+
[showArea, points, drawingArea],
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
if (!linePath) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<>
|
|
57
|
+
{showArea && areaPath && resolvedStroke && (
|
|
58
|
+
<>
|
|
59
|
+
<Defs>
|
|
60
|
+
<LinearGradient id={gradientId} x1='0' y1='0' x2='0' y2='1'>
|
|
61
|
+
<Stop
|
|
62
|
+
offset='0%'
|
|
63
|
+
stopColor={resolvedStroke}
|
|
64
|
+
stopOpacity={AREA_GRADIENT_OPACITY}
|
|
65
|
+
/>
|
|
66
|
+
<Stop offset='100%' stopColor={resolvedStroke} stopOpacity={0} />
|
|
67
|
+
</LinearGradient>
|
|
68
|
+
</Defs>
|
|
69
|
+
<Path
|
|
70
|
+
testID='line-area'
|
|
71
|
+
d={areaPath}
|
|
72
|
+
fill={`url(#${gradientId})`}
|
|
73
|
+
stroke='none'
|
|
74
|
+
/>
|
|
75
|
+
</>
|
|
76
|
+
)}
|
|
77
|
+
<Path
|
|
78
|
+
testID='line-path'
|
|
79
|
+
d={linePath}
|
|
80
|
+
fill='none'
|
|
81
|
+
stroke={resolvedStroke}
|
|
82
|
+
strokeWidth={STROKE_WIDTH}
|
|
83
|
+
strokeLinecap='round'
|
|
84
|
+
strokeLinejoin='round'
|
|
85
|
+
/>
|
|
86
|
+
</>
|
|
87
|
+
);
|
|
88
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type LineProps = {
|
|
2
|
+
/**
|
|
3
|
+
* The ID of the series to render.
|
|
4
|
+
* Must match a `Series.id` provided to the parent chart.
|
|
5
|
+
*/
|
|
6
|
+
seriesId: string;
|
|
7
|
+
/**
|
|
8
|
+
* Override color for the line stroke.
|
|
9
|
+
* When omitted, falls back to the `stroke` defined on the series.
|
|
10
|
+
*/
|
|
11
|
+
stroke?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Whether to show a filled area under the line.
|
|
14
|
+
* @default false
|
|
15
|
+
*/
|
|
16
|
+
showArea?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Area fill style.
|
|
19
|
+
* When `'gradient'`, renders a vertical gradient from the stroke color to transparent.
|
|
20
|
+
* @default 'gradient'
|
|
21
|
+
*/
|
|
22
|
+
areaType?: 'gradient';
|
|
23
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { area, curveBumpX, line } from 'd3-shape';
|
|
2
|
+
|
|
3
|
+
import { isCategoricalScale } from '../../utils/scales/scales';
|
|
4
|
+
import type {
|
|
5
|
+
ChartScaleFunction,
|
|
6
|
+
DrawingArea,
|
|
7
|
+
NumericScale,
|
|
8
|
+
} from '../../utils/types';
|
|
9
|
+
|
|
10
|
+
type Point = [x: number, y: number];
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Project series data into scaled [x, y] pixel coordinates, skipping nulls.
|
|
14
|
+
*
|
|
15
|
+
* When `xData` contains numeric values, those values are fed into the scale
|
|
16
|
+
* instead of the array index so the points honour a numeric X domain.
|
|
17
|
+
*/
|
|
18
|
+
export const toScaledPoints = (
|
|
19
|
+
data: (number | null)[],
|
|
20
|
+
xScale: ChartScaleFunction,
|
|
21
|
+
yScale: NumericScale,
|
|
22
|
+
xData?: readonly (string | number)[],
|
|
23
|
+
): Point[] | null => {
|
|
24
|
+
const pts: Point[] = [];
|
|
25
|
+
|
|
26
|
+
for (let i = 0; i < data.length; i++) {
|
|
27
|
+
const value = data[i];
|
|
28
|
+
if (value === null) continue;
|
|
29
|
+
|
|
30
|
+
const xInput =
|
|
31
|
+
xData && typeof xData[i] === 'number' ? (xData[i] as number) : i;
|
|
32
|
+
|
|
33
|
+
const x = isCategoricalScale(xScale)
|
|
34
|
+
? (xScale(xInput) ?? 0) + xScale.bandwidth() / 2
|
|
35
|
+
: xScale(xInput);
|
|
36
|
+
const y = yScale(value);
|
|
37
|
+
|
|
38
|
+
pts.push([x as number, y]);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return pts.length >= 2 ? pts : null;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Build the SVG `d` attribute for the line stroke.
|
|
46
|
+
*/
|
|
47
|
+
export const buildLinePath = (points: Point[]): string | null => {
|
|
48
|
+
return (
|
|
49
|
+
line<Point>()
|
|
50
|
+
.x((d) => d[0])
|
|
51
|
+
.y((d) => d[1])
|
|
52
|
+
.curve(curveBumpX)(points) ?? null
|
|
53
|
+
);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Build the SVG `d` attribute for the filled area beneath the line.
|
|
58
|
+
*/
|
|
59
|
+
export const buildAreaPath = (
|
|
60
|
+
points: Point[],
|
|
61
|
+
drawingArea: DrawingArea,
|
|
62
|
+
): string | null => {
|
|
63
|
+
const yBottom = drawingArea.y + drawingArea.height;
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
area<Point>()
|
|
67
|
+
.x((d) => d[0])
|
|
68
|
+
.y0(yBottom)
|
|
69
|
+
.y1((d) => d[1])
|
|
70
|
+
.curve(curveBumpX)(points) ?? null
|
|
71
|
+
);
|
|
72
|
+
};
|