From 3ab883025ab3ae5f4b6d27d89dfda6f41cb3e2dc Mon Sep 17 00:00:00 2001 From: Michael Nesen Date: Mon, 4 Dec 2023 20:56:26 +0000 Subject: [PATCH] Add legend with values + trend indicator data --- packages/polaris-viz/CHANGELOG.md | 6 +- .../src/components/DonutChart/Chart.tsx | 56 ++++++++--- .../src/components/DonutChart/DonutChart.tsx | 5 + .../components/InnerValue/InnerValue.tsx | 4 +- .../components/LegendValues/LegendValues.scss | 14 +++ .../components/LegendValues/LegendValues.tsx | 95 +++++++++++++++++++ .../components/LegendValues/index.ts | 1 + .../components/DonutChart/components/index.ts | 1 + .../stories/WithLegendValues.stories.tsx | 60 ++++++++++++ .../components/DonutChart/stories/meta.tsx | 4 + .../src/components/DonutChart/types.ts | 13 +++ packages/polaris-viz/src/components/index.ts | 2 +- .../shared/HorizontalBars/HorizontalBars.tsx | 2 +- .../polaris-viz/src/storybook/constants.ts | 15 +++ packages/polaris-viz/src/types.ts | 3 + .../utilities/getTrendIndicatorData.ts | 9 +- 16 files changed, 269 insertions(+), 21 deletions(-) create mode 100644 packages/polaris-viz/src/components/DonutChart/components/LegendValues/LegendValues.scss create mode 100644 packages/polaris-viz/src/components/DonutChart/components/LegendValues/LegendValues.tsx create mode 100644 packages/polaris-viz/src/components/DonutChart/components/LegendValues/index.ts create mode 100644 packages/polaris-viz/src/components/DonutChart/stories/WithLegendValues.stories.tsx create mode 100644 packages/polaris-viz/src/components/DonutChart/types.ts rename packages/polaris-viz/src/{components/shared/HorizontalBars => }/utilities/getTrendIndicatorData.ts (58%) diff --git a/packages/polaris-viz/CHANGELOG.md b/packages/polaris-viz/CHANGELOG.md index 0e35e6df7a..c158d8629b 100644 --- a/packages/polaris-viz/CHANGELOG.md +++ b/packages/polaris-viz/CHANGELOG.md @@ -5,7 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). - +## Unreleased + +### Added + +- Added support for showing values and trends in `` legend with new prop `showLegendValues`. ## [10.1.0] - 2023-12-04 diff --git a/packages/polaris-viz/src/components/DonutChart/Chart.tsx b/packages/polaris-viz/src/components/DonutChart/Chart.tsx index 921200d0cd..e09de98620 100644 --- a/packages/polaris-viz/src/components/DonutChart/Chart.tsx +++ b/packages/polaris-viz/src/components/DonutChart/Chart.tsx @@ -1,4 +1,4 @@ -import {Fragment, useState} from 'react'; +import {Fragment, useMemo, useState} from 'react'; import {pie} from 'd3-shape'; import { clamp, @@ -27,6 +27,7 @@ import { } from '../../hooks'; import {Arc} from '../Arc'; import type { + ColorVisionInteractionMethods, LegendPosition, RenderInnerValueContent, RenderLegendContent, @@ -34,7 +35,7 @@ import type { import {ChartSkeleton} from '../../components/ChartSkeleton'; import styles from './DonutChart.scss'; -import {InnerValue} from './components'; +import {InnerValue, LegendValues} from './components'; const ERROR_ANIMATION_PADDING = 40; const FULL_CIRCLE = Math.PI * 2; @@ -46,6 +47,7 @@ export interface ChartProps { labelFormatter: LabelFormatter; legendPosition: LegendPosition; showLegend: boolean; + showLegendValues: boolean; state: ChartState; theme: string; accessibilityLabel?: string; @@ -63,6 +65,7 @@ export function Chart({ labelFormatter, legendPosition = 'right', showLegend, + showLegendValues, state, theme, accessibilityLabel = '', @@ -92,22 +95,24 @@ export function Chart({ ? 'vertical' : 'horizontal'; - const longestLegendWidth = data.reduce((previous, current) => { - const estimatedLegendWidth = estimateLegendItemWidth( - current.name ?? '', - characterWidths, - ); + const longestLegendWidth = useMemo(() => { + return data.reduce((previous, current) => { + const estimatedLegendWidth = estimateLegendItemWidth( + `${current.name ?? ''} ${current.data[0].value} `, + characterWidths, + ); - if (estimatedLegendWidth > previous) { - return estimatedLegendWidth; - } + if (estimatedLegendWidth > previous) { + return estimatedLegendWidth; + } - return previous; - }, 0); + return previous; + }, 0); + }, [characterWidths, data]); const maxLegendWidth = legendDirection === 'vertical' - ? Math.min( + ? Math.max( longestLegendWidth, dimensions.width * MAX_LEGEND_WIDTH_PERCENTAGE, ) @@ -167,6 +172,25 @@ export function Chart({ const containerAlignmentStyle = getContainerAlignmentForLegend(legendPosition); + const renderLegendContentWithValues = ({ + getColorVisionStyles, + getColorVisionEventAttrs, + }: ColorVisionInteractionMethods) => { + return ( + + ); + }; + + const shouldRenderLegendContentWithValues = + !renderLegendContent && + showLegendValues && + (legendPosition === 'right' || legendPosition === 'left'); + return (
@@ -258,7 +282,11 @@ export function Chart({ direction={legendDirection} position={legendPosition} maxWidth={maxLegendWidth} - renderLegendContent={renderLegendContent} + renderLegendContent={ + shouldRenderLegendContentWithValues + ? renderLegendContentWithValues + : renderLegendContent + } /> )}
diff --git a/packages/polaris-viz/src/components/DonutChart/DonutChart.tsx b/packages/polaris-viz/src/components/DonutChart/DonutChart.tsx index 1ffd7124ef..c71206dede 100644 --- a/packages/polaris-viz/src/components/DonutChart/DonutChart.tsx +++ b/packages/polaris-viz/src/components/DonutChart/DonutChart.tsx @@ -13,10 +13,13 @@ import type { } from '../../types'; import {Chart} from './Chart'; +import type {DonutChartDataSeries} from './types'; export type DonutChartProps = { + data: DonutChartDataSeries[]; comparisonMetric?: ComparisonMetricProps; showLegend?: boolean; + showLegendValues?: boolean; labelFormatter?: LabelFormatter; legendFullWidth?: boolean; legendPosition?: LegendPosition; @@ -32,6 +35,7 @@ export function DonutChart(props: DonutChartProps) { theme = defaultTheme, comparisonMetric, showLegend = true, + showLegendValues = false, labelFormatter = (value) => `${value}`, legendFullWidth, legendPosition = 'left', @@ -61,6 +65,7 @@ export function DonutChart(props: DonutChartProps) { labelFormatter={labelFormatter} comparisonMetric={comparisonMetric} showLegend={showLegend} + showLegendValues={showLegendValues} legendFullWidth={legendFullWidth} legendPosition={legendPosition} renderInnerValueContent={renderInnerValueContent} diff --git a/packages/polaris-viz/src/components/DonutChart/components/InnerValue/InnerValue.tsx b/packages/polaris-viz/src/components/DonutChart/components/InnerValue/InnerValue.tsx index ac213a57eb..08a8c8eb96 100644 --- a/packages/polaris-viz/src/components/DonutChart/components/InnerValue/InnerValue.tsx +++ b/packages/polaris-viz/src/components/DonutChart/components/InnerValue/InnerValue.tsx @@ -50,7 +50,9 @@ export function InnerValue({ ); const activeValueExists = activeValue !== null && activeValue !== undefined; - const valueToDisplay = activeValueExists ? activeValue : animatedTotalValue; + const valueToDisplay = activeValueExists + ? labelFormatter(activeValue) + : animatedTotalValue; const innerContent = renderInnerValueContent?.({ activeValue, diff --git a/packages/polaris-viz/src/components/DonutChart/components/LegendValues/LegendValues.scss b/packages/polaris-viz/src/components/DonutChart/components/LegendValues/LegendValues.scss new file mode 100644 index 0000000000..7a300ca787 --- /dev/null +++ b/packages/polaris-viz/src/components/DonutChart/components/LegendValues/LegendValues.scss @@ -0,0 +1,14 @@ +.Table { + min-width: 200px; + width: 100%; + border-collapse: separate; + border-spacing: 0 10px; + + .alignLeft { + text-align: left; + } + + .alignRight { + text-align: right; + } +} diff --git a/packages/polaris-viz/src/components/DonutChart/components/LegendValues/LegendValues.tsx b/packages/polaris-viz/src/components/DonutChart/components/LegendValues/LegendValues.tsx new file mode 100644 index 0000000000..c2ea8c7734 --- /dev/null +++ b/packages/polaris-viz/src/components/DonutChart/components/LegendValues/LegendValues.tsx @@ -0,0 +1,95 @@ +import type {ColorVisionInteractionMethods, DataSeries} from 'index'; +import type {LabelFormatter} from '@shopify/polaris-viz-core'; +import {useTheme} from '@shopify/polaris-viz-core'; + +import {TrendIndicator} from '../../../TrendIndicator'; +import {getTrendIndicatorData} from '../../../../utilities/getTrendIndicatorData'; +import {SquareColorPreview} from '../../../../components/SquareColorPreview'; + +import styles from './LegendValues.scss'; + +interface LegendContentProps { + data: DataSeries[]; + labelFormatter: LabelFormatter; + getColorVisionStyles: ColorVisionInteractionMethods['getColorVisionStyles']; + getColorVisionEventAttrs: ColorVisionInteractionMethods['getColorVisionEventAttrs']; +} + +export function LegendValues({ + data, + labelFormatter, + getColorVisionStyles, + getColorVisionEventAttrs, +}: LegendContentProps) { + const selectedTheme = useTheme(); + + const maxTrendIndicatorWidth = data.reduce((maxWidth, {metadata}) => { + if (!metadata?.trend) { + return maxWidth; + } + + const {trendIndicatorWidth} = getTrendIndicatorData(metadata.trend); + + return Math.max(maxWidth, trendIndicatorWidth); + }, 0); + + return ( + + {data.map(({name, data, metadata}, index) => { + const value = data[0].value || 0; + + return ( + + + + + + + + + + ); + })} +
+ + + + {name} + + + + + + {labelFormatter(value)} + + + + {metadata?.trend && } + +
+ ); +} diff --git a/packages/polaris-viz/src/components/DonutChart/components/LegendValues/index.ts b/packages/polaris-viz/src/components/DonutChart/components/LegendValues/index.ts new file mode 100644 index 0000000000..77c93d52c2 --- /dev/null +++ b/packages/polaris-viz/src/components/DonutChart/components/LegendValues/index.ts @@ -0,0 +1 @@ +export {LegendValues} from './LegendValues'; diff --git a/packages/polaris-viz/src/components/DonutChart/components/index.ts b/packages/polaris-viz/src/components/DonutChart/components/index.ts index f966c32ddb..b28a64f07b 100644 --- a/packages/polaris-viz/src/components/DonutChart/components/index.ts +++ b/packages/polaris-viz/src/components/DonutChart/components/index.ts @@ -1 +1,2 @@ export {InnerValue} from './InnerValue'; +export {LegendValues} from './LegendValues'; diff --git a/packages/polaris-viz/src/components/DonutChart/stories/WithLegendValues.stories.tsx b/packages/polaris-viz/src/components/DonutChart/stories/WithLegendValues.stories.tsx new file mode 100644 index 0000000000..2ca5b082ae --- /dev/null +++ b/packages/polaris-viz/src/components/DonutChart/stories/WithLegendValues.stories.tsx @@ -0,0 +1,60 @@ +import type {Story} from '@storybook/react'; + +export {META as default} from './meta'; + +import type {DonutChartProps} from '../../DonutChart'; + +import {DEFAULT_PROPS, Template} from './data'; + +export const WithLegendValues: Story = Template.bind({}); + +WithLegendValues.args = { + ...DEFAULT_PROPS, + showLegend: true, + showLegendValues: true, + legendPosition: 'right', + labelFormatter: (value) => `$${value}`, + data: [ + { + name: 'Shopify Payments', + data: [{key: 'april - march', value: 50000}], + metadata: { + trend: { + value: '5%', + }, + }, + }, + { + name: 'Paypal', + data: [{key: 'april - march', value: 25000}], + metadata: { + trend: { + value: '50%', + direction: 'downward', + trend: 'negative', + }, + }, + }, + { + name: 'Other', + data: [{key: 'april - march', value: 10000}], + metadata: { + trend: { + value: '100%', + direction: 'upward', + trend: 'positive', + }, + }, + }, + { + name: 'Amazon Pay', + data: [{key: 'april - march', value: 5000}], + metadata: { + trend: { + direction: 'upward', + trend: 'positive', + }, + }, + }, + ], +}; diff --git a/packages/polaris-viz/src/components/DonutChart/stories/meta.tsx b/packages/polaris-viz/src/components/DonutChart/stories/meta.tsx index 5336ad1a8d..48177204c9 100644 --- a/packages/polaris-viz/src/components/DonutChart/stories/meta.tsx +++ b/packages/polaris-viz/src/components/DonutChart/stories/meta.tsx @@ -7,6 +7,8 @@ import { LEGEND_FULL_WIDTH_ARGS, LEGEND_POSITION_ARGS, RENDER_LEGEND_CONTENT_ARGS, + SHOW_LEGEND_ARGS, + SHOW_LEGEND_VALUES_ARGS, THEME_CONTROL_ARGS, } from '../../../storybook/constants'; import type {DonutChartProps} from '../DonutChart'; @@ -29,6 +31,8 @@ export const META: Meta = { data: DATA_SERIES_ARGS, legendFullWidth: LEGEND_FULL_WIDTH_ARGS, legendPosition: LEGEND_POSITION_ARGS, + showLegend: SHOW_LEGEND_ARGS, + showLegendValues: SHOW_LEGEND_VALUES_ARGS, renderLegendContent: RENDER_LEGEND_CONTENT_ARGS, theme: THEME_CONTROL_ARGS, state: CHART_STATE_CONTROL_ARGS, diff --git a/packages/polaris-viz/src/components/DonutChart/types.ts b/packages/polaris-viz/src/components/DonutChart/types.ts new file mode 100644 index 0000000000..f87dea5c8e --- /dev/null +++ b/packages/polaris-viz/src/components/DonutChart/types.ts @@ -0,0 +1,13 @@ +import type {DataSeries} from '@shopify/polaris-viz-core/src/types'; + +import type {TrendIndicatorProps} from '../TrendIndicator'; + +export type MetaDataTrendIndicator = Omit; + +export interface MetaData { + trend?: MetaDataTrendIndicator; +} + +export interface DonutChartDataSeries extends DataSeries { + metadata?: MetaData; +} diff --git a/packages/polaris-viz/src/components/index.ts b/packages/polaris-viz/src/components/index.ts index 1399bb30f6..8529e49904 100644 --- a/packages/polaris-viz/src/components/index.ts +++ b/packages/polaris-viz/src/components/index.ts @@ -47,5 +47,5 @@ export {ChartSkeleton} from './ChartSkeleton'; export {ComboChart} from './ComboChart'; export type {ComboChartProps} from './ComboChart'; export {XAxis} from './XAxis'; -export {TrendIndicator} from './TrendIndicator'; +export {TrendIndicator, estimateTrendIndicatorWidth} from './TrendIndicator'; export {LineChartRelational} from './LineChartRelational'; diff --git a/packages/polaris-viz/src/components/shared/HorizontalBars/HorizontalBars.tsx b/packages/polaris-viz/src/components/shared/HorizontalBars/HorizontalBars.tsx index bba739a111..dc7fd479d0 100644 --- a/packages/polaris-viz/src/components/shared/HorizontalBars/HorizontalBars.tsx +++ b/packages/polaris-viz/src/components/shared/HorizontalBars/HorizontalBars.tsx @@ -9,6 +9,7 @@ import { clamp, } from '@shopify/polaris-viz-core'; +import {getTrendIndicatorData} from '../../../utilities/getTrendIndicatorData'; import {TREND_INDICATOR_HEIGHT, TrendIndicator} from '../../TrendIndicator'; import {getHoverZoneOffset} from '../../../utilities'; import { @@ -21,7 +22,6 @@ import {getGradientDefId} from '../GradientDefs'; import {Label, Bar, LabelWrapper} from './components'; import styles from './HorizontalBars.scss'; -import {getTrendIndicatorData} from './utilities/getTrendIndicatorData'; const SERIES_DELAY = 150; diff --git a/packages/polaris-viz/src/storybook/constants.ts b/packages/polaris-viz/src/storybook/constants.ts index ac4d47d215..87ec0d2602 100644 --- a/packages/polaris-viz/src/storybook/constants.ts +++ b/packages/polaris-viz/src/storybook/constants.ts @@ -42,6 +42,21 @@ export const LEGEND_FULL_WIDTH_ARGS = { }, }; +export const SHOW_LEGEND_ARGS = { + description: 'Whether to show the legend or not.', + control: { + type: 'boolean', + }, +}; + +export const SHOW_LEGEND_VALUES_ARGS = { + description: + 'Whether to show the values in the legend or not. If `showLegend` is false, or `legendPosition` is not `left`/`right`, this prop will have no effect.', + control: { + type: 'boolean', + }, +}; + export const RENDER_LEGEND_CONTENT_ARGS = { description: 'This accepts a function that is called to render the legend content instead of the given legend. If `showLegend` is false, this prop will have no effect.', diff --git a/packages/polaris-viz/src/types.ts b/packages/polaris-viz/src/types.ts index dcdb3ec086..313f3724b4 100644 --- a/packages/polaris-viz/src/types.ts +++ b/packages/polaris-viz/src/types.ts @@ -8,6 +8,7 @@ import type { } from '@shopify/polaris-viz-core'; import type {Series, SeriesPoint} from 'd3-shape'; import type {ScaleLinear} from 'd3-scale'; +import type {TrendIndicatorProps} from 'components/TrendIndicator'; export interface YAxisTick { value: number; @@ -194,6 +195,8 @@ export type LegendPosition = | 'bottom' | 'left'; +export type MetaDataTrendIndicator = Omit; + export interface ColorVisionInteractionMethods { getColorVisionEventAttrs: ( index: number, diff --git a/packages/polaris-viz/src/components/shared/HorizontalBars/utilities/getTrendIndicatorData.ts b/packages/polaris-viz/src/utilities/getTrendIndicatorData.ts similarity index 58% rename from packages/polaris-viz/src/components/shared/HorizontalBars/utilities/getTrendIndicatorData.ts rename to packages/polaris-viz/src/utilities/getTrendIndicatorData.ts index 80164b9457..105126fa14 100644 --- a/packages/polaris-viz/src/components/shared/HorizontalBars/utilities/getTrendIndicatorData.ts +++ b/packages/polaris-viz/src/utilities/getTrendIndicatorData.ts @@ -1,11 +1,14 @@ -import type {MetaDataTrendIndicator} from '../../../SimpleBarChart'; -import {estimateTrendIndicatorWidth} from '../../../TrendIndicator'; +import type {MetaDataTrendIndicator} from 'types'; + +import {estimateTrendIndicatorWidth} from '../components'; export function getTrendIndicatorData( trendMetadata: MetaDataTrendIndicator | undefined, ) { if (trendMetadata != null) { - const {totalWidth} = estimateTrendIndicatorWidth(`${trendMetadata.value}`); + const {totalWidth} = estimateTrendIndicatorWidth( + `${trendMetadata.value || ''}`, + ); return { trendIndicatorProps: trendMetadata,