@automattic/charts 4.1.1 → 4.2.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 (73) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/TOKENS.md +1 -1
  3. package/dist/index.cjs +875 -237
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.css +30 -1
  6. package/dist/index.d.cts +106 -6
  7. package/dist/index.d.ts +106 -6
  8. package/dist/index.js +875 -239
  9. package/dist/index.js.map +1 -1
  10. package/package.json +8 -8
  11. package/src/charts/area-chart/private/overlays.tsx +1 -2
  12. package/src/charts/bar-chart/bar-chart.tsx +67 -17
  13. package/src/charts/bar-chart/index.ts +1 -1
  14. package/src/charts/bar-chart/private/band-highlight.tsx +85 -0
  15. package/src/charts/bar-chart/private/band-scale.ts +13 -0
  16. package/src/charts/bar-chart/private/band-tooltip.tsx +138 -0
  17. package/src/charts/bar-chart/private/comparison-bars.tsx +5 -10
  18. package/src/charts/bar-chart/private/index.ts +2 -0
  19. package/src/charts/bar-chart/private/test/band-highlight.test.tsx +109 -0
  20. package/src/charts/bar-chart/private/test/band-tooltip.test.ts +26 -0
  21. package/src/charts/bar-chart/private/truncated-tick-component.tsx +1 -1
  22. package/src/charts/bar-chart/private/use-bar-chart-options.ts +1 -1
  23. package/src/charts/bar-chart/test/bar-chart-pointer-events.test.tsx +11 -23
  24. package/src/charts/bar-chart/test/bar-chart.test.tsx +565 -1
  25. package/src/charts/bar-list-chart/bar-list-chart.tsx +5 -3
  26. package/src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx +1 -1
  27. package/src/charts/conversion-funnel-chart/types.ts +4 -2
  28. package/src/charts/geo-chart/types.ts +4 -2
  29. package/src/charts/heatmap-chart/build-calendar-data.ts +5 -11
  30. package/src/charts/heatmap-chart/build-month-calendar-data.ts +124 -0
  31. package/src/charts/heatmap-chart/heatmap-chart.module.scss +32 -0
  32. package/src/charts/heatmap-chart/heatmap-chart.tsx +191 -114
  33. package/src/charts/heatmap-chart/index.ts +6 -0
  34. package/src/charts/heatmap-chart/private/civil-day.ts +26 -0
  35. package/src/charts/heatmap-chart/private/column-groups.ts +71 -0
  36. package/src/charts/heatmap-chart/private/heatmap-legend.tsx +19 -3
  37. package/src/charts/heatmap-chart/private/index.ts +2 -0
  38. package/src/charts/heatmap-chart/private/keyboard-navigation.ts +201 -0
  39. package/src/charts/heatmap-chart/test/build-calendar-data.test.ts +36 -0
  40. package/src/charts/heatmap-chart/test/build-month-calendar-data.test.ts +194 -0
  41. package/src/charts/heatmap-chart/test/column-groups.test.ts +83 -0
  42. package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +428 -2
  43. package/src/charts/heatmap-chart/test/keyboard-navigation.test.ts +25 -0
  44. package/src/charts/heatmap-chart/test/use-month-calendar-heatmap-data.test.tsx +90 -0
  45. package/src/charts/heatmap-chart/test/zoned-month-calendar-data.test.ts +23 -0
  46. package/src/charts/heatmap-chart/types.ts +59 -3
  47. package/src/charts/heatmap-chart/use-month-calendar-heatmap-data.ts +34 -0
  48. package/src/charts/leaderboard-chart/types.ts +13 -14
  49. package/src/charts/line-chart/line-chart.tsx +1 -1
  50. package/src/charts/line-chart/private/line-chart-annotation.tsx +1 -1
  51. package/src/charts/line-chart/types.ts +2 -2
  52. package/src/charts/private/time-axis-options.ts +1 -1
  53. package/src/charts/private/with-responsive/with-responsive.tsx +2 -2
  54. package/src/charts/private/x-zoom/test/x-zoom.test.tsx +1 -3
  55. package/src/charts/private/x-zoom/x-zoom.tsx +1 -2
  56. package/src/components/legend/private/base-legend.tsx +1 -1
  57. package/src/components/tooltip/accessible-tooltip.tsx +127 -40
  58. package/src/components/tooltip/base-tooltip.module.scss +6 -1
  59. package/src/components/tooltip/private/bounded-tooltip.tsx +12 -6
  60. package/src/components/tooltip/test/accessible-tooltip.test.tsx +128 -0
  61. package/src/components/tooltip/test/bounded-tooltip.test.tsx +17 -0
  62. package/src/components/tooltip/test/xy-chart-tooltip.test.tsx +7 -0
  63. package/src/components/tooltip/xy-chart-tooltip.tsx +9 -10
  64. package/src/hooks/test/use-element-size.test.tsx +13 -13
  65. package/src/hooks/use-xychart-theme.ts +1 -1
  66. package/src/index.ts +7 -1
  67. package/src/providers/chart-context/themes.ts +1 -0
  68. package/src/styles/test/chart-scope.test.ts +1 -1
  69. package/src/types.ts +6 -13
  70. package/src/utils/format-metric-value.ts +4 -4
  71. package/src/utils/get-edge-tick-widths.ts +1 -1
  72. package/src/utils/get-styles.ts +1 -1
  73. package/src/visx/types.ts +7 -10
@@ -413,7 +413,7 @@ const ConversionFunnelChartInternal: FC< ConversionFunnelChartProps > = ( {
413
413
  top: tooltipTop,
414
414
  left: tooltipLeft,
415
415
  className: styles[ 'tooltip-wrapper' ],
416
- } )
416
+ } )
417
417
  : renderDefaultTooltip( tooltipData as FunnelStep );
418
418
 
419
419
  // Don't render tooltip if renderTooltip returns falsy
@@ -56,8 +56,10 @@ export interface TooltipRenderProps {
56
56
  /**
57
57
  * Props for the ConversionFunnelChart component
58
58
  */
59
- export interface ConversionFunnelChartProps
60
- extends Pick< BaseChartProps, 'className' | 'chartId' | 'animation' > {
59
+ export interface ConversionFunnelChartProps extends Pick<
60
+ BaseChartProps,
61
+ 'className' | 'chartId' | 'animation'
62
+ > {
61
63
  /** Main conversion rate to highlight */
62
64
  mainRate: number;
63
65
  /** Change indicator (e.g., +2%, -1.5%) */
@@ -22,8 +22,10 @@ export interface GeoChartError {
22
22
  options?: Record< string, unknown >;
23
23
  }
24
24
 
25
- export interface GeoChartProps
26
- extends Pick< BaseChartProps, 'className' | 'chartId' | 'width' | 'height' > {
25
+ export interface GeoChartProps extends Pick<
26
+ BaseChartProps,
27
+ 'className' | 'chartId' | 'width' | 'height'
28
+ > {
27
29
  /**
28
30
  * Data in Google Charts native format for maximum flexibility.
29
31
  * First row contains column headers, subsequent rows contain data.
@@ -1,9 +1,10 @@
1
- import { createDateFormatter, sanitizeFormatting } from '../../utils/date-formatting';
1
+ import { sanitizeFormatting } from '../../utils/date-formatting';
2
2
  import { warnOnce } from '../../utils/warn-once';
3
3
  import {
4
4
  addCivilDays,
5
5
  civilDate,
6
6
  civilKey,
7
+ civilLabelFormatters,
7
8
  civilWeekSpan,
8
9
  instantDayReader,
9
10
  pointDayKey,
@@ -90,7 +91,7 @@ export const buildCalendarHeatmapData = (
90
91
  offending
91
92
  ? `${ JSON.stringify(
92
93
  offending
93
- ) } is not a day this can read, so its point is left out of the calendar. A \`dateString\` must start \`yyyy-MM-dd\`.`
94
+ ) } is not a day this can read, so its point is left out of the calendar. A \`dateString\` must start \`yyyy-MM-dd\`.`
94
95
  : 'A point carries neither `date` nor `dateString`, so it is left out of the calendar.'
95
96
  );
96
97
  continue;
@@ -118,15 +119,8 @@ export const buildCalendarHeatmapData = (
118
119
  const requestedMinDate = civilDate( requestedMinDayKey ) as CivilDate;
119
120
  const gridMaxDate = civilDate( gridMaxDayKey ) as CivilDate;
120
121
 
121
- // The grid walks UTC proxies, so the label formatters read UTC. The host's zone
122
- // was already spent on bucketing.
123
- const labelFormatting = { locale, timeZone: 'UTC' };
124
- const formatWeekday = createDateFormatter( { weekday: 'short' }, labelFormatting );
125
- const formatMonth = createDateFormatter( { month: 'short' }, labelFormatting );
126
- const formatDay = createDateFormatter(
127
- { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric' },
128
- labelFormatting
129
- );
122
+ // The host's zone was already spent on bucketing; labels read the UTC proxies.
123
+ const { formatWeekday, formatMonth, formatDay } = civilLabelFormatters( locale );
130
124
 
131
125
  const gridStart = startOfCivilWeek( requestedMinDate, weekStartsOn );
132
126
 
@@ -0,0 +1,124 @@
1
+ import { sanitizeFormatting } from '../../utils/date-formatting';
2
+ import { warnOnce } from '../../utils/warn-once';
3
+ import {
4
+ addCivilDays,
5
+ civilDate,
6
+ civilKey,
7
+ civilLabelFormatters,
8
+ startOfCivilWeek,
9
+ } from './private/civil-day';
10
+ import { isPresent } from './private/use-heatmap-colors';
11
+ import type { CivilDate, DayKey } from './private/civil-day';
12
+ import type {
13
+ HeatmapCell,
14
+ HeatmapColumn,
15
+ HeatmapColumnGroup,
16
+ MonthCalendarHeatmapOptions,
17
+ MonthCalendarHeatmapRange,
18
+ MonthCalendarHeatmapResult,
19
+ } from './types';
20
+
21
+ /** Week rows per month, fixed so every month block is the same height. */
22
+ const MONTH_ROWS = 6;
23
+
24
+ const DAYS_PER_WEEK = 7;
25
+
26
+ const startOfCivilMonth = ( date: CivilDate ): CivilDate => {
27
+ const first = new Date( date.getTime() ) as CivilDate;
28
+ first.setUTCDate( 1 );
29
+ return first;
30
+ };
31
+
32
+ const startOfNextCivilMonth = ( date: CivilDate ): CivilDate => {
33
+ const next = startOfCivilMonth( date );
34
+ next.setUTCMonth( next.getUTCMonth() + 1 );
35
+ return next;
36
+ };
37
+
38
+ /**
39
+ * Lay a day-keyed map out as one month calendar per month, weekdays across and
40
+ * weeks down, for `HeatmapChart` with `columnGroups`.
41
+ *
42
+ * @param valueByDay - Value per `yyyy-MM-dd`. A missing day is `null`.
43
+ * @param range - The measured days; every month from `start`'s through `end`'s is drawn.
44
+ * @param options - Week start and label locale.
45
+ * @return Columns and one group per month; empty for an invalid or reversed range.
46
+ */
47
+ export const buildMonthCalendarHeatmapData = (
48
+ valueByDay: Record< string, number | null >,
49
+ range: MonthCalendarHeatmapRange,
50
+ options: MonthCalendarHeatmapOptions = {}
51
+ ): MonthCalendarHeatmapResult => {
52
+ const start = civilDate( range.start );
53
+ const end = civilDate( range.end );
54
+ if ( ! start || ! end || start.getTime() > end.getTime() ) {
55
+ warnOnce(
56
+ `heatmap:monthRange:${ range.start }:${ range.end }`,
57
+ `range ${ JSON.stringify(
58
+ range
59
+ ) } must be two \`yyyy-MM-dd\` days with start on or before end, so the calendar is empty.`
60
+ );
61
+ return { data: [], columnGroups: [] };
62
+ }
63
+ const startKey = civilKey( start );
64
+ const endKey = civilKey( end );
65
+ const weekStartsOn = options.weekStartsOn ?? 1;
66
+ const { locale } = sanitizeFormatting( { locale: options.locale } );
67
+
68
+ for ( const key of Object.keys( valueByDay ) ) {
69
+ if ( ! civilDate( key ) ) {
70
+ warnOnce(
71
+ `heatmap:monthKey:${ key }`,
72
+ `${ JSON.stringify(
73
+ key
74
+ ) } is not a \`yyyy-MM-dd\` day, so its value is left out of the calendar.`
75
+ );
76
+ }
77
+ }
78
+ const readValue = ( key: DayKey ): number | null => {
79
+ const value = valueByDay[ key ];
80
+ return isPresent( value ) ? value : null;
81
+ };
82
+
83
+ const { formatMonth, formatMonthYear, formatDay } = civilLabelFormatters( locale );
84
+
85
+ const data: HeatmapColumn[] = [];
86
+ const columnGroups: HeatmapColumnGroup[] = [];
87
+ const lastMonthStart = startOfCivilMonth( end );
88
+ // Past twelve months a month name would repeat, so the year tells them apart.
89
+ const monthCount =
90
+ ( lastMonthStart.getUTCFullYear() - start.getUTCFullYear() ) * 12 +
91
+ lastMonthStart.getUTCMonth() -
92
+ start.getUTCMonth() +
93
+ 1;
94
+ const formatGroup = monthCount > 12 ? formatMonthYear : formatMonth;
95
+
96
+ for (
97
+ let monthStart = startOfCivilMonth( start );
98
+ monthStart.getTime() <= lastMonthStart.getTime();
99
+ monthStart = startOfNextCivilMonth( monthStart )
100
+ ) {
101
+ const monthStartKey = civilKey( monthStart );
102
+ const monthEndKey = civilKey( addCivilDays( startOfNextCivilMonth( monthStart ), -1 ) );
103
+ const gridStart = startOfCivilWeek( monthStart, weekStartsOn );
104
+
105
+ for ( let weekday = 0; weekday < DAYS_PER_WEEK; weekday++ ) {
106
+ const cells: HeatmapCell[] = [];
107
+ for ( let week = 0; week < MONTH_ROWS; week++ ) {
108
+ const day = addCivilDays( gridStart, week * DAYS_PER_WEEK + weekday );
109
+ const key = civilKey( day );
110
+ if ( key < monthStartKey || key > monthEndKey ) {
111
+ cells.push( { value: null, hidden: true } );
112
+ } else if ( key < startKey || key > endKey ) {
113
+ cells.push( { label: formatDay( day ), value: null, placeholder: true } );
114
+ } else {
115
+ cells.push( { label: formatDay( day ), value: readValue( key ) } );
116
+ }
117
+ }
118
+ data.push( { data: cells } );
119
+ }
120
+ columnGroups.push( { label: formatGroup( monthStart ), span: DAYS_PER_WEEK } );
121
+ }
122
+
123
+ return { data, columnGroups };
124
+ };
@@ -5,6 +5,8 @@ $focus-ring-width: var(--wpds-border-width-focus);
5
5
 
6
6
  .heatmap-chart {
7
7
  position: relative;
8
+ // Keeps the tooltip's z-index inside the chart, as `.xy-plot` does.
9
+ isolation: isolate;
8
10
  width: 100%;
9
11
  height: 100%;
10
12
  min-height: 0;
@@ -53,6 +55,13 @@ $focus-ring-width: var(--wpds-border-width-focus);
53
55
  min-height: 0;
54
56
  }
55
57
 
58
+ // Grouped compact grids fill the container so the flexible gap tracks can share
59
+ // the leftover width, and never shrink below the cells plus the minimum gaps.
60
+ .heatmap-chart__grid--flex-gaps {
61
+ width: 100%;
62
+ min-width: max-content;
63
+ }
64
+
56
65
  // A cell-height cap makes the row tracks content-bound, so the grid must not
57
66
  // stretch: leftover container height would otherwise be absorbed by the auto
58
67
  // label row. A width-only cap does not change vertical flex sizing.
@@ -70,6 +79,8 @@ $focus-ring-width: var(--wpds-border-width-focus);
70
79
  .heatmap-chart__row-label {
71
80
  color: var(--a8c-charts-color-label-secondary);
72
81
  font-size: var(--wpds-typography-font-size-xs);
82
+ // Pinned so the label height does not follow the host's line-height.
83
+ line-height: var(--wpds-typography-line-height-xs);
73
84
  white-space: nowrap;
74
85
  overflow: visible;
75
86
  }
@@ -79,6 +90,16 @@ $focus-ring-width: var(--wpds-border-width-focus);
79
90
  text-align: left;
80
91
  }
81
92
 
93
+ // Beneath its columns, set off from the last row by more than the cell gap.
94
+ .heatmap-chart__group-label {
95
+ color: var(--a8c-charts-color-label-secondary);
96
+ font-size: var(--wpds-typography-font-size-xs);
97
+ line-height: var(--wpds-typography-line-height-xs);
98
+ white-space: nowrap;
99
+ text-align: center;
100
+ padding-block-start: var(--wpds-dimension-padding-xs);
101
+ }
102
+
82
103
  .heatmap-chart__row-label {
83
104
  align-self: center;
84
105
  justify-self: end;
@@ -178,3 +199,14 @@ $focus-ring-width: var(--wpds-border-width-focus);
178
199
  border-radius: var(--a8c-charts-border-radius-cell);
179
200
  background: color-mix(in sRGB, var(--a8c-charts-color-heatmap-primary) calc((0.15 + 0.85 * var(--a8c-charts-heatmap-cell-intensity)) * 100%), var(--a8c-charts-color-background, #fff));
180
201
  }
202
+
203
+ // One band: the steps butt against each other and only the band's ends round.
204
+ .heatmap-chart__legend-scale--bar {
205
+ overflow: hidden;
206
+ border-radius: var(--a8c-charts-border-radius-cell);
207
+
208
+ .heatmap-chart__legend-swatch {
209
+ width: var(--wpds-dimension-size-sm);
210
+ border-radius: 0;
211
+ }
212
+ }