Skip to content

Commit b039193

Browse files
Anush2303Anush
andauthored
feat(react-charts): support image export in v9 (#34929)
Co-authored-by: Anush <anushgupta@microsoft.com>
1 parent ea2fbd6 commit b039193

File tree

24 files changed

+643
-28
lines changed

24 files changed

+643
-28
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "patch",
3+
"comment": "support image export in v9 charts",
4+
"packageName": "@fluentui/react-charts",
5+
"email": "anushgupta@microsoft.com",
6+
"dependentChangeType": "patch"
7+
}

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,8 @@ export interface CartesianChartStyles {
229229
export interface Chart {
230230
// (undocumented)
231231
chartContainer: HTMLElement | null;
232+
// (undocumented)
233+
toImage?: (opts?: ImageExportOptions) => Promise<string>;
232234
}
233235

234236
// @public
@@ -959,6 +961,16 @@ export interface Legend {
959961
title: string;
960962
}
961963

964+
// @public (undocumented)
965+
export interface LegendContainer {
966+
// (undocumented)
967+
toSVG: (svgWidth: number, isRTL?: boolean) => {
968+
node: SVGGElement | null;
969+
width: number;
970+
height: number;
971+
};
972+
}
973+
962974
// @public (undocumented)
963975
export interface LegendDataItem {
964976
legendColor: string;
@@ -980,6 +992,7 @@ export interface LegendsProps {
980992
defaultSelectedLegend?: string;
981993
defaultSelectedLegends?: string[];
982994
enabledWrapLines?: boolean;
995+
legendRef?: React_2.RefObject<LegendContainer>;
983996
legends: Legend[];
984997
onChange?: (selectedLegends: string[], event: React_2.MouseEvent<HTMLButtonElement>, currentLegend?: Legend) => void;
985998
overflowStyles?: React_2.CSSProperties;

packages/charts/react-charts/library/src/components/AreaChart/AreaChart.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
YValueHover,
1818
ChartPopoverProps,
1919
Chart,
20+
ImageExportOptions,
2021
} from '../../index';
2122
import {
2223
calloutData,
@@ -38,10 +39,12 @@ import {
3839
domainRangeOfNumericForAreaChart,
3940
domainRangeOfDateForAreaLineVerticalBarChart,
4041
createStringYAxis,
42+
useRtl,
4143
} from '../../utilities/index';
4244
import { useId } from '@fluentui/react-utilities';
43-
import { Legend, Legends } from '../Legends/index';
45+
import { Legend, LegendContainer, Legends } from '../Legends/index';
4446
import { ScaleLinear } from 'd3-scale';
47+
import { toImage } from '../../utilities/image-export-utils';
4548

4649
// eslint-disable-next-line @typescript-eslint/no-explicit-any
4750
const bisect = bisector((d: any) => d.x).left;
@@ -103,6 +106,8 @@ export const AreaChart: React.FunctionComponent<AreaChartProps> = React.forwardR
103106
// determines if the given area chart has multiple stacked bar charts
104107
let _isMultiStackChart: boolean;
105108
const cartesianChartRef = React.useRef<Chart>(null);
109+
const _legendsRef = React.useRef<LegendContainer>(null);
110+
const _isRTL: boolean = useRtl();
106111

107112
const [selectedLegends, setSelectedLegends] = React.useState<string[]>(props.legendProps?.selectedLegends || []);
108113
const [activeLegend, setActiveLegend] = React.useState<string | undefined>(undefined);
@@ -135,6 +140,9 @@ export const AreaChart: React.FunctionComponent<AreaChartProps> = React.forwardR
135140
props.componentRef,
136141
() => ({
137142
chartContainer: cartesianChartRef.current?.chartContainer ?? null,
143+
toImage: (opts?: ImageExportOptions): Promise<string> => {
144+
return toImage(cartesianChartRef.current?.chartContainer, _legendsRef.current?.toSVG, _isRTL, opts);
145+
},
138146
}),
139147
[],
140148
);
@@ -508,6 +516,7 @@ export const AreaChart: React.FunctionComponent<AreaChartProps> = React.forwardR
508516
enabledWrapLines={props.enabledLegendsWrapLines}
509517
{...props.legendProps}
510518
onChange={_onLegendSelectionChange}
519+
legendRef={_legendsRef}
511520
/>
512521
);
513522
}

packages/charts/react-charts/library/src/components/DeclarativeChart/DeclarativeChart.tsx

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,7 @@ import { SankeyChart } from '../SankeyChart/SankeyChart';
3939
import { GaugeChart } from '../GaugeChart/index';
4040
import { GroupedVerticalBarChart } from '../GroupedVerticalBarChart/index';
4141
import { VerticalBarChart } from '../VerticalBarChart/index';
42-
import { ImageExportOptions, toImage } from './imageExporter';
43-
import { Chart } from '../../types/index';
42+
import { Chart, ImageExportOptions } from '../../types/index';
4443
import { ScatterChart } from '../ScatterChart/index';
4544

4645
import { withResponsiveContainer } from '../ResponsiveContainer/withResponsiveContainer';
@@ -229,11 +228,20 @@ export const DeclarativeChart: React.FunctionComponent<DeclarativeChartProps> =
229228
};
230229

231230
// TODO
232-
const exportAsImage = React.useCallback((opts?: ImageExportOptions) => {
233-
return toImage(chartRef.current?.chartContainer, {
234-
background: tokens.colorNeutralBackground1,
235-
scale: 5,
236-
...opts,
231+
const exportAsImage = React.useCallback((opts?: ImageExportOptions): Promise<string> => {
232+
return new Promise((resolve, reject) => {
233+
if (!chartRef.current || typeof chartRef.current.toImage !== 'function') {
234+
return reject(Error('Chart cannot be exported as image'));
235+
}
236+
237+
chartRef.current
238+
.toImage({
239+
background: tokens.colorNeutralBackground1,
240+
scale: 5,
241+
...opts,
242+
})
243+
.then(resolve)
244+
.catch(reject);
237245
});
238246
}, []);
239247

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
11
export * from './DeclarativeChart';
2-
export type { ImageExportOptions } from './imageExporter';

packages/charts/react-charts/library/src/components/DonutChart/DonutChart.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ import { DonutChartProps } from './DonutChart.types';
55
import { useDonutChartStyles } from './useDonutChartStyles.styles';
66
import { ChartDataPoint } from '../../DonutChart';
77
import { formatToLocaleString } from '@fluentui/chart-utilities';
8-
import { getColorFromToken, getNextColor } from '../../utilities/index';
9-
import { Legend, Legends } from '../../index';
8+
import { getColorFromToken, getNextColor, useRtl } from '../../utilities/index';
9+
import { Legend, Legends, LegendContainer } from '../../index';
1010
import { useId } from '@fluentui/react-utilities';
1111
import { useFocusableGroup } from '@fluentui/react-tabster';
1212
import { ChartPopover } from '../CommonComponents/ChartPopover';
13+
import { ImageExportOptions } from '../../types/index';
14+
import { toImage } from '../../utilities/image-export-utils';
1315

1416
const MIN_LEGEND_CONTAINER_HEIGHT = 40;
1517

@@ -41,6 +43,8 @@ export const DonutChart: React.FunctionComponent<DonutChartProps> = React.forwar
4143
const [dataPointCalloutProps, setDataPointCalloutProps] = React.useState<ChartDataPoint | undefined>();
4244
const [clickPosition, setClickPosition] = React.useState({ x: 0, y: 0 });
4345
const [isPopoverOpen, setPopoverOpen] = React.useState(false);
46+
const _legendsRef = React.useRef<LegendContainer>(null);
47+
const _isRTL: boolean = useRtl();
4448

4549
React.useEffect(() => {
4650
_fitParentContainer();
@@ -58,6 +62,9 @@ export const DonutChart: React.FunctionComponent<DonutChartProps> = React.forwar
5862
props.componentRef,
5963
() => ({
6064
chartContainer: _rootElem.current,
65+
toImage: (opts?: ImageExportOptions): Promise<string> => {
66+
return toImage(_rootElem.current, _legendsRef.current?.toSVG, _isRTL, opts);
67+
},
6168
}),
6269
[],
6370
);
@@ -113,6 +120,7 @@ export const DonutChart: React.FunctionComponent<DonutChartProps> = React.forwar
113120
centerLegends
114121
overflowText={props.legendsOverflowText}
115122
{...props.legendProps}
123+
legendRef={_legendsRef}
116124
/>
117125
);
118126
return legends;

packages/charts/react-charts/library/src/components/FunnelChart/FunnelChart.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as React from 'react';
22
import { useId } from '@fluentui/react-utilities';
33
import { useRtl } from '../../utilities/index';
44
import { FunnelChartDataPoint, FunnelChartProps } from './FunnelChart.types';
5-
import { Legend, Legends } from '../Legends/index';
5+
import { Legend, Legends, LegendContainer } from '../Legends/index';
66
import { useFocusableGroup } from '@fluentui/react-tabster';
77
import { ChartPopover } from '../CommonComponents/ChartPopover';
88
import { formatToLocaleString } from '@fluentui/chart-utilities';
@@ -15,7 +15,8 @@ import {
1515
getStackedHorizontalFunnelSegmentGeometry,
1616
getStackedVerticalFunnelSegmentGeometry,
1717
} from './funnelGeometry';
18-
import { ChartPopoverProps } from '../../index';
18+
import { ChartPopoverProps, ImageExportOptions } from '../../index';
19+
import { toImage } from '../../utilities/image-export-utils';
1920

2021
export const FunnelChart: React.FunctionComponent<FunnelChartProps> = React.forwardRef<
2122
HTMLDivElement,
@@ -31,13 +32,24 @@ export const FunnelChart: React.FunctionComponent<FunnelChartProps> = React.forw
3132
const [isPopoverOpen, setPopoverOpen] = React.useState(false);
3233
const chartContainerRef = React.useRef<HTMLDivElement | null>(null);
3334
const isStacked = isStackedFunnelData(props.data);
35+
const _legendsRef = React.useRef<LegendContainer>(null);
3436

3537
React.useEffect(() => {
3638
if (props.legendProps?.selectedLegends) {
3739
setSelectedLegends(props.legendProps.selectedLegends);
3840
}
3941
}, [props.legendProps?.selectedLegends]);
4042

43+
React.useImperativeHandle(
44+
props.componentRef,
45+
() => ({
46+
toImage: (opts?: ImageExportOptions): Promise<string> => {
47+
return toImage(chartContainerRef.current, _legendsRef.current?.toSVG, isRTL, opts);
48+
},
49+
}),
50+
[],
51+
);
52+
4153
function _handleHover(data: FunnelChartDataPoint, mouseEvent: React.MouseEvent<SVGElement>) {
4254
mouseEvent?.persist();
4355
updatePosition(mouseEvent.clientX, mouseEvent.clientY);
@@ -404,6 +416,7 @@ export const FunnelChart: React.FunctionComponent<FunnelChartProps> = React.forw
404416
centerLegends={true}
405417
onChange={_onLegendSelectionChangeCallback}
406418
{...props.legendProps}
419+
legendRef={_legendsRef}
407420
/>
408421
</div>
409422
);

packages/charts/react-charts/library/src/components/GanttChart/GanttChart.tsx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ import * as React from 'react';
22
import { max as d3Max, min as d3Min } from 'd3-array';
33
import { ScaleLinear, ScaleBand, ScaleTime } from 'd3-scale';
44
import { useId } from '@fluentui/react-utilities';
5-
import { Legend, Legends } from '../Legends/index';
5+
import { Legend, Legends, LegendContainer } from '../Legends/index';
66
import { Margins, GanttChartDataPoint } from '../../types/DataPoint';
77
import { CartesianChart, ModifiedCartesianChartProps } from '../CommonComponents/index';
88
import { GanttChartProps } from './GanttChart.types';
99
import { ChartPopover } from '../CommonComponents/ChartPopover';
10-
import { ChartPopoverProps } from '../../index';
10+
import { ChartPopoverProps, ImageExportOptions, Chart } from '../../index';
1111
import {
1212
ChartTypes,
1313
YAxisType,
@@ -25,8 +25,10 @@ import {
2525
getColorFromToken,
2626
getScalePadding,
2727
getDateFormatLevel,
28+
useRtl,
2829
} from '../../utilities/index';
2930
import { formatDateToLocaleString, getMultiLevelDateTimeFormatOptions } from '@fluentui/chart-utilities';
31+
import { toImage } from '../../utilities/image-export-utils';
3032

3133
type NumberScale = ScaleLinear<number, number>;
3234
type StringScale = ScaleBand<string>;
@@ -54,6 +56,9 @@ export const GanttChart: React.FunctionComponent<GanttChartProps> = React.forwar
5456
const [calloutDataPoint, setCalloutDataPoint] = React.useState<GanttChartDataPoint>();
5557
const [clickPosition, setClickPosition] = React.useState({ x: 0, y: 0 });
5658
const [isPopoverOpen, setPopoverOpen] = React.useState(false);
59+
const cartesianChartRef = React.useRef<Chart>(null);
60+
const _legendsRef = React.useRef<LegendContainer>(null);
61+
const _isRTL = useRtl();
5762

5863
React.useEffect(() => {
5964
if (!areArraysEqual(_prevProps.current.legendProps?.selectedLegends, props.legendProps?.selectedLegends)) {
@@ -62,6 +67,17 @@ export const GanttChart: React.FunctionComponent<GanttChartProps> = React.forwar
6267
_prevProps.current = props;
6368
}, [props]);
6469

70+
React.useImperativeHandle(
71+
props.componentRef,
72+
() => ({
73+
chartContainer: cartesianChartRef.current?.chartContainer ?? null,
74+
toImage: (opts?: ImageExportOptions): Promise<string> => {
75+
return toImage(cartesianChartRef.current?.chartContainer, _legendsRef.current?.toSVG, _isRTL, opts);
76+
},
77+
}),
78+
[],
79+
);
80+
6581
const _points = React.useMemo(() => {
6682
_legendMap.current = {};
6783
let colorIndex = 0;
@@ -494,6 +510,7 @@ export const GanttChart: React.FunctionComponent<GanttChartProps> = React.forwar
494510
overflowText={props.legendsOverflowText}
495511
onChange={_onLegendSelectionChange}
496512
{...props.legendProps}
513+
legendRef={_legendsRef}
497514
/>
498515
);
499516
return legends;
@@ -585,6 +602,7 @@ export const GanttChart: React.FunctionComponent<GanttChartProps> = React.forwar
585602
chartType={ChartTypes.GanttChart}
586603
xAxisType={_xAxisType}
587604
yAxisType={_yAxisType}
605+
componentRef={cartesianChartRef}
588606
stringDatasetForYAxisDomain={_yAxisLabels}
589607
calloutProps={calloutProps}
590608
tickParams={tickParams}

packages/charts/react-charts/library/src/components/GaugeChart/GaugeChart.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@ import {
1515
} from '../../utilities/index';
1616
import { formatToLocaleString } from '@fluentui/chart-utilities';
1717
import { SVGTooltipText } from '../../utilities/SVGTooltipText';
18-
import { Legend, LegendShape, Legends, Shape } from '../Legends/index';
18+
import { Legend, LegendShape, Legends, Shape, LegendContainer } from '../Legends/index';
1919
import { GaugeChartVariant, GaugeValueFormat, GaugeChartProps, GaugeChartSegment } from './GaugeChart.types';
2020
import { useFocusableGroup } from '@fluentui/react-tabster';
2121
import { ChartPopover } from '../CommonComponents/ChartPopover';
22+
import { ImageExportOptions } from '../../types/index';
23+
import { toImage } from '../../utilities/image-export-utils';
2224

2325
const GAUGE_MARGIN = 16;
2426
const LABEL_WIDTH = 36;
@@ -101,6 +103,7 @@ export interface ExtendedSegment extends GaugeChartSegment {
101103

102104
export const GaugeChart: React.FunctionComponent<GaugeChartProps> = React.forwardRef<HTMLDivElement, GaugeChartProps>(
103105
(props, forwardedRef) => {
106+
const _legendsRef = React.useRef<LegendContainer>(null);
104107
const _getMargins = () => {
105108
const { hideMinMax, chartTitle, sublabel } = props;
106109
return {
@@ -159,6 +162,9 @@ export const GaugeChart: React.FunctionComponent<GaugeChartProps> = React.forwar
159162
props.componentRef,
160163
() => ({
161164
chartContainer: _rootElem.current,
165+
toImage: (opts?: ImageExportOptions): Promise<string> => {
166+
return toImage(_rootElem.current, _legendsRef.current?.toSVG, _isRTL, opts);
167+
},
162168
}),
163169
[],
164170
);
@@ -307,6 +313,7 @@ export const GaugeChart: React.FunctionComponent<GaugeChartProps> = React.forwar
307313
{...props.legendProps}
308314
// eslint-disable-next-line react/jsx-no-bind
309315
onChange={_onLegendSelectionChange}
316+
legendRef={_legendsRef}
310317
/>
311318
</div>
312319
);

packages/charts/react-charts/library/src/components/GroupedVerticalBarChart/GroupedVerticalBarChart.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,10 @@ import {
4343
getColorFromToken,
4444
ChartPopoverProps,
4545
Chart,
46+
ImageExportOptions,
47+
LegendContainer,
4648
} from '../../index';
49+
import { toImage } from '../../utilities/image-export-utils';
4750

4851
type StringAxis = D3Axis<string>;
4952
type NumericAxis = D3Axis<number | { valueOf(): number }>;
@@ -89,6 +92,7 @@ export const GroupedVerticalBarChart: React.FC<GroupedVerticalBarChartProps> = R
8992
let _xAxisOuterPadding: number = 0;
9093
const cartesianChartRef = React.useRef<Chart>(null);
9194
const Y_ORIGIN: number = 0;
95+
const _legendsRef = React.useRef<LegendContainer>(null);
9296

9397
const [color, setColor] = React.useState<string>('');
9498
const [dataForHoverCard, setDataForHoverCard] = React.useState<number>(0);
@@ -116,6 +120,9 @@ export const GroupedVerticalBarChart: React.FC<GroupedVerticalBarChartProps> = R
116120
props.componentRef,
117121
() => ({
118122
chartContainer: cartesianChartRef.current?.chartContainer ?? null,
123+
toImage: (opts?: ImageExportOptions): Promise<string> => {
124+
return toImage(cartesianChartRef.current?.chartContainer, _legendsRef.current?.toSVG, _useRtl, opts);
125+
},
119126
}),
120127
[],
121128
);
@@ -231,6 +238,7 @@ export const GroupedVerticalBarChart: React.FC<GroupedVerticalBarChartProps> = R
231238
overflowText={props.legendsOverflowText}
232239
{...props.legendProps}
233240
onChange={onLegendSelectionChange}
241+
legendRef={_legendsRef}
234242
/>
235243
);
236244
};

0 commit comments

Comments
 (0)