I am new to D3 and having trouble setting the bounds for my force directed layout. I have managed to piece together (from examples) what I would like, but I need the graph to be contained. In the tick function, a transform/translate will display my graph correctly, but when i use cx and cy with Math.max/min (See commented code), the nodes are pinned to the
top left corner while the lines are contained properly.
Here is what I have below... what am I doing wrong??
var w=960, h=500, r=8, z = d3.scale.category20();
var color = d3.scale.category20();
var force = d3.layout.force()
.linkDistance( function(d) { return (d.value*180) } )
.linkStrength( function(d) { return (1/(1+d.value)) } )
.charge(-1000)
//.gravity(.08)
.size([w, h]);
var vis = d3.select("#chart").append("svg:svg")
.attr("width", w)
.attr("height", h)
.append("svg:g")
.attr("transform", "translate(" + w / 4 + "," + h / 3 + ")");
vis.append("svg:rect")
.attr("width", w)
.attr("height", h)
.style("stroke", "#000");
d3.json("miserables.json", function(json) {
var link = vis.selectAll("line.link")
.data(json.links);
link.enter().append("svg:line")
.attr("class", "link")
.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.source.x; })
.attr("y2", function(d) { return d.source.y; })
.style("stroke-width", function(d) { return (1/(1+d.value))*5 });
var node = vis.selectAll("g.node")
.data(json.nodes);
var nodeEnter = node.enter().append("svg:g")
.attr("class", "node")
.on("mouseover", fade(.1))
.on("mouseout", fade(1))
.call(force.drag);
nodeEnter.append("svg:circle")
.attr("r", r)
.style("fill", function(d) { return z(d.group); })
.style("stroke", function(d) { return
d3.rgb(z(d.group)).darker(); });
nodeEnter.append("svg:text")
.attr("text-anchor", "middle")
.attr("dy", ".35em")
.text(function(d) { return d.name; });
force
.nodes(json.nodes)
.links(json.links)
.on("tick", tick)
.start();
function tick() {
// This works
node.attr("transform", function(d) { return "translate(" + d.x + ","
+ d.y + ")"; });
// This contains the lines within the boundary, but the nodes are
stuck in the top left corner
//node.attr("cx", function(d) { return d.x = Math.max(r, Math.min(w
- r, d.x)); })
// .attr("cy", function(d) { return d.y = Math.max(r, Math.min(h -
r, d.y)); });
link.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
}
var linkedByIndex = {};
json.links.forEach(function(d) {
linkedByIndex[d.source.index + "," + d.target.index] = 1;
});
function isConnected(a, b) {
return linkedByIndex[a.index + "," + b.index] ||
linkedByIndex[b.index + "," + a.index] || a.index == b.index;
}
function fade(opacity) {
return function(d) {
node.style("stroke-opacity", function(o) {
thisOpacity = isConnected(d, o) ? 1 : opacity;
this.setAttribute('fill-opacity', thisOpacity);
return thisOpacity;
});
link.style("stroke-opacity", opacity).style("stroke-opacity",
function(o) {
return o.source === d || o.target === d ? 1 : opacity;
});
};
}
});
There's a bounding box example in my talk on force layouts. The position Verlet integration allows you to define geometric constraints (such as bounding boxes and collision detection) inside the "tick" event listener; simply move the nodes to comply with the constraint and the simulation will adapt accordingly.
That said, gravity is definitely a more flexible way to deal with this problem, since it allows users to drag the graph outside the bounding box temporarily and then the graph will recover. Depend on the size of the graph and the size of the displayed area, you should experiment with different relative strengths of gravity and charge (repulsion) to get your graph to fit.
A custom force is a possible solution too. I like this approch more since not only the displayed nodes are repositioned but the whole simulation works with the bounding force.
let simulation = d3.forceSimulation(nodes)
...
.force("bounds", boxingForce);
// Custom force to put all nodes in a box
function boxingForce() {
const radius = 500;
for (let node of nodes) {
// Of the positions exceed the box, set them to the boundary position.
// You may want to include your nodes width to not overlap with the box.
node.x = Math.max(-radius, Math.min(radius, node.x));
node.y = Math.max(-radius, Math.min(radius, node.y));
}
}
The commented code works on node which is, from your definition, a svg g(rouping) element and does not operate the cx/cy attributes. Select the circle element inside node to make these attributes come alive:
node.select("circle") // select the circle element in that node
.attr("cx", function(d) { return d.x = Math.max(r, Math.min(w - r, d.x)); })
.attr("cy", function(d) { return d.y = Math.max(r, Math.min(h - r, d.y)); });
Related
I'm doing a visual project to show natural disaster in 1900-2018 using d3. I want add an interactive action that one can choose the first year and last year to show.
Originally I create the picture as the following:
d3.csv("output.csv", rowConventer, function (data) {
dataset = data;
var xScale = d3.scaleBand()
.domain(d3.range(dataset.length))
.range([padding, width - padding])
.paddingInner(0.05);
var yScale = d3.scaleLinear()
.domain([0,
d3.max(dataset, function (d) {
return d.AllNaturalDisasters;
})])
.range([height - padding, padding])
.nice();
stack = d3.stack().keys(["Drought", "Earthquake", "ExtremeTemperature", "ExtremeWeather", "Flood", "Impact", "Landslide", "MassMovementDry", "VolcanicActivity", "Wildfire"]);
series = stack(dataset);
gr = svg.append("g");
groups = gr.selectAll("g")
.data(series)
.enter()
.append("g")
.style("fill", function(d, i) {
return colors(i);
})
.attr("class", "groups");
rects = groups.selectAll("rect")
.data(function(d) { return d; })
.enter()
.append("rect")
.attr("x", function(d, i) {
return xScale(i);
})
.attr("y", function(d) {
return yScale(d[1]);
})
.attr("height", function(d) {
return yScale(d[0]) - yScale(d[1]);
})
.attr("width", xScale.bandwidth())
.append("title")
.text(function (d) {
var rect = this.parentNode;// the rectangle, parent of the title
var g = rect.parentNode;// the g, parent of the rect.
return d.data.Year + ", " + d3.select(g).datum().key + "," + (d[1]-d[0]);
});
d3.select("button")
.on("click", choosePeriod);
I have simplified some code to make my question simple. At the last row, I add an event listener to achieve what I described above. And the update function is choosePeriod. Now it is as following:
function choosePeriod() {
firstYear = parseInt(document.getElementById("FirstYear").value);
lastYear = parseInt(document.getElementById("LastYear").value);
d3.csv("output.csv", rowConventer, function (newdata) {
dataset = newdata;
series=stack(dataset);
groups.data(series);
groups.selectAll("rect")
.data(function (d) {
return d;
})
.enter()
.append("rect")
.attr("x", function(d, i) {
return xScales(i);
})
.attr("y", function(d) {
return yScales(d[1]);
})
.attr("height", function(d) {
return yScales(d[0]) - yScales(d[1]);
})
.attr("width", xScales.bandwidth())
.append("title")
.text(function (d) {
var rect = this.parentNode;// the rectangle, parent of the title
var g = rect.parentNode;// the g, parent of the rect.
return d.data.Year + ", " + d3.select(g).datum().key + "," + (d[1]-d[0]);
});
groups.selectAll("rect")
.data(function (d) {
return d;
})
.exit()
.remove();
})
}
The change of dataset is achieved by rowConventer, which is not important in this question. Now the functionchoosePeriod is not running as envisioned! Theenter and the exit and update are all not work well! The whole picture is a mess! What I want is, for instance, if I input the firstYear=1900 and the lastYear=2000, then the picture should be updated with the period 1900-2000 to show. How can I achieve it?
I am unfamiliar the arrangement of the entire structure, I mean, at some place using d3.select() by class or id instead of label is better, right?
It looks like you've dealt with the enter and the exit selections. The only bit you're missing is the update selection, which will deal with the rectangles that already exist and don't need adding or removing. To do this copy your update pattern but just remove the enter().append() bit, e.g.:
groups.selectAll("rect")
.data(function (d) {
return d;
})
.attr("x", function(d, i) {
return xScales(i);
})
.attr("y", function(d) {
return yScales(d[1]);
})
.attr("height", function(d) {
return yScales(d[0]) - yScales(d[1]);
})
.attr("width", xScales.bandwidth())
.append("title")
.text(function (d) {
var rect = this.parentNode;// the rectangle, parent of the title
var g = rect.parentNode;// the g, parent of the rect.
return d.data.Year + ", " + d3.select(g).datum().key + "," + (d[1]-d[0]);
})
I am making a d3 v4 vertical tree diagram based on http://bl.ocks.org/d3noob/8326869, and I'm trying to add text labels to the paths (eg. Yes, No, etc.). This is the code in the links section that I have so far:
function update (source) {
// Assigns the x and y position for the nodes
var treeData = treemap(root);
// Compute the new tree layout.
var nodes = treeData.descendants(),
links = treeData.descendants().slice(1);
// Normalize for fixed-depth.
nodes.forEach(function (d) {
d.y = d.depth * 95
});
// ****************** Nodes section ***************************
// Update the nodes...
var node = svg.selectAll('g.node')
.data(nodes, function (d) {
return d.id || (d.id = ++i);
});
// Enter any new modes at the parent's previous position.
var nodeEnter = node.enter().append('g')
.attr('class', 'node')
.attr("transform", function (d) {
return "translate(" + source.x0 + "," + source.y0 + ")";
})
.attr("data-info", function (d) { return d.data.info })
.on('click', click);
// Add Circle for the nodes
/*nodeEnter.append('circle')
.attr('class', 'node')
.attr("id", function(d){return "node" + d.id;})//id of the node.
.attr('r', 1e-6)
.style("fill", function (d) {
return d._children ? "lightsteelblue" : "#fff";
});*/
nodeEnter.append('rect')
.attr('class', 'node')
.attr('width', 170)
.attr('height', 55)
.attr('x', -85)
.attr('y', -22)
.attr('rx',7) //rounding
.attr('ry',7)
.attr("id", function(d){return "node" + d.id;});//id of the node.
nodeEnter.append('text')
//.attr("dy", ".25em")
//.attr('x', -23)
.attr('class', 'node-text')
.attr("text-anchor", "middle")
.text(function (d) {
return d.data.name;
})
.call(wrap, 165);
// UPDATE
var nodeUpdate = nodeEnter.merge(node);
// Transition to the proper position for the node
nodeUpdate.transition()
.duration(duration)
.attr("transform", function (d) {
return "translate(" + d.x + "," + d.y + ")";
});
// Update the node attributes and style
nodeUpdate.select('rect.node')
.style("fill", function (d) {
if (d._children) {
return "#007bff"; // dark blue
}
})
.attr('cursor', 'pointer');
// Remove any exiting nodes
var nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function (d) {
return "translate(" + source.x + "," + source.y + ")";
})
.remove();
// On exit reduce the node circles size to 0
nodeExit.select('circle')
.attr('r', 1e-6);
// On exit reduce the opacity of text labels
nodeExit.select('text')
.style('fill-opacity', 1e-6);
// ****************** links section ***************************
// Update the links...
var link = svg.selectAll('path.link')
.data(links, function (d) {
return d.id;
});
// Enter any new links at the parent's previous position.
var linkEnter = link.enter().insert('path', 'g')
.attr("id", function(d){ return ("link" + d.id)})//unique id
.attr("class", "link")
.attr('d', function (d) {
var o = {x: source.x0, y: source.y0}
return diagonal(o, o);
});
// UPDATE
var linkUpdate = linkEnter.merge(link);
var linkLabel = link.enter().insert("text","g")
.attr("class", "link2")
.attr("id", function(d){ return ("link-label" + d.id)})//unique id
.attr("dx",function(d){ return (d.parent.x + d.x)/2 })
.attr("dy",function(d){ return (d.parent.y + d.y)/2 })
.text(function(d) {
if (d.data.label === "Yes") {
this.setAttribute("x",-30);
} else {
this.setAttribute("x",10);
}
return d.data.label;
});
linkUpdate.merge(linkLabel);
// Transition back to the parent element position
linkUpdate.transition()
.duration(duration)
.attr('d', function (d) {
svg.select("#link-label" + d.id).transition().duration(duration).attr("dx",function(d){ return (d.parent.x + d.x)/2 })
.attr("dy",function(d){ return (d.parent.y + d.y)/2});
return diagonal(d, d.parent)
});
// Remove any exiting links
var linkExit = link.exit().transition()
.duration(duration)
.attr('d', function (d) {
var o = {x: source.x, y: source.y};
svg.selectAll("#link-label" + d.id).remove();
return diagonal(o, o)
})
.remove();
// Store the old positions for transition.
nodes.forEach(function (d) {
d.x0 = d.x;
d.y0 = d.y;
});
The "linkEnter.insert("text","g")" part is where I am having trouble. It looks like it keeps appending to the element instead of being a sibling so that it doesn't display at all, and I'm not sure how to code it so that it isn't a child of , while still being animated appropriately when a node is hidden/shown.
I'm also not sure how to set up the coordinates once I do manage to fix the above issue so that the text label is in the middle of the path (or at least close to it).
I've tried adding similar code in the node section instead but then it seems harder to find the right positioning to align to the path.
========
Edit: I've updated the code as it seems like it's working now. I created linkLabel which pulls data from the JSON variable. It's positioned to be next to the paths but not overlapping them. The animations aren't smooth but at least it seems to work.
Anyone managed to make the example
Sankey diagram with horizontal and vertical node movement
work in v4
since sankey.relayout() is not available anymore
d3.drag has no .origin anymore.
My attempt do the wildest things while attempting to drag a node and since some behaviors have changed in both sankey and drag specifications I'm unable to figure how to migrate the example to v4.
var graph = { "nodes": [...], "links": [...] };
var layout = d3.sankey();
layout
.extent([[1, 1], [width - 1, height - 6]])
.nodeId(function (d) {
return d.id;
})
.nodeWidth(12)
.nodePadding(padding)
.nodeAlign(d3.sankeyRight)
layout(graph);
// Add Links
var link = svg.append("g")
.attr("fill", "none")
.selectAll(".link")
.data(graph.links)
.enter()
.append("g")
.attr("class", "link")
link.append("path")
.attr("d", d3.sankeyLinkHorizontal())
.attr("stroke", "#000")
.style("stroke-width", function (d) {
return d.width;
})
.append("title")
.text("Some text");
// Drag behavior for node elements
var drag = d3.drag()
//.origin(function (d) { return d; }) // Deprecated but maybe unnecessary
.on("drag", dragmove)
// Add nodes
var node = svg.append("g")
.selectAll(".node")
.data(graph.nodes)
.enter()
.append("g")
.attr("transform", function (d) {
return "translate(" + [d.x0, d.y0] + ")";
})
.call(drag) // The g element should be now draggable
// Add element inside g element
node.append("rect")
.attr("height", function (d) { return d.y1 - d.y0; })
.attr("width", function (d) { return d.x1 - d.x0; })
.attr("fill", ...)
node.append("text")
.attr("x", function (d) { return (d.x1 - d.x0) - 6; })
.attr("y", function (d) { return (d.y1 - d.y0) / 2; })
.attr("dy", "0.35em")
.attr("text-anchor", "end")
.text(function (d) { return d.name; })
.filter(function (d) { return d.x0 < width / 2; })
.attr("x", function (d) { return (d.x1 - d.x0) + 6 })
.attr("text-anchor", "start");
// Called by d3.drag
function dragmove(d) {
var dx = Math.round(d.x = Math.max(0, Math.min(width , evt.x)));
var dy = Math.round(d.y = Math.max(0, Math.min(height, evt.y)));
d3.select(this).attr("transform", "translate(" + [dx, dy] + ")")
// Now should redraw the links but
sankey.relayout(); // not a function anymore.
// path references sankey.link() that is also deprecated in v4 in
// favour of d3.sankeyLinkHorizontal() ? (I'm not sure)
// link references the g element containing the path elements
// classed .link
link.attr("d", path);
};
I have created a cluster tree layout and I want to add custom node styles to selected nodes. To be more precise, I'm adding treemap as node.
I managed to add those, but they are not positioned in the center of node.
I have tried all sort of x,y attributes and translations but I quess I don't get svg that much yet.
Part of code where I add the node is here (for JSfiddle see below):
nodeEnter.each(function(d) {
if (d.status == "D") {
var treemap = d3.layout.treemap()
.size([20, 20])
.sticky(true)
.value(function(d) {
return 1;
});
var cell = d3.select(this)
.selectAll("g")
.data(function(d) {
return treemap.nodes(d.annotations);
})
.enter().append("g")
.attr("class", "cell")
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
cell.append("rect")
.attr("width", function(d) {
return d.dx;
})
.attr("height", function(d) {
return d.dy;
})
.style("fill", function(d) {
return d.children ? null : hex2rgb(color(d.parent.name));
});
}
})
Any help would be appreciated
Here is my JSfiddle.
L.
Assuming you wanted the lines to connect to the middle of the appended rect. I just added a third .attr to your JSfiddle
cell.append("rect")
.attr("width", function(d) {
return d.dx;
})
.attr("height", function(d) {
return d.dy;
})
.attr("transform","translate(0,-10)")
.style("fill", function(d) {
return d.children ? null : hex2rgb(color(d.parent.name));
});
I have implemented panning zooming and dragging in d3 force layout code. They are working fine. However,I noticed one issue with panning/zooming whenever I pan or zoom, the nodes do not expand to complete viewport.
You may get more clarity with the screenshots attached.
There is no need to change the cx and cy attributes of circle since it is already within the node group. Just need to transform the node group elements. Also note that position of links should be updated after nodes since position of links is calculated from node positions.
Try replacing your tick function as shown below.
function tick(d) {
node.attr("transform", function(d) {
var radius = d.children ? 22 : isNaN(parseInt(d.name)) ? 16 : 10;
d.x = Math.max(radius, Math.min(w - radius, d.x));
d.y = Math.max(radius, Math.min(sh - radius, d.y));
return "translate(" + d.x + "," + d.y + ")";
});
link.attr("x1", function(d) {
return d.source.x;
})
.attr("y1", function(d) {
return d.source.y;
})
.attr("x2", function(d) {
return d.target.x;
})
.attr("y2", function(d) {
return d.target.y;
});
}
Here is the working JSFiddle