Skip to content

Commit

Permalink
feat(charts): add RTL legend support (#9570)
Browse files Browse the repository at this point in the history
* feat(charts): add RTL legend support

* clone data/label components

* remove legend positions, add beta

* remove last position update

* update logic to account for text length

* update clones
  • Loading branch information
kmcfaul authored Sep 22, 2023
1 parent 0f8ac62 commit 99d8d97
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 5 deletions.
28 changes: 27 additions & 1 deletion packages/react-charts/src/components/Chart/Chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,13 @@ import { ChartCommonStyles } from '../ChartTheme/ChartStyles';
import { ChartThemeDefinition } from '../ChartTheme/ChartTheme';
import { getClassName } from '../ChartUtils/chart-helpers';
import { getLabelTextSize } from '../ChartUtils/chart-label';
import { getComputedLegend, getLegendItemsExtraHeight } from '../ChartUtils/chart-legend';
import { getComputedLegend, getLegendItemsExtraHeight, getLegendMaxTextWidth } from '../ChartUtils/chart-legend';
import { getPaddingForSide } from '../ChartUtils/chart-padding';
import { getPatternDefs, mergePatternData, useDefaultPatternProps } from '../ChartUtils/chart-patterns';
import { getChartTheme } from '../ChartUtils/chart-theme-types';
import { useEffect } from 'react';
import { ChartLabel } from '../ChartLabel/ChartLabel';
import { ChartPoint } from '../ChartPoint/ChartPoint';

/**
* Chart is a wrapper component that reconciles the domain for all its children, controls the layout of the chart,
Expand Down Expand Up @@ -284,6 +286,10 @@ export interface ChartProps extends VictoryChartProps {
* cases, the legend may not be visible until enough padding is applied.
*/
legendPosition?: 'bottom' | 'bottom-left' | 'right';
/**
* @beta Text direction of the legend labels.
*/
legendDirection?: 'ltr' | 'rtl';
/**
* The maxDomain prop defines a maximum domain value for a chart. This prop is useful in situations where the maximum
* domain of a chart is static, while the minimum value depends on data or other variable information. If the domain
Expand Down Expand Up @@ -471,6 +477,7 @@ export const Chart: React.FunctionComponent<ChartProps> = ({
legendComponent = <ChartLegend />,
legendData,
legendPosition = ChartCommonStyles.legend.position,
legendDirection = 'ltr',
name,
padding,
patternScale,
Expand Down Expand Up @@ -522,11 +529,30 @@ export const Chart: React.FunctionComponent<ChartProps> = ({
...(labelComponent && { labelComponent }) // Override label component props
});

let legendXOffset = 0;
if (legendDirection === 'rtl') {
legendXOffset = getLegendMaxTextWidth(legendData, theme);
}

const legend = React.cloneElement(legendComponent, {
data: legendData,
...(name && { name: `${name}-${(legendComponent as any).type.displayName}` }),
orientation: legendOrientation,
theme,
...(legendDirection === 'rtl' && {
dataComponent: legendComponent.props.dataComponent ? (
React.cloneElement(legendComponent.props.dataComponent, { transform: `translate(${legendXOffset})` })
) : (
<ChartPoint transform={`translate(${legendXOffset})`} />
)
}),
...(legendDirection === 'rtl' && {
labelComponent: legendComponent.props.labelComponent ? (
React.cloneElement(legendComponent.props.labelComponent, { direction: 'rtl', dx: legendXOffset - 30 })
) : (
<ChartLabel direction="rtl" dx={legendXOffset - 30} />
)
}),
...legendComponent.props
});

Expand Down
37 changes: 36 additions & 1 deletion packages/react-charts/src/components/ChartBullet/ChartBullet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { ChartLegend } from '../ChartLegend/ChartLegend';
import { ChartThemeDefinition } from '../ChartTheme/ChartTheme';
import { ChartTooltip } from '../ChartTooltip/ChartTooltip';
import { ChartBulletStyles } from '../ChartTheme/ChartStyles';
import { getComputedLegend, getLegendItemsExtraHeight } from '../ChartUtils/chart-legend';
import { getComputedLegend, getLegendItemsExtraHeight, getLegendMaxTextWidth } from '../ChartUtils/chart-legend';
import { ChartBulletComparativeErrorMeasure } from './ChartBulletComparativeErrorMeasure';
import { ChartBulletComparativeMeasure } from './ChartBulletComparativeMeasure';
import { ChartBulletComparativeWarningMeasure } from './ChartBulletComparativeWarningMeasure';
Expand All @@ -29,6 +29,8 @@ import { getBulletDomain } from './utils/chart-bullet-domain';
import { getBulletThemeWithLegendColorScale } from './utils/chart-bullet-theme';
import { getPaddingForSide } from '../ChartUtils/chart-padding';
import { useEffect } from 'react';
import { ChartPoint } from '../ChartPoint/ChartPoint';
import { ChartLabel } from '../ChartLabel/ChartLabel';

/**
* ChartBullet renders a dataset as a bullet chart.
Expand Down Expand Up @@ -252,6 +254,10 @@ export interface ChartBulletProps {
* cases, the legend may not be visible until enough padding is applied.
*/
legendPosition?: 'bottom' | 'bottom-left' | 'right';
/**
* @beta Text direction of the legend labels.
*/
legendDirection?: 'ltr' | 'rtl';
/**
* The maxDomain prop defines a maximum domain value for a chart. This prop is useful in situations where the maximum
* domain of a chart is static, while the minimum value depends on data or other variable information. If the domain
Expand Down Expand Up @@ -509,6 +515,7 @@ export const ChartBullet: React.FunctionComponent<ChartBulletProps> = ({
legendComponent = <ChartLegend />,
legendItemsPerRow,
legendPosition = 'bottom',
legendDirection = 'ltr',
maxDomain,
minDomain,
name,
Expand Down Expand Up @@ -656,6 +663,20 @@ export const ChartBullet: React.FunctionComponent<ChartBulletProps> = ({
...comparativeZeroMeasureComponent.props
});

let legendXOffset = 0;
if (legendDirection === 'rtl') {
legendXOffset = getLegendMaxTextWidth(
[
...(primaryDotMeasureLegendData ? primaryDotMeasureLegendData : []),
...(primarySegmentedMeasureLegendData ? primarySegmentedMeasureLegendData : []),
...(comparativeWarningMeasureLegendData ? comparativeWarningMeasureLegendData : []),
...(comparativeErrorMeasureLegendData ? comparativeErrorMeasureLegendData : []),
...(qualitativeRangeLegendData ? qualitativeRangeLegendData : [])
],
theme
);
}

// Legend
const legend = React.cloneElement(legendComponent, {
data: [
Expand All @@ -670,6 +691,20 @@ export const ChartBullet: React.FunctionComponent<ChartBulletProps> = ({
orientation: legendOrientation,
position: legendPosition,
theme,
...(legendDirection === 'rtl' && {
dataComponent: legendComponent.props.dataComponent ? (
React.cloneElement(legendComponent.props.dataComponent, { transform: `translate(${legendXOffset})` })
) : (
<ChartPoint transform={`translate(${legendXOffset})`} />
)
}),
...(legendDirection === 'rtl' && {
labelComponent: legendComponent.props.labelComponent ? (
React.cloneElement(legendComponent.props.labelComponent, { direction: 'rtl', dx: legendXOffset - 30 })
) : (
<ChartLabel direction="rtl" dx={legendXOffset - 30} />
)
}),
...legendComponent.props
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,10 @@ export interface ChartDonutProps extends ChartPieProps {
* cases, the legend may not be visible until enough padding is applied.
*/
legendPosition?: 'bottom' | 'right';
/**
* @beta Text direction of the legend labels.
*/
legendDirection?: 'ltr' | 'rtl';
/**
* The name prop is typically used to reference a component instance when defining shared events. However, this
* optional prop may also be applied to child elements as an ID prefix. This is a workaround to ensure Victory
Expand Down Expand Up @@ -573,6 +577,7 @@ export const ChartDonut: React.FunctionComponent<ChartDonutProps> = ({
containerComponent = <ChartContainer />,
innerRadius,
legendPosition = ChartCommonStyles.legend.position,
legendDirection = 'ltr',
name,
padAngle,
padding,
Expand Down Expand Up @@ -698,6 +703,7 @@ export const ChartDonut: React.FunctionComponent<ChartDonutProps> = ({
innerRadius={chartInnerRadius > 0 ? chartInnerRadius : 0}
key="pf-chart-donut-pie"
legendPosition={legendPosition}
legendDirection={legendDirection}
name={name}
padAngle={padAngle !== undefined ? padAngle : getPadAngle}
padding={padding}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,10 @@ export interface ChartDonutUtilizationProps extends ChartDonutProps {
* cases, the legend may not be visible until enough padding is applied.
*/
legendPosition?: 'bottom' | 'right';
/**
* @beta Text direction of the legend labels.
*/
legendDirection?: 'ltr' | 'rtl';
/**
* The labelRadius prop defines the radius of the arc that will be used for positioning each slice label.
* If this prop is not set, the label radius will default to the radius of the pie + label padding.
Expand Down Expand Up @@ -589,6 +593,7 @@ export const ChartDonutUtilization: React.FunctionComponent<ChartDonutUtilizatio
invert = false,
isStatic = true,
legendPosition = ChartCommonStyles.legend.position,
legendDirection = 'ltr',
padding,
standalone = true,
themeColor,
Expand Down Expand Up @@ -679,6 +684,7 @@ export const ChartDonutUtilization: React.FunctionComponent<ChartDonutUtilizatio
height={height}
key="pf-chart-donut-utilization"
legendPosition={legendPosition}
legendDirection={legendDirection}
padding={padding}
standalone={false}
theme={getThresholdTheme()}
Expand Down
30 changes: 27 additions & 3 deletions packages/react-charts/src/components/ChartPie/ChartPie.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,13 @@ import { ChartLegend } from '../ChartLegend/ChartLegend';
import { ChartCommonStyles } from '../ChartTheme/ChartStyles';
import { ChartThemeDefinition } from '../ChartTheme/ChartTheme';
import { ChartTooltip } from '../ChartTooltip/ChartTooltip';
import { getComputedLegend, getLegendItemsExtraHeight } from '../ChartUtils/chart-legend';
import { getComputedLegend, getLegendItemsExtraHeight, getLegendMaxTextWidth } from '../ChartUtils/chart-legend';
import { getPaddingForSide } from '../ChartUtils/chart-padding';
import { getPatternDefs, useDefaultPatternProps } from '../ChartUtils/chart-patterns';
import { getTheme } from '../ChartUtils/chart-theme';
import { useEffect } from 'react';
import { ChartPoint } from '../ChartPoint/ChartPoint';
import { ChartLabel } from '../ChartLabel/ChartLabel';

/**
* ChartPie renders a dataset as a pie chart.
Expand Down Expand Up @@ -333,6 +335,10 @@ export interface ChartPieProps extends VictoryPieProps {
* cases, the legend may not be visible until enough padding is applied.
*/
legendPosition?: 'bottom' | 'right';
/**
* @beta Text direction of the legend labels.
*/
legendDirection?: 'ltr' | 'rtl';
/**
* The name prop is typically used to reference a component instance when defining shared events. However, this
* optional prop may also be applied to child elements as an ID prefix. This is a workaround to ensure Victory
Expand Down Expand Up @@ -498,16 +504,15 @@ export const ChartPie: React.FunctionComponent<ChartPieProps> = ({
legendComponent = <ChartLegend />,
legendData,
legendPosition = ChartCommonStyles.legend.position,
legendDirection = 'ltr',
name,
patternScale,
patternUnshiftIndex,

padding,
radius,
standalone = true,
style,
themeColor,

// destructure last
theme = getTheme(themeColor),
labelComponent = allowTooltip ? (
Expand Down Expand Up @@ -576,13 +581,32 @@ export const ChartPie: React.FunctionComponent<ChartPieProps> = ({
/>
);

let legendXOffset = 0;
if (legendDirection === 'rtl') {
legendXOffset = getLegendMaxTextWidth(legendData, theme);
}

const legend = React.cloneElement(legendComponent, {
colorScale,
data: legendData,
...(name && { name: `${name}-${(legendComponent as any).type.displayName}` }),
key: 'pf-chart-pie-legend',
orientation: legendOrientation,
theme,
...(legendDirection === 'rtl' && {
dataComponent: legendComponent.props.dataComponent ? (
React.cloneElement(legendComponent.props.dataComponent, { transform: `translate(${legendXOffset})` })
) : (
<ChartPoint transform={`translate(${legendXOffset})`} />
)
}),
...(legendDirection === 'rtl' && {
labelComponent: legendComponent.props.labelComponent ? (
React.cloneElement(legendComponent.props.labelComponent, { direction: 'rtl', dx: legendXOffset - 30 })
) : (
<ChartLabel direction="rtl" dx={legendXOffset - 30} />
)
}),
...legendComponent.props
});

Expand Down
17 changes: 17 additions & 0 deletions packages/react-charts/src/components/ChartUtils/chart-legend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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';
import * as React from 'react';

Expand Down Expand Up @@ -49,6 +50,22 @@ interface ChartLegendTextMaxSizeInterface {
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
*/

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
Expand Down

0 comments on commit 99d8d97

Please sign in to comment.