Skip to content

Commit

Permalink
Add tooltips to Funnel Chart Next
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelnesen committed Jan 14, 2025
1 parent 1f18106 commit 29a6655
Show file tree
Hide file tree
Showing 12 changed files with 403 additions and 6 deletions.
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 Tooltips to `<FunnelChartNext />`

## [15.7.0] - 2025-01-08

Expand Down
84 changes: 80 additions & 4 deletions packages/polaris-viz/src/components/FunnelChartNext/Chart.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type {ReactNode} from 'react';
import {Fragment, useMemo} from 'react';
import {Fragment, useMemo, useState} from 'react';
import {scaleBand, scaleLinear} from 'd3-scale';
import type {DataSeries, LabelFormatter} from '@shopify/polaris-viz-core';
import {
Expand All @@ -16,18 +16,28 @@ import {
import {FunnelChartSegment} from '../shared';
import {ChartElements} from '../ChartElements';

import {FunnelChartLabels} from './components';
import {
FunnelChartLabels,
FunnelTooltip,
Tooltip,
TooltipWithPortal,
} from './components';
import {
LABELS_HEIGHT,
LINE_GRADIENT,
LINE_OFFSET,
LINE_WIDTH,
GAP,
SEGMENT_WIDTH_RATIO,
TOOLTIP_HORIZONTAL_OFFSET,
TOOLTIP_HEIGHT,
TOOLTIP_WIDTH,
} from './constants';
import type {FunnelChartNextProps} from './FunnelChartNext';

export interface ChartProps {
data: DataSeries[];
tooltipLabels: FunnelChartNextProps['tooltipLabels'];
seriesNameFormatter: LabelFormatter;
labelFormatter: LabelFormatter;
percentageFormatter?: (value: number) => string;
Expand All @@ -36,20 +46,27 @@ export interface ChartProps {

export function Chart({
data,
tooltipLabels,
seriesNameFormatter,
labelFormatter,
percentageFormatter = (value: number) => {
return labelFormatter(value);
},
renderScaleIconTooltipContent,
}: ChartProps) {
const [tooltipIndex, setTooltipIndex] = useState<number | null>(null);
const {containerBounds} = useChartContext();
const dataSeries = data[0].data;
const xValues = dataSeries.map(({key}) => key) as string[];
const yValues = dataSeries.map(({value}) => value) as [number, number];
const sanitizedYValues = yValues.map((value) => Math.max(0, value));

const {width: drawableWidth, height: drawableHeight} = containerBounds ?? {
const {
width: drawableWidth,
height: drawableHeight,
x: chartX,
y: chartY,
} = containerBounds ?? {
width: 0,
height: 0,
x: 0,
Expand Down Expand Up @@ -107,9 +124,18 @@ export function Chart({
return labelFormatter(dataPoint.value);
});

const handleChartBlur = (event: React.FocusEvent) => {
const currentTarget = event.currentTarget;
const relatedTarget = event.relatedTarget as Node;

if (!currentTarget.contains(relatedTarget)) {
setTooltipIndex(null);
}
};

return (
<ChartElements.Svg height={drawableHeight} width={drawableWidth}>
<g>
<g onBlur={handleChartBlur}>
<FunnelChartConnectorGradient />

<LinearGradientWithStops
Expand Down Expand Up @@ -152,6 +178,8 @@ export function Chart({
barWidth={barWidth}
index={index}
isLast={isLast}
onMouseEnter={(index) => setTooltipIndex(index)}
onMouseLeave={() => setTooltipIndex(null)}
shouldApplyScaling={shouldApplyScaling}
x={x}
>
Expand Down Expand Up @@ -182,7 +210,55 @@ export function Chart({
</Fragment>
);
})}
<TooltipWithPortal>{getTooltipMarkup()}</TooltipWithPortal>
</g>
</ChartElements.Svg>
);

function getTooltipMarkup() {
if (tooltipIndex == null) {
return null;
}

const activeDataSeries = dataSeries[tooltipIndex];

if (activeDataSeries == null) {
return null;
}

const xPosition = getXPosition();
const yPosition = getYPosition();

return (
<FunnelTooltip x={xPosition} y={yPosition}>
<Tooltip
activeIndex={tooltipIndex}
dataSeries={dataSeries}
tooltipLabels={tooltipLabels}
labelFormatter={labelFormatter}
percentageFormatter={percentageFormatter}
/>
</FunnelTooltip>
);

function getXPosition() {
if (tooltipIndex === 0) {
return chartX + barWidth + TOOLTIP_HORIZONTAL_OFFSET;
}

const xOffset = (barWidth - TOOLTIP_WIDTH) / 2;
return chartX + (xScale(activeDataSeries.key.toString()) ?? 0) + xOffset;
}

function getYPosition() {
const barHeight = getBarHeight(activeDataSeries.value ?? 0);
const yPosition = chartY + drawableHeight - barHeight;

if (tooltipIndex === 0) {
return yPosition;
}

return yPosition - TOOLTIP_HEIGHT;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import {ChartSkeleton} from '../';
import {Chart} from './Chart';

export type FunnelChartNextProps = {
tooltipLabels: {
reached: string;
dropped: string;
};
seriesNameFormatter?: LabelFormatter;
labelFormatter?: LabelFormatter;
renderScaleIconTooltipContent?: () => ReactNode;
Expand All @@ -30,7 +34,7 @@ export function FunnelChartNext(props: FunnelChartNextProps) {
isAnimated,
state,
errorText,

tooltipLabels,
seriesNameFormatter = DEFAULT_LABEL_FORMATTER,
labelFormatter = DEFAULT_LABEL_FORMATTER,
percentageFormatter,
Expand Down Expand Up @@ -59,6 +63,7 @@ export function FunnelChartNext(props: FunnelChartNextProps) {
) : (
<Chart
data={data}
tooltipLabels={tooltipLabels}
seriesNameFormatter={seriesNameFormatter}
labelFormatter={labelFormatter}
percentageFormatter={percentageFormatter}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
.Rows {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}

.Row {
font-size: 12px;
display: grid;
grid-column: 1 / -1;
grid-template-columns: subgrid;
color: rgba(97, 97, 97, 1);
align-items: center;
}

.Keys {
display: flex;
align-items: center;
gap: 4px;
}

.Values {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 4px;
font-weight: 600;

strong {
font-weight: 600;
color: rgba(31, 33, 36, 1);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import {Fragment} from 'react';
import type {Color, DataPoint, LabelFormatter} from '@shopify/polaris-viz-core';
import {DEFAULT_THEME_NAME} from '@shopify/polaris-viz-core';

import {TOOLTIP_WIDTH} from '../../constants';
import {FUNNEL_CHART_CONNECTOR_GRADIENT} from '../../../shared/FunnelChartConnector';
import {FUNNEL_CHART_SEGMENT_FILL} from '../../../shared/FunnelChartSegment';
import type {FunnelChartNextProps} from '../../FunnelChartNext';
import {SeriesIcon} from '../../../shared/SeriesIcon';
import {calculateDropOff} from '../../utilities/calculate-dropoff';
import {TooltipContentContainer, TooltipTitle} from '../../../TooltipContent';

import styles from './Tooltip.scss';

export interface TooltipContentProps {
activeIndex: number;
dataSeries: DataPoint[];
tooltipLabels: FunnelChartNextProps['tooltipLabels'];
labelFormatter: LabelFormatter;
percentageFormatter: (value: number) => string;
}

interface Data {
key: string;
value: string;
color: Color;
percent: number;
}

export function Tooltip({
activeIndex,
dataSeries,
tooltipLabels,
labelFormatter,
percentageFormatter,
}: TooltipContentProps) {
const point = dataSeries[activeIndex];
const previousPoint = dataSeries[activeIndex - 1];
const isFirst = activeIndex === 0;

const dropOffPercentage = Math.abs(
calculateDropOff(previousPoint?.value ?? 0, point?.value ?? 0),
);

const data: Data[] = [
{
key: tooltipLabels.reached,
value: labelFormatter(point.value),
color: FUNNEL_CHART_SEGMENT_FILL,
percent: isFirst ? 100 : 100 - dropOffPercentage,
},
];

if (!isFirst) {
data.push({
key: tooltipLabels.dropped,
value: labelFormatter((previousPoint?.value ?? 0) - (point.value ?? 0)),
percent: dropOffPercentage,
color: FUNNEL_CHART_CONNECTOR_GRADIENT,
});
}

return (
<TooltipContentContainer
maxWidth={TOOLTIP_WIDTH}
theme={DEFAULT_THEME_NAME}
>
{() => (
<Fragment>
<TooltipTitle theme={DEFAULT_THEME_NAME}>{point.key}</TooltipTitle>
<div className={styles.Rows}>
{data.map(({key, value, color, percent}, index) => {
return (
<div className={styles.Row} key={`row-${index}-${key}`}>
<div className={styles.Keys}>
<SeriesIcon color={color!} shape="Bar" />
<span>{key}</span>
</div>
<div className={styles.Values}>
<span>{value}</span>
<span>
<strong>{percentageFormatter(percent)}</strong>
</span>
</div>
</div>
);
})}
</div>
</Fragment>
)}
</TooltipContentContainer>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {Tooltip} from './Tooltip';
Loading

0 comments on commit 29a6655

Please sign in to comment.