From cdbfbd9c65b639c5691ad088093975a791a6eea5 Mon Sep 17 00:00:00 2001 From: Matt Vickers Date: Mon, 3 Feb 2025 14:53:02 -0600 Subject: [PATCH] Broken --- packages/polaris-viz-core/src/types.ts | 1 + .../src/utilities/getColorVisionEventAttrs.ts | 1 + .../polaris-viz/src/components/Arc/Arc.tsx | 1 + .../ConicGradientWithStops.tsx | 29 +- .../src/components/DonutChart/Chart.tsx | 13 + .../src/components/LineChart/Chart.tsx | 30 +- .../TooltipWrapper/TooltipWrapperNext.tsx | 265 ++++++++++++++++++ .../components/TooltipAnimatedContainer.tsx | 74 +---- .../src/components/VerticalBarChart/Chart.tsx | 27 +- .../components/BarGroup/BarGroup.tsx | 34 ++- .../VerticalBarGroup/VerticalBarGroup.tsx | 4 + 11 files changed, 374 insertions(+), 105 deletions(-) create mode 100644 packages/polaris-viz/src/components/TooltipWrapper/TooltipWrapperNext.tsx diff --git a/packages/polaris-viz-core/src/types.ts b/packages/polaris-viz-core/src/types.ts index f17b076d7..dce7b3850 100644 --- a/packages/polaris-viz-core/src/types.ts +++ b/packages/polaris-viz-core/src/types.ts @@ -316,6 +316,7 @@ export enum InternalChartType { HorizontalBar = 'HorizontalBar', Combo = 'Combo', Line = 'Line', + Donut = 'Donut', } export enum Hue { diff --git a/packages/polaris-viz-core/src/utilities/getColorVisionEventAttrs.ts b/packages/polaris-viz-core/src/utilities/getColorVisionEventAttrs.ts index a71805489..7713aa37a 100644 --- a/packages/polaris-viz-core/src/utilities/getColorVisionEventAttrs.ts +++ b/packages/polaris-viz-core/src/utilities/getColorVisionEventAttrs.ts @@ -11,5 +11,6 @@ export function getColorVisionEventAttrs({type, index, watch = true}: Props) { [`${COLOR_VISION_EVENT.dataAttribute}-watch`]: watch, [`${COLOR_VISION_EVENT.dataAttribute}-type`]: type, [`${COLOR_VISION_EVENT.dataAttribute}-index`]: index, + ['data-tooltip-type']: 'blah', }; } diff --git a/packages/polaris-viz/src/components/Arc/Arc.tsx b/packages/polaris-viz/src/components/Arc/Arc.tsx index 1de745d92..27d866afa 100644 --- a/packages/polaris-viz/src/components/Arc/Arc.tsx +++ b/packages/polaris-viz/src/components/Arc/Arc.tsx @@ -140,6 +140,7 @@ export function Arc({ height={radius * 4} width={radius * 4} gradient={gradient} + index={index} /> diff --git a/packages/polaris-viz/src/components/ConicGradientWithStops/ConicGradientWithStops.tsx b/packages/polaris-viz/src/components/ConicGradientWithStops/ConicGradientWithStops.tsx index 2fc690129..9e42bd519 100644 --- a/packages/polaris-viz/src/components/ConicGradientWithStops/ConicGradientWithStops.tsx +++ b/packages/polaris-viz/src/components/ConicGradientWithStops/ConicGradientWithStops.tsx @@ -1,4 +1,5 @@ import type {GradientStop} from '@shopify/polaris-viz-core'; +import {Fragment} from 'react'; import {createCSSConicGradient} from '../../utilities'; @@ -16,18 +17,30 @@ export function ConicGradientWithStops({ width, x = 0, y = 0, + index, }: ConicGradientWithStopsProps) { const conicGradientValue = createCSSConicGradient(gradient); return ( - -
+ +
+ + - + ); } diff --git a/packages/polaris-viz/src/components/DonutChart/Chart.tsx b/packages/polaris-viz/src/components/DonutChart/Chart.tsx index 2776da65c..e066fb388 100644 --- a/packages/polaris-viz/src/components/DonutChart/Chart.tsx +++ b/packages/polaris-viz/src/components/DonutChart/Chart.tsx @@ -9,6 +9,8 @@ import { useChartContext, THIN_ARC_CORNER_THICKNESS, isInfinity, + InternalChartType, + DataType, } from '@shopify/polaris-viz-core'; import type { DataPoint, @@ -17,6 +19,7 @@ import type { Direction, } from '@shopify/polaris-viz-core'; +import {TooltipWrapperNext} from '../../components/TooltipWrapper/TooltipWrapperNext'; import {getAnimationDelayForItems} from '../../utilities/getAnimationDelayForItems'; import {getContainerAlignmentForLegend} from '../../utilities'; import type {ComparisonMetricProps} from '../ComparisonMetric'; @@ -84,6 +87,7 @@ export function Chart({ const chartId = useUniqueId('Donut'); const [activeIndex, setActiveIndex] = useState(-1); const selectedTheme = useTheme(); + const [svgRef, setSvgRef] = useState(null); const seriesCount = clamp({ amount: data.length, @@ -208,6 +212,7 @@ export function Chart({ viewBox={`${minX} ${minY} ${viewBoxDimensions.width} ${viewBoxDimensions.height}`} height={diameter} width={diameter} + ref={setSvgRef} > {isLegendMounted && ( @@ -300,6 +305,14 @@ export function Chart({ } /> )} + { + return
Tooltip {index}
; + }} + parentElement={svgRef} + />
); } diff --git a/packages/polaris-viz/src/components/LineChart/Chart.tsx b/packages/polaris-viz/src/components/LineChart/Chart.tsx index eb92914eb..79b10e55b 100644 --- a/packages/polaris-viz/src/components/LineChart/Chart.tsx +++ b/packages/polaris-viz/src/components/LineChart/Chart.tsx @@ -55,6 +55,7 @@ import {VisuallyHiddenRows} from '../VisuallyHiddenRows'; import {YAxis} from '../YAxis'; import {HorizontalGridLines} from '../HorizontalGridLines'; import {ChartElements} from '../ChartElements'; +import {TooltipWrapperNext} from '../../components/TooltipWrapper/TooltipWrapperNext'; import {useLineChartTooltipContent} from './hooks/useLineChartTooltipContent'; import {PointsAndCrosshair} from './components'; @@ -242,6 +243,8 @@ export function Chart({ const halfXAxisLabelWidth = xAxisDetails.labelWidth / 2; + console.log({longestSeriesLength}); + return ( { + return ( + + ); + })} + {data.map((singleSeries, index) => { if (singleSeries.metadata?.isVisuallyHidden === true) { return null; @@ -371,15 +392,11 @@ export function Chart({ {longestSeriesLength !== -1 && ( - { if (index != null && isPerformanceImpacted) { moveCrosshair(index); @@ -388,9 +405,6 @@ export function Chart({ } }} parentElement={svgRef} - usePortal - xScale={xScale} - yScale={yScale} /> )} diff --git a/packages/polaris-viz/src/components/TooltipWrapper/TooltipWrapperNext.tsx b/packages/polaris-viz/src/components/TooltipWrapper/TooltipWrapperNext.tsx new file mode 100644 index 000000000..c40cd96c5 --- /dev/null +++ b/packages/polaris-viz/src/components/TooltipWrapper/TooltipWrapperNext.tsx @@ -0,0 +1,265 @@ +import type {ReactNode} from 'react'; +import {useEffect, useRef, useState, useMemo, useCallback} from 'react'; +import {useChartContext, InternalChartType} from '@shopify/polaris-viz-core'; +import type {DataType} from '@shopify/polaris-viz-core'; +import {createPortal} from 'react-dom'; + +import {useRootContainer} from '../../hooks/useRootContainer'; +import {SwallowErrors} from '../SwallowErrors'; +import {TOOLTIP_ID} from '../../constants'; + +import type {TooltipPosition} from './types'; +import {TooltipAnimatedContainer} from './components/TooltipAnimatedContainer'; +import {getXYFromEventType} from './utilities/eventPoint'; + +const TOUCH_START_DELAY = 300; + +interface BaseProps { + chartType: InternalChartType; + focusElementDataType: DataType; + getMarkup: (index: number) => ReactNode; + parentElement: SVGSVGElement | null; + onIndexChange?: (index: number | null) => void; + id?: string; +} + +function TooltipWrapperRaw(props: BaseProps) { + const {chartType, focusElementDataType, id, onIndexChange, parentElement} = + props; + const {isTouchDevice} = useChartContext(); + const [position, setPosition] = useState({ + x: 0, + y: 0, + activeIndex: -1, + }); + + const activeIndexRef = useRef(null); + const touchStartTimer = useRef(0); + const isLongTouch = useRef(false); + + const focusElements = useMemo | undefined>(() => { + return parentElement?.querySelectorAll( + `[data-type="${focusElementDataType}"][aria-hidden="false"]`, + ); + }, [focusElementDataType, parentElement]); + + useEffect(() => { + activeIndexRef.current = position.activeIndex; + }, [position.activeIndex]); + + const alwaysUpdatePosition = + [InternalChartType.Line, InternalChartType.Donut].includes(chartType) && + !isTouchDevice; + + const getPosition = useCallback( + (event: MouseEvent | TouchEvent) => { + if (event == null || event.target == null) { + // console.log('no event or target', event); + return position; + } + + const target = event.target as HTMLElement; + + if (target.dataset.tooltipType == null) { + // console.log('no type', target); + return { + x: 0, + y: 0, + activeIndex: -1, + }; + } + + const activeIndex = target.dataset.tooltipIndex; + + let x = Number(target.dataset.tooltipX); + let y = Number(target.dataset.tooltipY); + + if (alwaysUpdatePosition) { + const {x: eventX, y: eventY} = getXYFromEventType(event); + + x = eventX; + y = eventY; + } + + if (y == null || x == null) { + // console.log('no x or y', target); + return position; + } + + // console.log('x or y', {x, y, activeIndex}); + + return { + x, + y, + activeIndex: + activeIndex == null ? position.activeIndex : Number(activeIndex), + }; + }, + [alwaysUpdatePosition, position], + ); + + const showAndPositionTooltip = useCallback( + (event: MouseEvent | TouchEvent) => { + const newPosition = getPosition(event); + + if ( + !alwaysUpdatePosition && + activeIndexRef.current === newPosition.activeIndex + ) { + return; + } + + setPosition(newPosition); + + if (newPosition.activeIndex != null) { + onIndexChange?.(newPosition.activeIndex); + } + }, + [alwaysUpdatePosition, getPosition, onIndexChange], + ); + + const onMouseMove = useCallback( + (event: MouseEvent | TouchEvent) => { + window.clearTimeout(touchStartTimer.current); + + if (typeof TouchEvent !== 'undefined' && event instanceof TouchEvent) { + if (isLongTouch.current === true) { + // prevents scrolling after long touch (since it is supposed to move the tooltip/datapoint vs scroll) + event?.preventDefault(); + } else { + return; + } + } + + showAndPositionTooltip(event); + }, + [showAndPositionTooltip], + ); + + const onMouseLeave = useCallback(() => { + isLongTouch.current = false; + window.clearTimeout(touchStartTimer.current); + onIndexChange?.(null); + setPosition((prevState) => { + return {...prevState, activeIndex: -1}; + }); + }, [onIndexChange]); + + const onTouchStart = useCallback( + (event: TouchEvent) => { + touchStartTimer.current = window.setTimeout(() => { + event.preventDefault(); + + isLongTouch.current = true; + + showAndPositionTooltip(event); + }, TOUCH_START_DELAY); + }, + [showAndPositionTooltip], + ); + + const onFocus = useCallback( + (event: FocusEvent) => { + // const target = event.currentTarget as SVGSVGElement; + // if (!target) { + // return; + // } + // const index = Number(target.dataset.index); + // const newPosition = getPosition({, eveindexntType: 'focus'}); + // setPosition(newPosition); + // onIndexChange?.(newPosition.activeIndex); + }, + [getPosition, onIndexChange], + ); + + const onFocusIn = useCallback(() => { + if (!parentElement?.contains(document.activeElement)) { + onMouseLeave(); + } + }, [parentElement, onMouseLeave]); + + const setFocusListeners = useCallback( + (attach: boolean) => { + if (!focusElements) { + return; + } + + focusElements.forEach((el: SVGPathElement) => { + if (attach) { + el.addEventListener('focus', onFocus); + } else { + el.removeEventListener('focus', onFocus); + } + }); + }, + [focusElements, onFocus], + ); + + useEffect(() => { + if (!parentElement) { + return; + } + + parentElement.addEventListener('mousemove', onMouseMove); + parentElement.addEventListener('mouseleave', onMouseLeave); + parentElement.addEventListener('touchstart', onTouchStart); + parentElement.addEventListener('touchmove', onMouseMove); + parentElement.addEventListener('touchend', onMouseLeave); + + setFocusListeners(true); + + return () => { + parentElement.removeEventListener('mousemove', onMouseMove); + parentElement.removeEventListener('mouseleave', onMouseLeave); + parentElement.removeEventListener('touchstart', onTouchStart); + parentElement.removeEventListener('touchmove', onMouseMove); + parentElement.removeEventListener('touchend', onMouseLeave); + + setFocusListeners(false); + }; + }, [ + parentElement, + onMouseLeave, + onTouchStart, + setFocusListeners, + onMouseMove, + ]); + + useEffect(() => { + document.addEventListener('focusin', onFocusIn); + + return () => { + document.removeEventListener('focusin', onFocusIn); + }; + }, [parentElement, onFocusIn]); + + if (position.activeIndex == null || position.activeIndex < 0) { + return null; + } + + return ( + + {props.getMarkup(position.activeIndex)} + + ); +} + +export function TooltipWrapperNext(props: BaseProps) { + return ; +} + +function TooltipWithPortal(props: BaseProps) { + const container = useRootContainer(TOOLTIP_ID); + + return createPortal( + + + , + container, + ); +} diff --git a/packages/polaris-viz/src/components/TooltipWrapper/components/TooltipAnimatedContainer.tsx b/packages/polaris-viz/src/components/TooltipWrapper/components/TooltipAnimatedContainer.tsx index a4017dece..fe7e3e113 100644 --- a/packages/polaris-viz/src/components/TooltipWrapper/components/TooltipAnimatedContainer.tsx +++ b/packages/polaris-viz/src/components/TooltipWrapper/components/TooltipAnimatedContainer.tsx @@ -1,84 +1,43 @@ import type {ReactNode} from 'react'; import {useEffect, useRef, useState, useMemo} from 'react'; -import type {BoundingRect, Dimensions} from '@shopify/polaris-viz-core'; -import {useChartContext, InternalChartType} from '@shopify/polaris-viz-core'; - -import type {Margin} from '../../../types'; -import type {TooltipPositionOffset} from '../types'; -import {DEFAULT_TOOLTIP_POSITION} from '../constants'; -import {getAlteredLineChartPosition} from '../utilities/getAlteredLineChartPosition'; -import {getAlteredHorizontalBarPosition} from '../utilities/getAlteredHorizontalBarPosition'; -import {getAlteredVerticalBarPosition} from '../utilities/getAlteredVerticalBarPosition'; +import type {Dimensions} from '@shopify/polaris-viz-core'; +import {useChartContext} from '@shopify/polaris-viz-core'; import styles from './TooltipAnimatedContainer.scss'; export interface TooltipAnimatedContainerProps { children: ReactNode; - margin: Margin; activePointIndex: number; currentX: number; currentY: number; - chartBounds: BoundingRect; - chartType: InternalChartType; - position?: TooltipPositionOffset; id?: string; - bandwidth?: number; } export function TooltipAnimatedContainer({ activePointIndex, - bandwidth = 0, - chartBounds, - chartType, children, currentX, currentY, id = '', - margin, - position = DEFAULT_TOOLTIP_POSITION, }: TooltipAnimatedContainerProps) { - const { - isPerformanceImpacted, - scrollContainer, - containerBounds, - isTouchDevice, - } = useChartContext(); + const {isPerformanceImpacted} = useChartContext(); const tooltipRef = useRef(null); const [tooltipDimensions, setTooltipDimensions] = useState(null); const firstRender = useRef(true); - const getAlteredPositionFunction = useMemo(() => { - switch (chartType) { - case InternalChartType.Line: - return getAlteredLineChartPosition; - case InternalChartType.HorizontalBar: - return getAlteredHorizontalBarPosition; - case InternalChartType.Bar: - default: - return getAlteredVerticalBarPosition; - } - }, [chartType]); - const {x, y, opacity, immediate} = useMemo(() => { if (tooltipDimensions == null) { return {x: 0, y: 0, opacity: 0}; } - const {x, y} = getAlteredPositionFunction({ - currentX, - currentY, - position, - tooltipDimensions, - chartBounds, - margin, - bandwidth, - isPerformanceImpacted, - isTouchDevice, - containerBounds, - scrollContainer, - }); + // const x = currentX - tooltipDimensions.width / 2; + // const y = currentY - tooltipDimensions.height; + const x = currentX; + const y = currentY; + + console.log({x, y}); const shouldRenderImmediate = firstRender.current; firstRender.current = false; @@ -89,20 +48,7 @@ export function TooltipAnimatedContainer({ opacity: 1, immediate: isPerformanceImpacted || shouldRenderImmediate, }; - }, [ - bandwidth, - chartBounds, - currentX, - currentY, - getAlteredPositionFunction, - margin, - position, - isPerformanceImpacted, - tooltipDimensions, - containerBounds, - scrollContainer, - isTouchDevice, - ]); + }, [currentX, currentY, isPerformanceImpacted, tooltipDimensions]); useEffect(() => { if (tooltipRef.current == null) { diff --git a/packages/polaris-viz/src/components/VerticalBarChart/Chart.tsx b/packages/polaris-viz/src/components/VerticalBarChart/Chart.tsx index 3e4015591..0c3cd71e6 100644 --- a/packages/polaris-viz/src/components/VerticalBarChart/Chart.tsx +++ b/packages/polaris-viz/src/components/VerticalBarChart/Chart.tsx @@ -18,7 +18,6 @@ import type { ChartType, XAxisOptions, YAxisOptions, - BoundingRect, LabelFormatter, } from '@shopify/polaris-viz-core'; import {stackOffsetDiverging, stackOrderNone} from 'd3-shape'; @@ -39,8 +38,8 @@ import {useFormattedLabels} from '../../hooks/useFormattedLabels'; import {XAxis} from '../XAxis'; import {LegendContainer, useLegend} from '../LegendContainer'; import {GradientDefs} from '../shared'; -import {ANNOTATIONS_LABELS_OFFSET, ChartMargin} from '../../constants'; -import {TooltipWrapper} from '../TooltipWrapper'; +import {ANNOTATIONS_LABELS_OFFSET} from '../../constants'; +import {TooltipWrapperNext} from '../TooltipWrapper/TooltipWrapperNext'; import {getStackedValues, getStackedMinMax} from '../../utilities'; import {YAxis} from '../YAxis'; import {HorizontalGridLines} from '../HorizontalGridLines'; @@ -194,16 +193,11 @@ export function Chart({ yAxisWidth: yAxisLabelWidth, }); + console.log({chartYPosition, containerBounds}); + const annotationsDrawableHeight = chartYPosition + drawableHeight + ANNOTATIONS_LABELS_OFFSET; - const chartBounds: BoundingRect = { - width, - height, - x: chartXPosition, - y: chartYPosition, - }; - const {sortedData, areAllNegative, xScale, gapWidth} = useVerticalBarChart({ data, drawableWidth, @@ -296,6 +290,8 @@ export function Chart({ yAxisOptions={yAxisOptions} yScale={yScale} areAllNegative={areAllNegative} + chartXPosition={chartXPosition} + chartYPosition={chartYPosition} /> @@ -334,20 +330,11 @@ export function Chart({ {sortedData.length > 0 && ( - )} diff --git a/packages/polaris-viz/src/components/VerticalBarChart/components/BarGroup/BarGroup.tsx b/packages/polaris-viz/src/components/VerticalBarChart/components/BarGroup/BarGroup.tsx index 7a54ddaa6..8692dd023 100644 --- a/packages/polaris-viz/src/components/VerticalBarChart/components/BarGroup/BarGroup.tsx +++ b/packages/polaris-viz/src/components/VerticalBarChart/components/BarGroup/BarGroup.tsx @@ -62,9 +62,12 @@ export function BarGroup({ gapWidth, theme, areAllNegative, + chartXPosition, + chartYPosition, }: BarGroupProps) { + console.log({chartXPosition}); const groupAriaLabel = formatAriaLabel(accessibilityData[barGroupIndex]); - const {id, isPerformanceImpacted} = useChartContext(); + const {id, isPerformanceImpacted, containerBounds} = useChartContext(); const selectedTheme = useTheme(theme); @@ -202,8 +205,12 @@ export function BarGroup({ width={barWidth * dataLength + gapWidth} x={x - gapWidth / 2} height={drawableHeight} - fill="transparent" + fill="blue" aria-hidden="true" + data-tooltip-type={DataType.Bar} + data-tooltip-index={barGroupIndex} + data-tooltip-x={containerBounds.x + chartXPosition + x - gapWidth / 2} + data-tooltip-y={containerBounds.y + chartYPosition} /> {data.map((rawValue, index) => { @@ -227,14 +234,17 @@ export function BarGroup({ position: 'vertical', }); + const rectX = x + barWidth * index; + const rectY = isNegative || areAllNegative ? y : y - offset; + return ( diff --git a/packages/polaris-viz/src/components/VerticalBarChart/components/VerticalBarGroup/VerticalBarGroup.tsx b/packages/polaris-viz/src/components/VerticalBarChart/components/VerticalBarGroup/VerticalBarGroup.tsx index b7dd7ed6b..b724144a4 100644 --- a/packages/polaris-viz/src/components/VerticalBarChart/components/VerticalBarGroup/VerticalBarGroup.tsx +++ b/packages/polaris-viz/src/components/VerticalBarChart/components/VerticalBarGroup/VerticalBarGroup.tsx @@ -50,6 +50,8 @@ export function VerticalBarGroup({ yScale, yAxisOptions, areAllNegative, + chartXPosition, + chartYPosition, }: VerticalBarGroupProps) { const {id: chartId, isPerformanceImpacted} = useChartContext(); @@ -138,6 +140,8 @@ export function VerticalBarGroup({ x={xPosition == null ? 0 : xPosition} yScale={yScale} areAllNegative={areAllNegative} + chartXPosition={chartXPosition} + chartYPosition={chartYPosition} /> ); })}