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.
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'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.
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
I'm trying to rework a pen (http://codepen.io/anon/pen/JgyCz) by Travis Palmer so that I can use it on multiple elements. We are trying to place several <div class="donut" data-donut="x">'s on a page.
So it would look similar to the html below:
////// HTML
<div class="donut" data-donut="22"></div>
<div class="donut" data-donut="48"></div>
<div class="donut" data-donut="75></div>
The D3.js / jQuery example I'm trying to convert to a reusable compunent is below. (To see full working example go to this link - http://codepen.io/anon/pen/JgyCz)
////// D3.js
var duration = 500,
transition = 200;
drawDonutChart(
'.donut',
$('.donut').data('donut'),
290,
290,
".35em"
);
function drawDonutChart(element, percent, width, height, text_y) {
width = typeof width !== 'undefined' ? width : 290;
height = typeof height !== 'undefined' ? height : 290;
text_y = typeof text_y !== 'undefined' ? text_y : "-.10em";
var dataset = {
lower: calcPercent(0),
upper: calcPercent(percent)
},
radius = Math.min(width, height) / 2,
pie = d3.layout.pie().sort(null),
format = d3.format(".0%");
var arc = d3.svg.arc()
.innerRadius(radius - 20)
.outerRadius(radius);
var svg = d3.select(element).append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var path = svg.selectAll("path")
.data(pie(dataset.lower))
.enter().append("path")
.attr("class", function(d, i) { return "color" + i })
.attr("d", arc)
.each(function(d) { this._current = d; }); // store the initial values
var text = svg.append("text")
.attr("text-anchor", "middle")
.attr("dy", text_y);
if (typeof(percent) === "string") {
text.text(percent);
}
else {
var progress = 0;
var timeout = setTimeout(function () {
clearTimeout(timeout);
path = path.data(pie(dataset.upper)); // update the data
path.transition().duration(duration).attrTween("d", function (a) {
// Store the displayed angles in _current.
// Then, interpolate from _current to the new angles.
// During the transition, _current is updated in-place by d3.interpolate.
var i = d3.interpolate(this._current, a);
var i2 = d3.interpolate(progress, percent)
this._current = i(0);
return function(t) {
text.text( format(i2(t) / 100) );
return arc(i(t));
};
}); // redraw the arcs
}, 200);
}
};
function calcPercent(percent) {
return [percent, 100-percent];
};
The best way to do this is to use angular directives. An angular directive basically wraps html inside a custom tag and let's you stamp the directive over and over across multiple pages or multiple times a page. See this video: http://www.youtube.com/watch?v=aqHBLS_6gF8
There is also a library that is out called nvd3.js that contains prebuilt angular directives that can be re-used: http://nvd3.org/
Hope this helps.
ok, I figured it out. I feel a bit dumb in hindsight, but what can I say, I'm a js n00b. All you have to do is make a few more call to the drawDonutChart() method. In short:
drawDonutChart(
'#donut1',
$('#donut1').data('donut'),
220,
220,
".35em"
);
drawDonutChart(
'#donut2',
$('#donut2').data('donut'),
120,
120,
".35em"
);
drawDonutChart(
'#donut3',
$('#donut3').data('donut'),
150,
150,
".2em"
);
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.