From d0cf03281d2315de0144f36e22fb8c4e83d8bced Mon Sep 17 00:00:00 2001 From: krkshitij <110246001+krkshitij@users.noreply.github.com> Date: Wed, 22 Jan 2025 05:42:38 +0000 Subject: [PATCH] export legend data from charts --- .../components/AreaChart/AreaChart.base.tsx | 24 +++++-- .../CommonComponents/CartesianChart.base.tsx | 2 +- .../DeclarativeChart/DeclarativeChart.tsx | 2 +- .../DeclarativeChart/imageExporter.ts | 72 ++++++++----------- .../components/DonutChart/DonutChart.base.tsx | 19 +++-- .../components/GaugeChart/GaugeChart.base.tsx | 20 ++++-- .../GroupedVerticalBarChart.base.tsx | 16 ++++- .../HeatMapChart/HeatMapChart.base.tsx | 19 ++++- .../HorizontalBarChartWithAxis.base.tsx | 14 +++- .../components/LineChart/LineChart.base.tsx | 18 ++++- .../SankeyChart/SankeyChart.base.tsx | 5 +- .../VerticalBarChart.base.tsx | 13 +++- .../VerticalStackedBarChart.base.tsx | 27 ++++--- .../react-charting/src/types/IDataPoint.ts | 3 +- 14 files changed, 173 insertions(+), 81 deletions(-) diff --git a/packages/charts/react-charting/src/components/AreaChart/AreaChart.base.tsx b/packages/charts/react-charting/src/components/AreaChart/AreaChart.base.tsx index d4411e51c3d67..2c5bfdde7eac3 100644 --- a/packages/charts/react-charting/src/components/AreaChart/AreaChart.base.tsx +++ b/packages/charts/react-charting/src/components/AreaChart/AreaChart.base.tsx @@ -129,6 +129,7 @@ export class AreaChartBase extends React.Component; + private _points:ILineChartPoints[]=[] public constructor(props: IAreaChartProps) { super(props); @@ -183,14 +184,14 @@ export class AreaChartBase extends React.Component { + private _getLegendData = (points: ILineChartPoints[]): ILegend[] => { const data = points; const actions: ILegend[] = []; @@ -651,6 +656,13 @@ export class AreaChartBase extends React.Component { + const actions = this._getLegendData(points); + return ( - implements IChart + implements Omit { public chartContainer: HTMLDivElement; private _classNames: IProcessedStyleSet; diff --git a/packages/charts/react-charting/src/components/DeclarativeChart/DeclarativeChart.tsx b/packages/charts/react-charting/src/components/DeclarativeChart/DeclarativeChart.tsx index 728f5ddfbbf79..74d760794a197 100644 --- a/packages/charts/react-charting/src/components/DeclarativeChart/DeclarativeChart.tsx +++ b/packages/charts/react-charting/src/components/DeclarativeChart/DeclarativeChart.tsx @@ -177,7 +177,7 @@ export const DeclarativeChart: React.FunctionComponent = const exportAsImage = React.useCallback( (opts?: IImageExportOptions) => { - return toImage(chartRef.current?.chartContainer, { + return toImage(chartRef.current?.chartContainer, chartRef.current?.legends, { background: theme.semanticColors.bodyBackground, scale: 3, ...opts, diff --git a/packages/charts/react-charting/src/components/DeclarativeChart/imageExporter.ts b/packages/charts/react-charting/src/components/DeclarativeChart/imageExporter.ts index cab5167bf6ccb..f94838bec8196 100644 --- a/packages/charts/react-charting/src/components/DeclarativeChart/imageExporter.ts +++ b/packages/charts/react-charting/src/components/DeclarativeChart/imageExporter.ts @@ -1,4 +1,6 @@ import { create as d3Create, select as d3Select, Selection } from 'd3-selection'; +import { ILegend } from '../Legends/Legends.types'; +import { calculateLongestLabelWidth } from '../../utilities/utilities'; /** * {@docCategory DeclarativeChart} @@ -10,7 +12,11 @@ export interface IImageExportOptions { background?: string; } -export function toImage(chartContainer?: HTMLElement | null, opts: IImageExportOptions = {}): Promise { +export function toImage( + chartContainer?: HTMLElement | null, + legends?: ILegend[], + opts: IImageExportOptions = {}, +): Promise { return new Promise((resolve, reject) => { if (!chartContainer) { return reject(new Error('Chart container is not defined')); @@ -19,7 +25,7 @@ export function toImage(chartContainer?: HTMLElement | null, opts: IImageExportO try { const background = typeof opts.background === 'string' ? resolveCSSVariables(chartContainer, opts.background) : 'transparent'; - const svg = toSVG(chartContainer, background); + const svg = toSVG(chartContainer, legends, background); const svgData = new XMLSerializer().serializeToString(svg.node); const svgDataUrl = 'data:image/svg+xml;base64,' + btoa(unescapePonyfill(encodeURIComponent(svgData))); @@ -37,7 +43,7 @@ export function toImage(chartContainer?: HTMLElement | null, opts: IImageExportO }); } -function toSVG(chartContainer: HTMLElement, background: string) { +function toSVG(chartContainer: HTMLElement, legends: ILegend[] | undefined, background: string) { const svg = chartContainer.querySelector('svg'); if (!svg) { throw new Error('SVG not found'); @@ -45,7 +51,7 @@ function toSVG(chartContainer: HTMLElement, background: string) { const { width: svgWidth, height: svgHeight } = svg.getBoundingClientRect(); const classNames = new Set(); - const legendGroup = cloneLegendsToSVG(chartContainer, svgWidth, svgHeight, classNames); + const legendGroup = cloneLegendsToSVG(legends, svgWidth, svgHeight, classNames); const w1 = Math.max(svgWidth, legendGroup.width); const h1 = svgHeight + legendGroup.height; const clonedSvg = d3Select(svg.cloneNode(true) as SVGSVGElement) @@ -102,13 +108,13 @@ function toSVG(chartContainer: HTMLElement, background: string) { }; } -function cloneLegendsToSVG(chartContainer: HTMLElement, svgWidth: number, svgHeight: number, classNames: Set) { - const legendButtons = chartContainer.querySelectorAll(` - button[class^="legend-"], - [class^="legendContainer-"] div[class^="overflowIndicationTextStyle-"], - [class^="legendsContainer-"] div[class^="overflowIndicationTextStyle-"] - `); - if (legendButtons.length === 0) { +function cloneLegendsToSVG( + legends: ILegend[] | undefined, + svgWidth: number, + svgHeight: number, + classNames: Set, +) { + if (!legends || legends.length === 0) { return { node: null, width: 0, @@ -123,8 +129,9 @@ function cloneLegendsToSVG(chartContainer: HTMLElement, svgWidth: number, svgHei const legendLines: (typeof legendLine)[] = []; const legendLineWidths: number[] = []; - for (let i = 0; i < legendButtons.length; i++) { - const { width: legendWidth } = legendButtons[i].getBoundingClientRect(); + for (let i = 0; i < legends.length; i++) { + const textOffset = 28; + const legendWidth = calculateLongestLabelWidth([legends[i].title]) + textOffset; const legendItem = legendGroup.append('g'); legendLine.push(legendItem); @@ -138,40 +145,23 @@ function cloneLegendsToSVG(chartContainer: HTMLElement, svgWidth: number, svgHei legendY += 32; } - let legendText: HTMLDivElement | null; - let textOffset = 0; - - if (legendButtons[i].tagName.toLowerCase() === 'button') { - const legendRect = legendButtons[i].querySelector('[class^="rect"]'); - const { backgroundColor: legendColor, borderColor: legendBorderColor } = getComputedStyle(legendRect!); - - legendText = legendButtons[i].querySelector('[class^="text"]'); - legendText!.classList.forEach(className => classNames.add(`.${className}`)); - legendItem - .append('rect') - .attr('x', legendX + 8) - .attr('y', svgHeight + legendY + 8) - .attr('width', 12) - .attr('height', 12) - .attr('fill', legendColor) - .attr('stroke-width', 1) - .attr('stroke', legendBorderColor); - textOffset = 28; - } else { - legendText = legendButtons[i] as HTMLDivElement; - legendText.classList.forEach(className => classNames.add(`.${className}`)); - textOffset = 8; - } + legendItem + .append('rect') + .attr('x', legendX + 8) + .attr('y', svgHeight + legendY + 8) + .attr('width', 12) + .attr('height', 12) + .attr('fill', legends[i].color) + .attr('stroke-width', 1) + .attr('stroke', legends[i].color); - const { color: textColor } = getComputedStyle(legendText!); legendItem .append('text') .attr('x', legendX + textOffset) .attr('y', svgHeight + legendY + 8) .attr('dominant-baseline', 'hanging') - .attr('class', legendText!.getAttribute('class')) - .attr('fill', textColor) - .text(legendText!.textContent); + .attr('fill', 'black') + .text(legends[i].title); legendX += legendWidth; } diff --git a/packages/charts/react-charting/src/components/DonutChart/DonutChart.base.tsx b/packages/charts/react-charting/src/components/DonutChart/DonutChart.base.tsx index 5fc0da0219250..e33f0567b7418 100644 --- a/packages/charts/react-charting/src/components/DonutChart/DonutChart.base.tsx +++ b/packages/charts/react-charting/src/components/DonutChart/DonutChart.base.tsx @@ -50,6 +50,7 @@ export class DonutChartBase extends React.Component, @@ -113,7 +114,7 @@ export class DonutChartBase extends React.Component d.data! >= 0)); + const chartData = this._elevateToMinimums(this._points.filter((d: IChartDataPoint) => d.data! >= 0)); const valueInsideDonut = this.props.innerRadius !== 0 ? this._valueInsideDonut(this.props.valueInsideDonut!, chartData!) : ''; return !this._isChartEmpty() ? ( @@ -212,6 +213,10 @@ export class DonutChartBase extends React.Component { this.setState({ showHover: false, @@ -254,7 +259,7 @@ export class DonutChartBase extends React.Component { const color: string = this.props.enableGradient ? point.gradient?.[0] || getNextGradient(index, 0, this.props.theme?.isInverted)[0] @@ -275,6 +280,12 @@ export class DonutChartBase extends React.Component { const { hideMinMax, chartTitle, sublabel } = this.props; @@ -484,11 +488,7 @@ export class GaugeChartBase extends React.Component { - if (this.props.hideLegend) { - return null; - } - + private _getLegendData = () => { const legends: ILegend[] = this._segments.map((segment, index) => { const color: string = this.props.enableGradient ? segment.gradient?.[0] || getNextGradient(index, 0, this.props.theme!.isInverted)[0] @@ -506,6 +506,16 @@ export class GaugeChartBase extends React.Component { + if (this.props.hideLegend) { + return null; + } + + const legends = this._getLegendData() + return (
{ return { startValue: 0, endValue: 0 }; }; @@ -596,7 +600,7 @@ export class GroupedVerticalBarChartBase }); } - private _getLegendData = (points: IGroupedVerticalBarChartData[], palette: IPalette): JSX.Element => { + private _getLegendData = (points: IGroupedVerticalBarChartData[], palette: IPalette): ILegend[] => { const data = points; const defaultPalette: string[] = [palette.blueLight, palette.blue, palette.blueMid, palette.red, palette.black]; const actions: ILegend[] = []; @@ -627,6 +631,14 @@ export class GroupedVerticalBarChartBase actions.push(legend); }); }); + + return actions; + }; + + private _renderLegends = (points: IGroupedVerticalBarChartData[], palette: IPalette): JSX.Element => { + const data = points; + const actions = this._getLegendData(data, palette); + return ( { return { startValue: 0, endValue: 0 }; }; @@ -486,8 +490,9 @@ export class HeatMapChartBase extends React.Component { - const { data, legendProps } = this.props; + + private _getLegendData = (): ILegend[] => { + const { data } = this.props; const legends: ILegend[] = []; data.forEach((item: IHeatMapChartData) => { const legend: ILegend = { @@ -506,6 +511,14 @@ export class HeatMapChartBase extends React.Component { + const { legendProps } = this.props; + const legends = this._getLegendData(); + return ; }; diff --git a/packages/charts/react-charting/src/components/HorizontalBarChartWithAxis/HorizontalBarChartWithAxis.base.tsx b/packages/charts/react-charting/src/components/HorizontalBarChartWithAxis/HorizontalBarChartWithAxis.base.tsx index 9bd1d116c7bc8..1ab8183a88194 100644 --- a/packages/charts/react-charting/src/components/HorizontalBarChartWithAxis/HorizontalBarChartWithAxis.base.tsx +++ b/packages/charts/react-charting/src/components/HorizontalBarChartWithAxis/HorizontalBarChartWithAxis.base.tsx @@ -134,7 +134,7 @@ export class HorizontalBarChartWithAxisBase d3Max(this._points, (point: IHorizontalBarChartWithAxisDataPoint) => point.x)!, this.props.xMaxValue || 0, ); - const legendBars: JSX.Element = this._getLegendData(this._points, this.props.theme!.palette); + const legendBars: JSX.Element = this._renderLegends(this._points, this.props.theme!.palette); this._classNames = getClassNames(this.props.styles!, { theme: this.props.theme!, legendColor: this.state.color, @@ -199,6 +199,10 @@ export class HorizontalBarChartWithAxisBase return this._cartesianChartRef.current?.chartContainer || null; } + public get legends(): ILegend[] { + return this._getLegendData(this._points, this.props.theme!.palette); + } + private _getDomainNRangeValues = ( points: IHorizontalBarChartWithAxisDataPoint[], margins: IMargins, @@ -740,7 +744,7 @@ export class HorizontalBarChartWithAxisBase } } - private _getLegendData = (data: IHorizontalBarChartWithAxisDataPoint[], palette: IPalette): JSX.Element => { + private _getLegendData = (data: IHorizontalBarChartWithAxisDataPoint[], palette: IPalette): ILegend[] => { const { useSingleColor } = this.props; const actions: ILegend[] = []; @@ -772,6 +776,12 @@ export class HorizontalBarChartWithAxisBase }; actions.push(legend); }); + + return actions; + }; + + private _renderLegends = (data: IHorizontalBarChartWithAxisDataPoint[], palette: IPalette): JSX.Element => { + const actions = this._getLegendData(data, palette); const legends = ( this._createLegends(data)); + this._createLegendsMemoized = memoizeFunction((data: LineChartDataWithIndex[]) => this._renderLegends(data)); this._firstRenderOptimization = true; this._emptyChartId = getId('_LineChart_empty'); this._cartesianChartRef = React.createRef(); @@ -379,6 +379,10 @@ export class LineChartBase extends React.Component { @@ -546,9 +550,17 @@ export class LineChartBase extends React.Component implements IChart { +export class SankeyChartBase + extends React.Component + implements Omit +{ public static defaultProps: Partial = { enableReflow: true, }; diff --git a/packages/charts/react-charting/src/components/VerticalBarChart/VerticalBarChart.base.tsx b/packages/charts/react-charting/src/components/VerticalBarChart/VerticalBarChart.base.tsx index 484461c1a1cab..eb450227aa3f8 100644 --- a/packages/charts/react-charting/src/components/VerticalBarChart/VerticalBarChart.base.tsx +++ b/packages/charts/react-charting/src/components/VerticalBarChart/VerticalBarChart.base.tsx @@ -162,7 +162,7 @@ export class VerticalBarChartBase d3Min(this._points, (point: IVerticalBarChartDataPoint) => point.y)!, this.props.yMinValue || 0, ); - const legendBars: JSX.Element = this._getLegendData(this._points, this.props.theme!.palette); + const legendBars: JSX.Element = this._renderLegends(this._points, this.props.theme!.palette); this._classNames = getClassNames(this.props.styles!, { theme: this.props.theme!, legendColor: this.state.color, @@ -256,6 +256,10 @@ export class VerticalBarChartBase return this._cartesianChartRef.current?.chartContainer || null; } + public get legends(): ILegend[] { + return this._getLegendData(this._points, this.props.theme!.palette); + } + private _getDomainNRangeValues = ( points: IDataPoint[], margins: IMargins, @@ -1081,7 +1085,7 @@ export class VerticalBarChartBase }); } - private _getLegendData = (data: IVerticalBarChartDataPoint[], palette: IPalette): JSX.Element => { + private _getLegendData = (data: IVerticalBarChartDataPoint[], palette: IPalette): ILegend[] => { const { theme, useSingleColor } = this.props; const { lineLegendText, lineLegendColor = theme!.palette.yellow } = this.props; const actions: ILegend[] = []; @@ -1129,6 +1133,11 @@ export class VerticalBarChartBase }; actions.unshift(lineLegend); } + return actions; + }; + + private _renderLegends = (data: IVerticalBarChartDataPoint[], palette: IPalette): JSX.Element => { + const actions = this._getLegendData(data, palette); const legends = ( ; - } + private _getLegendData(data: IVerticalStackedChartProps[], palette: IPalette, lineLegends: LineLegends[]): ILegend[] { const defaultPalette: string[] = [palette.blueLight, palette.blue, palette.blueMid, palette.red, palette.black]; const actions: ILegend[] = []; const { allowHoverOnLegend = true, theme } = this.props; @@ -650,6 +647,18 @@ export class VerticalStackedBarChartBase }); } const totalLegends: ILegend[] = legendsOfLine.concat(actions); + return totalLegends; + } + + private _renderLegends( + data: IVerticalStackedChartProps[], + palette: IPalette, + lineLegends: LineLegends[], + ): JSX.Element { + if (this.props.hideLegend) { + return <>; + } + const totalLegends = this._getLegendData(data, palette, lineLegends); return (