d3 drag jumping to other position when clicked - origin? - javascript

Trying to drag groups. Why doesn't origin work here? Notice how it jumps when you first click on it? JSFIDDLE Based on this: http://bl.ocks.org/mbostock/1557377
var drag = d3.behavior.drag() // construct drag behavior
.origin(function() {
var t = d3.select(this);
return {x: t.attr("x"), y: t.attr("y")};
})
.on("drag", function(d,i) {
d.x += d3.event.dx
d.y += d3.event.dy
d3.select(this).attr("transform", function(d,i){
return "translate(" + [ d.x,d.y ] + ")"
})
});

You're mixing different ways of setting positions -- you're setting transform and cx and cyon the circles, but not on thegelements that you want to drag. While it can be made to work by computing the various offsets, it's much easier if you set the position for the things you're interested in (i.e. theg` elements) and that the drag behaviour is called on.
var svgG = svg.append("g")
.attr("transform", function(d) { return "translate(" + [ d.x,d.y ] + ")"; })
.call(drag);
Complete example here.

Related

d3 how to tie text to top right corner of view port while zooming and panning

I am creating a mapping application in d3 and want to tie some text to the top right corner of my view port. Additionally, I want the text to remain in the top right corner while I zoom and pan across the application.I think I can solve my problem by figuring out how to get the coordinates of the top right corner of my view. Knowing this information would allow me to then set the coordinates of my text element. I've tried manually setting the dimensions of the containing svg element and then moving the text to that location but interestingly this didn't work. I was hoping to be able to find the coordinates programatically rather than setting coordinates manually. How can I do this in d3/javascript?
EDIT:
My code is a modification of this code by Andy Barefoot: https://codepen.io/nb123456/pen/zLdqvM
My own zooming and panning code has essentially remained the same as the above example:
function zoomed() {
t = d3
.event
.transform
;
countriesGroup
.attr("transform","translate(" + [t.x, t.y] + ")scale(" + t.k + ")")
;
}
I'm trying to append the text at the very bottom of the code:
countriesGroup.append("text")
.attr("transform", "translate(" How do I get top right coordinates? ")")
.style("fill", "#ff0000")
.attr("font-size", "50px")
.text("This is a test");
My idea is to be able to get the top right coordinates of the view port through the code rather than setting it manually and then have the coordinates of the text update as the user zooms or pans.
To keep something in place while zooming and panning you could invert the zoom:
point == invertZoom(applyZoom(point))
This isn't particularly efficient, as we are using two operations to get to the original number. The zoom is applied here:
countriesGroup
.attr("transform","translate(" + [t.x, t.y] + ")scale(" + t.k + ")");
While the inversion would need to look something like:
text.attr("x", d3.zoom.transform.invert(point)[0])
.attr("y", d3.zoom.transform.invert(point)[1])
.attr("font-size", baseFontSize / d3.zoom.transform.k);
Where point and base font size are the original anchor point and font size. This means storing that data somewhere. In the example below I assign it as a datum to the text element:
var width = 500;
var height = 200;
var data = d3.range(100).map(function() {
return {x:Math.random()*width,y:Math.random()*height}
})
var zoom = d3.zoom()
.on("zoom",zoomed);
var svg = d3.select("body")
.append("svg")
.attr("width",width)
.attr("height",height)
.call(zoom);
var g = svg.append("g")
var circles = g.selectAll()
.data(data)
.enter()
.append("circle")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", 5)
.attr("fill","steelblue")
var text = g.append("text")
.datum({x: width-10, y: 20, fontSize: 12})
.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; })
.style("text-anchor","end")
.attr("font-size",function(d) { return d.fontSize; })
.text("This is a test");
function zoomed() {
g.attr("transform", d3.event.transform);
var d = text.datum();
var p = d3.event.transform.invert([d.x,d.y]);
var x1 = p[0];
var y1 = p[1];
text.attr("x",x1)
.attr("y",y1)
.attr("font-size", d.fontSize / d3.event.transform.k)
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
Better Solution
The above is the solution to the approach you seem to be looking for. But the end result is best achieved by a different method. As I mention in my comment, the above approach goes through extra steps that can be avoided. There can also be some size/clarity changes in the text when zooming (quickly) using the above method
As noted above, you are applying the zoom here:
countriesGroup
.attr("transform","translate(" + [t.x, t.y] + ")scale(" + t.k + ")")
The zoom transform is applied only to countriesGroup, if your label happens to be in a different g (and not a child of countriesGroup), it won't be scaled or panned.
We wouldn't need to apply and invert the zoom, and we wouldn't need to update the position or font size of the text at all.
var width = 500;
var height = 200;
var data = d3.range(100).map(function() {
return {x:Math.random()*width,y:Math.random()*height}
})
var zoom = d3.zoom()
.on("zoom",zoomed);
var svg = d3.select("body")
.append("svg")
.attr("width",width)
.attr("height",height)
.call(zoom);
var g = svg.append("g");
var g2 = svg.append("g"); // order does matter in layering
var circles = g.selectAll()
.data(data)
.enter()
.append("circle")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", 5)
.attr("fill","steelblue")
// position once and leave it alone:
var text = g2.append("text")
.attr("x", width - 10)
.attr("y", 20 )
.style("text-anchor","end")
.attr("font-size", 12)
.text("This is a test");
function zoomed() {
// apply the zoom to the g that has zoomable content:
g.attr("transform", d3.event.transform);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>

Adding multiple objects to svg and adding drag behaviour issue

I am totally new to SVG and d3 library. I need to add dynamically add 5 circle to svg which all contains draggable event handler. I have written code for adding a single circle and adding draggable behaviour to the same and its working fine. Now I am trying the same thing inside for loop in order to add 5 circle. It displays all circle but when I drag a particular circle and put it some where then it stays there and again when i touch another circle old circle get vanished from the position where we placed and appears on new circle where we started next. Please have a look at below mentioned code. Any help regarding this would be appreciated.
function addCircles()
{
var box = d3.select(".box");
for(var i = 0;i<5;i++)
{
var drag = d3.behavior.drag()
.on('dragstart', function() { console.log("dragstart"); circle.style('fill', 'red'); })
.on('drag', function() { console.log("drag X - " + d3.event.x + " Y - " + d3.event.y); circle.attr('cx', d3.event.x)
.attr('cy', d3.event.y); })
.on('dragend', function() { console.log("dragend - " + d3.event.x);
circle.style('fill', 'green'); });
var circle = box.selectAll('.draggableCircle'+i)
.data([{ x: i*15, y: i*15, r: 10 }])
.enter()
.append('svg:circle')
.attr('class', 'draggableCircle'+i)
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; })
.attr('r', function(d) { return d.r; })
.call(drag)
.style('fill', 'green');
}
}
I have checked out after debugging code in chrome and found out that position of dragEnd is not being detected.
Your fiddle didnt work so I had to make my own from the code provided : http://jsfiddle.net/Qh9X5/6932/
Create the data first then draw circles from that data, rather than doing it all at once.
var nodeData = [];
for(var i = 1;i<15;i++) //change the value 15 to however many circles you want
{
nodeData.push({
x:i*15,
y:i*15,
r:10
})
}
Then use this data to create circles :
var circle = box.selectAll('.draggableCircle'+i)
//.data([{ x: i*15, y: i*15, r: 10 }])
.data(nodeData)
.enter()
.append('svg:circle')
.attr('class', function(d,i){
return 'draggableCircle'+i; //changed this to use i in the loop
//through the nodes not i in the for loop
})
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; })
.attr('r', function(d) { return d.r; })
.style('fill', 'green')
.call(drag)
Also your drag wasn't done correctly. You had this line :
circle.attr('cx', d3.event.x).attr('cy', d3.event.y);
You dont want this as your going through each circle and calling the drag on all of them. You only want to call it on the element you're 'dragging' like so :
function dragmove(d, i) //-updates the co-ordinates
{
d.x += d3.event.dx;
d.y += d3.event.dy;
d3.select(this).attr("transform", function(d,i)
{
return "translate(" + [ d.x,d.y ] + ")";
});
}
I think that was all the changes I made to make it work.
On a side note, with JSFiddle, you have to include the D3 library on the left hand side, other wise it wont work. Also, when calling your 'drawCircles()' function in the html, you have to change the loading of the fiddle otherwise it wont be able to find the function. Also, with all this said, if you were to use JSFiddle again in a question, please make sure it works before sending SO users a link.
EDIT
I added this line to get the correct circle positions on load :
circle.attr("transform", function(d){
return "translate(" + [ d.x,d.y ] + ")";
})
Now the drag works perfectly :)) Hope this helps

Drag behavior not working right?

I'm trying to find the problem with the drag behavior setup that I have in my program, because it seems like the drag won't even activate. I'm using http://jsfiddle.net/da37B/317/ as the reference code for my program.
Here's the relevant code:
vis.selectAll(".nodes")
.data(nodes)
.enter().append("circle")
.attr("class", "nodes")
.attr("cx", function (d) {
return xRange(d.x);
})
.attr("cy", function (d) {
return yRange(d.y);
})
.attr("r", "10px")
.attr("fill", "black")
.attr("transform", "translate(" + p.x + "," + p.y + ")")
.call(drag); <------
// Define drag beavior
var drag = d3.behavior.drag()
.on("drag", dragmove);
function dragmove(d) {
var x = d3.event.x;
var y = d3.event.y;
d3.select(this).attr("transform", "translate(" + x + "," + y + ")");
}
And here's the full code: https://jsfiddle.net/4o5pch1q/1/
The reason you don't see any effect is that you have an error in your jsfiddle. Please check the console for such obvious things in the future.
Once the obvious error is fixed (including moving the definition of drag up so that it's defined before it's being used), the only thing that remains is to tell D3 how to get the origin of the element being dragged (otherwise the circle "jumps" on drag):
var drag = d3.behavior.drag()
.origin(function(d) { return d; })
.on("drag", dragmove);
Complete demo here.

How to statically position elements in D3

I have currently have a line graph that looks like this:
on jsfiddle http://jsfiddle.net/vertaire/kttndjgc/1/
I've been trying to manually position the values on the graph so they get get printed next to the legend looking something like this:
Unintentional Injuries: 1980, 388437
I tried to set the positions manually, but it seems when I try and adjust to positioning, that positioning is relative to the that of the circle on the line like this:
How can I set the coordinates so that the values appear next to the legend?
Here is the code snippet for printing the values:
var mouseCircle = causation.append("g") // for each line, add group to hold text and circle
.attr("class","mouseCircle");
mouseCircle.append("circle") // add a circle to follow along path
.attr("r", 7)
.style("stroke", function(d) { console.log(d); return color(d.key); })
.style("fill", function(d) { console.log(d); return color(d.key); })
.style("stroke-width", "1px");
mouseCircle.append("text")
.attr("transform", "translate(10,3)"); // text to hold coordinates
.on('mousemove', function() { // mouse moving over canvas
if(!frozen) {
d3.select(".mouseLine")
.attr("d", function(){
yRange = y.range(); // range of y axis
var xCoor = d3.mouse(this)[0]; // mouse position in x
var xDate = x.invert(xCoor); // date corresponding to mouse x
d3.selectAll('.mouseCircle') // for each circle group
.each(function(d,i){
var rightIdx = bisect(data[1].values, xDate); // find date in data that right off mouse
yVal = data[i].values[rightIdx-1].VALUE;
yCoor = y(yVal);
var interSect = get_line_intersection(xCoor, // get the intersection of our vertical line and the data line
yRange[0],
xCoor,
yRange[1],
x(data[i].values[rightIdx-1].YEAR),
y(data[i].values[rightIdx-1].VALUE),
x(data[i].values[rightIdx].YEAR),
y(data[i].values[rightIdx].VALUE));
d3.select(this) // move the circle to intersection
.attr('transform', 'translate(' + interSect.x + ',' + interSect.y + ')');
d3.select(this.children[1]) // write coordinates out
.text(xDate.getFullYear() + "," + yVal);
yearCurrent = xDate.getFullYear();
console.log(yearCurrent)
return yearCurrent;
});
return "M"+ xCoor +"," + yRange[0] + "L" + xCoor + "," + yRange[1]; // position vertical line
});
}
});
First thing I would do is create the legend dynamically instead of hard coding each item:
var legEnter = chart1.append("g")
.attr("class","legend")
.selectAll('.legendItem')
.data(data)
.enter();
legEnter.append("text")
.attr("class","legendItem")
.attr("x",750)
.attr("y", function(d,i){
return 6 + (20 * i);
})
.text(function(d){
return d.key;
});
legEnter.append("circle")
.attr("cx",740)
.attr("cy", function(d,i){
return 4 + (20 * i);
})
.attr("r", 7)
.attr("fill", function(d,i){
return color(d.key);
});
Even if you leave it as you have it, the key here is to assign each text a class of legendItem. Then in your mouseover, find it and update it's value:
d3.select(d3.selectAll(".legendItem")[0][i]) // find it by index
.text(function(d,i){
return d.key + ": " + xDate.getFullYear() + "," + yVal;
});
Updated fiddle.

Transition on labels in d3 pie/donut chart

I'm making a donut chart that can be switched between several different data sets. I have been able to get the slices to transition nicely, and am positioning the labels with arc.centroid, but I can't figure out how to apply the arc tweening function to the labels. I think I've almost got it, any hints would be appreciated.
Here's a live example: http://jsbin.com/otAjUSO/1/edit?html,output
Add same transition effect to label group also
DEMO
label_group.data(pie)
.transition().duration(750)
.attr("transform", function(d) {
var c = arc.centroid(d);
return "translate(" + c[0] +"," + c[1] + ")";
})
Simply add a transition to the group:
label_group.data(pie)
.transition().duration(750)
// The above transition is all you need
.attr("transform", function(d) {
var c = arc.centroid(d);
return "translate(" + c[0] +"," + c[1] + ")";
});

Categories

Resources