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

feature: export chart to data url #1903

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { isDate } from '../utils/types';
import { Color } from '../utils/color-sets';
import { ScaleType } from './types/scale-type.enum';
import { ViewDimensions } from './types/view-dimension.interface';
import { toCanvas, toJpeg, toPng, toSvg } from '@swimlane/ngx-charts/utils/data-url';

@Component({
selector: 'base-chart',
Expand Down Expand Up @@ -209,4 +210,60 @@ export class BaseChartComponent implements OnChanges, AfterViewInit, OnDestroy,

return results;
}

get chartEl() {
return this.chartElement;
}

toDataURL<T extends 'png' | 'jpg' | 'svg'>(
options: {
type?: T;
canvasOptions?: { pixelRatio?: number; transparentBackground?: boolean; width?: number; height?: number };
} = {}
): Promise<string> {
if (this.getContainerDims() === null) {
// If not browser
return null;
}

const chartEl = this.chartElement.nativeElement.firstElementChild;

let [width, height] = [this.width, this.height];
if (isPlatformBrowser(this.platformId)) {
const rect = this.chartElement.nativeElement.getBoundingClientRect();
width = rect.width;
height = rect.height;
}
const ops = {
width,
height,
...(options.canvasOptions || {})
};
const { type } = options;
if (type === 'svg') {
return toSvg(chartEl, ops);
}
if (type === 'png') {
return toPng(chartEl, ops);
}
if (type === 'jpg') {
return toJpeg(chartEl, ops);
}
eric-gitta-moore marked this conversation as resolved.
Show resolved Hide resolved

throw new Error(`Unable to convert to type ${type}`);
}

toCanvas(options: { pixelRatio?: number; transparentBackground?: boolean }): Promise<HTMLCanvasElement> {
if (this.getContainerDims() === null) {
// If not browser
return null;
}

const ops = {
width: this.width,
height: this.height,
...options
};
return toCanvas(this.chartElement.nativeElement.firstElementChild, ops);
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
.advanced-pie-legend {
float: left;
position: relative;
top: 50%;
transform: translate(0, -50%);
height: 100%;
display: flex;
align-items: flex-start;
flex-direction: column;
justify-content: center;

.total-value {
font-size: 36px;
Expand Down
111 changes: 111 additions & 0 deletions projects/swimlane/ngx-charts/src/lib/utils/data-url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
export interface Options {
width: number;
height: number;
pixelRatio?: number;
transparentBackground?: boolean;
}

export interface StyleAble extends Element {
style: CSSStyleDeclaration;
}

function isType<T>(obj: unknown, test: (...args: any[]) => boolean): obj is T {
return test(obj);
}

export function cloneNodeWithStyle<T extends Element>(originNode: T): T {
const clonedNode = originNode.cloneNode(false) as T;

if (isType<StyleAble>(clonedNode, e => e?.style)) {
const computedStyle = window.getComputedStyle(originNode);
const styleText = Array.from(computedStyle)
.map(e => `${e}:${computedStyle.getPropertyValue(e)}`)
.join(';');
clonedNode.style.cssText = styleText;
}

if (!(originNode instanceof Element)) return clonedNode;
const children = Array.from(originNode.childNodes).map(cloneNodeWithStyle);
clonedNode.append(...children.filter(e => !!e));
return clonedNode;
}

export function svgToDataURL(svg: SVGElement): Promise<string> {
return Promise.resolve(new XMLSerializer().serializeToString(svg))
.then(encodeURIComponent)
.then(html => `data:image/svg+xml;charset=utf-8,${html}`);
}

export function nodeToDataURL(node: Element, options: Options): Promise<string> {
const { width, height } = options;
const xmlns = 'http://www.w3.org/2000/svg';
const svg = document.createElementNS(xmlns, 'svg');
const foreignObject = document.createElementNS(xmlns, 'foreignObject');

svg.setAttribute('width', `${width}`);
svg.setAttribute('height', `${height}`);
svg.setAttribute('viewBox', `0 0 ${width} ${height}`);

foreignObject.setAttribute('width', '100%');
foreignObject.setAttribute('height', '100%');
foreignObject.setAttribute('x', '0');
foreignObject.setAttribute('y', '0');
foreignObject.setAttribute('externalResourcesRequired', 'true');

svg.appendChild(foreignObject);
foreignObject.appendChild(node);
return svgToDataURL(svg);
}

export async function toSvg<T extends HTMLElement>(node: T, options: Options): Promise<string> {
return nodeToDataURL(cloneNodeWithStyle(node), options);
}

export async function toPng<T extends HTMLElement>(node: T, options: Options): Promise<string> {
const canvas = await toCanvas(node, options);
return canvas.toDataURL();
}

export async function toJpeg<T extends HTMLElement>(node: T, options: Options): Promise<string> {
const canvas = await toCanvas(node, options);
return canvas.toDataURL('image/jpeg');
}

export function createImage(url: string): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.crossOrigin = 'anonymous';
img.decoding = 'async';
img.src = url;
img.decode().then(() => resolve(img));
});
}

export async function toCanvas<T extends HTMLElement>(node: T, options: Options): Promise<HTMLCanvasElement> {
const svg = await toSvg(node, options);
const img = await createImage(svg);

const canvas = document.createElement('canvas');
const context = canvas.getContext('2d')!;
const ratio = options.pixelRatio || window.devicePixelRatio;
const canvasWidth = options.width;
const canvasHeight = options.height;

canvas.width = canvasWidth * ratio;
canvas.height = canvasHeight * ratio;

canvas.style.width = `${canvasWidth}`;
canvas.style.height = `${canvasHeight}`;

if (!options.transparentBackground) {
context.clearRect(0, 0, canvas.width, canvas.height);
context.fillStyle = '#fff';
context.fillRect(0, 0, canvas.width, canvas.height);
}

context.drawImage(img, 0, 0, canvas.width, canvas.height);

return canvas;
}