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,