Skip to content

Commit cd80b91

Browse files
committed
Use a portal for horizontal tooltips
1 parent a648037 commit cd80b91

File tree

9 files changed

+89
-103
lines changed

9 files changed

+89
-103
lines changed

Diff for: packages/polaris-viz/CHANGELOG.md

+5-1
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+
### Changed
11+
12+
- Changed `<HorizontalBarChart />` to use a react portal to allow tooltips to render outside the bounds of the chart.
913

1014
## [15.8.1] - 2025-01-21
1115

Diff for: packages/polaris-viz/src/components/BarChart/stories/playground/ExternalTooltip.stories.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ export const ExternalTooltipWithFrame: Story<BarChartProps> =
7777
TemplateWithFrame.bind({});
7878

7979
ExternalTooltip.args = {
80+
direction: 'horizontal',
8081
data: [
8182
{
8283
name: 'Apr 1 – Apr 14, 2020',

Diff for: packages/polaris-viz/src/components/HorizontalBarChart/Chart.tsx

+12-11
Original file line numberDiff line numberDiff line change
@@ -148,15 +148,14 @@ export function Chart({
148148
longestLabel,
149149
});
150150

151-
const {barHeight, chartHeight, groupBarsAreaHeight, groupHeight} =
152-
useHorizontalBarSizes({
153-
chartDimensions: {width: drawableWidth, height: drawableHeight},
154-
isSimple: xAxisOptions.hide,
155-
isStacked,
156-
seriesLength: longestSeriesCount,
157-
singleBarCount: data.length,
158-
xAxisHeight,
159-
});
151+
const {barHeight, chartHeight, groupHeight} = useHorizontalBarSizes({
152+
chartDimensions: {width: drawableWidth, height: drawableHeight},
153+
isSimple: xAxisOptions.hide,
154+
isStacked,
155+
seriesLength: longestSeriesCount,
156+
singleBarCount: data.length,
157+
xAxisHeight,
158+
});
160159

161160
const annotationsDrawableHeight =
162161
chartYPosition + chartHeight + ANNOTATIONS_LABELS_OFFSET;
@@ -293,17 +292,19 @@ export function Chart({
293292

294293
{highestValueForSeries.length !== 0 && (
295294
<TooltipWrapper
296-
bandwidth={groupBarsAreaHeight}
295+
bandwidth={groupHeight}
297296
chartBounds={chartBounds}
298297
chartType={InternalChartType.HorizontalBar}
299298
data={data}
300299
focusElementDataType={DataType.BarGroup}
301300
getMarkup={getTooltipMarkup}
302-
margin={ChartMargin}
301+
margin={{...ChartMargin, Top: chartYPosition}}
303302
parentElement={svgRef}
304303
longestSeriesIndex={longestSeriesIndex}
304+
highestValueForSeries={highestValueForSeries}
305305
xScale={xScale}
306306
type={type}
307+
usePortal
307308
/>
308309
)}
309310

Diff for: packages/polaris-viz/src/components/TooltipWrapper/TooltipWrapper.tsx

+10
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ interface BaseProps {
3737
xScale: ScaleLinear<number, number> | ScaleBand<string>;
3838
bandwidth?: number;
3939
onIndexChange?: (index: number | null) => void;
40+
highestValueForSeries?: number[];
4041
id?: string;
4142
type?: ChartType;
4243
yScale?: ScaleLinear<number, number>;
@@ -56,6 +57,7 @@ function TooltipWrapperRaw(props: BaseProps) {
5657
type,
5758
xScale,
5859
yScale,
60+
highestValueForSeries,
5961
} = props;
6062
const {scrollContainer, isTouchDevice, containerBounds} = useChartContext();
6163
const [position, setPosition] = useState<TooltipPosition>({
@@ -111,13 +113,19 @@ function TooltipWrapperRaw(props: BaseProps) {
111113
case InternalChartType.HorizontalBar:
112114
return getHorizontalBarChartTooltipPosition({
113115
chartBounds,
116+
containerBounds,
114117
data,
115118
event,
116119
eventType,
117120
index,
118121
longestSeriesIndex,
119122
type,
120123
xScale: xScale as ScaleLinear<number, number>,
124+
highestValueForSeries: highestValueForSeries ?? [],
125+
bandwidth,
126+
scrollY: scrollContainer
127+
? scrollContainer.scrollTop
128+
: window.scrollY,
121129
});
122130
case InternalChartType.Bar:
123131
default:
@@ -136,6 +144,8 @@ function TooltipWrapperRaw(props: BaseProps) {
136144
}
137145
},
138146
[
147+
highestValueForSeries,
148+
bandwidth,
139149
chartBounds,
140150
containerBounds,
141151
chartType,
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import type {BoundingRect, Dimensions} from '@shopify/polaris-viz-core';
2-
import {HORIZONTAL_GROUP_LABEL_HEIGHT} from '@shopify/polaris-viz-core';
1+
import type {Dimensions} from '@shopify/polaris-viz-core';
2+
import {clamp, HORIZONTAL_GROUP_LABEL_HEIGHT} from '@shopify/polaris-viz-core';
33

4-
import {TOOLTIP_MARGIN} from '../constants';
4+
import {SCROLLBAR_WIDTH, TOOLTIP_MARGIN} from '../constants';
55
import type {AlteredPositionProps, AlteredPositionReturn} from '../types';
66

77
export function getAlteredHorizontalBarPosition(
@@ -20,100 +20,59 @@ function getNegativeOffset(props: AlteredPositionProps): AlteredPositionReturn {
2020
const yOffset = (bandwidth - tooltipDimensions.height) / 2;
2121

2222
const y = currentY - tooltipDimensions.height;
23+
2324
if (flippedX - tooltipDimensions.width < 0) {
24-
return {x: flippedX, y: y < 0 ? 0 : y};
25+
return clampPosition({
26+
x: flippedX,
27+
y: y < 0 ? 0 : y,
28+
tooltipDimensions,
29+
});
2530
}
2631

27-
return {
32+
return clampPosition({
2833
x: flippedX - tooltipDimensions.width - TOOLTIP_MARGIN,
2934
y: currentY + HORIZONTAL_GROUP_LABEL_HEIGHT + yOffset,
30-
};
35+
tooltipDimensions,
36+
});
3137
}
3238

3339
function getPositiveOffset(props: AlteredPositionProps): AlteredPositionReturn {
34-
const {bandwidth, currentX, currentY, tooltipDimensions, chartBounds} = props;
40+
const {currentX, currentY} = props;
3541

36-
const isOutside = isOutsideBounds({
42+
return clampPosition({
3743
x: currentX,
3844
y: currentY,
39-
tooltipDimensions,
40-
chartBounds,
45+
tooltipDimensions: props.tooltipDimensions,
4146
});
42-
43-
if (isOutside.top && isOutside.right) {
44-
return {
45-
x: chartBounds.width - tooltipDimensions.width,
46-
y: 0,
47-
};
48-
}
49-
50-
if (isOutside.top && !isOutside.right) {
51-
return {
52-
x: currentX + TOOLTIP_MARGIN,
53-
y: 0,
54-
};
55-
}
56-
57-
if (!isOutside.right && !isOutside.bottom) {
58-
const yOffset = (bandwidth - tooltipDimensions.height) / 2;
59-
return {
60-
x: currentX + TOOLTIP_MARGIN,
61-
y: currentY + HORIZONTAL_GROUP_LABEL_HEIGHT + yOffset,
62-
};
63-
}
64-
65-
if (isOutside.right) {
66-
const x = currentX - tooltipDimensions.width;
67-
const y =
68-
currentY -
69-
tooltipDimensions.height +
70-
HORIZONTAL_GROUP_LABEL_HEIGHT -
71-
TOOLTIP_MARGIN;
72-
73-
if (y < 0) {
74-
return {
75-
x,
76-
y: bandwidth + HORIZONTAL_GROUP_LABEL_HEIGHT + TOOLTIP_MARGIN,
77-
};
78-
}
79-
80-
return {
81-
x,
82-
y,
83-
};
84-
}
85-
86-
if (isOutside.bottom) {
87-
return {
88-
x: currentX + TOOLTIP_MARGIN,
89-
y:
90-
chartBounds.height -
91-
tooltipDimensions.height -
92-
HORIZONTAL_GROUP_LABEL_HEIGHT,
93-
};
94-
}
95-
96-
return {x: currentX, y: currentY};
9747
}
9848

99-
function isOutsideBounds({
49+
function clampPosition({
10050
x,
10151
y,
10252
tooltipDimensions,
103-
chartBounds,
10453
}: {
10554
x: number;
10655
y: number;
10756
tooltipDimensions: Dimensions;
108-
chartBounds: BoundingRect;
10957
}) {
110-
const right = x + TOOLTIP_MARGIN + tooltipDimensions.width;
111-
const bottom = y + tooltipDimensions.height;
112-
11358
return {
114-
left: x <= 0,
115-
right: right > chartBounds.width,
116-
bottom: bottom > chartBounds.height,
117-
top: y <= 0,
59+
x: clamp({
60+
amount: x,
61+
min: TOOLTIP_MARGIN,
62+
max:
63+
window.innerWidth -
64+
tooltipDimensions.width -
65+
TOOLTIP_MARGIN -
66+
SCROLLBAR_WIDTH,
67+
}),
68+
y: clamp({
69+
amount: y,
70+
min: window.scrollY + TOOLTIP_MARGIN,
71+
max:
72+
window.scrollY +
73+
window.innerHeight -
74+
tooltipDimensions.height -
75+
TOOLTIP_MARGIN,
76+
}),
11877
};
11978
}

Diff for: packages/polaris-viz/src/components/TooltipWrapper/utilities/getHorizontalBarChartTooltipPosition.ts

+25-11
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,37 @@
11
import type {ScaleLinear} from 'd3-scale';
2+
import type {BoundingRect} from '@shopify/polaris-viz-core';
23

34
import {getStackedValuesFromDataSeries} from '../../../utilities/getStackedValuesFromDataSeries';
45
import type {TooltipPosition, TooltipPositionParams} from '../types';
56
import {TOOLTIP_POSITION_DEFAULT_RETURN} from '../constants';
67

78
import {eventPointNative} from './eventPoint';
89

10+
const SPACING = 10;
11+
912
interface Props extends Omit<TooltipPositionParams, 'xScale'> {
13+
bandwidth: number;
14+
containerBounds: BoundingRect;
15+
highestValueForSeries: number[];
16+
scrollY: number;
1017
xScale: ScaleLinear<number, number>;
1118
}
1219

1320
export function getHorizontalBarChartTooltipPosition({
14-
chartBounds,
21+
containerBounds,
1522
data,
1623
event,
1724
eventType,
1825
index,
1926
longestSeriesIndex,
2027
type,
2128
xScale,
29+
scrollY,
30+
highestValueForSeries,
31+
bandwidth,
2232
}: Props): TooltipPosition {
23-
const groupHeight = chartBounds.height / data[longestSeriesIndex].data.length;
33+
const groupHeight = bandwidth;
34+
2435
const isStacked = type === 'stacked';
2536

2637
if (eventType === 'mouse' && event) {
@@ -32,7 +43,7 @@ export function getHorizontalBarChartTooltipPosition({
3243

3344
const {svgY} = point;
3445

35-
const currentPoint = svgY - 0;
46+
const currentPoint = svgY - scrollY;
3647
const currentIndex = Math.floor(currentPoint / groupHeight);
3748

3849
if (
@@ -42,14 +53,17 @@ export function getHorizontalBarChartTooltipPosition({
4253
return TOOLTIP_POSITION_DEFAULT_RETURN;
4354
}
4455

45-
return formatPositionForTooltip(currentIndex);
56+
return formatPositionForTooltip(currentIndex, containerBounds);
4657
} else if (index != null) {
47-
return formatPositionForTooltip(index);
58+
return formatPositionForTooltip(index, containerBounds);
4859
}
4960

5061
return TOOLTIP_POSITION_DEFAULT_RETURN;
5162

52-
function formatPositionForTooltip(index: number): TooltipPosition {
63+
function formatPositionForTooltip(
64+
index: number,
65+
containerBounds: BoundingRect,
66+
): TooltipPosition {
5367
if (isStacked) {
5468
const {formattedStackedValues} = getStackedValuesFromDataSeries(data);
5569

@@ -64,18 +78,18 @@ export function getHorizontalBarChartTooltipPosition({
6478
}, xScale(0));
6579

6680
return {
67-
x: chartBounds.x + x,
68-
y: chartBounds.y + groupHeight * index,
81+
x: containerBounds.x + x,
82+
y: containerBounds.y + groupHeight * index,
6983
activeIndex: index,
7084
};
7185
}
7286

73-
const highestValue = data[longestSeriesIndex].data[index].value ?? 0;
74-
const x = chartBounds.x + (xScale(highestValue ?? 0) ?? 0);
87+
const highestValue = highestValueForSeries[index] ?? 0;
88+
const x = containerBounds.x + (xScale(highestValue ?? 0) ?? 0) + SPACING;
7589

7690
return {
7791
x: highestValue < 0 ? -x : x,
78-
y: groupHeight * index,
92+
y: containerBounds.y + groupHeight * index,
7993
activeIndex: index,
8094
};
8195
}

Diff for: packages/polaris-viz/src/components/shared/HorizontalGroup/HorizontalGroup.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ export function HorizontalGroup({
106106
className={style.Group}
107107
>
108108
<rect
109-
fill="transparent"
109+
fill="green"
110110
height={groupHeight}
111111
width={containerWidth}
112112
y={-(groupHeight - rowHeight) / 2}

Diff for: packages/polaris-viz/src/hooks/tests/useHorizontalBarSizes.test.tsx

+1-3
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ describe('useHorizontalBarSizes()', () => {
3232
expect(data).toStrictEqual({
3333
barHeight: 44.333333333333336,
3434
chartHeight: 380,
35-
groupBarsAreaHeight: 88.66666666666667,
35+
3636
groupHeight: 126.66666666666667,
3737
});
3838
});
@@ -51,7 +51,6 @@ describe('useHorizontalBarSizes()', () => {
5151
expect(data).toStrictEqual({
5252
barHeight: 28.88888888888889,
5353
chartHeight: 380,
54-
groupBarsAreaHeight: 86.66666666666667,
5554
groupHeight: 126.66666666666667,
5655
});
5756
});
@@ -69,7 +68,6 @@ describe('useHorizontalBarSizes()', () => {
6968
const data = parseData(result);
7069

7170
expect(data.chartHeight).toStrictEqual(416);
72-
expect(data.groupBarsAreaHeight).toStrictEqual(100.66666666666666);
7371
expect(data.groupHeight).toStrictEqual(138.66666666666666);
7472
});
7573
});

Diff for: packages/polaris-viz/src/hooks/useHorizontalBarSizes.ts

-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@ export function useHorizontalBarSizes({
6565
return {
6666
barHeight,
6767
chartHeight,
68-
groupBarsAreaHeight,
6968
groupHeight,
7069
};
7170
}, [

0 commit comments

Comments
 (0)