diff --git a/packages/polaris-viz/CHANGELOG.md b/packages/polaris-viz/CHANGELOG.md
index bd5a03f725..9bb896a821 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
+
+### Changed
+
+- Changed `` to use a react portal to allow tooltips to render outside the bounds of the chart.
## [13.2.0] - 2024-05-27
diff --git a/packages/polaris-viz/src/components/StackedAreaChart/Chart.tsx b/packages/polaris-viz/src/components/StackedAreaChart/Chart.tsx
index 7514e5335c..823de91cde 100644
--- a/packages/polaris-viz/src/components/StackedAreaChart/Chart.tsx
+++ b/packages/polaris-viz/src/components/StackedAreaChart/Chart.tsx
@@ -6,7 +6,6 @@ import type {
DataPoint,
XAxisOptions,
YAxisOptions,
- Dimensions,
BoundingRect,
LabelFormatter,
} from '@shopify/polaris-viz-core';
@@ -18,6 +17,7 @@ import {
COLOR_VISION_SINGLE_ITEM,
useChartPositions,
LINE_HEIGHT,
+ clamp,
} from '@shopify/polaris-viz-core';
import {ChartElements} from '../ChartElements';
@@ -80,7 +80,7 @@ export interface Props {
theme: string;
xAxisOptions: Required;
yAxisOptions: Required;
- dimensions?: Dimensions;
+ dimensions?: BoundingRect;
renderLegendContent?: RenderLegendContent;
renderHiddenLegendLabel?: (count: number) => string;
}
@@ -127,6 +127,7 @@ export function Chart({
const {
stackedValues,
+ longestSeriesIndex,
longestSeriesLength,
labels: formattedLabels,
} = useStackedData({
@@ -256,8 +257,8 @@ export function Chart({
const chartBounds: BoundingRect = {
width,
height,
- x: chartXPosition,
- y: chartYPosition,
+ x: dimensions?.x ?? chartXPosition,
+ y: dimensions?.y ?? chartYPosition,
};
const {hasXAxisAnnotations, hasYAxisAnnotations} = checkAvailableAnnotations(
@@ -395,6 +396,7 @@ export function Chart({
margin={ChartMargin}
onIndexChange={(index) => setActivePointIndex(index)}
parentRef={svgRef}
+ usePortal
/>
)}
@@ -424,20 +426,29 @@ export function Chart({
return TOOLTIP_POSITION_DEFAULT_RETURN;
}
- const {svgX, svgY} = point;
+ const {svgX} = point;
const closestIndex = Math.round(xScale.invert(svgX - chartXPosition));
+ const activeIndex = clamp({
+ amount: closestIndex,
+ min: 0,
+ max: data[longestSeriesIndex].data.length - 1,
+ });
+
return {
- x: svgX,
- y: svgY,
+ x: (event as MouseEvent).pageX,
+ y: (event as MouseEvent).pageY,
position: TOOLTIP_POSITION,
- activeIndex: Math.min(longestSeriesLength, closestIndex),
+ activeIndex,
};
} else if (index != null) {
+ const activeIndex = index ?? 0;
+ const x = xScale?.(activeIndex) ?? 0;
+
return {
- x: xScale?.(index) ?? 0,
- y: 0,
+ x: x + (dimensions?.x ?? 0),
+ y: dimensions?.y ?? 0,
position: TOOLTIP_POSITION,
activeIndex: index,
};
diff --git a/packages/polaris-viz/src/components/StackedAreaChart/hooks/useStackedData.ts b/packages/polaris-viz/src/components/StackedAreaChart/hooks/useStackedData.ts
index 153acef850..0b188c4c23 100644
--- a/packages/polaris-viz/src/components/StackedAreaChart/hooks/useStackedData.ts
+++ b/packages/polaris-viz/src/components/StackedAreaChart/hooks/useStackedData.ts
@@ -30,5 +30,20 @@ export function useStackedData({data, xAxisOptions}: Props) {
return Math.max(...stackedValues.map((stack) => stack.length)) - 1;
}, [stackedValues]);
- return {labels: formattedLabels, longestSeriesLength, stackedValues};
+ const longestSeriesIndex = useMemo(
+ () =>
+ data.reduce((maxIndex, currentSeries, currentIndex) => {
+ return data[maxIndex].data.length < currentSeries.data.length
+ ? currentIndex
+ : maxIndex;
+ }, 0),
+ [data],
+ );
+
+ return {
+ labels: formattedLabels,
+ longestSeriesIndex,
+ longestSeriesLength,
+ stackedValues,
+ };
}
diff --git a/packages/polaris-viz/src/components/StackedAreaChart/stories/playground/ExternalTooltipPortal.stories.tsx b/packages/polaris-viz/src/components/StackedAreaChart/stories/playground/ExternalTooltipPortal.stories.tsx
new file mode 100644
index 0000000000..27c0431135
--- /dev/null
+++ b/packages/polaris-viz/src/components/StackedAreaChart/stories/playground/ExternalTooltipPortal.stories.tsx
@@ -0,0 +1,52 @@
+import type {Story} from '@storybook/react';
+
+import type {StackedAreaChartProps} from '../../StackedAreaChart';
+import {StackedAreaChart} from '../../StackedAreaChart';
+import {META} from '../meta';
+import {DEFAULT_DATA, DEFAULT_PROPS} from '../data';
+
+export default {
+ ...META,
+ title: `${META.title}/Playground`,
+ decorators: [],
+};
+
+function Card(args: StackedAreaChartProps) {
+ return (
+
+
+
+ );
+}
+
+const Template: Story = (
+ args: StackedAreaChartProps,
+) => {
+ return (
+
+ );
+};
+
+export const ExternalTooltipPortal: Story =
+ Template.bind({});
+
+ExternalTooltipPortal.args = {
+ ...DEFAULT_PROPS,
+ data: DEFAULT_DATA,
+};
diff --git a/packages/polaris-viz/src/components/StackedAreaChart/utilities/getAlteredStackedAreaChartPosition.ts b/packages/polaris-viz/src/components/StackedAreaChart/utilities/getAlteredStackedAreaChartPosition.ts
index b01023770a..264c904ea4 100644
--- a/packages/polaris-viz/src/components/StackedAreaChart/utilities/getAlteredStackedAreaChartPosition.ts
+++ b/packages/polaris-viz/src/components/StackedAreaChart/utilities/getAlteredStackedAreaChartPosition.ts
@@ -1,10 +1,13 @@
import type {Dimensions} from '@shopify/polaris-viz-core';
+import {clamp} from '@shopify/polaris-viz-core';
+import {getRightPosition} from '../../../components/TooltipWrapper';
import type {TooltipPositionOffset} from '../../TooltipWrapper';
import type {Margin} from '../../../types';
// The space between the cursor and the tooltip
const TOOLTIP_MARGIN = 20;
+const SCROLLBAR_WIDTH = 20;
export interface AlteredPositionProps {
bandwidth: number;
@@ -26,23 +29,18 @@ export type AlteredPosition = (
props: AlteredPositionProps,
) => AlteredPositionReturn;
-export function getAlteredStackedAreaChartPosition({
- currentX,
- currentY,
- chartBounds,
- margin,
- tooltipDimensions,
-}: AlteredPositionProps): AlteredPositionReturn {
- const x = Math.min(
- Math.max(currentX, TOOLTIP_MARGIN),
- chartBounds.width - tooltipDimensions.width - TOOLTIP_MARGIN,
- );
+export function getAlteredStackedAreaChartPosition(
+ props: AlteredPositionProps,
+): AlteredPositionReturn {
+ const {currentX, currentY, chartBounds, margin, tooltipDimensions} = props;
+
+ let x = currentX;
+ let y = currentY;
// Y POSITIONING
// If y is below the chart, adjust the tooltip position to the bottom of the chart
//
-
- const y =
+ y =
currentY >= chartBounds.y + chartBounds.height
? chartBounds.height -
tooltipDimensions.height -
@@ -50,5 +48,40 @@ export function getAlteredStackedAreaChartPosition({
margin.Bottom
: currentY;
- return {x, y};
+ // X POSITIONING
+ const right = getRightPosition(x, props);
+ x = right.value;
+
+ if (right.wasOutsideBounds) {
+ const left = getLeftPosition(x);
+ x = left.value;
+ }
+
+ return {
+ x: clamp({
+ amount: x,
+ min: TOOLTIP_MARGIN,
+ max:
+ window.innerWidth -
+ props.tooltipDimensions.width -
+ TOOLTIP_MARGIN -
+ SCROLLBAR_WIDTH,
+ }),
+ y: clamp({
+ amount: y,
+ min: window.scrollY + TOOLTIP_MARGIN,
+ max:
+ window.scrollY +
+ window.innerHeight -
+ props.tooltipDimensions.height -
+ TOOLTIP_MARGIN,
+ }),
+ };
+}
+
+function getLeftPosition(value: number): {
+ value: number;
+ wasOutsideBounds: boolean;
+} {
+ return {value: value - TOOLTIP_MARGIN, wasOutsideBounds: false};
}
diff --git a/packages/polaris-viz/src/components/StackedAreaChart/utilities/tests/getAlteredStackedAreaChartPosition.test.ts b/packages/polaris-viz/src/components/StackedAreaChart/utilities/tests/getAlteredStackedAreaChartPosition.test.ts
index c01005e5b9..b0f147a09e 100644
--- a/packages/polaris-viz/src/components/StackedAreaChart/utilities/tests/getAlteredStackedAreaChartPosition.test.ts
+++ b/packages/polaris-viz/src/components/StackedAreaChart/utilities/tests/getAlteredStackedAreaChartPosition.test.ts
@@ -22,7 +22,25 @@ const BASE_PROPS: AlteredPositionProps = {
},
};
+let windowSpy;
+
+function mockWindow({scrollY = 0, innerHeight = 1000, innerWidth = 500}) {
+ windowSpy.mockImplementation(() => ({
+ scrollY,
+ innerHeight,
+ innerWidth,
+ }));
+}
+
describe('getAlteredStackedAreaChartPosition', () => {
+ beforeEach(() => {
+ windowSpy = jest.spyOn(window, 'window', 'get');
+ });
+
+ afterEach(() => {
+ windowSpy.mockRestore();
+ });
+
it('returns the original position of y when currentY is within the chart bounds', () => {
const props = {
...BASE_PROPS,
@@ -32,7 +50,6 @@ describe('getAlteredStackedAreaChartPosition', () => {
const result = getAlteredStackedAreaChartPosition(props);
- expect(result.x).toBe(50);
expect(result.y).toBe(50);
});
@@ -47,8 +64,6 @@ describe('getAlteredStackedAreaChartPosition', () => {
const margin = props.margin;
const result = getAlteredStackedAreaChartPosition(props);
- expect(result.x).toBe(20);
-
expect(result.y).toBe(
chartBounds.height -
tooltipDimensions.height -
@@ -68,8 +83,6 @@ describe('getAlteredStackedAreaChartPosition', () => {
const margin = props.margin;
const result = getAlteredStackedAreaChartPosition(props);
- expect(result.x).toBe(20);
-
expect(result.y).toBe(
chartBounds.height -
tooltipDimensions.height -
@@ -77,4 +90,34 @@ describe('getAlteredStackedAreaChartPosition', () => {
margin.Bottom,
);
});
+
+ describe('x', () => {
+ it('clamps to the left of a window', () => {
+ mockWindow({
+ scrollY: 0,
+ });
+
+ expect(
+ getAlteredStackedAreaChartPosition({
+ ...BASE_PROPS,
+ currentX: -1000,
+ currentY: 0,
+ }),
+ ).toStrictEqual({x: 20, y: 20});
+ });
+
+ it('clamps to the right of a window', () => {
+ mockWindow({
+ scrollY: 0,
+ });
+
+ expect(
+ getAlteredStackedAreaChartPosition({
+ ...BASE_PROPS,
+ currentX: 1000,
+ currentY: 0,
+ }),
+ ).toStrictEqual({x: 400, y: 20});
+ });
+ });
});
diff --git a/packages/polaris-viz/src/components/TooltipWrapper/index.ts b/packages/polaris-viz/src/components/TooltipWrapper/index.ts
index 72dea45352..0c96727a0f 100644
--- a/packages/polaris-viz/src/components/TooltipWrapper/index.ts
+++ b/packages/polaris-viz/src/components/TooltipWrapper/index.ts
@@ -6,7 +6,11 @@ export type {
TooltipPositionOffset,
} from './types';
export {TooltipHorizontalOffset, TooltipVerticalOffset} from './types';
-export {getAlteredVerticalBarPosition, TOOLTIP_MARGIN} from './utilities';
+export {
+ getAlteredVerticalBarPosition,
+ getRightPosition,
+ TOOLTIP_MARGIN,
+} from './utilities';
export type {
AlteredPositionProps,
AlteredPositionReturn,