@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.
Files changed (61) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/TOKENS.md +6 -3
  3. package/dist/index.cjs +212 -95
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.css +10 -9
  6. package/dist/index.d.cts +60 -20
  7. package/dist/index.d.ts +60 -20
  8. package/dist/index.js +212 -97
  9. package/dist/index.js.map +1 -1
  10. package/package.json +1 -1
  11. package/src/charts/area-chart/area-chart.tsx +2 -2
  12. package/src/charts/area-chart/test/area-chart.test.tsx +1 -1
  13. package/src/charts/area-chart/test/tooltip-resolution-branches.test.tsx +1 -1
  14. package/src/charts/bar-chart/bar-chart.module.scss +7 -0
  15. package/src/charts/bar-chart/bar-chart.tsx +20 -14
  16. package/src/charts/bar-chart/private/comparison-bars.tsx +1 -0
  17. package/src/charts/bar-chart/test/bar-chart.test.tsx +25 -0
  18. package/src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx +1 -1
  19. package/src/charts/conversion-funnel-chart/test/conversion-funnel-chart.test.tsx +9 -0
  20. package/src/charts/heatmap-chart/heatmap-chart.module.scss +6 -4
  21. package/src/charts/heatmap-chart/heatmap-chart.tsx +39 -29
  22. package/src/charts/heatmap-chart/private/heatmap-context.ts +2 -2
  23. package/src/charts/heatmap-chart/private/heatmap-legend.tsx +2 -2
  24. package/src/charts/heatmap-chart/private/heatmap-scale.ts +74 -0
  25. package/src/charts/heatmap-chart/private/index.ts +2 -1
  26. package/src/charts/heatmap-chart/private/use-heatmap-colors.ts +24 -6
  27. package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +99 -7
  28. package/src/charts/heatmap-chart/test/heatmap-scale.test.ts +78 -0
  29. package/src/charts/heatmap-chart/test/use-heatmap-colors.test.ts +18 -6
  30. package/src/charts/heatmap-chart/types.ts +4 -3
  31. package/src/charts/line-chart/line-chart.tsx +2 -2
  32. package/src/charts/pie-chart/pie-chart.tsx +4 -4
  33. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +4 -4
  34. package/src/charts/pie-semi-circle-chart/test/pie-semi-circle-chart.test.tsx +1 -1
  35. package/src/charts/private/test/label-text-color.test.ts +6 -8
  36. package/src/components/tooltip/base-tooltip.tsx +31 -32
  37. package/src/components/tooltip/index.ts +3 -1
  38. package/src/components/tooltip/private/bounded-tooltip.tsx +15 -15
  39. package/src/components/tooltip/private/label-value-content.tsx +8 -0
  40. package/src/components/tooltip/private/xy-chart-tooltip-overlay.tsx +336 -0
  41. package/src/components/tooltip/test/tool-tip.test.tsx +8 -6
  42. package/src/components/tooltip/test/tooltip-box.test.tsx +53 -0
  43. package/src/components/tooltip/test/xy-chart-tooltip-overlay.test.tsx +445 -0
  44. package/src/components/tooltip/test/xy-chart-tooltip.test.tsx +282 -365
  45. package/src/components/tooltip/{base-tooltip.module.scss → tooltip-box.module.scss} +5 -8
  46. package/src/components/tooltip/tooltip-box.tsx +43 -0
  47. package/src/components/tooltip/types.ts +7 -1
  48. package/src/components/tooltip/xy-chart-tooltip.tsx +410 -300
  49. package/src/hooks/test/use-chart-margin.test.tsx +8 -4
  50. package/src/hooks/use-chart-margin.tsx +21 -18
  51. package/src/index.ts +8 -3
  52. package/src/providers/chart-context/global-charts-provider.tsx +5 -14
  53. package/src/providers/chart-context/hooks/use-default-hidden-series.ts +1 -1
  54. package/src/providers/chart-context/private/catalog-pointers.ts +1 -0
  55. package/src/providers/chart-context/private/resolve-opaque-hex.ts +16 -0
  56. package/src/providers/chart-context/test/use-default-hidden-series.test.tsx +1 -1
  57. package/src/styles/chart-scope.scss +1 -0
  58. package/src/test-utils/zoned-parsing-suite.ts +0 -10
  59. package/src/utils/test/date-formatting.test.ts +0 -10
  60. package/src/components/tooltip/accessible-tooltip.tsx +0 -433
  61. 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, baseTheme )
294
+ useChartMargin( 300, datedXOptions( { display: false, orientation } ), data, theme )
290
295
  );
291
296
 
292
297
  expect( mockGetEdgeTickWidths ).not.toHaveBeenCalled();
293
- expect( result.current.right ).toBe( 20 );
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 to use when the X-axis is rendered at the top.
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 DEFAULT_BOTTOM_FOR_TOP_AXIS = 10;
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
- // Dynamically compute X-axis margin (bottom by default, or top if orientation is 'top').
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 { BaseTooltipProps, TooltipData, TooltipProps } from './components/tooltip';
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, wrapperRef.current ) ?? BACKGROUND_FALLBACK;
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, wrapperRef.current ) )
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
- // the accessible tooltip watches, which closes a tooltip mid-navigation.
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 the accessible tooltip's
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(