diff --git a/packages/patternfly-4/react-charts/src/components/ChartDonutUtilization/ChartDonutThreshold.tsx b/packages/patternfly-4/react-charts/src/components/ChartDonutUtilization/ChartDonutThreshold.tsx index fde2c785622..5e3f7343830 100644 --- a/packages/patternfly-4/react-charts/src/components/ChartDonutUtilization/ChartDonutThreshold.tsx +++ b/packages/patternfly-4/react-charts/src/components/ChartDonutUtilization/ChartDonutThreshold.tsx @@ -122,8 +122,14 @@ export interface ChartDonutThresholdProps extends ChartPieProps { * height of the chart in number of pixels, but instead define an aspect ratio for the chart. The exact number of * pixels will depend on the size of the container the chart is rendered into. * - * Note: The parent container must be set to the same height in order to maintain the aspect ratio. Otherwise, the - * innerRadius may need to be set when using this property. + * Note: When adding a legend, height (the overall SVG height) may need to be larger than donutHeight (the donut size) + * in order to accommodate the extra legend. + * + * By default, donutHeight is the min. of either height or width. This covers most use cases in order to accommodate + * legends within the same SVG. However, donutHeight (not height) may need to be set in order to adjust the donut + * height. + * + * The innerRadius may also need to be set when changing the donut size. */ donutHeight?: number; /** @@ -138,8 +144,13 @@ export interface ChartDonutThresholdProps extends ChartPieProps { * height of the chart in number of pixels, but instead define an aspect ratio for the chart. The exact number of * pixels will depend on the size of the container the chart is rendered into. * - * Note: The parent container must be set to the same height in order to maintain the aspect ratio. Otherwise, the - * innerRadius may need to be set when using this property. + * Note: When adding a legend, width (the overall SVG width) may need to be larger than donutWidth (the donut size) + * in order to accommodate the extra legend. + * + * By default, donutWidth is the min. of either height or width. This covers most use cases in order to accommodate + * legends within the same SVG. However, donutWidth (not width) may need to be set in order to adjust the donut width. + * + * The innerRadius may also need to be set when changing the donut size. */ donutWidth?: number; /** @@ -211,7 +222,10 @@ export interface ChartDonutThresholdProps extends ChartPieProps { * height of the chart in number of pixels, but instead define an aspect ratio for the chart. The exact number of * pixels will depend on the size of the container the chart is rendered into. * - * Note: innerRadius may need to be set when using this property. + * Note: When adding a legend, height (the overall SVG height) may need to be larger than donutHeight (the donut size) + * in order to accommodate the extra legend. + * + * Typically, the parent container is set to the same height in order to maintain the aspect ratio. */ height?: number; /** @@ -344,7 +358,10 @@ export interface ChartDonutThresholdProps extends ChartPieProps { * height of the chart in number of pixels, but instead define an aspect ratio for the chart. The exact number of * pixels will depend on the size of the container the chart is rendered into. * - * Note: innerRadius may need to be set when using this property. + * Note: When adding a legend, width (the overall SVG width) may need to be larger than donutWidth (the donut size) + * in order to accommodate the extra legend. + * + * Typically, the parent container is set to the same width in order to maintain the aspect ratio. */ width?: number; /** @@ -383,11 +400,12 @@ export const ChartDonutThreshold: React.FunctionComponent { // Returns computed data representing pie chart slices @@ -410,42 +428,48 @@ export const ChartDonutThreshold: React.FunctionComponent { + const dynamicWidth = donutWidth - (theme.pie.width - dynamicTheme.pie.width); switch (orientation) { case 'left': - return Math.round((theme.pie.width - dynamicTheme.pie.width) / 2); + return Math.round((donutWidth - dynamicWidth) / 2); case 'right': - return -Math.round((theme.pie.width - dynamicTheme.pie.width) / 2); + return -Math.round((donutWidth - dynamicWidth) / 2); default: return 0; } }; // Returns the vertical shift for the dynamic utilization donut cart - const getDynamicDonutDy = (dynamicTheme: ChartThemeDefinition) => - Math.round((theme.pie.height - dynamicTheme.pie.height) / 2); + const getDynamicDonutDy = (dynamicTheme: ChartThemeDefinition) => { + const dynamicHeight = donutHeight - (theme.pie.height - dynamicTheme.pie.height); + return Math.round((donutHeight - dynamicHeight) / 2); + } // Render dynamic utilization donut cart const renderChildren = () => React.Children.toArray(children).map(child => { - const datum = getData([{ ...child.props.data }]); - const orientation = child.props.donutOrientation || donutOrientation; - const dynamicTheme = - child.props.theme || - getDonutThresholdDynamicTheme(child.props.themeColor || themeColor, - child.props.themeVariant || themeVariant); - return React.cloneElement(child, { - donutDx: child.props.donutDx || getDynamicDonutDx(dynamicTheme, orientation), - donutDy: child.props.donutDy || getDynamicDonutDy(dynamicTheme), - donutHeight: child.props.donutHeight || dynamicTheme.pie.height, - donutOrientation: orientation, - donutWidth: child.props.donutWidth || dynamicTheme.pie.width, - endAngle: child.props.endAngle || 360 * (datum[0]._y ? datum[0]._y / 100 : 100), - height: child.props.height || height, - showStatic: child.props.showStatic || false, - standalone: false, - theme: dynamicTheme, - width: child.props.width || width - }); + if (child.props) { + const datum = getData([{ ...child.props.data }]); + const orientation = child.props.donutOrientation || donutOrientation; + const dynamicTheme = + child.props.theme || + getDonutThresholdDynamicTheme(child.props.themeColor || themeColor, + child.props.themeVariant || themeVariant); + return React.cloneElement(child, { + donutDx: child.props.donutDx || getDynamicDonutDx(dynamicTheme, orientation), + donutDy: child.props.donutDy || getDynamicDonutDy(dynamicTheme), + donutHeight: child.props.donutHeight || donutHeight - (theme.pie.height - dynamicTheme.pie.height), + donutOrientation: orientation, + donutWidth: child.props.donutWidth || donutWidth - (theme.pie.width - dynamicTheme.pie.width), + endAngle: child.props.endAngle || 360 * (datum[0]._y ? datum[0]._y / 100 : 100), + height: child.props.height || height, + showStatic: child.props.showStatic || false, + standalone: false, + theme: dynamicTheme, + width: child.props.width || width + }); + } + return child; }); // Static threshold dount chart diff --git a/packages/patternfly-4/react-charts/src/components/ChartDonutUtilization/ChartDonutUtilization.tsx b/packages/patternfly-4/react-charts/src/components/ChartDonutUtilization/ChartDonutUtilization.tsx index 425be3bd16e..42874621cc6 100644 --- a/packages/patternfly-4/react-charts/src/components/ChartDonutUtilization/ChartDonutUtilization.tsx +++ b/packages/patternfly-4/react-charts/src/components/ChartDonutUtilization/ChartDonutUtilization.tsx @@ -60,6 +60,13 @@ export interface ChartDonutUtilizationProps extends ChartPieProps { * {duration: 500, onExit: () => {}, onEnter: {duration: 500, before: () => ({y: 0})})} */ animate?: AnimatePropTypeInterface; + /** + * The capHeight prop defines a text metric for the font being used: the expected height of capital letters. + * This is necessary because of SVG, which (a) positions the *bottom* of the text at `y`, and (b) has no notion of + * line height. The value should ideally use the same units as `lineHeight` and `dy`, preferably ems. If given a + * unitless number, it is assumed to be ems. + */ + capHeight?: StringOrNumberOrCallback; /** * The categories prop specifies how categorical data for a chart should be ordered. * This prop should be given as an array of string values, or an object with @@ -134,8 +141,14 @@ export interface ChartDonutUtilizationProps extends ChartPieProps { * height of the chart in number of pixels, but instead define an aspect ratio for the chart. The exact number of * pixels will depend on the size of the container the chart is rendered into. * - * Note: The parent container must be set to the same height in order to maintain the aspect ratio. Otherwise, the - * innerRadius may need to be set when using this property. + * Note: When adding a legend, height (the overall SVG height) may need to be larger than donutHeight (the donut size) + * in order to accommodate the extra legend. + * + * By default, donutHeight is the min. of either height or width. This covers most use cases in order to accommodate + * legends within the same SVG. However, donutHeight (not height) may need to be set in order to adjust the donut + * height. + * + * The innerRadius may also need to be set when changing the donut size. */ donutHeight?: number; /** @@ -150,8 +163,13 @@ export interface ChartDonutUtilizationProps extends ChartPieProps { * height of the chart in number of pixels, but instead define an aspect ratio for the chart. The exact number of * pixels will depend on the size of the container the chart is rendered into. * - * Note: The parent container must be set to the same height in order to maintain the aspect ratio. Otherwise, the - * innerRadius may need to be set when using this property. + * Note: When adding a legend, width (the overall SVG width) may need to be larger than donutWidth (the donut size) + * in order to accommodate the extra legend. + * + * By default, donutWidth is the min. of either height or width. This covers most use cases in order to accommodate + * legends within the same SVG. However, donutWidth (not width) may need to be set in order to adjust the donut width. + * + * The innerRadius may also need to be set when changing the donut size. */ donutWidth?: number; /** @@ -223,7 +241,10 @@ export interface ChartDonutUtilizationProps extends ChartPieProps { * height of the chart in number of pixels, but instead define an aspect ratio for the chart. The exact number of * pixels will depend on the size of the container the chart is rendered into. * - * Note: innerRadius may need to be set when using this property. + * Note: When adding a legend, height (the overall SVG height) may need to be larger than donutHeight (the donut size) + * in order to accommodate the extra legend. + * + * Typically, the parent container is set to the same height in order to maintain the aspect ratio. */ height?: number; /** @@ -250,7 +271,9 @@ export interface ChartDonutUtilizationProps extends ChartPieProps { */ labelPosition?: 'startAngle' | 'endAngle' | 'centroid'; /** - * The legend component to render with chart. This overrides other legend props. + * The legend component to render with chart. + * + * Note: Default legend properties may be applied */ legendComponent?: React.ReactElement; /** @@ -261,8 +284,6 @@ export interface ChartDonutUtilizationProps extends ChartPieProps { * Each data point may be any format you wish (depending on the `x` and `y` accessor props), * but by default, an object with x and y properties is expected. * - * Note: Not compatible with legendComponent prop. - * * @example legendData={[{ name: `GBps capacity - 45%` }, { name: 'Unused' }]} */ legendData?: any[]; @@ -274,9 +295,6 @@ export interface ChartDonutUtilizationProps extends ChartPieProps { * and text-wrapping is not currently supported, so "vertical" * orientation is both the default setting and recommended for * displaying many series of data. - * - * Note: May need to set legendHeight and legendWidth in order to position properly. - * Not compatible with legendComponent prop. */ legendOrientation?: 'horizontal' | 'vertical'; /** @@ -402,7 +420,10 @@ export interface ChartDonutUtilizationProps extends ChartPieProps { * height of the chart in number of pixels, but instead define an aspect ratio for the chart. The exact number of * pixels will depend on the size of the container the chart is rendered into. * - * Note: innerRadius may need to be set when using this property. + * Note: When adding a legend, width (the overall SVG width) may need to be larger than donutWidth (the donut size) + * in order to accommodate the extra legend. + * + * Typically, the parent container is set to the same width in order to maintain the aspect ratio. */ width?: number; /** @@ -449,11 +470,12 @@ export const ChartDonutUtilization: React.FunctionComponent { // Returns computed data representing pie chart slices @@ -490,9 +512,31 @@ export const ChartDonutUtilization: React.FunctionComponent { if (legendComponent) { - return legendComponent; - } - if (legendData) { + const props = legendComponent.props; + return React.cloneElement(legendComponent, { + data: props.data ? props.data : legendData, + orientation: props.legendOrientation ? props.legendOrientation : legendOrientation, + standalone: false, + theme: props.theme ? props.theme : theme, + x: props.x ? props.x : getLegendX({ + chartOrientation: donutOrientation, + legendOrientation: props.legendOrientation ? props.legendOrientation : legendOrientation, + legendWidth: getLegendDimensions().width, + theme, + width + }), + y: props.y ? props.y : getLegendY({ + chartDy: donutDy, + chartHeight: donutHeight, + chartOrientation: donutOrientation, + chartType: 'pie', + height, + legendData: props.data ? props.data : legendData, + legendHeight: getLegendDimensions().height, + theme + }) + }); + } else if (legendData) { return ( @@ -522,6 +566,25 @@ export const ChartDonutUtilization: React.FunctionComponent { + if (legendComponent) { + const props = legendComponent.props; + return (VictoryLegend as any).getDimensions({ + data: props.data ? props.data : legendData, + orientation: props.legendOrientation ? props.legendOrientation : legendOrientation, + theme: props.theme ? props.theme : theme, + }); + } else if (legendData) { + return (VictoryLegend as any).getDimensions({ + data: legendData, + orientation: legendOrientation, + theme + }); + } + return {}; + } + // Returns theme based on threshold and current value const getThresholdTheme = () => { const newTheme = { ...theme }; @@ -543,18 +606,10 @@ export const ChartDonutUtilization: React.FunctionComponent `; diff --git a/packages/patternfly-4/react-charts/src/components/ChartDonutUtilization/__snapshots__/ChartDonutUtilization.test.tsx.snap b/packages/patternfly-4/react-charts/src/components/ChartDonutUtilization/__snapshots__/ChartDonutUtilization.test.tsx.snap index 31cd7c1e016..846aca8d1e8 100644 --- a/packages/patternfly-4/react-charts/src/components/ChartDonutUtilization/__snapshots__/ChartDonutUtilization.test.tsx.snap +++ b/packages/patternfly-4/react-charts/src/components/ChartDonutUtilization/__snapshots__/ChartDonutUtilization.test.tsx.snap @@ -1015,8 +1015,8 @@ exports[`renders component data 1`] = ` } textAnchor="middle" verticalAnchor="middle" - x={115} - y={115} + x={100} + y={100} /> `; diff --git a/packages/patternfly-4/react-charts/src/components/ChartDonutUtilization/examples/ChartDonutUtilization.md b/packages/patternfly-4/react-charts/src/components/ChartDonutUtilization/examples/ChartDonutUtilization.md index 83c9a08756d..d0eff3dd7eb 100644 --- a/packages/patternfly-4/react-charts/src/components/ChartDonutUtilization/examples/ChartDonutUtilization.md +++ b/packages/patternfly-4/react-charts/src/components/ChartDonutUtilization/examples/ChartDonutUtilization.md @@ -58,7 +58,7 @@ class UtilizationChart extends React.Component { const { spacer, used } = this.state; return (
-
+
datum.x ? `${datum.x} - ${datum.y}%` : null} @@ -108,7 +108,7 @@ class UtilizationChart extends React.Component { const { spacer, used } = this.state; return (
-
+
datum.x ? `${datum.x} - ${datum.y}%` : null} @@ -133,9 +133,10 @@ import React from 'react'; import { ChartDonutUtilization } from '@patternfly/react-charts';
-
+
datum.x ? `${datum.x} - ${datum.y}%` : null} @@ -157,7 +158,7 @@ import React from 'react'; import { ChartDonutUtilization } from '@patternfly/react-charts';
-
+
-
+
datum.x ? datum.x : null} - width={475} > -
+
datum.x ? datum.x : null} @@ -245,7 +245,7 @@ class ThresholdChart extends React.Component { } ``` -## Green donut utilization chart with static thresholds and right-aligned legend +## Green donut utilization chart with static thresholds and right-aligned (custom) legend ```js import React from 'react'; import { ChartDonutThreshold, ChartDonutUtilization, ChartThemeColor, ChartThemeVariant } from '@patternfly/react-charts'; @@ -273,7 +273,7 @@ class ThresholdChart extends React.Component { const { used } = this.state; return (
-
+
datum.x ? datum.x : null} @@ -282,7 +282,11 @@ class ThresholdChart extends React.Component { datum.x ? `${datum.x} - ${datum.y}%` : null} - legendData={[{ name: `GBps capacity - ${used}%` }, { name: 'Warning threshold at - 60%' }, { name: 'Danger threshold at - 90%' }]} + legendComponent={ + + } subTitle="of 100 GBps" title={`${used}%`} themeColor={ChartThemeColor.green} @@ -303,13 +307,13 @@ import React from 'react'; import { ChartDonutThreshold, ChartDonutUtilization } from '@patternfly/react-charts';
-
+
datum.x ? datum.x : null} - width={275} + width={230} > -
+
``` + +## Small donut utilization chart +```js +import React from 'react'; +import { ChartDonutUtilization } from '@patternfly/react-charts'; + +
+
+ datum.x ? `${datum.x} - ${datum.y}%` : null} + subTitle="of 100 GBps" + title="75%" + width={150} + /> +
+
+``` + +## Small donut utilization chart with right-aligned legend +```js +import React from 'react'; +import { ChartDonutUtilization } from '@patternfly/react-charts'; + +class UtilizationChart extends React.Component { + constructor(props) { + super(props); + this.state = { + spacer: '', + used: 0 + }; + } + + componentDidMount() { + this.interval = setInterval(() => { + const { used } = this.state; + const val = (used + 10) % 100; + this.setState({ + spacer: val < 10 ? ' ' : '', + used: val + }); + }, 1000); + } + + componentWillUnmount() { + clearInterval(this.interval); + } + + render() { + const { spacer, used } = this.state; + return ( +
+
+ datum.x ? `${datum.x} - ${datum.y}%` : null} + legendData={[{ name: `GBps capacity - ${spacer}${used}%` }, { name: 'Unused' }]} + subTitle="of 100 GBps" + title={`${used}%`} + thresholds={[{ value: 60 }, { value: 90 }]} + width={350} + /> +
+
+ ); + } +} +``` + +## Small donut utilization chart with static thresholds +```js +import React from 'react'; +import { ChartDonutThreshold, ChartDonutUtilization } from '@patternfly/react-charts'; + +
+
+ datum.x ? datum.x : null} + width={175} + > + datum.x ? `${datum.x} - ${datum.y}%` : null} + subTitle="of 100 GBps" + title="45%" + /> + +
+
+``` + +## Small donut utilization chart with static thresholds and right-aligned legend +```js +import React from 'react'; +import { ChartDonutThreshold, ChartDonutUtilization } from '@patternfly/react-charts'; + +class ThresholdChart extends React.Component { + constructor(props) { + super(props); + this.state = { + used: 0 + }; + } + + componentDidMount() { + this.interval = setInterval(() => { + const { used } = this.state; + this.setState({ used: (used + 10) % 100 }); + }, 1000); + } + + componentWillUnmount() { + clearInterval(this.interval); + } + + render() { + const { used } = this.state; + return ( +
+
+ datum.x ? datum.x : null} + width={425} + > + datum.x ? `${datum.x} - ${datum.y}%` : null} + legendData={[{ name: `GBps capacity - ${used}%` }, { name: 'Warning threshold at - 60%' }, { name: 'Danger threshold at - 90%' }]} + subTitle="of 100 GBps" + title={`${used}%`} + thresholds={[{ value: 60 }, { value: 90 }]} + /> + +
+
+ ); + } +} +``` diff --git a/packages/patternfly-4/react-charts/src/components/ChartDonutUtilization/examples/chart-donut-utilization.scss b/packages/patternfly-4/react-charts/src/components/ChartDonutUtilization/examples/chart-donut-utilization.scss index 13de880e58a..8e37a9a8f27 100644 --- a/packages/patternfly-4/react-charts/src/components/ChartDonutUtilization/examples/chart-donut-utilization.scss +++ b/packages/patternfly-4/react-charts/src/components/ChartDonutUtilization/examples/chart-donut-utilization.scss @@ -1,18 +1,33 @@ .ws-preview { & > * { - .donut-threshold-chart-horz { + .donut-threshold-chart { height: 230px; - width: 492px; + width: 230px; + } + + .donut-threshold-chart-sm { + height: 175px; + width: 175px; } - .donut-threshold-chart-horz-sm { + .donut-threshold-chart-legend-bottom { + height: 325px; + width: 230px; + } + + .donut-threshold-chart-legend-left { height: 230px; width: 475px; } - .donut-threshold-chart-vert { - height: 325px; - width: 275px; + .donut-threshold-chart-legend-right { + height: 230px; + width: 492px; + } + + .donut-threshold-chart-legend-right-sm { + height: 175px; + width: 425px; } .donut-utilization-chart { @@ -20,19 +35,29 @@ width: 230px; } - .donut-utilization-chart-horz { - height: 230px; - width: 435px; + .donut-utilization-chart-sm { + height: 150px; + width: 150px; + } + + .donut-utilization-chart-legend-bottom { + height: 275px; + width: 300px; } - .donut-utilization-chart-horz-sm { + .donut-utilization-chart-legend-left { height: 230px; width: 425px; } - .donut-utilization-chart-vert { - height: 275px; - width: 300px; + .donut-utilization-chart-legend-right { + height: 230px; + width: 435px; + } + + .donut-utilization-chart-legend-right-sm { + height: 150px; + width: 350px; } } }