Skip to content

Commit

Permalink
feat(api): Enhance .export() option to specify size
Browse files Browse the repository at this point in the history
Make to accept option to specify in different size.

Close #815
Fix #1969
  • Loading branch information
netil authored Mar 19, 2021
1 parent c79aebb commit 3c2de80
Show file tree
Hide file tree
Showing 8 changed files with 126 additions and 34 deletions.
20 changes: 17 additions & 3 deletions demo/demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -4799,9 +4799,13 @@ d3.select(".chart_area")
options: {
data: {
columns: [
["sample", 30, 200, 100, 400, 150, 250]
["data1", 30, 200, 100, 400, 150, 250],
["data2", 5000, 2000, 1000, 4000, 1500, 2500]
],
type: "line"
types: {
data1: "bar",
data2: "area"
}
}
},
func: function(chart) {
Expand All @@ -4814,7 +4818,17 @@ d3.select(".chart_area")
.insertAdjacentElement("afterend", exported);

// Call after the chart finished rendering
chart.export("image/png", function(dataUrl) {
// (1) Default option
chart.export(null, function(dataUrl) {
// append an image element
var img = document.createElement("img");

img.src = dataUrl;
exported.appendChild(img);
});

// (2) Specify exported image size
chart.export({width: 500, height:100, preserveAspectRatio: false}, function(dataUrl) {
// append an image element
var img = document.createElement("img");

Expand Down
6 changes: 3 additions & 3 deletions src/Chart/api/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import {DataItem} from "../../../types/types";
import {extend, isUndefined, isArray} from "../../module/util";

type dataParam = {x: number, value: number, id: string, index: number}[];
type DataParam = {x: number, value: number, id: string, index: number}[];

/**
* Get data loaded in the chart.
Expand Down Expand Up @@ -173,7 +173,7 @@ extend(data, {
* chart.data.min();
* // --> [{x: 0, value: 30, id: "data1", index: 0}, ...]
*/
min: function(): dataParam {
min: function(): DataParam {
return this.internal.getMinMaxData().min;
},

Expand All @@ -188,7 +188,7 @@ extend(data, {
* chart.data.max();
* // --> [{x: 3, value: 400, id: "data1", index: 3}, ...]
*/
max: function(): dataParam {
max: function(): DataParam {
return this.internal.getMinMaxData().max;
}
});
Expand Down
58 changes: 47 additions & 11 deletions src/Chart/api/export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,17 @@
*/
import {namespaces as d3Namespaces} from "d3-selection";
import {document} from "../../module/browser";
import {isFunction, toArray, getCssRules} from "../../module/util";
import {isFunction, toArray, getCssRules, mergeObj} from "../../module/util";

type Size = {
width: number;
height: number;
};

type ExportOption = Size & {
mimeType: string;
preserveAspectRatio: boolean;
}

/**
* Encode to base64
Expand All @@ -21,11 +31,13 @@ const b64EncodeUnicode = (str: string): string => btoa(
/**
* Convert svg node to data url
* @param {HTMLElement} node target node
* @param {object} size object containing {width, height}
* @param {object} option object containing {width, height, preserveAspectRatio}
* @param {object} orgSize object containing {width, height}
* @returns {string}
* @private
*/
function nodeToSvgDataUrl(node, size) {
function nodeToSvgDataUrl(node, option: ExportOption, orgSize: Size) {
const {width, height} = option || orgSize;
const serializer = new XMLSerializer();
const clone = node.cloneNode(true);
const cssText = getCssRules(toArray(document.styleSheets))
Expand All @@ -45,7 +57,9 @@ function nodeToSvgDataUrl(node, size) {

// foreignObject not supported in IE11 and below
// https://msdn.microsoft.com/en-us/library/hh834675(v=vs.85).aspx
const dataStr = `<svg xmlns="${d3Namespaces.svg}" width="${size.width}" height="${size.height}">
const dataStr = `<svg xmlns="${d3Namespaces.svg}" width="${width}" height="${height}"
viewBox="0 0 ${orgSize.width} ${orgSize.height}"
preserveAspectRatio="${option && option.preserveAspectRatio === false ? "none" : "xMinYMid meet"}">
<foreignObject width="100%" height="100%">
${styleXml}
${nodeXml.replace(/(url\()[^#]+/g, "$1")}
Expand All @@ -64,15 +78,19 @@ export default {
* @function export
* @instance
* @memberof Chart
* @param {string} [mimeType=image/png] The desired output image format. (ex. 'image/png' for png, 'image/jpeg' for jpeg format)
* @param {object} option Export option
* @param {string} [option.mimeType="image/png"] The desired output image format. (ex. 'image/png' for png, 'image/jpeg' for jpeg format)
* @param {number} [option.width={currentWidth}] width
* @param {number} [option.height={currentHeigth}] height
* @param {boolean} [option.preserveAspectRatio=true] Preserve aspect ratio on given size
* @param {Function} [callback] The callback to be invoked when export is ready.
* @returns {string} dataURI
* @example
* chart.export();
* // --> "data:image/svg+xml;base64,PHN..."
*
* // Initialize the download automatically
* chart.export("image/png", dataUrl => {
* chart.export({mimeType: "image/png"}, dataUrl => {
* const link = document.createElement("a");
*
* link.download = `${Date.now()}.png`;
Expand All @@ -81,12 +99,30 @@ export default {
*
* document.body.appendChild(link);
* });
*
* // Resize the exported image
* chart.export(
* {
* width: 800,
* height: 600,
* preserveAspectRatio: false,
* mimeType: "image/png"
* },
* dataUrl => { ... }
* );
*/
export(mimeType?: string, callback?: (dataUrl: string) => void): string {
export(option?: ExportOption, callback?: (dataUrl: string) => void): string {
const $$ = this.internal;
const {state, $el: {chart}} = $$;
const {width, height} = state.current;
const svgDataUrl = nodeToSvgDataUrl(chart.node(), {width, height});
const opt = mergeObj({
width,
height,
preserveAspectRatio: true,
mimeType: "image/png"
}, option) as ExportOption;

const svgDataUrl = nodeToSvgDataUrl(chart.node(), opt, {width, height});

if (callback && isFunction(callback)) {
const img = new Image();
Expand All @@ -96,11 +132,11 @@ export default {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");

canvas.width = width;
canvas.height = height;
canvas.width = opt.width || width;
canvas.height = opt.height || height;
ctx.drawImage(img, 0, 0);

callback.bind(this)(canvas.toDataURL(mimeType));
callback.bind(this)(canvas.toDataURL(opt.mimeType));
};

img.src = svgDataUrl;
Expand Down
8 changes: 4 additions & 4 deletions src/Chart/api/focus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import {select as d3Select} from "d3-selection";
import CLASS from "../../config/classes";

type focusParam = string | string[];
type FocusParam = string | string[];

export default {
/**
Expand All @@ -25,7 +25,7 @@ export default {
* // all targets will be highlighted
* chart.focus();
*/
focus(targetIdsValue?: focusParam): void {
focus(targetIdsValue?: FocusParam): void {
const $$ = this.internal;
const {state} = $$;
const targetIds = $$.mapToTargetIds(targetIdsValue);
Expand Down Expand Up @@ -68,7 +68,7 @@ export default {
* // all targets will be faded out.
* chart.defocus();
*/
defocus(targetIdsValue?: focusParam): void {
defocus(targetIdsValue?: FocusParam): void {
const $$ = this.internal;
const {state} = $$;
const targetIds = $$.mapToTargetIds(targetIdsValue);
Expand Down Expand Up @@ -108,7 +108,7 @@ export default {
* // all targets will be reverted.
* chart.revert();
*/
revert(targetIdsValue?: focusParam): void {
revert(targetIdsValue?: FocusParam): void {
const $$ = this.internal;
const {config, state, $el} = $$;
const targetIds = $$.mapToTargetIds(targetIdsValue);
Expand Down
8 changes: 4 additions & 4 deletions src/Chart/api/grid.x.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/
import {extend} from "../../module/util";

type gridsParam = {value?: number, class?: string, text?: string}[];
type GridsParam = {value?: number, class?: string, text?: string}[];

/**
* Update x grid lines.
Expand All @@ -21,7 +21,7 @@ type gridsParam = {value?: number, class?: string, text?: string}[];
* ]);
* // --> Returns: [{value: 1, text: "Label 1"}, {value: 4, text: "Label 4"}]
*/
function xgrids(grids: gridsParam): gridsParam {
function xgrids(grids: GridsParam): GridsParam {
const $$ = this.internal;
const {config} = $$;

Expand Down Expand Up @@ -56,7 +56,7 @@ extend(xgrids, {
* {value: 4, text: "Label 4"}
* ]);
*/
add: function(grids: gridsParam): gridsParam {
add: function(grids: GridsParam): GridsParam {
return this.xgrids(
this.internal.config.grid_x_lines
.concat(grids || [])
Expand All @@ -82,7 +82,7 @@ extend(xgrids, {
* // all of x grid lines will be removed
* chart.xgrids.remove();
*/
remove: function(params?: gridsParam): void { // TODO: multiple
remove: function(params?: GridsParam): void { // TODO: multiple
this.internal.removeGridLines(params, true);
}
});
Expand Down
8 changes: 4 additions & 4 deletions src/Chart/api/regions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import CLASS from "../../config/classes";
import {getOption, extend} from "../../module/util";

type regionsParam = {axis?: string, class?: string, start?: number, end?: number}[];
type RegionsParam = {axis?: string, class?: string, start?: number, end?: number}[];

/**
* Update regions.
Expand All @@ -21,7 +21,7 @@ type regionsParam = {axis?: string, class?: string, start?: number, end?: number
* {axis: "y", end: 50, class: "regionY"}
* ]);
*/
function regions(regions: regionsParam): regionsParam {
function regions(regions: RegionsParam): RegionsParam {
const $$ = this.internal;
const {config} = $$;

Expand Down Expand Up @@ -56,7 +56,7 @@ extend(regions, {
* {axis: "y", end: 50, class: "regionY"}
*]);
*/
add: function(regions: regionsParam): regionsParam {
add: function(regions: RegionsParam): RegionsParam {
const $$ = this.internal;
const {config} = $$;

Expand Down Expand Up @@ -89,7 +89,7 @@ extend(regions, {
* // all of regions will be removed.
* chart.regions.remove();
*/
remove: function(optionsValue: regionsParam): regionsParam {
remove: function(optionsValue: RegionsParam): RegionsParam {
const $$ = this.internal;
const {config} = $$;

Expand Down
39 changes: 36 additions & 3 deletions test/api/export-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ describe("API export", () => {
}

expect(/^data:image\/svg\+xml;base64,.+/.test(chart.export())).to.be.true;
chart.export("image/png", exportCallback);
chart.export(null, exportCallback);
});

it("should export chart as image/png", done => {
Expand All @@ -47,9 +47,42 @@ describe("API export", () => {
done();
}

chart.export("image/png", exportCallback);
chart.export({mimeType: "image/png"}, exportCallback);
});

it("should export in different size", done => {
const expectedDataURL = [
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA+gAAAJYCAYAAADxHswlAAAgAElEQVR4XuzdCZhlVX3v/e9/",
"AAAABJRU5ErkJggg=="
];

setTimeout(() => {
chart.export({
width: 1000, height: 600
}, data => {
expect(
expectedDataURL.every(v => data.indexOf(v) >= 0)
).to.be.true;

done();
});
}, 500);
});

it("should export in different aspectRatio", done => {
const expectedDataURL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAAEsCAYAAACG+vy+AAAgAElEQVR4Xu19CZRcVbX2t8+t";

setTimeout(() => {
chart.export({
width: 200, height: 300, preserveAspectRatio: false
}, data => {
expect(data.indexOf(expectedDataURL) >= 0).to.be.true;

done();
});
}, 500);
});

it("set options", () => {
args = {
size: {
Expand Down Expand Up @@ -97,7 +130,7 @@ describe("API export", () => {
];

setTimeout(() => {
chart.export("image/png", data => {
chart.export(null, data => {
expect(expectedDataURL.indexOf(data) >= 0).to.be.true;
done();
});
Expand Down
13 changes: 11 additions & 2 deletions types/chart.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -478,10 +478,19 @@ export interface Chart {
* - NOTE:
* - IE11 and below not work properly due to the lack of the feature(foreignObject) support
* - The basic CSS file(ex. billboard.css) should be at same domain as API call context to get correct styled export image.
* @param mimeType The desired output image format. (ex. 'image/png' for png, 'image/jpeg' for jpeg format)
* @param option Export options
* @param [option.mimeType="image/png"] The desired output image format. (ex. 'image/png' for png, 'image/jpeg' for jpeg format)
* @param [option.width={currentWidth}] width
* @param [option.height={currentHeigth}] height
* @param [option.preserveAspectRatio=true] Preserve aspect ratio on given size
* @param callback The callback to be invoked when export is ready.
*/
export(this: Chart, mimeType?: string, callback?: (dataUrl: string) => string): string;
export(this: Chart, option?: {
width?: number;
height?: number;
mimeType: string;
preserveAspectRatio: boolean;
}, callback?: (dataUrl: string) => string): string;

/**
* Get or set single config option value.
Expand Down

0 comments on commit 3c2de80

Please sign in to comment.