diff --git a/packages/polaris-viz-core/src/components/LineSeries/LineSeries.tsx b/packages/polaris-viz-core/src/components/LineSeries/LineSeries.tsx index 850d89ec29..746ead68c3 100644 --- a/packages/polaris-viz-core/src/components/LineSeries/LineSeries.tsx +++ b/packages/polaris-viz-core/src/components/LineSeries/LineSeries.tsx @@ -42,13 +42,15 @@ export function LineSeries({ activeLineIndex = -1, data, hiddenIndexes = [], - index = 0, + index: lineSeriesIndex = 0, svgDimensions, theme, type = 'default', xScale, yScale, }: LineSeriesProps) { + const index = data?.metadata?.relatedIndex ?? lineSeriesIndex; + const { // eslint-disable-next-line id-length components: {Defs, Mask, G, Rect, Path}, diff --git a/packages/polaris-viz/src/components/ComboChart/components/ComboLineChart/ComboLineChart.tsx b/packages/polaris-viz/src/components/ComboChart/components/ComboLineChart/ComboLineChart.tsx index 3fc0909fea..abc6cd9202 100644 --- a/packages/polaris-viz/src/components/ComboChart/components/ComboLineChart/ComboLineChart.tsx +++ b/packages/polaris-viz/src/components/ComboChart/components/ComboLineChart/ComboLineChart.tsx @@ -39,7 +39,7 @@ export function ComboLineChart({ const dataWithDefaults = getLineChartDataWithDefaults(data.series, colors); - const {reversedSeries, longestSeriesIndex} = useFormatData(dataWithDefaults); + const {longestSeriesIndex} = useFormatData(dataWithDefaults); return ( @@ -60,10 +60,10 @@ export function ComboLineChart({ })} { + {data.map((singleSeries, index) => { return ( { + const filledData = fillMissingDataPoints(dataSeries); + const dataWithComparisonFirst = moveComparisonToStartOfArray(filledData); + + return dataWithComparisonFirst; + }, [dataSeries]); const selectedTheme = useTheme(theme); const seriesColors = useThemeSeriesColors(data, selectedTheme); @@ -126,3 +132,7 @@ export function LineChart(props: LineChartProps) { ); } + +function moveComparisonToStartOfArray(data: DataSeries[]) { + return [...data.sort((_, dataSeries) => (dataSeries.isComparison ? 1 : -1))]; +} diff --git a/packages/polaris-viz/src/components/LineChart/components/Points/Points.tsx b/packages/polaris-viz/src/components/LineChart/components/Points/Points.tsx index 2b66613251..5e950b0e70 100644 --- a/packages/polaris-viz/src/components/LineChart/components/Points/Points.tsx +++ b/packages/polaris-viz/src/components/LineChart/components/Points/Points.tsx @@ -64,10 +64,10 @@ export function Points({ return ( - {data.map((singleSeries, index) => { - const unreversedIndex = data.length - 1 - index; + {data.map((singleSeries, seriesIndex) => { + const index = singleSeries.metadata?.relatedIndex ?? seriesIndex; - if (hiddenIndexes.includes(unreversedIndex)) { + if (hiddenIndexes.includes(index)) { return null; } @@ -85,11 +85,13 @@ export function Points({ : singleData[activeIndex ?? -1]?.value != null; const isLineActive = - activeLineIndex !== -1 && activeLineIndex !== unreversedIndex; + activeLineIndex !== -1 && activeLineIndex !== index; - const hidePoint = + const isPointVisuallyHidden = !hasValidData || animatedCoordinates == null || isLineActive; + const isPointActive = hasValidData && activeIndex != null; + const pointColor = isGradientType(color) ? `url(#${pointGradientId})` : changeColorOpacity(color); @@ -112,11 +114,11 @@ export function Points({ color={pointColor} cx={getXPosition({isCrosshair: false})} cy={animatedYPosition} - active={hasValidData && activeIndex != null} + active={isPointActive} index={index} tabIndex={-1} isAnimated={shouldAnimate} - visuallyHidden={hidePoint} + visuallyHidden={isPointVisuallyHidden} ariaHidden /> ) : null} @@ -131,7 +133,7 @@ export function Points({ key={`${name}-${index}-${dataIndex}`} style={getColorVisionStylesForActiveIndex({ activeIndex: activeLineIndex, - index: data.length - 1 - index, + index, fadedOpacity: 0, })} > diff --git a/packages/polaris-viz/src/components/LineChart/components/PointsAndCrosshair/PointsAndCrosshair.tsx b/packages/polaris-viz/src/components/LineChart/components/PointsAndCrosshair/PointsAndCrosshair.tsx index 7d31898a66..0dfb0ba437 100644 --- a/packages/polaris-viz/src/components/LineChart/components/PointsAndCrosshair/PointsAndCrosshair.tsx +++ b/packages/polaris-viz/src/components/LineChart/components/PointsAndCrosshair/PointsAndCrosshair.tsx @@ -19,24 +19,24 @@ import {Points} from '../Points'; interface PointsAndCrosshairProps { activeIndex: number | null; + data: LineChartDataSeriesWithDefaults[]; drawableHeight: number; emptyState: boolean; + hiddenIndexes?: number[]; longestSeriesIndex: number; - reversedSeries: LineChartDataSeriesWithDefaults[]; theme: string; tooltipId: string; xScale: ScaleLinear; yScale: ScaleLinear; - hiddenIndexes?: number[]; } export function PointsAndCrosshair({ activeIndex, + data, drawableHeight, emptyState, hiddenIndexes = [], longestSeriesIndex, - reversedSeries, theme, tooltipId, xScale, @@ -59,7 +59,7 @@ export function PointsAndCrosshair({ }, [selectedTheme.line.hasSpline, xScale, yScale]); const {animatedCoordinates} = useLinearChartAnimations({ - data: reversedSeries, + data, lineGenerator, activeIndex, }); @@ -97,7 +97,7 @@ export function PointsAndCrosshair({ data.slice().reverse(), [data]); - const longestSeriesIndex = useMemo( () => - reversedSeries.reduce((maxIndex, currentSeries, currentIndex) => { - return reversedSeries[maxIndex].data.length < currentSeries.data.length + data.reduce((maxIndex, currentSeries, currentIndex) => { + return data[maxIndex].data.length < currentSeries.data.length ? currentIndex : maxIndex; }, 0), - [reversedSeries], + [data], ); - const longestSeriesLength = reversedSeries[longestSeriesIndex] - ? reversedSeries[longestSeriesIndex].data.length - 1 + const longestSeriesLength = data[longestSeriesIndex] + ? data[longestSeriesIndex].data.length - 1 : 0; - return {reversedSeries, longestSeriesLength, longestSeriesIndex}; + return {longestSeriesLength, longestSeriesIndex}; } diff --git a/packages/polaris-viz/src/components/LineChartPredictive/LineChartPredictive.tsx b/packages/polaris-viz/src/components/LineChartPredictive/LineChartPredictive.tsx index 57fc7a04c5..371ca58dee 100644 --- a/packages/polaris-viz/src/components/LineChartPredictive/LineChartPredictive.tsx +++ b/packages/polaris-viz/src/components/LineChartPredictive/LineChartPredictive.tsx @@ -1,14 +1,18 @@ +import type {DataSeries} from '@shopify/polaris-viz-core'; import { DEFAULT_CHART_PROPS, DEFAULT_THEME_NAME, useTheme, useThemeSeriesColors, } from '@shopify/polaris-viz-core'; +import {useMemo} from 'react'; +import type {RenderTooltipContentData} from 'types'; import {LineChart} from '../LineChart'; import type {LineChartPredictiveProps} from './types'; -import {CustomLegend, PredictiveLineSeries} from './components'; +import {CustomLegend, PredictiveLinePoints} from './components'; +import {renderLinearPredictiveTooltipContent} from './utilities/renderLinearPredictiveTooltipContent'; export function LineChartPredictive(props: LineChartPredictiveProps) { const { @@ -22,7 +26,7 @@ export function LineChartPredictive(props: LineChartPredictiveProps) { skipLinkText, state, theme, - tooltipOptions, + tooltipOptions: initialTooltipOptions, xAxisOptions, yAxisOptions, } = { @@ -41,19 +45,44 @@ export function LineChartPredictive(props: LineChartPredictiveProps) { } } + const selectedTheme = useTheme(theme); + const seriesColors = useThemeSeriesColors(nonPredictiveData, selectedTheme); + const predictiveSeriesNames = predictiveData .map(({metadata}) => { return data[metadata?.relatedIndex ?? -1].name; }) .filter((value) => value != null) as string[]; - const selectedTheme = useTheme(theme); - const seriesColors = useThemeSeriesColors(nonPredictiveData, selectedTheme); + const dataWithColors: DataSeries[] = []; + let index = -1; + + for (const series of data) { + if (series.metadata?.relatedIndex == null) { + index += 1; + } + + dataWithColors.push({ + ...series, + color: seriesColors[index], + }); + } + + const tooltipOptions = useMemo(() => { + function renderTooltipContent(tooltipData: RenderTooltipContentData) { + return renderLinearPredictiveTooltipContent(tooltipData); + } + + return { + ...initialTooltipOptions, + renderTooltipContent, + }; + }, [initialTooltipOptions]); return ( { return ( - ); diff --git a/packages/polaris-viz/src/components/LineChartPredictive/components/CustomLegend/CustomLegend.scss b/packages/polaris-viz/src/components/LineChartPredictive/components/CustomLegend/CustomLegend.scss index d1e05d3d4d..7900c36491 100644 --- a/packages/polaris-viz/src/components/LineChartPredictive/components/CustomLegend/CustomLegend.scss +++ b/packages/polaris-viz/src/components/LineChartPredictive/components/CustomLegend/CustomLegend.scss @@ -4,11 +4,3 @@ flex-wrap: wrap; list-style: none; } - -.IconContainer { - display: flex; - align-items: center; - justify-items: center; - height: 12px; - width: 20px; -} diff --git a/packages/polaris-viz/src/components/LineChartPredictive/components/CustomLegend/CustomLegend.tsx b/packages/polaris-viz/src/components/LineChartPredictive/components/CustomLegend/CustomLegend.tsx index f577a7b092..b962237da6 100644 --- a/packages/polaris-viz/src/components/LineChartPredictive/components/CustomLegend/CustomLegend.tsx +++ b/packages/polaris-viz/src/components/LineChartPredictive/components/CustomLegend/CustomLegend.tsx @@ -1,25 +1,13 @@ -import { - LinearGradientWithStops, - changeGradientOpacity, - isGradientType, - uniqueId, -} from '@shopify/polaris-viz-core'; -import type {Color} from '@shopify/polaris-viz-core'; -import {useMemo} from 'react'; - import type {LineChartPredictiveDataSeries} from '../../../../components/LineChartPredictive/types'; import type {ColorVisionInteractionMethods} from '../../../../types'; -import {getLineChartDataWithDefaults} from '../../../../utilities/getLineChartDataWithDefaults'; import {LegendItem} from '../../../../components/Legend'; +import {SeriesIcon} from '../SeriesIcon'; import styles from './CustomLegend.scss'; interface Props extends ColorVisionInteractionMethods { data: LineChartPredictiveDataSeries[]; predictiveSeriesNames: string[]; - getColorVisionEventAttrs: any; - getColorVisionStyles: any; - seriesColors: Color[]; theme: string; } @@ -28,20 +16,17 @@ export function CustomLegend({ predictiveSeriesNames, getColorVisionEventAttrs, getColorVisionStyles, - seriesColors, theme, }: Props) { - const id = useMemo(() => uniqueId('CustomLegen'), []); - - const dataWithDefaults = getLineChartDataWithDefaults(data, seriesColors); - return ( - {dataWithDefaults.map(({color, name, isComparison}, index) => { - const gradientId = `${id}-${index}`; + {data.map(({color, name, isComparison, metadata}, index) => { + if (metadata?.isPredictive) { + return null; + } function renderSeriesIcon() { - return ; + return ; } return ( @@ -71,35 +56,3 @@ export function CustomLegend({ ); } - -function SeriesIcon({color, gradientId}: {color: Color; gradientId: string}) { - return ( - - - - {isGradientType(color) ? ( - - - - ) : null} - - - ); -} diff --git a/packages/polaris-viz/src/components/LineChartPredictive/components/PredictiveLineSeries/PredictiveLineSeries.tsx b/packages/polaris-viz/src/components/LineChartPredictive/components/PredictiveLinePoints/PredictiveLinePoints.tsx similarity index 64% rename from packages/polaris-viz/src/components/LineChartPredictive/components/PredictiveLineSeries/PredictiveLineSeries.tsx rename to packages/polaris-viz/src/components/LineChartPredictive/components/PredictiveLinePoints/PredictiveLinePoints.tsx index b4eec68f88..2ae6c58b5e 100644 --- a/packages/polaris-viz/src/components/LineChartPredictive/components/PredictiveLineSeries/PredictiveLineSeries.tsx +++ b/packages/polaris-viz/src/components/LineChartPredictive/components/PredictiveLinePoints/PredictiveLinePoints.tsx @@ -1,7 +1,5 @@ -import type {Color} from '@shopify/polaris-viz-core'; import { COLOR_VISION_SINGLE_ITEM, - LineSeries, LinearGradientWithStops, changeColorOpacity, changeGradientOpacity, @@ -11,46 +9,42 @@ import { import {Fragment, useMemo, useState} from 'react'; import type {LineChartSlotProps} from 'types'; -import {Point} from '../../../../components/Point'; +import {Point} from '../../../Point'; import {useWatchColorVisionEvents} from '../../../../hooks'; -import {getLineChartDataWithDefaults} from '../../../../utilities/getLineChartDataWithDefaults'; import type {LineChartPredictiveProps} from '../../types'; -interface PredictiveLinesProps extends LineChartSlotProps { +interface PredictiveLinePointsProps extends LineChartSlotProps { data: LineChartPredictiveProps['data']; - seriesColors: Color[]; - theme: string; } -export function PredictiveLineSeries({ +export function PredictiveLinePoints({ data, - drawableHeight, - drawableWidth, - seriesColors, - theme, xScale, yScale, -}: PredictiveLinesProps) { +}: PredictiveLinePointsProps) { const [activeLineIndex, setActiveLineIndex] = useState(-1); - const id = useMemo(() => uniqueId('PredictiveLines'), []); + const id = useMemo(() => uniqueId('PredictiveLinePoints'), []); useWatchColorVisionEvents({ type: COLOR_VISION_SINGLE_ITEM, onIndexChange: ({detail}) => setActiveLineIndex(detail.index), }); - const dataWithDefaults = getLineChartDataWithDefaults(data, seriesColors); - return ( - {dataWithDefaults.map((series, index) => { + {data.map((series, seriesIndex) => { + if (series.metadata?.isPredictive == null) { + return false; + } + + const index = series.metadata?.relatedIndex ?? seriesIndex; const pointGradientId = `${id}-point-${index}`; const predictiveStartIndex = series.data.findIndex( ({key}) => key === series.metadata?.startKey, ); - const color = series.color; + const color = series.color!; const pointColor = isGradientType(color) ? `url(#${pointGradientId})` @@ -58,18 +52,6 @@ export function PredictiveLineSeries({ return ( - {isGradientType(color) ? ( uniqueId('SeriesIcon'), []); + + return ( + + + + {isGradientType(color) ? ( + + + + ) : null} + + + ); +} diff --git a/packages/polaris-viz/src/components/LineChartPredictive/components/SeriesIcon/index.ts b/packages/polaris-viz/src/components/LineChartPredictive/components/SeriesIcon/index.ts new file mode 100644 index 0000000000..67ffefcd7e --- /dev/null +++ b/packages/polaris-viz/src/components/LineChartPredictive/components/SeriesIcon/index.ts @@ -0,0 +1 @@ +export {SeriesIcon} from './SeriesIcon'; diff --git a/packages/polaris-viz/src/components/LineChartPredictive/components/index.ts b/packages/polaris-viz/src/components/LineChartPredictive/components/index.ts index 62b896d03d..fe18bba2b1 100644 --- a/packages/polaris-viz/src/components/LineChartPredictive/components/index.ts +++ b/packages/polaris-viz/src/components/LineChartPredictive/components/index.ts @@ -1,2 +1,3 @@ -export {PredictiveLineSeries} from './PredictiveLineSeries'; +export {PredictiveLinePoints} from './PredictiveLinePoints'; export {CustomLegend} from './CustomLegend'; +export {SeriesIcon} from './SeriesIcon'; diff --git a/packages/polaris-viz/src/components/LineChartPredictive/stories/Default.stories.tsx b/packages/polaris-viz/src/components/LineChartPredictive/stories/Default.stories.tsx index bb81e4c9b8..c074c92228 100644 --- a/packages/polaris-viz/src/components/LineChartPredictive/stories/Default.stories.tsx +++ b/packages/polaris-viz/src/components/LineChartPredictive/stories/Default.stories.tsx @@ -3,7 +3,7 @@ import type {Story} from '@storybook/react'; export {META as default} from './meta'; import {DEFAULT_DATA, DEFAULT_PROPS, Template} from './data'; -import type {LineChartProps} from 'components/LineChart/LineChart'; +import type {LineChartProps} from '../../LineChart/LineChart'; export const Default: Story = Template.bind({}); diff --git a/packages/polaris-viz/src/components/LineChartPredictive/stories/playground/SeriesColors.stories.tsx b/packages/polaris-viz/src/components/LineChartPredictive/stories/playground/SeriesColors.stories.tsx new file mode 100644 index 0000000000..56c989b490 --- /dev/null +++ b/packages/polaris-viz/src/components/LineChartPredictive/stories/playground/SeriesColors.stories.tsx @@ -0,0 +1,87 @@ +import type {Story} from '@storybook/react'; + +export {META as default} from '../meta'; + +import {DEFAULT_PROPS, Template} from '../data'; +import type {LineChartProps} from '../../../LineChart/LineChart'; +import {generateDayRange, randomNumber} from '../../../Docs/utilities'; +import type {DataSeries} from '@shopify/polaris-viz-core'; + +export const SeriesColors: Story = Template.bind({}); + +const data = generateMultipleSeries(13); + +SeriesColors.args = { + ...DEFAULT_PROPS, + data, + isAnimated: false, + showLegend: true, +}; + +function generateMultipleSeries(quantity: number, dataSetLength = 10) { + const dataSeries: DataSeries[] = []; + + for (const [index] of Array(quantity).fill(null).entries()) { + const data = generateDataSet(dataSetLength); + + dataSeries.push({ + name: `Series ${index + 1}`, + data: alterData(data, false), + }); + + dataSeries.push({ + name: `Predictive ${index + 1}`, + data: alterData(data, true), + metadata: { + isPredictive: true, + startKey: data[5].key, + }, + styleOverride: { + line: { + strokeDasharray: '1 10 1', + hasArea: false, + }, + }, + }); + } + + for (const [index, series] of dataSeries.entries()) { + if (series?.metadata == null) { + continue; + } + + if (series.metadata.isPredictive) { + series.metadata.relatedIndex = index - 1; + } + } + + return dataSeries; +} + +function alterData(data, isPredictive) { + const half = data.length / 2; + + return [...data].map((series, index) => { + const isNull = + ((isPredictive && index < half) || (!isPredictive && index > half)) ?? + false; + + return { + ...series, + value: isNull ? null : series.value, + }; + }); +} + +function generateDataSet(dataLength: number) { + const dates = generateDayRange(dataLength); + + return Array(dataLength) + .fill(null) + .map((_, index) => { + return { + value: randomNumber(20, 50), + key: dates[index], + }; + }); +} diff --git a/packages/polaris-viz/src/components/LineChartPredictive/utilities/Styles.scss b/packages/polaris-viz/src/components/LineChartPredictive/utilities/Styles.scss new file mode 100644 index 0000000000..a72dfb67e5 --- /dev/null +++ b/packages/polaris-viz/src/components/LineChartPredictive/utilities/Styles.scss @@ -0,0 +1,6 @@ +.Icon { + display: flex; + align-items: center; + justify-content: center; + margin-right: 4px; +} diff --git a/packages/polaris-viz/src/components/LineChartPredictive/utilities/renderLinearPredictiveTooltipContent.tsx b/packages/polaris-viz/src/components/LineChartPredictive/utilities/renderLinearPredictiveTooltipContent.tsx new file mode 100644 index 0000000000..4432b9b75c --- /dev/null +++ b/packages/polaris-viz/src/components/LineChartPredictive/utilities/renderLinearPredictiveTooltipContent.tsx @@ -0,0 +1,90 @@ +import type {ReactNode} from 'react'; +import {Fragment} from 'react'; + +import {PREVIEW_ICON_SIZE} from '../../../constants'; +import { + TooltipContentContainer, + TooltipTitle, + TooltipRow, + LinePreview, +} from '../../'; +import type {RenderTooltipContentData} from '../../../types'; +import {SeriesIcon} from '../components'; + +import styles from './Styles.scss'; + +export function renderLinearPredictiveTooltipContent( + tooltipData: RenderTooltipContentData, +): ReactNode { + const {theme} = tooltipData; + + const formatters = { + keyFormatter: (key) => `${key}`, + valueFormatter: (value) => `${value}`, + titleFormatter: (title) => `${title}`, + ...tooltipData.formatters, + }; + + function renderSeriesIcon(color, isComparison): ReactNode { + return ( + + {isComparison ? ( + + ) : ( + + )} + + ); + } + + function renderContent({ + activeColorVisionIndex, + }: { + activeColorVisionIndex: number; + }) { + const item = tooltipData.data[0]; + + return item.data.map(({color, key, value, isComparison}, seriesIndex) => { + const metadata = tooltipData.dataSeries[seriesIndex].metadata; + const activeKey = + tooltipData.dataSeries[seriesIndex].data[tooltipData.activeIndex].key; + const index = metadata?.relatedIndex ?? seriesIndex; + + const isNull = value == null; + const isPredictiveStartKey = metadata?.startKey === activeKey; + const isHidden = isNull || isPredictiveStartKey; + + return ( + renderSeriesIcon(color, isComparison)} + shape="Line" + value={formatters.valueFormatter(value ?? 0)} + /> + ); + }); + } + + return ( + + {({activeColorVisionIndex}) => ( + + {tooltipData.title != null && ( + + {formatters.titleFormatter(tooltipData.title)} + + )} + {renderContent({activeColorVisionIndex})} + + )} + + ); +} diff --git a/packages/polaris-viz/src/components/StackedAreaChart/components/StackedAreas/StackedAreas.tsx b/packages/polaris-viz/src/components/StackedAreaChart/components/StackedAreas/StackedAreas.tsx index a90b52916e..7b87bfddfc 100644 --- a/packages/polaris-viz/src/components/StackedAreaChart/components/StackedAreas/StackedAreas.tsx +++ b/packages/polaris-viz/src/components/StackedAreaChart/components/StackedAreas/StackedAreas.tsx @@ -95,7 +95,7 @@ export function StackedAreas({ return ( ', () => { const stacks = stackedArea.findAll(AnimatedArea); expect(stacks[0]).toHaveReactProps({ - animationIndex: 1, + animationIndex: 0, index: 0, }); expect(stacks[1]).toHaveReactProps({ - animationIndex: 0, + animationIndex: 1, index: 1, }); }); diff --git a/packages/polaris-viz/src/components/TooltipContent/components/TooltipRow/TooltipRow.tsx b/packages/polaris-viz/src/components/TooltipContent/components/TooltipRow/TooltipRow.tsx index 4d4e2ba0e9..d335cbfb4e 100644 --- a/packages/polaris-viz/src/components/TooltipContent/components/TooltipRow/TooltipRow.tsx +++ b/packages/polaris-viz/src/components/TooltipContent/components/TooltipRow/TooltipRow.tsx @@ -48,7 +48,7 @@ export function TooltipRow({ })} > {color != null && ( - + {renderSeriesIcon?.() ?? (