@automattic/charts 2.0.1 → 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 +29 -0
- package/TOKENS.md +109 -50
- package/dist/index.cjs +529 -233
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +118 -55
- package/dist/index.d.cts +90 -48
- package/dist/index.d.ts +90 -48
- package/dist/index.js +527 -235
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/charts/area-chart/area-chart.module.scss +8 -3
- package/src/charts/area-chart/area-chart.tsx +25 -25
- 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.module.scss +14 -6
- 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 +23 -18
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx +107 -86
- package/src/charts/conversion-funnel-chart/test/chart-scope.test.tsx +36 -0
- package/src/charts/geo-chart/geo-chart.module.scss +8 -0
- package/src/charts/geo-chart/geo-chart.tsx +12 -4
- package/src/charts/heatmap-chart/heatmap-chart.module.scss +22 -9
- package/src/charts/heatmap-chart/heatmap-chart.tsx +43 -13
- 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.module.scss +13 -7
- 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/leaderboard-chart/types.ts +0 -2
- package/src/charts/line-chart/line-chart.module.scss +12 -7
- package/src/charts/line-chart/line-chart.tsx +32 -28
- package/src/charts/line-chart/private/line-chart-annotation.tsx +4 -4
- 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 +12 -14
- 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 +12 -14
- 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/svg-empty-state.module.scss +1 -1
- package/src/charts/private/svg-empty-state/test/get-all-hidden-message.test.ts +15 -0
- package/src/charts/private/with-responsive/test/with-responsive.test.tsx +16 -0
- package/src/charts/private/with-responsive/with-responsive.tsx +18 -11
- package/src/charts/private/x-zoom/test/x-zoom.test.tsx +2 -2
- package/src/charts/private/x-zoom/x-zoom.module.scss +3 -3
- 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 +70 -19
- package/src/components/legend/test/legend.test.tsx +100 -4
- package/src/components/tooltip/accessible-tooltip.tsx +17 -4
- package/src/components/tooltip/base-tooltip.tsx +8 -1
- package/src/components/trend-indicator/test/trend-indicator.test.tsx +20 -0
- package/src/components/trend-indicator/trend-indicator.module.scss +4 -7
- package/src/components/trend-indicator/trend-indicator.tsx +3 -0
- package/src/hooks/index.ts +1 -1
- package/src/hooks/test/use-xychart-theme-provider-override.test.tsx +28 -0
- package/src/hooks/test/use-xychart-theme-style-reads.test.tsx +19 -13
- package/src/hooks/test/use-xychart-theme.test.tsx +25 -0
- package/src/hooks/{use-interactive-legend-data.ts → use-legend-visibility-data.ts} +29 -30
- package/src/hooks/use-xychart-theme.ts +6 -4
- package/src/index.ts +12 -3
- package/src/providers/chart-context/global-charts-provider.tsx +127 -30
- package/src/providers/chart-context/hooks/use-default-hidden-series.ts +47 -0
- package/src/providers/chart-context/private/theme-override-vars.ts +87 -0
- package/src/providers/chart-context/private/with-catalog-pointers.ts +50 -0
- package/src/providers/chart-context/test/chart-context.test.tsx +142 -40
- package/src/providers/chart-context/test/chart-scope-class.test.tsx +19 -0
- package/src/providers/chart-context/test/sparse-theme.test.ts +35 -0
- package/src/providers/chart-context/test/theme-override-vars-guard.test.tsx +29 -0
- package/src/providers/chart-context/test/theme-override-vars.test.ts +118 -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/test/with-catalog-pointers.test.ts +47 -0
- package/src/providers/chart-context/themes.ts +18 -27
- package/src/providers/chart-context/types.ts +7 -4
- package/src/providers/chart-scope/chart-scope-context.ts +8 -0
- package/src/providers/chart-scope/index.ts +3 -0
- package/src/providers/chart-scope/test/chart-scope.test.tsx +24 -0
- package/src/providers/chart-scope/test/use-standalone-scope-class.test.tsx +20 -0
- package/src/providers/chart-scope/use-chart-scope-element.ts +9 -0
- package/src/providers/chart-scope/use-standalone-scope-class.ts +13 -0
- package/src/providers/index.ts +1 -0
- package/src/styles/chart-scope-class.ts +6 -0
- package/src/styles/chart-scope.scss +83 -0
- package/src/styles/test/chart-scope.test.ts +165 -0
- package/src/styles/test/wpds-fallbacks.test.ts +131 -0
- package/src/types.ts +12 -34
- package/src/charts/private/grid-control/grid-control.module.scss +0 -8
- package/src/charts/private/grid-control/grid-control.tsx +0 -26
- package/src/charts/private/grid-control/index.ts +0 -1
- package/src/charts/private/grid-control/test/grid-control.test.tsx +0 -56
- package/src/charts/private/single-chart-context/index.ts +0 -8
|
@@ -29,12 +29,13 @@ import {
|
|
|
29
29
|
useGlobalChartsContext,
|
|
30
30
|
useGlobalChartsTheme,
|
|
31
31
|
} from '../../providers';
|
|
32
|
+
import { useDefaultHiddenSeries } from '../../providers/chart-context/hooks/use-default-hidden-series';
|
|
32
33
|
import { attachSubComponents } from '../../utils';
|
|
33
34
|
import { useChartChildren } from '../private/chart-composition';
|
|
35
|
+
import { ChartInstanceContext, type ChartInstanceRef } from '../private/chart-instance-context';
|
|
34
36
|
import { ChartLayout } from '../private/chart-layout';
|
|
35
37
|
import { DefaultGlyph } from '../private/default-glyph';
|
|
36
|
-
import {
|
|
37
|
-
import { SvgEmptyState } from '../private/svg-empty-state';
|
|
38
|
+
import { getAllHiddenMessage, SvgEmptyState } from '../private/svg-empty-state';
|
|
38
39
|
import { getCurveType, getFormatter, guessOptimalNumTicks } from '../private/time-axis';
|
|
39
40
|
import { withResponsive } from '../private/with-responsive';
|
|
40
41
|
import { useXZoom, ZoomResetButton, ZoomSelectionRect, ZoomClip } from '../private/x-zoom';
|
|
@@ -119,7 +120,7 @@ const validateData = ( data: SeriesData[] ) => {
|
|
|
119
120
|
|
|
120
121
|
// Inner component to access DataContext and provide scale data to ref
|
|
121
122
|
const LineChartScalesRef: FC< {
|
|
122
|
-
chartRef?: Ref<
|
|
123
|
+
chartRef?: Ref< ChartInstanceRef >;
|
|
123
124
|
width: number;
|
|
124
125
|
height: number;
|
|
125
126
|
margin?: { top?: number; right?: number; bottom?: number; left?: number };
|
|
@@ -150,7 +151,7 @@ const LineChartScalesRef: FC< {
|
|
|
150
151
|
return null; // This component only provides the ref interface
|
|
151
152
|
};
|
|
152
153
|
|
|
153
|
-
const LineChartInternal = forwardRef<
|
|
154
|
+
const LineChartInternal = forwardRef< ChartInstanceRef, LineChartProps >(
|
|
154
155
|
(
|
|
155
156
|
{
|
|
156
157
|
data,
|
|
@@ -180,6 +181,7 @@ const LineChartInternal = forwardRef< SingleChartRef, LineChartProps >(
|
|
|
180
181
|
onPointerOut = undefined,
|
|
181
182
|
zoomable = false,
|
|
182
183
|
rescaleYOnVisibilityChange = true,
|
|
184
|
+
defaultHiddenSeries,
|
|
183
185
|
children,
|
|
184
186
|
gridVisibility,
|
|
185
187
|
gap = 'md',
|
|
@@ -193,13 +195,18 @@ const LineChartInternal = forwardRef< SingleChartRef, LineChartProps >(
|
|
|
193
195
|
|
|
194
196
|
const providerTheme = useGlobalChartsTheme();
|
|
195
197
|
const theme = useXYChartTheme( data );
|
|
196
|
-
// Gradient stops apply this as an SVG attribute, where CSS var() cannot resolve. useXYChartTheme has already resolved the same role inside its memo, so read it back rather than paying another getComputedStyle on every render.
|
|
198
|
+
// Gradient stops apply this as an SVG attribute, where CSS var() cannot resolve. useXYChartTheme has already resolved the same role inside its memo, against the chart's scope element, so read it back rather than paying another getComputedStyle on every render.
|
|
197
199
|
const resolvedBackgroundColor = theme.backgroundColor ?? providerTheme.backgroundColor;
|
|
198
200
|
const chartId = useChartId( providedChartId );
|
|
201
|
+
const hiddenSeries = useDefaultHiddenSeries( chartId, defaultHiddenSeries );
|
|
202
|
+
const isSeriesVisible = useCallback(
|
|
203
|
+
( seriesLabel: string ) => ! hiddenSeries.has( seriesLabel ),
|
|
204
|
+
[ hiddenSeries ]
|
|
205
|
+
);
|
|
199
206
|
const chartRef = useRef< HTMLDivElement >( null );
|
|
200
207
|
const [ selectedIndex, setSelectedIndex ] = useState< number | undefined >( undefined );
|
|
201
208
|
const [ isNavigating, setIsNavigating ] = useState( false );
|
|
202
|
-
const internalChartRef = useRef<
|
|
209
|
+
const internalChartRef = useRef< ChartInstanceRef >( null );
|
|
203
210
|
|
|
204
211
|
const zoom = useXZoom< Date >( {
|
|
205
212
|
enabled: zoomable,
|
|
@@ -233,31 +240,30 @@ const LineChartInternal = forwardRef< SingleChartRef, LineChartProps >(
|
|
|
233
240
|
);
|
|
234
241
|
|
|
235
242
|
const dataSorted = useChartDataTransform( data );
|
|
236
|
-
const { getElementStyles
|
|
243
|
+
const { getElementStyles } = useGlobalChartsContext();
|
|
237
244
|
|
|
238
|
-
//
|
|
245
|
+
// Series visibility is owned by the provider, so it applies whether it changed
|
|
246
|
+
// through the interactive legend or programmatically.
|
|
239
247
|
const seriesWithVisibility = useMemo( () => {
|
|
240
|
-
if ( ! chartId || ! legendInteractive ) {
|
|
241
|
-
return dataSorted.map( ( series, index ) => ( { series, index, isVisible: true } ) );
|
|
242
|
-
}
|
|
243
248
|
return dataSorted.map( ( series, index ) => ( {
|
|
244
249
|
series,
|
|
245
250
|
index,
|
|
246
|
-
isVisible:
|
|
251
|
+
isVisible: ! hiddenSeries.has( series.label ),
|
|
247
252
|
} ) );
|
|
248
|
-
}, [ dataSorted,
|
|
253
|
+
}, [ dataSorted, hiddenSeries ] );
|
|
249
254
|
|
|
250
255
|
// Check if all series are hidden
|
|
251
256
|
const allSeriesHidden = useMemo( () => {
|
|
252
257
|
return seriesWithVisibility.every( ( { isVisible } ) => ! isVisible );
|
|
253
258
|
}, [ seriesWithVisibility ] );
|
|
254
259
|
|
|
255
|
-
// When
|
|
256
|
-
//
|
|
257
|
-
// domain to whatever is currently visible and
|
|
258
|
-
// matching the pre-existing behaviour
|
|
260
|
+
// When series visibility changes — via the interactive legend or programmatically —
|
|
261
|
+
// and rescaling is opted out, pin the value axis to the full data range so it stays
|
|
262
|
+
// put instead of visx rescaling the domain to whatever is currently visible and
|
|
263
|
+
// making the axis jump. Default is to rescale, matching the pre-existing behaviour
|
|
264
|
+
// and AreaChart's `rescaleYOnVisibilityChange`.
|
|
259
265
|
const stableYDomain = useMemo< [ number, number ] | undefined >( () => {
|
|
260
|
-
if (
|
|
266
|
+
if ( rescaleYOnVisibilityChange ) {
|
|
261
267
|
return undefined;
|
|
262
268
|
}
|
|
263
269
|
let min = Infinity;
|
|
@@ -272,7 +278,7 @@ const LineChartInternal = forwardRef< SingleChartRef, LineChartProps >(
|
|
|
272
278
|
}
|
|
273
279
|
}
|
|
274
280
|
return min < max ? [ min, max ] : undefined;
|
|
275
|
-
}, [
|
|
281
|
+
}, [ rescaleYOnVisibilityChange, dataSorted ] );
|
|
276
282
|
|
|
277
283
|
// Use the keyboard navigation hook
|
|
278
284
|
const { tooltipRef, onChartFocus, onChartBlur, onChartKeyDown } = useKeyboardNavigation( {
|
|
@@ -412,10 +418,11 @@ const LineChartInternal = forwardRef< SingleChartRef, LineChartProps >(
|
|
|
412
418
|
);
|
|
413
419
|
|
|
414
420
|
return (
|
|
415
|
-
<
|
|
421
|
+
<ChartInstanceContext.Provider
|
|
416
422
|
value={ {
|
|
417
423
|
chartId,
|
|
418
424
|
chartRef: internalChartRef,
|
|
425
|
+
isSeriesVisible,
|
|
419
426
|
chartWidth: width,
|
|
420
427
|
chartHeight: measuredChartHeight || 0,
|
|
421
428
|
} }
|
|
@@ -489,10 +496,7 @@ const LineChartInternal = forwardRef< SingleChartRef, LineChartProps >(
|
|
|
489
496
|
width={ width }
|
|
490
497
|
height={ chartHeight }
|
|
491
498
|
>
|
|
492
|
-
{
|
|
493
|
-
'All series are hidden. Click legend items to show data.',
|
|
494
|
-
'jetpack-charts'
|
|
495
|
-
) }
|
|
499
|
+
{ getAllHiddenMessage( legendInteractive, 'series' ) }
|
|
496
500
|
</SvgEmptyState>
|
|
497
501
|
) : null }
|
|
498
502
|
|
|
@@ -619,7 +623,7 @@ const LineChartInternal = forwardRef< SingleChartRef, LineChartProps >(
|
|
|
619
623
|
);
|
|
620
624
|
} }
|
|
621
625
|
</ChartLayout>
|
|
622
|
-
</
|
|
626
|
+
</ChartInstanceContext.Provider>
|
|
623
627
|
);
|
|
624
628
|
}
|
|
625
629
|
);
|
|
@@ -634,16 +638,16 @@ type LineChartAnnotationComponents = {
|
|
|
634
638
|
type LineChartBaseProps = Optional< LineChartProps, 'width' | 'height' | 'size' >;
|
|
635
639
|
|
|
636
640
|
type LineChartComponent = React.ForwardRefExoticComponent<
|
|
637
|
-
LineChartBaseProps & React.RefAttributes<
|
|
641
|
+
LineChartBaseProps & React.RefAttributes< ChartInstanceRef >
|
|
638
642
|
> &
|
|
639
643
|
LineChartAnnotationComponents;
|
|
640
644
|
|
|
641
645
|
type LineChartResponsiveComponent = React.ForwardRefExoticComponent<
|
|
642
|
-
LineChartBaseProps & ResponsiveConfig & React.RefAttributes<
|
|
646
|
+
LineChartBaseProps & ResponsiveConfig & React.RefAttributes< ChartInstanceRef >
|
|
643
647
|
> &
|
|
644
648
|
LineChartAnnotationComponents;
|
|
645
649
|
|
|
646
|
-
const LineChartWithProvider = forwardRef<
|
|
650
|
+
const LineChartWithProvider = forwardRef< ChartInstanceRef, LineChartProps >( ( props, ref ) => {
|
|
647
651
|
const existingContext = useContext( GlobalChartsContext );
|
|
648
652
|
|
|
649
653
|
// If we're already in a GlobalChartsProvider context, render the core component directly
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
import { DataContext } from '@visx/xychart';
|
|
10
10
|
import merge from 'deepmerge';
|
|
11
11
|
import { useContext, useRef, useEffect, useState, useMemo } from 'react';
|
|
12
|
-
import { useGlobalChartsTheme } from '../../../providers';
|
|
12
|
+
import { useChartScopeElement, useGlobalChartsTheme } from '../../../providers';
|
|
13
13
|
import { isSafari, resolveCssVariable } from '../../../utils';
|
|
14
14
|
import LineChartAnnotationLabelWithPopover, {
|
|
15
15
|
POPOVER_BUTTON_SIZE,
|
|
@@ -152,6 +152,7 @@ const LineChartAnnotation: FC< LineChartAnnotationProps > = ( {
|
|
|
152
152
|
renderLabelPopover,
|
|
153
153
|
} ) => {
|
|
154
154
|
const providerTheme = useGlobalChartsTheme();
|
|
155
|
+
const scopeElement = useChartScopeElement();
|
|
155
156
|
const { xScale, yScale } = useContext( DataContext ) || {};
|
|
156
157
|
const labelRef = useRef< SVGGElement >( null );
|
|
157
158
|
const [ height, setHeight ] = useState< number | null >( null );
|
|
@@ -159,10 +160,9 @@ const LineChartAnnotation: FC< LineChartAnnotationProps > = ( {
|
|
|
159
160
|
// Deep merge styles to preserve nested object properties
|
|
160
161
|
const styles = merge( providerTheme.annotationStyles ?? {}, datumStyles ?? {} );
|
|
161
162
|
|
|
162
|
-
// visx annotation parts apply these
|
|
163
|
-
// where CSS var() cannot resolve. Resolve WPDS tokens to concrete values first.
|
|
163
|
+
// visx annotation parts apply these colours as SVG presentation attributes, where CSS var() cannot resolve. Resolve against the chart's scope element, never :root.
|
|
164
164
|
const resolveColor = ( value?: string ): string | undefined =>
|
|
165
|
-
value ? resolveCssVariable( value ) ?? value : value;
|
|
165
|
+
value ? resolveCssVariable( value, scopeElement ) ?? value : value;
|
|
166
166
|
|
|
167
167
|
// Measure the label height once after initial render
|
|
168
168
|
useEffect( () => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DataContext } from '@visx/xychart';
|
|
2
2
|
import { useEffect, useState, useCallback } from 'react';
|
|
3
|
-
import {
|
|
3
|
+
import { useChartInstanceContext } from '../../private/chart-instance-context';
|
|
4
4
|
import styles from '../line-chart.module.scss';
|
|
5
5
|
import type { AxisScale } from '@visx/axis';
|
|
6
6
|
import type { FC, ReactNode } from 'react';
|
|
@@ -15,7 +15,7 @@ interface ScaleData {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
const LineChartAnnotationsOverlay: FC< LineChartAnnotationsProps > = ( { children } ) => {
|
|
18
|
-
const { chartRef, chartWidth, chartHeight } =
|
|
18
|
+
const { chartRef, chartWidth, chartHeight } = useChartInstanceContext();
|
|
19
19
|
|
|
20
20
|
const [ scales, setScales ] = useState< ScaleData | null >( null );
|
|
21
21
|
const [ scalesStable, setScalesStable ] = useState< boolean >( false );
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/* eslint-disable react/jsx-no-bind */
|
|
2
2
|
|
|
3
|
-
import { render, screen, waitFor, within } from '@testing-library/react';
|
|
3
|
+
import { render, screen, waitFor, within, act } from '@testing-library/react';
|
|
4
4
|
import userEvent from '@testing-library/user-event';
|
|
5
5
|
import { GlyphDiamond } from '@visx/glyph';
|
|
6
6
|
import { createElement, createRef } from 'react';
|
|
7
7
|
import { GlobalChartsProvider, defaultTheme } from '../../../providers';
|
|
8
|
+
import { useGlobalChartsContext } from '../../../providers/chart-context/hooks/use-global-charts-context';
|
|
8
9
|
import LineChart, { LineChartUnresponsive } from '../line-chart';
|
|
9
|
-
import type {
|
|
10
|
+
import type { GlobalChartsContextValue } from '../../../providers/chart-context/types';
|
|
11
|
+
import type { ChartInstanceRef } from '../../private/chart-instance-context';
|
|
10
12
|
|
|
11
13
|
// Mock useElementSize to return non-zero dimensions in jsdom so charts render
|
|
12
14
|
const mockRefCallback = jest.fn();
|
|
@@ -902,7 +904,7 @@ describe( 'LineChart', () => {
|
|
|
902
904
|
|
|
903
905
|
describe( 'Chart Ref Interface', () => {
|
|
904
906
|
test( 'exposes getScales method via ref', () => {
|
|
905
|
-
const ref = createRef<
|
|
907
|
+
const ref = createRef< ChartInstanceRef >();
|
|
906
908
|
renderUnwrappedWithTheme( {}, 'default', ref );
|
|
907
909
|
|
|
908
910
|
expect( ref.current?.getScales() ).toBeDefined();
|
|
@@ -911,7 +913,7 @@ describe( 'LineChart', () => {
|
|
|
911
913
|
} );
|
|
912
914
|
|
|
913
915
|
test( 'exposes getChartDimensions method via ref', () => {
|
|
914
|
-
const ref = createRef<
|
|
916
|
+
const ref = createRef< ChartInstanceRef >();
|
|
915
917
|
renderUnwrappedWithTheme( { width: 800, height: 400 }, 'default', ref );
|
|
916
918
|
|
|
917
919
|
const dimensions = ref.current?.getChartDimensions();
|
|
@@ -1298,7 +1300,7 @@ describe( 'LineChart', () => {
|
|
|
1298
1300
|
expect( buttons ).toHaveLength( 0 );
|
|
1299
1301
|
} );
|
|
1300
1302
|
|
|
1301
|
-
it( 'shows all series when
|
|
1303
|
+
it( 'shows all series when nothing has been hidden', () => {
|
|
1302
1304
|
render(
|
|
1303
1305
|
<GlobalChartsProvider>
|
|
1304
1306
|
<LineChartUnresponsive
|
|
@@ -1317,6 +1319,136 @@ describe( 'LineChart', () => {
|
|
|
1317
1319
|
expect( item ).toHaveAttribute( 'aria-pressed', 'true' );
|
|
1318
1320
|
} );
|
|
1319
1321
|
} );
|
|
1322
|
+
|
|
1323
|
+
it( 'hides a series programmatically when the legend is not interactive', () => {
|
|
1324
|
+
let context: GlobalChartsContextValue;
|
|
1325
|
+
const Grab = () => {
|
|
1326
|
+
context = useGlobalChartsContext();
|
|
1327
|
+
return null;
|
|
1328
|
+
};
|
|
1329
|
+
|
|
1330
|
+
render(
|
|
1331
|
+
<GlobalChartsProvider>
|
|
1332
|
+
<Grab />
|
|
1333
|
+
<LineChartUnresponsive
|
|
1334
|
+
width={ 500 }
|
|
1335
|
+
height={ 300 }
|
|
1336
|
+
withGradientFill={ false }
|
|
1337
|
+
showLegend={ true }
|
|
1338
|
+
legend={ { interactive: false } }
|
|
1339
|
+
chartId="test-programmatic-line"
|
|
1340
|
+
data={ [
|
|
1341
|
+
{
|
|
1342
|
+
label: 'Series A',
|
|
1343
|
+
data: [ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' } ],
|
|
1344
|
+
options: {},
|
|
1345
|
+
},
|
|
1346
|
+
{
|
|
1347
|
+
label: 'Series B',
|
|
1348
|
+
data: [ { date: new Date( '2024-01-01' ), value: 20, label: 'Jan 1' } ],
|
|
1349
|
+
options: {},
|
|
1350
|
+
},
|
|
1351
|
+
] }
|
|
1352
|
+
/>
|
|
1353
|
+
</GlobalChartsProvider>
|
|
1354
|
+
);
|
|
1355
|
+
|
|
1356
|
+
expect( screen.queryByText( /all series are hidden/i ) ).not.toBeInTheDocument();
|
|
1357
|
+
|
|
1358
|
+
act( () => {
|
|
1359
|
+
context.toggleSeriesVisibility( 'test-programmatic-line', 'Series A' );
|
|
1360
|
+
context.toggleSeriesVisibility( 'test-programmatic-line', 'Series B' );
|
|
1361
|
+
} );
|
|
1362
|
+
|
|
1363
|
+
expect( screen.getByText( /all series are hidden/i ) ).toBeInTheDocument();
|
|
1364
|
+
} );
|
|
1365
|
+
|
|
1366
|
+
it( 'omits the click instruction when the legend cannot be clicked', () => {
|
|
1367
|
+
let context: GlobalChartsContextValue;
|
|
1368
|
+
const Grab = () => {
|
|
1369
|
+
context = useGlobalChartsContext();
|
|
1370
|
+
return null;
|
|
1371
|
+
};
|
|
1372
|
+
|
|
1373
|
+
render(
|
|
1374
|
+
<GlobalChartsProvider>
|
|
1375
|
+
<Grab />
|
|
1376
|
+
<LineChartUnresponsive
|
|
1377
|
+
width={ 500 }
|
|
1378
|
+
height={ 300 }
|
|
1379
|
+
withGradientFill={ false }
|
|
1380
|
+
showLegend={ true }
|
|
1381
|
+
legend={ { interactive: false } }
|
|
1382
|
+
chartId="test-empty-copy-line"
|
|
1383
|
+
data={ [
|
|
1384
|
+
{
|
|
1385
|
+
label: 'Series A',
|
|
1386
|
+
data: [ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' } ],
|
|
1387
|
+
options: {},
|
|
1388
|
+
},
|
|
1389
|
+
] }
|
|
1390
|
+
/>
|
|
1391
|
+
</GlobalChartsProvider>
|
|
1392
|
+
);
|
|
1393
|
+
|
|
1394
|
+
act( () => {
|
|
1395
|
+
context.toggleSeriesVisibility( 'test-empty-copy-line', 'Series A' );
|
|
1396
|
+
} );
|
|
1397
|
+
|
|
1398
|
+
expect( screen.getByText( 'All series are hidden.' ) ).toBeInTheDocument();
|
|
1399
|
+
expect( screen.queryByText( /click legend items/i ) ).not.toBeInTheDocument();
|
|
1400
|
+
} );
|
|
1401
|
+
|
|
1402
|
+
it( 'pins the value axis across a programmatic hide when rescaleYOnVisibilityChange is false', () => {
|
|
1403
|
+
let context: GlobalChartsContextValue;
|
|
1404
|
+
const Grab = () => {
|
|
1405
|
+
context = useGlobalChartsContext();
|
|
1406
|
+
return null;
|
|
1407
|
+
};
|
|
1408
|
+
const ref = createRef< ChartInstanceRef >();
|
|
1409
|
+
|
|
1410
|
+
render(
|
|
1411
|
+
<GlobalChartsProvider>
|
|
1412
|
+
<Grab />
|
|
1413
|
+
<LineChartUnresponsive
|
|
1414
|
+
width={ 500 }
|
|
1415
|
+
height={ 300 }
|
|
1416
|
+
withGradientFill={ false }
|
|
1417
|
+
showLegend={ true }
|
|
1418
|
+
legend={ { interactive: false } }
|
|
1419
|
+
chartId="test-programmatic-pin-line"
|
|
1420
|
+
rescaleYOnVisibilityChange={ false }
|
|
1421
|
+
ref={ ref }
|
|
1422
|
+
data={ [
|
|
1423
|
+
{
|
|
1424
|
+
label: 'Series A',
|
|
1425
|
+
data: [ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' } ],
|
|
1426
|
+
options: {},
|
|
1427
|
+
},
|
|
1428
|
+
{
|
|
1429
|
+
label: 'Series B',
|
|
1430
|
+
data: [ { date: new Date( '2024-01-01' ), value: 200, label: 'Jan 1' } ],
|
|
1431
|
+
options: {},
|
|
1432
|
+
},
|
|
1433
|
+
] }
|
|
1434
|
+
/>
|
|
1435
|
+
</GlobalChartsProvider>
|
|
1436
|
+
);
|
|
1437
|
+
|
|
1438
|
+
const before = (
|
|
1439
|
+
ref.current?.getScales()?.yScale as { domain: () => number[] } | undefined
|
|
1440
|
+
)?.domain();
|
|
1441
|
+
expect( before ).toBeDefined();
|
|
1442
|
+
|
|
1443
|
+
act( () => {
|
|
1444
|
+
context.toggleSeriesVisibility( 'test-programmatic-pin-line', 'Series B' );
|
|
1445
|
+
} );
|
|
1446
|
+
|
|
1447
|
+
const after = (
|
|
1448
|
+
ref.current?.getScales()?.yScale as { domain: () => number[] } | undefined
|
|
1449
|
+
)?.domain();
|
|
1450
|
+
expect( after ).toEqual( before );
|
|
1451
|
+
} );
|
|
1320
1452
|
} );
|
|
1321
1453
|
|
|
1322
1454
|
// The line is not animated, so it clips only while actually zoomed.
|
|
@@ -1523,4 +1655,129 @@ describe( 'LineChart', () => {
|
|
|
1523
1655
|
).toBeInTheDocument();
|
|
1524
1656
|
} );
|
|
1525
1657
|
} );
|
|
1658
|
+
|
|
1659
|
+
describe( 'defaultHiddenSeries', () => {
|
|
1660
|
+
it( 'renders a series hidden when named in defaultHiddenSeries', () => {
|
|
1661
|
+
render(
|
|
1662
|
+
<GlobalChartsProvider>
|
|
1663
|
+
<LineChartUnresponsive
|
|
1664
|
+
width={ 500 }
|
|
1665
|
+
height={ 300 }
|
|
1666
|
+
withGradientFill={ false }
|
|
1667
|
+
showLegend={ true }
|
|
1668
|
+
legend={ { interactive: true } }
|
|
1669
|
+
chartId="test-default-hidden-line"
|
|
1670
|
+
defaultHiddenSeries={ [ 'Series B' ] }
|
|
1671
|
+
data={ [
|
|
1672
|
+
{
|
|
1673
|
+
label: 'Series A',
|
|
1674
|
+
data: [ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' } ],
|
|
1675
|
+
options: {},
|
|
1676
|
+
},
|
|
1677
|
+
{
|
|
1678
|
+
label: 'Series B',
|
|
1679
|
+
data: [ { date: new Date( '2024-01-01' ), value: 20, label: 'Jan 1' } ],
|
|
1680
|
+
options: {},
|
|
1681
|
+
},
|
|
1682
|
+
] }
|
|
1683
|
+
/>
|
|
1684
|
+
</GlobalChartsProvider>
|
|
1685
|
+
);
|
|
1686
|
+
|
|
1687
|
+
const items = screen.getAllByRole( 'button' );
|
|
1688
|
+
expect( items[ 0 ] ).toHaveAttribute( 'aria-pressed', 'true' );
|
|
1689
|
+
expect( items[ 1 ] ).toHaveAttribute( 'aria-pressed', 'false' );
|
|
1690
|
+
} );
|
|
1691
|
+
|
|
1692
|
+
it( 'lets the user reveal a series seeded hidden', async () => {
|
|
1693
|
+
const user = userEvent.setup();
|
|
1694
|
+
|
|
1695
|
+
render(
|
|
1696
|
+
<GlobalChartsProvider>
|
|
1697
|
+
<LineChartUnresponsive
|
|
1698
|
+
width={ 500 }
|
|
1699
|
+
height={ 300 }
|
|
1700
|
+
withGradientFill={ false }
|
|
1701
|
+
showLegend={ true }
|
|
1702
|
+
legend={ { interactive: true } }
|
|
1703
|
+
chartId="test-default-hidden-reveal-line"
|
|
1704
|
+
defaultHiddenSeries={ [ 'Series B' ] }
|
|
1705
|
+
data={ [
|
|
1706
|
+
{
|
|
1707
|
+
label: 'Series A',
|
|
1708
|
+
data: [ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' } ],
|
|
1709
|
+
options: {},
|
|
1710
|
+
},
|
|
1711
|
+
{
|
|
1712
|
+
label: 'Series B',
|
|
1713
|
+
data: [ { date: new Date( '2024-01-01' ), value: 20, label: 'Jan 1' } ],
|
|
1714
|
+
options: {},
|
|
1715
|
+
},
|
|
1716
|
+
] }
|
|
1717
|
+
/>
|
|
1718
|
+
</GlobalChartsProvider>
|
|
1719
|
+
);
|
|
1720
|
+
|
|
1721
|
+
await user.click( screen.getAllByRole( 'button' )[ 1 ] );
|
|
1722
|
+
|
|
1723
|
+
expect( screen.getAllByRole( 'button' )[ 1 ] ).toHaveAttribute( 'aria-pressed', 'true' );
|
|
1724
|
+
} );
|
|
1725
|
+
|
|
1726
|
+
it( 'keeps a revealed series visible after a data change', async () => {
|
|
1727
|
+
// Matters specifically at chart level: useChartRegistration unregisters and
|
|
1728
|
+
// re-registers whenever legendItems change, which a data prop change triggers.
|
|
1729
|
+
// A hook-level harness has no registration at all, so it can't catch a
|
|
1730
|
+
// regression where re-registration re-triggers the seeding effect.
|
|
1731
|
+
const user = userEvent.setup();
|
|
1732
|
+
const chartId = 'test-default-hidden-data-change-line';
|
|
1733
|
+
|
|
1734
|
+
const makeData = ( seriesAValue: number ) => [
|
|
1735
|
+
{
|
|
1736
|
+
label: 'Series A',
|
|
1737
|
+
data: [ { date: new Date( '2024-01-01' ), value: seriesAValue, label: 'Jan 1' } ],
|
|
1738
|
+
options: {},
|
|
1739
|
+
},
|
|
1740
|
+
{
|
|
1741
|
+
label: 'Series B',
|
|
1742
|
+
data: [ { date: new Date( '2024-01-01' ), value: 20, label: 'Jan 1' } ],
|
|
1743
|
+
options: {},
|
|
1744
|
+
},
|
|
1745
|
+
];
|
|
1746
|
+
|
|
1747
|
+
const { rerender } = render(
|
|
1748
|
+
<GlobalChartsProvider>
|
|
1749
|
+
<LineChartUnresponsive
|
|
1750
|
+
width={ 500 }
|
|
1751
|
+
height={ 300 }
|
|
1752
|
+
withGradientFill={ false }
|
|
1753
|
+
showLegend={ true }
|
|
1754
|
+
legend={ { interactive: true } }
|
|
1755
|
+
chartId={ chartId }
|
|
1756
|
+
defaultHiddenSeries={ [ 'Series B' ] }
|
|
1757
|
+
data={ makeData( 10 ) }
|
|
1758
|
+
/>
|
|
1759
|
+
</GlobalChartsProvider>
|
|
1760
|
+
);
|
|
1761
|
+
|
|
1762
|
+
await user.click( screen.getAllByRole( 'button' )[ 1 ] );
|
|
1763
|
+
expect( screen.getAllByRole( 'button' )[ 1 ] ).toHaveAttribute( 'aria-pressed', 'true' );
|
|
1764
|
+
|
|
1765
|
+
rerender(
|
|
1766
|
+
<GlobalChartsProvider>
|
|
1767
|
+
<LineChartUnresponsive
|
|
1768
|
+
width={ 500 }
|
|
1769
|
+
height={ 300 }
|
|
1770
|
+
withGradientFill={ false }
|
|
1771
|
+
showLegend={ true }
|
|
1772
|
+
legend={ { interactive: true } }
|
|
1773
|
+
chartId={ chartId }
|
|
1774
|
+
defaultHiddenSeries={ [ 'Series B' ] }
|
|
1775
|
+
data={ makeData( 30 ) }
|
|
1776
|
+
/>
|
|
1777
|
+
</GlobalChartsProvider>
|
|
1778
|
+
);
|
|
1779
|
+
|
|
1780
|
+
expect( screen.getAllByRole( 'button' )[ 1 ] ).toHaveAttribute( 'aria-pressed', 'true' );
|
|
1781
|
+
} );
|
|
1782
|
+
} );
|
|
1526
1783
|
} );
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
DataPointDate,
|
|
4
4
|
SeriesData,
|
|
5
5
|
SeriesChartLegendConfig,
|
|
6
|
+
SeriesVisibilityProps,
|
|
6
7
|
AnnotationStyles,
|
|
7
8
|
DataPoint,
|
|
8
9
|
} from '../../types';
|
|
@@ -28,7 +29,7 @@ export type RenderLineGlyphProps< Datum extends object > = GlyphProps< Datum > &
|
|
|
28
29
|
position?: 'start' | 'end';
|
|
29
30
|
};
|
|
30
31
|
|
|
31
|
-
export interface LineChartProps extends BaseChartProps< SeriesData[]
|
|
32
|
+
export interface LineChartProps extends BaseChartProps< SeriesData[] >, SeriesVisibilityProps {
|
|
32
33
|
/**
|
|
33
34
|
* Legend configuration. Supports `collapseGroups` on top of the shared options.
|
|
34
35
|
*/
|
|
@@ -53,8 +54,8 @@ export interface LineChartProps extends BaseChartProps< SeriesData[] > {
|
|
|
53
54
|
*/
|
|
54
55
|
zoomable?: boolean;
|
|
55
56
|
/**
|
|
56
|
-
* Whether the Y axis rescales to fit only the visible series when
|
|
57
|
-
*
|
|
57
|
+
* Whether the Y axis rescales to fit only the visible series when the set of
|
|
58
|
+
* visible series changes — via the interactive legend or otherwise.
|
|
58
59
|
* Defaults to `true` (the pre-existing behaviour). Set to `false` to pin the Y
|
|
59
60
|
* axis to the full data extent so hiding series does not move the chart's
|
|
60
61
|
* baseline — useful for comparison charts. Matches `AreaChart`.
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import { Group } from '@visx/group';
|
|
2
2
|
import { Pie } from '@visx/shape';
|
|
3
3
|
import { useTooltip, useTooltipInPortal } from '@visx/tooltip';
|
|
4
|
-
import { __ } from '@wordpress/i18n';
|
|
5
4
|
import clsx from 'clsx';
|
|
6
5
|
import { useCallback, useContext, useMemo } from 'react';
|
|
7
6
|
import { Legend, useChartLegendItems } from '../../components/legend';
|
|
8
7
|
import { BaseTooltip } from '../../components/tooltip';
|
|
9
8
|
import {
|
|
10
9
|
useDataWithPercentages,
|
|
11
|
-
|
|
10
|
+
useLegendVisibilityData,
|
|
12
11
|
usePrefersReducedMotion,
|
|
13
12
|
} from '../../hooks';
|
|
14
13
|
import {
|
|
@@ -19,14 +18,15 @@ import {
|
|
|
19
18
|
useGlobalChartsTheme,
|
|
20
19
|
GlobalChartsContext,
|
|
21
20
|
} from '../../providers';
|
|
21
|
+
import { CHART_SCOPE_CLASS } from '../../styles/chart-scope-class';
|
|
22
22
|
import { attachSubComponents, resolveFontSize } from '../../utils';
|
|
23
23
|
import { getStringWidth } from '../../visx/text';
|
|
24
24
|
import { Center } from '../private/center';
|
|
25
25
|
import { ChartSVG, ChartHTML, useChartChildren } from '../private/chart-composition';
|
|
26
|
+
import { ChartInstanceContext } from '../private/chart-instance-context';
|
|
26
27
|
import { ChartLayout } from '../private/chart-layout';
|
|
27
28
|
import { RadialWipeAnimation } from '../private/radial-wipe-animation/';
|
|
28
|
-
import {
|
|
29
|
-
import { SvgEmptyState } from '../private/svg-empty-state';
|
|
29
|
+
import { getAllHiddenMessage, SvgEmptyState } from '../private/svg-empty-state';
|
|
30
30
|
import { withResponsive, ResponsiveConfig } from '../private/with-responsive';
|
|
31
31
|
import styles from './pie-chart.module.scss';
|
|
32
32
|
import type { LegendValueDisplay } from '../../components/legend';
|
|
@@ -217,11 +217,10 @@ const PieChartInternal = ( {
|
|
|
217
217
|
// Calculate percentages from values (single source of truth)
|
|
218
218
|
const dataWithPercentages = useDataWithPercentages( data );
|
|
219
219
|
|
|
220
|
-
// Filter and recalculate data
|
|
221
|
-
const { visibleData, allSegmentsHidden, legendData } =
|
|
220
|
+
// Filter and recalculate data from the shared legend visibility state.
|
|
221
|
+
const { visibleData, allSegmentsHidden, legendData } = useLegendVisibilityData( {
|
|
222
222
|
data: dataWithPercentages,
|
|
223
223
|
chartId,
|
|
224
|
-
legendInteractive,
|
|
225
224
|
isSeriesVisible,
|
|
226
225
|
} );
|
|
227
226
|
|
|
@@ -307,7 +306,7 @@ const PieChartInternal = ( {
|
|
|
307
306
|
);
|
|
308
307
|
|
|
309
308
|
return (
|
|
310
|
-
<
|
|
309
|
+
<ChartInstanceContext.Provider value={ { chartId } }>
|
|
311
310
|
<ChartLayout
|
|
312
311
|
legendPosition={ legendPosition }
|
|
313
312
|
legendElement={ legendElement }
|
|
@@ -328,7 +327,9 @@ const PieChartInternal = ( {
|
|
|
328
327
|
<>
|
|
329
328
|
{ withTooltips && tooltipOpen && tooltipData && (
|
|
330
329
|
<TooltipInPortal top={ tooltipTop || 0 } left={ tooltipLeft || 0 }>
|
|
331
|
-
<div
|
|
330
|
+
<div className={ CHART_SCOPE_CLASS } role="tooltip">
|
|
331
|
+
{ renderTooltip( { tooltipData } ) }
|
|
332
|
+
</div>
|
|
332
333
|
</TooltipInPortal>
|
|
333
334
|
) }
|
|
334
335
|
{ htmlChildren }
|
|
@@ -382,10 +383,7 @@ const PieChartInternal = ( {
|
|
|
382
383
|
>
|
|
383
384
|
{ allSegmentsHidden ? (
|
|
384
385
|
<SvgEmptyState x={ 0 } y={ 0 } width={ width } height={ height }>
|
|
385
|
-
{
|
|
386
|
-
'All segments are hidden. Click legend items to show data.',
|
|
387
|
-
'jetpack-charts'
|
|
388
|
-
) }
|
|
386
|
+
{ getAllHiddenMessage( legendInteractive, 'segments' ) }
|
|
389
387
|
</SvgEmptyState>
|
|
390
388
|
) : (
|
|
391
389
|
<Pie< DataPointPercentageCalculated & { index: number } >
|
|
@@ -492,7 +490,7 @@ const PieChartInternal = ( {
|
|
|
492
490
|
);
|
|
493
491
|
} }
|
|
494
492
|
</ChartLayout>
|
|
495
|
-
</
|
|
493
|
+
</ChartInstanceContext.Provider>
|
|
496
494
|
);
|
|
497
495
|
};
|
|
498
496
|
|