@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.
Files changed (102) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/TOKENS.md +109 -50
  3. package/dist/index.cjs +529 -233
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.css +118 -55
  6. package/dist/index.d.cts +90 -48
  7. package/dist/index.d.ts +90 -48
  8. package/dist/index.js +527 -235
  9. package/dist/index.js.map +1 -1
  10. package/package.json +1 -1
  11. package/src/charts/area-chart/area-chart.module.scss +8 -3
  12. package/src/charts/area-chart/area-chart.tsx +25 -25
  13. package/src/charts/area-chart/private/overlays.tsx +3 -3
  14. package/src/charts/area-chart/test/area-chart.test.tsx +301 -11
  15. package/src/charts/area-chart/types.ts +4 -3
  16. package/src/charts/bar-chart/bar-chart.module.scss +14 -6
  17. package/src/charts/bar-chart/bar-chart.tsx +21 -15
  18. package/src/charts/bar-chart/test/bar-chart.test.tsx +255 -2
  19. package/src/charts/conversion-funnel-chart/conversion-funnel-chart.module.scss +23 -18
  20. package/src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx +107 -86
  21. package/src/charts/conversion-funnel-chart/test/chart-scope.test.tsx +36 -0
  22. package/src/charts/geo-chart/geo-chart.module.scss +8 -0
  23. package/src/charts/geo-chart/geo-chart.tsx +12 -4
  24. package/src/charts/heatmap-chart/heatmap-chart.module.scss +22 -9
  25. package/src/charts/heatmap-chart/heatmap-chart.tsx +43 -13
  26. package/src/charts/heatmap-chart/private/build-calendar-data.ts +65 -5
  27. package/src/charts/heatmap-chart/test/build-calendar-data.test.ts +125 -0
  28. package/src/charts/heatmap-chart/types.ts +8 -0
  29. package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +13 -7
  30. package/src/charts/leaderboard-chart/leaderboard-chart.tsx +17 -22
  31. package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +107 -1
  32. package/src/charts/leaderboard-chart/types.ts +0 -2
  33. package/src/charts/line-chart/line-chart.module.scss +12 -7
  34. package/src/charts/line-chart/line-chart.tsx +32 -28
  35. package/src/charts/line-chart/private/line-chart-annotation.tsx +4 -4
  36. package/src/charts/line-chart/private/line-chart-annotations-overlay.tsx +2 -2
  37. package/src/charts/line-chart/test/line-chart.test.tsx +262 -5
  38. package/src/charts/line-chart/types.ts +4 -3
  39. package/src/charts/pie-chart/pie-chart.tsx +12 -14
  40. package/src/charts/pie-chart/test/pie-chart.test.tsx +73 -1
  41. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.module.scss +2 -2
  42. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +12 -14
  43. package/src/charts/private/{single-chart-context/single-chart-context.tsx → chart-instance-context/chart-instance-context.ts} +1 -5
  44. package/src/charts/private/chart-instance-context/index.ts +3 -0
  45. package/src/charts/private/{single-chart-context/use-single-chart-context.ts → chart-instance-context/use-chart-instance-context.ts} +1 -3
  46. package/src/charts/private/svg-empty-state/get-all-hidden-message.ts +41 -0
  47. package/src/charts/private/svg-empty-state/index.ts +1 -0
  48. package/src/charts/private/svg-empty-state/svg-empty-state.module.scss +1 -1
  49. package/src/charts/private/svg-empty-state/test/get-all-hidden-message.test.ts +15 -0
  50. package/src/charts/private/with-responsive/test/with-responsive.test.tsx +16 -0
  51. package/src/charts/private/with-responsive/with-responsive.tsx +18 -11
  52. package/src/charts/private/x-zoom/test/x-zoom.test.tsx +2 -2
  53. package/src/charts/private/x-zoom/x-zoom.module.scss +3 -3
  54. package/src/charts/private/x-zoom/x-zoom.tsx +2 -2
  55. package/src/components/legend/legend.tsx +4 -4
  56. package/src/components/legend/private/base-legend.module.scss +1 -1
  57. package/src/components/legend/private/base-legend.tsx +70 -19
  58. package/src/components/legend/test/legend.test.tsx +100 -4
  59. package/src/components/tooltip/accessible-tooltip.tsx +17 -4
  60. package/src/components/tooltip/base-tooltip.tsx +8 -1
  61. package/src/components/trend-indicator/test/trend-indicator.test.tsx +20 -0
  62. package/src/components/trend-indicator/trend-indicator.module.scss +4 -7
  63. package/src/components/trend-indicator/trend-indicator.tsx +3 -0
  64. package/src/hooks/index.ts +1 -1
  65. package/src/hooks/test/use-xychart-theme-provider-override.test.tsx +28 -0
  66. package/src/hooks/test/use-xychart-theme-style-reads.test.tsx +19 -13
  67. package/src/hooks/test/use-xychart-theme.test.tsx +25 -0
  68. package/src/hooks/{use-interactive-legend-data.ts → use-legend-visibility-data.ts} +29 -30
  69. package/src/hooks/use-xychart-theme.ts +6 -4
  70. package/src/index.ts +12 -3
  71. package/src/providers/chart-context/global-charts-provider.tsx +127 -30
  72. package/src/providers/chart-context/hooks/use-default-hidden-series.ts +47 -0
  73. package/src/providers/chart-context/private/theme-override-vars.ts +87 -0
  74. package/src/providers/chart-context/private/with-catalog-pointers.ts +50 -0
  75. package/src/providers/chart-context/test/chart-context.test.tsx +142 -40
  76. package/src/providers/chart-context/test/chart-scope-class.test.tsx +19 -0
  77. package/src/providers/chart-context/test/sparse-theme.test.ts +35 -0
  78. package/src/providers/chart-context/test/theme-override-vars-guard.test.tsx +29 -0
  79. package/src/providers/chart-context/test/theme-override-vars.test.ts +118 -0
  80. package/src/providers/chart-context/test/use-default-hidden-series-paint.test.tsx +56 -0
  81. package/src/providers/chart-context/test/use-default-hidden-series.ssr.test.tsx +39 -0
  82. package/src/providers/chart-context/test/use-default-hidden-series.test.tsx +222 -0
  83. package/src/providers/chart-context/test/with-catalog-pointers.test.ts +47 -0
  84. package/src/providers/chart-context/themes.ts +18 -27
  85. package/src/providers/chart-context/types.ts +7 -4
  86. package/src/providers/chart-scope/chart-scope-context.ts +8 -0
  87. package/src/providers/chart-scope/index.ts +3 -0
  88. package/src/providers/chart-scope/test/chart-scope.test.tsx +24 -0
  89. package/src/providers/chart-scope/test/use-standalone-scope-class.test.tsx +20 -0
  90. package/src/providers/chart-scope/use-chart-scope-element.ts +9 -0
  91. package/src/providers/chart-scope/use-standalone-scope-class.ts +13 -0
  92. package/src/providers/index.ts +1 -0
  93. package/src/styles/chart-scope-class.ts +6 -0
  94. package/src/styles/chart-scope.scss +83 -0
  95. package/src/styles/test/chart-scope.test.ts +165 -0
  96. package/src/styles/test/wpds-fallbacks.test.ts +131 -0
  97. package/src/types.ts +12 -34
  98. package/src/charts/private/grid-control/grid-control.module.scss +0 -8
  99. package/src/charts/private/grid-control/grid-control.tsx +0 -26
  100. package/src/charts/private/grid-control/index.ts +0 -1
  101. package/src/charts/private/grid-control/test/grid-control.test.tsx +0 -56
  102. package/src/charts/private/single-chart-context/index.ts +0 -8
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automattic/charts",
3
- "version": "2.0.1",
3
+ "version": "3.1.0",
4
4
  "description": "Display charts within Automattic products.",
5
5
  "homepage": "https://github.com/Automattic/jetpack/tree/HEAD/projects/js-packages/charts/#readme",
6
6
  "bugs": {
@@ -1,14 +1,15 @@
1
1
  .area-chart {
2
2
  position: relative;
3
3
 
4
+ // Keep the collapsed state in `from` and never on the element: a dropped
5
+ // animation then fails visible. `both`, not `forwards`, for backwards fill.
4
6
  &--animated {
5
7
 
6
8
  path {
7
9
  transform-origin: 0 95%;
8
- transform: scaleY(0);
9
10
  animation:
10
- rise var(--wpds-motion-duration-xl, 400ms)
11
- var(--wpds-motion-easing-expressive, cubic-bezier(0.25, 0, 0, 1)) forwards;
11
+ rise var(--a8c-charts-motion-duration-entrance)
12
+ var(--a8c-charts-motion-easing-entrance) both;
12
13
  }
13
14
  }
14
15
 
@@ -19,6 +20,10 @@
19
20
 
20
21
  @keyframes rise {
21
22
 
23
+ from {
24
+ transform: scaleY(0);
25
+ }
26
+
22
27
  to {
23
28
  transform: scaleY(1);
24
29
  }
@@ -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 { SingleChartContext, type SingleChartRef } from '../private/single-chart-context';
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< SingleChartRef, AreaChartProps >(
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< SingleChartRef >( null );
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, isSeriesVisible } = useGlobalChartsContext();
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: isSeriesVisible( chartId, series.label ),
137
+ isVisible: ! hiddenSeries.has( series.label ),
134
138
  } ) );
135
- }, [ dataSorted, chartId, isSeriesVisible, legendInteractive ] );
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, legendInteractive, rescaleYOnVisibility ] );
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, legendInteractive, visibleLabels ]
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 || ! legendInteractive ? accessors.yAccessor : zeroYAccessor }
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
- <SingleChartContext.Provider
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
 
@@ -482,7 +482,7 @@ const AreaChartInternal = forwardRef< SingleChartRef, AreaChartProps >(
482
482
  stacked={ stacked }
483
483
  stackOffset={ stackOffset }
484
484
  getElementStyles={ getElementStyles }
485
- // useXYChartTheme resolved this role inside its memo; reading it back avoids a getComputedStyle on every render.
485
+ // useXYChartTheme resolved this role inside its memo, against the chart's scope element; reading it back avoids a getComputedStyle on every render.
486
486
  strokeColor={ theme.backgroundColor ?? providerTheme.backgroundColor }
487
487
  />
488
488
  </>
@@ -503,7 +503,7 @@ const AreaChartInternal = forwardRef< SingleChartRef, AreaChartProps >(
503
503
  );
504
504
  } }
505
505
  </ChartLayout>
506
- </SingleChartContext.Provider>
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< SingleChartRef >
518
+ AreaChartBaseProps & React.RefAttributes< ChartInstanceRef >
519
519
  > &
520
520
  AreaChartSubComponents;
521
521
 
522
522
  type AreaChartResponsiveComponent = React.ForwardRefExoticComponent<
523
- AreaChartBaseProps & ResponsiveConfig & React.RefAttributes< SingleChartRef >
523
+ AreaChartBaseProps & ResponsiveConfig & React.RefAttributes< ChartInstanceRef >
524
524
  > &
525
525
  AreaChartSubComponents;
526
526
 
527
- const AreaChartWithProvider = forwardRef< SingleChartRef, AreaChartProps >( ( props, ref ) => {
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 { SingleChartRef } from '../../private/single-chart-context';
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 `SingleChartRef` so consumers
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< SingleChartRef >;
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 { SingleChartRef } from '../../private/single-chart-context';
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< SingleChartRef > ) => {
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< SingleChartRef >();
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< SingleChartRef >();
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< SingleChartRef >();
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< SingleChartRef >();
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< SingleChartRef >();
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< SingleChartRef >();
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< SingleChartRef >();
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< SingleChartRef >();
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 series are
67
- * hidden or shown through the interactive legend.
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