@automattic/charts 3.1.1 → 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 +41 -0
- package/TOKENS.md +80 -46
- package/dist/index.cjs +1224 -662
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +72 -23
- package/dist/index.d.cts +230 -162
- package/dist/index.d.ts +230 -162
- package/dist/index.js +1224 -663
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
- 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 +38 -11
- 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-chart/test/bar-chart-pointer-events.test.tsx +67 -0
- package/src/charts/bar-chart/test/bar-chart.test.tsx +79 -0
- package/src/charts/bar-list-chart/bar-list-chart.tsx +45 -2
- package/src/charts/bar-list-chart/test/bar-list-chart.test.tsx +38 -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 +109 -25
- 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 +74 -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/sparkline.tsx +9 -3
- package/src/charts/sparkline/test/sparkline.test.tsx +29 -56
- package/src/charts/sparkline/types.ts +2 -2
- package/src/components/legend/private/base-legend.module.scss +9 -5
- package/src/components/tooltip/accessible-tooltip.tsx +38 -10
- 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 +67 -0
- 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-style-reads.test.tsx +17 -10
- package/src/hooks/test/use-xychart-theme.test.tsx +63 -5
- package/src/hooks/use-xychart-theme.ts +33 -23
- package/src/index.ts +4 -0
- package/src/providers/chart-context/global-charts-provider.tsx +64 -54
- 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 +35 -0
- package/src/providers/chart-context/test/catalog-pointers.test.ts +61 -0
- 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 -18
- package/src/providers/chart-context/themes.ts +7 -47
- package/src/providers/chart-context/types.ts +3 -0
- package/src/styles/chart-scope.scss +63 -58
- package/src/styles/test/chart-scope.test.ts +25 -40
- package/src/test-utils/series-fixtures.ts +31 -0
- package/src/types.ts +103 -73
- 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/get-styles.ts +7 -1
- 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 -87
- package/src/providers/chart-context/private/with-catalog-pointers.ts +0 -50
- package/src/providers/chart-context/test/sparse-theme.test.ts +0 -35
- 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 -118
- package/src/providers/chart-context/test/with-catalog-pointers.test.ts +0 -47
- package/src/styles/test/wpds-fallbacks.test.ts +0 -131
package/src/types.ts
CHANGED
|
@@ -108,8 +108,15 @@ export type ChartType =
|
|
|
108
108
|
|
|
109
109
|
export type OrientationType = ValueOf< typeof Orientation >;
|
|
110
110
|
|
|
111
|
+
/**
|
|
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
|
+
*/
|
|
111
116
|
export type AnnotationStyles = {
|
|
112
|
-
circleSubject?: Omit< CircleSubjectProps, 'x' | 'y' > & {
|
|
117
|
+
circleSubject?: Omit< CircleSubjectProps, 'x' | 'y' | 'fill' > & {
|
|
118
|
+
fill?: string;
|
|
119
|
+
};
|
|
113
120
|
lineSubject?: Omit< LineSubjectProps, 'x' | 'y' >;
|
|
114
121
|
connector?: Omit< ConnectorProps, 'x' | 'y' | 'dx' | 'dy' >;
|
|
115
122
|
label?: Omit< LabelProps, 'title' | 'subtitle' | 'x' | 'y' > & {
|
|
@@ -118,6 +125,23 @@ export type AnnotationStyles = {
|
|
|
118
125
|
};
|
|
119
126
|
};
|
|
120
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' >;
|
|
136
|
+
label?: Omit<
|
|
137
|
+
LabelProps,
|
|
138
|
+
'title' | 'subtitle' | 'x' | 'y' | 'anchorLineStroke' | 'backgroundFill'
|
|
139
|
+
> & {
|
|
140
|
+
x?: number | 'start' | 'end';
|
|
141
|
+
y?: number | 'start' | 'end';
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
|
|
121
145
|
export type DataPoint = {
|
|
122
146
|
label: string;
|
|
123
147
|
value: number;
|
|
@@ -305,7 +329,7 @@ export type DataPointPercentage = {
|
|
|
305
329
|
*/
|
|
306
330
|
valueDisplay?: string;
|
|
307
331
|
/**
|
|
308
|
-
* Color code for the segment, by default
|
|
332
|
+
* Color code for the segment, by default colors are taken from the theme but this property can overrides it
|
|
309
333
|
*/
|
|
310
334
|
color?: string;
|
|
311
335
|
/**
|
|
@@ -326,80 +350,55 @@ export type DataPointPercentageCalculated = DataPointPercentage & {
|
|
|
326
350
|
};
|
|
327
351
|
|
|
328
352
|
/**
|
|
329
|
-
* 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`.
|
|
330
356
|
*/
|
|
331
357
|
export type ChartTheme = {
|
|
332
|
-
/**
|
|
333
|
-
|
|
334
|
-
/** Background color for labels */
|
|
335
|
-
labelBackgroundColor?: string;
|
|
336
|
-
/** Text color for labels */
|
|
337
|
-
labelTextColor?: string;
|
|
338
|
-
/** Array of colors used for data visualization */
|
|
339
|
-
colors: string[];
|
|
340
|
-
/** Optional CSS styles for grid lines */
|
|
341
|
-
gridStyles?: GridStyles;
|
|
358
|
+
/** Optional CSS styles for grid lines. */
|
|
359
|
+
gridStyles?: Omit< GridStyles, 'stroke' >;
|
|
342
360
|
/** Length of axis ticks in pixels */
|
|
343
361
|
tickLength: number;
|
|
344
|
-
/** Color of the grid lines */
|
|
345
|
-
gridColor: string;
|
|
346
|
-
/** Color of the grid lines in dark mode */
|
|
347
|
-
gridColorDark: string;
|
|
348
362
|
/** Styles for x-axis tick lines */
|
|
349
|
-
xTickLineStyles?: LineStyles
|
|
363
|
+
xTickLineStyles?: Omit< LineStyles, 'stroke' >;
|
|
350
364
|
/** Styles for x-axis line */
|
|
351
|
-
xAxisLineStyles?: LineStyles
|
|
352
|
-
/**
|
|
353
|
-
|
|
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' > >;
|
|
354
372
|
/** Array of render functions for glyphs */
|
|
355
373
|
glyphs?: Array< < Datum extends object >( props: GlyphProps< Datum > ) => ReactNode >;
|
|
356
374
|
/** Legend specific settings */
|
|
357
375
|
legend?: {
|
|
358
376
|
/** Styles for legend shapes */
|
|
359
377
|
shapeStyles?: Record< string, unknown >[];
|
|
360
|
-
/** Styles for legend labels */
|
|
361
|
-
labelStyles?: CSSProperties
|
|
378
|
+
/** Styles for legend labels. */
|
|
379
|
+
labelStyles?: Omit< CSSProperties, 'color' >;
|
|
362
380
|
/** Styles for legend container */
|
|
363
381
|
containerStyles?: CSSProperties;
|
|
364
382
|
};
|
|
365
|
-
/**
|
|
366
|
-
|
|
383
|
+
/**
|
|
384
|
+
* Styles for small SVG text (eg. axis tick labels), passed through to the XYChart theme.
|
|
385
|
+
*
|
|
386
|
+
* `fontFamily: 'inherit'` is load-bearing here — it overrides the font stack
|
|
387
|
+
* `buildChartTheme` injects inline on SVG `<text>`.
|
|
388
|
+
*/
|
|
389
|
+
svgLabelSmall?: Omit< TextProps, 'fill' >;
|
|
367
390
|
/** Styles for large SVG text (eg. axis titles), passed through to the XYChart theme. */
|
|
368
391
|
svgLabelBig?: TextProps;
|
|
369
|
-
annotationStyles?:
|
|
370
|
-
/** GeoChart specific settings */
|
|
371
|
-
geoChart?: {
|
|
372
|
-
/** Default fill color for a geo chart feature (e.g. country) with no data */
|
|
373
|
-
featureFillColor?: string;
|
|
374
|
-
};
|
|
392
|
+
annotationStyles?: ThemeAnnotationStyles;
|
|
375
393
|
/** LeaderboardChart specific settings */
|
|
376
394
|
leaderboardChart?: {
|
|
377
|
-
/** Gap between rows in the leaderboard grid */
|
|
378
|
-
rowGap?: number;
|
|
379
|
-
/** Gap between columns in the leaderboard grid */
|
|
380
|
-
columnGap?: number;
|
|
381
395
|
/** Spacing between label and progress bars */
|
|
382
396
|
labelSpacing?: GapSize;
|
|
383
|
-
/** Primary color for current period bars */
|
|
384
|
-
primaryColor?: string;
|
|
385
|
-
/** Secondary color for comparison period bars */
|
|
386
|
-
secondaryColor?: string;
|
|
387
|
-
/** Delta colors: [negative, neutral, positive] */
|
|
388
|
-
deltaColors?: [ string, string, string ];
|
|
389
|
-
};
|
|
390
|
-
/** ConversionFunnelChart specific settings */
|
|
391
|
-
conversionFunnelChart?: {
|
|
392
|
-
/** Primary color for funnel bars */
|
|
393
|
-
primaryColor?: string;
|
|
394
|
-
/** Background color for chart container */
|
|
395
|
-
backgroundColor?: string;
|
|
396
|
-
/** Color for positive change indicators */
|
|
397
|
-
positiveChangeColor?: string;
|
|
398
|
-
/** Color for negative change indicators */
|
|
399
|
-
negativeChangeColor?: string;
|
|
400
397
|
};
|
|
401
398
|
lineChart?: {
|
|
402
|
-
lineStyles?: Partial<
|
|
399
|
+
lineStyles?: Partial<
|
|
400
|
+
Record< NonNullable< SeriesDataOptions[ 'type' ] >, Omit< LineStyles, 'stroke' > >
|
|
401
|
+
>;
|
|
403
402
|
};
|
|
404
403
|
barChart?: {
|
|
405
404
|
barStyles?: Partial< Record< NonNullable< SeriesDataOptions[ 'type' ] >, BarStyles > >;
|
|
@@ -418,14 +417,9 @@ export type ChartTheme = {
|
|
|
418
417
|
};
|
|
419
418
|
/**
|
|
420
419
|
* HeatmapChart settings. Cell gap, radius, value size and the selection ring come from
|
|
421
|
-
* WPDS tokens in CSS, so only the
|
|
420
|
+
* WPDS tokens in CSS, so only the compact sizing lives here.
|
|
422
421
|
*/
|
|
423
422
|
heatmapChart?: {
|
|
424
|
-
/**
|
|
425
|
-
* Color the cell scale interpolates toward at the highest value (prop > this >
|
|
426
|
-
* palette `colors[0]`), fed to CSS `color-mix`. Omit to use the palette color.
|
|
427
|
-
*/
|
|
428
|
-
primaryColor?: string;
|
|
429
423
|
/** Gap in px between cells in compact mode */
|
|
430
424
|
compactCellGap?: number;
|
|
431
425
|
/** Fixed square cell size in px for compact mode */
|
|
@@ -438,18 +432,9 @@ export type ChartTheme = {
|
|
|
438
432
|
* Useful for merged themes where defaults are provided for all optional properties.
|
|
439
433
|
*/
|
|
440
434
|
export type CompleteChartTheme = Required< ChartTheme > & {
|
|
441
|
-
leaderboardChart:
|
|
442
|
-
Required< NonNullable< ChartTheme[ 'leaderboardChart' ] > >,
|
|
443
|
-
'primaryColor' | 'secondaryColor'
|
|
444
|
-
> &
|
|
445
|
-
Pick< NonNullable< ChartTheme[ 'leaderboardChart' ] >, 'primaryColor' | 'secondaryColor' >;
|
|
446
|
-
conversionFunnelChart: Omit<
|
|
447
|
-
Required< NonNullable< ChartTheme[ 'conversionFunnelChart' ] > >,
|
|
448
|
-
'primaryColor'
|
|
449
|
-
> &
|
|
450
|
-
Pick< NonNullable< ChartTheme[ 'conversionFunnelChart' ] >, 'primaryColor' >;
|
|
435
|
+
leaderboardChart: Required< NonNullable< ChartTheme[ 'leaderboardChart' ] > >;
|
|
451
436
|
lineChart: {
|
|
452
|
-
lineStyles: Record< NonNullable< SeriesDataOptions[ 'type' ] >, LineStyles >;
|
|
437
|
+
lineStyles: Record< NonNullable< SeriesDataOptions[ 'type' ] >, Omit< LineStyles, 'stroke' > >;
|
|
453
438
|
};
|
|
454
439
|
barChart: {
|
|
455
440
|
barStyles: Record< NonNullable< SeriesDataOptions[ 'type' ] >, BarStyles >;
|
|
@@ -458,8 +443,7 @@ export type CompleteChartTheme = Required< ChartTheme > & {
|
|
|
458
443
|
sparkline: Required< NonNullable< ChartTheme[ 'sparkline' ] > > & {
|
|
459
444
|
margin: Required< NonNullable< ChartTheme[ 'sparkline' ] >[ 'margin' ] >;
|
|
460
445
|
};
|
|
461
|
-
heatmapChart:
|
|
462
|
-
Pick< NonNullable< ChartTheme[ 'heatmapChart' ] >, 'primaryColor' >;
|
|
446
|
+
heatmapChart: Required< NonNullable< ChartTheme[ 'heatmapChart' ] > >;
|
|
463
447
|
};
|
|
464
448
|
|
|
465
449
|
/**
|
|
@@ -468,6 +452,42 @@ export type CompleteChartTheme = Required< ChartTheme > & {
|
|
|
468
452
|
*/
|
|
469
453
|
export type TickResolution = 'hour' | 'day' | 'week' | 'month' | 'year';
|
|
470
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
|
+
|
|
471
491
|
export type AxisOptions = {
|
|
472
492
|
orientation?: OrientationType;
|
|
473
493
|
numTicks?: number;
|
|
@@ -626,7 +646,7 @@ export type ChartLegendConfig< T = DataPoint | DataPointDate | LeaderboardEntry
|
|
|
626
646
|
/**
|
|
627
647
|
* Legend config for charts built from `SeriesData` (line, bar, area). Adds `collapseGroups` on top
|
|
628
648
|
* of the shared config. It is intentionally absent from the base `ChartLegendConfig` so point-based
|
|
629
|
-
* charts (pie, semi-circle pie) — whose data points carry `group` only to coordinate
|
|
649
|
+
* charts (pie, semi-circle pie) — whose data points carry `group` only to coordinate colors — can't
|
|
630
650
|
* set it.
|
|
631
651
|
*/
|
|
632
652
|
export type SeriesChartLegendConfig = ChartLegendConfig< SeriesData[] > & {
|
|
@@ -706,6 +726,16 @@ export type BaseChartProps< T = DataPoint | DataPointDate | LeaderboardEntry > =
|
|
|
706
726
|
* Callback function for pointer out event
|
|
707
727
|
*/
|
|
708
728
|
onPointerOut?: ( event: PointerEvent< Element > ) => void;
|
|
729
|
+
/**
|
|
730
|
+
* Callback for Enter or Space on the point keyboard navigation has selected:
|
|
731
|
+
* the keyboard counterpart of a click. `index` is the point's position in its
|
|
732
|
+
* series and `key` the series label.
|
|
733
|
+
*/
|
|
734
|
+
onDatumActivate?: ( params: {
|
|
735
|
+
datum: DataPoint | DataPointDate;
|
|
736
|
+
index: number;
|
|
737
|
+
key: string;
|
|
738
|
+
} ) => void;
|
|
709
739
|
/**
|
|
710
740
|
* Whether to show tooltips on hover. False by default.
|
|
711
741
|
*/
|
|
@@ -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/get-styles.ts
CHANGED
|
@@ -50,9 +50,15 @@ export function getSeriesBarStyles(
|
|
|
50
50
|
/**
|
|
51
51
|
* Utility function to get stroke color for a series
|
|
52
52
|
*
|
|
53
|
+
* `themeColors` must already be resolved. `useGlobalChartsTheme().colors` is not: its entries are
|
|
54
|
+
* the palette's `var()` chains, which an SVG presentation attribute cannot resolve. Prefer
|
|
55
|
+
* `getElementStyles( { data, index } ).color` from
|
|
56
|
+
* `useGlobalChartsContext()`, which resolves at the chart's scope element and generates past the
|
|
57
|
+
* seeds.
|
|
58
|
+
*
|
|
53
59
|
* @param {SeriesData} seriesData - The series data containing styling options
|
|
54
60
|
* @param {number} index - The index of the series in the data array
|
|
55
|
-
* @param {string[]} themeColors -
|
|
61
|
+
* @param {string[]} themeColors - The resolved series palette
|
|
56
62
|
* @return {string} The stroke color for the series
|
|
57
63
|
*/
|
|
58
64
|
export function getSeriesStroke(
|
package/src/utils/index.ts
CHANGED