Skip to content
This repository was archived by the owner on Jun 6, 2025. It is now read-only.

Commit 0bfb45e

Browse files
committed
Add tooltips to Funnel Chart Next
1 parent 1f18106 commit 0bfb45e

File tree

12 files changed

+403
-6
lines changed

12 files changed

+403
-6
lines changed

packages/polaris-viz/CHANGELOG.md

Lines changed: 5 additions & 1 deletion
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 Tooltips to `<FunnelChartNext />`
913

1014
## [15.7.0] - 2025-01-08
1115

packages/polaris-viz/src/components/FunnelChartNext/Chart.tsx

Lines changed: 80 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type {ReactNode} from 'react';
2-
import {Fragment, useMemo} from 'react';
2+
import {Fragment, useMemo, useState} from 'react';
33
import {scaleBand, scaleLinear} from 'd3-scale';
44
import type {DataSeries, LabelFormatter} from '@shopify/polaris-viz-core';
55
import {
@@ -16,18 +16,28 @@ import {
1616
import {FunnelChartSegment} from '../shared';
1717
import {ChartElements} from '../ChartElements';
1818

19-
import {FunnelChartLabels} from './components';
19+
import {
20+
FunnelChartLabels,
21+
FunnelTooltip,
22+
Tooltip,
23+
TooltipWithPortal,
24+
} from './components';
2025
import {
2126
LABELS_HEIGHT,
2227
LINE_GRADIENT,
2328
LINE_OFFSET,
2429
LINE_WIDTH,
2530
GAP,
2631
SEGMENT_WIDTH_RATIO,
32+
TOOLTIP_HORIZONTAL_OFFSET,
33+
TOOLTIP_HEIGHT,
34+
TOOLTIP_WIDTH,
2735
} from './constants';
36+
import type {FunnelChartNextProps} from './FunnelChartNext';
2837

2938
export interface ChartProps {
3039
data: DataSeries[];
40+
tooltipLabels: FunnelChartNextProps['tooltipLabels'];
3141
seriesNameFormatter: LabelFormatter;
3242
labelFormatter: LabelFormatter;
3343
percentageFormatter?: (value: number) => string;
@@ -36,20 +46,27 @@ export interface ChartProps {
3646

3747
export function Chart({
3848
data,
49+
tooltipLabels,
3950
seriesNameFormatter,
4051
labelFormatter,
4152
percentageFormatter = (value: number) => {
4253
return labelFormatter(value);
4354
},
4455
renderScaleIconTooltipContent,
4556
}: ChartProps) {
57+
const [tooltipIndex, setTooltipIndex] = useState<number | null>(null);
4658
const {containerBounds} = useChartContext();
4759
const dataSeries = data[0].data;
4860
const xValues = dataSeries.map(({key}) => key) as string[];
4961
const yValues = dataSeries.map(({value}) => value) as [number, number];
5062
const sanitizedYValues = yValues.map((value) => Math.max(0, value));
5163

52-
const {width: drawableWidth, height: drawableHeight} = containerBounds ?? {
64+
const {
65+
width: drawableWidth,
66+
height: drawableHeight,
67+
x: chartX,
68+
y: chartY,
69+
} = containerBounds ?? {
5370
width: 0,
5471
height: 0,
5572
x: 0,
@@ -107,9 +124,18 @@ export function Chart({
107124
return labelFormatter(dataPoint.value);
108125
});
109126

127+
const handleChartBlur = (event: React.FocusEvent) => {
128+
const currentTarget = event.currentTarget;
129+
const relatedTarget = event.relatedTarget as Node;
130+
131+
if (!currentTarget.contains(relatedTarget)) {
132+
setTooltipIndex(null);
133+
}
134+
};
135+
110136
return (
111137
<ChartElements.Svg height={drawableHeight} width={drawableWidth}>
112-
<g>
138+
<g onBlur={handleChartBlur}>
113139
<FunnelChartConnectorGradient />
114140

115141
<LinearGradientWithStops
@@ -152,6 +178,8 @@ export function Chart({
152178
barWidth={barWidth}
153179
index={index}
154180
isLast={isLast}
181+
onMouseEnter={(index) => setTooltipIndex(index)}
182+
onMouseLeave={() => setTooltipIndex(null)}
155183
shouldApplyScaling={shouldApplyScaling}
156184
x={x}
157185
>
@@ -182,7 +210,55 @@ export function Chart({
182210
</Fragment>
183211
);
184212
})}
213+
<TooltipWithPortal>{getTooltipMarkup()}</TooltipWithPortal>
185214
</g>
186215
</ChartElements.Svg>
187216
);
217+
218+
function getTooltipMarkup() {
219+
if (tooltipIndex == null) {
220+
return null;
221+
}
222+
223+
const activeDataSeries = dataSeries[tooltipIndex];
224+
225+
if (activeDataSeries == null) {
226+
return null;
227+
}
228+
229+
const xPosition = getXPosition();
230+
const yPosition = getYPosition();
231+
232+
return (
233+
<FunnelTooltip x={xPosition} y={yPosition}>
234+
<Tooltip
235+
activeIndex={tooltipIndex}
236+
dataSeries={dataSeries}
237+
tooltipLabels={tooltipLabels}
238+
labelFormatter={labelFormatter}
239+
percentageFormatter={percentageFormatter}
240+
/>
241+
</FunnelTooltip>
242+
);
243+
244+
function getXPosition() {
245+
if (tooltipIndex === 0) {
246+
return chartX + barWidth + TOOLTIP_HORIZONTAL_OFFSET;
247+
}
248+
249+
const xOffset = (barWidth - TOOLTIP_WIDTH) / 2;
250+
return chartX + (xScale(activeDataSeries.key.toString()) ?? 0) + xOffset;
251+
}
252+
253+
function getYPosition() {
254+
const barHeight = getBarHeight(activeDataSeries.value ?? 0);
255+
const yPosition = chartY + drawableHeight - barHeight;
256+
257+
if (tooltipIndex === 0) {
258+
return yPosition;
259+
}
260+
261+
return yPosition - TOOLTIP_HEIGHT;
262+
}
263+
}
188264
}

packages/polaris-viz/src/components/FunnelChartNext/FunnelChartNext.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ import {ChartSkeleton} from '../';
1212
import {Chart} from './Chart';
1313

1414
export type FunnelChartNextProps = {
15+
tooltipLabels: {
16+
reached: string;
17+
dropped: string;
18+
};
1519
seriesNameFormatter?: LabelFormatter;
1620
labelFormatter?: LabelFormatter;
1721
renderScaleIconTooltipContent?: () => ReactNode;
@@ -30,7 +34,7 @@ export function FunnelChartNext(props: FunnelChartNextProps) {
3034
isAnimated,
3135
state,
3236
errorText,
33-
37+
tooltipLabels,
3438
seriesNameFormatter = DEFAULT_LABEL_FORMATTER,
3539
labelFormatter = DEFAULT_LABEL_FORMATTER,
3640
percentageFormatter,
@@ -59,6 +63,7 @@ export function FunnelChartNext(props: FunnelChartNextProps) {
5963
) : (
6064
<Chart
6165
data={data}
66+
tooltipLabels={tooltipLabels}
6267
seriesNameFormatter={seriesNameFormatter}
6368
labelFormatter={labelFormatter}
6469
percentageFormatter={percentageFormatter}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
.Rows {
2+
display: grid;
3+
grid-template-columns: 1fr 1fr;
4+
gap: 8px;
5+
}
6+
7+
.Row {
8+
font-size: 12px;
9+
display: grid;
10+
grid-column: 1 / -1;
11+
grid-template-columns: subgrid;
12+
color: rgba(97, 97, 97, 1);
13+
align-items: center;
14+
}
15+
16+
.Keys {
17+
display: flex;
18+
align-items: center;
19+
gap: 4px;
20+
}
21+
22+
.Values {
23+
display: flex;
24+
align-items: center;
25+
justify-content: flex-end;
26+
gap: 4px;
27+
font-weight: 600;
28+
29+
strong {
30+
font-weight: 600;
31+
color: rgba(31, 33, 36, 1);
32+
}
33+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import {Fragment} from 'react';
2+
import type {Color, DataPoint, LabelFormatter} from '@shopify/polaris-viz-core';
3+
import {DEFAULT_THEME_NAME} from '@shopify/polaris-viz-core';
4+
5+
import {TOOLTIP_WIDTH} from '../../constants';
6+
import {FUNNEL_CHART_CONNECTOR_GRADIENT} from '../../../shared/FunnelChartConnector';
7+
import {FUNNEL_CHART_SEGMENT_FILL} from '../../../shared/FunnelChartSegment';
8+
import type {FunnelChartNextProps} from '../../FunnelChartNext';
9+
import {SeriesIcon} from '../../../shared/SeriesIcon';
10+
import {calculateDropOff} from '../../utilities/calculate-dropoff';
11+
import {TooltipContentContainer, TooltipTitle} from '../../../TooltipContent';
12+
13+
import styles from './Tooltip.scss';
14+
15+
export interface TooltipContentProps {
16+
activeIndex: number;
17+
dataSeries: DataPoint[];
18+
tooltipLabels: FunnelChartNextProps['tooltipLabels'];
19+
labelFormatter: LabelFormatter;
20+
percentageFormatter: (value: number) => string;
21+
}
22+
23+
interface Data {
24+
key: string;
25+
value: string;
26+
color: Color;
27+
percent: number;
28+
}
29+
30+
export function Tooltip({
31+
activeIndex,
32+
dataSeries,
33+
tooltipLabels,
34+
labelFormatter,
35+
percentageFormatter,
36+
}: TooltipContentProps) {
37+
const point = dataSeries[activeIndex];
38+
const previousPoint = dataSeries[activeIndex - 1];
39+
const isFirst = activeIndex === 0;
40+
41+
const dropOffPercentage = Math.abs(
42+
calculateDropOff(previousPoint?.value ?? 0, point?.value ?? 0),
43+
);
44+
45+
const data: Data[] = [
46+
{
47+
key: tooltipLabels.reached,
48+
value: labelFormatter(point.value),
49+
color: FUNNEL_CHART_SEGMENT_FILL,
50+
percent: isFirst ? 100 : 100 - dropOffPercentage,
51+
},
52+
];
53+
54+
if (!isFirst) {
55+
data.push({
56+
key: tooltipLabels.dropped,
57+
value: labelFormatter((previousPoint?.value ?? 0) - (point.value ?? 0)),
58+
percent: dropOffPercentage,
59+
color: FUNNEL_CHART_CONNECTOR_GRADIENT,
60+
});
61+
}
62+
63+
return (
64+
<TooltipContentContainer
65+
maxWidth={TOOLTIP_WIDTH}
66+
theme={DEFAULT_THEME_NAME}
67+
>
68+
{() => (
69+
<Fragment>
70+
<TooltipTitle theme={DEFAULT_THEME_NAME}>{point.key}</TooltipTitle>
71+
<div className={styles.Rows}>
72+
{data.map(({key, value, color, percent}, index) => {
73+
return (
74+
<div className={styles.Row} key={`row-${index}-${key}`}>
75+
<div className={styles.Keys}>
76+
<SeriesIcon color={color!} shape="Bar" />
77+
<span>{key}</span>
78+
</div>
79+
<div className={styles.Values}>
80+
<span>{value}</span>
81+
<span>
82+
<strong>{percentageFormatter(percent)}</strong>
83+
</span>
84+
</div>
85+
</div>
86+
);
87+
})}
88+
</div>
89+
</Fragment>
90+
)}
91+
</TooltipContentContainer>
92+
);
93+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export {Tooltip} from './Tooltip';

0 commit comments

Comments
 (0)