import type {ReactNode} from 'react'; import {useState, useMemo} from 'react'; import {line} from 'd3-shape'; import type { DataSeries, DataPoint, XAxisOptions, YAxisOptions, Dimensions, BoundingRect, } from '@shopify/polaris-viz-core'; import { useUniqueId, curveStepRounded, DataType, useYScale, COLOR_VISION_SINGLE_ITEM, useChartPositions, LINE_HEIGHT, } from '@shopify/polaris-viz-core'; import {ChartElements} from '../ChartElements'; import { Annotations, checkAvailableAnnotations, YAxisAnnotations, } from '../Annotations'; import type { AnnotationLookupTable, GetXPosition, RenderLegendContent, RenderTooltipContentData, } from '../../types'; import {XAxis} from '../XAxis'; import {LegendContainer, useLegend} from '../LegendContainer'; import type { TooltipPosition, TooltipPositionOffset, TooltipPositionParams, } from '../TooltipWrapper'; import { TooltipHorizontalOffset, TooltipVerticalOffset, TooltipWrapper, TOOLTIP_POSITION_DEFAULT_RETURN, } from '../TooltipWrapper'; import { useLinearChartAnimations, useTheme, useThemeSeriesColors, useColorVisionEvents, useLinearLabelsAndDimensions, } from '../../hooks'; import {ChartMargin, ANNOTATIONS_LABELS_OFFSET} from '../../constants'; import {eventPointNative} from '../../utilities'; import {YAxis} from '../YAxis'; import {Crosshair} from '../Crosshair'; import {VisuallyHiddenRows} from '../VisuallyHiddenRows'; import {HorizontalGridLines} from '../HorizontalGridLines'; import {useStackedData} from './hooks'; import {StackedAreas, Points} from './components'; import {useStackedChartTooltipContent} from './hooks/useStackedChartTooltipContent'; import {yAxisMinMax} from './utilities/yAxisMinMax'; import {getAlteredStackedAreaChartPosition} from './utilities/getAlteredStackedAreaChartPosition'; import styles from './Chart.scss'; const TOOLTIP_POSITION: TooltipPositionOffset = { horizontal: TooltipHorizontalOffset.Left, vertical: TooltipVerticalOffset.Center, }; export interface Props { annotationsLookupTable: AnnotationLookupTable; data: DataSeries[]; renderTooltipContent(data: RenderTooltipContentData): ReactNode; showLegend: boolean; theme: string; xAxisOptions: Required; yAxisOptions: Required; dimensions?: Dimensions; renderLegendContent?: RenderLegendContent; } export function Chart({ annotationsLookupTable, xAxisOptions, data, dimensions, renderLegendContent, renderTooltipContent, showLegend, theme, yAxisOptions, }: Props) { useColorVisionEvents(data.length > 1); const selectedTheme = useTheme(theme); const seriesColors = useThemeSeriesColors(data, selectedTheme); const [activePointIndex, setActivePointIndex] = useState(null); const [svgRef, setSvgRef] = useState(null); const [xAxisHeight, setXAxisHeight] = useState(LINE_HEIGHT); const [annotationsHeight, setAnnotationsHeight] = useState(0); const {legend, setLegendDimensions, height, width} = useLegend({ colors: seriesColors, data: [ { shape: 'Line', series: data, }, ], dimensions, showLegend, }); const tooltipId = useUniqueId('stackedAreaChart'); const hideXAxis = xAxisOptions.hide || selectedTheme.xAxis.hide; const { stackedValues, longestSeriesLength, labels: formattedLabels, } = useStackedData({ data, xAxisOptions, }); const zeroLineData = data.map((series) => ({ ...series, data: series.data.map((point) => ({...point, value: 0})), })); const {stackedValues: zeroLineValues} = useStackedData({ data: zeroLineData, xAxisOptions, }); const {minY, maxY} = yAxisMinMax(stackedValues); const yScaleOptions = { formatYAxisLabel: yAxisOptions.labelFormatter, integersOnly: yAxisOptions.integersOnly, max: maxY, min: minY, }; const {yAxisLabelWidth} = useYScale({ ...yScaleOptions, drawableHeight: height, verticalOverflow: selectedTheme.grid.verticalOverflow, }); const { drawableWidth, drawableHeight, chartXPosition, chartYPosition, xAxisBounds, yAxisBounds, } = useChartPositions({ annotationsHeight, height, width, xAxisHeight, yAxisWidth: yAxisLabelWidth, }); const {xAxisDetails, xScale, labels} = useLinearLabelsAndDimensions({ data, drawableWidth, hideXAxis, labels: formattedLabels, longestSeriesLength, }); const {ticks, yScale} = useYScale({ ...yScaleOptions, drawableHeight, verticalOverflow: selectedTheme.grid.verticalOverflow, }); const annotationsDrawableHeight = chartYPosition + drawableHeight + ANNOTATIONS_LABELS_OFFSET; const getTooltipMarkup = useStackedChartTooltipContent({ data, renderTooltipContent, seriesColors, }); const lineGenerator = useMemo(() => { const generator = line() .x((_, index) => (xScale == null ? 0 : xScale(index))) .y(({value}) => yScale(value ?? 0)) .defined(({value}) => value != null); if (selectedTheme.line.hasSpline) { generator.curve(curveStepRounded); } return generator; }, [xScale, yScale, selectedTheme.line.hasSpline]); const seriesForAnimation: DataSeries[] = useMemo(() => { return stackedValues.map((value) => { return { name: '', data: value.map((val) => { return { key: '', value: val[1], }; }), }; }); }, [stackedValues]); const {animatedCoordinates} = useLinearChartAnimations({ data: seriesForAnimation, lineGenerator, activeIndex: activePointIndex, }); const getXPosition: GetXPosition = ({isCrosshair, index}) => { if (xScale == null) { return 0; } const offset = isCrosshair ? selectedTheme.crossHair.width / 2 : 0; if ( index != null && animatedCoordinates != null && animatedCoordinates[index] != null && animatedCoordinates[index] ) { return animatedCoordinates[index].to((coord) => coord.x - offset); } return xScale(activePointIndex == null ? 0 : activePointIndex) - offset; }; if (xScale == null || drawableWidth == null || yAxisLabelWidth == null) { return null; } const chartBounds: BoundingRect = { width, height, x: chartXPosition, y: chartYPosition, }; const {hasXAxisAnnotations, hasYAxisAnnotations} = checkAvailableAnnotations( annotationsLookupTable, ); const halfXAxisLabelWidth = xAxisDetails.labelWidth / 2; return ( {hideXAxis ? null : ( )} {selectedTheme.grid.showHorizontalLines ? ( ) : null} {activePointIndex == null ? null : ( )} {hasXAxisAnnotations && ( )} {hasYAxisAnnotations && ( )} {longestSeriesLength !== -1 && ( setActivePointIndex(index)} parentRef={svgRef} /> )} {showLegend && ( )} ); function getTooltipPosition({ event, index, eventType, }: TooltipPositionParams): TooltipPosition { if (eventType === 'mouse' && event) { const point = eventPointNative(event!); if (point == null || xScale == null) { return TOOLTIP_POSITION_DEFAULT_RETURN; } const {svgX, svgY} = point; const closestIndex = Math.round(xScale.invert(svgX - chartXPosition)); return { x: svgX, y: svgY, position: TOOLTIP_POSITION, activeIndex: Math.min(longestSeriesLength, closestIndex), }; } else if (index != null) { return { x: xScale?.(index) ?? 0, y: 0, position: TOOLTIP_POSITION, activeIndex: index, }; } return TOOLTIP_POSITION_DEFAULT_RETURN; } }