Skip to content

Commit d5a6890

Browse files
authored
feat(react-charting): add support for secondary y-axis in cartesian charts (#34301)
1 parent a7824c7 commit d5a6890

37 files changed

+1417
-305
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "patch",
3+
"comment": "feat: add support for secondary y-axis in cartesian charts",
4+
"packageName": "@fluentui/react-charting",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}

packages/charts/react-charting/etc/react-charting.api.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -439,7 +439,7 @@ export interface IChildProps {
439439
// (undocumented)
440440
xScale?: any;
441441
// (undocumented)
442-
yScale?: any;
442+
yScalePrimary?: any;
443443
// (undocumented)
444444
yScaleSecondary?: any;
445445
}
@@ -679,6 +679,7 @@ export interface IGVBarChartSeriesPoint {
679679
key: string;
680680
legend: string;
681681
onClick?: VoidFunction;
682+
useSecondaryYScale?: boolean;
682683
xAxisCalloutData?: string;
683684
yAxisCalloutData?: string;
684685
}
@@ -1008,6 +1009,7 @@ export interface ILineChartPoints {
10081009
onLegendClick?: (selectedLegend: string | null | string[]) => void;
10091010
onLineClick?: () => void;
10101011
opacity?: number;
1012+
useSecondaryYScale?: boolean;
10111013
}
10121014

10131015
// @public
@@ -1104,7 +1106,7 @@ export interface IModifiedCartesianChartProps extends ICartesianChartProps {
11041106
getDomainNRangeValues: (points: ILineChartPoints[] | IVerticalBarChartDataPoint[] | IVerticalStackedBarDataPoint[] | IHorizontalBarChartWithAxisDataPoint[] | IGroupedVerticalBarChartData[] | IHeatMapChartDataPoint[], margins: IMargins, width: number, chartType: ChartTypes, isRTL: boolean, xAxisType: XAxisTypes, barWidth: number, tickValues: Date[] | number[] | string[] | undefined, shiftX: number) => IDomainNRange;
11051107
getGraphData?: any;
11061108
getmargins?: (margins: IMargins) => void;
1107-
getMinMaxOfYAxis: (points: ILineChartPoints[] | IHorizontalBarChartWithAxisDataPoint[] | IVerticalBarChartDataPoint[] | IDataPoint[], yAxisType: YAxisType | undefined) => {
1109+
getMinMaxOfYAxis: (points: ILineChartPoints[] | IHorizontalBarChartWithAxisDataPoint[] | IVerticalBarChartDataPoint[] | IDataPoint[], yAxisType: YAxisType | undefined, useSecondaryYScale?: boolean) => {
11081110
startValue: number;
11091111
endValue: number;
11101112
};

packages/charts/react-charting/src/components/AreaChart/AreaChart.base.tsx

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import { ILegend, ILegendContainer, Legends } from '../Legends/index';
4949
import { DirectionalHint } from '@fluentui/react/lib/Callout';
5050
import { IChart, IImageExportOptions } from '../../types/index';
5151
import { toImage } from '../../utilities/image-export-utils';
52+
import { ScaleLinear } from 'd3-scale';
5253

5354
const getClassNames = classNamesFunction<IAreaChartStyleProps, IAreaChartStyles>();
5455

@@ -133,6 +134,7 @@ export class AreaChartBase extends React.Component<IAreaChartProps, IAreaChartSt
133134
private _emptyChartId: string;
134135
private _cartesianChartRef: React.RefObject<IChart>;
135136
private _legendsRef: React.RefObject<ILegendContainer>;
137+
private _containsSecondaryYAxis = false;
136138

137139
public constructor(props: IAreaChartProps) {
138140
super(props);
@@ -189,6 +191,8 @@ export class AreaChartBase extends React.Component<IAreaChartProps, IAreaChartSt
189191
if (!this._isChartEmpty()) {
190192
const { lineChartData } = this.props.data;
191193
const points = this._addDefaultColors(lineChartData);
194+
this._containsSecondaryYAxis =
195+
!!this.props.secondaryYScaleOptions && points.some(point => point.useSecondaryYScale);
192196
const { colors, opacity, data, calloutPoints } = this._createSet(points);
193197
this._calloutPoints = calloutPoints;
194198
const isXAxisDateType = getXAxisType(points);
@@ -443,7 +447,7 @@ export class AreaChartBase extends React.Component<IAreaChartProps, IAreaChartSt
443447
const renderPoints: Array<IAreaChartDataSetPoint[]> = [];
444448
let maxOfYVal = 0;
445449

446-
if (this.props.mode === 'tozeroy') {
450+
if (this._shouldFillToZeroY()) {
447451
keys.forEach((key, index) => {
448452
const currentLayer: IAreaChartDataSetPoint[] = [];
449453
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -480,7 +484,12 @@ export class AreaChartBase extends React.Component<IAreaChartProps, IAreaChartSt
480484
: renderPoints?.length > 1);
481485
return {
482486
renderData: renderPoints,
483-
maxOfYVal,
487+
// The maxOfYVal prop is only required for the primary y-axis. When the data includes
488+
// a secondary y-axis, the mode defaults to tozeroy, so maxOfYVal should be calculated using
489+
// only the data points associated with the primary y-axis.
490+
maxOfYVal: this._containsSecondaryYAxis
491+
? findNumericMinMaxOfY(this.props.data.lineChartData!).endValue
492+
: maxOfYVal,
484493
};
485494
};
486495

@@ -613,8 +622,10 @@ export class AreaChartBase extends React.Component<IAreaChartProps, IAreaChartSt
613622
containerHeight: number,
614623
containerWidth: number,
615624
xElement: SVGElement | null,
625+
yAxisElement?: SVGElement | null,
626+
yScaleSecondary?: ScaleLinear<number, number>,
616627
) => {
617-
this._chart = this._drawGraph(containerHeight, xAxis, yAxis, xElement!);
628+
this._chart = this._drawGraph(containerHeight, xAxis, yAxis, yScaleSecondary, xElement!);
618629
};
619630

620631
private _onLegendHover(legend: string): void {
@@ -727,15 +738,22 @@ export class AreaChartBase extends React.Component<IAreaChartProps, IAreaChartSt
727738
return fillColor;
728739
};
729740

730-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
731-
private _drawGraph = (containerHeight: number, xScale: any, yScale: any, xElement: SVGElement): JSX.Element[] => {
741+
private _drawGraph = (
742+
containerHeight: number,
743+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
744+
xScale: any,
745+
yScalePrimary: ScaleLinear<number, number>,
746+
yScaleSecondary: ScaleLinear<number, number> | undefined,
747+
xElement: SVGElement,
748+
): JSX.Element[] => {
732749
const points = this._addDefaultColors(this.props.data.lineChartData);
733750
const { pointOptions, pointLineOptions } = this.props.data;
734751

735752
const graph: JSX.Element[] = [];
736753
let lineColor: string;
737754
// eslint-disable-next-line @typescript-eslint/no-explicit-any
738755
this._data.forEach((singleStackedData: Array<any>, index: number) => {
756+
const yScale = points[index].useSecondaryYScale && yScaleSecondary ? yScaleSecondary : yScalePrimary;
739757
const curveFactory = getCurveFactory(points[index].lineOptions?.curve, d3CurveBasis);
740758
const area = d3Area()
741759
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -751,7 +769,7 @@ export class AreaChartBase extends React.Component<IAreaChartProps, IAreaChartSt
751769
// eslint-disable-next-line @typescript-eslint/no-explicit-any
752770
.y((d: any) => yScale(d.values[1]))
753771
.curve(curveFactory);
754-
const layerOpacity = this.props.mode === 'tozeroy' ? 0.8 : this._opacity[index];
772+
const layerOpacity = this._shouldFillToZeroY() ? 0.8 : this._opacity[index];
755773
graph.push(
756774
<React.Fragment key={`${index}-graph-${this._uniqueIdForGraph}`}>
757775
{this.props.enableGradient && (
@@ -821,6 +839,8 @@ export class AreaChartBase extends React.Component<IAreaChartProps, IAreaChartSt
821839
return;
822840
}
823841

842+
const yScale = points[index].useSecondaryYScale && yScaleSecondary ? yScaleSecondary : yScalePrimary;
843+
824844
if (!this.props.optimizeLargeData || singleStackedData.length === 1) {
825845
// Render circles for all data points
826846
graph.push(
@@ -1064,4 +1084,8 @@ export class AreaChartBase extends React.Component<IAreaChartProps, IAreaChartSt
10641084
const { chartTitle, lineChartData } = this.props.data;
10651085
return (chartTitle ? `${chartTitle}. ` : '') + `Area chart with ${lineChartData?.length || 0} data series. `;
10661086
};
1087+
1088+
private _shouldFillToZeroY() {
1089+
return this.props.mode === 'tozeroy' || this._containsSecondaryYAxis;
1090+
}
10671091
}

packages/charts/react-charting/src/components/CommonComponents/CartesianChart.base.tsx

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -394,18 +394,23 @@ export class CartesianChartBase
394394
* For area/line chart using same scales. For other charts, creating their own scales to draw the graph.
395395
*/
396396
// eslint-disable-next-line @typescript-eslint/no-explicit-any
397-
let yScale: any;
397+
let yScalePrimary: any;
398398
// eslint-disable-next-line @typescript-eslint/no-explicit-any
399399
let yScaleSecondary: any;
400400
const axisData: IAxisData = { yAxisDomainValues: [] };
401401
if (this.props.yAxisType && this.props.yAxisType === YAxisType.StringAxis) {
402-
yScale = this.props.createStringYAxis(
402+
yScalePrimary = this.props.createStringYAxis(
403403
YAxisParams,
404404
this.props.stringDatasetForYAxisDomain!,
405405
this._isRtl,
406406
this.props.barwidth,
407407
);
408408
} else {
409+
// TODO: Since the scale domain values are now computed independently for both the primary and
410+
// secondary y-axes, the yMinValue and yMaxValue props are no longer necessary for accurately
411+
// rendering the secondary y-axis. Therefore, rather than checking the secondaryYScaleOptions
412+
// prop to determine whether to create a secondary y-axis, it's more appropriate to check if any
413+
// data points are assigned to use the secondary y-scale.
409414
if (this.props?.secondaryYScaleOptions) {
410415
const YAxisParamsSecondary = {
411416
margins: this.margins,
@@ -417,12 +422,11 @@ export class CartesianChartBase
417422
yMinValue: this.props.secondaryYScaleOptions?.yMinValue || 0,
418423
yMaxValue: this.props.secondaryYScaleOptions?.yMaxValue ?? 100,
419424
tickPadding: 10,
420-
maxOfYVal: this.props.secondaryYScaleOptions?.yMaxValue ?? 100,
421-
yMinMaxValues: this.props.getMinMaxOfYAxis(points, this.props.yAxisType),
425+
yMinMaxValues: this.props.getMinMaxOfYAxis(points, this.props.yAxisType, true),
422426
yAxisPadding: this.props.yAxisPadding,
423427
};
424428

425-
yScaleSecondary = yScaleSecondary = this.props.createYAxis(
429+
yScaleSecondary = this.props.createYAxis(
426430
YAxisParamsSecondary,
427431
this._isRtl,
428432
axisData,
@@ -432,7 +436,7 @@ export class CartesianChartBase
432436
this.props.roundedTicks!,
433437
);
434438
}
435-
yScale = this.props.createYAxis(
439+
yScalePrimary = this.props.createYAxis(
436440
YAxisParams,
437441
this._isRtl,
438442
axisData,
@@ -449,23 +453,23 @@ export class CartesianChartBase
449453
or showing the whole string,
450454
* */
451455
chartTypesToCheck.includes(this.props.chartType) &&
452-
yScale &&
456+
yScalePrimary &&
453457
createYAxisLabels(
454458
this.yAxisElement,
455-
yScale,
459+
yScalePrimary,
456460
this.props.noOfCharsToTruncate || 4,
457461
this.props.showYAxisLablesTooltip || false,
458462
this._isRtl,
459463
);
460464

461465
this.props.getAxisData && this.props.getAxisData(axisData);
462466
// Callback function for chart, returns axis
463-
this._getData(xScale, yScale);
467+
this._getData(xScale, yScalePrimary, yScaleSecondary);
464468

465469
children = this.props.children({
466470
...this.state,
467471
xScale,
468-
yScale,
472+
yScalePrimary,
469473
yScaleSecondary,
470474
});
471475

@@ -889,15 +893,16 @@ export class CartesianChartBase
889893

890894
// Call back to the chart.
891895
// eslint-disable-next-line @typescript-eslint/no-explicit-any
892-
private _getData = (xScale: any, yScale: any) => {
896+
private _getData = (xScale: any, yScalePrimary: any, yScaleSecondary: any) => {
893897
this.props.getGraphData &&
894898
this.props.getGraphData(
895899
xScale,
896-
yScale,
900+
yScalePrimary,
897901
this.state.containerHeight - this.state._removalValueForTextTuncate!,
898902
this.state.containerWidth,
899903
this.xAxisElement,
900904
this.yAxisElement,
905+
yScaleSecondary,
901906
);
902907
};
903908

packages/charts/react-charting/src/components/CommonComponents/CartesianChart.types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -486,7 +486,7 @@ export interface IChildProps {
486486
// eslint-disable-next-line @typescript-eslint/no-explicit-any
487487
xScale?: any;
488488
// eslint-disable-next-line @typescript-eslint/no-explicit-any
489-
yScale?: any;
489+
yScalePrimary?: any;
490490
// eslint-disable-next-line @typescript-eslint/no-explicit-any
491491
yScaleSecondary?: any;
492492
containerHeight?: number;
@@ -670,6 +670,7 @@ export interface IModifiedCartesianChartProps extends ICartesianChartProps {
670670
getMinMaxOfYAxis: (
671671
points: ILineChartPoints[] | IHorizontalBarChartWithAxisDataPoint[] | IVerticalBarChartDataPoint[] | IDataPoint[],
672672
yAxisType: YAxisType | undefined,
673+
useSecondaryYScale?: boolean,
673674
) => { startValue: number; endValue: number };
674675

675676
/**

0 commit comments

Comments
 (0)