UNPKG

@patternfly/react-charts

Version:

This library provides a set of React chart components for use with the PatternFly reference implementation.

541 lines (507 loc) • 14.7 kB
import { cloneElement } from 'react'; import defaults from 'lodash/defaults'; import { Helpers, TextSize } from 'victory-core'; import { ElementPadding } from 'victory-core/src/victory-util/helpers'; import { VictoryLegend } from 'victory-legend'; import { ChartLegendProps } from '../ChartLegend/ChartLegend'; import { ChartCommonStyles } from '../ChartTheme/ChartStyles'; import { ChartThemeDefinition } from '../ChartTheme/ChartTheme'; import { getLabelTextSize } from '../ChartUtils/chart-label'; import { getPieOrigin } from './chart-origin'; interface ChartLegendInterface { allowWrap?: boolean; // Allow legend items to wrap to the next line chartType?: string; // The type of chart (e.g., pie) to lookup for props colorScale?: any; // The color scale that will be applied to the chart dx?: number; // Horizontal shift from the x coordinate dy?: number; // Vertical shift from the x coordinate height: number; // Overall height of SVG legendComponent: React.ReactElement<any>; // The base legend component to render orientation?: 'horizontal' | 'vertical'; // Orientation of legend padding: ElementPadding; // Chart padding patternScale?: string[]; // Legend symbol patterns position: 'bottom' | 'bottom-left' | 'right'; // The legend position theme: ChartThemeDefinition; // The theme that will be applied to the chart themeColor?: string; // The theme color that will be applied to the chart width: number; // Overall width of SVG } interface ChartLegendDimensionsInterface { legendData: any[]; // The legend data used to determine width legendOrientation: 'horizontal' | 'vertical'; // Orientation of legend legendProps: ChartLegendProps; // Legend properties theme: ChartThemeDefinition; // The theme that will be applied to the chart } interface ChartLegendPositionInterface { chartType?: string; // The type of chart (e.g., pie) to lookup for props dx?: number; // Horizontal shift from the x coordinate dy?: number; // Vertical shift from the x coordinate height?: number; // Overall height of SVG legendData: any[]; // The legend data used to determine width legendOrientation: 'horizontal' | 'vertical'; // Orientation of legend legendPosition: 'bottom' | 'bottom-left' | 'right'; // Position of legend legendProps: any; // The legend props used to determine width padding?: ElementPadding; // Chart padding theme: ChartThemeDefinition; // The theme that will be applied to the chart width?: number; // Overall width of SVG } interface ChartLegendTextMaxSizeInterface { legendData: any[]; // The legend data used to determine width theme: ChartThemeDefinition; // The theme that will be applied to the chart } /** * Returns the max text length in a legend data set to calculate the x offset for right aligned legends. * * @private Not intended as public API and subject to change */ export const getLegendMaxTextWidth = (legendData: any[], theme: ChartThemeDefinition) => { let legendXOffset = 0; legendData.map((data: any) => { const labelWidth = getLabelTextSize({ text: data.name, theme }).width; if (labelWidth > legendXOffset) { legendXOffset = labelWidth; } }); return legendXOffset; }; /** * Returns a legend which has been positioned per the given chart properties * * @private Not intended as public API and subject to change */ export const getComputedLegend = ({ allowWrap = true, chartType = 'chart', colorScale, dx = 0, dy = 0, height, legendComponent, padding, patternScale, position = ChartCommonStyles.legend.position, theme, themeColor, width, // destructure last orientation = theme.legend.orientation as any }: ChartLegendInterface) => { // Get the number of legend items per row const legendItemsProps = legendComponent.props ? legendComponent.props : {}; const legendItemsPerRow = allowWrap ? getLegendItemsPerRow({ dx, height, legendData: legendItemsProps.data, legendOrientation: legendItemsProps.legendOrientation ? legendItemsProps.legendOrientation : orientation, legendPosition: position, legendProps: legendItemsProps, padding, theme, width }) : undefined; // Include new itemsPerRow prop when determining x and y position const legendPositionProps = defaults({}, legendComponent.props, { itemsPerRow: legendItemsPerRow }); const legendX = getLegendX({ chartType, dx, height, legendData: legendPositionProps.data, legendOrientation: legendPositionProps.legendOrientation ? legendPositionProps.legendOrientation : orientation, legendPosition: position, legendProps: legendPositionProps, padding, theme, width }); const legendY = getLegendY({ chartType, dy, height, legendData: legendPositionProps.data, legendOrientation: legendPositionProps.legendOrientation ? legendPositionProps.legendOrientation : orientation, legendProps: legendPositionProps, legendPosition: position, padding, theme, width }); // Clone legend with updated props const legendProps = defaults({}, legendComponent.props, { colorScale, itemsPerRow: legendItemsPerRow, orientation, patternScale, standalone: false, theme, themeColor, x: legendX > 0 ? legendX : 0, y: legendY > 0 ? legendY : 0 }); return cloneElement(legendComponent, legendProps); }; /** * Returns legend dimensions * * @private Not intended as public API and subject to change */ export const getLegendDimensions = ({ legendData, legendOrientation, legendProps, theme }: ChartLegendDimensionsInterface) => { if (legendData || legendProps.data) { return (VictoryLegend as any).getDimensions({ data: legendData, orientation: legendOrientation, theme, ...legendProps // override above }); } return {}; }; /** * Returns true if the legend is smaller than its container * * @private Not intended as public API and subject to change */ const doesLegendFit = ({ dx = 0, height, legendPosition, legendData, legendOrientation, legendProps, padding, theme, width }: ChartLegendPositionInterface) => { const { left, right } = Helpers.getPadding(padding); const chartSize = { height, // Fixed size width: width - left - right }; const legendDimensions = getLegendDimensions({ legendData, legendOrientation, legendProps, theme }); let occupiedWidth = 0; switch (legendPosition) { case 'bottom-left': occupiedWidth = left + dx; break; case 'right': occupiedWidth = chartSize.width + ChartCommonStyles.legend.margin + left + dx; break; default: occupiedWidth = dx; break; } return width - occupiedWidth > legendDimensions.width; }; /** * Returns the number of legend items per row * * @private Not intended as public API and subject to change */ const getLegendItemsPerRow = ({ dx, height, legendPosition, legendData, legendOrientation, legendProps, padding, theme, width }: ChartLegendPositionInterface) => { let itemsPerRow = legendData ? legendData.length : 0; for (let i = itemsPerRow; i > 0; i--) { const fits = doesLegendFit({ dx, height, legendPosition, legendData, legendOrientation, legendProps: { ...legendProps, itemsPerRow: i }, padding, theme, width }); if (fits) { itemsPerRow = i; break; } } return itemsPerRow; }; /** * Returns the extra height required to accommodate wrapped legend items * * @private Not intended as public API and subject to change */ 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 Not intended as public API and subject to change */ const getLegendX = ({ chartType, ...rest }: ChartLegendPositionInterface) => chartType === 'pie' ? getPieLegendX(rest) : getChartLegendX(rest); /** * Returns y coordinate for legend * * @private Not intended as public API and subject to change */ const getLegendY = ({ chartType, ...rest }: ChartLegendPositionInterface) => { switch (chartType) { case 'pie': return getPieLegendY(rest); case 'bullet': return getBulletLegendY(rest); default: return getChartLegendY(rest); } }; /** * Returns y coordinate for bullet legends * * @private Not intended as public API and subject to change */ const getBulletLegendY = ({ dy = 0, height, legendPosition, legendData, legendOrientation, legendProps, padding, theme, width }: ChartLegendPositionInterface) => { const { left, right } = Helpers.getPadding(padding); const chartSize = { height, // Fixed size width: width - left - right }; switch (legendPosition) { case 'bottom': case 'bottom-left': return chartSize.height + ChartCommonStyles.legend.margin + dy; case 'right': { // Legend height with padding const legendDimensions = getLegendDimensions({ legendData, legendOrientation, legendProps, theme }); const legendPadding = (legendDataArr: any[]) => (legendDataArr && legendDataArr.length > 0 ? 17 : 0); return (chartSize.height - legendDimensions.height) / 2 + legendPadding(legendData); } default: return dy; } }; /** * Returns x coordinate for chart legends * * @private Not intended as public API and subject to change */ const getChartLegendX = ({ dx = 0, height, legendData, legendOrientation, legendPosition, legendProps, padding, theme, width }: ChartLegendPositionInterface) => { const { top, bottom, left, right } = Helpers.getPadding(padding); const chartSize = { height: Math.abs(height - (bottom + top)), width: Math.abs(width - (left + right)) }; const legendDimensions = getLegendDimensions({ legendData, legendOrientation, legendProps, theme }); switch (legendPosition) { case 'bottom': return width > legendDimensions.width ? Math.round((width - legendDimensions.width) / 2) + dx : dx; case 'bottom-left': return left + dx; case 'right': return chartSize.width + ChartCommonStyles.legend.margin + left + dx; default: return dx; } }; /** * Returns y coordinate for chart legends * * @private Not intended as public API and subject to change */ const getChartLegendY = ({ dy = 0, height, legendPosition, legendData, legendOrientation, legendProps, padding, theme, width }: ChartLegendPositionInterface) => { const { top, bottom, left, right } = Helpers.getPadding(padding); const chartSize = { height: Math.abs(height - (bottom + top)), width: Math.abs(width - (left + right)) }; switch (legendPosition) { case 'bottom': case 'bottom-left': return chartSize.height + ChartCommonStyles.legend.margin * 2 + top + dy; case 'right': { // Legend height with padding const legendDimensions = getLegendDimensions({ legendData, legendOrientation, legendProps, theme }); const originX = chartSize.height / 2 + top; const legendPadding = (legendDataArr: any[]) => (legendDataArr && legendDataArr.length > 0 ? 2 : 0); return originX - legendDimensions.height / 2 + legendPadding(legendData); } default: return dy; } }; /** * Returns x coordinate for pie legends * * @private Not intended as public API and subject to change */ const getPieLegendX = ({ dx = 0, height, legendData, legendOrientation, legendPosition, legendProps, padding, theme, width }: ChartLegendPositionInterface) => { const origin = getPieOrigin({ height, padding, width }); const radius = Helpers.getRadius({ height, width, padding }); const legendDimensions = getLegendDimensions({ legendData, legendOrientation, legendProps, theme }); switch (legendPosition) { case 'bottom': return width > legendDimensions.width ? Math.round((width - legendDimensions.width) / 2) + dx : dx; case 'right': return origin.x + ChartCommonStyles.label.margin + dx + radius; default: return dx; } }; /** * Returns y coordinate for pie legends * * @private Not intended as public API and subject to change */ const getPieLegendY = ({ dy = 0, height, legendPosition, legendData, legendOrientation, legendProps, padding, theme, width }: ChartLegendPositionInterface) => { const origin = getPieOrigin({ height, padding, width }); const radius = Helpers.getRadius({ height, width, padding }); switch (legendPosition) { case 'bottom': return origin.y + ChartCommonStyles.legend.margin + radius + dy; case 'right': { // Legend height with padding const legendDimensions = getLegendDimensions({ legendData, legendOrientation, legendProps, theme }); const legendPadding = (legendDataArr: any[]) => (legendDataArr && legendDataArr.length > 0 ? 2 : 0); return origin.y - legendDimensions.height / 2 + legendPadding(legendData); } default: return dy; } }; /** * Returns an approximation of longest text width based on legend styles * * @private Not intended as public API and subject to change */ // eslint-disable-next-line @typescript-eslint/no-unused-vars const getMaxLegendTextSize = ({ legendData, theme }: ChartLegendTextMaxSizeInterface) => { const style: any = theme && theme.legend && theme.legend.style ? theme.legend.style.labels : undefined; if (!(legendData && legendData.length)) { return 0; } let result = ''; legendData.forEach((data) => { if (data.name && data.name.length > result.length) { result = data.name; } }); // The approximateTextSize function returns height and width, but Victory incorrectly typed it as number const adjustedTextSize: any = TextSize.approximateTextSize(result, { ...style }); return adjustedTextSize.width; };