Using the zoom and pan functionality of d3 - d3.js

I'm trying to use the pan/zoom ability of d3 to draw boxes on the screen so that when you click on a box a new box appears and shifts the rest of the boxes to the right so that the new box is on the center of the canvas. The panning would allow me to scroll through all the boxes I've drawn.
Here is my jsfiddle: http://jsfiddle.net/uUTBE/1/
And here is my code for initializing the zoom/pan:
svg.call(d3.behavior.zoom().on("zoom", redraw));
function redraw() {
d3.select(".canvas").attr("transform",
"translate(" + d3.event.translate + ")"
+ " scale(" + d3.event.scale + ")");
}
And here is my code for drawing the boxes:
function drawBox(x, y) {
var boxGroup = canvas.append("g");
boxGroup.append("rect")
.attr("x", x)
.attr("y", y)
.attr("height", 100)
.attr("width", 100)
.attr("fill", function () {
var i = Math.floor(Math.random() * 4);
if (i === 1) return "red";
else if (i === 2) return "blue";
else if (i === 3) return "yellow";
else return "green";
})
.on("click", function () {
counter++;
d3.select(".canvas")
.transition()
.duration(1000)
.attr('transform', "translate(300,0)");
drawBox(x - counter * 120, y);
});
}
I have multiple problems with this fiddle, but two of my main concerns is:
1) How do I make it so that when I click on a new box a second time the boxes move accordingly (i.e. when I click on the box initially the old box shifts to the right and a new box appears, but when I click on the new box, the older boxes doesnn't shift to the right).
2)Why is it that when I click on the new box, the newer box has a big spacing between it? (only happens after trying to put 3 boxes on the screen).
Thanks any hints are appreciated!

I think there's some confusion here around transform. The transform attribute is static, not cumulative, for a single element - so setting .attr('transform', "translate(300,0)") more than once will have no effect after the first time. It also looks like your placement logic for the new boxes is off.
The positioning logic required here is pretty straightforward if you take a step back (assuming I understand what you're trying to do):
Every time a new box is added, the frame all boxes are in moves right 120px, so it needs a x-translation of 120 * counter.
New boxes need to be offset from the new frame position, so they need an x setting of -120 * counter.
Zoom needs to take the current canvas offset into account.
(1) above can be done in your click handler:
canvas
.transition()
.duration(1000)
.attr('transform', "translate(" + (offset * counter) + ",0)");
(2) is pretty easily applied to the g element you're wrapping boxes in:
var boxGroup = canvas.append("g")
.attr('transform', 'translate(' + (-offset * counter) + ',0)');
(3) can be added to your redraw handler:
function redraw() {
var translation = d3.event.translate,
newx = translation[0] + offset * counter,
newy = translation[1];
canvas.attr("transform",
"translate(" + newx + "," + newy + ")" + " scale(" + d3.event.scale + ")");
}
See the working fiddle here: http://jsfiddle.net/nrabinowitz/p3m8A/

Related

D3.js: How to Center svg map after Drag, Zoom, or MouseMove Events

My map projection and display goes awry when users drag, move the map (see: https://realtimeceap.brc.tamus.edu).
For example (duplicate event):
1. Select a field condition from dropdownlist.
2. Select a State from dropdownlist.
3. Select a County from dropdownlist.
4. Drag the map or move the mousewheel.
5. Then, select another State from dropdownlist. The map is not centered at the middle of the svg element and the scale is off, instead of scale 1.
Appreciate any help.
I reset the map on selecting a state as follows:
function resetMap() {
svg = d3.select("#svgMap2");
var w = 728;
var h = 500;
var project = d3.geoAlbersUsa()
.scale(1000)
.translate([w / 2, h / 2]);
var t = project.translate(); // the projection's default translation
var scale = project.scale;
//reset all features to original scale
d3.select("#svgMap2").select("#counties").selectAll(".county")
.transition()
.duration(750)
.style("stroke-width", "0.5px")
.style("stroke", "#808080")
.attr("transform", "translate(" + t[0] + "," + t[1] + ")scale(" + scale + ")");
}
Been working on this for a while and found the solution is so simple. I needed to reset the g element's transform attribute to null to redefine it. The id of my svg = svgMap2 and the id of my g element = counties. BTW, I'm using D3.js version 4.
function resetMap() {
var k = 1;
var t = [0, 0];
svg = d3.select("#svgMap2")
d3.select("#svgMap2").select("#counties").selectAll(".county").classed("active", false);
d3.select("#counties").attr("transform", null);
svg.select("#counties").selectAll(".county") //must use svg or it disables pan, zoom
.attr("width", width)
.attr("height", height)
.style("stroke-width", "0.5px")
.style("stroke", "#808080")
.attr("transform", "translate(" + t[0] + "," + t[1] + ")scale(" + k + ")");
}

Live updating line chart - Wont smoothly scroll off screen (wiggles)

I have a line chart that I've built for displaying live data. I've read through https://bost.ocks.org/mike/path/ and I can get it to smoothly come on screen, but for the life of me, I cant get it to not 'wiggle' when I remove a datapoint and the corresponding line section from the screen.
Here is the section of code where I am updating the line and x-axis.
lineselection.datum(data)
.transition()
.duration(1000)
.ease(d3.easeLinear)
.attr('d', line)
.attr('transform', 'translate(' + (xScale.range()[0]/data.length) + ',0)');
xScale = d3.scaleTime()
.domain([time, start_time])
.range([width - (margin * 2), 0])
.clamp(true);
g.selectAll('.x.axis')
.transition()
.duration(1000)
ease(d3.easeLinear)
.call(xAxis);
This produces a nice smooth line chart, where the new section of line is drawn outside of the clip-path, and then translated left into view. The x-axis scrolls along wonderfully with it.
In my data update method, I do a check to see if we are in my data view window (ie, we only want to view the last, say 60 seconds of data). If a point falls outside the time frame, we remove it, and update the start_time to reflect.
if ((time_frame + start_time) < time){
for (d = 0; d < data.length; d++){
if ((data[d].x + time_frame) < time){
data.shift();
}
else{
start_time = data[d].x;
break;
}
}
}
I am newer to both JS and D3, so please be gentle. Any help would be appreciated. Am I missing something obvious?
Here is a jsfiddle. https://jsfiddle.net/3kn56mb7/2/
Edit. Added a jsfiddle.
Here is the updated jsfiddle. https://jsfiddle.net/3kn56mb7/12/
Thank you elias for helping me get to this!
Here is my draw function. I used selection.interrupt() to overcome a jitter.
function draw(){
var line = d3.line()
.x(function(d) { return xScale(d.x); })
.y(function(d) { return yScale(d.y); });
var lineselection = svg.selectAll('.line_')
.select('path');
lineselection.interrupt()
.transition()
.duration(duration)
.ease(d3.easeLinear)
.attr('transform', 'translate(' + -(xScale.range()[0]/((duration / 100)-2)) + ',' + margin + ')');
if (data[0].x < time - time_frame - duration ){
console.log('shift');
data.shift();
}
lineselection.attr('d',line)
.attr('transform', 'translate(0,' + margin + ')');
start_time = time - (duration * 2) - time_frame;
xScale.domain([time, time + (duration * 2) - time_frame])
.range([width - (margin *2),0]);
d3.select('body')
.select('svg')
.select('g')
.selectAll('.x.axis')
.transition()
.duration(duration)
.ease(d3.easeLinear)
.call(xAxis);
}
EDIT: I updated the code to reflect the selection.interrupt() change as well as included the entire draw function. I also updated the jsfiddle link.
EDIT 2: Fixed a margin error and updated the jsfiddle link.
EDIT 3: I came back to use this as a reference. I am looking at this bit of code a year later, and couldnt figure out why I did some weird math. I must have been breathing diesel fumes. In anycase, I redid the math so that it made sense (why was I dividing duration by 100 and subtracting 2)?
Changed this: https://jsfiddle.net/3kn56mb7/11/
.attr('transform', 'translate(' + -(xScale.range()[0]/((duration / 100)-2)) + ',' + margin + ')');
To this: https://jsfiddle.net/3kn56mb7/12/
.attr('transform', 'translate(' + -(xScale(data[data.length-1].x) -xScale.range()[0]) + ',' + margin + ')');
I am now taking the last data point every draw cycle, and getting its time (x value) and converting that to a position in the range. Then I subtract the max range out (width-margin*2). Make the difference negative so it moves left.
The basic problem is that your data elements' indices are changing by removing the obsolete elements. I've faced the very same issue in a project of mine, and solved it in the following way:
I recommend that doing what you now in the transition in two separate steps:
when removing the element, draw a new path with the new indices - do this immediately, without a transition, so the wiggling does not appear
after this has been done, a transition can happen, which is then only a translation, during which no elements get deleted

How to move a point from d3 arc start to end without wrapping?

I am attempting to use d3.js to move a point along an arc from 0 to PI, say, without the point moving back along the innerRadius as seen here http://bl.ocks.org/mbostock/1705868.
I removed innerRadius hoping (unsuccessfully) that would work (http://jsfiddle.net/klin/23c5476v/). I had also tried setting the innerRadius with the same value as outerRadius.
Fragment I changed (changes marked with //) ...
var path = svg.append("svg:path")
.datum({endAngle: Math.PI}) //
.attr("d", d3.svg.arc()
// .innerRadius(h / 4) // Hoping removal would prevent inner transition
.outerRadius(h / 3)
.startAngle(0)
);//.endAngle(Math.PI));
Entire code ...
var svg = d3.select("body").append("svg:svg")
.attr("width", w)
.attr("height", h)
.append("svg:g")
.attr("transform", "translate(" + w / 2 + "," + h / 2 + ")");
var path = svg.append("svg:path")
.datum({endAngle: Math.PI}) //
.attr("d", d3.svg.arc()
// .innerRadius(h / 4) // Hoping removal would prevent inner transition
.outerRadius(h / 3)
.startAngle(0)
);//.endAngle(Math.PI));
var circle = svg.append("svg:circle")
.attr("r", 6.5)
.attr("transform", "translate(0," + -h / 3 + ")");
function transition() {
circle.transition()
.duration(5000)
.attrTween("transform", translateAlong(path.node()))
.each("end", transition);
}
transition();
// Returns an attrTween for translating along the specified path element.
function translateAlong(path) {
var l = path.getTotalLength();
return function(d, i, a) {
return function(t) {
var p = path.getPointAtLength(t * l);
return "translate(" + p.x + "," + p.y + ")";
};
};
}
The problem I think is that the arc shape has area, so the path must be closed, while the line shape does not. Eventually I'd like to be able to separately animate object movement along a series of consecutive arcs similar to the answer to Interpolating along consecutive paths with D3.js, but first I need to avoid the loop back movement.
Is a simple solution maybe to not use d3's arc generator, but instead use another where the end point actually is the terminus of the path?
Paul is right.
You can do next
var arc = d3.svg.arc(); //plus params
$path.attr('d',function(){
var d = arc();
return d.split('L')[0]; //will return half of arc without lines
});

Force layout node rotation

Ok, first - look at this fiddle.
You should see shapes rotating back and forth like crazy.
This is what is going on:
force.on("tick", function(e) {
vis.selectAll("path")
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")"
// this is the thing
+"rotate(" + Math.random() * 50 + ")";
});
});
On every tick I'm changing the transform: rotate() to Math.random() * 50 in this case.
Now what I want is a smooth rotation. Not this jerky stuff.
See this to better understand what I mean. Imagine the height as the rotation. The gray box represents what I have now, the blue - what I want.
I tried applying 'transition: all 1s ease' CSS to that element, but it just ignores it, I'm obviously doing it wrong.
So how do I make this infinite back and forth rotation smooth as if I was using CSS3 transitions?
Every tick you are randomly setting the rotation to something between 0 and 50 degrees of rotation. You need to maintain the current rotation, calculate an offset, and then set the rotation to the current + offset.
Here's an updated tick function:
force.on("tick", function(e) {
vis.selectAll("path")
.attr("transform", function(d) {
if(!d.rotate) {
d.rotate = Math.random() * 50;
} else {
d.rotate = d.rotate + 1;
}
return "translate(" + d.x + "," + d.y + ")"
+"rotate(" + d.rotate + ")";
});
});
Here's the updated working example: https://jsfiddle.net/1aLc7x4j/

D3 - How to get correct scale and translate origin after manual zoom and pan to country path

I've got a topology map with pan and zoom functionality.
Clicking on the country, I'm zoom/panning into the country, using this:
if (this.active === d) return
var g = this.vis.select('g')
g.selectAll(".active").classed("active", false)
d3.select(path).classed('active', active = d)
var b = this.path.bounds(d);
g.transition().duration(750).attr("transform","translate(" +
this.proj.translate() + ")" +
"scale(" + .95 / Math.max((b[1][0] - b[0][0]) / this.options.width, (b[1][1] - b[0][1]) / this.options.height) + ")" +
"translate(" + -(b[1][0] + b[0][0]) / 2 + "," + -(b[1][1] + b[0][1]) / 2 + ")");
g.selectAll('path')
.style("stroke-width", 1 / this.zoom.scale())
However, if I continue to drag pan, the map jerks back into the initial position before the click happens, before panning. Code to pan/zoom is here:
this.zoom = d3.behavior.zoom().on('zoom', redraw)
function redraw() {
console.log('redraw')
_this.vis.select('g').attr("transform","translate(" +
d3.event.translate.join(",") + ")scale(" + d3.event.scale + ")")
_this.vis.select('g').selectAll('path')
.style("stroke-width", 1 / _this.zoom.scale())
}
this.vis.call(this.zoom)
In another words, after zooming into a point by clicking, and then dragging by the redraw function, the redraw does not pick up the correct translate+scale to continue from.
To continue at the right 'zoom' after the transition, I had to set the zoom to the new translate and scale.
Example of reset which is applied similarly to my click and zoom event, the set new zoom point is the critical bit:
_this.vis.select('g').transition().duration(1000)
.attr('transform', "translate(0,0)scale(1)")
/* SET NEW ZOOM POINT */
_this.zoom.scale(1);
_this.zoom.translate([0, 0]);

Resources