Skip to content

Commit 8a4890f

Browse files
Create secondary y axis (microsoft#27965)
* Publish dist folder to npm feed that got removed due to node 16 upgrade * Add change file * POC to enable secondary y axis * Fixed margins + each scale having different ticks * Added eslint comment for yScaleSecondary * Added option to enable the secondary y scale * Fixed clipping issue with secondary y scale * Added options to the secondary y scale * Refactored boolean conditions * Added option for stacked bar chart lines to use secondary y axis * Fixed issue where the secondary y scale would be used for the bars * Added option for bar chart lines to use secondary y axis * Removed duplicate ref, renamed ref and move YAxisParamsSecondary into conditional * Refactored + fixed issue with VerticalBarChart useSecondaryYScale * Fixed useSecondaryYScale for VerticalBarChart * Updated changelog * Fixed potential bug with yMaxValue * Removed unnecessary import * Refactored useSecondaryYScale to avoid repetition * Changed changelog * Fixed 'possibly undefined' issue * Did changelog properly * Fixed typo when using secondaryYScale * Change changelog type to patch * Set condition for rendering secondary y axis element --------- Co-authored-by: Atishay Jain (atisjai) <atishay.jain@microsoft.com> Co-authored-by: Atishay Jain (atisjai) <98592573+AtishayMsft@users.noreply.github.com>
1 parent f376f28 commit 8a4890f

File tree

7 files changed

+137
-16
lines changed

7 files changed

+137
-16
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": "Added options to have a secondary y-axis for a vertical chart and options for the lines to use the secondary y-axis or not",
4+
"packageName": "@fluentui/react-charting",
5+
"email": "t-miarrieta@microsoft.com",
6+
"dependentChangeType": "patch"
7+
}

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

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export class CartesianChartBase extends React.Component<IModifiedCartesianChartP
6868
private minLegendContainerHeight: number = 32;
6969
private xAxisElement: SVGElement | null;
7070
private yAxisElement: SVGElement | null;
71+
private yAxisElementSecondary: SVGElement | null;
7172
private margins: IMargins;
7273
private idForGraph: string;
7374
private _reqID: number;
@@ -97,8 +98,16 @@ export class CartesianChartBase extends React.Component<IModifiedCartesianChartP
9798
this.margins = {
9899
top: this.props.margins?.top ?? 20,
99100
bottom: this.props.margins?.bottom ?? 35,
100-
right: this._isRtl ? this.props.margins?.left ?? 40 : this.props.margins?.right ?? 20,
101-
left: this._isRtl ? this.props.margins?.right ?? 20 : this.props.margins?.left ?? 40,
101+
right: this._isRtl
102+
? this.props.margins?.left ?? 40
103+
: this.props.margins?.right ?? this.props?.secondaryYScaleOptions
104+
? 40
105+
: 20,
106+
left: this._isRtl
107+
? this.props.margins?.right ?? this.props?.secondaryYScaleOptions
108+
? 40
109+
: 20
110+
: this.props.margins?.left ?? 40,
102111
};
103112
}
104113

@@ -240,7 +249,6 @@ export class CartesianChartBase extends React.Component<IModifiedCartesianChartP
240249
// http://using-d3js.com/04_07_ordinal_scales.html
241250
yAxisPadding: this.props.yAxisPadding || 0,
242251
};
243-
244252
/**
245253
* These scales used for 2 purposes.
246254
* 1. To create x and y axis
@@ -299,6 +307,8 @@ export class CartesianChartBase extends React.Component<IModifiedCartesianChartP
299307
*/
300308
// eslint-disable-next-line @typescript-eslint/no-explicit-any
301309
let yScale: any;
310+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
311+
let yScaleSecondary: any;
302312
const axisData: IAxisData = { yAxisDomainValues: [] };
303313
if (this.props.yAxisType && this.props.yAxisType === YAxisType.StringAxis) {
304314
yScale = createStringYAxis(
@@ -310,6 +320,31 @@ export class CartesianChartBase extends React.Component<IModifiedCartesianChartP
310320
culture,
311321
);
312322
} else {
323+
if (this.props?.secondaryYScaleOptions) {
324+
const YAxisParamsSecondary = {
325+
margins: this.margins,
326+
containerWidth: this.state.containerWidth,
327+
containerHeight: this.state.containerHeight - this.state._removalValueForTextTuncate!,
328+
yAxisElement: this.yAxisElementSecondary,
329+
yAxisTickFormat: this.props.yAxisTickFormat!,
330+
yAxisTickCount: this.props.yAxisTickCount!,
331+
yMinValue: this.props.secondaryYScaleOptions?.yMinValue || 0,
332+
yMaxValue: this.props.secondaryYScaleOptions?.yMaxValue ?? 100,
333+
tickPadding: 10,
334+
maxOfYVal: this.props.secondaryYScaleOptions?.yMaxValue ?? 100,
335+
yMinMaxValues: getMinMaxOfYAxis(points, chartType),
336+
yAxisPadding: this.props.yAxisPadding,
337+
};
338+
339+
yScaleSecondary = createYAxis(
340+
YAxisParamsSecondary,
341+
this._isRtl,
342+
axisData,
343+
chartType,
344+
this.props.barwidth!,
345+
true,
346+
);
347+
}
313348
yScale = createYAxis(YAxisParams, this._isRtl, axisData, chartType, this.props.barwidth!);
314349
}
315350

@@ -350,6 +385,7 @@ export class CartesianChartBase extends React.Component<IModifiedCartesianChartP
350385
...this.state,
351386
xScale,
352387
yScale,
388+
yScaleSecondary,
353389
});
354390

355391
let focusDirection;
@@ -399,6 +435,18 @@ export class CartesianChartBase extends React.Component<IModifiedCartesianChartP
399435
}, 0)`}
400436
className={this._classNames.yAxis}
401437
/>
438+
{this.props.secondaryYScaleOptions && (
439+
<g
440+
ref={(e: SVGElement | null) => {
441+
this.yAxisElementSecondary = e;
442+
}}
443+
id={`yAxisGElementSecondary${this.idForGraph}`}
444+
transform={`translate(${
445+
this._isRtl ? this.margins.left! : svgDimensions.width - this.margins.right!
446+
}, 0)`}
447+
className={this._classNames.yAxis}
448+
/>
449+
)}
402450
{children}
403451
</svg>
404452
</FocusZone>

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,17 @@ export interface ICartesianChartProps {
229229
// eslint-disable-next-line @typescript-eslint/no-explicit-any
230230
yAxisTickFormat?: any;
231231

232+
/**
233+
* Secondary y-scale options
234+
* By default this is not defined, meaning there will be no secondary y-scale.
235+
*/
236+
secondaryYScaleOptions?: {
237+
/** Minimum value (0 by default) */
238+
yMinValue?: number;
239+
/** Maximum value (100 by default) */
240+
yMaxValue?: number;
241+
};
242+
232243
/**
233244
* minimum data value point in y-axis
234245
*/
@@ -376,6 +387,8 @@ export interface IChildProps {
376387
xScale?: any;
377388
// eslint-disable-next-line @typescript-eslint/no-explicit-any
378389
yScale?: any;
390+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
391+
yScaleSecondary?: any;
379392
containerHeight?: number;
380393
containerWidth?: number;
381394
}

packages/react-charting/src/components/VerticalBarChart/VerticalBarChart.base.tsx

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,15 @@ export class VerticalBarChartBase extends React.Component<IVerticalBarChartProps
177177
<>
178178
<g>{this._bars}</g>
179179
{this._isHavingLine && (
180-
<g>{this._createLine(props.xScale!, props.yScale!, props.containerHeight, props.containerWidth)}</g>
180+
<g>
181+
{this._createLine(
182+
props.xScale!,
183+
props.yScale!,
184+
props.containerHeight,
185+
props.containerWidth,
186+
props.yScaleSecondary,
187+
)}
188+
</g>
181189
)}
182190
</>
183191
);
@@ -195,6 +203,8 @@ export class VerticalBarChartBase extends React.Component<IVerticalBarChartProps
195203
yScale: any,
196204
containerHeight: number = 0,
197205
containerWidth: number = 0,
206+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
207+
yScaleSecondary?: any,
198208
): React.ReactNode => {
199209
const isNumericAxis = this._xAxisType === XAxisTypes.NumericAxis;
200210
const { xBarScale } = this._getScales(containerHeight, containerWidth, isNumericAxis);
@@ -207,14 +217,20 @@ export class VerticalBarChartBase extends React.Component<IVerticalBarChartProps
207217
data &&
208218
data.forEach((item: IVerticalBarChartDataPoint, index: number) => {
209219
if (item.lineData && item.lineData.y) {
210-
lineData.push({ x: item.x, y: item.lineData!.y, point: item, index });
220+
lineData.push({
221+
x: item.x,
222+
y: item.lineData!.y,
223+
useSecondaryYScale: item.lineData!.useSecondaryYScale ?? false,
224+
point: item,
225+
index,
226+
});
211227
}
212228
});
213229
const linePath = d3Line()
214230
// eslint-disable-next-line @typescript-eslint/no-explicit-any
215231
.x((d: any) => (!isNumericAxis ? xBarScale(d.x) + 0.5 * xBarScale.bandwidth() : xScale(d.x)))
216232
// eslint-disable-next-line @typescript-eslint/no-explicit-any
217-
.y((d: any) => yScale(d.y));
233+
.y((d: any) => (d.useSecondaryYScale && yScaleSecondary ? yScaleSecondary(d.y) : yScale(d.y)));
218234
const shouldHighlight = this._legendHighlighted(lineLegendText!) || this._noLegendHighlighted() ? true : false;
219235
const lineBorderWidth = this.props.lineOptions?.lineBorderWidth
220236
? Number.parseFloat(this.props.lineOptions!.lineBorderWidth!.toString())
@@ -244,12 +260,21 @@ export class VerticalBarChartBase extends React.Component<IVerticalBarChartProps
244260
);
245261

246262
const dots: React.ReactNode[] = lineData.map(
247-
(item: { x: number | string; y: number; point: IVerticalBarChartDataPoint; index: number }, index: number) => {
263+
(
264+
item: {
265+
x: number | string;
266+
y: number;
267+
useSecondaryYScale: boolean;
268+
point: IVerticalBarChartDataPoint;
269+
index: number;
270+
},
271+
index: number,
272+
) => {
248273
return (
249274
<circle
250275
key={index}
251276
cx={!isNumericAxis ? xBarScale(item.x) + 0.5 * xBarScale.bandwidth() : xScale(item.x)}
252-
cy={yScale(item.y)}
277+
cy={item.useSecondaryYScale && yScaleSecondary ? yScaleSecondary(item.y) : yScale(item.y)}
253278
onMouseOver={this._onBarHover.bind(this, item.point, colorScale(item.y))}
254279
onMouseOut={this._onBarLeave}
255280
r={8}

packages/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.base.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,13 @@ export class VerticalStackedBarChartBase extends React.Component<
201201
<g>{this._bars}</g>
202202
<g>
203203
{_isHavingLines &&
204-
this._createLines(props.xScale!, props.yScale!, props.containerHeight!, props.containerWidth!)}
204+
this._createLines(
205+
props.xScale!,
206+
props.yScale!,
207+
props.containerHeight!,
208+
props.containerWidth!,
209+
props.yScaleSecondary,
210+
)}
205211
</g>
206212
</>
207213
);
@@ -277,6 +283,7 @@ export class VerticalStackedBarChartBase extends React.Component<
277283
yScale: NumericScale,
278284
containerHeight: number,
279285
containerWidth: number,
286+
secondaryYScale?: NumericScale,
280287
): JSX.Element => {
281288
const isNumeric = this._xAxisType === XAxisTypes.NumericAxis;
282289
const { xBarScale } = this._getScales(containerHeight, containerWidth, isNumeric);
@@ -297,12 +304,14 @@ export class VerticalStackedBarChartBase extends React.Component<
297304
? xScale(lineObject[item][i - 1].xItem.xAxisPoint as number)
298305
: // eslint-disable-next-line @typescript-eslint/no-explicit-any
299306
(xBarScale as any)(lineObject[item][i - 1].xItem.xAxisPoint as string);
300-
const y1 = yScale(lineObject[item][i - 1].y);
307+
const useSecondaryYScale =
308+
lineObject[item][i - 1].useSecondaryYScale && lineObject[item][i].useSecondaryYScale && secondaryYScale;
309+
const y1 = useSecondaryYScale ? secondaryYScale!(lineObject[item][i - 1].y) : yScale(lineObject[item][i - 1].y);
301310
const x2 = isNumeric
302311
? xScale(lineObject[item][i].xItem.xAxisPoint as number)
303312
: // eslint-disable-next-line @typescript-eslint/no-explicit-any
304313
(xBarScale as any)(lineObject[item][i].xItem.xAxisPoint as string);
305-
const y2 = yScale(lineObject[item][i].y);
314+
const y2 = useSecondaryYScale ? secondaryYScale!(lineObject[item][i].y) : yScale(lineObject[item][i].y);
306315

307316
if (lineBorderWidth > 0) {
308317
borderForLines.push(
@@ -352,7 +361,9 @@ export class VerticalStackedBarChartBase extends React.Component<
352361
: // eslint-disable-next-line @typescript-eslint/no-explicit-any
353362
(xBarScale as any)(circlePoint.xItem.xAxisPoint as string)
354363
}
355-
cy={yScale(circlePoint.y)}
364+
cy={
365+
circlePoint.useSecondaryYScale && secondaryYScale ? secondaryYScale(circlePoint.y) : yScale(circlePoint.y)
366+
}
356367
onMouseOver={
357368
this.state.selectedLegend === item
358369
? this._lineHover.bind(this, circlePoint)

packages/react-charting/src/types/IDataPoint.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,11 @@ export interface ILineDataInVerticalBarChart {
236236
* onClick action for each datapoint in the chart
237237
*/
238238
onClick?: VoidFunction;
239+
/**
240+
* Whether to use the secondary y scale or not
241+
* False by default.
242+
*/
243+
useSecondaryYScale?: boolean;
239244
}
240245

241246
export interface ILineChartDataPoint {
@@ -536,6 +541,11 @@ export interface ILineDataInVerticalStackedBarChart {
536541
*/
537542
data?: number;
538543
yAxisCalloutData?: string;
544+
/**
545+
* Whether to use the secondary y scale or not
546+
* False by default.
547+
*/
548+
useSecondaryYScale?: boolean;
539549
}
540550

541551
export interface IGVBarChartSeriesPoint {

packages/react-charting/src/utilities/utilities.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -315,12 +315,13 @@ export function createYAxis(
315315
axisData: IAxisData,
316316
chartType: ChartTypes,
317317
barWidth: number,
318+
useSecondaryYScale: boolean = false,
318319
) {
319320
switch (chartType) {
320321
case ChartTypes.HorizontalBarChartWithAxis:
321322
return createYAxisForHorizontalBarChartWithAxis(yAxisParams, isRtl, axisData, barWidth!);
322323
default:
323-
return createYAxisForOtherCharts(yAxisParams, isRtl, axisData);
324+
return createYAxisForOtherCharts(yAxisParams, isRtl, axisData, useSecondaryYScale);
324325
}
325326
}
326327

@@ -357,7 +358,12 @@ export function createYAxisForHorizontalBarChartWithAxis(
357358
return yAxisScale;
358359
}
359360

360-
export function createYAxisForOtherCharts(yAxisParams: IYAxisParams, isRtl: boolean, axisData: IAxisData) {
361+
export function createYAxisForOtherCharts(
362+
yAxisParams: IYAxisParams,
363+
isRtl: boolean,
364+
axisData: IAxisData,
365+
useSecondaryYScale: boolean = false,
366+
) {
361367
const {
362368
yMinMaxValues = { startValue: 0, endValue: 0 },
363369
yAxisElement = null,
@@ -382,13 +388,14 @@ export function createYAxisForOtherCharts(yAxisParams: IYAxisParams, isRtl: bool
382388
const yAxisScale = d3ScaleLinear()
383389
.domain([finalYmin, domainValues[domainValues.length - 1]])
384390
.range([containerHeight - margins.bottom!, margins.top! + (eventAnnotationProps! ? eventLabelHeight! : 0)]);
385-
const axis = isRtl ? d3AxisRight(yAxisScale) : d3AxisLeft(yAxisScale);
391+
const axis =
392+
(!isRtl && useSecondaryYScale) || (isRtl && !useSecondaryYScale) ? d3AxisRight(yAxisScale) : d3AxisLeft(yAxisScale);
386393
const yAxis = axis
387394
.tickPadding(tickPadding)
388395
.tickValues(domainValues)
389396
.tickSizeInner(-(containerWidth - margins.left! - margins.right!));
390-
yAxisTickFormat ? yAxis.tickFormat(yAxisTickFormat) : yAxis.tickFormat(d3Format('.2~s'));
391397

398+
yAxisTickFormat ? yAxis.tickFormat(yAxisTickFormat) : yAxis.tickFormat(d3Format('.2~s'));
392399
yAxisElement ? d3Select(yAxisElement).call(yAxis).selectAll('text').attr('aria-hidden', 'true') : '';
393400
axisData.yAxisDomainValues = domainValues;
394401
return yAxisScale;

0 commit comments

Comments
 (0)