Skip to content
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

feat(react-charting): Add support for line curves #33877

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
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
2 changes: 2 additions & 0 deletions packages/charts/react-charting/etc/react-charting.api.md
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🕵🏾‍♀️ visual regressions to review in the fluentuiv8 Visual Regression Report

react-charting-AreaChart 1 screenshots
Image Name Diff(in Pixels) Image Type
react-charting-AreaChart.Custom Accessibility.default.chromium.png 11 Changed
react-charting-LineChart 1 screenshots
Image Name Diff(in Pixels) Image Type
react-charting-LineChart.Gaps.default.chromium.png 1 Changed

```ts

import { CurveFactory } from 'd3-shape';
import { FocusZoneDirection } from '@fluentui/react-focus';
import { ICalloutContentStyleProps } from '@fluentui/react/lib/Callout';
import { ICalloutContentStyles } from '@fluentui/react/lib/Callout';
@@ -980,6 +981,7 @@ export interface ILineChartGap {

// @public (undocumented)
export interface ILineChartLineOptions extends React_2.SVGProps<SVGPathElement> {
curve?: 'linear' | 'natural' | 'step' | 'stepAfter' | 'stepBefore' | CurveFactory;
lineBorderColor?: string;
lineBorderWidth?: string | number;
strokeDasharray?: string | number;
Original file line number Diff line number Diff line change
@@ -42,6 +42,7 @@ import {
formatDate,
getSecureProps,
areArraysEqual,
getCurveFactory,
} from '../../utilities/index';
import { ILegend, Legends } from '../Legends/index';
import { DirectionalHint } from '@fluentui/react/lib/Callout';
@@ -725,25 +726,26 @@ export class AreaChartBase extends React.Component<IAreaChartProps, IAreaChartSt
private _drawGraph = (containerHeight: number, xScale: any, yScale: any, xElement: SVGElement): JSX.Element[] => {
const points = this._addDefaultColors(this.props.data.lineChartData);
const { pointOptions, pointLineOptions } = this.props.data;
const area = d3Area()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.x((d: any) => xScale(d.xVal))
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.y0((d: any) => yScale(d.values[0]))
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.y1((d: any) => yScale(d.values[1]))
.curve(d3CurveBasis);
const line = d3Line()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.x((d: any) => xScale(d.xVal))
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.y((d: any) => yScale(d.values[1]))
.curve(d3CurveBasis);

const graph: JSX.Element[] = [];
let lineColor: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this._data.forEach((singleStackedData: Array<any>, index: number) => {
const curveFactory = getCurveFactory(points[index].lineOptions?.curve, d3CurveBasis);
const area = d3Area()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.x((d: any) => xScale(d.xVal))
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.y0((d: any) => yScale(d.values[0]))
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.y1((d: any) => yScale(d.values[1]))
.curve(curveFactory);
const line = d3Line()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.x((d: any) => xScale(d.xVal))
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.y((d: any) => yScale(d.values[1]))
.curve(curveFactory);
const layerOpacity = this.props.mode === 'tozeroy' ? 0.8 : this._opacity[index];
graph.push(
<React.Fragment key={`${index}-graph-${this._uniqueIdForGraph}`}>
@@ -819,7 +821,6 @@ export class AreaChartBase extends React.Component<IAreaChartProps, IAreaChartSt
graph.push(
<g
key={`${index}-dots-${this._uniqueIdForGraph}`}
d={area(singleStackedData)!}
clipPath="url(#clip)"
role="region"
aria-label={`${points[index].legend}, series ${index + 1} of ${points.length} with ${
Original file line number Diff line number Diff line change
@@ -141,7 +141,6 @@ exports[`AreaChart - mouse events Should render callout correctly on mouseover 1
<g
aria-label="metaData1, series 1 of 1 with 2 data points."
clipPath="url(#clip)"
d="M40,115.625L-20,20L-20,275L40,275Z"
key="0-dots-areaChart_0"
role="region"
>
@@ -712,7 +711,6 @@ exports[`AreaChart - mouse events Should render customized callout on mouseover
<g
aria-label="metaData1, series 1 of 1 with 2 data points."
clipPath="url(#clip)"
d="M40,115.625L-20,20L-20,275L40,275Z"
key="0-dots-areaChart_0"
role="region"
>
@@ -1180,7 +1178,6 @@ exports[`AreaChart - mouse events Should render customized callout per stack on
<g
aria-label="metaData1, series 1 of 1 with 2 data points."
clipPath="url(#clip)"
d="M40,115.625L-20,20L-20,275L40,275Z"
key="0-dots-areaChart_0"
role="region"
>

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -18,6 +18,7 @@ import {
IGroupedVerticalBarChartData,
IVerticalBarChartDataPoint,
ISankeyChartData,
ILineChartLineOptions,
} from '../../types/IDataPoint';
import { ISankeyChartProps } from '../SankeyChart/index';
import { IVerticalStackedBarChartProps } from '../VerticalStackedBarChart/index';
@@ -31,8 +32,9 @@ import { IGroupedVerticalBarChartProps } from '../GroupedVerticalBarChart/index'
import { IVerticalBarChartProps } from '../VerticalBarChart/index';
import { findNumericMinMaxOfY } from '../../utilities/utilities';
import { Layout, PlotlySchema, PieData, PlotData, SankeyData } from './PlotlySchema';
import type { Datum, TypedArray } from './PlotlySchema';
import type { Datum, ScatterLine, TypedArray } from './PlotlySchema';
import { timeParse } from 'd3-time-format';
import { curveCardinal as d3CurveCardinal } from 'd3-shape';

interface ISecondaryYAxisValues {
secondaryYAxistitle?: string;
@@ -313,11 +315,9 @@ export const transformPlotlyJsonToVSBCProps = (
const color = getColor(legend, colorMap, isDarkTheme);
mapXToDataPoints[x].lineData!.push({
legend,
...(series.line?.dash && dashOptions[series.line.dash]
? { lineOptions: { ...dashOptions[series.line.dash] } }
: {}),
y: yVal,
color,
lineOptions: getLineOptions(series.line),
});
}

@@ -508,14 +508,12 @@ export const transformPlotlyJsonToScatterChartProps = (

return {
legend,
...(series.line?.dash && dashOptions[series.line.dash]
? { lineOptions: { ...dashOptions[series.line.dash] } }
: {}),
data: xValues.map((x, i: number) => ({
x: isString ? (isXDate ? new Date(x as string) : isXNumber ? parseFloat(x as string) : x) : x,
y: series.y[i],
})),
color: lineColor,
lineOptions: getLineOptions(series.line),
} as ILineChartPoints;
});

@@ -870,3 +868,35 @@ function crawlIntoTrace(container: any, i: number, astrPartial: any) {
}
}
}

function getLineOptions(line: Partial<ScatterLine> | undefined): ILineChartLineOptions | undefined {
if (!line) {
return;
}

let lineOptions: ILineChartLineOptions = {};
if (line.dash) {
lineOptions = { ...lineOptions, ...dashOptions[line.dash] };
}

switch (line.shape) {
case 'linear':
lineOptions.curve = 'linear';
break;
case 'spline':
const smoothing = typeof line.smoothing === 'number' ? line.smoothing : 1;
lineOptions.curve = d3CurveCardinal.tension(1 - smoothing / 1.3);
break;
case 'hv':
lineOptions.curve = 'stepAfter';
break;
case 'vh':
lineOptions.curve = 'stepBefore';
break;
case 'hvh':
lineOptions.curve = 'step';
break;
}

return lineOptions;
}
Original file line number Diff line number Diff line change
@@ -605,7 +605,6 @@ exports[`DeclarativeChart Should render areachart in DeclarativeChart 1`] = `
<g
aria-label="a, series 1 of 4 with 10 data points."
clip-path="url(#clip)"
d="M40,-41.807C60,-42.215,80,-42.623,100,-42.623C120,-42.623,140,-37.707,160,-37.707C180,-37.707,200,-37.825,220,-37.825C240,-37.825,260,-37.785,280,-37.706C300,-37.627,320,-36.113,340,-36.113C360,-36.113,380,-39.814,400,-39.814C420,-39.814,440,-37.736,460,-37.736C480,-37.736,500,-40.029,520,-40.029C540,-40.029,560,-39.373,580,-38.716L580,-43C560,-43,540,-43,520,-43C500,-43,480,-43,460,-43C440,-43,420,-43,400,-43C380,-43,360,-43,340,-43C320,-43,300,-43,280,-43C260,-43,240,-43,220,-43C200,-43,180,-43,160,-43C140,-43,120,-43,100,-43C80,-43,60,-43,40,-43Z"
role="region"
>
<circle
@@ -742,7 +741,6 @@ exports[`DeclarativeChart Should render areachart in DeclarativeChart 1`] = `
<g
aria-label="b, series 2 of 4 with 10 data points."
clip-path="url(#clip)"
d="M40,-34.162C60,-36.193,80,-38.224,100,-38.224C120,-38.224,140,-25.944,160,-25.944C180,-25.944,200,-29.605,220,-29.605C240,-29.605,260,-25.737,280,-25.737C300,-25.737,320,-27.608,340,-28.997C360,-30.386,380,-34.072,400,-34.072C420,-34.072,440,-30.602,460,-30.602C480,-30.602,500,-36.255,520,-36.255C540,-36.255,560,-34.327,580,-32.4L580,-38.716C560,-39.373,540,-40.029,520,-40.029C500,-40.029,480,-37.736,460,-37.736C440,-37.736,420,-39.814,400,-39.814C380,-39.814,360,-36.113,340,-36.113C320,-36.113,300,-37.627,280,-37.706C260,-37.785,240,-37.825,220,-37.825C200,-37.825,180,-37.707,160,-37.707C140,-37.707,120,-42.623,100,-42.623C80,-42.623,60,-42.215,40,-41.807Z"
role="region"
>
<circle
@@ -879,7 +877,6 @@ exports[`DeclarativeChart Should render areachart in DeclarativeChart 1`] = `
<g
aria-label="c, series 3 of 4 with 10 data points."
clip-path="url(#clip)"
d="M40,-23.602C60,-27.066,80,-30.529,100,-30.529C120,-30.529,140,-8.116,160,-8.116C180,-8.116,200,-20.408,220,-20.408C240,-20.408,260,-13.635,280,-13.635C300,-13.635,320,-17.787,340,-19.547C360,-21.306,380,-24.194,400,-24.194C420,-24.194,440,-22.73,460,-22.73C480,-22.73,500,-26.407,520,-26.407C540,-26.407,560,-26.171,580,-25.934L580,-32.4C560,-34.327,540,-36.255,520,-36.255C500,-36.255,480,-30.602,460,-30.602C440,-30.602,420,-34.072,400,-34.072C380,-34.072,360,-30.386,340,-28.997C320,-27.608,300,-25.737,280,-25.737C260,-25.737,240,-29.605,220,-29.605C200,-29.605,180,-25.944,160,-25.944C140,-25.944,120,-38.224,100,-38.224C80,-38.224,60,-36.193,40,-34.162Z"
role="region"
>
<circle
@@ -1016,7 +1013,6 @@ exports[`DeclarativeChart Should render areachart in DeclarativeChart 1`] = `
<g
aria-label="d, series 4 of 4 with 10 data points."
clip-path="url(#clip)"
d="M40,-10.212C60,-14.613,80,-19.014,100,-19.014C120,-19.014,140,16.551,160,16.551C180,16.551,200,-8.81,220,-8.81C240,-8.81,260,5.027,280,5.027C300,5.027,320,-0.51,340,-3.457C360,-6.403,380,-11.941,400,-12.65C420,-13.359,440,-13.714,460,-13.714C480,-13.714,500,-11.06,520,-11.06C540,-11.06,560,-12.802,580,-14.544L580,-25.934C560,-26.171,540,-26.407,520,-26.407C500,-26.407,480,-22.73,460,-22.73C440,-22.73,420,-24.194,400,-24.194C380,-24.194,360,-21.306,340,-19.547C320,-17.787,300,-13.635,280,-13.635C260,-13.635,240,-20.408,220,-20.408C200,-20.408,180,-8.116,160,-8.116C140,-8.116,120,-30.529,100,-30.529C80,-30.529,60,-27.066,40,-23.602Z"
role="region"
>
<circle
Original file line number Diff line number Diff line change
@@ -3176,6 +3176,7 @@ Object {
},
],
"legend": "a",
"lineOptions": Object {},
},
Object {
"color": "#9bd9db",
@@ -3222,6 +3223,7 @@ Object {
},
],
"legend": "b",
"lineOptions": Object {},
},
Object {
"color": "#b29ad4",
@@ -3268,6 +3270,7 @@ Object {
},
],
"legend": "c",
"lineOptions": Object {},
},
Object {
"color": "#a4cc6c",
@@ -3314,6 +3317,7 @@ Object {
},
],
"legend": "d",
"lineOptions": Object {},
},
],
},
@@ -3737,6 +3741,7 @@ Object {
},
],
"legend": "Trace 0",
"lineOptions": Object {},
},
Object {
"color": "#83bdeb",
@@ -4143,6 +4148,7 @@ Object {
},
],
"legend": "Trace 1",
"lineOptions": Object {},
},
Object {
"color": "#df8e64",
@@ -4549,6 +4555,7 @@ Object {
},
],
"legend": "Trace 2",
"lineOptions": Object {},
},
],
},
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@ import { Axis as D3Axis } from 'd3-axis';
import { select as d3Select, pointer } from 'd3-selection';
import { bisector } from 'd3-array';
import { ILegend, Legends } from '../Legends/index';
import { line as d3Line, curveLinear as d3curveLinear } from 'd3-shape';
import { line as d3Line } from 'd3-shape';
import {
classNamesFunction,
getId,
@@ -49,6 +49,7 @@ import {
createStringYAxis,
formatDate,
areArraysEqual,
getCurveFactory,
} from '../../utilities/index';
import { IChart } from '../../types/index';

@@ -693,15 +694,16 @@ export class LineChartBase extends React.Component<ILineChartProps, ILineChartSt

let gapIndex = 0;
const gaps = this._points[i].gaps?.sort((a, b) => a.startIndex - b.startIndex) ?? [];
const lineCurve = this._points[i].lineOptions?.curve;

// Use path rendering technique for larger datasets to optimize performance.
if (this.props.optimizeLargeData && this._points[i].data.length > 1) {
if ((this.props.optimizeLargeData || lineCurve) && this._points[i].data.length > 1) {
const line = d3Line()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.x((d: any) => this._xAxisScale(d[0]))
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.y((d: any) => this._yAxisScale(d[1]))
.curve(d3curveLinear);
.curve(getCurveFactory(lineCurve));

const lineId = `${this._lineId}_${i}`;
const borderId = `${this._borderId}_${i}`;
6 changes: 6 additions & 0 deletions packages/charts/react-charting/src/types/IDataPoint.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as React from 'react';
import { SankeyLink, SankeyNode } from 'd3-sankey';
import { LegendShape } from '../components/Legends/Legends.types';
import { CurveFactory } from 'd3-shape';

export interface IBasestate {
_width?: number;
@@ -397,6 +398,11 @@ export interface ILineChartLineOptions extends React.SVGProps<SVGPathElement> {
* Color of border around the line. Default white.
*/
lineBorderColor?: string;

/**
* @default 'linear'
*/
curve?: 'linear' | 'natural' | 'step' | 'stepAfter' | 'stepBefore' | CurveFactory;
}

/**
31 changes: 31 additions & 0 deletions packages/charts/react-charting/src/utilities/utilities.ts
Original file line number Diff line number Diff line change
@@ -41,8 +41,17 @@ import {
IVerticalStackedBarDataPoint,
IVerticalBarChartDataPoint,
IHorizontalBarChartWithAxisDataPoint,
ILineChartLineOptions,
} from '../index';
import { formatPrefix as d3FormatPrefix } from 'd3-format';
import {
CurveFactory,
curveLinear as d3CurveLinear,
curveNatural as d3CurveNatural,
curveStep as d3CurveStep,
curveStepAfter as d3CurveStepAfter,
curveStepBefore as d3CurveStepBefore,
} from 'd3-shape';

export type NumericAxis = D3Axis<number | { valueOf(): number }>;
export type StringAxis = D3Axis<string>;
@@ -1532,3 +1541,25 @@ export function resolveCSSVariables(chartContainer: HTMLElement, styleRules: str
return containerStyles.getPropertyValue(group1);
});
}

export function getCurveFactory(
curve: ILineChartLineOptions['curve'],
defaultFactory: CurveFactory = d3CurveLinear,
): CurveFactory {
if (typeof curve === 'function') {
return curve;
}

switch (curve) {
case 'natural':
return d3CurveNatural;
case 'step':
return d3CurveStep;
case 'stepAfter':
return d3CurveStepAfter;
case 'stepBefore':
return d3CurveStepBefore;
default:
return defaultFactory;
}
}
Loading
Oops, something went wrong.