@automattic/charts 3.0.0 → 3.1.1
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/CHANGELOG.md +18 -0
- package/TOKENS.md +3 -3
- package/dist/index.cjs +250 -136
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +23 -14
- package/dist/index.d.cts +51 -14
- package/dist/index.d.ts +51 -14
- package/dist/index.js +251 -138
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/charts/area-chart/area-chart.tsx +26 -26
- package/src/charts/area-chart/private/overlays.tsx +3 -3
- package/src/charts/area-chart/test/area-chart.test.tsx +346 -11
- package/src/charts/area-chart/types.ts +4 -3
- package/src/charts/bar-chart/bar-chart.tsx +21 -15
- package/src/charts/bar-chart/private/use-bar-chart-options.ts +19 -12
- package/src/charts/bar-chart/test/bar-chart.test.tsx +277 -2
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.module.scss +6 -6
- package/src/charts/geo-chart/geo-chart.module.scss +8 -0
- package/src/charts/heatmap-chart/heatmap-chart.module.scss +9 -0
- package/src/charts/heatmap-chart/heatmap-chart.tsx +31 -8
- package/src/charts/heatmap-chart/private/build-calendar-data.ts +65 -5
- package/src/charts/heatmap-chart/test/build-calendar-data.test.ts +125 -0
- package/src/charts/heatmap-chart/types.ts +8 -0
- package/src/charts/leaderboard-chart/leaderboard-chart.tsx +17 -22
- package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +107 -1
- package/src/charts/line-chart/line-chart.module.scss +2 -2
- package/src/charts/line-chart/line-chart.tsx +33 -29
- package/src/charts/line-chart/private/line-chart-annotations-overlay.tsx +2 -2
- package/src/charts/line-chart/test/line-chart.test.tsx +313 -5
- package/src/charts/line-chart/types.ts +4 -3
- package/src/charts/pie-chart/pie-chart.tsx +8 -13
- package/src/charts/pie-chart/test/pie-chart.test.tsx +73 -1
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.module.scss +2 -2
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +8 -13
- package/src/charts/private/{single-chart-context/single-chart-context.tsx → chart-instance-context/chart-instance-context.ts} +1 -5
- package/src/charts/private/chart-instance-context/index.ts +3 -0
- package/src/charts/private/{single-chart-context/use-single-chart-context.ts → chart-instance-context/use-chart-instance-context.ts} +1 -3
- package/src/charts/private/svg-empty-state/get-all-hidden-message.ts +41 -0
- package/src/charts/private/svg-empty-state/index.ts +1 -0
- package/src/charts/private/svg-empty-state/test/get-all-hidden-message.test.ts +15 -0
- package/src/charts/private/x-zoom/test/x-zoom.test.tsx +2 -2
- package/src/charts/private/x-zoom/x-zoom.tsx +2 -2
- package/src/components/legend/legend.tsx +4 -4
- package/src/components/legend/private/base-legend.module.scss +1 -1
- package/src/components/legend/private/base-legend.tsx +67 -18
- package/src/components/legend/test/legend.test.tsx +100 -4
- package/src/components/tooltip/accessible-tooltip.tsx +13 -2
- package/src/components/trend-indicator/trend-indicator.module.scss +1 -1
- package/src/hooks/index.ts +1 -1
- package/src/hooks/{use-interactive-legend-data.ts → use-legend-visibility-data.ts} +29 -30
- package/src/index.ts +3 -1
- package/src/providers/chart-context/global-charts-provider.tsx +92 -18
- package/src/providers/chart-context/hooks/use-default-hidden-series.ts +47 -0
- package/src/providers/chart-context/test/chart-context.test.tsx +142 -0
- package/src/providers/chart-context/test/use-default-hidden-series-paint.test.tsx +56 -0
- package/src/providers/chart-context/test/use-default-hidden-series.ssr.test.tsx +39 -0
- package/src/providers/chart-context/test/use-default-hidden-series.test.tsx +222 -0
- package/src/providers/chart-context/types.ts +7 -1
- package/src/styles/chart-scope.scss +2 -2
- package/src/styles/test/wpds-fallbacks.test.ts +131 -0
- package/src/types.ts +12 -0
- package/src/charts/private/single-chart-context/index.ts +0 -8
package/package.json
CHANGED
|
@@ -27,12 +27,13 @@ import {
|
|
|
27
27
|
useGlobalChartsContext,
|
|
28
28
|
useGlobalChartsTheme,
|
|
29
29
|
} from '../../providers';
|
|
30
|
+
import { useDefaultHiddenSeries } from '../../providers/chart-context/hooks/use-default-hidden-series';
|
|
30
31
|
import { attachSubComponents } from '../../utils';
|
|
31
32
|
import { renderDefaultTooltip } from '../line-chart';
|
|
32
33
|
import { useChartChildren } from '../private/chart-composition';
|
|
34
|
+
import { ChartInstanceContext, type ChartInstanceRef } from '../private/chart-instance-context';
|
|
33
35
|
import { ChartLayout } from '../private/chart-layout';
|
|
34
|
-
import {
|
|
35
|
-
import { SvgEmptyState } from '../private/svg-empty-state';
|
|
36
|
+
import { getAllHiddenMessage, SvgEmptyState } from '../private/svg-empty-state';
|
|
36
37
|
import { getCurveType, getFormatter, guessOptimalNumTicks } from '../private/time-axis';
|
|
37
38
|
import { withResponsive } from '../private/with-responsive';
|
|
38
39
|
import { useXZoom, ZoomResetButton, ZoomSelectionRect, ZoomClip } from '../private/x-zoom';
|
|
@@ -43,7 +44,7 @@ import type { DataPointDate, Optional } from '../../types';
|
|
|
43
44
|
import type { ResponsiveConfig } from '../private/with-responsive';
|
|
44
45
|
import type { TickFormatter } from '@visx/axis';
|
|
45
46
|
|
|
46
|
-
const AreaChartInternal = forwardRef<
|
|
47
|
+
const AreaChartInternal = forwardRef< ChartInstanceRef, AreaChartProps >(
|
|
47
48
|
(
|
|
48
49
|
{
|
|
49
50
|
data,
|
|
@@ -72,6 +73,7 @@ const AreaChartInternal = forwardRef< SingleChartRef, AreaChartProps >(
|
|
|
72
73
|
zoomable = false,
|
|
73
74
|
rescaleYOnVisibilityChange,
|
|
74
75
|
rescaleYOnLegendToggle,
|
|
76
|
+
defaultHiddenSeries,
|
|
75
77
|
children,
|
|
76
78
|
gridVisibility,
|
|
77
79
|
gap = 'md',
|
|
@@ -88,10 +90,15 @@ const AreaChartInternal = forwardRef< SingleChartRef, AreaChartProps >(
|
|
|
88
90
|
const providerTheme = useGlobalChartsTheme();
|
|
89
91
|
const theme = useXYChartTheme( data );
|
|
90
92
|
const chartId = useChartId( providedChartId );
|
|
93
|
+
const hiddenSeries = useDefaultHiddenSeries( chartId, defaultHiddenSeries );
|
|
94
|
+
const isSeriesVisible = useCallback(
|
|
95
|
+
( seriesLabel: string ) => ! hiddenSeries.has( seriesLabel ),
|
|
96
|
+
[ hiddenSeries ]
|
|
97
|
+
);
|
|
91
98
|
const chartRef = useRef< HTMLDivElement >( null );
|
|
92
99
|
const [ selectedIndex, setSelectedIndex ] = useState< number | undefined >( undefined );
|
|
93
100
|
const [ isNavigating, setIsNavigating ] = useState( false );
|
|
94
|
-
const internalChartRef = useRef<
|
|
101
|
+
const internalChartRef = useRef< ChartInstanceRef >( null );
|
|
95
102
|
|
|
96
103
|
const zoom = useXZoom< Date >( {
|
|
97
104
|
enabled: zoomable,
|
|
@@ -121,18 +128,15 @@ const AreaChartInternal = forwardRef< SingleChartRef, AreaChartProps >(
|
|
|
121
128
|
);
|
|
122
129
|
|
|
123
130
|
const dataSorted = useChartDataTransform( data );
|
|
124
|
-
const { getElementStyles
|
|
131
|
+
const { getElementStyles } = useGlobalChartsContext();
|
|
125
132
|
|
|
126
133
|
const seriesWithVisibility = useMemo( () => {
|
|
127
|
-
if ( ! chartId || ! legendInteractive ) {
|
|
128
|
-
return dataSorted.map( ( series, index ) => ( { series, index, isVisible: true } ) );
|
|
129
|
-
}
|
|
130
134
|
return dataSorted.map( ( series, index ) => ( {
|
|
131
135
|
series,
|
|
132
136
|
index,
|
|
133
|
-
isVisible:
|
|
137
|
+
isVisible: ! hiddenSeries.has( series.label ),
|
|
134
138
|
} ) );
|
|
135
|
-
}, [ dataSorted,
|
|
139
|
+
}, [ dataSorted, hiddenSeries ] );
|
|
136
140
|
|
|
137
141
|
const allSeriesHidden = useMemo(
|
|
138
142
|
() => seriesWithVisibility.every( ( { isVisible } ) => ! isVisible ),
|
|
@@ -157,7 +161,6 @@ const AreaChartInternal = forwardRef< SingleChartRef, AreaChartProps >(
|
|
|
157
161
|
const fixedYDomain = useMemo< [ number, number ] | undefined >( () => {
|
|
158
162
|
if (
|
|
159
163
|
rescaleYOnVisibility ||
|
|
160
|
-
! legendInteractive ||
|
|
161
164
|
! dataSorted.length ||
|
|
162
165
|
! dataSorted[ 0 ].data.length ||
|
|
163
166
|
( stacked && stackOffset !== 'none' )
|
|
@@ -199,11 +202,11 @@ const AreaChartInternal = forwardRef< SingleChartRef, AreaChartProps >(
|
|
|
199
202
|
}
|
|
200
203
|
if ( max === -Infinity ) return undefined;
|
|
201
204
|
return [ Math.min( 0, min ), max ];
|
|
202
|
-
}, [ dataSorted, stacked, stackOffset,
|
|
205
|
+
}, [ dataSorted, stacked, stackOffset, rescaleYOnVisibility ] );
|
|
203
206
|
|
|
204
207
|
const chartOptions = useMemo( () => {
|
|
205
|
-
const { tickResolution, ...xAxisOptions } = options?.axis?.x ?? {};
|
|
206
|
-
const formatter =
|
|
208
|
+
const { tickResolution, tickFormat, ...xAxisOptions } = options?.axis?.x ?? {};
|
|
209
|
+
const formatter = tickFormat || getFormatter( dataSorted, tickResolution );
|
|
207
210
|
|
|
208
211
|
return {
|
|
209
212
|
axis: {
|
|
@@ -283,7 +286,6 @@ const AreaChartInternal = forwardRef< SingleChartRef, AreaChartProps >(
|
|
|
283
286
|
);
|
|
284
287
|
const filteredRenderTooltip = useCallback(
|
|
285
288
|
( params: Parameters< typeof renderTooltip >[ 0 ] ) => {
|
|
286
|
-
if ( ! legendInteractive ) return renderTooltip( params );
|
|
287
289
|
const datumByKey = params?.tooltipData?.datumByKey;
|
|
288
290
|
if ( ! datumByKey ) return renderTooltip( params );
|
|
289
291
|
const filtered = Object.fromEntries(
|
|
@@ -307,7 +309,7 @@ const AreaChartInternal = forwardRef< SingleChartRef, AreaChartProps >(
|
|
|
307
309
|
} as typeof params.tooltipData,
|
|
308
310
|
} );
|
|
309
311
|
},
|
|
310
|
-
[ renderTooltip,
|
|
312
|
+
[ renderTooltip, visibleLabels ]
|
|
311
313
|
);
|
|
312
314
|
|
|
313
315
|
// Defaults that depend on stacked vs overlapping mode.
|
|
@@ -354,7 +356,7 @@ const AreaChartInternal = forwardRef< SingleChartRef, AreaChartProps >(
|
|
|
354
356
|
dataKey={ seriesData?.label }
|
|
355
357
|
data={ seriesData.data as DataPointDate[] }
|
|
356
358
|
xAccessor={ accessors.xAccessor }
|
|
357
|
-
yAccessor={ isVisible
|
|
359
|
+
yAccessor={ isVisible ? accessors.yAccessor : zeroYAccessor }
|
|
358
360
|
fill={ color }
|
|
359
361
|
fillOpacity={ resolvedFillOpacity }
|
|
360
362
|
{ ...( stacked ? {} : { renderLine: resolvedWithStroke, curve } ) }
|
|
@@ -365,10 +367,11 @@ const AreaChartInternal = forwardRef< SingleChartRef, AreaChartProps >(
|
|
|
365
367
|
};
|
|
366
368
|
|
|
367
369
|
return (
|
|
368
|
-
<
|
|
370
|
+
<ChartInstanceContext.Provider
|
|
369
371
|
value={ {
|
|
370
372
|
chartId,
|
|
371
373
|
chartRef: internalChartRef,
|
|
374
|
+
isSeriesVisible,
|
|
372
375
|
chartWidth: width,
|
|
373
376
|
chartHeight: measuredChartHeight || 0,
|
|
374
377
|
} }
|
|
@@ -437,10 +440,7 @@ const AreaChartInternal = forwardRef< SingleChartRef, AreaChartProps >(
|
|
|
437
440
|
width={ width }
|
|
438
441
|
height={ chartHeight }
|
|
439
442
|
>
|
|
440
|
-
{
|
|
441
|
-
'All series are hidden. Click legend items to show data.',
|
|
442
|
-
'jetpack-charts'
|
|
443
|
-
) }
|
|
443
|
+
{ getAllHiddenMessage( legendInteractive, 'series' ) }
|
|
444
444
|
</SvgEmptyState>
|
|
445
445
|
) : null }
|
|
446
446
|
|
|
@@ -503,7 +503,7 @@ const AreaChartInternal = forwardRef< SingleChartRef, AreaChartProps >(
|
|
|
503
503
|
);
|
|
504
504
|
} }
|
|
505
505
|
</ChartLayout>
|
|
506
|
-
</
|
|
506
|
+
</ChartInstanceContext.Provider>
|
|
507
507
|
);
|
|
508
508
|
}
|
|
509
509
|
);
|
|
@@ -515,16 +515,16 @@ type AreaChartSubComponents = {
|
|
|
515
515
|
type AreaChartBaseProps = Optional< AreaChartProps, 'width' | 'height' | 'size' >;
|
|
516
516
|
|
|
517
517
|
type AreaChartComponent = React.ForwardRefExoticComponent<
|
|
518
|
-
AreaChartBaseProps & React.RefAttributes<
|
|
518
|
+
AreaChartBaseProps & React.RefAttributes< ChartInstanceRef >
|
|
519
519
|
> &
|
|
520
520
|
AreaChartSubComponents;
|
|
521
521
|
|
|
522
522
|
type AreaChartResponsiveComponent = React.ForwardRefExoticComponent<
|
|
523
|
-
AreaChartBaseProps & ResponsiveConfig & React.RefAttributes<
|
|
523
|
+
AreaChartBaseProps & ResponsiveConfig & React.RefAttributes< ChartInstanceRef >
|
|
524
524
|
> &
|
|
525
525
|
AreaChartSubComponents;
|
|
526
526
|
|
|
527
|
-
const AreaChartWithProvider = forwardRef<
|
|
527
|
+
const AreaChartWithProvider = forwardRef< ChartInstanceRef, AreaChartProps >( ( props, ref ) => {
|
|
528
528
|
const existingContext = useContext( GlobalChartsContext );
|
|
529
529
|
|
|
530
530
|
if ( existingContext ) {
|
|
@@ -2,7 +2,7 @@ import { DataContext, TooltipContext } from '@visx/xychart';
|
|
|
2
2
|
import { useContext, useImperativeHandle } from 'react';
|
|
3
3
|
import type { ElementStyles, GetElementStylesParams } from '../../../providers';
|
|
4
4
|
import type { DataPointDate, SeriesData } from '../../../types';
|
|
5
|
-
import type {
|
|
5
|
+
import type { ChartInstanceRef } from '../../private/chart-instance-context';
|
|
6
6
|
import type { FC, ReactNode, Ref } from 'react';
|
|
7
7
|
|
|
8
8
|
export type VisibleSeriesEntry = { series: SeriesData; index: number; isVisible: boolean };
|
|
@@ -11,10 +11,10 @@ export type VisibleSeriesEntry = { series: SeriesData; index: number; isVisible:
|
|
|
11
11
|
// instead of spreading `any`. `Number(...)` + `isFinite` guards every call.
|
|
12
12
|
type ScaleFn = ( input: Date | number ) => number;
|
|
13
13
|
|
|
14
|
-
// Bridges visx's `DataContext` to the chart's `
|
|
14
|
+
// Bridges visx's `DataContext` to the chart's `ChartInstanceRef` so consumers
|
|
15
15
|
// can read scales and dimensions imperatively. Must be inside `<XYChart>`.
|
|
16
16
|
export const AreaChartScalesRef: FC< {
|
|
17
|
-
chartRef?: Ref<
|
|
17
|
+
chartRef?: Ref< ChartInstanceRef >;
|
|
18
18
|
width: number;
|
|
19
19
|
height: number;
|
|
20
20
|
margin?: { top?: number; right?: number; bottom?: number; left?: number };
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { render, screen } from '@testing-library/react';
|
|
1
|
+
import { render, screen, act } from '@testing-library/react';
|
|
2
2
|
import userEvent from '@testing-library/user-event';
|
|
3
3
|
import { createRef } from 'react';
|
|
4
4
|
import { GlobalChartsProvider } from '../../../providers';
|
|
5
|
+
import { useGlobalChartsContext } from '../../../providers/chart-context/hooks/use-global-charts-context';
|
|
5
6
|
import AreaChart, { AreaChartUnresponsive } from '../area-chart';
|
|
6
|
-
import type {
|
|
7
|
+
import type { GlobalChartsContextValue } from '../../../providers/chart-context/types';
|
|
8
|
+
import type { ChartInstanceRef } from '../../private/chart-instance-context';
|
|
7
9
|
|
|
8
10
|
const mockRefCallback = jest.fn();
|
|
9
11
|
jest.mock( '../../../hooks/use-element-size', () => ( {
|
|
@@ -42,7 +44,7 @@ describe( 'AreaChart', () => {
|
|
|
42
44
|
);
|
|
43
45
|
};
|
|
44
46
|
|
|
45
|
-
const renderUnresponsive = ( props = {}, ref?: React.Ref<
|
|
47
|
+
const renderUnresponsive = ( props = {}, ref?: React.Ref< ChartInstanceRef > ) => {
|
|
46
48
|
return render(
|
|
47
49
|
<GlobalChartsProvider>
|
|
48
50
|
<AreaChartUnresponsive { ...defaultProps } { ...props } ref={ ref } />
|
|
@@ -110,6 +112,51 @@ describe( 'AreaChart', () => {
|
|
|
110
112
|
} );
|
|
111
113
|
} );
|
|
112
114
|
|
|
115
|
+
describe( 'X-Axis Ticks', () => {
|
|
116
|
+
const monthlySeries = [
|
|
117
|
+
{
|
|
118
|
+
label: 'Series A',
|
|
119
|
+
data: [
|
|
120
|
+
{ date: new Date( '2024-01-01' ), value: 10 },
|
|
121
|
+
{ date: new Date( '2024-04-01' ), value: 20 },
|
|
122
|
+
{ date: new Date( '2024-07-01' ), value: 30 },
|
|
123
|
+
{ date: new Date( '2024-10-01' ), value: 40 },
|
|
124
|
+
{ date: new Date( '2025-03-01' ), value: 50 },
|
|
125
|
+
],
|
|
126
|
+
},
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
test( 'keeps the derived month formatter when tickFormat is passed as undefined', () => {
|
|
130
|
+
renderWithProvider( {
|
|
131
|
+
options: { axis: { x: { tickFormat: undefined } } },
|
|
132
|
+
data: monthlySeries,
|
|
133
|
+
} );
|
|
134
|
+
|
|
135
|
+
// January is absent: formatMonthOrYearTick renders it as the year instead.
|
|
136
|
+
const ticks = screen.getAllByText( /^(Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)$/ );
|
|
137
|
+
expect( ticks.length ).toBeGreaterThan( 1 );
|
|
138
|
+
expect(
|
|
139
|
+
screen.queryByText( /^(February|March|April|June|July|August|September|October)$/ )
|
|
140
|
+
).not.toBeInTheDocument();
|
|
141
|
+
} );
|
|
142
|
+
|
|
143
|
+
test( 'honors an explicit tickFormat over the derived formatter', () => {
|
|
144
|
+
renderWithProvider( {
|
|
145
|
+
options: {
|
|
146
|
+
axis: {
|
|
147
|
+
x: {
|
|
148
|
+
tickFormat: ( date: Date | number ) =>
|
|
149
|
+
`tick-${ new Date( Number( date ) ).getUTCMonth() }`,
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
data: monthlySeries,
|
|
154
|
+
} );
|
|
155
|
+
|
|
156
|
+
expect( screen.getAllByText( /^tick-\d+$/ ).length ).toBeGreaterThan( 0 );
|
|
157
|
+
} );
|
|
158
|
+
} );
|
|
159
|
+
|
|
113
160
|
describe( 'Stacking', () => {
|
|
114
161
|
test( 'is stacked by default', () => {
|
|
115
162
|
renderWithProvider();
|
|
@@ -242,7 +289,7 @@ describe( 'AreaChart', () => {
|
|
|
242
289
|
|
|
243
290
|
test( 'y-axis rescales across legend toggles by default', async () => {
|
|
244
291
|
const user = userEvent.setup();
|
|
245
|
-
const ref = createRef<
|
|
292
|
+
const ref = createRef< ChartInstanceRef >();
|
|
246
293
|
render(
|
|
247
294
|
<GlobalChartsProvider>
|
|
248
295
|
<AreaChartUnresponsive
|
|
@@ -274,7 +321,7 @@ describe( 'AreaChart', () => {
|
|
|
274
321
|
// Exercises the non-stacked branch of fixedYDomain, which scans the
|
|
275
322
|
// raw min/max across all series rather than summing stack columns.
|
|
276
323
|
const user = userEvent.setup();
|
|
277
|
-
const ref = createRef<
|
|
324
|
+
const ref = createRef< ChartInstanceRef >();
|
|
278
325
|
render(
|
|
279
326
|
<GlobalChartsProvider>
|
|
280
327
|
<AreaChartUnresponsive
|
|
@@ -306,7 +353,7 @@ describe( 'AreaChart', () => {
|
|
|
306
353
|
|
|
307
354
|
test( 'y-axis stays pinned when rescaleYOnLegendToggle is false', async () => {
|
|
308
355
|
const user = userEvent.setup();
|
|
309
|
-
const ref = createRef<
|
|
356
|
+
const ref = createRef< ChartInstanceRef >();
|
|
310
357
|
render(
|
|
311
358
|
<GlobalChartsProvider>
|
|
312
359
|
<AreaChartUnresponsive
|
|
@@ -335,7 +382,7 @@ describe( 'AreaChart', () => {
|
|
|
335
382
|
|
|
336
383
|
test( 'y-axis stays pinned when rescaleYOnVisibilityChange is false', async () => {
|
|
337
384
|
const user = userEvent.setup();
|
|
338
|
-
const ref = createRef<
|
|
385
|
+
const ref = createRef< ChartInstanceRef >();
|
|
339
386
|
render(
|
|
340
387
|
<GlobalChartsProvider>
|
|
341
388
|
<AreaChartUnresponsive
|
|
@@ -362,12 +409,196 @@ describe( 'AreaChart', () => {
|
|
|
362
409
|
expect( afterToggleDomain ).toEqual( initialDomain );
|
|
363
410
|
} );
|
|
364
411
|
|
|
412
|
+
it( 'hides a series programmatically when the legend is not interactive', () => {
|
|
413
|
+
let context: GlobalChartsContextValue;
|
|
414
|
+
const Grab = () => {
|
|
415
|
+
context = useGlobalChartsContext();
|
|
416
|
+
return null;
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
render(
|
|
420
|
+
<GlobalChartsProvider>
|
|
421
|
+
<Grab />
|
|
422
|
+
<AreaChartUnresponsive
|
|
423
|
+
width={ 500 }
|
|
424
|
+
height={ 300 }
|
|
425
|
+
showLegend={ true }
|
|
426
|
+
legend={ { interactive: false } }
|
|
427
|
+
chartId="test-programmatic-area"
|
|
428
|
+
data={ [
|
|
429
|
+
{
|
|
430
|
+
label: 'Series A',
|
|
431
|
+
data: [ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' } ],
|
|
432
|
+
options: {},
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
label: 'Series B',
|
|
436
|
+
data: [ { date: new Date( '2024-01-01' ), value: 20, label: 'Jan 1' } ],
|
|
437
|
+
options: {},
|
|
438
|
+
},
|
|
439
|
+
] }
|
|
440
|
+
/>
|
|
441
|
+
</GlobalChartsProvider>
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
expect( screen.queryByText( /all series are hidden/i ) ).not.toBeInTheDocument();
|
|
445
|
+
|
|
446
|
+
act( () => {
|
|
447
|
+
context.toggleSeriesVisibility( 'test-programmatic-area', 'Series A' );
|
|
448
|
+
context.toggleSeriesVisibility( 'test-programmatic-area', 'Series B' );
|
|
449
|
+
} );
|
|
450
|
+
|
|
451
|
+
expect( screen.getByText( /all series are hidden/i ) ).toBeInTheDocument();
|
|
452
|
+
} );
|
|
453
|
+
|
|
454
|
+
it( 'omits the click instruction when the legend cannot be clicked', () => {
|
|
455
|
+
let context: GlobalChartsContextValue;
|
|
456
|
+
const Grab = () => {
|
|
457
|
+
context = useGlobalChartsContext();
|
|
458
|
+
return null;
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
render(
|
|
462
|
+
<GlobalChartsProvider>
|
|
463
|
+
<Grab />
|
|
464
|
+
<AreaChartUnresponsive
|
|
465
|
+
width={ 500 }
|
|
466
|
+
height={ 300 }
|
|
467
|
+
showLegend={ true }
|
|
468
|
+
legend={ { interactive: false } }
|
|
469
|
+
chartId="test-empty-copy-area"
|
|
470
|
+
data={ [
|
|
471
|
+
{
|
|
472
|
+
label: 'Series A',
|
|
473
|
+
data: [ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' } ],
|
|
474
|
+
options: {},
|
|
475
|
+
},
|
|
476
|
+
] }
|
|
477
|
+
/>
|
|
478
|
+
</GlobalChartsProvider>
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
act( () => {
|
|
482
|
+
context.toggleSeriesVisibility( 'test-empty-copy-area', 'Series A' );
|
|
483
|
+
} );
|
|
484
|
+
|
|
485
|
+
expect( screen.getByText( 'All series are hidden.' ) ).toBeInTheDocument();
|
|
486
|
+
expect( screen.queryByText( /click legend items/i ) ).not.toBeInTheDocument();
|
|
487
|
+
} );
|
|
488
|
+
|
|
489
|
+
it( 'pins the value axis across a programmatic hide when rescaleYOnVisibilityChange is false', () => {
|
|
490
|
+
let context: GlobalChartsContextValue;
|
|
491
|
+
const Grab = () => {
|
|
492
|
+
context = useGlobalChartsContext();
|
|
493
|
+
return null;
|
|
494
|
+
};
|
|
495
|
+
const ref = createRef< ChartInstanceRef >();
|
|
496
|
+
|
|
497
|
+
render(
|
|
498
|
+
<GlobalChartsProvider>
|
|
499
|
+
<Grab />
|
|
500
|
+
<AreaChartUnresponsive
|
|
501
|
+
width={ 500 }
|
|
502
|
+
height={ 300 }
|
|
503
|
+
showLegend={ true }
|
|
504
|
+
legend={ { interactive: false } }
|
|
505
|
+
chartId="test-programmatic-pin-area"
|
|
506
|
+
rescaleYOnVisibilityChange={ false }
|
|
507
|
+
ref={ ref }
|
|
508
|
+
data={ [
|
|
509
|
+
{
|
|
510
|
+
label: 'Series A',
|
|
511
|
+
data: [ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' } ],
|
|
512
|
+
options: {},
|
|
513
|
+
},
|
|
514
|
+
{
|
|
515
|
+
label: 'Series B',
|
|
516
|
+
data: [ { date: new Date( '2024-01-01' ), value: 200, label: 'Jan 1' } ],
|
|
517
|
+
options: {},
|
|
518
|
+
},
|
|
519
|
+
] }
|
|
520
|
+
/>
|
|
521
|
+
</GlobalChartsProvider>
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
const before = (
|
|
525
|
+
ref.current?.getScales()?.yScale as { domain: () => number[] } | undefined
|
|
526
|
+
)?.domain();
|
|
527
|
+
expect( before ).toBeDefined();
|
|
528
|
+
|
|
529
|
+
act( () => {
|
|
530
|
+
context.toggleSeriesVisibility( 'test-programmatic-pin-area', 'Series B' );
|
|
531
|
+
} );
|
|
532
|
+
|
|
533
|
+
const after = (
|
|
534
|
+
ref.current?.getScales()?.yScale as { domain: () => number[] } | undefined
|
|
535
|
+
)?.domain();
|
|
536
|
+
expect( after ).toEqual( before );
|
|
537
|
+
} );
|
|
538
|
+
|
|
539
|
+
it( 'collapses only the hidden series when the legend is not interactive', () => {
|
|
540
|
+
// With a non-interactive legend, `renderSeries` must still zero out a
|
|
541
|
+
// programmatically-hidden series via `yAccessor` — the same collapse the
|
|
542
|
+
// interactive legend gets — rather than rendering its real data. With the
|
|
543
|
+
// default `rescaleYOnVisibilityChange` (true), visx derives the y-scale
|
|
544
|
+
// domain from what's actually rendered, so hiding the taller of two
|
|
545
|
+
// series must shrink the domain if the collapse took effect.
|
|
546
|
+
let context: GlobalChartsContextValue;
|
|
547
|
+
const Grab = () => {
|
|
548
|
+
context = useGlobalChartsContext();
|
|
549
|
+
return null;
|
|
550
|
+
};
|
|
551
|
+
const ref = createRef< ChartInstanceRef >();
|
|
552
|
+
|
|
553
|
+
render(
|
|
554
|
+
<GlobalChartsProvider>
|
|
555
|
+
<Grab />
|
|
556
|
+
<AreaChartUnresponsive
|
|
557
|
+
width={ 500 }
|
|
558
|
+
height={ 300 }
|
|
559
|
+
showLegend={ true }
|
|
560
|
+
legend={ { interactive: false } }
|
|
561
|
+
chartId="test-programmatic-collapse-area"
|
|
562
|
+
ref={ ref }
|
|
563
|
+
data={ [
|
|
564
|
+
{
|
|
565
|
+
label: 'Series A',
|
|
566
|
+
data: [ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' } ],
|
|
567
|
+
options: {},
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
label: 'Series B',
|
|
571
|
+
data: [ { date: new Date( '2024-01-01' ), value: 200, label: 'Jan 1' } ],
|
|
572
|
+
options: {},
|
|
573
|
+
},
|
|
574
|
+
] }
|
|
575
|
+
/>
|
|
576
|
+
</GlobalChartsProvider>
|
|
577
|
+
);
|
|
578
|
+
|
|
579
|
+
const before = (
|
|
580
|
+
ref.current?.getScales()?.yScale as { domain: () => number[] } | undefined
|
|
581
|
+
)?.domain();
|
|
582
|
+
expect( before ).toBeDefined();
|
|
583
|
+
expect( before![ 1 ] ).toBeGreaterThanOrEqual( 200 );
|
|
584
|
+
|
|
585
|
+
act( () => {
|
|
586
|
+
context.toggleSeriesVisibility( 'test-programmatic-collapse-area', 'Series B' );
|
|
587
|
+
} );
|
|
588
|
+
|
|
589
|
+
const after = (
|
|
590
|
+
ref.current?.getScales()?.yScale as { domain: () => number[] } | undefined
|
|
591
|
+
)?.domain();
|
|
592
|
+
expect( after ).toBeDefined();
|
|
593
|
+
expect( after![ 1 ] ).toBeLessThan( before![ 1 ] );
|
|
594
|
+
} );
|
|
595
|
+
|
|
365
596
|
test( 'supports negative stacked values without clipping (with pinned Y)', () => {
|
|
366
597
|
// The mixed-sign full-extent pin only kicks in when the consumer
|
|
367
598
|
// opts into pinned-Y behavior; visx's natural domain derivation for
|
|
368
599
|
// a `stackOffset: 'none'` stack does not extend below zero for
|
|
369
600
|
// purely-negative series, which is what this test guards against.
|
|
370
|
-
const ref = createRef<
|
|
601
|
+
const ref = createRef< ChartInstanceRef >();
|
|
371
602
|
render(
|
|
372
603
|
<GlobalChartsProvider>
|
|
373
604
|
<AreaChartUnresponsive
|
|
@@ -406,7 +637,7 @@ describe( 'AreaChart', () => {
|
|
|
406
637
|
} );
|
|
407
638
|
|
|
408
639
|
test( 'does not pin domain for non-default stack offsets', () => {
|
|
409
|
-
const ref = createRef<
|
|
640
|
+
const ref = createRef< ChartInstanceRef >();
|
|
410
641
|
render(
|
|
411
642
|
<GlobalChartsProvider>
|
|
412
643
|
<AreaChartUnresponsive
|
|
@@ -430,6 +661,44 @@ describe( 'AreaChart', () => {
|
|
|
430
661
|
} );
|
|
431
662
|
} );
|
|
432
663
|
|
|
664
|
+
describe( 'Programmatic visibility (non-interactive legend)', () => {
|
|
665
|
+
test( 'tooltip omits a series hidden programmatically even though the legend is not clickable', async () => {
|
|
666
|
+
const user = userEvent.setup();
|
|
667
|
+
const chartId = 'test-noninteractive-tooltip';
|
|
668
|
+
let context: GlobalChartsContextValue | undefined;
|
|
669
|
+
|
|
670
|
+
const Harness = () => {
|
|
671
|
+
context = useGlobalChartsContext();
|
|
672
|
+
return (
|
|
673
|
+
<AreaChart
|
|
674
|
+
{ ...defaultProps }
|
|
675
|
+
chartId={ chartId }
|
|
676
|
+
showLegend
|
|
677
|
+
legend={ { interactive: false } }
|
|
678
|
+
/>
|
|
679
|
+
);
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
render(
|
|
683
|
+
<GlobalChartsProvider>
|
|
684
|
+
<Harness />
|
|
685
|
+
</GlobalChartsProvider>
|
|
686
|
+
);
|
|
687
|
+
|
|
688
|
+
act( () => {
|
|
689
|
+
context?.toggleSeriesVisibility( chartId, 'Series A' );
|
|
690
|
+
} );
|
|
691
|
+
|
|
692
|
+
const chart = screen.getByRole( 'grid', { name: /area chart/i } );
|
|
693
|
+
chart.focus();
|
|
694
|
+
await user.keyboard( '{ArrowRight}' );
|
|
695
|
+
|
|
696
|
+
const tooltip = await screen.findByRole( 'tooltip' );
|
|
697
|
+
expect( tooltip ).not.toHaveTextContent( 'Series A' );
|
|
698
|
+
expect( tooltip ).toHaveTextContent( 'Series B' );
|
|
699
|
+
} );
|
|
700
|
+
} );
|
|
701
|
+
|
|
433
702
|
describe( 'Without GlobalChartsProvider', () => {
|
|
434
703
|
test( 'self-wraps in a provider when none is present', () => {
|
|
435
704
|
render( <AreaChartUnresponsive { ...defaultProps } /> );
|
|
@@ -448,7 +717,7 @@ describe( 'AreaChart', () => {
|
|
|
448
717
|
|
|
449
718
|
describe( 'Chart Ref Interface', () => {
|
|
450
719
|
test( 'exposes getScales via ref', () => {
|
|
451
|
-
const ref = createRef<
|
|
720
|
+
const ref = createRef< ChartInstanceRef >();
|
|
452
721
|
renderUnresponsive( {}, ref );
|
|
453
722
|
|
|
454
723
|
expect( ref.current?.getScales() ).toBeDefined();
|
|
@@ -457,7 +726,7 @@ describe( 'AreaChart', () => {
|
|
|
457
726
|
} );
|
|
458
727
|
|
|
459
728
|
test( 'exposes getChartDimensions via ref', () => {
|
|
460
|
-
const ref = createRef<
|
|
729
|
+
const ref = createRef< ChartInstanceRef >();
|
|
461
730
|
renderUnresponsive( { width: 800, height: 400 }, ref );
|
|
462
731
|
|
|
463
732
|
const dimensions = ref.current?.getChartDimensions();
|
|
@@ -646,4 +915,70 @@ describe( 'AreaChart', () => {
|
|
|
646
915
|
).toBeInTheDocument();
|
|
647
916
|
} );
|
|
648
917
|
} );
|
|
918
|
+
|
|
919
|
+
describe( 'defaultHiddenSeries', () => {
|
|
920
|
+
it( 'renders a series hidden when named in defaultHiddenSeries', () => {
|
|
921
|
+
render(
|
|
922
|
+
<GlobalChartsProvider>
|
|
923
|
+
<AreaChartUnresponsive
|
|
924
|
+
width={ 500 }
|
|
925
|
+
height={ 300 }
|
|
926
|
+
showLegend={ true }
|
|
927
|
+
legend={ { interactive: true } }
|
|
928
|
+
chartId="test-default-hidden-area"
|
|
929
|
+
defaultHiddenSeries={ [ 'Series B' ] }
|
|
930
|
+
data={ [
|
|
931
|
+
{
|
|
932
|
+
label: 'Series A',
|
|
933
|
+
data: [ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' } ],
|
|
934
|
+
options: {},
|
|
935
|
+
},
|
|
936
|
+
{
|
|
937
|
+
label: 'Series B',
|
|
938
|
+
data: [ { date: new Date( '2024-01-01' ), value: 20, label: 'Jan 1' } ],
|
|
939
|
+
options: {},
|
|
940
|
+
},
|
|
941
|
+
] }
|
|
942
|
+
/>
|
|
943
|
+
</GlobalChartsProvider>
|
|
944
|
+
);
|
|
945
|
+
|
|
946
|
+
const items = screen.getAllByRole( 'button' );
|
|
947
|
+
expect( items[ 0 ] ).toHaveAttribute( 'aria-pressed', 'true' );
|
|
948
|
+
expect( items[ 1 ] ).toHaveAttribute( 'aria-pressed', 'false' );
|
|
949
|
+
} );
|
|
950
|
+
|
|
951
|
+
it( 'lets the user reveal a series seeded hidden', async () => {
|
|
952
|
+
const user = userEvent.setup();
|
|
953
|
+
|
|
954
|
+
render(
|
|
955
|
+
<GlobalChartsProvider>
|
|
956
|
+
<AreaChartUnresponsive
|
|
957
|
+
width={ 500 }
|
|
958
|
+
height={ 300 }
|
|
959
|
+
showLegend={ true }
|
|
960
|
+
legend={ { interactive: true } }
|
|
961
|
+
chartId="test-default-hidden-reveal-area"
|
|
962
|
+
defaultHiddenSeries={ [ 'Series B' ] }
|
|
963
|
+
data={ [
|
|
964
|
+
{
|
|
965
|
+
label: 'Series A',
|
|
966
|
+
data: [ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' } ],
|
|
967
|
+
options: {},
|
|
968
|
+
},
|
|
969
|
+
{
|
|
970
|
+
label: 'Series B',
|
|
971
|
+
data: [ { date: new Date( '2024-01-01' ), value: 20, label: 'Jan 1' } ],
|
|
972
|
+
options: {},
|
|
973
|
+
},
|
|
974
|
+
] }
|
|
975
|
+
/>
|
|
976
|
+
</GlobalChartsProvider>
|
|
977
|
+
);
|
|
978
|
+
|
|
979
|
+
await user.click( screen.getAllByRole( 'button' )[ 1 ] );
|
|
980
|
+
|
|
981
|
+
expect( screen.getAllByRole( 'button' )[ 1 ] ).toHaveAttribute( 'aria-pressed', 'true' );
|
|
982
|
+
} );
|
|
983
|
+
} );
|
|
649
984
|
} );
|
|
@@ -3,12 +3,13 @@ import type {
|
|
|
3
3
|
DataPointDate,
|
|
4
4
|
SeriesData,
|
|
5
5
|
SeriesChartLegendConfig,
|
|
6
|
+
SeriesVisibilityProps,
|
|
6
7
|
} from '../../types';
|
|
7
8
|
import type { RenderTooltipParams } from '../../visx/types';
|
|
8
9
|
import type { CurveType } from '../line-chart/types';
|
|
9
10
|
import type { ReactNode } from 'react';
|
|
10
11
|
|
|
11
|
-
export interface AreaChartProps extends BaseChartProps< SeriesData[]
|
|
12
|
+
export interface AreaChartProps extends BaseChartProps< SeriesData[] >, SeriesVisibilityProps {
|
|
12
13
|
/**
|
|
13
14
|
* Legend configuration. Supports `collapseGroups` on top of the shared options.
|
|
14
15
|
*/
|
|
@@ -63,8 +64,8 @@ export interface AreaChartProps extends BaseChartProps< SeriesData[] > {
|
|
|
63
64
|
*/
|
|
64
65
|
zoomable?: boolean;
|
|
65
66
|
/**
|
|
66
|
-
* Whether the Y axis rescales to fit only the visible series when
|
|
67
|
-
*
|
|
67
|
+
* Whether the Y axis rescales to fit only the visible series when the set of
|
|
68
|
+
* visible series changes — via the interactive legend or otherwise.
|
|
68
69
|
* Defaults to `true`, matching LineChart. Set to `false` to pin the Y axis to
|
|
69
70
|
* the full data extent so hiding series does not move the chart's baseline.
|
|
70
71
|
* @default true
|