@automattic/charts 4.6.0 → 4.8.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 +32 -0
- package/TOKENS.md +15 -10
- package/dist/index.cjs +427 -211
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +40 -26
- package/dist/index.d.cts +69 -29
- package/dist/index.d.ts +69 -29
- package/dist/index.js +427 -213
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/charts/area-chart/area-chart.tsx +2 -2
- package/src/charts/area-chart/test/area-chart.test.tsx +1 -1
- package/src/charts/area-chart/test/tooltip-resolution-branches.test.tsx +1 -1
- package/src/charts/bar-chart/bar-chart.module.scss +7 -0
- package/src/charts/bar-chart/bar-chart.tsx +20 -14
- package/src/charts/bar-chart/private/comparison-bars.tsx +1 -0
- package/src/charts/bar-chart/test/bar-chart.test.tsx +25 -0
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.module.scss +0 -10
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx +5 -3
- package/src/charts/conversion-funnel-chart/test/conversion-funnel-chart.test.tsx +20 -1
- package/src/charts/conversion-funnel-chart/types.ts +3 -0
- package/src/charts/heatmap-chart/heatmap-chart.module.scss +16 -7
- package/src/charts/heatmap-chart/heatmap-chart.tsx +80 -53
- package/src/charts/heatmap-chart/private/heatmap-context.ts +2 -2
- package/src/charts/heatmap-chart/private/heatmap-legend.tsx +2 -2
- package/src/charts/heatmap-chart/private/heatmap-scale.ts +74 -0
- package/src/charts/heatmap-chart/private/index.ts +2 -1
- package/src/charts/heatmap-chart/private/use-heatmap-colors.ts +24 -6
- package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +221 -25
- package/src/charts/heatmap-chart/test/heatmap-scale.test.ts +78 -0
- package/src/charts/heatmap-chart/test/use-heatmap-colors.test.ts +18 -6
- package/src/charts/heatmap-chart/types.ts +6 -9
- package/src/charts/line-chart/crosshair-style-types.fixture.ts +23 -0
- package/src/charts/line-chart/line-chart.module.scss +0 -5
- package/src/charts/line-chart/line-chart.tsx +10 -42
- package/src/charts/line-chart/test/line-chart.test.tsx +19 -47
- package/src/charts/pie-chart/pie-chart.module.scss +9 -0
- package/src/charts/pie-chart/pie-chart.tsx +26 -80
- package/src/charts/pie-chart/test/pie-chart.test.tsx +21 -8
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +4 -4
- package/src/charts/pie-semi-circle-chart/test/pie-semi-circle-chart.test.tsx +1 -1
- package/src/charts/private/chart-composition/chart-html.tsx +1 -1
- package/src/charts/private/chart-composition/chart-svg.tsx +1 -1
- package/src/charts/private/label-text-color.ts +116 -0
- package/src/charts/private/radial-wipe-animation/radial-wipe-animation.tsx +2 -0
- package/src/charts/private/svg-empty-state/svg-empty-state.tsx +1 -1
- package/src/charts/private/test/label-text-color.test.ts +189 -0
- package/src/charts/private/with-responsive/test/with-responsive.test.tsx +1 -0
- package/src/charts/private/with-responsive/with-responsive.tsx +2 -5
- package/src/components/tooltip/base-tooltip.tsx +31 -30
- package/src/components/tooltip/index.ts +3 -1
- package/src/components/tooltip/private/bounded-tooltip.tsx +32 -13
- package/src/components/tooltip/private/label-value-content.tsx +8 -0
- package/src/components/tooltip/private/tooltip-theme.tsx +37 -0
- package/src/components/tooltip/private/xy-chart-tooltip-overlay.tsx +336 -0
- package/src/components/tooltip/test/bounded-tooltip.test.tsx +82 -1
- package/src/components/tooltip/test/tool-tip.test.tsx +10 -3
- package/src/components/tooltip/test/tooltip-box.test.tsx +53 -0
- package/src/components/tooltip/test/tooltip-theme.test.tsx +61 -0
- package/src/components/tooltip/test/xy-chart-tooltip-overlay.test.tsx +445 -0
- package/src/components/tooltip/test/xy-chart-tooltip.test.tsx +286 -356
- package/src/components/tooltip/tooltip-box.module.scss +21 -0
- package/src/components/tooltip/tooltip-box.tsx +43 -0
- package/src/components/tooltip/types.ts +7 -1
- package/src/components/tooltip/xy-chart-tooltip.tsx +411 -301
- package/src/components/trend-indicator/trend-indicator.tsx +1 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/test/use-chart-margin.test.tsx +8 -4
- package/src/hooks/test/use-xychart-theme.test.tsx +3 -3
- package/src/hooks/use-chart-margin.tsx +21 -18
- package/src/hooks/use-isomorphic-layout-effect.ts +5 -0
- package/src/hooks/use-xychart-theme.ts +4 -12
- package/src/index.ts +8 -3
- package/src/providers/chart-context/global-charts-provider.tsx +5 -14
- package/src/providers/chart-context/hooks/use-default-hidden-series.ts +3 -5
- package/src/providers/chart-context/private/catalog-pointers.ts +1 -0
- package/src/providers/chart-context/private/perceptual-color.ts +45 -4
- package/src/providers/chart-context/private/resolve-opaque-hex.ts +16 -0
- package/src/providers/chart-context/test/perceptual-color.test.ts +31 -0
- package/src/providers/chart-context/test/use-default-hidden-series.test.tsx +1 -1
- package/src/styles/chart-scope-class.ts +5 -0
- package/src/styles/chart-scope.scss +10 -6
- package/src/styles/test/chart-scope.test.ts +3 -3
- package/src/test-utils/zoned-parsing-suite.ts +0 -10
- package/src/utils/color-utils.ts +2 -1
- package/src/utils/test/date-formatting.test.ts +0 -10
- package/src/visx/types.ts +1 -1
- package/src/charts/line-chart/test/crosshair-style-types.test.ts +0 -31
- package/src/components/tooltip/accessible-tooltip.tsx +0 -433
- package/src/components/tooltip/base-tooltip.module.scss +0 -20
- package/src/components/tooltip/test/accessible-tooltip.test.tsx +0 -331
|
@@ -2,6 +2,7 @@ import clsx from 'clsx';
|
|
|
2
2
|
import { useStandaloneScopeClass } from '../../providers/chart-scope';
|
|
3
3
|
import styles from './trend-indicator.module.scss';
|
|
4
4
|
import type { TrendIndicatorProps, TrendDirection } from './types';
|
|
5
|
+
import type { JSX } from 'react';
|
|
5
6
|
|
|
6
7
|
const DIRECTION_LABELS: Record< TrendDirection, string > = {
|
|
7
8
|
up: 'Increase',
|
package/src/hooks/index.ts
CHANGED
|
@@ -9,3 +9,4 @@ export { useZeroValueDisplay } from './use-zero-value-display';
|
|
|
9
9
|
export { useDataWithPercentages } from './use-data-with-percentages';
|
|
10
10
|
export { useLegendVisibilityData } from './use-legend-visibility-data';
|
|
11
11
|
export { usePrefersReducedMotion } from './use-prefers-reduced-motion';
|
|
12
|
+
export { useIsomorphicLayoutEffect } from './use-isomorphic-layout-effect';
|
|
@@ -282,16 +282,20 @@ describe( 'useChartMargin', () => {
|
|
|
282
282
|
expect( result.current.left ).toBe( 60 );
|
|
283
283
|
} );
|
|
284
284
|
|
|
285
|
-
it( 'reserves nothing for a hidden x axis',
|
|
285
|
+
it.each( [ 'bottom', 'top' ] )( 'reserves nothing for a hidden %s x axis', orientation => {
|
|
286
286
|
mockGetEdgeTickWidths.mockReturnValue( { first: 120, last: 60 } );
|
|
287
|
+
const largeLabels = { axisLabel: { fontSize: 16 }, tickLength: 10 } as unknown as never;
|
|
288
|
+
const theme = {
|
|
289
|
+
...baseTheme,
|
|
290
|
+
axisStyles: { ...baseTheme.axisStyles, x: { top: largeLabels, bottom: largeLabels } },
|
|
291
|
+
} as XYChartTheme;
|
|
287
292
|
|
|
288
293
|
const { result } = renderHook( () =>
|
|
289
|
-
useChartMargin( 300, datedXOptions( { display: false } ), data,
|
|
294
|
+
useChartMargin( 300, datedXOptions( { display: false, orientation } ), data, theme )
|
|
290
295
|
);
|
|
291
296
|
|
|
292
297
|
expect( mockGetEdgeTickWidths ).not.toHaveBeenCalled();
|
|
293
|
-
expect( result.current
|
|
294
|
-
expect( result.current.left ).toBe( 51 );
|
|
298
|
+
expect( result.current ).toEqual( { top: 10, right: 20, bottom: 10, left: 51 } );
|
|
295
299
|
} );
|
|
296
300
|
} );
|
|
297
301
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { renderHook } from '@testing-library/react';
|
|
2
|
+
import { CATALOG_POINTERS } from '../../providers/chart-context/private/catalog-pointers';
|
|
2
3
|
import { ChartScopeContext } from '../../providers/chart-scope';
|
|
3
4
|
import { useXYChartTheme } from '../use-xychart-theme';
|
|
4
5
|
import type { ReactNode } from 'react';
|
|
@@ -48,8 +49,7 @@ describe( 'useXYChartTheme', () => {
|
|
|
48
49
|
document.body.removeChild( scope );
|
|
49
50
|
} );
|
|
50
51
|
|
|
51
|
-
|
|
52
|
-
it( 'resolves the tooltip label color while the tick labels keep the pointer', () => {
|
|
52
|
+
it( 'hands visx the label pointer for the HTML label, even with an override in scope', () => {
|
|
53
53
|
const scope = document.createElement( 'div' );
|
|
54
54
|
scope.style.setProperty( '--a8c-charts-color-label-axis', '#0000ff' );
|
|
55
55
|
document.body.appendChild( scope );
|
|
@@ -60,7 +60,7 @@ describe( 'useXYChartTheme', () => {
|
|
|
60
60
|
|
|
61
61
|
const { result } = renderHook( () => useXYChartTheme( [] ), { wrapper } );
|
|
62
62
|
|
|
63
|
-
expect( result.current.htmlLabel.color ).toBe(
|
|
63
|
+
expect( result.current.htmlLabel.color ).toBe( CATALOG_POINTERS.labelAxis );
|
|
64
64
|
expect( result.current.svgLabelSmall.fill ).toBe(
|
|
65
65
|
'var(--a8c-charts-color-label-axis, #1e1e1e)'
|
|
66
66
|
);
|
|
@@ -27,10 +27,10 @@ const DEFAULT_MARGIN_BOTTOM = 20;
|
|
|
27
27
|
const DEFAULT_MARGIN_LEFT = 20;
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
* Bottom margin
|
|
30
|
+
* Bottom margin when no X-axis sits below the chart, because it is on top or hidden.
|
|
31
31
|
* We only need a small buffer below the chart in that case.
|
|
32
32
|
*/
|
|
33
|
-
const
|
|
33
|
+
const DEFAULT_BOTTOM_WITHOUT_X_AXIS = 10;
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
36
|
* Fallback font size used when we cannot derive a font size
|
|
@@ -168,23 +168,24 @@ export const useChartMargin = (
|
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
-
//
|
|
172
|
-
// This mirrors Y-axis behavior where margin is based on label size and tick length,
|
|
173
|
-
// but keeps the padding minimal so consumers can control container spacing themselves.
|
|
174
|
-
const xOrientation = options.axis?.x?.orientation === 'top' ? 'top' : 'bottom';
|
|
175
|
-
const { fontSize, tickLength, tickLabelStyle } = getXAxisLabelMetrics( theme, xOrientation );
|
|
176
|
-
const computedXMargin = fontSize + tickLength;
|
|
177
|
-
|
|
178
|
-
if ( xOrientation === 'top' ) {
|
|
179
|
-
defaultMargin.top = Math.max( defaultMargin.top, computedXMargin );
|
|
180
|
-
defaultMargin.bottom = DEFAULT_BOTTOM_FOR_TOP_AXIS;
|
|
181
|
-
} else {
|
|
182
|
-
defaultMargin.bottom = Math.max( defaultMargin.bottom, computedXMargin );
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// An X-axis label is centered on its tick, so the ones at either end of the
|
|
186
|
-
// scale hang half their width outside the plot area and clip at the SVG edge.
|
|
171
|
+
// A hidden x axis reserves nothing either, whichever side it sits on.
|
|
187
172
|
if ( options.axis?.x?.display !== false ) {
|
|
173
|
+
// Dynamically compute X-axis margin (bottom by default, or top if orientation is 'top').
|
|
174
|
+
// This mirrors Y-axis behavior where margin is based on label size and tick length,
|
|
175
|
+
// but keeps the padding minimal so consumers can control container spacing themselves.
|
|
176
|
+
const xOrientation = options.axis?.x?.orientation === 'top' ? 'top' : 'bottom';
|
|
177
|
+
const { fontSize, tickLength, tickLabelStyle } = getXAxisLabelMetrics( theme, xOrientation );
|
|
178
|
+
const computedXMargin = fontSize + tickLength;
|
|
179
|
+
|
|
180
|
+
if ( xOrientation === 'top' ) {
|
|
181
|
+
defaultMargin.top = Math.max( defaultMargin.top, computedXMargin );
|
|
182
|
+
defaultMargin.bottom = DEFAULT_BOTTOM_WITHOUT_X_AXIS;
|
|
183
|
+
} else {
|
|
184
|
+
defaultMargin.bottom = Math.max( defaultMargin.bottom, computedXMargin );
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// An X-axis label is centered on its tick, so the ones at either end of the
|
|
188
|
+
// scale hang half their width outside the plot area and clip at the SVG edge.
|
|
188
189
|
const { first, last } = getEdgeTickWidths(
|
|
189
190
|
options.axis?.x?.tickValues ?? [],
|
|
190
191
|
options.axis?.x?.tickFormat,
|
|
@@ -193,6 +194,8 @@ export const useChartMargin = (
|
|
|
193
194
|
|
|
194
195
|
defaultMargin.left = Math.max( defaultMargin.left, Math.ceil( first / 2 ) );
|
|
195
196
|
defaultMargin.right = Math.max( defaultMargin.right, Math.ceil( last / 2 ) );
|
|
197
|
+
} else {
|
|
198
|
+
defaultMargin.bottom = DEFAULT_BOTTOM_WITHOUT_X_AXIS;
|
|
196
199
|
}
|
|
197
200
|
|
|
198
201
|
return defaultMargin;
|
|
@@ -3,7 +3,7 @@ import { useMemo } from 'react';
|
|
|
3
3
|
import { useGlobalChartsTheme } from '../providers';
|
|
4
4
|
import { CATALOG_POINTERS } from '../providers/chart-context/private/catalog-pointers';
|
|
5
5
|
import { useChartScopeElement } from '../providers/chart-scope';
|
|
6
|
-
import { createCssVariableResolver
|
|
6
|
+
import { createCssVariableResolver } from '../utils';
|
|
7
7
|
import type { SeriesData } from '../types';
|
|
8
8
|
|
|
9
9
|
export const useXYChartTheme = ( data: SeriesData[] ) => {
|
|
@@ -18,9 +18,9 @@ export const useXYChartTheme = ( data: SeriesData[] ) => {
|
|
|
18
18
|
);
|
|
19
19
|
|
|
20
20
|
return useMemo( () => {
|
|
21
|
-
// Only what is read as a string is resolved: the palette
|
|
21
|
+
// Only what is read as a string is resolved: the palette and `backgroundColor`.
|
|
22
22
|
// At the chart's scope element, never :root, so an override inside the provider tree is seen.
|
|
23
|
-
// One resolver for
|
|
23
|
+
// One resolver for both, so they share a single getComputedStyle call.
|
|
24
24
|
const resolve = createCssVariableResolver( scopeElement );
|
|
25
25
|
const resolveColor = ( value?: string ): string | undefined =>
|
|
26
26
|
value ? ( resolve( value ) ?? value ) : value;
|
|
@@ -33,14 +33,6 @@ export const useXYChartTheme = ( data: SeriesData[] ) => {
|
|
|
33
33
|
.map( color => resolveColor( color ) )
|
|
34
34
|
.filter( ( color ): color is string => Boolean( color ) && ! color.includes( 'var(' ) );
|
|
35
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
36
|
// visx's fallbacks for whichever axis or grid style object it is not given. All four are
|
|
45
37
|
// supplied, so these reach nothing; they stay because the config type requires them.
|
|
46
38
|
return buildChartTheme( {
|
|
@@ -49,7 +41,7 @@ export const useXYChartTheme = ( data: SeriesData[] ) => {
|
|
|
49
41
|
gridColorDark: '',
|
|
50
42
|
colors: paletteColors,
|
|
51
43
|
backgroundColor: resolveColor( CATALOG_POINTERS.background ),
|
|
52
|
-
htmlLabel:
|
|
44
|
+
htmlLabel: { color: CATALOG_POINTERS.labelAxis },
|
|
53
45
|
gridStyles: { ...theme.gridStyles, stroke: CATALOG_POINTERS.grid },
|
|
54
46
|
xAxisLineStyles: { ...theme.xAxisLineStyles, stroke: CATALOG_POINTERS.axisX },
|
|
55
47
|
xTickLineStyles: { ...theme.xTickLineStyles, stroke: CATALOG_POINTERS.tickX },
|
package/src/index.ts
CHANGED
|
@@ -21,7 +21,7 @@ export { PieSemiCircleChart, PieSemiCircleChartUnresponsive } from './charts/pie
|
|
|
21
21
|
export { Sparkline, SparklineUnresponsive } from './charts/sparkline';
|
|
22
22
|
|
|
23
23
|
// Components
|
|
24
|
-
export { BaseTooltip } from './components/tooltip';
|
|
24
|
+
export { BaseTooltip, TooltipBox } from './components/tooltip';
|
|
25
25
|
export { Legend, useChartLegendItems } from './components/legend';
|
|
26
26
|
export { TrendIndicator } from './components/trend-indicator';
|
|
27
27
|
|
|
@@ -97,8 +97,13 @@ export type { LineStyles, GridStyles, EventHandlerParams } from '@visx/xychart';
|
|
|
97
97
|
export { useLeaderboardLegendItems } from './charts/leaderboard-chart/hooks';
|
|
98
98
|
|
|
99
99
|
// Previously available via '@automattic/charts/tooltip', '@automattic/charts/legend'
|
|
100
|
-
export { AccessibleTooltip } from './components/tooltip';
|
|
101
|
-
export type {
|
|
100
|
+
export { AccessibleTooltip, XYChartTooltip } from './components/tooltip';
|
|
101
|
+
export type {
|
|
102
|
+
BaseTooltipProps,
|
|
103
|
+
TooltipBoxProps,
|
|
104
|
+
TooltipData,
|
|
105
|
+
TooltipProps,
|
|
106
|
+
} from './components/tooltip';
|
|
102
107
|
export type { LegendProps, BaseLegendProps, ChartLegendOptions } from './components/legend';
|
|
103
108
|
|
|
104
109
|
// Previously available via '@automattic/charts/bar-chart', '@automattic/charts/line-chart', etc.
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { color as d3Color } from '@visx/vendor/d3-color';
|
|
2
1
|
import {
|
|
3
2
|
createContext,
|
|
4
3
|
useCallback,
|
|
@@ -14,6 +13,7 @@ import {
|
|
|
14
13
|
import '../../styles/chart-scope.scss';
|
|
15
14
|
import { CHART_SCOPE_CLASS } from '../../styles/chart-scope-class';
|
|
16
15
|
import {
|
|
16
|
+
createCssVariableResolver,
|
|
17
17
|
getItemShapeStyles,
|
|
18
18
|
getSeriesBarStyles,
|
|
19
19
|
getSeriesLineStyles,
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
LABEL_INVERSE_FALLBACK,
|
|
33
33
|
} from './private/catalog-pointers';
|
|
34
34
|
import { createPaletteGenerator } from './private/palette-generator';
|
|
35
|
+
import { resolveOpaqueHex } from './private/resolve-opaque-hex';
|
|
35
36
|
import { SERIES_PALETTE_POINTERS, SERIES_SLOT_1_FALLBACK } from './private/series-palette';
|
|
36
37
|
import { defaultTheme } from './themes';
|
|
37
38
|
import type { GlobalChartsContextValue, ChartRegistration } from './types';
|
|
@@ -45,17 +46,6 @@ interface ColorCache {
|
|
|
45
46
|
colorAt: ( index: number ) => string;
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
// A see-through color (transparent, or any alpha below 1) says nothing about what it will look like
|
|
49
|
-
// over the chart, so it resolves to null rather than let its RGB leak into the palette.
|
|
50
|
-
const resolveOpaqueHex = ( pointer: string, element: HTMLElement | null ): string | null => {
|
|
51
|
-
const raw = resolveCssVariable( pointer, element );
|
|
52
|
-
if ( ! raw || d3Color( raw )?.opacity !== 1 ) {
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
const hex = normalizeColorToHex( pointer, element, resolveCssVariable );
|
|
56
|
-
return isValidHexColor( hex ) ? hex : null;
|
|
57
|
-
};
|
|
58
|
-
|
|
59
49
|
const PLACEHOLDER_LABEL_COLORS = [ LABEL_FALLBACK, LABEL_INVERSE_FALLBACK ];
|
|
60
50
|
|
|
61
51
|
export const GlobalChartsContext = createContext< GlobalChartsContextValue | null >( null );
|
|
@@ -143,11 +133,12 @@ export const GlobalChartsProvider: FC< GlobalChartsProviderProps > = ( {
|
|
|
143
133
|
}
|
|
144
134
|
}
|
|
145
135
|
|
|
136
|
+
const resolveAtWrapper = createCssVariableResolver( wrapperRef.current );
|
|
146
137
|
const backgroundHex =
|
|
147
|
-
resolveOpaqueHex( CATALOG_POINTERS.background,
|
|
138
|
+
resolveOpaqueHex( CATALOG_POINTERS.background, resolveAtWrapper ) ?? BACKGROUND_FALLBACK;
|
|
148
139
|
// The two roles pie labels choose between on a fill; one left see-through is never painted there.
|
|
149
140
|
const labelColors = [ CATALOG_POINTERS.label, CATALOG_POINTERS.labelInverse ]
|
|
150
|
-
.map( pointer => resolveOpaqueHex( pointer,
|
|
141
|
+
.map( pointer => resolveOpaqueHex( pointer, resolveAtWrapper ) )
|
|
151
142
|
.filter( ( hex ): hex is string => hex !== null );
|
|
152
143
|
|
|
153
144
|
setColorCache( {
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useMemo, useRef } from 'react';
|
|
2
|
+
import { useIsomorphicLayoutEffect } from '../../../hooks';
|
|
2
3
|
import { useGlobalChartsContext } from './use-global-charts-context';
|
|
3
4
|
|
|
4
|
-
// Hide seeded series before paint without triggering React's SSR warning.
|
|
5
|
-
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
|
|
6
|
-
|
|
7
5
|
/**
|
|
8
6
|
* Seeds a chart's hidden series once per chart ID, the first time the provider
|
|
9
7
|
* sees that ID.
|
|
@@ -39,7 +37,7 @@ export const useDefaultHiddenSeries = (
|
|
|
39
37
|
// getHiddenSeries hands out a fresh copy per call, so hold the set still between
|
|
40
38
|
// visibility changes. Charts derive their rendered series from it, and a new
|
|
41
39
|
// identity every render invalidates every memo downstream — including the one
|
|
42
|
-
//
|
|
40
|
+
// `XYChartTooltip` watches, which closes a tooltip mid-navigation.
|
|
43
41
|
return useMemo(
|
|
44
42
|
() => ( shouldUseDefaults ? new Set( labelsRef.current ) : getHiddenSeries( chartId ) ),
|
|
45
43
|
[ shouldUseDefaults, getHiddenSeries, chartId ]
|
|
@@ -24,6 +24,7 @@ export const CATALOG_POINTERS = {
|
|
|
24
24
|
annotation: 'var(--a8c-charts-color-annotation, #1e1e1e)',
|
|
25
25
|
surface: 'var(--a8c-charts-color-surface, #fff)',
|
|
26
26
|
surfaceSecondary: 'var(--a8c-charts-color-surface-secondary, #f4f4f4)',
|
|
27
|
+
track: 'var(--a8c-charts-color-track, #f0f0f0)',
|
|
27
28
|
// Only for `renderMainMetric`, which hands a consumer's own markup something to paint with.
|
|
28
29
|
trendUp: 'var(--a8c-charts-color-trend-up, #008030)',
|
|
29
30
|
trendDown: 'var(--a8c-charts-color-trend-down, #cc1818)',
|
|
@@ -2,6 +2,9 @@ import { relativeLuminance, validateHexColor } from '../../../utils';
|
|
|
2
2
|
|
|
3
3
|
export type Lab = readonly [ number, number, number ];
|
|
4
4
|
|
|
5
|
+
/** sRGB channels from 0 to 255, left unrounded. */
|
|
6
|
+
export type Rgb = readonly [ number, number, number ];
|
|
7
|
+
|
|
5
8
|
/** A color as seen in normal vision, then under deuteranopia and protanopia. */
|
|
6
9
|
export type ColorViews = readonly [ Lab, Lab, Lab ];
|
|
7
10
|
|
|
@@ -23,13 +26,22 @@ const toLinear = ( channel: number ): number =>
|
|
|
23
26
|
const fromLinear = ( channel: number ): number =>
|
|
24
27
|
channel <= 0.0031308 ? 12.92 * channel : 1.055 * Math.pow( channel, 1 / 2.4 ) - 0.055;
|
|
25
28
|
|
|
26
|
-
|
|
29
|
+
/**
|
|
30
|
+
* The sRGB channels of a hex color.
|
|
31
|
+
*
|
|
32
|
+
* @param hex - A six-digit hex color.
|
|
33
|
+
* @return Channels from 0 to 255.
|
|
34
|
+
* @throws {Error} if the hex string is malformed
|
|
35
|
+
*/
|
|
36
|
+
export const hexToRgb = ( hex: string ): Rgb => {
|
|
27
37
|
validateHexColor( hex );
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
);
|
|
38
|
+
const [ r, g, b ] = [ 1, 3, 5 ].map( start => parseInt( hex.slice( start, start + 2 ), 16 ) );
|
|
39
|
+
return [ r, g, b ];
|
|
31
40
|
};
|
|
32
41
|
|
|
42
|
+
const hexToLinear = ( hex: string ): number[] =>
|
|
43
|
+
hexToRgb( hex ).map( channel => toLinear( channel / 255 ) );
|
|
44
|
+
|
|
33
45
|
const linearToHex = ( linear: number[] ): string =>
|
|
34
46
|
'#' +
|
|
35
47
|
linear
|
|
@@ -197,6 +209,35 @@ export const hexToOklch = ( hex: string ): { chroma: number; hue: number } => {
|
|
|
197
209
|
};
|
|
198
210
|
};
|
|
199
211
|
|
|
212
|
+
/**
|
|
213
|
+
* `foreground` over `background` at `weight`, left unrounded as CSS `color-mix` and alpha compositing leave it.
|
|
214
|
+
*
|
|
215
|
+
* A mix rounded to hex can land on the other side of a contrast threshold from the fill the browser paints.
|
|
216
|
+
*
|
|
217
|
+
* @param foreground - The color weighted by `weight`.
|
|
218
|
+
* @param background - The color it is mixed over.
|
|
219
|
+
* @param weight - Share of `foreground`, from 0 to 1.
|
|
220
|
+
* @return The mixed channels.
|
|
221
|
+
*/
|
|
222
|
+
export const blendRgb = ( foreground: Rgb, background: Rgb, weight: number ): Rgb => {
|
|
223
|
+
const share = Math.min( 1, Math.max( 0, weight ) );
|
|
224
|
+
const [ r, g, b ] = foreground.map(
|
|
225
|
+
( channel, i ) => background[ i ] + ( channel - background[ i ] ) * share
|
|
226
|
+
);
|
|
227
|
+
return [ r, g, b ];
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* WCAG relative luminance of unrounded sRGB channels.
|
|
232
|
+
*
|
|
233
|
+
* @param rgb - Channels from 0 to 255.
|
|
234
|
+
* @return Relative luminance from 0 to 1.
|
|
235
|
+
*/
|
|
236
|
+
export const rgbLuminance = ( rgb: Rgb ): number => {
|
|
237
|
+
const [ r, g, b ] = rgb.map( channel => toLinear( channel / 255 ) );
|
|
238
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
239
|
+
};
|
|
240
|
+
|
|
200
241
|
/**
|
|
201
242
|
* WCAG contrast ratio between two relative luminances.
|
|
202
243
|
*
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { color as d3Color } from '@visx/vendor/d3-color';
|
|
2
|
+
import { isValidHexColor, normalizeColorToHex } from '../../../utils/color-utils';
|
|
3
|
+
|
|
4
|
+
// A see-through color (transparent, or any alpha below 1) says nothing about what it will look like
|
|
5
|
+
// over the chart, so it resolves to null rather than let its RGB stand in for the color.
|
|
6
|
+
export const resolveOpaqueHex = (
|
|
7
|
+
pointer: string,
|
|
8
|
+
resolve: ( value: string ) => string | null
|
|
9
|
+
): string | null => {
|
|
10
|
+
const raw = resolve( pointer );
|
|
11
|
+
if ( ! raw || d3Color( raw )?.opacity !== 1 ) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
const hex = normalizeColorToHex( raw );
|
|
15
|
+
return isValidHexColor( hex ) ? hex : null;
|
|
16
|
+
};
|
|
@@ -3,6 +3,9 @@ import {
|
|
|
3
3
|
deltaE2000,
|
|
4
4
|
hexToOklch,
|
|
5
5
|
hexToViews,
|
|
6
|
+
blendRgb,
|
|
7
|
+
hexToRgb,
|
|
8
|
+
rgbLuminance,
|
|
6
9
|
oklchToHex,
|
|
7
10
|
viewDistance,
|
|
8
11
|
} from '../private/perceptual-color';
|
|
@@ -73,3 +76,31 @@ describe( 'contrastRatio', () => {
|
|
|
73
76
|
expect( contrastRatio( '#3858e9', '#3858e9' ) ).toBe( 1 );
|
|
74
77
|
} );
|
|
75
78
|
} );
|
|
79
|
+
|
|
80
|
+
describe( 'hexToRgb', () => {
|
|
81
|
+
it( 'reads each channel', () => {
|
|
82
|
+
expect( hexToRgb( '#3858e9' ) ).toEqual( [ 56, 88, 233 ] );
|
|
83
|
+
} );
|
|
84
|
+
} );
|
|
85
|
+
|
|
86
|
+
describe( 'blendRgb', () => {
|
|
87
|
+
const blue = hexToRgb( '#3858e9' );
|
|
88
|
+
const white = hexToRgb( '#ffffff' );
|
|
89
|
+
|
|
90
|
+
it( 'matches each endpoint at full weight', () => {
|
|
91
|
+
expect( blendRgb( blue, white, 1 ) ).toEqual( [ 56, 88, 233 ] );
|
|
92
|
+
expect( blendRgb( blue, white, 0 ) ).toEqual( [ 255, 255, 255 ] );
|
|
93
|
+
} );
|
|
94
|
+
|
|
95
|
+
it( 'does not round the mix to a hex channel', () => {
|
|
96
|
+
expect( blendRgb( hexToRgb( '#000000' ), white, 0.5 ) ).toEqual( [ 127.5, 127.5, 127.5 ] );
|
|
97
|
+
} );
|
|
98
|
+
} );
|
|
99
|
+
|
|
100
|
+
describe( 'rgbLuminance', () => {
|
|
101
|
+
it( 'reads unrounded channels', () => {
|
|
102
|
+
// Rounded to #808080 this would read 0.2159.
|
|
103
|
+
expect( rgbLuminance( [ 127.5, 127.5, 127.5 ] ) ).toBeCloseTo( 0.214, 4 );
|
|
104
|
+
expect( rgbLuminance( [ 56, 88, 233 ] ) ).toBeCloseTo( 0.137, 4 );
|
|
105
|
+
} );
|
|
106
|
+
} );
|
|
@@ -177,7 +177,7 @@ describe( 'useDefaultHiddenSeries', () => {
|
|
|
177
177
|
|
|
178
178
|
it( 'keeps the same set across renders until visibility changes', () => {
|
|
179
179
|
// Charts memoize their rendered series off this set. A fresh identity per
|
|
180
|
-
// render invalidates those memos, which re-runs
|
|
180
|
+
// render invalidates those memos, which re-runs `XYChartTooltip`'s
|
|
181
181
|
// effect and closes a tooltip the user is still navigating with.
|
|
182
182
|
const Harness = () => (
|
|
183
183
|
<GlobalChartsProvider>
|
|
@@ -4,3 +4,8 @@
|
|
|
4
4
|
* Deliberately unhashed: consumers are documented as being able to target it to override a token on the provider element.
|
|
5
5
|
*/
|
|
6
6
|
export const CHART_SCOPE_CLASS = 'a8c-charts-scope';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Carries the catalog on the tooltip box, re-declared under the tooltip's own theme. Separate from `CHART_SCOPE_CLASS` so a page-wide override on that class does not reach the dark box.
|
|
10
|
+
*/
|
|
11
|
+
export const TOOLTIP_SCOPE_CLASS = 'a8c-charts-tooltip-scope';
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
// the same element wins. Chart roots deliberately do NOT re-declare these: an
|
|
11
11
|
// element's own declaration beats an inherited one, so re-declaring lower down
|
|
12
12
|
// would shadow a consumer override set inside the provider tree.
|
|
13
|
-
|
|
13
|
+
// The tooltip box re-declares the catalog under its own class, so a rule on
|
|
14
|
+
// .a8c-charts-scope does not land on the dark box.
|
|
15
|
+
:where(.a8c-charts-scope, .a8c-charts-tooltip-scope) {
|
|
14
16
|
// The series palette. GlobalChartsProvider resolves these five slots at its
|
|
15
17
|
// wrapper and seeds ColorCache with whatever resolves; a slot that resolves
|
|
16
18
|
// to nothing is skipped, so setting only slots 1 and 3 gives a two-color
|
|
@@ -49,14 +51,15 @@
|
|
|
49
51
|
--a8c-charts-color-trend-down: var(--wpds-color-foreground-content-error-weak);
|
|
50
52
|
--a8c-charts-color-trend-neutral: var(--wpds-color-foreground-content-neutral-weak);
|
|
51
53
|
--a8c-charts-color-background: var(--wpds-color-background-surface-neutral-strong);
|
|
52
|
-
// Surfaces floating over the chart —
|
|
53
|
-
//
|
|
54
|
+
// Surfaces floating over the chart — the annotation label, the zoom-reset
|
|
55
|
+
// control. Same WPDS token as --a8c-charts-color-background but
|
|
54
56
|
// deliberately not derived from it, so either can move without the other.
|
|
55
57
|
--a8c-charts-color-surface: var(--wpds-color-background-surface-neutral-strong);
|
|
56
58
|
--a8c-charts-color-surface-secondary: var(--wpds-color-background-surface-neutral-weak);
|
|
57
59
|
--a8c-charts-color-track: var(--wpds-color-background-track-neutral-weak);
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
+
// The background @wordpress/ui's Tooltip themes its popup with; WPDS has
|
|
61
|
+
// no inverted surface token yet.
|
|
62
|
+
--a8c-charts-color-tooltip-surface: #1e1e1e;
|
|
60
63
|
--a8c-charts-color-zoom-selection: var(--wpds-color-background-interactive-brand-strong);
|
|
61
64
|
--a8c-charts-color-zoom-selection-stroke: var(--wpds-color-stroke-interactive-brand);
|
|
62
65
|
|
|
@@ -77,6 +80,7 @@
|
|
|
77
80
|
--a8c-charts-motion-duration-entrance: var(--wpds-motion-duration-xl);
|
|
78
81
|
--a8c-charts-motion-easing-entrance: var(--wpds-motion-easing-expressive);
|
|
79
82
|
--a8c-charts-border-radius-bar: var(--wpds-border-radius-md);
|
|
83
|
+
--a8c-charts-border-radius-bar-chart: 0;
|
|
80
84
|
--a8c-charts-border-radius-cell: var(--wpds-border-radius-sm);
|
|
81
85
|
// Pill shape; no WPDS radius reaches it.
|
|
82
86
|
--a8c-charts-border-radius-leaderboard-bar: 9999px;
|
|
@@ -87,7 +91,7 @@
|
|
|
87
91
|
|
|
88
92
|
@media (forced-colors: active) {
|
|
89
93
|
|
|
90
|
-
:where(.a8c-charts-scope) {
|
|
94
|
+
:where(.a8c-charts-scope, .a8c-charts-tooltip-scope) {
|
|
91
95
|
--a8c-charts-color-label-axis: CanvasText;
|
|
92
96
|
--a8c-charts-color-grid: GrayText;
|
|
93
97
|
--a8c-charts-color-axis-x: GrayText;
|
|
@@ -127,7 +127,7 @@ describe( 'chart scope catalog', () => {
|
|
|
127
127
|
expect( mediaRules[ 0 ].cssRules ).toHaveLength( 1 );
|
|
128
128
|
|
|
129
129
|
const rule = mediaRules[ 0 ].cssRules[ 0 ] as CSSStyleRule;
|
|
130
|
-
expect( rule.selectorText ).toBe( ':where(.a8c-charts-scope)' );
|
|
130
|
+
expect( rule.selectorText ).toBe( ':where(.a8c-charts-scope, .a8c-charts-tooltip-scope)' );
|
|
131
131
|
expect(
|
|
132
132
|
Object.fromEntries(
|
|
133
133
|
Array.from( rule.style ).map( property => [
|
|
@@ -171,8 +171,8 @@ describe( 'chart scope catalog', () => {
|
|
|
171
171
|
expect( documented.get( token )?.fallback ).toEqual( declared.get( token )?.fallback );
|
|
172
172
|
} );
|
|
173
173
|
|
|
174
|
-
it( 'scopes the catalog to
|
|
175
|
-
expect( stylesheet ).toMatch( /:where\(\.a8c-charts-scope\)\s*{/ );
|
|
174
|
+
it( 'scopes the catalog to the chart and tooltip scope classes rather than :root', () => {
|
|
175
|
+
expect( stylesheet ).toMatch( /:where\(\.a8c-charts-scope, \.a8c-charts-tooltip-scope\)\s*{/ );
|
|
176
176
|
expect( stylesheet ).not.toMatch( /(^|\s):root\s*{/ );
|
|
177
177
|
} );
|
|
178
178
|
|
|
@@ -1,15 +1,5 @@
|
|
|
1
1
|
import { parseAsLocalDate } from '../utils/date-parsing';
|
|
2
2
|
|
|
3
|
-
// The `jetpack-js-tools` console guard registers the matcher but ships no types for it.
|
|
4
|
-
declare global {
|
|
5
|
-
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
6
|
-
namespace jest {
|
|
7
|
-
interface Matchers< R > {
|
|
8
|
-
toHaveWarnedWith( ...args: unknown[] ): R;
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
|
|
13
3
|
// Every case asserts an absolute instant, so running the suite from more than one
|
|
14
4
|
// worker zone is what proves the result does not depend on the viewer's.
|
|
15
5
|
const CASES: Array< [ string, string, string ] > = [
|
package/src/utils/color-utils.ts
CHANGED
|
@@ -262,7 +262,8 @@ export const relativeLuminance = ( hex: string ): number => {
|
|
|
262
262
|
* Whether light text reads better than dark text on the given background, using the W3C
|
|
263
263
|
* luminance threshold (0.179) that maximizes contrast against black vs white.
|
|
264
264
|
*
|
|
265
|
-
* @
|
|
265
|
+
* @deprecated Assumes black and white text, so it ignores label role overrides and cannot guarantee AA; charts no longer use it, and it will be removed in a future major version.
|
|
266
|
+
* @param backgroundHex - Hex background color
|
|
266
267
|
* @return true if light text should be used; false (dark text) for malformed colors
|
|
267
268
|
*/
|
|
268
269
|
export const prefersLightText = ( backgroundHex: string ): boolean => {
|
|
@@ -1,15 +1,5 @@
|
|
|
1
1
|
import { createDateFormatter, createZonedClock, sanitizeFormatting } from '../date-formatting';
|
|
2
2
|
|
|
3
|
-
// The `jetpack-js-tools` console guard registers the matcher but ships no types for it.
|
|
4
|
-
declare global {
|
|
5
|
-
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
6
|
-
namespace jest {
|
|
7
|
-
interface Matchers< R > {
|
|
8
|
-
toHaveWarned(): R;
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
|
|
13
3
|
describe( 'createDateFormatter', () => {
|
|
14
4
|
it( 'renders an invalid date rather than throwing', () => {
|
|
15
5
|
const format = createDateFormatter(
|
package/src/visx/types.ts
CHANGED
|
@@ -33,7 +33,7 @@ export type XyChartTooltipProps< Datum extends object > = {
|
|
|
33
33
|
tooltipPlacement?: TooltipPlacement;
|
|
34
34
|
/** Override the tooltip top anchor in SVG coordinates, including negative offsets. */
|
|
35
35
|
tooltipAnchorTop?: number;
|
|
36
|
-
/**
|
|
36
|
+
/** Inline overrides on the tooltip surface; `unstyled` drops both. */
|
|
37
37
|
style?: VisxTooltipProps[ 'style' ];
|
|
38
38
|
snapTooltipToDatumX?: boolean;
|
|
39
39
|
snapTooltipToDatumY?: boolean;
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import type { LineChartProps } from '../types';
|
|
2
|
-
|
|
3
|
-
type Crosshairs = NonNullable< LineChartProps[ 'withTooltipCrosshairs' ] >;
|
|
4
|
-
|
|
5
|
-
describe( 'line chart crosshair style types', () => {
|
|
6
|
-
test( 'accepts paint attributes', () => {
|
|
7
|
-
const crosshairs: Crosshairs = { verticalStyle: { stroke: 'purple', strokeWidth: 2 } };
|
|
8
|
-
|
|
9
|
-
expect( crosshairs.verticalStyle?.stroke ).toBe( 'purple' );
|
|
10
|
-
} );
|
|
11
|
-
|
|
12
|
-
test( 'reserves geometry attributes for the chart', () => {
|
|
13
|
-
const crosshairs: Crosshairs = {
|
|
14
|
-
// @ts-expect-error Crosshair coordinates are computed from the active datum.
|
|
15
|
-
verticalStyle: { x1: 12 },
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
const transformedCrosshairs: Crosshairs = {
|
|
19
|
-
// @ts-expect-error Crosshair transforms are not paint attributes.
|
|
20
|
-
verticalStyle: { transform: 'translate(100 0)' },
|
|
21
|
-
};
|
|
22
|
-
const transformedStyles: Crosshairs = {
|
|
23
|
-
// @ts-expect-error Inline crosshair styles accept only paint properties.
|
|
24
|
-
horizontalStyle: { style: { transform: 'translateX(100px)' } },
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
expect( crosshairs ).toBeDefined();
|
|
28
|
-
expect( transformedCrosshairs ).toBeDefined();
|
|
29
|
-
expect( transformedStyles ).toBeDefined();
|
|
30
|
-
} );
|
|
31
|
-
} );
|