Skip to content

Commit

Permalink
Add tooltip support for Donut Chart
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelnesen authored and envex committed Jan 31, 2025
1 parent a648037 commit 8808f40
Show file tree
Hide file tree
Showing 22 changed files with 332 additions and 101 deletions.
4 changes: 3 additions & 1 deletion packages/polaris-viz-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export interface DataGroup {
yAxisOptions?: YAxisOptions;
}

export type Shape = 'Line' | 'Bar';
export type Shape = 'Line' | 'Bar' | 'Donut';

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

Expand Down Expand Up @@ -212,6 +212,7 @@ export enum DataType {
Point = 'Point',
BarGroup = 'BarGroup',
Bar = 'Bar',
Arc = 'Arc',
}

export type ChartType = 'default' | 'stacked';
Expand Down Expand Up @@ -316,6 +317,7 @@ export enum InternalChartType {
HorizontalBar = 'HorizontalBar',
Combo = 'Combo',
Line = 'Line',
Donut = 'Donut',
}

export enum Hue {
Expand Down
6 changes: 5 additions & 1 deletion packages/polaris-viz/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

<!-- ## Unreleased -->
## Unreleased

### Added

- Added tooltip support for `<DonutChart />`

## [15.8.1] - 2025-01-21

Expand Down
7 changes: 7 additions & 0 deletions packages/polaris-viz/src/components/Arc/Arc.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
getColorVisionStylesForActiveIndex,
COLOR_VISION_SINGLE_ITEM,
useSpringConfig,
DataType,
} from '@shopify/polaris-viz-core';
import type {Color} from '@shopify/polaris-viz-core';
import {useSpring, animated, to} from '@react-spring/web';
Expand Down Expand Up @@ -80,6 +81,7 @@ export function Arc({
animatedPadAngle: ARC_PAD_ANGLE,
from: {
animatedOuterRadius: radius - thickness,
animatedInnerRadius: radius - thickness,
},
...springConfig,
});
Expand Down Expand Up @@ -133,6 +135,11 @@ export function Arc({
index,
})}
clipPath={`url(#${gradientId})`}
data-type={DataType.Arc}
data-index={index}
aria-hidden={false}
data-start-angle={startAngle}
data-end-angle={endAngle}
>
<ConicGradientWithStops
x={width / -2 - ANIMATION_SIZE_BUFFER}
Expand Down
160 changes: 95 additions & 65 deletions packages/polaris-viz/src/components/DonutChart/Chart.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {Fragment, useState} from 'react';
import type {ReactNode} from 'react';
import {Fragment, useRef, useState} from 'react';
import {pie} from 'd3-shape';
import {
clamp,
Expand All @@ -9,16 +10,22 @@ import {
useChartContext,
THIN_ARC_CORNER_THICKNESS,
isInfinity,
DataType,
ChartMargin,
InternalChartType,
} from '@shopify/polaris-viz-core';
import type {
DataPoint,
DataSeries,
LabelFormatter,
Direction,
BoundingRect,
} from '@shopify/polaris-viz-core';

import {getAnimationDelayForItems} from '../../utilities/getAnimationDelayForItems';
import {getContainerAlignmentForLegend} from '../../utilities';
import {useDonutChartTooltipContents} from '../../hooks/useDonutChartTooltipContents';
import {TooltipWrapper} from '../../components/TooltipWrapper';
import type {ComparisonMetricProps} from '../ComparisonMetric';
import {LegendContainer, useLegend} from '../../components/LegendContainer';
import {
Expand All @@ -33,6 +40,7 @@ import type {
RenderHiddenLegendLabel,
RenderInnerValueContent,
RenderLegendContent,
RenderTooltipContentData,
} from '../../types';
import {ChartSkeleton} from '../../components/ChartSkeleton';

Expand All @@ -59,6 +67,7 @@ export interface ChartProps {
renderInnerValueContent?: RenderInnerValueContent;
renderLegendContent?: RenderLegendContent;
renderHiddenLegendLabel?: RenderHiddenLegendLabel;
renderTooltipContent?: (data: RenderTooltipContentData) => ReactNode;
total?: number;
}

Expand All @@ -78,12 +87,14 @@ export function Chart({
renderLegendContent,
renderHiddenLegendLabel,
seriesNameFormatter,
renderTooltipContent,
total,
}: ChartProps) {
const {shouldAnimate, containerBounds} = useChartContext();
const chartId = useUniqueId('Donut');
const [activeIndex, setActiveIndex] = useState<number>(-1);
const selectedTheme = useTheme();
const [svgRef, setSvgRef] = useState<SVGSVGElement | null>(null);

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

const seriesColor = getSeriesColors(seriesCount, selectedTheme);

const chartBounds: BoundingRect = {
width: containerBounds.width,
height: containerBounds.height,
x: 0,
y: 0,
};

const getTooltipMarkup = useDonutChartTooltipContents({
renderTooltipContent,
data,
seriesColors: seriesColor,
});

const legendDirection: Direction =
legendPosition === 'right' || legendPosition === 'left'
? 'vertical'
Expand All @@ -100,19 +124,16 @@ export function Chart({
const maxLegendWidth =
legendDirection === 'vertical' ? containerBounds.width / 2 : 0;

const {height, width, legend, setLegendDimensions, isLegendMounted} =
useLegend({
data: [{series: data, shape: 'Bar'}],
showLegend,
direction: legendDirection,
colors: seriesColor,
maxWidth: maxLegendWidth,
seriesNameFormatter,
});
const {height, width, legend, setLegendDimensions} = useLegend({
data: [{series: data, shape: 'Bar'}],
showLegend,
direction: legendDirection,
colors: seriesColor,
maxWidth: maxLegendWidth,
seriesNameFormatter,
});

const shouldUseColorVisionEvents = Boolean(
width && height && isLegendMounted,
);
const shouldUseColorVisionEvents = Boolean(width && height);

useColorVisionEvents({
enabled: shouldUseColorVisionEvents,
Expand Down Expand Up @@ -208,60 +229,59 @@ export function Chart({
viewBox={`${minX} ${minY} ${viewBoxDimensions.width} ${viewBoxDimensions.height}`}
height={diameter}
width={diameter}
ref={setSvgRef}
>
{isLegendMounted && (
<g className={styles.DonutChart}>
{emptyState ? (
<g aria-hidden>
<Arc
isAnimated={shouldAnimate}
width={diameter}
height={diameter}
radius={radius}
startAngle={0}
endAngle={FULL_CIRCLE}
color={selectedTheme.grid.color}
cornerRadius={selectedTheme.arc.cornerRadius}
thickness={thickness}
/>
</g>
) : (
pieChartData.map(
({data: pieData, startAngle, endAngle}, index) => {
const color = data[index]?.color ?? seriesColor[index];
const name = data[index].name;
const accessibilityLabel = `${name}: ${pieData.key} - ${pieData.value}`;
<g className={styles.DonutChart}>
{emptyState ? (
<g aria-hidden>
<Arc
isAnimated={shouldAnimate}
width={diameter}
height={diameter}
radius={radius}
startAngle={0}
endAngle={FULL_CIRCLE}
color={selectedTheme.grid.color}
cornerRadius={selectedTheme.arc.cornerRadius}
thickness={thickness}
/>
</g>
) : (
pieChartData.map(
({data: pieData, startAngle, endAngle}, index) => {
const color = data[index]?.color ?? seriesColor[index];
const name = data[index].name;
const accessibilityLabel = `${name}: ${pieData.key} - ${pieData.value}`;

return (
<g
key={`${chartId}-arc-${index}`}
className={styles.DonutChart}
aria-label={accessibilityLabel}
role="img"
>
<Arc
isAnimated={shouldAnimate}
animationDelay={getAnimationDelayForItems(
pieChartData.length,
)}
index={index}
activeIndex={activeIndex}
width={diameter}
height={diameter}
radius={radius}
startAngle={startAngle}
endAngle={endAngle}
color={color}
cornerRadius={selectedTheme.arc.cornerRadius}
thickness={thickness}
/>
</g>
);
},
)
)}
</g>
)}
return (
<g
key={`${chartId}-arc-${index}`}
className={styles.DonutChart}
aria-label={accessibilityLabel}
role="img"
>
<Arc
isAnimated={shouldAnimate}
animationDelay={getAnimationDelayForItems(
pieChartData.length,
)}
index={index}
activeIndex={activeIndex}
width={diameter}
height={diameter}
radius={radius}
startAngle={startAngle}
endAngle={endAngle}
color={color}
cornerRadius={selectedTheme.arc.cornerRadius}
thickness={thickness}
/>
</g>
);
},
)
)}
</g>
</svg>
<InnerValue
activeValue={activeValue}
Expand Down Expand Up @@ -300,6 +320,16 @@ export function Chart({
}
/>
)}
<TooltipWrapper
chartBounds={chartBounds}
chartType={InternalChartType.Donut}
focusElementDataType={DataType.Arc}
forceActiveIndex={activeIndex}
getMarkup={getTooltipMarkup}
margin={ChartMargin}
parentElement={svgRef}
usePortal
/>
</div>
);
}
12 changes: 12 additions & 0 deletions packages/polaris-viz/src/components/DonutChart/DonutChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import {
usePolarisVizContext,
} from '@shopify/polaris-viz-core';

import {getTooltipContentRenderer} from '../../utilities/getTooltipContentRenderer';
import {ChartContainer} from '../ChartContainer';
import type {ComparisonMetricProps} from '../ComparisonMetric';
import type {
LegendPosition,
RenderHiddenLegendLabel,
RenderInnerValueContent,
RenderLegendContent,
TooltipOptions,
} from '../../types';
import {bucketDataSeries} from '../../utilities/bucketDataSeries';

Expand All @@ -26,6 +28,7 @@ export type DonutChartProps = {
labelFormatter?: LabelFormatter;
legendFullWidth?: boolean;
legendPosition?: LegendPosition;
tooltipOptions?: TooltipOptions;
renderInnerValueContent?: RenderInnerValueContent;
renderLegendContent?: RenderLegendContent;
renderHiddenLegendLabel?: RenderHiddenLegendLabel;
Expand All @@ -51,6 +54,7 @@ export function DonutChart(props: DonutChartProps) {
isAnimated,
state,
errorText,
tooltipOptions,
renderInnerValueContent,
renderLegendContent,
renderHiddenLegendLabel,
Expand All @@ -65,6 +69,13 @@ export function DonutChart(props: DonutChartProps) {
? bucketDataSeries({dataSeries, maxSeries, renderBucketLegendLabel})
: dataSeries;

const renderTooltip = getTooltipContentRenderer({
tooltipOptions,
theme,
data,
ignoreColorVisionEvents: true,
});

return (
<ChartContainer
skeletonType="Donut"
Expand All @@ -88,6 +99,7 @@ export function DonutChart(props: DonutChartProps) {
renderLegendContent={renderLegendContent}
renderHiddenLegendLabel={renderHiddenLegendLabel}
seriesNameFormatter={seriesNameFormatter}
renderTooltipContent={renderTooltip}
theme={theme}
/>
</ChartContainer>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type {Story} from '@storybook/react';

export {META as default} from './meta';

import type {DonutChartProps} from '../DonutChart';

import {DEFAULT_PROPS, DEFAULT_DATA, Template} from './data';

export const Tooltip: Story<DonutChartProps> = Template.bind({});

Tooltip.args = {
...DEFAULT_PROPS,
data: DEFAULT_DATA,
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
CHART_STATE_CONTROL_ARGS,
CONTROLS_ARGS,
DATA_SERIES_ARGS,
DONUT_CHART_TOOLTIP_OPTIONS_ARGS,
LEGEND_FULL_WIDTH_ARGS,
LEGEND_POSITION_ARGS,
MAX_SERIES_ARGS,
Expand Down Expand Up @@ -42,5 +43,6 @@ export const META: Meta<DonutChartProps> = {
renderBucketLegendLabel: RENDER_BUCKET_LEGEND_LABEL_ARGS,
theme: THEME_CONTROL_ARGS,
state: CHART_STATE_CONTROL_ARGS,
tooltipOptions: DONUT_CHART_TOOLTIP_OPTIONS_ARGS,
},
};
Loading

0 comments on commit 8808f40

Please sign in to comment.