@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
package/package.json
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
useCallback,
|
|
13
13
|
} from 'react';
|
|
14
14
|
import { Legend, useChartLegendItems } from '../../components/legend';
|
|
15
|
-
import {
|
|
15
|
+
import { XYChartTooltip, useKeyboardNavigation } from '../../components/tooltip';
|
|
16
16
|
import {
|
|
17
17
|
useXYChartTheme,
|
|
18
18
|
useChartDataTransform,
|
|
@@ -509,7 +509,7 @@ const AreaChartInternal = forwardRef< ChartInstanceRef, AreaChartProps >(
|
|
|
509
509
|
|
|
510
510
|
{ withTooltips && (
|
|
511
511
|
<>
|
|
512
|
-
<
|
|
512
|
+
<XYChartTooltip
|
|
513
513
|
detectBounds
|
|
514
514
|
snapTooltipToDatumX
|
|
515
515
|
// Stacked mode: yAccessor returns raw value, not stacked y — snapping mispositions.
|
|
@@ -817,7 +817,7 @@ describe( 'AreaChart', () => {
|
|
|
817
817
|
} );
|
|
818
818
|
|
|
819
819
|
describe( 'Hover glyphs', () => {
|
|
820
|
-
// Trigger the
|
|
820
|
+
// Trigger the XYChartTooltip's keyboard nav so a tooltip is opened
|
|
821
821
|
// against a known datum index, which is the only reliable way to
|
|
822
822
|
// surface the visx TooltipContext state in a jsdom environment.
|
|
823
823
|
const focusFirstDatum = async () => {
|
|
@@ -19,7 +19,7 @@ type CapturedRenderTooltip = ( params: RenderTooltipParams< DataPointDate > ) =>
|
|
|
19
19
|
let capturedRenderTooltip: CapturedRenderTooltip | undefined;
|
|
20
20
|
jest.mock( '../../../components/tooltip', () => ( {
|
|
21
21
|
...jest.requireActual( '../../../components/tooltip' ),
|
|
22
|
-
|
|
22
|
+
XYChartTooltip: ( props: { renderTooltip?: CapturedRenderTooltip } ) => {
|
|
23
23
|
capturedRenderTooltip = props.renderTooltip;
|
|
24
24
|
return null;
|
|
25
25
|
},
|
|
@@ -4,6 +4,13 @@
|
|
|
4
4
|
overflow: visible;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
// The CSS property, not the attribute: an SVG geometry attribute does
|
|
8
|
+
// not substitute var().
|
|
9
|
+
:global(.visx-bar),
|
|
10
|
+
:global(.bar-chart__comparison-bar) {
|
|
11
|
+
rx: var(--a8c-charts-border-radius-bar-chart);
|
|
12
|
+
}
|
|
13
|
+
|
|
7
14
|
// Keep the collapsed state in `from` and never on the element: a dropped
|
|
8
15
|
// animation then fails visible. `both`, not `forwards`, for backwards fill.
|
|
9
16
|
&--animated rect {
|
|
@@ -4,7 +4,7 @@ import { __, sprintf } from '@wordpress/i18n';
|
|
|
4
4
|
import clsx from 'clsx';
|
|
5
5
|
import { useCallback, useContext, useState, useRef, useMemo } from 'react';
|
|
6
6
|
import { Legend, useChartLegendItems } from '../../components/legend';
|
|
7
|
-
import {
|
|
7
|
+
import { XYChartTooltip, useKeyboardNavigation } from '../../components/tooltip';
|
|
8
8
|
import {
|
|
9
9
|
useXYChartTheme,
|
|
10
10
|
useChartDataTransform,
|
|
@@ -242,7 +242,7 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
242
242
|
);
|
|
243
243
|
|
|
244
244
|
// The keyboard-navigation index space and the highlight CSS both stride over primary
|
|
245
|
-
// bars only;
|
|
245
|
+
// bars only; `XYChartTooltip` must use the same list, or its datum diverges from
|
|
246
246
|
// the highlighted bar once a comparison series shifts the indices.
|
|
247
247
|
const primarySeries = useMemo(
|
|
248
248
|
() => primaryEntries.map( ( { series } ) => series ),
|
|
@@ -474,7 +474,7 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
474
474
|
// so a patterned shadow gets the same outline as its primary bar.
|
|
475
475
|
return `
|
|
476
476
|
.visx-bar[fill="url(#${ patternId })"],
|
|
477
|
-
.bar-chart__comparison-
|
|
477
|
+
.bar-chart__comparison-bar[fill="url(#${ patternId })"] {
|
|
478
478
|
stroke: ${ color };
|
|
479
479
|
stroke-width: 1;
|
|
480
480
|
}
|
|
@@ -767,23 +767,29 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
767
767
|
<WholeNumberTicks { ...wholeNumberTicksProps }>
|
|
768
768
|
{ valueTicks => (
|
|
769
769
|
<>
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
770
|
+
{ chartOptions.axis.x.display !== false && (
|
|
771
|
+
<Axis
|
|
772
|
+
{ ...chartOptions.axis.x }
|
|
773
|
+
{ ...( horizontal && valueTicks
|
|
774
|
+
? { tickValues: valueTicks }
|
|
775
|
+
: {} ) }
|
|
776
|
+
/>
|
|
777
|
+
) }
|
|
778
|
+
{ chartOptions.axis.y.display !== false && (
|
|
779
|
+
<Axis
|
|
780
|
+
{ ...chartOptions.axis.y }
|
|
781
|
+
{ ...( ! horizontal && valueTicks
|
|
782
|
+
? { tickValues: valueTicks }
|
|
783
|
+
: {} ) }
|
|
784
|
+
/>
|
|
785
|
+
) }
|
|
780
786
|
</>
|
|
781
787
|
) }
|
|
782
788
|
</WholeNumberTicks>
|
|
783
789
|
) }
|
|
784
790
|
|
|
785
791
|
{ withTooltips && (
|
|
786
|
-
<
|
|
792
|
+
<XYChartTooltip
|
|
787
793
|
tooltipPlacement={ tooltipPlacement }
|
|
788
794
|
tooltipAnchorTop={ tooltipAnchorTop }
|
|
789
795
|
style={ tooltipStyle }
|
|
@@ -806,6 +806,31 @@ describe( 'BarChart', () => {
|
|
|
806
806
|
);
|
|
807
807
|
} );
|
|
808
808
|
|
|
809
|
+
describe( 'Axis display', () => {
|
|
810
|
+
test.each( [
|
|
811
|
+
[ 'vertical', 'x', 'y' ],
|
|
812
|
+
[ 'vertical', 'y', 'x' ],
|
|
813
|
+
[ 'horizontal', 'x', 'y' ],
|
|
814
|
+
[ 'horizontal', 'y', 'x' ],
|
|
815
|
+
] )( 'skips a hidden axis (%s, hidden %s)', ( orientation, hidden, shown ) => {
|
|
816
|
+
renderWithTheme( {
|
|
817
|
+
orientation,
|
|
818
|
+
options: {
|
|
819
|
+
axis: {
|
|
820
|
+
[ hidden ]: { display: false, axisClassName: 'test-hidden-axis' },
|
|
821
|
+
[ shown ]: { axisClassName: 'test-shown-axis' },
|
|
822
|
+
},
|
|
823
|
+
},
|
|
824
|
+
} );
|
|
825
|
+
const chart = screen.getByRole( 'grid' );
|
|
826
|
+
|
|
827
|
+
// eslint-disable-next-line testing-library/no-node-access -- See the visx node constraint above.
|
|
828
|
+
expect( chart.querySelector( '.test-hidden-axis' ) ).toBeNull();
|
|
829
|
+
// eslint-disable-next-line testing-library/no-node-access -- See the visx node constraint above.
|
|
830
|
+
expect( chart.querySelector( '.test-shown-axis' ) ).not.toBeNull();
|
|
831
|
+
} );
|
|
832
|
+
} );
|
|
833
|
+
|
|
809
834
|
describe( 'Grid Visibility', () => {
|
|
810
835
|
test( 'renders with different grid visibility options', () => {
|
|
811
836
|
const { rerender } = renderWithTheme( { gridVisibility: 'x' } );
|
|
@@ -114,16 +114,6 @@
|
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
.tooltip-wrapper {
|
|
118
|
-
background: var(--a8c-charts-color-surface);
|
|
119
|
-
|
|
120
|
-
// Override .visx-tooltip inline styles.
|
|
121
|
-
border-radius: var(--wpds-border-radius-md) !important;
|
|
122
|
-
padding: var(--wpds-dimension-padding-md) !important;
|
|
123
|
-
box-shadow: var(--a8c-charts-elevation-sm, 0 1px 2px 0 #0000000d, 0 2px 3px 0 #0000000a, 0 6px 6px 0 #00000008, 0 8px 8px 0 #00000005) !important;
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
|
|
127
117
|
.tooltip-title {
|
|
128
118
|
color: var(--a8c-charts-color-label);
|
|
129
119
|
font-size: var(--wpds-typography-font-size-sm);
|
|
@@ -36,6 +36,7 @@ import type { FunnelStep, ConversionFunnelChartProps } from './types';
|
|
|
36
36
|
* @param props.renderStepRate - Custom render function for step rates
|
|
37
37
|
* @param props.renderMainMetric - Custom render function for the entire main metric section
|
|
38
38
|
* @param props.renderTooltip - Custom render function for tooltip content
|
|
39
|
+
* @param props.tooltipStyle - Inline styles merged over the tooltip box defaults
|
|
39
40
|
* @return JSX element representing the conversion funnel chart
|
|
40
41
|
*/
|
|
41
42
|
const ConversionFunnelChartInternal: FC< ConversionFunnelChartProps > = ( {
|
|
@@ -52,6 +53,7 @@ const ConversionFunnelChartInternal: FC< ConversionFunnelChartProps > = ( {
|
|
|
52
53
|
renderStepRate,
|
|
53
54
|
renderMainMetric,
|
|
54
55
|
renderTooltip,
|
|
56
|
+
tooltipStyle,
|
|
55
57
|
} ) => {
|
|
56
58
|
const chartId = useChartId( providedChartId );
|
|
57
59
|
const { getElementStyles, isColorPaletteResolved } = useGlobalChartsContext();
|
|
@@ -412,7 +414,6 @@ const ConversionFunnelChartInternal: FC< ConversionFunnelChartProps > = ( {
|
|
|
412
414
|
index: steps.findIndex( s => s.id === ( tooltipData as FunnelStep ).id ),
|
|
413
415
|
top: tooltipTop,
|
|
414
416
|
left: tooltipLeft,
|
|
415
|
-
className: styles[ 'tooltip-wrapper' ],
|
|
416
417
|
} )
|
|
417
418
|
: renderDefaultTooltip( tooltipData as FunnelStep );
|
|
418
419
|
|
|
@@ -423,9 +424,10 @@ const ConversionFunnelChartInternal: FC< ConversionFunnelChartProps > = ( {
|
|
|
423
424
|
<BoundedTooltip
|
|
424
425
|
top={ tooltipTop }
|
|
425
426
|
left={ tooltipLeft }
|
|
426
|
-
className={
|
|
427
|
+
className={ standaloneScopeClass }
|
|
428
|
+
style={ tooltipStyle }
|
|
427
429
|
>
|
|
428
|
-
{ tooltipContent }
|
|
430
|
+
<div role="tooltip">{ tooltipContent }</div>
|
|
429
431
|
</BoundedTooltip>
|
|
430
432
|
);
|
|
431
433
|
} )() }
|
|
@@ -427,10 +427,29 @@ describe( 'ConversionFunnelChart', () => {
|
|
|
427
427
|
index: 1,
|
|
428
428
|
top: expect.any( Number ),
|
|
429
429
|
left: expect.any( Number ),
|
|
430
|
-
className: 'tooltip-wrapper',
|
|
431
430
|
} );
|
|
432
431
|
} );
|
|
433
432
|
|
|
433
|
+
it( 'exposes the tooltip content with role="tooltip"', async () => {
|
|
434
|
+
const user = userEvent.setup();
|
|
435
|
+
renderWithoutTheme( <ConversionFunnelChart { ...defaultProps } /> );
|
|
436
|
+
|
|
437
|
+
await user.click( screen.getByRole( 'button', { name: /cart/i } ) );
|
|
438
|
+
|
|
439
|
+
expect( screen.getByRole( 'tooltip' ) ).toHaveTextContent( 'Cart' );
|
|
440
|
+
} );
|
|
441
|
+
|
|
442
|
+
it( 'merges tooltipStyle onto the tooltip box', async () => {
|
|
443
|
+
const user = userEvent.setup();
|
|
444
|
+
renderWithoutTheme(
|
|
445
|
+
<ConversionFunnelChart { ...defaultProps } tooltipStyle={ { background: 'purple' } } />
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
await user.click( screen.getByRole( 'button', { name: /cart/i } ) );
|
|
449
|
+
|
|
450
|
+
expect( screen.getByTestId( 'bounded-tooltip' ) ).toHaveStyle( { background: 'purple' } );
|
|
451
|
+
} );
|
|
452
|
+
|
|
434
453
|
it( 'disables tooltip when renderTooltip returns null', async () => {
|
|
435
454
|
const user = userEvent.setup();
|
|
436
455
|
const customRenderTooltip = jest.fn( () => null );
|
|
@@ -50,6 +50,7 @@ export interface TooltipRenderProps {
|
|
|
50
50
|
index: number;
|
|
51
51
|
top: number;
|
|
52
52
|
left: number;
|
|
53
|
+
/** @deprecated The tooltip box draws the package surface; this is no longer set. */
|
|
53
54
|
className?: string;
|
|
54
55
|
}
|
|
55
56
|
|
|
@@ -80,4 +81,6 @@ export interface ConversionFunnelChartProps extends Pick<
|
|
|
80
81
|
renderMainMetric?: ( props: MainMetricRenderProps ) => React.ReactNode;
|
|
81
82
|
/** Custom render function for tooltip content */
|
|
82
83
|
renderTooltip?: ( props: TooltipRenderProps ) => React.ReactNode;
|
|
84
|
+
/** Inline styles merged over the tooltip box defaults. */
|
|
85
|
+
tooltipStyle?: React.CSSProperties;
|
|
83
86
|
}
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
// module, so it needs no runtime cascade and never lands on the DOM.
|
|
4
4
|
$focus-ring-width: var(--wpds-border-width-focus);
|
|
5
5
|
|
|
6
|
+
// JS sets the ends (getHeatmapScale): 3:1 against background and empty cell at
|
|
7
|
+
// the low end. Unresolved, the floor is 15% of the primary.
|
|
8
|
+
$heatmap-fill: color-mix(in sRGB, var(--a8c-charts-color-heatmap-high, var(--a8c-charts-color-heatmap-primary)) calc(var(--a8c-charts-heatmap-cell-intensity) * 100%), var(--a8c-charts-color-heatmap-low, color-mix(in sRGB, var(--a8c-charts-color-heatmap-primary) 15%, var(--a8c-charts-color-background, #fff))));
|
|
9
|
+
|
|
6
10
|
.heatmap-chart {
|
|
7
11
|
position: relative;
|
|
8
12
|
// Keeps the tooltip's z-index inside the chart, as `.xy-plot` does.
|
|
@@ -165,10 +169,8 @@ $focus-ring-width: var(--wpds-border-width-focus);
|
|
|
165
169
|
margin-inline-end: var(--a8c-charts-dimension-heatmap-cell-gap, var(--wpds-dimension-gap-xs));
|
|
166
170
|
}
|
|
167
171
|
|
|
168
|
-
// Floor the mix at 15% so the lowest value stays visibly tinted, distinct from
|
|
169
|
-
// an empty cell.
|
|
170
172
|
.heatmap-chart__cell--filled {
|
|
171
|
-
background:
|
|
173
|
+
background: $heatmap-fill;
|
|
172
174
|
}
|
|
173
175
|
|
|
174
176
|
.heatmap-chart__cell--selected {
|
|
@@ -183,12 +185,19 @@ $focus-ring-width: var(--wpds-border-width-focus);
|
|
|
183
185
|
font-size: var(--wpds-typography-font-size-md);
|
|
184
186
|
}
|
|
185
187
|
|
|
186
|
-
//
|
|
187
|
-
|
|
188
|
-
.heatmap-chart__cell--strong .heatmap-chart__cell-value {
|
|
188
|
+
// Chosen per cell in JS from the blended fill; see pickLabelTextColorForFill.
|
|
189
|
+
.heatmap-chart__cell-value--inverse {
|
|
189
190
|
color: var(--a8c-charts-color-label-inverse);
|
|
190
191
|
}
|
|
191
192
|
|
|
193
|
+
.heatmap-chart__cell-value--black {
|
|
194
|
+
color: #000;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.heatmap-chart__cell-value--white {
|
|
198
|
+
color: #fff;
|
|
199
|
+
}
|
|
200
|
+
|
|
192
201
|
.heatmap-chart__legend-label {
|
|
193
202
|
color: var(--a8c-charts-color-label);
|
|
194
203
|
}
|
|
@@ -197,7 +206,7 @@ $focus-ring-width: var(--wpds-border-width-focus);
|
|
|
197
206
|
width: var(--wpds-dimension-size-3xs);
|
|
198
207
|
height: var(--wpds-dimension-size-3xs);
|
|
199
208
|
border-radius: var(--a8c-charts-border-radius-cell);
|
|
200
|
-
background:
|
|
209
|
+
background: $heatmap-fill;
|
|
201
210
|
}
|
|
202
211
|
|
|
203
212
|
// One band: the steps butt against each other and only the band's ends round.
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { formatNumber, formatNumberCompact } from '@automattic/number-formatters';
|
|
2
|
-
import {
|
|
2
|
+
import { useTooltip } from '@visx/tooltip';
|
|
3
3
|
import { __ } from '@wordpress/i18n';
|
|
4
4
|
import clsx from 'clsx';
|
|
5
|
+
import isEqual from 'fast-deep-equal';
|
|
5
6
|
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
6
|
-
import tooltipStyles from '../../components/tooltip/base-tooltip.module.scss';
|
|
7
7
|
import { BoundedTooltip, TOOLTIP_Z_INDEX } from '../../components/tooltip/private/bounded-tooltip';
|
|
8
|
+
import { useIsomorphicLayoutEffect } from '../../hooks';
|
|
8
9
|
import {
|
|
9
10
|
GlobalChartsProvider,
|
|
10
11
|
useChartId,
|
|
@@ -12,27 +13,30 @@ import {
|
|
|
12
13
|
useGlobalChartsContext,
|
|
13
14
|
GlobalChartsContext,
|
|
14
15
|
} from '../../providers';
|
|
15
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
BACKGROUND_FALLBACK,
|
|
18
|
+
CATALOG_POINTERS,
|
|
19
|
+
} from '../../providers/chart-context/private/catalog-pointers';
|
|
20
|
+
import { blendRgb, hexToRgb } from '../../providers/chart-context/private/perceptual-color';
|
|
21
|
+
import { resolveOpaqueHex } from '../../providers/chart-context/private/resolve-opaque-hex';
|
|
16
22
|
import { useStandaloneScopeClass } from '../../providers/chart-scope';
|
|
17
23
|
import { attachSubComponents } from '../../utils';
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
mixHexColors,
|
|
21
|
-
normalizeColorToHex,
|
|
22
|
-
prefersLightText,
|
|
23
|
-
} from '../../utils/color-utils';
|
|
24
|
-
import { resolveCssVariable } from '../../utils/resolve-css-var';
|
|
24
|
+
import { isValidHexColor, normalizeColorToHex } from '../../utils/color-utils';
|
|
25
|
+
import { createCssVariableResolver } from '../../utils/resolve-css-var';
|
|
25
26
|
import { Center } from '../private/center';
|
|
26
27
|
import { useChartChildren } from '../private/chart-composition';
|
|
27
28
|
import { ChartInstanceContext } from '../private/chart-instance-context';
|
|
28
29
|
import { ChartLayout } from '../private/chart-layout';
|
|
30
|
+
import { pickLabelTextColorForFill, resolveLabelRoles } from '../private/label-text-color';
|
|
29
31
|
import { withResponsive } from '../private/with-responsive';
|
|
30
32
|
import styles from './heatmap-chart.module.scss';
|
|
31
33
|
import {
|
|
34
|
+
getHeatmapScale,
|
|
32
35
|
getValueExtent,
|
|
33
36
|
getNormalizedValue,
|
|
34
37
|
HeatmapContext,
|
|
35
38
|
HeatmapLegend,
|
|
39
|
+
isEmptyValue,
|
|
36
40
|
isPresent,
|
|
37
41
|
resolveColumnGroups,
|
|
38
42
|
} from './private';
|
|
@@ -46,22 +50,23 @@ import {
|
|
|
46
50
|
import type { HeatmapContextValue } from './private';
|
|
47
51
|
import type { CellBlock, CellPosition } from './private/keyboard-navigation';
|
|
48
52
|
import type { HeatmapChartProps, HeatmapTooltipData } from './types';
|
|
53
|
+
import type { LabelRoles, LabelTextColor } from '../private/label-text-color';
|
|
49
54
|
import type { ResponsiveConfig } from '../private/with-responsive';
|
|
50
55
|
import type { CSSProperties, FC } from 'react';
|
|
51
56
|
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
57
|
+
// `label` is the stylesheet's default, so it needs no modifier.
|
|
58
|
+
const CELL_VALUE_MODIFIER: Record< LabelTextColor, string | undefined > = {
|
|
59
|
+
label: undefined,
|
|
60
|
+
'label-inverse': styles[ 'heatmap-chart__cell-value--inverse' ],
|
|
61
|
+
black: styles[ 'heatmap-chart__cell-value--black' ],
|
|
62
|
+
white: styles[ 'heatmap-chart__cell-value--white' ],
|
|
63
|
+
};
|
|
55
64
|
|
|
56
65
|
// One instance, not a `[]` default in the signature: `buildTooltipData` keys on
|
|
57
66
|
// it, and a fresh array per render re-ran the keyboard tooltip effect endlessly.
|
|
58
67
|
const NO_ROW_LABELS: string[] = [];
|
|
59
68
|
|
|
60
|
-
|
|
61
|
-
const TOOLTIP_BOX_STYLES: Record< 'light' | 'dark', CSSProperties > = {
|
|
62
|
-
light: { ...visxTooltipStyles, zIndex: TOOLTIP_Z_INDEX },
|
|
63
|
-
dark: { zIndex: TOOLTIP_Z_INDEX },
|
|
64
|
-
};
|
|
69
|
+
const TOOLTIP_BOX_STYLE: CSSProperties = { zIndex: TOOLTIP_Z_INDEX };
|
|
65
70
|
|
|
66
71
|
// The cell's own label wins; otherwise the group, column and row labels name it.
|
|
67
72
|
const cellName = ( info: HeatmapTooltipData ) =>
|
|
@@ -88,14 +93,13 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
88
93
|
gap = 'md',
|
|
89
94
|
withTooltips = false,
|
|
90
95
|
renderTooltip,
|
|
91
|
-
tooltipVariant = 'light',
|
|
92
96
|
tooltipStyle,
|
|
93
97
|
children,
|
|
94
98
|
} ) => {
|
|
95
99
|
const chartId = useChartId( providedChartId );
|
|
96
100
|
const tooltipBoxStyle = tooltipStyle
|
|
97
|
-
? { ...
|
|
98
|
-
:
|
|
101
|
+
? { ...TOOLTIP_BOX_STYLE, ...tooltipStyle }
|
|
102
|
+
: TOOLTIP_BOX_STYLE;
|
|
99
103
|
const { getElementStyles, theme } = useGlobalChartsContext();
|
|
100
104
|
const scopeElement = useChartScopeElement();
|
|
101
105
|
const { heatmapChart: heatmapChartSettings } = theme;
|
|
@@ -106,6 +110,7 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
106
110
|
useTooltip< HeatmapTooltipData >();
|
|
107
111
|
const standaloneScopeClass = useStandaloneScopeClass();
|
|
108
112
|
const containerRef = useRef< HTMLDivElement >( null );
|
|
113
|
+
const [ labelRoles, setLabelRoles ] = useState< LabelRoles | null >( null );
|
|
109
114
|
// The chart root positions the tooltip, so pointer and cell coordinates are
|
|
110
115
|
// measured against it — found by its id rather than by walking up, so
|
|
111
116
|
// whatever ChartLayout wraps the grid in cannot shift the origin.
|
|
@@ -122,32 +127,56 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
122
127
|
overrideColor: primaryColor,
|
|
123
128
|
} );
|
|
124
129
|
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
);
|
|
130
|
+
// Both read at the scope element, so an override on the chart's own class reaches neither the
|
|
131
|
+
// fill scale nor the text color (CHARTS-255). See-through counts as white, as for the palette.
|
|
132
|
+
const resolveAtScope = createCssVariableResolver( scopeElement );
|
|
133
|
+
const chartBackgroundHex =
|
|
134
|
+
resolveOpaqueHex( CATALOG_POINTERS.background, resolveAtScope ) ?? BACKGROUND_FALLBACK;
|
|
135
|
+
const emptyCellHex = resolveOpaqueHex( CATALOG_POINTERS.track, resolveAtScope );
|
|
132
136
|
|
|
133
|
-
//
|
|
134
|
-
// If either color cannot resolve to hex, keep dark text.
|
|
137
|
+
// If the primary cannot resolve to hex, the stylesheet falls back to its own blend.
|
|
135
138
|
const primaryHex = normalizeColorToHex( primaryColorHex );
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
139
|
+
const scale = useMemo(
|
|
140
|
+
() =>
|
|
141
|
+
isValidHexColor( primaryHex )
|
|
142
|
+
? getHeatmapScale( primaryHex, chartBackgroundHex, emptyCellHex )
|
|
143
|
+
: null,
|
|
144
|
+
[ primaryHex, chartBackgroundHex, emptyCellHex ]
|
|
145
|
+
);
|
|
146
|
+
const fillVars = useMemo(
|
|
147
|
+
() => ( {
|
|
148
|
+
'--a8c-charts-color-heatmap-primary': primaryColorHex,
|
|
149
|
+
...( scale && {
|
|
150
|
+
'--a8c-charts-color-heatmap-low': scale.low,
|
|
151
|
+
'--a8c-charts-color-heatmap-high': scale.high,
|
|
152
|
+
} ),
|
|
153
|
+
} ),
|
|
154
|
+
[ primaryColorHex, scale ]
|
|
155
|
+
);
|
|
156
|
+
// Choose text color from the fill the cell paints, mirroring .heatmap-chart__cell--filled.
|
|
157
|
+
const cellTextColor = ( intensity: number ): LabelTextColor =>
|
|
158
|
+
scale
|
|
159
|
+
? pickLabelTextColorForFill(
|
|
160
|
+
blendRgb( hexToRgb( scale.high ), hexToRgb( scale.low ), intensity ),
|
|
161
|
+
labelRoles,
|
|
162
|
+
'label'
|
|
163
|
+
)
|
|
164
|
+
: 'label';
|
|
165
|
+
|
|
166
|
+
// Read inside the chart's own class, where the cell text inherits them. Keyed on `data` too,
|
|
167
|
+
// because the grid is not mounted while there is nothing to draw.
|
|
168
|
+
useIsomorphicLayoutEffect( () => {
|
|
169
|
+
if ( ! containerRef.current ) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const next = resolveLabelRoles( createCssVariableResolver( containerRef.current ) );
|
|
173
|
+
setLabelRoles( previous => ( isEqual( previous, next ) ? previous : next ) );
|
|
174
|
+
}, [ data, className, primaryColorHex, chartBackgroundHex ] );
|
|
146
175
|
|
|
147
176
|
const extent = useMemo( () => getValueExtent( data ), [ data ] );
|
|
148
177
|
const heatmapContext = useMemo< HeatmapContextValue >(
|
|
149
|
-
() => ( { extent,
|
|
150
|
-
[ extent,
|
|
178
|
+
() => ( { extent, fillVars } ),
|
|
179
|
+
[ extent, fillVars ]
|
|
151
180
|
);
|
|
152
181
|
|
|
153
182
|
const columns = data.length;
|
|
@@ -371,7 +400,7 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
371
400
|
)
|
|
372
401
|
.join( ' ' );
|
|
373
402
|
const gridStyle: Record< string, string | number > = {
|
|
374
|
-
|
|
403
|
+
...fillVars,
|
|
375
404
|
gridTemplateColumns: `auto ${ columnTracks }`,
|
|
376
405
|
gridTemplateRows: `${ hasColumnLabels ? 'auto ' : '' }repeat(${ rows }, ${ rowTrack })${
|
|
377
406
|
hasGroups ? ' auto' : ''
|
|
@@ -523,7 +552,7 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
523
552
|
const value = cell?.value ?? null;
|
|
524
553
|
const present = isPresent( value );
|
|
525
554
|
// A summary cell is on another scale, so it takes no fill.
|
|
526
|
-
const filled = present && ! column.summary;
|
|
555
|
+
const filled = present && ! column.summary && ! isEmptyValue( value, extent );
|
|
527
556
|
const normalized = filled ? getNormalizedValue( value, extent ) : 0;
|
|
528
557
|
const info = buildTooltipData( columnIndex, rowIndex );
|
|
529
558
|
const accessibleLabel = `${ cellName( info ) }: ${
|
|
@@ -547,8 +576,6 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
547
576
|
data-row={ rowIndex }
|
|
548
577
|
className={ clsx( styles[ 'heatmap-chart__cell' ], {
|
|
549
578
|
[ styles[ 'heatmap-chart__cell--filled' ] ]: filled,
|
|
550
|
-
[ styles[ 'heatmap-chart__cell--strong' ] ]:
|
|
551
|
-
filled && cellHasLightText( normalized ),
|
|
552
579
|
[ styles[ 'heatmap-chart__cell--summary' ] ]: column.summary,
|
|
553
580
|
...summaryGaps( columnIndex ),
|
|
554
581
|
[ styles[ 'heatmap-chart__cell--selected' ] ]:
|
|
@@ -566,7 +593,12 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
566
593
|
onMouseLeave={ handleCellMouseLeave }
|
|
567
594
|
>
|
|
568
595
|
{ ( drawValues || column.summary ) && present && (
|
|
569
|
-
<span
|
|
596
|
+
<span
|
|
597
|
+
className={ clsx(
|
|
598
|
+
styles[ 'heatmap-chart__cell-value' ],
|
|
599
|
+
filled && CELL_VALUE_MODIFIER[ cellTextColor( normalized ) ]
|
|
600
|
+
) }
|
|
601
|
+
>
|
|
570
602
|
{ /* Compact display; tooltip and aria-label keep full precision. */ }
|
|
571
603
|
{ formatNumberCompact( value ) }
|
|
572
604
|
</span>
|
|
@@ -596,12 +628,7 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
596
628
|
) }
|
|
597
629
|
</div>
|
|
598
630
|
{ withTooltips && tooltipOpen && tooltipData && (
|
|
599
|
-
<BoundedTooltip
|
|
600
|
-
top={ tooltipTop }
|
|
601
|
-
left={ tooltipLeft }
|
|
602
|
-
className={ tooltipVariant === 'dark' ? tooltipStyles.surface : undefined }
|
|
603
|
-
style={ tooltipBoxStyle }
|
|
604
|
-
>
|
|
631
|
+
<BoundedTooltip top={ tooltipTop } left={ tooltipLeft } style={ tooltipBoxStyle }>
|
|
605
632
|
<div className={ standaloneScopeClass } role="tooltip" tabIndex={ -1 }>
|
|
606
633
|
{ ( renderTooltip ?? defaultRenderTooltip )( tooltipData ) }
|
|
607
634
|
</div>
|
|
@@ -2,8 +2,8 @@ import { createContext } from 'react';
|
|
|
2
2
|
|
|
3
3
|
export type HeatmapContextValue = {
|
|
4
4
|
extent: [ number, number ];
|
|
5
|
-
/** The
|
|
6
|
-
|
|
5
|
+
/** The custom properties the fill reads, set on the grid and on each legend swatch. */
|
|
6
|
+
fillVars: Record< string, string >;
|
|
7
7
|
};
|
|
8
8
|
|
|
9
9
|
/** Shared by the chart and legend without importing back from `heatmap-chart.tsx`. */
|
|
@@ -29,7 +29,7 @@ export const HeatmapLegend: FC< HeatmapLegendProps > = ( {
|
|
|
29
29
|
if ( ! context ) {
|
|
30
30
|
return null;
|
|
31
31
|
}
|
|
32
|
-
const {
|
|
32
|
+
const { fillVars } = context;
|
|
33
33
|
const labelStyle = legend.labelStyles;
|
|
34
34
|
|
|
35
35
|
return (
|
|
@@ -57,7 +57,7 @@ export const HeatmapLegend: FC< HeatmapLegendProps > = ( {
|
|
|
57
57
|
className={ styles[ 'heatmap-chart__legend-swatch' ] }
|
|
58
58
|
style={
|
|
59
59
|
{
|
|
60
|
-
|
|
60
|
+
...fillVars,
|
|
61
61
|
'--a8c-charts-heatmap-cell-intensity': intensity,
|
|
62
62
|
} as CSSProperties
|
|
63
63
|
}
|