diff --git a/projects/js-packages/charts/changelog/charts-41-legends-extract-common-container-logic-into-a-shared-helper b/projects/js-packages/charts/changelog/charts-41-legends-extract-common-container-logic-into-a-shared-helper new file mode 100644 index 0000000000000..81e69ce5749be --- /dev/null +++ b/projects/js-packages/charts/changelog/charts-41-legends-extract-common-container-logic-into-a-shared-helper @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Charts: legends - extract common container logic to shared helper diff --git a/projects/js-packages/charts/src/components/bar-chart/bar-chart.module.scss b/projects/js-packages/charts/src/components/bar-chart/bar-chart.module.scss index e28b33fc32788..726b8bd0e17a7 100644 --- a/projects/js-packages/charts/src/components/bar-chart/bar-chart.module.scss +++ b/projects/js-packages/charts/src/components/bar-chart/bar-chart.module.scss @@ -1,12 +1,10 @@ .bar-chart { - display: flex; - flex-direction: column; svg { overflow: visible; } - &-legend { + &__legend { margin-top: 1rem; } } diff --git a/projects/js-packages/charts/src/components/bar-chart/bar-chart.tsx b/projects/js-packages/charts/src/components/bar-chart/bar-chart.tsx index 72b5635536ba0..69050a50d2796 100644 --- a/projects/js-packages/charts/src/components/bar-chart/bar-chart.tsx +++ b/projects/js-packages/charts/src/components/bar-chart/bar-chart.tsx @@ -9,6 +9,7 @@ import { useZeroValueDisplay, useChartMargin, useElementHeight, + useLegendLayout, } from '../../hooks'; import { GlobalChartsProvider, @@ -18,6 +19,7 @@ import { useGlobalChartsTheme, GlobalChartsContext, } from '../../providers'; +import containerStyles from '../../styles/chart-container.module.scss'; import { attachSubComponents } from '../../utils'; import { Legend, useChartLegendItems } from '../legend'; import { SingleChartContext } from '../private/single-chart-context'; @@ -276,6 +278,15 @@ const BarChartInternal: FC< BarChartProps > = ( { return generatedStyles; }, [ selectedIndex, data, chartId ] ); + // Use the legend layout hook for consistent calculations + const { adjustedChartHeight, adjustedMargin, containerClassName } = useLegendLayout( { + height, + showLegend, + legendPosition, + legendHeight, + defaultMargin: { ...defaultMargin, ...margin }, + } ); + // Validate data first const error = validateData( dataSorted ); const isDataValid = ! error; @@ -310,19 +321,23 @@ const BarChartInternal: FC< BarChartProps > = ( { value={ { chartId, chartWidth: width, - chartHeight: height - ( showLegend ? legendHeight : 0 ), + chartHeight: adjustedChartHeight, } } >
= ( { = ( { { allSeriesHidden ? ( = ( { } } >
diff --git a/projects/js-packages/charts/src/components/line-chart/line-chart.tsx b/projects/js-packages/charts/src/components/line-chart/line-chart.tsx index 1ff267c587fb9..a1ddfe4803361 100644 --- a/projects/js-packages/charts/src/components/line-chart/line-chart.tsx +++ b/projects/js-packages/charts/src/components/line-chart/line-chart.tsx @@ -12,6 +12,7 @@ import { useChartDataTransform, useChartMargin, useElementHeight, + useLegendLayout, } from '../../hooks'; import { GlobalChartsProvider, @@ -21,6 +22,7 @@ import { useGlobalChartsContext, useGlobalChartsTheme, } from '../../providers'; +import containerStyles from '../../styles/chart-container.module.scss'; import { attachSubComponents } from '../../utils'; import { Legend, useChartLegendItems } from '../legend'; import { DefaultGlyph } from '../private/default-glyph'; @@ -379,6 +381,15 @@ const LineChartInternal = forwardRef< SingleChartRef, LineChartProps >( const defaultMargin = useChartMargin( height, chartOptions, dataSorted, theme ); + // Use the legend layout hook for consistent calculations + const { adjustedChartHeight, adjustedMargin, containerClassName } = useLegendLayout( { + height, + showLegend, + legendPosition, + legendHeight, + defaultMargin: { ...defaultMargin, ...margin }, + } ); + const error = validateData( dataSorted ); const isDataValid = ! error; @@ -433,17 +444,21 @@ const LineChartInternal = forwardRef< SingleChartRef, LineChartProps >( chartId, chartRef: internalChartRef, chartWidth: width, - chartHeight: height - ( showLegend ? legendHeight : 0 ), + chartHeight: adjustedChartHeight, } } >
@@ -459,14 +474,8 @@ const LineChartInternal = forwardRef< SingleChartRef, LineChartProps >( ( { allSeriesHidden ? ( @@ -231,16 +243,12 @@ const PieChartInternal = ( { ); } - const width = size; - const height = size; - const adjustedHeight = showLegend && legendPosition === 'top' ? height - legendHeight : height; - // Calculate radius based on width/height - const radius = Math.min( width, adjustedHeight ) / 2; + const radius = Math.min( width, adjustedChartHeight ) / 2; // Center the chart in the available space const centerX = width / 2; - const centerY = adjustedHeight / 2; + const centerY = adjustedChartHeight / 2; // Calculate the angle between each (use original data length for consistent spacing) const padAngle = gapScale * ( ( 2 * Math.PI ) / data.length ); @@ -273,22 +281,24 @@ const PieChartInternal = ( { value={ { chartId, chartWidth: width, - chartHeight: adjustedHeight, + chartHeight: adjustedChartHeight, } } >
{ allSegmentsHidden ? ( diff --git a/projects/js-packages/charts/src/components/pie-semi-circle-chart/pie-semi-circle-chart.tsx b/projects/js-packages/charts/src/components/pie-semi-circle-chart/pie-semi-circle-chart.tsx index d59f4b511172f..0e51a9e2a8257 100644 --- a/projects/js-packages/charts/src/components/pie-semi-circle-chart/pie-semi-circle-chart.tsx +++ b/projects/js-packages/charts/src/components/pie-semi-circle-chart/pie-semi-circle-chart.tsx @@ -6,7 +6,7 @@ import { useTooltip, useTooltipInPortal } from '@visx/tooltip'; import { __ } from '@wordpress/i18n'; import clsx from 'clsx'; import { useCallback, useContext, useMemo } from 'react'; -import { useElementHeight, useInteractiveLegendData } from '../../hooks'; +import { useElementHeight, useInteractiveLegendData, useLegendLayout } from '../../hooks'; import { GlobalChartsProvider, useChartId, @@ -14,6 +14,7 @@ import { useGlobalChartsContext, GlobalChartsContext, } from '../../providers'; +import containerStyles from '../../styles/chart-container.module.scss'; import { attachSubComponents } from '../../utils'; import { Legend, useChartLegendItems } from '../legend'; import { ChartSVG, ChartHTML, useChartChildren } from '../private/chart-composition'; @@ -249,6 +250,18 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( { metadata: chartMetadata, } ); + // Calculate chart dimensions + // TODO: we might want to accept height as a prop in the future, because the height of container might not always be enough. + const height = width / 2; + + // Use the legend layout hook for consistent calculations + const { adjustedChartHeight, containerClassName } = useLegendLayout( { + height, + showLegend, + legendPosition, + legendHeight, + } ); + if ( ! isValid ) { return (
@@ -260,13 +273,7 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( {
); } - - // Calculate chart dimensions - // TODO: we might want to accept height as a prop in the future, because the height of container might not always be enough. - const height = width / 2; - // The chart only takes the height minus the legend height. - const chartHeight = height - ( showLegend && legendPosition === 'top' ? legendHeight : 0 ); - const radius = Math.min( width / 2, chartHeight ); + const radius = Math.min( width / 2, adjustedChartHeight ); const innerRadius = radius * ( 1 - thickness ); // Map data with index for color assignment @@ -293,21 +300,23 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( { >
{ /* Main chart group centered horizontally and positioned at bottom */ } - + { allSegmentsHidden ? ( { + return useMemo( () => { + // Calculate adjusted chart height + const adjustedChartHeight = + showLegend && legendPosition === 'top' ? height - legendHeight : height; + + // Calculate adjusted margin for top legend position, ensuring all properties are defined + const adjustedMargin = { + top: + ( defaultMargin.top || 0 ) + ( showLegend && legendPosition === 'top' ? legendHeight : 0 ), + right: defaultMargin.right || 0, + bottom: defaultMargin.bottom || 0, + left: defaultMargin.left || 0, + }; + + // Determine container class name + const containerClassName = + showLegend && legendPosition === 'top' + ? 'chart-container--legend-top' + : 'chart-container--legend-bottom'; + + return { + adjustedChartHeight, + adjustedMargin, + containerClassName, + }; + }, [ + height, + showLegend, + legendPosition, + legendHeight, + defaultMargin.top, + defaultMargin.right, + defaultMargin.bottom, + defaultMargin.left, + ] ); +}; diff --git a/projects/js-packages/charts/src/styles/chart-container.module.scss b/projects/js-packages/charts/src/styles/chart-container.module.scss new file mode 100644 index 0000000000000..7f22ba87ee654 --- /dev/null +++ b/projects/js-packages/charts/src/styles/chart-container.module.scss @@ -0,0 +1,16 @@ +/** + * Shared chart container styles for legend positioning + * Using BEM naming convention + */ + +.chart-container { + display: flex; + + &--legend-top { + flex-direction: column-reverse; + } + + &--legend-bottom { + flex-direction: column; + } +} diff --git a/projects/js-packages/charts/src/types.ts b/projects/js-packages/charts/src/types.ts index 96f421d0192ba..5c4b285257045 100644 --- a/projects/js-packages/charts/src/types.ts +++ b/projects/js-packages/charts/src/types.ts @@ -15,6 +15,8 @@ export type Optional< T, K extends keyof T > = Pick< Partial< T >, K > & Omit< T export type OrientationType = ValueOf< typeof Orientation >; +export type LegendPosition = 'top' | 'bottom'; + export type AnnotationStyles = { circleSubject?: Omit< CircleSubjectProps, 'x' | 'y' > & { fill?: string }; lineSubject?: Omit< LineSubjectProps, 'x' | 'y' >; @@ -342,7 +344,7 @@ export type BaseChartProps< T = DataPoint | DataPointDate | LeaderboardEntry > = * Legend position (where the legend appears) * TODO: Add 'left' | 'right' positioning support in future implementation */ - legendPosition?: 'top' | 'bottom'; + legendPosition?: LegendPosition; /** * Legend alignment within its position */