Skip to content

Commit

Permalink
bug(react-charting): Bug fixes for declarative chart control (#33507)
Browse files Browse the repository at this point in the history
Co-authored-by: Atishay Jain (atisjai) <[email protected]>
  • Loading branch information
Anush2303 and AtishayMsft authored Dec 27, 2024
1 parent 3e4cc98 commit 99cbe5c
Show file tree
Hide file tree
Showing 10 changed files with 151 additions and 35 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "plotly examples bug fixes",
"packageName": "@fluentui/react-charting",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import {
isArrayOrTypedArray,
isDateArray,
isNumberArray,
isMonthArray,
sanitizeJson,
updateXValues,
transformPlotlyJsonToDonutProps,
transformPlotlyJsonToVSBCProps,
transformPlotlyJsonToScatterChartProps,
Expand Down Expand Up @@ -90,6 +92,7 @@ export const DeclarativeChart: React.FunctionComponent<DeclarativeChartProps> =
const xValues = data[0].x;
const isXDate = isDateArray(xValues);
const isXNumber = isNumberArray(xValues);
const isXMonth = isMonthArray(xValues);
const colorMap = useColorMapping();
const theme = useTheme();
const isDarkTheme = theme?.isInverted ?? false;
Expand Down Expand Up @@ -166,7 +169,7 @@ export const DeclarativeChart: React.FunctionComponent<DeclarativeChartProps> =
return (
<GroupedVerticalBarChart
{...transformPlotlyJsonToGVBCProps(plotlySchema, colorMap, isDarkTheme)}
legendProps={legendProps}
legendProps={{ ...legendProps, canSelectMultipleLegends: true, selectedLegends: activeLegends }}
componentRef={chartRef}
calloutProps={{ layerProps: { eventBubblingEnabled: true } }}
/>
Expand All @@ -175,38 +178,56 @@ export const DeclarativeChart: React.FunctionComponent<DeclarativeChartProps> =
return (
<VerticalStackedBarChart
{...transformPlotlyJsonToVSBCProps(plotlySchema, colorMap, isDarkTheme)}
legendProps={legendProps}
legendProps={{ ...legendProps, canSelectMultipleLegends: true, selectedLegends: activeLegends }}
componentRef={chartRef}
calloutProps={{ layerProps: { eventBubblingEnabled: true } }}
/>
);
}
case 'scatter':
const isAreaChart = data.some((series: any) => series.fill === 'tonexty' || series.fill === 'tozeroy');
if (isXDate || isXNumber) {
const renderChart = (chartProps: any) => {
if (isAreaChart) {
return (
<AreaChart
{...transformPlotlyJsonToScatterChartProps({ data, layout }, true, colorMap, isDarkTheme)}
legendProps={legendProps}
componentRef={chartRef}
calloutProps={{ layerProps: { eventBubblingEnabled: true } }}
/>
);
return <AreaChart {...chartProps} />;
}
return (
<LineChart
{...transformPlotlyJsonToScatterChartProps({ data, layout }, false, colorMap, isDarkTheme)}
legendProps={{ ...legendProps, canSelectMultipleLegends: true, selectedLegends: activeLegends }}
componentRef={chartRef}
calloutProps={{ layerProps: { eventBubblingEnabled: true } }}
{...{
...chartProps,
legendProps: {
onChange: onActiveLegendsChange,
canSelectMultipleLegends: true,
selectedLegends: activeLegends,
},
}}
/>
);
};
if (isXDate || isXNumber) {
const chartProps = {
...transformPlotlyJsonToScatterChartProps({ data, layout }, isAreaChart, colorMap, isDarkTheme),
legendProps,
componentRef: chartRef,
calloutProps: { layerProps: { eventBubblingEnabled: true } },
};
return renderChart(chartProps);
} else if (isXMonth) {
const updatedData = data.map((dataPoint: any) => ({
...dataPoint,
x: updateXValues(dataPoint.x),
}));
const chartProps = {
...transformPlotlyJsonToScatterChartProps({ data: updatedData, layout }, isAreaChart, colorMap, isDarkTheme),
legendProps,
componentRef: chartRef,
calloutProps: { layerProps: { eventBubblingEnabled: true } },
};
return renderChart(chartProps);
}
return (
<VerticalStackedBarChart
{...transformPlotlyJsonToVSBCProps(plotlySchema, colorMap, isDarkTheme)}
legendProps={legendProps}
legendProps={{ ...legendProps, canSelectMultipleLegends: true, selectedLegends: activeLegends }}
componentRef={chartRef}
calloutProps={{ layerProps: { eventBubblingEnabled: true } }}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,38 @@ const isDate = (value: any): boolean => !isNaN(Date.parse(value));
const isNumber = (value: any): boolean => !isNaN(parseFloat(value)) && isFinite(value);
export const isDateArray = (array: any[]): boolean => isArrayOrTypedArray(array) && array.every(isDate);
export const isNumberArray = (array: any[]): boolean => isArrayOrTypedArray(array) && array.every(isNumber);
export const isMonthArray = (array: any[]): boolean => {
if (array && array.length > 0) {
const presentYear = new Date().getFullYear();
return array.every(possiblyMonthValue => {
return isDate(`${possiblyMonthValue} 01, ${presentYear}`);
});
}
return false;
};

export const updateXValues = (xValues: any[]): any[] => {
const presentYear = new Date().getFullYear();
const dates = xValues.map(possiblyMonthValue => {
const parsedDate = `${possiblyMonthValue} 01, ${presentYear}`;
return isDate(parsedDate) ? new Date(parsedDate) : null;
});
for (let i = dates.length - 1; i > 0; i--) {
const currentMonth = dates[i]!.getMonth();
const previousMonth = dates[i - 1]!.getMonth();
const currentYear = dates[i]!.getFullYear();
const previousYear = dates[i - 1]!.getFullYear();
if (previousMonth >= currentMonth) {
dates[i - 1]!.setFullYear(dates[i]!.getFullYear() - 1);
} else if (previousYear > currentYear) {
dates[i - 1]!.setFullYear(currentYear);
}
}
xValues = xValues.map((month, index) => {
return `${month} 01, ${dates[index]!.getFullYear()}`;
});
return xValues;
};
export const getColor = (
legendLabel: string,
colorMap: React.MutableRefObject<Map<string, string>>,
Expand Down Expand Up @@ -112,7 +143,7 @@ export const transformPlotlyJsonToVSBCProps = (
mapXToDataPoints[x] = { xAxisPoint: x, chartData: [], lineData: [] };
}
const legend: string = series.name || `Series ${index1 + 1}`;
if (series.type === 'bar') {
if (series.type === 'bar' || series.type === 'scatter') {
const color = getColor(legend, colorMap, isDarkTheme);
mapXToDataPoints[x].chartData.push({
legend,
Expand Down Expand Up @@ -162,6 +193,7 @@ export const transformPlotlyJsonToGVBCProps = (
mapXToDataPoints[x].series.push({
key: legend,
data: series.y?.[index2],
xAxisCalloutData: x as string,
color,
legend,
});
Expand Down Expand Up @@ -458,15 +490,23 @@ export const transformPlotlyJsonToGaugeProps = (
const { data, layout } = jsonObj;
const firstData = data[0];

const segments = firstData.gauge?.steps?.map((step: any, index: number): IGaugeChartSegment => {
const legend = step.name || `Segment ${index + 1}`;
const color = getColor(legend, colorMap, isDarkTheme);
return {
legend,
size: step.range?.[1] - step.range?.[0],
color,
};
});
const segments = firstData.gauge?.steps?.length
? firstData.gauge.steps.map((step: any, index: number): IGaugeChartSegment => {
const legend = step.name || `Segment ${index + 1}`;
const color = getColor(legend, colorMap, isDarkTheme);
return {
legend,
size: step.range?.[1] - step.range?.[0],
color,
};
})
: [
{
legend: 'Segment 1',
size: (firstData.gauge?.range?.[1] ?? 0) - (firstData.gauge?.range?.[0] ?? 0),
color: getColor('Segment 1', colorMap, isDarkTheme),
},
];

let sublabel: string | undefined;
let sublabelColor: string | undefined;
Expand Down Expand Up @@ -500,7 +540,6 @@ export const transformPlotlyJsonToGaugeProps = (
chartValueFormat: () => firstData.value,
width: typeof layout?.width === 'number' ? layout?.width : 0,
height: typeof layout?.height === 'number' ? layout?.height : 0,
hideLegend: true,
styles,
};
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function toImage(chartContainer?: HTMLElement | null, opts: IImageExportO
}

try {
const background = opts.background || 'white';
const background = 'white'; // Background is coming as --var(xxx) when used with v8 wrapper in v9
const svg = toSVG(chartContainer, background);

const svgData = new XMLSerializer().serializeToString(svg.node);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ import { FocusZone, FocusZoneDirection, FocusZoneTabbableElements } from '@fluen
import { IAccessibilityProps, ChartHoverCard, ILegend, Legends } from '../../index';
import { Pie } from './Pie/index';
import { IChartDataPoint, IDonutChartProps, IDonutChartStyleProps, IDonutChartStyles } from './index';
import { getAccessibleDataObject, getColorFromToken, getNextColor, getNextGradient } from '../../utilities/index';
import {
getAccessibleDataObject,
getColorFromToken,
getNextColor,
getNextGradient,
areArraysEqual,
} from '../../utilities/index';
import { convertToLocaleString } from '../../utilities/locale-util';
import { IChart } from '../../types/index';

Expand Down Expand Up @@ -98,7 +104,7 @@ export class DonutChartBase extends React.Component<IDonutChartProps, IDonutChar
}

public componentDidUpdate(prevProps: IDonutChartProps): void {
if (prevProps.legendProps?.selectedLegends !== this.props.legendProps?.selectedLegends) {
if (!areArraysEqual(prevProps.legendProps?.selectedLegends, this.props.legendProps?.selectedLegends)) {
this.setState({
selectedLegends: this.props.legendProps?.selectedLegends || [],
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
createStringYAxis,
getNextGradient,
getNextColor,
areArraysEqual,
} from '../../utilities/index';
import {
IAccessibilityProps,
Expand Down Expand Up @@ -146,9 +147,9 @@ export class GroupedVerticalBarChartBase
}

public componentDidUpdate(prevProps: IGroupedVerticalBarChartProps): void {
if (prevProps.legendProps?.selectedLegend !== this.props.legendProps?.selectedLegend) {
if (!areArraysEqual(prevProps.legendProps?.selectedLegends, this.props.legendProps?.selectedLegends)) {
this.setState({
selectedLegend: this.props.legendProps?.selectedLegend ?? '',
selectedLegends: this.props.legendProps?.selectedLegends || [],
});
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import {
domainRangeOfNumericForAreaChart,
createStringYAxis,
formatDate,
areArraysEqual,
} from '../../utilities/index';
import { IChart } from '../../types/index';

Expand Down Expand Up @@ -232,7 +233,7 @@ export class LineChartBase extends React.Component<ILineChartProps, ILineChartSt
public componentDidUpdate(prevProps: ILineChartProps): void {
if (
prevProps.legendProps?.selectedLegend !== this.props.legendProps?.selectedLegend ||
prevProps.legendProps?.selectedLegends !== this.props.legendProps?.selectedLegends
!areArraysEqual(prevProps.legendProps?.selectedLegends, this.props.legendProps?.selectedLegends)
) {
this.setState({
selectedLegend: this.props.legendProps?.selectedLegend ?? '',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import {
createStringYAxis,
formatDate,
getNextGradient,
areArraysEqual,
} from '../../utilities/index';
import { IChart } from '../../types/index';

Expand Down Expand Up @@ -171,9 +172,9 @@ export class VerticalStackedBarChartBase
}

public componentDidUpdate(prevProps: IVerticalStackedBarChartProps): void {
if (prevProps.legendProps?.selectedLegend !== this.props.legendProps?.selectedLegend) {
if (!areArraysEqual(prevProps.legendProps?.selectedLegends, this.props.legendProps?.selectedLegends)) {
this.setState({
selectedLegend: this.props.legendProps?.selectedLegend ?? '',
selectedLegends: this.props.legendProps?.selectedLegends || [],
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1182,3 +1182,25 @@ describe('getGradientFromToken', () => {
expect(result).toEqual(['invalidTokenWithoutDot', 'invalidTokenWithoutDot']);
});
});

describe('test array equality utility', () => {
it('both arrays are undefined', () => {
expect(utils.areArraysEqual(undefined, undefined) === true);
});

it('second array is undefined', () => {
expect(utils.areArraysEqual(['ac', 'bd'], undefined) === true);
});

it('first array is undefined', () => {
expect(utils.areArraysEqual(undefined, ['cg', 'df']) === false);
});

it('both arrays are unequal', () => {
expect(utils.areArraysEqual(['ae', 'bf'], ['cg', 'dh']) === false);
});

it('both arrays are equal', () => {
expect(utils.areArraysEqual(['ab', 'cd', 'ef', 'gh'], ['ab', 'cd', 'ef', 'gh']) === true);
});
});
18 changes: 18 additions & 0 deletions packages/charts/react-charting/src/utilities/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1415,3 +1415,21 @@ export function getSecureProps(props: Record<string, any> = {}): Record<string,
const { dangerouslySetInnerHTML, ...result } = props;
return result;
}

export function areArraysEqual(arr1?: string[], arr2?: string[]): boolean {
if (arr1 === arr2) {
return true;
}
if (!arr1 && !arr2) {
return true;
}
if (!arr1 || !arr2 || arr1.length !== arr2.length) {
return false;
}
for (let i = 0; i < arr1.length; i++) {
if (arr1[i] !== arr2[i]) {
return false;
}
}
return true;
}

0 comments on commit 99cbe5c

Please sign in to comment.