Skip to content

Commit

Permalink
Merge branch 'master' of https://github.com/microsoft/fluentui into u…
Browse files Browse the repository at this point in the history
…sers/srmukher/multiLegendVBC
  • Loading branch information
srmukher committed Dec 26, 2024
2 parents a652abc + bdba671 commit b689f93
Show file tree
Hide file tree
Showing 21 changed files with 342 additions and 350 deletions.
30 changes: 30 additions & 0 deletions packages/charts/react-charting/CHANGELOG.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,36 @@
{
"name": "@fluentui/react-charting",
"entries": [
{
"date": "Thu, 26 Dec 2024 07:21:14 GMT",
"tag": "@fluentui/react-charting_v5.23.31",
"version": "5.23.31",
"comments": {
"patch": [
{
"author": "110246001+krkshitij@users.noreply.github.com",
"package": "@fluentui/react-charting",
"commit": "0dd9a713b54a6d5b5929ac7febd1c7e44c83efca",
"comment": "fix: remove duplicate legends in vertical bar chart"
}
]
}
},
{
"date": "Wed, 25 Dec 2024 07:21:55 GMT",
"tag": "@fluentui/react-charting_v5.23.30",
"version": "5.23.30",
"comments": {
"patch": [
{
"author": "120183316+srmukher@users.noreply.github.com",
"package": "@fluentui/react-charting",
"commit": "fa08fec97a5bc070e2866633c792f7066f0c86e8",
"comment": "Legends multi selection for Donut Charts"
}
]
}
},
{
"date": "Mon, 23 Dec 2024 07:22:57 GMT",
"tag": "@fluentui/react-charting_v5.23.29",
Expand Down
20 changes: 19 additions & 1 deletion packages/charts/react-charting/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,27 @@
# Change Log - @fluentui/react-charting

This log was last generated on Mon, 23 Dec 2024 07:22:57 GMT and should not be manually modified.
This log was last generated on Thu, 26 Dec 2024 07:21:14 GMT and should not be manually modified.

<!-- Start content -->

## [5.23.31](https://github.com/microsoft/fluentui/tree/@fluentui/react-charting_v5.23.31)

Thu, 26 Dec 2024 07:21:14 GMT
[Compare changes](https://github.com/microsoft/fluentui/compare/@fluentui/react-charting_v5.23.30..@fluentui/react-charting_v5.23.31)

### Patches

- fix: remove duplicate legends in vertical bar chart ([PR #33518](https://github.com/microsoft/fluentui/pull/33518) by 110246001+krkshitij@users.noreply.github.com)

## [5.23.30](https://github.com/microsoft/fluentui/tree/@fluentui/react-charting_v5.23.30)

Wed, 25 Dec 2024 07:21:55 GMT
[Compare changes](https://github.com/microsoft/fluentui/compare/@fluentui/react-charting_v5.23.29..@fluentui/react-charting_v5.23.30)

### Patches

- Legends multi selection for Donut Charts ([PR #33447](https://github.com/microsoft/fluentui/pull/33447) by 120183316+srmukher@users.noreply.github.com)

## [5.23.29](https://github.com/microsoft/fluentui/tree/@fluentui/react-charting_v5.23.29)

Mon, 23 Dec 2024 07:22:57 GMT
Expand Down
2 changes: 1 addition & 1 deletion packages/charts/react-charting/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@fluentui/react-charting",
"version": "5.23.29",
"version": "5.23.31",
"description": "React web charting controls for Microsoft fluentui system.",
"main": "lib-commonjs/index.js",
"module": "lib/index.js",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ export const DeclarativeChart: React.FunctionComponent<DeclarativeChartProps> =
return (
<DonutChart
{...transformPlotlyJsonToDonutProps(plotlySchema, colorMap, isDarkTheme)}
legendProps={legendProps}
legendProps={{ ...legendProps, canSelectMultipleLegends: true }}
componentRef={chartRef}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ export const transformPlotlyJsonToVBCProps = (
chartTitle: typeof layout?.title === 'string' ? layout?.title : '',
// width: layout?.width,
// height: layout?.height,
hideLegend: true,
// hideLegend: true,
barWidth: 24,
supportNegativeData: true,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,11 @@ export class Arc extends React.Component<IArcProps, IArcState> {
}

public render(): JSX.Element {
const { arc, href, focusedArcId } = this.props;
const { arc, href, focusedArcId, activeArc } = this.props;
const getClassNames = classNamesFunction<IArcStyleProps, IArcStyles>();
const id = this.props.uniqText! + this.props.data!.data.legend!.replace(/\s+/, '') + this.props.data!.data.data;
const opacity: number =
this.props.activeArc === this.props.data!.data.legend || this.props.activeArc === '' ? 1 : 0.1;

activeArc && activeArc.length > 0 ? (activeArc.includes(this.props.data?.data.legend!) ? 1 : 0.1) : 1;
const startAngle = this.props.data?.startAngle ?? 0;
const endAngle = (this.props.data?.endAngle ?? 0) - startAngle;
const cornerRadius = this.props.roundCorners ? 3 : 0;
Expand Down Expand Up @@ -70,7 +69,9 @@ export class Arc extends React.Component<IArcProps, IArcState> {
d={arc.cornerRadius(cornerRadius)(this.props.data)}
onFocus={this._onFocus.bind(this, this.props.data!.data, id)}
className={classNames.root}
data-is-focusable={this.props.activeArc === this.props.data!.data.legend || this.props.activeArc === ''}
data-is-focusable={
this._shouldHighlightArc(this.props.data!.data.legend!) || this.props.activeArc?.length === 0
}
onMouseOver={this._hoverOn.bind(this, this.props.data!.data)}
onMouseMove={this._hoverOn.bind(this, this.props.data!.data)}
onMouseLeave={this._hoverOff}
Expand Down Expand Up @@ -123,13 +124,18 @@ export class Arc extends React.Component<IArcProps, IArcState> {
return point.callOutAccessibilityData?.ariaLabel || (legend ? `${legend}, ` : '') + `${yValue}.`;
};

private _renderArcLabel = (className: string) => {
const { arc, data, innerRadius, outerRadius, showLabelsInPercent, totalValue, hideLabels, activeArc } = this.props;
private _shouldHighlightArc = (legend?: string): boolean => {
const { activeArc } = this.props;
// If no activeArc is provided, highlight all arcs. Otherwise, only highlight the arcs that are active.
return !activeArc || activeArc.length === 0 || legend === undefined || activeArc.includes(legend);
};

private _renderArcLabel = (className: string) => {
const { arc, data, innerRadius, outerRadius, showLabelsInPercent, totalValue, hideLabels } = this.props;
if (
hideLabels ||
Math.abs(data!.endAngle - data!.startAngle) < Math.PI / 12 ||
(activeArc !== data!.data.legend && activeArc !== '')
!this._shouldHighlightArc(data!.data.legend!)
) {
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export interface IArcProps {
/**
* Active Arc for chart
*/
activeArc?: string;
activeArc?: string[];

/**
* internal prop for href
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ export interface IDonutChartState {
xCalloutValue?: string;
yCalloutValue?: string;
focusedArcId?: string;
selectedLegend: string;
dataPointCalloutProps?: IChartDataPoint;
callOutAccessibilityData?: IAccessibilityProps;
selectedLegends: string[];
}

export class DonutChartBase extends React.Component<IDonutChartProps, IDonutChartState> implements IChart {
Expand Down Expand Up @@ -73,12 +73,12 @@ export class DonutChartBase extends React.Component<IDonutChartProps, IDonutChar
legend: '',
_width: this.props.width || 200,
_height: this.props.height || 200,
activeLegend: '',
activeLegend: undefined,
color: '',
xCalloutValue: '',
yCalloutValue: '',
selectedLegend: props.legendProps?.selectedLegend ?? '',
focusedArcId: '',
selectedLegends: [],
};
this._hoverCallback = this._hoverCallback.bind(this);
this._focusCallback = this._focusCallback.bind(this);
Expand Down Expand Up @@ -115,7 +115,6 @@ export class DonutChartBase extends React.Component<IDonutChartProps, IDonutChar
Math.min(this.state._width! - donutMarginHorizontal, this.state._height! - donutMarginVertical) / 2;
const chartData = this._elevateToMinimums(points.filter((d: IChartDataPoint) => d.data! >= 0));
const valueInsideDonut = this._valueInsideDonut(this.props.valueInsideDonut!, chartData!);

return !this._isChartEmpty() ? (
<div
className={this._classNames.root}
Expand Down Expand Up @@ -248,23 +247,17 @@ export class DonutChartBase extends React.Component<IDonutChartProps, IDonutChar
const legend: ILegend = {
title: point.legend!,
color,
action: () => {
if (this.state.selectedLegend === point.legend) {
this.setState({ selectedLegend: '' });
} else {
this.setState({ selectedLegend: point.legend! });
}
},
hoverAction: () => {
this._handleChartMouseLeave();
this.setState({ activeLegend: point.legend! });
},
onMouseOutAction: () => {
this.setState({ activeLegend: '' });
this.setState({ activeLegend: undefined });
},
};
return legend;
});

const legends = (
<Legends
legends={legendDataItems}
Expand All @@ -273,16 +266,33 @@ export class DonutChartBase extends React.Component<IDonutChartProps, IDonutChar
focusZonePropsInHoverCard={this.props.focusZonePropsForLegendsInHoverCard}
overflowText={this.props.legendsOverflowText}
{...this.props.legendProps}
// eslint-disable-next-line react/jsx-no-bind
onChange={this._onLegendSelectionChange.bind(this)}
/>
);
return legends;
}

private _onLegendSelectionChange(
selectedLegends: string[],
event: React.MouseEvent<HTMLButtonElement>,
currentLegend?: ILegend,
): void {
if (this.props.legendProps && this.props.legendProps?.canSelectMultipleLegends) {
this.setState({ selectedLegends });
} else {
this.setState({ selectedLegends: selectedLegends.slice(-1) });
}
if (this.props.legendProps?.onChange) {
this.props.legendProps.onChange(selectedLegends, event, currentLegend);
}
}

private _focusCallback = (data: IChartDataPoint, id: string, element: SVGPathElement): void => {
this._currentHoverElement = element;
this.setState({
/** Show the callout if highlighted arc is focused and Hide it if unhighlighted arc is focused */
showHover: this.state.selectedLegend === '' || this.state.selectedLegend === data.legend,
showHover: this._noLegendsHighlighted() || this._isLegendHighlighted(data.legend),
value: data.data!.toString(),
legend: data.legend,
color: data.color!,
Expand All @@ -307,7 +317,7 @@ export class DonutChartBase extends React.Component<IDonutChartProps, IDonutChar

this.setState({
/** Show the callout if highlighted arc is hovered and Hide it if unhighlighted arc is hovered */
showHover: this.state.selectedLegend === '' || this.state.selectedLegend === data.legend,
showHover: this._noLegendsHighlighted() || this._isLegendHighlighted(data.legend),
value: data.data!.toString(),
legend: data.legend,
color,
Expand All @@ -332,16 +342,22 @@ export class DonutChartBase extends React.Component<IDonutChartProps, IDonutChar
};

private _valueInsideDonut(valueInsideDonut: string | number | undefined, data: IChartDataPoint[]) {
const highlightedLegend = this._getHighlightedLegend();
if (valueInsideDonut !== undefined && (highlightedLegend !== '' || this.state.showHover)) {
let legendValue = valueInsideDonut;
data!.map((point: IChartDataPoint, index: number) => {
if (point.legend === highlightedLegend || (this.state.showHover && point.legend === this.state.legend)) {
legendValue = point.yAxisCalloutData ? point.yAxisCalloutData : point.data!;
const highlightedLegends = this._getHighlightedLegend();
if (valueInsideDonut !== undefined && (highlightedLegends.length === 1 || this.state.showHover)) {
const pointValue = data.find(point => this._isLegendHighlighted(point.legend));
return pointValue
? pointValue.yAxisCalloutData
? pointValue.yAxisCalloutData
: pointValue.data!
: valueInsideDonut;
} else if (highlightedLegends.length > 0) {
let totalValue = 0;
data.forEach(point => {
if (highlightedLegends.includes(point.legend!)) {
totalValue += point.data!;
}
return;
});
return legendValue;
return totalValue;
} else {
return valueInsideDonut;
}
Expand All @@ -359,12 +375,23 @@ export class DonutChartBase extends React.Component<IDonutChartProps, IDonutChar
* This function returns
* the selected legend if there is one
* or the hovered legend if none of the legends is selected.
* Note: This won't work in case of multiple legends selection.
*/
private _getHighlightedLegend() {
return this.state.selectedLegend || this.state.activeLegend;
return this.state.selectedLegends.length > 0
? this.state.selectedLegends
: this.state.activeLegend
? [this.state.activeLegend]
: [];
}

private _isLegendHighlighted = (legend: string | undefined): boolean => {
return this._getHighlightedLegend().includes(legend!);
};

private _noLegendsHighlighted = (): boolean => {
return this._getHighlightedLegend().length === 0;
};

private _isChartEmpty(): boolean {
return !(
this.props.data &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ describe('Donut chart interactions', () => {
beforeEach(() => {
sharedBeforeEach();
jest.spyOn(global.Math, 'random').mockReturnValue(0.1);
// Mock the implementation of wrapTextInsideDonut as it internally calls a Browser Function like
// getComputedTextLength() which will otherwise lead to a crash if mounted
jest.spyOn(utils, 'wrapTextInsideDonut').mockImplementation(() => '20000');
});
afterEach(() => {
jest.spyOn(global.Math, 'random').mockRestore();
Expand Down Expand Up @@ -70,7 +73,7 @@ describe('Donut chart interactions', () => {

test('Should highlight the corresponding Pie on mouse over on legends', () => {
// Arrange
const { container } = render(<DonutChart data={chartPointsDC} innerRadius={55} hideLegend={false} />);
const { container } = render(<DonutChart data={chartPointsDC} innerRadius={55} hideLabels={false} />);

// Act
const legend = screen.queryByText('first');
Expand Down Expand Up @@ -157,9 +160,6 @@ describe('Donut chart interactions', () => {
});

test('Should change value inside donut with the legend value on mouseOver legend ', () => {
// Mock the implementation of wrapTextInsideDonut as it internally calls a Browser Function like
// getComputedTextLength() which will otherwise lead to a crash if mounted
jest.spyOn(utils, 'wrapTextInsideDonut').mockImplementation(() => '1000');
// Arrange
const { container } = render(
<DonutChart data={chartPointsDC} innerRadius={55} hideLegend={false} valueInsideDonut={1000} />,
Expand All @@ -184,6 +184,38 @@ describe('Donut chart interactions', () => {
// Assert
expect(container).toMatchSnapshot();
});

// add test for legend multi select
test('Should select multiple legends on click', () => {
// Arrange
const { container } = render(
<DonutChart
data={chartPointsDC}
innerRadius={55}
hideLegend={false}
legendProps={{
canSelectMultipleLegends: true,
}}
/>,
);

// Act
const firstLegend = screen.queryByText('first')?.closest('button');
const secondLegend = screen.queryByText('second')?.closest('button');
expect(firstLegend).toBeDefined();
expect(secondLegend).toBeDefined();
fireEvent.click(firstLegend!);
fireEvent.click(secondLegend!);

// Assert
expect(firstLegend).toHaveAttribute('aria-selected', 'true');
expect(secondLegend).toHaveAttribute('aria-selected', 'true');

const getById = queryAllByAttribute.bind(null, 'id');
expect(getById(container, /Pie.*?first/i)[0]).toHaveStyle('opacity: 1.0');
expect(getById(container, /Pie.*?second/i)[0]).toHaveStyle('opacity: 1.0');
expect(getById(container, /Pie.*?third/i)[0]).toHaveStyle('opacity: 0.1');
});
});

describe('Donut Chart - axe-core', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export interface IPieProps {
/**
* Active Arc for chart
*/
activeArc?: string;
activeArc?: string[];

/**
* string for callout id
Expand Down
Loading

0 comments on commit b689f93

Please sign in to comment.