From ba24357eb771660d2db9b3a8929df36403cf0152 Mon Sep 17 00:00:00 2001 From: Dan Labrecque Date: Wed, 4 May 2022 19:38:36 -0400 Subject: [PATCH] feat(charts): Added support for patterns https://github.com/patternfly/patternfly-design/issues/1136 --- .../src/components/Chart/Chart.tsx | 91 +- .../src/components/ChartDonut/ChartDonut.tsx | 22 + .../ChartDonutThreshold.tsx | 48 +- .../ChartDonutUtilization.tsx | 62 +- .../src/components/ChartGroup/ChartGroup.tsx | 45 +- .../components/ChartLegend/ChartLegend.tsx | 44 + .../ChartLegend/examples/ChartLegend.md | 2 +- .../ChartLegendTooltip/ChartLegendTooltip.tsx | 14 + .../ChartLegendTooltipContent.tsx | 15 + .../src/components/ChartPie/ChartPie.tsx | 89 +- .../src/components/ChartStack/ChartStack.tsx | 40 +- .../ChartTheme/examples/ChartTheme.md | 2 +- .../ChartTooltip/examples/ChartTooltip.md | 2 +- .../ChartUtils/chart-interactive-legend.ts | 16 +- .../src/components/ChartUtils/chart-legend.ts | 3 + .../components/ChartUtils/chart-patterns.tsx | 275 ++++++ .../components/ChartUtils/chart-tooltip.ts | 6 +- .../src/components/ChartUtils/index.ts | 1 + .../components/Patterns/examples/patterms.md | 786 ++++++++++++++++++ 19 files changed, 1525 insertions(+), 38 deletions(-) create mode 100644 packages/react-charts/src/components/ChartUtils/chart-patterns.tsx create mode 100644 packages/react-charts/src/components/Patterns/examples/patterms.md diff --git a/packages/react-charts/src/components/Chart/Chart.tsx b/packages/react-charts/src/components/Chart/Chart.tsx index 2dc91be9842..065c9e556b1 100644 --- a/packages/react-charts/src/components/Chart/Chart.tsx +++ b/packages/react-charts/src/components/Chart/Chart.tsx @@ -23,7 +23,17 @@ import { AxesType, VictoryChart, VictoryChartProps } from 'victory-chart'; import { ChartContainer } from '../ChartContainer'; import { ChartLegend, ChartLegendOrientation, ChartLegendPosition } from '../ChartLegend'; import { ChartCommonStyles, ChartThemeDefinition } from '../ChartTheme'; -import { getChartTheme, getClassName, getComputedLegend, getLabelTextSize, getPaddingForSide } from '../ChartUtils'; +import { + getChartTheme, + getClassName, + getComputedLegend, + getLabelTextSize, + getPaddingForSide, + getPatternId, + getPatternDefs, + getDefaultColorScale, + getDefaultPatternScale +} from '../ChartUtils'; /** * See https://github.com/FormidableLabs/victory/blob/master/packages/victory-core/src/index.d.ts @@ -291,6 +301,24 @@ export interface ChartProps extends VictoryChartProps { * @propType number | { top: number, bottom: number, left: number, right: number } */ padding?: PaddingProps; + /** + * The optional ID to prefix pattern defs + * + * @example patternId="pattern" + */ + patternId?: string; + /** + * The patternScale prop is an optional prop that defines a pattern to be applied to the children, where applicable. + * This prop should be given as an array of CSS colors, or as a string corresponding to a URL. Patterns will be + * assigned to children by index, unless they are explicitly specified in styles. Patterns will repeat when there are + * more children than patterns in the provided patternScale. Functionality may be overridden via the `style.data.fill` + * property. + * + * Note: Not all components are supported; for example, ChartLine, ChartBullet, ChartThreshold, etc. + * + * @example patternScale={['url("#pattern:0")', 'url("#pattern:1")', 'url("#pattern:2")']} + */ + patternScale?: string[]; /** * Note: This prop should not be set manually. * @@ -407,6 +435,10 @@ export interface ChartProps extends VictoryChartProps { * @deprecated Use PatternFly's pf-theme-dark CSS selector */ themeVariant?: string; + /** + * Generate default pattern defs and populate patternScale + */ + usePatternDefs?: boolean; /** * Specifies the width of the svg viewBox of the chart container. This value should be given as a * number of pixels. @@ -423,6 +455,7 @@ export const Chart: React.FunctionComponent = ({ ariaDesc, ariaTitle, children, + colorScale, legendAllowWrap = false, legendComponent = , legendData, @@ -432,6 +465,9 @@ export const Chart: React.FunctionComponent = ({ themeColor, // eslint-disable-next-line @typescript-eslint/no-unused-vars themeVariant, + patternId = getPatternId(), + patternScale, + usePatternDefs = false, // destructure last theme = getChartTheme(themeColor, showAxis), @@ -448,13 +484,30 @@ export const Chart: React.FunctionComponent = ({ top: getPaddingForSide('top', padding, theme.chart.padding) }; + const defaultColorScale = getDefaultColorScale(colorScale as any, theme.chart.colorScale as string[]); + const defaultPatternScale = getDefaultPatternScale(defaultColorScale, patternScale, patternId, usePatternDefs); + + // Add pattern props for legend tooltip + let labelComponent; + if ( + containerComponent.props.labelComponent && + containerComponent.props.labelComponent.type.displayName === 'ChartLegendTooltip' + ) { + labelComponent = React.cloneElement(containerComponent.props.labelComponent, { + patternId, + ...(defaultPatternScale && { patternScale: defaultPatternScale }), + ...containerComponent.props.labelComponent.props + }); + } + // Clone so users can override container props const container = React.cloneElement(containerComponent, { desc: ariaDesc, title: ariaTitle, theme, ...containerComponent.props, - className: getClassName({ className: containerComponent.props.className }) // Override VictoryContainer class name + className: getClassName({ className: containerComponent.props.className }), // Override VictoryContainer class name + ...(labelComponent && { labelComponent }) // Override label component props }); const legend = React.cloneElement(legendComponent, { @@ -502,15 +555,46 @@ export const Chart: React.FunctionComponent = ({ height, legendComponent: legend, padding: defaultPadding, + ...(defaultPatternScale && { patternScale: defaultPatternScale }), position: legendPosition, theme, width }); }; + // Merge pattern IDs with `data.fill` property, used by interactive pie chart legend + const getDefaultData = (data: any) => { + if (!defaultPatternScale) { + return data; + } + return data.map((datum: any, index: number) => { + const pattern = defaultPatternScale[index % defaultPatternScale.length]; + return { + ...(pattern && pattern !== null && { fill: pattern }), + ...datum + }; + }); + }; + + // Render children + const renderChildren = () => + React.Children.toArray(children).map(child => { + if (React.isValidElement(child)) { + const { ...childProps } = child.props; + return React.cloneElement(child, { + ...(defaultPatternScale && { patternScale: defaultPatternScale }), + ...childProps, + ...((child as any).type.displayName === 'ChartPie' && { data: getDefaultData(childProps.data) }), // Override child props + patternId + }); + } + return child; + }); + // Note: containerComponent is required for theme return ( = ({ width={width} {...rest} > - {children} + {renderChildren()} {getLegend()} + {usePatternDefs && getPatternDefs(patternId, defaultColorScale)} ); }; diff --git a/packages/react-charts/src/components/ChartDonut/ChartDonut.tsx b/packages/react-charts/src/components/ChartDonut/ChartDonut.tsx index a8d6a8315cc..be63702a4ca 100644 --- a/packages/react-charts/src/components/ChartDonut/ChartDonut.tsx +++ b/packages/react-charts/src/components/ChartDonut/ChartDonut.tsx @@ -356,6 +356,24 @@ export interface ChartDonutProps extends ChartPieProps { * @propType number | Function */ padAngle?: NumberOrCallback; + /** + * The optional ID to prefix pattern defs + * + * @example patternId="pattern" + */ + patternId?: string; + /** + * The patternScale prop is an optional prop that defines a pattern to be applied to the children, where applicable. + * This prop should be given as an array of CSS colors, or as a string corresponding to a URL. Patterns will be + * assigned to children by index, unless they are explicitly specified in styles. Patterns will repeat when there are + * more children than patterns in the provided patternScale. Functionality may be overridden via the `style.data.fill` + * property. + * + * Note: Not all components are supported; for example, ChartLine, ChartBullet, ChartThreshold, etc. + * + * @example patternScale={['url("#pattern:0")', 'url("#pattern:1")', 'url("#pattern:2")']} + */ + patternScale?: string[]; /** * The padding props specifies the amount of padding in number of pixels between * the edge of the chart and any rendered child components. This prop can be given @@ -504,6 +522,10 @@ export interface ChartDonutProps extends ChartPieProps { * Note: Default label properties may be applied */ titleComponent?: React.ReactElement; + /** + * Generate default pattern defs and populate patternScale + */ + usePatternDefs?: boolean; /** * Specifies the width of the svg viewBox of the chart container. This value should be given as a number of pixels. * diff --git a/packages/react-charts/src/components/ChartDonutUtilization/ChartDonutThreshold.tsx b/packages/react-charts/src/components/ChartDonutUtilization/ChartDonutThreshold.tsx index 663978c1e8d..c72214f336c 100644 --- a/packages/react-charts/src/components/ChartDonutUtilization/ChartDonutThreshold.tsx +++ b/packages/react-charts/src/components/ChartDonutUtilization/ChartDonutThreshold.tsx @@ -21,7 +21,15 @@ import hoistNonReactStatics from 'hoist-non-react-statics'; import { ChartContainer } from '../ChartContainer'; import { ChartDonut, ChartDonutProps } from '../ChartDonut'; import { ChartDonutStyles, ChartThemeDefinition } from '../ChartTheme'; -import { getDonutThresholdDynamicTheme, getDonutThresholdStaticTheme, getPaddingForSide } from '../ChartUtils'; +import { + getDefaultColorScale, + getDefaultPatternScale, + getDonutThresholdDynamicTheme, + getDonutThresholdStaticTheme, + getPaddingForSide, + getPatternDefs, + getPatternId +} from '../ChartUtils'; export enum ChartDonutThresholdDonutOrientation { left = 'left', @@ -307,6 +315,24 @@ export interface ChartDonutThresholdProps extends ChartDonutProps { * @propType number | { top: number, bottom: number, left: number, right: number } */ padding?: PaddingProps; + /** + * The optional ID to prefix pattern defs + * + * @example patternId="pattern" + */ + patternId?: string; + /** + * The patternScale prop is an optional prop that defines a pattern to be applied to the children, where applicable. + * This prop should be given as an array of CSS colors, or as a string corresponding to a URL. Patterns will be + * assigned to children by index, unless they are explicitly specified in styles. Patterns will repeat when there are + * more children than patterns in the provided patternScale. Functionality may be overridden via the `style.data.fill` + * property. + * + * Note: Not all components are supported; for example, ChartLine, ChartBullet, ChartThreshold, etc. + * + * @example patternScale={['url("#pattern:0")', 'url("#pattern:1")', 'url("#pattern:2")']} + */ + patternScale?: string[]; /** * Specifies the radius of the chart. If this property is not provided it is computed * from width, height, and padding props @@ -398,6 +424,10 @@ export interface ChartDonutThresholdProps extends ChartDonutProps { * The title for the donut chart */ title?: string; + /** + * Generate default pattern defs and populate patternScale + */ + usePatternDefs?: boolean; /** * Specifies the width of the svg viewBox of the chart container. This value should be given as a number of pixels. * @@ -440,18 +470,22 @@ export const ChartDonutThreshold: React.FunctionComponent, data = [], invert = false, labels = [], // Don't show any tooltip labels by default, let consumer override if needed padding, + patternId = getPatternId(), + patternScale, radius, standalone = true, subTitlePosition = ChartDonutStyles.label.subTitlePosition as ChartDonutThresholdSubTitlePosition, themeColor, // eslint-disable-next-line @typescript-eslint/no-unused-vars themeVariant, + usePatternDefs = false, x, y, @@ -475,6 +509,9 @@ export const ChartDonutThreshold: React.FunctionComponent { // Format and sort data. Sorting ensures thresholds are displayed in the correct order and simplifies calculations. @@ -513,17 +550,20 @@ export const ChartDonutThreshold: React.FunctionComponent {chart} {renderChildren()} + {usePatternDefs && getPatternDefs(patternId, defaultColorScale, 1)} ); }; diff --git a/packages/react-charts/src/components/ChartDonutUtilization/ChartDonutUtilization.tsx b/packages/react-charts/src/components/ChartDonutUtilization/ChartDonutUtilization.tsx index c7929d8d0bf..a51c1eb50ce 100644 --- a/packages/react-charts/src/components/ChartDonutUtilization/ChartDonutUtilization.tsx +++ b/packages/react-charts/src/components/ChartDonutUtilization/ChartDonutUtilization.tsx @@ -21,7 +21,7 @@ import { SliceProps, VictoryPie, VictorySliceLabelPositionType } from 'victory-p import { ChartContainer } from '../ChartContainer'; import { ChartDonut, ChartDonutProps } from '../ChartDonut'; import { ChartCommonStyles, ChartThemeDefinition, ChartDonutUtilizationStyles } from '../ChartTheme'; -import { getDonutUtilizationTheme } from '../ChartUtils'; +import { getDefaultColorScale, getDefaultPatternScale, getDonutUtilizationTheme, getPatternId } from '../ChartUtils'; export enum ChartDonutUtilizationLabelPosition { centroid = 'centroid', @@ -377,6 +377,24 @@ export interface ChartDonutUtilizationProps extends ChartDonutProps { * @propType number | { top: number, bottom: number, left: number, right: number } */ padding?: PaddingProps; + /** + * The optional ID to prefix pattern defs + * + * @example patternId="pattern" + */ + patternId?: string; + /** + * The patternScale prop is an optional prop that defines a pattern to be applied to the children, where applicable. + * This prop should be given as an array of CSS colors, or as a string corresponding to a URL. Patterns will be + * assigned to children by index, unless they are explicitly specified in styles. Patterns will repeat when there are + * more children than patterns in the provided patternScale. Functionality may be overridden via the `style.data.fill` + * property. + * + * Note: Not all components are supported; for example, ChartLine, ChartBullet, ChartThreshold, etc. + * + * @example patternScale={['url("#pattern:0")', 'url("#pattern:1")', 'url("#pattern:2")']} + */ + patternScale?: string[]; /** * Specifies the radius of the chart. If this property is not provided it is computed * from width, height, and padding props @@ -393,13 +411,19 @@ export interface ChartDonutUtilizationProps extends ChartDonutProps { */ sharedEvents?: { events: any[]; getEventState: Function }; /** - * This will show the static, unused portion of the donut chart. + * This will show the static, unused portion of the donut utilization chart. * * Note: This prop should not be set manually. * * @hide */ showStatic?: boolean; + /** + * This will apply patterns for the static, unused portion of the donut utilization chart. + * + * @hide + */ + showStaticPattern?: boolean; /** * Use the sortKey prop to indicate how data should be sorted. This prop * is given directly to the lodash sortBy function to be executed on the @@ -531,6 +555,16 @@ export interface ChartDonutUtilizationProps extends ChartDonutProps { * @example thresholds={[{ value: 60, color: '#F0AB00' }, { value: 90, color: '#C9190B' }]} */ thresholds?: any[]; + /** + * Generate default pattern defs and populate patternScale + */ + usePatternDefs?: boolean; + /** + * Flag indicating parent usePatternDefs prop is in use + * + * @hide + */ + _usePatternDefs?: boolean; /** * Specifies the width of the svg viewBox of the chart container. This value should be given as a number of pixels. * @@ -572,17 +606,23 @@ export const ChartDonutUtilization: React.FunctionComponent, data, invert = false, legendPosition = ChartCommonStyles.legend.position as ChartDonutUtilizationLegendPosition, padding, + patternId = getPatternId(), + patternScale, showStatic = true, + showStaticPattern = false, standalone = true, themeColor, // eslint-disable-next-line @typescript-eslint/no-unused-vars themeVariant, thresholds, + usePatternDefs = false, + _usePatternDefs, x, y, @@ -592,6 +632,20 @@ export const ChartDonutUtilization: React.FunctionComponent { + const defaultColorScale = getDefaultColorScale(colorScale, theme.pie.colorScale as string[]); + const defaultPatternScale = getDefaultPatternScale(defaultColorScale, patternScale, patternId, usePatternDefs); + + // Hide static pattern and handle edge case where parent does not use usePatternDefs + const hideStaticPattern = showStatic && !showStaticPattern; + const hideThresholdPatterns = !patternScale && _usePatternDefs === false; + if (defaultPatternScale && (hideStaticPattern || hideThresholdPatterns)) { + for (let i = 0; i < defaultPatternScale.length; i++) { + if (i !== 0) { + defaultPatternScale[i] = null; + } + } + } + // Returns computed data representing pie chart slices const getComputedData = () => { const datum = getData(); @@ -665,13 +719,17 @@ export const ChartDonutUtilization: React.FunctionComponent diff --git a/packages/react-charts/src/components/ChartGroup/ChartGroup.tsx b/packages/react-charts/src/components/ChartGroup/ChartGroup.tsx index 93f66603ea6..a466c55906b 100644 --- a/packages/react-charts/src/components/ChartGroup/ChartGroup.tsx +++ b/packages/react-charts/src/components/ChartGroup/ChartGroup.tsx @@ -21,7 +21,15 @@ import { import { VictoryGroup, VictoryGroupProps, VictoryGroupTTargetType } from 'victory-group'; import { ChartContainer } from '../ChartContainer'; import { ChartThemeDefinition } from '../ChartTheme'; -import { getClassName, getTheme } from '../ChartUtils'; +import { + getClassName, + getDefaultColorScale, + getDefaultPatternScale, + getPatternId, + getPatternDefs, + getTheme, + renderChildrenWithPatterns +} from '../ChartUtils'; export enum ChartGroupSortOrder { ascending = 'ascending', @@ -285,6 +293,24 @@ export interface ChartGroupProps extends VictoryGroupProps { * @propType number | { top: number, bottom: number, left: number, right: number } */ padding?: PaddingProps; + /** + * The optional ID to prefix pattern defs + * + * @example patternId="pattern" + */ + patternId?: string; + /** + * The patternScale prop is an optional prop that defines a pattern to be applied to the children, where applicable. + * This prop should be given as an array of CSS colors, or as a string corresponding to a URL. Patterns will be + * assigned to children by index, unless they are explicitly specified in styles. Patterns will repeat when there are + * more children than patterns in the provided patternScale. Functionality may be overridden via the `style.data.fill` + * property. + * + * Note: Not all components are supported; for example, ChartLine, ChartBullet, ChartThreshold, etc. + * + * @example patternScale={['url("#pattern:0")', 'url("#pattern:1")', 'url("#pattern:2")']} + */ + patternScale?: string[]; /** * Victory components can pass a boolean polar prop to specify whether a label is part of a polar chart. */ @@ -403,6 +429,10 @@ export interface ChartGroupProps extends VictoryGroupProps { * @deprecated Use PatternFly's pf-theme-dark CSS selector */ themeVariant?: string; + /** + * Generate default pattern defs and populate patternScale + */ + usePatternDefs?: boolean; /** * The width props specifies the width of the svg viewBox of the chart container * This value should be given as a number of pixels @@ -449,14 +479,17 @@ export const ChartGroup: React.FunctionComponent = ({ ariaDesc, ariaTitle, children, + colorScale, containerComponent = , + patternId = getPatternId(), + patternScale, themeColor, // eslint-disable-next-line @typescript-eslint/no-unused-vars themeVariant, + usePatternDefs = false, // destructure last theme = getTheme(themeColor), - ...rest }: ChartGroupProps) => { // Clone so users can override container props @@ -468,10 +501,14 @@ export const ChartGroup: React.FunctionComponent = ({ className: getClassName({ className: containerComponent.props.className }) // Override VictoryContainer class name }); + const defaultColorScale = getDefaultColorScale(colorScale, theme.group.colorScale as string[]); + const defaultPatternScale = getDefaultPatternScale(defaultColorScale, patternScale, patternId, usePatternDefs); + // Note: containerComponent is required for theme return ( - - {children} + + {renderChildrenWithPatterns(defaultPatternScale, patternId, children)} + {usePatternDefs && getPatternDefs(patternId, defaultColorScale)} ); }; diff --git a/packages/react-charts/src/components/ChartLegend/ChartLegend.tsx b/packages/react-charts/src/components/ChartLegend/ChartLegend.tsx index 687e5304a54..0f97232ad6b 100644 --- a/packages/react-charts/src/components/ChartLegend/ChartLegend.tsx +++ b/packages/react-charts/src/components/ChartLegend/ChartLegend.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import hoistNonReactStatics from 'hoist-non-react-statics'; +import merge from 'lodash/merge'; import { BlockProps, EventCallbackInterface, @@ -201,6 +202,18 @@ export interface ChartLegendProps extends VictoryLegendProps { * @propType number | { top: number, bottom: number, left: number, right: number } */ padding?: PaddingProps; + /** + * The patternScale prop is an optional prop that defines a pattern to be applied to the children, where applicable. + * This prop should be given as an array of CSS colors, or as a string corresponding to a URL. Patterns will be + * assigned to children by index, unless they are explicitly specified in styles. Patterns will repeat when there are + * more children than patterns in the provided patternScale. Functionality may be overridden via the `data.symbol.fill` + * property. + * + * Note: Not all components are supported; for example, ChartLine, ChartBullet, ChartThreshold, etc. + * + * @example patternScale={['url("#pattern:0")', 'url("#pattern:1")', 'url("#pattern:2")']} + */ + patternScale?: string[]; /** * The responsive prop specifies whether the rendered container should be a responsive container with a viewBox * attribute, or a static container with absolute width and height. @@ -314,13 +327,20 @@ export interface ChartLegendProps extends VictoryLegendProps { * The x and y props define the base position of the legend element. */ y?: number; + /** + * Generate default pattern defs and populate patternScale + */ + usePatternDefs?: boolean; } export const ChartLegend: React.FunctionComponent = ({ + colorScale, containerComponent = , dataComponent = , labelComponent = , + patternScale, responsive = true, + style, themeColor, // eslint-disable-next-line @typescript-eslint/no-unused-vars themeVariant, @@ -330,6 +350,28 @@ export const ChartLegend: React.FunctionComponent = ({ theme = getTheme(themeColor), ...rest }: ChartLegendProps) => { + // Merge pattern IDs with `style.data.fill` property + const getDefaultStyle = () => { + if (!patternScale) { + return style; + } + + // Note: ChartLegendTooltipContent overrides patterns via `data.symbol.fill` property + const _style = { ...style }; + _style.data = merge(_style.data, { + fill: ({ index }: any) => { + const themeColor = + theme && theme.legend && theme.legend.colorScale + ? theme.legend.colorScale[index % theme.legend.colorScale.length] + : undefined; + const pattern = patternScale[index % patternScale.length]; + const color = colorScale ? colorScale[index % colorScale.length] : themeColor; // Sync color scale + return pattern && pattern !== null ? pattern : color; + } + }); + return _style; + }; + // Clone so users can override container props const container = React.cloneElement(containerComponent, { responsive, @@ -340,9 +382,11 @@ export const ChartLegend: React.FunctionComponent = ({ // Note: containerComponent is required for theme return ( , legendData, + patternScale, text, themeColor, // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -365,6 +378,7 @@ export const ChartLegendTooltip: React.FunctionComponent, legendComponent = , legendData, + patternScale, text, themeColor, // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -297,11 +310,13 @@ export const ChartLegendTooltipContent: React.FunctionComponent = ({ allowTooltip = true, ariaDesc, ariaTitle, + colorScale, constrainToVisibleArea = false, containerComponent = , - labels, legendAllowWrap = false, legendComponent = , legendData, legendPosition = ChartCommonStyles.legend.position as ChartPieLegendPosition, + patternId = getPatternId(), + patternScale, padding, radius, standalone = true, + style, themeColor, // eslint-disable-next-line @typescript-eslint/no-unused-vars themeVariant, + usePatternDefs = false, // destructure last theme = getTheme(themeColor), @@ -508,15 +543,34 @@ export const ChartPie: React.FunctionComponent = ({ }; const chartRadius = radius ? radius : getDefaultRadius(); + const defaultColorScale = getDefaultColorScale(colorScale, theme.pie.colorScale as string[]); + const defaultPatternScale = getDefaultPatternScale(defaultColorScale, patternScale, patternId, usePatternDefs); + + // Merge pattern IDs with `style.data.fill` property + const getDefaultStyle = () => { + if (!defaultPatternScale) { + return style; + } + const _style = { ...style }; + _style.data = merge(_style.data, { + fill: ({ slice }: any) => { + const pattern = defaultPatternScale[slice.index % defaultPatternScale.length]; + return pattern && pattern !== null ? pattern : defaultColorScale[slice.index % defaultColorScale.length]; + } + }); + return _style; + }; + const chart = ( = ({ ); const legend = React.cloneElement(legendComponent, { + colorScale, data: legendData, key: 'pf-chart-pie-legend', orientation: legendOrientation, @@ -542,6 +597,7 @@ export const ChartPie: React.FunctionComponent = ({ height, legendComponent: legend, padding: defaultPadding, + ...(defaultPatternScale && { patternScale: defaultPatternScale }), position: legendPosition, theme, width @@ -549,18 +605,20 @@ export const ChartPie: React.FunctionComponent = ({ }; // Clone so users can override container props - const container = React.cloneElement( - containerComponent, - { - desc: ariaDesc, - height, - title: ariaTitle, - width, - theme, - ...containerComponent.props - }, - [chart, getLegend()] - ); + const container = standalone + ? React.cloneElement( + containerComponent, + { + desc: ariaDesc, + height, + title: ariaTitle, + width, + theme, + ...containerComponent.props + }, + [chart, getLegend(), usePatternDefs && getPatternDefs(patternId, defaultColorScale)] + ) + : null; return standalone ? ( {container} @@ -568,6 +626,7 @@ export const ChartPie: React.FunctionComponent = ({ {chart} {getLegend()} + {usePatternDefs && getPatternDefs(patternId, defaultColorScale)} ); }; diff --git a/packages/react-charts/src/components/ChartStack/ChartStack.tsx b/packages/react-charts/src/components/ChartStack/ChartStack.tsx index faeccdd0b99..34a8f52e523 100644 --- a/packages/react-charts/src/components/ChartStack/ChartStack.tsx +++ b/packages/react-charts/src/components/ChartStack/ChartStack.tsx @@ -19,7 +19,14 @@ import { import { VictoryStack, VictoryStackProps, VictoryStackTTargetType } from 'victory-stack'; import { ChartContainer } from '../ChartContainer'; import { ChartThemeDefinition } from '../ChartTheme'; -import { getClassName, getTheme } from '../ChartUtils'; +import { + getClassName, + getDefaultColorScale, + getDefaultPatternScale, + getPatternId, + getTheme, + renderChildrenWithPatterns +} from '../ChartUtils'; /** * See https://github.com/FormidableLabs/victory/blob/master/packages/victory-core/src/index.d.ts @@ -265,6 +272,24 @@ export interface ChartStackProps extends VictoryStackProps { * @propType number | { top: number, bottom: number, left: number, right: number } */ padding?: PaddingProps; + /** + * The optional ID to prefix pattern defs + * + * @example patternId="pattern" + */ + patternId?: string; + /** + * The patternScale prop is an optional prop that defines a pattern to be applied to the children, where applicable. + * This prop should be given as an array of CSS colors, or as a string corresponding to a URL. Patterns will be + * assigned to children by index, unless they are explicitly specified in styles. Patterns will repeat when there are + * more children than patterns in the provided patternScale. Functionality may be overridden via the `style.data.fill` + * property. + * + * Note: Not all components are supported; for example, ChartLine, ChartBullet, ChartThreshold, etc. + * + * @example patternScale={['url("#pattern:0")', 'url("#pattern:1")', 'url("#pattern:2")']} + */ + patternScale?: string[]; /** * Victory components can pass a boolean polar prop to specify whether a label is part of a polar chart. * @@ -368,6 +393,10 @@ export interface ChartStackProps extends VictoryStackProps { * @deprecated Use PatternFly's pf-theme-dark CSS selector */ themeVariant?: string; + /** + * Generate default pattern defs and populate patternScale + */ + usePatternDefs?: boolean; /** * The width props specifies the width of the svg viewBox of the chart container * This value should be given as a number of pixels @@ -384,10 +413,14 @@ export const ChartStack: React.FunctionComponent = ({ ariaDesc, ariaTitle, children, + colorScale, containerComponent = , + patternId = getPatternId(), + patternScale, themeColor, // eslint-disable-next-line @typescript-eslint/no-unused-vars themeVariant, + usePatternDefs = false, // destructure last theme = getTheme(themeColor), @@ -402,10 +435,13 @@ export const ChartStack: React.FunctionComponent = ({ className: getClassName({ className: containerComponent.props.className }) // Override VictoryContainer class name }); + const defaultColorScale = getDefaultColorScale(colorScale, theme.stack.colorScale as string[]); + const defaultPatternScale = getDefaultPatternScale(defaultColorScale, patternScale, patternId, usePatternDefs); + // Note: containerComponent is required for theme return ( - {children} + {renderChildrenWithPatterns(defaultPatternScale, patternId, children)} ); }; diff --git a/packages/react-charts/src/components/ChartTheme/examples/ChartTheme.md b/packages/react-charts/src/components/ChartTheme/examples/ChartTheme.md index efe57b9de15..9a539671cee 100644 --- a/packages/react-charts/src/components/ChartTheme/examples/ChartTheme.md +++ b/packages/react-charts/src/components/ChartTheme/examples/ChartTheme.md @@ -1,5 +1,5 @@ --- -id: Themed charts +id: Themes section: charts hideDarkMode: true --- diff --git a/packages/react-charts/src/components/ChartTooltip/examples/ChartTooltip.md b/packages/react-charts/src/components/ChartTooltip/examples/ChartTooltip.md index 4fe53ded2e2..2bd11087560 100644 --- a/packages/react-charts/src/components/ChartTooltip/examples/ChartTooltip.md +++ b/packages/react-charts/src/components/ChartTooltip/examples/ChartTooltip.md @@ -1,5 +1,5 @@ --- -id: Tooltip chart +id: Tooltips section: charts propComponents: ['ChartTooltip'] hideDarkMode: true diff --git a/packages/react-charts/src/components/ChartUtils/chart-interactive-legend.ts b/packages/react-charts/src/components/ChartUtils/chart-interactive-legend.ts index 4c9733c26a0..3ff131ee3f3 100644 --- a/packages/react-charts/src/components/ChartUtils/chart-interactive-legend.ts +++ b/packages/react-charts/src/components/ChartUtils/chart-interactive-legend.ts @@ -1,6 +1,7 @@ /* eslint-disable camelcase */ import chart_area_Opacity from '@patternfly/react-tokens/dist/esm/chart_area_Opacity'; import chart_color_black_500 from '@patternfly/react-tokens/dist/esm/chart_color_black_500'; +import { Helpers } from 'victory-core'; interface ChartInteractiveLegendInterface { // The names or groups of names associated with each data series @@ -117,13 +118,16 @@ const getInteractiveLegendTargetEvents = ({ : ({ // Skip if hidden style: - props.padAngle !== undefined // Support for pie chart + props.slice !== undefined // Support for pie chart ? { - ...props.style, - ...(index !== props.index && { opacity: chart_area_Opacity.value }) + ...Helpers.evaluateStyle(props.style, props), + ...(index !== props.slice.index && { opacity: chart_area_Opacity.value }), + ...(props.data[props.slice.index].fill && { + fill: props.data[props.slice.index].fill + }) } : { - ...props.style, + ...Helpers.evaluateStyle(props.style, props), opacity: chart_area_Opacity.value } } as any) @@ -139,7 +143,7 @@ const getInteractiveLegendTargetEvents = ({ : { // Skip if hidden style: { - ...props.style, + ...Helpers.evaluateStyle(props.style, props), opacity: chart_area_Opacity.value } } @@ -156,7 +160,7 @@ const getInteractiveLegendTargetEvents = ({ : { // Skip if hidden style: { - ...props.style, + ...Helpers.evaluateStyle(props.style, props), opacity: chart_area_Opacity.value } }; diff --git a/packages/react-charts/src/components/ChartUtils/chart-legend.ts b/packages/react-charts/src/components/ChartUtils/chart-legend.ts index 0f7efa49b3c..4defc3d956e 100644 --- a/packages/react-charts/src/components/ChartUtils/chart-legend.ts +++ b/packages/react-charts/src/components/ChartUtils/chart-legend.ts @@ -15,6 +15,7 @@ interface ChartLegendInterface { legendComponent: React.ReactElement; // The base legend component to render orientation?: 'horizontal' | 'vertical'; // Orientation of legend padding: PaddingProps; // 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 width: number; // Overall width of SVG @@ -55,6 +56,7 @@ export const getComputedLegend = ({ height, legendComponent, padding, + patternScale, position = ChartCommonStyles.legend.position as ChartLegendPosition, theme, width, @@ -114,6 +116,7 @@ export const getComputedLegend = ({ const legendProps = defaults({}, legendComponent.props, { itemsPerRow: legendItemsPerRow, orientation, + patternScale, standalone: false, theme, x: legendX > 0 ? legendX : 0, diff --git a/packages/react-charts/src/components/ChartUtils/chart-patterns.tsx b/packages/react-charts/src/components/ChartUtils/chart-patterns.tsx new file mode 100644 index 00000000000..e25fb7680c9 --- /dev/null +++ b/packages/react-charts/src/components/ChartUtils/chart-patterns.tsx @@ -0,0 +1,275 @@ +import * as React from 'react'; +import merge from 'lodash/merge'; +import uniqueId from 'lodash/uniqueId'; + +const patterns: any = [ + // Set 1 + { + // Left diagonal lines + d: 'M 0 0 L 5 5 M 4.5 -0.5 L 5.5 0.5 M -0.5 4.5 L 0.5 5.5', + height: 5, + fill: 'none', + patternContentUnits: 'userSpaceOnUse', + patternUnits: 'userSpaceOnUse', + patternTransform: 'scale(1.4 1.4)', + strokeWidth: 2, + width: 5, + x: 0, + y: 0 + }, + { + // Right diagonal lines + d: 'M 0 5 L 5 0 M -0.5 0.5 L 0.5 -0.5 M 4.5 5.5 L 5.5 4.5', + height: 5, + fill: 'none', + patternContentUnits: 'userSpaceOnUse', + patternUnits: 'userSpaceOnUse', + patternTransform: 'scale(1.4 1.4)', + strokeWidth: 2, + width: 5, + x: 0, + y: 0 + }, + { + // Vertical offset line + d: 'M 2 0 L 2 5 M 4 0 L 4 5', + height: 5, + fill: 'none', + patternContentUnits: 'userSpaceOnUse', + patternUnits: 'userSpaceOnUse', + patternTransform: 'scale(1.4 1.4)', + strokeWidth: 2, + width: 5, + x: 0, + y: 0 + }, + { + // Horizontal lines + d: 'M 0 2 L 5 2 M 0 4 L 5 4', + height: 5, + fill: 'none', + patternContentUnits: 'userSpaceOnUse', + patternUnits: 'userSpaceOnUse', + patternTransform: 'scale(1.4 1.4)', + strokeWidth: 2, + width: 5, + x: 0, + y: 0 + }, + { + // Vertical wave + d: 'M 0 1.5 L 2.5 1.5 L 2.5 0 M 2.5 5 L 2.5 3.5 L 5 3.5', + height: 5, + fill: 'none', + patternContentUnits: 'userSpaceOnUse', + patternUnits: 'userSpaceOnUse', + patternTransform: 'scale(1.4 1.4)', + strokeWidth: 2, + width: 5, + x: 0, + y: 0 + }, + + // Set 2 + { + // Horizontal wave + d: 'M 0 0 L 5 10 L 10 0', + height: 10, + fill: 'none', + patternContentUnits: 'userSpaceOnUse', + patternUnits: 'userSpaceOnUse', + strokeWidth: 2, + width: 10, + x: 0, + y: 0 + }, + { + // Squares + d: 'M 3 3 L 8 3 L 8 8 L 3 8 Z', + height: 10, + fill: 'none', + patternContentUnits: 'userSpaceOnUse', + patternUnits: 'userSpaceOnUse', + strokeWidth: 2, + width: 10, + x: 0, + y: 0 + }, + { + // Circles + d: 'M 5 5 m -4 0 a 4 4 0 1 1 8 0 a 4 4 0 1 1 -8 0', + height: 10, + fill: 'none', + patternContentUnits: 'userSpaceOnUse', + patternUnits: 'userSpaceOnUse', + strokeWidth: 2, + width: 10, + x: 0, + y: 0 + }, + { + // Left diagonal lines (thin) + d: 'M 0 0 L 10 10 M 9 -1 L 11 1 M -1 9 L 1 11', + height: 10, + fill: 'none', + patternContentUnits: 'userSpaceOnUse', + patternUnits: 'userSpaceOnUse', + strokeWidth: 2, + width: 10, + x: 0, + y: 0 + }, + { + // Right diagonal lines (thin) + d: 'M 0 10 L 10 0 M -1 1 L 1 -1 M 9 11 L 11 9', + height: 10, + fill: 'none', + patternContentUnits: 'userSpaceOnUse', + patternUnits: 'userSpaceOnUse', + strokeWidth: 2, + width: 10, + x: 0, + y: 0 + }, + + // Set 3 + { + // Diamonds + d: 'M 2 5 L 5 2 L 8 5 L 5 8 Z', + height: 10, + fill: 'none', + patternContentUnits: 'userSpaceOnUse', + patternUnits: 'userSpaceOnUse', + strokeWidth: 2, + width: 10, + x: 0, + y: 0 + }, + { + // Thin vertical lines + d: 'M 3 0 L 3 10 M 8 0 L 8 10', + height: 5, + fill: 'none', + patternContentUnits: 'userSpaceOnUse', + patternUnits: 'userSpaceOnUse', + patternTransform: 'scale(1.4 1.4)', + strokeWidth: 2, + width: 5, + x: 0, + y: 0 + }, + { + // Left zig zag + d: 'M 10 3 L 5 3 L 5 0 M 5 10 L 5 7 L 0 7', + height: 10, + fill: 'none', + patternContentUnits: 'userSpaceOnUse', + patternUnits: 'userSpaceOnUse', + strokeWidth: 2, + width: 10, + x: 0, + y: 0 + }, + { + // Thin horizontal lines + d: 'M 0 3 L 10 3 M 0 8 L 10 8', + height: 5, + fill: 'none', + patternContentUnits: 'userSpaceOnUse', + patternUnits: 'userSpaceOnUse', + patternTransform: 'scale(1.4 1.4)', + strokeWidth: 2, + width: 5, + x: 0, + y: 0 + }, + { + // Right zig zag + d: 'M 0 3 L 5 3 L 5 0 M 5 10 L 5 7 L 10 7', + height: 10, + fill: 'none', + patternContentUnits: 'userSpaceOnUse', + patternUnits: 'userSpaceOnUse', + strokeWidth: 2, + width: 10, + x: 0, + y: 0 + } +]; + +export const getPatternId = () => uniqueId('pf-pattern'); + +export const getPatternDefsId = (prefix: string, index: number) => { + const id = `${prefix}:${index}`; + return id; +}; + +// Returns pattern defs +// +// Note that this is wrapped in an empty tag so Victory does not apply child props to defs +export const getPatternDefs = (patternId: string, colorScale: string[], offset: number = 0) => { + const defs = ( + + + {colorScale.map((c: string, index: number) => { + const { d, fill, stroke = c, strokeWidth, ...rest } = patterns[(index + offset) % patterns.length]; + const id = getPatternDefsId(patternId, index); + return ( + + + + ); + })} + + + ); + return defs; +}; + +// Return pattern IDs to use as color scale +export const getPatternScale = (patternId: string, colorScale: string[]) => + colorScale.map((c: any, index: number) => `url(#${getPatternDefsId(patternId, index)})`); + +// Return color scale +export const getDefaultColorScale = (colorScale: string[], themeColorScale: string[]) => + colorScale ? colorScale : themeColorScale; + +// Return pattern scale +export const getDefaultPatternScale = ( + colorScale: string[], + patternScale: string[], + patternId: string, + usePatternDefs: boolean +) => { + if (patternScale) { + return patternScale; + } + if (usePatternDefs) { + return getPatternScale(patternId, colorScale); + } + return undefined; +}; + +// Render children +export const renderChildrenWithPatterns = (defaultPatternScale: string[], patternId: string, children: any) => + React.Children.toArray(children).map((child, index) => { + if (React.isValidElement(child)) { + const { ...childProps } = child.props; + + // Merge pattern IDs with `style.data.fill` property + if (defaultPatternScale) { + const _style = { ...childProps.style }; + const fill = defaultPatternScale[index % defaultPatternScale.length]; + _style.data = merge(_style.data, { + ...(fill && fill !== null && { fill }) + }); + childProps.style = _style; + } + return React.cloneElement(child, { + patternId, + ...(defaultPatternScale && { patternScale: defaultPatternScale }), + ...childProps + }); + } + return child; + }); diff --git a/packages/react-charts/src/components/ChartUtils/chart-tooltip.ts b/packages/react-charts/src/components/ChartUtils/chart-tooltip.ts index 459c936c283..548199f9b99 100644 --- a/packages/react-charts/src/components/ChartUtils/chart-tooltip.ts +++ b/packages/react-charts/src/components/ChartUtils/chart-tooltip.ts @@ -28,6 +28,7 @@ interface ChartLegendTooltipVisibleDataInterface { activePoints?: any[]; colorScale?: string[]; legendData: any; + patternScale?: string[]; // Legend symbol patterns text?: StringOrNumberOrCallback | string[] | number[]; textAsLegendData?: boolean; theme: ChartThemeDefinition; @@ -200,6 +201,7 @@ export const getLegendTooltipVisibleData = ({ activePoints, colorScale, legendData, + patternScale, text, textAsLegendData = false, theme @@ -225,10 +227,12 @@ export const getLegendTooltipVisibleData = ({ theme && theme.legend && theme.legend.colorScale ? theme.legend.colorScale[i % theme.legend.colorScale.length] : undefined; + const pattern = patternScale ? patternScale[i % patternScale.length] : undefined; + const color = colorScale ? colorScale[i % colorScale.length] : themeColor; // Sync color scale result.push({ name: textAsLegendData ? _text[index] : data.name, symbol: { - fill: colorScale ? colorScale[i % colorScale.length] : themeColor, // Sync color scale + fill: pattern && pattern !== null ? pattern : color, ...data.symbol } }); diff --git a/packages/react-charts/src/components/ChartUtils/index.ts b/packages/react-charts/src/components/ChartUtils/index.ts index 500274f357f..76949488e64 100644 --- a/packages/react-charts/src/components/ChartUtils/index.ts +++ b/packages/react-charts/src/components/ChartUtils/index.ts @@ -6,6 +6,7 @@ export * from './chart-label'; export * from './chart-legend'; export * from './chart-origin'; export * from './chart-padding'; +export * from './chart-patterns'; export * from './chart-resize'; export * from './chart-theme'; export * from './chart-tooltip'; diff --git a/packages/react-charts/src/components/Patterns/examples/patterms.md b/packages/react-charts/src/components/Patterns/examples/patterms.md new file mode 100644 index 00000000000..c403acf746b --- /dev/null +++ b/packages/react-charts/src/components/Patterns/examples/patterms.md @@ -0,0 +1,786 @@ +--- +id: Patterns +section: charts +propComponents: [ + 'ChartLegend' +] +hideDarkMode: true +--- + +import { + Chart, + ChartArea, + ChartAxis, + ChartBar, + ChartDonut, + ChartDonutThreshold, + ChartDonutUtilization, + ChartGroup, + ChartLegend, + ChartLegendTooltip, + ChartPie, + ChartScatter, + ChartStack, + ChartThemeColor, + ChartVoronoiContainer, + createContainer, + getInteractiveLegendEvents, + getInteractiveLegendItemStyles, + getResizeObserver +} from '@patternfly/react-charts'; +import chart_area_Opacity from '@patternfly/react-tokens/dist/esm/chart_area_Opacity'; +import chart_color_black_500 from '@patternfly/react-tokens/dist/esm/chart_color_black_500'; +import chart_color_blue_300 from '@patternfly/react-tokens/dist/esm/chart_color_blue_300'; +import chart_color_green_300 from '@patternfly/react-tokens/dist/esm/chart_color_green_300'; +import chart_color_cyan_300 from '@patternfly/react-tokens/dist/esm/chart_color_cyan_300'; +import chart_color_gold_300 from '@patternfly/react-tokens/dist/esm/chart_color_gold_300'; +import '@patternfly/patternfly/patternfly-charts.css'; + +## Introduction +Note: PatternFly React charts live in its own package at [@patternfly/react-charts](https://www.npmjs.com/package/@patternfly/react-charts)! + +PatternFly React charts are based on the [Victory](https://formidable.com/open-source/victory/docs/victory-chart/) chart library, along with additional functionality, custom components, and theming for PatternFly. This provides a collection of React based components you can use to build PatternFly patterns with consistent markup, styling, and behavior. + +## Examples +### Basic pie chart +```js +import React from 'react'; +import { ChartPie, ChartThemeColor } from '@patternfly/react-charts'; + +
+ `${datum.x}: ${datum.y}`} + legendData={[{ name: 'Cats: 35' }, { name: 'Dogs: 55' }, { name: 'Birds: 10' }]} + legendOrientation="vertical" + legendPosition="right" + padding={{ + bottom: 20, + left: 20, + right: 140, // Adjusted to accommodate legend + top: 20 + }} + usePatternDefs + width={350} + /> +
+``` + +### Pie chart with all patterns +```js +import React from 'react'; +import { ChartPie, ChartThemeColor } from '@patternfly/react-charts'; + +
+ `${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" + padding={{ + bottom: 110, + left: 20, + right: 20, + top: 20 + }} + themeColor={ChartThemeColor.multiOrdered} + usePatternDefs + width={600} + /> +
+``` + +### Interactive legend with pie chart + +This demonstrates how to add an interactive legend to a pie chart using events such as `onMouseOver`, `onMouseOut`, and `onClick`. + +```js +import React from 'react'; +import { + Chart, + ChartLegend, + ChartThemeColor, + ChartPie, + getInteractiveLegendEvents, + getInteractiveLegendItemStyles +} from '@patternfly/react-charts'; + +class InteractivePieLegendChart extends React.Component { + constructor(props) { + super(props); + this.state = { + hiddenSeries: new Set(), + width: 0 + }; + this.series = [{ + datapoints: { x: 'Cats', y: 25 }, + legendItem: { name: 'Cats: 35' } + }, { + datapoints: { x: 'Dogs', y: 25 }, + legendItem: { name: 'Dogs: 25' } + }, { + datapoints: { x: 'Birds', y: 10 }, + legendItem: { name: 'Birds: 10' } + }]; + + // Returns groups of chart names associated with each data series + this.getChartNames = () => { + const result = []; + this.series.map((_, index) => { + // Provide names for each series hidden / shown -- use the same name for a pie chart + result.push(['pie']); + }); + return result; + }; + + // Returns onMouseOver, onMouseOut, and onClick events for the interactive legend + this.getEvents = () => getInteractiveLegendEvents({ + chartNames: this.getChartNames(), + isHidden: this.isHidden, + legendName: 'legend', + onLegendClick: this.handleLegendClick + }); + + // Returns legend data styled per hiddenSeries + this.getLegendData = () => { + const { hiddenSeries } = this.state; + return this.series.map((s, index) => { + return { + ...s.legendItem, // name property + ...getInteractiveLegendItemStyles(hiddenSeries.has(index)) // hidden styles + }; + }); + }; + + // Hide each data series individually + this.handleLegendClick = (props) => { + if (!this.state.hiddenSeries.delete(props.index)) { + this.state.hiddenSeries.add(props.index); + } + this.setState({ hiddenSeries: new Set(this.state.hiddenSeries) }); + }; + + // Returns true if data series is hidden + this.isHidden = (index) => { + const { hiddenSeries } = this.state; // Skip if already hidden + return hiddenSeries.has(index); + }; + + this.isDataAvailable = () => { + const { hiddenSeries } = this.state; + return hiddenSeries.size !== this.series.length; + }; + }; + + render() { + const { hiddenSeries, width } = this.state; + + const data = []; + this.series.map((s, index) => { + data.push(!hiddenSeries.has(index) ? s.datapoints : [{ y: null}]); + }); + + return ( +
+ `${datum.x}: ${datum.y}`} + legendComponent={} + legendPosition="bottom" + padding={{ + bottom: 65, + left: 20, + right: 20, + top: 20 + }} + patternId="pattern_a" // Required for interactive legend functionality + showAxis={false} + themeColor={ChartThemeColor.multiUnordered} + usePatternDefs + width={500} + > + + +
+ ); + } +} +``` + +### Interactive legend with area chart + +This demonstrates how to add an interactive legend using events such as `onMouseOver`, `onMouseOut`, and `onClick`. + +```js +import React from 'react'; +import { + Chart, + ChartArea, + ChartAxis, + ChartGroup, + ChartLegend, + ChartLegendTooltip, + ChartScatter, + ChartThemeColor, + ChartVoronoiContainer, + createContainer, + getInteractiveLegendEvents, + getInteractiveLegendItemStyles, + getResizeObserver +} from '@patternfly/react-charts'; +// import '@patternfly/patternfly/patternfly-charts.css'; // For mixed blend mode + +class InteractiveLegendChart extends React.Component { + constructor(props) { + super(props); + this.containerRef = React.createRef(); + this.observer = () => {}; + this.state = { + hiddenSeries: new Set(), + width: 0 + }; + this.series = [{ + datapoints: [ + { x: '2015', y: 3 }, + { x: '2016', y: 4 }, + { x: '2017', y: 8 }, + { x: '2018', y: 6 } + ], + legendItem: { name: 'Cats' } + }, { + datapoints: [ + { x: '2015', y: 2 }, + { x: '2016', y: 3 }, + { x: '2017', y: 4 }, + { x: '2018', y: 5 }, + { x: '2019', y: 6 } + ], + legendItem: { name: 'Dogs' } + }, { + datapoints: [ + { x: '2015', y: 1 }, + { x: '2016', y: 2 }, + { x: '2017', y: 3 }, + { x: '2018', y: 2 }, + { x: '2019', y: 4 } + ], + legendItem: { name: 'Birds' } + }]; + + // Returns groups of chart names associated with each data series + this.getChartNames = () => { + const result = []; + this.series.map((_, index) => { + // Each group of chart names are hidden / shown together + result.push([`area-${index}`, `scatter-${index}`]); + }); + return result; + }; + + // Returns onMouseOver, onMouseOut, and onClick events for the interactive legend + this.getEvents = () => getInteractiveLegendEvents({ + chartNames: this.getChartNames(), + isHidden: this.isHidden, + legendName: 'legend', + onLegendClick: this.handleLegendClick + }); + + // Returns legend data styled per hiddenSeries + this.getLegendData = () => { + const { hiddenSeries } = this.state; + return this.series.map((s, index) => { + return { + childName: `area-${index}`, // Sync tooltip legend with the series associated with given chart name + ...s.legendItem, // name property + ...getInteractiveLegendItemStyles(hiddenSeries.has(index)) // hidden styles + }; + }); + }; + + // Hide each data series individually + this.handleLegendClick = (props) => { + if (!this.state.hiddenSeries.delete(props.index)) { + this.state.hiddenSeries.add(props.index); + } + this.setState({ hiddenSeries: new Set(this.state.hiddenSeries) }); + }; + + // Set chart width per current window size + this.handleResize = () => { + if (this.containerRef.current && this.containerRef.current.clientWidth) { + this.setState({ width: this.containerRef.current.clientWidth }); + } + }; + + // Returns true if data series is hidden + this.isHidden = (index) => { + const { hiddenSeries } = this.state; // Skip if already hidden + return hiddenSeries.has(index); + }; + + this.isDataAvailable = () => { + const { hiddenSeries } = this.state; + return hiddenSeries.size !== this.series.length; + }; + + // Note: Container order is important + const CursorVoronoiContainer = createContainer("voronoi", "cursor"); + + this.cursorVoronoiContainer = ( + datum.childName.includes('area-') && datum.y !== null ? `${datum.y}` : null} + labelComponent={ datum.x}/>} + mouseFollowTooltips + voronoiDimension="x" + voronoiPadding={50} + /> + ); + }; + + componentDidMount() { + this.observer = getResizeObserver(this.containerRef.current, this.handleResize); + this.handleResize(); + } + + componentWillUnmount() { + this.observer(); + } + + // Tips: + // 1. Omitting hidden components will reassign color scale, use null data instead or custom colors + // 2. Set domain or tick axis labels to account for when all data series are hidden + // 3. Omit tooltip for ChartScatter component by checking childName prop + // 4. Omit tooltip when all data series are hidden + // 5. Clone original container to ensure tooltip events are not lost when data series are hidden / shown + render() { + const { hiddenSeries, width } = this.state; + + const container = React.cloneElement( + this.cursorVoronoiContainer, + { + disable: !this.isDataAvailable() + } + ); + + return ( +
+
+ } + legendPosition="bottom-left" + padding={{ + bottom: 75, // Adjusted to accommodate legend + left: 50, + right: 50, + top: 50, + }} + maxDomain={{y: 9}} + themeColor={ChartThemeColor.multiUnordered} + usePatternDefs + width={width} + > + + + + {this.series.map((s, index) => { + return ( + (active ? 5 : 3)} + /> + ); + })} + + + {this.series.map((s, index) => { + return ( + + ); + })} + + +
+
+ ); + } +} +``` + +### Bar chart +```js +import React from 'react'; +import { Chart, ChartAxis, ChartBar, ChartGroup, ChartThemeColor, ChartVoronoiContainer } from '@patternfly/react-charts'; + +
+ `${datum.name}: ${datum.y}`} constrainToVisibleArea />} + domainPadding={{ x: [30, 25] }} + legendData={[{ name: 'Cats' }, { name: 'Dogs' }, { name: 'Birds' }, { name: 'Mice' }]} + legendPosition="bottom" + height={275} + padding={{ + bottom: 75, // Adjusted to accommodate legend + left: 50, + right: 50, + top: 50 + }} + themeColor={ChartThemeColor.purple} + usePatternDefs + width={450} + > + + + + + + + + + +
+``` + +### Stack chart +```js +import React from 'react'; +import { Chart, ChartAxis, ChartBar, ChartStack, ChartVoronoiContainer } from '@patternfly/react-charts'; + +
+ `${datum.name}: ${datum.y}`} constrainToVisibleArea />} + domainPadding={{ x: [30, 25] }} + legendData={[{ name: 'Cats' }, { name: 'Dogs' }, { name: 'Birds' }, { name: 'Mice' }]} + legendOrientation="vertical" + legendPosition="right" + height={250} + padding={{ + bottom: 50, + left: 50, + right: 200, // Adjusted to accommodate legend + top: 50 + }} + themeColor={ChartThemeColor.green} + usePatternDefs + width={600} + > + + + + + + + + + +
+``` + +### Donut chart +```js +import React from 'react'; +import { ChartDonut } from '@patternfly/react-charts'; + +
+ `${datum.x}: ${datum.y}%`} + legendData={[{ name: 'Cats: 35' }, { name: 'Dogs: 55' }, { name: 'Birds: 10' }]} + legendOrientation="vertical" + legendPosition="right" + padding={{ + bottom: 20, + left: 20, + right: 140, // Adjusted to accommodate legend + top: 20 + }} + subTitle="Pets" + title="100" + themeColor={ChartThemeColor.gold} + usePatternDefs + width={350} + /> +
+``` + +### Donut utilization chart + +This demonstrates how to apply a pattern to the static, unused portion of the donut utilization chart. + +```js +import React from 'react'; +import { ChartDonutUtilization } from '@patternfly/react-charts'; + +
+ datum.x ? `${datum.x}: ${datum.y}%` : null} + legendData={[{ name: `Storage capacity: 45%` }, { name: 'Unused' }]} + legendPosition="bottom" + padding={{ + bottom: 65, // Adjusted to accommodate legend + left: 20, + right: 20, + top: 20 + }} + showStaticPattern + subTitle="of 100 GBps" + title="45%" + themeColor={ChartThemeColor.green} + thresholds={[{ value: 60 }, { value: 90 }]} + usePatternDefs + width={300} + /> +
+``` + +### Donut utilization chart with thresholds + +This demonstrates how to apply patterns to thresholds. + +```js +import React from 'react'; +import { ChartDonutThreshold, ChartDonutUtilization } from '@patternfly/react-charts'; + +
+ datum.x ? datum.x : null} + padding={{ + bottom: 65, // Adjusted to accommodate legend + left: 20, + right: 20, + top: 20 + }} + width={675} + usePatternDefs + > + datum.x ? `${datum.x}: ${datum.y}%` : null} + legendData={[{ name: `Storage capacity: 45%` }, { name: 'Warning threshold at 60%' }, { name: 'Danger threshold at 90%' }]} + legendPosition="bottom" + subTitle="of 100 GBps" + title="45%" + themeColor={ChartThemeColor.orange} + usePatternDefs + /> + +
+``` + +### Custom pattern IDs + +This demonstrates how to omit patterns from pie chart segments. + +The approach uses `usePatternDefs` to generate default pattern defs using the given `patternId` prefix. The `patternScale` property is then used to apply indexed pattern IDs to each pie chart segment. If you want to omit a particular pattern from a pie segment, simply provide `null` instead. + +```js +import React from 'react'; +import { ChartPie, ChartThemeColor } from '@patternfly/react-charts'; + +
+ `${datum.x}: ${datum.y}`} + legendData={[{ name: 'Cats: 15' }, { name: 'Dogs: 15' }, { name: 'Birds: 15' }, { name: 'Fish: 25' }, { name: 'Rabbits: 30' }]} + legendOrientation="vertical" + legendPosition="right" + padding={{ + bottom: 20, + left: 20, + right: 140, // Adjusted to accommodate legend + top: 20 + }} + patternId="pattern_b" + patternScale={['url("#pattern_b:0")', 'url("#pattern_b:1")', null, null, null]} + themeColor={ChartThemeColor.multiUnordered} + usePatternDefs + width={350} + /> +
+``` + +### Custom color scale + +This demonstrates how to apply a custom color scale. + +The approach uses `usePatternDefs` to generate default pattern defs using the given `patternId` prefix and custom `colorScale`. The `patternScale` property is then used to apply indexed pattern IDs to each pie chart segment. If you want to omit a particular pattern from a pie segment, simply provide `null` instead. + +```js +import React from 'react'; +import { ChartPie, ChartThemeColor } from '@patternfly/react-charts'; +import chart_color_blue_300 from '@patternfly/react-tokens/dist/esm/chart_color_blue_300'; +import chart_color_gold_300 from '@patternfly/react-tokens/dist/esm/chart_color_gold_300'; +import chart_color_green_300 from '@patternfly/react-tokens/dist/esm/chart_color_green_300'; + +
+ `${datum.x}: ${datum.y}`} + legendData={[{ name: 'Cats: 35' }, { name: 'Dogs: 55' }, { name: 'Birds: 10' }]} + legendOrientation="vertical" + legendPosition="right" + padding={{ + bottom: 20, + left: 20, + right: 140, // Adjusted to accommodate legend + top: 20 + }} + patternId="pattern_c" + patternScale={['url("#pattern_c:0")', 'url("#pattern_c:1")', null]} + usePatternDefs + width={350} + /> +
+``` + +### Custom pattern defs + +This demonstrates how to create custom patterns. + +The approach uses custom pattern defs. The `patternScale` property is used to apply pattern IDs to each pie chart segment. If you want to omit a particular pattern from a pie segment, simply provide `null` instead. + +Note that `usePatternDefs` and `patternId` are not used here. + +```js +import React from 'react'; +import { ChartPie, ChartThemeColor } from '@patternfly/react-charts'; +import chart_color_blue_300 from '@patternfly/react-tokens/dist/esm/chart_color_blue_300'; +import chart_color_green_300 from '@patternfly/react-tokens/dist/esm/chart_color_green_300'; + +
+ + + + + + + + + + + `${datum.x}: ${datum.y}`} + legendData={[{ name: 'Cats: 35' }, { name: 'Dogs: 55' }, { name: 'Birds: 10' }]} + legendOrientation="vertical" + legendPosition="right" + padding={{ + bottom: 20, + left: 20, + right: 140, // Adjusted to accommodate legend + top: 20 + }} + patternScale={['url("#pattern_d:0")', 'url("#pattern_d:1")', null]} + themeColor={ChartThemeColor.multiUnordered} + width={350} + /> +
+``` + +## Documentation +### Tips +- See Victory's [FAQ](https://formidable.com/open-source/victory/docs/faq) +- `ChartLegend` may be used as a standalone component, instead of using `legendData` + +### Note +Currently, the generated documention below is not able to resolve type definitions from Victory imports. For the +components used in the examples above, Victory pass-thru props are also documented here: + +- For `Chart` props, see [VictoryChart](https://formidable.com/open-source/victory/docs/victory-chart) +- For `ChartArea` props, see [VictoryArea](https://formidable.com/open-source/victory/docs/victory-area) +- For `ChartAxis` props, see [VictoryAxis](https://formidable.com/open-source/victory/docs/victory-axis) +- For `ChartLegend` props, see [VictoryLegend](https://formidable.com/open-source/victory/docs/victory-legend) +- For `ChartGroup` props, see [VictoryGroup](https://formidable.com/open-source/victory/docs/victory-group) +- For `ChartPie` props, see [VictoryPie](https://formidable.com/open-source/victory/docs/victory-pie) +- For `ChartScatter` props, see [VictoryScatter](https://formidable.com/open-source/victory/docs/victory-scatter) +- For `ChartVoronoiContainer` props, see [VictoryVoronoiContainer](https://formidable.com/open-source/victory/docs/victory-voronoi-container)