@automattic/charts 4.7.0 → 4.8.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 +16 -0
- package/TOKENS.md +6 -3
- package/dist/index.cjs +212 -95
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +10 -9
- package/dist/index.d.cts +60 -20
- package/dist/index.d.ts +60 -20
- package/dist/index.js +212 -97
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/charts/area-chart/area-chart.tsx +2 -2
- package/src/charts/area-chart/test/area-chart.test.tsx +1 -1
- package/src/charts/area-chart/test/tooltip-resolution-branches.test.tsx +1 -1
- package/src/charts/bar-chart/bar-chart.module.scss +7 -0
- package/src/charts/bar-chart/bar-chart.tsx +20 -14
- package/src/charts/bar-chart/private/comparison-bars.tsx +1 -0
- package/src/charts/bar-chart/test/bar-chart.test.tsx +25 -0
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx +1 -1
- package/src/charts/conversion-funnel-chart/test/conversion-funnel-chart.test.tsx +9 -0
- package/src/charts/heatmap-chart/heatmap-chart.module.scss +6 -4
- package/src/charts/heatmap-chart/heatmap-chart.tsx +39 -29
- package/src/charts/heatmap-chart/private/heatmap-context.ts +2 -2
- package/src/charts/heatmap-chart/private/heatmap-legend.tsx +2 -2
- package/src/charts/heatmap-chart/private/heatmap-scale.ts +74 -0
- package/src/charts/heatmap-chart/private/index.ts +2 -1
- package/src/charts/heatmap-chart/private/use-heatmap-colors.ts +24 -6
- package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +99 -7
- package/src/charts/heatmap-chart/test/heatmap-scale.test.ts +78 -0
- package/src/charts/heatmap-chart/test/use-heatmap-colors.test.ts +18 -6
- package/src/charts/heatmap-chart/types.ts +4 -3
- package/src/charts/line-chart/line-chart.tsx +2 -2
- package/src/charts/pie-chart/pie-chart.tsx +4 -4
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +4 -4
- package/src/charts/pie-semi-circle-chart/test/pie-semi-circle-chart.test.tsx +1 -1
- package/src/charts/private/test/label-text-color.test.ts +6 -8
- package/src/components/tooltip/base-tooltip.tsx +31 -32
- package/src/components/tooltip/index.ts +3 -1
- package/src/components/tooltip/private/bounded-tooltip.tsx +15 -15
- package/src/components/tooltip/private/label-value-content.tsx +8 -0
- package/src/components/tooltip/private/xy-chart-tooltip-overlay.tsx +336 -0
- package/src/components/tooltip/test/tool-tip.test.tsx +8 -6
- package/src/components/tooltip/test/tooltip-box.test.tsx +53 -0
- package/src/components/tooltip/test/xy-chart-tooltip-overlay.test.tsx +445 -0
- package/src/components/tooltip/test/xy-chart-tooltip.test.tsx +282 -365
- package/src/components/tooltip/{base-tooltip.module.scss → tooltip-box.module.scss} +5 -8
- package/src/components/tooltip/tooltip-box.tsx +43 -0
- package/src/components/tooltip/types.ts +7 -1
- package/src/components/tooltip/xy-chart-tooltip.tsx +410 -300
- package/src/hooks/test/use-chart-margin.test.tsx +8 -4
- package/src/hooks/use-chart-margin.tsx +21 -18
- package/src/index.ts +8 -3
- package/src/providers/chart-context/global-charts-provider.tsx +5 -14
- package/src/providers/chart-context/hooks/use-default-hidden-series.ts +1 -1
- package/src/providers/chart-context/private/catalog-pointers.ts +1 -0
- package/src/providers/chart-context/private/resolve-opaque-hex.ts +16 -0
- package/src/providers/chart-context/test/use-default-hidden-series.test.tsx +1 -1
- package/src/styles/chart-scope.scss +1 -0
- package/src/test-utils/zoned-parsing-suite.ts +0 -10
- package/src/utils/test/date-formatting.test.ts +0 -10
- package/src/components/tooltip/accessible-tooltip.tsx +0 -433
- package/src/components/tooltip/test/accessible-tooltip.test.tsx +0 -331
|
@@ -282,16 +282,20 @@ describe( 'useChartMargin', () => {
|
|
|
282
282
|
expect( result.current.left ).toBe( 60 );
|
|
283
283
|
} );
|
|
284
284
|
|
|
285
|
-
it( 'reserves nothing for a hidden x axis',
|
|
285
|
+
it.each( [ 'bottom', 'top' ] )( 'reserves nothing for a hidden %s x axis', orientation => {
|
|
286
286
|
mockGetEdgeTickWidths.mockReturnValue( { first: 120, last: 60 } );
|
|
287
|
+
const largeLabels = { axisLabel: { fontSize: 16 }, tickLength: 10 } as unknown as never;
|
|
288
|
+
const theme = {
|
|
289
|
+
...baseTheme,
|
|
290
|
+
axisStyles: { ...baseTheme.axisStyles, x: { top: largeLabels, bottom: largeLabels } },
|
|
291
|
+
} as XYChartTheme;
|
|
287
292
|
|
|
288
293
|
const { result } = renderHook( () =>
|
|
289
|
-
useChartMargin( 300, datedXOptions( { display: false } ), data,
|
|
294
|
+
useChartMargin( 300, datedXOptions( { display: false, orientation } ), data, theme )
|
|
290
295
|
);
|
|
291
296
|
|
|
292
297
|
expect( mockGetEdgeTickWidths ).not.toHaveBeenCalled();
|
|
293
|
-
expect( result.current
|
|
294
|
-
expect( result.current.left ).toBe( 51 );
|
|
298
|
+
expect( result.current ).toEqual( { top: 10, right: 20, bottom: 10, left: 51 } );
|
|
295
299
|
} );
|
|
296
300
|
} );
|
|
297
301
|
|
|
@@ -27,10 +27,10 @@ const DEFAULT_MARGIN_BOTTOM = 20;
|
|
|
27
27
|
const DEFAULT_MARGIN_LEFT = 20;
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
* Bottom margin
|
|
30
|
+
* Bottom margin when no X-axis sits below the chart, because it is on top or hidden.
|
|
31
31
|
* We only need a small buffer below the chart in that case.
|
|
32
32
|
*/
|
|
33
|
-
const
|
|
33
|
+
const DEFAULT_BOTTOM_WITHOUT_X_AXIS = 10;
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
36
|
* Fallback font size used when we cannot derive a font size
|
|
@@ -168,23 +168,24 @@ export const useChartMargin = (
|
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
-
//
|
|
172
|
-
// This mirrors Y-axis behavior where margin is based on label size and tick length,
|
|
173
|
-
// but keeps the padding minimal so consumers can control container spacing themselves.
|
|
174
|
-
const xOrientation = options.axis?.x?.orientation === 'top' ? 'top' : 'bottom';
|
|
175
|
-
const { fontSize, tickLength, tickLabelStyle } = getXAxisLabelMetrics( theme, xOrientation );
|
|
176
|
-
const computedXMargin = fontSize + tickLength;
|
|
177
|
-
|
|
178
|
-
if ( xOrientation === 'top' ) {
|
|
179
|
-
defaultMargin.top = Math.max( defaultMargin.top, computedXMargin );
|
|
180
|
-
defaultMargin.bottom = DEFAULT_BOTTOM_FOR_TOP_AXIS;
|
|
181
|
-
} else {
|
|
182
|
-
defaultMargin.bottom = Math.max( defaultMargin.bottom, computedXMargin );
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// An X-axis label is centered on its tick, so the ones at either end of the
|
|
186
|
-
// scale hang half their width outside the plot area and clip at the SVG edge.
|
|
171
|
+
// A hidden x axis reserves nothing either, whichever side it sits on.
|
|
187
172
|
if ( options.axis?.x?.display !== false ) {
|
|
173
|
+
// Dynamically compute X-axis margin (bottom by default, or top if orientation is 'top').
|
|
174
|
+
// This mirrors Y-axis behavior where margin is based on label size and tick length,
|
|
175
|
+
// but keeps the padding minimal so consumers can control container spacing themselves.
|
|
176
|
+
const xOrientation = options.axis?.x?.orientation === 'top' ? 'top' : 'bottom';
|
|
177
|
+
const { fontSize, tickLength, tickLabelStyle } = getXAxisLabelMetrics( theme, xOrientation );
|
|
178
|
+
const computedXMargin = fontSize + tickLength;
|
|
179
|
+
|
|
180
|
+
if ( xOrientation === 'top' ) {
|
|
181
|
+
defaultMargin.top = Math.max( defaultMargin.top, computedXMargin );
|
|
182
|
+
defaultMargin.bottom = DEFAULT_BOTTOM_WITHOUT_X_AXIS;
|
|
183
|
+
} else {
|
|
184
|
+
defaultMargin.bottom = Math.max( defaultMargin.bottom, computedXMargin );
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// An X-axis label is centered on its tick, so the ones at either end of the
|
|
188
|
+
// scale hang half their width outside the plot area and clip at the SVG edge.
|
|
188
189
|
const { first, last } = getEdgeTickWidths(
|
|
189
190
|
options.axis?.x?.tickValues ?? [],
|
|
190
191
|
options.axis?.x?.tickFormat,
|
|
@@ -193,6 +194,8 @@ export const useChartMargin = (
|
|
|
193
194
|
|
|
194
195
|
defaultMargin.left = Math.max( defaultMargin.left, Math.ceil( first / 2 ) );
|
|
195
196
|
defaultMargin.right = Math.max( defaultMargin.right, Math.ceil( last / 2 ) );
|
|
197
|
+
} else {
|
|
198
|
+
defaultMargin.bottom = DEFAULT_BOTTOM_WITHOUT_X_AXIS;
|
|
196
199
|
}
|
|
197
200
|
|
|
198
201
|
return defaultMargin;
|
package/src/index.ts
CHANGED
|
@@ -21,7 +21,7 @@ export { PieSemiCircleChart, PieSemiCircleChartUnresponsive } from './charts/pie
|
|
|
21
21
|
export { Sparkline, SparklineUnresponsive } from './charts/sparkline';
|
|
22
22
|
|
|
23
23
|
// Components
|
|
24
|
-
export { BaseTooltip } from './components/tooltip';
|
|
24
|
+
export { BaseTooltip, TooltipBox } from './components/tooltip';
|
|
25
25
|
export { Legend, useChartLegendItems } from './components/legend';
|
|
26
26
|
export { TrendIndicator } from './components/trend-indicator';
|
|
27
27
|
|
|
@@ -97,8 +97,13 @@ export type { LineStyles, GridStyles, EventHandlerParams } from '@visx/xychart';
|
|
|
97
97
|
export { useLeaderboardLegendItems } from './charts/leaderboard-chart/hooks';
|
|
98
98
|
|
|
99
99
|
// Previously available via '@automattic/charts/tooltip', '@automattic/charts/legend'
|
|
100
|
-
export { AccessibleTooltip } from './components/tooltip';
|
|
101
|
-
export type {
|
|
100
|
+
export { AccessibleTooltip, XYChartTooltip } from './components/tooltip';
|
|
101
|
+
export type {
|
|
102
|
+
BaseTooltipProps,
|
|
103
|
+
TooltipBoxProps,
|
|
104
|
+
TooltipData,
|
|
105
|
+
TooltipProps,
|
|
106
|
+
} from './components/tooltip';
|
|
102
107
|
export type { LegendProps, BaseLegendProps, ChartLegendOptions } from './components/legend';
|
|
103
108
|
|
|
104
109
|
// Previously available via '@automattic/charts/bar-chart', '@automattic/charts/line-chart', etc.
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { color as d3Color } from '@visx/vendor/d3-color';
|
|
2
1
|
import {
|
|
3
2
|
createContext,
|
|
4
3
|
useCallback,
|
|
@@ -14,6 +13,7 @@ import {
|
|
|
14
13
|
import '../../styles/chart-scope.scss';
|
|
15
14
|
import { CHART_SCOPE_CLASS } from '../../styles/chart-scope-class';
|
|
16
15
|
import {
|
|
16
|
+
createCssVariableResolver,
|
|
17
17
|
getItemShapeStyles,
|
|
18
18
|
getSeriesBarStyles,
|
|
19
19
|
getSeriesLineStyles,
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
LABEL_INVERSE_FALLBACK,
|
|
33
33
|
} from './private/catalog-pointers';
|
|
34
34
|
import { createPaletteGenerator } from './private/palette-generator';
|
|
35
|
+
import { resolveOpaqueHex } from './private/resolve-opaque-hex';
|
|
35
36
|
import { SERIES_PALETTE_POINTERS, SERIES_SLOT_1_FALLBACK } from './private/series-palette';
|
|
36
37
|
import { defaultTheme } from './themes';
|
|
37
38
|
import type { GlobalChartsContextValue, ChartRegistration } from './types';
|
|
@@ -45,17 +46,6 @@ interface ColorCache {
|
|
|
45
46
|
colorAt: ( index: number ) => string;
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
// A see-through color (transparent, or any alpha below 1) says nothing about what it will look like
|
|
49
|
-
// over the chart, so it resolves to null rather than let its RGB leak into the palette.
|
|
50
|
-
const resolveOpaqueHex = ( pointer: string, element: HTMLElement | null ): string | null => {
|
|
51
|
-
const raw = resolveCssVariable( pointer, element );
|
|
52
|
-
if ( ! raw || d3Color( raw )?.opacity !== 1 ) {
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
const hex = normalizeColorToHex( pointer, element, resolveCssVariable );
|
|
56
|
-
return isValidHexColor( hex ) ? hex : null;
|
|
57
|
-
};
|
|
58
|
-
|
|
59
49
|
const PLACEHOLDER_LABEL_COLORS = [ LABEL_FALLBACK, LABEL_INVERSE_FALLBACK ];
|
|
60
50
|
|
|
61
51
|
export const GlobalChartsContext = createContext< GlobalChartsContextValue | null >( null );
|
|
@@ -143,11 +133,12 @@ export const GlobalChartsProvider: FC< GlobalChartsProviderProps > = ( {
|
|
|
143
133
|
}
|
|
144
134
|
}
|
|
145
135
|
|
|
136
|
+
const resolveAtWrapper = createCssVariableResolver( wrapperRef.current );
|
|
146
137
|
const backgroundHex =
|
|
147
|
-
resolveOpaqueHex( CATALOG_POINTERS.background,
|
|
138
|
+
resolveOpaqueHex( CATALOG_POINTERS.background, resolveAtWrapper ) ?? BACKGROUND_FALLBACK;
|
|
148
139
|
// The two roles pie labels choose between on a fill; one left see-through is never painted there.
|
|
149
140
|
const labelColors = [ CATALOG_POINTERS.label, CATALOG_POINTERS.labelInverse ]
|
|
150
|
-
.map( pointer => resolveOpaqueHex( pointer,
|
|
141
|
+
.map( pointer => resolveOpaqueHex( pointer, resolveAtWrapper ) )
|
|
151
142
|
.filter( ( hex ): hex is string => hex !== null );
|
|
152
143
|
|
|
153
144
|
setColorCache( {
|
|
@@ -37,7 +37,7 @@ export const useDefaultHiddenSeries = (
|
|
|
37
37
|
// getHiddenSeries hands out a fresh copy per call, so hold the set still between
|
|
38
38
|
// visibility changes. Charts derive their rendered series from it, and a new
|
|
39
39
|
// identity every render invalidates every memo downstream — including the one
|
|
40
|
-
//
|
|
40
|
+
// `XYChartTooltip` watches, which closes a tooltip mid-navigation.
|
|
41
41
|
return useMemo(
|
|
42
42
|
() => ( shouldUseDefaults ? new Set( labelsRef.current ) : getHiddenSeries( chartId ) ),
|
|
43
43
|
[ shouldUseDefaults, getHiddenSeries, chartId ]
|
|
@@ -24,6 +24,7 @@ export const CATALOG_POINTERS = {
|
|
|
24
24
|
annotation: 'var(--a8c-charts-color-annotation, #1e1e1e)',
|
|
25
25
|
surface: 'var(--a8c-charts-color-surface, #fff)',
|
|
26
26
|
surfaceSecondary: 'var(--a8c-charts-color-surface-secondary, #f4f4f4)',
|
|
27
|
+
track: 'var(--a8c-charts-color-track, #f0f0f0)',
|
|
27
28
|
// Only for `renderMainMetric`, which hands a consumer's own markup something to paint with.
|
|
28
29
|
trendUp: 'var(--a8c-charts-color-trend-up, #008030)',
|
|
29
30
|
trendDown: 'var(--a8c-charts-color-trend-down, #cc1818)',
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { color as d3Color } from '@visx/vendor/d3-color';
|
|
2
|
+
import { isValidHexColor, normalizeColorToHex } from '../../../utils/color-utils';
|
|
3
|
+
|
|
4
|
+
// A see-through color (transparent, or any alpha below 1) says nothing about what it will look like
|
|
5
|
+
// over the chart, so it resolves to null rather than let its RGB stand in for the color.
|
|
6
|
+
export const resolveOpaqueHex = (
|
|
7
|
+
pointer: string,
|
|
8
|
+
resolve: ( value: string ) => string | null
|
|
9
|
+
): string | null => {
|
|
10
|
+
const raw = resolve( pointer );
|
|
11
|
+
if ( ! raw || d3Color( raw )?.opacity !== 1 ) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
const hex = normalizeColorToHex( raw );
|
|
15
|
+
return isValidHexColor( hex ) ? hex : null;
|
|
16
|
+
};
|
|
@@ -177,7 +177,7 @@ describe( 'useDefaultHiddenSeries', () => {
|
|
|
177
177
|
|
|
178
178
|
it( 'keeps the same set across renders until visibility changes', () => {
|
|
179
179
|
// Charts memoize their rendered series off this set. A fresh identity per
|
|
180
|
-
// render invalidates those memos, which re-runs
|
|
180
|
+
// render invalidates those memos, which re-runs `XYChartTooltip`'s
|
|
181
181
|
// effect and closes a tooltip the user is still navigating with.
|
|
182
182
|
const Harness = () => (
|
|
183
183
|
<GlobalChartsProvider>
|
|
@@ -80,6 +80,7 @@
|
|
|
80
80
|
--a8c-charts-motion-duration-entrance: var(--wpds-motion-duration-xl);
|
|
81
81
|
--a8c-charts-motion-easing-entrance: var(--wpds-motion-easing-expressive);
|
|
82
82
|
--a8c-charts-border-radius-bar: var(--wpds-border-radius-md);
|
|
83
|
+
--a8c-charts-border-radius-bar-chart: 0;
|
|
83
84
|
--a8c-charts-border-radius-cell: var(--wpds-border-radius-sm);
|
|
84
85
|
// Pill shape; no WPDS radius reaches it.
|
|
85
86
|
--a8c-charts-border-radius-leaderboard-bar: 9999px;
|
|
@@ -1,15 +1,5 @@
|
|
|
1
1
|
import { parseAsLocalDate } from '../utils/date-parsing';
|
|
2
2
|
|
|
3
|
-
// The `jetpack-js-tools` console guard registers the matcher but ships no types for it.
|
|
4
|
-
declare global {
|
|
5
|
-
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
6
|
-
namespace jest {
|
|
7
|
-
interface Matchers< R > {
|
|
8
|
-
toHaveWarnedWith( ...args: unknown[] ): R;
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
|
|
13
3
|
// Every case asserts an absolute instant, so running the suite from more than one
|
|
14
4
|
// worker zone is what proves the result does not depend on the viewer's.
|
|
15
5
|
const CASES: Array< [ string, string, string ] > = [
|
|
@@ -1,15 +1,5 @@
|
|
|
1
1
|
import { createDateFormatter, createZonedClock, sanitizeFormatting } from '../date-formatting';
|
|
2
2
|
|
|
3
|
-
// The `jetpack-js-tools` console guard registers the matcher but ships no types for it.
|
|
4
|
-
declare global {
|
|
5
|
-
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
6
|
-
namespace jest {
|
|
7
|
-
interface Matchers< R > {
|
|
8
|
-
toHaveWarned(): R;
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
|
|
13
3
|
describe( 'createDateFormatter', () => {
|
|
14
4
|
it( 'renders an invalid date rather than throwing', () => {
|
|
15
5
|
const format = createDateFormatter(
|