@automattic/charts 1.10.3 → 1.12.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 +19 -0
- package/TOKENS.md +83 -0
- package/dist/index.cjs +894 -139
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +58 -62
- package/dist/index.d.cts +75 -13
- package/dist/index.d.ts +75 -13
- package/dist/index.js +894 -139
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/charts/area-chart/area-chart.tsx +28 -8
- package/src/charts/area-chart/test/area-chart.test.tsx +102 -0
- package/src/charts/area-chart/types.ts +20 -6
- package/src/charts/bar-chart/bar-chart.tsx +33 -9
- package/src/charts/bar-chart/test/bar-chart.test.tsx +117 -0
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.module.scss +7 -7
- package/src/charts/heatmap-chart/heatmap-chart.module.scss +18 -13
- package/src/charts/heatmap-chart/heatmap-chart.tsx +11 -7
- package/src/charts/heatmap-chart/private/heatmap-legend.tsx +3 -3
- package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +13 -7
- package/src/charts/leaderboard-chart/hooks/index.ts +1 -0
- package/src/charts/leaderboard-chart/hooks/use-fitted-row-count.ts +115 -0
- package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +39 -12
- package/src/charts/leaderboard-chart/leaderboard-chart.tsx +67 -14
- package/src/charts/leaderboard-chart/test/leaderboard-chart-fit-rows.test.tsx +365 -0
- package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +73 -2
- package/src/charts/leaderboard-chart/types.ts +12 -0
- package/src/charts/line-chart/line-chart.module.scss +2 -2
- package/src/charts/line-chart/line-chart.tsx +40 -5
- package/src/charts/line-chart/test/line-chart.test.tsx +190 -1
- package/src/charts/line-chart/types.ts +14 -0
- package/src/charts/private/grid-control/grid-control.module.scss +1 -1
- package/src/charts/private/svg-empty-state/svg-empty-state.module.scss +1 -1
- package/src/charts/private/x-zoom/index.ts +1 -0
- package/src/charts/private/{test → x-zoom/test}/x-zoom.test.tsx +46 -3
- package/src/charts/private/x-zoom/x-zoom.module.scss +43 -0
- package/src/charts/private/{x-zoom.tsx → x-zoom/x-zoom.tsx} +52 -24
- package/src/components/legend/hooks/test/use-chart-legend-items.test.tsx +152 -0
- package/src/components/legend/hooks/use-chart-legend-items.ts +118 -16
- package/src/components/legend/private/base-legend.tsx +21 -9
- package/src/components/legend/test/legend-config-types.test.tsx +32 -0
- package/src/components/legend/test/legend.test.tsx +121 -1
- package/src/components/legend/types.ts +6 -0
- package/src/components/tooltip/base-tooltip.module.scss +3 -2
- package/src/components/trend-indicator/trend-indicator.module.scss +6 -5
- package/src/index.ts +1 -0
- package/src/providers/chart-context/themes.ts +33 -19
- package/src/types.ts +18 -2
- package/src/charts/private/x-zoom.module.scss +0 -48
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@automattic/charts",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.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": {
|
|
@@ -105,6 +105,7 @@
|
|
|
105
105
|
"@testing-library/user-event": "^14.4.3",
|
|
106
106
|
"@tsdown/css": "0.22.2",
|
|
107
107
|
"@types/jest": "^30.0.0",
|
|
108
|
+
"@types/node": "^24.12.0",
|
|
108
109
|
"@types/react": "18.3.28",
|
|
109
110
|
"@types/react-dom": "18.3.7",
|
|
110
111
|
"@typescript/native-preview": "7.0.0-dev.20260707.2",
|
|
@@ -70,7 +70,8 @@ const AreaChartInternal = forwardRef< SingleChartRef, AreaChartProps >(
|
|
|
70
70
|
onPointerMove,
|
|
71
71
|
onPointerOut,
|
|
72
72
|
zoomable = false,
|
|
73
|
-
|
|
73
|
+
rescaleYOnVisibilityChange,
|
|
74
|
+
rescaleYOnLegendToggle,
|
|
74
75
|
children,
|
|
75
76
|
gridVisibility,
|
|
76
77
|
gap = 'md',
|
|
@@ -81,6 +82,9 @@ const AreaChartInternal = forwardRef< SingleChartRef, AreaChartProps >(
|
|
|
81
82
|
const legendShape = legend.shape ?? 'rect';
|
|
82
83
|
const legendPosition = legend.position ?? 'bottom';
|
|
83
84
|
|
|
85
|
+
// New prop wins; fall back to the deprecated `rescaleYOnLegendToggle`; default to rescaling.
|
|
86
|
+
const rescaleYOnVisibility = rescaleYOnVisibilityChange ?? rescaleYOnLegendToggle ?? true;
|
|
87
|
+
|
|
84
88
|
const providerTheme = useGlobalChartsTheme();
|
|
85
89
|
const theme = useXYChartTheme( data );
|
|
86
90
|
const chartId = useChartId( providedChartId );
|
|
@@ -147,12 +151,12 @@ const AreaChartInternal = forwardRef< SingleChartRef, AreaChartProps >(
|
|
|
147
151
|
// Computed from the full data set (ignoring legend visibility) so the y-axis stays
|
|
148
152
|
// fixed when series are toggled off - otherwise visx auto-fits to the remaining
|
|
149
153
|
// data and the chart's baseline appears to move. Opt-in via
|
|
150
|
-
// `
|
|
154
|
+
// `rescaleYOnVisibilityChange={ false }`. Skipped for non-default stack offsets,
|
|
151
155
|
// which reshape the y-extent (`expand` -> [0,1], `wiggle`/`silhouette` -> centred
|
|
152
156
|
// around zero); letting visx derive the domain is correct there.
|
|
153
157
|
const fixedYDomain = useMemo< [ number, number ] | undefined >( () => {
|
|
154
158
|
if (
|
|
155
|
-
|
|
159
|
+
rescaleYOnVisibility ||
|
|
156
160
|
! legendInteractive ||
|
|
157
161
|
! dataSorted.length ||
|
|
158
162
|
! dataSorted[ 0 ].data.length ||
|
|
@@ -195,7 +199,7 @@ const AreaChartInternal = forwardRef< SingleChartRef, AreaChartProps >(
|
|
|
195
199
|
}
|
|
196
200
|
if ( max === -Infinity ) return undefined;
|
|
197
201
|
return [ Math.min( 0, min ), max ];
|
|
198
|
-
}, [ dataSorted, stacked, stackOffset, legendInteractive,
|
|
202
|
+
}, [ dataSorted, stacked, stackOffset, legendInteractive, rescaleYOnVisibility ] );
|
|
199
203
|
|
|
200
204
|
const chartOptions = useMemo( () => {
|
|
201
205
|
const formatter = options?.axis?.x?.tickFormat || getFormatter( dataSorted );
|
|
@@ -238,7 +242,14 @@ const AreaChartInternal = forwardRef< SingleChartRef, AreaChartProps >(
|
|
|
238
242
|
const error = validateData( dataSorted );
|
|
239
243
|
const isDataValid = ! error;
|
|
240
244
|
|
|
241
|
-
const legendOptions = useMemo(
|
|
245
|
+
const legendOptions = useMemo(
|
|
246
|
+
() => ( {
|
|
247
|
+
withGlyph: false,
|
|
248
|
+
glyphSize: 0,
|
|
249
|
+
collapseGroups: legend.collapseGroups ?? false,
|
|
250
|
+
} ),
|
|
251
|
+
[ legend.collapseGroups ]
|
|
252
|
+
);
|
|
242
253
|
const legendItems = useChartLegendItems( dataSorted, legendOptions, legendShape );
|
|
243
254
|
|
|
244
255
|
const chartMetadata = useMemo(
|
|
@@ -405,9 +416,18 @@ const AreaChartInternal = forwardRef< SingleChartRef, AreaChartProps >(
|
|
|
405
416
|
onPointerOut={ onPointerOut }
|
|
406
417
|
pointerEventsDataKey="nearest"
|
|
407
418
|
>
|
|
408
|
-
{
|
|
409
|
-
|
|
410
|
-
|
|
419
|
+
{ /* With every series hidden the value scale collapses, so the grid and axes
|
|
420
|
+
are dropped while the empty state stands in — otherwise they render
|
|
421
|
+
squished at the top. */ }
|
|
422
|
+
{ ! allSeriesHidden && gridVisibility !== 'none' && (
|
|
423
|
+
<Grid columns={ false } numTicks={ 4 } />
|
|
424
|
+
) }
|
|
425
|
+
{ ! allSeriesHidden && chartOptions.axis.x.display && (
|
|
426
|
+
<Axis { ...chartOptions.axis.x } />
|
|
427
|
+
) }
|
|
428
|
+
{ ! allSeriesHidden && chartOptions.axis.y.display && (
|
|
429
|
+
<Axis { ...chartOptions.axis.y } />
|
|
430
|
+
) }
|
|
411
431
|
|
|
412
432
|
{ allSeriesHidden ? (
|
|
413
433
|
<SvgEmptyState
|
|
@@ -333,6 +333,35 @@ describe( 'AreaChart', () => {
|
|
|
333
333
|
expect( afterToggleDomain ).toEqual( initialDomain );
|
|
334
334
|
} );
|
|
335
335
|
|
|
336
|
+
test( 'y-axis stays pinned when rescaleYOnVisibilityChange is false', async () => {
|
|
337
|
+
const user = userEvent.setup();
|
|
338
|
+
const ref = createRef< SingleChartRef >();
|
|
339
|
+
render(
|
|
340
|
+
<GlobalChartsProvider>
|
|
341
|
+
<AreaChartUnresponsive
|
|
342
|
+
{ ...defaultProps }
|
|
343
|
+
showLegend
|
|
344
|
+
chartId="test-interactive-domain-pin-new"
|
|
345
|
+
legend={ { interactive: true } }
|
|
346
|
+
rescaleYOnVisibilityChange={ false }
|
|
347
|
+
ref={ ref }
|
|
348
|
+
/>
|
|
349
|
+
</GlobalChartsProvider>
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
const initialDomain = (
|
|
353
|
+
ref.current?.getScales()?.yScale as { domain: () => number[] } | undefined
|
|
354
|
+
)?.domain();
|
|
355
|
+
expect( initialDomain ).toBeDefined();
|
|
356
|
+
|
|
357
|
+
await user.click( screen.getByText( 'Series A' ) );
|
|
358
|
+
|
|
359
|
+
const afterToggleDomain = (
|
|
360
|
+
ref.current?.getScales()?.yScale as { domain: () => number[] } | undefined
|
|
361
|
+
)?.domain();
|
|
362
|
+
expect( afterToggleDomain ).toEqual( initialDomain );
|
|
363
|
+
} );
|
|
364
|
+
|
|
336
365
|
test( 'supports negative stacked values without clipping (with pinned Y)', () => {
|
|
337
366
|
// The mixed-sign full-extent pin only kicks in when the consumer
|
|
338
367
|
// opts into pinned-Y behavior; visx's natural domain derivation for
|
|
@@ -544,4 +573,77 @@ describe( 'AreaChart', () => {
|
|
|
544
573
|
'url(#chart-zoom-clip-zoomtest)'
|
|
545
574
|
);
|
|
546
575
|
} );
|
|
576
|
+
|
|
577
|
+
describe( 'Legend group collapsing', () => {
|
|
578
|
+
const groupedData = [
|
|
579
|
+
{
|
|
580
|
+
label: 'Views',
|
|
581
|
+
group: 'views',
|
|
582
|
+
data: [ { date: new Date( '2024-01-01' ), value: 20, label: 'Jan 1' } ],
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
label: 'Views — previous',
|
|
586
|
+
group: 'views',
|
|
587
|
+
options: { type: 'comparison' as const },
|
|
588
|
+
data: [ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' } ],
|
|
589
|
+
},
|
|
590
|
+
];
|
|
591
|
+
|
|
592
|
+
test( 'renders one legend item per series by default', () => {
|
|
593
|
+
renderWithProvider( { showLegend: true, data: groupedData } );
|
|
594
|
+
|
|
595
|
+
expect( screen.getByText( 'Views' ) ).toBeInTheDocument();
|
|
596
|
+
expect( screen.getByText( 'Views — previous' ) ).toBeInTheDocument();
|
|
597
|
+
} );
|
|
598
|
+
|
|
599
|
+
test( 'collapses a group to one item when legend.collapseGroups is set', () => {
|
|
600
|
+
renderWithProvider( {
|
|
601
|
+
showLegend: true,
|
|
602
|
+
legend: { collapseGroups: true },
|
|
603
|
+
data: groupedData,
|
|
604
|
+
} );
|
|
605
|
+
|
|
606
|
+
expect( screen.getByText( 'Views' ) ).toBeInTheDocument();
|
|
607
|
+
expect( screen.queryByText( 'Views — previous' ) ).not.toBeInTheDocument();
|
|
608
|
+
} );
|
|
609
|
+
} );
|
|
610
|
+
|
|
611
|
+
describe( 'All series hidden', () => {
|
|
612
|
+
it( 'drops the grid so it does not collapse when every series is hidden', async () => {
|
|
613
|
+
const user = userEvent.setup();
|
|
614
|
+
|
|
615
|
+
renderWithProvider( {
|
|
616
|
+
showLegend: true,
|
|
617
|
+
legend: { interactive: true },
|
|
618
|
+
chartId: 'area-hidden-axes',
|
|
619
|
+
data: [
|
|
620
|
+
{
|
|
621
|
+
label: 'Series A',
|
|
622
|
+
data: [ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' } ],
|
|
623
|
+
},
|
|
624
|
+
{
|
|
625
|
+
label: 'Series B',
|
|
626
|
+
data: [ { date: new Date( '2024-01-01' ), value: 20, label: 'Jan 1' } ],
|
|
627
|
+
},
|
|
628
|
+
],
|
|
629
|
+
} );
|
|
630
|
+
|
|
631
|
+
// The visx grid has no role/testid, so query its internal class from within the plot.
|
|
632
|
+
/* eslint-disable testing-library/no-node-access */
|
|
633
|
+
const plot = screen.getByRole( 'grid' );
|
|
634
|
+
expect( plot.querySelector( '.visx-rows' ) ).toBeInTheDocument();
|
|
635
|
+
|
|
636
|
+
const buttons = screen.getAllByRole( 'button' );
|
|
637
|
+
await user.click( buttons[ 0 ] );
|
|
638
|
+
await user.click( buttons[ 1 ] );
|
|
639
|
+
|
|
640
|
+
// With no visible data the value scale collapses, so the grid is removed rather than
|
|
641
|
+
// rendered squished at the top behind the empty state.
|
|
642
|
+
expect( plot.querySelector( '.visx-rows' ) ).not.toBeInTheDocument();
|
|
643
|
+
/* eslint-enable testing-library/no-node-access */
|
|
644
|
+
expect(
|
|
645
|
+
screen.getByText( /all series are hidden.*click legend items to show data/i )
|
|
646
|
+
).toBeInTheDocument();
|
|
647
|
+
} );
|
|
648
|
+
} );
|
|
547
649
|
} );
|
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
BaseChartProps,
|
|
3
|
+
DataPointDate,
|
|
4
|
+
SeriesData,
|
|
5
|
+
SeriesChartLegendConfig,
|
|
6
|
+
} from '../../types';
|
|
2
7
|
import type { RenderTooltipParams } from '../../visx/types';
|
|
3
8
|
import type { CurveType } from '../line-chart/types';
|
|
4
9
|
import type { ReactNode } from 'react';
|
|
5
10
|
|
|
6
11
|
export interface AreaChartProps extends BaseChartProps< SeriesData[] > {
|
|
12
|
+
/**
|
|
13
|
+
* Legend configuration. Supports `collapseGroups` on top of the shared options.
|
|
14
|
+
*/
|
|
15
|
+
legend?: SeriesChartLegendConfig;
|
|
7
16
|
/**
|
|
8
17
|
* Whether series should be stacked on top of each other.
|
|
9
18
|
* When false, series are rendered as overlapping filled areas.
|
|
@@ -54,13 +63,18 @@ export interface AreaChartProps extends BaseChartProps< SeriesData[] > {
|
|
|
54
63
|
*/
|
|
55
64
|
zoomable?: boolean;
|
|
56
65
|
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* the
|
|
61
|
-
* not move the chart's baseline.
|
|
66
|
+
* Whether the Y axis rescales to fit only the visible series when series are
|
|
67
|
+
* hidden or shown through the interactive legend.
|
|
68
|
+
* Defaults to `true`, matching LineChart. Set to `false` to pin the Y axis to
|
|
69
|
+
* the full data extent so hiding series does not move the chart's baseline.
|
|
62
70
|
* @default true
|
|
63
71
|
*/
|
|
72
|
+
rescaleYOnVisibilityChange?: boolean;
|
|
73
|
+
/**
|
|
74
|
+
* @deprecated Use `rescaleYOnVisibilityChange`. The behaviour keys off series
|
|
75
|
+
* visibility changing, not specifically a legend toggle. Still honoured when
|
|
76
|
+
* `rescaleYOnVisibilityChange` is not set.
|
|
77
|
+
*/
|
|
64
78
|
rescaleYOnLegendToggle?: boolean;
|
|
65
79
|
children?: ReactNode;
|
|
66
80
|
}
|
|
@@ -37,12 +37,22 @@ import {
|
|
|
37
37
|
BASE_BAND_PADDING_INNER,
|
|
38
38
|
} from './private';
|
|
39
39
|
import type { ComparisonSeriesEntry } from './private';
|
|
40
|
-
import type {
|
|
40
|
+
import type {
|
|
41
|
+
BaseChartProps,
|
|
42
|
+
DataPointDate,
|
|
43
|
+
SeriesData,
|
|
44
|
+
SeriesChartLegendConfig,
|
|
45
|
+
Optional,
|
|
46
|
+
} from '../../types';
|
|
41
47
|
import type { RenderTooltipParams } from '../../visx/types';
|
|
42
48
|
import type { ResponsiveConfig } from '../private/with-responsive';
|
|
43
49
|
import type { FC, ReactNode, ComponentType } from 'react';
|
|
44
50
|
|
|
45
51
|
export interface BarChartProps extends BaseChartProps< SeriesData[] > {
|
|
52
|
+
/**
|
|
53
|
+
* Legend configuration. Supports `collapseGroups` on top of the shared options.
|
|
54
|
+
*/
|
|
55
|
+
legend?: SeriesChartLegendConfig;
|
|
46
56
|
renderTooltip?: ( params: RenderTooltipParams< DataPointDate > ) => ReactNode;
|
|
47
57
|
orientation?: 'horizontal' | 'vertical';
|
|
48
58
|
withPatterns?: boolean;
|
|
@@ -117,6 +127,7 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
117
127
|
gap = 'md',
|
|
118
128
|
} ) => {
|
|
119
129
|
const legendInteractive = legend.interactive ?? false;
|
|
130
|
+
const legendCollapseGroups = legend.collapseGroups ?? false;
|
|
120
131
|
const horizontal = orientation === 'horizontal';
|
|
121
132
|
const chartId = useChartId( providedChartId );
|
|
122
133
|
const theme = useXYChartTheme( data );
|
|
@@ -131,7 +142,11 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
131
142
|
} );
|
|
132
143
|
|
|
133
144
|
// Create legend items using the reusable hook
|
|
134
|
-
const
|
|
145
|
+
const legendOptions = useMemo(
|
|
146
|
+
() => ( { collapseGroups: legendCollapseGroups } ),
|
|
147
|
+
[ legendCollapseGroups ]
|
|
148
|
+
);
|
|
149
|
+
const legendItems = useChartLegendItems( dataSorted, legendOptions );
|
|
135
150
|
const chartOptions = useBarChartOptions( dataWithVisibleZeros, horizontal, options );
|
|
136
151
|
const defaultMargin = useChartMargin( height, chartOptions, dataSorted, theme, horizontal );
|
|
137
152
|
const chartRef = useRef< HTMLDivElement >( null );
|
|
@@ -544,11 +559,13 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
544
559
|
horizontal={ horizontal }
|
|
545
560
|
pointerEventsDataKey="nearest"
|
|
546
561
|
>
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
562
|
+
{ ! allSeriesHidden && (
|
|
563
|
+
<Grid
|
|
564
|
+
columns={ gridVisibility.includes( 'y' ) }
|
|
565
|
+
rows={ gridVisibility.includes( 'x' ) }
|
|
566
|
+
numTicks={ 4 }
|
|
567
|
+
/>
|
|
568
|
+
) }
|
|
552
569
|
|
|
553
570
|
{ withPatterns && (
|
|
554
571
|
<>
|
|
@@ -615,8 +632,15 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
615
632
|
) ) }
|
|
616
633
|
</BarGroup>
|
|
617
634
|
|
|
618
|
-
|
|
619
|
-
|
|
635
|
+
{ /* With every series hidden there is no data to build the value scale from, so
|
|
636
|
+
visx collapses the domain and the axes render squished at the top. Drop them
|
|
637
|
+
while the empty state stands in. */ }
|
|
638
|
+
{ ! allSeriesHidden && (
|
|
639
|
+
<>
|
|
640
|
+
<Axis { ...chartOptions.axis.x } />
|
|
641
|
+
<Axis { ...chartOptions.axis.y } />
|
|
642
|
+
</>
|
|
643
|
+
) }
|
|
620
644
|
|
|
621
645
|
{ withTooltips && (
|
|
622
646
|
<AccessibleTooltip
|
|
@@ -1194,5 +1194,122 @@ describe( 'BarChart', () => {
|
|
|
1194
1194
|
screen.getByText( /all series are hidden.*click legend items to show data/i )
|
|
1195
1195
|
).toBeInTheDocument();
|
|
1196
1196
|
} );
|
|
1197
|
+
|
|
1198
|
+
it( 'drops the value-axis ticks when all series are hidden so the axes do not collapse', async () => {
|
|
1199
|
+
const user = userEvent.setup();
|
|
1200
|
+
|
|
1201
|
+
renderWithTheme( {
|
|
1202
|
+
showLegend: true,
|
|
1203
|
+
gridVisibility: 'both',
|
|
1204
|
+
legend: { interactive: true },
|
|
1205
|
+
chartId: 'test-hidden-axes-bar-chart',
|
|
1206
|
+
data: [
|
|
1207
|
+
{
|
|
1208
|
+
label: 'Series A',
|
|
1209
|
+
data: [ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' } ],
|
|
1210
|
+
options: {},
|
|
1211
|
+
},
|
|
1212
|
+
{
|
|
1213
|
+
label: 'Series B',
|
|
1214
|
+
data: [ { date: new Date( '2024-01-01' ), value: 20, label: 'Jan 1' } ],
|
|
1215
|
+
options: {},
|
|
1216
|
+
},
|
|
1217
|
+
],
|
|
1218
|
+
} );
|
|
1219
|
+
|
|
1220
|
+
// The value axis renders numeric tick labels while there is data to scale against.
|
|
1221
|
+
// (Series labels carry no digits and the empty-state message is text-only, so a bare
|
|
1222
|
+
// number can only be an axis tick.)
|
|
1223
|
+
const numericTick = /^[\d,]+$/;
|
|
1224
|
+
expect( screen.getAllByText( numericTick ).length ).toBeGreaterThan( 0 );
|
|
1225
|
+
|
|
1226
|
+
const legendItems = screen.getAllByRole( 'button' );
|
|
1227
|
+
await user.click( legendItems[ 0 ] );
|
|
1228
|
+
await user.click( legendItems[ 1 ] );
|
|
1229
|
+
|
|
1230
|
+
// With no visible data the value scale would collapse, so the axes are removed rather
|
|
1231
|
+
// than rendered squished at the top — no tick labels remain.
|
|
1232
|
+
expect( screen.queryAllByText( numericTick ) ).toHaveLength( 0 );
|
|
1233
|
+
expect(
|
|
1234
|
+
screen.getByText( /all series are hidden.*click legend items to show data/i )
|
|
1235
|
+
).toBeInTheDocument();
|
|
1236
|
+
} );
|
|
1237
|
+
} );
|
|
1238
|
+
|
|
1239
|
+
describe( 'Legend group collapsing', () => {
|
|
1240
|
+
const comparisonPair = [
|
|
1241
|
+
{
|
|
1242
|
+
label: 'Views',
|
|
1243
|
+
group: 'views',
|
|
1244
|
+
data: [ { date: new Date( '2024-01-01' ), value: 20, label: 'Jan 1' } ],
|
|
1245
|
+
},
|
|
1246
|
+
{
|
|
1247
|
+
label: 'Views — previous',
|
|
1248
|
+
group: 'views',
|
|
1249
|
+
options: { type: 'comparison' as const },
|
|
1250
|
+
data: [ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' } ],
|
|
1251
|
+
},
|
|
1252
|
+
];
|
|
1253
|
+
|
|
1254
|
+
it( 'renders one legend item per series by default', () => {
|
|
1255
|
+
renderWithTheme( {
|
|
1256
|
+
showLegend: true,
|
|
1257
|
+
chartId: 'bar-legend-groups-default',
|
|
1258
|
+
data: comparisonPair,
|
|
1259
|
+
} );
|
|
1260
|
+
|
|
1261
|
+
expect( screen.getByText( 'Views' ) ).toBeInTheDocument();
|
|
1262
|
+
expect( screen.getByText( 'Views — previous' ) ).toBeInTheDocument();
|
|
1263
|
+
} );
|
|
1264
|
+
|
|
1265
|
+
it( 'collapses a group to one item when legend.collapseGroups is set', () => {
|
|
1266
|
+
renderWithTheme( {
|
|
1267
|
+
showLegend: true,
|
|
1268
|
+
legend: { collapseGroups: true },
|
|
1269
|
+
chartId: 'bar-legend-groups-collapsed',
|
|
1270
|
+
data: comparisonPair,
|
|
1271
|
+
} );
|
|
1272
|
+
|
|
1273
|
+
expect( screen.getByText( 'Views' ) ).toBeInTheDocument();
|
|
1274
|
+
expect( screen.queryByText( 'Views — previous' ) ).not.toBeInTheDocument();
|
|
1275
|
+
} );
|
|
1276
|
+
|
|
1277
|
+
it( 'toggles only the clicked series when interactive without collapseGroups', async () => {
|
|
1278
|
+
const user = userEvent.setup();
|
|
1279
|
+
|
|
1280
|
+
renderWithTheme( {
|
|
1281
|
+
showLegend: true,
|
|
1282
|
+
legend: { interactive: true },
|
|
1283
|
+
chartId: 'bar-legend-groups-interactive',
|
|
1284
|
+
data: comparisonPair,
|
|
1285
|
+
} );
|
|
1286
|
+
|
|
1287
|
+
const buttons = screen.getAllByRole( 'button' );
|
|
1288
|
+
await user.click( buttons[ 0 ] );
|
|
1289
|
+
|
|
1290
|
+
expect( buttons[ 0 ] ).toHaveAttribute( 'aria-pressed', 'false' );
|
|
1291
|
+
expect( buttons[ 1 ] ).toHaveAttribute( 'aria-pressed', 'true' );
|
|
1292
|
+
} );
|
|
1293
|
+
|
|
1294
|
+
it( 'toggles the whole group when interactive with collapseGroups', async () => {
|
|
1295
|
+
const user = userEvent.setup();
|
|
1296
|
+
|
|
1297
|
+
renderWithTheme( {
|
|
1298
|
+
showLegend: true,
|
|
1299
|
+
legend: { interactive: true, collapseGroups: true },
|
|
1300
|
+
chartId: 'bar-legend-groups-interactive-collapsed',
|
|
1301
|
+
data: comparisonPair,
|
|
1302
|
+
} );
|
|
1303
|
+
|
|
1304
|
+
const buttons = screen.getAllByRole( 'button' );
|
|
1305
|
+
expect( buttons ).toHaveLength( 1 );
|
|
1306
|
+
|
|
1307
|
+
await user.click( buttons[ 0 ] );
|
|
1308
|
+
|
|
1309
|
+
expect( buttons[ 0 ] ).toHaveAttribute( 'aria-pressed', 'false' );
|
|
1310
|
+
expect(
|
|
1311
|
+
screen.getByText( /all series are hidden.*click legend items to show data/i )
|
|
1312
|
+
).toBeInTheDocument();
|
|
1313
|
+
} );
|
|
1197
1314
|
} );
|
|
1198
1315
|
} );
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
.main-rate {
|
|
14
14
|
overflow: hidden;
|
|
15
|
-
color: var(--wpds-color-foreground-content-neutral, #1e1e1e);
|
|
15
|
+
color: var(--a8c-charts-color-label, var(--wpds-color-foreground-content-neutral, #1e1e1e));
|
|
16
16
|
text-overflow: ellipsis;
|
|
17
17
|
font-size: var(--wpds-typography-font-size-xl, 18px);
|
|
18
18
|
font-style: normal;
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
.step-label {
|
|
57
|
-
color: var(--wpds-color-foreground-content-neutral-weak, #707070);
|
|
57
|
+
color: var(--a8c-charts-color-label-secondary, var(--wpds-color-foreground-content-neutral-weak, #707070));
|
|
58
58
|
font-size: var(--wpds-typography-font-size-sm, 12px);
|
|
59
59
|
font-weight: var(--wpds-typography-font-weight-regular, 400);
|
|
60
60
|
line-height: var(--wpds-typography-line-height-xs, 16px);
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
.step-rate {
|
|
67
|
-
color: var(--wpds-color-foreground-content-neutral, #1e1e1e);
|
|
67
|
+
color: var(--a8c-charts-color-label, var(--wpds-color-foreground-content-neutral, #1e1e1e));
|
|
68
68
|
font-size: var(--wpds-typography-font-size-md, 13px);
|
|
69
69
|
font-weight: var(--wpds-typography-font-weight-medium, 499);
|
|
70
70
|
line-height: var(--wpds-typography-line-height-xs, 16px);
|
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
.tooltip-wrapper {
|
|
103
|
-
background: var(--wpds-color-background-surface-neutral-strong, #fff);
|
|
103
|
+
background: var(--a8c-charts-color-background, var(--wpds-color-background-surface-neutral-strong, #fff));
|
|
104
104
|
|
|
105
105
|
// Override .visx-tooltip inline styles.
|
|
106
106
|
border-radius: var(--wpds-border-radius-md, 4px) !important;
|
|
@@ -110,7 +110,7 @@
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
.tooltip-title {
|
|
113
|
-
color: var(--wpds-color-foreground-content-neutral, #1e1e1e);
|
|
113
|
+
color: var(--a8c-charts-color-label, var(--wpds-color-foreground-content-neutral, #1e1e1e));
|
|
114
114
|
font-size: var(--wpds-typography-font-size-sm, 12px);
|
|
115
115
|
font-style: normal;
|
|
116
116
|
font-weight: var(--wpds-typography-font-weight-regular, 400);
|
|
@@ -118,7 +118,7 @@
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
.tooltip-content {
|
|
121
|
-
color: var(--wpds-color-foreground-content-neutral, #1e1e1e);
|
|
121
|
+
color: var(--a8c-charts-color-label, var(--wpds-color-foreground-content-neutral, #1e1e1e));
|
|
122
122
|
font-size: var(--wpds-typography-font-size-md, 13px);
|
|
123
123
|
font-style: normal;
|
|
124
124
|
font-weight: var(--wpds-typography-font-weight-medium, 499);
|
|
@@ -127,6 +127,6 @@
|
|
|
127
127
|
|
|
128
128
|
.empty-state {
|
|
129
129
|
text-align: center;
|
|
130
|
-
color: var(--wpds-color-foreground-content-neutral-weak, #707070);
|
|
130
|
+
color: var(--a8c-charts-color-label-secondary, var(--wpds-color-foreground-content-neutral-weak, #707070));
|
|
131
131
|
font-size: var(--wpds-typography-font-size-lg, 16px);
|
|
132
132
|
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// Focus-ring width shared across the grid and cell-selected focus outlines. A
|
|
2
|
+
// Sass var, not a custom property: a compile-time constant scoped to this
|
|
3
|
+
// module, so it needs no runtime cascade and never lands on the DOM.
|
|
4
|
+
$focus-ring-width: var(--wpds-border-width-focus, var(--wp-admin-border-width-focus, 2px));
|
|
5
|
+
|
|
1
6
|
.heatmap-chart {
|
|
2
7
|
width: 100%;
|
|
3
8
|
height: 100%;
|
|
@@ -14,14 +19,14 @@
|
|
|
14
19
|
|
|
15
20
|
.heatmap-chart__empty {
|
|
16
21
|
padding: var(--wpds-dimension-padding-lg, 16px);
|
|
17
|
-
color: var(--wpds-color-foreground-content-neutral-weak, #707070);
|
|
22
|
+
color: var(--a8c-charts-color-label-secondary, var(--wpds-color-foreground-content-neutral-weak, #707070));
|
|
18
23
|
}
|
|
19
24
|
|
|
20
25
|
// Track template is set inline because CSS `repeat()` won't take a `var()`
|
|
21
26
|
// count. ARIA rows are `display: contents` so their cells join this grid.
|
|
22
27
|
.heatmap-chart__grid {
|
|
23
28
|
display: grid;
|
|
24
|
-
gap: var(--heatmap-cell-gap, var(--wpds-dimension-gap-xs, 4px));
|
|
29
|
+
gap: var(--a8c-charts-dimension-heatmap-cell-gap, var(--wpds-dimension-gap-xs, 4px));
|
|
25
30
|
flex: 1 1 0;
|
|
26
31
|
width: 100%;
|
|
27
32
|
// Override flex's auto minimum so short widgets do not scroll.
|
|
@@ -29,8 +34,8 @@
|
|
|
29
34
|
|
|
30
35
|
&:focus-visible {
|
|
31
36
|
outline:
|
|
32
|
-
|
|
33
|
-
var(--wpds-color-stroke-focus, var(--wp-admin-theme-color, #3858e9));
|
|
37
|
+
$focus-ring-width solid
|
|
38
|
+
var(--a8c-charts-color-focus, var(--wpds-color-stroke-focus, var(--wp-admin-theme-color, #3858e9)));
|
|
34
39
|
outline-offset: 2px;
|
|
35
40
|
border-radius: var(--wpds-border-radius-sm, 2px);
|
|
36
41
|
}
|
|
@@ -58,7 +63,7 @@
|
|
|
58
63
|
|
|
59
64
|
.heatmap-chart__col-label,
|
|
60
65
|
.heatmap-chart__row-label {
|
|
61
|
-
color: var(--wpds-color-foreground-content-neutral-weak, #707070);
|
|
66
|
+
color: var(--a8c-charts-color-label-secondary, var(--wpds-color-foreground-content-neutral-weak, #707070));
|
|
62
67
|
font-size: var(--wpds-typography-font-size-xs, 11px);
|
|
63
68
|
white-space: nowrap;
|
|
64
69
|
overflow: visible;
|
|
@@ -84,7 +89,7 @@
|
|
|
84
89
|
min-height: 0;
|
|
85
90
|
border-radius: var(--wpds-border-radius-sm, 2px);
|
|
86
91
|
// Empty (no-data) default; cells with a value override it below.
|
|
87
|
-
background: var(--wpds-color-background-track-neutral-weak, #f0f0f0);
|
|
92
|
+
background: var(--a8c-charts-color-track, var(--wpds-color-background-track-neutral-weak, #f0f0f0));
|
|
88
93
|
}
|
|
89
94
|
|
|
90
95
|
// An empty grid slot (calendar ragged edges): keeps its track but paints and
|
|
@@ -97,30 +102,30 @@
|
|
|
97
102
|
// Floor the mix at 15% so the lowest value stays visibly tinted, distinct from
|
|
98
103
|
// an empty cell.
|
|
99
104
|
.heatmap-chart__cell--filled {
|
|
100
|
-
background: color-mix(in sRGB, var(--heatmap-primary) calc((0.15 + 0.85 * var(--intensity)) * 100%), var(--heatmap-
|
|
105
|
+
background: color-mix(in sRGB, var(--a8c-charts-color-heatmap-primary) calc((0.15 + 0.85 * var(--a8c-charts-heatmap-cell-intensity)) * 100%), var(--a8c-charts-color-heatmap-background, #fff));
|
|
101
106
|
}
|
|
102
107
|
|
|
103
108
|
.heatmap-chart__cell--selected {
|
|
104
109
|
outline:
|
|
105
|
-
|
|
106
|
-
var(--wpds-color-stroke-focus, var(--wp-admin-theme-color, #3858e9));
|
|
107
|
-
outline-offset: calc(-1 *
|
|
110
|
+
$focus-ring-width solid
|
|
111
|
+
var(--a8c-charts-color-focus, var(--wpds-color-stroke-focus, var(--wp-admin-theme-color, #3858e9)));
|
|
112
|
+
outline-offset: calc(-1 * #{$focus-ring-width});
|
|
108
113
|
}
|
|
109
114
|
|
|
110
115
|
.heatmap-chart__cell-value {
|
|
111
|
-
color: var(--wpds-color-foreground-content-neutral, #1e1e1e);
|
|
116
|
+
color: var(--a8c-charts-color-label, var(--wpds-color-foreground-content-neutral, #1e1e1e));
|
|
112
117
|
font-size: var(--wpds-typography-font-size-md, 13px);
|
|
113
118
|
}
|
|
114
119
|
|
|
115
120
|
// Light text on fills dark enough to out-contrast dark text (decided in JS from
|
|
116
121
|
// the blended fill luminance).
|
|
117
122
|
.heatmap-chart__cell--strong .heatmap-chart__cell-value {
|
|
118
|
-
color: var(--wpds-color-foreground-interactive-neutral-strong, #f0f0f0);
|
|
123
|
+
color: var(--a8c-charts-color-label-inverse, var(--wpds-color-foreground-interactive-neutral-strong, #f0f0f0));
|
|
119
124
|
}
|
|
120
125
|
|
|
121
126
|
.heatmap-chart__legend-swatch {
|
|
122
127
|
width: var(--wpds-dimension-size-3xs, 12px);
|
|
123
128
|
height: var(--wpds-dimension-size-3xs, 12px);
|
|
124
129
|
border-radius: var(--wpds-border-radius-sm, 2px);
|
|
125
|
-
background: color-mix(in sRGB, var(--heatmap-primary) calc((0.15 + 0.85 * var(--intensity)) * 100%), var(--heatmap-
|
|
130
|
+
background: color-mix(in sRGB, var(--a8c-charts-color-heatmap-primary) calc((0.15 + 0.85 * var(--a8c-charts-heatmap-cell-intensity)) * 100%), var(--a8c-charts-color-heatmap-background, #fff));
|
|
126
131
|
}
|
|
@@ -277,20 +277,20 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
277
277
|
// and a min floor makes the grid overflow (for a scrollable wrapper)
|
|
278
278
|
// rather than crushing cells on long ranges.
|
|
279
279
|
const columnTrack = compact
|
|
280
|
-
? 'var(--heatmap-cell-size)'
|
|
280
|
+
? 'var(--a8c-charts-dimension-heatmap-cell-size)'
|
|
281
281
|
: `minmax(${ minCellWidth ?? 0 }px, ${ maxCellWidth ? `${ maxCellWidth }px` : '1fr' })`;
|
|
282
282
|
const rowTrack = compact
|
|
283
|
-
? 'var(--heatmap-cell-size)'
|
|
283
|
+
? 'var(--a8c-charts-dimension-heatmap-cell-size)'
|
|
284
284
|
: `minmax(${ minCellHeight ?? 0 }px, ${ maxCellHeight ? `${ maxCellHeight }px` : '1fr' })`;
|
|
285
285
|
const gridStyle: Record< string, string | number > = {
|
|
286
|
-
'--heatmap-primary': primaryColorHex,
|
|
287
|
-
'--heatmap-
|
|
286
|
+
'--a8c-charts-color-heatmap-primary': primaryColorHex,
|
|
287
|
+
'--a8c-charts-color-heatmap-background': theme.backgroundColor,
|
|
288
288
|
gridTemplateColumns: `auto repeat(${ columns }, ${ columnTrack })`,
|
|
289
289
|
gridTemplateRows: `auto repeat(${ rows }, ${ rowTrack })`,
|
|
290
290
|
};
|
|
291
291
|
if ( compact ) {
|
|
292
|
-
gridStyle[ '--heatmap-cell-gap' ] = `${ compactCellGap }px`;
|
|
293
|
-
gridStyle[ '--heatmap-cell-size' ] = `${ compactCellSize }px`;
|
|
292
|
+
gridStyle[ '--a8c-charts-dimension-heatmap-cell-gap' ] = `${ compactCellGap }px`;
|
|
293
|
+
gridStyle[ '--a8c-charts-dimension-heatmap-cell-size' ] = `${ compactCellSize }px`;
|
|
294
294
|
}
|
|
295
295
|
|
|
296
296
|
const activeDescendant =
|
|
@@ -419,7 +419,11 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
419
419
|
selectedIndex === flatIndex,
|
|
420
420
|
} ) }
|
|
421
421
|
style={
|
|
422
|
-
present
|
|
422
|
+
present
|
|
423
|
+
? ( {
|
|
424
|
+
'--a8c-charts-heatmap-cell-intensity': normalized,
|
|
425
|
+
} as CSSProperties )
|
|
426
|
+
: undefined
|
|
423
427
|
}
|
|
424
428
|
onMouseMove={ handleCellMouseMove }
|
|
425
429
|
onMouseLeave={ handleCellMouseLeave }
|
|
@@ -37,9 +37,9 @@ export const HeatmapLegend: FC< HeatmapLegendProps > = ( { steps = 5, lessLabel,
|
|
|
37
37
|
className={ styles[ 'heatmap-chart__legend-swatch' ] }
|
|
38
38
|
style={
|
|
39
39
|
{
|
|
40
|
-
'--heatmap-primary': primaryColorHex,
|
|
41
|
-
'--heatmap-
|
|
42
|
-
'--intensity': intensity,
|
|
40
|
+
'--a8c-charts-color-heatmap-primary': primaryColorHex,
|
|
41
|
+
'--a8c-charts-color-heatmap-background': backgroundColor,
|
|
42
|
+
'--a8c-charts-heatmap-cell-intensity': intensity,
|
|
43
43
|
} as CSSProperties
|
|
44
44
|
}
|
|
45
45
|
/>
|