From a9fbbdcaefb8d4b22814ae5bb6834d597f26a3bd Mon Sep 17 00:00:00 2001 From: Michael Nesen Date: Tue, 19 Sep 2023 14:02:57 +0000 Subject: [PATCH] Add tooltip support for Donut Chart --- packages/polaris-viz-core/src/types.ts | 2 +- packages/polaris-viz/CHANGELOG.md | 6 +- .../src/components/DonutChart/Chart.tsx | 80 ++++++++++++++++++- .../src/components/DonutChart/DonutChart.tsx | 7 ++ .../DonutChart/stories/Tooltip.stories.tsx | 18 +++++ .../components/DonutChart/stories/meta.tsx | 2 + .../src/hooks/useDonutChartTooltipContents.ts | 49 ++++++++++++ .../polaris-viz/src/storybook/constants.ts | 8 ++ 8 files changed, 166 insertions(+), 6 deletions(-) create mode 100644 packages/polaris-viz/src/components/DonutChart/stories/Tooltip.stories.tsx create mode 100644 packages/polaris-viz/src/hooks/useDonutChartTooltipContents.ts diff --git a/packages/polaris-viz-core/src/types.ts b/packages/polaris-viz-core/src/types.ts index c24bfaa40..eae546340 100644 --- a/packages/polaris-viz-core/src/types.ts +++ b/packages/polaris-viz-core/src/types.ts @@ -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'; diff --git a/packages/polaris-viz/CHANGELOG.md b/packages/polaris-viz/CHANGELOG.md index 50fb53f00..109df8279 100644 --- a/packages/polaris-viz/CHANGELOG.md +++ b/packages/polaris-viz/CHANGELOG.md @@ -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 + +### Added + +- Added tooltip support for `` ## [9.11.0] - 2023-09-12 diff --git a/packages/polaris-viz/src/components/DonutChart/Chart.tsx b/packages/polaris-viz/src/components/DonutChart/Chart.tsx index 8427bc265..b9c90b532 100644 --- a/packages/polaris-viz/src/components/DonutChart/Chart.tsx +++ b/packages/polaris-viz/src/components/DonutChart/Chart.tsx @@ -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, @@ -7,6 +8,8 @@ import { useUniqueId, ChartState, useChartContext, + DataType, + ChartMargin, } from '@shopify/polaris-viz-core'; import type { DataPoint, @@ -14,10 +17,23 @@ import type { 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'; @@ -31,6 +47,7 @@ import type { LegendPosition, RenderInnerValueContent, RenderLegendContent, + RenderTooltipContentData, } from '../../types'; import {ChartSkeleton} from '../../components/ChartSkeleton'; @@ -56,6 +73,7 @@ export interface ChartProps { legendFullWidth?: boolean; renderInnerValueContent?: RenderInnerValueContent; renderLegendContent?: RenderLegendContent; + renderTooltipContent?: (data: RenderTooltipContentData) => ReactNode; total?: number; } @@ -73,12 +91,14 @@ export function Chart({ legendFullWidth = false, renderInnerValueContent, renderLegendContent, + renderTooltipContent, total, }: ChartProps) { const {shouldAnimate, characterWidths} = useChartContext(); const chartId = useUniqueId('Donut'); const [activeIndex, setActiveIndex] = useState(-1); const selectedTheme = useTheme(); + const svgRef = useRef(null); const seriesCount = clamp({ amount: data.length, @@ -86,6 +106,49 @@ export function Chart({ 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( @@ -94,8 +157,6 @@ export function Chart({ ) .slice(0, seriesCount); - const seriesColor = getSeriesColors(seriesCount, selectedTheme); - const legendDirection: Direction = legendPosition === 'right' || legendPosition === 'left' ? 'vertical' @@ -186,6 +247,7 @@ export function Chart({ viewBox={`${minX} ${minY} ${viewBoxDimensions.width} ${viewBoxDimensions.height}`} height={diameter} width={diameter} + ref={svgRef} > {isLegendMounted && ( @@ -270,6 +332,16 @@ export function Chart({ renderLegendContent={renderLegendContent} /> )} + ); } diff --git a/packages/polaris-viz/src/components/DonutChart/DonutChart.tsx b/packages/polaris-viz/src/components/DonutChart/DonutChart.tsx index 1ffd7124e..c0307f77c 100644 --- a/packages/polaris-viz/src/components/DonutChart/DonutChart.tsx +++ b/packages/polaris-viz/src/components/DonutChart/DonutChart.tsx @@ -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'; @@ -20,6 +22,7 @@ export type DonutChartProps = { labelFormatter?: LabelFormatter; legendFullWidth?: boolean; legendPosition?: LegendPosition; + tooltipOptions?: TooltipOptions; renderInnerValueContent?: RenderInnerValueContent; renderLegendContent?: RenderLegendContent; } & ChartProps; @@ -39,6 +42,7 @@ export function DonutChart(props: DonutChartProps) { isAnimated, state, errorText, + tooltipOptions, renderInnerValueContent, renderLegendContent, } = { @@ -46,6 +50,8 @@ export function DonutChart(props: DonutChartProps) { ...props, }; + const renderTooltip = useRenderTooltipContent({tooltipOptions, theme, data}); + return ( diff --git a/packages/polaris-viz/src/components/DonutChart/stories/Tooltip.stories.tsx b/packages/polaris-viz/src/components/DonutChart/stories/Tooltip.stories.tsx new file mode 100644 index 000000000..97557739b --- /dev/null +++ b/packages/polaris-viz/src/components/DonutChart/stories/Tooltip.stories.tsx @@ -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 = Template.bind({}); + +Tooltip.args = { + ...DEFAULT_PROPS, + data: DEFAULT_DATA, + tooltipOptions: { + titleFormatter: (value) => value?.toString() || '', + valueFormatter: (value) => value?.toString() || '', + }, +}; diff --git a/packages/polaris-viz/src/components/DonutChart/stories/meta.tsx b/packages/polaris-viz/src/components/DonutChart/stories/meta.tsx index 5336ad1a8..f3001ff16 100644 --- a/packages/polaris-viz/src/components/DonutChart/stories/meta.tsx +++ b/packages/polaris-viz/src/components/DonutChart/stories/meta.tsx @@ -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, @@ -32,5 +33,6 @@ export const META: Meta = { renderLegendContent: RENDER_LEGEND_CONTENT_ARGS, theme: THEME_CONTROL_ARGS, state: CHART_STATE_CONTROL_ARGS, + tooltipOptions: DONUT_CHART_TOOLTIP_OPTIONS_ARGS, }, }; diff --git a/packages/polaris-viz/src/hooks/useDonutChartTooltipContents.ts b/packages/polaris-viz/src/hooks/useDonutChartTooltipContents.ts new file mode 100644 index 000000000..2e47711f6 --- /dev/null +++ b/packages/polaris-viz/src/hooks/useDonutChartTooltipContents.ts @@ -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], + ); +} diff --git a/packages/polaris-viz/src/storybook/constants.ts b/packages/polaris-viz/src/storybook/constants.ts index ac4d47d21..cda8b0825 100644 --- a/packages/polaris-viz/src/storybook/constants.ts +++ b/packages/polaris-viz/src/storybook/constants.ts @@ -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', + }, +};