@automattic/charts 4.5.0 → 4.7.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 +24 -2
- package/TOKENS.md +12 -10
- package/dist/index.cjs +248 -146
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +33 -20
- package/dist/index.d.cts +22 -11
- package/dist/index.d.ts +22 -11
- package/dist/index.js +249 -147
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.module.scss +0 -10
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx +4 -2
- package/src/charts/conversion-funnel-chart/test/conversion-funnel-chart.test.tsx +11 -1
- package/src/charts/conversion-funnel-chart/types.ts +3 -0
- package/src/charts/geo-chart/geo-chart.tsx +4 -0
- package/src/charts/geo-chart/index.ts +7 -1
- package/src/charts/geo-chart/test/geo-chart.test.tsx +8 -0
- package/src/charts/geo-chart/types.ts +12 -0
- package/src/charts/heatmap-chart/heatmap-chart.module.scss +10 -3
- package/src/charts/heatmap-chart/heatmap-chart.tsx +54 -37
- package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +129 -25
- package/src/charts/heatmap-chart/types.ts +2 -6
- 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 +8 -40
- 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 +22 -76
- package/src/charts/pie-chart/test/pie-chart.test.tsx +21 -8
- 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 +191 -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/accessible-tooltip.tsx +1 -1
- package/src/components/tooltip/base-tooltip.module.scss +12 -8
- package/src/components/tooltip/base-tooltip.tsx +11 -9
- package/src/components/tooltip/private/bounded-tooltip.tsx +27 -8
- package/src/components/tooltip/private/tooltip-theme.tsx +37 -0
- package/src/components/tooltip/test/bounded-tooltip.test.tsx +82 -1
- package/src/components/tooltip/test/tool-tip.test.tsx +5 -0
- package/src/components/tooltip/test/tooltip-theme.test.tsx +61 -0
- package/src/components/tooltip/test/xy-chart-tooltip.test.tsx +41 -28
- package/src/components/tooltip/xy-chart-tooltip.tsx +29 -29
- package/src/components/trend-indicator/trend-indicator.tsx +1 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/test/use-xychart-theme.test.tsx +3 -3
- package/src/hooks/use-isomorphic-layout-effect.ts +5 -0
- package/src/hooks/use-xychart-theme.ts +4 -12
- package/src/index.ts +7 -1
- package/src/providers/chart-context/hooks/use-default-hidden-series.ts +2 -4
- package/src/providers/chart-context/private/perceptual-color.ts +45 -4
- package/src/providers/chart-context/test/perceptual-color.test.ts +31 -0
- package/src/styles/chart-scope-class.ts +5 -0
- package/src/styles/chart-scope.scss +9 -6
- package/src/styles/test/chart-scope.test.ts +3 -3
- package/src/utils/color-utils.ts +2 -1
- package/src/visx/types.ts +1 -1
- package/src/charts/line-chart/test/crosshair-style-types.test.ts +0 -31
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { render, screen, waitFor } from '@testing-library/react';
|
|
2
|
-
import { BarSeries,
|
|
2
|
+
import { BarSeries, LineSeries, TooltipContext, XYChart } from '@visx/xychart';
|
|
3
3
|
import { useContext, useEffect } from 'react';
|
|
4
4
|
import { XyChartTooltip } from '../xy-chart-tooltip';
|
|
5
5
|
import type { XyChartTooltipProps } from '../../../visx/types';
|
|
@@ -103,20 +103,35 @@ const renderChart = (
|
|
|
103
103
|
|
|
104
104
|
describe( 'XyChartTooltip', () => {
|
|
105
105
|
test.each( [
|
|
106
|
-
[ '
|
|
107
|
-
[ '
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
106
|
+
[ 'bounded', {} ],
|
|
107
|
+
[ 'unbounded', { detectBounds: false } ],
|
|
108
|
+
[ 'below-axis', { tooltipPlacement: 'below-axis' as const } ],
|
|
109
|
+
] )( 'draws the %s box on the package surface, with no visx box styles', async ( _, props ) => {
|
|
110
|
+
renderChart( props );
|
|
111
|
+
const box = await screen.findByTestId( 'tooltip-box' );
|
|
112
|
+
expect( box ).toHaveClass( 'surface' );
|
|
113
|
+
expect( box ).not.toHaveAttribute( 'style', expect.stringContaining( 'box-shadow' ) );
|
|
114
|
+
expect( box ).not.toHaveAttribute( 'style', expect.stringContaining( 'background-color' ) );
|
|
115
|
+
expect( box ).not.toHaveAttribute( 'style', expect.stringContaining( 'padding' ) );
|
|
116
|
+
expect( box ).not.toHaveAttribute( 'style', expect.stringContaining( 'border-radius' ) );
|
|
117
|
+
} );
|
|
118
|
+
|
|
119
|
+
test( 'strips the surface from an unbounded box when unstyled', async () => {
|
|
120
|
+
renderChart( { detectBounds: false, unstyled: true } );
|
|
121
|
+
const box = await screen.findByTestId( 'tooltip-box' );
|
|
122
|
+
expect( box ).not.toHaveClass( 'surface' );
|
|
123
|
+
} );
|
|
124
|
+
|
|
125
|
+
test( 'strips the surface from a bounded box when unstyled', async () => {
|
|
126
|
+
renderChart( { unstyled: true } );
|
|
127
|
+
const box = await screen.findByTestId( 'tooltip-box' );
|
|
128
|
+
expect( box ).not.toHaveClass( 'surface' );
|
|
129
|
+
} );
|
|
118
130
|
|
|
119
|
-
|
|
131
|
+
test( 'keeps a caller class beside the surface on an unbounded box', async () => {
|
|
132
|
+
renderChart( { detectBounds: false, className: 'extra' } );
|
|
133
|
+
const box = await screen.findByTestId( 'tooltip-box' );
|
|
134
|
+
expect( box ).toHaveClass( 'surface', 'extra' );
|
|
120
135
|
} );
|
|
121
136
|
|
|
122
137
|
test( 'renders the tooltip box inside the chart wrapper, not in a body-level portal', async () => {
|
|
@@ -351,25 +366,23 @@ describe( 'XyChartTooltip', () => {
|
|
|
351
366
|
} );
|
|
352
367
|
|
|
353
368
|
test.each( [ 'auto', 'below-axis' ] as const )(
|
|
354
|
-
'
|
|
369
|
+
'keeps the %s surface and stacking under a partial style override',
|
|
355
370
|
async tooltipPlacement => {
|
|
356
|
-
const { unmount } = renderChart( { tooltipPlacement } );
|
|
357
|
-
const defaultBox = await screen.findByTestId( 'tooltip-box' );
|
|
358
|
-
const background = defaultBox.style.backgroundColor;
|
|
359
|
-
expect( background ).not.toBe( '' );
|
|
360
|
-
expect( background ).not.toBe( 'transparent' );
|
|
361
|
-
unmount();
|
|
362
|
-
|
|
363
371
|
renderChart( { tooltipPlacement, style: { color: 'red' } } );
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
zIndex: '3',
|
|
368
|
-
color: 'rgb(255, 0, 0)',
|
|
369
|
-
} );
|
|
372
|
+
const box = await screen.findByTestId( 'tooltip-box' );
|
|
373
|
+
expect( box ).toHaveClass( 'surface' );
|
|
374
|
+
expect( box ).toHaveStyle( { zIndex: '3', color: 'rgb(255, 0, 0)' } );
|
|
370
375
|
}
|
|
371
376
|
);
|
|
372
377
|
|
|
378
|
+
test( 'lets a style override paint the box', async () => {
|
|
379
|
+
renderChart( { style: { background: 'rgb(0, 0, 0)', color: 'rgb(255, 255, 255)' } } );
|
|
380
|
+
await expect( screen.findByTestId( 'tooltip-box' ) ).resolves.toHaveStyle( {
|
|
381
|
+
background: 'rgb(0, 0, 0)',
|
|
382
|
+
color: 'rgb(255, 255, 255)',
|
|
383
|
+
} );
|
|
384
|
+
} );
|
|
385
|
+
|
|
373
386
|
test( 'accepts the portal-era options without passing them to the box', async () => {
|
|
374
387
|
renderChart( { scroll: true, debounce: 50, resizeObserverPolyfill: undefined } );
|
|
375
388
|
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import { Tooltip
|
|
1
|
+
import { Tooltip } from '@visx/tooltip';
|
|
2
2
|
import { DataContext, TooltipContext } from '@visx/xychart';
|
|
3
|
+
import clsx from 'clsx';
|
|
3
4
|
import { useCallback, useContext, useState } from 'react';
|
|
4
5
|
import { createPortal } from 'react-dom';
|
|
5
|
-
import {
|
|
6
|
+
import { CHART_SCOPE_CLASS } from '../../styles/chart-scope-class';
|
|
7
|
+
import styles from './base-tooltip.module.scss';
|
|
6
8
|
import { BoundedTooltip, TOOLTIP_Z_INDEX } from './private/bounded-tooltip';
|
|
9
|
+
import { TooltipTheme } from './private/tooltip-theme';
|
|
7
10
|
import type {
|
|
8
11
|
CrosshairStyle,
|
|
9
12
|
RenderTooltipGlyphProps,
|
|
@@ -215,16 +218,29 @@ const XyChartTooltipContent = < Datum extends object >( {
|
|
|
215
218
|
const marginLeft = margin?.left ?? 0;
|
|
216
219
|
|
|
217
220
|
const TooltipComponent = detectBounds || tooltipPlacement !== 'auto' ? BoundedTooltip : Tooltip;
|
|
218
|
-
const boxStyle: CSSProperties = {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
221
|
+
const boxStyle: CSSProperties = { zIndex, ...style };
|
|
222
|
+
// BoundedTooltip themes its own box.
|
|
223
|
+
const themesOwnBox = TooltipComponent === Tooltip && ! tooltipProps.unstyled;
|
|
224
|
+
const box = (
|
|
225
|
+
<TooltipComponent
|
|
226
|
+
left={ tooltipLeft }
|
|
227
|
+
top={
|
|
228
|
+
tooltipPlacement === 'below-axis'
|
|
229
|
+
? marginTop + innerHeight + ( margin?.bottom ?? 0 )
|
|
230
|
+
: ( tooltipAnchorTop ?? tooltipTop )
|
|
231
|
+
}
|
|
232
|
+
style={ boxStyle }
|
|
233
|
+
applyPositionStyle
|
|
234
|
+
{ ...tooltipProps }
|
|
235
|
+
className={ clsx(
|
|
236
|
+
themesOwnBox && [ CHART_SCOPE_CLASS, styles.surface ],
|
|
237
|
+
tooltipProps.className
|
|
238
|
+
) }
|
|
239
|
+
{ ...( tooltipPlacement !== 'auto' && { placement: tooltipPlacement } ) }
|
|
240
|
+
>
|
|
241
|
+
{ tooltipContent }
|
|
242
|
+
</TooltipComponent>
|
|
243
|
+
);
|
|
228
244
|
|
|
229
245
|
return (
|
|
230
246
|
<>
|
|
@@ -258,23 +274,7 @@ const XyChartTooltipContent = < Datum extends object >( {
|
|
|
258
274
|
{ glyphs }
|
|
259
275
|
</g>
|
|
260
276
|
{ container &&
|
|
261
|
-
createPortal(
|
|
262
|
-
<TooltipComponent
|
|
263
|
-
left={ tooltipLeft }
|
|
264
|
-
top={
|
|
265
|
-
tooltipPlacement === 'below-axis'
|
|
266
|
-
? marginTop + innerHeight + ( margin?.bottom ?? 0 )
|
|
267
|
-
: ( tooltipAnchorTop ?? tooltipTop )
|
|
268
|
-
}
|
|
269
|
-
style={ boxStyle }
|
|
270
|
-
applyPositionStyle
|
|
271
|
-
{ ...tooltipProps }
|
|
272
|
-
{ ...( tooltipPlacement !== 'auto' && { placement: tooltipPlacement } ) }
|
|
273
|
-
>
|
|
274
|
-
{ tooltipContent }
|
|
275
|
-
</TooltipComponent>,
|
|
276
|
-
container
|
|
277
|
-
) }
|
|
277
|
+
createPortal( themesOwnBox ? <TooltipTheme>{ box }</TooltipTheme> : box, container ) }
|
|
278
278
|
</>
|
|
279
279
|
);
|
|
280
280
|
};
|
|
@@ -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';
|
|
@@ -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
|
);
|
|
@@ -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
|
@@ -82,7 +82,13 @@ export type {
|
|
|
82
82
|
PieSemiCircleChartProps,
|
|
83
83
|
PieSemiCircleChartRenderTooltipParams,
|
|
84
84
|
} from './charts/pie-semi-circle-chart';
|
|
85
|
-
export type {
|
|
85
|
+
export type {
|
|
86
|
+
GeoChartProps,
|
|
87
|
+
GeoRegion,
|
|
88
|
+
GeoResolution,
|
|
89
|
+
GeoDisplayMode,
|
|
90
|
+
GeoChartError,
|
|
91
|
+
} from './charts/geo-chart';
|
|
86
92
|
export type { LegendValueDisplay, BaseLegendItem } from './components/legend';
|
|
87
93
|
export type { TrendIndicatorProps, TrendDirection } from './components/trend-indicator';
|
|
88
94
|
export type { LineStyles, GridStyles, EventHandlerParams } from '@visx/xychart';
|
|
@@ -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.
|
|
@@ -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
|
*
|
|
@@ -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
|
+
} );
|
|
@@ -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
|
|
|
@@ -87,7 +90,7 @@
|
|
|
87
90
|
|
|
88
91
|
@media (forced-colors: active) {
|
|
89
92
|
|
|
90
|
-
:where(.a8c-charts-scope) {
|
|
93
|
+
:where(.a8c-charts-scope, .a8c-charts-tooltip-scope) {
|
|
91
94
|
--a8c-charts-color-label-axis: CanvasText;
|
|
92
95
|
--a8c-charts-color-grid: GrayText;
|
|
93
96
|
--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
|
|
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 => {
|
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
|
-
} );
|