Related
Assume I have a histogram script that builds a 960 500 svg graphic. How do I make this responsive so on resize the graphic widths and heights are dynamic?
<script>
var n = 10000, // number of trials
m = 10, // number of random variables
data = [];
// Generate an Irwin-Hall distribution.
for (var i = 0; i < n; i++) {
for (var s = 0, j = 0; j < m; j++) {
s += Math.random();
}
data.push(s);
}
var histogram = d3.layout.histogram()
(data);
var width = 960,
height = 500;
var x = d3.scale.ordinal()
.domain(histogram.map(function(d) { return d.x; }))
.rangeRoundBands([0, width]);
var y = d3.scale.linear()
.domain([0, d3.max(histogram.map(function(d) { return d.y; }))])
.range([0, height]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
svg.selectAll("rect")
.data(histogram)
.enter().append("rect")
.attr("width", x.rangeBand())
.attr("x", function(d) { return x(d.x); })
.attr("y", function(d) { return height - y(d.y); })
.attr("height", function(d) { return y(d.y); });
svg.append("line")
.attr("x1", 0)
.attr("x2", width)
.attr("y1", height)
.attr("y2", height);
</script>
Full example histogram gist is:
https://gist.github.com/993912
There's another way to do this that doesn't require redrawing the graph, and it involves modifying the viewBox and preserveAspectRatio attributes on the <svg> element:
<svg id="chart" viewBox="0 0 960 500"
preserveAspectRatio="xMidYMid meet">
</svg>
Update 11/24/15: most modern browsers can infer the aspect ratio of SVG elements from the viewBox, so you may not need to keep the chart's size up to date. If you need to support older browsers, you can resize your element when the window resizes like so:
var aspect = width / height,
chart = d3.select('#chart');
d3.select(window)
.on("resize", function() {
var targetWidth = chart.node().getBoundingClientRect().width;
chart.attr("width", targetWidth);
chart.attr("height", targetWidth / aspect);
});
And the svg contents will be scaled automatically. You can see a working example of this (with some modifications) here: just resize the window or the bottom right pane to see how it reacts.
Look for 'responsive SVG' it is pretty simple to make a SVG responsive and you don't have to worry about sizes any more.
Here is how I did it:
d3.select("div#chartId")
.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", "0 0 600 400")
//class to make it responsive
.classed("svg-content-responsive", true);
The CSS code:
.svg-container {
display: inline-block;
position: relative;
width: 100%;
padding-bottom: 100%; /* aspect ratio */
vertical-align: top;
overflow: hidden;
}
.svg-content-responsive {
display: inline-block;
position: absolute;
top: 10px;
left: 0;
}
More info / tutorials:
http://demosthenes.info/blog/744/Make-SVG-Responsive
http://soqr.fr/testsvg/embed-svg-liquid-layout-responsive-web-design.php
I've coded up a small gist to solve this.
The general solution pattern is this:
Breakout the script into computation and drawing functions.
Ensure the drawing function draws dynamically and is driven of
visualisation width and height variables (The best way to do this is
to use the d3.scale api)
Bind/chain the drawing to a reference
element in the markup. (I used jquery for this, so imported it).
Remember to remove it if it's already drawn. Get the dimensions from
the referenced element using jquery.
Bind/chain the draw function to
the window resize function. Introduce a debounce (timeout) to this
chain to ensure we only redraw after a timeout.
I also added the minified d3.js script for speed.
The gist is here: https://gist.github.com/2414111
jquery reference back code:
$(reference).empty()
var width = $(reference).width();
Debounce code:
var debounce = function(fn, timeout)
{
var timeoutID = -1;
return function() {
if (timeoutID > -1) {
window.clearTimeout(timeoutID);
}
timeoutID = window.setTimeout(fn, timeout);
}
};
var debounced_draw = debounce(function() {
draw_histogram(div_name, pos_data, neg_data);
}, 125);
$(window).resize(debounced_draw);
Enjoy!
Without Using ViewBox
Here is an example of a solution that does not rely on using a viewBox:
The key is in updating the range of the scales which are used to place data.
First, calculate your original aspect ratio:
var ratio = width / height;
Then, on each resize, update the range of x and y:
function resize() {
x.rangeRoundBands([0, window.innerWidth]);
y.range([0, window.innerWidth / ratio]);
svg.attr("height", window.innerHeight);
}
Note that the height is based on the width and the aspect ratio, so that your original proportions are maintained.
Finally, "redraw" the chart – update any attribute that depends on either of the x or y scales:
function redraw() {
rects.attr("width", x.rangeBand())
.attr("x", function(d) { return x(d.x); })
.attr("y", function(d) { return y.range()[1] - y(d.y); })
.attr("height", function(d) { return y(d.y); });
}
Note that in re-sizing the rects you can use the upper-bound of the range of y, rather than explicitly using the height:
.attr("y", function(d) { return y.range()[1] - y(d.y); })
var n = 10000, // number of trials
m = 10, // number of random variables
data = [];
// Generate an Irwin-Hall distribution.
for (var i = 0; i < n; i++) {
for (var s = 0, j = 0; j < m; j++) {
s += Math.random();
}
data.push(s);
}
var histogram = d3.layout.histogram()
(data);
var width = 960,
height = 500;
var ratio = width / height;
var x = d3.scale.ordinal()
.domain(histogram.map(function(d) {
return d.x;
}))
var y = d3.scale.linear()
.domain([0, d3.max(histogram, function(d) {
return d.y;
})])
var svg = d3.select("body").append("svg")
.attr("width", "100%")
.attr("height", height);
var rects = svg.selectAll("rect").data(histogram);
rects.enter().append("rect");
function redraw() {
rects.attr("width", x.rangeBand())
.attr("x", function(d) {
return x(d.x);
})
// .attr("y", function(d) { return height - y(d.y); })
.attr("y", function(d) {
return y.range()[1] - y(d.y);
})
.attr("height", function(d) {
return y(d.y);
});
}
function resize() {
x.rangeRoundBands([0, window.innerWidth]);
y.range([0, window.innerWidth / ratio]);
svg.attr("height", window.innerHeight);
}
d3.select(window).on('resize', function() {
resize();
redraw();
})
resize();
redraw();
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
Lots of complex answers here.
Basically all you need to do is ditch the width and height attributes in favor of the viewBox attribute:
width = 500;
height = 500;
const svg = d3
.select("#chart")
.append("svg")
.attr("viewBox", `0 0 ${width} ${height}`)
If you have margins, you can just add them there into the width/height then just append the g thereafter and transform it like you would normally.
If you are using d3.js through c3.js the solution to the responsiveness issue is quite straightforward :
var chart = c3.generate({bindTo:"#chart",...});
chart.resize($("#chart").width(),$("#chart").height());
where the generated HTML looks like :
<div id="chart">
<svg>...</svg>
</div>
In the case that you are using a d3 wrapper like plottable.js, be aware that the easiest solution might be adding an event listener and then calling a redraw function (redraw in plottable.js). In the case of plottable.js this will work excellently (this approach is poorly documented):
window.addEventListener("resize", function() {
table.redraw();
});
Shawn Allen's answer was great. But you may not want to do this every single time. If you host it on vida.io, you get automatic responsive for your svg visualization.
You can get responsive iframe with this simple embed code:
<div id="vida-embed">
<iframe src="http://embed.vida.io/documents/9Pst6wmB83BgRZXgx" width="auto" height="525" seamless frameBorder="0" scrolling="no"></iframe>
</div>
#vida-embed iframe {
position: absolute;
top:0;
left: 0;
width: 100%;
height: 100%;
}
http://jsfiddle.net/dnprock/npxp3v9d/1/
Disclosure: I build this feature at vida.io.
In case people are still visiting this question - here’s what worked for me:
Enclose the iframe in a div and use css to add a padding of, say, 40% to that div (the percentage depending on the aspect ratio you want). Then set both width and height of the iframe itself to 100%.
In the html doc containing the chart to be loaded in the iframe, set width to the width of the div that the svg is appended to (or to the width of the body) and set height to width * aspect ratio.
Write a function that reloads the iframe content upon window resize, so as to adapt the size of the chart when people rotate their phone.
Example here on my website:
http://dirkmjk.nl/en/2016/05/embedding-d3js-charts-responsive-website
UPDATE 30 Dec 2016
The approach I described above has some drawbacks, especially that it doesn’t take the height into account of any title and captions that are not part of the D3-created svg. I’ve since come across what I think is a better approach:
Set the width of the D3 chart to the width of the div it’s attached to and use the aspect ratio to set its height accordingly;
Have the embedded page send its height and url to the parent page using HTML5’s postMessage;
On the parent page, use the url to identify the corresponding iframe (useful if you have more than one iframe on your page) and update its height to the height of the embedded page.
Example here on my website: http://dirkmjk.nl/en/2016/12/embedding-d3js-charts-responsive-website-better-solution
One of the basic principles of the D3 data-join is that it is idempotent. In other words, if you repeatedly evaluate a data-join with the same data, the rendered output is the same. Therefore, as long as you render your chart correctly, taking care withe your enter, update and exit selections - all you have to do when the size changes, is re-render the chart in its entirety.
There are a couple of other things you should do, one is de-bounce the window resize handler in order to throttle it. Also, rather than hard-coding widths / heights, this should be achieved by measuring the containing element.
As an alternative, here is your chart rendered using d3fc, which is a set of D3 components that correctly handle data-joins. It also has a cartesian chart that measures it containing element making it easy to create 'responsive' charts:
// create some test data
var data = d3.range(50).map(function(d) {
return {
x: d / 4,
y: Math.sin(d / 4),
z: Math.cos(d / 4) * 0.7
};
});
var yExtent = fc.extentLinear()
.accessors([
function(d) { return d.y; },
function(d) { return d.z; }
])
.pad([0.4, 0.4])
.padUnit('domain');
var xExtent = fc.extentLinear()
.accessors([function(d) { return d.x; }]);
// create a chart
var chart = fc.chartSvgCartesian(
d3.scaleLinear(),
d3.scaleLinear())
.yDomain(yExtent(data))
.yLabel('Sine / Cosine')
.yOrient('left')
.xDomain(xExtent(data))
.xLabel('Value')
.chartLabel('Sine/Cosine Line/Area Chart');
// create a pair of series and some gridlines
var sinLine = fc.seriesSvgLine()
.crossValue(function(d) { return d.x; })
.mainValue(function(d) { return d.y; })
.decorate(function(selection) {
selection.enter()
.style('stroke', 'purple');
});
var cosLine = fc.seriesSvgArea()
.crossValue(function(d) { return d.x; })
.mainValue(function(d) { return d.z; })
.decorate(function(selection) {
selection.enter()
.style('fill', 'lightgreen')
.style('fill-opacity', 0.5);
});
var gridlines = fc.annotationSvgGridline();
// combine using a multi-series
var multi = fc.seriesSvgMulti()
.series([gridlines, sinLine, cosLine]);
chart.plotArea(multi);
// render
d3.select('#simple-chart')
.datum(data)
.call(chart);
You can see it in action in this codepen:
https://codepen.io/ColinEberhardt/pen/dOBvOy
where you can resize the window and verify that the chart is correctly re-rendered.
Please note, as a full disclosure, I am one of the maintainers of d3fc.
I would avoid resize/tick solutions like the plague since they are inefficient and can cause issues in your app (e.g. a tooltip re-calculates the position it should appear on window resize, then a moment later your chart resizes too and the page re-layouts and now your tooltip is wrong again).
You can simulate this behaviour in some older browsers that don't properly support it like IE11 too using a <canvas> element which maintains it's aspect.
Given 960x540 which is an aspect of 16:9:
<div style="position: relative">
<canvas width="16" height="9" style="width: 100%"></canvas>
<svg viewBox="0 0 960 540" preserveAspectRatio="xMidYMid meet" style="position: absolute; top: 0; right: 0; bottom: 0; left: 0; -webkit-tap-highlight-color: transparent;">
</svg>
</div>
You can also use bootstrap 3 to adapt the size of a visualization. For example, we can set up the HTML code as:
<div class="container>
<div class="row">
<div class='col-sm-6 col-md-4' id="month-view" style="height:345px;">
<div id ="responsivetext">Something to write</div>
</div>
</div>
</div>
I have set up a fixed height because of my needs, but you can leave the size auto as well. The "col-sm-6 col-md-4" makes the div responsive for different devices. You can learn more at http://getbootstrap.com/css/#grid-example-basic
We can access the graph with the help of the id month-view.
I won't go into much detail about the d3 code, I will only input the part that is needed for adapting to different screen sizes.
var width = document.getElementById('month-view').offsetWidth;
var height = document.getElementById('month-view').offsetHeight - document.getElementById('responsivetext2').offsetHeight;
The width is set by getting the width of the div with the id month-view.
The height in my case should not include the entire area. I also have some text above the bar so I need to calculate that area as well. That's why I identified the area of the text with the id responsivetext. For calculating the allowed height of the bar, I subtracted the height of the text from the height of the div.
This allows you to have a bar that will adopt all the different screen/div sizes. It might not be the best way of doing it, but it surely works for the needs of my project.
I am plotting points on a Google Map (so far so good) and then plotting a line between each point based on the point order in the underlying data (not so good). Like a trail or route.
Unlike the handful of examples I've seen doing this, my data are not in GeoJSON format, and I would really like to keep it that way if at all possible. I have tried to adapt the exampels posted here and here but without success.
My results end up with no lines being drawn, and I can't tell if that's because of a projection error or something else syntactical with D3. I have tried to debug through console.log() statements, but I am very week on GIS projections.
Here is the code to plots the points
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="initial-scale=1.0, user-scalable=no"/>
<script type="text/javascript" src="https://maps.google.com/maps/api/js?sensor=true"></script>
<script src="../js/d3.v3.min.js"></script>
<style type="text/css">
html, body, #map {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
.markers {
position: absolute;
}
svg.pts {
position: absolute;
}
.markers border {
position: absolute;
stroke: black;
stroke-width: 2px;
}
.markers svg.pts {
width: 60px;
height: 20px;
padding-right: 100px;
font: 10px sans-serif;
}
.markers circle {
fill: brown;
stroke: black;
stroke-width: 1.5px;
}
.SvgOverlay path {
stroke: Orange;
stroke-width: 2px;
fill: Orange;
fill-opacity: .3;
}
</style>
</head>
<body>
<div id="map"></div>
<script type="text/javascript">
var map = new google.maps.Map(d3.select("#map").node(), {
zoom: 15,
center: new google.maps.LatLng(29.371397, -81.54938), //N/S E/W
mapTypeId: google.maps.MapTypeId.ROADMAP
});
var data = [ //note this is not in GeoJSON format
{name:"pt1",lng:-81.55082967,lat:29.374915304},
{name:"pt2",lng:-81.55211713,lat:29.373504039},
{name:"pt3",lng:-81.5842252,lat:29.417969924},
{name:"pt4",lng:-81.55230021,lat:29.374245073},
{name:"pt5",lng:-81.55115,lat:29.37263},
{name:"pt6",lng:-81.58737814,lat:29.358476912},
{name:"pt7",lng:-81.59230268,lat:29.359308171},
{name:"pt8",lng:-81.58783883,lat:29.356449048},
{name:"pt9",lng:-81.58189168,lat:29.420264027},
{name:"pt10",lng:-81.58288,lat:29.4202},
{name:"pt11",lng:-81.56079477,lat:29.359527893},
{name:"pt12",lng:-81.55861145,lat:29.356670068},
{name:"pt13",lng:-81.57961314,lat:29.420893275},
{name:"pt14",lng:-81.579302,lat:29.419368},
{name:"pt15",lng:-81.55979967,lat:29.359768002},
{name:"pt16",lng:-81.55823261,lat:29.36122515},
{name:"pt17",lng:-81.58189168,lat:29.420264027},
{name:"pt18",lng:-81.57997524,lat:29.421120323},
{name:"pt19",lng:-81.58148399,lat:29.420030491},
{name:"pt20",lng:-81.57839075,lat:29.420766158},
{name:"pt21",lng:-81.57982489,lat:29.42002304},
{name:"pt22",lng:-81.580266,lat:29.420212},
{name:"pt23",lng:-81.5820392,lat:29.42048164},
{name:"pt24",lng:-81.57894731,lat:29.420509033},
{name:"pt25",lng:-81.57819629,lat:29.418834169}
];
var overlay = new google.maps.OverlayView();
overlay.onAdd = function() {
var layer = d3.select(this.getPanes().overlayLayer).append("div")
.attr("height", "100%")
.attr("width", "100%")
.attr("class", "markers")
.attr("id", "layer");
layer[0][0].style.width = "1366px";
layer[0][0].parentNode.style.width = "100%";
layer[0][0].parentNode.style.height = "100%";
layer[0][0].parentNode.parentNode.style.width = "100%";
layer[0][0].parentNode.parentNode.style.height = "100%";
layer[0][0].parentNode.parentNode.parentNode.style.width = "100%";
layer[0][0].parentNode.parentNode.parentNode.style.height = "100%";
layer[0][0].parentNode.parentNode.parentNode.parentNode.style.width = "100%";
layer[0][0].parentNode.parentNode.parentNode.parentNode.style.height = "100%";
// Add points
overlay.draw = function() {
var projection = this.getProjection(),
padding = 10;
var point = layer.selectAll("svg")
.data( data )
.each(transform) // update existing markers
.enter().append("svg:svg")
.each(transform)
.attr("class", "point pts")
// Add marker on points
point.append("svg:circle")
.attr("r", 4.5)
.attr("cx", padding )
.attr("cy", padding );
// Add a label on points
point.append("svg:text")
.attr("x", padding + 7)
.attr("y", padding)
.attr("dy", ".31em")
.text( function(d) {
return d.name; }
);
//Here is where I'd like to add lines connecting the points, in order
//of appearance in the data object
function _projection( lat, lng ) {
e = new google.maps.LatLng( lat, lng );
e = projection.fromLatLngToDivPixel(e);
return [ e.x - padding, e.y - padding]
// return [ e.x, e.y ]
}
function transform(d) {
//console.log(d);
e = _projection( d.lat, d.lng )
return d3.select(this)
.style("left", e[0] + "px")
.style("top", e[1] + "px");
}
};
};
// Bind overlay to the map…
overlay.setMap(map);
</script>
</body>
</html>
And here is a JSFiddle
Suggestions to get the path added via the data object as presented are most appreciated.
Ok, so I took a look at your code and refactored it a bit. But here is a basic working version of a path drawn between the points: http://jsfiddle.net/AJvt4/3/. There is one caveat though, and that is that the overlayPane doesn't expand when the map pans. I'm not too familiar with google maps so not sure how much I can help there. Here's an explanation of the changes made:
First I created a encompassing svg to house all of your d3 elements in the onAdd event:
var svg = layer.append('svg')
.attr('x', 0)
.attr('y', 0)
Also in the onAdd event I added a d3 path generator (you can read more here):
var lineFn = d3.svg.line()
.x(function (d) {
e = _projection(d.lat, d.lng);
return e[0] + padding
})
.y(function (d) {
e = _projection(d.lat, d.lng);
return e[1] + padding
})
To actually draw the line, I added this in the add event handler:
var line = svg.selectAll('.path').data([data])
line.enter().append('path')
line.attr('class', 'path')
.attr('d', lineFn)
It's important to note the array around the data ([data]). This is because d3 expects an array of arrays with each inner array holding points to a line. This makes it easier to draw multiple lines. In your case there is only one line.
You'll notice a few other changes to make the code a bit more d3-esque. Hope that helps get you started!
I'm drawing a scatterplot with d3.js. With the help of this question :
Get the size of the screen, current web page and browser window
I'm using this answer :
var w = window,
d = document,
e = d.documentElement,
g = d.getElementsByTagName('body')[0],
x = w.innerWidth || e.clientWidth || g.clientWidth,
y = w.innerHeight|| e.clientHeight|| g.clientHeight;
So I'm able to fit my plot to the user's window like this :
var svg = d3.select("body").append("svg")
.attr("width", x)
.attr("height", y)
.append("g");
Now I'd like that something takes care of resizing the plot when the user resize the window.
PS : I'm not using jQuery in my code.
Look for 'responsive SVG' it is pretty simple to make a SVG responsive and you don't have to worry about sizes any more.
Here is how I did it:
d3.select("div#chartId")
.append("div")
// Container class to make it responsive.
.classed("svg-container", true)
.append("svg")
// Responsive SVG needs these 2 attributes and no width and height attr.
.attr("preserveAspectRatio", "xMinYMin meet")
.attr("viewBox", "0 0 600 400")
// Class to make it responsive.
.classed("svg-content-responsive", true)
// Fill with a rectangle for visualization.
.append("rect")
.classed("rect", true)
.attr("width", 600)
.attr("height", 400);
.svg-container {
display: inline-block;
position: relative;
width: 100%;
padding-bottom: 100%; /* aspect ratio */
vertical-align: top;
overflow: hidden;
}
.svg-content-responsive {
display: inline-block;
position: absolute;
top: 10px;
left: 0;
}
svg .rect {
fill: gold;
stroke: steelblue;
stroke-width: 5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="chartId"></div>
Note: Everything in the SVG image will scale with the window width. This includes stroke width and font sizes (even those set with CSS). If this is not desired, there are more involved alternate solutions below.
More info / tutorials:
http://thenewcode.com/744/Make-SVG-Responsive
http://soqr.fr/testsvg/embed-svg-liquid-layout-responsive-web-design.php
Use window.onresize:
function updateWindow(){
x = w.innerWidth || e.clientWidth || g.clientWidth;
y = w.innerHeight|| e.clientHeight|| g.clientHeight;
svg.attr("width", x).attr("height", y);
}
d3.select(window).on('resize.updatesvg', updateWindow);
http://jsfiddle.net/Zb85u/1/
UPDATE just use the new way from #cminatti
old answer for historic purposes
IMO it's better to use select() and on() since that way you can have multiple resize event handlers... just don't get too crazy
d3.select(window).on('resize', resize);
function resize() {
// update width
width = parseInt(d3.select('#chart').style('width'), 10);
width = width - margin.left - margin.right;
// resize the chart
x.range([0, width]);
d3.select(chart.node().parentNode)
.style('height', (y.rangeExtent()[1] + margin.top + margin.bottom) + 'px')
.style('width', (width + margin.left + margin.right) + 'px');
chart.selectAll('rect.background')
.attr('width', width);
chart.selectAll('rect.percent')
.attr('width', function(d) { return x(d.percent); });
// update median ticks
var median = d3.median(chart.selectAll('.bar').data(),
function(d) { return d.percent; });
chart.selectAll('line.median')
.attr('x1', x(median))
.attr('x2', x(median));
// update axes
chart.select('.x.axis.top').call(xAxis.orient('top'));
chart.select('.x.axis.bottom').call(xAxis.orient('bottom'));
}
http://eyeseast.github.io/visible-data/2013/08/28/responsive-charts-with-d3/
It's kind of ugly if the resizing code is almost as long as the code for building the graph in first place. So instead of resizing every element of the existing chart, why not simply reloading it? Here is how it worked for me:
function data_display(data){
e = document.getElementById('data-div');
var w = e.clientWidth;
// remove old svg if any -- otherwise resizing adds a second one
d3.select('svg').remove();
// create canvas
var svg = d3.select('#data-div').append('svg')
.attr('height', 100)
.attr('width', w);
// now add lots of beautiful elements to your graph
// ...
}
data_display(my_data); // call on page load
window.addEventListener('resize', function(event){
data_display(my_data); // just call it again...
}
The crucial line is d3.select('svg').remove();. Otherwise each resizing will add another SVG element below the previous one.
In force layouts simply setting the 'height' and 'width' attributes will not work to re-center/move the plot into the svg container. However, there's a very simple answer that works for Force Layouts found here. In summary:
Use same (any) eventing you like.
window.on('resize', resize);
Then assuming you have svg & force variables:
var svg = /* D3 Code */;
var force = /* D3 Code */;
function resize(e){
// get width/height with container selector (body also works)
// or use other method of calculating desired values
var width = $('#myselector').width();
var height = $('#myselector').height();
// set attrs and 'resume' force
svg.attr('width', width);
svg.attr('height', height);
force.size([width, height]).resume();
}
In this way, you don't re-render the graph entirely, we set the attributes and d3 re-calculates things as necessary. This at least works when you use a point of gravity. I'm not sure if that's a prerequisite for this solution. Can anyone confirm or deny ?
Cheers, g
If you want to bind custom logic to resize event, nowadays you may start using ResizeObserver browser API for the bounding box of an SVGElement.
This will also handle the case when container is resized because of the nearby elements size change.
There is a polyfill for broader browser support.
This is how it may work in UI component:
function redrawGraph(container, { width, height }) {
d3
.select(container)
.select('svg')
.attr('height', height)
.attr('width', width)
.select('rect')
.attr('height', height)
.attr('width', width);
}
// Setup observer in constructor
const resizeObserver = new ResizeObserver((entries, observer) => {
for (const entry of entries) {
// on resize logic specific to this component
redrawGraph(entry.target, entry.contentRect);
}
})
// Observe the container
const container = document.querySelector('.graph-container');
resizeObserver.observe(container)
.graph-container {
height: 75vh;
width: 75vw;
}
.graph-container svg rect {
fill: gold;
stroke: steelblue;
stroke-width: 3px;
}
<script src="https://unpkg.com/resize-observer-polyfill#1.5.1/dist/ResizeObserver.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<figure class="graph-container">
<svg width="100" height="100">
<rect x="0" y="0" width="100" height="100" />
</svg>
</figure>
// unobserve in component destroy method
this.resizeObserver.disconnect()
For those using force directed graphs in D3 v4/v5, the size method doesn't exist any more. Something like the following worked for me (based on this github issue):
simulation
.force("center", d3.forceCenter(width / 2, height / 2))
.force("x", d3.forceX(width / 2))
.force("y", d3.forceY(height / 2))
.alpha(0.1).restart();
I can't seem to find any other examples which have a similar application to what I'm trying to do, so I thought I'd just ask.
I'm trying to mock up a funky mind map tool using the awesome d3.js library from mike bostock. I'm still trying to learn d3 and as it also turns out, basic coding! What I want to have is a blank canvas, and 3 buttons; 'add', 'remove' & 'edit'. Very basic I hope!
When you select the 'add' button (the top image), and then click on the blank canvas, a node will be added. If you click again nearby, another node will be added and will be linked to the first node.
Selecting the 'remove' button (the middle image) then clicking on a node will delete that node and all touching links.
Selecting the 'edit' button (the bottom image) will allow you to label nodes.
I have step 1 down, and half of step 2. The problem I'm running into goes like this:
Click 'add' button once, add function turned 'on'. Works
Add some nodes. Works
Click 'add' button again, add function turned 'off'. Works
Click the canvas, no nodes are added, but existing nodes are drag-able. Works
Click 'remove' button once, remove function turned 'on'. Works
Click a node to remove it. Broken. Removes everything.
Click 'remove' button again, remove function turned 'off'. Broken
Click 'add' button again, add function turned 'on'. Broken
Does anyone have any suggestions as to why I'm having this problem & how they would go about solving this? I think it has something to do with confusion between state selection. When turning off the 'remove' function, it calls the same function as when you turn off the 'add' function, so it doesnt know what to do and does nothing... I thought that they should not be mutually selectable, but is one state remaining on after it is switched on? I am really stumped :(
I hope there may be parts of this that are useful to other people as well.
Thanks!
Seb
.js below..>>>>
//==D3 STUFFS ======================================================
//height & width of the interactive area
var divh = document.getElementById('container').offsetHeight;
var divw = document.getElementById('container').offsetWidth;
//node size
var radius = 20;
//define the nodes and links as empty data sets
var nodes = [];
var links = [];
//place the interactive area onto the browser UI with the dimensions defined above
var interactiveArea = d3.select("#container").append("svg:svg").attr("width", divw).attr("height", divh);
//enable dragging of node elements
var drag = d3.behavior.drag()
.origin(Object)
.on("drag", dragmove);
//define the physics parameters that will take effect on the nodes and links
var force = d3.layout.force()
.gravity(0.01)
.charge(-80)
.linkDistance(60)
.nodes(nodes)
.links(links)
.size([divw, divh]);
//apply the physics parameters defined above on the nodes and links
force.on("tick", function()
{
interactiveArea.selectAll("line.link")
.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
interactiveArea.selectAll("circle.node")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
});
//update the position of the object on drag
function dragmove(d)
{
d3.select(this)
.attr("cx", d.x = Math.max(radius, Math.min(divw - radius, d3.event.x)))
.attr("cy", d.y = Math.max(radius, Math.min(divh - radius, d3.event.y)));
}
//update the force layout
function update()
{
interactiveArea.selectAll("line.link")
.data(links)
.enter().insert("svg:line", "circle.node")
.attr("class", "link")
.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
interactiveArea.selectAll("circle.node")
.data(nodes)
.enter().insert("svg:circle", "circle.cursor")
.attr("class", "node")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", 10)
.call(force.drag);
force.start();
}
//==============================================================
//==BUTTON & EVENT SELECTOR=======================================
var addCounter = 0;
var removeCounter = 0;
var editCounter = 0;
function addButton_Off()
{
//alert("ADD - off");
document.images["add-button"].src = "love.lost.PNG";
all_Off();
return true;
}
function removeButton_Off()
{
//alert("REMOVE - off");
document.images["remove-button"].src = "love.lost.PNG";
//all_Off();
return true;
}
function editButton_Off()
{
//alert("EDIT - off");
document.images["edit-button"].src = "love.lost.PNG";
return true;
}
function addButton()
{
addCounter++;
if (addCounter%2 == 0)
addButton_Off();
else
addButton_On();
if (removeCounter%2 == 1)
removeCounter++;
removeButton_Off();
if (editCounter%2 == 1)
editCounter++;
editButton_Off();
function addButton_On()
{
//alert("ADD - on");
document.images["add-button"].src = "pop.cloud.PNG";
add_Nodes();
return true;
}
}
function removeButton()
{
removeCounter++;
if (removeCounter%2 == 0)
removeButton_Off();
else
removeButton_On();
if (addCounter%2 == 1)
addCounter++;
addButton_Off();
if (editCounter%2 == 1)
editCounter++;
editButton_Off();
function removeButton_On()
{
//alert("REMOVE - on");
document.images["remove-button"].src = "pop.cloud.PNG";
remove_Nodes();
return true;
}
}
function editButton()
{
editCounter++;
if (editCounter%2 == 0)
editButton_Off();
else
editButton_On();
if (addCounter%2 == 1)
addCounter++;
addButton_Off();
if (removeCounter%2 == 1)
removeCounter++;
removeButton_Off();
function editButton_On()
{
//alert("EDIT - on");
document.images["edit-button"].src = "pop.cloud.PNG";
return true;
}
}
//=============================================================
//==EVENT ACTIONS========================================================
function all_Off()
{
interactiveArea.on("mousedown", function()
{
update();
});
}
function add_Nodes()
{
//do the following actions when the mouse is clicked on the interactiveArea
interactiveArea.on("mousedown", function()
{
// add a node under the mouse cursor
var point = d3.svg.mouse(this),
node = {x: point[0], y: point[1]},
n = nodes.push(node);
nodes.forEach(function(target)
{
var x = target.x - node.x,
y = target.y - node.y;
//if there is a node less than 30 pixels? away, add a link between the 2 nodes
if (Math.sqrt(x * x + y * y) < 30)
{
// add links to any nearby nodes
links.push({source: node, target: target});
}
});
update();
});
}
function remove_Nodes()
{
interactiveArea.on("click", function()
{
var point = d3.select(this);
point.remove();
update();
});
}
//function edit_Nodes()
//==========================================================
html below...>>>>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
<link type="text/css" rel="stylesheet" href="style.css">
<script src="http://code.jquery.com/jquery-latest.js"></script>
</head>
<body>
<div id="enclosure">
<div id="title">
/
</div>
<div id="button-menu">
<a onMouseDown="return addButton()">
<img name="add-button" id="add-button-img" src="love.lost.PNG" width="80px" height="80px" border = "0" alt="fuchs">
</a>
<a onMouseDown="return removeButton()">
<img name="remove-button" id="remove-button-img" src="love.lost.PNG" width="80px" height="80px" border = "0" alt="fuchs">
</a>
<a onMouseDown="return editButton()">
<img name="edit-button" id="edit-button-img" src="love.lost.PNG" width="80px" height="80px" border = "0" alt="fuchs">
</a>
</div>
<div id="container">
<script type="text/javascript" src="http://mbostock.github.com/d3/talk/20111116/d3/d3.js"></script>
<script type="text/javascript" src="http://mbostock.github.com/d3/talk/20111116/d3/d3.geom.js"></script>
<script type="text/javascript" src="http://mbostock.github.com/d3/talk/20111116/d3/d3.layout.js"></script>
<script type="text/javascript" src="bonquiqui.js"></script>
<!--<script type="text/javascript" src="origin.js"></script>-->
</div>
</div>
</body>
</html>
css below..>>>>
body {
font: 300 36px "Lane - Posh";
height: 100%;
width: 100%;
margin: auto;
overflow: hidden;
position: absolute;
text-align: center;
background: #fff;
}
#enclosure {
margin-top: 3%;
}
#title {
background: #fff;
font: 300 220% "Lane - Posh";
height: 100px;
width: 60%;
margin-left: auto;
margin-right: auto;
}
#button-menu {
background: #eee;
height: 20%;
width: 4%;
position: absolute;
top: 48.0%;
left: 81%;
}
#add-button {
cursor: pointer;
position: relative;
top: 5%;
}
#remove-button {
cursor: pointer;
position: relative;
top: 5%;
}
#edit-button {
cursor: pointer;
position: relative;
top: 5%;
}
#container {
height: 60%;
width: 60%;
margin: auto;
margin-top: 1%;
background: #eee;
overflow: hidden;
}
circle.node
{
cursor: pointer;
stroke: #000;
stroke-width: .5px;
}
line.link
{
fill: none;
stroke: #9ecae1;
stroke-width: 1.5px;
}
Assume I have a histogram script that builds a 960 500 svg graphic. How do I make this responsive so on resize the graphic widths and heights are dynamic?
<script>
var n = 10000, // number of trials
m = 10, // number of random variables
data = [];
// Generate an Irwin-Hall distribution.
for (var i = 0; i < n; i++) {
for (var s = 0, j = 0; j < m; j++) {
s += Math.random();
}
data.push(s);
}
var histogram = d3.layout.histogram()
(data);
var width = 960,
height = 500;
var x = d3.scale.ordinal()
.domain(histogram.map(function(d) { return d.x; }))
.rangeRoundBands([0, width]);
var y = d3.scale.linear()
.domain([0, d3.max(histogram.map(function(d) { return d.y; }))])
.range([0, height]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
svg.selectAll("rect")
.data(histogram)
.enter().append("rect")
.attr("width", x.rangeBand())
.attr("x", function(d) { return x(d.x); })
.attr("y", function(d) { return height - y(d.y); })
.attr("height", function(d) { return y(d.y); });
svg.append("line")
.attr("x1", 0)
.attr("x2", width)
.attr("y1", height)
.attr("y2", height);
</script>
Full example histogram gist is:
https://gist.github.com/993912
There's another way to do this that doesn't require redrawing the graph, and it involves modifying the viewBox and preserveAspectRatio attributes on the <svg> element:
<svg id="chart" viewBox="0 0 960 500"
preserveAspectRatio="xMidYMid meet">
</svg>
Update 11/24/15: most modern browsers can infer the aspect ratio of SVG elements from the viewBox, so you may not need to keep the chart's size up to date. If you need to support older browsers, you can resize your element when the window resizes like so:
var aspect = width / height,
chart = d3.select('#chart');
d3.select(window)
.on("resize", function() {
var targetWidth = chart.node().getBoundingClientRect().width;
chart.attr("width", targetWidth);
chart.attr("height", targetWidth / aspect);
});
And the svg contents will be scaled automatically. You can see a working example of this (with some modifications) here: just resize the window or the bottom right pane to see how it reacts.
Look for 'responsive SVG' it is pretty simple to make a SVG responsive and you don't have to worry about sizes any more.
Here is how I did it:
d3.select("div#chartId")
.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", "0 0 600 400")
//class to make it responsive
.classed("svg-content-responsive", true);
The CSS code:
.svg-container {
display: inline-block;
position: relative;
width: 100%;
padding-bottom: 100%; /* aspect ratio */
vertical-align: top;
overflow: hidden;
}
.svg-content-responsive {
display: inline-block;
position: absolute;
top: 10px;
left: 0;
}
More info / tutorials:
http://demosthenes.info/blog/744/Make-SVG-Responsive
http://soqr.fr/testsvg/embed-svg-liquid-layout-responsive-web-design.php
I've coded up a small gist to solve this.
The general solution pattern is this:
Breakout the script into computation and drawing functions.
Ensure the drawing function draws dynamically and is driven of
visualisation width and height variables (The best way to do this is
to use the d3.scale api)
Bind/chain the drawing to a reference
element in the markup. (I used jquery for this, so imported it).
Remember to remove it if it's already drawn. Get the dimensions from
the referenced element using jquery.
Bind/chain the draw function to
the window resize function. Introduce a debounce (timeout) to this
chain to ensure we only redraw after a timeout.
I also added the minified d3.js script for speed.
The gist is here: https://gist.github.com/2414111
jquery reference back code:
$(reference).empty()
var width = $(reference).width();
Debounce code:
var debounce = function(fn, timeout)
{
var timeoutID = -1;
return function() {
if (timeoutID > -1) {
window.clearTimeout(timeoutID);
}
timeoutID = window.setTimeout(fn, timeout);
}
};
var debounced_draw = debounce(function() {
draw_histogram(div_name, pos_data, neg_data);
}, 125);
$(window).resize(debounced_draw);
Enjoy!
Without Using ViewBox
Here is an example of a solution that does not rely on using a viewBox:
The key is in updating the range of the scales which are used to place data.
First, calculate your original aspect ratio:
var ratio = width / height;
Then, on each resize, update the range of x and y:
function resize() {
x.rangeRoundBands([0, window.innerWidth]);
y.range([0, window.innerWidth / ratio]);
svg.attr("height", window.innerHeight);
}
Note that the height is based on the width and the aspect ratio, so that your original proportions are maintained.
Finally, "redraw" the chart – update any attribute that depends on either of the x or y scales:
function redraw() {
rects.attr("width", x.rangeBand())
.attr("x", function(d) { return x(d.x); })
.attr("y", function(d) { return y.range()[1] - y(d.y); })
.attr("height", function(d) { return y(d.y); });
}
Note that in re-sizing the rects you can use the upper-bound of the range of y, rather than explicitly using the height:
.attr("y", function(d) { return y.range()[1] - y(d.y); })
var n = 10000, // number of trials
m = 10, // number of random variables
data = [];
// Generate an Irwin-Hall distribution.
for (var i = 0; i < n; i++) {
for (var s = 0, j = 0; j < m; j++) {
s += Math.random();
}
data.push(s);
}
var histogram = d3.layout.histogram()
(data);
var width = 960,
height = 500;
var ratio = width / height;
var x = d3.scale.ordinal()
.domain(histogram.map(function(d) {
return d.x;
}))
var y = d3.scale.linear()
.domain([0, d3.max(histogram, function(d) {
return d.y;
})])
var svg = d3.select("body").append("svg")
.attr("width", "100%")
.attr("height", height);
var rects = svg.selectAll("rect").data(histogram);
rects.enter().append("rect");
function redraw() {
rects.attr("width", x.rangeBand())
.attr("x", function(d) {
return x(d.x);
})
// .attr("y", function(d) { return height - y(d.y); })
.attr("y", function(d) {
return y.range()[1] - y(d.y);
})
.attr("height", function(d) {
return y(d.y);
});
}
function resize() {
x.rangeRoundBands([0, window.innerWidth]);
y.range([0, window.innerWidth / ratio]);
svg.attr("height", window.innerHeight);
}
d3.select(window).on('resize', function() {
resize();
redraw();
})
resize();
redraw();
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
Lots of complex answers here.
Basically all you need to do is ditch the width and height attributes in favor of the viewBox attribute:
width = 500;
height = 500;
const svg = d3
.select("#chart")
.append("svg")
.attr("viewBox", `0 0 ${width} ${height}`)
If you have margins, you can just add them there into the width/height then just append the g thereafter and transform it like you would normally.
If you are using d3.js through c3.js the solution to the responsiveness issue is quite straightforward :
var chart = c3.generate({bindTo:"#chart",...});
chart.resize($("#chart").width(),$("#chart").height());
where the generated HTML looks like :
<div id="chart">
<svg>...</svg>
</div>
In the case that you are using a d3 wrapper like plottable.js, be aware that the easiest solution might be adding an event listener and then calling a redraw function (redraw in plottable.js). In the case of plottable.js this will work excellently (this approach is poorly documented):
window.addEventListener("resize", function() {
table.redraw();
});
Shawn Allen's answer was great. But you may not want to do this every single time. If you host it on vida.io, you get automatic responsive for your svg visualization.
You can get responsive iframe with this simple embed code:
<div id="vida-embed">
<iframe src="http://embed.vida.io/documents/9Pst6wmB83BgRZXgx" width="auto" height="525" seamless frameBorder="0" scrolling="no"></iframe>
</div>
#vida-embed iframe {
position: absolute;
top:0;
left: 0;
width: 100%;
height: 100%;
}
http://jsfiddle.net/dnprock/npxp3v9d/1/
Disclosure: I build this feature at vida.io.
In case people are still visiting this question - here’s what worked for me:
Enclose the iframe in a div and use css to add a padding of, say, 40% to that div (the percentage depending on the aspect ratio you want). Then set both width and height of the iframe itself to 100%.
In the html doc containing the chart to be loaded in the iframe, set width to the width of the div that the svg is appended to (or to the width of the body) and set height to width * aspect ratio.
Write a function that reloads the iframe content upon window resize, so as to adapt the size of the chart when people rotate their phone.
Example here on my website:
http://dirkmjk.nl/en/2016/05/embedding-d3js-charts-responsive-website
UPDATE 30 Dec 2016
The approach I described above has some drawbacks, especially that it doesn’t take the height into account of any title and captions that are not part of the D3-created svg. I’ve since come across what I think is a better approach:
Set the width of the D3 chart to the width of the div it’s attached to and use the aspect ratio to set its height accordingly;
Have the embedded page send its height and url to the parent page using HTML5’s postMessage;
On the parent page, use the url to identify the corresponding iframe (useful if you have more than one iframe on your page) and update its height to the height of the embedded page.
Example here on my website: http://dirkmjk.nl/en/2016/12/embedding-d3js-charts-responsive-website-better-solution
One of the basic principles of the D3 data-join is that it is idempotent. In other words, if you repeatedly evaluate a data-join with the same data, the rendered output is the same. Therefore, as long as you render your chart correctly, taking care withe your enter, update and exit selections - all you have to do when the size changes, is re-render the chart in its entirety.
There are a couple of other things you should do, one is de-bounce the window resize handler in order to throttle it. Also, rather than hard-coding widths / heights, this should be achieved by measuring the containing element.
As an alternative, here is your chart rendered using d3fc, which is a set of D3 components that correctly handle data-joins. It also has a cartesian chart that measures it containing element making it easy to create 'responsive' charts:
// create some test data
var data = d3.range(50).map(function(d) {
return {
x: d / 4,
y: Math.sin(d / 4),
z: Math.cos(d / 4) * 0.7
};
});
var yExtent = fc.extentLinear()
.accessors([
function(d) { return d.y; },
function(d) { return d.z; }
])
.pad([0.4, 0.4])
.padUnit('domain');
var xExtent = fc.extentLinear()
.accessors([function(d) { return d.x; }]);
// create a chart
var chart = fc.chartSvgCartesian(
d3.scaleLinear(),
d3.scaleLinear())
.yDomain(yExtent(data))
.yLabel('Sine / Cosine')
.yOrient('left')
.xDomain(xExtent(data))
.xLabel('Value')
.chartLabel('Sine/Cosine Line/Area Chart');
// create a pair of series and some gridlines
var sinLine = fc.seriesSvgLine()
.crossValue(function(d) { return d.x; })
.mainValue(function(d) { return d.y; })
.decorate(function(selection) {
selection.enter()
.style('stroke', 'purple');
});
var cosLine = fc.seriesSvgArea()
.crossValue(function(d) { return d.x; })
.mainValue(function(d) { return d.z; })
.decorate(function(selection) {
selection.enter()
.style('fill', 'lightgreen')
.style('fill-opacity', 0.5);
});
var gridlines = fc.annotationSvgGridline();
// combine using a multi-series
var multi = fc.seriesSvgMulti()
.series([gridlines, sinLine, cosLine]);
chart.plotArea(multi);
// render
d3.select('#simple-chart')
.datum(data)
.call(chart);
You can see it in action in this codepen:
https://codepen.io/ColinEberhardt/pen/dOBvOy
where you can resize the window and verify that the chart is correctly re-rendered.
Please note, as a full disclosure, I am one of the maintainers of d3fc.
I would avoid resize/tick solutions like the plague since they are inefficient and can cause issues in your app (e.g. a tooltip re-calculates the position it should appear on window resize, then a moment later your chart resizes too and the page re-layouts and now your tooltip is wrong again).
You can simulate this behaviour in some older browsers that don't properly support it like IE11 too using a <canvas> element which maintains it's aspect.
Given 960x540 which is an aspect of 16:9:
<div style="position: relative">
<canvas width="16" height="9" style="width: 100%"></canvas>
<svg viewBox="0 0 960 540" preserveAspectRatio="xMidYMid meet" style="position: absolute; top: 0; right: 0; bottom: 0; left: 0; -webkit-tap-highlight-color: transparent;">
</svg>
</div>
You can also use bootstrap 3 to adapt the size of a visualization. For example, we can set up the HTML code as:
<div class="container>
<div class="row">
<div class='col-sm-6 col-md-4' id="month-view" style="height:345px;">
<div id ="responsivetext">Something to write</div>
</div>
</div>
</div>
I have set up a fixed height because of my needs, but you can leave the size auto as well. The "col-sm-6 col-md-4" makes the div responsive for different devices. You can learn more at http://getbootstrap.com/css/#grid-example-basic
We can access the graph with the help of the id month-view.
I won't go into much detail about the d3 code, I will only input the part that is needed for adapting to different screen sizes.
var width = document.getElementById('month-view').offsetWidth;
var height = document.getElementById('month-view').offsetHeight - document.getElementById('responsivetext2').offsetHeight;
The width is set by getting the width of the div with the id month-view.
The height in my case should not include the entire area. I also have some text above the bar so I need to calculate that area as well. That's why I identified the area of the text with the id responsivetext. For calculating the allowed height of the bar, I subtracted the height of the text from the height of the div.
This allows you to have a bar that will adopt all the different screen/div sizes. It might not be the best way of doing it, but it surely works for the needs of my project.