Skip to content

feat(react-charts): Add support for secondary y axis in cartesian charts #34665

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Jun 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "make change in createNumericYAxis function in utilities ",
"packageName": "@fluentui/react-charting",
"email": "anushgupta@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "port v8 changes to v9",
"packageName": "@fluentui/react-charts",
"email": "anushgupta@microsoft.com",
"dependentChangeType": "patch"
}
4 changes: 2 additions & 2 deletions packages/charts/react-charting/src/utilities/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -665,10 +665,10 @@ export function createNumericYAxis(
} = yAxisParams;

// maxOfYVal coming from only area chart and Grouped vertical bar chart(Calculation done at base file)
const tempVal = maxOfYVal || yMinMaxValues.endValue;
const tempVal = maxOfYVal || yMinMaxValues.endValue || 0;
const finalYmax = tempVal > yMaxValue ? tempVal : yMaxValue!;
const finalYmin = supportNegativeData
? Math.min(yMinMaxValues.startValue, yMinValue || 0)
? Math.min(yMinMaxValues.startValue || 0, yMinValue || 0)
: yMinMaxValues.startValue < yMinValue
? 0
: yMinValue!;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ export interface ChildProps {
// (undocumented)
xScale?: any;
// (undocumented)
yScale?: any;
yScalePrimary?: any;
// (undocumented)
yScaleSecondary?: any;
}
Expand Down Expand Up @@ -632,6 +632,7 @@ export interface GVBarChartSeriesPoint {
key: string;
legend: string;
onClick?: VoidFunction;
useSecondaryYScale?: boolean;
xAxisCalloutData?: string;
yAxisCalloutData?: string;
}
Expand Down Expand Up @@ -949,6 +950,7 @@ export interface LineChartPoints {
onLegendClick?: (selectedLegend: string | null | string[]) => void;
onLineClick?: () => void;
opacity?: number;
useSecondaryYScale?: boolean;
}

// @public
Expand Down Expand Up @@ -1034,6 +1036,10 @@ export interface ModifiedCartesianChartProps extends CartesianChartProps {
getDomainMargins?: (containerWidth: number) => Margins;
getGraphData?: any;
getmargins?: (margins: Margins) => void;
getMinMaxOfYAxis?: (points: DataPoint[], yAxisType: YAxisType | undefined, useSecondaryYScale?: boolean) => {
startValue: number;
endValue: number;
};
isCalloutForStack?: boolean;
legendBars: JSX.Element | null;
maxOfYVal?: number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@ import {
areArraysEqual,
getCurveFactory,
find,
findNumericMinMaxOfY,
} from '../../utilities/index';
import { useId } from '@fluentui/react-utilities';
import { Legend, Legends } from '../Legends/index';
import { ScaleLinear } from 'd3-scale';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const bisect = bisector((d: any) => d.x).left;
Expand Down Expand Up @@ -74,6 +76,7 @@ export const AreaChart: React.FunctionComponent<AreaChartProps> = React.forwardR
const _enableComputationOptimization: boolean = true;
const _firstRenderOptimization: boolean = true;
const _emptyChartId: string = useId('_AreaChart_empty');
let _containsSecondaryYAxis = false;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let _calloutPoints: any;
let _createSet: (data: LineChartPoints[]) => {
Expand Down Expand Up @@ -250,7 +253,7 @@ export const AreaChart: React.FunctionComponent<AreaChartProps> = React.forwardR
const renderPoints: Array<AreaChartDataSetPoint[]> = [];
let maxOfYVal = 0;

if (props.mode === 'tozeroy') {
if (_shouldFillToZeroY()) {
keys.forEach((key, index) => {
const currentLayer: AreaChartDataSetPoint[] = [];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down Expand Up @@ -287,7 +290,10 @@ export const AreaChart: React.FunctionComponent<AreaChartProps> = React.forwardR
: renderPoints?.length > 1);
return {
renderData: renderPoints,
maxOfYVal,
// The maxOfYVal prop is only required for the primary y-axis. When the data includes
// a secondary y-axis, the mode defaults to tozeroy, so maxOfYVal should be calculated using
// only the data points associated with the primary y-axis.
maxOfYVal: _containsSecondaryYAxis ? findNumericMinMaxOfY(props.data.lineChartData!).endValue : maxOfYVal,
};
}

Expand Down Expand Up @@ -421,8 +427,10 @@ export const AreaChart: React.FunctionComponent<AreaChartProps> = React.forwardR
containerHeight: number,
containerWidth: number,
xElement: SVGElement | null,
yAxisElement?: SVGElement | null,
yScaleSecondary?: ScaleLinear<number, number>,
) {
_chart = _drawGraph(containerHeight, xAxis, yAxis, xElement!);
_chart = _drawGraph(containerHeight, xAxis, yAxis, yScaleSecondary, xElement!);
}

function _onLegendHover(legend: string): void {
Expand Down Expand Up @@ -528,15 +536,22 @@ export const AreaChart: React.FunctionComponent<AreaChartProps> = React.forwardR
return fillColor;
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function _drawGraph(containerHeight: number, xScale: any, yScale: any, xElement: SVGElement): JSX.Element[] {
function _drawGraph(
containerHeight: number,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
xScale: any,
yScalePrimary: ScaleLinear<number, number>,
yScaleSecondary: ScaleLinear<number, number> | undefined,
xElement: SVGElement,
): JSX.Element[] {
const points = _addDefaultColors(props.data.lineChartData);
const { pointOptions, pointLineOptions } = props.data;

const graph: JSX.Element[] = [];
let lineColor: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
_data.forEach((singleStackedData: Array<any>, index: number) => {
const yScale = points[index].useSecondaryYScale && yScaleSecondary ? yScaleSecondary : yScalePrimary;
const curveFactory = getCurveFactory(points[index].lineOptions?.curve, d3CurveBasis);
const area = d3Area()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand All @@ -552,7 +567,7 @@ export const AreaChart: React.FunctionComponent<AreaChartProps> = React.forwardR
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.y((d: any) => yScale(d.values[1]))
.curve(curveFactory);
const layerOpacity = props.mode === 'tozeroy' ? 0.8 : _opacity[index];
const layerOpacity = _shouldFillToZeroY() ? 0.8 : _opacity[index];
graph.push(
<React.Fragment key={`${index}-graph-${_uniqueIdForGraph}`}>
{props.enableGradient && (
Expand Down Expand Up @@ -621,6 +636,7 @@ export const AreaChart: React.FunctionComponent<AreaChartProps> = React.forwardR
if (points.length === index) {
return;
}
const yScale = points[index].useSecondaryYScale && yScaleSecondary ? yScaleSecondary : yScalePrimary;

if (!props.optimizeLargeData || singleStackedData.length === 1) {
// Render circles for all data points
Expand Down Expand Up @@ -848,9 +864,14 @@ export const AreaChart: React.FunctionComponent<AreaChartProps> = React.forwardR
return (chartTitle ? `${chartTitle}. ` : '') + `Area chart with ${lineChartData?.length || 0} data series. `;
}

function _shouldFillToZeroY() {
return props.mode === 'tozeroy' || _containsSecondaryYAxis;
}

if (!_isChartEmpty()) {
const { lineChartData } = props.data;
const points = _addDefaultColors(lineChartData);
_containsSecondaryYAxis = !!props.secondaryYScaleOptions && points.some(point => point.useSecondaryYScale);
_createSet = _createDataSet;
const { colors, opacity, data, calloutPoints } = _createSet(points);
_calloutPoints = calloutPoints;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -326,12 +326,12 @@ export const CartesianChart: React.FunctionComponent<ModifiedCartesianChartProps
* For area/line chart using same scales. For other charts, creating their own scales to draw the graph.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let yScale: any;
let yScalePrimary: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let yScaleSecondary: any;
const axisData: IAxisData = { yAxisDomainValues: [] };
if (props.yAxisType && props.yAxisType === YAxisType.StringAxis) {
yScale = createStringYAxis(
yScalePrimary = createStringYAxis(
YAxisParams,
props.stringDatasetForYAxisDomain!,
_useRtl,
Expand All @@ -340,6 +340,11 @@ export const CartesianChart: React.FunctionComponent<ModifiedCartesianChartProps
culture,
);
} else {
// TODO: Since the scale domain values are now computed independently for both the primary and
// secondary y-axes, the yMinValue and yMaxValue props are no longer necessary for accurately
// rendering the secondary y-axis. Therefore, rather than checking the secondaryYScaleOptions
// prop to determine whether to create a secondary y-axis, it's more appropriate to check if any
// data points are assigned to use the secondary y-scale.
if (props?.secondaryYScaleOptions) {
const YAxisParamsSecondary = {
margins: margins,
Expand All @@ -351,8 +356,9 @@ export const CartesianChart: React.FunctionComponent<ModifiedCartesianChartProps
yMinValue: props.secondaryYScaleOptions?.yMinValue || 0,
yMaxValue: props.secondaryYScaleOptions?.yMaxValue ?? 100,
tickPadding: 10,
maxOfYVal: props.secondaryYScaleOptions?.yMaxValue ?? 100,
yMinMaxValues: getMinMaxOfYAxis(points, chartType),
yMinMaxValues: props.getMinMaxOfYAxis
? props.getMinMaxOfYAxis(points, props.yAxisType, true)
: getMinMaxOfYAxis(points, props.chartType, props.yAxisType, true),
yAxisPadding: props.yAxisPadding,
};

Expand All @@ -367,7 +373,7 @@ export const CartesianChart: React.FunctionComponent<ModifiedCartesianChartProps
props.roundedTicks!,
);
}
yScale = createYAxis(
yScalePrimary = createYAxis(
YAxisParams,
_useRtl,
axisData,
Expand All @@ -385,10 +391,10 @@ export const CartesianChart: React.FunctionComponent<ModifiedCartesianChartProps
or showing the whole string,
* */
chartTypesToCheck.includes(props.chartType) &&
yScale &&
yScalePrimary &&
createYAxisLabels(
yAxisElement.current!,
yScale,
yScalePrimary,
props.noOfCharsToTruncate || 4,
props.showYAxisLablesTooltip || false,
startFromX,
Expand All @@ -397,25 +403,26 @@ export const CartesianChart: React.FunctionComponent<ModifiedCartesianChartProps

// Call back to the chart.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const _getData = (xScale: any, yScale: any) => {
const _getData = (xScale: any, yScalePrimary: any, yScaleSecondary: any) => {
props.getGraphData &&
props.getGraphData(
xScale,
yScale,
yScalePrimary,
containerHeight - removalValueForTextTuncate!,
containerWidth,
xAxisElement.current,
yAxisElement.current,
yScaleSecondary,
);
};

props.getAxisData && props.getAxisData(axisData);
// Callback function for chart, returns axis
_getData(xScale, yScale);
_getData(xScale, yScalePrimary, yScaleSecondary);

children = props.children({
xScale,
yScale,
yScalePrimary,
yScaleSecondary,
containerHeight,
containerWidth,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';
import { LegendsProps } from '../Legends/index';
import { AccessibilityProps, Chart, Margins } from '../../types/index';
import { AccessibilityProps, Chart, Margins, DataPoint } from '../../types/index';
import { ChartTypes, XAxisTypes, YAxisType } from '../../utilities/index';
import { TimeLocaleDefinition } from 'd3-time-format';
import { ChartPopoverProps } from './ChartPopover.types';
Expand Down Expand Up @@ -424,7 +424,7 @@ export interface ChildProps {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
xScale?: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
yScale?: any;
yScalePrimary?: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
yScaleSecondary?: any;
containerHeight?: number;
Expand Down Expand Up @@ -576,4 +576,13 @@ export interface ModifiedCartesianChartProps extends CartesianChartProps {
* Used to control the first render cycle Performance optimization code.
*/
enableFirstRenderOptimization?: boolean;

/**
* Get the min and max values of the y-axis
*/
getMinMaxOfYAxis?: (
points: DataPoint[],
yAxisType: YAxisType | undefined,
useSecondaryYScale?: boolean,
) => { startValue: number; endValue: number };
}
Loading
Loading