From 798f4010bee02d36c797dd5d8e0de6bf4d0ed209 Mon Sep 17 00:00:00 2001 From: krkshitij <110246001+krkshitij@users.noreply.github.com> Date: Thu, 20 Feb 2025 19:22:04 +0000 Subject: [PATCH] add support for line curves --- .../react-charting/etc/react-charting.api.md | 2 + .../components/AreaChart/AreaChart.base.tsx | 31 ++++++------- .../__snapshots__/AreaChart.test.tsx.snap | 3 -- .../__snapshots__/AreaChartRTL.test.tsx.snap | 21 --------- .../DeclarativeChart/PlotlySchemaAdapter.ts | 44 ++++++++++++++++--- .../DeclarativeChartRTL.test.tsx.snap | 4 -- .../PlotlySchemaAdapterUT.test.tsx.snap | 7 +++ .../components/LineChart/LineChart.base.tsx | 8 ++-- .../react-charting/src/types/IDataPoint.ts | 6 +++ .../react-charting/src/utilities/utilities.ts | 31 +++++++++++++ 10 files changed, 104 insertions(+), 53 deletions(-) diff --git a/packages/charts/react-charting/etc/react-charting.api.md b/packages/charts/react-charting/etc/react-charting.api.md index b40734e15b8632..ff8183ae2de264 100644 --- a/packages/charts/react-charting/etc/react-charting.api.md +++ b/packages/charts/react-charting/etc/react-charting.api.md @@ -4,6 +4,7 @@ ```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 { + curve?: 'linear' | 'natural' | 'step' | 'stepAfter' | 'stepBefore' | CurveFactory; lineBorderColor?: string; lineBorderWidth?: string | number; strokeDasharray?: string | number; diff --git a/packages/charts/react-charting/src/components/AreaChart/AreaChart.base.tsx b/packages/charts/react-charting/src/components/AreaChart/AreaChart.base.tsx index d4411e51c3d674..9949fdfeb50111 100644 --- a/packages/charts/react-charting/src/components/AreaChart/AreaChart.base.tsx +++ b/packages/charts/react-charting/src/components/AreaChart/AreaChart.base.tsx @@ -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 { 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, 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( @@ -819,7 +821,6 @@ export class AreaChartBase extends React.Component @@ -712,7 +711,6 @@ exports[`AreaChart - mouse events Should render customized callout on mouseover @@ -1180,7 +1178,6 @@ exports[`AreaChart - mouse events Should render customized callout per stack on diff --git a/packages/charts/react-charting/src/components/AreaChart/__snapshots__/AreaChartRTL.test.tsx.snap b/packages/charts/react-charting/src/components/AreaChart/__snapshots__/AreaChartRTL.test.tsx.snap index d151a2c1ec4583..4a27e3d3b5d09a 100644 --- a/packages/charts/react-charting/src/components/AreaChart/__snapshots__/AreaChartRTL.test.tsx.snap +++ b/packages/charts/react-charting/src/components/AreaChart/__snapshots__/AreaChartRTL.test.tsx.snap @@ -356,7 +356,6 @@ exports[`Area chart rendering Should render the Area Chart with negative y value ({ 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 | 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; +} diff --git a/packages/charts/react-charting/src/components/DeclarativeChart/__snapshots__/DeclarativeChartRTL.test.tsx.snap b/packages/charts/react-charting/src/components/DeclarativeChart/__snapshots__/DeclarativeChartRTL.test.tsx.snap index 692f2a5817067e..19b3c8e6c1d1cd 100644 --- a/packages/charts/react-charting/src/components/DeclarativeChart/__snapshots__/DeclarativeChartRTL.test.tsx.snap +++ b/packages/charts/react-charting/src/components/DeclarativeChart/__snapshots__/DeclarativeChartRTL.test.tsx.snap @@ -605,7 +605,6 @@ exports[`DeclarativeChart Should render areachart in DeclarativeChart 1`] = ` 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}`; diff --git a/packages/charts/react-charting/src/types/IDataPoint.ts b/packages/charts/react-charting/src/types/IDataPoint.ts index 5ca0f1393677e1..62444d6b6c7264 100644 --- a/packages/charts/react-charting/src/types/IDataPoint.ts +++ b/packages/charts/react-charting/src/types/IDataPoint.ts @@ -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 { * Color of border around the line. Default white. */ lineBorderColor?: string; + + /** + * @default 'linear' + */ + curve?: 'linear' | 'natural' | 'step' | 'stepAfter' | 'stepBefore' | CurveFactory; } /** diff --git a/packages/charts/react-charting/src/utilities/utilities.ts b/packages/charts/react-charting/src/utilities/utilities.ts index 10d42324914d15..9959dd753fb139 100644 --- a/packages/charts/react-charting/src/utilities/utilities.ts +++ b/packages/charts/react-charting/src/utilities/utilities.ts @@ -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; export type StringAxis = D3Axis; @@ -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; + } +}