@automattic/charts 3.0.0 → 3.1.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/CHANGELOG.md +13 -0
- package/TOKENS.md +3 -3
- package/dist/index.cjs +235 -123
- 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 +236 -125
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/charts/area-chart/area-chart.tsx +24 -24
- package/src/charts/area-chart/private/overlays.tsx +3 -3
- package/src/charts/area-chart/test/area-chart.test.tsx +301 -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/test/bar-chart.test.tsx +255 -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 +31 -27
- package/src/charts/line-chart/private/line-chart-annotations-overlay.tsx +2 -2
- package/src/charts/line-chart/test/line-chart.test.tsx +262 -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,7 +202,7 @@ 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
208
|
const { tickResolution, ...xAxisOptions } = options?.axis?.x ?? {};
|
|
@@ -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 } />
|
|
@@ -242,7 +244,7 @@ describe( 'AreaChart', () => {
|
|
|
242
244
|
|
|
243
245
|
test( 'y-axis rescales across legend toggles by default', async () => {
|
|
244
246
|
const user = userEvent.setup();
|
|
245
|
-
const ref = createRef<
|
|
247
|
+
const ref = createRef< ChartInstanceRef >();
|
|
246
248
|
render(
|
|
247
249
|
<GlobalChartsProvider>
|
|
248
250
|
<AreaChartUnresponsive
|
|
@@ -274,7 +276,7 @@ describe( 'AreaChart', () => {
|
|
|
274
276
|
// Exercises the non-stacked branch of fixedYDomain, which scans the
|
|
275
277
|
// raw min/max across all series rather than summing stack columns.
|
|
276
278
|
const user = userEvent.setup();
|
|
277
|
-
const ref = createRef<
|
|
279
|
+
const ref = createRef< ChartInstanceRef >();
|
|
278
280
|
render(
|
|
279
281
|
<GlobalChartsProvider>
|
|
280
282
|
<AreaChartUnresponsive
|
|
@@ -306,7 +308,7 @@ describe( 'AreaChart', () => {
|
|
|
306
308
|
|
|
307
309
|
test( 'y-axis stays pinned when rescaleYOnLegendToggle is false', async () => {
|
|
308
310
|
const user = userEvent.setup();
|
|
309
|
-
const ref = createRef<
|
|
311
|
+
const ref = createRef< ChartInstanceRef >();
|
|
310
312
|
render(
|
|
311
313
|
<GlobalChartsProvider>
|
|
312
314
|
<AreaChartUnresponsive
|
|
@@ -335,7 +337,7 @@ describe( 'AreaChart', () => {
|
|
|
335
337
|
|
|
336
338
|
test( 'y-axis stays pinned when rescaleYOnVisibilityChange is false', async () => {
|
|
337
339
|
const user = userEvent.setup();
|
|
338
|
-
const ref = createRef<
|
|
340
|
+
const ref = createRef< ChartInstanceRef >();
|
|
339
341
|
render(
|
|
340
342
|
<GlobalChartsProvider>
|
|
341
343
|
<AreaChartUnresponsive
|
|
@@ -362,12 +364,196 @@ describe( 'AreaChart', () => {
|
|
|
362
364
|
expect( afterToggleDomain ).toEqual( initialDomain );
|
|
363
365
|
} );
|
|
364
366
|
|
|
367
|
+
it( 'hides a series programmatically when the legend is not interactive', () => {
|
|
368
|
+
let context: GlobalChartsContextValue;
|
|
369
|
+
const Grab = () => {
|
|
370
|
+
context = useGlobalChartsContext();
|
|
371
|
+
return null;
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
render(
|
|
375
|
+
<GlobalChartsProvider>
|
|
376
|
+
<Grab />
|
|
377
|
+
<AreaChartUnresponsive
|
|
378
|
+
width={ 500 }
|
|
379
|
+
height={ 300 }
|
|
380
|
+
showLegend={ true }
|
|
381
|
+
legend={ { interactive: false } }
|
|
382
|
+
chartId="test-programmatic-area"
|
|
383
|
+
data={ [
|
|
384
|
+
{
|
|
385
|
+
label: 'Series A',
|
|
386
|
+
data: [ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' } ],
|
|
387
|
+
options: {},
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
label: 'Series B',
|
|
391
|
+
data: [ { date: new Date( '2024-01-01' ), value: 20, label: 'Jan 1' } ],
|
|
392
|
+
options: {},
|
|
393
|
+
},
|
|
394
|
+
] }
|
|
395
|
+
/>
|
|
396
|
+
</GlobalChartsProvider>
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
expect( screen.queryByText( /all series are hidden/i ) ).not.toBeInTheDocument();
|
|
400
|
+
|
|
401
|
+
act( () => {
|
|
402
|
+
context.toggleSeriesVisibility( 'test-programmatic-area', 'Series A' );
|
|
403
|
+
context.toggleSeriesVisibility( 'test-programmatic-area', 'Series B' );
|
|
404
|
+
} );
|
|
405
|
+
|
|
406
|
+
expect( screen.getByText( /all series are hidden/i ) ).toBeInTheDocument();
|
|
407
|
+
} );
|
|
408
|
+
|
|
409
|
+
it( 'omits the click instruction when the legend cannot be clicked', () => {
|
|
410
|
+
let context: GlobalChartsContextValue;
|
|
411
|
+
const Grab = () => {
|
|
412
|
+
context = useGlobalChartsContext();
|
|
413
|
+
return null;
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
render(
|
|
417
|
+
<GlobalChartsProvider>
|
|
418
|
+
<Grab />
|
|
419
|
+
<AreaChartUnresponsive
|
|
420
|
+
width={ 500 }
|
|
421
|
+
height={ 300 }
|
|
422
|
+
showLegend={ true }
|
|
423
|
+
legend={ { interactive: false } }
|
|
424
|
+
chartId="test-empty-copy-area"
|
|
425
|
+
data={ [
|
|
426
|
+
{
|
|
427
|
+
label: 'Series A',
|
|
428
|
+
data: [ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' } ],
|
|
429
|
+
options: {},
|
|
430
|
+
},
|
|
431
|
+
] }
|
|
432
|
+
/>
|
|
433
|
+
</GlobalChartsProvider>
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
act( () => {
|
|
437
|
+
context.toggleSeriesVisibility( 'test-empty-copy-area', 'Series A' );
|
|
438
|
+
} );
|
|
439
|
+
|
|
440
|
+
expect( screen.getByText( 'All series are hidden.' ) ).toBeInTheDocument();
|
|
441
|
+
expect( screen.queryByText( /click legend items/i ) ).not.toBeInTheDocument();
|
|
442
|
+
} );
|
|
443
|
+
|
|
444
|
+
it( 'pins the value axis across a programmatic hide when rescaleYOnVisibilityChange is false', () => {
|
|
445
|
+
let context: GlobalChartsContextValue;
|
|
446
|
+
const Grab = () => {
|
|
447
|
+
context = useGlobalChartsContext();
|
|
448
|
+
return null;
|
|
449
|
+
};
|
|
450
|
+
const ref = createRef< ChartInstanceRef >();
|
|
451
|
+
|
|
452
|
+
render(
|
|
453
|
+
<GlobalChartsProvider>
|
|
454
|
+
<Grab />
|
|
455
|
+
<AreaChartUnresponsive
|
|
456
|
+
width={ 500 }
|
|
457
|
+
height={ 300 }
|
|
458
|
+
showLegend={ true }
|
|
459
|
+
legend={ { interactive: false } }
|
|
460
|
+
chartId="test-programmatic-pin-area"
|
|
461
|
+
rescaleYOnVisibilityChange={ false }
|
|
462
|
+
ref={ ref }
|
|
463
|
+
data={ [
|
|
464
|
+
{
|
|
465
|
+
label: 'Series A',
|
|
466
|
+
data: [ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' } ],
|
|
467
|
+
options: {},
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
label: 'Series B',
|
|
471
|
+
data: [ { date: new Date( '2024-01-01' ), value: 200, label: 'Jan 1' } ],
|
|
472
|
+
options: {},
|
|
473
|
+
},
|
|
474
|
+
] }
|
|
475
|
+
/>
|
|
476
|
+
</GlobalChartsProvider>
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
const before = (
|
|
480
|
+
ref.current?.getScales()?.yScale as { domain: () => number[] } | undefined
|
|
481
|
+
)?.domain();
|
|
482
|
+
expect( before ).toBeDefined();
|
|
483
|
+
|
|
484
|
+
act( () => {
|
|
485
|
+
context.toggleSeriesVisibility( 'test-programmatic-pin-area', 'Series B' );
|
|
486
|
+
} );
|
|
487
|
+
|
|
488
|
+
const after = (
|
|
489
|
+
ref.current?.getScales()?.yScale as { domain: () => number[] } | undefined
|
|
490
|
+
)?.domain();
|
|
491
|
+
expect( after ).toEqual( before );
|
|
492
|
+
} );
|
|
493
|
+
|
|
494
|
+
it( 'collapses only the hidden series when the legend is not interactive', () => {
|
|
495
|
+
// With a non-interactive legend, `renderSeries` must still zero out a
|
|
496
|
+
// programmatically-hidden series via `yAccessor` — the same collapse the
|
|
497
|
+
// interactive legend gets — rather than rendering its real data. With the
|
|
498
|
+
// default `rescaleYOnVisibilityChange` (true), visx derives the y-scale
|
|
499
|
+
// domain from what's actually rendered, so hiding the taller of two
|
|
500
|
+
// series must shrink the domain if the collapse took effect.
|
|
501
|
+
let context: GlobalChartsContextValue;
|
|
502
|
+
const Grab = () => {
|
|
503
|
+
context = useGlobalChartsContext();
|
|
504
|
+
return null;
|
|
505
|
+
};
|
|
506
|
+
const ref = createRef< ChartInstanceRef >();
|
|
507
|
+
|
|
508
|
+
render(
|
|
509
|
+
<GlobalChartsProvider>
|
|
510
|
+
<Grab />
|
|
511
|
+
<AreaChartUnresponsive
|
|
512
|
+
width={ 500 }
|
|
513
|
+
height={ 300 }
|
|
514
|
+
showLegend={ true }
|
|
515
|
+
legend={ { interactive: false } }
|
|
516
|
+
chartId="test-programmatic-collapse-area"
|
|
517
|
+
ref={ ref }
|
|
518
|
+
data={ [
|
|
519
|
+
{
|
|
520
|
+
label: 'Series A',
|
|
521
|
+
data: [ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' } ],
|
|
522
|
+
options: {},
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
label: 'Series B',
|
|
526
|
+
data: [ { date: new Date( '2024-01-01' ), value: 200, label: 'Jan 1' } ],
|
|
527
|
+
options: {},
|
|
528
|
+
},
|
|
529
|
+
] }
|
|
530
|
+
/>
|
|
531
|
+
</GlobalChartsProvider>
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
const before = (
|
|
535
|
+
ref.current?.getScales()?.yScale as { domain: () => number[] } | undefined
|
|
536
|
+
)?.domain();
|
|
537
|
+
expect( before ).toBeDefined();
|
|
538
|
+
expect( before![ 1 ] ).toBeGreaterThanOrEqual( 200 );
|
|
539
|
+
|
|
540
|
+
act( () => {
|
|
541
|
+
context.toggleSeriesVisibility( 'test-programmatic-collapse-area', 'Series B' );
|
|
542
|
+
} );
|
|
543
|
+
|
|
544
|
+
const after = (
|
|
545
|
+
ref.current?.getScales()?.yScale as { domain: () => number[] } | undefined
|
|
546
|
+
)?.domain();
|
|
547
|
+
expect( after ).toBeDefined();
|
|
548
|
+
expect( after![ 1 ] ).toBeLessThan( before![ 1 ] );
|
|
549
|
+
} );
|
|
550
|
+
|
|
365
551
|
test( 'supports negative stacked values without clipping (with pinned Y)', () => {
|
|
366
552
|
// The mixed-sign full-extent pin only kicks in when the consumer
|
|
367
553
|
// opts into pinned-Y behavior; visx's natural domain derivation for
|
|
368
554
|
// a `stackOffset: 'none'` stack does not extend below zero for
|
|
369
555
|
// purely-negative series, which is what this test guards against.
|
|
370
|
-
const ref = createRef<
|
|
556
|
+
const ref = createRef< ChartInstanceRef >();
|
|
371
557
|
render(
|
|
372
558
|
<GlobalChartsProvider>
|
|
373
559
|
<AreaChartUnresponsive
|
|
@@ -406,7 +592,7 @@ describe( 'AreaChart', () => {
|
|
|
406
592
|
} );
|
|
407
593
|
|
|
408
594
|
test( 'does not pin domain for non-default stack offsets', () => {
|
|
409
|
-
const ref = createRef<
|
|
595
|
+
const ref = createRef< ChartInstanceRef >();
|
|
410
596
|
render(
|
|
411
597
|
<GlobalChartsProvider>
|
|
412
598
|
<AreaChartUnresponsive
|
|
@@ -430,6 +616,44 @@ describe( 'AreaChart', () => {
|
|
|
430
616
|
} );
|
|
431
617
|
} );
|
|
432
618
|
|
|
619
|
+
describe( 'Programmatic visibility (non-interactive legend)', () => {
|
|
620
|
+
test( 'tooltip omits a series hidden programmatically even though the legend is not clickable', async () => {
|
|
621
|
+
const user = userEvent.setup();
|
|
622
|
+
const chartId = 'test-noninteractive-tooltip';
|
|
623
|
+
let context: GlobalChartsContextValue | undefined;
|
|
624
|
+
|
|
625
|
+
const Harness = () => {
|
|
626
|
+
context = useGlobalChartsContext();
|
|
627
|
+
return (
|
|
628
|
+
<AreaChart
|
|
629
|
+
{ ...defaultProps }
|
|
630
|
+
chartId={ chartId }
|
|
631
|
+
showLegend
|
|
632
|
+
legend={ { interactive: false } }
|
|
633
|
+
/>
|
|
634
|
+
);
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
render(
|
|
638
|
+
<GlobalChartsProvider>
|
|
639
|
+
<Harness />
|
|
640
|
+
</GlobalChartsProvider>
|
|
641
|
+
);
|
|
642
|
+
|
|
643
|
+
act( () => {
|
|
644
|
+
context?.toggleSeriesVisibility( chartId, 'Series A' );
|
|
645
|
+
} );
|
|
646
|
+
|
|
647
|
+
const chart = screen.getByRole( 'grid', { name: /area chart/i } );
|
|
648
|
+
chart.focus();
|
|
649
|
+
await user.keyboard( '{ArrowRight}' );
|
|
650
|
+
|
|
651
|
+
const tooltip = await screen.findByRole( 'tooltip' );
|
|
652
|
+
expect( tooltip ).not.toHaveTextContent( 'Series A' );
|
|
653
|
+
expect( tooltip ).toHaveTextContent( 'Series B' );
|
|
654
|
+
} );
|
|
655
|
+
} );
|
|
656
|
+
|
|
433
657
|
describe( 'Without GlobalChartsProvider', () => {
|
|
434
658
|
test( 'self-wraps in a provider when none is present', () => {
|
|
435
659
|
render( <AreaChartUnresponsive { ...defaultProps } /> );
|
|
@@ -448,7 +672,7 @@ describe( 'AreaChart', () => {
|
|
|
448
672
|
|
|
449
673
|
describe( 'Chart Ref Interface', () => {
|
|
450
674
|
test( 'exposes getScales via ref', () => {
|
|
451
|
-
const ref = createRef<
|
|
675
|
+
const ref = createRef< ChartInstanceRef >();
|
|
452
676
|
renderUnresponsive( {}, ref );
|
|
453
677
|
|
|
454
678
|
expect( ref.current?.getScales() ).toBeDefined();
|
|
@@ -457,7 +681,7 @@ describe( 'AreaChart', () => {
|
|
|
457
681
|
} );
|
|
458
682
|
|
|
459
683
|
test( 'exposes getChartDimensions via ref', () => {
|
|
460
|
-
const ref = createRef<
|
|
684
|
+
const ref = createRef< ChartInstanceRef >();
|
|
461
685
|
renderUnresponsive( { width: 800, height: 400 }, ref );
|
|
462
686
|
|
|
463
687
|
const dimensions = ref.current?.getChartDimensions();
|
|
@@ -646,4 +870,70 @@ describe( 'AreaChart', () => {
|
|
|
646
870
|
).toBeInTheDocument();
|
|
647
871
|
} );
|
|
648
872
|
} );
|
|
873
|
+
|
|
874
|
+
describe( 'defaultHiddenSeries', () => {
|
|
875
|
+
it( 'renders a series hidden when named in defaultHiddenSeries', () => {
|
|
876
|
+
render(
|
|
877
|
+
<GlobalChartsProvider>
|
|
878
|
+
<AreaChartUnresponsive
|
|
879
|
+
width={ 500 }
|
|
880
|
+
height={ 300 }
|
|
881
|
+
showLegend={ true }
|
|
882
|
+
legend={ { interactive: true } }
|
|
883
|
+
chartId="test-default-hidden-area"
|
|
884
|
+
defaultHiddenSeries={ [ 'Series B' ] }
|
|
885
|
+
data={ [
|
|
886
|
+
{
|
|
887
|
+
label: 'Series A',
|
|
888
|
+
data: [ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' } ],
|
|
889
|
+
options: {},
|
|
890
|
+
},
|
|
891
|
+
{
|
|
892
|
+
label: 'Series B',
|
|
893
|
+
data: [ { date: new Date( '2024-01-01' ), value: 20, label: 'Jan 1' } ],
|
|
894
|
+
options: {},
|
|
895
|
+
},
|
|
896
|
+
] }
|
|
897
|
+
/>
|
|
898
|
+
</GlobalChartsProvider>
|
|
899
|
+
);
|
|
900
|
+
|
|
901
|
+
const items = screen.getAllByRole( 'button' );
|
|
902
|
+
expect( items[ 0 ] ).toHaveAttribute( 'aria-pressed', 'true' );
|
|
903
|
+
expect( items[ 1 ] ).toHaveAttribute( 'aria-pressed', 'false' );
|
|
904
|
+
} );
|
|
905
|
+
|
|
906
|
+
it( 'lets the user reveal a series seeded hidden', async () => {
|
|
907
|
+
const user = userEvent.setup();
|
|
908
|
+
|
|
909
|
+
render(
|
|
910
|
+
<GlobalChartsProvider>
|
|
911
|
+
<AreaChartUnresponsive
|
|
912
|
+
width={ 500 }
|
|
913
|
+
height={ 300 }
|
|
914
|
+
showLegend={ true }
|
|
915
|
+
legend={ { interactive: true } }
|
|
916
|
+
chartId="test-default-hidden-reveal-area"
|
|
917
|
+
defaultHiddenSeries={ [ 'Series B' ] }
|
|
918
|
+
data={ [
|
|
919
|
+
{
|
|
920
|
+
label: 'Series A',
|
|
921
|
+
data: [ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' } ],
|
|
922
|
+
options: {},
|
|
923
|
+
},
|
|
924
|
+
{
|
|
925
|
+
label: 'Series B',
|
|
926
|
+
data: [ { date: new Date( '2024-01-01' ), value: 20, label: 'Jan 1' } ],
|
|
927
|
+
options: {},
|
|
928
|
+
},
|
|
929
|
+
] }
|
|
930
|
+
/>
|
|
931
|
+
</GlobalChartsProvider>
|
|
932
|
+
);
|
|
933
|
+
|
|
934
|
+
await user.click( screen.getAllByRole( 'button' )[ 1 ] );
|
|
935
|
+
|
|
936
|
+
expect( screen.getAllByRole( 'button' )[ 1 ] ).toHaveAttribute( 'aria-pressed', 'true' );
|
|
937
|
+
} );
|
|
938
|
+
} );
|
|
649
939
|
} );
|
|
@@ -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
|