@automattic/charts 3.2.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -0
- package/SECURITY.md +1 -0
- package/TOKENS.md +53 -58
- package/dist/index.cjs +1483 -664
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +93 -29
- package/dist/index.d.cts +233 -247
- package/dist/index.d.ts +233 -247
- package/dist/index.js +1484 -667
- package/dist/index.js.map +1 -1
- package/package.json +7 -6
- package/src/charts/area-chart/area-chart.tsx +39 -25
- package/src/charts/area-chart/test/area-chart.test.tsx +4 -33
- package/src/charts/area-chart/test/tick-placement.test.tsx +55 -0
- package/src/charts/area-chart/test/tooltip-resolution-branches.test.tsx +86 -0
- package/src/charts/area-chart/test/tooltip-resolution.test.tsx +69 -0
- package/src/charts/area-chart/types.ts +4 -7
- package/src/charts/bar-chart/bar-chart.tsx +2 -1
- package/src/charts/bar-chart/private/test/use-bar-chart-options-formatting.test.tsx +69 -0
- package/src/charts/bar-chart/private/use-bar-chart-options.ts +27 -15
- package/src/charts/bar-list-chart/bar-list-chart.tsx +5 -0
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.module.scss +33 -23
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx +138 -182
- package/src/charts/conversion-funnel-chart/test/conversion-funnel-chart.test.tsx +46 -0
- package/src/charts/geo-chart/geo-chart.module.scss +1 -0
- package/src/charts/geo-chart/geo-chart.tsx +5 -10
- package/src/charts/geo-chart/test/geo-chart.test.tsx +2 -2
- package/src/charts/heatmap-chart/heatmap-chart.module.scss +52 -16
- package/src/charts/heatmap-chart/heatmap-chart.tsx +89 -37
- package/src/charts/heatmap-chart/index.ts +2 -1
- package/src/charts/heatmap-chart/private/build-calendar-data.ts +126 -89
- package/src/charts/heatmap-chart/private/civil-day.ts +174 -0
- package/src/charts/heatmap-chart/private/heatmap-legend.tsx +11 -4
- package/src/charts/heatmap-chart/private/index.ts +1 -1
- package/src/charts/heatmap-chart/private/use-heatmap-colors.ts +5 -1
- package/src/charts/heatmap-chart/test/build-calendar-data.test.ts +3 -0
- package/src/charts/heatmap-chart/test/civil-day.test.ts +70 -0
- package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +142 -12
- package/src/charts/heatmap-chart/test/use-calendar-heatmap-data.test.tsx +118 -0
- package/src/charts/heatmap-chart/test/use-heatmap-colors.test.ts +8 -0
- package/src/charts/heatmap-chart/test/zoned-calendar-data.test.ts +248 -0
- package/src/charts/heatmap-chart/types.ts +8 -2
- package/src/charts/heatmap-chart/use-calendar-heatmap-data.ts +42 -0
- package/src/charts/leaderboard-chart/hooks/use-fitted-row-count.ts +2 -2
- package/src/charts/leaderboard-chart/hooks/use-leaderboard-legend-items.ts +3 -5
- package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +31 -19
- package/src/charts/leaderboard-chart/leaderboard-chart.tsx +27 -17
- package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +14 -0
- package/src/charts/leaderboard-chart/test/use-leaderboard-legend-items.test.tsx +2 -9
- package/src/charts/leaderboard-chart/types.ts +2 -2
- package/src/charts/line-chart/line-chart.module.scss +10 -18
- package/src/charts/line-chart/line-chart.tsx +128 -26
- package/src/charts/line-chart/private/line-chart-annotation.tsx +18 -19
- package/src/charts/line-chart/test/crosshair-style-types.test.ts +31 -0
- package/src/charts/line-chart/test/default-tooltip-formatting.test.tsx +74 -0
- package/src/charts/line-chart/test/line-chart.test.tsx +174 -41
- package/src/charts/line-chart/test/tick-placement.test.tsx +84 -0
- package/src/charts/line-chart/test/tooltip-resolution.test.tsx +140 -0
- package/src/charts/line-chart/test/zoned-date-string.test.tsx +37 -0
- package/src/charts/line-chart/types.ts +17 -3
- package/src/charts/pie-chart/pie-chart.module.scss +13 -1
- package/src/charts/pie-chart/pie-chart.tsx +30 -40
- package/src/charts/pie-chart/test/pie-chart.test.tsx +59 -12
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.module.scss +8 -3
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +23 -37
- package/src/charts/pie-semi-circle-chart/test/pie-semi-circle-chart.test.tsx +50 -0
- package/src/charts/private/chart-layout/chart-layout.module.scss +3 -2
- package/src/charts/private/svg-empty-state/svg-empty-state.module.scss +1 -1
- package/src/charts/private/test/axis-formatting.test.tsx +85 -0
- package/src/charts/private/test/time-axis-formatting.test.ts +109 -0
- package/src/charts/private/test/time-axis-options.test.ts +267 -0
- package/src/charts/private/test/time-axis-tick-invariants.test.ts +153 -0
- package/src/charts/private/test/time-axis-tick-values.test.ts +280 -0
- package/src/charts/private/test/time-axis.test.ts +9 -69
- package/src/charts/private/time-axis-options.ts +106 -0
- package/src/charts/private/time-axis.ts +318 -136
- package/src/charts/private/x-zoom/x-zoom.module.scss +3 -3
- package/src/charts/private/xy-plot/xy-plot.module.scss +8 -0
- package/src/charts/sparkline/test/sparkline.test.tsx +29 -56
- package/src/charts/sparkline/types.ts +1 -1
- package/src/components/legend/private/base-legend.module.scss +9 -5
- package/src/components/tooltip/accessible-tooltip.tsx +21 -17
- package/src/components/tooltip/base-tooltip.module.scss +3 -3
- package/src/components/tooltip/private/bounded-tooltip.tsx +227 -0
- package/src/components/tooltip/test/accessible-tooltip.test.tsx +30 -4
- package/src/components/tooltip/test/bounded-tooltip.test.tsx +225 -0
- package/src/components/tooltip/test/xy-chart-tooltip.test.tsx +379 -0
- package/src/components/tooltip/xy-chart-tooltip.tsx +326 -0
- package/src/components/trend-indicator/trend-indicator.module.scss +2 -2
- package/src/hooks/test/use-chart-data-transform-zone.test.tsx +36 -0
- package/src/hooks/test/use-chart-margin.test.tsx +173 -3
- package/src/hooks/test/use-xychart-theme.test.tsx +16 -3
- package/src/hooks/use-chart-data-transform.ts +10 -3
- package/src/hooks/use-chart-margin.tsx +54 -9
- package/src/hooks/use-xychart-theme.ts +25 -9
- package/src/index.ts +8 -0
- package/src/providers/chart-context/global-charts-provider.tsx +37 -38
- package/src/providers/chart-context/hooks/use-chart-formatting.ts +22 -0
- package/src/providers/chart-context/index.ts +1 -0
- package/src/providers/chart-context/private/catalog-pointers.ts +42 -0
- package/src/providers/chart-context/private/series-palette.ts +3 -13
- package/src/providers/chart-context/test/{sparse-theme.test.ts → catalog-pointers.test.ts} +22 -10
- package/src/providers/chart-context/test/chart-context.test.tsx +177 -792
- package/src/providers/chart-context/test/use-global-charts-theme.test.tsx +7 -25
- package/src/providers/chart-context/themes.ts +7 -52
- package/src/providers/chart-context/types.ts +3 -0
- package/src/styles/chart-scope.scss +39 -62
- package/src/styles/test/chart-scope.test.ts +19 -47
- package/src/test-utils/series-fixtures.ts +31 -0
- package/src/test-utils/zoned-parsing-suite.ts +90 -0
- package/src/types.ts +79 -166
- package/src/utils/bucket-info.ts +87 -0
- package/src/utils/color-utils.ts +0 -27
- package/src/utils/date-formatting.ts +157 -0
- package/src/utils/date-parsing.ts +103 -52
- package/src/utils/get-edge-tick-widths.ts +33 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/test/bucket-info.test.ts +185 -0
- package/src/utils/test/color-utils.test.ts +1 -48
- package/src/utils/test/date-formatting.test.ts +89 -0
- package/src/utils/test/date-parsing.auckland.test.ts +6 -0
- package/src/utils/test/date-parsing.santiago.test.ts +6 -0
- package/src/utils/test/date-parsing.test.ts +16 -1
- package/src/utils/test/get-edge-tick-widths.test.ts +55 -0
- package/src/utils/test/get-styles.test.ts +10 -7
- package/src/utils/test/merge-themes.test.ts +18 -46
- package/src/utils/warn-once.ts +23 -0
- package/src/visx/types.ts +56 -5
- package/src/hooks/test/use-xychart-theme-provider-override.test.tsx +0 -28
- package/src/providers/chart-context/private/theme-override-vars.ts +0 -131
- package/src/providers/chart-context/private/with-catalog-pointers.ts +0 -93
- package/src/providers/chart-context/test/theme-override-vars-guard.test.tsx +0 -29
- package/src/providers/chart-context/test/theme-override-vars.test.ts +0 -183
- package/src/providers/chart-context/test/with-catalog-pointers.test.ts +0 -104
- package/src/styles/test/wpds-fallbacks.test.ts +0 -131
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
.leaderboardChart {
|
|
2
2
|
transition:
|
|
3
|
-
opacity var(--wpds-motion-duration-lg
|
|
4
|
-
var(--wpds-motion-easing-subtle
|
|
3
|
+
opacity var(--wpds-motion-duration-lg)
|
|
4
|
+
var(--wpds-motion-easing-subtle);
|
|
5
5
|
|
|
6
6
|
&--responsive {
|
|
7
7
|
height: 100%;
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
.label {
|
|
52
|
-
padding-left: var(--wpds-dimension-padding-sm
|
|
52
|
+
padding-left: var(--wpds-dimension-padding-sm);
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
min-height: 6px;
|
|
59
59
|
// Keeps a sliver of bar visible for tiny shares, and stops the interactive
|
|
60
60
|
// row's fixed-pixel hover inset from collapsing a narrow bar to nothing.
|
|
61
|
-
min-width: var(--wpds-dimension-size-5xs
|
|
61
|
+
min-width: var(--wpds-dimension-size-5xs);
|
|
62
62
|
|
|
63
63
|
border-radius: var(--a8c-charts-border-radius-leaderboard-bar);
|
|
64
64
|
z-index: -1; /* places it behind the label */
|
|
@@ -97,6 +97,18 @@
|
|
|
97
97
|
font-variant-numeric: tabular-nums;
|
|
98
98
|
min-width: 3ch;
|
|
99
99
|
text-align: end;
|
|
100
|
+
|
|
101
|
+
&--down {
|
|
102
|
+
color: var(--a8c-charts-color-trend-down);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
&--neutral {
|
|
106
|
+
color: var(--a8c-charts-color-trend-neutral);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
&--up {
|
|
110
|
+
color: var(--a8c-charts-color-trend-up);
|
|
111
|
+
}
|
|
100
112
|
}
|
|
101
113
|
|
|
102
114
|
.deltaPlaceholder {
|
|
@@ -108,10 +120,10 @@
|
|
|
108
120
|
}
|
|
109
121
|
|
|
110
122
|
.emptyState {
|
|
111
|
-
padding: var(--wpds-dimension-padding-3xl
|
|
123
|
+
padding: var(--wpds-dimension-padding-3xl) var(--wpds-dimension-padding-lg);
|
|
112
124
|
text-align: center;
|
|
113
125
|
color: var(--a8c-charts-color-label-secondary);
|
|
114
|
-
font-size: var(--wpds-typography-font-size-md
|
|
126
|
+
font-size: var(--wpds-typography-font-size-md);
|
|
115
127
|
font-style: italic;
|
|
116
128
|
}
|
|
117
129
|
|
|
@@ -133,7 +145,7 @@
|
|
|
133
145
|
// interactive row's outline (and its inset offset) stay equal. A Sass var,
|
|
134
146
|
// not a custom property: a compile-time constant scoped to this module, so it
|
|
135
147
|
// needs no runtime cascade and never lands on the DOM as an unprefixed var.
|
|
136
|
-
$focus-ring-width: var(--wpds-border-width-focus
|
|
148
|
+
$focus-ring-width: var(--wpds-border-width-focus);
|
|
137
149
|
|
|
138
150
|
.row {
|
|
139
151
|
display: grid;
|
|
@@ -169,16 +181,16 @@ $focus-ring-width: var(--wpds-border-width-focus, var(--wp-admin-border-width-fo
|
|
|
169
181
|
// value slide is given an RTL override below. No row background.
|
|
170
182
|
.bar {
|
|
171
183
|
transition:
|
|
172
|
-
opacity var(--wpds-motion-duration-sm
|
|
173
|
-
var(--wpds-motion-easing-subtle
|
|
174
|
-
width var(--wpds-motion-duration-sm
|
|
175
|
-
var(--wpds-motion-easing-subtle
|
|
184
|
+
opacity var(--wpds-motion-duration-sm)
|
|
185
|
+
var(--wpds-motion-easing-subtle),
|
|
186
|
+
width var(--wpds-motion-duration-sm)
|
|
187
|
+
var(--wpds-motion-easing-subtle);
|
|
176
188
|
}
|
|
177
189
|
|
|
178
190
|
.valueContainer {
|
|
179
191
|
transition:
|
|
180
|
-
transform var(--wpds-motion-duration-sm
|
|
181
|
-
var(--wpds-motion-easing-subtle
|
|
192
|
+
transform var(--wpds-motion-duration-sm)
|
|
193
|
+
var(--wpds-motion-easing-subtle);
|
|
182
194
|
}
|
|
183
195
|
|
|
184
196
|
&:hover,
|
|
@@ -186,7 +198,7 @@ $focus-ring-width: var(--wpds-border-width-focus, var(--wp-admin-border-width-fo
|
|
|
186
198
|
|
|
187
199
|
// Fixed reserve for the chevron — the value slides by it in full, and the
|
|
188
200
|
// bar width inset (see leaderboard-chart.tsx) scales it by the bar's share.
|
|
189
|
-
--a8c-charts-dimension-leaderboard-bar-hover-inset: var(--wpds-dimension-gap-2xl
|
|
201
|
+
--a8c-charts-dimension-leaderboard-bar-hover-inset: var(--wpds-dimension-gap-2xl);
|
|
190
202
|
|
|
191
203
|
.bar {
|
|
192
204
|
opacity: 0.5;
|
|
@@ -208,10 +220,10 @@ $focus-ring-width: var(--wpds-border-width-focus, var(--wp-admin-border-width-fo
|
|
|
208
220
|
}
|
|
209
221
|
|
|
210
222
|
&:focus-visible {
|
|
211
|
-
border-radius: var(--wpds-border-radius-sm
|
|
223
|
+
border-radius: var(--wpds-border-radius-sm);
|
|
212
224
|
outline:
|
|
213
225
|
$focus-ring-width solid
|
|
214
|
-
var(--wpds-color-stroke-focus
|
|
226
|
+
var(--wpds-color-stroke-focus);
|
|
215
227
|
// Inset by its own width so the outer edge hugs the row boundary: the
|
|
216
228
|
// outline then sits in the shared row padding, clear of the bar, and stays
|
|
217
229
|
// inside the overflow:auto container that would clip an outset ring.
|
|
@@ -221,15 +233,15 @@ $focus-ring-width: var(--wpds-border-width-focus, var(--wp-admin-border-width-fo
|
|
|
221
233
|
|
|
222
234
|
.chevron {
|
|
223
235
|
position: absolute;
|
|
224
|
-
inset-inline-end: var(--wpds-dimension-padding-xs
|
|
236
|
+
inset-inline-end: var(--wpds-dimension-padding-xs);
|
|
225
237
|
top: 0;
|
|
226
238
|
bottom: 0;
|
|
227
239
|
margin-block: auto;
|
|
228
240
|
opacity: 0;
|
|
229
241
|
color: var(--a8c-charts-color-label-secondary);
|
|
230
242
|
transition:
|
|
231
|
-
opacity var(--wpds-motion-duration-sm
|
|
232
|
-
var(--wpds-motion-easing-subtle
|
|
243
|
+
opacity var(--wpds-motion-duration-sm)
|
|
244
|
+
var(--wpds-motion-easing-subtle);
|
|
233
245
|
pointer-events: none;
|
|
234
246
|
|
|
235
247
|
// The glyph points toward the inline end, so flip it to point left in RTL.
|
|
@@ -26,6 +26,15 @@ import styles from './leaderboard-chart.module.scss';
|
|
|
26
26
|
import type { LeaderboardChartProps } from './types';
|
|
27
27
|
import type { LeaderboardEntry } from '../../types';
|
|
28
28
|
|
|
29
|
+
// Handed to `Grid` as a chain rather than set in this component's stylesheet: `Grid` writes
|
|
30
|
+
// its own gaps through an Emotion class, which a module class of equal specificity would not
|
|
31
|
+
// reliably outrank. The chain lands on the element and resolves there.
|
|
32
|
+
const ROW_GAP = 'var(--a8c-charts-dimension-leaderboard-row-gap, 12px)';
|
|
33
|
+
const COLUMN_GAP = 'var(--a8c-charts-dimension-leaderboard-column-gap, 4px)';
|
|
34
|
+
|
|
35
|
+
/** Indexed by `Math.sign( delta ) + 1`, so [negative, neutral, positive]. */
|
|
36
|
+
const DELTA_TREND_CLASS = [ 'deltaValue--down', 'deltaValue--neutral', 'deltaValue--up' ] as const;
|
|
37
|
+
|
|
29
38
|
/**
|
|
30
39
|
* Default value formatter using formatMetricValue
|
|
31
40
|
*
|
|
@@ -198,22 +207,15 @@ const LeaderboardChartInternal: FC< LeaderboardChartProps > = ( {
|
|
|
198
207
|
|
|
199
208
|
// Process children to extract compound components
|
|
200
209
|
const { legendChildren, nonLegendChildren } = useChartChildren( children, 'LeaderboardChart' );
|
|
201
|
-
const {
|
|
202
|
-
labelSpacing,
|
|
203
|
-
rowGap,
|
|
204
|
-
columnGap,
|
|
205
|
-
primaryColor: settingsPrimaryColor,
|
|
206
|
-
secondaryColor: settingsSecondaryColor,
|
|
207
|
-
deltaColors,
|
|
208
|
-
} = leaderboardChartSettings;
|
|
210
|
+
const { labelSpacing } = leaderboardChartSettings;
|
|
209
211
|
const { getElementStyles, isSeriesVisible } = useGlobalChartsContext();
|
|
210
212
|
const { color: resolvedPrimaryColor } = getElementStyles( {
|
|
211
213
|
index: 0,
|
|
212
|
-
overrideColor: primaryColor
|
|
214
|
+
overrideColor: primaryColor,
|
|
213
215
|
} );
|
|
214
216
|
const { color: resolvedSecondaryColor } = getElementStyles( {
|
|
215
217
|
index: 1,
|
|
216
|
-
overrideColor: secondaryColor
|
|
218
|
+
overrideColor: secondaryColor,
|
|
217
219
|
} );
|
|
218
220
|
|
|
219
221
|
// Create legend items using the custom hook
|
|
@@ -292,6 +294,7 @@ const LeaderboardChartInternal: FC< LeaderboardChartProps > = ( {
|
|
|
292
294
|
legendElement={ false }
|
|
293
295
|
legendChildren={ legendChildren }
|
|
294
296
|
className={ clsx(
|
|
297
|
+
'leaderboard-chart',
|
|
295
298
|
styles.leaderboardChart,
|
|
296
299
|
{
|
|
297
300
|
[ styles[ 'leaderboardChart--responsive' ] ]: ! propWidth && ! propHeight,
|
|
@@ -336,6 +339,7 @@ const LeaderboardChartInternal: FC< LeaderboardChartProps > = ( {
|
|
|
336
339
|
legendElement={ legendElement }
|
|
337
340
|
legendChildren={ legendChildren }
|
|
338
341
|
className={ clsx(
|
|
342
|
+
'leaderboard-chart',
|
|
339
343
|
styles.leaderboardChart,
|
|
340
344
|
{
|
|
341
345
|
[ styles[ 'leaderboardChart--responsive' ] ]: ! propWidth && ! propHeight,
|
|
@@ -372,8 +376,8 @@ const LeaderboardChartInternal: FC< LeaderboardChartProps > = ( {
|
|
|
372
376
|
) : (
|
|
373
377
|
<Grid
|
|
374
378
|
templateColumns="minmax(0, 1fr) auto"
|
|
375
|
-
rowGap={
|
|
376
|
-
columnGap={
|
|
379
|
+
rowGap={ ROW_GAP }
|
|
380
|
+
columnGap={ COLUMN_GAP }
|
|
377
381
|
data-leaderboard-grid
|
|
378
382
|
>
|
|
379
383
|
{ data.map( ( entry, rowIndex ) => {
|
|
@@ -389,8 +393,11 @@ const LeaderboardChartInternal: FC< LeaderboardChartProps > = ( {
|
|
|
389
393
|
const hasDelta = hasDeltaValue( entry );
|
|
390
394
|
const showComparisonValue = showComparisonColumn && hasDelta;
|
|
391
395
|
const showComparisonPlaceholder = showComparisonColumn && ! hasDelta;
|
|
392
|
-
|
|
393
|
-
const
|
|
396
|
+
// Math.sign gives -1/0/1; the placeholder has no delta to read, so it takes neutral.
|
|
397
|
+
const deltaTrendClass =
|
|
398
|
+
styles[
|
|
399
|
+
DELTA_TREND_CLASS[ showComparisonValue ? Math.sign( entry.delta ) + 1 : 1 ]
|
|
400
|
+
];
|
|
394
401
|
|
|
395
402
|
const rowCells = (
|
|
396
403
|
<>
|
|
@@ -417,15 +424,18 @@ const LeaderboardChartInternal: FC< LeaderboardChartProps > = ( {
|
|
|
417
424
|
{ isPrimaryVisible && <Text>{ valueFormatter( entry.currentValue ) }</Text> }
|
|
418
425
|
|
|
419
426
|
{ showComparisonValue && (
|
|
420
|
-
<Text className={ styles.deltaValue
|
|
427
|
+
<Text className={ clsx( styles.deltaValue, deltaTrendClass ) }>
|
|
421
428
|
{ deltaFormatter( entry.delta ) }
|
|
422
429
|
</Text>
|
|
423
430
|
) }
|
|
424
431
|
|
|
425
432
|
{ showComparisonPlaceholder && (
|
|
426
433
|
<Text
|
|
427
|
-
className={ clsx(
|
|
428
|
-
|
|
434
|
+
className={ clsx(
|
|
435
|
+
styles.deltaValue,
|
|
436
|
+
styles.deltaPlaceholder,
|
|
437
|
+
deltaTrendClass
|
|
438
|
+
) }
|
|
429
439
|
>
|
|
430
440
|
<span aria-hidden="true">—</span>
|
|
431
441
|
<VisuallyHidden as="span">
|
|
@@ -665,4 +665,18 @@ describe( 'LeaderboardChart', () => {
|
|
|
665
665
|
expect( screen.getAllByRole( 'button' ) ).toHaveLength( 1 );
|
|
666
666
|
} );
|
|
667
667
|
} );
|
|
668
|
+
|
|
669
|
+
describe( 'grid gaps', () => {
|
|
670
|
+
// eslint-disable-next-line testing-library/no-node-access -- The grid is rendered by `Grid` from @wordpress/components, which takes no test id.
|
|
671
|
+
const grid = () => document.querySelector( '[data-leaderboard-grid]' );
|
|
672
|
+
|
|
673
|
+
it( 'hands the catalog role to the grid', () => {
|
|
674
|
+
render( <LeaderboardChart data={ mockData } /> );
|
|
675
|
+
|
|
676
|
+
expect( grid() ).toHaveStyle( {
|
|
677
|
+
gridRowGap: 'var(--a8c-charts-dimension-leaderboard-row-gap, 12px)',
|
|
678
|
+
gridColumnGap: 'var(--a8c-charts-dimension-leaderboard-column-gap, 4px)',
|
|
679
|
+
} );
|
|
680
|
+
} );
|
|
681
|
+
} );
|
|
668
682
|
} );
|
|
@@ -113,14 +113,8 @@ describe( 'useLeaderboardLegendItems', () => {
|
|
|
113
113
|
} );
|
|
114
114
|
|
|
115
115
|
describe( 'Color handling', () => {
|
|
116
|
-
it( '
|
|
117
|
-
const
|
|
118
|
-
leaderboardChart: {
|
|
119
|
-
primaryColor: '#FF0000',
|
|
120
|
-
secondaryColor: '#00FF00',
|
|
121
|
-
},
|
|
122
|
-
};
|
|
123
|
-
const wrapper = createWrapper( customTheme );
|
|
116
|
+
it( 'gives both legend items a color from the palette', () => {
|
|
117
|
+
const wrapper = createWrapper();
|
|
124
118
|
const { result } = renderHook(
|
|
125
119
|
() =>
|
|
126
120
|
useLeaderboardLegendItems( {
|
|
@@ -131,7 +125,6 @@ describe( 'useLeaderboardLegendItems', () => {
|
|
|
131
125
|
{ wrapper }
|
|
132
126
|
);
|
|
133
127
|
|
|
134
|
-
// Note: The actual color will be resolved by the context, but we can check structure
|
|
135
128
|
expect( result.current[ 0 ].color ).toBeTruthy();
|
|
136
129
|
expect( result.current[ 1 ].color ).toBeTruthy();
|
|
137
130
|
} );
|
|
@@ -36,9 +36,9 @@ export interface LeaderboardChartProps
|
|
|
36
36
|
secondaryColor?: string;
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
|
-
* Formatter for values
|
|
39
|
+
* Formatter for values. May return an element, e.g. to attach a tooltip.
|
|
40
40
|
*/
|
|
41
|
-
valueFormatter?: ( value: number ) =>
|
|
41
|
+
valueFormatter?: ( value: number ) => ReactNode;
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
44
|
* Formatter for delta values
|
|
@@ -21,21 +21,21 @@
|
|
|
21
21
|
&__annotation-label-popover {
|
|
22
22
|
background: var(--a8c-charts-color-surface);
|
|
23
23
|
color: var(--a8c-charts-color-label);
|
|
24
|
-
padding: var(--wpds-dimension-padding-sm
|
|
24
|
+
padding: var(--wpds-dimension-padding-sm);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
&__tooltip-date {
|
|
28
|
-
font-weight: var(--wpds-typography-font-weight-emphasis
|
|
29
|
-
padding-bottom: var(--wpds-dimension-padding-md
|
|
28
|
+
font-weight: var(--wpds-typography-font-weight-emphasis);
|
|
29
|
+
padding-bottom: var(--wpds-dimension-padding-md);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
&__tooltip-row {
|
|
33
|
-
padding: var(--wpds-dimension-padding-xs
|
|
33
|
+
padding: var(--wpds-dimension-padding-xs) 0;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
&__tooltip-label {
|
|
37
|
-
font-weight: var(--wpds-typography-font-weight-emphasis
|
|
38
|
-
padding-right: var(--wpds-dimension-padding-lg
|
|
37
|
+
font-weight: var(--wpds-typography-font-weight-emphasis);
|
|
38
|
+
padding-right: var(--wpds-dimension-padding-lg);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
&__annotations-overlay {
|
|
@@ -64,14 +64,14 @@
|
|
|
64
64
|
&__annotation-label-popover {
|
|
65
65
|
min-width: 125px;
|
|
66
66
|
border: none;
|
|
67
|
-
border-radius: var(--wpds-border-radius-md
|
|
68
|
-
font-size: var(--wpds-typography-font-size-md
|
|
67
|
+
border-radius: var(--wpds-border-radius-md);
|
|
68
|
+
font-size: var(--wpds-typography-font-size-md);
|
|
69
69
|
box-shadow: var(--a8c-charts-elevation-sm, 0 1px 2px 0 #0000000d, 0 2px 3px 0 #0000000a, 0 6px 6px 0 #00000008, 0 8px 8px 0 #00000005);
|
|
70
70
|
position: fixed;
|
|
71
71
|
// Without margin set, the popover has margin:auto which upsets the
|
|
72
72
|
// positioning relative to the trigger button. A gap token is appropriate
|
|
73
73
|
// because it creates a gap to the trigger button.
|
|
74
|
-
margin: var(--wpds-dimension-gap-sm
|
|
74
|
+
margin: var(--wpds-dimension-gap-sm);
|
|
75
75
|
visibility: hidden;
|
|
76
76
|
|
|
77
77
|
&--visible {
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
&__annotation-label-popover-content {
|
|
88
|
-
padding: var(--wpds-dimension-padding-sm
|
|
88
|
+
padding: var(--wpds-dimension-padding-sm);
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
&__annotation-label-popover-close-button {
|
|
@@ -101,14 +101,6 @@
|
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
:global(.visx-tooltip-glyph) {
|
|
105
|
-
|
|
106
|
-
svg {
|
|
107
|
-
width: 10px;
|
|
108
|
-
height: 10px;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
104
|
@keyframes rise {
|
|
113
105
|
|
|
114
106
|
from {
|
|
@@ -24,30 +24,41 @@ import {
|
|
|
24
24
|
import {
|
|
25
25
|
GlobalChartsProvider,
|
|
26
26
|
GlobalChartsContext,
|
|
27
|
+
useChartFormatting,
|
|
27
28
|
useChartId,
|
|
28
29
|
useChartRegistration,
|
|
29
30
|
useGlobalChartsContext,
|
|
30
|
-
useGlobalChartsTheme,
|
|
31
31
|
} from '../../providers';
|
|
32
32
|
import { useDefaultHiddenSeries } from '../../providers/chart-context/hooks/use-default-hidden-series';
|
|
33
33
|
import { attachSubComponents } from '../../utils';
|
|
34
|
+
import { getBucketInfo } from '../../utils/bucket-info';
|
|
35
|
+
import { createDateFormatter } from '../../utils/date-formatting';
|
|
34
36
|
import { useChartChildren } from '../private/chart-composition';
|
|
35
37
|
import { ChartInstanceContext, type ChartInstanceRef } from '../private/chart-instance-context';
|
|
36
38
|
import { ChartLayout } from '../private/chart-layout';
|
|
37
39
|
import { DefaultGlyph } from '../private/default-glyph';
|
|
38
40
|
import { getAllHiddenMessage, SvgEmptyState } from '../private/svg-empty-state';
|
|
39
|
-
import { getCurveType
|
|
41
|
+
import { getCurveType } from '../private/time-axis';
|
|
42
|
+
import { buildTimeAxisOptions } from '../private/time-axis-options';
|
|
40
43
|
import { withResponsive } from '../private/with-responsive';
|
|
41
44
|
import { useXZoom, ZoomResetButton, ZoomSelectionRect, ZoomClip } from '../private/x-zoom';
|
|
45
|
+
import plotStyles from '../private/xy-plot/xy-plot.module.scss';
|
|
42
46
|
import styles from './line-chart.module.scss';
|
|
43
47
|
import { LineChartAnnotation, LineChartAnnotationsOverlay, LineChartGlyph } from './private';
|
|
44
48
|
import type { RenderLineGlyphProps, LineChartProps, TooltipDatum } from './types';
|
|
45
|
-
import type {
|
|
49
|
+
import type {
|
|
50
|
+
BucketInfo,
|
|
51
|
+
DataPoint,
|
|
52
|
+
DataPointDate,
|
|
53
|
+
SeriesData,
|
|
54
|
+
Optional,
|
|
55
|
+
TickResolution,
|
|
56
|
+
} from '../../types';
|
|
46
57
|
import type { RenderTooltipParams } from '../../visx/types';
|
|
47
58
|
import type { ResponsiveConfig } from '../private/with-responsive';
|
|
48
59
|
import type { TickFormatter } from '@visx/axis';
|
|
49
60
|
import type { GlyphProps } from '@visx/xychart';
|
|
50
|
-
import type { FC, Ref } from 'react';
|
|
61
|
+
import type { CSSProperties, FC, Ref } from 'react';
|
|
51
62
|
|
|
52
63
|
const defaultRenderGlyph = < Datum extends object >( props: RenderLineGlyphProps< Datum > ) => {
|
|
53
64
|
return <DefaultGlyph { ...props } key={ props.key } />;
|
|
@@ -58,16 +69,54 @@ const toNumber = ( val?: number | string | null ): number | undefined => {
|
|
|
58
69
|
return isNaN( num ) ? undefined : num;
|
|
59
70
|
};
|
|
60
71
|
|
|
72
|
+
// No date-part options, which is what `Intl` defaults to a numeric date for —
|
|
73
|
+
// the heading the tooltip printed with `toLocaleDateString()` before it could
|
|
74
|
+
// be told a locale and a zone.
|
|
75
|
+
const TOOLTIP_DATE: Intl.DateTimeFormatOptions = {};
|
|
76
|
+
|
|
77
|
+
// Only the hour case adds to the plain date heading. A coarser bucket keeps the
|
|
78
|
+
// day: 'month' covers any spacing from 28 days up, so dropping it would give two
|
|
79
|
+
// distinct points one heading.
|
|
80
|
+
const TOOLTIP_FORMAT_BY_RESOLUTION: Record<
|
|
81
|
+
Exclude< TickResolution, 'week' >,
|
|
82
|
+
Intl.DateTimeFormatOptions
|
|
83
|
+
> = {
|
|
84
|
+
hour: { year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric' },
|
|
85
|
+
day: TOOLTIP_DATE,
|
|
86
|
+
month: TOOLTIP_DATE,
|
|
87
|
+
year: TOOLTIP_DATE,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// A component rather than a call, because `renderDefaultTooltip` is a plain
|
|
91
|
+
// function a consumer may pass around: the context has to be read where the
|
|
92
|
+
// heading renders, not where the tooltip is built.
|
|
93
|
+
const TooltipDate: FC< { date?: Date; displayResolution: Exclude< TickResolution, 'week' > } > = ( {
|
|
94
|
+
date,
|
|
95
|
+
displayResolution,
|
|
96
|
+
} ) => {
|
|
97
|
+
const formatting = useChartFormatting();
|
|
98
|
+
const format = useMemo(
|
|
99
|
+
() => createDateFormatter( TOOLTIP_FORMAT_BY_RESOLUTION[ displayResolution ], formatting ),
|
|
100
|
+
[ displayResolution, formatting ]
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
return <>{ date ? format( date ) : null }</>;
|
|
104
|
+
};
|
|
105
|
+
|
|
61
106
|
/**
|
|
62
107
|
* Default visx-tooltip render that prints the hovered date as a heading and
|
|
63
108
|
* one row per visible series (label + formatted value), sorted descending by
|
|
64
109
|
* value. Reused by AreaChart, which has the same multi-series shape.
|
|
65
110
|
*
|
|
66
|
-
* @param params
|
|
111
|
+
* @param params - visx tooltip data and the chart's optional `bucketInfo`.
|
|
112
|
+
* @param contentStyle - Explicit tooltip content color overrides.
|
|
67
113
|
* @return Tooltip JSX, or `null` when no datum is hovered.
|
|
68
114
|
*/
|
|
69
|
-
export const renderDefaultTooltip = (
|
|
70
|
-
|
|
115
|
+
export const renderDefaultTooltip = (
|
|
116
|
+
params: RenderTooltipParams< DataPointDate > & { bucketInfo?: BucketInfo },
|
|
117
|
+
contentStyle?: Pick< CSSProperties, 'color' | 'background' | 'backgroundColor' >
|
|
118
|
+
) => {
|
|
119
|
+
const { tooltipData, bucketInfo } = params;
|
|
71
120
|
const nearestDatum = tooltipData?.nearestDatum?.datum;
|
|
72
121
|
if ( ! nearestDatum ) return null;
|
|
73
122
|
|
|
@@ -79,9 +128,16 @@ export const renderDefaultTooltip = ( params: RenderTooltipParams< DataPointDate
|
|
|
79
128
|
.sort( ( a, b ) => b.value - a.value );
|
|
80
129
|
|
|
81
130
|
return (
|
|
82
|
-
<div
|
|
131
|
+
<div
|
|
132
|
+
className={ styles[ 'line-chart__tooltip' ] }
|
|
133
|
+
data-testid="line-chart-tooltip-content"
|
|
134
|
+
style={ contentStyle }
|
|
135
|
+
>
|
|
83
136
|
<div className={ styles[ 'line-chart__tooltip-date' ] }>
|
|
84
|
-
|
|
137
|
+
<TooltipDate
|
|
138
|
+
date={ nearestDatum.date }
|
|
139
|
+
displayResolution={ bucketInfo?.displayResolution ?? 'day' }
|
|
140
|
+
/>
|
|
85
141
|
</div>
|
|
86
142
|
{ tooltipPoints.map( point => (
|
|
87
143
|
<Stack
|
|
@@ -171,6 +227,8 @@ const LineChartInternal = forwardRef< ChartInstanceRef, LineChartProps >(
|
|
|
171
227
|
smoothing = true,
|
|
172
228
|
curveType,
|
|
173
229
|
renderTooltip = renderDefaultTooltip,
|
|
230
|
+
tooltipPlacement,
|
|
231
|
+
tooltipStyle,
|
|
174
232
|
withStartGlyphs = false,
|
|
175
233
|
withEndGlyphs = false,
|
|
176
234
|
animation,
|
|
@@ -194,10 +252,11 @@ const LineChartInternal = forwardRef< ChartInstanceRef, LineChartProps >(
|
|
|
194
252
|
const legendShape = legend.shape ?? 'line';
|
|
195
253
|
const legendPosition = legend.position ?? 'bottom';
|
|
196
254
|
|
|
197
|
-
const
|
|
255
|
+
const formatting = useChartFormatting();
|
|
198
256
|
const theme = useXYChartTheme( data );
|
|
199
|
-
//
|
|
200
|
-
|
|
257
|
+
// A gradient stop reads its color as a string, so it has to be resolved. Read back what
|
|
258
|
+
// `useXYChartTheme` already resolved rather than paying another getComputedStyle per render.
|
|
259
|
+
const resolvedBackgroundColor = theme.backgroundColor;
|
|
201
260
|
const chartId = useChartId( providedChartId );
|
|
202
261
|
const hiddenSeries = useDefaultHiddenSeries( chartId, defaultHiddenSeries );
|
|
203
262
|
const isSeriesVisible = useCallback(
|
|
@@ -303,21 +362,22 @@ const LineChartInternal = forwardRef< ChartInstanceRef, LineChartProps >(
|
|
|
303
362
|
chartRef,
|
|
304
363
|
totalPoints: dataSorted[ 0 ]?.data.length || 0,
|
|
305
364
|
onActivate: activateSelectedPoint,
|
|
365
|
+
preventTooltipScroll: tooltipPlacement === 'below-axis',
|
|
306
366
|
} );
|
|
307
367
|
|
|
308
368
|
const chartOptions = useMemo( () => {
|
|
309
|
-
const { tickResolution, tickFormat, ...xAxisOptions } = options?.axis?.x ?? {};
|
|
310
|
-
const formatter = tickFormat || getFormatter( dataSorted, tickResolution );
|
|
311
|
-
|
|
312
369
|
return {
|
|
313
370
|
axis: {
|
|
314
|
-
x: {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
371
|
+
x: buildTimeAxisOptions( {
|
|
372
|
+
dataSorted,
|
|
373
|
+
width,
|
|
374
|
+
axisOptions: options?.axis?.x,
|
|
375
|
+
scaleDomain: options?.xScale?.domain,
|
|
376
|
+
zoomDomain: zoom.domain,
|
|
377
|
+
formatting,
|
|
378
|
+
// A hidden line is unmounted, so visx scales to the rest.
|
|
379
|
+
isSeriesRendered: series => isSeriesVisible( series.label ),
|
|
380
|
+
} ),
|
|
321
381
|
y: {
|
|
322
382
|
orientation: 'left' as const,
|
|
323
383
|
numTicks: 4,
|
|
@@ -339,7 +399,18 @@ const LineChartInternal = forwardRef< ChartInstanceRef, LineChartProps >(
|
|
|
339
399
|
...options?.yScale,
|
|
340
400
|
},
|
|
341
401
|
};
|
|
342
|
-
}, [ options, dataSorted, width, zoom.domain, stableYDomain ] );
|
|
402
|
+
}, [ options, dataSorted, width, zoom.domain, stableYDomain, formatting, isSeriesVisible ] );
|
|
403
|
+
|
|
404
|
+
// Classified from the rendered series, like the axis above: a hidden
|
|
405
|
+
// hourly line must not leave the tooltip naming an hour the axis dropped.
|
|
406
|
+
const bucketInfo = useMemo(
|
|
407
|
+
() =>
|
|
408
|
+
getBucketInfo(
|
|
409
|
+
dataSorted.filter( series => isSeriesVisible( series.label ) ),
|
|
410
|
+
options?.axis?.x?.tickResolution
|
|
411
|
+
),
|
|
412
|
+
[ dataSorted, isSeriesVisible, options?.axis?.x?.tickResolution ]
|
|
413
|
+
);
|
|
343
414
|
|
|
344
415
|
const tooltipRenderGlyph = useMemo( () => {
|
|
345
416
|
return ( props: GlyphProps< DataPointDate > ) => {
|
|
@@ -411,7 +482,34 @@ const LineChartInternal = forwardRef< ChartInstanceRef, LineChartProps >(
|
|
|
411
482
|
yAccessor: ( d: DataPointDate ) => d?.value,
|
|
412
483
|
};
|
|
413
484
|
|
|
414
|
-
|
|
485
|
+
const resolvedTooltipStyle = useMemo( () => {
|
|
486
|
+
if ( renderTooltip !== renderDefaultTooltip || ! tooltipStyle ) return tooltipStyle;
|
|
487
|
+
if ( ! tooltipStyle.color || tooltipStyle.background || tooltipStyle.backgroundColor ) {
|
|
488
|
+
return tooltipStyle;
|
|
489
|
+
}
|
|
490
|
+
return {
|
|
491
|
+
backgroundColor: 'var(--a8c-charts-color-tooltip-surface, rgb(0 0 0 / 85%))',
|
|
492
|
+
...tooltipStyle,
|
|
493
|
+
};
|
|
494
|
+
}, [ renderTooltip, tooltipStyle ] );
|
|
495
|
+
|
|
496
|
+
// Augments every renderTooltip call with the chart's bucket classification,
|
|
497
|
+
// default or custom, so a heading keyed on it can't disagree with the axis.
|
|
498
|
+
const tooltipRenderer = useMemo(
|
|
499
|
+
() => ( params: RenderTooltipParams< DataPointDate > ) =>
|
|
500
|
+
renderTooltip === renderDefaultTooltip
|
|
501
|
+
? renderDefaultTooltip(
|
|
502
|
+
{ ...params, bucketInfo },
|
|
503
|
+
{
|
|
504
|
+
color: tooltipStyle?.color,
|
|
505
|
+
background: tooltipStyle?.background,
|
|
506
|
+
backgroundColor: tooltipStyle?.backgroundColor,
|
|
507
|
+
}
|
|
508
|
+
)
|
|
509
|
+
: renderTooltip( { ...params, bucketInfo } ),
|
|
510
|
+
[ renderTooltip, bucketInfo, tooltipStyle ]
|
|
511
|
+
);
|
|
512
|
+
|
|
415
513
|
if ( error ) {
|
|
416
514
|
return <div className={ clsx( 'line-chart', styles[ 'line-chart' ] ) }>{ error }</div>;
|
|
417
515
|
}
|
|
@@ -472,7 +570,7 @@ const LineChartInternal = forwardRef< ChartInstanceRef, LineChartProps >(
|
|
|
472
570
|
onBlur={ onChartBlur }
|
|
473
571
|
>
|
|
474
572
|
{ chartHeight > 0 && (
|
|
475
|
-
<div ref={ chartRef }
|
|
573
|
+
<div ref={ chartRef } className={ plotStyles[ 'xy-plot' ] }>
|
|
476
574
|
{ zoomable && zoom.domain && <ZoomResetButton onClick={ zoom.reset } /> }
|
|
477
575
|
<XYChart
|
|
478
576
|
theme={ theme }
|
|
@@ -607,13 +705,17 @@ const LineChartInternal = forwardRef< ChartInstanceRef, LineChartProps >(
|
|
|
607
705
|
<AccessibleTooltip
|
|
608
706
|
detectBounds
|
|
609
707
|
snapTooltipToDatumX
|
|
708
|
+
tooltipPlacement={ tooltipPlacement }
|
|
709
|
+
style={ resolvedTooltipStyle }
|
|
610
710
|
snapTooltipToDatumY
|
|
611
711
|
showSeriesGlyphs
|
|
612
|
-
renderTooltip={
|
|
712
|
+
renderTooltip={ tooltipRenderer }
|
|
613
713
|
renderGlyph={ tooltipRenderGlyph }
|
|
614
714
|
glyphStyle={ glyphStyle }
|
|
615
715
|
showVerticalCrosshair={ withTooltipCrosshairs?.showVertical }
|
|
616
716
|
showHorizontalCrosshair={ withTooltipCrosshairs?.showHorizontal }
|
|
717
|
+
verticalCrosshairStyle={ withTooltipCrosshairs?.verticalStyle }
|
|
718
|
+
horizontalCrosshairStyle={ withTooltipCrosshairs?.horizontalStyle }
|
|
617
719
|
selectedIndex={ selectedIndex }
|
|
618
720
|
tooltipRef={ tooltipRef }
|
|
619
721
|
keyboardFocusedClassName={
|