d3 v2 Prevent Zooming and Panning Chart Outside Viewport - javascript

I've been looking at all the examples I can find that deal with this and I've been trying to get this to work with my chart with no success so far.
I have the chart panning and zooming behaviour working but at the moment I can pan and zoom the contents of the chart well outside the bounds of the viewport.
You can see a demo of this in this fiddle:
http://jsfiddle.net/Jammer/wvL422zq/
All I'm trying to do is prevent the extremes of data being scrolled completely out of view and their appears to be so many completely different examples of getting this to work that I'm struggling to get the method that will work for my chart.
The Panning and Zooming is handled with this at the moment:
var zoom = d3.behavior.zoom()
.x(x)
.scaleExtent([1, 30])
.scale(2)
.on("zoom", function () {
svg.select(".x.axis").call(xAxis);
svg.selectAll("path.lines")
.attr("d", function(d) { return line(d.values); });
svg.selectAll("circle")
.attr("cx", function(d) { return x(d.date); })
.attr("cy", function(d) { return y(d.value); });
});
A lot of the examples out there appear to be for older or newer versions of d3 as well.
How do I prevent the chart contents from being zoomed or panned out of the viewport?

You can restrict the panning in the zoom function. I only done on one side. You might want to use the scale for the other side. I leave it to you. Here is my solution.
Edit: I done the other side also. It was tricky.
var zoom = d3.behavior.zoom()
.x(x)
.scaleExtent([1, 30])
.scale(2)
.on("zoom", function () {
var panX = d3.event.translate[0];
var panY = d3.event.translate[1];
var scale = d3.event.scale;
panX = panX > 10 ? 10 : panX;
var maxX = -(scale-1)*width-10;
panX = panX < maxX ? maxX : panX;
zoom.translate([panX, panY]);
console.log("x: "+panX+" scale: "+scale);
//console.log("x: "+(panX/scale));
svg.select(".x.axis").call(xAxis);
svg.selectAll("path.lines")
.attr("d", function(d) { return line(d.values); });
svg.selectAll("circle")
.attr("cx", function(d) { return x(d.date); })
.attr("cy", function(d) { return y(d.value); });
});

Related

d3js v4: Scale circles with zoom

I have a world map made with d3js v4 and topojson which has Zoom / Drag / Circles. Everything seems fine except I cant scale the circles togheter with the zoom.
When I scroll into the map, my circles stay at the same size, which makes them way to big compared to the map.
How can I apply the transformation to the circles when I zoom?
var width = 660,
height = 400;
var zoom = d3.zoom()
.scaleExtent([1, 10])
.on("zoom", zoomed);
var projection = d3.geoMercator()
.center([50, 10]) //long and lat starting position
.scale(150) //starting zoom position
.rotate([10,0]); //where world split occurs
var svg = d3.select("svg")
.attr("width", width)
.attr("height", height)
.call(zoom);
var path = d3.geoPath()
.projection(projection);
var g = svg.append("g");
//Zoom functionality
function zoomed() {
const currentTransform = d3.event.transform;
g.attr("transform", currentTransform);
}
d3.select(".zoom-in").on("click", function() {
zoom.scaleBy(svg.transition().duration(750), 1.2);
});
d3.select(".zoom-out").on("click", function() {
zoom.scaleBy(svg.transition().duration(750), 0.8);
});
// load and display the world and locations
d3.json("https://gist.githubusercontent.com/d3noob/5193723/raw/world-110m2.json", function(error, topology) {
var world = g.selectAll("path")
.data(topojson.object(topology, topology.objects.countries).geometries)
.enter()
.append("path")
.attr("d", path)
;
var locations = g.selectAll("circle")
.data(devicesAll)
.enter()
.append("circle")
.attr("cx", function(d) {return projection([d.LastLocation.lon, d.LastLocation.lat])[0];})
.attr("cy", function(d) {return projection([d.LastLocation.lon, d.LastLocation.lat])[1];})
.attr("r", 2)
.style("fill", "black")
.style("opacity", 1)
;
var simulation = d3.forceSimulation()
.force('x', d3.forceX().x(function(d) {return projection([d.LastLocation.lon, d.LastLocation.lat])[0]}))
.force('y', d3.forceY().y(function(d) {return projection([d.LastLocation.lon, d.LastLocation.lat])[1]}))
.force("charge", d3.forceManyBody().strength(0.5)) // Nodes are attracted one each other of value is > 0
.force("collide", d3.forceCollide().strength(.1).radius(2).iterations(2)) // Force that avoids circle overlapping
// Apply these forces to the nodes and update their positions.
// Once the force algorithm is happy with positions ('alpha' value is low enough), simulations will stop.
simulation
.nodes(devicesAll)
.on("tick", function(d){
locations
.attr("cx", function(d){ return d.x; })
.attr("cy", function(d){ return d.y; })
});
If i understood your problem correctly, you need to add it to your zoom behaviour.
//Zoom functionality
function zoomed() {
const currentTransform = d3.event.transform;
g.attr("transform", currentTransform);
}
here, you are applying your transformation to the elements, which is fine. However, you're not applying any logic to the radius.
That logic is up to you to make, and it will depend on the k property of the transform event (currentTransform.k).
I will use a some dummy logic for your radius. Your scale extent is between 1 and 10, you need a logic in which the radius decreases as the zoom increases (bigger k). It is also important that your radius doesn't go lower than 1, because the area of the circle will decrease much faster (remember the area depends on r^2, and r^2 < r for r < 1)
So my logic will be: the radius is 2.1 - (k / 10). Again, I'm oversimplifying, you can change it or tune it for your specific case.
In the end, it should look something like this:
//Zoom functionality
function zoomed() {
const currentTransform = d3.event.transform;
g.attr("transform", currentTransform);
g.selectAll("circle")
.attr("r", 2.1 - (currentTransform.k / 10))
}
I haven't tested the code, but tell me if this works! Maybe you can add it to a jsfiddle if needed

Zoom with drill-down capability

I'm trying to implement drill-down capability in zoom function, i.e., I want that my initial plot shows, for example, 50 points, and when the user makes zoom the number of points increases to 500.
My attempt consists in redraw inside the zoom function all the points and remove part of them when the zoom scale is under a threshold. As you can see in this JSFIDDLE, the implementation reproduces the drill-down capability.
However, I suspect that there is a more efficient way to implement the drill-down. Therefore, the question is if I'm in the correct way or there is a standard (more efficient and elegant) way for doing this effect.
My example code:
var width = 300,
height = 300;
var randomX = d3.random.normal(width / 2, 40),
randomY = d3.random.normal(height / 2, 40);
var data = d3.range(500).map(function() {
return [randomX(), randomY()];
});
var svg = d3.select("body").append("svg");
var zoomBehav = d3.behavior.zoom();
svg.attr("height", height)
.attr("width", width)
.call(zoomBehav
.scaleExtent([1, 10])
.on("zoom", zoom));
// Initial plot
d3.select("svg").selectAll("circle")
.data(data, function(d,i) {return i;})
.enter()
.append("circle")
.attr("r", 3)
.attr("cx", function(d) {return d[0]; })
.attr("cy", function(d) {return d[1]; })
.style("fill", "red");
d3.selectAll("circle")
.filter(function(d, i) {
if (zoomBehav.scale() < 2) { return i > 50; }
})
.remove();
function zoom(){
var selection = d3.select("svg")
.selectAll("circle")
.data(data, function(d,i) { return i; });
selection
.attr("cx", function(d) { return d3.event.translate[0] + d3.event.scale * d[0]; })
.attr("cy", function(d) { return d3.event.translate[1] + d3.event.scale * d[1]; });
selection.enter()
.append("circle")
.attr("r", 3)
.attr("cx", function(d) { return d3.event.translate[0] + d3.event.scale * d[0]; })
.attr("cy", function(d) { return d3.event.translate[1] + d3.event.scale * d[1]; })
.style("fill", "red");
d3.selectAll("circle")
.filter(function(d, i) {
if (zoomBehav.scale() < 2) { return i > 50; }
})
.remove();
}
If you're interested in dealing with semantic zoom of elements on an XY canvas, then you'll want to look into d3.geom.quadtree:
https://github.com/mbostock/d3/wiki/Quadtree-Geom
You can pass your points to a quadtree and they'll be spatially nested. Then, you can tie the nesting level to the zoom level and have automatic grid clustering. It's rather more involved than would fit into this answer, since you have to come up with mechanisms for representing the clustered points, and you'll also need to get into recursive functions to deal with the hierarchical level of points.
Here's an example using quadtrees for semantic zoom and clustering for mapping:
http://bl.ocks.org/emeeks/066e20c1ce5008f884eb

javascript d3.js: initialize brush with brush.extent and stop data from spilling over margin

I have 2 issues I want to fix with my current d3 app which is based off this:
Here's the fiddle: http://jsfiddle.net/zoa5m20z/
I want to initialize my brush so that only a small specific portion is brushed by default when the app starts up. I tried the following with .extent but with no luck.
//date parsing function
var parseDate = d3.time.format("%Y-%m-%d %H:%M:%S").parse;
//setting up brush and defaultExtent
var brush = d3.svg.brush()
.x(x2)
.extent([parseDate("2014-08-11 10:20:00"), parseDate("2014-08-11 18:20:00")]) //trying to intialize brush
.on("brush", brushed);
I want to stop my plotted circles from overlapping with the yaxis. I'd like to only plot/show circles to the right of the y-axis when the brush is moved. Just like the canonical Bostock Focus+Context via Brushing Block. I tried playing with the margins and the scales, domains, ranges but to no avail yet.
What I'm trying to avoid:
I'm new to d3.js, so all tips & advice is welcome and appreciated!
For your first question, you need to call brush.event to get the brush to pick up the new extent.
brush = d3.svg.brush()
.x(x)
.extent([config.start, d3.time.day.offset(config.start, config.range)])
.on("brush", brushmove)
.on("brushend", brushend);
gBrush = svg.select('.brush')
.call(brush);
gBrush.call(brush.event);
For your second question, I usually just filter out the data that is outside the brush extent such that I am only drawing the visible data. Here is an example that would be called on your brush move/zoom event:
// update the data so that only visible points are shown
var points= svg.selectAll('.point')
.data(function(d) { return onScreen(d, brush.extent); },
function(d) { return d.id; });
// draw the points that are now onscreen
var pointsEnter = points.enter().append('g').attr('class', 'point');
// remove any points that are now offscreen
points.exit().remove();
// up date the x/y position of your points based on the new axis.
// ... left up to you
It's helpful to have a unique id for the points so that they can just be translated to their new positions as the brush moves instead of having to destroy them and redraw them.
I have an example that uses these techniques at http://bl.ocks.org/bunkat/1962173.
Here is a working example based on your code: http://jsfiddle.net/26sd8uc9/4/
1 - You are right about the .extent, the problem is that you haven't specify the domain for you x2 scale. By adding the following code it works:
x2 = d3.time.scale()
.domain([
parseDate("2014-08-11 05:30:00"),
parseDate("2014-08-12 19:25:00")
])
.nice(d3.time.minute)
.range([0, width]);
And to initialize the circles, you also have to call the brushed event after creating the brush by adding .call(brush.event):
// brush slider display
context.append("g")
.attr("class", "x brush")
.call(brush)
.call(brush.event)
.selectAll("rect")
.attr("y", -6)
.attr("height", height2 + 7);
2 - use a variable to keep track where is the current range under the brush, and hide the circles that are not in the range by setting the radius to zero(alternatively you can set the visibility)
var currentRange;
var inRange = function(d) {
if(!currentRange || d.time < currentRange[0] || d.time > currentRange[1] ) {
return 0;
} else {
return 5;
}
}
function brushed() {
currentRange = (brush.empty()? undefined : brush.extent());
x.domain(brush.empty() ? x2.domain() : brush.extent());
focus.select(".x.axis").call(xAxis);
mydots.selectAll(".circle")
.attr("cx", xMap)
.attr("cy", yMap)
.attr("r", inRange); // set radius to zero if it's not in range
console.log(brush.extent())
}
For the scale definition, it will be better to write something like this:
.domain([
d3.min(dataset, function(d){ return d.time; }),
d3.max(dataset, function(d){ return d.time; })
])
In your example, you have to make sure to parse and initialize the data before you do this.

How do I disable my zoom functionality in D3.js?

I have a zoom functionality made in D3, but I'd like to make it optional, so I'd like a way to turn it off. Here's my code:
//Zoom command ...
var zoom = d3.behavior.zoom()
.x(xScale)
.y(yScale)
.scaleExtent([1,10])
.on("zoom", zoomTargets);
var SVGbody = SVG.append("g")
.attr("clip-path", "url(#clip)")
.call(zoom);
/
/The function handleling the zoom. Nothing is zoomed automatically, every elemnt must me defined here.
function zoomTargets() {
if($("#enableZoom").is(':checked')){
var translate = zoom.translate(),
scale = zoom.scale();
tx = Math.min(0, Math.max(width * (1 - scale), translate[0]));
ty = Math.min(0, Math.max(height * (1 - scale), translate[1]));
//This line applies the tx and ty which prevents the graphs from moving out of the limits. This means it can't be moved until zoomed in first.
zoom.translate([tx, ty]);
SVG.select(".x.axis").call(xAxis)
.selectAll("text")
.style("text-anchor", "end")
.style("font-size", AXIS_FONTSIZE)
.attr("transform", function(d) {
return "rotate(-30)"
});
SVG.select(".y.axis").call(yAxis)
.selectAll("text")
.style("font-size", AXIS_FONTSIZE);
SVG.selectAll("circle")
.attr("cx", function(d, i){ return xScale(graphDataX[i]);})
.attr("cy",function(d){return yScale(d);});
SVGMedian.selectAll("ellipse")
.attr("cx", function(d, i){ return xScale((i*100)+100);})
.attr("cy", function(d){ return yScale(d-0.01);});
}
}
As you can see I tried using an if-statement to prevent the zoom functionality from working when a checkbox isn't ticked. This prevents users from scrolling on the page when the mouse is inside of the SVG frame.
I'd like the correct way to do this. Thanks very much in advance!
This is a way to disable d3 zoom behavior
SVGbody.select("g").call(d3.behavior.zoom().on("zoom", null));

Basics of D3's Force-directed Layout

I'm plowing into the exciting world of force-directed layouts with d3.js. I've got a grasp of the fundamentals of d3, but I can't figure out the basic system for setting up a force-directed layout.
Right now, I'm trying to create a simple layout with a few unconnected bubbles that float to the center. Pretty simple right!? The circles with the correct are created, but nothing happens.
Edit: the problem seems to be that the force.nodes() returns the initial data array. On working scripts, force.nodes() returns an array of objects.
Here's my code:
<script>
$(function(){
var width = 600,
height = 400;
var data = [2,5,7,3,4,6,3,6];
//create chart
var chart = d3.select('body').append('svg')
.attr('class','chart')
.attr('width', width)
.attr('height', height);
//create force layout
var force = d3.layout.force()
.gravity(30)
.alpha(.2)
.size([width, height])
.nodes(data)
.links([])
.charge(30)
.start();
//create visuals
var circles = chart.selectAll('.circle')
.data(force.nodes()) //what should I put here???
.enter()
.append('circle')
.attr('class','circles')
.attr('r', function(d) { return d; });
//update locations
force.on("tick", function(e) {
circles.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
});
});
</script>
Here was the problem. The array you feed into force.nodes(array) needs to be an array of objects.
So:
var data = [{number:1, value:20}, {number:2, value:30}...];
works. Or even just
var data=[{value:1},{value:2}];
makes the script work fine.
You need to update cx and cy in force.on handler.
//update locations
force.on("tick", function(e) {
circles.attr("cx", function(d) { return d.x - deltaX(d, e) ; })
.attr("cy", function(d) { return d.y - deltaY(d, e); });
});
And functions deltaX and deltaY depends on your force model.

Categories