Skip to content

Commit c67b404

Browse files
authored
Merge pull request #1796 from Shopify/envex/add-tooltip-support-donut-chart
Add tooltip support for Donut Chart
2 parents 7a02eb8 + fce6349 commit c67b404

23 files changed

+335
-105
lines changed

Diff for: packages/polaris-viz-core/src/types.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export interface DataGroup {
3939
yAxisOptions?: YAxisOptions;
4040
}
4141

42-
export type Shape = 'Line' | 'Bar';
42+
export type Shape = 'Line' | 'Bar' | 'Donut';
4343

4444
export type LineStyle = 'solid' | 'dotted' | 'dashed';
4545

@@ -212,6 +212,7 @@ export enum DataType {
212212
Point = 'Point',
213213
BarGroup = 'BarGroup',
214214
Bar = 'Bar',
215+
Arc = 'Arc',
215216
}
216217

217218
export type ChartType = 'default' | 'stacked';
@@ -316,6 +317,7 @@ export enum InternalChartType {
316317
HorizontalBar = 'HorizontalBar',
317318
Combo = 'Combo',
318319
Line = 'Line',
320+
Donut = 'Donut',
319321
}
320322

321323
export enum Hue {

Diff for: packages/polaris-viz/CHANGELOG.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
66
and adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
77

8-
<!-- ## Unreleased -->
8+
## Unreleased
9+
10+
### Added
11+
12+
- Added tooltip support for `<DonutChart />`
913

1014
## [15.8.1] - 2025-01-21
1115

Diff for: packages/polaris-viz/src/components/Arc/Arc.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
getColorVisionStylesForActiveIndex,
1010
COLOR_VISION_SINGLE_ITEM,
1111
useSpringConfig,
12+
DataType,
1213
} from '@shopify/polaris-viz-core';
1314
import type {Color} from '@shopify/polaris-viz-core';
1415
import {useSpring, animated, to} from '@react-spring/web';
@@ -80,6 +81,7 @@ export function Arc({
8081
animatedPadAngle: ARC_PAD_ANGLE,
8182
from: {
8283
animatedOuterRadius: radius - thickness,
84+
animatedInnerRadius: radius - thickness,
8385
},
8486
...springConfig,
8587
});
@@ -133,6 +135,11 @@ export function Arc({
133135
index,
134136
})}
135137
clipPath={`url(#${gradientId})`}
138+
data-type={DataType.Arc}
139+
data-index={index}
140+
aria-hidden={false}
141+
data-start-angle={startAngle}
142+
data-end-angle={endAngle}
136143
>
137144
<ConicGradientWithStops
138145
x={width / -2 - ANIMATION_SIZE_BUFFER}

Diff for: packages/polaris-viz/src/components/DonutChart/Chart.tsx

+94-64
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type {ReactNode} from 'react';
12
import {Fragment, useState} from 'react';
23
import {pie} from 'd3-shape';
34
import {
@@ -9,16 +10,22 @@ import {
910
useChartContext,
1011
THIN_ARC_CORNER_THICKNESS,
1112
isInfinity,
13+
DataType,
14+
ChartMargin,
15+
InternalChartType,
1216
} from '@shopify/polaris-viz-core';
1317
import type {
1418
DataPoint,
1519
DataSeries,
1620
LabelFormatter,
1721
Direction,
22+
BoundingRect,
1823
} from '@shopify/polaris-viz-core';
1924

2025
import {getAnimationDelayForItems} from '../../utilities/getAnimationDelayForItems';
2126
import {getContainerAlignmentForLegend} from '../../utilities';
27+
import {useDonutChartTooltipContents} from '../../hooks/useDonutChartTooltipContents';
28+
import {TooltipWrapper} from '../../components/TooltipWrapper';
2229
import type {ComparisonMetricProps} from '../ComparisonMetric';
2330
import {LegendContainer, useLegend} from '../../components/LegendContainer';
2431
import {
@@ -33,6 +40,7 @@ import type {
3340
RenderHiddenLegendLabel,
3441
RenderInnerValueContent,
3542
RenderLegendContent,
43+
RenderTooltipContentData,
3644
} from '../../types';
3745
import {ChartSkeleton} from '../../components/ChartSkeleton';
3846

@@ -59,6 +67,7 @@ export interface ChartProps {
5967
renderInnerValueContent?: RenderInnerValueContent;
6068
renderLegendContent?: RenderLegendContent;
6169
renderHiddenLegendLabel?: RenderHiddenLegendLabel;
70+
renderTooltipContent?: (data: RenderTooltipContentData) => ReactNode;
6271
total?: number;
6372
}
6473

@@ -78,12 +87,14 @@ export function Chart({
7887
renderLegendContent,
7988
renderHiddenLegendLabel,
8089
seriesNameFormatter,
90+
renderTooltipContent,
8191
total,
8292
}: ChartProps) {
8393
const {shouldAnimate, containerBounds} = useChartContext();
8494
const chartId = useUniqueId('Donut');
8595
const [activeIndex, setActiveIndex] = useState<number>(-1);
8696
const selectedTheme = useTheme();
97+
const [svgRef, setSvgRef] = useState<SVGSVGElement | null>(null);
8798

8899
const seriesCount = clamp({
89100
amount: data.length,
@@ -92,6 +103,19 @@ export function Chart({
92103

93104
const seriesColor = getSeriesColors(seriesCount, selectedTheme);
94105

106+
const chartBounds: BoundingRect = {
107+
width: containerBounds.width,
108+
height: containerBounds.height,
109+
x: 0,
110+
y: 0,
111+
};
112+
113+
const getTooltipMarkup = useDonutChartTooltipContents({
114+
renderTooltipContent,
115+
data,
116+
seriesColors: seriesColor,
117+
});
118+
95119
const legendDirection: Direction =
96120
legendPosition === 'right' || legendPosition === 'left'
97121
? 'vertical'
@@ -100,19 +124,16 @@ export function Chart({
100124
const maxLegendWidth =
101125
legendDirection === 'vertical' ? containerBounds.width / 2 : 0;
102126

103-
const {height, width, legend, setLegendDimensions, isLegendMounted} =
104-
useLegend({
105-
data: [{series: data, shape: 'Bar'}],
106-
showLegend,
107-
direction: legendDirection,
108-
colors: seriesColor,
109-
maxWidth: maxLegendWidth,
110-
seriesNameFormatter,
111-
});
127+
const {height, width, legend, setLegendDimensions} = useLegend({
128+
data: [{series: data, shape: 'Bar'}],
129+
showLegend,
130+
direction: legendDirection,
131+
colors: seriesColor,
132+
maxWidth: maxLegendWidth,
133+
seriesNameFormatter,
134+
});
112135

113-
const shouldUseColorVisionEvents = Boolean(
114-
width && height && isLegendMounted,
115-
);
136+
const shouldUseColorVisionEvents = Boolean(width && height);
116137

117138
useColorVisionEvents({
118139
enabled: shouldUseColorVisionEvents,
@@ -208,60 +229,59 @@ export function Chart({
208229
viewBox={`${minX} ${minY} ${viewBoxDimensions.width} ${viewBoxDimensions.height}`}
209230
height={diameter}
210231
width={diameter}
232+
ref={setSvgRef}
211233
>
212-
{isLegendMounted && (
213-
<g className={styles.DonutChart}>
214-
{emptyState ? (
215-
<g aria-hidden>
216-
<Arc
217-
isAnimated={shouldAnimate}
218-
width={diameter}
219-
height={diameter}
220-
radius={radius}
221-
startAngle={0}
222-
endAngle={FULL_CIRCLE}
223-
color={selectedTheme.grid.color}
224-
cornerRadius={selectedTheme.arc.cornerRadius}
225-
thickness={thickness}
226-
/>
227-
</g>
228-
) : (
229-
pieChartData.map(
230-
({data: pieData, startAngle, endAngle}, index) => {
231-
const color = data[index]?.color ?? seriesColor[index];
232-
const name = data[index].name;
233-
const accessibilityLabel = `${name}: ${pieData.key} - ${pieData.value}`;
234+
<g className={styles.DonutChart}>
235+
{emptyState ? (
236+
<g aria-hidden>
237+
<Arc
238+
isAnimated={shouldAnimate}
239+
width={diameter}
240+
height={diameter}
241+
radius={radius}
242+
startAngle={0}
243+
endAngle={FULL_CIRCLE}
244+
color={selectedTheme.grid.color}
245+
cornerRadius={selectedTheme.arc.cornerRadius}
246+
thickness={thickness}
247+
/>
248+
</g>
249+
) : (
250+
pieChartData.map(
251+
({data: pieData, startAngle, endAngle}, index) => {
252+
const color = data[index]?.color ?? seriesColor[index];
253+
const name = data[index].name;
254+
const accessibilityLabel = `${name}: ${pieData.key} - ${pieData.value}`;
234255

235-
return (
236-
<g
237-
key={`${chartId}-arc-${index}`}
238-
className={styles.DonutChart}
239-
aria-label={accessibilityLabel}
240-
role="img"
241-
>
242-
<Arc
243-
isAnimated={shouldAnimate}
244-
animationDelay={getAnimationDelayForItems(
245-
pieChartData.length,
246-
)}
247-
index={index}
248-
activeIndex={activeIndex}
249-
width={diameter}
250-
height={diameter}
251-
radius={radius}
252-
startAngle={startAngle}
253-
endAngle={endAngle}
254-
color={color}
255-
cornerRadius={selectedTheme.arc.cornerRadius}
256-
thickness={thickness}
257-
/>
258-
</g>
259-
);
260-
},
261-
)
262-
)}
263-
</g>
264-
)}
256+
return (
257+
<g
258+
key={`${chartId}-arc-${index}`}
259+
className={styles.DonutChart}
260+
aria-label={accessibilityLabel}
261+
role="img"
262+
>
263+
<Arc
264+
isAnimated={shouldAnimate}
265+
animationDelay={getAnimationDelayForItems(
266+
pieChartData.length,
267+
)}
268+
index={index}
269+
activeIndex={activeIndex}
270+
width={diameter}
271+
height={diameter}
272+
radius={radius}
273+
startAngle={startAngle}
274+
endAngle={endAngle}
275+
color={color}
276+
cornerRadius={selectedTheme.arc.cornerRadius}
277+
thickness={thickness}
278+
/>
279+
</g>
280+
);
281+
},
282+
)
283+
)}
284+
</g>
265285
</svg>
266286
<InnerValue
267287
activeValue={activeValue}
@@ -300,6 +320,16 @@ export function Chart({
300320
}
301321
/>
302322
)}
323+
<TooltipWrapper
324+
chartBounds={chartBounds}
325+
chartType={InternalChartType.Donut}
326+
focusElementDataType={DataType.Arc}
327+
forceActiveIndex={activeIndex}
328+
getMarkup={getTooltipMarkup}
329+
margin={ChartMargin}
330+
parentElement={svgRef}
331+
usePortal
332+
/>
303333
</div>
304334
);
305335
}

Diff for: packages/polaris-viz/src/components/DonutChart/DonutChart.tsx

+12
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ import {
44
usePolarisVizContext,
55
} from '@shopify/polaris-viz-core';
66

7+
import {getTooltipContentRenderer} from '../../utilities/getTooltipContentRenderer';
78
import {ChartContainer} from '../ChartContainer';
89
import type {ComparisonMetricProps} from '../ComparisonMetric';
910
import type {
1011
LegendPosition,
1112
RenderHiddenLegendLabel,
1213
RenderInnerValueContent,
1314
RenderLegendContent,
15+
TooltipOptions,
1416
} from '../../types';
1517
import {bucketDataSeries} from '../../utilities/bucketDataSeries';
1618

@@ -26,6 +28,7 @@ export type DonutChartProps = {
2628
labelFormatter?: LabelFormatter;
2729
legendFullWidth?: boolean;
2830
legendPosition?: LegendPosition;
31+
tooltipOptions?: TooltipOptions;
2932
renderInnerValueContent?: RenderInnerValueContent;
3033
renderLegendContent?: RenderLegendContent;
3134
renderHiddenLegendLabel?: RenderHiddenLegendLabel;
@@ -51,6 +54,7 @@ export function DonutChart(props: DonutChartProps) {
5154
isAnimated,
5255
state,
5356
errorText,
57+
tooltipOptions,
5458
renderInnerValueContent,
5559
renderLegendContent,
5660
renderHiddenLegendLabel,
@@ -65,6 +69,13 @@ export function DonutChart(props: DonutChartProps) {
6569
? bucketDataSeries({dataSeries, maxSeries, renderBucketLegendLabel})
6670
: dataSeries;
6771

72+
const renderTooltip = getTooltipContentRenderer({
73+
tooltipOptions,
74+
theme,
75+
data,
76+
ignoreColorVisionEvents: true,
77+
});
78+
6879
return (
6980
<ChartContainer
7081
skeletonType="Donut"
@@ -88,6 +99,7 @@ export function DonutChart(props: DonutChartProps) {
8899
renderLegendContent={renderLegendContent}
89100
renderHiddenLegendLabel={renderHiddenLegendLabel}
90101
seriesNameFormatter={seriesNameFormatter}
102+
renderTooltipContent={renderTooltip}
91103
theme={theme}
92104
/>
93105
</ChartContainer>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import type {Story} from '@storybook/react';
2+
3+
export {META as default} from './meta';
4+
5+
import type {DonutChartProps} from '../DonutChart';
6+
7+
import {DEFAULT_PROPS, DEFAULT_DATA, Template} from './data';
8+
9+
export const Tooltip: Story<DonutChartProps> = Template.bind({});
10+
11+
Tooltip.args = {
12+
...DEFAULT_PROPS,
13+
data: DEFAULT_DATA,
14+
};

Diff for: packages/polaris-viz/src/components/DonutChart/stories/meta.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
CHART_STATE_CONTROL_ARGS,
55
CONTROLS_ARGS,
66
DATA_SERIES_ARGS,
7+
DONUT_CHART_TOOLTIP_OPTIONS_ARGS,
78
LEGEND_FULL_WIDTH_ARGS,
89
LEGEND_POSITION_ARGS,
910
MAX_SERIES_ARGS,
@@ -42,5 +43,6 @@ export const META: Meta<DonutChartProps> = {
4243
renderBucketLegendLabel: RENDER_BUCKET_LEGEND_LABEL_ARGS,
4344
theme: THEME_CONTROL_ARGS,
4445
state: CHART_STATE_CONTROL_ARGS,
46+
tooltipOptions: DONUT_CHART_TOOLTIP_OPTIONS_ARGS,
4547
},
4648
};

Diff for: packages/polaris-viz/src/components/FunnelChartNext/utilities/get-tooltip-position.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ export function getTooltipPosition({
2424
}: Props): TooltipPosition {
2525
const {event, index, eventType} = tooltipPosition;
2626

27-
if (eventType === 'mouse' && event) {
27+
if (
28+
eventType === 'mouse' &&
29+
(event instanceof MouseEvent || event instanceof TouchEvent)
30+
) {
2831
const point = eventPointNative(event);
2932

3033
if (point == null) {

0 commit comments

Comments
 (0)