@automattic/charts 0.56.7 → 0.57.0
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/AGENTS.md +28 -98
- package/CHANGELOG.md +16 -0
- package/dist/charts/bar-chart/index.cjs +5 -6
- package/dist/charts/bar-chart/index.cjs.map +1 -1
- package/dist/charts/bar-chart/index.d.cts +3 -3
- package/dist/charts/bar-chart/index.d.ts +3 -3
- package/dist/charts/bar-chart/index.js +4 -5
- package/dist/charts/bar-list-chart/index.cjs +6 -7
- package/dist/charts/bar-list-chart/index.cjs.map +1 -1
- package/dist/charts/bar-list-chart/index.d.cts +3 -3
- package/dist/charts/bar-list-chart/index.d.ts +3 -3
- package/dist/charts/bar-list-chart/index.js +5 -6
- package/dist/charts/conversion-funnel-chart/index.cjs +5 -6
- package/dist/charts/conversion-funnel-chart/index.cjs.map +1 -1
- package/dist/charts/conversion-funnel-chart/index.d.cts +1 -1
- package/dist/charts/conversion-funnel-chart/index.d.ts +1 -1
- package/dist/charts/conversion-funnel-chart/index.js +4 -5
- package/dist/charts/geo-chart/index.cjs +4 -4
- package/dist/charts/geo-chart/index.d.cts +1 -1
- package/dist/charts/geo-chart/index.d.ts +1 -1
- package/dist/charts/geo-chart/index.js +3 -3
- package/dist/charts/leaderboard-chart/index.cjs +5 -5
- package/dist/charts/leaderboard-chart/index.css +8 -9
- package/dist/charts/leaderboard-chart/index.css.map +1 -1
- package/dist/charts/leaderboard-chart/index.d.cts +3 -3
- package/dist/charts/leaderboard-chart/index.d.ts +3 -3
- package/dist/charts/leaderboard-chart/index.js +4 -4
- package/dist/charts/line-chart/index.cjs +5 -6
- package/dist/charts/line-chart/index.cjs.map +1 -1
- package/dist/charts/line-chart/index.d.cts +3 -3
- package/dist/charts/line-chart/index.d.ts +3 -3
- package/dist/charts/line-chart/index.js +4 -5
- package/dist/charts/pie-chart/index.cjs +5 -6
- package/dist/charts/pie-chart/index.cjs.map +1 -1
- package/dist/charts/pie-chart/index.d.cts +4 -4
- package/dist/charts/pie-chart/index.d.ts +4 -4
- package/dist/charts/pie-chart/index.js +4 -5
- package/dist/charts/pie-semi-circle-chart/index.cjs +5 -6
- package/dist/charts/pie-semi-circle-chart/index.cjs.map +1 -1
- package/dist/charts/pie-semi-circle-chart/index.d.cts +4 -4
- package/dist/charts/pie-semi-circle-chart/index.d.ts +4 -4
- package/dist/charts/pie-semi-circle-chart/index.js +4 -5
- package/dist/charts/sparkline/index.cjs +6 -7
- package/dist/charts/sparkline/index.cjs.map +1 -1
- package/dist/charts/sparkline/index.js +5 -6
- package/dist/{chunk-XD2HV7M5.js → chunk-2NCY7R4G.js} +127 -762
- package/dist/chunk-2NCY7R4G.js.map +1 -0
- package/dist/{chunk-RFSHE3HL.js → chunk-32DH6JDF.js} +64 -43
- package/dist/chunk-32DH6JDF.js.map +1 -0
- package/dist/{chunk-SSFFCBCF.js → chunk-4OPFE4RM.js} +11 -8
- package/dist/chunk-4OPFE4RM.js.map +1 -0
- package/dist/{chunk-CAFJRZPZ.cjs → chunk-77OKCVQN.cjs} +17 -17
- package/dist/{chunk-CAFJRZPZ.cjs.map → chunk-77OKCVQN.cjs.map} +1 -1
- package/dist/{chunk-K6TGILHX.cjs → chunk-7FQX4ALL.cjs} +6 -6
- package/dist/{chunk-K6TGILHX.cjs.map → chunk-7FQX4ALL.cjs.map} +1 -1
- package/dist/{chunk-7FDQGBY7.js → chunk-BCX5THDQ.js} +9 -7
- package/dist/chunk-BCX5THDQ.js.map +1 -0
- package/dist/{chunk-KHQPN77E.js → chunk-CZGYJKG6.js} +4 -4
- package/dist/{chunk-3EXJP67N.cjs → chunk-D2UH4CFE.cjs} +9 -9
- package/dist/{chunk-3EXJP67N.cjs.map → chunk-D2UH4CFE.cjs.map} +1 -1
- package/dist/{chunk-TE63Y5PX.js → chunk-DAU3HNEG.js} +2 -2
- package/dist/chunk-DAU3HNEG.js.map +1 -0
- package/dist/{chunk-MDRCAGKZ.js → chunk-H2V4JMSA.js} +3 -3
- package/dist/{chunk-UFRBUT2D.cjs → chunk-I35UYJJR.cjs} +49 -6
- package/dist/chunk-I35UYJJR.cjs.map +1 -0
- package/dist/{chunk-GWBS65VC.js → chunk-IU4DYUAV.js} +3 -3
- package/dist/{chunk-E62LCBGD.js → chunk-PXLEMUGJ.js} +3 -3
- package/dist/{chunk-YDVHT7GS.cjs → chunk-RHHVEJHJ.cjs} +83 -62
- package/dist/chunk-RHHVEJHJ.cjs.map +1 -0
- package/dist/{chunk-YAXY5L7I.cjs → chunk-TO3OQBXG.cjs} +5 -5
- package/dist/{chunk-YAXY5L7I.cjs.map → chunk-TO3OQBXG.cjs.map} +1 -1
- package/dist/{chunk-VPAEBI2F.js → chunk-V36ERY7Y.js} +9 -7
- package/dist/chunk-V36ERY7Y.js.map +1 -0
- package/dist/{chunk-X7JL2NYJ.cjs → chunk-VJM5XCB4.cjs} +33 -30
- package/dist/chunk-VJM5XCB4.cjs.map +1 -0
- package/dist/{chunk-ZVGEDXDP.cjs → chunk-VTS3PNMS.cjs} +2 -2
- package/dist/{chunk-ZVGEDXDP.cjs.map → chunk-VTS3PNMS.cjs.map} +1 -1
- package/dist/{chunk-OMS5QIJN.js → chunk-WLODYNLB.js} +9 -7
- package/dist/chunk-WLODYNLB.js.map +1 -0
- package/dist/{chunk-NQJE2CC7.cjs → chunk-XKRJL2QT.cjs} +25 -23
- package/dist/chunk-XKRJL2QT.cjs.map +1 -0
- package/dist/{chunk-O2JIANHK.cjs → chunk-YE2T52VZ.cjs} +33 -31
- package/dist/chunk-YE2T52VZ.cjs.map +1 -0
- package/dist/{chunk-IS5YYLTV.js → chunk-Z26M4V2M.js} +46 -3
- package/dist/chunk-Z26M4V2M.js.map +1 -0
- package/dist/{chunk-55ZCOYDF.cjs → chunk-Z45KX47P.cjs} +153 -788
- package/dist/chunk-Z45KX47P.cjs.map +1 -0
- package/dist/{chunk-BXFD7JIG.cjs → chunk-ZH4F5RMG.cjs} +26 -24
- package/dist/chunk-ZH4F5RMG.cjs.map +1 -0
- package/dist/components/legend/index.cjs +3 -3
- package/dist/components/legend/index.d.cts +4 -4
- package/dist/components/legend/index.d.ts +4 -4
- package/dist/components/legend/index.js +2 -2
- package/dist/components/tooltip/index.d.cts +1 -1
- package/dist/components/tooltip/index.d.ts +1 -1
- package/dist/hooks/index.cjs +3 -3
- package/dist/hooks/index.d.cts +7 -3
- package/dist/hooks/index.d.ts +7 -3
- package/dist/hooks/index.js +2 -2
- package/dist/index.cjs +13 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +8 -9
- package/dist/index.css.map +1 -1
- package/dist/index.d.cts +7 -7
- package/dist/index.d.ts +7 -7
- package/dist/index.js +12 -13
- package/dist/{leaderboard-chart-BSgEw_Um.d.ts → leaderboard-chart-BKYYXcg2.d.ts} +5 -9
- package/dist/{leaderboard-chart-COtgamhe.d.cts → leaderboard-chart-DR7CGb0L.d.cts} +5 -9
- package/dist/{legend-C9ahiwOt.d.cts → legend-C2grwnWk.d.cts} +1 -1
- package/dist/{legend-jjMmhSg3.d.ts → legend-Cj0xM5dU.d.ts} +1 -1
- package/dist/providers/index.cjs +3 -3
- package/dist/providers/index.d.cts +3 -3
- package/dist/providers/index.d.ts +3 -3
- package/dist/providers/index.js +2 -2
- package/dist/{themes-DQzmaSze.d.ts → themes-BmVGrYnF.d.ts} +2 -2
- package/dist/{themes-CVR5rmIs.d.cts → themes-CyjKm-P_.d.cts} +2 -2
- package/dist/{types-DQNnq5Fr.d.ts → types-CuUEszrM.d.ts} +1 -1
- package/dist/{types-CzdN7rUe.d.cts → types-DZordNiO.d.cts} +11 -7
- package/dist/{types-CzdN7rUe.d.ts → types-DZordNiO.d.ts} +11 -7
- package/dist/types-I67mddpr.d.cts +78 -0
- package/dist/types-I67mddpr.d.ts +78 -0
- package/dist/{types-BBwg4Evw.d.cts → types-KtOPPzPX.d.cts} +1 -1
- package/dist/utils/index.cjs +2 -2
- package/dist/utils/index.d.cts +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +1 -1
- package/package.json +6 -4
- package/src/charts/bar-chart/bar-chart.tsx +4 -3
- package/src/charts/bar-chart/test/bar-chart.test.tsx +30 -0
- package/src/charts/conversion-funnel-chart/test/conversion-funnel-chart.test.tsx +2 -2
- package/src/charts/leaderboard-chart/hooks/use-leaderboard-legend-items.ts +0 -2
- package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +9 -10
- package/src/charts/leaderboard-chart/leaderboard-chart.tsx +95 -70
- package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +58 -29
- package/src/charts/leaderboard-chart/test/use-leaderboard-legend-items.test.tsx +2 -5
- package/src/charts/leaderboard-chart/types.ts +4 -7
- package/src/charts/line-chart/line-chart.tsx +2 -3
- package/src/charts/pie-chart/pie-chart.tsx +2 -3
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +2 -3
- package/src/components/legend/index.ts +8 -1
- package/src/components/legend/private/base-legend.tsx +32 -22
- package/src/components/legend/test/legend.test.tsx +148 -52
- package/src/components/legend/types.ts +42 -16
- package/src/hooks/test/use-zero-value-display.test.tsx +206 -0
- package/src/hooks/use-zero-value-display.ts +52 -23
- package/src/index.ts +7 -1
- package/src/providers/chart-context/test/chart-context.test.tsx +12 -6
- package/src/providers/chart-context/themes.ts +6 -4
- package/src/types.ts +11 -7
- package/src/utils/get-styles.ts +1 -1
- package/src/utils/test/get-styles.test.ts +12 -10
- package/dist/chunk-55ZCOYDF.cjs.map +0 -1
- package/dist/chunk-7FDQGBY7.js.map +0 -1
- package/dist/chunk-BXFD7JIG.cjs.map +0 -1
- package/dist/chunk-IS5YYLTV.js.map +0 -1
- package/dist/chunk-KNIMXN6Z.js +0 -51
- package/dist/chunk-KNIMXN6Z.js.map +0 -1
- package/dist/chunk-NQJE2CC7.cjs.map +0 -1
- package/dist/chunk-O2JIANHK.cjs.map +0 -1
- package/dist/chunk-OMS5QIJN.js.map +0 -1
- package/dist/chunk-RFSHE3HL.js.map +0 -1
- package/dist/chunk-SSFFCBCF.js.map +0 -1
- package/dist/chunk-SUDERBUA.cjs +0 -51
- package/dist/chunk-SUDERBUA.cjs.map +0 -1
- package/dist/chunk-TE63Y5PX.js.map +0 -1
- package/dist/chunk-UFRBUT2D.cjs.map +0 -1
- package/dist/chunk-VPAEBI2F.js.map +0 -1
- package/dist/chunk-X7JL2NYJ.cjs.map +0 -1
- package/dist/chunk-XD2HV7M5.js.map +0 -1
- package/dist/chunk-YDVHT7GS.cjs.map +0 -1
- package/dist/types-C05PdDJa.d.cts +0 -57
- package/dist/types-C05PdDJa.d.ts +0 -57
- /package/dist/{chunk-KHQPN77E.js.map → chunk-CZGYJKG6.js.map} +0 -0
- /package/dist/{chunk-MDRCAGKZ.js.map → chunk-H2V4JMSA.js.map} +0 -0
- /package/dist/{chunk-GWBS65VC.js.map → chunk-IU4DYUAV.js.map} +0 -0
- /package/dist/{chunk-E62LCBGD.js.map → chunk-PXLEMUGJ.js.map} +0 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { renderHook } from '@testing-library/react';
|
|
2
|
+
import { useZeroValueDisplay } from '../use-zero-value-display';
|
|
3
|
+
import type { SeriesData } from '../../types';
|
|
4
|
+
|
|
5
|
+
describe( 'useZeroValueDisplay', () => {
|
|
6
|
+
const mockData: SeriesData[] = [
|
|
7
|
+
{
|
|
8
|
+
label: 'Series 1',
|
|
9
|
+
data: [
|
|
10
|
+
{ label: 'A', value: 0 },
|
|
11
|
+
{ label: 'B', value: 100 },
|
|
12
|
+
{ label: 'C', value: 200 },
|
|
13
|
+
],
|
|
14
|
+
},
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
test( 'returns original data when disabled', () => {
|
|
18
|
+
const { result } = renderHook( () =>
|
|
19
|
+
useZeroValueDisplay( mockData, { enabled: false, valueAxisLength: 100 } )
|
|
20
|
+
);
|
|
21
|
+
expect( result.current ).toBe( mockData );
|
|
22
|
+
} );
|
|
23
|
+
|
|
24
|
+
test( 'returns original data when valueAxisLength is not provided', () => {
|
|
25
|
+
const { result } = renderHook( () => useZeroValueDisplay( mockData, { enabled: true } ) );
|
|
26
|
+
expect( result.current ).toBe( mockData );
|
|
27
|
+
} );
|
|
28
|
+
|
|
29
|
+
test( 'adds visualValue for zero values', () => {
|
|
30
|
+
const { result } = renderHook( () =>
|
|
31
|
+
useZeroValueDisplay( mockData, { enabled: true, valueAxisLength: 100 } )
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const enhancedData = result.current;
|
|
35
|
+
expect( enhancedData[ 0 ].data[ 0 ] ).toHaveProperty( 'visualValue' );
|
|
36
|
+
expect(
|
|
37
|
+
( enhancedData[ 0 ].data[ 0 ] as { visualValue?: number } ).visualValue
|
|
38
|
+
).toBeGreaterThan( 0 );
|
|
39
|
+
} );
|
|
40
|
+
|
|
41
|
+
test( 'adds visualValue for near-zero values that would render below minimum', () => {
|
|
42
|
+
const data: SeriesData[] = [
|
|
43
|
+
{
|
|
44
|
+
label: 'Series 1',
|
|
45
|
+
data: [
|
|
46
|
+
{ label: 'A', value: 1 }, // Would render as 1px (below 3px minimum)
|
|
47
|
+
{ label: 'B', value: 100 },
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
// With axis=100 and max=100, 3px threshold = 3
|
|
53
|
+
// Value of 1 < 3, so it gets boosted to 3
|
|
54
|
+
const { result } = renderHook( () =>
|
|
55
|
+
useZeroValueDisplay( data, { enabled: true, valueAxisLength: 100 } )
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const enhancedData = result.current;
|
|
59
|
+
expect( enhancedData[ 0 ].data[ 0 ] ).toHaveProperty( 'visualValue' );
|
|
60
|
+
expect( ( enhancedData[ 0 ].data[ 0 ] as { visualValue?: number } ).visualValue ).toBe( 3 );
|
|
61
|
+
} );
|
|
62
|
+
|
|
63
|
+
test( 'does not add visualValue for values above minimum threshold', () => {
|
|
64
|
+
const data: SeriesData[] = [
|
|
65
|
+
{
|
|
66
|
+
label: 'Series 1',
|
|
67
|
+
data: [
|
|
68
|
+
{ label: 'A', value: 10 }, // Would render as 10px (above 3px minimum)
|
|
69
|
+
{ label: 'B', value: 100 },
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
const { result } = renderHook( () =>
|
|
75
|
+
useZeroValueDisplay( data, { enabled: true, valueAxisLength: 100 } )
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const enhancedData = result.current;
|
|
79
|
+
expect( enhancedData[ 0 ].data[ 0 ] ).not.toHaveProperty( 'visualValue' );
|
|
80
|
+
expect( enhancedData[ 0 ].data[ 1 ] ).not.toHaveProperty( 'visualValue' );
|
|
81
|
+
} );
|
|
82
|
+
|
|
83
|
+
test( 'zero values get 2px equivalent (1px less than near-zero)', () => {
|
|
84
|
+
// mockData has values [0, 100, 200], max = 200
|
|
85
|
+
// zeroVisualValue = (2 / 100) * 200 = 4
|
|
86
|
+
const { result } = renderHook( () =>
|
|
87
|
+
useZeroValueDisplay( mockData, { enabled: true, valueAxisLength: 100 } )
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const visualValue = ( result.current[ 0 ].data[ 0 ] as { visualValue?: number } ).visualValue;
|
|
91
|
+
expect( visualValue ).toBe( 4 ); // 2px equivalent
|
|
92
|
+
} );
|
|
93
|
+
|
|
94
|
+
test( 'near-zero values get 3px equivalent', () => {
|
|
95
|
+
const data: SeriesData[] = [
|
|
96
|
+
{
|
|
97
|
+
label: 'Series 1',
|
|
98
|
+
data: [
|
|
99
|
+
{ label: 'A', value: 1 }, // Would render as 1px
|
|
100
|
+
{ label: 'B', value: 100 },
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
// minNonZeroValue = (3 / 100) * 100 = 3
|
|
106
|
+
const { result } = renderHook( () =>
|
|
107
|
+
useZeroValueDisplay( data, { enabled: true, valueAxisLength: 100 } )
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const visualValue = ( result.current[ 0 ].data[ 0 ] as { visualValue?: number } ).visualValue;
|
|
111
|
+
expect( visualValue ).toBe( 3 ); // 3px equivalent
|
|
112
|
+
} );
|
|
113
|
+
|
|
114
|
+
test( 'zeros and near-zeros have 1px visual difference', () => {
|
|
115
|
+
const data: SeriesData[] = [
|
|
116
|
+
{
|
|
117
|
+
label: 'Series 1',
|
|
118
|
+
data: [
|
|
119
|
+
{ label: 'Zero', value: 0 },
|
|
120
|
+
{ label: 'NearZero', value: 1 },
|
|
121
|
+
{ label: 'Max', value: 100 },
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
const { result } = renderHook( () =>
|
|
127
|
+
useZeroValueDisplay( data, { enabled: true, valueAxisLength: 100 } )
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const zeroVisual = ( result.current[ 0 ].data[ 0 ] as { visualValue?: number } ).visualValue;
|
|
131
|
+
const nearZeroVisual = ( result.current[ 0 ].data[ 1 ] as { visualValue?: number } )
|
|
132
|
+
.visualValue;
|
|
133
|
+
|
|
134
|
+
// Zero = 2px equivalent (2), near-zero = 3px equivalent (3)
|
|
135
|
+
expect( zeroVisual ).toBe( 2 );
|
|
136
|
+
expect( nearZeroVisual ).toBe( 3 );
|
|
137
|
+
expect( nearZeroVisual! - zeroVisual! ).toBe( 1 ); // 1px difference
|
|
138
|
+
} );
|
|
139
|
+
|
|
140
|
+
test( 'handles data with only zero values', () => {
|
|
141
|
+
const zeroOnlyData: SeriesData[] = [
|
|
142
|
+
{
|
|
143
|
+
label: 'Series 1',
|
|
144
|
+
data: [
|
|
145
|
+
{ label: 'A', value: 0 },
|
|
146
|
+
{ label: 'B', value: 0 },
|
|
147
|
+
],
|
|
148
|
+
},
|
|
149
|
+
];
|
|
150
|
+
|
|
151
|
+
const { result } = renderHook( () =>
|
|
152
|
+
useZeroValueDisplay( zeroOnlyData, { enabled: true, valueAxisLength: 100 } )
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
// Should return original data since there are no non-zero values to calculate from
|
|
156
|
+
expect( result.current ).toBe( zeroOnlyData );
|
|
157
|
+
} );
|
|
158
|
+
|
|
159
|
+
test( 'negative near-zero values preserve sign when boosted', () => {
|
|
160
|
+
const data: SeriesData[] = [
|
|
161
|
+
{
|
|
162
|
+
label: 'Series 1',
|
|
163
|
+
data: [
|
|
164
|
+
{ label: 'NegativeNearZero', value: -1 }, // Would render as 1px (below 3px minimum)
|
|
165
|
+
{ label: 'NegativeLarge', value: -100 },
|
|
166
|
+
{ label: 'PositiveMax', value: 100 },
|
|
167
|
+
],
|
|
168
|
+
},
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
const { result } = renderHook( () =>
|
|
172
|
+
useZeroValueDisplay( data, { enabled: true, valueAxisLength: 100 } )
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
const negativeNearZero = result.current[ 0 ].data[ 0 ] as { visualValue?: number };
|
|
176
|
+
|
|
177
|
+
// Should have a boosted magnitude but keep the negative sign
|
|
178
|
+
expect( negativeNearZero.visualValue ).toBeLessThan( 0 );
|
|
179
|
+
expect( Math.abs( negativeNearZero.visualValue! ) ).toBeGreaterThan( 1 );
|
|
180
|
+
} );
|
|
181
|
+
|
|
182
|
+
test( 'null values remain untouched without visualValue', () => {
|
|
183
|
+
const data: SeriesData[] = [
|
|
184
|
+
{
|
|
185
|
+
label: 'Series 1',
|
|
186
|
+
data: [
|
|
187
|
+
{ label: 'NullValue', value: null },
|
|
188
|
+
{ label: 'NonZero', value: 50 },
|
|
189
|
+
],
|
|
190
|
+
},
|
|
191
|
+
];
|
|
192
|
+
|
|
193
|
+
const { result } = renderHook( () =>
|
|
194
|
+
useZeroValueDisplay( data, { enabled: true, valueAxisLength: 100 } )
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
const nullPoint = result.current[ 0 ].data[ 0 ] as {
|
|
198
|
+
value: number | null;
|
|
199
|
+
visualValue?: number;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// The original null value should be preserved and no visualValue should be added
|
|
203
|
+
expect( nullPoint.value ).toBeNull();
|
|
204
|
+
expect( nullPoint ).not.toHaveProperty( 'visualValue' );
|
|
205
|
+
} );
|
|
206
|
+
} );
|
|
@@ -11,57 +11,86 @@ export interface EnhancedSeriesData extends Omit< SeriesData, 'data' > {
|
|
|
11
11
|
|
|
12
12
|
export interface UseZeroValueDisplayOptions {
|
|
13
13
|
enabled: boolean;
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
/**
|
|
15
|
+
* The pixel length of the value axis (height for vertical charts, width for
|
|
16
|
+
* horizontal charts). Used to calculate a minimum visible value that ensures
|
|
17
|
+
* zero-value bars are at least MIN_PIXEL_HEIGHT pixels tall along that axis.
|
|
18
|
+
*/
|
|
19
|
+
valueAxisLength?: number;
|
|
16
20
|
}
|
|
17
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Minimum pixel size for near-zero bars (non-zero values that would render too small).
|
|
24
|
+
* Using 3px to be visible but not misleading - larger values might look like actual data.
|
|
25
|
+
*/
|
|
26
|
+
const MIN_PIXEL_SIZE = 3;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Pixel size for zero-value bars (1px less than near-zero to be visually distinguishable).
|
|
30
|
+
*/
|
|
31
|
+
const ZERO_PIXEL_SIZE = MIN_PIXEL_SIZE - 1;
|
|
32
|
+
|
|
18
33
|
export const useZeroValueDisplay = (
|
|
19
34
|
data: SeriesData[],
|
|
20
35
|
options: UseZeroValueDisplayOptions = { enabled: false }
|
|
21
36
|
): SeriesData[] | EnhancedSeriesData[] => {
|
|
22
|
-
const { enabled,
|
|
37
|
+
const { enabled, valueAxisLength } = options;
|
|
23
38
|
|
|
24
39
|
return useMemo( () => {
|
|
25
|
-
if ( ! enabled ) return data;
|
|
26
|
-
|
|
27
|
-
// Collect all non-zero, non-null values (both positive and negative)
|
|
28
|
-
const nonZeroValues: number[] = [];
|
|
40
|
+
if ( ! enabled || ! valueAxisLength || valueAxisLength <= 0 ) return data;
|
|
29
41
|
|
|
42
|
+
// Find max absolute value
|
|
43
|
+
let maxAbsoluteValue = 0;
|
|
30
44
|
for ( const series of data ) {
|
|
31
45
|
for ( const point of series.data ) {
|
|
32
46
|
if ( point.value !== null && point.value !== 0 ) {
|
|
33
|
-
|
|
47
|
+
maxAbsoluteValue = Math.max( maxAbsoluteValue, Math.abs( point.value ) );
|
|
34
48
|
}
|
|
35
49
|
}
|
|
36
50
|
}
|
|
37
51
|
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
// Convert to absolute values to find the range
|
|
41
|
-
const absoluteValues = nonZeroValues.map( Math.abs );
|
|
52
|
+
if ( maxAbsoluteValue === 0 ) return data;
|
|
42
53
|
|
|
43
|
-
// Calculate
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
54
|
+
// Calculate values that render as specific pixel sizes, clamped to maxAbsoluteValue
|
|
55
|
+
// to prevent visual distortion when valueAxisLength is very small
|
|
56
|
+
const minNonZeroValue = Math.min(
|
|
57
|
+
( MIN_PIXEL_SIZE / valueAxisLength ) * maxAbsoluteValue,
|
|
58
|
+
maxAbsoluteValue
|
|
59
|
+
);
|
|
60
|
+
const zeroVisualValue = Math.min(
|
|
61
|
+
( ZERO_PIXEL_SIZE / valueAxisLength ) * maxAbsoluteValue,
|
|
62
|
+
maxAbsoluteValue
|
|
51
63
|
);
|
|
52
64
|
|
|
53
65
|
return data.map( series => ( {
|
|
54
66
|
...series,
|
|
55
|
-
data: series.data.map( ( point ): EnhancedDataPoint => {
|
|
67
|
+
data: series.data.map( ( point: DataPointDate ): EnhancedDataPoint => {
|
|
68
|
+
// Zero values get a smaller visual representation (2px)
|
|
56
69
|
if ( point.value === 0 ) {
|
|
57
70
|
return {
|
|
58
71
|
...point,
|
|
59
|
-
visualValue:
|
|
72
|
+
visualValue: zeroVisualValue,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Null values should remain untouched
|
|
77
|
+
if ( point.value === null ) {
|
|
78
|
+
return point;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const absValue = Math.abs( point.value );
|
|
82
|
+
|
|
83
|
+
// Near-zero values that would render below MIN_PIXEL_SIZE get boosted to 3px
|
|
84
|
+
// Preserve the sign for negative values
|
|
85
|
+
if ( absValue < minNonZeroValue ) {
|
|
86
|
+
return {
|
|
87
|
+
...point,
|
|
88
|
+
visualValue: Math.sign( point.value ) * minNonZeroValue,
|
|
60
89
|
};
|
|
61
90
|
}
|
|
62
91
|
|
|
63
92
|
return point;
|
|
64
93
|
} ),
|
|
65
94
|
} ) );
|
|
66
|
-
}, [ data, enabled,
|
|
95
|
+
}, [ data, enabled, valueAxisLength ] );
|
|
67
96
|
};
|
package/src/index.ts
CHANGED
|
@@ -37,7 +37,13 @@ export type {
|
|
|
37
37
|
PieSemiCircleChartRenderTooltipParams,
|
|
38
38
|
} from './charts/pie-semi-circle-chart';
|
|
39
39
|
export type { GeoChartProps, GeoRegion, GeoResolution } from './charts/geo-chart';
|
|
40
|
-
export type {
|
|
40
|
+
export type {
|
|
41
|
+
LegendValueDisplay,
|
|
42
|
+
BaseLegendItem,
|
|
43
|
+
LegendItemStyles,
|
|
44
|
+
LegendLabelStyles,
|
|
45
|
+
LegendShapeStyles,
|
|
46
|
+
} from './components/legend';
|
|
41
47
|
export type { TrendIndicatorProps, TrendDirection } from './components/trend-indicator';
|
|
42
48
|
export type { LineStyles, GridStyles, EventHandlerParams } from '@visx/xychart';
|
|
43
49
|
export type {
|
|
@@ -987,10 +987,12 @@ describe( 'ChartContext', () => {
|
|
|
987
987
|
|
|
988
988
|
const extendedTheme = {
|
|
989
989
|
...mockTheme,
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
990
|
+
legend: {
|
|
991
|
+
shapeStyles: [
|
|
992
|
+
{ fill: '#LEGEND1', stroke: '#BORDER1' },
|
|
993
|
+
{ fill: '#LEGEND2', strokeWidth: 3 },
|
|
994
|
+
],
|
|
995
|
+
},
|
|
994
996
|
};
|
|
995
997
|
|
|
996
998
|
render(
|
|
@@ -1087,7 +1089,9 @@ describe( 'ChartContext', () => {
|
|
|
1087
1089
|
},
|
|
1088
1090
|
},
|
|
1089
1091
|
},
|
|
1090
|
-
|
|
1092
|
+
legend: {
|
|
1093
|
+
shapeStyles: [ { fill: '#LEGEND1', stroke: '#BORDER1' } ],
|
|
1094
|
+
},
|
|
1091
1095
|
};
|
|
1092
1096
|
|
|
1093
1097
|
render(
|
|
@@ -1234,7 +1238,9 @@ describe( 'ChartContext', () => {
|
|
|
1234
1238
|
...mockTheme,
|
|
1235
1239
|
glyphs: [ mockGlyph ],
|
|
1236
1240
|
seriesLineStyles: [ { strokeWidth: 2 } ],
|
|
1237
|
-
|
|
1241
|
+
legend: {
|
|
1242
|
+
shapeStyles: [ { fill: '#SHAPE1' } ],
|
|
1243
|
+
},
|
|
1238
1244
|
};
|
|
1239
1245
|
|
|
1240
1246
|
render(
|
|
@@ -17,12 +17,14 @@ const defaultTheme: CompleteChartTheme = {
|
|
|
17
17
|
gridColorDark: '',
|
|
18
18
|
xTickLineStyles: { stroke: 'black' },
|
|
19
19
|
xAxisLineStyles: { stroke: '#DCDCDE', strokeWidth: 1 },
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
legend: {
|
|
21
|
+
labelStyles: {
|
|
22
|
+
color: 'var(--jp-gray-80, #2c3338)',
|
|
23
|
+
},
|
|
24
|
+
containerStyles: {},
|
|
25
|
+
shapeStyles: [],
|
|
22
26
|
},
|
|
23
|
-
legendContainerStyles: {},
|
|
24
27
|
seriesLineStyles: [],
|
|
25
|
-
legendShapeStyles: [],
|
|
26
28
|
glyphs: [],
|
|
27
29
|
svgLabelSmall: { fill: 'var(--jp-gray-80, #2c3338)' },
|
|
28
30
|
annotationStyles: {
|
package/src/types.ts
CHANGED
|
@@ -210,14 +210,17 @@ export type ChartTheme = {
|
|
|
210
210
|
xAxisLineStyles?: LineStyles;
|
|
211
211
|
/** Styles for series lines */
|
|
212
212
|
seriesLineStyles?: LineStyles[];
|
|
213
|
-
/** Styles for legend shapes */
|
|
214
|
-
legendShapeStyles?: Record< string, unknown >[];
|
|
215
213
|
/** Array of render functions for glyphs */
|
|
216
214
|
glyphs?: Array< < Datum extends object >( props: GlyphProps< Datum > ) => ReactNode >;
|
|
217
|
-
/**
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
215
|
+
/** Legend specific settings */
|
|
216
|
+
legend?: {
|
|
217
|
+
/** Styles for legend shapes */
|
|
218
|
+
shapeStyles?: Record< string, unknown >[];
|
|
219
|
+
/** Styles for legend labels */
|
|
220
|
+
labelStyles?: CSSProperties;
|
|
221
|
+
/** Styles for legend container */
|
|
222
|
+
containerStyles?: CSSProperties;
|
|
223
|
+
};
|
|
221
224
|
/** Styles for small SVG text (eg. axis tick labels), passed through to the XYChart theme. */
|
|
222
225
|
svgLabelSmall?: TextProps;
|
|
223
226
|
annotationStyles?: AnnotationStyles;
|
|
@@ -287,6 +290,7 @@ export type CompleteChartTheme = Required< ChartTheme > & {
|
|
|
287
290
|
lineChart: {
|
|
288
291
|
lineStyles: Record< NonNullable< SeriesDataOptions[ 'type' ] >, LineStyles >;
|
|
289
292
|
};
|
|
293
|
+
legend: Required< NonNullable< ChartTheme[ 'legend' ] > >;
|
|
290
294
|
sparkline: Required< NonNullable< ChartTheme[ 'sparkline' ] > > & {
|
|
291
295
|
margin: Required< NonNullable< ChartTheme[ 'sparkline' ] >[ 'margin' ] >;
|
|
292
296
|
};
|
|
@@ -446,7 +450,7 @@ export type BaseChartProps< T = DataPoint | DataPointDate | LeaderboardEntry > =
|
|
|
446
450
|
legendItemClassName?: string;
|
|
447
451
|
/**
|
|
448
452
|
* Enable interactive legend items that can toggle series visibility.
|
|
449
|
-
* Supported for
|
|
453
|
+
* Supported for all chart types that render series.
|
|
450
454
|
* Requires chartId and GlobalChartsProvider.
|
|
451
455
|
* For pie charts, percentages are recalculated so visible segments total 100%.
|
|
452
456
|
*/
|
package/src/utils/get-styles.ts
CHANGED
|
@@ -62,7 +62,7 @@ export function getItemShapeStyles(
|
|
|
62
62
|
): Record< string, unknown > {
|
|
63
63
|
const seriesShapeStyles = series.options?.legendShapeStyle ?? {};
|
|
64
64
|
const lineStyles = legendShape === 'line' ? getSeriesLineStyles( series, index, theme ) : {};
|
|
65
|
-
const themeShapeStyles = theme.
|
|
65
|
+
const themeShapeStyles = theme.legend?.shapeStyles?.[ index ];
|
|
66
66
|
|
|
67
67
|
const itemShapeStyles = {
|
|
68
68
|
...seriesShapeStyles,
|
|
@@ -20,10 +20,12 @@ describe( 'Series styling utility functions', () => {
|
|
|
20
20
|
},
|
|
21
21
|
},
|
|
22
22
|
seriesLineStyles: [ { strokeWidth: 2 }, { strokeWidth: 3, strokeDasharray: '2 2' } ],
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
legend: {
|
|
24
|
+
shapeStyles: [
|
|
25
|
+
{ fill: '#LEGEND1', stroke: '#BORDER1' },
|
|
26
|
+
{ fill: '#LEGEND2', strokeWidth: 3 },
|
|
27
|
+
],
|
|
28
|
+
},
|
|
27
29
|
};
|
|
28
30
|
|
|
29
31
|
describe( 'getSeriesStroke', () => {
|
|
@@ -152,7 +154,7 @@ describe( 'Series styling utility functions', () => {
|
|
|
152
154
|
};
|
|
153
155
|
|
|
154
156
|
const result = getItemShapeStyles( comparisonSeries, 0, mockTheme as ChartTheme, 'rect' );
|
|
155
|
-
expect( result ).toEqual( mockTheme.
|
|
157
|
+
expect( result ).toEqual( mockTheme.legend.shapeStyles[ 0 ] );
|
|
156
158
|
} );
|
|
157
159
|
|
|
158
160
|
it( 'merges custom shape styles with line styles for line shape', () => {
|
|
@@ -186,13 +188,13 @@ describe( 'Series styling utility functions', () => {
|
|
|
186
188
|
mockTheme as ChartTheme,
|
|
187
189
|
'rect'
|
|
188
190
|
);
|
|
189
|
-
expect( result ).toEqual( mockTheme.
|
|
191
|
+
expect( result ).toEqual( mockTheme.legend.shapeStyles[ 1 ] );
|
|
190
192
|
} );
|
|
191
193
|
|
|
192
194
|
it( 'returns empty object when no theme shape styles and no meaningful custom styles', () => {
|
|
193
195
|
const themeWithoutShapeStyles = {
|
|
194
196
|
...mockTheme,
|
|
195
|
-
|
|
197
|
+
legend: { shapeStyles: undefined },
|
|
196
198
|
} as ChartTheme;
|
|
197
199
|
|
|
198
200
|
const result = getItemShapeStyles( mockSeriesData, 0, themeWithoutShapeStyles, 'rect' );
|
|
@@ -254,7 +256,7 @@ describe( 'Series styling utility functions', () => {
|
|
|
254
256
|
|
|
255
257
|
it( 'works without legendShape parameter', () => {
|
|
256
258
|
const result = getItemShapeStyles( mockSeriesData, 0, mockTheme as ChartTheme );
|
|
257
|
-
expect( result ).toEqual( mockTheme.
|
|
259
|
+
expect( result ).toEqual( mockTheme.legend.shapeStyles[ 0 ] );
|
|
258
260
|
} );
|
|
259
261
|
|
|
260
262
|
it( 'handles other legendShape string values like "circle"', () => {
|
|
@@ -265,7 +267,7 @@ describe( 'Series styling utility functions', () => {
|
|
|
265
267
|
|
|
266
268
|
const result = getItemShapeStyles( comparisonSeries, 0, mockTheme as ChartTheme, 'circle' );
|
|
267
269
|
// Should not include line styles for circle shape
|
|
268
|
-
expect( result ).toEqual( mockTheme.
|
|
270
|
+
expect( result ).toEqual( mockTheme.legend.shapeStyles[ 0 ] );
|
|
269
271
|
} );
|
|
270
272
|
|
|
271
273
|
it( 'handles React component as legendShape parameter', () => {
|
|
@@ -282,7 +284,7 @@ describe( 'Series styling utility functions', () => {
|
|
|
282
284
|
CustomShape
|
|
283
285
|
);
|
|
284
286
|
// Should not include line styles for component shape
|
|
285
|
-
expect( result ).toEqual( mockTheme.
|
|
287
|
+
expect( result ).toEqual( mockTheme.legend.shapeStyles[ 0 ] );
|
|
286
288
|
} );
|
|
287
289
|
|
|
288
290
|
it( 'prioritizes series line styles over theme line styles when legendShape is line', () => {
|