Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add tooltip support for Donut Chart #1587

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/polaris-viz-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export interface DataGroup {
yAxisOptions?: YAxisOptions;
}

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

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

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 />`

## [9.11.0] - 2023-09-12

Expand Down
80 changes: 76 additions & 4 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 @@ -7,17 +8,32 @@ import {
useUniqueId,
ChartState,
useChartContext,
DataType,
ChartMargin,
} from '@shopify/polaris-viz-core';
import type {
DataPoint,
DataSeries,
Dimensions,
LabelFormatter,
Direction,
BoundingRect,
} from '@shopify/polaris-viz-core';

import {useDonutChartTooltipContents} from '../../hooks/useDonutChartTooltipContents';
import type {
TooltipPosition,
TooltipPositionParams,
} from '../../components/TooltipWrapper';
import {
TooltipWrapper,
TOOLTIP_POSITION_DEFAULT_RETURN,
} from '../../components/TooltipWrapper';
import {DONUT_CHART_MAX_SERIES_COUNT} from '../../constants';
import {getContainerAlignmentForLegend} from '../../utilities';
import {
eventPointNative,
getContainerAlignmentForLegend,
} from '../../utilities';
import {estimateLegendItemWidth} from '../Legend';
import type {ComparisonMetricProps} from '../ComparisonMetric';
import {LegendContainer, useLegend} from '../../components/LegendContainer';
Expand All @@ -31,6 +47,7 @@ import type {
LegendPosition,
RenderInnerValueContent,
RenderLegendContent,
RenderTooltipContentData,
} from '../../types';
import {ChartSkeleton} from '../../components/ChartSkeleton';

Expand All @@ -56,6 +73,7 @@ export interface ChartProps {
legendFullWidth?: boolean;
renderInnerValueContent?: RenderInnerValueContent;
renderLegendContent?: RenderLegendContent;
renderTooltipContent?: (data: RenderTooltipContentData) => ReactNode;
total?: number;
}

Expand All @@ -73,19 +91,64 @@ export function Chart({
legendFullWidth = false,
renderInnerValueContent,
renderLegendContent,
renderTooltipContent,
total,
}: ChartProps) {
const {shouldAnimate, characterWidths} = useChartContext();
const chartId = useUniqueId('Donut');
const [activeIndex, setActiveIndex] = useState<number>(-1);
const selectedTheme = useTheme();
const svgRef = useRef<SVGSVGElement>(null);

const seriesCount = clamp({
amount: data.length,
min: 1,
max: DONUT_CHART_MAX_SERIES_COUNT,
});

const seriesColor = getSeriesColors(seriesCount, selectedTheme);

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

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

function getTooltipPosition({
event,
index,
eventType,
}: TooltipPositionParams): TooltipPosition {
if (eventType === 'mouse') {
const point = eventPointNative(event!);

if (point == null) {
return TOOLTIP_POSITION_DEFAULT_RETURN;
}

return {
x: (event as MouseEvent).pageX,
y: (event as MouseEvent).pageY,
activeIndex,
};
} else {
const activeIndex = index ?? 0;

return {
x: dimensions?.width ?? 0,
y: dimensions?.height ?? 0,
activeIndex,
};
}
}

const seriesData = data
.filter(({data}) => Number(data[0]?.value) > 0)
.sort(
Expand All @@ -94,8 +157,6 @@ export function Chart({
)
.slice(0, seriesCount);

const seriesColor = getSeriesColors(seriesCount, selectedTheme);

const legendDirection: Direction =
legendPosition === 'right' || legendPosition === 'left'
? 'vertical'
Expand Down Expand Up @@ -186,6 +247,7 @@ export function Chart({
viewBox={`${minX} ${minY} ${viewBoxDimensions.width} ${viewBoxDimensions.height}`}
height={diameter}
width={diameter}
ref={svgRef}
>
{isLegendMounted && (
<g className={styles.DonutChart}>
Expand Down Expand Up @@ -270,6 +332,16 @@ export function Chart({
renderLegendContent={renderLegendContent}
/>
)}
<TooltipWrapper
alwaysUpdatePosition
chartBounds={chartBounds}
focusElementDataType={DataType.Point}
getMarkup={getTooltipMarkup}
getPosition={getTooltipPosition}
margin={ChartMargin}
parentRef={svgRef.current}
usePortal
/>
</div>
);
}
7 changes: 7 additions & 0 deletions packages/polaris-viz/src/components/DonutChart/DonutChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import {
usePolarisVizContext,
} from '@shopify/polaris-viz-core';

import {useRenderTooltipContent} from '../../hooks';
import {ChartContainer} from '../ChartContainer';
import type {ComparisonMetricProps} from '../ComparisonMetric';
import type {
LegendPosition,
RenderInnerValueContent,
RenderLegendContent,
TooltipOptions,
} from '../../types';

import {Chart} from './Chart';
Expand All @@ -20,6 +22,7 @@ export type DonutChartProps = {
labelFormatter?: LabelFormatter;
legendFullWidth?: boolean;
legendPosition?: LegendPosition;
tooltipOptions?: TooltipOptions;
renderInnerValueContent?: RenderInnerValueContent;
renderLegendContent?: RenderLegendContent;
} & ChartProps;
Expand All @@ -39,13 +42,16 @@ export function DonutChart(props: DonutChartProps) {
isAnimated,
state,
errorText,
tooltipOptions,
renderInnerValueContent,
renderLegendContent,
} = {
...DEFAULT_CHART_PROPS,
...props,
};

const renderTooltip = useRenderTooltipContent({tooltipOptions, theme, data});

return (
<ChartContainer
skeletonType="Donut"
Expand All @@ -65,6 +71,7 @@ export function DonutChart(props: DonutChartProps) {
legendPosition={legendPosition}
renderInnerValueContent={renderInnerValueContent}
renderLegendContent={renderLegendContent}
renderTooltipContent={tooltipOptions ? renderTooltip : undefined}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, tooltipOptions is what enables the tooltip and defines its options in the donut chart. I believe we want the tooltip to be an opt-in experience cc @rachelng. But perhaps another prop like showTooltip would make more sense? Interested to see what others think

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the showTooltip prop. If we decide to go that route, we should add it to all the other components that can render tooltips.

theme={theme}
/>
</ChartContainer>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
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,
tooltipOptions: {
titleFormatter: (value) => value?.toString() || '',
valueFormatter: (value) => value?.toString() || '',
},
};
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,
RENDER_LEGEND_CONTENT_ARGS,
Expand Down Expand Up @@ -32,5 +33,6 @@ export const META: Meta<DonutChartProps> = {
renderLegendContent: RENDER_LEGEND_CONTENT_ARGS,
theme: THEME_CONTROL_ARGS,
state: CHART_STATE_CONTROL_ARGS,
tooltipOptions: DONUT_CHART_TOOLTIP_OPTIONS_ARGS,
},
};
49 changes: 49 additions & 0 deletions packages/polaris-viz/src/hooks/useDonutChartTooltipContents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type {ReactNode} from 'react';
import {useCallback} from 'react';
import type {Color, DataSeries} from '@shopify/polaris-viz-core';
import {useChartContext} from '@shopify/polaris-viz-core';

import type {RenderTooltipContentData} from '../types';

export interface Props {
data: DataSeries[];
seriesColors: Color[];
renderTooltipContent?: (data: RenderTooltipContentData) => ReactNode;
}

export function useDonutChartTooltipContents({
data,
renderTooltipContent,
seriesColors,
}: Props) {
const {theme} = useChartContext();

return useCallback(
(activeIndex: number) => {
if (activeIndex === -1 || !renderTooltipContent) {
return null;
}

const tooltipData: RenderTooltipContentData['data'] = [
{
shape: 'Donut',
data: [],
},
];

tooltipData[0].data.push({
key: `${data[activeIndex].name}`,
value: data[activeIndex].data[0].value,
color: data[activeIndex].color ?? seriesColors[activeIndex],
});

return renderTooltipContent({
data: tooltipData,
activeIndex,
dataSeries: data,
theme,
});
},
[data, seriesColors, theme, renderTooltipContent],
);
}
8 changes: 8 additions & 0 deletions packages/polaris-viz/src/storybook/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,11 @@ export const EMPTY_STATE_TEXT_ARGS = {
description:
'Used to indicate to screen readers that a chart with no series data has been rendered, in the case that an empty array is passed as the data. If the series prop could be an empty array, it is strongly recommended to include this prop.',
};

export const DONUT_CHART_TOOLTIP_OPTIONS_ARGS = {
description:
'An object that when passed in, enables the tooltip and defines its options in the donut chart.',
control: {
type: 'object',
},
};
Loading