@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.
- package/CHANGELOG.md +29 -0
- package/TOKENS.md +53 -58
- package/dist/index.cjs +1012 -542
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +72 -29
- package/dist/index.d.cts +151 -179
- package/dist/index.d.ts +151 -179
- package/dist/index.js +1013 -544
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
- package/src/charts/area-chart/area-chart.tsx +39 -25
- package/src/charts/area-chart/test/area-chart.test.tsx +4 -33
- package/src/charts/area-chart/test/tick-placement.test.tsx +55 -0
- package/src/charts/area-chart/test/tooltip-resolution-branches.test.tsx +86 -0
- package/src/charts/area-chart/test/tooltip-resolution.test.tsx +69 -0
- package/src/charts/area-chart/types.ts +4 -7
- package/src/charts/bar-chart/bar-chart.tsx +2 -1
- package/src/charts/bar-chart/private/test/use-bar-chart-options-formatting.test.tsx +69 -0
- package/src/charts/bar-chart/private/use-bar-chart-options.ts +27 -15
- package/src/charts/bar-list-chart/bar-list-chart.tsx +5 -0
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.module.scss +33 -23
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx +138 -182
- package/src/charts/conversion-funnel-chart/test/conversion-funnel-chart.test.tsx +46 -0
- package/src/charts/geo-chart/geo-chart.module.scss +1 -0
- package/src/charts/geo-chart/geo-chart.tsx +5 -10
- package/src/charts/geo-chart/test/geo-chart.test.tsx +2 -2
- package/src/charts/heatmap-chart/heatmap-chart.module.scss +21 -16
- package/src/charts/heatmap-chart/heatmap-chart.tsx +45 -28
- package/src/charts/heatmap-chart/private/build-calendar-data.ts +3 -0
- package/src/charts/heatmap-chart/private/heatmap-legend.tsx +11 -4
- package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +51 -12
- package/src/charts/heatmap-chart/types.ts +2 -2
- package/src/charts/leaderboard-chart/hooks/use-fitted-row-count.ts +2 -2
- package/src/charts/leaderboard-chart/hooks/use-leaderboard-legend-items.ts +3 -5
- package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +31 -19
- package/src/charts/leaderboard-chart/leaderboard-chart.tsx +27 -17
- package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +14 -0
- package/src/charts/leaderboard-chart/test/use-leaderboard-legend-items.test.tsx +2 -9
- package/src/charts/line-chart/line-chart.module.scss +10 -18
- package/src/charts/line-chart/line-chart.tsx +93 -24
- package/src/charts/line-chart/private/line-chart-annotation.tsx +18 -19
- package/src/charts/line-chart/test/default-tooltip-formatting.test.tsx +74 -0
- package/src/charts/line-chart/test/line-chart.test.tsx +27 -3
- package/src/charts/line-chart/test/tick-placement.test.tsx +84 -0
- package/src/charts/line-chart/test/tooltip-resolution.test.tsx +140 -0
- package/src/charts/line-chart/types.ts +4 -1
- package/src/charts/pie-chart/pie-chart.module.scss +13 -1
- package/src/charts/pie-chart/pie-chart.tsx +30 -40
- package/src/charts/pie-chart/test/pie-chart.test.tsx +56 -0
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.module.scss +8 -3
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +23 -37
- package/src/charts/pie-semi-circle-chart/test/pie-semi-circle-chart.test.tsx +50 -0
- package/src/charts/private/chart-layout/chart-layout.module.scss +3 -2
- package/src/charts/private/svg-empty-state/svg-empty-state.module.scss +1 -1
- package/src/charts/private/test/axis-formatting.test.tsx +85 -0
- package/src/charts/private/test/time-axis-formatting.test.ts +109 -0
- package/src/charts/private/test/time-axis-options.test.ts +267 -0
- package/src/charts/private/test/time-axis-tick-invariants.test.ts +153 -0
- package/src/charts/private/test/time-axis-tick-values.test.ts +280 -0
- package/src/charts/private/test/time-axis.test.ts +9 -69
- package/src/charts/private/time-axis-options.ts +106 -0
- package/src/charts/private/time-axis.ts +318 -136
- package/src/charts/private/x-zoom/x-zoom.module.scss +3 -3
- package/src/charts/private/xy-plot/xy-plot.module.scss +8 -0
- package/src/charts/sparkline/test/sparkline.test.tsx +29 -56
- package/src/charts/sparkline/types.ts +1 -1
- package/src/components/legend/private/base-legend.module.scss +9 -5
- package/src/components/tooltip/accessible-tooltip.tsx +13 -15
- package/src/components/tooltip/base-tooltip.module.scss +3 -3
- package/src/components/tooltip/private/bounded-tooltip.tsx +172 -0
- package/src/components/tooltip/test/accessible-tooltip.test.tsx +2 -3
- package/src/components/tooltip/test/bounded-tooltip.test.tsx +129 -0
- package/src/components/tooltip/test/xy-chart-tooltip.test.tsx +279 -0
- package/src/components/tooltip/xy-chart-tooltip.tsx +288 -0
- package/src/components/trend-indicator/trend-indicator.module.scss +2 -2
- package/src/hooks/test/use-xychart-theme.test.tsx +16 -3
- package/src/hooks/use-xychart-theme.ts +25 -9
- package/src/index.ts +4 -0
- package/src/providers/chart-context/global-charts-provider.tsx +37 -38
- package/src/providers/chart-context/hooks/use-chart-formatting.ts +22 -0
- package/src/providers/chart-context/index.ts +1 -0
- package/src/providers/chart-context/private/catalog-pointers.ts +42 -0
- package/src/providers/chart-context/private/series-palette.ts +3 -13
- package/src/providers/chart-context/test/{sparse-theme.test.ts → catalog-pointers.test.ts} +22 -10
- package/src/providers/chart-context/test/chart-context.test.tsx +177 -792
- package/src/providers/chart-context/test/use-global-charts-theme.test.tsx +7 -25
- package/src/providers/chart-context/themes.ts +7 -52
- package/src/providers/chart-context/types.ts +3 -0
- package/src/styles/chart-scope.scss +39 -62
- package/src/styles/test/chart-scope.test.ts +19 -47
- package/src/test-utils/series-fixtures.ts +31 -0
- package/src/types.ts +79 -166
- package/src/utils/bucket-info.ts +87 -0
- package/src/utils/color-utils.ts +0 -27
- package/src/utils/date-formatting.ts +174 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/test/bucket-info.test.ts +185 -0
- package/src/utils/test/color-utils.test.ts +1 -48
- package/src/utils/test/date-formatting.test.ts +89 -0
- package/src/utils/test/get-styles.test.ts +10 -7
- package/src/utils/test/merge-themes.test.ts +18 -46
- package/src/visx/types.ts +29 -2
- package/src/hooks/test/use-xychart-theme-provider-override.test.tsx +0 -28
- package/src/providers/chart-context/private/theme-override-vars.ts +0 -131
- package/src/providers/chart-context/private/with-catalog-pointers.ts +0 -93
- package/src/providers/chart-context/test/theme-override-vars-guard.test.tsx +0 -29
- package/src/providers/chart-context/test/theme-override-vars.test.ts +0 -183
- package/src/providers/chart-context/test/with-catalog-pointers.test.ts +0 -104
- 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
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
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'
|
|
123
|
-
|
|
124
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
-
|
|
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
|
|
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?:
|
|
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<
|
|
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
|
|
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:
|
|
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:
|
|
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
|
+
} );
|
package/src/utils/color-utils.ts
CHANGED
|
@@ -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
|
+
};
|
package/src/utils/index.ts
CHANGED