trouble understanding d3 tree node transitions - javascript

I am modifying a open source angular library, angular-d3-tree. I am modifying because I want to make my nodes render as rectangles. I got the rectangle part worked out but I cannot figure out how to keep the rectangles from over lapping:
I have noticed these transform attributes getting add to nodes like this:
var nodeEnter = node.enter().append('g')
.attr('class', 'node')
.attr("transform", function(d) {
return " translate(" + source.y0 + "," + source.x0 + ")";
//return 'translate(' + source.y + ',' + (source.x + 150) + ')';
// return 'translate(' + source.y + ',' + (source.x + 150) + ')';
// return "translate(" + source.y0 + "," + (source.x0 + 150) + ")";
});
.
.
.
var nodeUpdate = nodeEnter.merge(node);
nodeUpdate.transition()
.duration(this.duration)
.attr("transform", function(d) {
console.log('RBC trans d is:');
console.log(d);
return "translate(" + d.y + "," + d.x + ")";
// return "translate(" + d.y + "," + d.x + ")";
// return "translate(" + d.y + "," + (d.x + 150) + ")";
});
Anytime I make a change to the transform attributes either nothing changes or I just make things worse. Could some please explain to me how I can keep my rectangles from overlapping? I am just grasping at straws here.
Thanks!

Using a static modification of the transform will only shift every node in one direction or the other. However, we can use d3.tree to avoid overlap out of the box, assuming we know the size of the rectangles (or max size).
D3.tree offers two methods for sizing:
tree.size([width,height]), this will take your tree and place its nodes within a box of the provided dimensions. It makes no assumptions about the size of each node: the more nodes and/or the larger the nodes are, the more likely there is overlap. With this method the tree size is specified and nodes are positioned accordingly.
tree.nodeSize([width,height]), this takes a tree and places nodes so that they are spaced in a manner that gives each node the specified size. The overall size of the tree is dictated by the structure of the tree and the spacing of the nodes. With this method the node spacing is specified and the tree is sized accordingly.
*With the second approach the root node is placed at [0,0], this requires a transform to be applied on all nodes to place them properly. The first approach does not require this since the dimension of the plot area is known and the root is placed properly.
width and height are in this order for a vertical tree, horizontal trees will be reversed.
If we know the size of the rectangles, we can set nodeSize to those dimensions, plus some additional margin to allow the connecting paths to be seen. For example, adopting this block, we can convert it to use rectangular nodes by:
specifying a node size
appending rectangles rather than circles
reposition the text slightly
and change the translate on the parent g so [0,0] is half way down the page on the left (where our root node will appear):
var data = { "name": "Parent", "children": [
{ "name": "Child A", "children": [ { "name": "Grandchild" } ] },
{ "name": "Child B", }
] };
var width = 800;
var height = 200;
margin = {left: 100, top: 0, right: 50, bottom: 50}
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var g = svg.append("g").attr('transform','translate('+ margin.left +','+ (height/2) +')');
var root = d3.hierarchy(data);
var tree = d3.tree()
.nodeSize([50,100]); // because we are using a horizontal tree this is [height,width] of each node
var link = g.selectAll(".link")
.data(tree(root).links())
.enter().append("path")
.attr("class", "link")
.attr("d", d3.linkHorizontal()
.x(function(d) { return d.y; })
.y(function(d) { return d.x; }));
var node = g.selectAll(".node")
.data(root.descendants())
.enter().append("g")
.attr("class", function(d) { return "node" + (d.children ? " node--internal" : " node--leaf"); })
.attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; })
node.append("rect")
.attr("width", 80)
.attr("height", 40)
.attr("x", -40) // half of width
.attr("y", -20) // half of height
.attr("rx",5)
.attr("ry",5)
.attr("fill","steelblue")
node.append("text")
.text(function(d) { return d.data.name; })
.attr('y',5)
.attr('text-anchor','middle');
.link {
fill: none;
stroke: #ccc;
stroke-width: 2px;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
Here's a full size version.

Related

How to get innerHTML attribute value "r" in this code?

I'm using D3.js for my personal project.
But, I faced with some troubles.
bubble.nodes(root)
svg = d3.select("svg").attr("class", "bubble")
var node = svg.selectAll(".node")
.data(bubble.nodes(root)
.filter(function(d) {
return !d.children;
}))
.enter()
.append("g")
.attr("class", "node")
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
})
In the picture, I want to get "r" value in the circle.
How can I get this value?
Also, this circle is one part of bubble chart node.
Using vanilla JS... getElementsByTagName('circle'), then getAttribute('r') to find the value of r
let circle = document.getElementsByTagName('circle');
let target = circle[0].getAttribute('r');
console.log(target);
<circle r="0.75847397430597"></circle>

Disable D3 zoom on circle PNG background

I am trying to disable the D3 zoom on a particular element. This element happens to be the PNG background to a circle.
Right now this is not working. I have tried to offset the scale parameter in the zoom, but the background PNG still 'grows' with the circle. Here is my jsfiddle.
This is how I try to offset the zoom:
d3.selectAll("#grump_avatar").attr("transform", "scale(" + 1/d3.event.scale + ")");
I know there are similar questions on SO, but please note none of them have received a satisfactory response thus far. Better luck here, hopefully.
Lots of issues with this code:
Matching by id is an exact match.
Your ids are on def attributes, which aren't the objects, you don't want to scale (those would be the circles).
To match multiple objects, you should be using a class on the circles.
You apply the zoom directly to the svg, you should be wrapping everything in a g. SVG handles the events, g is the zoomable "canvas".
Once you apply the zoom correctly you are going to lose your circle placement because you overwrite the transform without reapplying the translate.
You've made no use of d3 data-binding, so you can't persist your data correctly.
All this in mind, here is how I would refactor your code:
var config = {
"avatar_size": 100
}
var body = d3.select("body");
var svg = body.append("svg")
.attr("width", 500)
.attr("height", 500);
var g = svg.append("g");
var defs = svg.append('svg:defs');
data = [{
posx: 100,
posy: 100,
img: "https://cdn0.iconfinder.com/data/icons/flat-round-system/512/android-128.png",
}, {
posx: 200,
posy: 200,
img: "https://cdn1.iconfinder.com/data/icons/social-media-set/24/Reverbnation-128.png"
}, {
posx: 300,
posy: 300,
img: "https://cdn1.iconfinder.com/data/icons/user-pictures/100/male3-128.png"
}];
defs.selectAll("pattern")
.data(data)
.enter()
.append("pattern")
.attr("id", (d, i) => "grump_avatar" + i)
.attr("width", config.avatar_size)
.attr("height", config.avatar_size)
.attr("patternUnits", "userSpaceOnUse")
.append("svg:image")
.attr("xlink:href", (d) => d.img)
.attr("width", config.avatar_size)
.attr("height", config.avatar_size)
.attr("x", 0)
.attr("y", 0);
g.selectAll(".grump_avatar")
.data(data)
.enter()
.append("circle")
.attr("class", "grump_avatar")
.attr("transform", (d) => "translate(" + d.posx + "," + d.posy + ")")
.attr("cx", config.avatar_size / 2)
.attr("cy", config.avatar_size / 2)
.attr("r", config.avatar_size / 2)
.style("fill", "white")
.style("fill", (d, i) => "url(#grump_avatar" + i + ")");
var zoom = d3.behavior.zoom()
.on("zoom", function() {
g.attr('transform', 'translate(' + d3.event.translate + ') scale(' + d3.event.scale + ')');
d3.selectAll(".grump_avatar").attr("transform", (d) => {
return "scale(" + 1 / d3.event.scale + ")" + "translate(" + (d.posx - d3.event.translate[0]) + "," + (d.posy - d3.event.translate[1]) + ")";
});
});
svg.call(zoom);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
EDITS based on comments:
To scale the circles opposite the zoom and position them, the key is:
d3.selectAll("circle")
.attr("transform", function(d){
return 'scale(' + 1 / d3.event.scale + ')'; // inverse of scale for size
})
.attr("cx", function(d){
return d.x * d3.event.scale; // change position based on scale, d.x is the original unscaled position
})
.attr("cy", function(d){
return d.y * d3.event.scale;
});

D3 Dragging on SVG "Path" Element Breaks when d.x is defined, hidden datum?

I'm a beginner to D3/Javascript.
I have a working D3 script where a bunch of path elements are drawn and can be dragged around.
However, when I add seemingly unrelated code elsewhere, that sets the d.x and d.y of the data (to its proper values BTW) the dragging breaks. The element jumps, so that it starts off some distance away and needs to be dragged back to its original place.
(The undesirable "jumping" is orderly way, consistent with a linear transformation of the mouse coordinates)
The "offending" code that seems to cause this behavior is:
hexdata.forEach(function(d) {
d["x"] = grid_x(d);
d["y"] = grid_y(d.grid_y);
});
The code that constructs the nodes and path that works without the code above is:
var node = svg.selectAll('g')
.data(hexdata)
.enter()
.append("g")
.call(d3.drag()
.on("drag", dragged))
.attr("class", "node");
node.append('path')
.attr("d", function (d) {
hex_alignment = (d.grid_y%2==1) ? hexRadius : 0
return "M" + (d.x *hexRadius*2 + 100) + "," + ((d.y*1.75 +100)) + hexPath;
})
function dragged(d) {
d3.select(this).attr("transform", "translate(" + d3.event.x + "," + d3.event.y + ")");
}
Does anyone know what is going on?
As mentioned in the comments, setting the origin(v3)/subject(v4) will fix this.
However, if you don't want (for any reason) to set the origin(v3)/subject(v4) in the drag function, simply change the property names for something else, like a and b. We know that x and y is the most common choice for naming the coordinates, but it will cause a conflict in the drag function (whose explanation is beyond the scope here, since you said you are a beginner).
This is easy to show. Here is a simple code using x and y, drag the circle around: it will jump.
var svg = d3.select("svg");
var circle = svg.append("circle")
.datum({
x: 150,
y: 75
})
.attr("transform", d => "translate(" + d.x + "," + d.y + ")")
.attr("r", 10)
.call(d3.drag()
.on("drag", dragged))
function dragged(d) {
d3.select(this).attr("transform", "translate(" + d3.event.x + "," + d3.event.y + ")");
}
svg{
border: 1px solid gray;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>
Now the same code, using a and b. The circle will not jump:
var svg = d3.select("svg");
var circle = svg.append("circle")
.datum({
a: 150,
b: 75
})
.attr("transform", d => "translate(" + d.a + "," + d.b + ")")
.attr("r", 10)
.call(d3.drag()
.on("drag", dragged))
function dragged(d) {
d3.select(this).attr("transform", "translate(" + d3.event.x + "," + d3.event.y + ")");
}
svg{
border: 1px solid gray;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>
Another alternative is reassigning d.x and d.y in the drag function:
d3.select(this).attr("transform", "translate(" + (d.x = d3.event.x) + ","
+ (d.y = d3.event.y) + ")");
Here is the code:
var svg = d3.select("svg");
var circle = svg.append("circle")
.datum({
x: 150,
y: 75
})
.attr("transform", d => "translate(" + d.x + "," + d.y + ")")
.attr("r", 10)
.call(d3.drag()
.on("drag", dragged))
function dragged(d) {
d3.select(this).attr("transform", "translate(" + (d.x = d3.event.x) + "," + (d.y = d3.event.y) + ")");
}
svg{
border: 1px solid gray;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>

Calling D3.JS function on 'load' rather than on mouse event

I am trying to make modifications to the D3 sankey example here:
http://bl.ocks.org/d3noob/c2637e28b79fb3bfea13
I want to shift the y-position of each node to a specified location (300px in the example below).
The most straight forward way I can to see to achieve this is to simply repurpose dragmove() to be called after the the SVG elements have been added. I've made changes to d.y in this function to shift to 300px:
function dragmove(d) {
d3.select(this).attr("transform",
"translate(" + d.x + "," + (
d.y = 300
) + ")");
sankey.relayout();
link.attr("d", path);
}
This function is called when adding the nodes:
var node = svg.append("g").selectAll(".node")
.data(graph.nodes)
.enter().append("g")
.attr("class", "node")
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")"; })
.call(d3.behavior.drag()
.origin(function(d) { return d; })
.on("dragstart", function() {
this.parentNode.appendChild(this); })
.on("drag", dragmove));
At present, it shifts to 300px as expected on the specified mouse event, but I want it to shift on its own after all the SVG elements have been added instead.
Simply using .call() without the mouse event doesn't work.
I've also tried to incorporate the shift in var node instead:
return "translate(" + d.x + "," + 300 + ")"; })
However this leads to a mismatch between the leads and the nodes, and calling sankey.relayout() doesn't seem to make a difference.
Found a solution.
First I removed .call() at the end of var node, as I don't need the drag event:
var node = svg.append("g").selectAll(".node")
.data(graph.nodes)
.enter().append("g")
.attr("class", "node")
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")"; });
Then I set d.y to the arbitrary location (300):
return "translate(" + d.x + "," + (d.y = 300) + ")"; });
Finally I forced it to re-draw the connecting links immediately after var node.
sankey.relayout();
link.attr("d", path);

Creating a sunburst diagram with dynamically chosen data source using D3

I am trying to create an interactive sunburst diagram using D3, where the user can select a data source from a dropdown menu. Once the data source is selected, any existing sunburst would be erased and redraw using the new data. This is based off the D3 example called "Sequences Sunburst" http://bl.ocks.org/kerryrodden/7090426
Having done a bit of research, it looks like you need to follow the add/append/transition/exit pattern.
Here is a link to a semi-functioning example on JSFiddle: http://jsfiddle.net/DanGinMD/dhpsxm64/14/
When you select the first data source, the sunburst diagram is created. When you select the second data source, a second sunburst is added. Each one appears to be connected to its unique data source. How do I erase the first sunburst before drawing the second sunburst?
Here is the code for listener event for the dropdown box:
// an event listener that (re)draws the breadcrumb trail and chart
d3.select('#optionsList')
.on('change', function() {
var newData = eval(d3.select(this).property('value'));
createVisualization(newData);
});
Here is the code that draws the sunburst diagram:
function createVisualization(json) {
sysName = json.sysName;
var titletext = sysName + " - Impact to Organization";
d3.select("#title2").text(titletext);
initializeBreadcrumbTrail();
var vis = d3.select("#chart").append("svg:svg")
.attr("width", width)
.attr("height", height)
.append("svg:g")
.attr("id", "container")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var partition = d3.layout.partition()
.size([2 * Math.PI, radius * radius])
.value(function(d) { return d.size; });
var arc = d3.svg.arc()
.startAngle(function(d) { return d.x; })
.endAngle(function(d) { return d.x + d.dx; })
.innerRadius(function(d) { return Math.sqrt(d.y); })
.outerRadius(function(d) { return Math.sqrt(d.y + d.dy); });
// Bounding circle underneath the sunburst, to make it
// easier to detect when the mouse leaves the parent g.
vis.append("svg:circle")
.attr("r", radius)
.style("opacity", 0);
// For efficiency, filter nodes to keep only those large enough to see.
var nodes = partition.nodes(json)
.filter(function(d) {
return (d.dx > 0.005); // 0.005 radians = 0.29 degrees
});
var path = vis.data([json]).selectAll("path")
.data(nodes)
.enter().append("svg:path")
.attr("display", function(d) { return d.depth ? null : "none"; })
.attr("d", arc)
.attr("fill-rule", "evenodd")
.style("fill", function(d) { return colors[d.category]; })
.style("opacity", 1)
.on("mouseover", mouseover);
// Add the mouseleave handler to the bounding circle.
d3.select("#container").on("mouseleave", mouseleave);
// Get total size of the tree = value of root node from partition.
totalSize = path.node().__data__.value;
path.exit().remove();
nodes.exit().remove();
arc.exit().remove();
partition.exit().remove();
vis.exit().remove();
}
Note the following call that appends a new svg at visualization initialization:
var vis = d3.select("#chart").append("svg:svg")
.attr("width", width)
.attr("height", height)
.append("svg:g")
.attr("id", "container")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
You just need to remove any old svg before this statement:
d3.select("#chart svg").remove();
var vis = d3.select("#chart").append("svg:svg")
.attr("width", width)
.attr("height", height)
.append("svg:g")
.attr("id", "container")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
fiddle

Categories