@ledgerhq/lumen-ui-rnative-visualization 0.1.2 → 0.1.3
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 +41 -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 +55 -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 +24 -0
- package/dist/typescript/src/lib/utils/scales/scales.d.ts.map +1 -0
- package/dist/typescript/src/lib/utils/ticks/ticks.d.ts +27 -0
- package/dist/typescript/src/lib/utils/ticks/ticks.d.ts.map +1 -0
- package/dist/typescript/src/lib/utils/types.d.ts +90 -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 +111 -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 +110 -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 +67 -0
- package/src/lib/utils/ticks/ticks.test.ts +136 -0
- package/src/lib/utils/ticks/ticks.ts +82 -0
- package/src/lib/utils/types.ts +97 -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,101 @@
|
|
|
1
|
+
import type { AxisBounds, AxisConfigProps, Series } from '../types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Compute the X domain (index-based) from series data and axis config.
|
|
5
|
+
* For the X axis the domain is typically 0..N-1 where N = longest series length,
|
|
6
|
+
* unless explicit `data` is provided on the axis config.
|
|
7
|
+
*/
|
|
8
|
+
export const computeXDomain = (
|
|
9
|
+
series: Series[],
|
|
10
|
+
axisConfig?: Partial<AxisConfigProps>,
|
|
11
|
+
): AxisBounds => {
|
|
12
|
+
const axisData = axisConfig?.data;
|
|
13
|
+
|
|
14
|
+
let autoBounds: AxisBounds;
|
|
15
|
+
|
|
16
|
+
if (axisData && axisData.length > 0) {
|
|
17
|
+
if (typeof axisData[0] === 'number') {
|
|
18
|
+
const nums = axisData as number[];
|
|
19
|
+
let min = nums[0];
|
|
20
|
+
let max = nums[0];
|
|
21
|
+
for (let i = 1; i < nums.length; i += 1) {
|
|
22
|
+
const value = nums[i];
|
|
23
|
+
if (value < min) min = value;
|
|
24
|
+
if (value > max) max = value;
|
|
25
|
+
}
|
|
26
|
+
autoBounds = { min, max };
|
|
27
|
+
} else {
|
|
28
|
+
autoBounds = { min: 0, max: axisData.length - 1 };
|
|
29
|
+
}
|
|
30
|
+
} else {
|
|
31
|
+
const maxLen = series.reduce(
|
|
32
|
+
(max, s) => Math.max(max, s.data?.length ?? 0),
|
|
33
|
+
0,
|
|
34
|
+
);
|
|
35
|
+
autoBounds = { min: 0, max: Math.max(0, maxLen - 1) };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return applyDomainOverride(autoBounds, axisConfig?.domain);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Compute the Y domain (value-based) from series data and axis config.
|
|
43
|
+
* Scans all non-null values across all series to find min/max.
|
|
44
|
+
*/
|
|
45
|
+
export const computeYDomain = (
|
|
46
|
+
series: Series[],
|
|
47
|
+
axisConfig?: Partial<AxisConfigProps>,
|
|
48
|
+
): AxisBounds => {
|
|
49
|
+
let min = 0;
|
|
50
|
+
let max = 0;
|
|
51
|
+
let hasValue = false;
|
|
52
|
+
|
|
53
|
+
for (const s of series) {
|
|
54
|
+
if (!s.data) continue;
|
|
55
|
+
for (const v of s.data) {
|
|
56
|
+
if (v !== null && v !== undefined) {
|
|
57
|
+
if (!hasValue) {
|
|
58
|
+
min = v;
|
|
59
|
+
max = v;
|
|
60
|
+
hasValue = true;
|
|
61
|
+
} else {
|
|
62
|
+
if (v < min) min = v;
|
|
63
|
+
if (v > max) max = v;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const autoBounds: AxisBounds = hasValue ? { min, max } : { min: 0, max: 1 };
|
|
70
|
+
|
|
71
|
+
return applyDomainOverride(autoBounds, axisConfig?.domain);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Compute the data length (number of discrete positions on the index axis).
|
|
76
|
+
*/
|
|
77
|
+
export const computeDataLength = (
|
|
78
|
+
series: Series[],
|
|
79
|
+
axisConfig?: Partial<AxisConfigProps>,
|
|
80
|
+
): number => {
|
|
81
|
+
if (axisConfig?.data && axisConfig.data.length > 0) {
|
|
82
|
+
return axisConfig.data.length;
|
|
83
|
+
}
|
|
84
|
+
return series.reduce((max, s) => Math.max(max, s.data?.length ?? 0), 0);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const applyDomainOverride = (
|
|
88
|
+
autoBounds: AxisBounds,
|
|
89
|
+
domainOverride?: AxisConfigProps['domain'],
|
|
90
|
+
): AxisBounds => {
|
|
91
|
+
if (!domainOverride) return autoBounds;
|
|
92
|
+
|
|
93
|
+
if (typeof domainOverride === 'function') {
|
|
94
|
+
return domainOverride(autoBounds);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
min: domainOverride.min ?? autoBounds.min,
|
|
99
|
+
max: domainOverride.max ?? autoBounds.max,
|
|
100
|
+
};
|
|
101
|
+
};
|
package/src/lib/utils/index.ts
CHANGED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { describe, expect, it } from '@jest/globals';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
getCategoricalScale,
|
|
5
|
+
getNumericScale,
|
|
6
|
+
isBandScaleType,
|
|
7
|
+
isCategoricalScale,
|
|
8
|
+
isNumericScale,
|
|
9
|
+
} from './scales';
|
|
10
|
+
|
|
11
|
+
describe('getNumericScale', () => {
|
|
12
|
+
it('should create a linear scale mapping domain to range', () => {
|
|
13
|
+
const scale = getNumericScale({
|
|
14
|
+
scaleType: 'linear',
|
|
15
|
+
domain: { min: 0, max: 100 },
|
|
16
|
+
range: { min: 0, max: 500 },
|
|
17
|
+
});
|
|
18
|
+
expect(scale(0)).toBe(0);
|
|
19
|
+
expect(scale(50)).toBe(250);
|
|
20
|
+
expect(scale(100)).toBe(500);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should create a log scale', () => {
|
|
24
|
+
const scale = getNumericScale({
|
|
25
|
+
scaleType: 'log',
|
|
26
|
+
domain: { min: 1, max: 1000 },
|
|
27
|
+
range: { min: 0, max: 300 },
|
|
28
|
+
});
|
|
29
|
+
expect(scale(1)).toBe(0);
|
|
30
|
+
expect(scale(1000)).toBe(300);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should set correct domain', () => {
|
|
34
|
+
const scale = getNumericScale({
|
|
35
|
+
scaleType: 'linear',
|
|
36
|
+
domain: { min: 10, max: 50 },
|
|
37
|
+
range: { min: 0, max: 200 },
|
|
38
|
+
});
|
|
39
|
+
const domain = scale.domain();
|
|
40
|
+
expect(domain[0]).toBe(10);
|
|
41
|
+
expect(domain[1]).toBe(50);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should set correct range', () => {
|
|
45
|
+
const scale = getNumericScale({
|
|
46
|
+
scaleType: 'linear',
|
|
47
|
+
domain: { min: 0, max: 100 },
|
|
48
|
+
range: { min: 20, max: 400 },
|
|
49
|
+
});
|
|
50
|
+
const range = scale.range();
|
|
51
|
+
expect(range[0]).toBe(20);
|
|
52
|
+
expect(range[1]).toBe(400);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('getCategoricalScale', () => {
|
|
57
|
+
it('should create a band scale with correct domain', () => {
|
|
58
|
+
const scale = getCategoricalScale({
|
|
59
|
+
domain: { min: 0, max: 3 },
|
|
60
|
+
range: { min: 0, max: 400 },
|
|
61
|
+
padding: 0,
|
|
62
|
+
});
|
|
63
|
+
expect(scale.domain()).toEqual([0, 1, 2, 3]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should have non-zero bandwidth', () => {
|
|
67
|
+
const scale = getCategoricalScale({
|
|
68
|
+
domain: { min: 0, max: 4 },
|
|
69
|
+
range: { min: 0, max: 500 },
|
|
70
|
+
});
|
|
71
|
+
expect(scale.bandwidth()).toBeGreaterThan(0);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should map values within the range', () => {
|
|
75
|
+
const scale = getCategoricalScale({
|
|
76
|
+
domain: { min: 0, max: 2 },
|
|
77
|
+
range: { min: 0, max: 300 },
|
|
78
|
+
padding: 0,
|
|
79
|
+
});
|
|
80
|
+
expect(scale(0)).toBeGreaterThanOrEqual(0);
|
|
81
|
+
expect((scale(2) ?? 0) + scale.bandwidth()).toBeLessThanOrEqual(300);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should handle custom domain offset', () => {
|
|
85
|
+
const scale = getCategoricalScale({
|
|
86
|
+
domain: { min: 5, max: 7 },
|
|
87
|
+
range: { min: 0, max: 300 },
|
|
88
|
+
padding: 0,
|
|
89
|
+
});
|
|
90
|
+
expect(scale.domain()).toEqual([5, 6, 7]);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('isBandScaleType', () => {
|
|
95
|
+
it('should return true for band scale type', () => {
|
|
96
|
+
expect(isBandScaleType('band')).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should return false for linear scale type', () => {
|
|
100
|
+
expect(isBandScaleType('linear')).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should return false for log scale type', () => {
|
|
104
|
+
expect(isBandScaleType('log')).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should return false for undefined', () => {
|
|
108
|
+
expect(isBandScaleType(undefined)).toBe(false);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('isCategoricalScale / isNumericScale', () => {
|
|
113
|
+
const numericScale = getNumericScale({
|
|
114
|
+
scaleType: 'linear',
|
|
115
|
+
domain: { min: 0, max: 100 },
|
|
116
|
+
range: { min: 0, max: 500 },
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const bandScale = getCategoricalScale({
|
|
120
|
+
domain: { min: 0, max: 4 },
|
|
121
|
+
range: { min: 0, max: 500 },
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should identify a numeric scale', () => {
|
|
125
|
+
expect(isNumericScale(numericScale)).toBe(true);
|
|
126
|
+
expect(isCategoricalScale(numericScale)).toBe(false);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should identify a categorical scale', () => {
|
|
130
|
+
expect(isCategoricalScale(bandScale)).toBe(true);
|
|
131
|
+
expect(isNumericScale(bandScale)).toBe(false);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { scaleBand, scaleLinear, scaleLog } from 'd3-scale';
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
AxisBounds,
|
|
5
|
+
AxisConfigProps,
|
|
6
|
+
CategoricalScale,
|
|
7
|
+
ChartScaleFunction,
|
|
8
|
+
NumericScale,
|
|
9
|
+
} from '../types';
|
|
10
|
+
|
|
11
|
+
export const getNumericScale = ({
|
|
12
|
+
scaleType,
|
|
13
|
+
domain,
|
|
14
|
+
range,
|
|
15
|
+
}: {
|
|
16
|
+
scaleType: 'linear' | 'log';
|
|
17
|
+
domain: AxisBounds;
|
|
18
|
+
range: AxisBounds;
|
|
19
|
+
}): NumericScale => {
|
|
20
|
+
const scale = scaleType === 'log' ? scaleLog() : scaleLinear();
|
|
21
|
+
return scale.domain([domain.min, domain.max]).range([range.min, range.max]);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const getCategoricalScale = ({
|
|
25
|
+
domain,
|
|
26
|
+
range,
|
|
27
|
+
padding = 0.1,
|
|
28
|
+
}: {
|
|
29
|
+
domain: AxisBounds;
|
|
30
|
+
range: AxisBounds;
|
|
31
|
+
padding?: number;
|
|
32
|
+
}): CategoricalScale => {
|
|
33
|
+
const domainArray = Array.from(
|
|
34
|
+
{ length: domain.max - domain.min + 1 },
|
|
35
|
+
(_, i) => domain.min + i,
|
|
36
|
+
);
|
|
37
|
+
return scaleBand<number>()
|
|
38
|
+
.domain(domainArray)
|
|
39
|
+
.range([range.min, range.max])
|
|
40
|
+
.paddingInner(padding)
|
|
41
|
+
.paddingOuter(padding / 2);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Checks if a scale type config value refers to a band (categorical) scale.
|
|
46
|
+
*/
|
|
47
|
+
export const isBandScaleType = (
|
|
48
|
+
scaleType: AxisConfigProps['scaleType'],
|
|
49
|
+
): scaleType is 'band' => scaleType === 'band';
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Checks if a scale is a categorical scale.
|
|
53
|
+
*/
|
|
54
|
+
export const isCategoricalScale = (
|
|
55
|
+
scale: ChartScaleFunction,
|
|
56
|
+
): scale is CategoricalScale => {
|
|
57
|
+
return 'bandwidth' in scale && typeof scale.bandwidth === 'function';
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Checks if a scale is a numeric scale.
|
|
62
|
+
*/
|
|
63
|
+
export const isNumericScale = (
|
|
64
|
+
scale: ChartScaleFunction,
|
|
65
|
+
): scale is NumericScale => {
|
|
66
|
+
return !isCategoricalScale(scale);
|
|
67
|
+
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { describe, expect, it, jest } from '@jest/globals';
|
|
2
|
+
|
|
3
|
+
import { getCategoricalScale, getNumericScale } from '../scales/scales';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
buildTicksData,
|
|
7
|
+
getTickLabel,
|
|
8
|
+
getTickPosition,
|
|
9
|
+
getTickValues,
|
|
10
|
+
} from './ticks';
|
|
11
|
+
|
|
12
|
+
const numericScale = getNumericScale({
|
|
13
|
+
scaleType: 'linear',
|
|
14
|
+
domain: { min: 0, max: 100 },
|
|
15
|
+
range: { min: 0, max: 500 },
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const bandScale = getCategoricalScale({
|
|
19
|
+
domain: { min: 0, max: 4 },
|
|
20
|
+
range: { min: 0, max: 500 },
|
|
21
|
+
padding: 0,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('getTickValues', () => {
|
|
25
|
+
it('should return explicit ticks when provided', () => {
|
|
26
|
+
expect(getTickValues(numericScale, [0, 25, 50])).toEqual([0, 25, 50]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should return scale.ticks() for numeric scales', () => {
|
|
30
|
+
const ticks = getTickValues(numericScale);
|
|
31
|
+
expect(ticks.length).toBeGreaterThan(0);
|
|
32
|
+
expect(ticks).toContain(0);
|
|
33
|
+
expect(ticks).toContain(100);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should return full domain for band scales', () => {
|
|
37
|
+
expect(getTickValues(bandScale)).toEqual([0, 1, 2, 3, 4]);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('getTickPosition', () => {
|
|
42
|
+
it('should map value through numeric scale', () => {
|
|
43
|
+
expect(getTickPosition(numericScale, 0)).toBe(0);
|
|
44
|
+
expect(getTickPosition(numericScale, 50)).toBe(250);
|
|
45
|
+
expect(getTickPosition(numericScale, 100)).toBe(500);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should center within band for categorical scale', () => {
|
|
49
|
+
const pos = getTickPosition(bandScale, 0);
|
|
50
|
+
const bandwidth = bandScale.bandwidth();
|
|
51
|
+
expect(pos).toBe((bandScale(0) ?? 0) + bandwidth / 2);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('getTickLabel', () => {
|
|
56
|
+
it('should return stringified tick value by default', () => {
|
|
57
|
+
expect(getTickLabel(42, undefined)).toBe('42');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should look up string labels from axis data', () => {
|
|
61
|
+
const axisData = ['Jan', 'Feb', 'Mar'];
|
|
62
|
+
expect(getTickLabel(0, axisData)).toBe('Jan');
|
|
63
|
+
expect(getTickLabel(1, axisData)).toBe('Feb');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should fall back to tick value when index is out of range', () => {
|
|
67
|
+
const axisData = ['Jan', 'Feb'];
|
|
68
|
+
expect(getTickLabel(5, axisData)).toBe('5');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should apply formatter with raw string label when available', () => {
|
|
72
|
+
const axisData = ['January', 'February'];
|
|
73
|
+
const formatter = jest.fn(
|
|
74
|
+
(v: number | string) => `${String(v).slice(0, 3)}`,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
expect(getTickLabel(0, axisData, formatter)).toBe('Jan');
|
|
78
|
+
expect(formatter).toHaveBeenCalledWith('January');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should apply formatter with numeric value when no string labels', () => {
|
|
82
|
+
const formatter = jest.fn((v: number | string) => `$${v}`);
|
|
83
|
+
|
|
84
|
+
expect(getTickLabel(10, undefined, formatter)).toBe('$10');
|
|
85
|
+
expect(formatter).toHaveBeenCalledWith(10);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should apply formatter with numeric data', () => {
|
|
89
|
+
const axisData = [100, 200, 300];
|
|
90
|
+
const formatter = jest.fn((v: number | string) => `${v}!`);
|
|
91
|
+
|
|
92
|
+
expect(getTickLabel(1, axisData, formatter)).toBe('1!');
|
|
93
|
+
expect(formatter).toHaveBeenCalledWith(1);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('buildTicksData', () => {
|
|
98
|
+
it('should combine position, value, and label for each tick', () => {
|
|
99
|
+
const result = buildTicksData(numericScale);
|
|
100
|
+
expect(result.length).toBeGreaterThan(0);
|
|
101
|
+
for (const tick of result) {
|
|
102
|
+
expect(tick).toHaveProperty('position');
|
|
103
|
+
expect(tick).toHaveProperty('value');
|
|
104
|
+
expect(tick).toHaveProperty('label');
|
|
105
|
+
expect(typeof tick.position).toBe('number');
|
|
106
|
+
expect(typeof tick.label).toBe('string');
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should use explicit ticks when provided', () => {
|
|
111
|
+
const result = buildTicksData(numericScale, undefined, [0, 50, 100]);
|
|
112
|
+
expect(result).toHaveLength(3);
|
|
113
|
+
expect(result.map((t: { value: number }) => t.value)).toEqual([0, 50, 100]);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should resolve string labels from axis config', () => {
|
|
117
|
+
const result = buildTicksData(bandScale, {
|
|
118
|
+
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'],
|
|
119
|
+
});
|
|
120
|
+
expect(result[0].label).toBe('Mon');
|
|
121
|
+
expect(result[1].label).toBe('Tue');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should apply tick formatter', () => {
|
|
125
|
+
const formatter = (v: number | string): string => `$${v}`;
|
|
126
|
+
const result = buildTicksData(numericScale, undefined, [0, 50], formatter);
|
|
127
|
+
expect(result[0].label).toBe('$0');
|
|
128
|
+
expect(result[1].label).toBe('$50');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should compute correct positions for numeric scale', () => {
|
|
132
|
+
const result = buildTicksData(numericScale, undefined, [0, 100]);
|
|
133
|
+
expect(result[0].position).toBe(0);
|
|
134
|
+
expect(result[1].position).toBe(500);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { isCategoricalScale, isNumericScale } from '../scales/scales';
|
|
2
|
+
import type { AxisConfigProps, ChartScaleFunction } from '../types';
|
|
3
|
+
|
|
4
|
+
export const APPROXIMATE_TICK_COUNT = 5;
|
|
5
|
+
|
|
6
|
+
export type TickData = {
|
|
7
|
+
position: number;
|
|
8
|
+
value: number;
|
|
9
|
+
label: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Resolves which numeric tick values should appear on the axis.
|
|
14
|
+
* Explicit ticks take priority, then scale-specific defaults.
|
|
15
|
+
*/
|
|
16
|
+
export const getTickValues = (
|
|
17
|
+
scale: ChartScaleFunction,
|
|
18
|
+
explicitTicks?: number[],
|
|
19
|
+
): number[] => {
|
|
20
|
+
if (explicitTicks) return explicitTicks;
|
|
21
|
+
if (isCategoricalScale(scale)) return scale.domain();
|
|
22
|
+
if (isNumericScale(scale)) return scale.ticks(APPROXIMATE_TICK_COUNT);
|
|
23
|
+
return [];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Converts a tick value to its pixel position on the axis.
|
|
28
|
+
* Band scales are centered within the band.
|
|
29
|
+
*/
|
|
30
|
+
export const getTickPosition = (
|
|
31
|
+
scale: ChartScaleFunction,
|
|
32
|
+
tick: number,
|
|
33
|
+
): number => {
|
|
34
|
+
if (isCategoricalScale(scale)) {
|
|
35
|
+
return (scale(tick) ?? 0) + scale.bandwidth() / 2;
|
|
36
|
+
}
|
|
37
|
+
return (scale as (v: number) => number)(tick);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Resolves the display label for a tick value.
|
|
42
|
+
* Priority: formatter > string label lookup > raw value.
|
|
43
|
+
*/
|
|
44
|
+
export const getTickLabel = (
|
|
45
|
+
tick: number,
|
|
46
|
+
axisData: AxisConfigProps['data'],
|
|
47
|
+
formatter?: (value: number | string) => string,
|
|
48
|
+
): string => {
|
|
49
|
+
const hasStringLabels =
|
|
50
|
+
axisData && Array.isArray(axisData) && typeof axisData[0] === 'string';
|
|
51
|
+
|
|
52
|
+
if (formatter) {
|
|
53
|
+
const rawValue =
|
|
54
|
+
hasStringLabels && axisData[tick] !== undefined ? axisData[tick] : tick;
|
|
55
|
+
return String(formatter(rawValue as number | string));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (hasStringLabels && axisData[tick] !== undefined) {
|
|
59
|
+
return String(axisData[tick]);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return String(tick);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Builds the complete tick data array from a scale and axis configuration.
|
|
67
|
+
*/
|
|
68
|
+
export const buildTicksData = (
|
|
69
|
+
scale: ChartScaleFunction,
|
|
70
|
+
axisConfig?: AxisConfigProps,
|
|
71
|
+
explicitTicks?: number[],
|
|
72
|
+
formatter?: (value: number | string) => string,
|
|
73
|
+
): TickData[] => {
|
|
74
|
+
const tickValues = getTickValues(scale, explicitTicks);
|
|
75
|
+
const axisData = axisConfig?.data;
|
|
76
|
+
|
|
77
|
+
return tickValues.map((tick) => ({
|
|
78
|
+
position: getTickPosition(scale, tick),
|
|
79
|
+
value: tick,
|
|
80
|
+
label: getTickLabel(tick, axisData, formatter),
|
|
81
|
+
}));
|
|
82
|
+
};
|
package/src/lib/utils/types.ts
CHANGED
|
@@ -1,11 +1,102 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import type { ScaleBand, ScaleLinear, ScaleLogarithmic } from 'd3-scale';
|
|
2
|
+
|
|
3
|
+
export type AxisBounds = { min: number; max: number };
|
|
4
|
+
|
|
5
|
+
export type ChartInset = {
|
|
6
|
+
top: number;
|
|
7
|
+
right: number;
|
|
8
|
+
bottom: number;
|
|
9
|
+
left: number;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Pixel bounds of the drawable region.
|
|
14
|
+
*/
|
|
15
|
+
export type DrawingArea = {
|
|
16
|
+
x: number;
|
|
17
|
+
y: number;
|
|
18
|
+
width: number;
|
|
19
|
+
height: number;
|
|
4
20
|
};
|
|
5
21
|
|
|
6
22
|
export type Series = {
|
|
23
|
+
/**
|
|
24
|
+
* Unique identifier for the series.
|
|
25
|
+
*/
|
|
7
26
|
id: string;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Numeric data points; `null` entries represent gaps.
|
|
29
|
+
*/
|
|
30
|
+
data?: Array<number | null>;
|
|
31
|
+
/**
|
|
32
|
+
* Human-readable label used in legends and tooltips.
|
|
33
|
+
*/
|
|
34
|
+
label?: string;
|
|
35
|
+
/**
|
|
36
|
+
* CSS color applied to the series line/mark.
|
|
37
|
+
*/
|
|
38
|
+
stroke: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type AxisConfigProps = {
|
|
42
|
+
/**
|
|
43
|
+
* Scale algorithm used by this axis.
|
|
44
|
+
* @default 'linear'
|
|
45
|
+
*/
|
|
46
|
+
scaleType?: 'linear' | 'log' | 'band';
|
|
47
|
+
/**
|
|
48
|
+
* Explicit data values for band scales or category labels.
|
|
49
|
+
* For band scales, provides the discrete domain. For numeric scales, string values
|
|
50
|
+
* are used as tick labels at corresponding indices.
|
|
51
|
+
*/
|
|
52
|
+
data?: string[] | number[];
|
|
53
|
+
/**
|
|
54
|
+
* Fixed domain bounds or a function that adjusts the computed bounds.
|
|
55
|
+
* A partial object overrides only the specified bound(s).
|
|
56
|
+
* A function receives the auto-computed bounds and returns adjusted ones.
|
|
57
|
+
*/
|
|
58
|
+
domain?: Partial<AxisBounds> | ((bounds: AxisBounds) => AxisBounds);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export type NumericScale =
|
|
62
|
+
| ScaleLinear<number, number>
|
|
63
|
+
| ScaleLogarithmic<number, number>;
|
|
64
|
+
|
|
65
|
+
export type CategoricalScale = ScaleBand<number>;
|
|
66
|
+
|
|
67
|
+
export type ChartScaleFunction = NumericScale | CategoricalScale;
|
|
68
|
+
|
|
69
|
+
export type CartesianChartContextValue = {
|
|
70
|
+
/**
|
|
71
|
+
* All data series registered in the chart.
|
|
72
|
+
*/
|
|
73
|
+
series: Series[];
|
|
74
|
+
/**
|
|
75
|
+
* Lookup map for series by ID. Stable reference when series identity is unchanged.
|
|
76
|
+
*/
|
|
77
|
+
seriesMap: Map<string, Series>;
|
|
78
|
+
/**
|
|
79
|
+
* Returns the x-axis scale. Accepts an optional axis ID for future multi-axis support.
|
|
80
|
+
*/
|
|
81
|
+
getXScale: (id?: string) => ChartScaleFunction | undefined;
|
|
82
|
+
/**
|
|
83
|
+
* Returns the y-axis scale. Accepts an optional axis ID for future multi-axis support.
|
|
84
|
+
*/
|
|
85
|
+
getYScale: (id?: string) => ChartScaleFunction | undefined;
|
|
86
|
+
/**
|
|
87
|
+
* Returns the x-axis config. Accepts an optional axis ID for future multi-axis support.
|
|
88
|
+
*/
|
|
89
|
+
getXAxisConfig: (id?: string) => AxisConfigProps | undefined;
|
|
90
|
+
/**
|
|
91
|
+
* Returns the y-axis config. Accepts an optional axis ID for future multi-axis support.
|
|
92
|
+
*/
|
|
93
|
+
getYAxisConfig: (id?: string) => AxisConfigProps | undefined;
|
|
94
|
+
/**
|
|
95
|
+
* Pixel bounds of the drawable region.
|
|
96
|
+
*/
|
|
97
|
+
drawingArea: DrawingArea;
|
|
98
|
+
/**
|
|
99
|
+
* Number of data points along the x-axis.
|
|
100
|
+
*/
|
|
101
|
+
dataLength: number;
|
|
11
102
|
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"names":[],"sourceRoot":"../../../../../src","sources":["lib/utils/math/index.ts"],"mappings":"AAAA;AACA;AAAA","ignoreList":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../../src/lib/utils/math/index.ts"],"names":[],"mappings":""}
|