@automattic/charts 4.1.0 → 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 (80) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/TOKENS.md +7 -5
  3. package/dist/index.cjs +1115 -471
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.css +39 -1
  6. package/dist/index.d.cts +116 -16
  7. package/dist/index.d.ts +116 -16
  8. package/dist/index.js +1115 -473
  9. package/dist/index.js.map +1 -1
  10. package/package.json +11 -11
  11. package/src/charts/area-chart/area-chart.tsx +2 -1
  12. package/src/charts/area-chart/private/overlays.tsx +1 -2
  13. package/src/charts/area-chart/test/area-chart.test.tsx +31 -0
  14. package/src/charts/bar-chart/bar-chart.tsx +67 -17
  15. package/src/charts/bar-chart/index.ts +1 -1
  16. package/src/charts/bar-chart/private/band-highlight.tsx +85 -0
  17. package/src/charts/bar-chart/private/band-scale.ts +13 -0
  18. package/src/charts/bar-chart/private/band-tooltip.tsx +138 -0
  19. package/src/charts/bar-chart/private/comparison-bars.tsx +5 -10
  20. package/src/charts/bar-chart/private/index.ts +2 -0
  21. package/src/charts/bar-chart/private/test/band-highlight.test.tsx +109 -0
  22. package/src/charts/bar-chart/private/test/band-tooltip.test.ts +26 -0
  23. package/src/charts/bar-chart/private/truncated-tick-component.tsx +1 -1
  24. package/src/charts/bar-chart/private/use-bar-chart-options.ts +1 -1
  25. package/src/charts/bar-chart/test/bar-chart-pointer-events.test.tsx +11 -23
  26. package/src/charts/bar-chart/test/bar-chart.test.tsx +565 -1
  27. package/src/charts/bar-list-chart/bar-list-chart.tsx +5 -3
  28. package/src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx +1 -1
  29. package/src/charts/conversion-funnel-chart/types.ts +4 -2
  30. package/src/charts/geo-chart/types.ts +4 -2
  31. package/src/charts/heatmap-chart/{private/build-calendar-data.ts → build-calendar-data.ts} +15 -45
  32. package/src/charts/heatmap-chart/build-month-calendar-data.ts +124 -0
  33. package/src/charts/heatmap-chart/heatmap-chart.module.scss +32 -0
  34. package/src/charts/heatmap-chart/heatmap-chart.tsx +191 -114
  35. package/src/charts/heatmap-chart/index.ts +15 -3
  36. package/src/charts/heatmap-chart/private/civil-day.ts +26 -0
  37. package/src/charts/heatmap-chart/private/column-groups.ts +71 -0
  38. package/src/charts/heatmap-chart/private/heatmap-legend.tsx +19 -3
  39. package/src/charts/heatmap-chart/private/index.ts +2 -2
  40. package/src/charts/heatmap-chart/private/keyboard-navigation.ts +201 -0
  41. package/src/charts/heatmap-chart/test/build-calendar-data.test.ts +37 -1
  42. package/src/charts/heatmap-chart/test/build-month-calendar-data.test.ts +194 -0
  43. package/src/charts/heatmap-chart/test/column-groups.test.ts +83 -0
  44. package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +428 -2
  45. package/src/charts/heatmap-chart/test/keyboard-navigation.test.ts +25 -0
  46. package/src/charts/heatmap-chart/test/use-calendar-heatmap-data.test.tsx +1 -1
  47. package/src/charts/heatmap-chart/test/use-month-calendar-heatmap-data.test.tsx +90 -0
  48. package/src/charts/heatmap-chart/test/zoned-calendar-data.test.ts +1 -1
  49. package/src/charts/heatmap-chart/test/zoned-month-calendar-data.test.ts +23 -0
  50. package/src/charts/heatmap-chart/types.ts +88 -3
  51. package/src/charts/heatmap-chart/use-calendar-heatmap-data.ts +2 -2
  52. package/src/charts/heatmap-chart/use-month-calendar-heatmap-data.ts +34 -0
  53. package/src/charts/leaderboard-chart/types.ts +13 -14
  54. package/src/charts/line-chart/line-chart.tsx +3 -2
  55. package/src/charts/line-chart/private/line-chart-annotation.tsx +1 -1
  56. package/src/charts/line-chart/test/line-chart.test.tsx +31 -0
  57. package/src/charts/line-chart/types.ts +2 -2
  58. package/src/charts/private/time-axis-options.ts +1 -1
  59. package/src/charts/private/with-responsive/with-responsive.tsx +2 -2
  60. package/src/charts/private/x-zoom/test/x-zoom.test.tsx +1 -3
  61. package/src/charts/private/x-zoom/x-zoom.tsx +1 -2
  62. package/src/components/legend/private/base-legend.tsx +1 -1
  63. package/src/components/tooltip/accessible-tooltip.tsx +146 -37
  64. package/src/components/tooltip/base-tooltip.module.scss +6 -1
  65. package/src/components/tooltip/private/bounded-tooltip.tsx +12 -6
  66. package/src/components/tooltip/test/accessible-tooltip.test.tsx +160 -0
  67. package/src/components/tooltip/test/bounded-tooltip.test.tsx +17 -0
  68. package/src/components/tooltip/test/xy-chart-tooltip.test.tsx +29 -3
  69. package/src/components/tooltip/xy-chart-tooltip.tsx +11 -11
  70. package/src/hooks/test/use-element-size.test.tsx +13 -13
  71. package/src/hooks/use-xychart-theme.ts +1 -1
  72. package/src/index.ts +7 -1
  73. package/src/providers/chart-context/themes.ts +1 -0
  74. package/src/styles/chart-scope.scss +11 -3
  75. package/src/styles/test/chart-scope.test.ts +36 -2
  76. package/src/types.ts +6 -13
  77. package/src/utils/format-metric-value.ts +4 -4
  78. package/src/utils/get-edge-tick-widths.ts +1 -1
  79. package/src/utils/get-styles.ts +1 -1
  80. package/src/visx/types.ts +7 -10
@@ -1,47 +1,24 @@
1
- import { createDateFormatter, sanitizeFormatting } from '../../../utils/date-formatting';
2
- import { warnOnce } from '../../../utils/warn-once';
1
+ import { sanitizeFormatting } from '../../utils/date-formatting';
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,
10
11
  startOfCivilWeek,
11
12
  writtenDayKey,
12
- } from './civil-day';
13
- import type { DataPointDate } from '../../../types';
14
- import type { HeatmapCell, HeatmapColumn } from '../types';
15
- import type { CivilDate, DayKey } from './civil-day';
16
-
17
- export type CalendarHeatmapResult = {
18
- data: HeatmapColumn[];
19
- rowLabels: string[];
20
- };
21
-
22
- export type CalendarHeatmapOptions = {
23
- weekStartsOn?: 0 | 1;
24
- /**
25
- * Mark the days completing the first and last week outside the series' span as
26
- * hidden cells rather than blank ones, giving the calendar ragged edges. Days
27
- * inside the span stay blank even where the series has no entry.
28
- */
29
- hideOutOfRangeDays?: boolean;
30
- /**
31
- * Draw the grid over this span (`yyyy-MM-dd` bounds) instead of the series' own.
32
- * Days inside the grid but outside the series become placeholder cells: painted,
33
- * but reporting nothing, since they were never measured. A start bound opens from
34
- * the beginning of its week, and bounds narrower than the series are ignored.
35
- */
36
- gridSpan?: { start?: string; end?: string };
37
- /** BCP-47 tag the labels are written in. Defaults to the runtime's locale. */
38
- locale?: string;
39
- /**
40
- * IANA zone the series' instants are bucketed into days in. Defaults to the
41
- * runtime's zone. A `dateString` carrying no offset is taken as written.
42
- */
43
- timeZone?: string;
44
- };
13
+ } from './private/civil-day';
14
+ import type { CivilDate, DayKey } from './private/civil-day';
15
+ import type {
16
+ CalendarHeatmapOptions,
17
+ CalendarHeatmapResult,
18
+ HeatmapCell,
19
+ HeatmapColumn,
20
+ } from './types';
21
+ import type { DataPointDate } from '../../types';
45
22
 
46
23
  /** Rows that get a weekday label (Mon, Wed, Fri with a Monday week start). */
47
24
  const LABELLED_ROWS = [ 0, 2, 4 ];
@@ -114,7 +91,7 @@ export const buildCalendarHeatmapData = (
114
91
  offending
115
92
  ? `${ JSON.stringify(
116
93
  offending
117
- ) } 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\`.`
118
95
  : 'A point carries neither `date` nor `dateString`, so it is left out of the calendar.'
119
96
  );
120
97
  continue;
@@ -142,15 +119,8 @@ export const buildCalendarHeatmapData = (
142
119
  const requestedMinDate = civilDate( requestedMinDayKey ) as CivilDate;
143
120
  const gridMaxDate = civilDate( gridMaxDayKey ) as CivilDate;
144
121
 
145
- // The grid walks UTC proxies, so the label formatters read UTC. The host's zone
146
- // was already spent on bucketing.
147
- const labelFormatting = { locale, timeZone: 'UTC' };
148
- const formatWeekday = createDateFormatter( { weekday: 'short' }, labelFormatting );
149
- const formatMonth = createDateFormatter( { month: 'short' }, labelFormatting );
150
- const formatDay = createDateFormatter(
151
- { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric' },
152
- labelFormatting
153
- );
122
+ // The host's zone was already spent on bucketing; labels read the UTC proxies.
123
+ const { formatWeekday, formatMonth, formatDay } = civilLabelFormatters( locale );
154
124
 
155
125
  const gridStart = startOfCivilWeek( requestedMinDate, weekStartsOn );
156
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
+ }