Responsive d3 bubble chart - d3.js

I'm trying to implement a responsive d3 bubble chart that updates its container svg (and the nodes it contains) while the page is being resized (kinda like this shows: http://jsfiddle.net/63K4n/)
But instead of just shrinking the nodes down to a size that fits the screen, I want to increase the height of the container as the width is being decreased to create the additional space necessary for the nodes to fall into without being shrunk.
I played with updating the last few lines of the fiddle to this...
chart.attr("width", targetWidth);
chart.attr("height", Math.round(921600/targetWidth)); //was targetWidth/aspectRatio
(where 921,600 was simply the diameter [960] squared - i.e. the total required space)
But of course although this increases the height of the container correctly, the nodes don't exploit the new space - that's what I'm struggling to figure out - how to change the shape of the bubble chart to fit the new space (if that's even possible!)
If not possible with a standard bubble chart, I'd also be happy to try a static force layout (e.g. http://bl.ocks.org/mbostock/1667139) combined with a resizable example like http://bl.ocks.org/mbostock/3355967. Again the problem is finding a way to reshape the nodes to within the confines of their new container, so I'm guessing I'd have to adapt collision detection to account for the svg container boundaries. And with all that going on I'm pretty sure there'd be a decent lag on a mobile device while you're waiting for the force layout to tick its way to something I can update the page with.
Sorry if that's all a little vague, but I'm really just hoping someone has a general suggestion on an appropriate way to tackle the problem. Huge thanks for any thoughts at all!

When a resize event occurs call a function with:
d3.selectAll('#chart svg').remove()
to clear the current chart then pass the new height/size info into a update function that redraws the chart. Its kinda hard to give you a more specific answer without your code and where you're stuck...but that's generally how I redraw my D3 stuff when data or other groovy things change.
Ok, based on the two fiddles/code pens it looks like
var diameter = 500, // <---Feed new variable here
format = d3.format(",d"),
color = d3.scale.category20c();
var width = 450, height = 2000;
var bubble = d3.layout.pack()
.sort(null)
.size([diameter, diameter])
.padding(1.5);
var svg = d3.select("body").append("svg")
.attr("viewBox","0 0 500 500") //<----and Here
.attr("width", diameter)
.attr("height", diameter)
.attr("class", "bubble");
Changing the diameter and the veiwBox attribute makes everything shrink in the proper proportion. Sorry if i miss understood the question, and if I'm still not getting it. But a redraw function that feeds in new data there based on a change in window should work.
Also, just changing the
var height = XXX
doesn't look like its changing the height, since the
.attr('width',diameter)
is using diameter instead of height.
Anyway, I hope that helps.

Related

Fit D3 map in SVG

In this jsfiddle I have a D3 map that I took from here, but I'm trying to fit it in an svg that is half the original size. For that, I changed:
var width = 480;
var height = 300;
.....
var path = d3.geoPath(d3.geoIdentity().translate([width/2, height/2]).scale(height*.5));
But it's not working. How to make the map fit the svg?
D3's geoIdentity exposes almost all the standard methods of a d3 projection (off the top of my head, only rotation is not possible as the identity assumes cartesian data). Most importantly here, it exposes the fitSize and fitExtent methods. These methods set translate and scale based on the coordinate extent of displayed geojson data and the pixel extent of the svg/canvas.
To scale your features with a geo identity you can use:
d3.geoIdentity().fitSize([width,height],geojsonObject)
Note that an array of geojson features won't work, but a geojson feature collection or any individual feature/geometry object works too. The width and height are of the svg/canvas.
If you want to apply a margin, you can use:
d3.geoIdentity().fitExtent([[margin,margin],[width-margin,height-margin]],geojsonObject)
The margin doesn't need to be uniform, the format is [[left,top],[right,bottom]],geojsonObject
If using fitSize or fitExtent there is no need to set center, translate or scale manually and setting these afterwards will recenter or rescale the map.

I want to be able to zoom in and out at the same time proportionally

So I want to be able to zoom one thing out (my scale) and one thing in (my slider) so that when my slider that was on April of 2005 stays on April of 2005 even as the scale it is on zooms out to a broader perspective (like going from showing just 2005 to showing 2000-2010), the scale zooms out and the slider(which is a brush by the way) zooms in proportionally.
The difference between my problem and the Brush and Zoom example
is that I want the scale underneath to change, the graph over it to stay the same, and the brush to change proportionally to the scale change.
My problem is the event.transform object that I use to zoom, it gives me a K value and an X value that I can't figure out how to reverse engineer, is there any way around this?
Since someone wanted some code to look at:
function zoomScale() {
let t = d3.event.transform;
if(t) {
xScale1.domain(t.rescaleX(xScale2).domain());
context.select(".axis--x").call(xAxis2);
context.select(".brush").call(brush.move, xScale2.range().map(t.invertX, t));
}
}
This makes the scale bigger AND the brush bigger, I want the brush to get smaller proportionally to how much bigger the scale gets.
Ok so I finally figured it out. Every time you have a selection on the scale you save it as a variable like in my case latestBrushDateSelection. You will also need a new TimeScale that saves the original domain of your axis, in my case xScale3. Then the Zoom function will look like this:
function zoomChart() {
const t = d3.event.transform
xScale2.domain(t.rescaleX(xScale3).domain())
context.select('.axis--x').call(xAxis2)
context.select('.brush').call(brush.move, latestBrushDateSelection.map(d => xScale2(d)))
}

D3 Time based scales overflowing axis

I've been working on a similar stacked bar chart to that in the example here https://bl.ocks.org/mbostock/1134768 however running d3 v4 as shown in the fiddle here https://jsfiddle.net/z75L7cfz/7/
However as you can see in the fiddle, I have a problem that the last element in the graph extends off the end of the axis. I mean I guess this makes sense. But I'm not sure how to make this show in the graph and ensure all the elements fit into the supplied width of the graph.
I assume I need to modify the domain of the x-axis which is currently
x.domain(d3.extent(data, function(d) { return d.Date; }));
But other than somehow manually adding an extra data point with no data in but the subsequent date I'm not sure how to manage this. If anyone could supply any pointers it would be hugely appreciated!
For bar charts, it is best to use band scale. It calculates the bar sizes, location and padding.
var x = d3.scaleBand()
.range([0, width])
.padding(0.05);
Here is the updated fiddle:
https://jsfiddle.net/dtb02eze/2/
Actually, there is nothing wrong with the code per se. The problem here is the very concept of a time scale, and the use of a bar chart with such scale.
If you look closely in your fiddle, each bar starts exactly where it should start, that is, the x value of each bar corresponds to the temporal value in the x axis. But a bar has a width, and here lies the problem: when you set the width to 18px (or any other value), the bar will overflow the time scale. That's normal and expected.
A time scale should be used with dots, where every dot (adimentional) sits exactly in the correct position of the time scale, or with a line, which is simply a path connecting the dots.
Having said all that, you have two options:
Use an ordinal scale. A bar chart is not supposed to use a time scale, and a time scale is not supposed to be used with a bar chart. Bostock's code, for instance, uses an ordinal scale:
var x = d3.scale.ordinal()
You can use scaleOrdinal or, better yet, scaleBand.
If you want to stick to the time scale, change your chart to a line chart. Technically speaking, a line chart is the best option to visualize a trend in data over intervals of time.

sync d3.js map and leaflet map

In one application I have both d3 map and leaflet map, I follow Mike's code to sync them(http://bost.ocks.org/mike/leaflet/) and draw svg points on leaflet map. The problem is that some points being cutoff at the edge of SVG container, and other SVG elements added later on (an animated pulse in this case) will only partially shown. I wonder if there's anyway to expand the SVG container based on the features (points) bound.
a working demo is here: http://xqin1.github.io/usschools/usac_school_stat.html, to reproduce the issue:
1. select the 'Number of Students' slider bar to 5300-6545, the two points on Leaftlet map only half shown.
2. click the first table row, map will zoom to the point, but the animated pulse being cut off.
Any suggestions are highly appreciated.
thanks
xiaoming
I had this problem as well, and found a solution. Referencing mbostock's sample code you just need to add a little bit of padding to the projected bounding box that is calculated each time reset is called:
var bottomLeft = project(bounds[0]),
topRight = project(bounds[1]);
Insert something like this (just after the above declarations):
var padding = 25; // In pixels, choose large enough to prevent edge clipping
// of your largest element
bottomLeft = [bottomLeft[0]-padding, bottomLeft[1]+padding]
topRight = [topRight[0]+padding, topRight[1]-padding]

D3 force layout: making pan on drag (zoom) smoother

I've got a d3.js static force layout graph that can get rather big (sometimes parts of it are clipped), so I'd like to let the user pan the whole graph by dragging. I don't think I need dragging of individual nodes, I've got a feeling that's just going to be confusing, but would like to make it possible to show the parts of the graph that are clipped by the svg boundaries.
I've got a minimal example at http://bl.ocks.org/3811811 which uses
visF.append("rect")
.attr("class", "background")
.attr("width", width)
.attr("height", height)
.call(d3.behavior.zoom().on("zoom", redrawVisF));
function redrawVisF () {
visF.attr("transform","translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")");
}
to implement the panning, but I find it really "skittery" and not very smooth at all, to the point where I'm guessing it will stop people from trying the drag function at all. Has anyone got a clue why this happens and/or an idea for how to fix it?
The problem is that d3.behavior.zoom retrieves the current mouse position relative to the clicked item's container element, and you are moving the container element! So the relative position is constantly changing, hence the jittering effect you're seeing.
You probably want to move the background <rect> so that it's a direct child of the <svg> element. This achieves two things:
The position will now be relative to the <svg> container, which isn't moving.
Currently, you are moving the <rect> when you zoom or pan, so the zoomable area changes and some parts of the viewport are no longer zoomable. Having the background <rect> in the same place fixes this problem too.

Resources