@automattic/charts 1.0.1 → 1.0.2
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 +9 -0
- package/dist/index.cjs +84 -37
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +6 -13
- package/dist/index.css.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +84 -37
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.module.scss +7 -16
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx +18 -5
- package/src/charts/conversion-funnel-chart/test/conversion-funnel-chart.test.tsx +11 -0
- package/src/providers/chart-context/global-charts-provider.tsx +13 -0
- package/src/providers/chart-context/test/chart-context.test.tsx +51 -0
- package/src/providers/chart-context/types.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@automattic/charts",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Display charts within Automattic products.",
|
|
5
5
|
"homepage": "https://github.com/Automattic/jetpack/tree/HEAD/projects/js-packages/charts/#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"typecheck": "tsgo --noEmit"
|
|
64
64
|
},
|
|
65
65
|
"dependencies": {
|
|
66
|
-
"@automattic/number-formatters": "^1.1.
|
|
66
|
+
"@automattic/number-formatters": "^1.1.4",
|
|
67
67
|
"@babel/runtime": "7.28.6",
|
|
68
68
|
"@react-spring/web": "9.7.5",
|
|
69
69
|
"@visx/annotation": "^3.12.0",
|
|
@@ -99,8 +99,8 @@
|
|
|
99
99
|
"@babel/core": "7.29.0",
|
|
100
100
|
"@babel/preset-react": "7.28.5",
|
|
101
101
|
"@babel/preset-typescript": "7.28.5",
|
|
102
|
-
"@storybook/addon-docs": "10.3.
|
|
103
|
-
"@storybook/react": "10.3.
|
|
102
|
+
"@storybook/addon-docs": "10.3.3",
|
|
103
|
+
"@storybook/react": "10.3.3",
|
|
104
104
|
"@testing-library/dom": "^10.0.0",
|
|
105
105
|
"@testing-library/jest-dom": "^6.0.0",
|
|
106
106
|
"@testing-library/react": "^16.0.0",
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
"@wordpress/element": "6.42.0",
|
|
115
115
|
"babel-jest": "30.3.0",
|
|
116
116
|
"babel-plugin-react-remove-properties": "^0.3.1",
|
|
117
|
-
"esbuild": "0.
|
|
117
|
+
"esbuild": "0.27.4",
|
|
118
118
|
"esbuild-plugin-babel": "^0.2.3",
|
|
119
119
|
"esbuild-sass-plugin": "^3.1.0",
|
|
120
120
|
"identity-obj-proxy": "^3.0.0",
|
|
@@ -125,7 +125,7 @@
|
|
|
125
125
|
"react": "18.3.1",
|
|
126
126
|
"react-dom": "18.3.1",
|
|
127
127
|
"sass-embedded": "1.97.3",
|
|
128
|
-
"storybook": "10.3.
|
|
128
|
+
"storybook": "10.3.3",
|
|
129
129
|
"tsup": "8.5.1",
|
|
130
130
|
"typescript": "5.9.3"
|
|
131
131
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
.
|
|
1
|
+
.conversion-funnel-chart {
|
|
2
2
|
font-family: var(--funnel-font-family, "SF Pro Text");
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
&--loading {
|
|
5
5
|
opacity: 0.6;
|
|
6
6
|
pointer-events: none;
|
|
7
7
|
}
|
|
@@ -53,9 +53,12 @@
|
|
|
53
53
|
display: flex;
|
|
54
54
|
flex-direction: column;
|
|
55
55
|
height: 100%;
|
|
56
|
-
transition: all 0.3s ease;
|
|
57
56
|
|
|
58
|
-
|
|
57
|
+
&--animated {
|
|
58
|
+
transition: opacity 0.3s ease;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
&--blurred {
|
|
59
62
|
opacity: 0.3;
|
|
60
63
|
}
|
|
61
64
|
}
|
|
@@ -95,24 +98,12 @@
|
|
|
95
98
|
border-radius: 4px;
|
|
96
99
|
position: relative;
|
|
97
100
|
cursor: pointer;
|
|
98
|
-
transition: all 0.2s ease;
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
&.disabled {
|
|
102
|
-
cursor: pointer;
|
|
103
|
-
}
|
|
104
101
|
}
|
|
105
102
|
|
|
106
103
|
.funnel-bar {
|
|
107
104
|
width: 100%;
|
|
108
105
|
min-height: 4px;
|
|
109
106
|
border-radius: 4px 4px 0 0;
|
|
110
|
-
transition: all 0.3s ease;
|
|
111
|
-
|
|
112
|
-
&.selected {
|
|
113
|
-
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
|
|
114
|
-
filter: brightness(1.1);
|
|
115
|
-
}
|
|
116
107
|
|
|
117
108
|
&--animated {
|
|
118
109
|
transform-origin: bottom;
|
|
@@ -52,7 +52,7 @@ const ConversionFunnelChartInternal: FC< ConversionFunnelChartProps > = ( {
|
|
|
52
52
|
} ) => {
|
|
53
53
|
const chartId = useChartId( providedChartId );
|
|
54
54
|
const { conversionFunnelChart: conversionFunnelChartSettings } = useGlobalChartsTheme();
|
|
55
|
-
const { getElementStyles } = useGlobalChartsContext();
|
|
55
|
+
const { getElementStyles, isColorPaletteResolved } = useGlobalChartsContext();
|
|
56
56
|
const chartRef = useRef< HTMLDivElement >( null );
|
|
57
57
|
const selectedBarRef = useRef< HTMLDivElement | null >( null );
|
|
58
58
|
|
|
@@ -301,7 +301,11 @@ const ConversionFunnelChartInternal: FC< ConversionFunnelChartProps > = ( {
|
|
|
301
301
|
<Stack
|
|
302
302
|
direction="column"
|
|
303
303
|
data-testid="conversion-funnel-chart"
|
|
304
|
-
className={ clsx(
|
|
304
|
+
className={ clsx(
|
|
305
|
+
styles[ 'conversion-funnel-chart' ],
|
|
306
|
+
loading && styles[ 'conversion-funnel-chart--loading' ],
|
|
307
|
+
className
|
|
308
|
+
) }
|
|
305
309
|
style={ { ...style, height: resolvedHeight } }
|
|
306
310
|
>
|
|
307
311
|
<div className={ styles[ 'empty-state' ] }>
|
|
@@ -324,7 +328,11 @@ const ConversionFunnelChartInternal: FC< ConversionFunnelChartProps > = ( {
|
|
|
324
328
|
portalContainerRef( node );
|
|
325
329
|
chartRef.current = node;
|
|
326
330
|
} }
|
|
327
|
-
className={ clsx(
|
|
331
|
+
className={ clsx(
|
|
332
|
+
styles[ 'conversion-funnel-chart' ],
|
|
333
|
+
loading && styles[ 'conversion-funnel-chart--loading' ],
|
|
334
|
+
className
|
|
335
|
+
) }
|
|
328
336
|
style={ { ...style, height: resolvedHeight } }
|
|
329
337
|
>
|
|
330
338
|
{ /* Main Metric */ }
|
|
@@ -348,7 +356,12 @@ const ConversionFunnelChartInternal: FC< ConversionFunnelChartProps > = ( {
|
|
|
348
356
|
return (
|
|
349
357
|
<div
|
|
350
358
|
key={ step.id }
|
|
351
|
-
|
|
359
|
+
data-testid="funnel-step"
|
|
360
|
+
className={ clsx(
|
|
361
|
+
styles[ 'funnel-step' ],
|
|
362
|
+
isColorPaletteResolved && styles[ 'funnel-step--animated' ],
|
|
363
|
+
isBlurred && styles[ 'funnel-step--blurred' ]
|
|
364
|
+
) }
|
|
352
365
|
>
|
|
353
366
|
{ /* Step Label and Rate */ }
|
|
354
367
|
<div className={ styles[ 'step-header' ] }>
|
|
@@ -376,7 +389,7 @@ const ConversionFunnelChartInternal: FC< ConversionFunnelChartProps > = ( {
|
|
|
376
389
|
|
|
377
390
|
{ /* Funnel Bar */ }
|
|
378
391
|
<div
|
|
379
|
-
className={
|
|
392
|
+
className={ styles[ 'bar-container' ] }
|
|
380
393
|
onClick={ stepHandlers.get( step.id )?.onClick }
|
|
381
394
|
onKeyDown={ stepHandlers.get( step.id )?.onKeyDown }
|
|
382
395
|
role="button"
|
|
@@ -461,4 +461,15 @@ describe( 'ConversionFunnelChart', () => {
|
|
|
461
461
|
expect( customRenderMainMetric ).toHaveBeenCalled();
|
|
462
462
|
} );
|
|
463
463
|
} );
|
|
464
|
+
|
|
465
|
+
describe( 'Color palette readiness', () => {
|
|
466
|
+
it( 'enables transitions once color palette is resolved', () => {
|
|
467
|
+
render( <ConversionFunnelChart { ...defaultProps } /> );
|
|
468
|
+
|
|
469
|
+
// After render, effects have run (via useEffect in GlobalChartsProvider),
|
|
470
|
+
// so the palette is resolved and the animated class is applied to funnel steps
|
|
471
|
+
const funnelStep = screen.getAllByTestId( 'funnel-step' )[ 0 ];
|
|
472
|
+
expect( funnelStep ).toHaveClass( 'funnel-step--animated' );
|
|
473
|
+
} );
|
|
474
|
+
} );
|
|
464
475
|
} );
|
|
@@ -70,11 +70,16 @@ export const GlobalChartsProvider: FC< GlobalChartsProviderProps > = ( {
|
|
|
70
70
|
maxHue: 0,
|
|
71
71
|
} ) );
|
|
72
72
|
|
|
73
|
+
// Track if the color palette has been resolved from the DOM
|
|
74
|
+
// Useful for animations that should only run after the color palette is resolved
|
|
75
|
+
const [ isColorPaletteResolved, setIsColorPaletteResolved ] = useState( false );
|
|
76
|
+
|
|
73
77
|
// Compute color cache after DOM is updated (so CSS variables are available)
|
|
74
78
|
// Resolves CSS variables from the wrapper element's scope to handle scoped variables
|
|
75
79
|
// Note: Only re-runs when providerTheme changes, not when wrapper element changes.
|
|
76
80
|
// This is intentional, as wrapperRef is expected to be stable for the lifetime of the provider.
|
|
77
81
|
useLayoutEffect( () => {
|
|
82
|
+
setIsColorPaletteResolved( false );
|
|
78
83
|
const { colors } = providerTheme;
|
|
79
84
|
const resolvedColors: string[] = [];
|
|
80
85
|
const hues: number[] = [];
|
|
@@ -125,6 +130,12 @@ export const GlobalChartsProvider: FC< GlobalChartsProviderProps > = ( {
|
|
|
125
130
|
} );
|
|
126
131
|
}, [ providerTheme ] );
|
|
127
132
|
|
|
133
|
+
useEffect( () => {
|
|
134
|
+
if ( colorCache.colors.length > 0 ) {
|
|
135
|
+
setIsColorPaletteResolved( true );
|
|
136
|
+
}
|
|
137
|
+
}, [ colorCache ] );
|
|
138
|
+
|
|
128
139
|
const [ groupToColorMap, setGroupToColorMap ] = useState< Map< string, string > >(
|
|
129
140
|
() => new Map()
|
|
130
141
|
);
|
|
@@ -271,6 +282,7 @@ export const GlobalChartsProvider: FC< GlobalChartsProviderProps > = ( {
|
|
|
271
282
|
toggleSeriesVisibility,
|
|
272
283
|
isSeriesVisible,
|
|
273
284
|
getHiddenSeries,
|
|
285
|
+
isColorPaletteResolved,
|
|
274
286
|
} ),
|
|
275
287
|
[
|
|
276
288
|
charts,
|
|
@@ -282,6 +294,7 @@ export const GlobalChartsProvider: FC< GlobalChartsProviderProps > = ( {
|
|
|
282
294
|
toggleSeriesVisibility,
|
|
283
295
|
isSeriesVisible,
|
|
284
296
|
getHiddenSeries,
|
|
297
|
+
isColorPaletteResolved,
|
|
285
298
|
]
|
|
286
299
|
);
|
|
287
300
|
|
|
@@ -47,6 +47,57 @@ describe( 'ChartContext', () => {
|
|
|
47
47
|
expect( contextValue.charts ).toBeInstanceOf( Map );
|
|
48
48
|
} );
|
|
49
49
|
|
|
50
|
+
it( 'exposes isColorPaletteResolved as true after render', () => {
|
|
51
|
+
let contextValue: GlobalChartsContextValue;
|
|
52
|
+
|
|
53
|
+
const TestComponent = () => {
|
|
54
|
+
contextValue = useGlobalChartsContext();
|
|
55
|
+
return <div>Test</div>;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
render(
|
|
59
|
+
<GlobalChartsProvider>
|
|
60
|
+
<TestComponent />
|
|
61
|
+
</GlobalChartsProvider>
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// After render and effects, isColorPaletteResolved should be true
|
|
65
|
+
expect( contextValue.isColorPaletteResolved ).toBe( true );
|
|
66
|
+
} );
|
|
67
|
+
|
|
68
|
+
it( 'resolves palette again after theme change', () => {
|
|
69
|
+
let contextValue: GlobalChartsContextValue;
|
|
70
|
+
|
|
71
|
+
const TestComponent = () => {
|
|
72
|
+
contextValue = useGlobalChartsContext();
|
|
73
|
+
return <div>Test</div>;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const theme1: Partial< ChartTheme > = {
|
|
77
|
+
colors: [ '#006DAB', '#1F9828' ],
|
|
78
|
+
};
|
|
79
|
+
const theme2: Partial< ChartTheme > = {
|
|
80
|
+
colors: [ '#FF0000', '#00FF00' ],
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const { rerender } = render(
|
|
84
|
+
<GlobalChartsProvider theme={ theme1 }>
|
|
85
|
+
<TestComponent />
|
|
86
|
+
</GlobalChartsProvider>
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
expect( contextValue.isColorPaletteResolved ).toBe( true );
|
|
90
|
+
|
|
91
|
+
rerender(
|
|
92
|
+
<GlobalChartsProvider theme={ theme2 }>
|
|
93
|
+
<TestComponent />
|
|
94
|
+
</GlobalChartsProvider>
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
// After theme change, palette should re-resolve to true
|
|
98
|
+
expect( contextValue.isColorPaletteResolved ).toBe( true );
|
|
99
|
+
} );
|
|
100
|
+
|
|
50
101
|
it( 'throws error when useGlobalChartsContext is used outside provider', () => {
|
|
51
102
|
const TestComponent = () => {
|
|
52
103
|
useGlobalChartsContext();
|
|
@@ -35,4 +35,5 @@ export interface GlobalChartsContextValue {
|
|
|
35
35
|
toggleSeriesVisibility: ( chartId: string, seriesLabel: string ) => void;
|
|
36
36
|
isSeriesVisible: ( chartId: string, seriesLabel: string ) => boolean;
|
|
37
37
|
getHiddenSeries: ( chartId: string ) => Set< string >;
|
|
38
|
+
isColorPaletteResolved: boolean;
|
|
38
39
|
}
|