Skip to content

Commit

Permalink
Merge pull request #1721 from Shopify/envex/scroll-container
Browse files Browse the repository at this point in the history
Add scrollContainer prop to charts that use portal for tooltips
  • Loading branch information
envex authored Sep 13, 2024
2 parents ce88a8e + a219372 commit 1d67d8e
Show file tree
Hide file tree
Showing 14 changed files with 131 additions and 11 deletions.
1 change: 1 addition & 0 deletions packages/polaris-viz-core/src/contexts/ChartContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface ChartContextValues {
shouldAnimate: boolean;
theme: string;
isPerformanceImpacted: boolean;
scrollContainer?: HTMLElement | null;
}

export const ChartContext = createContext<ChartContextValues>({
Expand Down
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 `scrollContainer` prop to charts that use a portal (`BarChart`, `LineChart`, `StackedAreaChart`) for their tooltips. This allows consumers to render charts in a scrollable container and still position the tooltips in the correct position.

## [14.8.0] - 2024-09-06

Expand Down
3 changes: 3 additions & 0 deletions packages/polaris-viz/src/components/BarChart/BarChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export type BarChartProps = {
yAxisOptions?: Partial<YAxisOptions>;
renderHiddenLegendLabel?: (count: number) => string;
renderBucketLegendLabel?: () => string;
scrollContainer?: HTMLElement | null;
} & ChartProps;

export function BarChart(props: BarChartProps) {
Expand Down Expand Up @@ -78,6 +79,7 @@ export function BarChart(props: BarChartProps) {
renderHiddenLegendLabel,
renderBucketLegendLabel,
seriesNameFormatter = (value) => `${value}`,
scrollContainer,
} = {
...DEFAULT_CHART_PROPS,
...props,
Expand Down Expand Up @@ -154,6 +156,7 @@ export function BarChart(props: BarChartProps) {
onError={onError}
theme={theme}
type={InternalChartType.Bar}
scrollContainer={scrollContainer}
>
{state !== ChartState.Success ? (
<ChartSkeleton state={state} errorText={errorText} theme={theme} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {Story} from '@storybook/react';
import type {BarChartProps} from '../../BarChart';
import {BarChart} from '../../BarChart';
import {META} from '../meta';
import {useState} from 'react';

export default {
...META,
Expand Down Expand Up @@ -40,7 +41,30 @@ const Template: Story<BarChartProps> = (args: BarChartProps) => {
);
};

const TemplateWithFrame: Story<BarChartProps> = (args: BarChartProps) => {
const [ref, setRef] = useState<HTMLDivElement | null>(null);

const props = {...args, scrollContainer: ref};

return (
<div style={{overflow: 'hidden', position: 'fixed', inset: 0}}>
<div style={{height: 100, background: 'black', width: '100%'}}></div>
<div style={{overflow: 'auto', height: '100vh'}} ref={setRef}>
<Card {...props} />
<div style={{height: 700, width: 10}} />
<div style={{display: 'flex', justifyContent: 'space-between'}}>
<Card {...props} />
<Card {...props} />
<Card {...props} />
</div>
</div>
</div>
);
};

export const ExternalTooltip: Story<BarChartProps> = Template.bind({});
export const ExternalTooltipWithFrame: Story<BarChartProps> =
TemplateWithFrame.bind({});

ExternalTooltip.args = {
data: [
Expand Down Expand Up @@ -71,3 +95,5 @@ ExternalTooltip.args = {
},
],
};

ExternalTooltipWithFrame.args = ExternalTooltip.args;
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ interface Props {
isAnimated: boolean;
theme: string;
onError?: ErrorBoundaryResponse;
scrollContainer?: HTMLElement | null;
sparkChart?: boolean;
skeletonType?: SkeletonType;
type?: InternalChartType;
Expand Down Expand Up @@ -55,6 +56,7 @@ export const ChartContainer = (props: Props) => {
characterWidthOffsets,
theme: printFriendlyTheme,
isPerformanceImpacted: dataTooBigToAnimate,
scrollContainer: props.scrollContainer,
};
}, [
id,
Expand All @@ -63,6 +65,7 @@ export const ChartContainer = (props: Props) => {
props.isAnimated,
props.theme,
dataTooBigToAnimate,
props.scrollContainer,
]);

const {chartContainer, grid} = useTheme(value.theme);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ interface ChartDimensionsProps {
children: ReactElement;
data: DataSeries[] | DataGroup[];
onIsPrintingChange: Dispatch<SetStateAction<boolean>>;
scrollContainer?: HTMLElement | null;
sparkChart?: boolean;
skeletonType?: SkeletonType;
onError?: ErrorBoundaryResponse;
Expand All @@ -33,6 +34,7 @@ export function ChartDimensions({
children,
data,
onIsPrintingChange,
scrollContainer,
sparkChart = false,
skeletonType = 'Default',
onError,
Expand Down Expand Up @@ -65,8 +67,11 @@ export function ChartDimensions({
const {width, height} = entry.contentRect;
const {x, y} = entry.target.getBoundingClientRect();

setChartDimensions({width, height, x, y: y + window.scrollY});
}, [entry, previousEntry?.contentRect]);
const scrollY =
scrollContainer == null ? window.scrollY : scrollContainer.scrollTop;

setChartDimensions({width, height, x, y: y + scrollY});
}, [entry, previousEntry?.contentRect, scrollContainer]);

const debouncedUpdateDimensions = useDebouncedCallback(() => {
updateDimensions();
Expand Down
3 changes: 3 additions & 0 deletions packages/polaris-viz/src/components/LineChart/LineChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export type LineChartProps = {
slots?: {
chart?: (props: LineChartSlotProps) => JSX.Element;
};
scrollContainer?: HTMLElement | null;
} & ChartProps;

export function LineChart(props: LineChartProps) {
Expand All @@ -75,6 +76,7 @@ export function LineChart(props: LineChartProps) {
tooltipOptions,
xAxisOptions,
yAxisOptions,
scrollContainer,
} = {
...DEFAULT_CHART_PROPS,
...props,
Expand Down Expand Up @@ -113,6 +115,7 @@ export function LineChart(props: LineChartProps) {
isAnimated={isAnimated}
type={InternalChartType.Line}
onError={onError}
scrollContainer={scrollContainer}
>
{state !== ChartState.Success ? (
<ChartSkeleton state={state} errorText={errorText} theme={theme} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {Story} from '@storybook/react';
import {LineChart, LineChartProps} from '../../LineChart';
import {META} from '../meta';
import {randomNumber} from '../../../Docs/utilities';
import {useState} from 'react';

export default {
...META,
Expand Down Expand Up @@ -54,7 +55,30 @@ const Template: Story<LineChartProps> = (args: LineChartProps) => {
);
};

const TemplateWithFrame: Story<LineChartProps> = (args: LineChartProps) => {
const [ref, setRef] = useState<HTMLDivElement | null>(null);

const props = {...args, scrollContainer: ref};

return (
<div style={{overflow: 'hidden', position: 'fixed', inset: 0}}>
<div style={{height: 100, background: 'black', width: '100%'}}></div>
<div style={{overflow: 'auto', height: '100vh'}} ref={setRef}>
<Card {...props} />
<div style={{height: 700, width: 10}} />
<div style={{display: 'flex', justifyContent: 'space-between'}}>
<Card {...props} data={HOURLY_DATA} />
<Card {...props} />
<Card {...props} />
</div>
</div>
</div>
);
};

export const ExternalTooltip: Story<LineChartProps> = Template.bind({});
export const ExternalTooltipWithFrame: Story<LineChartProps> =
TemplateWithFrame.bind({});

ExternalTooltip.args = {
data: [
Expand Down Expand Up @@ -92,3 +116,5 @@ ExternalTooltip.args = {
},
],
};

ExternalTooltipWithFrame.args = ExternalTooltip.args;
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface AlteredPositionProps {
margin: Margin;
position: TooltipPositionOffset;
tooltipDimensions: Dimensions;
scrollContainer?: HTMLElement | null;
}

export interface AlteredPositionReturn {
Expand All @@ -31,7 +32,7 @@ export type AlteredPosition = (
export function getAlteredLineChartPosition(
props: AlteredPositionProps,
): AlteredPositionReturn {
const {currentX, currentY, chartBounds} = props;
const {currentX, currentY, chartBounds, scrollContainer} = props;

let x = currentX;
let y = currentY;
Expand All @@ -41,7 +42,7 @@ export function getAlteredLineChartPosition(
//

if (props.isPerformanceImpacted) {
y = chartBounds.y ?? 0;
y = chartBounds.y - (scrollContainer?.scrollTop ?? 0) ?? 0;
}

//
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export type StackedAreaChartProps = {
yAxisOptions?: Partial<YAxisOptions>;
renderHiddenLegendLabel?: (count: number) => string;
seriesNameFormatter?: LabelFormatter;
scrollContainer?: HTMLElement | null;
} & ChartProps;

export function StackedAreaChart(props: StackedAreaChartProps) {
Expand All @@ -65,6 +66,7 @@ export function StackedAreaChart(props: StackedAreaChartProps) {
skipLinkText,
theme = defaultTheme,
renderHiddenLegendLabel,
scrollContainer,
} = {
...DEFAULT_CHART_PROPS,
...props,
Expand Down Expand Up @@ -99,6 +101,7 @@ export function StackedAreaChart(props: StackedAreaChartProps) {
id={id}
isAnimated={isAnimated}
onError={onError}
scrollContainer={scrollContainer}
>
{state !== ChartState.Success ? (
<ChartSkeleton state={state} errorText={errorText} theme={theme} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {StackedAreaChartProps} from '../../StackedAreaChart';
import {StackedAreaChart} from '../../StackedAreaChart';
import {META} from '../meta';
import {DEFAULT_DATA, DEFAULT_PROPS} from '../data';
import {useState} from 'react';

export default {
...META,
Expand Down Expand Up @@ -43,10 +44,37 @@ const Template: Story<StackedAreaChartProps> = (
);
};

const TemplateWithFrame: Story<StackedAreaChartProps> = (
args: StackedAreaChartProps,
) => {
const [ref, setRef] = useState<HTMLDivElement | null>(null);

const props = {...args, scrollContainer: ref};

return (
<div style={{overflow: 'hidden', position: 'fixed', inset: 0}}>
<div style={{height: 100, background: 'black', width: '100%'}}></div>
<div style={{overflow: 'auto', height: '100vh'}} ref={setRef}>
<Card {...props} />
<div style={{height: 700, width: 10}} />
<div style={{display: 'flex', justifyContent: 'space-between'}}>
<Card {...props} />
<Card {...props} />
<Card {...props} />
</div>
</div>
</div>
);
};

export const ExternalTooltipPortal: Story<StackedAreaChartProps> =
Template.bind({});
export const ExternalTooltipWithFrame: Story<StackedAreaChartProps> =
TemplateWithFrame.bind({});

ExternalTooltipPortal.args = {
...DEFAULT_PROPS,
data: DEFAULT_DATA,
};

ExternalTooltipWithFrame.args = ExternalTooltipPortal.args;
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type {ReactNode} from 'react';
import {useEffect, useRef, useState, useMemo, useCallback} from 'react';
import {useChartContext} from '@shopify/polaris-viz-core';
import type {DataType, BoundingRect} from '@shopify/polaris-viz-core';
import {createPortal} from 'react-dom';

Expand Down Expand Up @@ -43,6 +44,7 @@ function TooltipWrapperRaw(props: BaseProps) {
parentRef,
chartDimensions,
} = props;
const {scrollContainer} = useChartContext();
const [position, setPosition] = useState<TooltipPosition>({
x: 0,
y: 0,
Expand All @@ -65,9 +67,12 @@ function TooltipWrapperRaw(props: BaseProps) {
(event: MouseEvent | TouchEvent) => {
const newPosition = getPosition({event, eventType: 'mouse'});

const scrollContainerTop = Number(scrollContainer?.scrollTop ?? 0);
const y = newPosition.y + scrollContainerTop;

if (
alwaysUpdatePosition &&
(newPosition.x < chartBounds.x || newPosition.y < chartBounds.y)
(newPosition.x < chartBounds.x || y < chartBounds.y)
) {
return;
}
Expand All @@ -86,7 +91,13 @@ function TooltipWrapperRaw(props: BaseProps) {
setPosition(newPosition);
onIndexChange?.(newPosition.activeIndex);
},
[alwaysUpdatePosition, chartBounds, getPosition, onIndexChange],
[
alwaysUpdatePosition,
chartBounds,
getPosition,
onIndexChange,
scrollContainer,
],
);

const onMouseLeave = useCallback(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export function TooltipAnimatedContainer({
position = DEFAULT_TOOLTIP_POSITION,
chartDimensions,
}: TooltipAnimatedContainerProps) {
const {isPerformanceImpacted} = useChartContext();
const {isPerformanceImpacted, scrollContainer} = useChartContext();

const tooltipRef = useRef<HTMLDivElement | null>(null);
const [tooltipDimensions, setTooltipDimensions] =
Expand All @@ -60,6 +60,7 @@ export function TooltipAnimatedContainer({
bandwidth,
isPerformanceImpacted,
chartDimensions,
scrollContainer,
});

const shouldRenderImmediate = firstRender.current;
Expand All @@ -82,6 +83,7 @@ export function TooltipAnimatedContainer({
isPerformanceImpacted,
tooltipDimensions,
chartDimensions,
scrollContainer,
]);

useEffect(() => {
Expand Down
Loading

0 comments on commit 1d67d8e

Please sign in to comment.