@automattic/charts 1.9.0 → 1.10.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/dist/index.cjs +5661 -5466
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +40 -28
- package/dist/index.d.cts +31 -7
- package/dist/index.d.ts +32 -8
- package/dist/index.js +1239 -1046
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.module.scss +7 -7
- package/src/charts/geo-chart/geo-chart.tsx +148 -3
- package/src/charts/geo-chart/index.ts +1 -1
- package/src/charts/geo-chart/test/geo-chart.test.tsx +122 -2
- package/src/charts/geo-chart/types.ts +11 -0
- package/src/charts/heatmap-chart/heatmap-chart.module.scss +11 -11
- package/src/charts/heatmap-chart/heatmap-chart.tsx +37 -36
- package/src/charts/heatmap-chart/private/build-calendar-data.ts +17 -1
- package/src/charts/heatmap-chart/private/heatmap-context.ts +10 -0
- package/src/charts/heatmap-chart/private/heatmap-legend.tsx +3 -2
- package/src/charts/heatmap-chart/private/index.ts +2 -0
- package/src/charts/heatmap-chart/test/build-calendar-data.test.ts +23 -1
- package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +10 -3
- package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +15 -3
- package/src/charts/leaderboard-chart/leaderboard-chart.tsx +65 -37
- package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +46 -0
- package/src/charts/line-chart/line-chart.module.scss +2 -2
- package/src/charts/private/svg-empty-state/svg-empty-state.module.scss +1 -1
- package/src/charts/private/x-zoom.module.scss +2 -2
- package/src/components/trend-indicator/trend-indicator.module.scss +3 -3
- package/src/hooks/test/use-chart-margin.test.tsx +8 -5
- package/src/hooks/use-chart-margin.tsx +11 -1
- package/src/index.ts +1 -1
- package/src/providers/chart-context/global-charts-provider.tsx +7 -0
- package/src/providers/chart-context/test/chart-context.test.tsx +40 -0
- package/src/providers/chart-context/themes.ts +17 -14
- package/src/providers/chart-context/types.ts +3 -0
- package/src/types.ts +9 -6
- package/src/utils/color-utils.ts +28 -0
- package/src/utils/test/color-utils.test.ts +29 -0
|
@@ -2,15 +2,7 @@ import { formatNumber, formatNumberCompact } from '@automattic/number-formatters
|
|
|
2
2
|
import { useTooltip, useTooltipInPortal } from '@visx/tooltip';
|
|
3
3
|
import { __ } from '@wordpress/i18n';
|
|
4
4
|
import clsx from 'clsx';
|
|
5
|
-
import {
|
|
6
|
-
createContext,
|
|
7
|
-
useCallback,
|
|
8
|
-
useContext,
|
|
9
|
-
useEffect,
|
|
10
|
-
useMemo,
|
|
11
|
-
useRef,
|
|
12
|
-
useState,
|
|
13
|
-
} from 'react';
|
|
5
|
+
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
14
6
|
import {
|
|
15
7
|
GlobalChartsProvider,
|
|
16
8
|
useChartId,
|
|
@@ -20,7 +12,7 @@ import {
|
|
|
20
12
|
import { attachSubComponents } from '../../utils';
|
|
21
13
|
import {
|
|
22
14
|
isValidHexColor,
|
|
23
|
-
|
|
15
|
+
mixHexColors,
|
|
24
16
|
normalizeColorToHex,
|
|
25
17
|
prefersLightText,
|
|
26
18
|
} from '../../utils/color-utils';
|
|
@@ -30,19 +22,18 @@ import { ChartLayout } from '../private/chart-layout';
|
|
|
30
22
|
import { SingleChartContext } from '../private/single-chart-context';
|
|
31
23
|
import { withResponsive } from '../private/with-responsive';
|
|
32
24
|
import styles from './heatmap-chart.module.scss';
|
|
33
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
getValueExtent,
|
|
27
|
+
getNormalizedValue,
|
|
28
|
+
HeatmapContext,
|
|
29
|
+
HeatmapLegend,
|
|
30
|
+
isPresent,
|
|
31
|
+
} from './private';
|
|
32
|
+
import type { HeatmapContextValue } from './private';
|
|
34
33
|
import type { HeatmapChartProps, HeatmapTooltipData } from './types';
|
|
35
34
|
import type { ResponsiveConfig } from '../private/with-responsive';
|
|
36
35
|
import type { CSSProperties, FC } from 'react';
|
|
37
36
|
|
|
38
|
-
export type HeatmapContextValue = {
|
|
39
|
-
extent: [ number, number ];
|
|
40
|
-
/** The resolved primary color (full intensity); the legend mixes toward it in CSS. */
|
|
41
|
-
primaryColorHex: string;
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
export const HeatmapContext = createContext< HeatmapContextValue | null >( null );
|
|
45
|
-
|
|
46
37
|
// Mirrors the color-mix floor in heatmap-chart.module.scss (.heatmap-chart__cell--filled):
|
|
47
38
|
// the rendered fill is the primary mixed over the chart background at 0.15 + 0.85 * intensity.
|
|
48
39
|
const CELL_MIX_FLOOR = 0.15;
|
|
@@ -63,7 +54,7 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
63
54
|
children,
|
|
64
55
|
} ) => {
|
|
65
56
|
const chartId = useChartId( providedChartId );
|
|
66
|
-
const { getElementStyles, theme } = useGlobalChartsContext();
|
|
57
|
+
const { getElementStyles, resolveThemeColor, theme } = useGlobalChartsContext();
|
|
67
58
|
const { heatmapChart: heatmapChartSettings } = theme;
|
|
68
59
|
const { nonLegendChildren } = useChartChildren( children, 'HeatmapChart' );
|
|
69
60
|
|
|
@@ -84,14 +75,22 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
84
75
|
overrideColor: primaryColor || heatmapChartSettings.primaryColor,
|
|
85
76
|
} );
|
|
86
77
|
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
|
|
78
|
+
// Resolve the background in the provider's theme scope so the blended-fill text
|
|
79
|
+
// color tracks a themed (e.g. dark) background.
|
|
80
|
+
const chartBackgroundHex = resolveThemeColor( theme.backgroundColor );
|
|
81
|
+
|
|
82
|
+
// Choose text color from the blended fill, not the raw value.
|
|
83
|
+
// If either color cannot resolve to hex, keep dark text.
|
|
90
84
|
const primaryHex = normalizeColorToHex( primaryColorHex );
|
|
91
85
|
const cellHasLightText = ( intensity: number ): boolean =>
|
|
92
86
|
isValidHexColor( primaryHex ) &&
|
|
87
|
+
isValidHexColor( chartBackgroundHex ) &&
|
|
93
88
|
prefersLightText(
|
|
94
|
-
|
|
89
|
+
mixHexColors(
|
|
90
|
+
primaryHex,
|
|
91
|
+
chartBackgroundHex,
|
|
92
|
+
1 - ( CELL_MIX_FLOOR + ( 1 - CELL_MIX_FLOOR ) * intensity )
|
|
93
|
+
)
|
|
95
94
|
);
|
|
96
95
|
|
|
97
96
|
const extent = useMemo( () => getValueExtent( data ), [ data ] );
|
|
@@ -247,6 +246,7 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
247
246
|
const trackSize = compact ? 'var(--heatmap-cell-size)' : 'minmax(0, 1fr)';
|
|
248
247
|
const gridStyle: Record< string, string | number > = {
|
|
249
248
|
'--heatmap-primary': primaryColorHex,
|
|
249
|
+
'--heatmap-bg': theme.backgroundColor,
|
|
250
250
|
gridTemplateColumns: `auto repeat(${ columns }, ${ trackSize })`,
|
|
251
251
|
gridTemplateRows: `auto repeat(${ rows }, ${ trackSize })`,
|
|
252
252
|
};
|
|
@@ -292,17 +292,18 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
292
292
|
} ) }
|
|
293
293
|
style={ gridStyle as CSSProperties }
|
|
294
294
|
>
|
|
295
|
-
{ /*
|
|
296
|
-
<
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
295
|
+
{ /* Header row preserves the grid structure; cell aria-labels include this text. */ }
|
|
296
|
+
<div role="row" aria-hidden="true" className={ styles[ 'heatmap-chart__row' ] }>
|
|
297
|
+
<span />
|
|
298
|
+
{ data.map( ( column, columnIndex ) => (
|
|
299
|
+
<span
|
|
300
|
+
key={ `col-${ columnIndex }` }
|
|
301
|
+
className={ styles[ 'heatmap-chart__col-label' ] }
|
|
302
|
+
>
|
|
303
|
+
{ column.label }
|
|
304
|
+
</span>
|
|
305
|
+
) ) }
|
|
306
|
+
</div>
|
|
306
307
|
|
|
307
308
|
{ Array.from( { length: rows } ).map( ( _row, rowIndex ) => {
|
|
308
309
|
const labelVisible = ! compact || rowIndex % 2 === 0;
|
|
@@ -360,7 +361,7 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
360
361
|
>
|
|
361
362
|
{ drawValues && present && (
|
|
362
363
|
<span className={ styles[ 'heatmap-chart__cell-value' ] }>
|
|
363
|
-
{ /* Compact
|
|
364
|
+
{ /* Compact display; tooltip and aria-label keep full precision. */ }
|
|
364
365
|
{ formatNumberCompact( value ) }
|
|
365
366
|
</span>
|
|
366
367
|
) }
|
|
@@ -57,12 +57,28 @@ export const buildCalendarHeatmapData = (
|
|
|
57
57
|
LABELLED_ROWS.includes( row ) ? format( addDays( gridStart, row ), 'EEE' ) : ''
|
|
58
58
|
);
|
|
59
59
|
|
|
60
|
+
// Hide short partial first-month labels when a later month follows; compact
|
|
61
|
+
// cells make adjacent labels collide. Keep the label for single-month ranges.
|
|
62
|
+
const MIN_FIRST_MONTH_WEEKS = 2;
|
|
63
|
+
const firstMonth = gridStart.getMonth();
|
|
64
|
+
let firstMonthWeeks = 0;
|
|
65
|
+
while (
|
|
66
|
+
firstMonthWeeks < weekCount &&
|
|
67
|
+
addDays( gridStart, firstMonthWeeks * 7 ).getMonth() === firstMonth
|
|
68
|
+
) {
|
|
69
|
+
firstMonthWeeks++;
|
|
70
|
+
}
|
|
71
|
+
const spansLaterMonth = firstMonthWeeks < weekCount;
|
|
72
|
+
const showFirstMonthLabel = ! spansLaterMonth || firstMonthWeeks >= MIN_FIRST_MONTH_WEEKS;
|
|
73
|
+
|
|
60
74
|
const data: HeatmapColumn[] = [];
|
|
61
75
|
let previousMonth = -1;
|
|
62
76
|
for ( let week = 0; week < weekCount; week++ ) {
|
|
63
77
|
const columnStart = addDays( gridStart, week * 7 );
|
|
64
78
|
const month = columnStart.getMonth();
|
|
65
|
-
const
|
|
79
|
+
const isNewMonth = month !== previousMonth;
|
|
80
|
+
const label =
|
|
81
|
+
isNewMonth && ( week !== 0 || showFirstMonthLabel ) ? format( columnStart, 'MMM' ) : '';
|
|
66
82
|
previousMonth = month;
|
|
67
83
|
|
|
68
84
|
const cells: HeatmapCell[] = [];
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createContext } from 'react';
|
|
2
|
+
|
|
3
|
+
export type HeatmapContextValue = {
|
|
4
|
+
extent: [ number, number ];
|
|
5
|
+
/** The resolved primary color (full intensity); the legend mixes toward it in CSS. */
|
|
6
|
+
primaryColorHex: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/** Shared by the chart and legend without importing back from `heatmap-chart.tsx`. */
|
|
10
|
+
export const HeatmapContext = createContext< HeatmapContextValue | null >( null );
|
|
@@ -2,8 +2,8 @@ import { __ } from '@wordpress/i18n';
|
|
|
2
2
|
import { Stack, Text } from '@wordpress/ui';
|
|
3
3
|
import { useContext } from 'react';
|
|
4
4
|
import { useGlobalChartsTheme } from '../../../providers';
|
|
5
|
-
import { HeatmapContext } from '../heatmap-chart';
|
|
6
5
|
import styles from '../heatmap-chart.module.scss';
|
|
6
|
+
import { HeatmapContext } from './heatmap-context';
|
|
7
7
|
import type { CSSProperties, FC } from 'react';
|
|
8
8
|
|
|
9
9
|
export interface HeatmapLegendProps {
|
|
@@ -15,7 +15,7 @@ export interface HeatmapLegendProps {
|
|
|
15
15
|
|
|
16
16
|
export const HeatmapLegend: FC< HeatmapLegendProps > = ( { steps = 5, lessLabel, moreLabel } ) => {
|
|
17
17
|
const context = useContext( HeatmapContext );
|
|
18
|
-
const { legend } = useGlobalChartsTheme();
|
|
18
|
+
const { legend, backgroundColor } = useGlobalChartsTheme();
|
|
19
19
|
if ( ! context ) {
|
|
20
20
|
return null;
|
|
21
21
|
}
|
|
@@ -38,6 +38,7 @@ export const HeatmapLegend: FC< HeatmapLegendProps > = ( { steps = 5, lessLabel,
|
|
|
38
38
|
style={
|
|
39
39
|
{
|
|
40
40
|
'--heatmap-primary': primaryColorHex,
|
|
41
|
+
'--heatmap-bg': backgroundColor,
|
|
41
42
|
'--intensity': intensity,
|
|
42
43
|
} as CSSProperties
|
|
43
44
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { getValueExtent, getNormalizedValue, isPresent } from './use-heatmap-colors';
|
|
2
2
|
export { buildCalendarHeatmapData } from './build-calendar-data';
|
|
3
3
|
export { HeatmapLegend } from './heatmap-legend';
|
|
4
|
+
export { HeatmapContext } from './heatmap-context';
|
|
4
5
|
export type { CalendarHeatmapResult } from './build-calendar-data';
|
|
5
6
|
export type { HeatmapLegendProps } from './heatmap-legend';
|
|
7
|
+
export type { HeatmapContextValue } from './heatmap-context';
|
|
@@ -35,7 +35,7 @@ describe( 'buildCalendarHeatmapData', () => {
|
|
|
35
35
|
|
|
36
36
|
test( 'labels only the first column of each month', () => {
|
|
37
37
|
const multiMonth: DataPointDate[] = [
|
|
38
|
-
{ dateString: '2024-01-
|
|
38
|
+
{ dateString: '2024-01-01', value: 1 },
|
|
39
39
|
{ dateString: '2024-02-05', value: 1 },
|
|
40
40
|
];
|
|
41
41
|
const { data } = buildCalendarHeatmapData( multiMonth );
|
|
@@ -44,6 +44,28 @@ describe( 'buildCalendarHeatmapData', () => {
|
|
|
44
44
|
expect( labels ).toContain( 'Feb' );
|
|
45
45
|
} );
|
|
46
46
|
|
|
47
|
+
test( 'suppresses a partial first month label so it cannot collide with the next', () => {
|
|
48
|
+
// Jan 29 is the only January column; February should be the first visible label.
|
|
49
|
+
const partialFirstMonth: DataPointDate[] = [
|
|
50
|
+
{ dateString: '2024-01-29', value: 1 },
|
|
51
|
+
{ dateString: '2024-02-05', value: 1 },
|
|
52
|
+
];
|
|
53
|
+
const { data } = buildCalendarHeatmapData( partialFirstMonth, { weekStartsOn: 1 } );
|
|
54
|
+
expect( data[ 0 ].label ).toBe( '' );
|
|
55
|
+
expect( data.map( c => c.label ).filter( Boolean )[ 0 ] ).toBe( 'Feb' );
|
|
56
|
+
} );
|
|
57
|
+
|
|
58
|
+
test( 'keeps the first month label when the range never reaches a second month', () => {
|
|
59
|
+
// Single-month ranges keep their lone month label.
|
|
60
|
+
const singleMonth: DataPointDate[] = [
|
|
61
|
+
{ dateString: '2024-01-01', value: 1 },
|
|
62
|
+
{ dateString: '2024-01-03', value: 2 },
|
|
63
|
+
];
|
|
64
|
+
const { data } = buildCalendarHeatmapData( singleMonth, { weekStartsOn: 1 } );
|
|
65
|
+
expect( data ).toHaveLength( 1 );
|
|
66
|
+
expect( data[ 0 ].label ).toBe( 'Jan' );
|
|
67
|
+
} );
|
|
68
|
+
|
|
47
69
|
test( 'filters out entries with unparseable or missing dates', () => {
|
|
48
70
|
const mixed: DataPointDate[] = [
|
|
49
71
|
{ dateString: '2024-01-01', value: 3 },
|
|
@@ -222,12 +222,19 @@ describe( 'HeatmapChart', () => {
|
|
|
222
222
|
} );
|
|
223
223
|
} );
|
|
224
224
|
|
|
225
|
-
test( '
|
|
225
|
+
test( 'keeps a strict grid → row structure (column labels live in a row, not directly under the grid)', () => {
|
|
226
|
+
renderChart();
|
|
227
|
+
// The header row is aria-hidden, so include hidden elements when querying for it.
|
|
228
|
+
const rows = screen.getAllByRole( 'row', { hidden: true } );
|
|
229
|
+
const headerRow = rows.find( row => within( row ).queryByText( 'W1' ) );
|
|
230
|
+
expect( headerRow ).toBeDefined();
|
|
231
|
+
} );
|
|
232
|
+
|
|
233
|
+
test( 'leaves the cell gap to WPDS tokens in non-compact mode (no inline override)', () => {
|
|
226
234
|
renderChart();
|
|
227
235
|
const grid = screen.getByRole( 'grid', { name: /heatmap/i } );
|
|
228
|
-
// Non-compact sets no inline gap
|
|
236
|
+
// Non-compact sets no inline gap — the SCSS falls back to the WPDS token.
|
|
229
237
|
expect( grid.style.getPropertyValue( '--heatmap-cell-gap' ) ).toBe( '' );
|
|
230
|
-
expect( grid.style.getPropertyValue( '--heatmap-cell-radius' ) ).toBe( '' );
|
|
231
238
|
} );
|
|
232
239
|
|
|
233
240
|
test( 'applies the compact gap inline from the theme compactCellGap', () => {
|
|
@@ -73,9 +73,21 @@
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
.valueContainer {
|
|
76
|
+
align-items: center;
|
|
76
77
|
justify-content: flex-end;
|
|
77
78
|
}
|
|
78
79
|
|
|
80
|
+
.deltaValue {
|
|
81
|
+
display: inline-block;
|
|
82
|
+
font-variant-numeric: tabular-nums;
|
|
83
|
+
min-width: 3ch;
|
|
84
|
+
text-align: end;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.deltaPlaceholder {
|
|
88
|
+
text-align: center;
|
|
89
|
+
}
|
|
90
|
+
|
|
79
91
|
.overlayLabel {
|
|
80
92
|
align-items: center;
|
|
81
93
|
}
|
|
@@ -83,7 +95,7 @@
|
|
|
83
95
|
.emptyState {
|
|
84
96
|
padding: var(--wpds-dimension-padding-3xl, 32px) var(--wpds-dimension-padding-lg, 16px);
|
|
85
97
|
text-align: center;
|
|
86
|
-
color: var(--wpds-color-
|
|
98
|
+
color: var(--wpds-color-foreground-content-neutral-weak, #707070);
|
|
87
99
|
font-size: var(--wpds-typography-font-size-md, 13px);
|
|
88
100
|
font-style: italic;
|
|
89
101
|
}
|
|
@@ -165,7 +177,7 @@
|
|
|
165
177
|
border-radius: var(--wpds-border-radius-sm, 2px);
|
|
166
178
|
outline:
|
|
167
179
|
var(--focus-ring-width) solid
|
|
168
|
-
var(--wpds-color-stroke-focus
|
|
180
|
+
var(--wpds-color-stroke-focus, var(--wp-admin-theme-color, #3858e9));
|
|
169
181
|
// Inset by its own width so the outer edge hugs the row boundary: the
|
|
170
182
|
// outline then sits in the row's padding (also a ring width), clear of the
|
|
171
183
|
// bar, and stays inside the overflow:auto container that would clip an
|
|
@@ -181,7 +193,7 @@
|
|
|
181
193
|
bottom: 0;
|
|
182
194
|
margin-block: auto;
|
|
183
195
|
opacity: 0;
|
|
184
|
-
color: var(--wpds-color-
|
|
196
|
+
color: var(--wpds-color-foreground-content-neutral-weak, #707070);
|
|
185
197
|
transition:
|
|
186
198
|
opacity var(--wpds-motion-duration-sm, 100ms)
|
|
187
199
|
var(--wpds-motion-easing-subtle, cubic-bezier(0.15, 0, 0.15, 1));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* eslint-disable @wordpress/no-unsafe-wp-apis */
|
|
2
|
-
import { __experimentalGrid as Grid } from '@wordpress/components';
|
|
2
|
+
import { __experimentalGrid as Grid, VisuallyHidden } from '@wordpress/components';
|
|
3
3
|
import { Fragment } from '@wordpress/element';
|
|
4
4
|
import { __ } from '@wordpress/i18n';
|
|
5
5
|
import { Icon, chevronRight } from '@wordpress/icons';
|
|
@@ -65,6 +65,14 @@ const defaultDeltaFormatter = ( value: number ): string => {
|
|
|
65
65
|
const getBarWidth = ( share: number ): string =>
|
|
66
66
|
`calc(${ share }% - var(--a8c--charts--leaderboard--bar--hover-inset, 0px) * ${ share } / 100)`;
|
|
67
67
|
|
|
68
|
+
const hasComparisonValue = (
|
|
69
|
+
entry: LeaderboardEntry
|
|
70
|
+
): entry is LeaderboardEntry & {
|
|
71
|
+
previousValue: number;
|
|
72
|
+
previousShare: number;
|
|
73
|
+
delta: number;
|
|
74
|
+
} => entry.previousValue != null && entry.previousShare != null && entry.delta != null;
|
|
75
|
+
|
|
68
76
|
const BarLabel = ( { label }: { label: LeaderboardEntry[ 'label' ] } ) => (
|
|
69
77
|
<>{ typeof label === 'string' ? <Text className={ styles.label }>{ label }</Text> : label }</>
|
|
70
78
|
);
|
|
@@ -87,39 +95,43 @@ const BarWithLabel = ( {
|
|
|
87
95
|
isPrimaryVisible?: boolean;
|
|
88
96
|
isComparisonVisible?: boolean;
|
|
89
97
|
animation?: boolean;
|
|
90
|
-
} ) =>
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
<
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
98
|
+
} ) => {
|
|
99
|
+
const showComparisonBar = withComparison && ! withOverlayLabel && isComparisonVisible;
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div
|
|
103
|
+
className={ clsx( styles.barWithLabelContainer, {
|
|
104
|
+
[ styles[ 'is-overlay' ] ]: withOverlayLabel,
|
|
105
|
+
} ) }
|
|
106
|
+
>
|
|
107
|
+
<BarLabel label={ entry.label } />
|
|
108
|
+
|
|
109
|
+
{ isPrimaryVisible && (
|
|
110
|
+
<div
|
|
111
|
+
className={ clsx( styles.bar, {
|
|
112
|
+
[ styles[ 'bar--animated' ] ]: animation,
|
|
113
|
+
} ) }
|
|
114
|
+
style={ {
|
|
115
|
+
width: getBarWidth( entry.currentShare ),
|
|
116
|
+
backgroundColor: primaryColor,
|
|
117
|
+
} }
|
|
118
|
+
></div>
|
|
119
|
+
) }
|
|
120
|
+
|
|
121
|
+
{ showComparisonBar && hasComparisonValue( entry ) && (
|
|
122
|
+
<div
|
|
123
|
+
className={ clsx( styles.bar, {
|
|
124
|
+
[ styles[ 'bar--animated' ] ]: animation,
|
|
125
|
+
} ) }
|
|
126
|
+
style={ {
|
|
127
|
+
width: getBarWidth( entry.previousShare ),
|
|
128
|
+
backgroundColor: secondaryColor,
|
|
129
|
+
} }
|
|
130
|
+
></div>
|
|
131
|
+
) }
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
};
|
|
123
135
|
|
|
124
136
|
/**
|
|
125
137
|
* LeaderboardChart component displays a ranked list of data with progress bars
|
|
@@ -335,7 +347,11 @@ const LeaderboardChartInternal: FC< LeaderboardChartProps > = ( {
|
|
|
335
347
|
) : (
|
|
336
348
|
<Grid templateColumns="minmax(0, 1fr) auto" rowGap={ rowGap } columnGap={ columnGap }>
|
|
337
349
|
{ data.map( entry => {
|
|
338
|
-
const
|
|
350
|
+
const showComparisonColumn = withComparison && isComparisonVisible;
|
|
351
|
+
const hasDeltaValue = hasComparisonValue( entry );
|
|
352
|
+
const showComparisonValue = showComparisonColumn && hasDeltaValue;
|
|
353
|
+
const showComparisonPlaceholder = showComparisonColumn && ! hasDeltaValue;
|
|
354
|
+
const colorIndex = showComparisonValue ? Math.sign( entry.delta ) + 1 : 1;
|
|
339
355
|
const deltaColor = deltaColors[ colorIndex ];
|
|
340
356
|
|
|
341
357
|
const rowCells = (
|
|
@@ -362,11 +378,23 @@ const LeaderboardChartInternal: FC< LeaderboardChartProps > = ( {
|
|
|
362
378
|
>
|
|
363
379
|
{ isPrimaryVisible && <Text>{ valueFormatter( entry.currentValue ) }</Text> }
|
|
364
380
|
|
|
365
|
-
{
|
|
366
|
-
<Text style={ { color: deltaColor } }>
|
|
381
|
+
{ showComparisonValue && (
|
|
382
|
+
<Text className={ styles.deltaValue } style={ { color: deltaColor } }>
|
|
367
383
|
{ deltaFormatter( entry.delta ) }
|
|
368
384
|
</Text>
|
|
369
385
|
) }
|
|
386
|
+
|
|
387
|
+
{ showComparisonPlaceholder && (
|
|
388
|
+
<Text
|
|
389
|
+
className={ clsx( styles.deltaValue, styles.deltaPlaceholder ) }
|
|
390
|
+
style={ { color: deltaColor } }
|
|
391
|
+
>
|
|
392
|
+
<span aria-hidden="true">-</span>
|
|
393
|
+
<VisuallyHidden as="span">
|
|
394
|
+
{ __( 'No comparison data', 'jetpack-charts' ) }
|
|
395
|
+
</VisuallyHidden>
|
|
396
|
+
</Text>
|
|
397
|
+
) }
|
|
370
398
|
</Stack>
|
|
371
399
|
</>
|
|
372
400
|
);
|
|
@@ -78,6 +78,30 @@ describe( 'LeaderboardChart', () => {
|
|
|
78
78
|
expect( screen.getByText( '-8%' ) ).toBeInTheDocument();
|
|
79
79
|
} );
|
|
80
80
|
|
|
81
|
+
it( 'shows a placeholder when a row has no previous value', () => {
|
|
82
|
+
render(
|
|
83
|
+
<LeaderboardChart
|
|
84
|
+
data={ [
|
|
85
|
+
...mockData,
|
|
86
|
+
{
|
|
87
|
+
id: 'new',
|
|
88
|
+
label: 'New Source',
|
|
89
|
+
currentValue: 100,
|
|
90
|
+
currentShare: 1,
|
|
91
|
+
},
|
|
92
|
+
] }
|
|
93
|
+
withComparison={ true }
|
|
94
|
+
/>
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
expect( screen.getByText( '+25%' ) ).toBeInTheDocument();
|
|
98
|
+
expect( screen.getByText( '-8%' ) ).toBeInTheDocument();
|
|
99
|
+
expect( screen.getByText( 'New Source' ) ).toBeInTheDocument();
|
|
100
|
+
expect( screen.getByText( '-' ) ).toHaveAttribute( 'aria-hidden', 'true' );
|
|
101
|
+
expect( screen.getByText( 'No comparison data' ) ).toBeInTheDocument();
|
|
102
|
+
expect( screen.queryByText( '+100%' ) ).not.toBeInTheDocument();
|
|
103
|
+
} );
|
|
104
|
+
|
|
81
105
|
it( 'shows custom label when provided', () => {
|
|
82
106
|
render(
|
|
83
107
|
<LeaderboardChart
|
|
@@ -403,6 +427,28 @@ describe( 'LeaderboardChart', () => {
|
|
|
403
427
|
expect( screen.getByRole( 'button' ) ).toHaveAccessibleName( /Direct.*12\.5K/ );
|
|
404
428
|
} );
|
|
405
429
|
|
|
430
|
+
it( 'does not include the missing comparison dash in interactive row names', () => {
|
|
431
|
+
render(
|
|
432
|
+
<LeaderboardChart
|
|
433
|
+
data={ [
|
|
434
|
+
{
|
|
435
|
+
id: 'new',
|
|
436
|
+
label: 'New Source',
|
|
437
|
+
currentValue: 100,
|
|
438
|
+
currentShare: 1,
|
|
439
|
+
onClick: jest.fn(),
|
|
440
|
+
},
|
|
441
|
+
] }
|
|
442
|
+
withComparison={ true }
|
|
443
|
+
/>
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
expect( screen.getByRole( 'button' ) ).toHaveAccessibleName(
|
|
447
|
+
/New Source.*100.*No comparison data/
|
|
448
|
+
);
|
|
449
|
+
expect( screen.getByRole( 'button' ) ).not.toHaveAccessibleName( /-/ );
|
|
450
|
+
} );
|
|
451
|
+
|
|
406
452
|
it( 'derives the accessible name from an image label via its alt text', () => {
|
|
407
453
|
render(
|
|
408
454
|
<LeaderboardChart
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
|
|
19
19
|
&__tooltip,
|
|
20
20
|
&__annotation-label-popover {
|
|
21
|
-
background: var(--wpds-color-
|
|
22
|
-
color: var(--wpds-color-
|
|
21
|
+
background: var(--wpds-color-background-surface-neutral-strong, #fff);
|
|
22
|
+
color: var(--wpds-color-foreground-content-neutral, #1e1e1e);
|
|
23
23
|
padding: var(--wpds-dimension-padding-sm, 8px);
|
|
24
24
|
}
|
|
25
25
|
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
height: 28px;
|
|
23
23
|
padding: 0;
|
|
24
24
|
background: var(--charts-zoom-reset-bg, rgba(255, 255, 255, 0.92));
|
|
25
|
-
color: var(--charts-zoom-reset-fg, var(--wpds-color-
|
|
25
|
+
color: var(--charts-zoom-reset-fg, var(--wpds-color-foreground-content-neutral, #1e1e1e));
|
|
26
26
|
border:
|
|
27
27
|
var(--wpds-border-width-xs, 1px) solid
|
|
28
28
|
var(--charts-zoom-reset-border, rgba(0, 0, 0, 0.16));
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
&:focus-visible {
|
|
38
|
-
outline: 2px solid var(--charts-zoom-reset-focus, var(--wpds-color-stroke-focus
|
|
38
|
+
outline: 2px solid var(--charts-zoom-reset-focus, var(--wpds-color-stroke-focus, #3858e9));
|
|
39
39
|
outline-offset: 1px;
|
|
40
40
|
}
|
|
41
41
|
}
|
|
@@ -9,15 +9,15 @@
|
|
|
9
9
|
// Trend colors use the documented --charts-trend-* override API, falling
|
|
10
10
|
// back to the accessible WPDS semantic tokens.
|
|
11
11
|
&--up {
|
|
12
|
-
color: var(--charts-trend-up-color, var(--wpds-color-
|
|
12
|
+
color: var(--charts-trend-up-color, var(--wpds-color-foreground-content-success-weak, #008030));
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
&--down {
|
|
16
|
-
color: var(--charts-trend-down-color, var(--wpds-color-
|
|
16
|
+
color: var(--charts-trend-down-color, var(--wpds-color-foreground-content-error-weak, #cc1818));
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
&--neutral {
|
|
20
|
-
color: var(--charts-trend-neutral-color, var(--wpds-color-
|
|
20
|
+
color: var(--charts-trend-neutral-color, var(--wpds-color-foreground-content-neutral-weak, #707070));
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
&__icon {
|
|
@@ -13,8 +13,8 @@ describe( 'useChartMargin', () => {
|
|
|
13
13
|
const baseTheme = {
|
|
14
14
|
axisStyles: {
|
|
15
15
|
y: {
|
|
16
|
-
left: { axisLabel: { fontSize: 11 }, tickLength: 8 },
|
|
17
|
-
right: { axisLabel: { fontSize: 11 }, tickLength: 8 },
|
|
16
|
+
left: { axisLabel: { fontSize: 12 }, tickLabel: { fontSize: 11 }, tickLength: 8 },
|
|
17
|
+
right: { axisLabel: { fontSize: 12 }, tickLabel: { fontSize: 11 }, tickLength: 8 },
|
|
18
18
|
},
|
|
19
19
|
},
|
|
20
20
|
} as XYChartTheme;
|
|
@@ -64,7 +64,8 @@ describe( 'useChartMargin', () => {
|
|
|
64
64
|
options.axis.y.tickFormat,
|
|
65
65
|
theme.axisStyles.y.left.axisLabel
|
|
66
66
|
);
|
|
67
|
-
|
|
67
|
+
// 40 label width + 8 tick length + ceil(11 * 0.25) label dx offset
|
|
68
|
+
expect( result.current.left ).toBe( 51 );
|
|
68
69
|
} );
|
|
69
70
|
|
|
70
71
|
it( 'calculates right margin for right y axis', () => {
|
|
@@ -86,7 +87,8 @@ describe( 'useChartMargin', () => {
|
|
|
86
87
|
options.axis.y.tickFormat,
|
|
87
88
|
theme.axisStyles.y.right.axisLabel
|
|
88
89
|
);
|
|
89
|
-
|
|
90
|
+
// 40 label width + 8 tick length + ceil(11 * 0.25) label dx offset
|
|
91
|
+
expect( result.current.right ).toBe( 51 );
|
|
90
92
|
} );
|
|
91
93
|
|
|
92
94
|
it( 'uses explicit y tickValues when provided', () => {
|
|
@@ -133,7 +135,8 @@ describe( 'useChartMargin', () => {
|
|
|
133
135
|
const height = 300;
|
|
134
136
|
const theme = baseTheme;
|
|
135
137
|
const { result } = renderHook( () => useChartMargin( height, options, data, theme ) );
|
|
136
|
-
|
|
138
|
+
// 40 label width + 8 tick length + ceil(11 * 0.25) label dx offset
|
|
139
|
+
expect( result.current.left ).toBe( 51 );
|
|
137
140
|
expect( result.current.top ).toBe( 10 );
|
|
138
141
|
// 12px font + 8 tick length = 20
|
|
139
142
|
expect( result.current.bottom ).toBe( 20 );
|
|
@@ -114,7 +114,17 @@ export const useChartMargin = (
|
|
|
114
114
|
options.axis?.y?.tickFormat,
|
|
115
115
|
yAxisStyles.axisLabel
|
|
116
116
|
);
|
|
117
|
-
|
|
117
|
+
// visx's default axis theme pushes y-axis tick labels a further 0.25em
|
|
118
|
+
// away from the axis (dx of -0.25em on the left, 0.25em on the right), so
|
|
119
|
+
// reserve that on top of the measured label width — without it the widest
|
|
120
|
+
// label clips at the SVG edge. The em resolves against the tick label's own
|
|
121
|
+
// font size (theme tickLabel), not the axis-title font size (axisLabel).
|
|
122
|
+
const yTickLabelFontSize =
|
|
123
|
+
resolveFontSize( yAxisStyles?.tickLabel?.fontSize ) || DEFAULT_FONT_SIZE;
|
|
124
|
+
const yMarginValue =
|
|
125
|
+
( yTickWidth ?? DEFAULT_Y_TICK_WIDTH ) +
|
|
126
|
+
( yAxisStyles?.tickLength ?? 0 ) +
|
|
127
|
+
Math.ceil( yTickLabelFontSize * 0.25 );
|
|
118
128
|
|
|
119
129
|
if ( yAxisOrientation === 'right' ) {
|
|
120
130
|
defaultMargin.right = yMarginValue;
|
package/src/index.ts
CHANGED
|
@@ -72,7 +72,7 @@ export type {
|
|
|
72
72
|
PieSemiCircleChartProps,
|
|
73
73
|
PieSemiCircleChartRenderTooltipParams,
|
|
74
74
|
} from './charts/pie-semi-circle-chart';
|
|
75
|
-
export type { GeoChartProps, GeoRegion, GeoResolution } from './charts/geo-chart';
|
|
75
|
+
export type { GeoChartProps, GeoRegion, GeoResolution, GeoChartError } from './charts/geo-chart';
|
|
76
76
|
export type { LegendValueDisplay, BaseLegendItem } from './components/legend';
|
|
77
77
|
export type { TrendIndicatorProps, TrendDirection } from './components/trend-indicator';
|
|
78
78
|
export type { LineStyles, GridStyles, EventHandlerParams } from '@visx/xychart';
|