@automattic/charts 0.56.7 → 0.58.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 +30 -0
- package/dist/charts/bar-chart/index.cjs +7 -6
- package/dist/charts/bar-chart/index.cjs.map +1 -1
- package/dist/charts/bar-chart/index.css +12 -24
- package/dist/charts/bar-chart/index.css.map +1 -1
- package/dist/charts/bar-chart/index.d.cts +3 -4
- package/dist/charts/bar-chart/index.d.ts +3 -4
- package/dist/charts/bar-chart/index.js +6 -5
- package/dist/charts/bar-list-chart/index.cjs +8 -7
- package/dist/charts/bar-list-chart/index.cjs.map +1 -1
- package/dist/charts/bar-list-chart/index.css +12 -24
- package/dist/charts/bar-list-chart/index.css.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 +7 -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.css +0 -94
- package/dist/charts/conversion-funnel-chart/index.css.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.css +0 -94
- package/dist/charts/geo-chart/index.css.map +1 -1
- 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 +7 -6
- package/dist/charts/leaderboard-chart/index.cjs.map +1 -1
- package/dist/charts/leaderboard-chart/index.css +20 -33
- 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 +6 -5
- package/dist/charts/line-chart/index.cjs +7 -6
- package/dist/charts/line-chart/index.cjs.map +1 -1
- package/dist/charts/line-chart/index.css +12 -24
- package/dist/charts/line-chart/index.css.map +1 -1
- package/dist/charts/line-chart/index.d.cts +3 -4
- package/dist/charts/line-chart/index.d.ts +3 -4
- package/dist/charts/line-chart/index.js +6 -5
- package/dist/charts/pie-chart/index.cjs +7 -7
- package/dist/charts/pie-chart/index.css +12 -24
- package/dist/charts/pie-chart/index.css.map +1 -1
- package/dist/charts/pie-chart/index.d.cts +7 -13
- package/dist/charts/pie-chart/index.d.ts +7 -13
- package/dist/charts/pie-chart/index.js +6 -6
- package/dist/charts/pie-semi-circle-chart/index.cjs +7 -7
- package/dist/charts/pie-semi-circle-chart/index.css +12 -24
- package/dist/charts/pie-semi-circle-chart/index.css.map +1 -1
- package/dist/charts/pie-semi-circle-chart/index.d.cts +7 -13
- package/dist/charts/pie-semi-circle-chart/index.d.ts +7 -13
- package/dist/charts/pie-semi-circle-chart/index.js +6 -6
- package/dist/charts/sparkline/index.cjs +8 -7
- package/dist/charts/sparkline/index.cjs.map +1 -1
- package/dist/charts/sparkline/index.css +12 -24
- package/dist/charts/sparkline/index.css.map +1 -1
- package/dist/charts/sparkline/index.js +7 -6
- package/dist/{chunk-RFSHE3HL.js → chunk-2I67QUIV.js} +84 -431
- package/dist/chunk-2I67QUIV.js.map +1 -0
- package/dist/{chunk-OMS5QIJN.js → chunk-2ICEEQOC.js} +31 -25
- package/dist/chunk-2ICEEQOC.js.map +1 -0
- package/dist/{chunk-GWBS65VC.js → chunk-4B7BL2DD.js} +3 -3
- package/dist/{chunk-7FDQGBY7.js → chunk-4OXMTKAL.js} +24 -24
- package/dist/chunk-4OXMTKAL.js.map +1 -0
- package/dist/{chunk-SSFFCBCF.js → chunk-B6NLZFRW.js} +32 -26
- package/dist/chunk-B6NLZFRW.js.map +1 -0
- package/dist/{chunk-3EXJP67N.cjs → chunk-BBAUQOW6.cjs} +9 -9
- package/dist/{chunk-3EXJP67N.cjs.map → chunk-BBAUQOW6.cjs.map} +1 -1
- package/dist/{chunk-NQJE2CC7.cjs → chunk-CMMHCTBX.cjs} +45 -45
- package/dist/chunk-CMMHCTBX.cjs.map +1 -0
- package/dist/{chunk-O2JIANHK.cjs → chunk-CPPXJATQ.cjs} +51 -45
- package/dist/chunk-CPPXJATQ.cjs.map +1 -0
- package/dist/{chunk-MDRCAGKZ.js → chunk-DKU775VC.js} +3 -3
- package/dist/{chunk-BXFD7JIG.cjs → chunk-GRA7Y2ZG.cjs} +46 -46
- package/dist/chunk-GRA7Y2ZG.cjs.map +1 -0
- package/dist/{chunk-TE63Y5PX.js → chunk-JJIMABHT.js} +10 -3
- package/dist/chunk-JJIMABHT.js.map +1 -0
- package/dist/{chunk-KHQPN77E.js → chunk-KJHWXOCZ.js} +4 -4
- package/dist/{chunk-6CCZL2JJ.js → chunk-KRWGSOJ2.js} +30 -2
- package/dist/chunk-KRWGSOJ2.js.map +1 -0
- package/dist/{chunk-VPAEBI2F.js → chunk-LTFH7SEG.js} +24 -24
- package/dist/chunk-LTFH7SEG.js.map +1 -0
- package/dist/{chunk-E62LCBGD.js → chunk-MUNOKLLE.js} +3 -3
- package/dist/{chunk-ZVGEDXDP.cjs → chunk-MXGLYWVP.cjs} +10 -3
- package/dist/chunk-MXGLYWVP.cjs.map +1 -0
- package/dist/{chunk-55ZCOYDF.cjs → chunk-OYC34VTO.cjs} +252 -827
- package/dist/chunk-OYC34VTO.cjs.map +1 -0
- package/dist/{chunk-CAFJRZPZ.cjs → chunk-PQL5I3F6.cjs} +17 -17
- package/dist/{chunk-CAFJRZPZ.cjs.map → chunk-PQL5I3F6.cjs.map} +1 -1
- package/dist/{chunk-UFRBUT2D.cjs → chunk-REZTQ4PH.cjs} +87 -24
- package/dist/chunk-REZTQ4PH.cjs.map +1 -0
- package/dist/{chunk-RCY6XLGU.cjs → chunk-TZRUHQOH.cjs} +36 -8
- package/dist/chunk-TZRUHQOH.cjs.map +1 -0
- package/dist/{chunk-XD2HV7M5.js → chunk-UTYVIOWZ.js} +226 -801
- package/dist/chunk-UTYVIOWZ.js.map +1 -0
- package/dist/{chunk-YAXY5L7I.cjs → chunk-W2LDIX26.cjs} +5 -5
- package/dist/{chunk-YAXY5L7I.cjs.map → chunk-W2LDIX26.cjs.map} +1 -1
- package/dist/{chunk-K6TGILHX.cjs → chunk-WSG64BVN.cjs} +6 -6
- package/dist/{chunk-K6TGILHX.cjs.map → chunk-WSG64BVN.cjs.map} +1 -1
- package/dist/chunk-WTQYGUNF.js +400 -0
- package/dist/chunk-WTQYGUNF.js.map +1 -0
- package/dist/{chunk-YDVHT7GS.cjs → chunk-WYK7EL5R.cjs} +100 -447
- package/dist/chunk-WYK7EL5R.cjs.map +1 -0
- package/dist/{chunk-X7JL2NYJ.cjs → chunk-XC4KHJYX.cjs} +51 -45
- package/dist/chunk-XC4KHJYX.cjs.map +1 -0
- package/dist/chunk-XVBH5XHE.cjs +400 -0
- package/dist/chunk-XVBH5XHE.cjs.map +1 -0
- package/dist/{chunk-IS5YYLTV.js → chunk-YAFQVVDI.js} +85 -22
- package/dist/chunk-YAFQVVDI.js.map +1 -0
- package/dist/components/legend/index.cjs +4 -3
- package/dist/components/legend/index.cjs.map +1 -1
- package/dist/components/legend/index.css +12 -24
- package/dist/components/legend/index.css.map +1 -1
- package/dist/components/legend/index.d.cts +4 -4
- package/dist/components/legend/index.d.ts +4 -4
- package/dist/components/legend/index.js +3 -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 -5
- package/dist/hooks/index.cjs.map +1 -1
- package/dist/hooks/index.css +0 -94
- package/dist/hooks/index.css.map +1 -1
- package/dist/hooks/index.d.cts +9 -13
- package/dist/hooks/index.d.ts +9 -13
- package/dist/hooks/index.js +2 -4
- package/dist/index.cjs +18 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +20 -33
- package/dist/index.css.map +1 -1
- package/dist/index.d.cts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +17 -16
- package/dist/{leaderboard-chart-COtgamhe.d.cts → leaderboard-chart-BSbg0ufV.d.cts} +3 -11
- package/dist/{leaderboard-chart-BSgEw_Um.d.ts → leaderboard-chart-odEYxxEC.d.ts} +3 -11
- package/dist/{legend-C9ahiwOt.d.cts → legend-DFkosEvC.d.cts} +1 -1
- package/dist/{legend-jjMmhSg3.d.ts → legend-DLswHhOk.d.ts} +1 -1
- package/dist/providers/index.cjs +3 -3
- package/dist/providers/index.css +0 -94
- package/dist/providers/index.css.map +1 -1
- 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-CVR5rmIs.d.cts → themes-D0qc5JaW.d.cts} +2 -2
- package/dist/{themes-DQzmaSze.d.ts → themes-itO4Ht5g.d.ts} +2 -2
- package/dist/{types-BBwg4Evw.d.cts → types-B5f6XQ7Q.d.cts} +1 -1
- package/dist/{types-DQNnq5Fr.d.ts → types-BsHooDbM.d.ts} +1 -1
- package/dist/{types-C05PdDJa.d.cts → types-BuSrRM4p.d.ts} +15 -23
- package/dist/{types-CzdN7rUe.d.cts → types-ChOUI9-N.d.cts} +90 -46
- package/dist/{types-CzdN7rUe.d.ts → types-ChOUI9-N.d.ts} +90 -46
- package/dist/{types-C05PdDJa.d.ts → types-Dfw9VOKI.d.cts} +15 -23
- 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 +10 -8
- package/src/charts/bar-chart/bar-chart.tsx +19 -19
- package/src/charts/bar-chart/test/bar-chart.test.tsx +78 -31
- 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 +124 -102
- package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +61 -33
- package/src/charts/leaderboard-chart/test/use-leaderboard-legend-items.test.tsx +2 -5
- package/src/charts/leaderboard-chart/types.ts +2 -15
- package/src/charts/line-chart/line-chart.tsx +18 -17
- package/src/charts/line-chart/test/line-chart.test.tsx +49 -27
- package/src/charts/line-chart/types.ts +0 -1
- package/src/charts/pie-chart/pie-chart.tsx +23 -23
- package/src/charts/pie-chart/test/composition-api.test.tsx +41 -0
- package/src/charts/pie-chart/test/pie-chart.test.tsx +9 -9
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +21 -24
- package/src/charts/pie-semi-circle-chart/test/pie-semi-circle-chart.test.tsx +33 -5
- package/src/charts/private/chart-composition/index.ts +2 -0
- package/src/charts/private/chart-composition/render-legend-slot.ts +22 -0
- package/src/charts/private/chart-composition/test/render-legend-slot.test.tsx +60 -0
- package/src/charts/private/chart-composition/test/use-chart-children.test.tsx +91 -0
- package/src/charts/private/chart-composition/use-chart-children.ts +34 -2
- package/src/components/legend/private/base-legend.module.scss +19 -37
- package/src/components/legend/private/base-legend.tsx +32 -24
- package/src/components/legend/test/legend.test.tsx +148 -52
- package/src/components/legend/types.ts +23 -24
- package/src/hooks/index.ts +0 -1
- package/src/hooks/test/use-zero-value-display.test.tsx +206 -0
- package/src/hooks/use-zero-value-display.ts +52 -23
- 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 +93 -44
- package/src/utils/date-parsing.ts +10 -1
- package/src/utils/get-styles.ts +1 -1
- package/src/utils/test/date-parsing.test.ts +12 -0
- package/src/utils/test/get-styles.test.ts +12 -10
- package/src/utils/test/resolve-css-var.test.ts +2 -2
- package/tsup.config.ts +1 -1
- package/dist/chunk-55ZCOYDF.cjs.map +0 -1
- package/dist/chunk-6CCZL2JJ.js.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-RCY6XLGU.cjs.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/chunk-ZVGEDXDP.cjs.map +0 -1
- package/src/hooks/use-has-legend-child.ts +0 -22
- /package/dist/{chunk-GWBS65VC.js.map → chunk-4B7BL2DD.js.map} +0 -0
- /package/dist/{chunk-MDRCAGKZ.js.map → chunk-DKU775VC.js.map} +0 -0
- /package/dist/{chunk-KHQPN77E.js.map → chunk-KJHWXOCZ.js.map} +0 -0
- /package/dist/{chunk-E62LCBGD.js.map → chunk-MUNOKLLE.js.map} +0 -0
|
@@ -1,34 +1,33 @@
|
|
|
1
1
|
import { LegendOrdinal } from '@visx/legend';
|
|
2
|
+
import type {
|
|
3
|
+
LegendItemStyles,
|
|
4
|
+
LegendLabelStyles,
|
|
5
|
+
LegendPosition,
|
|
6
|
+
LegendShapeStyles,
|
|
7
|
+
} from '../../types';
|
|
2
8
|
import type { GlyphProps, LineStyles } from '@visx/xychart';
|
|
3
9
|
import type { ComponentProps, CSSProperties, ReactNode } from 'react';
|
|
4
10
|
|
|
5
|
-
|
|
6
|
-
|
|
11
|
+
type VisxLegendProps = Pick<
|
|
12
|
+
ComponentProps< typeof LegendOrdinal >,
|
|
13
|
+
'className' | 'shape' | 'fill' | 'size' | 'labelFormat' | 'labelTransform'
|
|
14
|
+
>;
|
|
7
15
|
|
|
8
|
-
export type BaseLegendProps =
|
|
16
|
+
export type BaseLegendProps = VisxLegendProps & {
|
|
9
17
|
items: BaseLegendItem[];
|
|
10
18
|
orientation?: 'horizontal' | 'vertical';
|
|
11
|
-
|
|
12
|
-
* TODO: Add 'left' | 'right' positioning support in future implementation
|
|
13
|
-
*/
|
|
14
|
-
position?: 'top' | 'bottom';
|
|
19
|
+
position?: LegendPosition;
|
|
15
20
|
alignment?: 'start' | 'center' | 'end';
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
textOverflow?: 'ellipsis' | 'wrap';
|
|
27
|
-
/**
|
|
28
|
-
* Additional CSS class name for legend items.
|
|
29
|
-
* This allows consumers to customize individual legend item styling.
|
|
30
|
-
*/
|
|
31
|
-
legendItemClassName?: string;
|
|
21
|
+
/** Additional CSS class name for legend items. */
|
|
22
|
+
itemClassName?: string;
|
|
23
|
+
/** CSS styles for each legend item (margin, flexDirection). */
|
|
24
|
+
itemStyles?: LegendItemStyles;
|
|
25
|
+
/** Additional CSS class name for legend labels. */
|
|
26
|
+
labelClassName?: string;
|
|
27
|
+
/** CSS styles for legend labels (justifyContent, flex, margin). */
|
|
28
|
+
labelStyles?: LegendLabelStyles;
|
|
29
|
+
/** Styles for legend shapes (width, height, margin). */
|
|
30
|
+
shapeStyles?: LegendShapeStyles;
|
|
32
31
|
/**
|
|
33
32
|
* Function for rendering a custom legend layout.
|
|
34
33
|
*/
|
|
@@ -51,7 +50,7 @@ export type LegendProps = Omit< BaseLegendProps, 'items' > & {
|
|
|
51
50
|
|
|
52
51
|
export type BaseLegendItem = {
|
|
53
52
|
label: string;
|
|
54
|
-
value
|
|
53
|
+
value?: number | string;
|
|
55
54
|
color: string;
|
|
56
55
|
glyphSize?: number;
|
|
57
56
|
renderGlyph?: < Datum extends object >( props: GlyphProps< Datum > ) => ReactNode;
|
package/src/hooks/index.ts
CHANGED
|
@@ -4,7 +4,6 @@ export { useXYChartTheme } from './use-xychart-theme';
|
|
|
4
4
|
export { useChartDataTransform } from './use-chart-data-transform';
|
|
5
5
|
export { useChartMargin } from './use-chart-margin';
|
|
6
6
|
export { useElementSize } from './use-element-size';
|
|
7
|
-
export { useHasLegendChild } from './use-has-legend-child';
|
|
8
7
|
export { useTextTruncation } from './use-text-truncation';
|
|
9
8
|
export { useZeroValueDisplay } from './use-zero-value-display';
|
|
10
9
|
export { useInteractiveLegendData } from './use-interactive-legend-data';
|
|
@@ -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
|
};
|
|
@@ -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
|
};
|
|
@@ -348,6 +352,86 @@ export type ScaleOptions = {
|
|
|
348
352
|
paddingOuter?: number;
|
|
349
353
|
};
|
|
350
354
|
|
|
355
|
+
export type LegendItemStyles = {
|
|
356
|
+
/** Margin around each legend item. */
|
|
357
|
+
margin?: CSSProperties[ 'margin' ];
|
|
358
|
+
/** Flex direction for items within each legend entry. */
|
|
359
|
+
flexDirection?: 'row' | 'row-reverse' | 'column' | 'column-reverse';
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
export type LegendLabelStyles = Pick< CSSProperties, 'justifyContent' | 'flex' | 'margin' > & {
|
|
363
|
+
/**
|
|
364
|
+
* Maximum width for legend label text as a CSS value (e.g. '200px', '50%', '10rem').
|
|
365
|
+
* When set, text overflow behavior is controlled by textOverflow.
|
|
366
|
+
*/
|
|
367
|
+
maxWidth?: string;
|
|
368
|
+
/**
|
|
369
|
+
* Controls how text behaves when it exceeds maxWidth.
|
|
370
|
+
* - 'ellipsis': Truncate with ellipsis (ideal for widgets/small devices)
|
|
371
|
+
* - 'wrap': Wrap text to multiple lines (default, ideal for larger displays)
|
|
372
|
+
*/
|
|
373
|
+
textOverflow?: 'ellipsis' | 'wrap';
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
export type LegendShapeStyles = {
|
|
377
|
+
/** Width of the legend shape in pixels. */
|
|
378
|
+
width?: number;
|
|
379
|
+
/** Height of the legend shape in pixels. */
|
|
380
|
+
height?: number;
|
|
381
|
+
/** Margin around the legend shape. */
|
|
382
|
+
margin?: CSSProperties[ 'margin' ];
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
/** Position of the legend relative to chart content. */
|
|
386
|
+
export type LegendPosition = 'top' | 'bottom';
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Configuration object for chart legend appearance and behavior.
|
|
390
|
+
* Consolidates all legend styling and layout props into a single structured object.
|
|
391
|
+
*/
|
|
392
|
+
export type ChartLegendConfig< T = DataPoint | DataPointDate | LeaderboardEntry > = {
|
|
393
|
+
/**
|
|
394
|
+
* Layout direction of legend items.
|
|
395
|
+
*/
|
|
396
|
+
orientation?: 'horizontal' | 'vertical';
|
|
397
|
+
/**
|
|
398
|
+
* Position of the legend relative to the chart.
|
|
399
|
+
* TODO: Add 'left' | 'right' positioning support in future implementation
|
|
400
|
+
*/
|
|
401
|
+
position?: LegendPosition;
|
|
402
|
+
/**
|
|
403
|
+
* Alignment of the legend within its position.
|
|
404
|
+
*/
|
|
405
|
+
alignment?: 'start' | 'center' | 'end';
|
|
406
|
+
/**
|
|
407
|
+
* Shape of the legend marker icon.
|
|
408
|
+
*/
|
|
409
|
+
shape?: LegendShape< T, number >;
|
|
410
|
+
/**
|
|
411
|
+
* Enable interactive legend items that can toggle series visibility.
|
|
412
|
+
* Supported for all chart types that render series.
|
|
413
|
+
* Requires chartId and GlobalChartsProvider.
|
|
414
|
+
* For pie charts, percentages are recalculated so visible segments total 100%.
|
|
415
|
+
*/
|
|
416
|
+
interactive?: boolean;
|
|
417
|
+
/**
|
|
418
|
+
* Additional CSS class name for individual legend items.
|
|
419
|
+
*/
|
|
420
|
+
itemClassName?: string;
|
|
421
|
+
/**
|
|
422
|
+
* CSS styles for each legend item (margin, flexDirection).
|
|
423
|
+
*/
|
|
424
|
+
itemStyles?: LegendItemStyles;
|
|
425
|
+
/**
|
|
426
|
+
* CSS styles for legend labels (maxWidth, textOverflow, justifyContent, flex, margin).
|
|
427
|
+
*/
|
|
428
|
+
labelStyles?: LegendLabelStyles;
|
|
429
|
+
/**
|
|
430
|
+
* Styles for legend shapes (width, height, margin).
|
|
431
|
+
*/
|
|
432
|
+
shapeStyles?: LegendShapeStyles;
|
|
433
|
+
};
|
|
434
|
+
|
|
351
435
|
/**
|
|
352
436
|
* Base properties shared across all chart components
|
|
353
437
|
*/
|
|
@@ -412,45 +496,10 @@ export type BaseChartProps< T = DataPoint | DataPointDate | LeaderboardEntry > =
|
|
|
412
496
|
*/
|
|
413
497
|
showLegend?: boolean;
|
|
414
498
|
/**
|
|
415
|
-
* Legend
|
|
416
|
-
|
|
417
|
-
legendOrientation?: 'horizontal' | 'vertical';
|
|
418
|
-
/**
|
|
419
|
-
* Legend shape
|
|
420
|
-
*/
|
|
421
|
-
legendShape?: LegendShape< T, number >;
|
|
422
|
-
/**
|
|
423
|
-
* Legend position (where the legend appears)
|
|
424
|
-
* TODO: Add 'left' | 'right' positioning support in future implementation
|
|
425
|
-
*/
|
|
426
|
-
legendPosition?: 'top' | 'bottom';
|
|
427
|
-
/**
|
|
428
|
-
* Legend alignment within its position
|
|
429
|
-
*/
|
|
430
|
-
legendAlignment?: 'start' | 'center' | 'end';
|
|
431
|
-
/**
|
|
432
|
-
* Maximum width for legend items. When set, text overflow behavior is controlled by legendTextOverflow.
|
|
433
|
-
* Should be a CSS value string (e.g. '200px', '50%', '10rem')
|
|
434
|
-
*/
|
|
435
|
-
legendMaxWidth?: string;
|
|
436
|
-
/**
|
|
437
|
-
* Controls how text behaves when it exceeds legendMaxWidth.
|
|
438
|
-
* - 'ellipsis': Truncate with ellipsis (ideal for widgets/small devices)
|
|
439
|
-
* - 'wrap': Wrap text to multiple lines (default, ideal for larger displays)
|
|
440
|
-
*/
|
|
441
|
-
legendTextOverflow?: 'ellipsis' | 'wrap';
|
|
442
|
-
/**
|
|
443
|
-
* Additional CSS class name for legend items.
|
|
444
|
-
* This allows consumers to customize individual legend item styling.
|
|
445
|
-
*/
|
|
446
|
-
legendItemClassName?: string;
|
|
447
|
-
/**
|
|
448
|
-
* Enable interactive legend items that can toggle series visibility.
|
|
449
|
-
* Supported for LineChart, PieChart, and PieSemiCircleChart.
|
|
450
|
-
* Requires chartId and GlobalChartsProvider.
|
|
451
|
-
* For pie charts, percentages are recalculated so visible segments total 100%.
|
|
499
|
+
* Legend configuration object for controlling legend appearance and behavior.
|
|
500
|
+
* Includes orientation, position, alignment, shape, styling, and interactivity options.
|
|
452
501
|
*/
|
|
453
|
-
|
|
502
|
+
legend?: ChartLegendConfig< T >;
|
|
454
503
|
/**
|
|
455
504
|
* Grid visibility. x is default when orientation is vertical. y is default when orientation is horizontal.
|
|
456
505
|
*/
|
|
@@ -44,7 +44,16 @@ import { parse, parseISO, isValid } from 'date-fns';
|
|
|
44
44
|
* @return {boolean} True if the date string contains timezone information, false otherwise
|
|
45
45
|
*/
|
|
46
46
|
const hasTimezone = ( dateString: string ): boolean => {
|
|
47
|
-
|
|
47
|
+
const tIndex = dateString.indexOf( 'T' );
|
|
48
|
+
if ( tIndex === -1 ) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if ( dateString.endsWith( 'Z' ) ) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return /[+-]\d{2}:?\d{2}$/.test( dateString.slice( tIndex + 1 ) );
|
|
48
57
|
};
|
|
49
58
|
|
|
50
59
|
/**
|
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,
|
|
@@ -268,6 +268,18 @@ describe( 'parseAsLocalDate', () => {
|
|
|
268
268
|
} );
|
|
269
269
|
} );
|
|
270
270
|
|
|
271
|
+
describe( 'ReDoS resilience', () => {
|
|
272
|
+
test( 'should handle adversarial input without catastrophic backtracking', () => {
|
|
273
|
+
const malicious = 'T' + 'a'.repeat( 8000000 ) + 'X';
|
|
274
|
+
const start = performance.now();
|
|
275
|
+
const result = parseAsLocalDate( malicious );
|
|
276
|
+
const elapsed = performance.now() - start;
|
|
277
|
+
|
|
278
|
+
expect( isNaN( result.getTime() ) ).toBe( true );
|
|
279
|
+
expect( elapsed ).toBeLessThan( 50 );
|
|
280
|
+
} );
|
|
281
|
+
} );
|
|
282
|
+
|
|
271
283
|
describe( 'Performance and consistency', () => {
|
|
272
284
|
test( 'should consistently parse the same input', () => {
|
|
273
285
|
const dateString = '2025-01-15T14:30:45Z';
|