@automattic/charts 3.0.0 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/TOKENS.md +3 -3
- package/dist/index.cjs +250 -136
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +23 -14
- package/dist/index.d.cts +51 -14
- package/dist/index.d.ts +51 -14
- package/dist/index.js +251 -138
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/charts/area-chart/area-chart.tsx +26 -26
- package/src/charts/area-chart/private/overlays.tsx +3 -3
- package/src/charts/area-chart/test/area-chart.test.tsx +346 -11
- package/src/charts/area-chart/types.ts +4 -3
- package/src/charts/bar-chart/bar-chart.tsx +21 -15
- package/src/charts/bar-chart/private/use-bar-chart-options.ts +19 -12
- package/src/charts/bar-chart/test/bar-chart.test.tsx +277 -2
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.module.scss +6 -6
- package/src/charts/geo-chart/geo-chart.module.scss +8 -0
- package/src/charts/heatmap-chart/heatmap-chart.module.scss +9 -0
- package/src/charts/heatmap-chart/heatmap-chart.tsx +31 -8
- package/src/charts/heatmap-chart/private/build-calendar-data.ts +65 -5
- package/src/charts/heatmap-chart/test/build-calendar-data.test.ts +125 -0
- package/src/charts/heatmap-chart/types.ts +8 -0
- package/src/charts/leaderboard-chart/leaderboard-chart.tsx +17 -22
- package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +107 -1
- package/src/charts/line-chart/line-chart.module.scss +2 -2
- package/src/charts/line-chart/line-chart.tsx +33 -29
- package/src/charts/line-chart/private/line-chart-annotations-overlay.tsx +2 -2
- package/src/charts/line-chart/test/line-chart.test.tsx +313 -5
- package/src/charts/line-chart/types.ts +4 -3
- package/src/charts/pie-chart/pie-chart.tsx +8 -13
- package/src/charts/pie-chart/test/pie-chart.test.tsx +73 -1
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.module.scss +2 -2
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +8 -13
- package/src/charts/private/{single-chart-context/single-chart-context.tsx → chart-instance-context/chart-instance-context.ts} +1 -5
- package/src/charts/private/chart-instance-context/index.ts +3 -0
- package/src/charts/private/{single-chart-context/use-single-chart-context.ts → chart-instance-context/use-chart-instance-context.ts} +1 -3
- package/src/charts/private/svg-empty-state/get-all-hidden-message.ts +41 -0
- package/src/charts/private/svg-empty-state/index.ts +1 -0
- package/src/charts/private/svg-empty-state/test/get-all-hidden-message.test.ts +15 -0
- package/src/charts/private/x-zoom/test/x-zoom.test.tsx +2 -2
- package/src/charts/private/x-zoom/x-zoom.tsx +2 -2
- package/src/components/legend/legend.tsx +4 -4
- package/src/components/legend/private/base-legend.module.scss +1 -1
- package/src/components/legend/private/base-legend.tsx +67 -18
- package/src/components/legend/test/legend.test.tsx +100 -4
- package/src/components/tooltip/accessible-tooltip.tsx +13 -2
- package/src/components/trend-indicator/trend-indicator.module.scss +1 -1
- package/src/hooks/index.ts +1 -1
- package/src/hooks/{use-interactive-legend-data.ts → use-legend-visibility-data.ts} +29 -30
- package/src/index.ts +3 -1
- package/src/providers/chart-context/global-charts-provider.tsx +92 -18
- package/src/providers/chart-context/hooks/use-default-hidden-series.ts +47 -0
- package/src/providers/chart-context/test/chart-context.test.tsx +142 -0
- package/src/providers/chart-context/test/use-default-hidden-series-paint.test.tsx +56 -0
- package/src/providers/chart-context/test/use-default-hidden-series.ssr.test.tsx +39 -0
- package/src/providers/chart-context/test/use-default-hidden-series.test.tsx +222 -0
- package/src/providers/chart-context/types.ts +7 -1
- package/src/styles/chart-scope.scss +2 -2
- package/src/styles/test/wpds-fallbacks.test.ts +131 -0
- package/src/types.ts +12 -0
- package/src/charts/private/single-chart-context/index.ts +0 -8
|
@@ -20,11 +20,12 @@ import {
|
|
|
20
20
|
useGlobalChartsContext,
|
|
21
21
|
GlobalChartsContext,
|
|
22
22
|
} from '../../providers';
|
|
23
|
+
import { useDefaultHiddenSeries } from '../../providers/chart-context/hooks/use-default-hidden-series';
|
|
23
24
|
import { attachSubComponents } from '../../utils';
|
|
24
25
|
import { useChartChildren } from '../private/chart-composition';
|
|
26
|
+
import { ChartInstanceContext } from '../private/chart-instance-context';
|
|
25
27
|
import { ChartLayout } from '../private/chart-layout';
|
|
26
|
-
import {
|
|
27
|
-
import { SvgEmptyState } from '../private/svg-empty-state';
|
|
28
|
+
import { getAllHiddenMessage, SvgEmptyState } from '../private/svg-empty-state';
|
|
28
29
|
import { withResponsive } from '../private/with-responsive';
|
|
29
30
|
import styles from './bar-chart.module.scss';
|
|
30
31
|
import {
|
|
@@ -42,13 +43,14 @@ import type {
|
|
|
42
43
|
DataPointDate,
|
|
43
44
|
SeriesData,
|
|
44
45
|
SeriesChartLegendConfig,
|
|
46
|
+
SeriesVisibilityProps,
|
|
45
47
|
Optional,
|
|
46
48
|
} from '../../types';
|
|
47
49
|
import type { RenderTooltipParams } from '../../visx/types';
|
|
48
50
|
import type { ResponsiveConfig } from '../private/with-responsive';
|
|
49
51
|
import type { FC, ReactNode, ComponentType } from 'react';
|
|
50
52
|
|
|
51
|
-
export interface BarChartProps extends BaseChartProps< SeriesData[]
|
|
53
|
+
export interface BarChartProps extends BaseChartProps< SeriesData[] >, SeriesVisibilityProps {
|
|
52
54
|
/**
|
|
53
55
|
* Legend configuration. Supports `collapseGroups` on top of the shared options.
|
|
54
56
|
*/
|
|
@@ -122,6 +124,7 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
122
124
|
orientation = 'vertical',
|
|
123
125
|
withPatterns = false,
|
|
124
126
|
showZeroValues = false,
|
|
127
|
+
defaultHiddenSeries,
|
|
125
128
|
animation,
|
|
126
129
|
children,
|
|
127
130
|
gap = 'md',
|
|
@@ -130,6 +133,11 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
130
133
|
const legendCollapseGroups = legend.collapseGroups ?? false;
|
|
131
134
|
const horizontal = orientation === 'horizontal';
|
|
132
135
|
const chartId = useChartId( providedChartId );
|
|
136
|
+
const hiddenSeries = useDefaultHiddenSeries( chartId, defaultHiddenSeries );
|
|
137
|
+
const isSeriesVisible = useCallback(
|
|
138
|
+
( seriesLabel: string ) => ! hiddenSeries.has( seriesLabel ),
|
|
139
|
+
[ hiddenSeries ]
|
|
140
|
+
);
|
|
133
141
|
const theme = useXYChartTheme( data );
|
|
134
142
|
|
|
135
143
|
const dataSorted = useChartDataTransform( data );
|
|
@@ -148,14 +156,14 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
148
156
|
);
|
|
149
157
|
const legendItems = useChartLegendItems( dataSorted, legendOptions );
|
|
150
158
|
|
|
151
|
-
const { getElementStyles
|
|
159
|
+
const { getElementStyles } = useGlobalChartsContext();
|
|
152
160
|
|
|
153
161
|
// A hidden series is unmounted, so it is not on the band scale either — the
|
|
154
|
-
// axis has to choose its tick values from what is left.
|
|
162
|
+
// axis has to choose its tick values from what is left. Visibility is owned by
|
|
163
|
+
// the provider, whether it changed through the legend or programmatically.
|
|
155
164
|
const isSeriesRendered = useCallback(
|
|
156
|
-
( series: SeriesData ) =>
|
|
157
|
-
|
|
158
|
-
[ chartId, legendInteractive, isSeriesVisible ]
|
|
165
|
+
( series: SeriesData ) => isSeriesVisible( series.label ),
|
|
166
|
+
[ isSeriesVisible ]
|
|
159
167
|
);
|
|
160
168
|
|
|
161
169
|
const chartOptions = useBarChartOptions(
|
|
@@ -198,7 +206,7 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
198
206
|
totalPoints,
|
|
199
207
|
} );
|
|
200
208
|
|
|
201
|
-
// Add visibility information
|
|
209
|
+
// Add visibility information from the shared legend state.
|
|
202
210
|
const seriesWithVisibility = useMemo(
|
|
203
211
|
() =>
|
|
204
212
|
dataWithVisibleZeros.map( ( series, index ) => ( {
|
|
@@ -514,9 +522,10 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
514
522
|
);
|
|
515
523
|
|
|
516
524
|
return (
|
|
517
|
-
<
|
|
525
|
+
<ChartInstanceContext.Provider
|
|
518
526
|
value={ {
|
|
519
527
|
chartId,
|
|
528
|
+
isSeriesVisible,
|
|
520
529
|
chartWidth: width,
|
|
521
530
|
chartHeight: measuredChartHeight || 0,
|
|
522
531
|
} }
|
|
@@ -606,10 +615,7 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
606
615
|
width={ width }
|
|
607
616
|
height={ chartHeight }
|
|
608
617
|
>
|
|
609
|
-
{
|
|
610
|
-
'All series are hidden. Click legend items to show data.',
|
|
611
|
-
'jetpack-charts'
|
|
612
|
-
) }
|
|
618
|
+
{ getAllHiddenMessage( legendInteractive, 'series' ) }
|
|
613
619
|
</SvgEmptyState>
|
|
614
620
|
) : null }
|
|
615
621
|
|
|
@@ -673,7 +679,7 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
673
679
|
);
|
|
674
680
|
} }
|
|
675
681
|
</ChartLayout>
|
|
676
|
-
</
|
|
682
|
+
</ChartInstanceContext.Provider>
|
|
677
683
|
);
|
|
678
684
|
};
|
|
679
685
|
|
|
@@ -154,23 +154,29 @@ export function useBarChartOptions(
|
|
|
154
154
|
const stableOptions = useDeepMemo( options );
|
|
155
155
|
|
|
156
156
|
// `labelOverflow` and `tickResolution` are consumed by this hook rather than
|
|
157
|
-
// forwarded — visx has an axis prop for neither —
|
|
158
|
-
//
|
|
157
|
+
// forwarded — visx has an axis prop for neither — and `tickFormat` is merged
|
|
158
|
+
// with the derived default explicitly, so an `undefined` passed by a caller
|
|
159
|
+
// cannot clobber it through the spread. They are split off the caller's axis
|
|
160
|
+
// options once, here, and only the rest reaches visx below.
|
|
159
161
|
const axisConfig = useMemo( () => {
|
|
160
162
|
const {
|
|
161
163
|
labelOverflow: xLabelOverflow,
|
|
162
164
|
tickResolution: xTickResolution,
|
|
165
|
+
tickFormat: xTickFormat,
|
|
163
166
|
...xAxisOptions
|
|
164
167
|
} = stableOptions.axis?.x || {};
|
|
165
168
|
const {
|
|
166
169
|
labelOverflow: yLabelOverflow,
|
|
167
170
|
tickResolution: yTickResolution,
|
|
171
|
+
tickFormat: yTickFormat,
|
|
168
172
|
...yAxisOptions
|
|
169
173
|
} = stableOptions.axis?.y || {};
|
|
170
174
|
|
|
171
175
|
return {
|
|
172
176
|
xLabelOverflow,
|
|
173
177
|
yLabelOverflow,
|
|
178
|
+
xTickFormat,
|
|
179
|
+
yTickFormat,
|
|
174
180
|
xAxisOptions,
|
|
175
181
|
yAxisOptions,
|
|
176
182
|
// The dates sit on the x axis normally, and on the y axis when the
|
|
@@ -243,8 +249,8 @@ export function useBarChartOptions(
|
|
|
243
249
|
return useMemo( () => {
|
|
244
250
|
const orientationKey = horizontal ? 'horizontal' : 'vertical';
|
|
245
251
|
const {
|
|
246
|
-
xTickFormat,
|
|
247
|
-
yTickFormat,
|
|
252
|
+
xTickFormat: defaultXTickFormat,
|
|
253
|
+
yTickFormat: defaultYTickFormat,
|
|
248
254
|
tooltipLabelFormatter: defaultTooltipLabelFormatter,
|
|
249
255
|
xAccessor,
|
|
250
256
|
yAccessor,
|
|
@@ -294,10 +300,11 @@ export function useBarChartOptions(
|
|
|
294
300
|
...( stableOptions.yScale || {} ),
|
|
295
301
|
...( ! horizontal ? valueScaleDomainOverride : {} ),
|
|
296
302
|
};
|
|
297
|
-
const { xLabelOverflow, yLabelOverflow, xAxisOptions, yAxisOptions } =
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
303
|
+
const { xLabelOverflow, yLabelOverflow, xTickFormat, yTickFormat, xAxisOptions, yAxisOptions } =
|
|
304
|
+
axisConfig;
|
|
305
|
+
// The dates sit on the y axis of a horizontal chart, so the caller's format
|
|
306
|
+
// for them moves with the orientation. It also labels the tooltip.
|
|
307
|
+
const dateAxisTickFormat = horizontal ? yTickFormat : xTickFormat;
|
|
301
308
|
|
|
302
309
|
// A band scale has no ticks of its own for visx to ask for, so it samples
|
|
303
310
|
// the domain by index and can miss the tick that dates the day or names the
|
|
@@ -306,7 +313,7 @@ export function useBarChartOptions(
|
|
|
306
313
|
const { timeAxis } = defaultOptions;
|
|
307
314
|
const dateAxisOptions = horizontal ? yAxisOptions : xAxisOptions;
|
|
308
315
|
const bandTickValues =
|
|
309
|
-
timeAxis && !
|
|
316
|
+
timeAxis && ! dateAxisTickFormat
|
|
310
317
|
? getBandTickValues(
|
|
311
318
|
timeAxis.domain,
|
|
312
319
|
timeAxis.tickFormatter,
|
|
@@ -327,7 +334,7 @@ export function useBarChartOptions(
|
|
|
327
334
|
x: {
|
|
328
335
|
orientation: 'bottom' as const,
|
|
329
336
|
numTicks: DEFAULT_NUM_TICKS,
|
|
330
|
-
tickFormat: xTickFormat,
|
|
337
|
+
tickFormat: xTickFormat || defaultXTickFormat,
|
|
331
338
|
...( horizontal ? {} : dateAxisTickValues ),
|
|
332
339
|
...( xLabelOverflow === 'ellipsis' ? { tickComponent: TruncatedXTickComponent } : {} ),
|
|
333
340
|
...xAxisOptions,
|
|
@@ -335,7 +342,7 @@ export function useBarChartOptions(
|
|
|
335
342
|
y: {
|
|
336
343
|
orientation: 'left' as const,
|
|
337
344
|
numTicks: DEFAULT_NUM_TICKS,
|
|
338
|
-
tickFormat: yTickFormat,
|
|
345
|
+
tickFormat: yTickFormat || defaultYTickFormat,
|
|
339
346
|
...( horizontal ? dateAxisTickValues : {} ),
|
|
340
347
|
...( yLabelOverflow === 'ellipsis' ? { tickComponent: TruncatedYTickComponent } : {} ),
|
|
341
348
|
...yAxisOptions,
|
|
@@ -345,7 +352,7 @@ export function useBarChartOptions(
|
|
|
345
352
|
padding: getGroupPadding( horizontal ? yScale : xScale ),
|
|
346
353
|
},
|
|
347
354
|
tooltip: {
|
|
348
|
-
labelFormatter:
|
|
355
|
+
labelFormatter: dateAxisTickFormat || defaultTooltipLabelFormatter,
|
|
349
356
|
},
|
|
350
357
|
};
|
|
351
358
|
}, [ defaultOptions, axisConfig, stableOptions, horizontal, data ] );
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { render, renderHook, screen, within } from '@testing-library/react';
|
|
1
|
+
import { render, renderHook, screen, within, act } from '@testing-library/react';
|
|
2
2
|
import userEvent from '@testing-library/user-event';
|
|
3
3
|
import { GlobalChartsProvider } from '../../../providers';
|
|
4
|
-
import
|
|
4
|
+
import { useGlobalChartsContext } from '../../../providers/chart-context/hooks/use-global-charts-context';
|
|
5
|
+
import BarChart, { BarChartUnresponsive } from '../bar-chart';
|
|
5
6
|
import { useBarChartOptions } from '../private';
|
|
7
|
+
import type { GlobalChartsContextValue } from '../../../providers/chart-context/types';
|
|
6
8
|
import type { SeriesData } from '../../../types';
|
|
7
9
|
|
|
8
10
|
// Mock useElementSize to return non-zero dimensions in jsdom so charts render
|
|
@@ -503,6 +505,28 @@ describe( 'BarChart', () => {
|
|
|
503
505
|
expect( distinctTexts( ticks ).size ).toBeGreaterThan( 1 );
|
|
504
506
|
} );
|
|
505
507
|
|
|
508
|
+
test( 'keeps the derived date formatter when tickFormat is passed as undefined', () => {
|
|
509
|
+
renderWithTheme( {
|
|
510
|
+
width: 800,
|
|
511
|
+
options: { axis: { x: { tickFormat: undefined } } },
|
|
512
|
+
data: [
|
|
513
|
+
{
|
|
514
|
+
label: 'Series A',
|
|
515
|
+
data: Array.from( { length: 14 }, ( _, i ) => ( {
|
|
516
|
+
date: new Date( 2024, 0, 1 + i ),
|
|
517
|
+
value: 10 + i,
|
|
518
|
+
} ) ),
|
|
519
|
+
options: {},
|
|
520
|
+
},
|
|
521
|
+
],
|
|
522
|
+
} );
|
|
523
|
+
|
|
524
|
+
const ticks = screen.getAllByText( /^Jan \d+$/ );
|
|
525
|
+
expect( distinctTexts( ticks ).size ).toBeGreaterThan( 1 );
|
|
526
|
+
// A clobbered formatter falls back to raw `Date.toString()` ticks.
|
|
527
|
+
expect( screen.queryByText( /GMT/ ) ).not.toBeInTheDocument();
|
|
528
|
+
} );
|
|
529
|
+
|
|
506
530
|
test( 'renders distinct hour ticks for sub-daily buckets in a single day', () => {
|
|
507
531
|
renderWithTheme( {
|
|
508
532
|
width: 800,
|
|
@@ -1017,6 +1041,64 @@ describe( 'BarChart', () => {
|
|
|
1017
1041
|
expect( screen.queryByTestId( 'chart-tooltip-0' ) ).not.toBeInTheDocument();
|
|
1018
1042
|
} );
|
|
1019
1043
|
|
|
1044
|
+
test( 'keeps the tooltip open once the chart re-renders under it', async () => {
|
|
1045
|
+
// visx hides its tooltip on a debounce and cancels only the most recently
|
|
1046
|
+
// scheduled hide, so anything that re-runs the tooltip effect while nothing
|
|
1047
|
+
// is shown leaves a hide pending that lands on the next tooltip. Seeding a
|
|
1048
|
+
// hidden series re-renders the chart right after mount, which is exactly
|
|
1049
|
+
// that shape.
|
|
1050
|
+
jest.useFakeTimers();
|
|
1051
|
+
|
|
1052
|
+
try {
|
|
1053
|
+
const user = userEvent.setup( { advanceTimers: jest.advanceTimersByTime } );
|
|
1054
|
+
renderWithTheme( {
|
|
1055
|
+
withTooltips: true,
|
|
1056
|
+
defaultHiddenSeries: [ 'Series B' ],
|
|
1057
|
+
data: [
|
|
1058
|
+
{
|
|
1059
|
+
label: 'Series A',
|
|
1060
|
+
data: [
|
|
1061
|
+
{ date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' },
|
|
1062
|
+
{ date: new Date( '2024-01-02' ), value: 20, label: 'Jan 2' },
|
|
1063
|
+
],
|
|
1064
|
+
options: {},
|
|
1065
|
+
},
|
|
1066
|
+
{
|
|
1067
|
+
label: 'Series B',
|
|
1068
|
+
data: [
|
|
1069
|
+
{ date: new Date( '2024-01-01' ), value: 15, label: 'Jan 1' },
|
|
1070
|
+
{ date: new Date( '2024-01-02' ), value: 25, label: 'Jan 2' },
|
|
1071
|
+
],
|
|
1072
|
+
options: {},
|
|
1073
|
+
},
|
|
1074
|
+
],
|
|
1075
|
+
} );
|
|
1076
|
+
|
|
1077
|
+
const chart = screen.getByRole( 'grid', { name: /bar chart/i } );
|
|
1078
|
+
chart.focus();
|
|
1079
|
+
|
|
1080
|
+
await user.keyboard( '{ArrowRight}' );
|
|
1081
|
+
expect( screen.getByTestId( 'chart-tooltip-0' ) ).toHaveTextContent( 'Series A' );
|
|
1082
|
+
|
|
1083
|
+
// Well past any pending hide.
|
|
1084
|
+
await act( async () => {
|
|
1085
|
+
jest.advanceTimersByTime( 5000 );
|
|
1086
|
+
} );
|
|
1087
|
+
|
|
1088
|
+
expect( screen.getByTestId( 'chart-tooltip-0' ) ).toBeInTheDocument();
|
|
1089
|
+
|
|
1090
|
+
// Leaving navigation still closes it.
|
|
1091
|
+
await user.keyboard( '{Escape}' );
|
|
1092
|
+
await act( async () => {
|
|
1093
|
+
jest.advanceTimersByTime( 5000 );
|
|
1094
|
+
} );
|
|
1095
|
+
|
|
1096
|
+
expect( screen.queryByTestId( 'chart-tooltip-0' ) ).not.toBeInTheDocument();
|
|
1097
|
+
} finally {
|
|
1098
|
+
jest.useRealTimers();
|
|
1099
|
+
}
|
|
1100
|
+
} );
|
|
1101
|
+
|
|
1020
1102
|
test( 'left arrow key navigates to previous data point', async () => {
|
|
1021
1103
|
const user = userEvent.setup();
|
|
1022
1104
|
renderWithTheme( {
|
|
@@ -1918,6 +2000,133 @@ describe( 'BarChart', () => {
|
|
|
1918
2000
|
screen.getByText( /all series are hidden.*click legend items to show data/i )
|
|
1919
2001
|
).toBeInTheDocument();
|
|
1920
2002
|
} );
|
|
2003
|
+
|
|
2004
|
+
it( 'hides a series programmatically when the legend is not interactive', () => {
|
|
2005
|
+
let context: GlobalChartsContextValue;
|
|
2006
|
+
const Grab = () => {
|
|
2007
|
+
context = useGlobalChartsContext();
|
|
2008
|
+
return null;
|
|
2009
|
+
};
|
|
2010
|
+
|
|
2011
|
+
render(
|
|
2012
|
+
<GlobalChartsProvider>
|
|
2013
|
+
<Grab />
|
|
2014
|
+
<BarChartUnresponsive
|
|
2015
|
+
width={ 500 }
|
|
2016
|
+
height={ 300 }
|
|
2017
|
+
showLegend={ true }
|
|
2018
|
+
legend={ { interactive: false } }
|
|
2019
|
+
chartId="test-programmatic-bar"
|
|
2020
|
+
data={ [
|
|
2021
|
+
{
|
|
2022
|
+
label: 'Series A',
|
|
2023
|
+
data: [ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' } ],
|
|
2024
|
+
options: {},
|
|
2025
|
+
},
|
|
2026
|
+
{
|
|
2027
|
+
label: 'Series B',
|
|
2028
|
+
data: [ { date: new Date( '2024-01-01' ), value: 20, label: 'Jan 1' } ],
|
|
2029
|
+
options: {},
|
|
2030
|
+
},
|
|
2031
|
+
] }
|
|
2032
|
+
/>
|
|
2033
|
+
</GlobalChartsProvider>
|
|
2034
|
+
);
|
|
2035
|
+
|
|
2036
|
+
act( () => {
|
|
2037
|
+
context.toggleSeriesVisibility( 'test-programmatic-bar', 'Series A' );
|
|
2038
|
+
context.toggleSeriesVisibility( 'test-programmatic-bar', 'Series B' );
|
|
2039
|
+
} );
|
|
2040
|
+
|
|
2041
|
+
expect( screen.getByText( /all series are hidden/i ) ).toBeInTheDocument();
|
|
2042
|
+
} );
|
|
2043
|
+
|
|
2044
|
+
it( 'omits the click instruction when the legend cannot be clicked', () => {
|
|
2045
|
+
let context: GlobalChartsContextValue;
|
|
2046
|
+
const Grab = () => {
|
|
2047
|
+
context = useGlobalChartsContext();
|
|
2048
|
+
return null;
|
|
2049
|
+
};
|
|
2050
|
+
|
|
2051
|
+
render(
|
|
2052
|
+
<GlobalChartsProvider>
|
|
2053
|
+
<Grab />
|
|
2054
|
+
<BarChartUnresponsive
|
|
2055
|
+
width={ 500 }
|
|
2056
|
+
height={ 300 }
|
|
2057
|
+
showLegend={ true }
|
|
2058
|
+
legend={ { interactive: false } }
|
|
2059
|
+
chartId="test-empty-copy-bar"
|
|
2060
|
+
data={ [
|
|
2061
|
+
{
|
|
2062
|
+
label: 'Series A',
|
|
2063
|
+
data: [ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' } ],
|
|
2064
|
+
options: {},
|
|
2065
|
+
},
|
|
2066
|
+
] }
|
|
2067
|
+
/>
|
|
2068
|
+
</GlobalChartsProvider>
|
|
2069
|
+
);
|
|
2070
|
+
|
|
2071
|
+
act( () => {
|
|
2072
|
+
context.toggleSeriesVisibility( 'test-empty-copy-bar', 'Series A' );
|
|
2073
|
+
} );
|
|
2074
|
+
|
|
2075
|
+
expect( screen.getByText( 'All series are hidden.' ) ).toBeInTheDocument();
|
|
2076
|
+
expect( screen.queryByText( /click legend items/i ) ).not.toBeInTheDocument();
|
|
2077
|
+
} );
|
|
2078
|
+
|
|
2079
|
+
it( 'still renders the other series when only one is hidden programmatically', () => {
|
|
2080
|
+
// A test that hides every series would pass even if the chart still forced
|
|
2081
|
+
// all series visible right up until `allSeriesHidden` short-circuited it —
|
|
2082
|
+
// that shape of test masked a real bug in a sibling chart. Hiding exactly
|
|
2083
|
+
// one of two series exercises the partial-hide render path: the bar count
|
|
2084
|
+
// only drops to one if the hidden series' `BarSeries` is actually excluded
|
|
2085
|
+
// from the `BarGroup`, not merely rendered with zero opacity.
|
|
2086
|
+
let context: GlobalChartsContextValue;
|
|
2087
|
+
const Grab = () => {
|
|
2088
|
+
context = useGlobalChartsContext();
|
|
2089
|
+
return null;
|
|
2090
|
+
};
|
|
2091
|
+
|
|
2092
|
+
render(
|
|
2093
|
+
<GlobalChartsProvider>
|
|
2094
|
+
<Grab />
|
|
2095
|
+
<BarChartUnresponsive
|
|
2096
|
+
width={ 500 }
|
|
2097
|
+
height={ 300 }
|
|
2098
|
+
showLegend={ true }
|
|
2099
|
+
legend={ { interactive: false } }
|
|
2100
|
+
chartId="test-programmatic-partial-bar"
|
|
2101
|
+
data={ [
|
|
2102
|
+
{
|
|
2103
|
+
label: 'Series A',
|
|
2104
|
+
data: [ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' } ],
|
|
2105
|
+
options: {},
|
|
2106
|
+
},
|
|
2107
|
+
{
|
|
2108
|
+
label: 'Series B',
|
|
2109
|
+
data: [ { date: new Date( '2024-01-01' ), value: 20, label: 'Jan 1' } ],
|
|
2110
|
+
options: {},
|
|
2111
|
+
},
|
|
2112
|
+
] }
|
|
2113
|
+
/>
|
|
2114
|
+
</GlobalChartsProvider>
|
|
2115
|
+
);
|
|
2116
|
+
|
|
2117
|
+
// eslint-disable-next-line testing-library/no-node-access
|
|
2118
|
+
const svgElement = screen.getByRole( 'grid', { name: /bar chart/i } ).querySelector( 'svg' );
|
|
2119
|
+
// eslint-disable-next-line testing-library/no-node-access
|
|
2120
|
+
expect( svgElement?.querySelectorAll( '.visx-bar-group rect' ) ).toHaveLength( 2 );
|
|
2121
|
+
|
|
2122
|
+
act( () => {
|
|
2123
|
+
context.toggleSeriesVisibility( 'test-programmatic-partial-bar', 'Series B' );
|
|
2124
|
+
} );
|
|
2125
|
+
|
|
2126
|
+
expect( screen.queryByText( /all series are hidden/i ) ).not.toBeInTheDocument();
|
|
2127
|
+
// eslint-disable-next-line testing-library/no-node-access
|
|
2128
|
+
expect( svgElement?.querySelectorAll( '.visx-bar-group rect' ) ).toHaveLength( 1 );
|
|
2129
|
+
} );
|
|
1921
2130
|
} );
|
|
1922
2131
|
|
|
1923
2132
|
describe( 'Legend group collapsing', () => {
|
|
@@ -1996,4 +2205,70 @@ describe( 'BarChart', () => {
|
|
|
1996
2205
|
).toBeInTheDocument();
|
|
1997
2206
|
} );
|
|
1998
2207
|
} );
|
|
2208
|
+
|
|
2209
|
+
describe( 'defaultHiddenSeries', () => {
|
|
2210
|
+
it( 'renders a series hidden when named in defaultHiddenSeries', () => {
|
|
2211
|
+
render(
|
|
2212
|
+
<GlobalChartsProvider>
|
|
2213
|
+
<BarChartUnresponsive
|
|
2214
|
+
width={ 500 }
|
|
2215
|
+
height={ 300 }
|
|
2216
|
+
showLegend={ true }
|
|
2217
|
+
legend={ { interactive: true } }
|
|
2218
|
+
chartId="test-default-hidden-bar"
|
|
2219
|
+
defaultHiddenSeries={ [ 'Series B' ] }
|
|
2220
|
+
data={ [
|
|
2221
|
+
{
|
|
2222
|
+
label: 'Series A',
|
|
2223
|
+
data: [ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' } ],
|
|
2224
|
+
options: {},
|
|
2225
|
+
},
|
|
2226
|
+
{
|
|
2227
|
+
label: 'Series B',
|
|
2228
|
+
data: [ { date: new Date( '2024-01-01' ), value: 20, label: 'Jan 1' } ],
|
|
2229
|
+
options: {},
|
|
2230
|
+
},
|
|
2231
|
+
] }
|
|
2232
|
+
/>
|
|
2233
|
+
</GlobalChartsProvider>
|
|
2234
|
+
);
|
|
2235
|
+
|
|
2236
|
+
const items = screen.getAllByRole( 'button' );
|
|
2237
|
+
expect( items[ 0 ] ).toHaveAttribute( 'aria-pressed', 'true' );
|
|
2238
|
+
expect( items[ 1 ] ).toHaveAttribute( 'aria-pressed', 'false' );
|
|
2239
|
+
} );
|
|
2240
|
+
|
|
2241
|
+
it( 'lets the user reveal a series seeded hidden', async () => {
|
|
2242
|
+
const user = userEvent.setup();
|
|
2243
|
+
|
|
2244
|
+
render(
|
|
2245
|
+
<GlobalChartsProvider>
|
|
2246
|
+
<BarChartUnresponsive
|
|
2247
|
+
width={ 500 }
|
|
2248
|
+
height={ 300 }
|
|
2249
|
+
showLegend={ true }
|
|
2250
|
+
legend={ { interactive: true } }
|
|
2251
|
+
chartId="test-default-hidden-reveal-bar"
|
|
2252
|
+
defaultHiddenSeries={ [ 'Series B' ] }
|
|
2253
|
+
data={ [
|
|
2254
|
+
{
|
|
2255
|
+
label: 'Series A',
|
|
2256
|
+
data: [ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' } ],
|
|
2257
|
+
options: {},
|
|
2258
|
+
},
|
|
2259
|
+
{
|
|
2260
|
+
label: 'Series B',
|
|
2261
|
+
data: [ { date: new Date( '2024-01-01' ), value: 20, label: 'Jan 1' } ],
|
|
2262
|
+
options: {},
|
|
2263
|
+
},
|
|
2264
|
+
] }
|
|
2265
|
+
/>
|
|
2266
|
+
</GlobalChartsProvider>
|
|
2267
|
+
);
|
|
2268
|
+
|
|
2269
|
+
await user.click( screen.getAllByRole( 'button' )[ 1 ] );
|
|
2270
|
+
|
|
2271
|
+
expect( screen.getAllByRole( 'button' )[ 1 ] ).toHaveAttribute( 'aria-pressed', 'true' );
|
|
2272
|
+
} );
|
|
2273
|
+
} );
|
|
1999
2274
|
} );
|
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
overflow: hidden;
|
|
15
15
|
color: var(--a8c-charts-color-label);
|
|
16
16
|
text-overflow: ellipsis;
|
|
17
|
-
font-size: var(--wpds-typography-font-size-xl,
|
|
17
|
+
font-size: var(--wpds-typography-font-size-xl, 20px);
|
|
18
18
|
font-style: normal;
|
|
19
|
-
font-weight: var(--wpds-typography-font-weight-emphasis,
|
|
19
|
+
font-weight: var(--wpds-typography-font-weight-emphasis, 600);
|
|
20
20
|
line-height: var(--wpds-typography-line-height-sm, 20px);
|
|
21
21
|
margin: 0;
|
|
22
22
|
}
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
text-overflow: ellipsis;
|
|
27
27
|
font-size: var(--wpds-typography-font-size-md, 13px);
|
|
28
28
|
font-style: normal;
|
|
29
|
-
font-weight: var(--wpds-typography-font-weight-emphasis,
|
|
29
|
+
font-weight: var(--wpds-typography-font-weight-emphasis, 600);
|
|
30
30
|
line-height: var(--wpds-typography-line-height-sm, 20px);
|
|
31
31
|
margin: 0;
|
|
32
32
|
}
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
.step-rate {
|
|
67
67
|
color: var(--a8c-charts-color-label);
|
|
68
68
|
font-size: var(--wpds-typography-font-size-md, 13px);
|
|
69
|
-
font-weight: var(--wpds-typography-font-weight-emphasis,
|
|
69
|
+
font-weight: var(--wpds-typography-font-weight-emphasis, 600);
|
|
70
70
|
line-height: var(--wpds-typography-line-height-xs, 16px);
|
|
71
71
|
}
|
|
72
72
|
|
|
@@ -126,12 +126,12 @@
|
|
|
126
126
|
color: var(--a8c-charts-color-label);
|
|
127
127
|
font-size: var(--wpds-typography-font-size-md, 13px);
|
|
128
128
|
font-style: normal;
|
|
129
|
-
font-weight: var(--wpds-typography-font-weight-emphasis,
|
|
129
|
+
font-weight: var(--wpds-typography-font-weight-emphasis, 600);
|
|
130
130
|
line-height: var(--wpds-typography-line-height-sm, 20px);
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
.empty-state {
|
|
134
134
|
text-align: center;
|
|
135
135
|
color: var(--a8c-charts-color-label-secondary);
|
|
136
|
-
font-size: var(--wpds-typography-font-size-lg,
|
|
136
|
+
font-size: var(--wpds-typography-font-size-lg, 15px);
|
|
137
137
|
}
|
|
@@ -1,3 +1,11 @@
|
|
|
1
1
|
.container {
|
|
2
2
|
position: relative;
|
|
3
|
+
|
|
4
|
+
// Google gives its HTML tooltip no width, so the box shrink-to-fits against
|
|
5
|
+
// whatever room is left beside the hovered feature. Near a map edge that
|
|
6
|
+
// breaks even a short label across lines. Sizing to content keeps each line
|
|
7
|
+
// whole and grows the box instead.
|
|
8
|
+
:global(.google-visualization-tooltip) {
|
|
9
|
+
white-space: nowrap;
|
|
10
|
+
}
|
|
3
11
|
}
|
|
@@ -103,6 +103,15 @@ $focus-ring-width: var(--wpds-border-width-focus, var(--wp-admin-border-width-fo
|
|
|
103
103
|
pointer-events: none;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
// Grid filler: keeps its slot so the grid reads as continuous, but stands for a
|
|
107
|
+
// day nothing was measured for — so it captures no pointer, and its track is
|
|
108
|
+
// faded to keep it apart from a measured day that simply scored zero. Mixed
|
|
109
|
+
// towards `transparent` rather than a lighter grey so it holds in either theme.
|
|
110
|
+
.heatmap-chart__cell--placeholder {
|
|
111
|
+
pointer-events: none;
|
|
112
|
+
background: color-mix(in sRGB, var(--a8c-charts-color-track) 45%, transparent);
|
|
113
|
+
}
|
|
114
|
+
|
|
106
115
|
// Floor the mix at 15% so the lowest value stays visibly tinted, distinct from
|
|
107
116
|
// an empty cell.
|
|
108
117
|
.heatmap-chart__cell--filled {
|
|
@@ -21,8 +21,8 @@ import {
|
|
|
21
21
|
import { resolveCssVariable } from '../../utils/resolve-css-var';
|
|
22
22
|
import { Center } from '../private/center';
|
|
23
23
|
import { useChartChildren } from '../private/chart-composition';
|
|
24
|
+
import { ChartInstanceContext } from '../private/chart-instance-context';
|
|
24
25
|
import { ChartLayout } from '../private/chart-layout';
|
|
25
|
-
import { SingleChartContext } from '../private/single-chart-context';
|
|
26
26
|
import { withResponsive } from '../private/with-responsive';
|
|
27
27
|
import styles from './heatmap-chart.module.scss';
|
|
28
28
|
import {
|
|
@@ -136,8 +136,13 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
136
136
|
hideTooltip();
|
|
137
137
|
}, [ hideTooltip ] );
|
|
138
138
|
|
|
139
|
-
|
|
140
|
-
|
|
139
|
+
// Both empty-slot kinds are skipped by navigation: `hidden` paints nothing,
|
|
140
|
+
// `placeholder` paints an empty cell, and neither has a value to report.
|
|
141
|
+
const isCellInert = useCallback(
|
|
142
|
+
( col: number, row: number ) => {
|
|
143
|
+
const cell = data[ col ]?.data[ row ];
|
|
144
|
+
return cell?.hidden === true || cell?.placeholder === true;
|
|
145
|
+
},
|
|
141
146
|
[ data ]
|
|
142
147
|
);
|
|
143
148
|
|
|
@@ -163,7 +168,7 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
163
168
|
// Start at the first navigable cell (a calendar's leading edge
|
|
164
169
|
// slots may be hidden).
|
|
165
170
|
for ( let index = 0; index < columns * rows; index++ ) {
|
|
166
|
-
if ( !
|
|
171
|
+
if ( ! isCellInert( Math.floor( index / rows ), index % rows ) ) {
|
|
167
172
|
setSelectedIndex( index );
|
|
168
173
|
return;
|
|
169
174
|
}
|
|
@@ -191,7 +196,7 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
191
196
|
do {
|
|
192
197
|
col += stepCol;
|
|
193
198
|
row += stepRow;
|
|
194
|
-
} while ( col >= 0 && col < columns && row >= 0 && row < rows &&
|
|
199
|
+
} while ( col >= 0 && col < columns && row >= 0 && row < rows && isCellInert( col, row ) );
|
|
195
200
|
|
|
196
201
|
if ( col < 0 || col >= columns || row < 0 || row >= rows ) {
|
|
197
202
|
return;
|
|
@@ -199,7 +204,7 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
199
204
|
|
|
200
205
|
setSelectedIndex( col * rows + row );
|
|
201
206
|
},
|
|
202
|
-
[ rows, columns, selectedIndex, hideTooltip,
|
|
207
|
+
[ rows, columns, selectedIndex, hideTooltip, isCellInert ]
|
|
203
208
|
);
|
|
204
209
|
|
|
205
210
|
const handleCellMouseMove = useCallback(
|
|
@@ -313,7 +318,7 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
313
318
|
|
|
314
319
|
return (
|
|
315
320
|
<HeatmapContext.Provider value={ heatmapContext }>
|
|
316
|
-
<
|
|
321
|
+
<ChartInstanceContext.Provider value={ { chartId } }>
|
|
317
322
|
<ChartLayout
|
|
318
323
|
legendPosition="bottom"
|
|
319
324
|
// Legend renders via trailingContent, not the legend slot.
|
|
@@ -391,6 +396,24 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
391
396
|
);
|
|
392
397
|
}
|
|
393
398
|
|
|
399
|
+
// Filler: drawn like an empty cell so the grid fills its
|
|
400
|
+
// container, but it stands for a day nothing was measured
|
|
401
|
+
// for, so it reports nothing to a pointer or a screen
|
|
402
|
+
// reader either.
|
|
403
|
+
if ( cell?.placeholder ) {
|
|
404
|
+
return (
|
|
405
|
+
<div
|
|
406
|
+
key={ `cell-${ columnIndex }-${ rowIndex }` }
|
|
407
|
+
data-testid="heatmap-cell-placeholder"
|
|
408
|
+
aria-hidden="true"
|
|
409
|
+
className={ clsx(
|
|
410
|
+
styles[ 'heatmap-chart__cell' ],
|
|
411
|
+
styles[ 'heatmap-chart__cell--placeholder' ]
|
|
412
|
+
) }
|
|
413
|
+
/>
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
394
417
|
const value = cell?.value ?? null;
|
|
395
418
|
const present = isPresent( value );
|
|
396
419
|
const normalized = present ? getNormalizedValue( value, extent ) : 0;
|
|
@@ -456,7 +479,7 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
456
479
|
</TooltipInPortal>
|
|
457
480
|
) }
|
|
458
481
|
</ChartLayout>
|
|
459
|
-
</
|
|
482
|
+
</ChartInstanceContext.Provider>
|
|
460
483
|
</HeatmapContext.Provider>
|
|
461
484
|
);
|
|
462
485
|
};
|