@automattic/charts 3.2.0 → 4.0.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 (109) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/TOKENS.md +53 -58
  3. package/dist/index.cjs +1012 -542
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.css +72 -29
  6. package/dist/index.d.cts +151 -179
  7. package/dist/index.d.ts +151 -179
  8. package/dist/index.js +1013 -544
  9. package/dist/index.js.map +1 -1
  10. package/package.json +6 -6
  11. package/src/charts/area-chart/area-chart.tsx +39 -25
  12. package/src/charts/area-chart/test/area-chart.test.tsx +4 -33
  13. package/src/charts/area-chart/test/tick-placement.test.tsx +55 -0
  14. package/src/charts/area-chart/test/tooltip-resolution-branches.test.tsx +86 -0
  15. package/src/charts/area-chart/test/tooltip-resolution.test.tsx +69 -0
  16. package/src/charts/area-chart/types.ts +4 -7
  17. package/src/charts/bar-chart/bar-chart.tsx +2 -1
  18. package/src/charts/bar-chart/private/test/use-bar-chart-options-formatting.test.tsx +69 -0
  19. package/src/charts/bar-chart/private/use-bar-chart-options.ts +27 -15
  20. package/src/charts/bar-list-chart/bar-list-chart.tsx +5 -0
  21. package/src/charts/conversion-funnel-chart/conversion-funnel-chart.module.scss +33 -23
  22. package/src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx +138 -182
  23. package/src/charts/conversion-funnel-chart/test/conversion-funnel-chart.test.tsx +46 -0
  24. package/src/charts/geo-chart/geo-chart.module.scss +1 -0
  25. package/src/charts/geo-chart/geo-chart.tsx +5 -10
  26. package/src/charts/geo-chart/test/geo-chart.test.tsx +2 -2
  27. package/src/charts/heatmap-chart/heatmap-chart.module.scss +21 -16
  28. package/src/charts/heatmap-chart/heatmap-chart.tsx +45 -28
  29. package/src/charts/heatmap-chart/private/build-calendar-data.ts +3 -0
  30. package/src/charts/heatmap-chart/private/heatmap-legend.tsx +11 -4
  31. package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +51 -12
  32. package/src/charts/heatmap-chart/types.ts +2 -2
  33. package/src/charts/leaderboard-chart/hooks/use-fitted-row-count.ts +2 -2
  34. package/src/charts/leaderboard-chart/hooks/use-leaderboard-legend-items.ts +3 -5
  35. package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +31 -19
  36. package/src/charts/leaderboard-chart/leaderboard-chart.tsx +27 -17
  37. package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +14 -0
  38. package/src/charts/leaderboard-chart/test/use-leaderboard-legend-items.test.tsx +2 -9
  39. package/src/charts/line-chart/line-chart.module.scss +10 -18
  40. package/src/charts/line-chart/line-chart.tsx +93 -24
  41. package/src/charts/line-chart/private/line-chart-annotation.tsx +18 -19
  42. package/src/charts/line-chart/test/default-tooltip-formatting.test.tsx +74 -0
  43. package/src/charts/line-chart/test/line-chart.test.tsx +27 -3
  44. package/src/charts/line-chart/test/tick-placement.test.tsx +84 -0
  45. package/src/charts/line-chart/test/tooltip-resolution.test.tsx +140 -0
  46. package/src/charts/line-chart/types.ts +4 -1
  47. package/src/charts/pie-chart/pie-chart.module.scss +13 -1
  48. package/src/charts/pie-chart/pie-chart.tsx +30 -40
  49. package/src/charts/pie-chart/test/pie-chart.test.tsx +56 -0
  50. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.module.scss +8 -3
  51. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +23 -37
  52. package/src/charts/pie-semi-circle-chart/test/pie-semi-circle-chart.test.tsx +50 -0
  53. package/src/charts/private/chart-layout/chart-layout.module.scss +3 -2
  54. package/src/charts/private/svg-empty-state/svg-empty-state.module.scss +1 -1
  55. package/src/charts/private/test/axis-formatting.test.tsx +85 -0
  56. package/src/charts/private/test/time-axis-formatting.test.ts +109 -0
  57. package/src/charts/private/test/time-axis-options.test.ts +267 -0
  58. package/src/charts/private/test/time-axis-tick-invariants.test.ts +153 -0
  59. package/src/charts/private/test/time-axis-tick-values.test.ts +280 -0
  60. package/src/charts/private/test/time-axis.test.ts +9 -69
  61. package/src/charts/private/time-axis-options.ts +106 -0
  62. package/src/charts/private/time-axis.ts +318 -136
  63. package/src/charts/private/x-zoom/x-zoom.module.scss +3 -3
  64. package/src/charts/private/xy-plot/xy-plot.module.scss +8 -0
  65. package/src/charts/sparkline/test/sparkline.test.tsx +29 -56
  66. package/src/charts/sparkline/types.ts +1 -1
  67. package/src/components/legend/private/base-legend.module.scss +9 -5
  68. package/src/components/tooltip/accessible-tooltip.tsx +13 -15
  69. package/src/components/tooltip/base-tooltip.module.scss +3 -3
  70. package/src/components/tooltip/private/bounded-tooltip.tsx +172 -0
  71. package/src/components/tooltip/test/accessible-tooltip.test.tsx +2 -3
  72. package/src/components/tooltip/test/bounded-tooltip.test.tsx +129 -0
  73. package/src/components/tooltip/test/xy-chart-tooltip.test.tsx +279 -0
  74. package/src/components/tooltip/xy-chart-tooltip.tsx +288 -0
  75. package/src/components/trend-indicator/trend-indicator.module.scss +2 -2
  76. package/src/hooks/test/use-xychart-theme.test.tsx +16 -3
  77. package/src/hooks/use-xychart-theme.ts +25 -9
  78. package/src/index.ts +4 -0
  79. package/src/providers/chart-context/global-charts-provider.tsx +37 -38
  80. package/src/providers/chart-context/hooks/use-chart-formatting.ts +22 -0
  81. package/src/providers/chart-context/index.ts +1 -0
  82. package/src/providers/chart-context/private/catalog-pointers.ts +42 -0
  83. package/src/providers/chart-context/private/series-palette.ts +3 -13
  84. package/src/providers/chart-context/test/{sparse-theme.test.ts → catalog-pointers.test.ts} +22 -10
  85. package/src/providers/chart-context/test/chart-context.test.tsx +177 -792
  86. package/src/providers/chart-context/test/use-global-charts-theme.test.tsx +7 -25
  87. package/src/providers/chart-context/themes.ts +7 -52
  88. package/src/providers/chart-context/types.ts +3 -0
  89. package/src/styles/chart-scope.scss +39 -62
  90. package/src/styles/test/chart-scope.test.ts +19 -47
  91. package/src/test-utils/series-fixtures.ts +31 -0
  92. package/src/types.ts +79 -166
  93. package/src/utils/bucket-info.ts +87 -0
  94. package/src/utils/color-utils.ts +0 -27
  95. package/src/utils/date-formatting.ts +174 -0
  96. package/src/utils/index.ts +3 -0
  97. package/src/utils/test/bucket-info.test.ts +185 -0
  98. package/src/utils/test/color-utils.test.ts +1 -48
  99. package/src/utils/test/date-formatting.test.ts +89 -0
  100. package/src/utils/test/get-styles.test.ts +10 -7
  101. package/src/utils/test/merge-themes.test.ts +18 -46
  102. package/src/visx/types.ts +29 -2
  103. package/src/hooks/test/use-xychart-theme-provider-override.test.tsx +0 -28
  104. package/src/providers/chart-context/private/theme-override-vars.ts +0 -131
  105. package/src/providers/chart-context/private/with-catalog-pointers.ts +0 -93
  106. package/src/providers/chart-context/test/theme-override-vars-guard.test.tsx +0 -29
  107. package/src/providers/chart-context/test/theme-override-vars.test.ts +0 -183
  108. package/src/providers/chart-context/test/with-catalog-pointers.test.ts +0 -104
  109. package/src/styles/test/wpds-fallbacks.test.ts +0 -131
package/src/types.ts CHANGED
@@ -109,30 +109,36 @@ export type ChartType =
109
109
  export type OrientationType = ValueOf< typeof Orientation >;
110
110
 
111
111
  /**
112
- * Annotation styling.
113
- *
114
- * Each color member is deprecated on its own rather than the field it sits in: `radius`, `stroke` widths and the label geometry alongside them have no catalog role behind them and still apply.
112
+ * Styling for one annotation, as its `styles` prop. Colors belong here because
113
+ * `--a8c-charts-color-annotation` moves every annotation on the page at once and no
114
+ * selector narrows it to one.
115
115
  */
116
116
  export type AnnotationStyles = {
117
117
  circleSubject?: Omit< CircleSubjectProps, 'x' | 'y' | 'fill' > & {
118
- /** @deprecated Set the `--a8c-charts-color-annotation` custom property inside the provider tree instead. See `TOKENS.md`. */
119
118
  fill?: string;
120
119
  };
121
120
  lineSubject?: Omit< LineSubjectProps, 'x' | 'y' >;
122
- connector?: Omit< ConnectorProps, 'x' | 'y' | 'dx' | 'dy' | 'stroke' > & {
123
- /** @deprecated Set the `--a8c-charts-color-annotation` custom property inside the provider tree instead. See `TOKENS.md`. */
124
- stroke?: ConnectorProps[ 'stroke' ];
121
+ connector?: Omit< ConnectorProps, 'x' | 'y' | 'dx' | 'dy' >;
122
+ label?: Omit< LabelProps, 'title' | 'subtitle' | 'x' | 'y' > & {
123
+ x?: number | 'start' | 'end';
124
+ y?: number | 'start' | 'end';
125
125
  };
126
+ };
127
+
128
+ /**
129
+ * Annotation styling on the theme: geometry only. Colors come from
130
+ * `--a8c-charts-color-annotation` and `--a8c-charts-color-surface`.
131
+ */
132
+ export type ThemeAnnotationStyles = {
133
+ circleSubject?: Omit< CircleSubjectProps, 'x' | 'y' | 'fill' | 'stroke' >;
134
+ lineSubject?: Omit< LineSubjectProps, 'x' | 'y' | 'stroke' >;
135
+ connector?: Omit< ConnectorProps, 'x' | 'y' | 'dx' | 'dy' | 'stroke' >;
126
136
  label?: Omit<
127
137
  LabelProps,
128
138
  'title' | 'subtitle' | 'x' | 'y' | 'anchorLineStroke' | 'backgroundFill'
129
139
  > & {
130
140
  x?: number | 'start' | 'end';
131
141
  y?: number | 'start' | 'end';
132
- /** @deprecated Set the `--a8c-charts-color-annotation` custom property inside the provider tree instead. See `TOKENS.md`. */
133
- anchorLineStroke?: LabelProps[ 'anchorLineStroke' ];
134
- /** @deprecated Set the `--a8c-charts-color-surface` custom property inside the provider tree instead. See `TOKENS.md`. */
135
- backgroundFill?: LabelProps[ 'backgroundFill' ];
136
142
  };
137
143
  };
138
144
 
@@ -344,167 +350,55 @@ export type DataPointPercentageCalculated = DataPointPercentage & {
344
350
  };
345
351
 
346
352
  /**
347
- * Base theme configuration for chart components with optional properties
353
+ * Base theme configuration for chart components: shape and spacing. Chart colors are
354
+ * `--a8c-charts-color-*` roles set in CSS inside the provider tree, catalogued in
355
+ * `TOKENS.md`.
348
356
  */
349
357
  export type ChartTheme = {
350
- /**
351
- * Background color for chart components.
352
- *
353
- * Still resolved in JS — gradient stops, the glyph stroke and `GeoChart`'s Google Charts config each need a concrete string — but that is about where the value is *consumed*, not where it is *set*. `withCatalogPointers` parks a consumer's value in the catalog role and restores the pointer, so the field is only ever a carrier for `--a8c-charts-color-background`, and setting the role directly does the same job.
354
- *
355
- * Optional so the deprecation is actionable: a consumer writing a full theme literal can now stop setting it. `CompleteChartTheme` is `Required< ChartTheme >`, so `defaultTheme` must still carry one.
356
- *
357
- * @deprecated Set the `--a8c-charts-color-background` custom property inside the provider tree instead. See `TOKENS.md`.
358
- */
359
- backgroundColor?: string;
360
- /**
361
- * Background color for labels.
362
- *
363
- * @deprecated Set the `--a8c-charts-color-label-background` custom property inside the provider tree instead. See `TOKENS.md`.
364
- */
365
- labelBackgroundColor?: string;
366
- /**
367
- * Text color for labels.
368
- *
369
- * @deprecated Set the `--a8c-charts-color-label-on-fill` custom property inside the provider tree instead. See `TOKENS.md`.
370
- */
371
- labelTextColor?: string;
372
- /**
373
- * Series palette seeds. Entry N publishes `--a8c-charts-color-series-{N+1}`; entries past the fifth are ignored.
374
- *
375
- * Optional so the deprecation is actionable: a consumer writing a full theme literal can now stop setting it. `CompleteChartTheme` is `Required< ChartTheme >`, so `defaultTheme` must still carry one.
376
- *
377
- * @deprecated Set the `--a8c-charts-color-series-1` … `-5` custom properties inside the provider tree instead, or `options.stroke` on a series for a single one. See `TOKENS.md`.
378
- */
379
- colors?: string[];
380
- /**
381
- * Optional CSS styles for grid lines.
382
- *
383
- * `stroke` is deprecated on its own, not the whole field: `strokeWidth` has no CSS role behind it and still applies. The same shape is used for the other fields whose color moved to CSS — annotating the field itself would strike through the half that stays.
384
- */
385
- gridStyles?: Omit< GridStyles, 'stroke' > & {
386
- /** @deprecated Set the `--a8c-charts-color-grid` custom property inside the provider tree instead. See `TOKENS.md`. */
387
- stroke?: GridStyles[ 'stroke' ];
388
- };
358
+ /** Optional CSS styles for grid lines. */
359
+ gridStyles?: Omit< GridStyles, 'stroke' >;
389
360
  /** Length of axis ticks in pixels */
390
361
  tickLength: number;
391
- /**
392
- * Set `--a8c-charts-color-grid` for the gridlines.
393
- *
394
- * @deprecated Do not drop this field without replacing it. Nothing in this package consults it, but `buildChartTheme` seeds `y.left/right.axisLine.stroke` and `y.*.tickLine.stroke` from it, and `defaultTheme`'s empty string is the only thing leaving the y axis unpainted — deleting the field hands those a black `#222` instead. CHARTS-263 replaces it with visx's own `yAxisLineStyles`/`yTickLineStyles` and a pair of catalog roles.
395
- */
396
- gridColor?: string;
397
- /**
398
- * @deprecated Paints nothing, and unlike `gridColor` is safe to delete: `buildChartTheme` seeds `x.*.axisLine.stroke` from it, but spreads `xAxisLineStyles` over the top and `defaultTheme` always carries that stroke. Charts follow the host's theme through the catalog rather than a dark-mode field of their own.
399
- */
400
- gridColorDark?: string;
401
362
  /** Styles for x-axis tick lines */
402
- xTickLineStyles?: Omit< LineStyles, 'stroke' > & {
403
- /** @deprecated Set the `--a8c-charts-color-tick` custom property inside the provider tree instead. See `TOKENS.md`. */
404
- stroke?: LineStyles[ 'stroke' ];
405
- };
363
+ xTickLineStyles?: Omit< LineStyles, 'stroke' >;
406
364
  /** Styles for x-axis line */
407
- xAxisLineStyles?: Omit< LineStyles, 'stroke' > & {
408
- /** @deprecated Set the `--a8c-charts-color-axis` custom property inside the provider tree instead. See `TOKENS.md`. */
409
- stroke?: LineStyles[ 'stroke' ];
410
- };
411
- /** Styles for series lines */
412
- seriesLineStyles?: LineStyles[];
365
+ xAxisLineStyles?: Omit< LineStyles, 'stroke' >;
366
+ /**
367
+ * Per-series line styling — dash pattern, width, cap. These spread on top of the
368
+ * palette-derived stroke, so a color here would override it; a single series takes
369
+ * one through `options.seriesLineStyle`.
370
+ */
371
+ seriesLineStyles?: Array< Omit< LineStyles, 'stroke' > >;
413
372
  /** Array of render functions for glyphs */
414
373
  glyphs?: Array< < Datum extends object >( props: GlyphProps< Datum > ) => ReactNode >;
415
374
  /** Legend specific settings */
416
375
  legend?: {
417
376
  /** Styles for legend shapes */
418
377
  shapeStyles?: Record< string, unknown >[];
419
- /**
420
- * Styles for legend labels.
421
- *
422
- * `color` is deprecated on its own, not the whole field: everything else a consumer sets here — font size, weight, spacing — has no catalog role behind it and still applies.
423
- */
424
- labelStyles?: Omit< CSSProperties, 'color' > & {
425
- /** @deprecated Set the `--a8c-charts-color-label` custom property inside the provider tree instead. See `TOKENS.md`. */
426
- color?: CSSProperties[ 'color' ];
427
- };
378
+ /** Styles for legend labels. */
379
+ labelStyles?: Omit< CSSProperties, 'color' >;
428
380
  /** Styles for legend container */
429
381
  containerStyles?: CSSProperties;
430
382
  };
431
383
  /**
432
384
  * Styles for small SVG text (eg. axis tick labels), passed through to the XYChart theme.
433
385
  *
434
- * `fontFamily: 'inherit'` is load-bearing here — it overrides the font stack `buildChartTheme` injects inline on SVG `<text>` — so only `fill` is deprecated.
386
+ * `fontFamily: 'inherit'` is load-bearing here — it overrides the font stack
387
+ * `buildChartTheme` injects inline on SVG `<text>`.
435
388
  */
436
- svgLabelSmall?: Omit< TextProps, 'fill' > & {
437
- /** @deprecated Set the `--a8c-charts-color-label-axis` custom property inside the provider tree instead, or `--a8c-charts-color-label` to move every label at once. See `TOKENS.md`. */
438
- fill?: TextProps[ 'fill' ];
439
- };
389
+ svgLabelSmall?: Omit< TextProps, 'fill' >;
440
390
  /** Styles for large SVG text (eg. axis titles), passed through to the XYChart theme. */
441
391
  svgLabelBig?: TextProps;
442
- annotationStyles?: AnnotationStyles;
443
- /** GeoChart specific settings */
444
- geoChart?: {
445
- /**
446
- * Default fill color for a geo chart feature (e.g. country) with no data.
447
- *
448
- * @deprecated Set the `--a8c-charts-color-surface-secondary` custom property inside the provider tree instead. Google Charts still takes a resolved hex, but the value is read from the role. See `TOKENS.md`.
449
- */
450
- featureFillColor?: string;
451
- };
392
+ annotationStyles?: ThemeAnnotationStyles;
452
393
  /** LeaderboardChart specific settings */
453
394
  leaderboardChart?: {
454
- /** Gap between rows in the leaderboard grid */
455
- rowGap?: number;
456
- /** Gap between columns in the leaderboard grid */
457
- columnGap?: number;
458
395
  /** Spacing between label and progress bars */
459
396
  labelSpacing?: GapSize;
460
- /**
461
- * Primary color for current period bars.
462
- *
463
- * @deprecated Use the `primaryColor` prop on `LeaderboardChart` for one chart, or set `--a8c-charts-color-series-1` to move the palette this falls back to. See `TOKENS.md`.
464
- */
465
- primaryColor?: string;
466
- /**
467
- * Secondary color for comparison period bars.
468
- *
469
- * @deprecated Use the `secondaryColor` prop on `LeaderboardChart` for one chart, or set `--a8c-charts-color-series-2` to move the palette this falls back to. See `TOKENS.md`.
470
- */
471
- secondaryColor?: string;
472
- /**
473
- * Delta colors: [negative, neutral, positive].
474
- *
475
- * @deprecated Set the `--a8c-charts-color-trend-down`, `-trend-neutral` and `-trend-up` custom properties inside the provider tree instead. See `TOKENS.md`.
476
- */
477
- deltaColors?: [ string, string, string ];
478
- };
479
- /** ConversionFunnelChart specific settings */
480
- conversionFunnelChart?: {
481
- /**
482
- * Primary color for funnel bars.
483
- *
484
- * @deprecated Set `--a8c-charts-color-series-1` to move the palette this falls back to. See `TOKENS.md`.
485
- */
486
- primaryColor?: string;
487
- /**
488
- * Background color for chart container.
489
- *
490
- * @deprecated Set the `--a8c-charts-color-surface-secondary` custom property inside the provider tree instead. See `TOKENS.md`.
491
- */
492
- backgroundColor?: string;
493
- /**
494
- * Color for positive change indicators.
495
- *
496
- * @deprecated Set the `--a8c-charts-color-trend-up` custom property inside the provider tree instead. See `TOKENS.md`.
497
- */
498
- positiveChangeColor?: string;
499
- /**
500
- * Color for negative change indicators.
501
- *
502
- * @deprecated Set the `--a8c-charts-color-trend-down` custom property inside the provider tree instead. See `TOKENS.md`.
503
- */
504
- negativeChangeColor?: string;
505
397
  };
506
398
  lineChart?: {
507
- lineStyles?: Partial< Record< NonNullable< SeriesDataOptions[ 'type' ] >, LineStyles > >;
399
+ lineStyles?: Partial<
400
+ Record< NonNullable< SeriesDataOptions[ 'type' ] >, Omit< LineStyles, 'stroke' > >
401
+ >;
508
402
  };
509
403
  barChart?: {
510
404
  barStyles?: Partial< Record< NonNullable< SeriesDataOptions[ 'type' ] >, BarStyles > >;
@@ -523,16 +417,9 @@ export type ChartTheme = {
523
417
  };
524
418
  /**
525
419
  * HeatmapChart settings. Cell gap, radius, value size and the selection ring come from
526
- * WPDS tokens in CSS, so only the scale color and the compact sizing live here.
420
+ * WPDS tokens in CSS, so only the compact sizing lives here.
527
421
  */
528
422
  heatmapChart?: {
529
- /**
530
- * Color the cell scale interpolates toward at the highest value (prop > this >
531
- * palette `colors[0]`), fed to CSS `color-mix`. Omit to use the palette color.
532
- *
533
- * @deprecated Use the `primaryColor` prop on `HeatmapChart` for one chart, or set `--a8c-charts-color-series-1` to move the palette this falls back to. See `TOKENS.md`.
534
- */
535
- primaryColor?: string;
536
423
  /** Gap in px between cells in compact mode */
537
424
  compactCellGap?: number;
538
425
  /** Fixed square cell size in px for compact mode */
@@ -545,18 +432,9 @@ export type ChartTheme = {
545
432
  * Useful for merged themes where defaults are provided for all optional properties.
546
433
  */
547
434
  export type CompleteChartTheme = Required< ChartTheme > & {
548
- leaderboardChart: Omit<
549
- Required< NonNullable< ChartTheme[ 'leaderboardChart' ] > >,
550
- 'primaryColor' | 'secondaryColor'
551
- > &
552
- Pick< NonNullable< ChartTheme[ 'leaderboardChart' ] >, 'primaryColor' | 'secondaryColor' >;
553
- conversionFunnelChart: Omit<
554
- Required< NonNullable< ChartTheme[ 'conversionFunnelChart' ] > >,
555
- 'primaryColor'
556
- > &
557
- Pick< NonNullable< ChartTheme[ 'conversionFunnelChart' ] >, 'primaryColor' >;
435
+ leaderboardChart: Required< NonNullable< ChartTheme[ 'leaderboardChart' ] > >;
558
436
  lineChart: {
559
- lineStyles: Record< NonNullable< SeriesDataOptions[ 'type' ] >, LineStyles >;
437
+ lineStyles: Record< NonNullable< SeriesDataOptions[ 'type' ] >, Omit< LineStyles, 'stroke' > >;
560
438
  };
561
439
  barChart: {
562
440
  barStyles: Record< NonNullable< SeriesDataOptions[ 'type' ] >, BarStyles >;
@@ -565,8 +443,7 @@ export type CompleteChartTheme = Required< ChartTheme > & {
565
443
  sparkline: Required< NonNullable< ChartTheme[ 'sparkline' ] > > & {
566
444
  margin: Required< NonNullable< ChartTheme[ 'sparkline' ] >[ 'margin' ] >;
567
445
  };
568
- heatmapChart: Omit< Required< NonNullable< ChartTheme[ 'heatmapChart' ] > >, 'primaryColor' > &
569
- Pick< NonNullable< ChartTheme[ 'heatmapChart' ] >, 'primaryColor' >;
446
+ heatmapChart: Required< NonNullable< ChartTheme[ 'heatmapChart' ] > >;
570
447
  };
571
448
 
572
449
  /**
@@ -575,6 +452,42 @@ export type CompleteChartTheme = Required< ChartTheme > & {
575
452
  */
576
453
  export type TickResolution = 'hour' | 'day' | 'week' | 'month' | 'year';
577
454
 
455
+ /**
456
+ * How a chart classified the buckets of the data it's drawing.
457
+ */
458
+ export type BucketInfo = {
459
+ /**
460
+ * The bucket the series has. Only reports `'week'` when declared: seven-day
461
+ * spacing is indistinguishable from sparse daily data.
462
+ */
463
+ bucket: TickResolution;
464
+ /** The classification tick formats are keyed on. `'week'` reads as `'day'`. */
465
+ displayResolution: Exclude< TickResolution, 'week' >;
466
+ };
467
+
468
+ /**
469
+ * The locale and time zone the time axis and the built-in tooltips render dates
470
+ * in, set once by the host on `GlobalChartsProvider`.
471
+ *
472
+ * Both halves are plain strings the host resolves for itself, so a non-WordPress
473
+ * consumer needs no WordPress package to supply them. Both are optional and
474
+ * default to the JavaScript runtime's own — the viewer's browser locale and
475
+ * browser time zone — which is what every version before this one used
476
+ * unconditionally.
477
+ *
478
+ * `timeZone` re-dates the instants the host supplies, so a value that is really a
479
+ * calendar day shifts under it: `new Date( '2026-08-02' )` is UTC midnight and
480
+ * labels as Aug 1 in `America/Los_Angeles`. Supply true instants alongside a
481
+ * `timeZone`, or set only `locale` for day-bucketed data. `HeatmapChart`'s
482
+ * calendar labels take neither — see `buildCalendarHeatmapData`.
483
+ */
484
+ export type ChartFormatting = {
485
+ /** BCP-47 language tag, e.g. `de-DE`. Defaults to the runtime's locale. */
486
+ locale?: string;
487
+ /** IANA time zone name, e.g. `Asia/Tokyo`. Defaults to the runtime's zone. */
488
+ timeZone?: string;
489
+ };
490
+
578
491
  export type AxisOptions = {
579
492
  orientation?: OrientationType;
580
493
  numTicks?: number;
@@ -0,0 +1,87 @@
1
+ import { differenceInHours } from 'date-fns';
2
+ import { parseAsLocalDate } from './date-parsing';
3
+ import type { BucketInfo, DataPoint, DataPointDate, SeriesData, TickResolution } from '../types';
4
+
5
+ // `dateString` is parsed here too: `getBucketInfo` is public, and hosts call it
6
+ // with the raw series, before `useChartDataTransform` has dated the points.
7
+ const getInstant = ( point?: DataPointDate | DataPoint ): number | undefined => {
8
+ if ( ! point ) {
9
+ return undefined;
10
+ }
11
+ if ( 'date' in point && point.date ) {
12
+ return point.date.getTime();
13
+ }
14
+ if ( 'dateString' in point && point.dateString ) {
15
+ return parseAsLocalDate( point.dateString ).getTime();
16
+ }
17
+ return undefined;
18
+ };
19
+
20
+ // Smallest interval between consecutive points across all series, in hours.
21
+ // Infinity when no series carries two distinct instants.
22
+ const getPointSpacingInHours = ( data: SeriesData[] ) => {
23
+ return data.reduce( ( spacing, datom ) => {
24
+ // Sorted and deduplicated rather than read in array order: `getBucketInfo`
25
+ // is public, and a repeated instant is padding, not a sub-daily gap.
26
+ const instants = [ ...new Set( datom.data.map( point => getInstant( point ) ) ) ]
27
+ .filter( ( time ): time is number => time !== undefined && Number.isFinite( time ) )
28
+ .sort( ( a, b ) => a - b );
29
+
30
+ return instants.reduce(
31
+ ( seriesSpacing, time, index ) =>
32
+ index === 0
33
+ ? seriesSpacing
34
+ : Math.min( seriesSpacing, Math.abs( differenceInHours( time, instants[ index - 1 ] ) ) ),
35
+ spacing
36
+ );
37
+ }, Number.POSITIVE_INFINITY );
38
+ };
39
+
40
+ // 23, not 24: a daily gap shrinks to 23 wall-clock hours across a
41
+ // spring-forward DST transition.
42
+ const SUB_DAILY_SPACING_HOURS = 23;
43
+
44
+ // The shortest month, so monthly buckets clear it but weekly ones don't.
45
+ const MONTHLY_SPACING_HOURS = 28 * 24;
46
+
47
+ // The bucket size behind the data, as the resolution label the tick formats are
48
+ // keyed on. Exported so consumers that label a single point — bar chart
49
+ // tooltips — read the same classification the axis does instead of inferring
50
+ // the resolution a second way. Weeks report as 'day': both are calendar-date
51
+ // buckets as far as labeling goes.
52
+ export const getBucketResolution = (
53
+ data: SeriesData[],
54
+ tickResolution?: TickResolution
55
+ ): Exclude< TickResolution, 'week' > => {
56
+ if ( tickResolution ) {
57
+ return tickResolution === 'week' ? 'day' : tickResolution;
58
+ }
59
+
60
+ const spacingInHours = getPointSpacingInHours( data );
61
+ if ( spacingInHours < SUB_DAILY_SPACING_HOURS ) {
62
+ return 'hour';
63
+ }
64
+ // Fewer than two points leaves the spacing unknowable, so fall back to
65
+ // calendar dates rather than reading Infinity as a very coarse bucket.
66
+ if ( ! Number.isFinite( spacingInHours ) || spacingInHours < MONTHLY_SPACING_HOURS ) {
67
+ return 'day';
68
+ }
69
+ // Twelve shortest months: above any monthly gap (31 days at most) and below
70
+ // any yearly one (365 days at least).
71
+ return spacingInHours < 12 * MONTHLY_SPACING_HOURS ? 'month' : 'year';
72
+ };
73
+
74
+ /**
75
+ * How this data was classified, for consumers that render their own labels.
76
+ *
77
+ * @param data - The series the chart draws, in any order, dated by `date` or `dateString`.
78
+ * @param tickResolution - Caller-declared bucket resolution, when known.
79
+ * @return The declared or inferred bucket, and the resolution formats key on.
80
+ */
81
+ export const getBucketInfo = (
82
+ data: SeriesData[],
83
+ tickResolution?: TickResolution
84
+ ): BucketInfo => ( {
85
+ bucket: tickResolution ?? getBucketResolution( data ),
86
+ displayResolution: getBucketResolution( data, tickResolution ),
87
+ } );
@@ -108,33 +108,6 @@ export const parseHslString = ( hslString: string ): [ number, number, number ]
108
108
  return [ h, parsed.s * 100, parsed.l * 100 ];
109
109
  };
110
110
 
111
- /**
112
- * Parse an RGB string like 'rgb(255, 0, 0)' into a hex color.
113
- *
114
- * @deprecated Use normalizeColorToHex() instead, which handles all color formats including rgb() and rgba().
115
- * @param rgbString - RGB color string (not RGBA)
116
- * @return hex color string or null if invalid
117
- */
118
- export const parseRgbString = ( rgbString: string ): string | null => {
119
- const lower = rgbString.toLowerCase().trim();
120
-
121
- // Check prefix - only handle rgb(), not rgba()
122
- // This is intentional - use normalizeColorToHex for rgba() support
123
- if ( ! lower.startsWith( 'rgb(' ) || lower.startsWith( 'rgba(' ) ) {
124
- return null;
125
- }
126
-
127
- const parsed = d3Color( lower );
128
-
129
- // d3Color returns null for invalid colors
130
- if ( ! parsed ) {
131
- return null;
132
- }
133
-
134
- // d3-color clamps values automatically
135
- return parsed.formatHex();
136
- };
137
-
138
111
  /**
139
112
  * Normalize any CSS color value to a hex color string.
140
113
  * Handles hex, HSL, HSLA, RGB, RGBA, named CSS colors, and CSS variables.
@@ -0,0 +1,174 @@
1
+ import type { ChartFormatting } from '../types';
2
+
3
+ /**
4
+ * `process.env.NODE_ENV` is replaced by the bundler at build time. Declare a
5
+ * minimal `process` locally so this file type-checks as source under `jetpack:src`.
6
+ */
7
+ declare const process: { env: Record< string, string | undefined > };
8
+
9
+ // One `Intl.DateTimeFormat` per locale, zone and options triple. The axis formats
10
+ // every bucket in the domain, and rebuilding the formatter is what costs; the tick
11
+ // formats themselves are rebuilt on every resize, so a per-closure cache is not
12
+ // enough. Bounded by the host's own locale and zone, so a handful of entries.
13
+ const formatters = new Map< string, Intl.DateTimeFormat >();
14
+
15
+ const getFormatter = (
16
+ options: Intl.DateTimeFormatOptions,
17
+ { locale, timeZone }: ChartFormatting
18
+ ): Intl.DateTimeFormat => {
19
+ const key = `${ locale ?? '' }|${ timeZone ?? '' }|${ JSON.stringify( options ) }`;
20
+ let formatter = formatters.get( key );
21
+
22
+ if ( ! formatter ) {
23
+ formatter = new Intl.DateTimeFormat( locale, { ...options, timeZone } );
24
+ formatters.set( key, formatter );
25
+ }
26
+
27
+ return formatter;
28
+ };
29
+
30
+ const warned = new Set< string >();
31
+
32
+ const warnOnce = ( key: string, message: string ): void => {
33
+ if ( warned.has( key ) || process.env.NODE_ENV === 'production' ) {
34
+ return;
35
+ }
36
+
37
+ warned.add( key );
38
+ // eslint-disable-next-line no-console
39
+ console.warn( `[Charts] ${ message }` );
40
+ };
41
+
42
+ const isUsable = ( formatting: ChartFormatting ): boolean => {
43
+ try {
44
+ // Constructing is the check: `Intl` throws on a tag or zone it cannot use.
45
+ Intl.DateTimeFormat( formatting.locale, { timeZone: formatting.timeZone } );
46
+ return true;
47
+ } catch {
48
+ return false;
49
+ }
50
+ };
51
+
52
+ // WordPress underscores its locales, where BCP-47 wants a hyphen: `get_locale()`
53
+ // returns `en_US` for what Intl knows as `en-US`.
54
+ const toBcp47 = ( locale?: string ) => locale?.replace( /_/g, '-' );
55
+
56
+ /**
57
+ * Repairs a host locale for `Intl`, dropping either half it still cannot use.
58
+ *
59
+ * Every formatter below is built during render, so an unusable string would throw
60
+ * where React unmounts the whole host tree rather than one chart. WordPress hands
61
+ * out the likeliest one either way: `timezone_string` is `''` on a site set to a
62
+ * raw UTC offset, and has no repair.
63
+ *
64
+ * @param formatting - Locale and time zone as the host supplied them.
65
+ * @return The usable halves, each `undefined` where the host's value was not.
66
+ */
67
+ export const sanitizeFormatting = ( formatting: ChartFormatting ): ChartFormatting => {
68
+ const repaired = { ...formatting, locale: toBcp47( formatting.locale ) };
69
+
70
+ if ( isUsable( repaired ) ) {
71
+ return repaired;
72
+ }
73
+
74
+ const locale = isUsable( { locale: repaired.locale } ) ? repaired.locale : undefined;
75
+ const timeZone = isUsable( { timeZone: repaired.timeZone } ) ? repaired.timeZone : undefined;
76
+
77
+ if ( locale === undefined && formatting.locale !== undefined ) {
78
+ warnOnce(
79
+ `locale:${ formatting.locale }`,
80
+ `locale ${ JSON.stringify(
81
+ formatting.locale
82
+ ) } is not a BCP-47 tag Intl accepts, so dates render in the browser's locale.`
83
+ );
84
+ }
85
+
86
+ if ( timeZone === undefined && formatting.timeZone !== undefined ) {
87
+ warnOnce(
88
+ `timeZone:${ formatting.timeZone }`,
89
+ `timeZone ${ JSON.stringify(
90
+ formatting.timeZone
91
+ ) } is not a zone Intl accepts, so dates render in the browser's zone. Pass an IANA name or a UTC offset such as "+05:30".`
92
+ );
93
+ }
94
+
95
+ return { locale, timeZone };
96
+ };
97
+
98
+ /**
99
+ * A date formatter bound to the host's locale and time zone.
100
+ *
101
+ * @param options - Date-part options, as for `Intl.DateTimeFormat`.
102
+ * @param formatting - Host locale and time zone; either half absent leaves the runtime's own.
103
+ * @return Formatter taking a timestamp or a `Date`.
104
+ */
105
+ export const createDateFormatter = (
106
+ options: Intl.DateTimeFormatOptions,
107
+ formatting: ChartFormatting
108
+ ) => {
109
+ return ( value: number | Date ): string => {
110
+ // `Intl.DateTimeFormat` throws on an invalid date where the `Date` methods
111
+ // it replaces return "Invalid Date", and charts are expected to render past
112
+ // a bad point rather than fail. visx hands its tick formatter a `Date`, so
113
+ // coerce before the check.
114
+ if ( ! Number.isFinite( Number( value ) ) ) {
115
+ return new Date( Number( value ) ).toLocaleDateString();
116
+ }
117
+ return getFormatter( options, formatting ).format( value );
118
+ };
119
+ };
120
+
121
+ // Enough of the calendar to place a tick on a boundary, read in the host's zone
122
+ // rather than the browser's. `en-US` pins the digits as Latin and the calendar as
123
+ // Gregorian: a non-Gregorian `locale` labels its own new year, but the boundary
124
+ // still fires on January 1.
125
+ const CLOCK_OPTIONS: Intl.DateTimeFormatOptions = {
126
+ month: 'numeric',
127
+ hour: 'numeric',
128
+ hourCycle: 'h23',
129
+ };
130
+
131
+ export type ZonedFields = {
132
+ /** 1-12. */
133
+ month: number;
134
+ /** 0-23. */
135
+ hour: number;
136
+ };
137
+
138
+ // What the `Date` getters answer for an invalid date, so a bad point leaves every
139
+ // boundary untriggered rather than throwing out of `formatToParts`.
140
+ const NO_FIELDS: ZonedFields = { month: NaN, hour: NaN };
141
+
142
+ /**
143
+ * A clock reading calendar fields in the host's time zone.
144
+ *
145
+ * `Date`'s own getters read the browser's zone, so a chart viewed from another
146
+ * country anchors its ticks to the wrong midnights and the wrong January
147
+ * without this.
148
+ *
149
+ * @param timeZone - IANA zone name; absent leaves the runtime's own.
150
+ * @return Reader taking a `Date`.
151
+ */
152
+ export const createZonedClock = ( timeZone?: string ) => {
153
+ // The `Date` getters already answer in the runtime zone, and cost nothing.
154
+ if ( ! timeZone ) {
155
+ return ( date: Date ): ZonedFields => ( {
156
+ month: date.getMonth() + 1,
157
+ hour: date.getHours(),
158
+ } );
159
+ }
160
+
161
+ return ( date: Date ): ZonedFields => {
162
+ if ( ! Number.isFinite( Number( date ) ) ) {
163
+ return NO_FIELDS;
164
+ }
165
+
166
+ const parts = getFormatter( CLOCK_OPTIONS, { locale: 'en-US', timeZone } ).formatToParts(
167
+ date
168
+ );
169
+ const read = ( type: Intl.DateTimeFormatPartTypes ) =>
170
+ Number( parts.find( part => part.type === type )?.value );
171
+
172
+ return { month: read( 'month' ), hour: read( 'hour' ) };
173
+ };
174
+ };
@@ -1,6 +1,9 @@
1
1
  // Chart composition utilities
2
2
  export { attachSubComponents } from './create-composition';
3
3
 
4
+ // Date bucket classification
5
+ export { getBucketInfo } from './bucket-info';
6
+
4
7
  // Date parsing utilities
5
8
  export { parseAsLocalDate } from './date-parsing';
6
9