Skip to content

Commit

Permalink
feat(react-charting): add plotly adapter for histogram (#33399)
Browse files Browse the repository at this point in the history
  • Loading branch information
krkshitij authored Dec 4, 2024
1 parent eca1dd1 commit fe4dad5
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "feat: add plotly adapter for histogram",
"packageName": "@fluentui/react-charting",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
transformPlotlyJsonToSankeyProps,
transformPlotlyJsonToGaugeProps,
transformPlotlyJsonToGVBCProps,
transformPlotlyJsonToVBCProps,
} from './PlotlySchemaAdapter';
import { LineChart } from '../LineChart/index';
import { HorizontalBarChartWithAxis } from '../HorizontalBarChartWithAxis/index';
Expand All @@ -22,6 +23,7 @@ import { HeatMapChart } from '../HeatMapChart/index';
import { SankeyChart } from '../SankeyChart/SankeyChart';
import { GaugeChart } from '../GaugeChart/index';
import { GroupedVerticalBarChart } from '../GroupedVerticalBarChart/index';
import { VerticalBarChart } from '../VerticalBarChart/index';

export interface Schema {
/**
Expand Down Expand Up @@ -108,6 +110,8 @@ export const DeclarativeChart: React.FunctionComponent<DeclarativeChartProps> =
return <GaugeChart {...transformPlotlyJsonToGaugeProps(plotlySchema, colorMap)} />;
}
return <div>Unsupported Schema</div>;
case 'histogram':
return <VerticalBarChart {...transformPlotlyJsonToVBCProps(plotlySchema, colorMap)} />;
default:
return <div>Unsupported Schema</div>;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
/* eslint-disable no-console */
/* eslint-disable @typescript-eslint/no-explicit-any */
import * as React from 'react';
import { bin as d3Bin, extent as d3Extent, sum as d3Sum, min as d3Min, max as d3Max, merge as d3Merge } from 'd3-array';
import { scaleLinear as d3ScaleLinear } from 'd3-scale';
import { format as d3Format, precisionFixed as d3PrecisionFixed } from 'd3-format';
import { IDonutChartProps } from '../DonutChart/index';
import {
IChartDataPoint,
Expand All @@ -14,6 +17,7 @@ import {
IHeatMapChartData,
IHeatMapChartDataPoint,
IGroupedVerticalBarChartData,
IVerticalBarChartDataPoint,
} from '../../types/IDataPoint';
import { ISankeyChartProps } from '../SankeyChart/index';
import { IVerticalStackedBarChartProps } from '../VerticalStackedBarChart/index';
Expand All @@ -24,6 +28,7 @@ import { IHeatMapChartProps } from '../HeatMapChart/index';
import { getNextColor } from '../../utilities/colors';
import { IGaugeChartProps, IGaugeChartSegment } from '../GaugeChart/index';
import { IGroupedVerticalBarChartProps } from '../GroupedVerticalBarChart/index';
import { IVerticalBarChartProps } from '../VerticalBarChart/index';

const isDate = (value: any): boolean => !isNaN(Date.parse(value));
const isNumber = (value: any): boolean => !isNaN(parseFloat(value)) && isFinite(value);
Expand Down Expand Up @@ -56,8 +61,6 @@ export const transformPlotlyJsonToDonutProps = (
};
});

// TODO: innerRadius as a fraction needs to be supported internally. The pixel value depends on
// chart dimensions, arc label dimensions and the legend container height, all of which are subject to change.
const width: number = layout?.width || 440;
const height: number = layout?.height || 220;
const hideLabels = firstData.textinfo ? !['value', 'percent'].includes(firstData.textinfo) : false;
Expand Down Expand Up @@ -87,8 +90,6 @@ export const transformPlotlyJsonToDonutProps = (
hideLabels,
showLabelsInPercent: firstData.textinfo ? firstData.textinfo === 'percent' : true,
styles,
// TODO: Render custom hover card based on textinfo
// onRenderCalloutPerDataPoint: undefined,
};
};

Expand Down Expand Up @@ -136,7 +137,6 @@ export const transformPlotlyJsonToVSBCProps = (
};
};

// TODO: Add support for continuous x-axis in grouped vertical bar chart
export const transformPlotlyJsonToGVBCProps = (
jsonObj: any,
colorMap: React.MutableRefObject<Map<string, string>>,
Expand Down Expand Up @@ -172,6 +172,91 @@ export const transformPlotlyJsonToGVBCProps = (
};
};

export const transformPlotlyJsonToVBCProps = (
jsonObj: any,
colorMap: React.MutableRefObject<Map<string, string>>,
): IVerticalBarChartProps => {
const { data, layout } = jsonObj;
const vbcData: IVerticalBarChartDataPoint[] = [];

data.forEach((series: any, index: number) => {
if (!series.x) {
return;
}

const scale = d3ScaleLinear()
.domain(d3Extent<number>(series.x) as [number, number])
.nice();
let [xMin, xMax] = scale.domain();

xMin = typeof series.xbins?.start === 'number' ? series.xbins.start : xMin;
xMax = typeof series.xbins?.end === 'number' ? series.xbins.end : xMax;

const bin = d3Bin().domain([xMin, xMax]);

if (typeof series.xbins?.size === 'number') {
const thresholds: number[] = [];
let th = xMin;
const precision = d3PrecisionFixed(series.xbins.size);
const format = d3Format(`.${precision}f`);

while (th < xMax + series.xbins.size) {
thresholds.push(parseFloat(format(th)));
th += series.xbins.size;
}

xMin = thresholds[0];
xMax = thresholds[thresholds.length - 1];
bin.domain([xMin, xMax]).thresholds(thresholds);
}

const buckets = bin(series.x);
// If the start or end of xbins is specified, then the number of datapoints may become less than x.length
const totalDataPoints = d3Merge(buckets).length;

buckets.forEach(bucket => {
const legend = series.name || `Series ${index + 1}`;
const color = getColor(legend, colorMap);
let y = bucket.length;

if (series.histnorm === 'percent') {
y = (bucket.length / totalDataPoints) * 100;
} else if (series.histnorm === 'probability') {
y = bucket.length / totalDataPoints;
} else if (series.histnorm === 'density') {
y = bucket.x0 === bucket.x1 ? 0 : bucket.length / (bucket.x1! - bucket.x0!);
} else if (series.histnorm === 'probability density') {
y = bucket.x0 === bucket.x1 ? 0 : bucket.length / (totalDataPoints * (bucket.x1! - bucket.x0!));
} else if (series.histfunc === 'sum') {
y = d3Sum(bucket);
} else if (series.histfunc === 'avg') {
y = bucket.length === 0 ? 0 : d3Sum(bucket) / bucket.length;
} else if (series.histfunc === 'min') {
y = d3Min(bucket)!;
} else if (series.histfunc === 'max') {
y = d3Max(bucket)!;
}

vbcData.push({
x: (bucket.x1! + bucket.x0!) / 2,
y,
legend,
color,
xAxisCalloutData: `[${bucket.x0} - ${bucket.x1})`,
});
});
});

return {
data: vbcData,
chartTitle: layout?.title,
// width: layout?.width,
// height: layout?.height,
hideLegend: true,
barWidth: 24,
};
};

export const transformPlotlyJsonToScatterChartProps = (
jsonObj: any,
isAreaChart: boolean,
Expand Down Expand Up @@ -255,8 +340,6 @@ export const transformPlotlyJsonToHorizontalBarWithAxisProps = (
};
};

// FIXME: Order of string axis ticks does not match the order in plotly json
// TODO: Add support for custom hover card
export const transformPlotlyJsonToHeatmapProps = (jsonObj: any): IHeatMapChartProps => {
const { data, layout } = jsonObj;
const firstData = data[0];
Expand Down

0 comments on commit fe4dad5

Please sign in to comment.