diff --git a/packages/polaris-viz-core/src/constants.ts b/packages/polaris-viz-core/src/constants.ts index 00478185f..b4c893c0c 100644 --- a/packages/polaris-viz-core/src/constants.ts +++ b/packages/polaris-viz-core/src/constants.ts @@ -355,6 +355,10 @@ export const LIGHT_THEME: Theme = { missingData: { lineColor: variables.colorGray40, }, + badge: { + backgroundColor: variables.colorGray60, + textColor: variables.colorGray150, + }, }; export const PRINT_THEME = { diff --git a/packages/polaris-viz/src/components/Annotations/components/AnnotationContent/AnnotationContent.tsx b/packages/polaris-viz/src/components/Annotations/components/AnnotationContent/AnnotationContent.tsx index 96342dca2..afcbfcc2b 100644 --- a/packages/polaris-viz/src/components/Annotations/components/AnnotationContent/AnnotationContent.tsx +++ b/packages/polaris-viz/src/components/Annotations/components/AnnotationContent/AnnotationContent.tsx @@ -1,7 +1,8 @@ -import {useEffect, useLayoutEffect, useState} from 'react'; +import {useLayoutEffect, useState} from 'react'; import {createPortal} from 'react-dom'; import {changeColorOpacity, clamp, useTheme} from '@shopify/polaris-viz-core'; +import {useHideTooltipWhenMounted} from '../../../../hooks/useHideTooltipWhenMounted'; import {TOOLTIP_BG_OPACITY} from '../../../../constants'; import {useBrowserCheck} from '../../../../hooks/useBrowserCheck'; import type {Annotation} from '../../../../types'; @@ -43,19 +44,7 @@ export function AnnotationContent({ setBounds(ref?.getBoundingClientRect()); }, [ref]); - useEffect(() => { - const tooltip = document.querySelector('[data-tooltip]'); - - if (tooltip) { - tooltip.style.display = 'none'; - } - - return () => { - if (tooltip) { - tooltip.style.display = 'block'; - } - }; - }, []); + useHideTooltipWhenMounted(); if (annotation.content == null) { return null; diff --git a/packages/polaris-viz/src/components/Labels/SingleTextLine.tsx b/packages/polaris-viz/src/components/Labels/SingleTextLine.tsx index d662d7391..c58a6d2a7 100644 --- a/packages/polaris-viz/src/components/Labels/SingleTextLine.tsx +++ b/packages/polaris-viz/src/components/Labels/SingleTextLine.tsx @@ -13,6 +13,8 @@ interface SingleTextLineProps { y: number; ariaHidden?: boolean; dominantBaseline?: 'middle' | 'hanging'; + fontWeight?: number; + id?: string; textAnchor?: 'left' | 'center' | 'right'; } @@ -20,6 +22,8 @@ export function SingleTextLine({ ariaHidden = false, color, dominantBaseline = 'hanging', + fontWeight = 300, + id, targetWidth, text, textAnchor = 'center', @@ -39,14 +43,16 @@ export function SingleTextLine({ {truncated} diff --git a/packages/polaris-viz/src/components/LineChart/LineChart.tsx b/packages/polaris-viz/src/components/LineChart/LineChart.tsx index 9f1e282a9..a1d366594 100644 --- a/packages/polaris-viz/src/components/LineChart/LineChart.tsx +++ b/packages/polaris-viz/src/components/LineChart/LineChart.tsx @@ -29,16 +29,19 @@ import {useTheme} from '../../hooks'; import type { Annotation, LineChartSlotProps, + MissingData, RenderLegendContent, TooltipOptions, } from '../../types'; import {Chart} from './Chart'; +import {MissingDataArea} from './components'; export type LineChartProps = { annotations?: Annotation[]; errorText?: string; emptyStateText?: string; + missingData?: MissingData; renderLegendContent?: RenderLegendContent; renderHiddenLegendLabel?: (count: number) => string; seriesNameFormatter?: LabelFormatter; @@ -62,6 +65,7 @@ export function LineChart(props: LineChartProps) { errorText, id, isAnimated, + missingData, onError, renderLegendContent, renderHiddenLegendLabel, @@ -127,7 +131,25 @@ export function LineChart(props: LineChartProps) { theme={theme} xAxisOptions={xAxisOptionsWithDefaults} yAxisOptions={yAxisOptionsWithDefaults} - slots={props.slots} + slots={ + props.slots == null + ? { + chart: (props) => { + if (missingData == null) { + return null; + } + + return ( + + ); + }, + } + : props.slots + } /> )} diff --git a/packages/polaris-viz/src/components/LineChart/components/MissingDataArea/MissingDataArea.tsx b/packages/polaris-viz/src/components/LineChart/components/MissingDataArea/MissingDataArea.tsx new file mode 100644 index 000000000..992bdfcd2 --- /dev/null +++ b/packages/polaris-viz/src/components/LineChart/components/MissingDataArea/MissingDataArea.tsx @@ -0,0 +1,102 @@ +import {Fragment, memo, useRef} from 'react'; +import type {LineChartSlotProps} from 'types'; +import type {DataSeries} from '@shopify/polaris-viz-core'; +import {uniqueId, useTheme} from '@shopify/polaris-viz-core'; + +import {useIndexForLabels} from '../../../../hooks/useIndexForLabels'; + +import {Pill} from './components/'; + +export interface Props extends LineChartSlotProps { + data: DataSeries[]; + missingData: any; +} + +function MissingDataAreaRaw({ + data, + drawableHeight, + missingData, + xScale, +}: Props) { + const selectedTheme = useTheme(); + const patternID = useRef(uniqueId('missingDataPattern')); + const indexForLabels = useIndexForLabels(data); + + let fromIndex = -1; + let toIndex = -1; + + data[indexForLabels].data.forEach(({key}, index) => { + if (key === missingData.to) { + toIndex = index; + } + + if (key === missingData.from) { + fromIndex = index; + } + }); + + const width = xScale(toIndex - fromIndex); + + const xPosition = xScale(fromIndex); + + return ( + + + + + + + + + + + + + + + + ); +} + +export const MissingDataArea = memo(MissingDataAreaRaw); diff --git a/packages/polaris-viz/src/components/LineChart/components/MissingDataArea/components/DescriptionPopover/DescriptionPopover.scss b/packages/polaris-viz/src/components/LineChart/components/MissingDataArea/components/DescriptionPopover/DescriptionPopover.scss new file mode 100644 index 000000000..2b041bf07 --- /dev/null +++ b/packages/polaris-viz/src/components/LineChart/components/MissingDataArea/components/DescriptionPopover/DescriptionPopover.scss @@ -0,0 +1,18 @@ +.Wrapper { + pointerevents: 'none'; + overflow: 'visible'; + position: fixed; + top: 0; + left: 0; +} + +.Container { + padding: 8px; + backdrop-filter: blur(5px); + border-radius: 5px; + box-shadow: 0 0 2px rgba(0, 0, 0, 0.2), 0 2px 10px rgba(0, 0, 0, 0.1); + max-width: 200px; + pointerevents: auto; + width: fit-content; + line-height: 16px; +} diff --git a/packages/polaris-viz/src/components/LineChart/components/MissingDataArea/components/DescriptionPopover/DescriptionPopover.tsx b/packages/polaris-viz/src/components/LineChart/components/MissingDataArea/components/DescriptionPopover/DescriptionPopover.tsx new file mode 100644 index 000000000..d300e1aa3 --- /dev/null +++ b/packages/polaris-viz/src/components/LineChart/components/MissingDataArea/components/DescriptionPopover/DescriptionPopover.tsx @@ -0,0 +1,53 @@ +import {createPortal} from 'react-dom'; +import {FONT_SIZE, useChartContext} from '@shopify/polaris-viz-core'; + +import {useHideTooltipWhenMounted} from '../../../../../../hooks/useHideTooltipWhenMounted'; +import {getChartId} from '../../../../../../utilities/getChartId'; + +import styles from './DescriptionPopover.scss'; + +export interface DescriptionPopoverProps { + description: string; + label: string; + onMouseLeave: () => void; + x: number; + y: number; + yOffset: number; +} + +export function DescriptionPopover({ + description, + label, + onMouseLeave, + x, + y, + yOffset, +}: DescriptionPopoverProps) { + const {id} = useChartContext(); + const chartId = getChartId(id); + + useHideTooltipWhenMounted(); + + return createPortal( +
+
+ {description} +
+
, + document.getElementById(chartId) ?? document.body, + ); +} diff --git a/packages/polaris-viz/src/components/LineChart/components/MissingDataArea/components/DescriptionPopover/index.ts b/packages/polaris-viz/src/components/LineChart/components/MissingDataArea/components/DescriptionPopover/index.ts new file mode 100644 index 000000000..ad40c829c --- /dev/null +++ b/packages/polaris-viz/src/components/LineChart/components/MissingDataArea/components/DescriptionPopover/index.ts @@ -0,0 +1 @@ +export {DescriptionPopover} from './DescriptionPopover'; diff --git a/packages/polaris-viz/src/components/LineChart/components/MissingDataArea/components/Pill/Pill.scss b/packages/polaris-viz/src/components/LineChart/components/MissingDataArea/components/Pill/Pill.scss new file mode 100644 index 000000000..b2fb13e12 --- /dev/null +++ b/packages/polaris-viz/src/components/LineChart/components/MissingDataArea/components/Pill/Pill.scss @@ -0,0 +1,5 @@ +@import '../../../../../../styles/common'; + +.Group { + @include no-outline; +} diff --git a/packages/polaris-viz/src/components/LineChart/components/MissingDataArea/components/Pill/Pill.tsx b/packages/polaris-viz/src/components/LineChart/components/MissingDataArea/components/Pill/Pill.tsx new file mode 100644 index 000000000..c5555fb89 --- /dev/null +++ b/packages/polaris-viz/src/components/LineChart/components/MissingDataArea/components/Pill/Pill.tsx @@ -0,0 +1,161 @@ +import type {Position} from '@shopify/polaris-viz-core'; +import {FONT_SIZE, LINE_HEIGHT, useTheme} from '@shopify/polaris-viz-core'; +import {useCallback, useLayoutEffect, useState} from 'react'; +import {useDebouncedCallback} from 'use-debounce'; + +import {estimateStringWidthWithOffset} from '../../../../../../utilities/'; +import {SingleTextLine} from '../../../../../Labels'; +import {DescriptionPopover} from '../DescriptionPopover'; + +import styles from './Pill.scss'; + +const PX_OFFSET = 1; + +const BUTTON_HEIGHT = 20; +const PILL_PADDING = 10; +const BUTTON_POSITION_OFFSET = 10; +const BUTTON_ITEM_GAP = 8; +const FONT_WEIGHT = 500; + +const ICON_SIZE = 14; +const ICON_Y_OFFSET = (BUTTON_HEIGHT - ICON_SIZE) / 2; +const ICON_X_OFFSET = 8; + +interface Props { + containerWidth: number; + label: string; + description: string; + x: number; +} + +export function Pill({containerWidth, label, description, x}: Props) { + const selectedTheme = useTheme(); + + const [ref, setRef] = useState(null); + const [bounds, setBounds] = useState({ + x: 0, + y: 0, + }); + + const [isShowingDescription, setIsShowingDescription] = useState(false); + + const labelWidth = estimateStringWidthWithOffset( + label, + FONT_SIZE, + FONT_WEIGHT, + ); + + function handleMouseEnter() { + setIsShowingDescription(true); + } + + function handleMouseLeave() { + setIsShowingDescription(false); + } + + const width = labelWidth + PILL_PADDING * 2; + + const updatePosition = useCallback(() => { + const bounds = ref?.getBoundingClientRect(); + + if (bounds == null) { + return; + } + + setBounds({ + x: bounds.left, + y: bounds.top, + }); + }, [ref]); + + const debouncedUpdatePosition = useDebouncedCallback(() => { + updatePosition(); + }, 100); + + useLayoutEffect(() => { + updatePosition(); + + const isServer = typeof window === 'undefined'; + + if (!isServer) { + window.addEventListener('resize', debouncedUpdatePosition); + } + + return () => { + if (!isServer) { + window.removeEventListener('resize', debouncedUpdatePosition); + } + }; + }, [updatePosition, debouncedUpdatePosition]); + + const pillWidth = + ICON_X_OFFSET + BUTTON_ITEM_GAP + labelWidth + PILL_PADDING * 2; + const isPillWiderThatContainer = + pillWidth + BUTTON_POSITION_OFFSET * 2 > containerWidth; + + const xPosition = isPillWiderThatContainer ? x + containerWidth : x; + + return ( + + + + + + + + {isShowingDescription && ( + + )} + + ); +} + +function Icon({fill}: {fill: string}) { + return ( + + + + + + ); +} diff --git a/packages/polaris-viz/src/components/LineChart/components/MissingDataArea/components/Pill/index.ts b/packages/polaris-viz/src/components/LineChart/components/MissingDataArea/components/Pill/index.ts new file mode 100644 index 000000000..6e78a3d7a --- /dev/null +++ b/packages/polaris-viz/src/components/LineChart/components/MissingDataArea/components/Pill/index.ts @@ -0,0 +1 @@ +export {Pill} from './Pill'; diff --git a/packages/polaris-viz/src/components/LineChart/components/MissingDataArea/components/index.ts b/packages/polaris-viz/src/components/LineChart/components/MissingDataArea/components/index.ts new file mode 100644 index 000000000..6e78a3d7a --- /dev/null +++ b/packages/polaris-viz/src/components/LineChart/components/MissingDataArea/components/index.ts @@ -0,0 +1 @@ +export {Pill} from './Pill'; diff --git a/packages/polaris-viz/src/components/LineChart/components/MissingDataArea/index.ts b/packages/polaris-viz/src/components/LineChart/components/MissingDataArea/index.ts new file mode 100644 index 000000000..7d6c5f1a5 --- /dev/null +++ b/packages/polaris-viz/src/components/LineChart/components/MissingDataArea/index.ts @@ -0,0 +1 @@ +export {MissingDataArea} from './MissingDataArea'; diff --git a/packages/polaris-viz/src/components/LineChart/components/index.ts b/packages/polaris-viz/src/components/LineChart/components/index.ts index b13bb3b8b..8af10a118 100644 --- a/packages/polaris-viz/src/components/LineChart/components/index.ts +++ b/packages/polaris-viz/src/components/LineChart/components/index.ts @@ -1,2 +1,3 @@ export {Points} from './Points'; export {PointsAndCrosshair} from './PointsAndCrosshair'; +export {MissingDataArea} from './MissingDataArea'; diff --git a/packages/polaris-viz/src/components/LineChart/stories/MissingData.stories.tsx b/packages/polaris-viz/src/components/LineChart/stories/MissingData.stories.tsx new file mode 100644 index 000000000..75b4b645f --- /dev/null +++ b/packages/polaris-viz/src/components/LineChart/stories/MissingData.stories.tsx @@ -0,0 +1,20 @@ +import type {Story} from '@storybook/react'; + +export {META as default} from './meta'; + +import type {LineChartProps} from '../../../components'; + +import {DEFAULT_DATA, DEFAULT_PROPS, Template} from './data'; + +export const MissingData: Story = Template.bind({}); + +MissingData.args = { + ...DEFAULT_PROPS, + data: DEFAULT_DATA, + missingData: { + from: '2020-04-03T12:00:00', + to: '2020-04-05T12:00:00', + label: 'Latency', + description: 'Benchmarks data available through Oct 15, 2022 Not available', + }, +}; diff --git a/packages/polaris-viz/src/components/LineChartRelational/stories/data.tsx b/packages/polaris-viz/src/components/LineChartRelational/stories/data.tsx index 7cdc7150b..cd18396c8 100644 --- a/packages/polaris-viz/src/components/LineChartRelational/stories/data.tsx +++ b/packages/polaris-viz/src/components/LineChartRelational/stories/data.tsx @@ -53,7 +53,7 @@ export const DEFAULT_PROPS: Partial = { return renderLinearTooltipContent(tooltipData, { title: tooltipData.title, groups: [ - {title: 'Your store', indexes: [0]}, + {title: 'Your store average', indexes: [0]}, {title: 'Similar stores', indexes: [1, 2, 3]}, ], }); diff --git a/packages/polaris-viz/src/hooks/useHideTooltipWhenMounted.ts b/packages/polaris-viz/src/hooks/useHideTooltipWhenMounted.ts new file mode 100644 index 000000000..52dd31475 --- /dev/null +++ b/packages/polaris-viz/src/hooks/useHideTooltipWhenMounted.ts @@ -0,0 +1,17 @@ +import {useEffect} from 'react'; + +export function useHideTooltipWhenMounted() { + useEffect(() => { + const tooltip = document.querySelector('[data-tooltip]'); + + if (tooltip) { + tooltip.style.display = 'none'; + } + + return () => { + if (tooltip) { + tooltip.style.display = 'block'; + } + }; + }, []); +} diff --git a/packages/polaris-viz/src/types.ts b/packages/polaris-viz/src/types.ts index aa224f3d8..fb374e3a0 100644 --- a/packages/polaris-viz/src/types.ts +++ b/packages/polaris-viz/src/types.ts @@ -244,3 +244,10 @@ export interface LineChartSlotProps { yScale: ScaleLinear; theme: string; } + +export interface MissingData { + from: string; + to: string; + label: string; + description: string; +} diff --git a/packages/polaris-viz/src/utilities/renderLinearTooltipContent.tsx b/packages/polaris-viz/src/utilities/renderLinearTooltipContent.tsx index ab34cb37a..38615b84f 100644 --- a/packages/polaris-viz/src/utilities/renderLinearTooltipContent.tsx +++ b/packages/polaris-viz/src/utilities/renderLinearTooltipContent.tsx @@ -88,11 +88,16 @@ export function renderLinearTooltipContent( }) .filter((series): series is TooltipDataSeries => Boolean(series)); - const hasTitle = dataSeries.some(({isHidden}) => isHidden !== true); + const visibleDataSeries = dataSeries.filter( + ({isHidden}) => isHidden === false, + ); return ( - - {hasTitle && ( + + {visibleDataSeries.length > 1 && ( {seriesName} )} {dataSeries.map(