I'm using D3 to zoom onto an image on click and on Mousewheel. Everything is working fine but the first zoom glitches a lot.
Here is the demo of the app.
This is how I'm zooming towards the objects:
const star = "https://gmg-world-media.github.io/skymap-v1dev/static/media/star.19b34dbf.svg";
const galaxy = "https://gmg-world-media.github.io/skymap-v1dev/static/media/galaxy.c5e7b011.svg";
const nebula = "https://gmg-world-media.github.io/skymap-v1dev/static/media/nebula.d65f45e5.svg";
const exotic = "https://gmg-world-media.github.io/skymap-v1dev/static/media/exotic.21ad5d39.svg";
const sWidth = window.innerWidth;
const sHeight = window.innerHeight;
const x = d3.scaleLinear().range([0, sWidth]).domain([-180, 180]);
const y = d3.scaleLinear().range([0, sHeight]).domain([-90, 90]);
const svg = d3.select("#render_map").append("svg").attr("width", sWidth).attr("height", sHeight);
const node = svg.append("g").attr('class', 'scale-holder');
const zoom = d3
.zoom()
.scaleExtent([1, 30])
.translateExtent([
[0, 0],
[sWidth, sHeight]
])
svg.call(zoom);
const imgG = node.append("g");
imgG
.insert("svg:image")
.attr("preserveAspectRatio", "none")
.attr("x", 0)
.attr("y", 0)
.attr("width", sWidth)
.attr("height", sHeight)
.attr("xlink:href", "https://gmg-world-media.github.io/skymap-v1dev/img-set/image-1.jpg");
imgG
.insert("svg:image")
.attr("preserveAspectRatio", "none")
.attr("x", 0)
.attr("y", 0)
.attr("width", sWidth)
.attr("height", sHeight)
.attr("xlink:href", "https://gmg-world-media.github.io/skymap-v1dev/img-set/image.jpg");
// Draw objects on map with icon size 8
drawObjects(8)
function drawObjects(size) {
const dataArray = [];
const to = -180;
const from = 180;
const fixed = 3;
const objectType = ["ST", "G", "N", "E"];
// Following loop is just for demo.
// Actual data comes from a JSON file.
for (let i = 0; i < 350; i++) {
const latitude = (Math.random() * (to - from) + from).toFixed(fixed) * 1;
const longitude = (Math.random() * (to - from) + from).toFixed(fixed) * 1;
const random = Math.floor(Math.random() * objectType.length);
dataArray.push({
"Longitude": longitude,
"Latitude": latitude,
"Category": objectType[random]
})
}
for (let index = 0; index < dataArray.length; index++) {
// Loop over the data
const item = dataArray[index]
const mY = y(Number(item.Latitude))
const mX = x(Number(item.Longitude))
if (node.select(".coords[index='" + index + "']").size() === 0) {
let shape = star;
// Plot various icons based on Category
switch (item.Category) {
case "ST":
shape = star;
break;
case "G":
shape = galaxy;
break;
case "N":
shape = nebula;
break;
case "E":
shape = exotic;
break;
}
const rect = node
.insert("svg:image")
.attr("class", "coords")
.attr("preserveAspectRatio", "none")
.attr("x", mX)
.attr("y", mY)
.attr("width", size)
.attr("height", size)
.attr("cursor", "pointer")
.attr("index", index)
.attr("xlink:href", shape)
.attr("opacity", "0")
.on("click", function() {
handleObjectClick(index, mX, mY)
})
// Add the objects on the map
rect.transition().duration(Math.random() * (2000 - 500) + 500).attr("opacity", "1")
}
}
}
function boxZoom(x, y) {
// Zoom towards the selected object
// This is the part responsible for zooming
svg
.transition()
.duration(1000)
.call(
zoom.transform,
d3.zoomIdentity
.translate(sWidth / 2, sHeight / 2)
.scale(6)
.translate(-x, -y)
);
}
function handleObjectClick(currentSelect, x, y) {
// Appending some thumbnails to the clicked object here...
//Call the zoom function
boxZoom(x, y)
}
#render_map {
width: 100vw;
height: 100vh;
margin: 0 auto;
overflow: hidden;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="render_map">
</div>
This zoom doesn't seem to be working here. But it does definitely work in the app. I've not modified the piece of code responsible for zooming. (See this demo instead.)
The problem is that zoom jumps when you do it for the first time after a page load, and then it fixes itself.
I don't understand what I'm doing wrong here. Any hints would be lovely.
TIA!
The issue seems caused by a very expensive CSS repaint cycle. I tested this in Firefox by going to Performance in the DEV tools and starting a recording, then zooming for the first time.
I saw the fps drop enormously, and that the repaint took as much as 250ms. Normally, that is 10-50ms.
I have some pointers:
Why do you have two images behind each other? Big images are definitely the reason why repainting takes this long, and your image is 8000x4000 pixels! Start by removing the image that we're not even seeing;
Try adding an initial value of transform="translate(0, 0) scale(1)" to .scale-holder. I have a feeling that adding this the first time is what forces the entire screen to be repainted. Maybe changing an existing scale value is an easier mathematical operation than applying a scale value to something that was not scaled before;
If that doesn't help, compress the image to at most 1600 or even 1080 pixels wide. Us mortals should not even be able to see the difference.
Related
I'm currently working on a circular progress bar. I've been able to create the progress bar but I need to at border-radius to the end and start of the progress and also change de font-size of the % number. I've searched around and in theory, the border-radius is added with stroke-linecap="round", but that doesn't seem to work for me. I haven't been able to find anything regarding the font-size.Adding a shadow to the bar would also be great, but that's not truly necessary.
I have already looked at this answer but I can't seem to get it right.
function drawProgress(percentage, element, svg) {
if (svg) {
svg.selectAll("*").remove();
}
var wrapper = element;
var start = 0;
var colours = {
fill: '#3F88FB',
track: '#DDDDDD',
text: '#444444',
}
var radius = 34;
var border = 8;
var strokeSpacing = 4;
var endAngle = Math.PI * 2;
var formatText = d3.format('.0%');
var boxSize = radius * 2;
var count = percentage;
var progress = start;
var step = percentage < start ? -0.01 : 0.01;
//Define the circle
var circle = d3.svg.arc()
.startAngle(0)
.innerRadius(radius)
.outerRadius(radius - border);
//setup SVG wrapper
svg = d3.select(wrapper)
.append('svg')
.attr('width', boxSize)
.attr('height', boxSize);
// ADD Group container
var g = svg.append('g')
.attr('transform', 'translate(' + boxSize / 2 + ',' + boxSize / 2 + ')');
//Setup track
var track = g.append('g').attr('class', 'radial-progress');
track.append('path')
.attr('fill', colours.track)
.attr('stroke-width', strokeSpacing + 'px')
.attr('d', circle.endAngle(endAngle));
//Add colour fill
var value = track.append('path')
.attr('fill', colours.fill)
.attr('stroke-width', strokeSpacing + 'px')
.attr('stroke-linecap', 'round');
//Add text value
var numberText = track.append('text')
.attr('fill', colours.text)
.attr('text-anchor', 'middle')
.attr('dy', '0.5rem');
//update position of endAngle
value.attr('d', circle.endAngle(endAngle * percentage));
//update text value
numberText.text(formatText(percentage));}
var svgVisitas;
drawProgress(50 / 100, document.getElementById('radialprogressVisitas'), svgVisitas);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div id="radialprogressVisitas"></div>
Thank you. If you need anything else, please specify it in the comments and I'll edit the question. The project is run on Visual Basic.
I followed this Observable post to easily create a legend.
Since the line
DOM.canvas(1, n)
in the ramp works only on Observable, I replaced it with
document.createElement("canvas")
and also modified the SVG so that it's appended to the main div tag. These changed do not cause any errors however the problem is that the legend is not displayed even though the legend SVG is present in the raw HTML.
Here's the link to a JSFiddle. Any guidance would be greatly appreciated.
The canvas is being created, that's not the problem. The problem is that, since you are now missing the width and height in...
const canvas = DOM.canvas(n, 1);
//these are w & h --------^--^
... you now need to set those yourself. For instance:
d3.select(canvas).attr("width", n)
.attr("height", 1);
Here is a simplified version of that JSFiddle, showing that the canvas works:
legend({
color: d3.scaleSequential([1, 10], d3.interpolateReds),
title: "Title"
})
function legend({
color,
title,
tickSize = 6,
width = 320,
height = 44 + tickSize,
marginTop = 18,
marginRight = 0,
marginBottom = 16 + tickSize,
marginLeft = 0,
ticks = width / 64,
tickFormat,
tickValues
} = {}) {
const svg = d3.select(".scatter").append("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.style("overflow", "visible")
.style("display", "block");
svg.append("image")
.attr("x", marginLeft)
.attr("y", marginTop)
.attr("width", width - marginLeft - marginRight)
.attr("height", height - marginTop - marginBottom)
.attr("preserveAspectRatio", "none")
.attr("xlink:href", ramp(color.interpolator()).toDataURL());
}
function ramp(color, n = 256) {
const canvas = document.createElement('canvas');
const context = canvas.getContext("2d");
d3.select(canvas).attr("width", n)
.attr("height", 1);
for (let i = 0; i < n; ++i) {
context.fillStyle = color(i / (n - 1));
context.fillRect(i, 0, 1, 1);
}
return canvas;
}
<script type="text/javascript" src="https://d3js.org/d3.v5.js"></script>
<div class="scatter">
</div>
By the way, there is no such element as <legend-svg>.
PS: This is the second question from you I'm answering on this subject. As you're new to JavaScript and D3, here is an advice: do not try to use that Observable notebook, that's way too complicated for your purposes. Just create the SVG, the canvas and a basic axis yourself, from scratch, it will be way easier.
I have a svg element ; the nodes, links, labels etc. are appended to it. I got the zoom-to-particular-node-by-name functionality running but the issue is after zooming automatically to the respective node , whenever I try to pan svg (by clicking and dragging it around), it resets the zoom and the coordinates to how it was before I zoomed to a particular node. I think it has to do with the way d3.event.transform works but I am not able to fix it. I want to be able to continue panning and zooming from the node I zoomed to without resetting any values.
(Also, from a bit of debugging , I observed that the cx and cy coordinates for the nodes did not change by zooming and panning from the code, but If I were to zoom and pan to a node manually , then it would. I guess that is the problem)
var svg1 = d3.select("svg");
var width = +screen.width;
var height = +screen.height - 500;
svg1.attr("width", width).attr("height", height);
var zoom = d3.zoom();
var svg = svg1
.call(
zoom.on("zoom", function() {
svg.attr("transform", d3.event.transform);
})
)
.on("dblclick.zoom", null)
.append("g");
function highlightNode() {
var userInput = document.getElementById("targetNode");
theNode = d3.select("#" + userInput.value);
const isEmpty = theNode.empty();
if (isEmpty) {
document.getElementById("output").innerHTML = "Given node doesn't exist";
} else {
document.getElementById("output").innerHTML = "";
}
svg
.transition()
.duration(750)
.attr(
"transform",
"translate(" +
-(theNode.attr("cx") - screen.width / 2) +
"," +
-(theNode.attr("cy") - screen.height / 4) +
")"
// This works correctly
);
}
I'm having trouble translating a D3 example with a zoom behavior from v3 to v5. My code is based on this example: https://bl.ocks.org/mbostock/2206340 by Mike Bostock. I use react and I get these errors "d3.zoom(...).translate is not a function" and "d3.zoom(...).scale is not a function". I looked in the documentation, but could not find scale or translate just scaleBy and translateTo and translateBy. I can't figure out how to do it either way.
componentDidMount() {
this.drawChart();
}
drawChart = () => {
var width = window.innerWidth * 0.66,
height = window.innerHeight * 0.7,
centered,
world_id;
window.addEventListener("resize", function() {
width = window.innerWidth * 0.66;
height = window.innerHeight * 0.7;
});
var tooltip = d3
.select("#container")
.append("div")
.attr("class", "tooltip hidden");
var projection = d3
.geoMercator()
.scale(100)
.translate([width / 2, height / 1.5]);
var path = d3.geoPath().projection(projection);
var zoom = d3
.zoom()
.translate(projection.translate())
.scale(projection.scale())
.scaleExtent([height * 0.197, 3 * height])
.on("zoom", zoomed);
var svg = d3
.select("#container")
.append("svg")
.attr("width", width)
.attr("class", "map card shadow")
.attr("height", height);
var g = svg.append("g").call(zoom);
g.append("rect")
.attr("class", "background")
.attr("width", width)
.attr("height", height);
var world_id = data2;
var world = data;
console.log(world);
var rawCountries = topojson.feature(world, world.objects.countries)
.features,
neighbors = topojson.neighbors(world.objects.countries.geometries);
console.log(rawCountries);
console.log(neighbors);
var countries = [];
// Splice(remove) random pieces
rawCountries.splice(145, 1);
rawCountries.splice(38, 1);
rawCountries.map(country => {
//console.log(parseInt(country.id) !== 010)
// Filter out Antartica and Kosovo
if (parseInt(country.id) !== parseInt("010")) {
countries.push(country);
} else {
console.log(country.id);
}
});
console.log(countries);
g.append("g")
.attr("id", "countries")
.selectAll(".country")
.data(countries)
.enter()
.insert("path", ".graticule")
.attr("class", "country")
.attr("d", path)
.attr("data-name", function(d) {
return d.id;
})
.on("click", clicked)
.on("mousemove", function(d, i) {
var mouse = d3.mouse(svg.node()).map(function(d) {
return parseInt(d);
});
tooltip
.classed("hidden", false)
.attr(
"style",
"left:" + mouse[0] + "px;top:" + (mouse[1] - 50) + "px"
)
.html(getCountryName(d.id));
})
.on("mouseout", function(d, i) {
tooltip.classed("hidden", true);
});
function getCountryName(id) {
var country = world_id.filter(
country => parseInt(country.iso_n3) == parseInt(id)
);
console.log(country[0].name);
console.log(id);
return country[0].name;
}
function updateCountry(d) {
console.log(world_id);
var country = world_id.filter(
country => parseInt(country.iso_n3) == parseInt(d.id)
);
console.log(country[0].name);
var iso_a2;
if (country[0].name === "Kosovo") {
iso_a2 = "xk";
} else {
iso_a2 = country[0].iso_a2.toLowerCase();
}
// Remove any current data
$("#countryName").empty();
$("#countryFlag").empty();
$("#countryName").text(country[0].name);
var src = "svg/" + iso_a2 + ".svg";
var img = "<img id='flag' class='flag' src=" + src + " />";
$("#countryFlag").append(img);
}
// Remove country when deselected
function removeCountry() {
$("#countryName").empty();
$("#countryFlag").empty();
}
// When clicked on a country
function clicked(d) {
if (d && centered !== d) {
centered = d;
updateCountry(d);
} else {
centered = null;
removeCountry();
}
g.selectAll("path").classed(
"active",
centered &&
function(d) {
return d === centered;
}
);
console.log("Clicked");
console.log(d);
console.log(d);
var centroid = path.centroid(d),
translate = projection.translate();
console.log(translate);
console.log(centroid);
projection.translate([
translate[0] - centroid[0] + width / 2,
translate[1] - centroid[1] + height / 2
]);
zoom.translate(projection.translate());
g.selectAll("path")
.transition()
.duration(700)
.attr("d", path);
}
// D3 zoomed
function zoomed() {
console.log("zoomed");
projection.translate(d3.event.translate).scale(d3.event.scale);
g.selectAll("path").attr("d", path);
}
};
render() {
return (
<div className="container-fluid bg">
<div class="row">
<div className="col-12">
<h2 className="header text-center p-3 mb-5">
Project 2 - World value survey
</h2>
</div>
</div>
<div className="row mx-auto">
<div className="col-md-8">
<div id="container" class="mx-auto" />
</div>
<div className="col-md-4">
<div id="countryInfo" className="card">
<h2 id="countryName" className="p-3 text-center" />
<div id="countryFlag" className="mx-auto" />
</div>
</div>
</div>
</div>
);
}
I won't go into the differences between v3 and v5 partly because it has been long enough that I have forgotten much of the specifics and details as to how v3 was different. Instead I'll just look at how to implement that example with v5. This answer would require adaptation for non-geographic cases - the geographic projection is doing the visual zooming in this case.
In your example, the zoom keeps track of the zoom state in order to set the projection properly. The zoom does not set a transform to any SVG element, instead the projection reprojects the features each zoom (or click).
So, to get started, with d3v5, after we call the zoom on our selection, we can set the zoom on a selected element with:
selection.call(zoom.transform, transformObject);
Where the base transform object is:
d3.zoomIdentity
d3.zoomIdentity has scale (k) of 1, translate x (x) and y (y) values of 0. There are some methods built into the identity prototype, so a plain object won't do, but we can use the identity to set new values for k, x, and y:
var transform = d3.zoomIdentity;
transform.x = projection.translate()[0]
transform.y = projection.translate()[1]
transform.k = projection.scale()
This is very similar to the example, but rather than providing the values to the zoom behavior itself, we are building an object that describes the zoom state. Now we can use selection.call(zoom.transform, transform) to apply the transform. This will:
set the zoom's transform to the provided values
trigger a zoom event
In our zoom function we want to take the updated zoom transform, apply it to the projection and then redraw our paths:
function zoomed() {
// Get the new zoom transform
transform = d3.event.transform;
// Apply the new transform to the projection
projection.translate([transform.x,transform.y]).scale(transform.k);
// Redraw the features based on the updaed projection:
g.selectAll("path").attr("d", path);
}
Note - d3.event.translate and d3.event.scale won't return anything in d3v5 - these are now the x,y,k properties of d3.event.transform
Without a click function, we might have this, which is directly adapted from the example in the question. The click function is not included, but you can still pan.
If we want to include a click to center function like the original, we can update our transform object with the new translate and call the zoom:
function clicked(d) {
var centroid = path.centroid(d),
translate = projection.translate();
// Update the translate as before:
projection.translate([
translate[0] - centroid[0] + width / 2,
translate[1] - centroid[1] + height / 2
]);
// Update the transform object:
transform.x = projection.translate()[0];
transform.y = projection.translate()[1];
// Apply the transform object:
g.call(zoom.transform, transform);
}
Similar to the v3 version - but by applying the zoom transform (just as we did initially) we trigger a zoom event, so we don't need to update the path as part of the click function.
All together that might look like this.
There is on detail I didn't include, the transition on click. As we triggering the zoomed function on both click and zoom, if we included a transition, panning would also transition - and panning triggers too many zoom events for transitions to perform as desired. One option we have is to trigger a transition only if the source event was a click. This modification might look like:
function zoomed() {
// Was the event a click?
var event = d3.event.sourceEvent ? d3.event.sourceEvent.type : null;
// Get the new zoom transform
transform = d3.event.transform;
// Apply the new transform to the projection
projection.translate([transform.x,transform.y]).scale(transform.k);
// Redraw the features based on the updaed projection:
(event == "click") ? g.selectAll("path").transition().attr("d",path) : g.selectAll("path").attr("d", path);
}
I'm a freshman using js and d3. How can I zoom a scatter plot matrix chart?
What I did,
I used svg to show the scatter plot matrix, following the example https://bl.ocks.org/Fil/6d9de24b31cb870fed2e6178a120b17d
Since the performance was too bad when the records over 10 thousands and the matrix size is 10*10, I changed the point draw with canvas
Axes are using svg and the dots are drew by canvas
Even if it spends some time to draw the chart with canvas, the page will not hang and oom when running 10*10 matrix over 10 thousands records
I'm not sure it's a formal way to do this.
On the other hand, I want to zoom this chart but I have no idea how can I do this.
From my understanding,
The axes splits into several parts according to the number of matrix, such as 10, including x-axis and y-axis
If I want to rescale each matrix cell, such as the cell on row 0 and column 0, how can I do this? I just tired with d3.event.transform.rescaleX/rescaleY to rescale the axes, this seems to work but how can I do on the canvas, how can I get the valid dots to redraw?
On the other hand, if I only want to zoom the whole chart not the single cell(That means, if I click on cell(0,0), this cell will zoom until it fill the whole chart and other cells will not be seen), how can I do this? I used modal to show the scaled large svg by viewBox="0 0 ' + width * scalar + ' ' + height, is there any other way to show image in large format?
draw_spm = function(data) {
var width = 700, traits = d3.keys(data[0]),
domain = {}, n = traits.length,
pointRadius = 1;
var size = width / n,
padding = size / 10;
var x = d3.scaleLinear().range([padding / 2, size - padding / 2]),
y = d3.scaleLinear().range([size - padding / 2, padding / 2]);
var x_axis = d3.axisBottom().scale(x).ticks(6),
y_axis = d3.axisLeft().scale(y).ticks(6);
traits.forEach(function(t) {
domain[t] = d3.extent(data, function(d) { return d[t]; });
});
x_axis.tickSize(size * n);
y_axis.tickSize(-size * n);
var zoom = d3.zoom()
.on('zoom', zoomed);
var svg = d3.select('#spm-svg')
.attr('class', 'plot svg-scatterplot')
.attr('width', size * n + 4*padding)
.attr('height', size * n + 4*padding)
.append('g')
.attr('transform', 'translate('+4*padding+','+padding/2+')');
var x_axis_svg = svg.selectAll('.x.axis')
.data(traits)
.enter().append('g')
.attr('class', 'x axis')
.attr('transform', function(d, i) { return 'translate('+size*i+',0)'; })
.each(function(d) { x.domain(domain[d]); d3.select(this).call(x_axis); });
var y_axis_svg = svg.selectAll('.y.axis')
.data(traits)
.enter().append('g')
.attr('class', 'y axis')
.attr('transform', function(d, i) { return 'translate(0,'+size*i+')'; })
.each(function(d) { y.domain(domain[d]); d3.select(this).call(y_axis); });
var canvas = d3.select('#spm-canvas')
.attr('width', size*n+4*padding)
.attr('height', size*n+4*padding)
.style('transform', 'translate('+4*padding+','+padding/2+')');
var ctx = canvas.node().getContext('2d');
ctx.fillStyle = 'steelblue';
var cell = svg.selectAll('.cell')
.data(cross(traits, traits))
.enter().append('g')
.attr('class', 'cell')
.attr('transform', function(d) {
return 'translate(' + d.i*size + ',' + d.j*size + ')';
})
.each(draw);
canvas.call(zoom).on('dblclick.zoom', null);
function draw(p) {
var cell = d3.select(this);
ctx.resetTransform();
ctx.transform(1, 0, 0, 1, p.i*size+4*padding, p.j*size+padding/2);
x.domain(domain[p.x]);
y.domain(domain[p.y]);
function draw_point(d) {
ctx.beginPath();
ctx.arc(x(d[p.x]), y(d[p.y]), pointRadius, 0, 2*Math.PI);
ctx.closePath();
ctx.fill();
ctx.stroke();
}
cell.append('rect')
.attr('class', 'frame')
.attr('x', padding / 2)
.attr('y', padding / 2)
.attr('width', size - padding)
.attr('height', size - padding);
data.forEach(function(d) {
draw_point(d);
});
}
function zoomed() {
// how to do this?
};
function cross(a, b) {
var c = [], n = a.length, m = b.length, i, j;
for (i = -1; ++i < n;)
for (j = -1; ++j < m;)
c.push({x: a[i], i: i, y: b[j], j: j});
return c;
}
};
cols = ['x0','x1','x2','x3','x4'];
function _data() {
var d = {};
for (var i = 0; i < cols.length; i++) {
d[cols[i]] = Math.floor(Math.random() * 10000);
}
return d;
}
var data = [];
for (var i = 0; i < 1000; i++) {
data[i] = _data();
}
draw_spm(data);
.svg-scatterplot .axis,.frame {shape-rendering:crispEdges;}
.svg-scatterplot .axis line {stroke:#ddd;}
.svg-scatterplot .axis path {display:none;}
.svg-scatterplot .cell text {font-weight:bold;text-transform: capitalize;fill: black;}
.svg-scatterplot .frame {fill:none;stroke:#aaa;}
.svg-scatterplot circle {fill-opacity:.7;}
.svg-scatterplot circle.hidden {fill:#ccc !important;}
.svg-scatterplot .extent {fill:#000;fill-opacity:.125;stroke:#fff;}
.plot {position: absolute;}
#spm-canvas {z-index: 2;}
#spm-svg {z-index: 1;}
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg id="spm-svg" class="plot"></svg>
<canvas id="spm-canvas" class="plot"></canvas>
Thanks for your help