Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[POC] Export legend data from charts #33712

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
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
Expand Up @@ -129,6 +129,7 @@ export class AreaChartBase extends React.Component<IAreaChartProps, IAreaChartSt
private _firstRenderOptimization: boolean;
private _emptyChartId: string;
private _cartesianChartRef: React.RefObject<IChart>;
private _points:ILineChartPoints[]=[]

public constructor(props: IAreaChartProps) {
super(props);
Expand Down Expand Up @@ -183,14 +184,14 @@ export class AreaChartBase extends React.Component<IAreaChartProps, IAreaChartSt
public render(): JSX.Element {
if (!this._isChartEmpty()) {
const { lineChartData } = this.props.data;
const points = this._addDefaultColors(lineChartData);
const { colors, opacity, data, calloutPoints } = this._createSet(points);
this._points = this._addDefaultColors(lineChartData);
const { colors, opacity, data, calloutPoints } = this._createSet(this._points);
this._calloutPoints = calloutPoints;
const isXAxisDateType = getXAxisType(points);
const isXAxisDateType = getXAxisType(this._points);
this._colors = colors;
this._opacity = opacity;
this._data = data.renderData;
const legends: JSX.Element = this._getLegendData(points);
const legends: JSX.Element = this._renderLegends(this._points);

const tickParams = {
tickValues: this.props.tickValues,
Expand All @@ -215,7 +216,7 @@ export class AreaChartBase extends React.Component<IAreaChartProps, IAreaChartSt
<CartesianChart
{...this.props}
chartTitle={this._getChartTitle()}
points={points}
points={this._points}
chartType={ChartTypes.AreaChart}
calloutProps={calloutProps}
legendBars={legends}
Expand Down Expand Up @@ -274,6 +275,10 @@ export class AreaChartBase extends React.Component<IAreaChartProps, IAreaChartSt
return this._cartesianChartRef.current?.chartContainer || null;
}

public get legends(): ILegend[] {
return this._getLegendData(this._points);
}

private _getDomainNRangeValues = (
points: ILineChartPoints[],
margins: IMargins,
Expand Down Expand Up @@ -624,7 +629,7 @@ export class AreaChartBase extends React.Component<IAreaChartProps, IAreaChartSt
});
}

private _getLegendData = (points: ILineChartPoints[]): JSX.Element => {
private _getLegendData = (points: ILineChartPoints[]): ILegend[] => {
const data = points;
const actions: ILegend[] = [];

Expand All @@ -651,6 +656,13 @@ export class AreaChartBase extends React.Component<IAreaChartProps, IAreaChartSt

actions.push(legend);
});

return actions;
};

private _renderLegends = (points: ILineChartPoints[]): JSX.Element => {
const actions = this._getLegendData(points);

return (
<Legends
legends={actions}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export interface ICartesianChartState {
*/
export class CartesianChartBase
extends React.Component<IModifiedCartesianChartProps, ICartesianChartState>
implements IChart
implements Omit<IChart, 'legends'>
{
public chartContainer: HTMLDivElement;
private _classNames: IProcessedStyleSet<ICartesianChartStyles>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ export const DeclarativeChart: React.FunctionComponent<DeclarativeChartProps> =

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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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}
Expand All @@ -10,7 +12,11 @@ export interface IImageExportOptions {
background?: string;
}

export function toImage(chartContainer?: HTMLElement | null, opts: IImageExportOptions = {}): Promise<string> {
export function toImage(
chartContainer?: HTMLElement | null,
legends?: ILegend[],
opts: IImageExportOptions = {},
): Promise<string> {
return new Promise((resolve, reject) => {
if (!chartContainer) {
return reject(new Error('Chart container is not defined'));
Expand All @@ -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)));
Expand All @@ -37,15 +43,15 @@ 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<SVGSVGElement>('svg');
if (!svg) {
throw new Error('SVG not found');
}

const { width: svgWidth, height: svgHeight } = svg.getBoundingClientRect();
const classNames = new Set<string>();
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)
Expand Down Expand Up @@ -102,13 +108,13 @@ function toSVG(chartContainer: HTMLElement, background: string) {
};
}

function cloneLegendsToSVG(chartContainer: HTMLElement, svgWidth: number, svgHeight: number, classNames: Set<string>) {
const legendButtons = chartContainer.querySelectorAll<HTMLElement>(`
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<string>,
) {
if (!legends || legends.length === 0) {
return {
node: null,
width: 0,
Expand All @@ -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);
Expand All @@ -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<HTMLDivElement>('[class^="rect"]');
const { backgroundColor: legendColor, borderColor: legendBorderColor } = getComputedStyle(legendRect!);

legendText = legendButtons[i].querySelector<HTMLDivElement>('[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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export class DonutChartBase extends React.Component<IDonutChartProps, IDonutChar
private _calloutId: string;
private _calloutAnchorPoint: IChartDataPoint | null;
private _emptyChartId: string | null;
private _points: IChartDataPoint[] = [];

public static getDerivedStateFromProps(
nextProps: Readonly<IDonutChartProps>,
Expand Down Expand Up @@ -113,7 +114,7 @@ export class DonutChartBase extends React.Component<IDonutChartProps, IDonutChar

public render(): JSX.Element {
const { data, hideLegend = false } = this.props;
const points = this._addDefaultColors(data?.chartData);
this._points = this._addDefaultColors(data?.chartData);

this._classNames = getClassNames(this.props.styles!, {
theme: this.props.theme!,
Expand All @@ -123,12 +124,12 @@ export class DonutChartBase extends React.Component<IDonutChartProps, IDonutChar
className: this.props.className!,
});

const legendBars = this._createLegends(points);
const legendBars = this._renderLegends(this._points);
const donutMarginHorizontal = this.props.hideLabels ? 0 : 80;
const donutMarginVertical = this.props.hideLabels ? 0 : 40;
const outerRadius =
Math.min(this.state._width! - donutMarginHorizontal, this.state._height! - donutMarginVertical) / 2;
const chartData = this._elevateToMinimums(points.filter((d: IChartDataPoint) => 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() ? (
Expand Down Expand Up @@ -212,6 +213,10 @@ export class DonutChartBase extends React.Component<IDonutChartProps, IDonutChar
return this._rootElem;
}

public get legends(): ILegend[] {
return this._getLegendData(this._points);
}

private _closeCallout = () => {
this.setState({
showHover: false,
Expand Down Expand Up @@ -254,7 +259,7 @@ export class DonutChartBase extends React.Component<IDonutChartProps, IDonutChar
node.setAttribute('viewBox', viewbox);
}

private _createLegends(chartData: IChartDataPoint[]): JSX.Element {
private _getLegendData(chartData: IChartDataPoint[]): ILegend[] {
const legendDataItems = chartData.map((point: IChartDataPoint, index: number) => {
const color: string = this.props.enableGradient
? point.gradient?.[0] || getNextGradient(index, 0, this.props.theme?.isInverted)[0]
Expand All @@ -275,6 +280,12 @@ export class DonutChartBase extends React.Component<IDonutChartProps, IDonutChar
return legend;
});

return legendDataItems;
}

private _renderLegends(chartData: IChartDataPoint[]): JSX.Element {
const legendDataItems = this._getLegendData(chartData);

const legends = (
<Legends
legends={legendDataItems}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,10 @@ export class GaugeChartBase extends React.Component<IGaugeChartProps, IGaugeChar
return this._rootElem;
}

public get legends(): ILegend[] {
return this._getLegendData();
}

private _getMargins = () => {
const { hideMinMax, chartTitle, sublabel } = this.props;

Expand Down Expand Up @@ -484,11 +488,7 @@ export class GaugeChartBase extends React.Component<IGaugeChartProps, IGaugeChar
);
};

private _renderLegends = () => {
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]
Expand All @@ -506,6 +506,16 @@ export class GaugeChartBase extends React.Component<IGaugeChartProps, IGaugeChar
};
});

return legends;
};

private _renderLegends = () => {
if (this.props.hideLegend) {
return null;
}

const legends = this._getLegendData()

return (
<div className={this._classNames.legendsContainer}>
<Legends
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ export class GroupedVerticalBarChartBase
this._xAxisLabels = xAxisLabels;
this._datasetForBars = datasetForBars;
this._xAxisType = getTypeOfAxis(points[0].name, true) as XAxisTypes;
const legends: JSX.Element = this._getLegendData(points, this.props.theme!.palette);
const legends: JSX.Element = this._renderLegends(points, this.props.theme!.palette);
this._adjustProps();

// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down Expand Up @@ -247,6 +247,10 @@ export class GroupedVerticalBarChartBase
return this._cartesianChartRef.current?.chartContainer || null;
}

public get legends(): ILegend[] {
return this._getLegendData(this.props.data, this.props.theme!.palette);
}

private _getMinMaxOfYAxis = () => {
return { startValue: 0, endValue: 0 };
};
Expand Down Expand Up @@ -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[] = [];
Expand Down Expand Up @@ -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 (
<Legends
legends={actions}
Expand Down
Loading
Loading