|
1 | 1 | <script> |
2 | 2 | import { getRetinaRatio } from './../helpers.js'; |
3 | 3 | import { DOWNLOAD_PATH } from './../icons.js'; |
| 4 | +import { create as d3_create } from 'd3'; |
4 | 5 |
|
5 | 6 |
|
6 | 7 | /** |
@@ -28,22 +29,29 @@ const addProp = function(slotArray, newProps) { |
28 | 29 | } |
29 | 30 |
|
30 | 31 | /** |
31 | | - * Given a canvas context, x and y offsets, and an image URI, render the image to the context. |
| 32 | + * Given an SVG DOM node, return the SVG contents as a data URI that can be saved to a file. |
32 | 33 | * @private |
33 | | - * @param {Context} ctx The canvas context. |
34 | | - * @param {string} uri The image data URI. |
35 | | - * @param {int} x The x offset. |
36 | | - * @param {int} y The y offset. |
| 34 | + * @param {any} svg The SVG node. |
| 35 | + * @returns {string} |
37 | 36 | */ |
38 | | -const renderToContext = function(ctx, uri, x, y, width, height) { |
39 | | - return new Promise((resolve, reject) => { |
40 | | - var img = new Image; |
41 | | - img.onload = () => { |
42 | | - ctx.drawImage(img, x, y, width, height); |
43 | | - resolve(); |
44 | | - }; |
45 | | - img.src = uri; |
46 | | - }); |
| 37 | +const svgToUri = function(svg) { |
| 38 | + // Reference: https://stackoverflow.com/a/23218877 |
| 39 | + const serializer = new XMLSerializer(); |
| 40 | + var source = serializer.serializeToString(svg); |
| 41 | +
|
| 42 | + // Add namespace. |
| 43 | + if(!source.match(/^<svg[^>]+xmlns="http\:\/\/www\.w3\.org\/2000\/svg"/)){ |
| 44 | + source = source.replace(/^<svg/, '<svg xmlns="http://www.w3.org/2000/svg"'); |
| 45 | + } |
| 46 | + if(!source.match(/^<svg[^>]+"http\:\/\/www\.w3\.org\/1999\/xlink"/)){ |
| 47 | + source = source.replace(/^<svg/, '<svg xmlns:xlink="http://www.w3.org/1999/xlink"'); |
| 48 | + } |
| 49 | +
|
| 50 | + // Add xml declaration. |
| 51 | + source = '<?xml version="1.0" standalone="no"?>\r\n' + source; |
| 52 | +
|
| 53 | + // Convert svg source to URI. |
| 54 | + return "data:image/svg+xml;charset=utf-8," + encodeURIComponent(source); |
47 | 55 | }; |
48 | 56 |
|
49 | 57 |
|
@@ -209,95 +217,95 @@ export default { |
209 | 217 | } |
210 | 218 | }, |
211 | 219 | methods: { |
212 | | - renderToContext(ctx, x, y, uri) { |
213 | | - var img = new Image; |
214 | | - img.onload = () => { |
215 | | - ctx.drawImage(img, x, y); // Or at whatever offset you like |
216 | | - }; |
217 | | - img.src = uri; |
218 | | - }, |
219 | 220 | downloadViaButton() { |
220 | | - this.downloadWithoutAxisBrushing() |
221 | | - .then((uri) => { |
222 | | - const downloadAnchorNode = document.createElement('a'); |
223 | | - downloadAnchorNode.setAttribute("href", uri); |
224 | | - downloadAnchorNode.setAttribute("download", this.downloadName + ".png"); |
225 | | - document.body.appendChild(downloadAnchorNode); // required for firefox |
226 | | - downloadAnchorNode.click(); |
227 | | - downloadAnchorNode.remove(); |
228 | | - }); |
229 | | - }, |
230 | | - downloadWithoutAxisBrushing() { |
231 | | - let axisTopDisableBrushing = this.$slots.axisTop.forEach((vnode) => { vnode.componentInstance.preDownload(); }); |
232 | | - let axisLeftDisableBrushing = this.$slots.axisLeft.forEach((vnode) => { vnode.componentInstance.preDownload(); }); |
233 | | - let axisRightDisableBrushing = this.$slots.axisRight.forEach((vnode) => { vnode.componentInstance.preDownload(); }); |
234 | | - let axisBottomDisableBrushing = this.$slots.axisBottom.forEach((vnode) => { vnode.componentInstance.preDownload(); }); |
235 | | -
|
236 | | - return new Promise((resolve, reject) => { |
237 | | - Promise.all([axisTopDisableBrushing, axisLeftDisableBrushing, axisRightDisableBrushing, axisBottomDisableBrushing]).then(() => { |
238 | | - this.download().then((uri) => { |
239 | | - resolve(uri); |
240 | | - this.$slots.axisTop.forEach((vnode) => { vnode.componentInstance.postDownload(); }); |
241 | | - this.$slots.axisLeft.forEach((vnode) => { vnode.componentInstance.postDownload(); }); |
242 | | - this.$slots.axisRight.forEach((vnode) => { vnode.componentInstance.postDownload(); }); |
243 | | - this.$slots.axisBottom.forEach((vnode) => { vnode.componentInstance.postDownload(); }); |
244 | | - }); |
245 | | - }); |
246 | | - }); |
| 221 | + const uri = this.download() |
| 222 | + const downloadAnchorNode = document.createElement('a'); |
| 223 | + downloadAnchorNode.setAttribute("href", uri); |
| 224 | + downloadAnchorNode.setAttribute("download", this.downloadName + ".svg"); |
| 225 | + document.body.appendChild(downloadAnchorNode); // required for firefox |
| 226 | + downloadAnchorNode.click(); |
| 227 | + downloadAnchorNode.remove(); |
247 | 228 | }, |
248 | 229 | download() { |
249 | | - const canvas = document.createElement('canvas'); |
250 | | - const ctx = canvas.getContext('2d'); |
| 230 | + const svg = d3_create("svg") |
| 231 | + .attr("width", this.fullWidth) |
| 232 | + .attr("height", this.fullHeight); |
| 233 | + |
| 234 | + const defs = svg |
| 235 | + .append("defs"); |
251 | 236 | |
252 | | - const ratio = getRetinaRatio(ctx); |
253 | | - const scaledWidth = this.fullWidth * ratio; |
254 | | - const scaledHeight = this.fullHeight * ratio; |
255 | | -
|
256 | | - canvas.width = scaledWidth; |
257 | | - canvas.height = scaledHeight; |
258 | | - ctx.scale(ratio, ratio); |
259 | | -
|
260 | 237 | const renderAxisToContext = (axisType) => { |
261 | 238 | if(this.$slots[axisType].length > 0) { |
262 | | - return this.$slots[axisType][0].componentInstance.downloadAxis() |
263 | | - .then((uri) => { |
264 | | - const x = this.$slots[axisType][0].componentInstance.computedLeft; |
265 | | - const y = this.$slots[axisType][0].componentInstance.computedTop; |
266 | | - const width = this.$slots[axisType][0].componentInstance.computedWidth; |
267 | | - const height = this.$slots[axisType][0].componentInstance.computedHeight; |
268 | | - return renderToContext(ctx, uri, x, y, width, height); |
269 | | - }); |
| 239 | + const x = this.$slots[axisType][0].componentInstance.computedLeft; |
| 240 | + const y = this.$slots[axisType][0].componentInstance.computedTop; |
| 241 | + const width = this.$slots[axisType][0].componentInstance.computedWidth; |
| 242 | + const height = this.$slots[axisType][0].componentInstance.computedHeight; |
| 243 | +
|
| 244 | + defs |
| 245 | + .append("clipPath") |
| 246 | + .attr("id", `cp-${axisType}`) |
| 247 | + .append("rect") |
| 248 | + .attr("width", width) |
| 249 | + .attr("height", height); |
| 250 | +
|
| 251 | + const axisSvg = d3_create("svg") |
| 252 | + .attr("width", width) |
| 253 | + .attr("height", height); |
| 254 | +
|
| 255 | + this.$slots[axisType][0].componentInstance.drawAxis(axisSvg, true); |
| 256 | + this.$slots[axisType][0].componentInstance.drawAxis(); |
| 257 | +
|
| 258 | + const axisG = svg |
| 259 | + .append("g") |
| 260 | + .attr("class", `download-g-${axisType}`) |
| 261 | + .attr("width", width) |
| 262 | + .attr("height", height) |
| 263 | + .attr("transform", `translate(${x},${y})`) |
| 264 | + .attr("clip-path", `url(#cp-${axisType})`); |
| 265 | + |
| 266 | + axisG.html(axisSvg.node().innerHTML); |
270 | 267 | } |
271 | | - return Promise.resolve(); |
272 | 268 | }; |
273 | 269 |
|
274 | 270 | const renderPlotToContext = () => { |
275 | 271 | if(this.$slots.plot.length > 0) { |
276 | | - return this.$slots.plot[0].componentInstance.downloadPlot() |
277 | | - .then((uri) => { |
278 | | - const x = this.pMarginLeft; |
279 | | - const y = this.pMarginTop; |
280 | | - const width = this.pWidth; |
281 | | - const height = this.pHeight; |
282 | | - return renderToContext(ctx, uri, x, y, width, height); |
283 | | - }); |
| 272 | + const x = this.pMarginLeft; |
| 273 | + const y = this.pMarginTop; |
| 274 | + const width = this.pWidth; |
| 275 | + const height = this.pHeight; |
| 276 | +
|
| 277 | + defs |
| 278 | + .append("clipPath") |
| 279 | + .attr("id", `cp-plot`) |
| 280 | + .append("rect") |
| 281 | + .attr("width", width) |
| 282 | + .attr("height", height); |
| 283 | +
|
| 284 | + const plotSvg = d3_create("svg") |
| 285 | + .attr("width", width) |
| 286 | + .attr("height", height); |
| 287 | + |
| 288 | + this.$slots.plot[0].componentInstance.drawPlot(plotSvg); |
| 289 | + this.$slots.plot[0].componentInstance.drawPlot(); |
| 290 | +
|
| 291 | + const plotG = svg |
| 292 | + .append("g") |
| 293 | + .attr("class", `download-g-plot`) |
| 294 | + .attr("width", width) |
| 295 | + .attr("height", height) |
| 296 | + .attr("transform", `translate(${x},${y})`) |
| 297 | + .attr("clip-path", `url(#cp-plot)`); |
| 298 | + plotG.html(plotSvg.node().innerHTML); |
284 | 299 | } |
285 | | - return Promise.resolve(); |
286 | 300 | }; |
287 | 301 | |
288 | | - const renderPromises = []; |
289 | | - renderPromises.push(renderAxisToContext("axisTop")); |
290 | | - renderPromises.push(renderAxisToContext("axisLeft")); |
291 | | - renderPromises.push(renderPlotToContext()); |
292 | | - renderPromises.push(renderAxisToContext("axisRight")); |
293 | | - renderPromises.push(renderAxisToContext("axisBottom")); |
| 302 | + renderAxisToContext("axisTop"); |
| 303 | + renderAxisToContext("axisLeft"); |
| 304 | + renderPlotToContext(); |
| 305 | + renderAxisToContext("axisRight"); |
| 306 | + renderAxisToContext("axisBottom"); |
294 | 307 |
|
295 | | - return new Promise((resolve, reject) => { |
296 | | - Promise.all(renderPromises).then(() => { |
297 | | - const uri = canvas.toDataURL("image/png"); |
298 | | - resolve(uri); |
299 | | - }); |
300 | | - }); |
| 308 | + return svgToUri(svg.node()); |
301 | 309 | } |
302 | 310 | } |
303 | 311 | } |
|
0 commit comments