How to make SVG responsive in D3 with viewbox? - javascript

I have noticed that in various examples, an SVG is responsive (changes size in response to changes in window size) and sometimes it is not responsive when using viewbox / preserveAspectRatio.
Here is a very simple example. I am using viewbox and preseverAspectiRatio on the SVG element like every other example, but yet, it is not responsive, why?
<html>
<meta charset="utf-8">
<body>
<div id ="chart"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<script>
var svgContainer = d3.select("#chart")
.append("svg")
.attr("width", 200)
.attr("height", 200)
.attr("viewBox", "0 0 100 100")
.attr("preserveAspectRatio", "xMinYMin meet")
//Draw the Circle
var circle = svgContainer.append("circle")
.attr("cx", 50)
.attr("cy", 50)
.attr("r", 50);
</script>
</body>
</html>

Currently your svg is not resize because you have specified a fixed with for the svg container of 200x200.
var svgContainer = d3.select("#chart")
.append("svg")
.attr("width", 200) // setting fixed width
.attr("height", 200) // setting fixed width
One solution is to change these to percentages, which will rescale to the size of its parent.
var svgContainer = d3.select("#chart")
.append("svg")
.attr("width", '100%') // percent width
.attr("height", 100%) // percent height
Another possible solution is to use the Window#resize event, and change the size of the svg based on the changes.
window.addEventListener('resize', function () {
// select svg and resize accordingly
});
I should add that in chrome you can use ResizeObserver to watch for changes to the svg parents size, and resize the svg accordingly from that.
const resizeOb = new ResizeObserver((entries: any[]) => {
for (const entry of entries) {
const cr = entry.contentRect;
const width = cr.width; // parent width
const height = cr.height; // parent height
// resize svg
}
});
this.resizeOb.observe(svgParentElement);

Related

D3 Convention for (re)drawing a single object

I'm using D3 to draw a custom graphs with a border rectangle around it. On window resize, I recalculate the SVG size and want to redraw this border. I'm currently doing this in a drawGraph() function which gets called on resize / new data:
...
svg.selectAll('rect')
.data([true])
.attr('width', w)
.attr('height', h)
.enter()
.append('rect')
.attr('x', 0)
.attr('y', 0)
.attr('width', w)
.attr('height', h)
.style('fill', 'none')
...
i.e. - I am binding a single value in an array ([true]) to the selection so that I can either draw the box, or just change its size if it's already been drawn once. It certainly works, but it feels slightly odd to bind data which is guaranteed to be ignored, and possibly a slightly hacky way of doing things.
Is this a well-used convention, or is there another standard way of doing this, such as:
A D3 method which I've not come across
Just using svg.append('rect') and then removing + redrawing it on resize
svg.selectAll('*').remove() and redrawing everything from scratch?
Anything else
Removing and redrawing elements is what I call lazy coding or lazy update in D3.
That being said, why don't you simply assign the rectangle to a variable and change it on resize? Something like this (click "full page" and resize the window):
var div = document.getElementById("div");
var svg = d3.select("svg")
var rect = svg.append("rect")//here you define your rectangle
.attr("width", 300)
.attr("height", 150)
.attr("stroke-width", 3)
.attr("stroke", "black")
.attr("fill", "none");
function resize() {
var width = div.clientWidth;
svg.attr("width", width)
.attr("height", width / 2);
//here you just change its width and height
rect.attr("width", width)
.attr("height", width / 2);
}
window.addEventListener("resize", resize);
svg {
background-color: tan;
}
<script src="//d3js.org/d3.v4.min.js"></script>
<div id="div">
<svg></svg>
</div>

D3.js pattern not working with arc or donut graph

I'm trying to have an image fill my donut chart, then rotate the image 60 degrees from its center.
I've had success filling a simple shape as a pattern with this method, but the pattern image gets all screwy and repeats itself when applied to a donut chart. The image is 300px x 300px - same size as the svg. The final result should look like this.
Here's my fiddle.
imgPath = "http://www.mikeespo.com/statDonkey/inner.png";
w = 300;
h = 300;
passingPercent = 60;
rotateStartPosition = 50;
var myScale = d3.scale.linear().domain([0, 100]).range([0, 2 * Math.PI]);
// MAKES SVG
var svg = d3.select("body")
.append("svg")
.attr("id", "svg_donut")
.attr("width", w)
.attr("height", h);
// MAKE DEFS
var defs = d3.select("#svg_donut")
.append("defs");
// MAKES PATTERN
defs.append('pattern')
.attr('id', 'pic1')
.attr('width', 300)
.attr('height', 300)
.attr('patternUnits', 'userSpaceOnUse')
.append('svg:image')
.attr('xlink:href', imgPath)
.attr("width", 300)
.attr("height", 300)
.attr("transform", "rotate(60 150 150)")
.attr("x", 0)
.attr("y", 0);
// CREATES VARIABLE *VIS* TO SVG
var vis = d3.select("#svg_donut");
// DEFINES DONUT GRAPH
var arc = d3.svg.arc()
.innerRadius(95)
.outerRadius(140)
.startAngle((myScale(0 + rotateStartPosition)))
.endAngle((myScale(passingPercent + rotateStartPosition)));
// APPENDS *VIS* TO SVG
vis.append("path")
.attr("id", "passing")
.attr("d", arc)
.attr("fill", "white")
.attr("transform", "translate(150,150)")
.attr("fill", "url(#pic1)");
I'm not exactly sure why this works to be honest but when I changed the width and height of the pattern element and removed the patternUnits attribute, I was able to achieve the desired look:
defs.append('pattern')
.attr('id', 'pic1')
.attr('width', 1)
.attr('height', 1)
.append('svg:image')
.attr('xlink:href', imgPath)
.attr("width", 300)
.attr("height", 300)
.attr("transform", "rotate(00 150 150)")
.attr("x", 0)
.attr("y", 0);
I don't understand it completely but it has something to do with the coordinate system and the way in which the pattern scales to the object you're applying it to. The width and height aren't defining the size of the image as you might initially think, but the way in which the pattern will map to the new coordinate system of the donut. A width and height of 1 indicates that the pattern will just scale to the width and height of the donut.
Getting my info from here and admittedly not fully grasping it all yet but hopefully this will help: https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Patterns

D3js - Reposition tree graph in viewbox

I have used viewbox to enable my tree graph to be able to resized based on the browser window size. However, I could not position the tree to be at the center of the view box. I have tried to apply transform-translate attribute to the graph, the position remain unchanged. What attribute/changes do I have to apply to position the tree graph? Below, the first image is the current output. Image two shows the desired output. I have attached the initialisation code for the tree graph.
var margin = { top: 40, right: 120, bottom: 20, left: 120 };
var width = 700 - margin.right - margin.left;
var height = 500 - margin.top - margin.bottom;
var i = 0, duration = 750;
var tree = d3.layout.tree()
.nodeSize([110,110])
var diagonal = d3.svg.diagonal()
.projection(function (d) { return [d.x, d.y]; });
var svg = d3.select(treeContainerDom)
.append("div")
.classed("svg-container", true) //container class to make it responsive
.append("svg")
//responsive SVG needs these 2 attributes and no width and height attr
.attr("preserveAspectRatio", "xMinYMin meet")
.attr("viewBox", "-50 0 1000 1000")
//.attr("viewBox", "0 0 "+width+" "+height)
//class to make it responsive
.classed("svg-content-responsive", true)
.call(zm = d3.behavior.zoom().scaleExtent([0.5,2]).on("zoom", redraw)).append("g");

Internet Explorer Scaling not working properly

I have a map created with d3. I set it to have the width of the parent div (with id map) and height with a ratio of 5/9 to to the width. The viewBox has been set to "0 0 width height".
Here is the setup:
var width = $("#map").width(),
height = width * 500 / 900,
active = d3.select(null);
var projection = d3.geo.albersUsa()
.scale(width)
.translate([width / 2, height / 2]);
var path = d3.geo.path()
.projection(projection);
projection = d3.geo.albersUsa()
.scale(width)
.translate([width / 2, height / 2]);
path = d3.geo.path()
.projection(projection);
svg = d3.select("#map").append("svg")
.attr("width", "100%")
.attr("height", "100%")
.attr("viewBox", "0 0 " + width + " " + height)
g = svg.append("g");
Here is how it looks like in Chrome:
But it looks like this in IE:
The scaling is completely messed up. I believe it has to do with the viewBox. Could someone explain how to solve this issue?
Thanks!
The problem you are having is that height: "100%" is handled differently by Chrome and IE. You have smartly set your height variable to be based on your width and aspect ratio, but to take advantage of this, you'll need to use these values when setting the dimensions of your svg as well as the viewBox:
var svg = d3.select("#map").append("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", "0 0 " + width + " " + height);

D3 geometric zoom when there is no SVG element under the cursor

I am implementing a geometric zoom behaviour as seen in this example
The problem is that if the cursor is on a white spot outside the green overlay rect or any other SVG element (line, circle etc.) the mousewheel event gets intercepted by the browser and scrolls down the page.
I would like to be able to freely zoom independently of where I am on the visualisation.
Here is a simplified jsFiddle recreating the problem.
var width = 300,
height = 300;
var randomX = d3.random.normal(width / 2, 80),
randomY = d3.random.normal(height / 2, 80);
var data = d3.range(2000).map(function() {
return [
randomX(),
randomY()
];
});
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.call(d3.behavior.zoom().scaleExtent([-8, 8]).on("zoom", zoom))
.append("g");
svg.append("rect")
.attr("class", "overlay")
.attr("width", width)
.attr("height", height);
svg.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("r", 2.5)
.attr("transform", function(d) { return "translate(" + d + ")"; });
function zoom() {
svg.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
Hope this isn't too late, I missed this question the first time around.
The reason it isn't working under Chrome is because Chrome hasn't yet implemented the standard CSS transform on html elements -- and as strange as it is to understand, the outermost <svg> tag on an SVG element embedded in a webpage is treated as an HTML element for layout purposes.
You have two options:
Use Chrome's custom transform syntax, -webkit-transform in addition to the regular transform syntax:
http://jsfiddle.net/aW9xC/5/
Kind of jumpy, since you are transforming the entire SVG and readjusting the page layout accordingly. For reasons I don't understand neither the CSS/webkit transform nor the SVG attribute transform work when applied to the "innerSVG" element.
Replace the nested SVG structure with an SVG <g> group element, which Chrome has no problem transforming:
http://jsfiddle.net/aW9xC/4/
Stick a transparent rect in front of everything so the mouse event has something to latch on to. In SVG events are only sent to rendered elements such as rects and not to the general unrendered background.
svg.append("rect")
.attr("fill", "none")
.attr("pointer-events", "all")
.attr("width", "100%")
.attr("height", "100%");
In order to make this work properly the SVG would have to cover the whole area so to get the same look as your original fiddle you'd want to clip to the original area which can be done either by setting a clipPath or (as I've done in the fiddle) by creating an innser <svg> element which will clip.
var svg = d3.select("body").append("svg")
.attr("width", "100%")
.attr("height", "100%")
.call(d3.behavior.zoom().scaleExtent([-8, 8]).on("zoom", zoom));
svg = svg.append("svg")
.attr("width", width)
.attr("height", height)
So altogether it looks like this...

Categories