diff --git a/packages/react-charts/src/components/Chart/Chart.tsx b/packages/react-charts/src/components/Chart/Chart.tsx index 9c311c1c840..9375f1ca2c7 100644 --- a/packages/react-charts/src/components/Chart/Chart.tsx +++ b/packages/react-charts/src/components/Chart/Chart.tsx @@ -31,8 +31,10 @@ import { getPaddingForSide, getPatternDefs, getDefaultData, - useDefaultPatternProps -} from '../ChartUtils'; + getLegendItemsExtraHeight, + useDefaultPatternProps, +} from "../ChartUtils"; +import { useEffect } from "react"; /** * Chart is a wrapper component that reconciles the domain for all its children, controls the layout of the chart, @@ -235,12 +237,20 @@ export interface ChartProps extends VictoryChartProps { */ innerRadius?: number; /** - * Allows legend items to wrap. A value of true allows the legend to wrap onto the next line - * if its container is not wide enough. + * @beta Allows legend items to wrap onto the next line if the chart is not wide enough. + * + * Note that the chart's SVG height and width are 100% by default, so it can be responsive itself. However, if you + * define the height and width of the chart's parent container, you must accommodate for extra legend height due to + * legend items wrapping onto the next line. When the height of the chart's parent container is too small, some legend + * items may not be visible. + * + * Alternatively, a callback function may be provided, which will be called after the legend's itemsPerRow property + * has been calculated. The value provided can be used to increase the chart's parent container height as legend + * items wrap onto the next line. If no adjustment is necessary, the value will be zero. * * Note: This is overridden by the legendItemsPerRow property */ - legendAllowWrap?: boolean; + legendAllowWrap?: boolean | ((extraHeight: number) => void); /** * The legend component to render with chart. * @@ -557,7 +567,7 @@ export const Chart: React.FunctionComponent = ({ } return getComputedLegend({ - allowWrap: legendAllowWrap, + allowWrap: legendAllowWrap === true || typeof legendAllowWrap === 'function', chartType: 'chart', colorScale, dx, @@ -594,6 +604,20 @@ export const Chart: React.FunctionComponent = ({ return child; }); + // Callback to compliment legendAllowWrap + const computedLegend = getLegend(); + useEffect(() => { + if (typeof legendAllowWrap === 'function') { + const extraHeight = getLegendItemsExtraHeight({ + legendData: computedLegend.props.data, + legendOrientation: computedLegend.props.orientation, + legendProps: computedLegend.props, + theme + }); + legendAllowWrap(extraHeight); + } + }, [computedLegend, legendAllowWrap, theme, width]); + // Note: containerComponent is required for theme return ( = ({ {...rest} > {renderChildren()} - {getLegend()} + {computedLegend} {isPatternDefs && getPatternDefs({ patternId, colorScale: defaultColorScale })} ); diff --git a/packages/react-charts/src/components/ChartBullet/ChartBullet.tsx b/packages/react-charts/src/components/ChartBullet/ChartBullet.tsx index 36c4b12cba6..0eff090f10a 100644 --- a/packages/react-charts/src/components/ChartBullet/ChartBullet.tsx +++ b/packages/react-charts/src/components/ChartBullet/ChartBullet.tsx @@ -25,7 +25,8 @@ import { ChartContainer } from '../ChartContainer'; import { ChartLegend, ChartLegendOrientation, ChartLegendPosition } from '../ChartLegend'; import { ChartBulletStyles, ChartThemeDefinition } from '../ChartTheme'; import { ChartTooltip } from '../ChartTooltip'; -import { getComputedLegend, getPaddingForSide } from '../ChartUtils'; +import { getComputedLegend, getLegendItemsExtraHeight, getPaddingForSide } from "../ChartUtils"; +import { useEffect } from "react"; /** * ChartBullet renders a dataset as a bullet chart. @@ -207,12 +208,20 @@ export interface ChartBulletProps { */ labels?: string[] | number[] | ((data: any) => string | number | null); /** - * Allows legend items to wrap. A value of true allows the legend to wrap onto the next line - * if its container is not wide enough. + * @beta Allows legend items to wrap onto the next line if the chart is not wide enough. + * + * Note that the chart's SVG height and width are 100% by default, so it can be responsive itself. However, if you + * define the height and width of the chart's parent container, you must accommodate for extra legend height due to + * legend items wrapping onto the next line. When the height of the chart's parent container is too small, some legend + * items may not be visible. + * + * Alternatively, a callback function may be provided, which will be called after the legend's itemsPerRow property + * has been calculated. The value provided can be used to increase the chart's parent container height as legend + * items wrap onto the next line. If no adjustment is necessary, the value will be zero. * * Note: This is overridden by the legendItemsPerRow property */ - legendAllowWrap?: boolean; + legendAllowWrap?: boolean | ((extraHeight: number) => void); /** * The legend component to render with chart. */ @@ -766,8 +775,9 @@ export const ChartBullet: React.FunctionComponent = ({ } dx = -10; } + return getComputedLegend({ - allowWrap: legendAllowWrap, + allowWrap: legendAllowWrap === true || typeof legendAllowWrap === 'function', chartType: 'bullet', dx, dy, @@ -825,6 +835,7 @@ export const ChartBullet: React.FunctionComponent = ({ ...axisComponent.props }); + const computedLegend = getLegend(); const bulletChart = ( {axis} @@ -836,10 +847,23 @@ export const ChartBullet: React.FunctionComponent = ({ {comparativeErrorMeasure} {comparativeWarningMeasure} {getComparativeZeroMeasure()} - {getLegend()} + {computedLegend} ); + // Callback to compliment legendAllowWrap + useEffect(() => { + if (typeof legendAllowWrap === 'function') { + const extraHeight = getLegendItemsExtraHeight({ + legendData: computedLegend.props.data, + legendOrientation: computedLegend.props.orientation, + legendProps: computedLegend.props, + theme + }); + legendAllowWrap(extraHeight); + } + }, [computedLegend, legendAllowWrap, theme, width]); + return standalone ? ( {bulletChart} diff --git a/packages/react-charts/src/components/ChartBullet/examples/ChartBullet.md b/packages/react-charts/src/components/ChartBullet/examples/ChartBullet.md index 04b9fc3f63a..cbba0f0628a 100644 --- a/packages/react-charts/src/components/ChartBullet/examples/ChartBullet.md +++ b/packages/react-charts/src/components/ChartBullet/examples/ChartBullet.md @@ -92,6 +92,7 @@ class BulletChart extends React.Component { this.containerRef = React.createRef(); this.observer = () => {}; this.state = { + extraHeight: 0, width: 0 }; this.handleResize = () => { @@ -99,6 +100,15 @@ class BulletChart extends React.Component { this.setState({ width: this.containerRef.current.clientWidth }); } }; + this.handleLegendAllowWrap = (extraHeight) => { + if (extraHeight !== this.state.extraHeight) { + this.setState({ extraHeight }); + } + } + this.getHeight = (baseHeight) => { + const { extraHeight } = this.state; + return baseHeight + extraHeight; + }; } componentDidMount() { @@ -112,17 +122,18 @@ class BulletChart extends React.Component { render() { const { width } = this.state; + const height = this.getHeight(200); return ( -
+
`${datum.name}: ${datum.y}`} - legendAllowWrap + legendAllowWrap={this.handleLegendAllowWrap} legendPosition="bottom-left" maxDomain={{y: 100}} name="chart3" diff --git a/packages/react-charts/src/components/ChartDonut/ChartDonut.tsx b/packages/react-charts/src/components/ChartDonut/ChartDonut.tsx index 309744cfd29..e02b893a11b 100644 --- a/packages/react-charts/src/components/ChartDonut/ChartDonut.tsx +++ b/packages/react-charts/src/components/ChartDonut/ChartDonut.tsx @@ -308,12 +308,20 @@ export interface ChartDonutProps extends ChartPieProps { */ labels?: string[] | number[] | ((data: any) => string | number | null); /** - * Allows legend items to wrap. A value of true allows the legend to wrap onto the next line - * if its container is not wide enough. + * @beta Allows legend items to wrap onto the next line if the chart is not wide enough. + * + * Note that the chart's SVG height and width are 100% by default, so it can be responsive itself. However, if you + * define the height and width of the chart's parent container, you must accommodate for extra legend height due to + * legend items wrapping onto the next line. When the height of the chart's parent container is too small, some legend + * items may not be visible. + * + * Alternatively, a callback function may be provided, which will be called after the legend's itemsPerRow property + * has been calculated. The value provided can be used to increase the chart's parent container height as legend + * items wrap onto the next line. If no adjustment is necessary, the value will be zero. * * Note: This is overridden by the legendItemsPerRow property */ - legendAllowWrap?: boolean; + legendAllowWrap?: boolean | ((extraHeight: number) => void); /** * The legend component to render with chart. * @@ -580,7 +588,6 @@ export const ChartDonut: React.FunctionComponent = ({ capHeight = 1.1, containerComponent = , innerRadius, - legendAllowWrap, legendPosition = ChartCommonStyles.legend.position as ChartPieLegendPosition, name, padAngle, @@ -706,7 +713,6 @@ export const ChartDonut: React.FunctionComponent = ({ height={height} innerRadius={chartInnerRadius > 0 ? chartInnerRadius : 0} key="pf-chart-donut-pie" - legendAllowWrap={legendAllowWrap} legendPosition={legendPosition} name={name} padAngle={padAngle !== undefined ? padAngle : getPadAngle} diff --git a/packages/react-charts/src/components/ChartDonutUtilization/ChartDonutUtilization.tsx b/packages/react-charts/src/components/ChartDonutUtilization/ChartDonutUtilization.tsx index 5650c008bd1..57f21d6f5a1 100644 --- a/packages/react-charts/src/components/ChartDonutUtilization/ChartDonutUtilization.tsx +++ b/packages/react-charts/src/components/ChartDonutUtilization/ChartDonutUtilization.tsx @@ -292,12 +292,20 @@ export interface ChartDonutUtilizationProps extends ChartDonutProps { */ isStatic?: boolean; /** - * Allows legend items to wrap. A value of true allows the legend to wrap onto the next line - * if its container is not wide enough. + * @beta Allows legend items to wrap onto the next line if the chart is not wide enough. + * + * Note that the chart's SVG height and width are 100% by default, so it can be responsive itself. However, if you + * define the height and width of the chart's parent container, you must accommodate for extra legend height due to + * legend items wrapping onto the next line. When the height of the chart's parent container is too small, some legend + * items may not be visible. + * + * Alternatively, a callback function may be provided, which will be called after the legend's itemsPerRow property + * has been calculated. The value provided can be used to increase the chart's parent container height as legend + * items wrap onto the next line. If no adjustment is necessary, the value will be zero. * * Note: This is overridden by the legendItemsPerRow property */ - legendAllowWrap?: boolean; + legendAllowWrap?: boolean | ((extraHeight: number) => void); /** * The labelComponent prop takes in an entire label component which will be used * to create a label for the area. The new element created from the passed labelComponent diff --git a/packages/react-charts/src/components/ChartLegend/examples/ChartLegend.md b/packages/react-charts/src/components/ChartLegend/examples/ChartLegend.md index 27201b4d578..21f9b8536aa 100644 --- a/packages/react-charts/src/components/ChartLegend/examples/ChartLegend.md +++ b/packages/react-charts/src/components/ChartLegend/examples/ChartLegend.md @@ -131,13 +131,23 @@ class BulletChart extends React.Component { this.containerRef = React.createRef(); this.observer = () => {}; this.state = { + extraHeight: 0, width: 0 }; this.handleResize = () => { - if(this.containerRef.current && this.containerRef.current.clientWidth){ + if (this.containerRef.current && this.containerRef.current.clientWidth) { this.setState({ width: this.containerRef.current.clientWidth }); } }; + this.handleLegendAllowWrap = (extraHeight) => { + if (extraHeight !== this.state.extraHeight) { + this.setState({ extraHeight }); + } + } + this.getHeight = (baseHeight) => { + const { extraHeight } = this.state; + return baseHeight + extraHeight; + }; } componentDidMount() { @@ -151,37 +161,36 @@ class BulletChart extends React.Component { render() { const { width } = this.state; + const height = this.getHeight(200); return ( -
-
- `${datum.name}: ${datum.y}`} - legendAllowWrap - legendPosition="bottom-left" - maxDomain={{y: 100}} - name="chart3" - padding={{ - bottom: 50, - left: 50, - right: 50, - top: 100 // Adjusted to accommodate labels - }} - primarySegmentedMeasureData={[{ name: 'Measure', y: 25 }, { name: 'Measure', y: 60 }]} - primarySegmentedMeasureLegendData={[{ name: 'Measure 1' }, { name: 'Measure 2' }]} - qualitativeRangeData={[{ name: 'Range', y: 50 }, { name: 'Range', y: 75 }]} - qualitativeRangeLegendData={[{ name: 'Range 1' }, { name: 'Range 2' }]} - subTitle="Measure details" - title="Text label" - titlePosition="top-left" - width={width} - /> -
+
+ `${datum.name}: ${datum.y}`} + legendAllowWrap={this.handleLegendAllowWrap} + legendPosition="bottom-left" + maxDomain={{y: 100}} + name="chart3" + padding={{ + bottom: 50, + left: 50, + right: 50, + top: 100 // Adjusted to accommodate labels + }} + primarySegmentedMeasureData={[{ name: 'Measure', y: 25 }, { name: 'Measure', y: 60 }]} + primarySegmentedMeasureLegendData={[{ name: 'Measure 1' }, { name: 'Measure 2' }]} + qualitativeRangeData={[{ name: 'Range', y: 50 }, { name: 'Range', y: 75 }]} + qualitativeRangeLegendData={[{ name: 'Range 1' }, { name: 'Range 2' }]} + subTitle="Measure details" + title="Text label" + titlePosition="top-left" + width={width} + />
); } diff --git a/packages/react-charts/src/components/ChartPie/ChartPie.tsx b/packages/react-charts/src/components/ChartPie/ChartPie.tsx index c5818da94c0..2bbb5e720ea 100644 --- a/packages/react-charts/src/components/ChartPie/ChartPie.tsx +++ b/packages/react-charts/src/components/ChartPie/ChartPie.tsx @@ -27,7 +27,15 @@ import { ChartContainer } from '../ChartContainer'; import { ChartLegend, ChartLegendOrientation } from '../ChartLegend'; import { ChartCommonStyles, ChartThemeDefinition } from '../ChartTheme'; import { ChartTooltip } from '../ChartTooltip'; -import { getComputedLegend, useDefaultPatternProps, getPaddingForSide, getPatternDefs, getTheme } from '../ChartUtils'; +import { + getComputedLegend, + useDefaultPatternProps, + getPaddingForSide, + getPatternDefs, + getTheme, + getLegendItemsExtraHeight +} from "../ChartUtils"; +import { useEffect } from 'react'; export enum ChartPieLabelPosition { centroid = 'centroid', @@ -293,12 +301,20 @@ export interface ChartPieProps extends VictoryPieProps { */ labels?: string[] | number[] | ((data: any) => string | number | null); /** - * Allows legend items to wrap. A value of true allows the legend to wrap onto the next line - * if its container is not wide enough. + * @beta Allows legend items to wrap onto the next line if the chart is not wide enough. + * + * Note that the chart's SVG height and width are 100% by default, so it can be responsive itself. However, if you + * define the height and width of the chart's parent container, you must accommodate for extra legend height due to + * legend items wrapping onto the next line. When the height of the chart's parent container is too small, some legend + * items may not be visible. + * + * Alternatively, a callback function may be provided, which will be called after the legend's itemsPerRow property + * has been calculated. The value provided can be used to increase the chart's parent container height as legend + * items wrap onto the next line. If no adjustment is necessary, the value will be zero. * * Note: This is overridden by the legendItemsPerRow property */ - legendAllowWrap?: boolean; + legendAllowWrap?: boolean | ((extraHeight: number) => void); /** * The legend component to render with chart. * @@ -598,7 +614,7 @@ export const ChartPie: React.FunctionComponent = ({ return null; } return getComputedLegend({ - allowWrap: legendAllowWrap, + allowWrap: legendAllowWrap === true || typeof legendAllowWrap === 'function', chartType: 'pie', height, legendComponent: legend, @@ -630,12 +646,26 @@ export const ChartPie: React.FunctionComponent = ({ ) : null; + // Callback to compliment legendAllowWrap + const computedLegend = getLegend(); + useEffect(() => { + if (typeof legendAllowWrap === 'function') { + const extraHeight = getLegendItemsExtraHeight({ + legendData: computedLegend.props.data, + legendOrientation: computedLegend.props.orientation, + legendProps: computedLegend.props, + theme + }); + legendAllowWrap(extraHeight); + } + }, [computedLegend, legendAllowWrap, theme, width]); + return standalone ? ( {container} ) : ( {chart} - {getLegend()} + {computedLegend} {isPatternDefs && getPatternDefs({ patternId, colorScale: defaultColorScale, patternUnshiftIndex })} ); diff --git a/packages/react-charts/src/components/ChartUtils/chart-legend.ts b/packages/react-charts/src/components/ChartUtils/chart-legend.ts index c4ebd392ea8..28b3bfe9c47 100644 --- a/packages/react-charts/src/components/ChartUtils/chart-legend.ts +++ b/packages/react-charts/src/components/ChartUtils/chart-legend.ts @@ -73,7 +73,6 @@ export const getComputedLegend = ({ const legendItemsProps = legendComponent.props ? legendComponent.props : {}; const legendItemsPerRow = allowWrap ? getLegendItemsPerRow({ - chartType, dx, height, legendData: legendItemsProps.data, @@ -233,6 +232,38 @@ export const getLegendItemsPerRow = ({ return itemsPerRow; }; +/** + * Returns the extra height required to accommodate wrapped legend items + * @private + */ +export const getLegendItemsExtraHeight = ({ + legendData, + legendOrientation, + legendProps, + theme +}: ChartLegendDimensionsInterface) => { + // Get legend dimensions + const legendDimensions = getLegendDimensions({ + legendData, + legendOrientation, + legendProps, + theme + }); + + // Get legend dimensions without any wrapped items + const legendDimensionsNoWrap = getLegendDimensions({ + legendData, + legendOrientation, + legendProps: { + ...legendProps, + itemsPerRow: undefined + }, + theme + }); + + return Math.abs(legendDimensions.height - legendDimensionsNoWrap.height); +}; + /** * Returns x coordinate for legend * @private diff --git a/packages/react-charts/src/components/Patterns/examples/patterms.md b/packages/react-charts/src/components/Patterns/examples/patterms.md index 95942336f3d..5269eb57ff6 100644 --- a/packages/react-charts/src/components/Patterns/examples/patterms.md +++ b/packages/react-charts/src/components/Patterns/examples/patterms.md @@ -713,62 +713,104 @@ import chart_color_green_300 from '@patternfly/react-tokens/dist/esm/chart_color ```js import React from 'react'; import { ChartPie, ChartThemeColor } from '@patternfly/react-charts'; +import { getResizeObserver } from '@patternfly/react-core'; -
- `${datum.x}: ${datum.y}`} - legendData={[ - { name: 'Cats: 6' }, - { name: 'Dogs: 6' }, - { name: 'Birds: 6' }, - { name: 'Fish: 6' }, - { name: 'Rabbits: 6' }, - { name: 'Squirels: 6' }, - { name: 'Chipmunks: 6' }, - { name: 'Bats: 6' }, - { name: 'Ducks: 6' }, - { name: 'Geese: 6' }, - { name: 'Bobcat: 6' }, - { name: 'Foxes: 6' }, - { name: 'Coyotes: 6' }, - { name: 'Deer: 6' }, - { name: 'Bears: 6' }, - ]} - legendAllowWrap - legendPosition="bottom" - name="chart12" - padding={{ - bottom: 110, - left: 20, - right: 20, - top: 20 - }} - themeColor={ChartThemeColor.multiOrdered} - width={600} - /> -
+class PatternsPie extends React.Component { + constructor(props) { + super(props); + this.containerRef = React.createRef(); + this.observer = () => {}; + this.state = { + extraHeight: 0, + width: 0 + }; + this.handleResize = () => { + if (this.containerRef.current && this.containerRef.current.clientWidth) { + this.setState({ width: this.containerRef.current.clientWidth }); + } + }; + this.handleLegendAllowWrap = (extraHeight) => { + if (extraHeight !== this.state.extraHeight) { + this.setState({ extraHeight }); + } + } + this.getHeight = (baseHeight) => { + const { extraHeight } = this.state; + return baseHeight + extraHeight; + }; + } + + componentDidMount() { + this.observer = getResizeObserver(this.containerRef.current, this.handleResize); + this.handleResize(); + } + + componentWillUnmount() { + this.observer(); + } + + render() { + const { width } = this.state; + const height = this.getHeight(260); + return ( +
+ `${datum.x}: ${datum.y}`} + legendData={[ + { name: 'Cats: 6' }, + { name: 'Dogs: 6' }, + { name: 'Birds: 6' }, + { name: 'Fish: 6' }, + { name: 'Rabbits: 6' }, + { name: 'Squirels: 6' }, + { name: 'Chipmunks: 6' }, + { name: 'Bats: 6' }, + { name: 'Ducks: 6' }, + { name: 'Geese: 6' }, + { name: 'Bobcat: 6' }, + { name: 'Foxes: 6' }, + { name: 'Coyotes: 6' }, + { name: 'Deer: 6' }, + { name: 'Bears: 6' }, + ]} + legendAllowWrap={this.handleLegendAllowWrap} + legendPosition="bottom" + name="chart12" + padding={{ + bottom: this.getHeight(50), // This must be adjusted to maintain the aspec ratio + left: 20, + right: 20, + top: 20 + }} + themeColor={ChartThemeColor.multiOrdered} + width={width} + /> +
+ ); + } +} ``` ## Documentation diff --git a/packages/react-charts/src/components/ResizeObserver/examples/resizeObserver.md b/packages/react-charts/src/components/ResizeObserver/examples/resizeObserver.md index 9a3bc0982ef..adfe14c99bc 100644 --- a/packages/react-charts/src/components/ResizeObserver/examples/resizeObserver.md +++ b/packages/react-charts/src/components/ResizeObserver/examples/resizeObserver.md @@ -62,6 +62,7 @@ class BulletChart extends React.Component { this.containerRef = React.createRef(); this.observer = () => {}; this.state = { + extraHeight: 0, width: 0 }; this.handleResize = () => { @@ -69,6 +70,15 @@ class BulletChart extends React.Component { this.setState({ width: this.containerRef.current.clientWidth }); } }; + this.handleLegendAllowWrap = (extraHeight) => { + if (extraHeight !== this.state.extraHeight) { + this.setState({ extraHeight }); + } + } + this.getHeight = (baseHeight) => { + const { extraHeight } = this.state; + return baseHeight + extraHeight; + }; } componentDidMount() { @@ -82,17 +92,18 @@ class BulletChart extends React.Component { render() { const { width } = this.state; + const height = this.getHeight(200); return ( -
+
`${datum.name}: ${datum.y}`} - legendAllowWrap + legendAllowWrap={this.handleLegendAllowWrap} legendPosition="bottom-left" maxDomain={{y: 100}} name="chart1"