@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
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
display: inline-flex;
|
|
3
3
|
align-items: center;
|
|
4
4
|
gap: 0.125em;
|
|
5
|
-
font-size: var(--wpds-typography-font-size-md
|
|
6
|
-
font-weight: var(--wpds-typography-font-weight-emphasis
|
|
5
|
+
font-size: var(--wpds-typography-font-size-md);
|
|
6
|
+
font-weight: var(--wpds-typography-font-weight-emphasis);
|
|
7
7
|
line-height: 1;
|
|
8
8
|
|
|
9
9
|
&--up {
|
|
@@ -4,7 +4,7 @@ import { useXYChartTheme } from '../use-xychart-theme';
|
|
|
4
4
|
import type { SeriesData } from '../../types';
|
|
5
5
|
import type { ReactNode } from 'react';
|
|
6
6
|
|
|
7
|
-
// The theme resolves
|
|
7
|
+
// The theme resolves the background role and the five palette slots. Each `getComputedStyle` call can force the browser to flush pending style, and this memo re-runs only when the scope element attaches or a series color changes, so a dashboard mounting N charts pays the count below N times over.
|
|
8
8
|
const STABLE_DATA: SeriesData[] = [];
|
|
9
9
|
|
|
10
10
|
describe( 'useXYChartTheme style reads', () => {
|
|
@@ -13,7 +13,6 @@ describe( 'useXYChartTheme style reads', () => {
|
|
|
13
13
|
|
|
14
14
|
beforeEach( () => {
|
|
15
15
|
scope = document.createElement( 'div' );
|
|
16
|
-
scope.style.setProperty( '--a8c-charts-color-grid', '#00ff00' );
|
|
17
16
|
document.body.appendChild( scope );
|
|
18
17
|
getComputedStyleSpy = jest.spyOn( window, 'getComputedStyle' );
|
|
19
18
|
} );
|
|
@@ -33,14 +32,13 @@ describe( 'useXYChartTheme style reads', () => {
|
|
|
33
32
|
|
|
34
33
|
it( 'reads every role from one computed-style snapshot, not one per role', () => {
|
|
35
34
|
scope.style.setProperty( '--a8c-charts-color-background', '#111111' );
|
|
36
|
-
scope.style.setProperty( '--a8c-charts-color-
|
|
35
|
+
scope.style.setProperty( '--a8c-charts-color-series-2', '#222222' );
|
|
37
36
|
|
|
38
37
|
const { result } = renderWithScope();
|
|
39
38
|
|
|
40
39
|
expect( getComputedStyleSpy ).toHaveBeenCalledTimes( 1 );
|
|
41
40
|
expect( result.current.backgroundColor ).toBe( '#111111' );
|
|
42
|
-
expect( result.current.
|
|
43
|
-
expect( result.current.axisStyles.x.bottom.axisLine.stroke ).toBe( '#222222' );
|
|
41
|
+
expect( result.current.colors ).toContain( '#222222' );
|
|
44
42
|
} );
|
|
45
43
|
|
|
46
44
|
it( 'does not re-read styles when the hook re-renders with the same inputs', () => {
|
|
@@ -74,16 +72,25 @@ describe( 'useXYChartTheme style reads', () => {
|
|
|
74
72
|
expect( result.current.colors[ 0 ] ).toBe( 'rebeccapurple' );
|
|
75
73
|
} );
|
|
76
74
|
|
|
77
|
-
// The memo key
|
|
75
|
+
// The memo key serializes the strokes rather than joining them, so a color containing a comma or a space survives the round-trip intact. `rgba(0, 0, 0, 0.5)` is the one that proves it: it holds both separators and comes back whole.
|
|
76
|
+
//
|
|
77
|
+
// The `var()` entry resolves to its fallback rather than passing through, which is the point of resolving the palette at all — visx paints these as SVG presentation attributes, where a `var()` resolves to nothing.
|
|
78
78
|
it( 'keeps a stroke that contains separators intact', () => {
|
|
79
79
|
const { result } = renderWithScope( [
|
|
80
80
|
{ label: 'A', options: { stroke: 'rgba(0, 0, 0, 0.5)' }, data: [] },
|
|
81
81
|
{ label: 'B', options: { stroke: 'var(--brand, #fff)' }, data: [] },
|
|
82
82
|
] as unknown as SeriesData[] );
|
|
83
83
|
|
|
84
|
-
expect( result.current.colors.slice( 0, 2 ) ).toEqual( [
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
expect( result.current.colors.slice( 0, 2 ) ).toEqual( [ 'rgba(0, 0, 0, 0.5)', '#fff' ] );
|
|
85
|
+
} );
|
|
86
|
+
|
|
87
|
+
// Four of the five palette slots carry no catalog default, so in jsdom they resolve to nothing. Passing those to visx unresolved would make them its default stroke for any series without an explicit one, painting nothing at all.
|
|
88
|
+
it( 'drops palette entries that resolve to nothing', () => {
|
|
89
|
+
const { result } = renderWithScope( [] );
|
|
90
|
+
|
|
91
|
+
expect( result.current.colors ).not.toEqual(
|
|
92
|
+
expect.arrayContaining( [ expect.stringContaining( 'var(' ) ] )
|
|
93
|
+
);
|
|
94
|
+
expect( result.current.colors.length ).toBeGreaterThan( 0 );
|
|
88
95
|
} );
|
|
89
96
|
} );
|
|
@@ -4,11 +4,11 @@ import { useXYChartTheme } from '../use-xychart-theme';
|
|
|
4
4
|
import type { ReactNode } from 'react';
|
|
5
5
|
|
|
6
6
|
describe( 'useXYChartTheme', () => {
|
|
7
|
-
it( 'resolves
|
|
8
|
-
document.documentElement.style.setProperty( '--a8c-charts-color-
|
|
7
|
+
it( 'resolves background color against the scope element, not the document root', () => {
|
|
8
|
+
document.documentElement.style.setProperty( '--a8c-charts-color-background', '#ff0000' );
|
|
9
9
|
|
|
10
10
|
const scope = document.createElement( 'div' );
|
|
11
|
-
scope.style.setProperty( '--a8c-charts-color-
|
|
11
|
+
scope.style.setProperty( '--a8c-charts-color-background', '#00ff00' );
|
|
12
12
|
document.body.appendChild( scope );
|
|
13
13
|
|
|
14
14
|
const wrapper = ( { children }: { children: ReactNode } ) => (
|
|
@@ -17,9 +17,67 @@ describe( 'useXYChartTheme', () => {
|
|
|
17
17
|
|
|
18
18
|
const { result } = renderHook( () => useXYChartTheme( [] ), { wrapper } );
|
|
19
19
|
|
|
20
|
-
expect( result.current.
|
|
20
|
+
expect( result.current.backgroundColor ).toBe( '#00ff00' );
|
|
21
21
|
|
|
22
|
-
document.documentElement.style.removeProperty( '--a8c-charts-color-
|
|
22
|
+
document.documentElement.style.removeProperty( '--a8c-charts-color-background' );
|
|
23
23
|
document.body.removeChild( scope );
|
|
24
24
|
} );
|
|
25
|
+
|
|
26
|
+
// jsdom does not compute `var()`, so what a pointer resolves *to* is covered in Storybook.
|
|
27
|
+
it( 'hands visx the catalog pointer for paint-only colors rather than a resolved value', () => {
|
|
28
|
+
const scope = document.createElement( 'div' );
|
|
29
|
+
document.body.appendChild( scope );
|
|
30
|
+
|
|
31
|
+
const wrapper = ( { children }: { children: ReactNode } ) => (
|
|
32
|
+
<ChartScopeContext.Provider value={ scope }>{ children }</ChartScopeContext.Provider>
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const { result } = renderHook( () => useXYChartTheme( [] ), { wrapper } );
|
|
36
|
+
|
|
37
|
+
expect( result.current.gridStyles.stroke ).toBe( 'var(--a8c-charts-color-grid, #dbdbdb)' );
|
|
38
|
+
expect( result.current.axisStyles.x.bottom.axisLine.stroke ).toBe(
|
|
39
|
+
'var(--a8c-charts-color-axis-x, #dbdbdb)'
|
|
40
|
+
);
|
|
41
|
+
expect( result.current.axisStyles.x.bottom.tickLine.stroke ).toBe(
|
|
42
|
+
'var(--a8c-charts-color-tick-x, #dbdbdb)'
|
|
43
|
+
);
|
|
44
|
+
expect( result.current.axisStyles.x.bottom.tickLabel.fill ).toBe(
|
|
45
|
+
'var(--a8c-charts-color-label-axis, #1e1e1e)'
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
document.body.removeChild( scope );
|
|
49
|
+
} );
|
|
50
|
+
|
|
51
|
+
// The one label color that has to arrive resolved: visx paints it on a portal container appended to `document.body`, where the catalog is not declared, and concatenates it into `box-shadow: 0 1px 2px ${color}55`, which a `var()` chain would invalidate.
|
|
52
|
+
it( 'resolves the tooltip label color while the tick labels keep the pointer', () => {
|
|
53
|
+
const scope = document.createElement( 'div' );
|
|
54
|
+
scope.style.setProperty( '--a8c-charts-color-label-axis', '#0000ff' );
|
|
55
|
+
document.body.appendChild( scope );
|
|
56
|
+
|
|
57
|
+
const wrapper = ( { children }: { children: ReactNode } ) => (
|
|
58
|
+
<ChartScopeContext.Provider value={ scope }>{ children }</ChartScopeContext.Provider>
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const { result } = renderHook( () => useXYChartTheme( [] ), { wrapper } );
|
|
62
|
+
|
|
63
|
+
expect( result.current.htmlLabel.color ).toBe( '#0000ff' );
|
|
64
|
+
expect( result.current.svgLabelSmall.fill ).toBe(
|
|
65
|
+
'var(--a8c-charts-color-label-axis, #1e1e1e)'
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
document.body.removeChild( scope );
|
|
69
|
+
} );
|
|
70
|
+
|
|
71
|
+
it( 'gives the y axis its own roles, resolving to none', () => {
|
|
72
|
+
const { result } = renderHook( () => useXYChartTheme( [] ) );
|
|
73
|
+
|
|
74
|
+
for ( const side of [ 'left', 'right' ] as const ) {
|
|
75
|
+
expect( result.current.axisStyles.y[ side ].axisLine.stroke ).toBe(
|
|
76
|
+
'var(--a8c-charts-color-axis-y, none)'
|
|
77
|
+
);
|
|
78
|
+
expect( result.current.axisStyles.y[ side ].tickLine.stroke ).toBe(
|
|
79
|
+
'var(--a8c-charts-color-tick-y, none)'
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
} );
|
|
25
83
|
} );
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { buildChartTheme } from '@visx/xychart';
|
|
2
2
|
import { useMemo } from 'react';
|
|
3
3
|
import { useGlobalChartsTheme } from '../providers';
|
|
4
|
+
import { CATALOG_POINTERS } from '../providers/chart-context/private/catalog-pointers';
|
|
4
5
|
import { useChartScopeElement } from '../providers/chart-scope';
|
|
5
|
-
import { createCssVariableResolver } from '../utils';
|
|
6
|
+
import { createCssVariableResolver, normalizeColorToHex } from '../utils';
|
|
6
7
|
import type { SeriesData } from '../types';
|
|
7
8
|
|
|
8
9
|
export const useXYChartTheme = ( data: SeriesData[] ) => {
|
|
9
10
|
const theme = useGlobalChartsTheme();
|
|
10
11
|
const scopeElement = useChartScopeElement();
|
|
11
12
|
|
|
12
|
-
// The only thing the theme takes from `data` is the series strokes, so key the memo on those rather than on the array's identity. A caller passing an inline literal — `<LineChart data={ [ … ] } />`, which the stories and several consumers do — otherwise rebuilds the whole theme on every render.
|
|
13
|
+
// The only thing the theme takes from `data` is the series strokes, so key the memo on those rather than on the array's identity. A caller passing an inline literal — `<LineChart data={ [ … ] } />`, which the stories and several consumers do — otherwise rebuilds the whole theme on every render. Serialized rather than joined: a stroke can be `rgba(0, 0, 0, 0.5)` or `var(--brand, #fff)`, and any separator that reads naturally inside a color cannot round-trip.
|
|
13
14
|
const seriesColorKey = JSON.stringify(
|
|
14
15
|
( data ?? [] )
|
|
15
16
|
.map( series => series.options?.stroke )
|
|
@@ -17,35 +18,44 @@ export const useXYChartTheme = ( data: SeriesData[] ) => {
|
|
|
17
18
|
);
|
|
18
19
|
|
|
19
20
|
return useMemo( () => {
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
// One resolver
|
|
21
|
+
// Only what is read as a string is resolved: the palette, `backgroundColor`, `htmlLabel.color`.
|
|
22
|
+
// At the chart's scope element, never :root, so an override inside the provider tree is seen.
|
|
23
|
+
// One resolver for all three, so they share a single getComputedStyle call.
|
|
23
24
|
const resolve = createCssVariableResolver( scopeElement );
|
|
24
25
|
const resolveColor = ( value?: string ): string | undefined =>
|
|
25
26
|
value ? resolve( value ) ?? value : value;
|
|
26
27
|
|
|
27
28
|
const seriesColors: string[] = JSON.parse( seriesColorKey );
|
|
28
29
|
|
|
30
|
+
// visx uses this as the default stroke for a series without one, so an
|
|
31
|
+
// unresolved entry paints nothing.
|
|
32
|
+
const paletteColors = [ ...seriesColors, ...CATALOG_POINTERS.series ]
|
|
33
|
+
.map( color => resolveColor( color ) )
|
|
34
|
+
.filter( ( color ): color is string => Boolean( color ) && ! color.includes( 'var(' ) );
|
|
35
|
+
|
|
36
|
+
// The tooltip is painted in a portal outside the scope, and visx concatenates this color into a `box-shadow` where a chain cannot take a suffix; see TOKENS.md#the-svg-bridge. Passing it explicitly leaves `svgLabelSmall.fill`, which visx derives it from, a chain for the SVG tick labels.
|
|
37
|
+
// Hex specifically: that concatenation appends `55`, which only yields a color
|
|
38
|
+
// after a 6-digit hex. An `rgb()` computed value takes the whole shadow down.
|
|
39
|
+
const resolvedLabelColor = resolveColor( CATALOG_POINTERS.labelAxis );
|
|
40
|
+
const htmlLabelColor = resolvedLabelColor
|
|
41
|
+
? normalizeColorToHex( resolvedLabelColor ) || resolvedLabelColor
|
|
42
|
+
: resolvedLabelColor;
|
|
43
|
+
|
|
44
|
+
// visx's fallbacks for whichever axis or grid style object it is not given. All four are
|
|
45
|
+
// supplied, so these reach nothing; they stay because the config type requires them.
|
|
29
46
|
return buildChartTheme( {
|
|
30
47
|
...theme,
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
},
|
|
37
|
-
xAxisLineStyles: theme.xAxisLineStyles
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
},
|
|
41
|
-
|
|
42
|
-
...theme.xTickLineStyles,
|
|
43
|
-
stroke: resolveColor( theme.xTickLineStyles.stroke ),
|
|
44
|
-
},
|
|
45
|
-
svgLabelSmall: theme.svgLabelSmall && {
|
|
46
|
-
...theme.svgLabelSmall,
|
|
47
|
-
fill: resolveColor( theme.svgLabelSmall.fill ),
|
|
48
|
-
},
|
|
48
|
+
gridColor: '',
|
|
49
|
+
gridColorDark: '',
|
|
50
|
+
colors: paletteColors,
|
|
51
|
+
backgroundColor: resolveColor( CATALOG_POINTERS.background ),
|
|
52
|
+
htmlLabel: htmlLabelColor ? { color: htmlLabelColor } : undefined,
|
|
53
|
+
gridStyles: { ...theme.gridStyles, stroke: CATALOG_POINTERS.grid },
|
|
54
|
+
xAxisLineStyles: { ...theme.xAxisLineStyles, stroke: CATALOG_POINTERS.axisX },
|
|
55
|
+
xTickLineStyles: { ...theme.xTickLineStyles, stroke: CATALOG_POINTERS.tickX },
|
|
56
|
+
yAxisLineStyles: { stroke: CATALOG_POINTERS.axisY },
|
|
57
|
+
yTickLineStyles: { stroke: CATALOG_POINTERS.tickY },
|
|
58
|
+
svgLabelSmall: { ...theme.svgLabelSmall, fill: CATALOG_POINTERS.labelAxis },
|
|
49
59
|
} );
|
|
50
60
|
}, [ theme, seriesColorKey, scopeElement ] );
|
|
51
61
|
};
|
package/src/index.ts
CHANGED
|
@@ -32,6 +32,7 @@ export {
|
|
|
32
32
|
GlobalChartsProvider,
|
|
33
33
|
useGlobalChartsContext,
|
|
34
34
|
useGlobalChartsTheme,
|
|
35
|
+
useChartFormatting,
|
|
35
36
|
GlobalChartsContext,
|
|
36
37
|
defaultTheme,
|
|
37
38
|
useChartScopeElement,
|
|
@@ -55,6 +56,7 @@ export type {
|
|
|
55
56
|
ChartTheme,
|
|
56
57
|
CompleteChartTheme,
|
|
57
58
|
AxisOptions,
|
|
59
|
+
ChartFormatting,
|
|
58
60
|
TickResolution,
|
|
59
61
|
ScaleOptions,
|
|
60
62
|
LegendItemStyles,
|
|
@@ -126,6 +128,7 @@ export type { SparklineProps, GradientConfig, SparklineDataPoint } from './chart
|
|
|
126
128
|
|
|
127
129
|
// Utilities
|
|
128
130
|
export {
|
|
131
|
+
getBucketInfo,
|
|
129
132
|
parseAsLocalDate,
|
|
130
133
|
formatMetricValue,
|
|
131
134
|
formatPercentage,
|
|
@@ -134,3 +137,4 @@ export {
|
|
|
134
137
|
} from './utils';
|
|
135
138
|
export * from './utils/color-utils';
|
|
136
139
|
export type { MetricValueType } from './utils';
|
|
140
|
+
export type { BucketInfo } from './types';
|
|
@@ -21,24 +21,39 @@ import {
|
|
|
21
21
|
resolveCssVariable,
|
|
22
22
|
normalizeColorToHex,
|
|
23
23
|
} from '../../utils';
|
|
24
|
+
import { sanitizeFormatting } from '../../utils/date-formatting';
|
|
24
25
|
// Imported from the module rather than the `chart-scope` barrel: the barrel also pulls `use-standalone-scope-class`, which imports `GlobalChartsContext` back from this file. That cycle resolves today only because the binding is read lazily inside the hook body.
|
|
25
26
|
import { ChartScopeContext } from '../chart-scope/chart-scope-context';
|
|
26
27
|
import { getChartColor, type ColorCache } from './private/get-chart-color';
|
|
27
|
-
import {
|
|
28
|
-
import { withCatalogPointers } from './private/with-catalog-pointers';
|
|
28
|
+
import { SERIES_PALETTE_POINTERS } from './private/series-palette';
|
|
29
29
|
import { defaultTheme } from './themes';
|
|
30
30
|
import type { GlobalChartsContextValue, ChartRegistration } from './types';
|
|
31
31
|
import type { ChartTheme, CompleteChartTheme } from '../../types';
|
|
32
|
-
import type {
|
|
32
|
+
import type { FC, ReactNode } from 'react';
|
|
33
33
|
|
|
34
34
|
export const GlobalChartsContext = createContext< GlobalChartsContextValue | null >( null );
|
|
35
35
|
|
|
36
36
|
export interface GlobalChartsProviderProps {
|
|
37
37
|
children: ReactNode;
|
|
38
38
|
theme?: Partial< ChartTheme >;
|
|
39
|
+
/**
|
|
40
|
+
* BCP-47 language tag every date label is rendered in, e.g. `de-DE`.
|
|
41
|
+
* Defaults to the viewer's browser locale.
|
|
42
|
+
*/
|
|
43
|
+
locale?: string;
|
|
44
|
+
/**
|
|
45
|
+
* IANA time zone every date label is dated in, e.g. `Asia/Tokyo`.
|
|
46
|
+
* Defaults to the viewer's browser time zone.
|
|
47
|
+
*/
|
|
48
|
+
timeZone?: string;
|
|
39
49
|
}
|
|
40
50
|
|
|
41
|
-
export const GlobalChartsProvider: FC< GlobalChartsProviderProps > = ( {
|
|
51
|
+
export const GlobalChartsProvider: FC< GlobalChartsProviderProps > = ( {
|
|
52
|
+
children,
|
|
53
|
+
theme,
|
|
54
|
+
locale,
|
|
55
|
+
timeZone,
|
|
56
|
+
} ) => {
|
|
42
57
|
const [ charts, setCharts ] = useState< Map< string, ChartRegistration > >( () => new Map() );
|
|
43
58
|
// Track hidden series per chart: chartId -> Set<seriesLabel>
|
|
44
59
|
const [ hiddenSeries, setHiddenSeries ] = useState< Map< string, Set< string > > >(
|
|
@@ -58,20 +73,11 @@ export const GlobalChartsProvider: FC< GlobalChartsProviderProps > = ( { childre
|
|
|
58
73
|
setScopeNode( node );
|
|
59
74
|
}, [] );
|
|
60
75
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
() => themeOverrideVars( theme ),
|
|
76
|
+
const providerTheme: CompleteChartTheme = useMemo(
|
|
77
|
+
() => ( theme ? mergeThemes( defaultTheme, theme ) : defaultTheme ),
|
|
64
78
|
[ theme ]
|
|
65
79
|
);
|
|
66
80
|
|
|
67
|
-
const providerTheme: CompleteChartTheme = useMemo( () => {
|
|
68
|
-
if ( ! theme ) {
|
|
69
|
-
return defaultTheme;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return withCatalogPointers( mergeThemes( defaultTheme, theme ), overriddenRoles );
|
|
73
|
-
}, [ theme, overriddenRoles ] );
|
|
74
|
-
|
|
75
81
|
// Cache expensive color computations that only change when theme colors change
|
|
76
82
|
// Using useState + useLayoutEffect instead of useMemo to ensure CSS variables
|
|
77
83
|
// in <style> tags are applied to the DOM before we try to resolve them
|
|
@@ -87,49 +93,42 @@ export const GlobalChartsProvider: FC< GlobalChartsProviderProps > = ( { childre
|
|
|
87
93
|
// Useful for animations that should only run after the color palette is resolved
|
|
88
94
|
const [ isColorPaletteResolved, setIsColorPaletteResolved ] = useState( false );
|
|
89
95
|
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
|
|
96
|
+
// A layout effect rather than a memo: the catalog reaches the wrapper as a stylesheet, which
|
|
97
|
+
// must be applied before `getComputedStyle` can answer. Mount only — the slots are a fixed
|
|
98
|
+
// manifest and `wrapperRef` is stable for the provider's life.
|
|
99
|
+
|
|
94
100
|
useLayoutEffect( () => {
|
|
95
101
|
setIsColorPaletteResolved( false );
|
|
96
|
-
const { colors } = providerTheme;
|
|
97
102
|
const resolvedColors: string[] = [];
|
|
98
103
|
const hues: number[] = [];
|
|
99
104
|
const existingHslColors: Array< [ number, number, number ] > = [];
|
|
100
105
|
let minHue = 360;
|
|
101
106
|
let maxHue = 0;
|
|
102
107
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
hues.push( hslTuple[ 0 ] );
|
|
128
|
-
existingHslColors.push( hslTuple );
|
|
129
|
-
minHue = Math.min( minHue, hslTuple[ 0 ] );
|
|
130
|
-
maxHue = Math.max( maxHue, hslTuple[ 0 ] );
|
|
131
|
-
}
|
|
132
|
-
}
|
|
108
|
+
for ( const color of SERIES_PALETTE_POINTERS ) {
|
|
109
|
+
// Normalize color to hex format, handling CSS variables, RGB, HSL, etc.
|
|
110
|
+
// This uses normalizeColorToHex which resolves CSS variables and converts
|
|
111
|
+
// rgb(), rgba(), hsl() formats to hex
|
|
112
|
+
const normalizedColor = normalizeColorToHex( color, wrapperRef.current, resolveCssVariable );
|
|
113
|
+
|
|
114
|
+
// Only process valid hex colors. An unset palette slot returns its own
|
|
115
|
+
// `var()` unchanged, so this is also what compacts the palette: slots the
|
|
116
|
+
// consumer never set drop out here and `getChartColor` generates past
|
|
117
|
+
// whatever survived.
|
|
118
|
+
if ( normalizedColor.startsWith( '#' ) ) {
|
|
119
|
+
resolvedColors.push( normalizedColor );
|
|
120
|
+
const hslColor = d3Hsl( normalizedColor );
|
|
121
|
+
// d3Hsl returns NaN values for invalid colors
|
|
122
|
+
if ( ! isNaN( hslColor.h ) ) {
|
|
123
|
+
const hslTuple: [ number, number, number ] = [
|
|
124
|
+
hslColor.h,
|
|
125
|
+
hslColor.s * 100,
|
|
126
|
+
hslColor.l * 100,
|
|
127
|
+
];
|
|
128
|
+
hues.push( hslTuple[ 0 ] );
|
|
129
|
+
existingHslColors.push( hslTuple );
|
|
130
|
+
minHue = Math.min( minHue, hslTuple[ 0 ] );
|
|
131
|
+
maxHue = Math.max( maxHue, hslTuple[ 0 ] );
|
|
133
132
|
}
|
|
134
133
|
}
|
|
135
134
|
}
|
|
@@ -141,7 +140,7 @@ export const GlobalChartsProvider: FC< GlobalChartsProviderProps > = ( { childre
|
|
|
141
140
|
minHue,
|
|
142
141
|
maxHue,
|
|
143
142
|
} );
|
|
144
|
-
}, [
|
|
143
|
+
}, [] );
|
|
145
144
|
|
|
146
145
|
useEffect( () => {
|
|
147
146
|
if ( colorCache.colors.length > 0 ) {
|
|
@@ -153,11 +152,14 @@ export const GlobalChartsProvider: FC< GlobalChartsProviderProps > = ( { childre
|
|
|
153
152
|
() => new Map()
|
|
154
153
|
);
|
|
155
154
|
|
|
156
|
-
//
|
|
155
|
+
// Keyed on the resolved colors rather than the cache object, so a consumer passing an inline
|
|
156
|
+
// `theme` cannot reset the map on every render.
|
|
157
|
+
const paletteKey = colorCache.colors.join( ',' );
|
|
158
|
+
|
|
157
159
|
useEffect( () => {
|
|
158
160
|
// Create a completely new Map instance to trigger dependencies, e.g. useChartLegendItems
|
|
159
161
|
setGroupToColorMap( new Map() );
|
|
160
|
-
}, [
|
|
162
|
+
}, [ paletteKey ] );
|
|
161
163
|
|
|
162
164
|
const registerChart = useCallback( ( id: string, data: ChartRegistration ) => {
|
|
163
165
|
setCharts( prev => new Map( prev ).set( id, data ) );
|
|
@@ -347,6 +349,12 @@ export const GlobalChartsProvider: FC< GlobalChartsProviderProps > = ( { childre
|
|
|
347
349
|
[ hiddenSeries ]
|
|
348
350
|
);
|
|
349
351
|
|
|
352
|
+
// Held as one object so a chart's formatting memos key on a single stable reference.
|
|
353
|
+
const formatting = useMemo(
|
|
354
|
+
() => sanitizeFormatting( { locale, timeZone } ),
|
|
355
|
+
[ locale, timeZone ]
|
|
356
|
+
);
|
|
357
|
+
|
|
350
358
|
const value: GlobalChartsContextValue = useMemo(
|
|
351
359
|
() => ( {
|
|
352
360
|
charts,
|
|
@@ -354,6 +362,7 @@ export const GlobalChartsProvider: FC< GlobalChartsProviderProps > = ( { childre
|
|
|
354
362
|
unregisterChart,
|
|
355
363
|
getChartData,
|
|
356
364
|
theme: providerTheme,
|
|
365
|
+
formatting,
|
|
357
366
|
getElementStyles,
|
|
358
367
|
toggleSeriesVisibility,
|
|
359
368
|
setSeriesVisibility,
|
|
@@ -370,6 +379,7 @@ export const GlobalChartsProvider: FC< GlobalChartsProviderProps > = ( { childre
|
|
|
370
379
|
unregisterChart,
|
|
371
380
|
getChartData,
|
|
372
381
|
providerTheme,
|
|
382
|
+
formatting,
|
|
373
383
|
getElementStyles,
|
|
374
384
|
toggleSeriesVisibility,
|
|
375
385
|
setSeriesVisibility,
|
|
@@ -388,7 +398,7 @@ export const GlobalChartsProvider: FC< GlobalChartsProviderProps > = ( { childre
|
|
|
388
398
|
ref={ setWrapperNode }
|
|
389
399
|
className={ CHART_SCOPE_CLASS }
|
|
390
400
|
data-testid="charts-scope"
|
|
391
|
-
style={ { display: 'contents'
|
|
401
|
+
style={ { display: 'contents' } }
|
|
392
402
|
>
|
|
393
403
|
<ChartScopeContext.Provider value={ scopeNode }>{ children }</ChartScopeContext.Provider>
|
|
394
404
|
</div>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
import { GlobalChartsContext } from '../global-charts-provider';
|
|
3
|
+
import type { ChartFormatting } from '../../../types';
|
|
4
|
+
|
|
5
|
+
// A stable reference, so a chart mounted without a provider doesn't rebuild its
|
|
6
|
+
// formatting memos on every render.
|
|
7
|
+
const RUNTIME_FORMATTING: ChartFormatting = {};
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The locale and time zone dates are rendered in, as set on `GlobalChartsProvider`.
|
|
11
|
+
*
|
|
12
|
+
* Falls back to the runtime's own rather than throwing: charts render outside a
|
|
13
|
+
* provider, and a host that sets neither gets exactly the browser-default
|
|
14
|
+
* behavior this package had before the context existed.
|
|
15
|
+
*
|
|
16
|
+
* @return The host's formatting context.
|
|
17
|
+
*/
|
|
18
|
+
export const useChartFormatting = (): ChartFormatting => {
|
|
19
|
+
const context = useContext( GlobalChartsContext );
|
|
20
|
+
|
|
21
|
+
return context?.formatting ?? RUNTIME_FORMATTING;
|
|
22
|
+
};
|
|
@@ -3,6 +3,7 @@ export { useGlobalChartsContext } from './hooks/use-global-charts-context';
|
|
|
3
3
|
export { useChartId } from './hooks/use-chart-id';
|
|
4
4
|
export { useChartRegistration } from './hooks/use-chart-registration';
|
|
5
5
|
export { useGlobalChartsTheme } from './hooks/use-global-charts-theme';
|
|
6
|
+
export { useChartFormatting } from './hooks/use-chart-formatting';
|
|
6
7
|
export type {
|
|
7
8
|
GlobalChartsContextValue,
|
|
8
9
|
ChartRegistration,
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { SERIES_PALETTE_POINTERS } from './series-palette';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* The `var()` chain for each color JS has to hand to something. Nothing here is resolved in JS —
|
|
5
|
+
* the chain lands on the element; the terminal literal covers SSR and jsdom. See TOKENS.md.
|
|
6
|
+
*/
|
|
7
|
+
export const CATALOG_POINTERS = {
|
|
8
|
+
background: 'var(--a8c-charts-color-background, #fff)',
|
|
9
|
+
labelAxis: 'var(--a8c-charts-color-label-axis, #1e1e1e)',
|
|
10
|
+
grid: 'var(--a8c-charts-color-grid, #dbdbdb)',
|
|
11
|
+
// The y pair resolves to `none`: that axis carries labels only until a consumer declares them.
|
|
12
|
+
axisX: 'var(--a8c-charts-color-axis-x, #dbdbdb)',
|
|
13
|
+
tickX: 'var(--a8c-charts-color-tick-x, #dbdbdb)',
|
|
14
|
+
axisY: 'var(--a8c-charts-color-axis-y, none)',
|
|
15
|
+
tickY: 'var(--a8c-charts-color-tick-y, none)',
|
|
16
|
+
annotation: 'var(--a8c-charts-color-annotation, #1e1e1e)',
|
|
17
|
+
surface: 'var(--a8c-charts-color-surface, #fff)',
|
|
18
|
+
surfaceSecondary: 'var(--a8c-charts-color-surface-secondary, #f4f4f4)',
|
|
19
|
+
// Only for `renderMainMetric`, which hands a consumer's own markup something to paint with.
|
|
20
|
+
trendUp: 'var(--a8c-charts-color-trend-up, #008030)',
|
|
21
|
+
trendDown: 'var(--a8c-charts-color-trend-down, #cc1818)',
|
|
22
|
+
series: SERIES_PALETTE_POINTERS,
|
|
23
|
+
} as const;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The annotation parts visx paints, in the shape `@visx/annotation` takes. `radius`
|
|
27
|
+
* rides along as the base the theme and the per-datum styles merge onto.
|
|
28
|
+
*/
|
|
29
|
+
export const ANNOTATION_POINTERS = {
|
|
30
|
+
label: {
|
|
31
|
+
anchorLineStroke: CATALOG_POINTERS.annotation,
|
|
32
|
+
backgroundFill: CATALOG_POINTERS.surface,
|
|
33
|
+
},
|
|
34
|
+
connector: {
|
|
35
|
+
stroke: CATALOG_POINTERS.annotation,
|
|
36
|
+
},
|
|
37
|
+
circleSubject: {
|
|
38
|
+
stroke: 'transparent',
|
|
39
|
+
fill: CATALOG_POINTERS.annotation,
|
|
40
|
+
radius: 5,
|
|
41
|
+
},
|
|
42
|
+
} as const;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The series palette's slot manifest. Which slots exist is a fact about the catalog
|
|
3
|
+
* `chart-scope.scss` emits, not a theme value, so it is declared here rather than in `themes.ts`.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** How many series-palette slots the catalog emits. */
|
|
7
|
+
export const SERIES_SLOT_COUNT = 5;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The catalog role holding one series-palette slot.
|
|
11
|
+
*
|
|
12
|
+
* @param slot - The one-based slot number.
|
|
13
|
+
* @return The role name.
|
|
14
|
+
*/
|
|
15
|
+
export const seriesRole = ( slot: number ): string => `--a8c-charts-color-series-${ slot }`;
|
|
16
|
+
|
|
17
|
+
/*
|
|
18
|
+
* Only slot 1 carries a terminal literal, and it is reached only where `getComputedStyle` resolves
|
|
19
|
+
* nothing — SSR and jsdom. The rest resolve to nothing until a consumer sets them, and the provider
|
|
20
|
+
* drops what resolves to nothing, so the palette compacts rather than repeating one color.
|
|
21
|
+
*
|
|
22
|
+
* The literal matches slot 1's own last-resort fallback in `chart-scope.scss`. It is deliberately
|
|
23
|
+
* the bare hex rather than that slot's full `var()` chain: this string is resolved against the DOM,
|
|
24
|
+
* where the chain has already been walked, and is only read at all when no DOM answered.
|
|
25
|
+
*/
|
|
26
|
+
const SERIES_SLOT_1_FALLBACK = '#3858e9';
|
|
27
|
+
|
|
28
|
+
/** The catalog pointer for every slot, in slot order. */
|
|
29
|
+
export const SERIES_PALETTE_POINTERS: readonly string[] = Array.from(
|
|
30
|
+
{ length: SERIES_SLOT_COUNT },
|
|
31
|
+
( _, index ) =>
|
|
32
|
+
index === 0
|
|
33
|
+
? `var(${ seriesRole( 1 ) }, ${ SERIES_SLOT_1_FALLBACK })`
|
|
34
|
+
: `var(${ seriesRole( index + 1 ) })`
|
|
35
|
+
);
|