Skip to content

Commit 5a3fe67

Browse files
committed
Merge branch 'master' of https://github.com/microsoft/fluentui into BugFixes_2025
2 parents d8db3d2 + 14a6cb6 commit 5a3fe67

File tree

41 files changed

+612
-182
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+612
-182
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "patch",
3+
"comment": "fix(react-charting): Decode binary data",
4+
"packageName": "@fluentui/chart-utilities",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "patch",
3+
"comment": "fix: modify check for scatter plot",
4+
"packageName": "@fluentui/chart-utilities",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "patch",
3+
"comment": "feat: add support for line curves",
4+
"packageName": "@fluentui/react-charting",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "patch",
3+
"comment": "Add scaling factor to heights of Vertical Stacked bars",
4+
"packageName": "@fluentui/react-charting",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "patch",
3+
"comment": "Updated Pie chart title color and arc text colors to fix accessibility issue",
4+
"packageName": "@fluentui/react-charting",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "patch",
3+
"comment": "fix(react-charting): Secondary Y Axis margin adjustment",
4+
"packageName": "@fluentui/react-charting",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "patch",
3+
"comment": "fix(react-charting): Decode binary data",
4+
"packageName": "@fluentui/react-charting",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}

packages/charts/chart-utilities/etc/chart-utilities.api.md

+3
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,9 @@ export type DataTransform = Partial<Transform>;
296296
// @public (undocumented)
297297
export type Datum = string | number | Date | null;
298298

299+
// @public (undocumented)
300+
export function decodeBase64Fields(plotlySchema: PlotlySchema): PlotlySchema;
301+
299302
// @public (undocumented)
300303
export interface Delta {
301304
// (undocumented)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { PlotlySchema } from './PlotlySchema';
2+
3+
function addBase64Padding(s: string): string {
4+
const paddingNeeded = (4 - (s.length % 4)) % 4;
5+
return s + '='.repeat(paddingNeeded);
6+
}
7+
// Function to check if a string is base64-encoded
8+
function isBase64(s: string): boolean {
9+
if (typeof s !== 'string') {
10+
return false;
11+
}
12+
13+
// Base64 strings must have a length that is a multiple of 4
14+
if (s.length % 4 !== 0) {
15+
s = addBase64Padding(s);
16+
}
17+
18+
// Use a regular expression to check if the string contains only valid base64 characters
19+
const base64Regex = /^[A-Za-z0-9+/]+={0,2}$/;
20+
if (!base64Regex.test(s)) {
21+
return false;
22+
}
23+
24+
try {
25+
decodeBase64FromString(s);
26+
return true;
27+
} catch {
28+
return false;
29+
}
30+
}
31+
32+
function decodeBase64FromString(base64String: string): Uint8Array {
33+
if (typeof window !== 'undefined' && typeof atob === 'function') {
34+
// For browsers
35+
const binaryString = atob(base64String);
36+
const binaryLength = binaryString.length;
37+
const bytes = new Uint8Array(binaryLength);
38+
for (let i = 0; i < binaryLength; i++) {
39+
bytes[i] = binaryString.charCodeAt(i);
40+
}
41+
return bytes;
42+
}
43+
throw new Error('Base64 decoding is not supported in this environment.');
44+
}
45+
46+
// Helper function to decode base64-encoded data based on dtype
47+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
48+
function decodeBase64(value: string, dtype: string): any {
49+
// Add padding if necessary
50+
value = addBase64Padding(value);
51+
52+
try {
53+
const decodedBytes = decodeBase64FromString(value);
54+
switch (dtype) {
55+
case 'f8':
56+
return Array.from(new Float64Array(decodedBytes.buffer));
57+
case 'i8':
58+
return Array.from(new BigInt64Array(decodedBytes.buffer));
59+
case 'u8':
60+
return Array.from(new BigUint64Array(decodedBytes.buffer));
61+
case 'i4':
62+
return Array.from(new Int32Array(decodedBytes.buffer));
63+
case 'i2':
64+
return Array.from(new Int16Array(decodedBytes.buffer));
65+
case 'i1':
66+
return Array.from(new Int8Array(decodedBytes.buffer));
67+
default:
68+
try {
69+
return decodedBytes.toString();
70+
} catch (error) {
71+
return decodedBytes;
72+
}
73+
}
74+
} catch (error) {
75+
throw new Error(`Failed to decode base64 value: ${value}`);
76+
}
77+
}
78+
79+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
80+
function decodeBdataInDict(d: any): void {
81+
for (const [key, value] of Object.entries(d)) {
82+
if (typeof value === 'object' && !Array.isArray(value) && value !== null) {
83+
decodeBdataInDict(value); // Recursively process nested objects
84+
} else if (key === 'bdata' && typeof value === 'string' && isBase64(value)) {
85+
const dtype = d.dtype || 'utf-8'; // Get dtype or default to 'utf-8'
86+
d[key] = decodeBase64(value, dtype); // Decode the base64-encoded value
87+
} else if (Array.isArray(value)) {
88+
for (let i = 0; i < value.length; i++) {
89+
if (typeof value[i] === 'object' && value[i] !== null) {
90+
decodeBdataInDict(value[i]); // Recursively process objects in arrays
91+
}
92+
}
93+
}
94+
}
95+
}
96+
97+
// Function to process a PlotlySchema object
98+
export function decodeBase64Fields(plotlySchema: PlotlySchema): PlotlySchema {
99+
// Create a deep copy of the original data
100+
const originalData = JSON.parse(JSON.stringify(plotlySchema.data));
101+
102+
// Decode base64-encoded 'bdata' in the JSON data
103+
decodeBdataInDict(plotlySchema.data);
104+
105+
// Check if the data has changed
106+
if (JSON.stringify(plotlySchema.data) !== JSON.stringify(originalData)) {
107+
let isNan = false;
108+
109+
// Overwrite the 'y', 'x', or 'z' value with the decoded 'bdata'
110+
for (const item of plotlySchema.data || []) {
111+
['y', 'x', 'z'].forEach(key => {
112+
if (
113+
item[key as keyof typeof item] &&
114+
typeof item[key as keyof typeof item] === 'object' &&
115+
'bdata' in (item[key as keyof typeof item] as Record<string, number[]>)
116+
) {
117+
const bdata = (item[key as keyof typeof item] as { bdata: number[] }).bdata;
118+
if (Array.isArray(bdata) && bdata.some(x => typeof x === 'number' && isNaN(x))) {
119+
isNan = true;
120+
} else {
121+
(item[key as keyof typeof item] as number[]) = bdata as number[];
122+
}
123+
}
124+
});
125+
if (isNan) {
126+
break;
127+
}
128+
}
129+
130+
if (!isNan) {
131+
return plotlySchema; // Return the decoded data
132+
}
133+
}
134+
135+
plotlySchema.data = originalData; // Restore the original data if no changes were made
136+
return plotlySchema; // Return the original data if no changes were made
137+
}

packages/charts/chart-utilities/src/PlotlySchemaConverter.ts

+11-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Datum, TypedArray, PlotData, PlotlySchema } from './PlotlySchema';
2+
import { decodeBase64Fields } from './DecodeBase64Data';
23

34
// eslint-disable-next-line @typescript-eslint/naming-convention
45
export interface OutputChartType {
@@ -155,7 +156,14 @@ export const mapFluentChart = (input: any): OutputChartType => {
155156
}
156157

157158
try {
158-
const validSchema: PlotlySchema = getValidSchema(input);
159+
let validSchema: PlotlySchema = getValidSchema(input);
160+
161+
try {
162+
validSchema = decodeBase64Fields(validSchema);
163+
} catch (error) {
164+
return { isValid: false, errorMessage: `Failed to decode plotly schema: ${error}` };
165+
}
166+
159167
switch (validSchema.data[0].type) {
160168
case 'pie':
161169
return { isValid: true, type: 'donut' };
@@ -191,11 +199,11 @@ export const mapFluentChart = (input: any): OutputChartType => {
191199
case 'histogram':
192200
return validateSeriesData(validSchema, 'verticalbar', false);
193201
case 'scatter':
194-
if (validSchema.data[0]?.mode?.includes('markers') && !isNumberArray(validSchema.data[0].y!)) {
202+
if (validSchema.data[0]?.mode === 'markers' && !isNumberArray(validSchema.data[0].y!)) {
195203
return {
196204
isValid: false,
197205
errorMessage: `Unsupported chart - type :${validSchema.data[0]?.type}, mode: ${validSchema.data[0]?.mode}
198-
, xAxisType: String or Date`,
206+
, yAxisType: String or Date`,
199207
};
200208
}
201209
const isAreaChart = validSchema.data.some(

packages/charts/chart-utilities/src/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ export type {
9393
XAxisName,
9494
YAxisName,
9595
} from './PlotlySchema';
96+
9697
export type { OutputChartType } from './PlotlySchemaConverter';
9798
export {
9899
mapFluentChart,
@@ -108,3 +109,5 @@ export {
108109
isTypedArray,
109110
isArrayOrTypedArray,
110111
} from './PlotlySchemaConverter';
112+
113+
export { decodeBase64Fields } from './DecodeBase64Data';

packages/charts/react-charting/etc/react-charting.api.md

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
55
```ts
66

7+
import { CurveFactory } from 'd3-shape';
78
import { FocusZoneDirection } from '@fluentui/react-focus';
89
import { ICalloutContentStyleProps } from '@fluentui/react/lib/Callout';
910
import { ICalloutContentStyles } from '@fluentui/react/lib/Callout';
@@ -986,6 +987,7 @@ export interface ILineChartGap {
986987

987988
// @public (undocumented)
988989
export interface ILineChartLineOptions extends React_2.SVGProps<SVGPathElement> {
990+
curve?: 'linear' | 'natural' | 'step' | 'stepAfter' | 'stepBefore' | CurveFactory;
989991
lineBorderColor?: string;
990992
lineBorderWidth?: string | number;
991993
strokeDasharray?: string | number;

packages/charts/react-charting/src/components/AreaChart/AreaChart.base.tsx

+16-15
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import {
4343
formatDate,
4444
getSecureProps,
4545
areArraysEqual,
46+
getCurveFactory,
4647
} from '../../utilities/index';
4748
import { ILegend, ILegendContainer, Legends } from '../Legends/index';
4849
import { DirectionalHint } from '@fluentui/react/lib/Callout';
@@ -734,25 +735,26 @@ export class AreaChartBase extends React.Component<IAreaChartProps, IAreaChartSt
734735
private _drawGraph = (containerHeight: number, xScale: any, yScale: any, xElement: SVGElement): JSX.Element[] => {
735736
const points = this._addDefaultColors(this.props.data.lineChartData);
736737
const { pointOptions, pointLineOptions } = this.props.data;
737-
const area = d3Area()
738-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
739-
.x((d: any) => xScale(d.xVal))
740-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
741-
.y0((d: any) => yScale(d.values[0]))
742-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
743-
.y1((d: any) => yScale(d.values[1]))
744-
.curve(d3CurveBasis);
745-
const line = d3Line()
746-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
747-
.x((d: any) => xScale(d.xVal))
748-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
749-
.y((d: any) => yScale(d.values[1]))
750-
.curve(d3CurveBasis);
751738

752739
const graph: JSX.Element[] = [];
753740
let lineColor: string;
754741
// eslint-disable-next-line @typescript-eslint/no-explicit-any
755742
this._data.forEach((singleStackedData: Array<any>, index: number) => {
743+
const curveFactory = getCurveFactory(points[index].lineOptions?.curve, d3CurveBasis);
744+
const area = d3Area()
745+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
746+
.x((d: any) => xScale(d.xVal))
747+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
748+
.y0((d: any) => yScale(d.values[0]))
749+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
750+
.y1((d: any) => yScale(d.values[1]))
751+
.curve(curveFactory);
752+
const line = d3Line()
753+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
754+
.x((d: any) => xScale(d.xVal))
755+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
756+
.y((d: any) => yScale(d.values[1]))
757+
.curve(curveFactory);
756758
const layerOpacity = this.props.mode === 'tozeroy' ? 0.8 : this._opacity[index];
757759
graph.push(
758760
<React.Fragment key={`${index}-graph-${this._uniqueIdForGraph}`}>
@@ -828,7 +830,6 @@ export class AreaChartBase extends React.Component<IAreaChartProps, IAreaChartSt
828830
graph.push(
829831
<g
830832
key={`${index}-dots-${this._uniqueIdForGraph}`}
831-
d={area(singleStackedData)!}
832833
clipPath="url(#clip)"
833834
role="region"
834835
aria-label={`${points[index].legend}, series ${index + 1} of ${points.length} with ${

packages/charts/react-charting/src/components/AreaChart/AreaChart.styles.ts

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export const getStyles = (props: IAreaChartStyleProps): IAreaChartStyles => {
1313
background: props.theme!.semanticColors.bodyBackground,
1414
borderRadius: '2px',
1515
pointerEvents: 'none',
16+
color: props.theme!.semanticColors.bodyText,
1617
},
1718
};
1819
};

packages/charts/react-charting/src/components/AreaChart/__snapshots__/AreaChart.test.tsx.snap

-3
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,6 @@ exports[`AreaChart - mouse events Should render callout correctly on mouseover 1
139139
<g
140140
aria-label="metaData1, series 1 of 1 with 2 data points."
141141
clipPath="url(#clip)"
142-
d="M40,115.625L-20,20L-20,275L40,275Z"
143142
key="0-dots-areaChart_0"
144143
role="region"
145144
>
@@ -708,7 +707,6 @@ exports[`AreaChart - mouse events Should render customized callout on mouseover
708707
<g
709708
aria-label="metaData1, series 1 of 1 with 2 data points."
710709
clipPath="url(#clip)"
711-
d="M40,115.625L-20,20L-20,275L40,275Z"
712710
key="0-dots-areaChart_0"
713711
role="region"
714712
>
@@ -1174,7 +1172,6 @@ exports[`AreaChart - mouse events Should render customized callout per stack on
11741172
<g
11751173
aria-label="metaData1, series 1 of 1 with 2 data points."
11761174
clipPath="url(#clip)"
1177-
d="M40,115.625L-20,20L-20,275L40,275Z"
11781175
key="0-dots-areaChart_0"
11791176
role="region"
11801177
>

0 commit comments

Comments
 (0)