Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "feat: add support for log scale in scatter charts",
"packageName": "@fluentui/chart-utilities",
"email": "kumarkshitij@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "feat: add support for log scale in scatter charts",
"packageName": "@fluentui/react-charting",
"email": "kumarkshitij@microsoft.com",
"dependentChangeType": "patch"
}
6 changes: 6 additions & 0 deletions packages/charts/chart-utilities/etc/chart-utilities.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,12 @@ export interface GaugeLine {
width: number;
}

// @public (undocumented)
export const getAxisIds: (data: Partial<PlotData>) => {
x: number;
y: number;
};

// @public
export function getMultiLevelDateTimeFormatOptions(startLevel?: number, endLevel?: number): Intl.DateTimeFormatOptions;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -772,8 +772,17 @@ describe('mapFluentChart UTs', () => {
layout: { xaxis: { type: 'log' } },
};
const result = mapFluentChart(input);
expect(result.isValid).toBe(true);
});

test('unsupported log axis type', () => {
const input = {
data: [{ type: 'bar', x: [1, 2, 3], y: [4, 5, 6] }],
layout: { xaxis: { type: 'log' } },
};
const result = mapFluentChart(input);
expect(result.isValid).toBe(false);
expect(result.errorMessage).toBe('Log axis type is not supported');
expect(result.errorMessage).toContain('log axis type not supported');
});

test('composite chart with multiple types', () => {
Expand Down
73 changes: 51 additions & 22 deletions packages/charts/chart-utilities/src/PlotlySchemaConverter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ const isScatterMarkers = (mode: string): boolean => {
return ['markers', 'text+markers', 'markers+text', 'text'].includes(mode);
};

const validateScatterData = (data: Partial<PlotData>) => {
const validateScatterData = (data: Partial<PlotData>, layout: Partial<Layout> | undefined) => {
const mode = data.mode ?? '';
const xAxisType = data && data.x && data.x.length > 0 ? typeof data?.x?.[0] : 'undefined';
const yAxisType = data && data.y && data.y.length > 0 ? typeof data?.y?.[0] : 'undefined';
Expand Down Expand Up @@ -277,16 +277,20 @@ const validateScatterData = (data: Partial<PlotData>) => {
} else {
throw new Error(`${UNSUPPORTED_MSG_PREFIX} ${data.type}, mode: ${mode}, Unsupported mode`);
}
};

const invalidateLogAxisType = (layout: Partial<Layout> | undefined): boolean => {
const isLogAxisType =
layout?.xaxis?.type === 'log' ||
layout?.yaxis?.type === 'log' ||
layout?.yaxis2?.type === 'log' ||
layout?.xaxis2?.type === 'log';
if (isScatterAreaChart(data)) {
invalidateLogAxisType(data, layout);
}
};

return isLogAxisType;
const invalidateLogAxisType = (data: Partial<PlotData>, layout: Partial<Layout> | undefined) => {
const axisIds = getAxisIds(data) as Record<string, number>;
Object.keys(axisIds).forEach(axLetter => {
const axisKey = (`${axLetter}axis` + (axisIds[axLetter] > 1 ? axisIds[axLetter] : '')) as keyof Layout;
if (layout?.[axisKey]?.type === 'log') {
throw new Error(`${UNSUPPORTED_MSG_PREFIX} ${data.type}, log axis type not supported.`);
}
});
};

/**
Expand Down Expand Up @@ -344,16 +348,20 @@ function findSankeyCycles(input: Partial<SankeyData>): boolean {
return false; // No cycles found
}

const DATA_VALIDATORS_MAP: Record<string, ((data: Data) => void)[]> = {
const DATA_VALIDATORS_MAP: Record<string, ((data: Data, layout: Partial<Layout> | undefined) => void)[]> = {
indicator: [
data => {
if (!(data as Partial<PlotData>).mode?.includes('gauge')) {
throw new Error(`${UNSUPPORTED_MSG_PREFIX} ${data.type}, mode: ${(data as Partial<PlotData>).mode}`);
}
},
],
histogram: [data => validateSeriesData(data as Partial<PlotData>, false)],
histogram: [
(data, layout) => invalidateLogAxisType(data as Partial<PlotData>, layout),
data => validateSeriesData(data as Partial<PlotData>, false),
],
bar: [
(data, layout) => invalidateLogAxisType(data as Partial<PlotData>, layout),
data => {
validateBarData(data as Partial<PlotData>);
},
Expand All @@ -365,8 +373,8 @@ const DATA_VALIDATORS_MAP: Record<string, ((data: Data) => void)[]> = {
}
},
],
scatter: [data => validateScatterData(data as Partial<PlotData>)],
scattergl: [data => validateScatterData(data as Partial<PlotData>)],
scatter: [(data, layout) => validateScatterData(data as Partial<PlotData>, layout)],
scattergl: [(data, layout) => validateScatterData(data as Partial<PlotData>, layout)],
scatterpolar: [
data => {
if (!isNumberArray((data as Partial<PlotData>).theta) && !isStringArray((data as Partial<PlotData>).theta)) {
Expand All @@ -378,10 +386,12 @@ const DATA_VALIDATORS_MAP: Record<string, ((data: Data) => void)[]> = {
},
],
funnel: [data => validateSeriesData(data as Partial<PlotData>, false)],
histogram2d: [(data, layout) => invalidateLogAxisType(data as Partial<PlotData>, layout)],
heatmap: [(data, layout) => invalidateLogAxisType(data as Partial<PlotData>, layout)],
};

const DEFAULT_CHART_TYPE = '';
const getValidTraces = (dataArr: Data[]) => {
const getValidTraces = (dataArr: Data[], layout: Partial<Layout> | undefined) => {
const errorMessages: string[] = [];
const validTraces = dataArr
.map((data, index): [number, string] => {
Expand All @@ -391,7 +401,7 @@ const getValidTraces = (dataArr: Data[]) => {
const validators = DATA_VALIDATORS_MAP[type];
for (const validator of validators) {
try {
validator(data);
validator(data, layout);
} catch (error) {
errorMessages.push(`data[${index}] - type: ${data.type}, ${error}`);
return [-1, DEFAULT_CHART_TYPE];
Expand Down Expand Up @@ -426,11 +436,7 @@ export const mapFluentChart = (input: any): OutputChartType => {
return { isValid: false, errorMessage: `Failed to decode plotly schema: ${error}` };
}

if (invalidateLogAxisType(validSchema.layout)) {
return { isValid: false, errorMessage: 'Log axis type is not supported' };
}

const validTraces = getValidTraces(validSchema.data);
const validTraces = getValidTraces(validSchema.data, validSchema.layout);
let mappedTraces = validTraces.map(trace => {
const traceIndex = trace[0];
const traceData = validSchema.data[traceIndex];
Expand Down Expand Up @@ -474,8 +480,7 @@ export const mapFluentChart = (input: any): OutputChartType => {
case 'scatter':
case 'scattergl':
const scatterData = traceData as Partial<PlotData>;
const isAreaChart =
scatterData.fill === 'tonexty' || scatterData.fill === 'tozeroy' || !!scatterData.stackgroup;
const isAreaChart = isScatterAreaChart(scatterData);
const isScatterChart = isScatterMarkers(scatterData.mode ?? '');
if (isScatterChart) {
return { isValid: true, traceIndex, type: 'scatter' };
Expand Down Expand Up @@ -563,3 +568,27 @@ export const mapFluentChart = (input: any): OutputChartType => {
const canMapToGantt = (data: Partial<PlotData>) => {
return isDateArray(data.base) || isNumberArray(data.base);
};

export const getAxisIds = (data: Partial<PlotData>) => {
let xAxisId = 1;
if (typeof data.xaxis === 'string' && /^x\d+$/.test(data.xaxis)) {
xAxisId = parseInt(data.xaxis.slice(1), 10);
}

let yAxisId = 1;
if (typeof data.yaxis === 'string' && /^y\d+$/.test(data.yaxis)) {
yAxisId = parseInt(data.yaxis.slice(1), 10);
}

return {
x: xAxisId,
y: yAxisId,
};
};

const isScatterAreaChart = (data: Partial<PlotData>) => {
return (
(data.type === 'scatter' || data.type === 'scattergl') &&
(data.fill === 'tonexty' || data.fill === 'tozeroy' || !!data.stackgroup)
);
};
1 change: 1 addition & 0 deletions packages/charts/chart-utilities/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ export {
isInvalidValue,
isStringArray,
isMonthArray,
getAxisIds,
} from './PlotlySchemaConverter';

export { decodeBase64Fields } from './DecodeBase64Data';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { ScatterChart } from '@fluentui/react-charting';

console.log(ScatterChart);

export default {
name: 'ScatterChart',
};
10 changes: 8 additions & 2 deletions packages/charts/react-charting/etc/react-charting.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ export const AreaChart: React_2.FunctionComponent<IAreaChartProps>;
// @public
export type AxisCategoryOrder = 'default' | 'data' | string[] | 'category ascending' | 'category descending' | 'total ascending' | 'total descending' | 'min ascending' | 'min descending' | 'max ascending' | 'max descending' | 'sum ascending' | 'sum descending' | 'mean ascending' | 'mean descending' | 'median ascending' | 'median descending';

// @public
export type AxisScaleType = 'default' | 'log';

// @public
export const CartesianChart: React_2.FunctionComponent<IModifiedCartesianChartProps>;

Expand Down Expand Up @@ -325,6 +328,7 @@ export interface ICartesianChartProps {
yMinValue?: number;
yMaxValue?: number;
};
secondaryYScaleType?: AxisScaleType;
showXAxisLablesTooltip?: boolean;
strokeWidth?: number;
styles?: IStyleFunctionOrObject<ICartesianChartStyleProps, ICartesianChartStyles>;
Expand All @@ -345,13 +349,15 @@ export interface ICartesianChartProps {
xAxistickSize?: number;
xAxisTitle?: string;
xMaxValue?: number;
xScaleType?: AxisScaleType;
yAxisAnnotation?: string;
yAxisCategoryOrder?: AxisCategoryOrder;
yAxisTickCount?: number;
yAxisTickFormat?: any;
yAxisTitle?: string;
yMaxValue?: number;
yMinValue?: number;
yScaleType?: AxisScaleType;
}

// @public
Expand Down Expand Up @@ -1246,7 +1252,7 @@ export interface IModifiedCartesianChartProps extends ICartesianChartProps {
createStringYAxis: (yAxisParams: IYAxisParams, dataPoints: string[], isRtl: boolean, barWidth: number | undefined, chartType?: ChartTypes) => ScaleBand<string>;
// Warning: (ae-forgotten-export) The symbol "IYAxisParams" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "IAxisData" needs to be exported by the entry point index.d.ts
createYAxis: (yAxisParams: IYAxisParams, isRtl: boolean, axisData: IAxisData, isIntegralDataset: boolean, useSecondaryYScale?: boolean, supportNegativeData?: boolean, roundedTicks?: boolean) => ScaleLinear<number, number, never>;
createYAxis: (yAxisParams: IYAxisParams, isRtl: boolean, axisData: IAxisData, isIntegralDataset: boolean, useSecondaryYScale?: boolean, supportNegativeData?: boolean, roundedTicks?: boolean, scaleType?: AxisScaleType) => ScaleLinear<number, number, never>;
culture?: string;
customizedCallout?: any;
datasetForXAxisDomain?: string[];
Expand All @@ -1259,7 +1265,7 @@ export interface IModifiedCartesianChartProps extends ICartesianChartProps {
getDomainNRangeValues: (points: ILineChartPoints[] | IVerticalBarChartDataPoint[] | IVerticalStackedBarDataPoint[] | IHorizontalBarChartWithAxisDataPoint[] | IGroupedVerticalBarChartData[] | IHeatMapChartDataPoint[] | IGanttChartDataPoint[], margins: IMargins, width: number, chartType: ChartTypes, isRTL: boolean, xAxisType: XAxisTypes, barWidth: number, tickValues: Date[] | number[] | string[] | undefined, shiftX: number) => IDomainNRange;
getGraphData?: any;
getmargins?: (margins: IMargins) => void;
getMinMaxOfYAxis: (points: ILineChartPoints[] | IHorizontalBarChartWithAxisDataPoint[] | IVerticalBarChartDataPoint[] | IDataPoint[] | IScatterChartDataPoint[] | IGanttChartDataPoint[], yAxisType: YAxisType | undefined, useSecondaryYScale?: boolean) => {
getMinMaxOfYAxis: (points: ILineChartPoints[] | IHorizontalBarChartWithAxisDataPoint[] | IVerticalBarChartDataPoint[] | IDataPoint[] | IScatterChartPoints[] | IGanttChartDataPoint[], yAxisType: YAxisType | undefined, useSecondaryYScale?: boolean) => {
startValue: number;
endValue: number;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ import {
findNumericMinMaxOfY,
createNumericYAxis,
IDomainNRange,
domainRangeOfNumericForAreaChart,
domainRangeOfDateForAreaLineVerticalBarChart,
domainRangeOfNumericForAreaLineScatterCharts,
domainRangeOfDateForAreaLineScatterVerticalBarCharts,
createStringYAxis,
getSecureProps,
areArraysEqual,
Expand Down Expand Up @@ -322,9 +322,9 @@ export class AreaChartBase extends React.Component<IAreaChartProps, IAreaChartSt
) => {
let domainNRangeValue: IDomainNRange;
if (xAxisType === XAxisTypes.NumericAxis) {
domainNRangeValue = domainRangeOfNumericForAreaChart(points, margins, width, isRTL);
domainNRangeValue = domainRangeOfNumericForAreaLineScatterCharts(points, margins, width, isRTL);
} else if (xAxisType === XAxisTypes.DateAxis) {
domainNRangeValue = domainRangeOfDateForAreaLineVerticalBarChart(
domainNRangeValue = domainRangeOfDateForAreaLineScatterVerticalBarCharts(
points,
margins,
width,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@ export class CartesianChartBase
this.props.tickParams!,
this.props.chartType,
culture,
this.props.xScaleType,
));
break;
case XAxisTypes.DateAxis:
Expand Down Expand Up @@ -357,6 +358,7 @@ export class CartesianChartBase
this.props.tickParams!,
this.props.chartType,
culture,
this.props.xScaleType,
));
}
this._xScale = xScale;
Expand Down Expand Up @@ -430,6 +432,7 @@ export class CartesianChartBase
true,
this.props.supportNegativeData!,
this.props.roundedTicks!,
this.props.secondaryYScaleType,
);
}
yScalePrimary = this.props.createYAxis(
Expand All @@ -440,6 +443,7 @@ export class CartesianChartBase
false,
this.props.supportNegativeData!,
this.props.roundedTicks!,
this.props.yScaleType,
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ICalloutProps } from '@fluentui/react/lib/Callout';
import { ILegendsProps } from '../Legends/index';
import {
AxisCategoryOrder,
AxisScaleType,
IAccessibilityProps,
IChart,
IDataPoint,
Expand All @@ -16,7 +17,7 @@ import {
IHorizontalBarChartWithAxisDataPoint,
ILineChartPoints,
IMargins,
IScatterChartDataPoint,
IScatterChartPoints,
IVerticalBarChartDataPoint,
IVerticalStackedBarDataPoint,
} from '../../types/index';
Expand Down Expand Up @@ -513,6 +514,24 @@ export interface ICartesianChartProps {
* @default 'default'
*/
yAxisCategoryOrder?: AxisCategoryOrder;

/**
* Defines the scale type for the x-axis.
* @default 'default'
*/
xScaleType?: AxisScaleType;

/**
* Defines the scale type for the primary y-axis.
* @default 'default'
*/
yScaleType?: AxisScaleType;

/**
* Defines the scale type for the secondary y-axis.
* @default 'default'
*/
secondaryYScaleType?: AxisScaleType;
}

export interface IYValueHover {
Expand Down Expand Up @@ -721,7 +740,7 @@ export interface IModifiedCartesianChartProps extends ICartesianChartProps {
| IHorizontalBarChartWithAxisDataPoint[]
| IVerticalBarChartDataPoint[]
| IDataPoint[]
| IScatterChartDataPoint[]
| IScatterChartPoints[]
| IGanttChartDataPoint[],
yAxisType: YAxisType | undefined,
useSecondaryYScale?: boolean,
Expand All @@ -738,6 +757,7 @@ export interface IModifiedCartesianChartProps extends ICartesianChartProps {
useSecondaryYScale?: boolean,
supportNegativeData?: boolean,
roundedTicks?: boolean,
scaleType?: AxisScaleType,
) => ScaleLinear<number, number, never>;

/**
Expand Down
Loading
Loading