@automattic/charts 2.0.1 → 3.1.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 +109 -50
- package/dist/index.cjs +529 -233
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +118 -55
- package/dist/index.d.cts +90 -48
- package/dist/index.d.ts +90 -48
- package/dist/index.js +527 -235
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/charts/area-chart/area-chart.module.scss +8 -3
- package/src/charts/area-chart/area-chart.tsx +25 -25
- package/src/charts/area-chart/private/overlays.tsx +3 -3
- package/src/charts/area-chart/test/area-chart.test.tsx +301 -11
- package/src/charts/area-chart/types.ts +4 -3
- package/src/charts/bar-chart/bar-chart.module.scss +14 -6
- package/src/charts/bar-chart/bar-chart.tsx +21 -15
- package/src/charts/bar-chart/test/bar-chart.test.tsx +255 -2
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.module.scss +23 -18
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx +107 -86
- package/src/charts/conversion-funnel-chart/test/chart-scope.test.tsx +36 -0
- package/src/charts/geo-chart/geo-chart.module.scss +8 -0
- package/src/charts/geo-chart/geo-chart.tsx +12 -4
- package/src/charts/heatmap-chart/heatmap-chart.module.scss +22 -9
- package/src/charts/heatmap-chart/heatmap-chart.tsx +43 -13
- package/src/charts/heatmap-chart/private/build-calendar-data.ts +65 -5
- package/src/charts/heatmap-chart/test/build-calendar-data.test.ts +125 -0
- package/src/charts/heatmap-chart/types.ts +8 -0
- package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +13 -7
- package/src/charts/leaderboard-chart/leaderboard-chart.tsx +17 -22
- package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +107 -1
- package/src/charts/leaderboard-chart/types.ts +0 -2
- package/src/charts/line-chart/line-chart.module.scss +12 -7
- package/src/charts/line-chart/line-chart.tsx +32 -28
- package/src/charts/line-chart/private/line-chart-annotation.tsx +4 -4
- package/src/charts/line-chart/private/line-chart-annotations-overlay.tsx +2 -2
- package/src/charts/line-chart/test/line-chart.test.tsx +262 -5
- package/src/charts/line-chart/types.ts +4 -3
- package/src/charts/pie-chart/pie-chart.tsx +12 -14
- package/src/charts/pie-chart/test/pie-chart.test.tsx +73 -1
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.module.scss +2 -2
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +12 -14
- package/src/charts/private/{single-chart-context/single-chart-context.tsx → chart-instance-context/chart-instance-context.ts} +1 -5
- package/src/charts/private/chart-instance-context/index.ts +3 -0
- package/src/charts/private/{single-chart-context/use-single-chart-context.ts → chart-instance-context/use-chart-instance-context.ts} +1 -3
- package/src/charts/private/svg-empty-state/get-all-hidden-message.ts +41 -0
- package/src/charts/private/svg-empty-state/index.ts +1 -0
- package/src/charts/private/svg-empty-state/svg-empty-state.module.scss +1 -1
- package/src/charts/private/svg-empty-state/test/get-all-hidden-message.test.ts +15 -0
- package/src/charts/private/with-responsive/test/with-responsive.test.tsx +16 -0
- package/src/charts/private/with-responsive/with-responsive.tsx +18 -11
- package/src/charts/private/x-zoom/test/x-zoom.test.tsx +2 -2
- package/src/charts/private/x-zoom/x-zoom.module.scss +3 -3
- package/src/charts/private/x-zoom/x-zoom.tsx +2 -2
- package/src/components/legend/legend.tsx +4 -4
- package/src/components/legend/private/base-legend.module.scss +1 -1
- package/src/components/legend/private/base-legend.tsx +70 -19
- package/src/components/legend/test/legend.test.tsx +100 -4
- package/src/components/tooltip/accessible-tooltip.tsx +17 -4
- package/src/components/tooltip/base-tooltip.tsx +8 -1
- package/src/components/trend-indicator/test/trend-indicator.test.tsx +20 -0
- package/src/components/trend-indicator/trend-indicator.module.scss +4 -7
- package/src/components/trend-indicator/trend-indicator.tsx +3 -0
- package/src/hooks/index.ts +1 -1
- package/src/hooks/test/use-xychart-theme-provider-override.test.tsx +28 -0
- package/src/hooks/test/use-xychart-theme-style-reads.test.tsx +19 -13
- package/src/hooks/test/use-xychart-theme.test.tsx +25 -0
- package/src/hooks/{use-interactive-legend-data.ts → use-legend-visibility-data.ts} +29 -30
- package/src/hooks/use-xychart-theme.ts +6 -4
- package/src/index.ts +12 -3
- package/src/providers/chart-context/global-charts-provider.tsx +127 -30
- package/src/providers/chart-context/hooks/use-default-hidden-series.ts +47 -0
- package/src/providers/chart-context/private/theme-override-vars.ts +87 -0
- package/src/providers/chart-context/private/with-catalog-pointers.ts +50 -0
- package/src/providers/chart-context/test/chart-context.test.tsx +142 -40
- package/src/providers/chart-context/test/chart-scope-class.test.tsx +19 -0
- package/src/providers/chart-context/test/sparse-theme.test.ts +35 -0
- package/src/providers/chart-context/test/theme-override-vars-guard.test.tsx +29 -0
- package/src/providers/chart-context/test/theme-override-vars.test.ts +118 -0
- package/src/providers/chart-context/test/use-default-hidden-series-paint.test.tsx +56 -0
- package/src/providers/chart-context/test/use-default-hidden-series.ssr.test.tsx +39 -0
- package/src/providers/chart-context/test/use-default-hidden-series.test.tsx +222 -0
- package/src/providers/chart-context/test/with-catalog-pointers.test.ts +47 -0
- package/src/providers/chart-context/themes.ts +18 -27
- package/src/providers/chart-context/types.ts +7 -4
- package/src/providers/chart-scope/chart-scope-context.ts +8 -0
- package/src/providers/chart-scope/index.ts +3 -0
- package/src/providers/chart-scope/test/chart-scope.test.tsx +24 -0
- package/src/providers/chart-scope/test/use-standalone-scope-class.test.tsx +20 -0
- package/src/providers/chart-scope/use-chart-scope-element.ts +9 -0
- package/src/providers/chart-scope/use-standalone-scope-class.ts +13 -0
- package/src/providers/index.ts +1 -0
- package/src/styles/chart-scope-class.ts +6 -0
- package/src/styles/chart-scope.scss +83 -0
- package/src/styles/test/chart-scope.test.ts +165 -0
- package/src/styles/test/wpds-fallbacks.test.ts +131 -0
- package/src/types.ts +12 -34
- package/src/charts/private/grid-control/grid-control.module.scss +0 -8
- package/src/charts/private/grid-control/grid-control.tsx +0 -26
- package/src/charts/private/grid-control/index.ts +0 -1
- package/src/charts/private/grid-control/test/grid-control.test.tsx +0 -56
- package/src/charts/private/single-chart-context/index.ts +0 -8
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { GlobalChartsProvider } from '../global-charts-provider';
|
|
3
|
+
|
|
4
|
+
// The stock defaultTheme's mapped fields are themselves self-referential pointers (e.g. `gridStyles.stroke: 'var(--a8c-charts-color-grid, #dbdbdb)'`), so themeOverrideVars' own self-reference guard already filters them out no matter which theme object the provider passes in. That makes "reads theme, not providerTheme" unfalsifiable against the real default theme. Mocking the default here to a plain, non-pointer value removes that overlap, so this test only passes when the provider reads the raw consumer `theme` prop rather than the merged `providerTheme`.
|
|
5
|
+
jest.mock( '../themes', () => {
|
|
6
|
+
const actual = jest.requireActual( '../themes' );
|
|
7
|
+
return {
|
|
8
|
+
...actual,
|
|
9
|
+
defaultTheme: {
|
|
10
|
+
...actual.defaultTheme,
|
|
11
|
+
gridStyles: { ...actual.defaultTheme.gridStyles, stroke: '#dbdbdb' },
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
} );
|
|
15
|
+
|
|
16
|
+
describe( 'GlobalChartsProvider wrapper style (discriminating guard test)', () => {
|
|
17
|
+
it( 'does not emit an instance override for a role the consumer left unset, even though the merged theme carries a concrete default value', () => {
|
|
18
|
+
render(
|
|
19
|
+
<GlobalChartsProvider theme={ { tickLength: 8 } }>
|
|
20
|
+
<div>child</div>
|
|
21
|
+
</GlobalChartsProvider>
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const wrapper = screen.getByTestId( 'charts-scope' );
|
|
25
|
+
|
|
26
|
+
expect( wrapper.style.getPropertyValue( '--a8c-charts-color-grid-theme' ) ).toBe( '' );
|
|
27
|
+
expect( wrapper.style.getPropertyValue( '--a8c-charts-color-grid' ) ).toBe( '' );
|
|
28
|
+
} );
|
|
29
|
+
} );
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import {
|
|
2
|
+
THEME_LAYERED_ROLES,
|
|
3
|
+
themeLayerVar,
|
|
4
|
+
themeOverrideVars,
|
|
5
|
+
} from '../private/theme-override-vars';
|
|
6
|
+
|
|
7
|
+
describe( 'themeOverrideVars', () => {
|
|
8
|
+
it( 'returns nothing for an absent theme', () => {
|
|
9
|
+
expect( themeOverrideVars( undefined ) ).toEqual( { vars: {}, roles: [] } );
|
|
10
|
+
} );
|
|
11
|
+
|
|
12
|
+
it( 'returns nothing for a theme that overrides no mapped role', () => {
|
|
13
|
+
expect( themeOverrideVars( { tickLength: 8 } ) ).toEqual( { vars: {}, roles: [] } );
|
|
14
|
+
} );
|
|
15
|
+
|
|
16
|
+
// The value is published one layer outside the role — `chart-scope.scss` declares the role as `var(<role>-theme, <catalog default>)`. Published as the role itself, a value that is invalid at computed-value time (`var(--wpds-token)` with the token unset) would make the role invalid too, and every bare `var(--a8c-charts-color-grid)` read site would paint `unset` instead of falling back to the catalog default.
|
|
17
|
+
it( 'maps a grid stroke override onto the grid role’s theme layer', () => {
|
|
18
|
+
expect( themeOverrideVars( { gridStyles: { stroke: 'red' } } ) ).toEqual( {
|
|
19
|
+
vars: { '--a8c-charts-color-grid-theme': 'red' },
|
|
20
|
+
roles: [ '--a8c-charts-color-grid' ],
|
|
21
|
+
} );
|
|
22
|
+
} );
|
|
23
|
+
|
|
24
|
+
it( 'maps every supported role at once', () => {
|
|
25
|
+
expect(
|
|
26
|
+
themeOverrideVars( {
|
|
27
|
+
backgroundColor: '#111',
|
|
28
|
+
gridStyles: { stroke: '#222' },
|
|
29
|
+
xAxisLineStyles: { stroke: '#333' },
|
|
30
|
+
xTickLineStyles: { stroke: '#444' },
|
|
31
|
+
svgLabelSmall: { fill: '#555' },
|
|
32
|
+
} )
|
|
33
|
+
).toEqual( {
|
|
34
|
+
vars: {
|
|
35
|
+
'--a8c-charts-color-background-theme': '#111',
|
|
36
|
+
'--a8c-charts-color-grid-theme': '#222',
|
|
37
|
+
'--a8c-charts-color-axis-theme': '#333',
|
|
38
|
+
'--a8c-charts-color-tick-theme': '#444',
|
|
39
|
+
'--a8c-charts-color-label-axis-theme': '#555',
|
|
40
|
+
},
|
|
41
|
+
roles: [
|
|
42
|
+
'--a8c-charts-color-background',
|
|
43
|
+
'--a8c-charts-color-grid',
|
|
44
|
+
'--a8c-charts-color-axis',
|
|
45
|
+
'--a8c-charts-color-tick',
|
|
46
|
+
'--a8c-charts-color-label-axis',
|
|
47
|
+
],
|
|
48
|
+
} );
|
|
49
|
+
} );
|
|
50
|
+
|
|
51
|
+
it( 'publishes only theme-layer variables, never a catalog role directly', () => {
|
|
52
|
+
const { vars } = themeOverrideVars( {
|
|
53
|
+
backgroundColor: '#111',
|
|
54
|
+
gridStyles: { stroke: '#222' },
|
|
55
|
+
xAxisLineStyles: { stroke: '#333' },
|
|
56
|
+
xTickLineStyles: { stroke: '#444' },
|
|
57
|
+
svgLabelSmall: { fill: '#555' },
|
|
58
|
+
} );
|
|
59
|
+
|
|
60
|
+
expect( Object.keys( vars ) ).toEqual( THEME_LAYERED_ROLES.map( themeLayerVar ) );
|
|
61
|
+
} );
|
|
62
|
+
|
|
63
|
+
// The narrow role is the whole point: the broad `--a8c-charts-color-label` is also read by legend labels, heatmap cell values, funnel labels and the line-chart tooltip, none of which `svgLabelSmall.fill` moved before the role was published as a custom property.
|
|
64
|
+
it( 'publishes an svg label override as the narrow axis-label role, not the broad one', () => {
|
|
65
|
+
expect( themeOverrideVars( { svgLabelSmall: { fill: 'red' } } ) ).toEqual( {
|
|
66
|
+
vars: { '--a8c-charts-color-label-axis-theme': 'red' },
|
|
67
|
+
roles: [ '--a8c-charts-color-label-axis' ],
|
|
68
|
+
} );
|
|
69
|
+
} );
|
|
70
|
+
|
|
71
|
+
it( 'ignores a value that is itself a pointer at the role it would override', () => {
|
|
72
|
+
expect(
|
|
73
|
+
themeOverrideVars( { gridStyles: { stroke: 'var(--a8c-charts-color-grid, #dbdbdb)' } } ).vars
|
|
74
|
+
).toEqual( {} );
|
|
75
|
+
} );
|
|
76
|
+
|
|
77
|
+
// A skipped value still reports its role, so `withCatalogPointers` restores the theme field. Without that, CSS would paint the catalog default while visx painted the consumer's literal — the divergence this whole mechanism exists to remove.
|
|
78
|
+
it( 'still reports the role of a value it refuses to publish', () => {
|
|
79
|
+
expect(
|
|
80
|
+
themeOverrideVars( {
|
|
81
|
+
gridStyles: { stroke: 'var(--brand, var(--a8c-charts-color-grid, red))' },
|
|
82
|
+
} )
|
|
83
|
+
).toEqual( { vars: {}, roles: [ '--a8c-charts-color-grid' ] } );
|
|
84
|
+
} );
|
|
85
|
+
|
|
86
|
+
// `var()` allows whitespace after the opening paren. The role reads its theme layer, so a value naming the role closes a cycle through the catalog entry: CSS marks every custom property in a cycle invalid at computed-value time, and the role's own fallback is not used, so the token resolves to nothing at all.
|
|
87
|
+
it( 'ignores a self-referential pointer padded with whitespace', () => {
|
|
88
|
+
expect(
|
|
89
|
+
themeOverrideVars( { gridStyles: { stroke: 'var( --a8c-charts-color-grid, #dbdbdb )' } } )
|
|
90
|
+
.vars
|
|
91
|
+
).toEqual( {} );
|
|
92
|
+
} );
|
|
93
|
+
|
|
94
|
+
it( 'still emits a value that merely mentions a different role', () => {
|
|
95
|
+
expect(
|
|
96
|
+
themeOverrideVars( { gridStyles: { stroke: 'var( --a8c-charts-color-axis, #dbdbdb )' } } )
|
|
97
|
+
.vars
|
|
98
|
+
).toEqual( {
|
|
99
|
+
'--a8c-charts-color-grid-theme': 'var( --a8c-charts-color-axis, #dbdbdb )',
|
|
100
|
+
} );
|
|
101
|
+
} );
|
|
102
|
+
|
|
103
|
+
// A mapped role can be a strict prefix of another custom property — a catalog role such as `--a8c-charts-color-label` of `--a8c-charts-color-label-axis`, the role's own theme layer, or a name the consumer defined themselves. A prefix match would read a legitimate cross-property pointer as a self-reference and silently drop the override, so the role must be followed by something that cannot continue an identifier.
|
|
104
|
+
it( 'does not mistake a longer property name for the role it guards', () => {
|
|
105
|
+
expect(
|
|
106
|
+
themeOverrideVars( {
|
|
107
|
+
xAxisLineStyles: { stroke: 'var(--a8c-charts-color-axis-emphasis, #000)' },
|
|
108
|
+
} ).vars
|
|
109
|
+
).toEqual( { '--a8c-charts-color-axis-theme': 'var(--a8c-charts-color-axis-emphasis, #000)' } );
|
|
110
|
+
} );
|
|
111
|
+
|
|
112
|
+
// `resolveCssVariable` also accepts a bare custom-property name with no `var()` wrapper, so `theme={ { gridStyles: { stroke: '--a8c-charts-color-grid' } } }` is legal input. It forms no cycle, so it must be caught explicitly or it survives as a literal string value and drops silently at the use site.
|
|
113
|
+
it( 'ignores a self-referential pointer written as a bare custom-property name', () => {
|
|
114
|
+
expect(
|
|
115
|
+
themeOverrideVars( { gridStyles: { stroke: '--a8c-charts-color-grid' } } ).vars
|
|
116
|
+
).toEqual( {} );
|
|
117
|
+
} );
|
|
118
|
+
} );
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { screen } from '@testing-library/react';
|
|
2
|
+
import { flushSync } from 'react-dom';
|
|
3
|
+
import { createRoot } from 'react-dom/client';
|
|
4
|
+
import { GlobalChartsProvider } from '../global-charts-provider';
|
|
5
|
+
import { useDefaultHiddenSeries } from '../hooks/use-default-hidden-series';
|
|
6
|
+
import { useGlobalChartsContext } from '../hooks/use-global-charts-context';
|
|
7
|
+
import type { Root } from 'react-dom/client';
|
|
8
|
+
|
|
9
|
+
describe( 'useDefaultHiddenSeries paint timing', () => {
|
|
10
|
+
let host: HTMLDivElement;
|
|
11
|
+
let root: Root;
|
|
12
|
+
let previousActEnvironment: boolean | undefined;
|
|
13
|
+
|
|
14
|
+
beforeEach( () => {
|
|
15
|
+
previousActEnvironment = globalThis.IS_REACT_ACT_ENVIRONMENT;
|
|
16
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = false;
|
|
17
|
+
host = document.createElement( 'div' );
|
|
18
|
+
document.body.appendChild( host );
|
|
19
|
+
root = createRoot( host );
|
|
20
|
+
} );
|
|
21
|
+
|
|
22
|
+
afterEach( () => {
|
|
23
|
+
flushSync( () => root.unmount() );
|
|
24
|
+
host.remove();
|
|
25
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = previousActEnvironment;
|
|
26
|
+
} );
|
|
27
|
+
|
|
28
|
+
it( 'updates provider visibility before the first paint', () => {
|
|
29
|
+
const Chart = () => {
|
|
30
|
+
useDefaultHiddenSeries( 'chart', [ 'Visitors' ] );
|
|
31
|
+
return null;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const Visibility = () => {
|
|
35
|
+
const { isSeriesVisible } = useGlobalChartsContext();
|
|
36
|
+
return (
|
|
37
|
+
<span data-testid="series-visibility">
|
|
38
|
+
{ isSeriesVisible( 'chart', 'Visitors' ) ? 'visible' : 'hidden' }
|
|
39
|
+
</span>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// flushSync lets us inspect the layout-effect result before a later passive
|
|
44
|
+
// effect could seed the provider.
|
|
45
|
+
flushSync( () => {
|
|
46
|
+
root.render(
|
|
47
|
+
<GlobalChartsProvider>
|
|
48
|
+
<Chart />
|
|
49
|
+
<Visibility />
|
|
50
|
+
</GlobalChartsProvider>
|
|
51
|
+
);
|
|
52
|
+
} );
|
|
53
|
+
|
|
54
|
+
expect( screen.getByTestId( 'series-visibility' ) ).toHaveTextContent( 'hidden' );
|
|
55
|
+
} );
|
|
56
|
+
} );
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** @jest-environment node */
|
|
2
|
+
|
|
3
|
+
import { renderToStaticMarkup } from 'react-dom/server';
|
|
4
|
+
import { useDefaultHiddenSeries } from '../hooks/use-default-hidden-series';
|
|
5
|
+
|
|
6
|
+
const mockGetHiddenSeries = jest.fn( () => new Set( [ 'Provider hidden' ] ) );
|
|
7
|
+
const mockSeedChartHiddenSeries = jest.fn();
|
|
8
|
+
const mockHasSeededChart = jest.fn( () => false );
|
|
9
|
+
|
|
10
|
+
jest.mock( '../hooks/use-global-charts-context', () => ( {
|
|
11
|
+
useGlobalChartsContext: () => ( {
|
|
12
|
+
getHiddenSeries: mockGetHiddenSeries,
|
|
13
|
+
seedChartHiddenSeries: mockSeedChartHiddenSeries,
|
|
14
|
+
hasSeededChart: mockHasSeededChart,
|
|
15
|
+
} ),
|
|
16
|
+
} ) );
|
|
17
|
+
|
|
18
|
+
describe( 'useDefaultHiddenSeries SSR', () => {
|
|
19
|
+
it( 'applies defaults to the server-rendered output before effects run', () => {
|
|
20
|
+
const Chart = () => {
|
|
21
|
+
const hiddenSeries = useDefaultHiddenSeries( 'chart', [ 'Visitors' ] );
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<>
|
|
25
|
+
{ [ 'Visitors', 'Views' ].map( label =>
|
|
26
|
+
! hiddenSeries.has( label ) ? <span key={ label }>{ label }</span> : null
|
|
27
|
+
) }
|
|
28
|
+
</>
|
|
29
|
+
);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const view = renderToStaticMarkup( <Chart /> );
|
|
33
|
+
|
|
34
|
+
expect( view ).not.toContain( 'Visitors' );
|
|
35
|
+
expect( view ).toContain( 'Views' );
|
|
36
|
+
expect( mockGetHiddenSeries ).not.toHaveBeenCalled();
|
|
37
|
+
expect( mockSeedChartHiddenSeries ).not.toHaveBeenCalled();
|
|
38
|
+
} );
|
|
39
|
+
} );
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { render, act } from '@testing-library/react';
|
|
2
|
+
import { GlobalChartsProvider } from '../global-charts-provider';
|
|
3
|
+
import { useDefaultHiddenSeries } from '../hooks/use-default-hidden-series';
|
|
4
|
+
import { useGlobalChartsContext } from '../hooks/use-global-charts-context';
|
|
5
|
+
import type { GlobalChartsContextValue } from '../types';
|
|
6
|
+
|
|
7
|
+
describe( 'useDefaultHiddenSeries', () => {
|
|
8
|
+
let contextValue: GlobalChartsContextValue;
|
|
9
|
+
let resolvedHiddenSeries: Set< string >;
|
|
10
|
+
|
|
11
|
+
const Grab = () => {
|
|
12
|
+
contextValue = useGlobalChartsContext();
|
|
13
|
+
return null;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const Chart = ( { defaults, chartId = 'chart' }: { defaults?: string[]; chartId?: string } ) => {
|
|
17
|
+
resolvedHiddenSeries = useDefaultHiddenSeries( chartId, defaults );
|
|
18
|
+
return null;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
it( 'hides the named series at mount', () => {
|
|
22
|
+
render(
|
|
23
|
+
<GlobalChartsProvider>
|
|
24
|
+
<Grab />
|
|
25
|
+
<Chart defaults={ [ 'Visitors' ] } />
|
|
26
|
+
</GlobalChartsProvider>
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
expect( contextValue.isSeriesVisible( 'chart', 'Visitors' ) ).toBe( false );
|
|
30
|
+
expect( contextValue.isSeriesVisible( 'chart', 'Views' ) ).toBe( true );
|
|
31
|
+
} );
|
|
32
|
+
|
|
33
|
+
it( 'writes nothing when no defaults are given', () => {
|
|
34
|
+
render(
|
|
35
|
+
<GlobalChartsProvider>
|
|
36
|
+
<Grab />
|
|
37
|
+
<Chart />
|
|
38
|
+
</GlobalChartsProvider>
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
expect( contextValue.getHiddenSeries( 'chart' ) ).toEqual( new Set() );
|
|
42
|
+
} );
|
|
43
|
+
|
|
44
|
+
it( 'reflects provider visibility changes after mount', () => {
|
|
45
|
+
render(
|
|
46
|
+
<GlobalChartsProvider>
|
|
47
|
+
<Grab />
|
|
48
|
+
<Chart />
|
|
49
|
+
</GlobalChartsProvider>
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
expect( resolvedHiddenSeries.has( 'Visitors' ) ).toBe( false );
|
|
53
|
+
|
|
54
|
+
act( () => {
|
|
55
|
+
contextValue.setSeriesVisibility( 'chart', 'Visitors', false );
|
|
56
|
+
} );
|
|
57
|
+
|
|
58
|
+
expect( resolvedHiddenSeries.has( 'Visitors' ) ).toBe( true );
|
|
59
|
+
} );
|
|
60
|
+
|
|
61
|
+
it( 'seeds the first defined value and ignores later values for the same chart ID', () => {
|
|
62
|
+
const Harness = ( { defaults }: { defaults?: string[] } ) => (
|
|
63
|
+
<GlobalChartsProvider>
|
|
64
|
+
<Grab />
|
|
65
|
+
<Chart defaults={ defaults } />
|
|
66
|
+
</GlobalChartsProvider>
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const { rerender } = render( <Harness /> );
|
|
70
|
+
|
|
71
|
+
rerender( <Harness defaults={ [ 'Visitors' ] } /> );
|
|
72
|
+
expect( contextValue.getHiddenSeries( 'chart' ) ).toEqual( new Set( [ 'Visitors' ] ) );
|
|
73
|
+
|
|
74
|
+
rerender( <Harness defaults={ [ 'Views' ] } /> );
|
|
75
|
+
expect( contextValue.getHiddenSeries( 'chart' ) ).toEqual( new Set( [ 'Visitors' ] ) );
|
|
76
|
+
} );
|
|
77
|
+
|
|
78
|
+
it( 'treats an empty array as a defined default', () => {
|
|
79
|
+
const Harness = ( { defaults }: { defaults: string[] } ) => (
|
|
80
|
+
<GlobalChartsProvider>
|
|
81
|
+
<Grab />
|
|
82
|
+
<Chart defaults={ defaults } />
|
|
83
|
+
</GlobalChartsProvider>
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const { rerender } = render( <Harness defaults={ [] } /> );
|
|
87
|
+
|
|
88
|
+
rerender( <Harness defaults={ [ 'Visitors' ] } /> );
|
|
89
|
+
|
|
90
|
+
expect( contextValue.getHiddenSeries( 'chart' ) ).toEqual( new Set() );
|
|
91
|
+
} );
|
|
92
|
+
|
|
93
|
+
it( 'does not re-hide a series the user revealed', () => {
|
|
94
|
+
const Harness = () => (
|
|
95
|
+
<GlobalChartsProvider>
|
|
96
|
+
<Grab />
|
|
97
|
+
<Chart defaults={ [ 'Visitors' ] } />
|
|
98
|
+
</GlobalChartsProvider>
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const { rerender } = render( <Harness /> );
|
|
102
|
+
|
|
103
|
+
act( () => {
|
|
104
|
+
contextValue.setSeriesVisibility( 'chart', 'Visitors', true );
|
|
105
|
+
} );
|
|
106
|
+
|
|
107
|
+
rerender( <Harness /> );
|
|
108
|
+
|
|
109
|
+
expect( contextValue.isSeriesVisible( 'chart', 'Visitors' ) ).toBe( true );
|
|
110
|
+
} );
|
|
111
|
+
|
|
112
|
+
it( "keeps the reader's visibility choices across a remount", () => {
|
|
113
|
+
// The provider stays mounted; only the chart unmounts and comes back — a
|
|
114
|
+
// chart-type switch, a tab round-trip, a resize flipping tabs into a
|
|
115
|
+
// dropdown. None of those read as a new chart, so none may re-seed.
|
|
116
|
+
const Harness = ( { mounted }: { mounted: boolean } ) => (
|
|
117
|
+
<GlobalChartsProvider>
|
|
118
|
+
<Grab />
|
|
119
|
+
{ mounted && <Chart defaults={ [ 'Visitors' ] } /> }
|
|
120
|
+
</GlobalChartsProvider>
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const { rerender } = render( <Harness mounted /> );
|
|
124
|
+
|
|
125
|
+
act( () => {
|
|
126
|
+
contextValue.setSeriesVisibility( 'chart', 'Views', false );
|
|
127
|
+
} );
|
|
128
|
+
expect( contextValue.isSeriesVisible( 'chart', 'Views' ) ).toBe( false );
|
|
129
|
+
|
|
130
|
+
rerender( <Harness mounted={ false } /> );
|
|
131
|
+
rerender( <Harness mounted /> );
|
|
132
|
+
|
|
133
|
+
expect( contextValue.isSeriesVisible( 'chart', 'Visitors' ) ).toBe( false );
|
|
134
|
+
expect( contextValue.isSeriesVisible( 'chart', 'Views' ) ).toBe( false );
|
|
135
|
+
} );
|
|
136
|
+
|
|
137
|
+
it( 'leaves a fully revealed chart revealed after a remount', () => {
|
|
138
|
+
// Revealing the last hidden series empties the chart's hidden set, and the
|
|
139
|
+
// provider drops the entry with it. The seeded record has to survive that,
|
|
140
|
+
// or the defaults come back the next time the chart mounts.
|
|
141
|
+
const Harness = ( { mounted }: { mounted: boolean } ) => (
|
|
142
|
+
<GlobalChartsProvider>
|
|
143
|
+
<Grab />
|
|
144
|
+
{ mounted && <Chart defaults={ [ 'Visitors' ] } /> }
|
|
145
|
+
</GlobalChartsProvider>
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const { rerender } = render( <Harness mounted /> );
|
|
149
|
+
|
|
150
|
+
act( () => {
|
|
151
|
+
contextValue.setSeriesVisibility( 'chart', 'Visitors', true );
|
|
152
|
+
} );
|
|
153
|
+
expect( contextValue.getHiddenSeries( 'chart' ) ).toEqual( new Set() );
|
|
154
|
+
|
|
155
|
+
rerender( <Harness mounted={ false } /> );
|
|
156
|
+
rerender( <Harness mounted /> );
|
|
157
|
+
|
|
158
|
+
expect( contextValue.isSeriesVisible( 'chart', 'Visitors' ) ).toBe( true );
|
|
159
|
+
} );
|
|
160
|
+
|
|
161
|
+
it( 'retains provider visibility when remounted without defaults', () => {
|
|
162
|
+
const Harness = ( { defaults, mounted }: { defaults?: string[]; mounted: boolean } ) => (
|
|
163
|
+
<GlobalChartsProvider>
|
|
164
|
+
<Grab />
|
|
165
|
+
{ mounted && <Chart defaults={ defaults } /> }
|
|
166
|
+
</GlobalChartsProvider>
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
const { rerender } = render( <Harness defaults={ [ 'Visitors' ] } mounted /> );
|
|
170
|
+
expect( contextValue.isSeriesVisible( 'chart', 'Visitors' ) ).toBe( false );
|
|
171
|
+
|
|
172
|
+
rerender( <Harness mounted={ false } /> );
|
|
173
|
+
rerender( <Harness mounted /> );
|
|
174
|
+
|
|
175
|
+
expect( contextValue.isSeriesVisible( 'chart', 'Visitors' ) ).toBe( false );
|
|
176
|
+
} );
|
|
177
|
+
|
|
178
|
+
it( 'keeps the same set across renders until visibility changes', () => {
|
|
179
|
+
// Charts memoize their rendered series off this set. A fresh identity per
|
|
180
|
+
// render invalidates those memos, which re-runs the accessible tooltip's
|
|
181
|
+
// effect and closes a tooltip the user is still navigating with.
|
|
182
|
+
const Harness = () => (
|
|
183
|
+
<GlobalChartsProvider>
|
|
184
|
+
<Grab />
|
|
185
|
+
<Chart defaults={ [ 'Visitors' ] } />
|
|
186
|
+
</GlobalChartsProvider>
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
const { rerender } = render( <Harness /> );
|
|
190
|
+
const seeded = resolvedHiddenSeries;
|
|
191
|
+
|
|
192
|
+
rerender( <Harness /> );
|
|
193
|
+
expect( resolvedHiddenSeries ).toBe( seeded );
|
|
194
|
+
|
|
195
|
+
act( () => {
|
|
196
|
+
contextValue.setSeriesVisibility( 'chart', 'Views', false );
|
|
197
|
+
} );
|
|
198
|
+
expect( resolvedHiddenSeries ).not.toBe( seeded );
|
|
199
|
+
} );
|
|
200
|
+
|
|
201
|
+
it( 're-seeds the defaults when chartId changes mid-mount', () => {
|
|
202
|
+
// Same mount, only the chartId prop changes (e.g. <LineChart chartId={ metric } ... />
|
|
203
|
+
// with `metric` going 'views' -> 'clicks'), so the component never unmounts.
|
|
204
|
+
const Harness = ( { chartId }: { chartId: string } ) => (
|
|
205
|
+
<GlobalChartsProvider>
|
|
206
|
+
<Grab />
|
|
207
|
+
<Chart chartId={ chartId } defaults={ [ 'Visitors' ] } />
|
|
208
|
+
</GlobalChartsProvider>
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const { rerender } = render( <Harness chartId="views" /> );
|
|
212
|
+
|
|
213
|
+
expect( contextValue.isSeriesVisible( 'views', 'Visitors' ) ).toBe( false );
|
|
214
|
+
|
|
215
|
+
rerender( <Harness chartId="clicks" /> );
|
|
216
|
+
|
|
217
|
+
// The new chart id gets the same declared defaults seeded...
|
|
218
|
+
expect( contextValue.isSeriesVisible( 'clicks', 'Visitors' ) ).toBe( false );
|
|
219
|
+
// ...and the old id's hidden set is left as it was, not silently cleared.
|
|
220
|
+
expect( contextValue.isSeriesVisible( 'views', 'Visitors' ) ).toBe( false );
|
|
221
|
+
} );
|
|
222
|
+
} );
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { mergeThemes } from '../../../utils';
|
|
2
|
+
import { withCatalogPointers } from '../private/with-catalog-pointers';
|
|
3
|
+
import { defaultTheme } from '../themes';
|
|
4
|
+
import type { ChartTheme } from '../../../types';
|
|
5
|
+
|
|
6
|
+
describe( 'withCatalogPointers', () => {
|
|
7
|
+
it( 'restores every overridden role to its catalog pointer, without mutating the input theme', () => {
|
|
8
|
+
const merged = mergeThemes( defaultTheme, {
|
|
9
|
+
backgroundColor: '#111',
|
|
10
|
+
gridStyles: { stroke: '#222' },
|
|
11
|
+
xAxisLineStyles: { stroke: '#333' },
|
|
12
|
+
xTickLineStyles: { stroke: '#444' },
|
|
13
|
+
svgLabelSmall: { fill: '#555' },
|
|
14
|
+
} as Partial< ChartTheme > );
|
|
15
|
+
|
|
16
|
+
const result = withCatalogPointers( merged, [
|
|
17
|
+
'--a8c-charts-color-background',
|
|
18
|
+
'--a8c-charts-color-grid',
|
|
19
|
+
'--a8c-charts-color-axis',
|
|
20
|
+
'--a8c-charts-color-tick',
|
|
21
|
+
'--a8c-charts-color-label-axis',
|
|
22
|
+
] );
|
|
23
|
+
|
|
24
|
+
expect( result.backgroundColor ).toBe( defaultTheme.backgroundColor );
|
|
25
|
+
expect( result.gridStyles.stroke ).toBe( defaultTheme.gridStyles.stroke );
|
|
26
|
+
expect( result.xAxisLineStyles.stroke ).toBe( defaultTheme.xAxisLineStyles.stroke );
|
|
27
|
+
expect( result.xTickLineStyles.stroke ).toBe( defaultTheme.xTickLineStyles.stroke );
|
|
28
|
+
expect( result.svgLabelSmall.fill ).toBe( defaultTheme.svgLabelSmall.fill );
|
|
29
|
+
|
|
30
|
+
expect( merged.backgroundColor ).toBe( '#111' );
|
|
31
|
+
expect( merged.gridStyles.stroke ).toBe( '#222' );
|
|
32
|
+
} );
|
|
33
|
+
|
|
34
|
+
it( 'leaves fields alone when no role was overridden', () => {
|
|
35
|
+
const merged = mergeThemes( defaultTheme, {
|
|
36
|
+
tickLength: 8,
|
|
37
|
+
leaderboardChart: { deltaColors: [ 'red', 'grey', 'green' ] },
|
|
38
|
+
} as Partial< ChartTheme > );
|
|
39
|
+
|
|
40
|
+
const result = withCatalogPointers( merged, [] );
|
|
41
|
+
|
|
42
|
+
expect( result.tickLength ).toBe( 8 );
|
|
43
|
+
expect( result.gridStyles.stroke ).toBe( defaultTheme.gridStyles.stroke );
|
|
44
|
+
// Not a mapped role, so its literal survives even when other roles are being restored.
|
|
45
|
+
expect( result.leaderboardChart.deltaColors ).toEqual( [ 'red', 'grey', 'green' ] );
|
|
46
|
+
} );
|
|
47
|
+
} );
|
|
@@ -4,31 +4,29 @@ import type { CompleteChartTheme } from '../../types';
|
|
|
4
4
|
* Default theme configuration
|
|
5
5
|
*/
|
|
6
6
|
const defaultTheme: CompleteChartTheme = {
|
|
7
|
-
backgroundColor:
|
|
8
|
-
'var(--a8c-charts-color-background, var(--wpds-color-background-surface-neutral-strong, #fff))',
|
|
7
|
+
backgroundColor: 'var(--a8c-charts-color-background, #fff)',
|
|
9
8
|
labelBackgroundColor: 'transparent', // label background color (transparent by default)
|
|
10
|
-
// White label text sits on top of arbitrary series colors, so it has no WPDS
|
|
11
|
-
// content-foreground equivalent; the role carries the hardcoded value as its fallback.
|
|
9
|
+
// White label text sits on top of arbitrary series colors, so it has no WPDS content-foreground equivalent. Every other colour here is a bare pointer at the catalog emitted by `chart-scope.scss`; the terminal literal is the last resort for the SSR and jsdom paths, where getComputedStyle resolves nothing.
|
|
12
10
|
labelTextColor: 'var(--a8c-charts-color-label-on-fill, #FFFFFF)',
|
|
13
11
|
colors: [ '#98C8DF', '#006DAB', '#A6DC80', '#1F9828', '#FF8C8F' ],
|
|
14
12
|
gridStyles: {
|
|
15
|
-
stroke: 'var(--a8c-charts-color-grid,
|
|
13
|
+
stroke: 'var(--a8c-charts-color-grid, #dbdbdb)',
|
|
16
14
|
strokeWidth: 1,
|
|
17
15
|
},
|
|
18
16
|
tickLength: 4,
|
|
19
17
|
gridColor: '',
|
|
20
18
|
gridColorDark: '',
|
|
21
19
|
xTickLineStyles: {
|
|
22
|
-
stroke: 'var(--a8c-charts-color-tick,
|
|
20
|
+
stroke: 'var(--a8c-charts-color-tick, #dbdbdb)',
|
|
23
21
|
strokeWidth: 1,
|
|
24
22
|
},
|
|
25
23
|
xAxisLineStyles: {
|
|
26
|
-
stroke: 'var(--a8c-charts-color-axis,
|
|
24
|
+
stroke: 'var(--a8c-charts-color-axis, #dbdbdb)',
|
|
27
25
|
strokeWidth: 1,
|
|
28
26
|
},
|
|
29
27
|
legend: {
|
|
30
28
|
labelStyles: {
|
|
31
|
-
color: 'var(--a8c-charts-color-label,
|
|
29
|
+
color: 'var(--a8c-charts-color-label, #1e1e1e)',
|
|
32
30
|
},
|
|
33
31
|
containerStyles: {},
|
|
34
32
|
shapeStyles: [],
|
|
@@ -41,30 +39,26 @@ const defaultTheme: CompleteChartTheme = {
|
|
|
41
39
|
// elements for axis labels and ticks. Setting `inherit` lets SVG text
|
|
42
40
|
// pick up the host application's font-family via normal CSS inheritance.
|
|
43
41
|
svgLabelSmall: {
|
|
44
|
-
fill: 'var(--a8c-charts-color-label
|
|
42
|
+
fill: 'var(--a8c-charts-color-label-axis, #1e1e1e)',
|
|
45
43
|
fontFamily: 'inherit',
|
|
46
44
|
},
|
|
47
45
|
svgLabelBig: { fontFamily: 'inherit' },
|
|
48
46
|
annotationStyles: {
|
|
49
47
|
label: {
|
|
50
|
-
anchorLineStroke:
|
|
51
|
-
|
|
52
|
-
backgroundFill:
|
|
53
|
-
'var(--a8c-charts-color-background, var(--wpds-color-background-surface-neutral-strong, #fff))',
|
|
48
|
+
anchorLineStroke: 'var(--a8c-charts-color-annotation, #1e1e1e)',
|
|
49
|
+
backgroundFill: 'var(--a8c-charts-color-surface, #fff)',
|
|
54
50
|
},
|
|
55
51
|
connector: {
|
|
56
|
-
stroke:
|
|
57
|
-
'var(--a8c-charts-color-annotation, var(--wpds-color-foreground-content-neutral, #1e1e1e))',
|
|
52
|
+
stroke: 'var(--a8c-charts-color-annotation, #1e1e1e)',
|
|
58
53
|
},
|
|
59
54
|
circleSubject: {
|
|
60
55
|
stroke: 'transparent',
|
|
61
|
-
fill: 'var(--a8c-charts-color-annotation,
|
|
56
|
+
fill: 'var(--a8c-charts-color-annotation, #1e1e1e)',
|
|
62
57
|
radius: 5,
|
|
63
58
|
},
|
|
64
59
|
},
|
|
65
60
|
geoChart: {
|
|
66
|
-
featureFillColor:
|
|
67
|
-
'var(--a8c-charts-color-surface-secondary, var(--wpds-color-background-surface-neutral-weak, #f4f4f4))',
|
|
61
|
+
featureFillColor: 'var(--a8c-charts-color-surface-secondary, #f4f4f4)',
|
|
68
62
|
},
|
|
69
63
|
leaderboardChart: {
|
|
70
64
|
rowGap: 12,
|
|
@@ -72,18 +66,15 @@ const defaultTheme: CompleteChartTheme = {
|
|
|
72
66
|
labelSpacing: 'xs',
|
|
73
67
|
// [negative, neutral, positive]
|
|
74
68
|
deltaColors: [
|
|
75
|
-
'var(--a8c-charts-color-trend-down,
|
|
76
|
-
'var(--a8c-charts-color-trend-neutral,
|
|
77
|
-
'var(--a8c-charts-color-trend-up,
|
|
69
|
+
'var(--a8c-charts-color-trend-down, #cc1818)',
|
|
70
|
+
'var(--a8c-charts-color-trend-neutral, #707070)',
|
|
71
|
+
'var(--a8c-charts-color-trend-up, #008030)',
|
|
78
72
|
],
|
|
79
73
|
},
|
|
80
74
|
conversionFunnelChart: {
|
|
81
|
-
backgroundColor:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
'var(--a8c-charts-color-trend-up, var(--wpds-color-foreground-content-success-weak, #008030))',
|
|
85
|
-
negativeChangeColor:
|
|
86
|
-
'var(--a8c-charts-color-trend-down, var(--wpds-color-foreground-content-error-weak, #cc1818))',
|
|
75
|
+
backgroundColor: 'var(--a8c-charts-color-surface-secondary, #f4f4f4)',
|
|
76
|
+
positiveChangeColor: 'var(--a8c-charts-color-trend-up, #008030)',
|
|
77
|
+
negativeChangeColor: 'var(--a8c-charts-color-trend-down, #cc1818)',
|
|
87
78
|
},
|
|
88
79
|
lineChart: {
|
|
89
80
|
lineStyles: {
|
|
@@ -38,11 +38,14 @@ export interface GlobalChartsContextValue {
|
|
|
38
38
|
getChartData: ( id: string ) => ChartRegistration | undefined;
|
|
39
39
|
theme: CompleteChartTheme;
|
|
40
40
|
getElementStyles: ( params: GetElementStylesParams ) => ElementStyles;
|
|
41
|
-
//
|
|
42
|
-
// Resolves at call time so it tracks ambient theme changes (e.g. a light/dark switch).
|
|
43
|
-
resolveThemeColor: ( value: string ) => string;
|
|
44
|
-
// Series visibility management for interactive legends
|
|
41
|
+
// Series visibility management shared by charts, legends, and programmatic controls.
|
|
45
42
|
toggleSeriesVisibility: ( chartId: string, seriesLabel: string ) => void;
|
|
43
|
+
setSeriesVisibility: ( chartId: string, seriesLabel: string, visible: boolean ) => void;
|
|
44
|
+
setChartHiddenSeries: ( chartId: string, seriesLabels: readonly string[] ) => void;
|
|
45
|
+
/** Applies a chart's defaults once. A no-op for a chart that has already been seeded. */
|
|
46
|
+
seedChartHiddenSeries: ( chartId: string, seriesLabels: readonly string[] ) => void;
|
|
47
|
+
/** Whether this chart's defaults have already been seeded, in this or an earlier mount. */
|
|
48
|
+
hasSeededChart: ( chartId: string ) => boolean;
|
|
46
49
|
isSeriesVisible: ( chartId: string, seriesLabel: string ) => boolean;
|
|
47
50
|
getHiddenSeries: ( chartId: string ) => Set< string >;
|
|
48
51
|
isColorPaletteResolved: boolean;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { createContext } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The nearest chart element, published so JS token resolution reads the same CSS cascade that paints the chart's own colours.
|
|
5
|
+
*
|
|
6
|
+
* All JS token resolution reads this element rather than `document.documentElement`, so a `getComputedStyle` call sees the same `--a8c-charts-*` values — including any override set inside the provider tree — that a CSS-painted sibling element does.
|
|
7
|
+
*/
|
|
8
|
+
export const ChartScopeContext = createContext< HTMLElement | null >( null );
|