diff --git a/packages/polaris-viz/src/components/DonutChart/Chart.tsx b/packages/polaris-viz/src/components/DonutChart/Chart.tsx index 921200d0cd..1c1e523e41 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, @@ -16,6 +16,7 @@ import type { Direction, } from '@shopify/polaris-viz-core'; +import {getTrendIndicatorData} from '../../utilities/getTrendIndicatorData'; import {getContainerAlignmentForLegend} from '../../utilities'; import {estimateLegendItemWidth} from '../Legend'; import type {ComparisonMetricProps} from '../ComparisonMetric'; @@ -27,6 +28,7 @@ import { } from '../../hooks'; import {Arc} from '../Arc'; import type { + ColorVisionInteractionMethods, LegendPosition, RenderInnerValueContent, RenderLegendContent, @@ -34,7 +36,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 +48,7 @@ export interface ChartProps { labelFormatter: LabelFormatter; legendPosition: LegendPosition; showLegend: boolean; + showLegendValues: boolean; state: ChartState; theme: string; accessibilityLabel?: string; @@ -63,6 +66,7 @@ export function Chart({ labelFormatter, legendPosition = 'right', showLegend, + showLegendValues, state, theme, accessibilityLabel = '', @@ -92,22 +96,34 @@ export function Chart({ ? 'vertical' : 'horizontal'; - const longestLegendWidth = data.reduce((previous, current) => { - const estimatedLegendWidth = estimateLegendItemWidth( - current.name ?? '', - characterWidths, - ); - - if (estimatedLegendWidth > previous) { - return estimatedLegendWidth; + const maxTrendIndicatorWidth = data.reduce((maxWidth, {metadata}) => { + if (!metadata?.trend) { + return maxWidth; } - return previous; + const {trendIndicatorWidth} = getTrendIndicatorData(metadata.trend); + + return Math.max(maxWidth, trendIndicatorWidth); }, 0); + const longestLegendWidth = useMemo(() => { + return data.reduce((previous, current) => { + const estimatedLegendWidth = estimateLegendItemWidth( + `${current.name ?? ''} ${current.data[0].value} `, + characterWidths, + ); + + if (estimatedLegendWidth > previous) { + return estimatedLegendWidth; + } + + return previous; + }, 0); + }, [characterWidths, data]); + const maxLegendWidth = legendDirection === 'vertical' - ? Math.min( + ? Math.max( longestLegendWidth, dimensions.width * MAX_LEGEND_WIDTH_PERCENTAGE, ) @@ -167,6 +183,27 @@ export function Chart({ const containerAlignmentStyle = getContainerAlignmentForLegend(legendPosition); + const renderLegendContentWithValues = ({ + getColorVisionStyles, + getColorVisionEventAttrs, + }: ColorVisionInteractionMethods) => { + return ( + + ); + }; + + const shouldRenderLegendContentWithValues = + showLegendValues && + !renderLegendContent && + (legendPosition === 'right' || legendPosition === 'left'); + return (
@@ -258,7 +295,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..41d3efebb7 --- /dev/null +++ b/packages/polaris-viz/src/components/DonutChart/components/LegendValues/LegendValues.scss @@ -0,0 +1,20 @@ +.Table { + min-width: 200px; + width: 100%; + padding-left: 0; + border-collapse: separate; + border-spacing: 0 10px; + + .values { + font-size: 12px; + line-height: 16px; + } + + .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..cc16db027c --- /dev/null +++ b/packages/polaris-viz/src/components/DonutChart/components/LegendValues/LegendValues.tsx @@ -0,0 +1,92 @@ +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 {SquareColorPreview} from '../../../../components/SquareColorPreview'; + +import styles from './LegendValues.scss'; + +interface LegendContentProps { + data: DataSeries[]; + totalValue: number; + maxTrendIndicatorWidth: number; + labelFormatter: LabelFormatter; + getColorVisionStyles: ColorVisionInteractionMethods['getColorVisionStyles']; + getColorVisionEventAttrs: ColorVisionInteractionMethods['getColorVisionEventAttrs']; +} + +export function LegendValues({ + data, + totalValue, + maxTrendIndicatorWidth, + labelFormatter, + getColorVisionStyles, + getColorVisionEventAttrs, +}: LegendContentProps) { + const selectedTheme = useTheme(); + + return ( + + {data.map(({name, data, metadata}, index) => { + const value = data[0].value || 0; + const percentageValue = ` (${((value / totalValue) * 100).toFixed( + 0, + )}%)`; + + return ( + + + + + + + + ); + })} +
+ + + + {name} + + + + + {labelFormatter(value)} + + + + {percentageValue} + + + + {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..2f8b0d3622 --- /dev/null +++ b/packages/polaris-viz/src/components/DonutChart/stories/WithLegendValues.stories.tsx @@ -0,0 +1,59 @@ +import type {Story} from '@storybook/react'; + +export {META as default} from './meta'; + +import type {DonutChartProps} from '../../DonutChart'; + +import {DEFAULT_DATA, DEFAULT_PROPS, Template} from './data'; + +export const WithLegendValues: Story = Template.bind({}); + +WithLegendValues.args = { + ...DEFAULT_PROPS, + showLegend: true, + showLegendValues: true, + 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/LegendContainer/hooks/useLegend.ts b/packages/polaris-viz/src/components/LegendContainer/hooks/useLegend.ts index 671b7873b7..6bc255f6dd 100644 --- a/packages/polaris-viz/src/components/LegendContainer/hooks/useLegend.ts +++ b/packages/polaris-viz/src/components/LegendContainer/hooks/useLegend.ts @@ -55,12 +55,13 @@ export function useLegend({ } const legends = data.map(({series, shape}) => { - return series.map(({name, color, isComparison}) => { + return series.map(({name, color, isComparison, metadata}) => { return { name: name ?? '', color, shape, isComparison, + ...(metadata?.trend ? {trend: metadata.trend} : {}), }; }); }); 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 57% rename from packages/polaris-viz/src/components/shared/HorizontalBars/utilities/getTrendIndicatorData.ts rename to packages/polaris-viz/src/utilities/getTrendIndicatorData.ts index 80164b9457..c9800f2116 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/TrendIndicator'; export function getTrendIndicatorData( trendMetadata: MetaDataTrendIndicator | undefined, ) { if (trendMetadata != null) { - const {totalWidth} = estimateTrendIndicatorWidth(`${trendMetadata.value}`); + const {totalWidth} = estimateTrendIndicatorWidth( + `${trendMetadata.value || ''}`, + ); return { trendIndicatorProps: trendMetadata,