Skip to content

Commit 62048e3

Browse files
committed
Add legend download buttons, refactor download code
1 parent e1840cf commit 62048e3

File tree

6 files changed

+247
-94
lines changed

6 files changed

+247
-94
lines changed

examples-src/App.vue

+6-5
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@
1515
<PlotContainer
1616
:pWidth="500"
1717
:pHeight="dynamicPlotHeight"
18-
:pMarginTop="120"
19-
:pMarginLeft="120"
20-
:pMarginRight="120"
18+
:pMarginTop="100"
19+
:pMarginLeft="100"
20+
:pMarginRight="100"
2121
:pMarginBottom="100"
2222
:showDownloadButton="true"
2323
:downloadButtonOffsetX="0"
24-
:downloadButtonOffsetY="0"
24+
:downloadButtonOffsetY="60"
2525
:showResizeButton="true"
2626
>
2727
<Axis
@@ -119,6 +119,7 @@
119119
:lWidth="250"
120120
:getScale="getScale"
121121
:getStack="getStack"
122+
:showDownloadButton="true"
122123
/>
123124

124125
<h3>&lt;ContinuousLegend/&gt;</h3>
@@ -127,6 +128,7 @@
127128
:lWidth="250"
128129
:getScale="getScale"
129130
:getStack="getStack"
131+
:showDownloadButton="true"
130132
/>
131133

132134
<h3>&lt;BarPlot/&gt;</h3>
@@ -1664,5 +1666,4 @@ a {
16641666
margin-bottom: 5px;
16651667
}
16661668
1667-
16681669
</style>

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "vueplotlib",
3-
"version": "1.11.4",
3+
"version": "1.11.5",
44
"private": false,
55
"scripts": {
66
"serve": "vue-cli-service serve --open ./examples-src/index.js",

src/components/PlotContainer.vue

+8-42
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
<script>
2-
import { DOWNLOAD_PATH } from './../icons.js';
32
import { create as d3_create, event as d3_event } from 'd3';
43
import { select as d3_select } from 'd3-selection';
54
import { drag as d3_drag } from 'd3-drag';
65
6+
import { DOWNLOAD_PATH } from './../icons.js';
7+
import { downloadSvg } from './../helpers.js';
8+
79
810
/**
911
* Function that takes in array of VNodes and adds props from a provided props object.
@@ -29,33 +31,6 @@ const addProp = function(slotArray, newProps) {
2931
return [];
3032
}
3133
32-
/**
33-
* Given an SVG DOM node, return the SVG contents as a data URI that can be saved to a file.
34-
* @private
35-
* @param {any} svg The SVG node.
36-
* @returns {string}
37-
*/
38-
const svgToUri = function(svg) {
39-
// Reference: https://stackoverflow.com/a/23218877
40-
const serializer = new XMLSerializer();
41-
var source = serializer.serializeToString(svg);
42-
43-
// Add namespace.
44-
if(!source.match(/^<svg[^>]+xmlns="http\:\/\/www\.w3\.org\/2000\/svg"/)){
45-
source = source.replace(/^<svg/, '<svg xmlns="http://www.w3.org/2000/svg"');
46-
}
47-
if(!source.match(/^<svg[^>]+"http\:\/\/www\.w3\.org\/1999\/xlink"/)){
48-
source = source.replace(/^<svg/, '<svg xmlns:xlink="http://www.w3.org/1999/xlink"');
49-
}
50-
51-
// Add xml declaration.
52-
source = '<?xml version="1.0" standalone="no"?>\r\n' + source;
53-
54-
// Convert svg source to URI.
55-
//return "data:image/svg+xml;charset=utf-8," + encodeURIComponent(source);
56-
return source;
57-
};
58-
5934
6035
/**
6136
* This component is a container for axis and plot components,
@@ -310,14 +285,8 @@ export default {
310285
},
311286
methods: {
312287
downloadViaButton() {
313-
const blob = this.download();
314-
const url = URL.createObjectURL(blob);
315-
const downloadAnchorNode = document.createElement('a');
316-
downloadAnchorNode.setAttribute("href", url);
317-
downloadAnchorNode.setAttribute("download", this.downloadName + ".svg");
318-
document.body.appendChild(downloadAnchorNode); // required for firefox
319-
downloadAnchorNode.click();
320-
downloadAnchorNode.remove();
288+
const svg = this.download();
289+
downloadSvg(svg, this.downloadName);
321290
},
322291
initResizeButton() {
323292
if(this.showResizeButton) {
@@ -347,7 +316,8 @@ export default {
347316
348317
const svg = d3_create("svg")
349318
.attr("width", this.fullWidth)
350-
.attr("height", this.fullHeight);
319+
.attr("height", this.fullHeight)
320+
.attr("viewBox", `0 0 ${this.fullWidth} ${this.fullHeight}`);
351321
352322
const defs = svg
353323
.append("defs");
@@ -443,11 +413,7 @@ export default {
443413
renderAxisToContext("axisRight");
444414
renderAxisToContext("axisBottom");
445415
446-
const svgContent = svgToUri(svg.node());
447-
448-
const blob = new Blob([svgContent], {'type': 'image/svg+xml'});
449-
450-
return blob;
416+
return svg;
451417
}
452418
}
453419
}

src/components/legends/CategoricalLegend.vue

+86-23
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
<script>
2929
import { scaleBand as d3_scaleBand } from 'd3-scale';
3030
import { select as d3_select } from 'd3-selection';
31+
import { create as d3_create } from 'd3';
3132
3233
3334
import CategoricalScale from './../../scales/CategoricalScale.js';
@@ -37,9 +38,11 @@ import HistoryStack from './../../history/HistoryStack.js';
3738
import ColorScalePicker from './../modals/ColorScalePicker.vue';
3839
import ColorPicker from './../modals/ColorPicker.vue';
3940
40-
import { COLOR_PICKER_PATH, EYE_PATH, EYE_DISABLED_PATH, PAINT_BUCKET_PATH } from './../../icons.js';
41+
import { COLOR_PICKER_PATH, EYE_PATH, EYE_DISABLED_PATH, PAINT_BUCKET_PATH, DOWNLOAD_PATH } from './../../icons.js';
4142
import { EVENT_TYPES, EVENT_SUBTYPES } from '../../history/base-events.js';
4243
44+
import { downloadSvg } from './../../helpers.js';
45+
4346
const STYLES = Object.freeze({ "BAR": 1, "DOT": 2, "LINE": 3, "SHAPE": 4 });
4447
4548
let uuid = 0;
@@ -89,6 +92,14 @@ export default {
8992
},
9093
'clickHandler': {
9194
type: Function
95+
},
96+
'showDownloadButton': {
97+
type: Boolean,
98+
default: false
99+
},
100+
'downloadName': {
101+
type: String,
102+
default: 'legend'
92103
}
93104
},
94105
data() {
@@ -193,7 +204,7 @@ export default {
193204
[scaleKey]
194205
));
195206
},
196-
drawLegend() {
207+
drawLegend(d3Node) {
197208
const vm = this;
198209
vm.removeLegend();
199210
@@ -204,43 +215,38 @@ export default {
204215
const textOffset = 30;
205216
const marginX = 4;
206217
const marginY = 2;
218+
const buttonWidth = 16;
207219
208220
vm.lHeight = vm.lItemHeight * varScale.domain.length + titleHeight;
209221
210222
/*
211223
* Create the SVG elements
212224
*/
213-
214-
const container = d3_select(vm.legendSelector)
215-
.append("svg")
216-
.attr("width", vm.computedWidth)
217-
.attr("height", vm.computedHeight);
225+
let container;
226+
if(d3Node) {
227+
container = d3Node;
228+
} else {
229+
container = d3_select(vm.legendSelector)
230+
.append("svg")
231+
.attr("width", vm.computedWidth)
232+
.attr("height", vm.computedHeight);
233+
}
218234
219235
const legend = container.append("g")
220236
.attr("class", "legend")
221237
.attr("transform", "translate(" + vm.computedTranslateX + "," + vm.computedTranslateY + ")");
222-
223-
224238
225239
const title = legend.append("g")
226240
.attr("width", vm.lWidth);
227241
228242
const titleText = title.append("text")
229243
.style("text-anchor", "start")
244+
.style("font-family", "Avenir")
230245
.text(varScale.name);
231246
const titleTextBbox = titleText.node().getBBox();
232247
titleText.attr("transform", "translate(" + 0 + "," + titleTextBbox.height + ")");
233248
234-
title.append("path")
235-
.attr("d", PAINT_BUCKET_PATH)
236-
.attr("width", 20)
237-
.attr("height", 20)
238-
.attr("transform", "translate(" + (vm.lWidth - 1.5*marginX) + "," + (titleTextBbox.height/2) + ") scale(-0.7 0.7)")
239-
.style("cursor", "pointer")
240-
.attr("fill", "silver")
241-
.on("click", () => {
242-
vm.showColorScalePicker = true;
243-
});
249+
244250
245251
const legendInner = legend.append("g")
246252
.attr("class", "legend-inner");
@@ -254,7 +260,8 @@ export default {
254260
.attr("width", vm.lWidth)
255261
.attr("height", "1px")
256262
.attr("fill", "black")
257-
.attr("fill-opacity", 0);
263+
.attr("fill-opacity", 0)
264+
.style("user-select", "none");
258265
259266
highlight.append("rect")
260267
.attr("x", 0)
@@ -263,7 +270,8 @@ export default {
263270
.attr("height", 1)
264271
.attr("fill", "black")
265272
.attr("fill-opacity", 0)
266-
.attr("transform", "translate(0," + (vm.lItemHeight) + ")");
273+
.attr("transform", "translate(0," + (vm.lItemHeight) + ")")
274+
.style("user-select", "none");
267275
268276
269277
@@ -299,6 +307,7 @@ export default {
299307
300308
const itemText = items.append("text")
301309
.style("text-anchor", "start")
310+
.style("font-family", "Avenir")
302311
.attr("y", scale.bandwidth() - 5)
303312
.attr("x", (textOffset + marginX) + "px")
304313
.style("font-size", "13px")
@@ -323,10 +332,49 @@ export default {
323332
.attr("fill", (d) => varScale.color(d))
324333
.attr("fill-opacity", (d) => varScale.domainFiltered.includes(d) ? 1 : 0);
325334
}
326-
335+
336+
if(d3Node) {
337+
return; /* SVG passed in to function, so not interactive */
338+
}
339+
327340
328341
// Action buttons
329-
const buttonWidth = 16;
342+
343+
const colorScaleButtonG = title
344+
.append("g")
345+
.attr("width", 20)
346+
.attr("height", 20)
347+
.attr("transform", "translate(" + (vm.lWidth - 1.5*marginX) + "," + (titleTextBbox.height/2) + ") scale(-0.7 0.7)")
348+
.style("cursor", "pointer")
349+
.on("click", () => {
350+
vm.showColorScalePicker = true;
351+
});
352+
colorScaleButtonG.append("rect")
353+
.attr("width", 20)
354+
.attr("height", 20)
355+
.attr("fill", "transparent");
356+
colorScaleButtonG.append("path")
357+
.attr("d", PAINT_BUCKET_PATH)
358+
.attr("fill", "silver");
359+
360+
if(vm.showDownloadButton) {
361+
const downloadButtonG = title
362+
.append("g")
363+
.attr("width", 20)
364+
.attr("height", 20)
365+
.attr("transform", "translate(" + (vm.lWidth - 2*(buttonWidth) + marginX/2) + "," + (titleTextBbox.height/2) + ") scale(-0.7 0.7)")
366+
.style("cursor", "pointer")
367+
.on("click", vm.downloadViaButton);
368+
369+
downloadButtonG.append("rect")
370+
.attr("width", 20)
371+
.attr("height", 20)
372+
.attr("fill", "transparent");
373+
downloadButtonG.append("path")
374+
.attr("d", DOWNLOAD_PATH)
375+
.attr("fill", "silver");
376+
377+
}
330378
331379
const filterButtons = items.append("g")
332380
.attr("transform", "translate(" + (vm.lWidth - 2*(buttonWidth + 2*marginX)) + ",0)")
@@ -409,6 +457,21 @@ export default {
409457
.attr("transform", "scale(0.7 0.7)")
410458
.attr("fill", "silver");
411459
460+
},
461+
download() {
462+
const svg = d3_create("svg")
463+
.attr("width", this.computedWidth)
464+
.attr("height", this.computedHeight)
465+
.attr("viewBox", `0 0 ${this.computedWidth} ${this.computedHeight}`);
466+
467+
this.drawLegend(svg);
468+
this.drawLegend();
469+
470+
return svg;
471+
},
472+
downloadViaButton() {
473+
const svg = this.download();
474+
downloadSvg(svg, this.downloadName);
412475
}
413476
}
414477
}

0 commit comments

Comments
 (0)