Skip to content

Commit

Permalink
feat(react-charting): strongly type plotly schema and bug fixes (#33621)
Browse files Browse the repository at this point in the history
Co-authored-by: Anush Gupta <[email protected]>
Co-authored-by: srmukher <[email protected]>
Co-authored-by: krkshitij <[email protected]>
Co-authored-by: Atishay Jain (from Dev Box) <[email protected]>
  • Loading branch information
5 people authored Jan 20, 2025
1 parent cc1756e commit b929a86
Show file tree
Hide file tree
Showing 14 changed files with 2,740 additions and 652 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "Adding fallback and fixes for test app crashes for 12 schema data ",
"packageName": "@fluentui/react-charting",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "Use strongly typed interfaces for plotly schema",
"packageName": "@fluentui/react-charting",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "fix: resolve bugs in declarative chart",
"packageName": "@fluentui/react-charting",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "StronglyType plotly schema bug fix",
"packageName": "@fluentui/react-charting",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { DeclarativeChart } from '@fluentui/react-charting';

console.log(DeclarativeChart);

export default {
name: 'DeclarativeChart',
};
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
/* eslint-disable @typescript-eslint/naming-convention */
/* eslint-disable @typescript-eslint/no-explicit-any */
import * as React from 'react';
import { useTheme } from '@fluentui/react';
import { IRefObject } from '@fluentui/react/lib/Utilities';
import { DonutChart } from '../DonutChart/index';
import { VerticalStackedBarChart } from '../VerticalStackedBarChart/index';
import { PlotData, PlotlySchema } from './PlotlySchema';
//import type { Data, Layout } from './PlotlySchema';
import {
isArrayOrTypedArray,
isDateArray,
Expand All @@ -21,10 +22,11 @@ import {
transformPlotlyJsonToGaugeProps,
transformPlotlyJsonToGVBCProps,
transformPlotlyJsonToVBCProps,
isLineData,
} from './PlotlySchemaAdapter';
import { LineChart } from '../LineChart/index';
import { LineChart, ILineChartProps } from '../LineChart/index';
import { HorizontalBarChartWithAxis } from '../HorizontalBarChartWithAxis/index';
import { AreaChart } from '../AreaChart/index';
import { AreaChart, IAreaChartProps } from '../AreaChart/index';
import { HeatMapChart } from '../HeatMapChart/index';
import { SankeyChart } from '../SankeyChart/SankeyChart';
import { GaugeChart } from '../GaugeChart/index';
Expand All @@ -41,6 +43,7 @@ export interface Schema {
/**
* Plotly schema represented as JSON object
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
plotlySchema: any;
}

Expand Down Expand Up @@ -87,12 +90,8 @@ export const DeclarativeChart: React.FunctionComponent<DeclarativeChartProps> =
DeclarativeChartProps
>((props, forwardedRef) => {
const { plotlySchema } = sanitizeJson(props.chartSchema);
const { data, layout } = plotlySchema;
const plotlyInput = plotlySchema as PlotlySchema;
let { selectedLegends } = plotlySchema;
const xValues = data[0].x;
const isXDate = isDateArray(xValues);
const isXNumber = isNumberArray(xValues);
const isXMonth = isMonthArray(xValues);
const colorMap = useColorMapping();
const theme = useTheme();
const isDarkTheme = theme?.isInverted ?? false;
Expand All @@ -106,7 +105,7 @@ export const DeclarativeChart: React.FunctionComponent<DeclarativeChartProps> =
const onActiveLegendsChange = (keys: string[]) => {
setActiveLegends(keys);
if (props.onSchemaChange) {
props.onSchemaChange({ plotlySchema: { data, layout, selectedLegends: keys } });
props.onSchemaChange({ plotlySchema: { plotlyInput, selectedLegends: keys } });
}
};

Expand All @@ -118,16 +117,69 @@ export const DeclarativeChart: React.FunctionComponent<DeclarativeChartProps> =
setActiveLegends(selectedLegends ?? []);
}, [props.chartSchema]);

const legendProps = {
canSelectMultipleLegends: false,
const multiSelectLegendProps = {
canSelectMultipleLegends: true,
onChange: onActiveLegendsChange,
selectedLegend: activeLegends.slice(0, 1)[0],
selectedLegends: activeLegends,
};

const commonProps = {
legendProps: multiSelectLegendProps,
componentRef: chartRef,
calloutProps: { layerProps: { eventBubblingEnabled: true } },
};

const checkAndRenderChart = (
renderChartJsx: (chartProps: ILineChartProps | IAreaChartProps) => JSX.Element,
isAreaChart: boolean = false,
) => {
let fallbackVSBC = false;
const xValues = (plotlyInput.data[0] as PlotData).x;
const isXDate = isDateArray(xValues);
const isXNumber = isNumberArray(xValues);
const isXMonth = isMonthArray(xValues);
if (isXDate || isXNumber) {
const chartProps: ILineChartProps | IAreaChartProps = {
...transformPlotlyJsonToScatterChartProps(
{ data: plotlyInput.data, layout: plotlyInput.layout },
isAreaChart,
colorMap,
isDarkTheme,
),
...commonProps,
};
return renderChartJsx(chartProps);
} else if (isXMonth) {
const updatedData = plotlyInput.data.map((dataPoint: PlotData) => ({
...dataPoint,
x: updateXValues(dataPoint.x),
}));
const chartProps: ILineChartProps | IAreaChartProps = {
...transformPlotlyJsonToScatterChartProps(
{ data: updatedData, layout: plotlyInput.layout },
isAreaChart,
colorMap,
isDarkTheme,
),
...commonProps,
};
return renderChartJsx(chartProps);
}
// Unsupported schema, render as VerticalStackedBarChart
fallbackVSBC = true;
return (
<VerticalStackedBarChart
{...transformPlotlyJsonToVSBCProps(plotlySchema, colorMap, isDarkTheme, fallbackVSBC)}
{...commonProps}
/>
);
};

const exportAsImage = React.useCallback(
(opts?: IImageExportOptions) => {
return toImage(chartRef.current?.chartContainer, {
background: theme.semanticColors.bodyBackground,
scale: 3,
...opts,
});
},
Expand All @@ -142,146 +194,78 @@ export const DeclarativeChart: React.FunctionComponent<DeclarativeChartProps> =
[exportAsImage],
);

const multiSelectLegendProps = {
...legendProps,
canSelectMultipleLegends: true,
selectedLegends: activeLegends,
};

switch (data[0].type) {
switch (plotlyInput.data[0].type) {
case 'pie':
return (
<DonutChart
{...transformPlotlyJsonToDonutProps(plotlySchema, colorMap, isDarkTheme)}
legendProps={multiSelectLegendProps}
componentRef={chartRef}
// Bubble event to prevent right click to open menu on the callout
calloutProps={{ layerProps: { eventBubblingEnabled: true } }}
/>
);
return <DonutChart {...transformPlotlyJsonToDonutProps(plotlySchema, colorMap, isDarkTheme)} {...commonProps} />;
case 'bar':
const orientation = data[0].orientation;
const orientation = plotlyInput.data[0].orientation;
if (orientation === 'h') {
return (
<HorizontalBarChartWithAxis
{...transformPlotlyJsonToHorizontalBarWithAxisProps(plotlySchema, colorMap, isDarkTheme)}
legendProps={multiSelectLegendProps}
componentRef={chartRef}
calloutProps={{ layerProps: { eventBubblingEnabled: true } }}
{...commonProps}
/>
);
} else {
if (['group', 'overlay'].includes(plotlySchema?.layout?.barmode)) {
const containsLines = plotlyInput.data.some(
series => series.type === 'scatter' || isLineData(series as Partial<PlotData>),
);
if (['group', 'overlay'].includes(plotlySchema?.layout?.barmode) && !containsLines) {
return (
<GroupedVerticalBarChart
{...transformPlotlyJsonToGVBCProps(plotlySchema, colorMap, isDarkTheme)}
legendProps={multiSelectLegendProps}
componentRef={chartRef}
calloutProps={{ layerProps: { eventBubblingEnabled: true } }}
{...commonProps}
/>
);
}
return (
<VerticalStackedBarChart
{...transformPlotlyJsonToVSBCProps(plotlySchema, colorMap, isDarkTheme)}
legendProps={multiSelectLegendProps}
componentRef={chartRef}
calloutProps={{ layerProps: { eventBubblingEnabled: true } }}
{...commonProps}
/>
);
}
case 'scatter':
const isAreaChart = data.some((series: any) => series.fill === 'tonexty' || series.fill === 'tozeroy');
const renderChart = (chartProps: any) => {
if (plotlyInput.data[0].mode === 'markers') {
throw new Error(`Unsupported chart - type :${plotlyInput.data[0]?.type}, mode: ${plotlyInput.data[0]?.mode}`);
}
const isAreaChart = plotlyInput.data.some(
(series: PlotData) => series.fill === 'tonexty' || series.fill === 'tozeroy',
);
const renderChartJsx = (chartProps: ILineChartProps | IAreaChartProps) => {
if (isAreaChart) {
return (
<AreaChart
{...chartProps}
legendProps={{ ...legendProps, canSelectMultipleLegends: true, selectedLegends: activeLegends }}
/>
);
return <AreaChart {...chartProps} />;
}
return (
<LineChart
{...{
...chartProps,
legendProps: {
onChange: onActiveLegendsChange,
canSelectMultipleLegends: true,
selectedLegends: activeLegends,
},
}}
/>
);
return <LineChart {...chartProps} />;
};
if (isXDate || isXNumber) {
const chartProps = {
...transformPlotlyJsonToScatterChartProps({ data, layout }, isAreaChart, colorMap, isDarkTheme),
legendProps,
componentRef: chartRef,
calloutProps: { layerProps: { eventBubblingEnabled: true } },
};
return renderChart(chartProps);
} else if (isXMonth) {
const updatedData = data.map((dataPoint: any) => ({
...dataPoint,
x: updateXValues(dataPoint.x),
}));
const chartProps = {
...transformPlotlyJsonToScatterChartProps({ data: updatedData, layout }, isAreaChart, colorMap, isDarkTheme),
legendProps,
componentRef: chartRef,
calloutProps: { layerProps: { eventBubblingEnabled: true } },
};
return renderChart(chartProps);
}
return (
<VerticalStackedBarChart
{...transformPlotlyJsonToVSBCProps(plotlySchema, colorMap, isDarkTheme)}
legendProps={multiSelectLegendProps}
componentRef={chartRef}
calloutProps={{ layerProps: { eventBubblingEnabled: true } }}
/>
);
return checkAndRenderChart(renderChartJsx, isAreaChart);
case 'heatmap':
return (
<HeatMapChart
{...transformPlotlyJsonToHeatmapProps(plotlySchema)}
componentRef={chartRef}
calloutProps={{ layerProps: { eventBubblingEnabled: true } }}
/>
);
return <HeatMapChart {...transformPlotlyJsonToHeatmapProps(plotlySchema)} {...commonProps} legendProps={{}} />;
case 'sankey':
return (
<SankeyChart
{...transformPlotlyJsonToSankeyProps(plotlySchema, colorMap, isDarkTheme)}
componentRef={chartRef}
calloutProps={{ layerProps: { eventBubblingEnabled: true } }}
/>
<SankeyChart {...transformPlotlyJsonToSankeyProps(plotlySchema, colorMap, isDarkTheme)} {...commonProps} />
);
case 'indicator':
if (data?.[0]?.mode?.includes('gauge')) {
if (plotlyInput.data?.[0]?.mode?.includes('gauge')) {
return (
<GaugeChart
{...transformPlotlyJsonToGaugeProps(plotlySchema, colorMap, isDarkTheme)}
legendProps={multiSelectLegendProps}
componentRef={chartRef}
calloutProps={{ layerProps: { eventBubblingEnabled: true } }}
/>
<GaugeChart {...transformPlotlyJsonToGaugeProps(plotlySchema, colorMap, isDarkTheme)} {...commonProps} />
);
}
return <div>Unsupported Schema</div>;
throw new Error(`Unsupported chart - type: ${plotlyInput.data[0]?.type}, mode: ${plotlyInput.data[0]?.mode}`);
case 'histogram':
return (
<VerticalBarChart
{...transformPlotlyJsonToVBCProps(plotlySchema, colorMap, isDarkTheme)}
legendProps={multiSelectLegendProps}
componentRef={chartRef}
calloutProps={{ layerProps: { eventBubblingEnabled: true } }}
/>
<VerticalBarChart {...transformPlotlyJsonToVBCProps(plotlySchema, colorMap, isDarkTheme)} {...commonProps} />
);
default:
throw new Error('Unsupported chart schema');
const xValues = (plotlyInput.data[0] as PlotData).x;
const yValues = (plotlyInput.data[0] as PlotData).y;
if (xValues && yValues && xValues.length > 0 && yValues.length > 0) {
const renderLineChartJsx = (chartProps: ILineChartProps) => {
return <LineChart {...chartProps} />;
};
return checkAndRenderChart(renderLineChartJsx);
}
throw new Error(`Unsupported chart type :${plotlyInput.data[0]?.type}`);
}
});
DeclarativeChart.displayName = 'DeclarativeChart';
Loading

0 comments on commit b929a86

Please sign in to comment.