Skip to content
Open
Show file tree
Hide file tree
Changes from all 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": "minor",
"comment": "feat(react-charts): Add annotation support for GaugeChart",
"packageName": "@fluentui/react-charts",
"email": "srmukher@microsoft.com",
"dependentChangeType": "patch"
}
65 changes: 64 additions & 1 deletion packages/charts/react-charts/library/etc/react-charts.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,9 +190,18 @@ export const BREAKPOINTS: {
fontSize: number;
}[];

// @public
export const calcAnnotationPosition: (value: number, position: "inner" | "outer" | "arc", minValue: number, maxValue: number, innerRadius: number, outerRadius: number, radialOffset?: number) => {
x: number;
y: number;
};

// @public (undocumented)
export const calcNeedleRotation: (chartValue: number, minValue: number, maxValue: number) => number;

// @public
export const calcValueToAngle: (value: number, minValue: number, maxValue: number) => number;

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

Expand Down Expand Up @@ -895,13 +904,58 @@ export interface GanttChartStyles extends CartesianChartStyles {
// @public (undocumented)
export const GaugeChart: React_2.FunctionComponent<GaugeChartProps>;

// @public
export interface GaugeChartAnnotation {
ariaLabel?: string;
arrow?: GaugeChartAnnotationArrow;
coordinates: GaugeChartAnnotationCoordinate;
id?: string;
style?: GaugeChartAnnotationStyle;
text: string;
}

// @public
export interface GaugeChartAnnotationArrow {
color?: string;
headSize?: number;
headStyle?: number;
show?: boolean;
tailOffsetX?: number;
tailOffsetY?: number;
width?: number;
}

// @public
export interface GaugeChartAnnotationCoordinate {
position?: GaugeChartAnnotationPosition;
radialOffset?: number;
value: number;
}

// @public
export type GaugeChartAnnotationPosition = 'inner' | 'outer' | 'arc';

// @public
export interface GaugeChartAnnotationStyle {
backgroundColor?: string;
borderColor?: string;
borderRadius?: number;
borderWidth?: number;
className?: string;
fontSize?: string;
fontWeight?: React_2.CSSProperties['fontWeight'];
padding?: string;
textColor?: string;
}

// @public
export interface GaugeChartProps {
annotations?: GaugeChartAnnotation[];
calloutProps?: Partial<ChartPopoverProps>;
chartTitle?: string;
chartValue: number;
chartValueFormat?: GaugeValueFormat | ((sweepFraction: [number, number]) => string);
componentRef?: React.Ref<Chart>;
componentRef?: React_2.Ref<Chart>;
culture?: string;
enableGradient?: boolean;
height?: number;
Expand All @@ -912,6 +966,7 @@ export interface GaugeChartProps {
legendProps?: Partial<LegendsProps>;
maxValue?: number;
minValue?: number;
onRenderAnnotation?: (annotation: GaugeChartAnnotation, defaultRender: (annotation: GaugeChartAnnotation) => React_2.ReactNode) => React_2.ReactNode;
roundCorners?: boolean;
segments: GaugeChartSegment[];
styles?: GaugeChartStyles;
Expand All @@ -932,6 +987,8 @@ export interface GaugeChartSegment {

// @public
export interface GaugeChartStyles {
annotationContainer?: string;
annotationText?: string;
calloutBlockContainer?: string;
calloutContentRoot?: string;
calloutContentX?: string;
Expand Down Expand Up @@ -961,6 +1018,9 @@ export type GaugeChartVariant = 'single-segment' | 'multiple-segments';
// @public (undocumented)
export type GaugeValueFormat = 'percentage' | 'fraction';

// @public
export const getArrowHeadPath: (headStyle: number, headSize: number, width: number) => string;

// @public (undocumented)
export const getChartValueLabel: (chartValue: number, minValue: number, maxValue: number, chartValueFormat?: GaugeValueFormat | ((sweepFraction: [number, number]) => string), forCallout?: boolean) => string;

Expand Down Expand Up @@ -1622,6 +1682,9 @@ export interface RefArrayData {
refElement?: SVGGElement;
}

// @public
export const renderAnnotationArrow: (startX: number, startY: number, endX: number, endY: number, arrow: GaugeChartAnnotationArrow, uniqueId: string) => React_2.ReactNode;

// @public (undocumented)
export interface ResolvedAnnotationPosition {
anchor: AnnotationPoint;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import { LineChartProps } from '../LineChart/index';
import { AreaChartProps } from '../AreaChart/index';
import { HeatMapChartProps } from '../HeatMapChart/index';
import { DataVizPalette, getColorFromToken } from '../../utilities/colors';
import { GaugeChartProps, GaugeChartSegment } from '../GaugeChart/index';
import { GaugeChartProps, GaugeChartSegment, GaugeChartAnnotation } from '../GaugeChart/index';
import { GroupedVerticalBarChartProps } from '../GroupedVerticalBarChart/index';
import { VerticalBarChartProps } from '../VerticalBarChart/index';
import { ChartTableProps } from '../ChartTable/index';
Expand Down Expand Up @@ -2721,6 +2721,86 @@ export const transformPlotlyJsonToSankeyProps = (
} as SankeyChartProps;
};

/**
* Transforms Plotly annotations to GaugeChartAnnotation format.
* Plotly uses paper coordinates (0-1) where:
* - x: 0=left, 0.5=center, 1=right (maps to gauge minValue-maxValue)
* - y: 0=bottom, 1=top (used to determine inner/outer/arc positioning)
*/
const transformPlotlyAnnotationsToGaugeAnnotations = (
annotations: Partial<Annotations>[] | undefined,
minValue: number,
maxValue: number,
): GaugeChartAnnotation[] => {
if (!annotations || annotations.length === 0) {
return [];
}

return annotations.map((annotation, index): GaugeChartAnnotation => {
// Map x coordinate (0-1) to gauge value (minValue-maxValue)
const x = typeof annotation.x === 'number' ? annotation.x : 0.5;
const y = typeof annotation.y === 'number' ? annotation.y : 0.5;
const gaugeValue = minValue + x * (maxValue - minValue);

// Determine position based on y coordinate
// y < 0.3: below gauge (outer-bottom)
// y > 0.6: above gauge arc (outer-top)
// 0.3 <= y <= 0.6: on the arc area
let position: 'inner' | 'outer' | 'arc' = 'outer';
let radialOffset = 0;
if (y >= 0.3 && y <= 0.6) {
position = 'arc';
} else if (y < 0.15) {
position = 'outer';
radialOffset = 20; // Further outside for bottom annotations
}

// Clean text content - replace <br> with spaces and strip HTML tags
const cleanedText = (annotation.text || '')
.replace(/<br\s*\/?>/gi, ' ')
.replace(/<[^>]*>/g, '')
.trim();

// Transform arrow properties from Plotly
const arrowConfig =
annotation.showarrow === true
? {
show: true,
headStyle: typeof annotation.arrowhead === 'number' ? annotation.arrowhead : 2,
color: annotation.arrowcolor?.toString() || '#333333',
width: typeof annotation.arrowwidth === 'number' ? annotation.arrowwidth : 1,
headSize: typeof annotation.arrowsize === 'number' ? annotation.arrowsize : 1,
// Convert Plotly ax/ay (pixel offsets) to tailOffsetX/tailOffsetY
// In Plotly, ax/ay define offset from arrow tip to tail/text position
// Plotly y-axis goes up, SVG y-axis goes down, so we DON'T negate ay
// (Plotly ay=-40 means text is 40px below tip in visual space, same in SVG)
tailOffsetX: typeof annotation.ax === 'number' ? annotation.ax : 0,
tailOffsetY: typeof annotation.ay === 'number' ? annotation.ay : 40,
}
: undefined;

return {
id: `plotly-annotation-${index}`,
text: cleanedText,
coordinates: {
value: gaugeValue,
position,
radialOffset,
},
style: {
textColor: annotation.font?.color?.toString(),
fontSize: annotation.font?.size ? `${annotation.font.size}px` : undefined,
fontWeight: annotation.font?.family?.includes('Bold') ? 'bold' : undefined,
backgroundColor: annotation.bgcolor?.toString(),
borderColor: annotation.bordercolor?.toString(),
borderWidth: annotation.borderwidth,
},
arrow: arrowConfig,
ariaLabel: cleanedText,
};
});
};

export const transformPlotlyJsonToGaugeProps = (
input: PlotlySchema,
isMultiPlot: boolean,
Expand Down Expand Up @@ -2824,6 +2904,17 @@ export const transformPlotlyJsonToGaugeProps = (

const { chartTitle, titleStyles } = getTitles(input.layout);

// Get min/max values for annotation transformation
const gaugeMinValue = typeof firstData.gauge?.axis?.range?.[0] === 'number' ? firstData.gauge?.axis?.range?.[0] : 0;
const gaugeMaxValue = typeof firstData.gauge?.axis?.range?.[1] === 'number' ? firstData.gauge?.axis?.range?.[1] : 100;

// Transform Plotly annotations to GaugeChartAnnotation format
const annotations = transformPlotlyAnnotationsToGaugeAnnotations(
input.layout?.annotations,
gaugeMinValue,
gaugeMaxValue,
);

return {
segments,
chartValue: firstData.value ?? 0,
Expand All @@ -2841,6 +2932,7 @@ export const transformPlotlyJsonToGaugeProps = (
styles,
roundCorners: true,
...(titleStyles ? { titleStyles } : {}),
...(annotations.length > 0 ? { annotations } : {}),
} as GaugeChartProps;
};

Expand Down
Loading