d3 adding drag and drop to packed circles - d3.js

I am trying to add the ability to drag a circle from within one parent into another using this as the starting point: https://bl.ocks.org/mbostock/7607535
I've added this code:
var drag = d3.behavior.drag()
.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 ] + ")"
})
});
and then tried attaching it like this:
svg.selectAll("circle").call(drag);
or
node.call(drag);
I've two problems, one is positioning the circle correctly as I drag (which I think is because I need to save the origin) and the other one is how to select a sub circle to drag as it is assuming I want the parent circle. Ideally I'd like to be able to select any circle and be able to drop it into any other circle. Is there a way of attaching the drag behaviour so this just works or do I need to look at the data structure in order to work out the lowest level circle I could be trying to drag?

There is a lot going on in the Zoomable example and some of it is colliding with your intent. The quick fixes:
One is positioning the circle correctly as I drag (which I think is because I need to save the origin).
Positioning the circle will require an update during the ondrag event. Here is what I used:
var drag = d3.behavior.drag()
.on("drag", function(d, i) {
d.x += d3.event.dx;
d.y += d3.event.dy;
draw();
});
function draw() {
var k = diameter / (root.r * 2 + margin);
node.attr("transform", function(d) {
return "translate(" + (d.x - root.x) * k + "," + (d.y - root.y) * k + ")";
});
circle.attr("r", function(d) {
return d.r * k;
});
}
This draw function replaces the zoomTo function from the Zoomable example. Getting zoom to work with dragging is possible but it requires some extra thought.
The other one is how to select a sub circle to drag as it is assuming I want the parent circle.
The Zoomable example has this CSS:
.label,
.node--root,
.node--leaf {
pointer-events: none;
}
If you want to target the leaf nodes, you will have to remove the .node--leaf portion of the CSS rule. You can also add a new rule to turn off events for the non-leaf nodes:
.attr("class", function(d) {
return d.parent ? d.children ? "node node--middle" : "node node--leaf" : "node node--root";
})
Note the addition of node--middle.
Ideally I'd like to be able to select any circle and be able to drop it into any other circle. Is there a way of attaching the drag behaviour so this just works or do I need to look at the data structure in order to work out the lowest level circle I could be trying to drag?
You can drag it around without much trouble but the drag behavior will not automatically propagate changes into the data structure. If you want the actual hierarchy to change then there are a few extra steps required:
Detect where the leaf node ended
Change the parent of the leaf node appropriately
Recalculate the entire packing
Redraw/animate the new packing
Step 1 will be the tricky one. I do not have an example handy for helping with this. You can quickly detect the location that a drag event ended using the dragstop event — the trick will be figuring out which node is underneath that stopping point.

It's not allowing you to drag the leaf-most children nodes because there's a css style with pointer-events: none applied to those nodes, so they never trigger drag. So you need to take out some or all of these classes:
.label,
.node--root,
.node--leaf {
pointer-events: none;
}
That'll make it possible to drag the children, but it will not prevent the ability to drag the parents. If you don't want the parents to be draggable, you have to NOT .call(drag) on them. The way to do it is to call drag on a subset of all circles, using filter.
So, after creating the circles, you can do this:
circle
.filter( function(d) { return d.children ? false : true; } )
.call(drag);
That will apply drag only to things that do not have children.
You are right about the incorrect positioning during drag being caused by not knowing the origin. I'm pretty sure you need to save the original circle position on dragstart and then add d3.event.dx to that position during drag.

Related

D3.js binding nested data

I'm really new to coding, and also to asking questions about coding. So let me know if my explanation is overly complex, or if you need more context on anything, etc.
I am creating an interactive map of migration flows on the Mediterranean Sea. The flows show origin and destination regions of the migrant flows, as well as the total number of migrants, for Italy and Greece. Flows should be displayed in a Sankey diagram like manner. Because I am displaying the flows on a map and not in a diagram fashion, I am not using D3’s Sankey plugin, but creating my own paths.
My flow map, as of now (curved flows are on top of each other, should line up next to each other)
For generating my flows I have four points:
2 points for the straight middle part of the flow (country total)
1 point each for the curved outer parts (origin and destination region), using the two points of the straight middle part as starting points
The straight middle and both curved outer parts are each generated independently from their own data source. Flow lines are updated by changing the data source and calling the function again. The flow lines are generated using the SVG path mini-language. In order for the curved outer parts of the flows to show correctly, I need them to be lined up next to each other. To line them up correctly, I need to shift their starting points. The distance of the shift for each path element is determined by the width of the path elements before it. So, grouping by country, each path element i needs to know the sum of the width of the elements 0-i in the same group.
After grouping my data with d3.nest(), which would allow me to iterate over each group, I am not able to bind the data correctly to the path elements
I also can't figure out a loop function that adds up values for all elements 0-i. Any help here? (Sorry if this is kind of unrelated to the issue of binding nested data)
Here is a working function for the curved paths, working for unnested data:
function lineFlow(data, flowSubGroup, flowDir) {
var flowSelect = svg.select(".flowGroup").select(flowSubGroup).selectAll("path");
var flow = flowSelect.data(data);
var flowDirection = flowDir;
flow.enter()
.append("path").append("title");
flow
.attr("stroke", "purple")
.attr("stroke-linecap", "butt")
.attr("fill", "none")
.attr("opacity", 0.75)
.transition()
.duration(transitionDur)
.ease(d3.easeCubic)
.attr("d", function(d) {
var
slope = (d.cy2-d.cy1)/(d.cx2-d.cx1),
dist = (Math.sqrt(Math.pow((d.rx2-d.rx1),2)+Math.pow((d.ry2-d.ry1),2)))*0.5,
ctrlx = d.rx1 + Math.sqrt((Math.pow(dist,2))/(1+Math.pow(slope,2)))*flowDirection,
ctrly = slope*(ctrlx-d.rx1)+d.ry1;
return "M"+d.rx1+","+d.ry1+"Q"+ctrlx+","+ctrly+","+d.rx2+","+d.ry2})
.attr("stroke-width", function(d) {return (d.totalmig)/flowScale});
flowSelect
.select("title")
.text(function(d) {
return d.region + "\n"
+ "Number of migrants: " + addSpaces(d.totalmig)});
};
I tried adapting the code to work with data grouped by country:
function lineFlowNested(data, flowSubGroup, flowDir) {
var g=svg.select(".flowGroup").select(flowSubGroup).append("g").data(data).enter();
var gflowSelect=g.selectAll("path");
var gflow=gflowSelect.data (function(d) {return d.values});
gflow.enter()
.append("path");
gflow.attr("stroke", "purple")
.attr("stroke-linecap", "butt")
.attr("fill", "none")
.attr("opacity", 0.75)
// .transition()
// .duration(transitionDur)
// .ease(d3.easeCubic)
.attr("d", function(d) {
var
slope = (d.cy2-d.cy1)/(d.cx2-d.cx1),
dist = (Math.sqrt(Math.pow((d.rx2-d.rx1),2)+Math.pow((d.ry2-d.ry1),2)))*0.5,
ctrlx = d.rx1 - Math.sqrt((Math.pow(dist,2))/(1+Math.pow(slope,2)))*flowDirection,
ctrly = slope*(ctrlx-d.rx1)+d.ry1;
return "M"+d.rx1+","+d.ry1+"Q"+ctrlx+","+ctrly+","+d.rx2+","+d.ry2})
.attr("stroke-width", function(d) {return (d.totalmig)/flowScale});
};
which isn't working. What am I doing wrong? Thanks for any hints!

D3 data binding [D3js in Action]

I'm new to d3.js, and am working my way through the book "D3.js in action". So far I have been able to figure out all the questions I had, but this one I can't completely answer on my own, it seems.
I post the source code from the book here, since it is available on the books website and the authors homepage. This is the bl.ocks: http://bl.ocks.org/emeeks/raw/186d62271bb3069446b5/
The basis idea of the code is to create a spreadsheet-like layout out of div elements filled with fictious twitter data. Also implemented is a sort function to sort the data by timestamp and reorder the sheet. As well as a function to reestablish the original order.
Here is the code (I left out the part where the table structure is created, except the part where the data is bound):
<html>
<...>
<body>
<div id="traditional">
</div>
</body>
<footer>
<script>
d3.json("tweets.json",function(error,data) { createSpreadsheet(data.tweets)});
function createSpreadsheet(incData) {
var keyValues = d3.keys(incData[0])
d3.select("div.table")
.selectAll("div.datarow")
.data(incData, function(d) {return d.content})
.enter()
.append("div")
.attr("class", "datarow")
.style("top", function(d,i) {return (40 + (i * 40)) + "px"});
d3.selectAll("div.datarow")
.selectAll("div.data")
.data(function(d) {return d3.entries(d)})
.enter()
.append("div")
.attr("class", "data")
.html(function (d) {return d.value})
.style("left", function(d,i,j) {return (i * 100) + "px"});
d3.select("#traditional").insert("button", ".table")
.on("click", sortSheet).html("sort")
d3.select("#traditional").insert("button", ".table")
.on("click", restoreSheet).html("restore")
function sortSheet() {
var dataset = d3.selectAll("div.datarow").data();
dataset.sort(function(a,b) {
var a = new Date(a.timestamp);
var b = new Date(b.timestamp);
return a>=b ? 1 : (a<b ? -1 : 0);
})
d3.selectAll("div.datarow")
.data(dataset, function(d) {return d.content})
.transition()
.duration(2000)
.style("top", function(d,i) {return (40 + (i * 40)) + "px"});
}
function restoreSheet() {
d3.selectAll("div.datarow")
.transition()
.duration(2000)
.style("top", function(d,i) {return (40 + (i * 40)) + "px"});
}
}
</script>
</footer>
</html>
What I don't fully understand is how sortSheet and restoreSheet work.
This part of sortSheet looks like it rebinds data, but after console logging I think it doesn't actually rebind data to the DOM. Instead it just seems to redraw the div.tablerow elements based on the array index of the sorted array.
But then what purpose does the key-function have?
And why is the transition working? How does it know which old element to put in which new position?
EDIT:
---After some more reading I now know that selectAll().data() does indeed return the update selection. Apparenty the already bound data identified by the key function is re-sorted to match the order of the keys in the new dataset? Is that correct?
So the update selection contains the existing div.datarow s, but in a new ordering. The transition() function works on the new order, drawing the newly ordered div.datarow s beginning with index 0 for the first element to determine its position on the page, to index n for the last element. The graphical transition then somehow (how? by way of the update selection?) knows where the redrawn div.datarow was before and creates the transition-effect.
Is that correct so far?---
d3.selectAll("div.datarow")
.data(dataset, function(d) {return d.content}) //why the key function?
.transition()
.duration(2000)
.style("top", function(d,i) {return (40 + (i * 40)) + "px"});
And what happens when the original order is restored? Apparently during both operations there is no actual rebinding of data, and the order of the div.datarows in the DOM does not change. So the restore function also redraws the layout based on the array index.
But what kind of selection does the .transition() work on? Is it an update? It is an update.
And why does the drawing using the index result in the old layout? Shouldn't the index of the DOM elements always be 0,1,...,n? I think it is. Apparently the old page layout is redrawn, with the DOM never having changed. But how can the transition() function create the appropriate graphical effect?
function restoreSheet() {
d3.selectAll("div.datarow")
.transition()
.duration(2000)
.style("top", function(d,i) {return (40 + (i * 40)) + "px"});
}
I have been thinking for hours about this, but I can't find the correct answer I think.
Thanks for your help!
It all becomes clear when you understand where all these functions were called: inside the json function, where the data was originally bound. When a button calls the sortSheet function, a new array of objects is made and bound to the rows. The transition simply starts with the original order and move the rows according to the new order of the objects inside the array.
And what happens when the original order is restored?
Now comes the interesting part: restoreSheet is called inside the json function and has no access to the dataset variable. So, the data restoreSheet uses is the original data. Then, a transition simply moves the rows according to the order of the objects inside the original array.
I just made a fiddle replicating this: https://jsfiddle.net/k9012vro/2/
Check the code: I have an array with the original data. Then, a button called "sort" creates a new array.
When I click "original" the rectangles move back to the original position. But there is nothing special in that function, no new data being bound:
d3.select("#button1").on("click", function(){
rects.transition()
.duration(500).attr("x", function(d, i){ return i * 30})
});
It moves all the rectangles to the original positions because this function uses the same original data.

Edge does not handle scaling and text-anchor:middle correctly in svg

I'm writing a graph display program using D3 and I found an issue in which Microsoft Edge does not handle edge scaling correctly. My code is below:
HTML:
<svg id="container" width="200" height="200">
<g id="inner-container">
</g>
</svg>
JS:
var container = d3.select("#container");
var innerContainer = container.select("g");
container.call(d3.behavior.zoom().scaleExtent([.5,4]).on("zoom", function(){
innerContainer.attr("transform", "translate(" + d3.event.translate + ") scale(" + d3.event.scale + ")");
}));
var nodes = innerContainer.append("g").selectAll(".graph-node");
var labels = innerContainer.append("g").selectAll(".graph-label");
var data = ["A", "B", "C", "D", "E"];
nodes.data(data).enter().call(function (selection){
selection = selection.append("g");
selection.attr("class", ".graph-node");
selection.attr("transform", function(d, i){
return ["translate(", (i * 20).toString(), " ", (i * 20).toString(), ")"].join("");
});
selection.append("rect")
.attr("width", 20)
.attr("height", 20)
.attr("rx", 2).attr("ry", 2)
.attr("fill", "red");
selection.append("text")
.text(function(d){ return d; })
.attr("text-anchor", "middle")
.attr("x", 10).attr("y", 15)
.style("fill", "white");
});
labels.data(data).enter().call(function (selection){
selection = selection.append("g");
selection.attr("class", "graph-label");
selection.append("text")
.text(function (d) { return "Node" + d; });
selection.attr("transform", function (d, i) {
return ["translate(", (i * 20 + 25).toString(), " ", (i * 20 + 15).toString(), ")"].join("");
});
});
View in JSFiddle
In Google Chrome, IE, and Firefox, zooming works exactly as expected. However, when I run this code in Microsoft Edge, the text labels on top of the nodes will shift to the right as I zoom in, entirely disappearing when they leave the rectangle. The problem disappears when I don't set text-anchor, but that will mean that I have to account for text positioning manually, especially considering when the site is used internationally. (The text characters themselves don't matter, since they're from a custom font in the final product).
How can I work around this, providing a centered text label and yet having it still display correctly on Edge?
Been in similar situations with browser incompatibilities in text layouting several times. If you fail to find the magic combination of CSS properties that works for all, here is one other option:
Add a <pre> element to your page. This element is invisible by definition. Select the element with D3's select. Add the text you want to layout as innerHTML. Use classed and style to style the element in the exact same way you will style the text. You can now get the text's exact length with the browser function getComputedTextLength or by reading the element's dimensions.
Hide all of this in a handy reuse function or TextLengthCalculator class.
Once you have the dimensions, you can calculate the desired offsets yourself and simply shift the text by adjusting its x and y attributes.
This is fast and takes localization and styling into account. It even is clean and reliable since it is still the browser itself that does the calculation. And although the browsers still sometimes produce different results here, at least each browser is consistent with its own calculation.

d3.js zoomable circle packing: change radius for specific circles

I am using the example Zoomable Circle Packing by Mike Bostock and I try to change the radius of some circles, which are identified by the type "secteur" in the dataset.
To do that, I tried to use the following line:
circle.filter(function(d) { return d.type === "secteur"; }).attr("r", function(d) { return d.r * 1.5 ; });
The example is available here.
Even without filtering by datum type (I tried to remove the .filter part) the line seems to have no effect on any radiuses, I didn't see any changes in the console.
I do not understand why, and I would appreciate insights if any
Thank you
Your zoomTo function set's the radius again after you set it with your filter.
You call:
zoomTo([root.x, root.y, root.r * 2 + margin]);
Which does:
circle.attr("r", function(d) { return d.r * k; });

How to properly add and use D3 Events?

I'm having trouble understanding using D3 events and dispatch functions. I have a chart example that I've been working on called: "Vertical Bar Charts With Legends."
Drawing the charts and the legends was easy enough but I'd like to add the ability to highlight each bar as I mouseover its correlating text legend, located to the right of the chart.
I've read through all of the event documentation and even looked at a number of examples, most of which are pretty complicated, but I seem to be missing something. Would anyone know how to best accomplish the text legend mouseover functionality that dispatches events to automatically change colors of the corresponding vertical bars?
This question is similar to the one you posted in the d3-js Google Group. Without duplicating what I wrote there, I would reiterate that you probably don't want d3.dispatch; that is intended for custom event abstractions (such as brushes and behaviors). It'll be simpler to use native events.
If you want your legend to change the color of the corresponding bar on mouseover, then breakdown the problem into steps:
Detect mouseover on the legend.
Select the corresponding bar.
Change the bar's fill color.
First, use selection.on to listen for "mouseover" events on the legend elements. Your listener function will be called when the mouse goes over a legend element, and will be called with two arguments: the data (d) and the index (i). You can use this information to select the corresponding bar via d3.select. Lastly, use selection.style to change the "fill" style with the new color.
If you're not sure how to select the corresponding bar on legend mouseover, there are typically several options. The most straightforward is to select by index, assuming that the number of legend elements and number of rect elements are the same, and they are in the same order. In that case, if a local variable rect contains the rect elements, you could say:
function mouseover(d, i) {
d3.select(rect[0][i]).style("fill", "red");
}
If you don't want to rely on index, another option is to scan for the matching bar based on identical data. This uses selection.filter:
function mouseover(d, i) {
rect.filter(function(p) { return d === p; }).style("fill", "red");
}
Yet another option is to give each rect a unique ID, and then select by id. For example, on initialization, you could say:
rect.attr("id", function(d, i) { return "rect-" + i; });
Then, you could select the rect by id on mouseover:
function mouseover(d, i) {
d3.select("#rect-" + i).style("fill", "red");
}
The above example is contrived since I used the index to generate the id attribute (in which case, it's simpler and faster to use the first technique of selecting by index). A more realistic example would be if your data had a name property; you could then use d.name to generate the id attribute, and likewise select by id. You could also select by other attributes or class, if you don't want to generate a unique id.
Mike's answer is great.
I used it come up with this for selecting a cell in a grid I was drawing:
.on('click', (d, i) ->
console.log("X:" + d.x, "Y:" + d.y) #displays the cell x y location
d3.select(this).style("fill", "red");
So when I am entering the data in I added the event listener and using d3.select(this).
See the code in context below:
vis.selectAll("rect")
.data(singleArray)
.enter().append("svg:rect")
.attr("stroke", "none")
.attr("fill", (d) ->
if d.lifeForm
return "green"
else
return "white")
.attr("x", (d) -> xs(d.x))
.attr("y", (d) -> ys(d.y))
.attr("width", cellWidth)
.attr("height", cellHeight)
.on('click', (d, i) ->
console.log("X:" + d.x, "Y:" + d.y)
d3.select(this).style("fill", "red");
return
)

Resources