Skip to content

Commit

Permalink
Tweak tooltips
Browse files Browse the repository at this point in the history
  • Loading branch information
envex committed Oct 17, 2024
1 parent 71cbaed commit 3ff9400
Show file tree
Hide file tree
Showing 14 changed files with 222 additions and 158 deletions.
3 changes: 2 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@
"UNSTABLE_telemetry"
]
}
]
],
"@shopify/strict-component-boundaries": "warn"
},
"overrides": [
{
Expand Down
174 changes: 92 additions & 82 deletions packages/polaris-viz/src/components/FunnelChartNext/Chart.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type {ReactNode} from 'react';
import {Fragment, useMemo, useCallback, useState} from 'react';
import {scaleBand, scaleLinear} from 'd3-scale';
import type {
Expand All @@ -6,32 +7,26 @@ import type {
XAxisOptions,
YAxisOptions,
} from '@shopify/polaris-viz-core';
import {
uniqueId,
LinearGradientWithStops,
DataType,
} from '@shopify/polaris-viz-core';
import {uniqueId, LinearGradientWithStops} from '@shopify/polaris-viz-core';
import {createPortal} from 'react-dom';

import {TOOLTIP_ROOT_ID} from '../TooltipWrapper/constants';
import {useRootContainer} from '../../hooks/useRootContainer';
import {FunnelChartConnectorGradient} from '../shared/FunnelChartConnector';
import {FunnelChartSegment} from '../shared';
import type {TooltipPosition, TooltipPositionParams} from '../TooltipWrapper';
import {
TOOLTIP_POSITION_DEFAULT_RETURN,
TooltipHorizontalOffset,
TooltipVerticalOffset,
TooltipWrapper,
} from '../TooltipWrapper';
import {SingleTextLine} from '../Labels';
import {ChartElements} from '../ChartElements';

import {FunnelChartXAxisLabels, Tooltip, FunnelConnector} from './components/';
import {getTooltipPosition} from './utilities/get-tooltip-position';
import {calculateDropOff} from './utilities/calculate-dropoff';
import type {FunnelChartNextProps} from './FunnelChartNext';
import {getFunnelBarHeight} from './utilities/get-funnel-bar-height';
import {FunnelTooltip} from './components/FunnelTooltip/FunnelTooltip';
import {FUNNEL_CONNECTOR_Y_OFFSET, TOOLTIP_WIDTH} from './constants';

export interface ChartProps {
data: DataSeries[];
showConnectionPercentage: boolean;
tooltipLabels: FunnelChartNextProps['tooltipLabels'];
xAxisOptions: Required<XAxisOptions>;
yAxisOptions: Required<YAxisOptions>;
Expand All @@ -40,9 +35,11 @@ export interface ChartProps {

const LINE_OFFSET = 3;
const LINE_WIDTH = 1;

const TOOLTIP_HEIGHT = 90;
const SHORT_TOOLTIP_HEIGHT = 65;
const GAP = 1;

const PERCENTAGE_COLOR = 'rgba(48, 48, 48, 1)';
const LINE_GRADIENT = [
{
color: 'rgba(227, 227, 227, 1)',
Expand All @@ -60,25 +57,26 @@ const PERCENTAGE_SUMMARY_HEIGHT = 30;
export function Chart({
data,
dimensions,
showConnectionPercentage,
tooltipLabels,
xAxisOptions,
yAxisOptions,
}: ChartProps) {
const [svgRef, setSvgRef] = useState<SVGSVGElement | null>(null);
const [tooltipIndex, setTooltipIndex] = useState<number | null>(null);

const dataSeries = data[0].data;

const xValues = dataSeries.map(({key}) => key) as string[];
const yValues = dataSeries.map(({value}) => value) as [number, number];

const {width: drawableWidth, height: drawableHeight} = dimensions ?? {
width: 0,
height: 0,
};

const chartBounds: BoundingRect = {
const {
width: drawableWidth,
height: drawableHeight,
x: chartX,
y: chartY,
} = dimensions ?? {
width: 0,
height: 0,
x: 0,
y: 0,
};
Expand All @@ -94,15 +92,22 @@ export function Chart({
.range([0, drawableWidth])
.domain(labels.map((_, index) => index.toString()));

const highestYValue = Math.max(...yValues);
const connectionPercentageHeight = showConnectionPercentage
? FUNNEL_CONNECTOR_Y_OFFSET / 2
: 0;

const yScale = scaleLinear()
.range([
0,
drawableHeight -
LABELS_HEIGHT -
PERCENTAGE_SUMMARY_HEIGHT -
PERCENTAGE_SUMMARY_HEIGHT,
connectionPercentageHeight,
])
.domain([0, Math.max(...yValues)]);
.domain([0, highestYValue]);

const tallestBarHeight = yScale(highestYValue);

const sectionWidth = xScale.bandwidth();
const barWidth = sectionWidth * 0.75;
Expand Down Expand Up @@ -137,11 +142,7 @@ export function Chart({
);

return (
<ChartElements.Svg
height={drawableHeight}
width={drawableWidth}
setRef={setSvgRef}
>
<ChartElements.Svg height={drawableHeight} width={drawableWidth}>
<FunnelChartConnectorGradient />

<LinearGradientWithStops
Expand All @@ -154,13 +155,14 @@ export function Chart({
/>

<SingleTextLine
color="rgba(48, 48, 48, 1)"
color={PERCENTAGE_COLOR}
fontWeight={600}
targetWidth={drawableWidth}
fontSize={24}
text={mainPercentage}
willTruncate={false}
/>

{xAxisOptions.hide === false && (
<g transform={`translate(0,${PERCENTAGE_SUMMARY_HEIGHT})`}>
<FunnelChartXAxisLabels
Expand All @@ -175,7 +177,7 @@ export function Chart({

{dataSeries.map((dataPoint, index: number) => {
const nextPoint = dataSeries[index + 1];
const xPosition = xScale(dataPoint.key as string);
const xPosition = xScale(dataPoint.key.toString());
const x = xPosition == null ? 0 : xPosition;
const nextBarHeight = getBarHeight(nextPoint?.value || 0);

Expand All @@ -200,6 +202,9 @@ export function Chart({
drawableHeight={drawableHeight}
index={index}
isLast={isLast}
onMouseEnter={(index) => setTooltipIndex(index)}
onMouseLeave={() => setTooltipIndex(null)}
tallestBarHeight={tallestBarHeight}
x={x}
>
{!isLast && (
Expand All @@ -212,6 +217,7 @@ export function Chart({
}
nextY={drawableHeight - nextBarHeight}
percentCalculation={formattedPercent}
showConnectionPercentage={showConnectionPercentage}
startX={x + barWidth + GAP}
startY={drawableHeight - barHeight}
width={sectionWidth - barWidth}
Expand All @@ -220,79 +226,83 @@ export function Chart({
</FunnelChartSegment>
{index > 0 && (
<rect
y={PERCENTAGE_SUMMARY_HEIGHT}
x={x - (LINE_OFFSET - LINE_WIDTH)}
width={LINE_WIDTH}
height={drawableHeight}
height={drawableHeight - PERCENTAGE_SUMMARY_HEIGHT}
fill={`url(#${lineGradientId})`}
/>
)}
</g>
</Fragment>
);
})}
<TooltipWrapper
bandwidth={xScale.bandwidth()}
chartBounds={chartBounds}
focusElementDataType={DataType.BarGroup}
getMarkup={getTooltipMarkup}
getPosition={getPosition}
margin={{Top: 0, Left: 0, Bottom: 0, Right: 0}}
parentRef={svgRef}
chartDimensions={dimensions}
usePortal
/>

<TooltipWithPortal>{getTooltipMarkup()}</TooltipWithPortal>
</ChartElements.Svg>
);

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

const tooltipHeight =
tooltipIndex === dataSeries.length - 1
? SHORT_TOOLTIP_HEIGHT
: TOOLTIP_HEIGHT;

const activeDataSeries = dataSeries[tooltipIndex];

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

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

return (
<Tooltip
activeIndex={index}
dataSeries={dataSeries}
isLast={index === dataSeries.length - 1}
tooltipLabels={tooltipLabels}
yAxisOptions={yAxisOptions}
/>
<FunnelTooltip x={xPosition} y={yPosition}>
<Tooltip
activeIndex={tooltipIndex}
dataSeries={dataSeries}
isLast={tooltipIndex === dataSeries.length - 1}
tooltipLabels={tooltipLabels}
yAxisOptions={yAxisOptions}
/>
</FunnelTooltip>
);
}

function formatPercentage(value: number) {
return `${yAxisOptions.labelFormatter(isNaN(value) ? 0 : value)}%`;
}
function getXPosition() {
if (tooltipIndex === 0) {
// Push the tooltip beside the bar
return chartX + barWidth + 10;
}

function formatPositionForTooltip(index: number | null): TooltipPosition {
// Don't render the tooltip for the first bar
if ((index === 0 && xAxisOptions.hide === false) || index == null) {
return TOOLTIP_POSITION_DEFAULT_RETURN;
// Center the tooltip over the bar
const xOffset = (barWidth - TOOLTIP_WIDTH) / 2;
return chartX + (xScale(activeDataSeries.key.toString()) ?? 0) + xOffset;
}

const xOffset = (sectionWidth - barWidth) / 2;
const x = labelXScale(`${index}`) ?? 0;
function getYPosition() {
const yPosition =
chartY + drawableHeight - yScale(activeDataSeries.value ?? 0);

const y = drawableHeight - yScale(dataSeries[index].value ?? 0);
if (tooltipIndex === 0) {
return yPosition;
}

return {
x: x - xOffset + (dimensions?.x ?? 0),
y: Math.abs(y) + (dimensions?.y ?? 0),
position: {
horizontal: TooltipHorizontalOffset.Center,
vertical: TooltipVerticalOffset.Above,
},
activeIndex: index,
};
return yPosition - tooltipHeight;
}
}

function getPosition({
event,
index,
eventType,
}: TooltipPositionParams): TooltipPosition {
return getTooltipPosition({
tooltipPosition: {event, index, eventType},
formatPositionForTooltip,
maxIndex: dataSeries.length - 1,
step: xScale.step(),
yMax: drawableHeight,
});
function formatPercentage(value: number) {
return `${yAxisOptions.labelFormatter(isNaN(value) ? 0 : value)}%`;
}
}

function TooltipWithPortal({children}: {children: ReactNode}) {
const container = useRootContainer(TOOLTIP_ROOT_ID);

return createPortal(children, container);
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {ChartSkeleton} from '../';
import {Chart} from './Chart';

export type FunnelChartNextProps = {
showConnectionPercentage?: boolean;
tooltipLabels: {
reached: string;
dropped: string;
Expand All @@ -40,6 +41,7 @@ export function FunnelChartNext(props: FunnelChartNextProps) {
state,
errorText,
onError,
showConnectionPercentage = false,
tooltipLabels,
} = {
...DEFAULT_CHART_PROPS,
Expand Down Expand Up @@ -70,9 +72,10 @@ export function FunnelChartNext(props: FunnelChartNextProps) {
) : (
<Chart
data={data}
showConnectionPercentage={showConnectionPercentage}
tooltipLabels={tooltipLabels}
xAxisOptions={xAxisOptionsForChart}
yAxisOptions={yAxisOptionsForChart}
tooltipLabels={tooltipLabels}
/>
)}
</ChartContainer>
Expand Down
Loading

0 comments on commit 3ff9400

Please sign in to comment.