Skip to content
Merged
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 @@
{
Comment thread
krkshitij marked this conversation as resolved.
"type": "patch",
"comment": "feat: enable multiplot image export",
"packageName": "@fluentui/react-charting",
"email": "kumarkshitij@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as React from 'react';
import { useTheme } from '@fluentui/react';
import { getRTL, useTheme } from '@fluentui/react';
import { mergeStyles } from '@fluentui/react/lib/Styling';
import { ChartAnnotationLayer } from '../CommonComponents/Annotations/ChartAnnotationLayer';
import { toImage as exportToImage } from '../../utilities/image-export-utils';
import { exportChartsAsImage } from '../../utilities/image-export-utils';
import type { IAnnotationOnlyChartProps } from './AnnotationOnlyChart.types';
import type { IChart, IImageExportOptions } from '../../types/index';
import type { IChartAnnotationContext } from '../CommonComponents/Annotations/ChartAnnotationLayer.types';
Expand Down Expand Up @@ -189,17 +189,13 @@ export const AnnotationOnlyChart: React.FC<IAnnotationOnlyChartProps> = props =>
const chartHandle: IChart = {
chartContainer: containerRef.current,
toImage: (opts?: IImageExportOptions) => {
if (!containerRef.current) {
return Promise.reject(new Error('Chart container is not defined'));
}

return exportToImage(containerRef.current, undefined, !!theme?.rtl, opts);
return exportChartsAsImage([{ container: containerRef.current }], undefined, getRTL(), opts);
},
};

return chartHandle;
},
[theme?.rtl],
[],
);

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ import {
import { ILegend, ILegendContainer, Legends } from '../Legends/index';
import { DirectionalHint } from '@fluentui/react/lib/Callout';
import { IChart, IImageExportOptions } from '../../types/index';
import { toImage } from '../../utilities/image-export-utils';
import { exportChartsAsImage } from '../../utilities/image-export-utils';
import { ScaleLinear } from 'd3-scale';
import type { JSXElement } from '@fluentui/utilities';

Expand Down Expand Up @@ -305,7 +305,12 @@ export class AreaChartBase extends React.Component<IAreaChartProps, IAreaChartSt
}

public toImage = (opts?: IImageExportOptions): Promise<string> => {
return toImage(this._cartesianChartRef.current?.chartContainer, this._legendsRef.current?.toSVG, getRTL(), opts);
return exportChartsAsImage(
[{ container: this._cartesianChartRef.current?.chartContainer }],
this.props.hideLegend ? undefined : this._legendsRef.current?.toSVG,
getRTL(),
opts,
);
};

private _getMinMaxOfYAxis = (points: ILineChartPoints[], yAxisType: YAxisType, useSecondaryYScale: boolean) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { IChartTableProps } from './ChartTable.types';
import { IChartTableStyleProps, IChartTableStyles } from '../../index';
import { classNamesFunction, getRTL, initializeComponentRef } from '@fluentui/react/lib/Utilities';
import { IImageExportOptions } from '../../types/index';
import { toImage } from '../../utilities/image-export-utils';
import { exportChartsAsImage } from '../../utilities/image-export-utils';
import * as d3 from 'd3-color';
import { getColorContrast } from '../../utilities/colors';
import { ITheme } from '@fluentui/react';
Expand Down Expand Up @@ -62,7 +62,7 @@ export class ChartTableBase extends React.Component<IChartTableProps> {
this._isRTL = getRTL(props.theme);
}
public toImage = (opts?: IImageExportOptions): Promise<string> => {
return toImage(this._rootElem, undefined, this._isRTL, opts);
return exportChartsAsImage([{ container: this._rootElem }], undefined, this._isRTL, opts);
};

// eslint-disable-next-line @typescript-eslint/no-deprecated
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/naming-convention */
import * as React from 'react';
import { useTheme } from '@fluentui/react';
import { IRefObject } from '@fluentui/react/lib/Utilities';
import { getRTL, IRefObject } from '@fluentui/react/lib/Utilities';
import { AnnotationOnlyChart } from '../AnnotationOnlyChart/AnnotationOnlyChart';
import { DonutChart } from '../DonutChart/index';
import { VerticalStackedBarChart } from '../VerticalStackedBarChart/index';
Expand Down Expand Up @@ -49,8 +49,10 @@ import { ScatterChart } from '../ScatterChart/index';
import { ChartTable } from '../ChartTable/index';
import { FunnelChart } from '../FunnelChart/FunnelChart';
import { GanttChart } from '../GanttChart/index';
import { ILegendsProps, Legends } from '../Legends/index';
import { ILegendContainer, ILegendsProps, Legends } from '../Legends/index';
import type { JSXElement } from '@fluentui/utilities';
import { exportChartsAsImage } from '../../utilities/image-export-utils';
import { resolveCSSVariables } from '../../utilities/utilities';

const ResponsiveDonutChart = withResponsiveContainer(DonutChart);
const ResponsiveVerticalStackedBarChart = withResponsiveContainer(VerticalStackedBarChart);
Expand Down Expand Up @@ -319,7 +321,7 @@ const chartMap: ChartTypeMap = {
export const DeclarativeChart: React.FunctionComponent<DeclarativeChartProps> = React.forwardRef<
HTMLDivElement,
DeclarativeChartProps
>(({ colorwayType = 'default', ...props }, forwardedRef) => {
>(({ colorwayType = 'default', onSchemaChange, ...props }, forwardedRef) => {
const { plotlySchema } = sanitizeJson(props.chartSchema);
const chart: OutputChartType = mapFluentChart(plotlySchema);
if (!chart.isValid) {
Expand All @@ -345,20 +347,25 @@ export const DeclarativeChart: React.FunctionComponent<DeclarativeChartProps> =
const colorMap = useColorMapping();
const theme = useTheme();
const isDarkTheme = theme?.isInverted ?? false;
const chartRef = React.useRef<IChart>(null);
const chartRefs = React.useRef<{ compRef: IChart | null; row: number; col: number }[]>([]);
const isMultiPlot = React.useRef(false);
const legendsRef = React.useRef<ILegendContainer>(null);
const containerRef = React.useRef<HTMLDivElement>(null);

if (!isArrayOrTypedArray(selectedLegends)) {
selectedLegends = [];
}

const [activeLegends, setActiveLegends] = React.useState<string[]>(selectedLegends);
const onActiveLegendsChange = (keys: string[]) => {
setActiveLegends(keys);
if (props.onSchemaChange) {
props.onSchemaChange({ plotlySchema: { plotlyInput, selectedLegends: keys } });
}
};
const onActiveLegendsChange = React.useCallback(
(keys: string[]) => {
setActiveLegends(keys);
if (onSchemaChange) {
onSchemaChange({ plotlySchema: { plotlyInput, selectedLegends: keys } });
}
},
[onSchemaChange, plotlyInput],
);

React.useEffect(() => {
// eslint-disable-next-line @typescript-eslint/no-shadow
Expand All @@ -374,41 +381,53 @@ export const DeclarativeChart: React.FunctionComponent<DeclarativeChartProps> =
selectedLegends: activeLegends,
};

const baseCommonProps = {
componentRef: chartRef,
};

const interactiveCommonProps = {
...baseCommonProps,
legendProps: multiSelectLegendProps,
calloutProps: { layerProps: { eventBubblingEnabled: true } },
};

// eslint-disable-next-line @typescript-eslint/no-deprecated
function createLegends(legendProps: ILegendsProps): JSXElement {
// eslint-disable-next-line react/jsx-no-bind
return <Legends {...legendProps} selectedLegends={activeLegends} onChange={onActiveLegendsChange} />;
return (
<Legends {...legendProps} selectedLegends={activeLegends} onChange={onActiveLegendsChange} ref={legendsRef} />
);
}

const exportAsImage = React.useCallback(
(opts?: IImageExportOptions): Promise<string> => {
return new Promise((resolve, reject) => {
if (isMultiPlot.current) {
return reject(Error('Exporting multi plot charts as image is not supported'));
}
if (!chartRef.current || typeof chartRef.current.toImage !== 'function') {
return reject(Error('Chart cannot be exported as image'));
async (opts?: IImageExportOptions) => {
if (!containerRef.current) {
throw new Error('Container reference is null');
}

const imgExpOpts = {
background: resolveCSSVariables(containerRef.current, theme.semanticColors.bodyBackground),
scale: 5,
...opts,
};

if (!isMultiPlot.current) {
if (!chartRefs.current[0]?.compRef?.toImage) {
throw new Error('Chart cannot be exported as image');
}

chartRef.current
.toImage({
background: theme.semanticColors.bodyBackground,
scale: 5,
...opts,
})
.then(resolve)
.catch(reject);
});
return chartRefs.current[0].compRef.toImage(imgExpOpts);
}

return exportChartsAsImage(
chartRefs.current.map(item => ({
container: item.compRef?.chartContainer,
row: item.row,
col: item.col,
})),
legendsRef.current?.toSVG,
getRTL(),
{
background: resolveCSSVariables(containerRef.current, theme.semanticColors.bodyBackground),
scale: 5,
...opts,
},
);
},
[theme],
);
Expand Down Expand Up @@ -510,8 +529,9 @@ export const DeclarativeChart: React.FunctionComponent<DeclarativeChartProps> =
gridTemplateRows: gridProperties.templateRows,
gridTemplateColumns: gridProperties.templateColumns,
}}
ref={containerRef}
>
{Object.entries(groupedTraces).map(([xAxisKey, index]) => {
{Object.entries(groupedTraces).map(([xAxisKey, index], chartIdx) => {
const plotlyInputForGroup: PlotlySchema = {
...plotlyInputWithValidData,
data: index.map(idx => plotlyInputWithValidData.data[idx]),
Expand Down Expand Up @@ -541,7 +561,7 @@ export const DeclarativeChart: React.FunctionComponent<DeclarativeChartProps> =

const resolvedCommonProps = (
chartType === 'annotation'
? baseCommonProps
? {}
: {
...interactiveCommonProps,
xAxisAnnotation: cellProperties?.xAnnotation,
Expand All @@ -557,6 +577,13 @@ export const DeclarativeChart: React.FunctionComponent<DeclarativeChartProps> =
...resolvedCommonProps,
xAxisAnnotation: cellProperties?.xAnnotation,
yAxisAnnotation: cellProperties?.yAnnotation,
componentRef: (ref: IChart | null) => {
chartRefs.current[chartIdx] = {
compRef: ref,
row: cellProperties?.row ?? 1,
col: cellProperties?.column ?? 1,
};
},
},
cellProperties?.row ?? 1,
cellProperties?.column ?? 1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
} from '../../utilities/index';
import { formatToLocaleString } from '@fluentui/chart-utilities';
import { IChart, IImageExportOptions } from '../../types/index';
import { toImage } from '../../utilities/image-export-utils';
import { exportChartsAsImage } from '../../utilities/image-export-utils';
import { ILegendContainer } from '../Legends/index';
import type { JSXElement } from '@fluentui/utilities';

Expand Down Expand Up @@ -234,7 +234,12 @@ export class DonutChartBase extends React.Component<IDonutChartProps, IDonutChar
}

public toImage = (opts?: IImageExportOptions): Promise<string> => {
return toImage(this._rootElem, this._legendsRef.current?.toSVG, getRTL(), opts);
return exportChartsAsImage(
[{ container: this._rootElem }],
this.props.hideLegend ? undefined : this._legendsRef.current?.toSVG,
getRTL(),
opts,
);
};

private _closeCallout = () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { Callout, DirectionalHint } from '@fluentui/react/lib/Callout';
import { ChartHoverCard } from '../../utilities/ChartHoverCard/ChartHoverCard';
import { formatToLocaleString } from '@fluentui/chart-utilities';
import { IImageExportOptions } from '../../types/index';
import { toImage as convertToImage } from '../../utilities/image-export-utils';
import { exportChartsAsImage } from '../../utilities/image-export-utils';
import { getContrastTextColor } from '../../utilities/utilities';
import {
getHorizontalFunnelSegmentGeometry,
Expand Down Expand Up @@ -53,10 +53,15 @@ export const FunnelChartBase: React.FunctionComponent<IFunnelChartProps> = React
() => ({
chartContainer: chartContainerRef ?? null,
toImage: (opts?: IImageExportOptions): Promise<string> => {
return convertToImage(chartContainerRef.current, legendsRef.current?.toSVG, getRTL(), opts);
return exportChartsAsImage(
[{ container: chartContainerRef.current }],
props.hideLegend ? undefined : legendsRef.current?.toSVG,
getRTL(),
opts,
);
},
}),
[],
[props.hideLegend],
);

function _handleHover(data: IFunnelChartDataPoint, mouseEvent: React.MouseEvent<SVGElement>) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
getScalePadding,
getDateFormatLevel,
} from '../../utilities/index';
import { toImage } from '../../utilities/image-export-utils';
import { exportChartsAsImage } from '../../utilities/image-export-utils';
import { formatDateToLocaleString, getMultiLevelDateTimeFormatOptions } from '@fluentui/chart-utilities';
import type { JSXElement } from '@fluentui/utilities';

Expand Down Expand Up @@ -73,10 +73,15 @@ export const GanttChartBase: React.FunctionComponent<IGanttChartProps> = React.f
() => ({
chartContainer: _cartesianChartRef.current?.chartContainer ?? null,
toImage: (opts?: IImageExportOptions): Promise<string> => {
return toImage(_cartesianChartRef.current?.chartContainer, _legendsRef.current?.toSVG, getRTL(), opts);
return exportChartsAsImage(
[{ container: _cartesianChartRef.current?.chartContainer }],
props.hideLegend ? undefined : _legendsRef.current?.toSVG,
getRTL(),
opts,
);
},
}),
[],
[props.hideLegend],
);

const _points = React.useMemo(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { IYValueHover } from '../../index';
import { SVGTooltipText } from '../../utilities/SVGTooltipText';
import { select as d3Select } from 'd3-selection';
import { IChart, IImageExportOptions } from '../../types/index';
import { toImage } from '../../utilities/image-export-utils';
import { exportChartsAsImage } from '../../utilities/image-export-utils';

const GAUGE_MARGIN = 16;
const LABEL_WIDTH = 36;
Expand Down Expand Up @@ -364,7 +364,12 @@ export class GaugeChartBase extends React.Component<IGaugeChartProps, IGaugeChar
}

public toImage = (opts?: IImageExportOptions): Promise<string> => {
return toImage(this._rootElem, this._legendsRef.current?.toSVG, this._isRTL, opts);
return exportChartsAsImage(
[{ container: this._rootElem }],
this.props.hideLegend ? undefined : this._legendsRef.current?.toSVG,
this._isRTL,
opts,
);
};

private _getMargins = () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ import {
IYValueHover,
} from '../../index';
import { IChart, IImageExportOptions, IBarSeries, ILineSeries } from '../../types/index';
import { toImage } from '../../utilities/image-export-utils';
import { exportChartsAsImage } from '../../utilities/image-export-utils';
import { ILegendContainer } from '../Legends/index';
import { rgb } from 'd3-color';
import type { JSXElement } from '@fluentui/utilities';
Expand Down Expand Up @@ -288,7 +288,12 @@ export class GroupedVerticalBarChartBase
}

public toImage = (opts?: IImageExportOptions): Promise<string> => {
return toImage(this._cartesianChartRef.current?.chartContainer, this._legendsRef.current?.toSVG, this._isRtl, opts);
return exportChartsAsImage(
[{ container: this._cartesianChartRef.current?.chartContainer }],
this.props.hideLegend ? undefined : this._legendsRef.current?.toSVG,
this._isRtl,
opts,
);
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import {
import { Target } from '@fluentui/react';
import { format as d3Format } from 'd3-format';
import { timeFormat as d3TimeFormat } from 'd3-time-format';
import { toImage } from '../../utilities/image-export-utils';
import { exportChartsAsImage } from '../../utilities/image-export-utils';
import type { JSXElement } from '@fluentui/utilities';

type DataSet = {
Expand Down Expand Up @@ -282,7 +282,12 @@ export class HeatMapChartBase extends React.Component<IHeatMapChartProps, IHeatM
}

public toImage = (opts?: IImageExportOptions): Promise<string> => {
return toImage(this._cartesianChartRef.current?.chartContainer, this._legendsRef.current?.toSVG, getRTL(), opts);
return exportChartsAsImage(
[{ container: this._cartesianChartRef.current?.chartContainer }],
this.props.hideLegend ? undefined : this._legendsRef.current?.toSVG,
getRTL(),
opts,
);
};

private _getMinMaxOfYAxis = () => {
Expand Down
Loading