@automattic/charts 4.13.0 → 4.15.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 +34 -29
- package/TOKENS.md +4 -1
- package/dist/index.cjs +1069 -1620
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +33 -1
- package/dist/index.d.cts +60 -15
- package/dist/index.d.ts +60 -15
- package/dist/index.js +1072 -1575
- package/dist/index.js.map +1 -1
- package/package.json +11 -10
- package/src/charts/area-chart/area-chart.tsx +29 -20
- package/src/charts/area-chart/types.ts +3 -1
- package/src/charts/bar-chart/bar-chart.module.scss +38 -0
- package/src/charts/bar-chart/bar-chart.tsx +159 -56
- package/src/charts/bar-chart/private/band-tooltip.tsx +40 -5
- package/src/charts/bar-chart/private/bar-layer.tsx +66 -13
- package/src/charts/bar-chart/private/category.ts +25 -0
- package/src/charts/bar-chart/private/comparison-bars-geometry.ts +13 -1
- package/src/charts/bar-chart/private/index.ts +3 -1
- package/src/charts/bar-chart/private/selected-bar-ring.tsx +17 -6
- package/src/charts/bar-chart/private/stack-layout.ts +99 -0
- package/src/charts/bar-chart/private/stacked-bar-series.tsx +145 -0
- package/src/charts/bar-chart/private/test/stack-layout.test.ts +81 -0
- package/src/charts/bar-chart/private/test/value-domain.test.ts +10 -0
- package/src/charts/bar-chart/private/use-bar-chart-options.ts +35 -11
- package/src/charts/bar-chart/private/{use-overlap-slots.ts → use-bar-slots.ts} +36 -38
- package/src/charts/bar-chart/private/use-bar-stops.ts +19 -19
- package/src/charts/bar-chart/private/value-domain.ts +10 -8
- package/src/charts/bar-chart/test/bar-chart-stacked.test.tsx +546 -0
- package/src/charts/bar-chart/test/bar-chart.test.tsx +75 -0
- package/src/charts/bar-chart/test/use-bar-legend-items.test.tsx +18 -0
- package/src/charts/bar-chart/types.ts +3 -1
- package/src/charts/bar-chart/use-bar-colors.ts +12 -0
- package/src/charts/bar-chart/use-bar-legend-items.ts +4 -2
- package/src/charts/bar-list-chart/bar-list-chart.tsx +2 -0
- package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +1 -1
- package/src/charts/leaderboard-chart/leaderboard-chart.tsx +26 -9
- package/src/charts/leaderboard-chart/private/resolve-leaderboard-entries.ts +53 -0
- package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +41 -0
- package/src/charts/leaderboard-chart/test/resolve-leaderboard-entries.test.ts +146 -0
- package/src/charts/leaderboard-chart/types.ts +6 -0
- package/src/charts/line-chart/line-chart.tsx +55 -26
- package/src/charts/line-chart/test/line-chart.test.tsx +86 -0
- package/src/charts/line-chart/types.ts +3 -1
- package/src/charts/private/test/compact-when-short.test.tsx +170 -0
- package/src/charts/private/test/tick-decimals.test.tsx +95 -0
- package/src/charts/private/tick-decimals.tsx +132 -0
- package/src/charts/private/use-compact-when-short.ts +18 -0
- package/src/components/legend/private/base-legend.tsx +13 -8
- package/src/components/legend/test/legend.test.tsx +125 -0
- package/src/components/legend/types.ts +9 -1
- package/src/providers/chart-context/themes.ts +4 -0
- package/src/styles/chart-scope.scss +1 -0
- package/src/types.ts +40 -9
- package/src/charts/private/test/whole-number-ticks.test.tsx +0 -57
- package/src/charts/private/whole-number-ticks.tsx +0 -79
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@automattic/charts",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.15.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": {
|
|
@@ -63,8 +63,8 @@
|
|
|
63
63
|
"typecheck": "tsgo --noEmit"
|
|
64
64
|
},
|
|
65
65
|
"dependencies": {
|
|
66
|
-
"@automattic/number-formatters": "^1.
|
|
67
|
-
"@babel/runtime": "7.29.
|
|
66
|
+
"@automattic/number-formatters": "^1.3.0",
|
|
67
|
+
"@babel/runtime": "7.29.10",
|
|
68
68
|
"@date-fns/tz": "1.5.0",
|
|
69
69
|
"@react-spring/web": "10.1.2",
|
|
70
70
|
"@visx/annotation": "^4.0.0",
|
|
@@ -83,10 +83,10 @@
|
|
|
83
83
|
"@visx/tooltip": "^4.0.0",
|
|
84
84
|
"@visx/vendor": "^4.0.0",
|
|
85
85
|
"@visx/xychart": "^4.0.0",
|
|
86
|
-
"@wordpress/i18n": "^
|
|
87
|
-
"@wordpress/icons": "^
|
|
88
|
-
"@wordpress/theme": "
|
|
89
|
-
"@wordpress/ui": "0.
|
|
86
|
+
"@wordpress/i18n": "^7.0.0",
|
|
87
|
+
"@wordpress/icons": "^20.0.0",
|
|
88
|
+
"@wordpress/theme": "4.0.0",
|
|
89
|
+
"@wordpress/ui": "0.26.0",
|
|
90
90
|
"clsx": "2.1.1",
|
|
91
91
|
"date-fns": "^4.1.0",
|
|
92
92
|
"deepmerge": "4.3.1",
|
|
@@ -111,9 +111,10 @@
|
|
|
111
111
|
"@types/react-dom": "19.3.0",
|
|
112
112
|
"@typescript/native-preview": "7.0.0-dev.20260707.2",
|
|
113
113
|
"@visx/glyph": "4.0.0",
|
|
114
|
-
"@wordpress/components": "
|
|
115
|
-
"@wordpress/element": "
|
|
116
|
-
"@wordpress/private-apis": "
|
|
114
|
+
"@wordpress/components": "44.0.0",
|
|
115
|
+
"@wordpress/element": "10.1.0",
|
|
116
|
+
"@wordpress/private-apis": "3.1.0",
|
|
117
|
+
"@wordpress/theme-build-tools": "1.0.0",
|
|
117
118
|
"babel-jest": "30.5.2",
|
|
118
119
|
"identity-obj-proxy": "^3.0.0",
|
|
119
120
|
"jest": "30.5.2",
|
|
@@ -36,9 +36,10 @@ import { useChartChildren } from '../private/chart-composition';
|
|
|
36
36
|
import { ChartInstanceContext, type ChartInstanceRef } from '../private/chart-instance-context';
|
|
37
37
|
import { ChartLayout } from '../private/chart-layout';
|
|
38
38
|
import { getAllHiddenMessage, SvgEmptyState } from '../private/svg-empty-state';
|
|
39
|
+
import { hasOnlyWholeNumbers, TickDecimals } from '../private/tick-decimals';
|
|
39
40
|
import { getCurveType } from '../private/time-axis';
|
|
40
41
|
import { buildTimeAxisOptions } from '../private/time-axis-options';
|
|
41
|
-
import {
|
|
42
|
+
import { useCompactWhenShort } from '../private/use-compact-when-short';
|
|
42
43
|
import { withResponsive } from '../private/with-responsive';
|
|
43
44
|
import { useXZoom, ZoomResetButton, ZoomSelectionRect, ZoomClip } from '../private/x-zoom';
|
|
44
45
|
import plotStyles from '../private/xy-plot/xy-plot.module.scss';
|
|
@@ -82,6 +83,7 @@ const AreaChartInternal = forwardRef< ChartInstanceRef, AreaChartProps >(
|
|
|
82
83
|
children,
|
|
83
84
|
gridVisibility,
|
|
84
85
|
gap = 'md',
|
|
86
|
+
compactWhenShort = false,
|
|
85
87
|
},
|
|
86
88
|
ref
|
|
87
89
|
) => {
|
|
@@ -109,6 +111,7 @@ const AreaChartInternal = forwardRef< ChartInstanceRef, AreaChartProps >(
|
|
|
109
111
|
} );
|
|
110
112
|
|
|
111
113
|
const { legendChildren, nonLegendChildren } = useChartChildren( children, 'AreaChart' );
|
|
114
|
+
const { isCompact, compactMargin } = useCompactWhenShort( compactWhenShort, height );
|
|
112
115
|
const [ measuredChartHeight, setMeasuredChartHeight ] = useState< number | undefined >();
|
|
113
116
|
|
|
114
117
|
const handleContentHeightChange = useCallback(
|
|
@@ -227,22 +230,26 @@ const AreaChartInternal = forwardRef< ChartInstanceRef, AreaChartProps >(
|
|
|
227
230
|
const chartOptions = useMemo( () => {
|
|
228
231
|
return {
|
|
229
232
|
axis: {
|
|
230
|
-
x:
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
233
|
+
x: {
|
|
234
|
+
...buildTimeAxisOptions( {
|
|
235
|
+
dataSorted,
|
|
236
|
+
width,
|
|
237
|
+
axisOptions: options?.axis?.x,
|
|
238
|
+
scaleDomain: options?.xScale?.domain,
|
|
239
|
+
zoomDomain: zoom.domain,
|
|
240
|
+
formatting,
|
|
241
|
+
// No `isSeriesRendered`: a hidden area stays mounted with a zeroed
|
|
242
|
+
// yAccessor, so it still contributes to the x domain.
|
|
243
|
+
} ),
|
|
244
|
+
...( isCompact && { display: false } ),
|
|
245
|
+
},
|
|
240
246
|
y: {
|
|
241
247
|
orientation: 'left' as const,
|
|
242
248
|
numTicks: 4,
|
|
243
249
|
tickFormat: formatNumberCompact as TickFormatter< number >,
|
|
244
250
|
display: true,
|
|
245
251
|
...options?.axis?.y,
|
|
252
|
+
...( isCompact && { display: false } ),
|
|
246
253
|
},
|
|
247
254
|
},
|
|
248
255
|
xScale: {
|
|
@@ -259,7 +266,7 @@ const AreaChartInternal = forwardRef< ChartInstanceRef, AreaChartProps >(
|
|
|
259
266
|
...options?.yScale,
|
|
260
267
|
},
|
|
261
268
|
};
|
|
262
|
-
}, [ options, dataSorted, width, stacked, fixedYDomain, zoom.domain, formatting ] );
|
|
269
|
+
}, [ options, dataSorted, width, stacked, fixedYDomain, zoom.domain, formatting, isCompact ] );
|
|
263
270
|
|
|
264
271
|
const defaultMargin = useChartMargin( height, chartOptions, dataSorted, theme );
|
|
265
272
|
|
|
@@ -357,7 +364,7 @@ const AreaChartInternal = forwardRef< ChartInstanceRef, AreaChartProps >(
|
|
|
357
364
|
return <div className={ clsx( 'area-chart', styles[ 'area-chart' ] ) }>{ error }</div>;
|
|
358
365
|
}
|
|
359
366
|
|
|
360
|
-
const legendElement = showLegend && (
|
|
367
|
+
const legendElement = showLegend && ! isCompact && (
|
|
361
368
|
<Legend
|
|
362
369
|
orientation={ legend.orientation ?? 'horizontal' }
|
|
363
370
|
alignment={ legend.alignment ?? 'center' }
|
|
@@ -416,7 +423,7 @@ const AreaChartInternal = forwardRef< ChartInstanceRef, AreaChartProps >(
|
|
|
416
423
|
<ChartLayout
|
|
417
424
|
legendPosition={ legendPosition }
|
|
418
425
|
legendElement={ legendElement }
|
|
419
|
-
legendChildren={ legendChildren }
|
|
426
|
+
legendChildren={ isCompact ? [] : legendChildren }
|
|
420
427
|
gap={ gap }
|
|
421
428
|
className={ clsx(
|
|
422
429
|
'area-chart',
|
|
@@ -451,7 +458,7 @@ const AreaChartInternal = forwardRef< ChartInstanceRef, AreaChartProps >(
|
|
|
451
458
|
theme={ theme }
|
|
452
459
|
width={ width }
|
|
453
460
|
height={ chartHeight }
|
|
454
|
-
margin={ { ...defaultMargin, ...margin } }
|
|
461
|
+
margin={ { ...defaultMargin, ...compactMargin, ...margin } }
|
|
455
462
|
xScale={ chartOptions.xScale }
|
|
456
463
|
yScale={ chartOptions.yScale }
|
|
457
464
|
onPointerDown={ zoom.handlers.onPointerDown }
|
|
@@ -463,18 +470,20 @@ const AreaChartInternal = forwardRef< ChartInstanceRef, AreaChartProps >(
|
|
|
463
470
|
{ /* With every series hidden the value scale collapses, so the grid and axes
|
|
464
471
|
are dropped while the empty state stands in — otherwise they render
|
|
465
472
|
squished at the top. */ }
|
|
466
|
-
<
|
|
473
|
+
<TickDecimals
|
|
467
474
|
axis="y"
|
|
468
475
|
numTicks={ chartOptions.axis.y.numTicks }
|
|
469
|
-
|
|
476
|
+
decimals={
|
|
470
477
|
hasWholeNumberValues &&
|
|
471
478
|
! chartOptions.axis.y.tickValues &&
|
|
472
479
|
! options?.yScale?.domain
|
|
480
|
+
? 0
|
|
481
|
+
: undefined
|
|
473
482
|
}
|
|
474
483
|
>
|
|
475
484
|
{ tickValues => (
|
|
476
485
|
<>
|
|
477
|
-
{ ! allSeriesHidden && gridVisibility !== 'none' && (
|
|
486
|
+
{ ! allSeriesHidden && gridVisibility !== 'none' && ! isCompact && (
|
|
478
487
|
<Grid
|
|
479
488
|
columns={ false }
|
|
480
489
|
numTicks={ chartOptions.axis.y.numTicks }
|
|
@@ -492,7 +501,7 @@ const AreaChartInternal = forwardRef< ChartInstanceRef, AreaChartProps >(
|
|
|
492
501
|
) }
|
|
493
502
|
</>
|
|
494
503
|
) }
|
|
495
|
-
</
|
|
504
|
+
</TickDecimals>
|
|
496
505
|
|
|
497
506
|
{ allSeriesHidden ? (
|
|
498
507
|
<SvgEmptyState
|
|
@@ -501,7 +510,7 @@ const AreaChartInternal = forwardRef< ChartInstanceRef, AreaChartProps >(
|
|
|
501
510
|
width={ width }
|
|
502
511
|
height={ chartHeight }
|
|
503
512
|
>
|
|
504
|
-
{ getAllHiddenMessage( legendInteractive, 'series' ) }
|
|
513
|
+
{ getAllHiddenMessage( legendInteractive && ! isCompact, 'series' ) }
|
|
505
514
|
</SvgEmptyState>
|
|
506
515
|
) : null }
|
|
507
516
|
|
|
@@ -5,12 +5,14 @@ import type {
|
|
|
5
5
|
SeriesData,
|
|
6
6
|
SeriesChartLegendConfig,
|
|
7
7
|
SeriesVisibilityProps,
|
|
8
|
+
CompactWhenShortProps,
|
|
8
9
|
} from '../../types';
|
|
9
10
|
import type { RenderTooltipParams } from '../../visx/types';
|
|
10
11
|
import type { CurveType } from '../line-chart/types';
|
|
11
12
|
import type { ReactNode } from 'react';
|
|
12
13
|
|
|
13
|
-
export interface AreaChartProps
|
|
14
|
+
export interface AreaChartProps
|
|
15
|
+
extends BaseChartProps< SeriesData[] >, SeriesVisibilityProps, CompactWhenShortProps {
|
|
14
16
|
/**
|
|
15
17
|
* Legend configuration. Supports `collapseGroups` on top of the shared options.
|
|
16
18
|
*/
|
|
@@ -50,4 +50,42 @@
|
|
|
50
50
|
transform: scaleX(1);
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
|
+
|
|
54
|
+
// A stack rises as one bar and rounds only its outer end, keeping seams flush.
|
|
55
|
+
&--stacked :global(.visx-bar-group) rect {
|
|
56
|
+
rx: 0;
|
|
57
|
+
animation: none;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
&--stacked :global(.visx-bar-group) rect[data-stack-end="top"] {
|
|
61
|
+
clip-path: inset(0 round var(--a8c-charts-border-radius-bar-chart) var(--a8c-charts-border-radius-bar-chart) 0 0);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
&--stacked :global(.visx-bar-group) rect[data-stack-end="right"] {
|
|
65
|
+
clip-path: inset(0 round 0 var(--a8c-charts-border-radius-bar-chart) var(--a8c-charts-border-radius-bar-chart) 0);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
&--stacked :global(.visx-bar-group) rect[data-stack-end="bottom"] {
|
|
69
|
+
clip-path: inset(0 round 0 0 var(--a8c-charts-border-radius-bar-chart) var(--a8c-charts-border-radius-bar-chart));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
&--stacked :global(.visx-bar-group) rect[data-stack-end="left"] {
|
|
73
|
+
clip-path: inset(0 round var(--a8c-charts-border-radius-bar-chart) 0 0 var(--a8c-charts-border-radius-bar-chart));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// A stack grows from the zero line, set inline as `transform-origin`;
|
|
77
|
+
// with negatives, the group box's own edge is not that line.
|
|
78
|
+
&--stacked#{&}--animated :global(.visx-bar-group) {
|
|
79
|
+
transform-box: view-box;
|
|
80
|
+
animation:
|
|
81
|
+
rise var(--a8c-charts-motion-duration-entrance)
|
|
82
|
+
var(--a8c-charts-motion-easing-entrance) both;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
&--stacked#{&}--animated-horizontal :global(.visx-bar-group) {
|
|
86
|
+
transform-box: view-box;
|
|
87
|
+
animation:
|
|
88
|
+
stretch var(--a8c-charts-motion-duration-entrance)
|
|
89
|
+
var(--a8c-charts-motion-easing-entrance) both;
|
|
90
|
+
}
|
|
53
91
|
}
|
|
@@ -27,7 +27,8 @@ import { ChartInstanceContext } from '../private/chart-instance-context';
|
|
|
27
27
|
import { ChartLayout } from '../private/chart-layout';
|
|
28
28
|
import { formatReading, isInvalidReading } from '../private/readings';
|
|
29
29
|
import { getAllHiddenMessage, SvgEmptyState } from '../private/svg-empty-state';
|
|
30
|
-
import { hasOnlyWholeNumbers,
|
|
30
|
+
import { getTickDecimals, hasOnlyWholeNumbers, TickDecimals } from '../private/tick-decimals';
|
|
31
|
+
import { useCompactWhenShort } from '../private/use-compact-when-short';
|
|
31
32
|
import { withResponsive } from '../private/with-responsive';
|
|
32
33
|
import plotStyles from '../private/xy-plot/xy-plot.module.scss';
|
|
33
34
|
import styles from './bar-chart.module.scss';
|
|
@@ -45,10 +46,11 @@ import {
|
|
|
45
46
|
isBarRendered,
|
|
46
47
|
SlotTooltipMembers,
|
|
47
48
|
useBarStops,
|
|
48
|
-
|
|
49
|
+
useBarSlots,
|
|
49
50
|
getSelectedSlot,
|
|
50
51
|
isSameCategory,
|
|
51
52
|
membersAt,
|
|
53
|
+
getDrawnSeries,
|
|
52
54
|
} from './private';
|
|
53
55
|
import { useBarColors } from './use-bar-colors';
|
|
54
56
|
import { useBarLegendItems } from './use-bar-legend-items';
|
|
@@ -133,10 +135,37 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
133
135
|
animation,
|
|
134
136
|
children,
|
|
135
137
|
gap = 'md',
|
|
138
|
+
compactWhenShort = false,
|
|
136
139
|
onPointerDown,
|
|
137
140
|
onPointerUp,
|
|
138
141
|
onDatumActivate,
|
|
139
142
|
} ) => {
|
|
143
|
+
if ( layout === 'stacked' && showZeroValues ) {
|
|
144
|
+
warnOnce(
|
|
145
|
+
'bar-chart-stacked-zero-values',
|
|
146
|
+
'BarChart: showZeroValues has no effect in a stacked layout.'
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
if ( layout === 'stacked' && data?.some( series => series.options?.type === 'comparison' ) ) {
|
|
150
|
+
warnOnce(
|
|
151
|
+
'bar-chart-stacked-comparison',
|
|
152
|
+
'BarChart: a stacked layout does not draw comparison series yet.'
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
const valueScaleOptions = orientation === 'horizontal' ? options.xScale : options.yScale;
|
|
156
|
+
if ( layout === 'stacked' && valueScaleOptions?.type && valueScaleOptions.type !== 'linear' ) {
|
|
157
|
+
warnOnce(
|
|
158
|
+
`bar-chart-stacked-${ valueScaleOptions.type }-scale`,
|
|
159
|
+
`BarChart: a stacked layout draws on a linear value scale, so the ${ valueScaleOptions.type } scale is ignored.`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
if ( layout === 'stacked' && valueScaleOptions?.zero === false && ! valueScaleOptions.domain ) {
|
|
163
|
+
warnOnce(
|
|
164
|
+
'bar-chart-stacked-zero',
|
|
165
|
+
'BarChart: every stack starts at zero, so zero: false has no effect in a stacked layout.'
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
140
169
|
if ( ! withTooltips && ( withBandHighlight || onBandHighlightChange ) ) {
|
|
141
170
|
warnOnce(
|
|
142
171
|
'bar-chart-band-highlight-without-tooltips',
|
|
@@ -150,6 +179,7 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
150
179
|
const legendComparisonSeriesItems = legend.comparisonSeriesItems;
|
|
151
180
|
const horizontal = orientation === 'horizontal';
|
|
152
181
|
const isOverlap = layout === 'overlap';
|
|
182
|
+
const isStacked = layout === 'stacked';
|
|
153
183
|
const chartId = useChartId( providedChartId );
|
|
154
184
|
const hiddenSeries = useDefaultHiddenSeries( chartId, defaultHiddenSeries );
|
|
155
185
|
const isSeriesVisible = useCallback(
|
|
@@ -158,12 +188,17 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
158
188
|
);
|
|
159
189
|
const theme = useXYChartTheme( data );
|
|
160
190
|
|
|
161
|
-
const
|
|
191
|
+
const transformedData = useChartDataTransform( data );
|
|
192
|
+
// A stacked layout does not draw comparisons yet, so they must not shade a primary's color or join the legend either.
|
|
193
|
+
const dataSorted = useMemo(
|
|
194
|
+
() => getDrawnSeries( transformedData, layout ),
|
|
195
|
+
[ layout, transformedData ]
|
|
196
|
+
);
|
|
162
197
|
|
|
163
198
|
// Transform data to add a small value for zero bars to make them visible
|
|
164
199
|
// For vertical bars, height determines bar pixel height; for horizontal bars, width does
|
|
165
200
|
const dataWithVisibleZeros = useZeroValueDisplay( dataSorted, {
|
|
166
|
-
enabled: showZeroValues,
|
|
201
|
+
enabled: showZeroValues && ! isStacked,
|
|
167
202
|
valueAxisLength: horizontal ? width : height,
|
|
168
203
|
} );
|
|
169
204
|
|
|
@@ -206,35 +241,24 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
206
241
|
[ dataSorted, isSeriesRendered ]
|
|
207
242
|
);
|
|
208
243
|
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
options,
|
|
213
|
-
isSeriesRendered
|
|
244
|
+
const { isCompact, compactMargin } = useCompactWhenShort(
|
|
245
|
+
compactWhenShort && ! horizontal,
|
|
246
|
+
height
|
|
214
247
|
);
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const handleContentHeightChange = useCallback(
|
|
230
|
-
( contentHeight: number ) => {
|
|
231
|
-
const chartHeight = contentHeight > 0 ? contentHeight : height;
|
|
232
|
-
setMeasuredChartHeight( chartHeight );
|
|
233
|
-
},
|
|
234
|
-
[ height ]
|
|
248
|
+
const resolvedOptions = useMemo(
|
|
249
|
+
() =>
|
|
250
|
+
isCompact
|
|
251
|
+
? {
|
|
252
|
+
...options,
|
|
253
|
+
axis: {
|
|
254
|
+
...options.axis,
|
|
255
|
+
x: { ...options.axis?.x, display: false },
|
|
256
|
+
y: { ...options.axis?.y, display: false },
|
|
257
|
+
},
|
|
258
|
+
}
|
|
259
|
+
: options,
|
|
260
|
+
[ isCompact, options ]
|
|
235
261
|
);
|
|
236
|
-
const [ selectedIndex, setSelectedIndex ] = useState< number | undefined >( undefined );
|
|
237
|
-
const [ isNavigating, setIsNavigating ] = useState( false );
|
|
238
262
|
|
|
239
263
|
// Add visibility information from the shared legend state.
|
|
240
264
|
const seriesWithVisibility = useMemo(
|
|
@@ -261,7 +285,39 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
261
285
|
[ seriesWithVisibility ]
|
|
262
286
|
);
|
|
263
287
|
|
|
264
|
-
const
|
|
288
|
+
const barSlots = useBarSlots( layout, seriesWithVisibility );
|
|
289
|
+
|
|
290
|
+
const chartOptions = useBarChartOptions(
|
|
291
|
+
dataWithVisibleZeros,
|
|
292
|
+
horizontal,
|
|
293
|
+
resolvedOptions,
|
|
294
|
+
isSeriesRendered,
|
|
295
|
+
barSlots?.kind === 'stacked' ? barSlots.stack : undefined
|
|
296
|
+
);
|
|
297
|
+
const valueAxis = horizontal ? chartOptions.axis.x : chartOptions.axis.y;
|
|
298
|
+
const callerValueDomain = horizontal ? options.xScale?.domain : options.yScale?.domain;
|
|
299
|
+
const valueTickDecimals = getTickDecimals( hasWholeNumberValues, chartOptions.valueTickDecimals );
|
|
300
|
+
const tickDecimalsProps = {
|
|
301
|
+
axis: horizontal ? ( 'x' as const ) : ( 'y' as const ),
|
|
302
|
+
numTicks: valueAxis.numTicks,
|
|
303
|
+
decimals: ! valueAxis.tickValues && ! callerValueDomain ? valueTickDecimals : undefined,
|
|
304
|
+
};
|
|
305
|
+
const defaultMargin = useChartMargin( height, chartOptions, dataSorted, theme, horizontal );
|
|
306
|
+
const chartRef = useRef< HTMLDivElement >( null );
|
|
307
|
+
|
|
308
|
+
// Process children for composition API (Legend, etc.)
|
|
309
|
+
const { legendChildren, nonLegendChildren } = useChartChildren( children, 'BarChart' );
|
|
310
|
+
const [ measuredChartHeight, setMeasuredChartHeight ] = useState< number | undefined >();
|
|
311
|
+
|
|
312
|
+
const handleContentHeightChange = useCallback(
|
|
313
|
+
( contentHeight: number ) => {
|
|
314
|
+
const chartHeight = contentHeight > 0 ? contentHeight : height;
|
|
315
|
+
setMeasuredChartHeight( chartHeight );
|
|
316
|
+
},
|
|
317
|
+
[ height ]
|
|
318
|
+
);
|
|
319
|
+
const [ selectedIndex, setSelectedIndex ] = useState< number | undefined >( undefined );
|
|
320
|
+
const [ isNavigating, setIsNavigating ] = useState( false );
|
|
265
321
|
|
|
266
322
|
const primaryKeys = useMemo(
|
|
267
323
|
() => primaryEntries.map( ( { series } ) => series.label ),
|
|
@@ -269,7 +325,7 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
269
325
|
);
|
|
270
326
|
|
|
271
327
|
// The keyboard index space, the highlight and `XYChartTooltip` must share these stops, or the tooltip's datum diverges from the highlighted bar.
|
|
272
|
-
const stops = useBarStops(
|
|
328
|
+
const stops = useBarStops( barSlots, primaryEntries );
|
|
273
329
|
const { barAt, stopOf } = stops;
|
|
274
330
|
|
|
275
331
|
const activateSelectedBar = useCallback(
|
|
@@ -305,8 +361,8 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
305
361
|
);
|
|
306
362
|
|
|
307
363
|
const comparisonEntries = useMemo( () => {
|
|
308
|
-
// An overlap layout draws its comparisons as slotted bars, not
|
|
309
|
-
if (
|
|
364
|
+
// An overlap layout draws its comparisons as slotted bars, and a stacked one does not draw them yet.
|
|
365
|
+
if ( barSlots ) {
|
|
310
366
|
return [];
|
|
311
367
|
}
|
|
312
368
|
|
|
@@ -333,7 +389,7 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
333
389
|
entries.push( { series, index, primaryKey: primary.label } );
|
|
334
390
|
} );
|
|
335
391
|
return entries;
|
|
336
|
-
}, [
|
|
392
|
+
}, [ barSlots, seriesWithVisibility, primaryEntries, primaryKeys ] );
|
|
337
393
|
|
|
338
394
|
// Comparison widthFactor (how much wider the shadow is than the primary) drives both the
|
|
339
395
|
// shadow width (in ComparisonBars) and the primary narrowing.
|
|
@@ -429,6 +485,45 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
429
485
|
[ chartOptions.tooltip, dataSorted ]
|
|
430
486
|
);
|
|
431
487
|
|
|
488
|
+
// Lists the stack last series first, a null reading as "No data", then the total.
|
|
489
|
+
const renderStackedTooltip = useCallback(
|
|
490
|
+
( { tooltipData }: RenderTooltipParams< DataPointDate > ) => {
|
|
491
|
+
const nearestDatum = tooltipData?.nearestDatum?.datum;
|
|
492
|
+
if ( ! nearestDatum || barSlots?.kind !== 'stacked' ) return null;
|
|
493
|
+
|
|
494
|
+
const datumByKey = tooltipData?.datumByKey ?? {};
|
|
495
|
+
const categoryLabel = chartOptions.tooltip.labelFormatter(
|
|
496
|
+
nearestDatum.label || ( nearestDatum.date ? nearestDatum.date.getTime() : 0 ),
|
|
497
|
+
0,
|
|
498
|
+
[]
|
|
499
|
+
);
|
|
500
|
+
|
|
501
|
+
const rows = [ ...barSlots.entries ].reverse().flatMap( ( { series } ) => {
|
|
502
|
+
const datum = datumByKey[ series.label ]?.datum;
|
|
503
|
+
return datum ? [ { label: series.label, datum } ] : [];
|
|
504
|
+
} );
|
|
505
|
+
const readings = rows.filter( ( { datum } ) => isBarRendered( datum ) ).length;
|
|
506
|
+
const [ negative, positive ] = barSlots.stack.extentAt( nearestDatum );
|
|
507
|
+
|
|
508
|
+
return (
|
|
509
|
+
<div className={ styles[ 'bar-chart__tooltip' ] }>
|
|
510
|
+
<div className={ styles[ 'bar-chart__tooltip-header' ] }>{ categoryLabel }</div>
|
|
511
|
+
{ rows.map( ( { label, datum } ) => (
|
|
512
|
+
<Fragment key={ label }>
|
|
513
|
+
{ renderTooltipRow( label, formatReading( datum.value ) ) }
|
|
514
|
+
</Fragment>
|
|
515
|
+
) ) }
|
|
516
|
+
{ readings > 1 &&
|
|
517
|
+
renderTooltipRow(
|
|
518
|
+
__( 'Total', 'jetpack-charts' ),
|
|
519
|
+
formatReading( negative + positive )
|
|
520
|
+
) }
|
|
521
|
+
</div>
|
|
522
|
+
);
|
|
523
|
+
},
|
|
524
|
+
[ chartOptions.tooltip, barSlots ]
|
|
525
|
+
);
|
|
526
|
+
|
|
432
527
|
const renderDefaultTooltip = useCallback(
|
|
433
528
|
( { tooltipData }: RenderTooltipParams< DataPointDate > ) => {
|
|
434
529
|
const nearestDatum = tooltipData?.nearestDatum?.datum;
|
|
@@ -525,35 +620,40 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
525
620
|
[ chartId ]
|
|
526
621
|
);
|
|
527
622
|
|
|
528
|
-
const
|
|
529
|
-
() =>
|
|
530
|
-
|
|
623
|
+
const barBandSlots = useMemo(
|
|
624
|
+
() =>
|
|
625
|
+
barSlots && {
|
|
626
|
+
keys: barSlots.slotKeys,
|
|
627
|
+
slotOf: barSlots.slotOf,
|
|
628
|
+
stack: barSlots.kind === 'stacked' ? barSlots.stack : undefined,
|
|
629
|
+
},
|
|
630
|
+
[ barSlots ]
|
|
531
631
|
);
|
|
532
632
|
|
|
533
633
|
const selectedSlot = useMemo(
|
|
534
|
-
() => getSelectedSlot(
|
|
535
|
-
[
|
|
634
|
+
() => getSelectedSlot( barSlots, selectedIndex ),
|
|
635
|
+
[ barSlots, selectedIndex ]
|
|
536
636
|
);
|
|
537
637
|
|
|
538
638
|
const selectedGroup = useMemo( () => {
|
|
539
|
-
const stop =
|
|
639
|
+
const stop = barSlots || selectedIndex === undefined ? undefined : barAt( selectedIndex );
|
|
540
640
|
if ( ! stop ) {
|
|
541
641
|
return undefined;
|
|
542
642
|
}
|
|
543
643
|
return membersAt( primaryEntries, stop.datum, stop.key );
|
|
544
|
-
}, [
|
|
644
|
+
}, [ barSlots, selectedIndex, barAt, primaryEntries ] );
|
|
545
645
|
|
|
546
646
|
const selectedMembers = selectedSlot?.members ?? selectedGroup;
|
|
547
647
|
|
|
548
648
|
const selectedBar = useMemo( (): SelectedBar | undefined => {
|
|
549
|
-
if (
|
|
649
|
+
if ( barSlots ) {
|
|
550
650
|
return selectedSlot;
|
|
551
651
|
}
|
|
552
652
|
const bar = selectedIndex === undefined ? undefined : barAt( selectedIndex );
|
|
553
653
|
return bar && isBarRendered( bar.datum )
|
|
554
654
|
? { data: [ bar.datum ], slotKey: bar.key, thickness: 1 }
|
|
555
655
|
: undefined;
|
|
556
|
-
}, [
|
|
656
|
+
}, [ barSlots, selectedSlot, selectedIndex, barAt ] );
|
|
557
657
|
|
|
558
658
|
// Validate data first
|
|
559
659
|
const error = validateData( dataSorted );
|
|
@@ -583,10 +683,10 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
583
683
|
return <div className={ clsx( 'bar-chart', styles[ 'bar-chart' ] ) }>{ error }</div>;
|
|
584
684
|
}
|
|
585
685
|
|
|
586
|
-
const gridVisibility = gridVisibilityProp ?? chartOptions.gridVisibility;
|
|
686
|
+
const gridVisibility = isCompact ? 'none' : ( gridVisibilityProp ?? chartOptions.gridVisibility );
|
|
587
687
|
|
|
588
688
|
const legendPosition = legend.position ?? 'bottom';
|
|
589
|
-
const legendElement = showLegend && (
|
|
689
|
+
const legendElement = showLegend && ! isCompact && (
|
|
590
690
|
<Legend
|
|
591
691
|
orientation={ legend.orientation ?? 'horizontal' }
|
|
592
692
|
position={ legendPosition }
|
|
@@ -614,12 +714,13 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
614
714
|
<ChartLayout
|
|
615
715
|
legendPosition={ legendPosition }
|
|
616
716
|
legendElement={ legendElement }
|
|
617
|
-
legendChildren={ legendChildren }
|
|
717
|
+
legendChildren={ isCompact ? [] : legendChildren }
|
|
618
718
|
gap={ gap }
|
|
619
719
|
className={ clsx(
|
|
620
720
|
'bar-chart',
|
|
621
721
|
styles[ 'bar-chart' ],
|
|
622
722
|
{
|
|
723
|
+
[ styles[ 'bar-chart--stacked' ] ]: isStacked,
|
|
623
724
|
[ styles[ `bar-chart--animated${ horizontal ? '-horizontal' : '' }` ] ]:
|
|
624
725
|
animation && ! prefersReducedMotion,
|
|
625
726
|
},
|
|
@@ -654,6 +755,7 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
654
755
|
height={ chartHeight }
|
|
655
756
|
margin={ {
|
|
656
757
|
...defaultMargin,
|
|
758
|
+
...compactMargin,
|
|
657
759
|
...margin,
|
|
658
760
|
} }
|
|
659
761
|
xScale={ xScale }
|
|
@@ -670,7 +772,7 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
670
772
|
) }
|
|
671
773
|
|
|
672
774
|
{ ! allSeriesHidden && (
|
|
673
|
-
<
|
|
775
|
+
<TickDecimals { ...tickDecimalsProps }>
|
|
674
776
|
{ valueTicks => (
|
|
675
777
|
<>
|
|
676
778
|
{ /* Visx forwards tickValues to its grid primitives but omits it from GridProps. */ }
|
|
@@ -696,7 +798,7 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
696
798
|
/>
|
|
697
799
|
</>
|
|
698
800
|
) }
|
|
699
|
-
</
|
|
801
|
+
</TickDecimals>
|
|
700
802
|
) }
|
|
701
803
|
|
|
702
804
|
{ withPatterns && (
|
|
@@ -719,7 +821,7 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
719
821
|
width={ width }
|
|
720
822
|
height={ chartHeight }
|
|
721
823
|
>
|
|
722
|
-
{ getAllHiddenMessage( legendInteractive, 'series' ) }
|
|
824
|
+
{ getAllHiddenMessage( legendInteractive && ! isCompact, 'series' ) }
|
|
723
825
|
</SvgEmptyState>
|
|
724
826
|
) : null }
|
|
725
827
|
|
|
@@ -740,7 +842,7 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
740
842
|
|
|
741
843
|
<BarLayer
|
|
742
844
|
chartId={ chartId }
|
|
743
|
-
|
|
845
|
+
barSlots={ barSlots }
|
|
744
846
|
selectedBar={ selectedBar }
|
|
745
847
|
primaryEntries={ primaryEntries }
|
|
746
848
|
primaryKeys={ primaryKeys }
|
|
@@ -754,8 +856,8 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
754
856
|
{ /* Do not reorder: for one key the last showTooltip wins, so this must run after the primary series. */ }
|
|
755
857
|
{ ( withTooltips || onPointerDown || onPointerUp ) && (
|
|
756
858
|
<BandTooltip
|
|
757
|
-
keys={
|
|
758
|
-
slots={
|
|
859
|
+
keys={ barSlots?.keys ?? primaryKeys }
|
|
860
|
+
slots={ barBandSlots }
|
|
759
861
|
groupPadding={ groupPadding }
|
|
760
862
|
withTooltips={ withTooltips }
|
|
761
863
|
onPointerDown={ onPointerDown }
|
|
@@ -768,7 +870,7 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
768
870
|
visx collapses the domain and the axes render squished at the top. Drop them
|
|
769
871
|
while the empty state stands in. */ }
|
|
770
872
|
{ ! allSeriesHidden && (
|
|
771
|
-
<
|
|
873
|
+
<TickDecimals { ...tickDecimalsProps }>
|
|
772
874
|
{ valueTicks => (
|
|
773
875
|
<>
|
|
774
876
|
{ chartOptions.axis.x.display !== false && (
|
|
@@ -789,7 +891,7 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
789
891
|
) }
|
|
790
892
|
</>
|
|
791
893
|
) }
|
|
792
|
-
</
|
|
894
|
+
</TickDecimals>
|
|
793
895
|
) }
|
|
794
896
|
|
|
795
897
|
{ withTooltips && selectedMembers && (
|
|
@@ -805,6 +907,7 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
805
907
|
snapTooltipToDatumY
|
|
806
908
|
renderTooltip={
|
|
807
909
|
renderTooltip ||
|
|
910
|
+
( isStacked && renderStackedTooltip ) ||
|
|
808
911
|
( isOverlap ? renderOverlapTooltip : renderDefaultTooltip )
|
|
809
912
|
}
|
|
810
913
|
selectedIndex={ selectedIndex }
|