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
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
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
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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}`}>
Expand Down Expand Up @@ -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 ${
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
>
Expand Down Expand Up @@ -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"
>
Expand Down Expand Up @@ -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"
>
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
IGroupedVerticalBarChartData,
IVerticalBarChartDataPoint,
ISankeyChartData,
ILineChartLineOptions,
} from '../../types/IDataPoint';
import { ISankeyChartProps } from '../SankeyChart/index';
import { IVerticalStackedBarChartProps } from '../VerticalStackedBarChart/index';
Expand All @@ -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;
Expand Down Expand Up @@ -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),
});
}

Expand Down Expand Up @@ -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;
});

Expand Down Expand Up @@ -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
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3176,6 +3176,7 @@ Object {
},
],
"legend": "a",
"lineOptions": Object {},
},
Object {
"color": "#9bd9db",
Expand Down Expand Up @@ -3222,6 +3223,7 @@ Object {
},
],
"legend": "b",
"lineOptions": Object {},
},
Object {
"color": "#b29ad4",
Expand Down Expand Up @@ -3268,6 +3270,7 @@ Object {
},
],
"legend": "c",
"lineOptions": Object {},
},
Object {
"color": "#a4cc6c",
Expand Down Expand Up @@ -3314,6 +3317,7 @@ Object {
},
],
"legend": "d",
"lineOptions": Object {},
},
],
},
Expand Down Expand Up @@ -3737,6 +3741,7 @@ Object {
},
],
"legend": "Trace 0",
"lineOptions": Object {},
},
Object {
"color": "#83bdeb",
Expand Down Expand Up @@ -4143,6 +4148,7 @@ Object {
},
],
"legend": "Trace 1",
"lineOptions": Object {},
},
Object {
"color": "#df8e64",
Expand Down Expand Up @@ -4549,6 +4555,7 @@ Object {
},
],
"legend": "Trace 2",
"lineOptions": Object {},
},
],
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -49,6 +49,7 @@ import {
createStringYAxis,
formatDate,
areArraysEqual,
getCurveFactory,
} from '../../utilities/index';
import { IChart } from '../../types/index';

Expand Down Expand Up @@ -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}`;
Expand Down
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;
Expand Down Expand Up @@ -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;
}

/**
Expand Down
31 changes: 31 additions & 0 deletions packages/charts/react-charting/src/utilities/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
Expand Down Expand Up @@ -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