@automattic/charts 4.1.1 → 4.3.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 (97) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/TOKENS.md +1 -1
  3. package/dist/index.cjs +1335 -348
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.css +30 -1
  6. package/dist/index.d.cts +129 -8
  7. package/dist/index.d.ts +129 -8
  8. package/dist/index.js +1336 -351
  9. package/dist/index.js.map +1 -1
  10. package/package.json +8 -8
  11. package/src/charts/area-chart/area-chart.tsx +42 -10
  12. package/src/charts/area-chart/private/overlays.tsx +1 -2
  13. package/src/charts/area-chart/private/validate-data.ts +2 -3
  14. package/src/charts/area-chart/test/area-chart.test.tsx +72 -1
  15. package/src/charts/bar-chart/bar-chart.tsx +137 -70
  16. package/src/charts/bar-chart/index.ts +1 -1
  17. package/src/charts/bar-chart/private/band-highlight.tsx +87 -0
  18. package/src/charts/bar-chart/private/band-scale.ts +13 -0
  19. package/src/charts/bar-chart/private/band-tooltip.tsx +138 -0
  20. package/src/charts/bar-chart/private/comparison-bars.tsx +5 -10
  21. package/src/charts/bar-chart/private/index.ts +3 -0
  22. package/src/charts/bar-chart/private/test/band-highlight.test.tsx +135 -0
  23. package/src/charts/bar-chart/private/test/band-tooltip.test.ts +26 -0
  24. package/src/charts/bar-chart/private/test/value-domain.test.ts +100 -0
  25. package/src/charts/bar-chart/private/truncated-tick-component.tsx +1 -1
  26. package/src/charts/bar-chart/private/use-bar-chart-options.ts +13 -40
  27. package/src/charts/bar-chart/private/value-domain.ts +90 -0
  28. package/src/charts/bar-chart/test/bar-chart-pointer-events.test.tsx +11 -23
  29. package/src/charts/bar-chart/test/bar-chart.test.tsx +1058 -19
  30. package/src/charts/bar-chart/types.ts +39 -0
  31. package/src/charts/bar-list-chart/bar-list-chart.tsx +5 -3
  32. package/src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx +1 -1
  33. package/src/charts/conversion-funnel-chart/types.ts +4 -2
  34. package/src/charts/geo-chart/types.ts +4 -2
  35. package/src/charts/heatmap-chart/build-calendar-data.ts +5 -11
  36. package/src/charts/heatmap-chart/build-month-calendar-data.ts +124 -0
  37. package/src/charts/heatmap-chart/heatmap-chart.module.scss +32 -0
  38. package/src/charts/heatmap-chart/heatmap-chart.tsx +191 -114
  39. package/src/charts/heatmap-chart/index.ts +6 -0
  40. package/src/charts/heatmap-chart/private/civil-day.ts +26 -0
  41. package/src/charts/heatmap-chart/private/column-groups.ts +71 -0
  42. package/src/charts/heatmap-chart/private/heatmap-legend.tsx +19 -3
  43. package/src/charts/heatmap-chart/private/index.ts +2 -0
  44. package/src/charts/heatmap-chart/private/keyboard-navigation.ts +201 -0
  45. package/src/charts/heatmap-chart/test/build-calendar-data.test.ts +36 -0
  46. package/src/charts/heatmap-chart/test/build-month-calendar-data.test.ts +194 -0
  47. package/src/charts/heatmap-chart/test/column-groups.test.ts +83 -0
  48. package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +428 -2
  49. package/src/charts/heatmap-chart/test/keyboard-navigation.test.ts +25 -0
  50. package/src/charts/heatmap-chart/test/use-month-calendar-heatmap-data.test.tsx +90 -0
  51. package/src/charts/heatmap-chart/test/zoned-month-calendar-data.test.ts +23 -0
  52. package/src/charts/heatmap-chart/types.ts +59 -3
  53. package/src/charts/heatmap-chart/use-month-calendar-heatmap-data.ts +34 -0
  54. package/src/charts/leaderboard-chart/types.ts +13 -14
  55. package/src/charts/line-chart/line-chart.tsx +149 -36
  56. package/src/charts/line-chart/private/index.ts +1 -0
  57. package/src/charts/line-chart/private/line-chart-annotation.tsx +1 -1
  58. package/src/charts/line-chart/private/line-chart-glyph.tsx +17 -3
  59. package/src/charts/line-chart/private/nearest-pointer-events.tsx +155 -0
  60. package/src/charts/line-chart/test/line-chart.test.tsx +604 -9
  61. package/src/charts/line-chart/types.ts +4 -3
  62. package/src/charts/private/readings.ts +34 -0
  63. package/src/charts/private/test/readings.test.ts +29 -0
  64. package/src/charts/private/test/whole-number-ticks.test.tsx +57 -0
  65. package/src/charts/private/time-axis-options.ts +1 -1
  66. package/src/charts/private/whole-number-ticks.tsx +79 -0
  67. package/src/charts/private/with-responsive/with-responsive.tsx +2 -2
  68. package/src/charts/private/x-zoom/test/x-zoom.test.tsx +1 -3
  69. package/src/charts/private/x-zoom/x-zoom.tsx +1 -2
  70. package/src/components/legend/hooks/test/use-chart-legend-items.test.tsx +124 -0
  71. package/src/components/legend/hooks/use-chart-legend-items.ts +60 -2
  72. package/src/components/legend/private/base-legend.tsx +37 -22
  73. package/src/components/legend/test/legend.test.tsx +84 -0
  74. package/src/components/legend/types.ts +2 -0
  75. package/src/components/legend/utils/index.ts +0 -1
  76. package/src/components/tooltip/accessible-tooltip.tsx +128 -41
  77. package/src/components/tooltip/base-tooltip.module.scss +6 -1
  78. package/src/components/tooltip/private/bounded-tooltip.tsx +12 -6
  79. package/src/components/tooltip/test/accessible-tooltip.test.tsx +128 -0
  80. package/src/components/tooltip/test/bounded-tooltip.test.tsx +17 -0
  81. package/src/components/tooltip/test/xy-chart-tooltip.test.tsx +7 -0
  82. package/src/components/tooltip/xy-chart-tooltip.tsx +9 -10
  83. package/src/hooks/test/use-chart-margin.test.tsx +35 -0
  84. package/src/hooks/test/use-element-size.test.tsx +13 -13
  85. package/src/hooks/use-chart-margin.tsx +7 -2
  86. package/src/hooks/use-xychart-theme.ts +1 -1
  87. package/src/index.ts +7 -1
  88. package/src/providers/chart-context/global-charts-provider.tsx +1 -1
  89. package/src/providers/chart-context/test/chart-context.test.tsx +4 -4
  90. package/src/providers/chart-context/themes.ts +1 -0
  91. package/src/styles/test/chart-scope.test.ts +1 -1
  92. package/src/types.ts +14 -13
  93. package/src/utils/format-metric-value.ts +4 -4
  94. package/src/utils/get-edge-tick-widths.ts +1 -1
  95. package/src/utils/get-styles.ts +1 -1
  96. package/src/visx/types.ts +7 -10
  97. package/src/components/legend/utils/label-transform-factory.ts +0 -24
@@ -3,7 +3,7 @@ import { DataContext, TooltipContext } from '@visx/xychart';
3
3
  import { useCallback, useContext, useState } from 'react';
4
4
  import { createPortal } from 'react-dom';
5
5
  import { isValidHexColor } from '../../utils';
6
- import { BoundedTooltip } from './private/bounded-tooltip';
6
+ import { BoundedTooltip, TOOLTIP_Z_INDEX } from './private/bounded-tooltip';
7
7
  import type {
8
8
  CrosshairStyle,
9
9
  RenderTooltipGlyphProps,
@@ -36,9 +36,6 @@ const crosshairPaintProps = ( props: CrosshairStyle = {} ) => {
36
36
  };
37
37
  const DEFAULT_GLYPH_RADIUS = 4;
38
38
  const FALLBACK_COLOR = '#222';
39
- // Above the chart's own overlays (the zoom reset button sits at 2). The chart
40
- // wrapper isolates its stacking context, so this never competes with page chrome.
41
- const DEFAULT_TOOLTIP_Z_INDEX = 3;
42
39
  // Portal-era options, accepted for compatibility and dropped before the box
43
40
  // renders: it sits in the chart wrapper and moves with it, and nothing
44
41
  // measures it any more.
@@ -110,7 +107,8 @@ const XyChartTooltipContent = < Datum extends object >( {
110
107
  horizontalCrosshairStyle,
111
108
  detectBounds = true,
112
109
  tooltipPlacement = 'auto',
113
- zIndex = DEFAULT_TOOLTIP_Z_INDEX,
110
+ tooltipAnchorTop,
111
+ zIndex = TOOLTIP_Z_INDEX,
114
112
  style,
115
113
  ...rest
116
114
  }: XyChartTooltipContentProps< Datum > ) => {
@@ -171,7 +169,9 @@ const XyChartTooltipContent = < Datum extends object >( {
171
169
  const size = Number( glyphStyle?.radius ?? DEFAULT_GLYPH_RADIUS );
172
170
  const labelColor = theme?.htmlLabel?.color ?? FALLBACK_COLOR;
173
171
  // visx colours a lone nearest-datum glyph like the gridlines and series glyphs like labels.
174
- const fallbackColor = showSeriesGlyphs ? labelColor : theme?.gridStyles?.stroke ?? labelColor;
172
+ const fallbackColor = showSeriesGlyphs
173
+ ? labelColor
174
+ : ( theme?.gridStyles?.stroke ?? labelColor );
175
175
  let entries: Array< { key: string; datum: Datum; index: number } > = [];
176
176
  if ( showSeriesGlyphs ) {
177
177
  entries = Object.values( tooltipContext.tooltipData?.datumByKey ?? {} );
@@ -214,8 +214,7 @@ const XyChartTooltipContent = < Datum extends object >( {
214
214
  const marginTop = margin?.top ?? 0;
215
215
  const marginLeft = margin?.left ?? 0;
216
216
 
217
- const TooltipComponent =
218
- detectBounds || tooltipPlacement === 'below-axis' ? BoundedTooltip : Tooltip;
217
+ const TooltipComponent = detectBounds || tooltipPlacement !== 'auto' ? BoundedTooltip : Tooltip;
219
218
  const boxStyle: CSSProperties = {
220
219
  ...defaultStyles,
221
220
  zIndex,
@@ -265,12 +264,12 @@ const XyChartTooltipContent = < Datum extends object >( {
265
264
  top={
266
265
  tooltipPlacement === 'below-axis'
267
266
  ? marginTop + innerHeight + ( margin?.bottom ?? 0 )
268
- : tooltipTop
267
+ : ( tooltipAnchorTop ?? tooltipTop )
269
268
  }
270
269
  style={ boxStyle }
271
270
  applyPositionStyle
272
271
  { ...tooltipProps }
273
- { ...( tooltipPlacement === 'below-axis' && { placement: tooltipPlacement } ) }
272
+ { ...( tooltipPlacement !== 'auto' && { placement: tooltipPlacement } ) }
274
273
  >
275
274
  { tooltipContent }
276
275
  </TooltipComponent>,
@@ -353,6 +353,41 @@ describe( 'useChartMargin', () => {
353
353
 
354
354
  expect( result.current.left ).toBe( 20 );
355
355
  } );
356
+
357
+ it( 'measures ticks from readings that exist', () => {
358
+ const partialData = [
359
+ {
360
+ label: 'Series 1',
361
+ data: [
362
+ { date: new Date( '2024-01-01' ), value: null },
363
+ { date: new Date( '2024-01-02' ), value: 100 },
364
+ { date: new Date( '2024-01-03' ), value: 200 },
365
+ ],
366
+ },
367
+ ];
368
+
369
+ renderHook( () => useChartMargin( 300, optionsBase, partialData, baseTheme ) );
370
+
371
+ const ticks = mockGetLongestTickWidth.mock.calls[ 0 ][ 0 ] as number[];
372
+ expect( Math.min( ...ticks ) ).toBe( 100 );
373
+ } );
374
+
375
+ it( 'measures a unit range when no bucket has a reading', () => {
376
+ const allNullData = [
377
+ {
378
+ label: 'Series 1',
379
+ data: [
380
+ { date: new Date( '2024-01-01' ), value: null },
381
+ { date: new Date( '2024-01-02' ), value: null },
382
+ ],
383
+ },
384
+ ];
385
+
386
+ renderHook( () => useChartMargin( 300, optionsBase, allNullData, baseTheme ) );
387
+
388
+ const ticks = mockGetLongestTickWidth.mock.calls[ 0 ][ 0 ] as number[];
389
+ expect( Math.max( ...ticks ) ).toBe( 1 );
390
+ } );
356
391
  } );
357
392
 
358
393
  describe( 'horizontal y ticks', () => {
@@ -1,4 +1,4 @@
1
- import { renderHook, waitFor } from '@testing-library/react';
1
+ import { act, renderHook, waitFor } from '@testing-library/react';
2
2
  import { useElementSize } from '../use-element-size';
3
3
 
4
4
  // Mock ResizeObserver
@@ -66,7 +66,7 @@ describe( 'useElementSize', () => {
66
66
  const [ refCallback ] = result.current;
67
67
 
68
68
  // Attach the element
69
- refCallback( mockElement as unknown as HTMLDivElement );
69
+ act( () => refCallback( mockElement as unknown as HTMLDivElement ) );
70
70
 
71
71
  await waitFor( () => {
72
72
  expect( result.current[ 1 ] ).toBe( 300 );
@@ -84,7 +84,7 @@ describe( 'useElementSize', () => {
84
84
  const { result } = renderHook( () => useElementSize() );
85
85
  const [ refCallback ] = result.current;
86
86
 
87
- refCallback( mockElement as unknown as HTMLDivElement );
87
+ act( () => refCallback( mockElement as unknown as HTMLDivElement ) );
88
88
 
89
89
  await waitFor( () => {
90
90
  expect( result.current[ 1 ] ).toBe( 0 );
@@ -100,7 +100,7 @@ describe( 'useElementSize', () => {
100
100
  const { result } = renderHook( () => useElementSize() );
101
101
  const [ refCallback ] = result.current;
102
102
 
103
- refCallback( mockElement as unknown as HTMLDivElement );
103
+ act( () => refCallback( mockElement as unknown as HTMLDivElement ) );
104
104
 
105
105
  await waitFor( () => {
106
106
  expect( result.current[ 1 ] ).toBe( 0 );
@@ -129,10 +129,10 @@ describe( 'useElementSize', () => {
129
129
  const [ refCallback ] = result.current;
130
130
 
131
131
  // Attach first element
132
- refCallback( mockElement1 as unknown as HTMLDivElement );
132
+ act( () => refCallback( mockElement1 as unknown as HTMLDivElement ) );
133
133
 
134
134
  // Attach second element
135
- refCallback( mockElement2 as unknown as HTMLDivElement );
135
+ act( () => refCallback( mockElement2 as unknown as HTMLDivElement ) );
136
136
 
137
137
  expect( disconnectSpy ).toHaveBeenCalled();
138
138
  } );
@@ -155,10 +155,10 @@ describe( 'useElementSize', () => {
155
155
  const [ refCallback ] = result.current;
156
156
 
157
157
  // Attach element
158
- refCallback( mockElement as unknown as HTMLDivElement );
158
+ act( () => refCallback( mockElement as unknown as HTMLDivElement ) );
159
159
 
160
160
  // Remove element
161
- refCallback( null as unknown as HTMLDivElement );
161
+ act( () => refCallback( null as unknown as HTMLDivElement ) );
162
162
 
163
163
  expect( disconnectSpy ).toHaveBeenCalled();
164
164
  } );
@@ -180,7 +180,7 @@ describe( 'useElementSize', () => {
180
180
  const { result } = renderHook( () => useElementSize() );
181
181
  const [ refCallback ] = result.current;
182
182
 
183
- refCallback( mockElement as unknown as HTMLDivElement );
183
+ act( () => refCallback( mockElement as unknown as HTMLDivElement ) );
184
184
 
185
185
  expect( globalThis.ResizeObserver ).toHaveBeenCalled();
186
186
  expect( observeSpy ).toHaveBeenCalledWith( mockElement );
@@ -207,7 +207,7 @@ describe( 'useElementSize', () => {
207
207
  const { result } = renderHook( () => useElementSize() );
208
208
  const [ refCallback ] = result.current;
209
209
 
210
- refCallback( mockSpanElement as unknown as HTMLDivElement );
210
+ act( () => refCallback( mockSpanElement as unknown as HTMLDivElement ) );
211
211
 
212
212
  await waitFor( () => {
213
213
  expect( result.current[ 1 ] ).toBe( 75 );
@@ -236,7 +236,7 @@ describe( 'useElementSize', () => {
236
236
  const { result } = renderHook( () => useElementSize() );
237
237
  const [ refCallback ] = result.current;
238
238
 
239
- refCallback( mockElement as unknown as HTMLDivElement );
239
+ act( () => refCallback( mockElement as unknown as HTMLDivElement ) );
240
240
 
241
241
  await waitFor( () => {
242
242
  expect( result.current[ 1 ] ).toBe( 100 );
@@ -244,7 +244,7 @@ describe( 'useElementSize', () => {
244
244
  } );
245
245
 
246
246
  // Simulate resize
247
- resizeCallback();
247
+ act( () => resizeCallback() );
248
248
 
249
249
  await waitFor( () => {
250
250
  expect( result.current[ 1 ] ).toBe( 200 );
@@ -269,7 +269,7 @@ describe( 'useElementSize', () => {
269
269
  const { result, unmount } = renderHook( () => useElementSize() );
270
270
  const [ refCallback ] = result.current;
271
271
 
272
- refCallback( mockElement as unknown as HTMLDivElement );
272
+ act( () => refCallback( mockElement as unknown as HTMLDivElement ) );
273
273
  unmount();
274
274
 
275
275
  // The observer should be cleaned up when the hook unmounts
@@ -111,8 +111,13 @@ export const useChartMargin = (
111
111
  return options.axis.y.tickValues;
112
112
  }
113
113
 
114
- const minY = Math.min( ...allDataPoints.map( d => d.value ) );
115
- const maxY = Math.max( ...allDataPoints.map( d => d.value ) );
114
+ const values = allDataPoints
115
+ .map( d => d.value )
116
+ .filter(
117
+ ( value ): value is number => typeof value === 'number' && Number.isFinite( value )
118
+ );
119
+ const minY = values.length ? Math.min( ...values ) : 0;
120
+ const maxY = values.length ? Math.max( ...values ) : 1;
116
121
  const yScale = createScale( {
117
122
  ...options.yScale,
118
123
  // A pinned domain is what the axis actually renders, so measure those
@@ -23,7 +23,7 @@ export const useXYChartTheme = ( data: SeriesData[] ) => {
23
23
  // One resolver for all three, so they share a single getComputedStyle call.
24
24
  const resolve = createCssVariableResolver( scopeElement );
25
25
  const resolveColor = ( value?: string ): string | undefined =>
26
- value ? resolve( value ) ?? value : value;
26
+ value ? ( resolve( value ) ?? value ) : value;
27
27
 
28
28
  const seriesColors: string[] = JSON.parse( seriesColorKey );
29
29
 
package/src/index.ts CHANGED
@@ -10,7 +10,9 @@ export {
10
10
  HeatmapChart,
11
11
  HeatmapChartUnresponsive,
12
12
  buildCalendarHeatmapData,
13
+ buildMonthCalendarHeatmapData,
13
14
  useCalendarHeatmapData,
15
+ useMonthCalendarHeatmapData,
14
16
  } from './charts/heatmap-chart';
15
17
  export { LeaderboardChart, LeaderboardChartUnresponsive } from './charts/leaderboard-chart';
16
18
  export { LineChart, LineChartUnresponsive } from './charts/line-chart';
@@ -95,7 +97,7 @@ export type { LegendProps, BaseLegendProps, ChartLegendOptions } from './compone
95
97
 
96
98
  // Previously available via '@automattic/charts/bar-chart', '@automattic/charts/line-chart', etc.
97
99
  export type { AreaChartProps } from './charts/area-chart';
98
- export type { BarChartProps } from './charts/bar-chart';
100
+ export type { BarChartProps, BandHighlightSelection } from './charts/bar-chart';
99
101
  export type {
100
102
  BarListChartProps,
101
103
  RenderLabelProps,
@@ -112,10 +114,14 @@ export type {
112
114
  export type {
113
115
  HeatmapChartProps,
114
116
  HeatmapColumn,
117
+ HeatmapColumnGroup,
115
118
  HeatmapCell,
116
119
  HeatmapTooltipData,
117
120
  CalendarHeatmapOptions,
118
121
  CalendarHeatmapResult,
122
+ MonthCalendarHeatmapOptions,
123
+ MonthCalendarHeatmapRange,
124
+ MonthCalendarHeatmapResult,
119
125
  } from './charts/heatmap-chart';
120
126
  export type { LeaderboardChartProps } from './charts/leaderboard-chart';
121
127
  export type {
@@ -219,7 +219,7 @@ export const GlobalChartsProvider: FC< GlobalChartsProviderProps > = ( {
219
219
 
220
220
  const getElementStyles = useCallback< GlobalChartsContextValue[ 'getElementStyles' ] >(
221
221
  ( { data, index, overrideColor, legendShape } ) => {
222
- const isSeriesData = data && typeof data === 'object' && 'data' in data && 'options' in data;
222
+ const isSeriesData = data && typeof data === 'object' && 'data' in data;
223
223
  // DataPointPercentage has a numeric 'value' directly, unlike SeriesData which has 'data' array
224
224
  const isPointPercentageData =
225
225
  data &&
@@ -1318,11 +1318,11 @@ describe( 'ChartContext', () => {
1318
1318
  expect( styles ).toHaveProperty( 'glyph' );
1319
1319
  expect( styles ).toHaveProperty( 'shapeStyles' );
1320
1320
 
1321
- // Verify types
1321
+ // A series without an `options` key is still a series, so theme styles apply.
1322
1322
  expect( typeof styles.color ).toBe( 'string' );
1323
- expect( typeof styles.lineStyles ).toBe( 'object' );
1324
- expect( typeof styles.glyph ).toBe( 'function' );
1325
- expect( typeof styles.shapeStyles ).toBe( 'object' );
1323
+ expect( styles.lineStyles ).toEqual( { strokeWidth: 2 } );
1324
+ expect( styles.glyph ).toBe( mockGlyph );
1325
+ expect( styles.shapeStyles ).toEqual( { fill: '#SHAPE1' } );
1326
1326
  } );
1327
1327
 
1328
1328
  it( 'returns complete ElementStyles object for DataPointPercentage', () => {
@@ -61,6 +61,7 @@ const defaultTheme: CompleteChartTheme = {
61
61
  heatmapChart: {
62
62
  compactCellGap: 2,
63
63
  compactCellSize: 11,
64
+ groupGap: 24,
64
65
  },
65
66
  };
66
67
 
@@ -101,7 +101,7 @@ function parseTokensDoc(): Map< string, Entry > {
101
101
  const token = /(--wp[\w-]+)/.exec( mapping );
102
102
 
103
103
  entries.set( role, {
104
- reads: unmapped ? null : derives?.[ 1 ] ?? token?.[ 1 ] ?? null,
104
+ reads: unmapped ? null : ( derives?.[ 1 ] ?? token?.[ 1 ] ?? null ),
105
105
  fallback: fallback === '—' ? null : normalize( fallback ),
106
106
  } );
107
107
  }
package/src/types.ts CHANGED
@@ -53,12 +53,7 @@ export type LegendShape< Data, Output > =
53
53
  | ComponentClass< LegendShapeRenderProps< Data, Output > >;
54
54
 
55
55
  export type GoogleDataTableColumnType =
56
- | 'string'
57
- | 'number'
58
- | 'boolean'
59
- | 'date'
60
- | 'datetime'
61
- | 'timeofday';
56
+ 'string' | 'number' | 'boolean' | 'date' | 'datetime' | 'timeofday';
62
57
 
63
58
  export enum GoogleDataTableColumnRoleType {
64
59
  annotation = 'annotation',
@@ -98,13 +93,7 @@ export type GoogleDataTableCell =
98
93
  export type GoogleDataTableRow = GoogleDataTableCell[];
99
94
 
100
95
  export type ChartType =
101
- | 'area'
102
- | 'bar'
103
- | 'conversion-funnel'
104
- | 'leaderboard'
105
- | 'line'
106
- | 'pie'
107
- | 'pie-semi-circle';
96
+ 'area' | 'bar' | 'conversion-funnel' | 'leaderboard' | 'line' | 'pie' | 'pie-semi-circle';
108
97
 
109
98
  export type OrientationType = ValueOf< typeof Orientation >;
110
99
 
@@ -145,6 +134,8 @@ export type ThemeAnnotationStyles = {
145
134
  export type DataPoint = {
146
135
  label: string;
147
136
  value: number;
137
+ /** Per-point fill override for BarChart; other built-in charts ignore it. */
138
+ color?: string;
148
139
  };
149
140
 
150
141
  /**
@@ -196,6 +187,8 @@ export type DataPointDate = {
196
187
  dateString?: string;
197
188
  value: number | null;
198
189
  label?: string;
190
+ /** Per-point fill override for BarChart; other built-in charts ignore it. */
191
+ color?: string;
199
192
  };
200
193
 
201
194
  export type LeaderboardEntry = {
@@ -424,6 +417,8 @@ export type ChartTheme = {
424
417
  compactCellGap?: number;
425
418
  /** Fixed square cell size in px for compact mode */
426
419
  compactCellSize?: number;
420
+ /** Gap in px between adjacent column groups; in compact mode the minimum, as the gaps share leftover width */
421
+ groupGap?: number;
427
422
  };
428
423
  };
429
424
 
@@ -657,6 +652,12 @@ export type SeriesChartLegendConfig = ChartLegendConfig< SeriesData[] > & {
657
652
  * uncollapsed one toggles only its own.
658
653
  */
659
654
  collapseGroups?: boolean;
655
+ /**
656
+ * Append a static, non-interactive item explaining the comparison overlay whenever a series
657
+ * has `options.type === 'comparison'`. Skipped when that series already has its own item.
658
+ * Pass a string to replace the default label.
659
+ */
660
+ comparisonItem?: boolean | string;
660
661
  };
661
662
 
662
663
  /**
@@ -47,13 +47,13 @@ export const formatMetricValue = (
47
47
  maximumFractionDigits: decimals ?? 2,
48
48
  signDisplay,
49
49
  },
50
- } )
50
+ } )
51
51
  : formatNumber( numericValue, {
52
52
  decimals: decimals ?? 2,
53
53
  numberFormatOptions: {
54
54
  signDisplay,
55
55
  },
56
- } );
56
+ } );
57
57
  return `$${ formatted }`;
58
58
  }
59
59
 
@@ -80,13 +80,13 @@ export const formatMetricValue = (
80
80
  maximumFractionDigits: decimals ?? 0,
81
81
  signDisplay,
82
82
  },
83
- } )
83
+ } )
84
84
  : formatNumber( numericValue, {
85
85
  decimals: decimals ?? 0,
86
86
  numberFormatOptions: {
87
87
  signDisplay,
88
88
  },
89
- } );
89
+ } );
90
90
  }
91
91
  }
92
92
  };
@@ -24,7 +24,7 @@ export const getEdgeTickWidths = < T extends AnyD3Scale >(
24
24
 
25
25
  const lastIndex = ticks.length - 1;
26
26
  const label = ( tick: ScaleInput< T >, index: number ) =>
27
- String( formatTick ? formatTick( tick, index, [] ) ?? '' : tick );
27
+ String( formatTick ? ( formatTick( tick, index, [] ) ?? '' ) : tick );
28
28
 
29
29
  return {
30
30
  first: getStringWidth( label( ticks[ 0 ], 0 ), labelStyle ) ?? 0,
@@ -103,7 +103,7 @@ export function getItemShapeStyles(
103
103
  const hasExplicitStyles = Object.values( explicitStyles ).some(
104
104
  value => value !== undefined && value !== null && value !== ''
105
105
  );
106
- const baseShapeStyles = hasExplicitStyles ? explicitStyles : themeShapeStyles ?? {};
106
+ const baseShapeStyles = hasExplicitStyles ? explicitStyles : ( themeShapeStyles ?? {} );
107
107
 
108
108
  // Layer the comparison bar opacity on top so the swatch matches the translucent bar
109
109
  // without discarding the base (custom or theme) shape styles.
package/src/visx/types.ts CHANGED
@@ -12,15 +12,10 @@ export interface RenderTooltipGlyphProps< Datum extends object > extends GlyphPr
12
12
  isNearestDatum: boolean;
13
13
  }
14
14
 
15
- export type TooltipPlacement = 'auto' | 'below-axis';
15
+ export type TooltipPlacement = 'auto' | 'below-axis' | 'beside';
16
16
 
17
17
  type CrosshairPaintProperty =
18
- | 'stroke'
19
- | 'strokeWidth'
20
- | 'strokeOpacity'
21
- | 'strokeDasharray'
22
- | 'strokeLinecap'
23
- | 'opacity';
18
+ 'stroke' | 'strokeWidth' | 'strokeOpacity' | 'strokeDasharray' | 'strokeLinecap' | 'opacity';
24
19
 
25
20
  export type CrosshairStyle = Pick< SVGProps< SVGLineElement >, CrosshairPaintProperty > & {
26
21
  className?: string;
@@ -31,11 +26,13 @@ export type XyChartTooltipProps< Datum extends object > = {
31
26
  renderTooltip: ( params: RenderTooltipParams< Datum > ) => ReactNode;
32
27
  renderGlyph?: ( params: RenderTooltipGlyphProps< Datum > ) => ReactNode;
33
28
  /**
34
- * Keep the panel below the x-axis label band, centered at the datum x and horizontally clamped.
35
- * Vertical bounds do not move this placement; clipping ancestors can still cut it off.
29
+ * Use below-axis placement for centered axis anchoring, or beside placement to avoid vertical flipping.
30
+ * Below-axis ignores vertical bounds; beside placement clamps to them.
36
31
  * @default 'auto'
37
32
  */
38
33
  tooltipPlacement?: TooltipPlacement;
34
+ /** Override the tooltip top anchor in SVG coordinates, including negative offsets. */
35
+ tooltipAnchorTop?: number;
39
36
  /** Merge overrides with the default box styles; use `unstyled` to strip the box styling. */
40
37
  style?: VisxTooltipProps[ 'style' ];
41
38
  snapTooltipToDatumX?: boolean;
@@ -52,7 +49,7 @@ export type XyChartTooltipProps< Datum extends object > = {
52
49
  * that clips its overflow, or the viewport when there is none. The box may
53
50
  * leave the chart wrapper. (It used to keep a body-level portal inside the
54
51
  * viewport.)
55
- * Ignored by `below-axis`.
52
+ * Non-auto placements always apply their own bounds handling.
56
53
  * @default true
57
54
  */
58
55
  detectBounds?: boolean;
@@ -1,24 +0,0 @@
1
- import type { ItemTransformer, LabelFormatter } from '@visx/legend';
2
- import type { AnyD3Scale, ScaleInput } from '@visx/scale';
3
-
4
- /**
5
- * Returns a function which takes a Datum and index as input, and returns a formatted label object.
6
- * @param {object} root0 - The object to return the value of.
7
- * @param {AnyD3Scale} root0.scale - The scale to use.
8
- * @param {LabelFormatter<ScaleInput<Scale>>} root0.labelFormat - The label format to use.
9
- * @return {ItemTransformer<ScaleInput<Scale>, ReturnType<Scale>>} The label transform factory.
10
- */
11
- export function labelTransformFactory< Scale extends AnyD3Scale >( {
12
- scale,
13
- labelFormat,
14
- }: {
15
- scale: Scale;
16
- labelFormat: LabelFormatter< ScaleInput< Scale > >;
17
- } ): ItemTransformer< ScaleInput< Scale >, ReturnType< Scale > > {
18
- return ( d, i ) => ( {
19
- datum: d,
20
- index: i,
21
- text: `${ labelFormat( d, i ) }`,
22
- value: scale( d ),
23
- } );
24
- }