import type {ReactNode} from 'react'; import {useState, useRef, Fragment} from 'react'; import { uniqueId, DataType, useYScale, LineSeries, COLOR_VISION_SINGLE_ITEM, clamp, DEFAULT_THEME_NAME, useChartPositions, useChartContext, LINE_HEIGHT, } from '@shopify/polaris-viz-core'; import type { XAxisOptions, YAxisOptions, LineChartDataSeriesWithDefaults, BoundingRect, } from '@shopify/polaris-viz-core'; import {useExternalHideEvents} from '../../hooks/ExternalEvents'; import {useIndexForLabels} from '../../hooks/useIndexForLabels'; import { Annotations, checkAvailableAnnotations, YAxisAnnotations, } from '../Annotations'; import type { AnnotationLookupTable, LineChartSlotProps, RenderLegendContent, RenderTooltipContentData, } from '../../types'; import {useFormattedLabels} from '../../hooks/useFormattedLabels'; import {XAxis} from '../XAxis'; import {useLegend, LegendContainer} from '../LegendContainer'; import type { TooltipPosition, TooltipPositionParams, } from '../../components/TooltipWrapper'; import { TooltipWrapper, TOOLTIP_POSITION_DEFAULT_RETURN, } from '../../components/TooltipWrapper'; import {eventPointNative} from '../../utilities'; import { useTheme, useColorVisionEvents, useWatchColorVisionEvents, useLinearLabelsAndDimensions, } from '../../hooks'; import { ChartMargin, ANNOTATIONS_LABELS_OFFSET, Y_AXIS_LABEL_OFFSET, CROSSHAIR_ID, } from '../../constants'; import {VisuallyHiddenRows} from '../VisuallyHiddenRows'; import {YAxis} from '../YAxis'; import {HorizontalGridLines} from '../HorizontalGridLines'; import {ChartElements} from '../ChartElements'; import {useLineChartTooltipContent} from './hooks/useLineChartTooltipContent'; import {PointsAndCrosshair} from './components'; import {useFormatData} from './hooks'; import {getAlteredLineChartPosition, yAxisMinMax} from './utilities'; export interface ChartProps { renderTooltipContent: (data: RenderTooltipContentData) => ReactNode; annotationsLookupTable: AnnotationLookupTable; data: LineChartDataSeriesWithDefaults[]; showLegend: boolean; xAxisOptions: Required; yAxisOptions: Required; dimensions?: BoundingRect; emptyStateText?: string; renderLegendContent?: RenderLegendContent; slots?: { chart?: (props: LineChartSlotProps) => JSX.Element; }; theme?: string; } export function Chart({ annotationsLookupTable, emptyStateText, data, dimensions, renderLegendContent, renderTooltipContent, showLegend = true, slots, theme = DEFAULT_THEME_NAME, xAxisOptions, yAxisOptions, }: ChartProps) { useColorVisionEvents(data.length > 1); const selectedTheme = useTheme(theme); const {isPerformanceImpacted} = useChartContext(); const [activeIndex, setActiveIndex] = useState(null); const [activeLineIndex, setActiveLineIndex] = useState(-1); const [xAxisHeight, setXAxisHeight] = useState(LINE_HEIGHT); const [annotationsHeight, setAnnotationsHeight] = useState(0); const {legend, setLegendDimensions, height, width} = useLegend({ data: [ { shape: 'Line', series: data, }, ], dimensions, showLegend, }); useWatchColorVisionEvents({ type: COLOR_VISION_SINGLE_ITEM, onIndexChange: ({detail}) => setActiveLineIndex(detail.index), }); const {hiddenIndexes: hiddenLineIndexes} = useExternalHideEvents(); const indexForLabels = useIndexForLabels(data); const {formattedLabels, unformattedLabels} = useFormattedLabels({ data: [data[indexForLabels]], labelFormatter: xAxisOptions.labelFormatter, }); const tooltipId = useRef(uniqueId('lineChart')); const [svgRef, setSvgRef] = useState(null); const emptyState = data.length === 0 || data.every((series) => series.data.length === 0); const {minY, maxY} = yAxisMinMax(data); const yScaleOptions = { formatYAxisLabel: yAxisOptions.labelFormatter, integersOnly: yAxisOptions.integersOnly, fixedWidth: yAxisOptions.fixedWidth, max: maxY, min: minY, useFittedDomain: yAxisOptions.useFittedDomain, shouldRoundUp: yAxisOptions.shouldRoundUp, }; const {yAxisLabelWidth} = useYScale({ ...yScaleOptions, drawableHeight: height, verticalOverflow: selectedTheme.grid.verticalOverflow, }); const {reversedSeries, longestSeriesLength, longestSeriesIndex} = useFormatData(data); const { drawableWidth, drawableHeight, chartXPosition, chartYPosition, xAxisBounds, yAxisBounds, } = useChartPositions({ annotationsHeight, height, width, xAxisHeight, yAxisWidth: yAxisLabelWidth, }); const hideXAxis = xAxisOptions.hide || selectedTheme.xAxis.hide; 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 = useLineChartTooltipContent({ data, renderTooltipContent, indexForLabels, hiddenIndexes: hiddenLineIndexes, }); if (xScale == null || drawableWidth == null || yAxisLabelWidth == null) { return null; } function getTooltipPosition({ event, index, eventType, }: TooltipPositionParams): TooltipPosition { if (eventType === 'mouse') { const point = eventPointNative(event!); if ( point == null || xScale == null || reversedSeries[longestSeriesIndex] == null ) { return TOOLTIP_POSITION_DEFAULT_RETURN; } const {svgX} = point; const closestIndex = Math.round(xScale.invert(svgX - chartXPosition)); const activeIndex = clamp({ amount: closestIndex, min: 0, max: reversedSeries[longestSeriesIndex].data.length - 1, }); return { x: (event as MouseEvent).pageX, y: (event as MouseEvent).pageY, activeIndex, }; } else { const activeIndex = index ?? 0; const x = xScale?.(activeIndex) ?? 0; return { x: x + (dimensions?.x ?? 0), y: dimensions?.y ?? 0, activeIndex, }; } } function moveCrosshair(index: number | null) { setActiveIndex(0); if (index == null) { return; } const crosshair = document.getElementById( `${tooltipId.current}-${CROSSHAIR_ID}`, ); if (crosshair == null) { return; } crosshair.setAttribute( 'x', `${xScale(index) - selectedTheme.crossHair.width / 2}`, ); } const chartBounds: BoundingRect = { width, height, x: dimensions?.x ?? chartXPosition, y: dimensions?.y ?? chartYPosition, }; const {hasXAxisAnnotations, hasYAxisAnnotations} = checkAvailableAnnotations( annotationsLookupTable, ); const halfXAxisLabelWidth = xAxisDetails.labelWidth / 2; return ( {hideXAxis ? null : ( )} {selectedTheme.grid.showHorizontalLines ? ( ) : null} {emptyState ? null : ( )} {slots?.chart?.({ yScale, xScale, drawableWidth, drawableHeight, })} {reversedSeries.map((singleSeries, index) => { return ( ); })} {hasXAxisAnnotations && ( )} {hasYAxisAnnotations && ( )} {longestSeriesLength !== -1 && ( { if (index != null && isPerformanceImpacted) { moveCrosshair(index); } else { setActiveIndex(index); } }} parentRef={svgRef} usePortal /> )} {showLegend && ( )} ); }