Make D3 force directed graph responsive and adhere to bounding box - d3.js

I have a force directed graph generated via D3 that isn't playing well with the responsive code or the bounding box code I've found. Since the radius of all of my circles varies, I think it's throwing some things off... Any help is appreciated!
I have to use a custom length on the lines because the nodes run into each other if I don't manually space them out because the radii aren't the same.
(Please don't link me to the d3 page with the code, I've tried it, but maybe I'm placing it in the wrong spot if you think it would work on this. I also tried to post an image, but I don't have enough reputation.)
var width = 876,
height = 600;
var color = d3.scale.category20();
var force = d3.layout.force()
.charge(-1010)
.linkDistance(function(d) { return d.distance; })
.size([width, height])
.gravity(0.7);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var tip = d3.tip()
.attr('class', 'd3-tip')
.offset([-5, 0])
.html(function (d) {
return d.name + " (" + d.instances + ")";
})
svg.call(tip);
d3.json("datawords.json", function(error, graph) {
force
.nodes(graph.nodes)
.links(graph.links)
.start();
var link = svg.selectAll(".link")
.data(graph.links)
.enter().append("line")
.attr("class", "link")
.attr("width", function(d) { return d.totalLength; })
.style("stroke-width", function(d) { return Math.sqrt(d.value); });
var node = svg.selectAll(".node")
.data(graph.nodes)
.enter().append("circle")
.attr("class", "node")
.attr("r", function(d) {return d.instances;})
.style("fill", function(d) { return color(d.instances); })
.call(force.drag)
.on('mouseover', tip.show)
.on('mouseout', tip.hide)
.on('click', connectedNodes)
force.on("tick", function() {
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; });
node.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
node.each(collide(0.5))
});
//Toggle stores whether the highlighting is on
var toggle = 0;
//Create an array logging what is connected to what
var linkedByIndex = {};
for (i = 0; i < graph.nodes.length; i++) {
linkedByIndex[i + "," + i] = 1;
};
graph.links.forEach(function (d) {
linkedByIndex[d.source.index + "," + d.target.index] = 1;
});
//This function looks up whether a pair are neighbours
function neighboring(a, b) {
return linkedByIndex[a.index + "," + b.index];
}
function connectedNodes() {
if (toggle == 0) {
//Reduce the opacity of all but the neighbouring nodes
d = d3.select(this).node().__data__;
node.style("opacity", function (o) {
return neighboring(d, o) | neighboring(o, d) ? 1 : 0.1;
});
link.style("opacity", function (o) {
return d.index==o.source.index | d.index==o.target.index ? 1 : 0.1;
});
//Reduce the op
toggle = 1;
} else {
//Put them back to opacity=1
node.style("opacity", 1);
link.style("opacity", 1);
toggle = 0;
};
};
var padding = 10, // separation between circles
radius=15;
function collide(alpha) {
var quadtree = d3.geom.quadtree(graph.nodes);
return function(d) {
var rb = 4*radius + padding,
nx1 = d.x - rb,
nx2 = d.x + rb,
ny1 = d.y - rb,
ny2 = d.y + rb;
quadtree.visit(function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== d)) {
var x = d.x - quad.point.x,
y = d.y - quad.point.y,
l = Math.sqrt(x * x + y * y);
if (l < rb) {
l = (l - rb) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
});
};
};
window.addEventListener('resize', resize);
function resize() {
width = window.innerWidth, height = window.innerHeight;
svg.attr("width", width).attr("height", height);
force.size([width, height]).resume();
}
});
.node {
stroke: #fff;
stroke-width: 1.5px;
}
.link {
stroke: #999;
stroke-opacity: .6;
}
.node-active{
stroke: #555;
stroke-width: 1.5px;
}
.node:hover{
stroke: #555;
stroke-width: 1.5px;
}
marker {
display:none;
}
.d3-tip {
line-height: 1;
font-weight: bold;
padding: 12px;
background: rgba(0, 0, 0, 0.8);
color: #fff;
border-radius: 2px;
}
.d3-tip.n:after {
margin: -1px 0 0 0;
top: 200%;
left: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<!DOCTYPE html>
<meta charset="utf-8">
<body>
<script src="d3/d3tip.js"></script>
<div class="graph"></div>
</body>

Related

How to drag nodes with children in d3 circle packing?

I have been trying to drag in d3 packed nodes using this code as a basis https://codepen.io/MrHen/pen/GZQOPW. Unfortunately I can't find a method to use so that when a node with children is dragged around it's (visible) children move with the parent. I use this function to drag circles around:
var drag = d3.behavior.drag()
.on("drag", function(d, i) {
d.x += d3.event.dx;
d.y += d3.event.dy;
draw();
})
I the example above for instance I want to be able to drag the nodes on layer 1 (light blue) and when I do this their children change their position so they stay (visually) in the borders of their parents.
Thank you!
First remove the pointer-events: none on the root and middle nodes. Then set yourself up a little recursion to walk a node's descendants and update their position:
function recurOnChildren(d) {
d.x += d3.event.dx;
d.y += d3.event.dy;
if (!d.children) return;
d.children.forEach(c => {
recurOnChildren(c);
});
}
And call from your drag handler:
var drag = d3.behavior.drag()
.on("drag", function(d, i) {
recurOnChildren(d);
draw();
})
Running code:
var margin = 20,
diameter = 960;
var color = d3.scale.linear()
.domain([-1, 5])
.range(["hsl(152,80%,80%)", "hsl(228,30%,40%)"])
.interpolate(d3.interpolateHcl);
var pack = d3.layout.pack()
.padding(2)
.size([diameter - margin, diameter - margin])
.value(function(d) {
return d.size;
})
var svg = d3.select("body").append("svg")
.attr("width", diameter)
.attr("height", diameter)
.append("g")
.attr("transform", "translate(" + diameter / 2 + "," + diameter / 2 + ")");
d3.json("https://gist.githubusercontent.com/mbostock/7607535/raw/695f8ed6298c06a946406c707200a1f6b21875d8/flare.json", function(error, root) {
if (error) throw error;
var focus = root,
nodes = pack.nodes(root),
view;
function recurOnChildren(d) {
d.x += d3.event.dx;
d.y += d3.event.dy;
if (!d.children) return;
d.children.forEach(c => {
recurOnChildren(c);
});
}
var drag = d3.behavior.drag()
.on("drag", function(d, i) {
recurOnChildren(d);
draw();
})
var circle = svg.selectAll("circle")
.data(nodes)
.enter().append("circle")
.attr("class", function(d) {
return d.parent ? d.children ? "node node--middle" : "node node--leaf" : "node node--root";
})
.style("fill", function(d) {
return d.children ? color(d.depth) : null;
});
var text = svg.selectAll("text")
.data(nodes)
.enter().append("text")
.attr("class", "label")
.style("fill-opacity", function(d) {
return d.parent === root ? 1 : 0;
})
.style("display", function(d) {
return d.parent === root ? "inline" : "none";
})
.text(function(d) {
return d.name;
});
var node = svg.selectAll("circle,text");
svg.selectAll(".node").call(drag);
d3.select("body")
.style("background", color(-1))
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;
});
}
});
.node {
cursor: pointer;
}
.node:hover {
stroke: #000;
stroke-width: 1.5px;
}
.node--leaf {
fill: white;
}
.label {
font: 11px "Helvetica Neue", Helvetica, Arial, sans-serif;
text-anchor: middle;
text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, -1px 0 0 #fff, 0 -1px 0 #fff;
}
.label {
pointer-events: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>

D3 tree with boxes

With my limited D3 knowledge, I've combined some tree layout examples to make a collapsible tree with branch labels. Here is a functional extract:
var root = {
children:[
{
title:"Node title",
children:[
{
type:"end",
items:[],
optionTitle:"Branch 1"
},
{
type:"end",
items:[],
optionTitle:"Branch 2"
}
]
}
]
}
var maxLabelLength = 23;
var i = 0;
var duration = 750;
// Define the root
root.x0 = viewerHeight / 2;
root.y0 = 0;
var viewerWidth = 800;
var viewerHeight = 300;
var tree = d3.layout.tree();
var diagonal = d3.svg.diagonal()
.projection(function(d) {
return [d.y, d.x];
});
function visit(parent, visitFn, childrenFn) {
if (!parent) return;
visitFn(parent);
var children = childrenFn(parent);
if (children) {
var count = children.length;
for (var i = 0; i < count; i++) {
visit(children[i], visitFn, childrenFn);
}
}
}
var baseSvg = d3.select('.tree').append("svg")
.attr("width", viewerWidth)
.attr("height", viewerHeight)
.attr("class", "tree-container");
// Helper functions for collapsing and expanding nodes.
function collapse(d) {
if (d.children) {
d._children = d.children;
d._children.forEach(collapse);
d.children = null;
}
}
function centerNode(source) {
var scale = 1;
var x = 20;
var y = -source.x0;
y = y * scale + viewerHeight / 2;
d3.select('g').transition()
.duration(duration)
.attr("transform", "translate(" + x + "," + y + ")scale(" + scale + ")");
}
function toggleChildren(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else if (d._children) {
expand(d);
}
return d;
}
function expand(d) {
if (d._children) {
d.children = d._children;
d._children = null;
if (d.children.length == 1) {
expand(d.children[0])
}
}
}
function click(d) {
if (d._children) {
if (d.type!='end') {
expandCollapse(d);
}
} else {
expandCollapse(d);
}
}
function expandCollapse(d) {
d = toggleChildren(d);
update(d);
centerNode(d);
}
function update(source) {
var levelWidth = [1];
var childCount = function(level, n) {
if (n.children && n.children.length > 0) {
if (levelWidth.length <= level + 1) levelWidth.push(0);
levelWidth[level + 1] += n.children.length;
n.children.forEach(function(d) {
childCount(level + 1, d);
});
}
};
childCount(0, root);
var newHeight = d3.max(levelWidth) * 25;
tree = tree.size([newHeight, viewerWidth]);
// Compute the new tree layout.
var nodes = tree.nodes(root).reverse(),
links = tree.links(nodes);
// Set widths between levels based on maxLabelLength.
nodes.forEach(function(d) {
d.y = (d.depth * (maxLabelLength * 8));
if (d.x<root.x) {
d.x -= (root.x-d.x)*3;
} else if (d.x>root.x) {
d.x += (d.x-root.x)*3;
}
});
// Update the nodes…
var node = svgGroup.selectAll("g.node")
.data(nodes, function(d) {
return d.id || (d.id = ++i);
});
// Enter any new nodes at the parent's previous position.
var nodeEnter = node.enter().append("g")
.attr("class", "node")
.attr("transform", function(d) {
return "translate(" + source.y0 + "," + source.x0 + ")";
})
.on('click', click);
nodeEnter.append("circle")
.attr('class', 'nodeCircle');
// Change the circle fill depending on whether it has children and is collapsed
node.select("circle.nodeCircle")
.attr("r", 6)
.style("fill", function(d) {
return getNodeFill(d);
});
nodeEnter.append("text")
.attr("x", function(d) {
return d.children || d._children ? -10 : 10;
})
.attr("dy", ".35em")
.attr('class', 'nodeText')
.attr("text-anchor", function(d) {
return d.children || d._children ? "end" : "start";
})
.text(function(d) {
return d.title;
})
.style("fill-opacity", 0);
// Update the text to reflect whether node has children or not.
node.select('text')
.attr("x", function(d) {
return d.children || d._children ? -10 : 10;
})
.text(function(d) {
if (d.type!='end') {
return d.title
} else {
return 'End node'
}
});
// Transition nodes to their new position.
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + d.y + "," + d.x + ")";
});
// Fade the text in
nodeUpdate.select("text")
.style("fill-opacity", 1);
// Transition exiting nodes to the parent's new position.
var nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + source.y + "," + source.x + ")";
})
.remove();
nodeExit.select("circle")
.attr("r", 0);
nodeExit.select("text")
.style("fill-opacity", 0);
// Update the links…
var link = svgGroup.selectAll("path.link")
.data(links, function(d) {
return d.target.id;
});
// Enter any new links at the parent's previous position.
link.enter().insert("path", "g")
.attr("class", "link")
.attr("d", function(d) {
var o = {
x: source.x0,
y: source.y0
};
return diagonal({
source: o,
target: o
});
});
// Transition links to their new position.
link.transition()
.duration(duration)
.attr("d", diagonal);
// Transition exiting nodes to the parent's new position.
link.exit().transition()
.duration(duration)
.attr("d", function(d) {
var o = {
x: source.x,
y: source.y
};
return diagonal({
source: o,
target: o
});
})
.remove();
// Update the link text
var linktext = svgGroup.selectAll("g.link")
.data(links, function (d) {
return d.target.id;
});
linktext.enter()
.insert("g")
.attr("class", "link")
.append("text")
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.text(function (d) {
return d.target.optionTitle;
});
// Transition link text to their new positions
linktext.transition()
.duration(duration)
.attr("transform", function (d) {
return "translate(" + ((d.source.y + d.target.y) / 2) + "," + ((d.source.x + d.target.x) / 2) + ")";
})
//Transition exiting link text to the parent's new position.
linktext.exit().transition().remove();
// Stash the old positions for transition.
nodes.forEach(function(d) {
d.x0 = d.x;
d.y0 = d.y;
});
}
var svgGroup = baseSvg.append("g");
// Layout the tree initially and center on the root node.
update(root);
centerNode(root);
svgGroup
.append('defs')
.append('pattern')
.attr('id', function(d,i){
return 'pic_plus';
})
.attr('height',60)
.attr('width',60)
.attr('x',0)
.attr('y',0)
.append('image')
.attr('xlink:href',function(d,i){
return 'https://s3-eu-west-1.amazonaws.com/eq-static/app/images/common/plus.png';
})
.attr('height',12)
.attr('width',12)
.attr('x',0)
.attr('y',0);
function getNodeFill(d) {
if (isFinal(d)) {
return '#0f0';
} else if (d._children || (!d._children&&!d.children)) {
return 'url(#pic_plus)'
} else {
return '#fff'
}
}
function isFinal(node) {
return node.type=='end';
}
body {
background-color: #ddd;
}
.tree-custom,
.tree {
width:100%;
height: 100%;
background-color: #fff;
}
.holder {
margin: 0 auto;
width: 1000px;
height: 800px;
background-color: #fff;
}
.node {
cursor: pointer;
}
.node circle {
fill: #fff;
stroke: steelblue;
stroke-width: 1.5px;
}
.node text {
font: 10px sans-serif;
}
path.link {
fill: none;
stroke: #ccc;
stroke-width: 1.5px;
}
.link text {
font: 10px sans-serif;
fill: #666;
}
<html>
<head>
<script src="https://d3js.org/d3.v3.min.js"></script>
</head>
<body>
<div class="tree"></div>
<script src="code.js"></script>
</body>
</html>
In my code, the nodes are circles with the node labels next to them:
When I collapse a node, a plussign shows up.
Now I'm trying to put the node labels in boxes as it's shown in this example code.
I know I have to change the circles by foreignObjectsas in the example code but when I do it, paths aren't adjusted to the boxes.
How could I change the circles by foreignObjects and maintain the same functionality expand/collapse/plus?
The changes that you need to make to your current layout are:
move the blue boxes to be vertically aligned with the circles and with the right edge adjacent to the left side of each circle;
alter the paths to terminate at the left side of each blue box
The first can be done using a transform to alter the position of the blue boxes, and the second by altering the coordinates for the target point of each line.
Looking at the sample you linked to, there are rect elements behind the foreignObject elements that provide the colour, and the foreignObject is offset from the position of the rect elements. I have taken the liberty of adding the rect elements to your code and grouping the rect and foreignObject elements together so they can be moved in a single transform:
var rectGrpEnter = nodeEnter.append('g')
.attr('class', 'node-rect-text-grp');
rectGrpEnter.append('rect')
.attr('rx', 6)
.attr('ry', 6)
.attr('width', rectNode.width)
.attr('height', rectNode.height)
.attr('class', 'node-rect');
rectGrpEnter.append('foreignObject')
.attr('x', rectNode.textMargin)
.attr('y', rectNode.textMargin)
.attr('width', function() {
return (rectNode.width - rectNode.textMargin * 2) < 0 ? 0 :
(rectNode.width - rectNode.textMargin * 2)
})
.attr('height', function() {
return (rectNode.height - rectNode.textMargin * 2) < 0 ? 0 :
(rectNode.height - rectNode.textMargin * 2)
})
.append('xhtml').html(function(d) {
return '<div style="width: ' +
(rectNode.width - rectNode.textMargin * 2) + 'px; height: ' +
(rectNode.height - rectNode.textMargin * 2) + 'px;" class="node-text wordwrap">' +
'<b>' + d.title + '</b>' +
'</div>';
});
If you look at the tree this produces, the rect/foreignObject group needs to be translated the length of the rect element + the circle radius along the x axis, and by half the height of the rect element along the y axis. So, first let's add a variable to represent the circle radius and replace the hard-coded number with that variable:
var circleRadius = 6;
// a bit further on
node.select("circle.nodeCircle")
.attr("r", circleRadius)
.style("fill", function(d) {
return getNodeFill(d);
});
Now write the transform:
var rectGrpEnter = nodeEnter.append('g')
.attr('class', 'node-rect-text-grp')
.attr('transform', 'translate('
+ -(rectNode.width + circleRadius) + ',' // note the transform is negative
+ -(rectNode.height/2) + ')' );
Check the resulting tree:
var rectNode = {
width: 120,
height: 45,
textMargin: 5
};
var root = {
slideId: 100,
children: [{
title: "Node title",
children: [{
type: "end",
items: [],
optionTitle: "Branch 1"
},
{
type: "end",
items: [],
optionTitle: "Branch 2"
}
]
}]
}
var maxLabelLength = 23;
var i = 0;
var duration = 750;
// Define the root
root.x0 = viewerHeight / 2;
root.y0 = 0;
var viewerWidth = 800;
var viewerHeight = 300;
var tree = d3.layout.tree();
var diagonal = d3.svg.diagonal()
.projection(function(d) {
return [d.y, d.x];
});
function visit(parent, visitFn, childrenFn) {
if (!parent) return;
visitFn(parent);
var children = childrenFn(parent);
if (children) {
var count = children.length;
for (var i = 0; i < count; i++) {
visit(children[i], visitFn, childrenFn);
}
}
}
var baseSvg = d3.select('.tree').append("svg")
.attr("width", viewerWidth)
.attr("height", viewerHeight)
.attr("class", "tree-container");
// Helper functions for collapsing and expanding nodes.
function collapse(d) {
if (d.children) {
d._children = d.children;
d._children.forEach(collapse);
d.children = null;
}
}
function centerNode(source) {
var scale = 1;
var x = 20;
var y = -source.x0;
y = y * scale + viewerHeight / 2;
d3.select('g').transition()
.duration(duration)
.attr("transform", "translate(" + x + "," + y + ")scale(" + scale + ")");
}
function toggleChildren(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else if (d._children) {
expand(d);
}
return d;
}
function expand(d) {
if (d._children) {
d.children = d._children;
d._children = null;
if (d.children.length == 1) {
expand(d.children[0])
}
}
}
function click(d) {
if (d._children) {
if (d.type != 'end') {
expandCollapse(d);
}
} else {
expandCollapse(d);
}
}
function expandCollapse(d) {
d = toggleChildren(d);
update(d);
centerNode(d);
}
function update(source) {
var levelWidth = [1];
var childCount = function(level, n) {
if (n.children && n.children.length > 0) {
if (levelWidth.length <= level + 1) levelWidth.push(0);
levelWidth[level + 1] += n.children.length;
n.children.forEach(function(d) {
childCount(level + 1, d);
});
}
};
childCount(0, root);
var newHeight = d3.max(levelWidth) * 25;
var circleRadius = 6;
tree = tree.size([newHeight, viewerWidth]);
// Compute the new tree layout.
var nodes = tree.nodes(root).reverse(),
links = tree.links(nodes);
// Set widths between levels based on maxLabelLength.
nodes.forEach(function(d) {
d.y = (d.depth * (maxLabelLength * 8));
if (d.x < root.x) {
d.x -= (root.x - d.x) * 3;
} else if (d.x > root.x) {
d.x += (d.x - root.x) * 3;
}
});
// Update the nodes…
var node = svgGroup.selectAll("g.node")
.data(nodes, function(d) {
return d.id || (d.id = ++i);
});
// Enter any new nodes at the parent's previous position.
var nodeEnter = node.enter().append("g")
.attr("class", "node")
.attr("transform", function(d) {
return "translate(" + source.y0 + "," + source.x0 + ")";
})
.on('click', click);
var rectGrpEnter = nodeEnter.append('g')
.attr('class', 'node-rect-text-grp')
.attr('transform', 'translate('
+ -(rectNode.width + circleRadius) + ',' // note the transform is negative
+ -(rectNode.height/2) + ')' );
rectGrpEnter.append('rect')
.attr('rx', 6)
.attr('ry', 6)
.attr('width', rectNode.width)
.attr('height', rectNode.height)
.attr('class', 'node-rect');
rectGrpEnter.append('foreignObject')
.attr('x', rectNode.textMargin)
.attr('y', rectNode.textMargin)
.attr('width', function() {
return (rectNode.width - rectNode.textMargin * 2) < 0 ? 0 :
(rectNode.width - rectNode.textMargin * 2)
})
.attr('height', function() {
return (rectNode.height - rectNode.textMargin * 2) < 0 ? 0 :
(rectNode.height - rectNode.textMargin * 2)
})
.append('xhtml').html(function(d) {
return '<div style="width: ' +
(rectNode.width - rectNode.textMargin * 2) + 'px; height: ' +
(rectNode.height - rectNode.textMargin * 2) + 'px;" class="node-text wordwrap">' +
'<b>' + d.title + '</b>' +
'</div>';
});
nodeEnter.append("circle")
.attr('class', 'nodeCircle');
// Change the circle fill depending on whether it has children and is collapsed
node.select("circle.nodeCircle")
.attr("r", circleRadius)
.style("fill", function(d) {
return getNodeFill(d);
});
nodeEnter.append("text")
.attr("x", function(d) {
return d.children || d._children ? -10 : 10;
})
.attr("dy", ".35em")
.attr('class', 'nodeText')
.attr("text-anchor", function(d) {
return d.children || d._children ? "end" : "start";
})
.text(function(d) {
return d.title;
})
.style("fill-opacity", 0);
// Update the text to reflect whether node has children or not.
node.select('text')
.attr("x", function(d) {
return d.children || d._children ? -10 : 10;
})
.text(function(d) {
if (d.type != 'end') {
return d.title
} else {
return 'End node'
}
});
// Transition nodes to their new position.
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + d.y + "," + d.x + ")";
});
// Fade the text in
nodeUpdate.select("text")
.style("fill-opacity", 1);
// Transition exiting nodes to the parent's new position.
var nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + source.y + "," + source.x + ")";
})
.remove();
nodeExit.select("circle")
.attr("r", 0);
nodeExit.select("text")
.style("fill-opacity", 0);
// Update the links…
var link = svgGroup.selectAll("path.link")
.data(links, function(d) {
return d.target.id;
});
// Enter any new links at the parent's previous position.
link.enter().insert("path", "g")
.attr("class", "link")
.attr("d", function(d) {
var o = {
x: source.x0,
y: source.y0
};
return diagonal({
source: o,
target: o
});
});
// Transition links to their new position.
link.transition()
.duration(duration)
.attr("d", diagonal);
// Transition exiting nodes to the parent's new position.
link.exit().transition()
.duration(duration)
.attr("d", function(d) {
var o = {
x: source.x,
y: source.y
};
return diagonal({
source: o,
target: o
});
})
.remove();
// Update the link text
var linktext = svgGroup.selectAll("g.link")
.data(links, function(d) {
return d.target.id;
});
linktext.enter()
.insert("g")
.attr("class", "link")
.append("text")
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.text(function(d) {
return d.target.optionTitle;
});
// Transition link text to their new positions
linktext.transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + ((d.source.y + d.target.y) / 2) + "," + ((d.source.x + d.target.x) / 2) + ")";
})
//Transition exiting link text to the parent's new position.
linktext.exit().transition().remove();
// Stash the old positions for transition.
nodes.forEach(function(d) {
d.x0 = d.x;
d.y0 = d.y;
});
}
var svgGroup = baseSvg.append("g");
// Layout the tree initially and center on the root node.
update(root);
centerNode(root);
svgGroup
.append('defs')
.append('pattern')
.attr('id', function(d, i) {
return 'pic_plus';
})
.attr('height', 60)
.attr('width', 60)
.attr('x', 0)
.attr('y', 0)
.append('image')
.attr('xlink:href', function(d, i) {
return 'https://s3-eu-west-1.amazonaws.com/eq-static/app/images/common/plus.png';
})
.attr('height', 12)
.attr('width', 12)
.attr('x', 0)
.attr('y', 0);
function getNodeFill(d) {
if (isFinal(d)) {
return '#0f0';
} else if (d._children || (!d._children && !d.children)) {
return 'url(#pic_plus)'
} else {
return '#fff'
}
}
function isFinal(node) {
return node.type == 'end';
}
function isCollapsed(node) {
return d._children || (!d._children && !d.children);
}
body {
background-color: #ddd;
}
.tree-custom,
.tree {
width: 100%;
height: 100%;
background-color: #fff;
}
.holder {
margin: 0 auto;
width: 1000px;
height: 800px;
background-color: #fff;
}
.node {
cursor: pointer;
}
.node circle {
fill: #fff;
stroke: steelblue;
stroke-width: 1.5px;
}
.node text {
font: 10px sans-serif;
}
path.link {
fill: none;
stroke: #ccc;
stroke-width: 1.5px;
}
.link text {
font: 10px sans-serif;
fill: #666;
}
.node-rect {
fill: #00f;
}
.node-text {
color: #fff;
}
<html>
<head>
<script src="https://d3js.org/d3.v3.min.js"></script>
</head>
<body>
<div class="tree"></div>
</body>
</html>
The next task is to alter the links to terminate at the edge of the rect boxes. If you check the code that covers the link positions and transitions, the enter and exit selections both use the position of the source node. The code we are interested in is this:
link.transition()
.duration(duration)
.attr("d", diagonal);
where the diagonal function is
var diagonal = d3.svg.diagonal()
.projection(function(d) {
return [d.y, d.x];
});
d3.svg.diagonal() takes an object of the form
{ source: { x: 10, y: 10 }, target: { x: 20, y: 50 } }
and if you look at each item in the tree.links array, you'll see it is in the form
{ source: { /* source node coordinates */ }, target: { /* target node coords */ }
so to alter the position of the link target, we need to create a new object with the target coordinates altered. Once again, the x axis alteration should be -(rectNode.width + circleRadius); the y axis is OK. Note that the diagonal function switches over the x and y values, though, so we need to alter the target's y value, not the x value. Thus, we have:
// Transition links to their new position.
link.transition()
.duration(duration)
.attr("d", function(d) {
return diagonal({
source: d.source, // this is the same
target: { x: d.target.x, y: d.target.y - (rectNode.width + circleRadius) }
});
});
Add that to our code:
var rectNode = {
width: 120,
height: 45,
textMargin: 5
};
var root = {
slideId: 100,
children: [{
title: "Node title",
children: [{
type: "end",
items: [],
optionTitle: "Branch 1"
},
{
type: "end",
items: [],
optionTitle: "Branch 2"
}
]
}]
}
var maxLabelLength = 23;
var i = 0;
var duration = 750;
// Define the root
root.x0 = viewerHeight / 2;
root.y0 = 0;
var viewerWidth = 800;
var viewerHeight = 300;
var tree = d3.layout.tree();
var diagonal = d3.svg.diagonal()
.projection(function(d) {
return [d.y, d.x];
});
function visit(parent, visitFn, childrenFn) {
if (!parent) return;
visitFn(parent);
var children = childrenFn(parent);
if (children) {
var count = children.length;
for (var i = 0; i < count; i++) {
visit(children[i], visitFn, childrenFn);
}
}
}
var baseSvg = d3.select('.tree').append("svg")
.attr("width", viewerWidth)
.attr("height", viewerHeight)
.attr("class", "tree-container");
// Helper functions for collapsing and expanding nodes.
function collapse(d) {
if (d.children) {
d._children = d.children;
d._children.forEach(collapse);
d.children = null;
}
}
function centerNode(source) {
var scale = 1;
var x = 20;
var y = -source.x0;
y = y * scale + viewerHeight / 2;
d3.select('g').transition()
.duration(duration)
.attr("transform", "translate(" + x + "," + y + ")scale(" + scale + ")");
}
function toggleChildren(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else if (d._children) {
expand(d);
}
return d;
}
function expand(d) {
if (d._children) {
d.children = d._children;
d._children = null;
if (d.children.length == 1) {
expand(d.children[0])
}
}
}
function click(d) {
if (d._children) {
if (d.type != 'end') {
expandCollapse(d);
}
} else {
expandCollapse(d);
}
}
function expandCollapse(d) {
d = toggleChildren(d);
update(d);
centerNode(d);
}
function update(source) {
var levelWidth = [1];
var childCount = function(level, n) {
if (n.children && n.children.length > 0) {
if (levelWidth.length <= level + 1) levelWidth.push(0);
levelWidth[level + 1] += n.children.length;
n.children.forEach(function(d) {
childCount(level + 1, d);
});
}
};
childCount(0, root);
var newHeight = d3.max(levelWidth) * 25;
var circleRadius = 6;
tree = tree.size([newHeight, viewerWidth]);
// Compute the new tree layout.
var nodes = tree.nodes(root).reverse(),
links = tree.links(nodes);
// Set widths between levels based on maxLabelLength.
nodes.forEach(function(d) {
d.y = (d.depth * (maxLabelLength * 8));
if (d.x < root.x) {
d.x -= (root.x - d.x) * 3;
} else if (d.x > root.x) {
d.x += (d.x - root.x) * 3;
}
});
// Update the nodes…
var node = svgGroup.selectAll("g.node")
.data(nodes, function(d) {
return d.id || (d.id = ++i);
});
// Enter any new nodes at the parent's previous position.
var nodeEnter = node.enter().append("g")
.attr("class", "node")
.attr("transform", function(d) {
return "translate(" + source.y0 + "," + source.x0 + ")";
})
.on('click', click);
var rectGrpEnter = nodeEnter.append('g')
.attr('class', 'node-rect-text-grp')
.attr('transform', 'translate(' +
-(rectNode.width + circleRadius) + ',' // note the transform is negative
+
-(rectNode.height / 2) + ')');
rectGrpEnter.append('rect')
.attr('rx', 6)
.attr('ry', 6)
.attr('width', rectNode.width)
.attr('height', rectNode.height)
.attr('class', 'node-rect');
rectGrpEnter.append('foreignObject')
.attr('x', rectNode.textMargin)
.attr('y', rectNode.textMargin)
.attr('width', function() {
return (rectNode.width - rectNode.textMargin * 2) < 0 ? 0 :
(rectNode.width - rectNode.textMargin * 2)
})
.attr('height', function() {
return (rectNode.height - rectNode.textMargin * 2) < 0 ? 0 :
(rectNode.height - rectNode.textMargin * 2)
})
.append('xhtml').html(function(d) {
return '<div style="width: ' +
(rectNode.width - rectNode.textMargin * 2) + 'px; height: ' +
(rectNode.height - rectNode.textMargin * 2) + 'px;" class="node-text wordwrap">' +
'<b>' + d.title + '</b>' +
'</div>';
});
nodeEnter.append("circle")
.attr('class', 'nodeCircle');
// Change the circle fill depending on whether it has children and is collapsed
node.select("circle.nodeCircle")
.attr("r", circleRadius)
.style("fill", function(d) {
return getNodeFill(d);
});
nodeEnter.append("text")
.attr("x", function(d) {
return d.children || d._children ? -10 : 10;
})
.attr("dy", ".35em")
.attr('class', 'nodeText')
.attr("text-anchor", function(d) {
return d.children || d._children ? "end" : "start";
})
.text(function(d) {
return d.title;
})
.style("fill-opacity", 0);
// Update the text to reflect whether node has children or not.
node.select('text')
.attr("x", function(d) {
return d.children || d._children ? -10 : 10;
})
.text(function(d) {
if (d.type != 'end') {
return d.title
} else {
return 'End node'
}
});
// Transition nodes to their new position.
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + d.y + "," + d.x + ")";
});
// Fade the text in
nodeUpdate.select("text")
.style("fill-opacity", 1);
// Transition exiting nodes to the parent's new position.
var nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + source.y + "," + source.x + ")";
})
.remove();
nodeExit.select("circle")
.attr("r", 0);
nodeExit.select("text")
.style("fill-opacity", 0);
// Update the links…
var link = svgGroup.selectAll("path.link")
.data(links, function(d) {
return d.target.id;
});
// Enter any new links at the parent's previous position.
link.enter().insert("path", "g")
.attr("class", "link")
.attr("d", function(d) {
var o = {
x: source.x0,
y: source.y0
};
return diagonal({
source: o,
target: o
});
});
// Transition links to their new position.
link.transition()
.duration(duration)
.attr("d", function(d) {
return diagonal({
source: d.source,
target: { x: d.target.x, y: d.target.y - (rectNode.width + circleRadius) }
});
});
// Transition exiting nodes to the parent's new position.
link.exit().transition()
.duration(duration)
.attr("d", function(d) {
var o = {
x: source.x,
y: source.y
};
return diagonal({
source: o,
target: o
});
})
.remove();
// Update the link text
var linktext = svgGroup.selectAll("g.link")
.data(links, function(d) {
return d.target.id;
});
linktext.enter()
.insert("g")
.attr("class", "link")
.append("text")
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.text(function(d) {
return d.target.optionTitle;
});
// Transition link text to their new positions
linktext.transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + ((d.source.y + d.target.y) / 2) + "," + ((d.source.x + d.target.x) / 2) + ")";
})
//Transition exiting link text to the parent's new position.
linktext.exit().transition().remove();
// Stash the old positions for transition.
nodes.forEach(function(d) {
d.x0 = d.x;
d.y0 = d.y;
});
}
var svgGroup = baseSvg.append("g");
// Layout the tree initially and center on the root node.
update(root);
centerNode(root);
svgGroup
.append('defs')
.append('pattern')
.attr('id', function(d, i) {
return 'pic_plus';
})
.attr('height', 60)
.attr('width', 60)
.attr('x', 0)
.attr('y', 0)
.append('image')
.attr('xlink:href', function(d, i) {
return 'https://s3-eu-west-1.amazonaws.com/eq-static/app/images/common/plus.png';
})
.attr('height', 12)
.attr('width', 12)
.attr('x', 0)
.attr('y', 0);
function getNodeFill(d) {
if (isFinal(d)) {
return '#0f0';
} else if (d._children || (!d._children && !d.children)) {
return 'url(#pic_plus)'
} else {
return '#fff'
}
}
function isFinal(node) {
return node.type == 'end';
}
function isCollapsed(node) {
return d._children || (!d._children && !d.children);
}
body {
background-color: #ddd;
}
.tree-custom,
.tree {
width: 100%;
height: 100%;
background-color: #fff;
}
.holder {
margin: 0 auto;
width: 1000px;
height: 800px;
background-color: #fff;
}
.node {
cursor: pointer;
}
.node circle {
fill: #fff;
stroke: steelblue;
stroke-width: 1.5px;
}
.node text {
font: 10px sans-serif;
}
path.link {
fill: none;
stroke: #ccc;
stroke-width: 1.5px;
}
.link text {
font: 10px sans-serif;
fill: #666;
}
.node-rect {
fill: #00f;
}
.node-text {
color: #fff;
}
<html>
<head>
<script src="https://d3js.org/d3.v3.min.js"></script>
</head>
<body>
<div class="tree"></div>
</body>
</html>
You can check that the links are terminating in the correct place by removing the fill on the rect elements.
There are now a number of other fixes to be made, but that should have proved your proof of concept; if you are unclear about any parts of the new code, please just ask.

D3 Nodes not appearing while updating it realtime

I am playing with the Mike Bostock's mobile patent suits example
I am adding ~100 nodes using forEach loop in this JSFiddle; In reality, these nodes will be passed by an external service in real-time.
The page is loading a few static nodes initially. The reset button calls the forEach loop to create the new nodes using the function add_prc. This method pushes the node to nodes list (used to display in SVG) and then calls the refresh method but the nodes do not appear on the screen properly.
I see a dot at top-left of the screen but I can't select it or drag it to the center.
Once you add the nodes/links using the forEach loop, you aren't translating them anywhere. Check out this screenshot of the console:
As I mentioned in the comments, the tick function applies transform to the previously added path, circle and text but does it add anything to the newly added nodes, links? NO. So that's the thing that's missing. As I know your previous question, I'm adding the code from that to this:
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;
});
node.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
Now, this would add all nodes at (100, 100) (I know you're just testing things). Anyway, I've used random x and ys. Here's a snippet and a JSFIDDLE
.node {
fill: #000;
}
.cursor {
fill: green;
stroke: brown;
pointer-events: none;
}
.node text {
pointer-events: none;
font: 10px sans-serif;
}
path.link {
fill: none;
stroke: #666;
stroke-width: 1.5px;
}
.link {
fill: none;
stroke: #666;
stroke-width: 1.5px;
}
#licensing {
fill: green;
}
.link.licensing {
stroke: green;
}
.link.resolved {
stroke-dasharray: 0,2 1;
}
circle {
fill: green;
stroke: red;
stroke-width: 1.5px;
}
text {
font: 10px sans-serif;
pointer-events: none;
text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, 0 -1px 0 #fff, -1px 0 0 #fff;
}
<button id="reset" onclick="reset()">reset</button>
<button id="ref" onclick="refresh()">refresh</button>
<script src="https://d3js.org/d3.v3.min.js"></script>
<script>
var width = 960, height = 500;
var links = [{source:"simulator",target:"monitor" ,type:"resolved"} , {source:"web",target:"monitor" ,type:"resolved"} ];
var links1 = [{"source":"ctfa","target":"tfa"},
{"source":"cea","target":"tea"},
{"source":"ctfe","target":"tfe"},
{"source":"ctee","target":"tee"},
{"source":"ctfu","target":"tfu"},
{"source":"cteu","target":"teu"},
{"source":"rfa","target":"tfa"},
{"source":"rea","target":"tea"},
{"source":"rfe","target":"tfe"},
{"source":"ree","target":"tee"},
{"source":"rfu","target":"tfu"},
{"source":"reu","target":"teu"},
{"source":"r1fa","target":"rfa"},
{"source":"r1fa","target":"gfa"},
{"source":"r1fa","target":"ggf"},
{"source":"r1ea","target":"rea"},
{"source":"r1ea","target":"gea"},
{"source":"r1ea","target":"gge"},
{"source":"r1fe","target":"rfe"},
{"source":"r1fe","target":"gfe"},
{"source":"r1fe","target":"ggf"},
{"source":"r1ee","target":"ree"},
{"source":"r1ee","target":"gee"},
{"source":"r1ee","target":"gge"},
{"source":"r1fu","target":"rfu"},
{"source":"r1fu","target":"gfu"},
{"source":"r1fu","target":"ggf"},
{"source":"r1eu","target":"reu"},
{"source":"r1eu","target":"geu"},
{"source":"r1eu","target":"gge"},
{"source":"hh1fa","target":"ggf"},
{"source":"hh1ea","target":"gge"},
{"source":"hh1fe","target":"ggf"},
{"source":"hh1ee","target":"gge"},
{"source":"hh1fu","target":"ggf"},
{"source":"hh1eu","target":"gge"},
{"source":"dbfa","target":"gfa"},
{"source":"dbea","target":"gea"},
{"source":"dbfe","target":"gfe"},
{"source":"dbee","target":"gee"},
{"source":"dbfu","target":"gfu"},
{"source":"dbeu","target":"geu"},
{"source":"hflse","target":"tee"},
{"source":"hfnyse","target":"teu"},
{"source":"hfnse","target":"teu"},
{"source":"hfret","target":"tfu"},
{"source":"hfebs","target":"tfe"},
{"source":"hfint","target":"tfu"},
{"source":"c1e","target":"ctee"},
{"source":"c1e","target":"gge"},
{"source":"c2e","target":"ctee"},
{"source":"c3e","target":"cteu"},
{"source":"c4e","target":"cteu"},
{"source":"c5e","target":"ggf"},
{"source":"d1e","target":"ctee"},
{"source":"c1f","target":"ctfe"},
{"source":"c2f","target":"ctfe"},
{"source":"c3f","target":"ggf"},
{"source":"c4f","target":"gge"},
{"source":"c5f","target":"ctfa"},
{"source":"d1f","target":"ctfe"}];
var nodes1 = [{"id":"tfa"},
{"id":"tea"},
{"id":"tfe"},
{"id":"tee"},
{"id":"tfu"},
{"id":"teu"},
{"id":"ctfa"},
{"id":"cea"},
{"id":"ctfe"},
{"id":"ctee"},
{"id":"ctfu"},
{"id":"cteu"},
{"id":"rfa"},
{"id":"rea"},
{"id":"rfe"},
{"id":"ree"},
{"id":"rfu"},
{"id":"reu"},
{"id":"r1fa"},
{"id":"r1ea"},
{"id":"r1fe"},
{"id":"r1ee"},
{"id":"r1fu"},
{"id":"r1eu"},
{"id":"hh1fa"},
{"id":"hh1ea"},
{"id":"hh1fe"},
{"id":"hh1ee"},
{"id":"hh1fu"},
{"id":"hh1eu"},
{"id":"dbfa"},
{"id":"dbea"},
{"id":"dbfe"},
{"id":"dbee"},
{"id":"dbfu"},
{"id":"dbeu"},
{"id":"gfa"},
{"id":"gea"},
{"id":"gfe"},
{"id":"gee"},
{"id":"gfu"},
{"id":"geu"},
{"id":"gge"},
{"id":"ggf"},
{"id":"hflse"},
{"id":"hfnyse"},
{"id":"hfnse"},
{"id":"hfret"},
{"id":"hfebs"},
{"id":"hfint"},
{"id":"c1e"},
{"id":"c2e"},
{"id":"c3e"},
{"id":"c4e"},
{"id":"c5e"},
{"id":"d1e"},
{"id":"c1f"},
{"id":"c2f"},
{"id":"c3f"},
{"id":"c4f"},
{"id":"c5f"},
{"id":"d1f"}];
var nodes = [ {"id":"monitor", "grp":"system"}, {"id":"simulator", "grp":"system"}, {id:"web", grp:"client"}];
function reset() {
nodes1.forEach(function(d){ add_prc(d) });
links1.forEach(function(d){ add_con(d) });
}
function add_prc(newNode) {
//console.log(newNode);
addNodeCanvas(newNode.id,newNode.grp);
}
function add_con(newConnection) {
//console.log(newConnection);
addLinkCanvas( newConnection.source,newConnection.target);
}
//setInterval(refresh, 15000);
function addNodeCanvas(nodeName,g) {
var node1 = { x: Math.floor(Math.random()*200+100), y: Math.floor(Math.random()*200+100), id: nodeName, grp:g };
var n = nodes.push(node1);
//console.log(node1);
refresh();
}
function addLinkCanvas(idSrc, idTarget) {
if (idSrc != idTarget) {
var s = {}, t = {};
nodes.forEach(function(curNode) {
if (typeof curNode.id != "undefined") {
if (curNode.id == idSrc) { s = curNode; }
if (curNode.id == idTarget) { t = curNode; }
}
});
//console.log( { s,t});
links.push({ source: s, target: t });
};
refresh();
}
var width = 900,
height = 600,
radius = 8;
var map = {}
nodes.forEach(function(d,i){
map[d.id] = i;
})
links.forEach(function(d) {
d.source = map[d.source];
d.target = map[d.target];
})
var force = d3.layout.force()
.nodes(d3.values(nodes))
.links(links)
.size([width, height])
.linkDistance(50)
.charge(-200)
.on("tick", tick)
.start();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
// Per-type markers, as they don't inherit styles.
svg.append("defs").selectAll("marker")
.data(["suit", "licensing", "resolved"])
.enter().append("marker")
.attr("id", function(d) { return d; })
.attr("viewBox", "0 -5 10 10")
.attr("refX", 15)
.attr("refY", -1.5)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.append("path")
.attr("d", "M0,-5L10,0L0,5");
var path = svg.append("g").selectAll("path")
.data(force.links())
.enter().append("path")
.attr("class", function(d) { return "link " + d.type; })
.attr("marker-end", function(d) { return "url(#" + d.type + ")"; });
var circle = svg.append("g").selectAll("circle")
.data(force.nodes())
.enter().append("circle")
.attr("r", 6)
.call(force.drag);
var text = svg.append("g").selectAll("text")
.data(force.nodes())
.enter().append("text")
.attr("x", 8)
.attr("y", ".31em")
.text(function(d) { return d.id; });
var node = svg.selectAll(".node"),
link = svg.selectAll(".link");
function mousedownNode(d, i) {
nodes.splice(i, 1);
links = links.filter(function(l) {
return l.source !== d && l.target !== d;
});
d3.event.stopPropagation();
refresh();
}
// Use elliptical arc path segments to doubly-encode directionality.
function tick() {
path.attr("d", linkArc);
circle.attr("transform", transform);
text.attr("transform", transform);
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;
});
node.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
}
function linkArc(d) {
var dx = d.target.x - d.source.x,
dy = d.target.y - d.source.y,
dr = Math.sqrt(dx * dx + dy * dy);
return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
}
function transform(d) {
return "translate(" + d.x + "," + d.y + ")";
}
function refresh(){
node = node.data(nodes);
node.enter().insert("circle", ".cursor")
.attr("class", "node")
.attr("r", 5)
.on("mousedown", mousedownNode);
node.exit()
.remove();
link = link.data(links);
link.enter().insert("line", ".node")
.attr("class", "link");
link.exit()
.remove();
force.start();
}
</script>

D3 transition along segments of path and pause at coordinate values

I would like to be able to click on a circle (coordinate points); bring the marker to the position of the circle and pause at the position of the circle and then resume again along the path.
In addition I would like to activate a circle when marker is paused on them - they are clicked (or their Voronoi cell is clicked). My intention is to have an on click function to an href for the circle coordinates eventually.
I think I need to pass the index of the path coordinates into the translateAlong function instead of the time variables but can't work out how to do this.
I’m not sure if the Voronoi cells are necessary - I tried to add this thinking I could pause my transition and activate my circles with the Voronoi cells. In any case I can’t activate the circle with the Voronoi cell.
I was helped considerably recently on Stackoverflow d3 on click on circle pause and resume transition of marker along line
and I am hoping for assistance again
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>basic_animateBetweenCircles</title>
<script src="https://d3js.org/d3.v4.min.js"></script>
<style>
path {
stroke: #848484;
fill: none;
}
circle {
fill: steelblue;
stroke: steelblue;
stroke-width: 3px;
}
.line {
fill: none;
stroke: #FE642E;
stroke-width: 4;
stroke-dasharray: 4px, 8px;
}
.point{
fill:#DF013A;
}
</style>
</head>
<body>
<script>
var width = 960,
height = 500;
var data = [
[480, 200],
[580, 400],
[680, 100],
[780, 300],
[180, 300],
[280, 100],
[380, 400]
];
//check index of path data
for (var i = 0; i < data.length; i++) {
var coordindex = i + " " + data[i];
console.log("Coordindex: " + coordindex);
//return coordindex;
};
var duration = 20000;
var line = d3.line()
.x(function(d) {return (d)[0];})
.y(function(d) {return (d)[1];});
var voronoi = d3.voronoi()
.extent([[0, 0], [width, height]]);
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
//path to animate - marker transitions along this path
var path = svg.append("path")
.data([data])
.attr("d", line)
.attr('class', 'line')
.attr("d", function(d) {
return line(d)
});
//voronoi
var voronoiPath = svg.append("g")
.selectAll("path")
.data(voronoi.polygons(data))
.enter().append("path")
.attr("d", polygon)
.on("touchmove mousemove", function() {
d3.select(this)
.style("fill", "purple");
});
//Want to activate circles when marker paused on them / in voronoi cell - intention is to have on click to href
svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("class", "point")
.attr("r", 10)
.attr("transform", function(d) { return "translate(" + d + ")"; })
.on('click', function(d, i) {
d3.select(this)
.style("fill", "green");
if (d3.active(this)) {
marker.transition();
setTimeout(function() {
pauseValues.lastTime = pauseValues.currentTime;
//console.log(pauseValues);
}, 100);
} else {
transition();
}
});
var pauseValues = {
lastTime: 0,
currentTime: 0
};
//marker to transition along path
var marker = svg.append("circle")
.attr("r", 19)
.attr("transform", "translate(" + (data[0]) + ")")
.on('click', function(d, i) {
if (d3.active(this)) {
marker.transition();
setTimeout(function() {
pauseValues.lastTime = pauseValues.currentTime;
//console.log(pauseValues);
}, 100);
} else {
transition();
}
});
function transition() {
marker.transition()
.duration(duration - (duration * pauseValues.lastTime))
.attrTween("transform", translateAlong(path.node()))
.on("end", function() {
pauseValues = {
lastTime: 0,
currentTime: 0
};
transition()
});
}
function translateAlong(path) {
var l = path.getTotalLength();
return function(d, i, a) {
return function(t) {
t += pauseValues.lastTime;
var p = path.getPointAtLength(t * l);
pauseValues.currentTime = t;
return "translate(" + p.x + "," + p.y + ")";
};
};
}
function polygon(d) {
return "M" + d.join("L") + "Z";
}
</script>
</body>
If you want to pause at points, I would not run one transition across the entire path. Instead, I would break it up into N transitions, moving from point to point. Before starting the circle on it's next leg, you can pause it for a time. To do this, I would just transition along each line segment with a little algebra:
// copy our data
transData = data.slice();
function transition() {
marker.transition()
.ease(d3.easeLinear)
.duration(duration)
.attrTween("transform", function(){
// get our two points
// slope between them
// and intercetp
var p0 = transData.shift(),
p1 = transData[0];
m = (p0[1] - p1[1]) / (p0[0] - p1[0]),
b = p0[1] - (m * p0[0]),
i = d3.interpolateNumber(p0[0], p1[0]);
// move the point along the line
return function(t){
var x = i(t),
y = m*x + b;
return "translate(" + x + "," + y + ")";
}
})
// one line segment is complete
.on("end", function(){
// if no more movements, stop
if (transData.length <= 1) return;
iter++;
// determine if this is a "pause"
setTimeout(transition, pausePoints.indexOf(iter) !== -1 ? pauseTime : 0);
});
Running code, click a dot to start you can pause a multiple points:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>basic_animateBetweenCircles</title>
<script src="https://d3js.org/d3.v4.min.js"></script>
<style>
path {
stroke: #848484;
fill: none;
}
circle {
fill: steelblue;
stroke: steelblue;
stroke-width: 3px;
}
.line {
fill: none;
stroke: #FE642E;
stroke-width: 4;
stroke-dasharray: 4px, 8px;
}
.point {
fill: #DF013A;
}
</style>
</head>
<body>
<script>
var width = 960,
height = 500;
var data = [
[480, 200],
[580, 400],
[680, 100],
[780, 300],
[180, 300],
[280, 100],
[380, 400]
];
var duration = 20000/data.length,
pauseTime = 2000;
var line = d3.line()
.x(function(d) {
return (d)[0];
})
.y(function(d) {
return (d)[1];
});
var voronoi = d3.voronoi()
.extent([
[0, 0],
[width, height]
]);
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
//path to animate - marker transitions along this path
var path = svg.append("path")
.data([data])
.attr("d", line)
.attr('class', 'line')
.attr("d", function(d) {
return line(d)
});
//voronoi
var voronoiPath = svg.append("g")
.selectAll("path")
.data(voronoi.polygons(data))
.enter().append("path")
.attr("d", polygon);
//Want to activate circles when marker paused on them / in voronoi cell - intention is to have on click to href
svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("class", "point")
.attr("r", 10)
.attr("transform", function(d) {
return "translate(" + d + ")";
})
.on('click', function(d, i) {
d3.select(this)
.style("fill", "green");
pausePoints.push(i);
if (pausePoints.length === 1)
transition();
});
//marker to transition along path
var marker = svg.append("circle")
.attr("r", 19)
.attr("transform", "translate(" + (data[0]) + ")");
var pausePoints = [],
iter = 0,
transData = data.slice();
function transition() {
marker.transition()
.ease(d3.easeLinear)
.duration(duration)
.attrTween("transform", function(){
var p0 = transData.shift(),
p1 = transData[0];
m = (p0[1] - p1[1]) / (p0[0] - p1[0]),
b = p0[1] - (m * p0[0]),
i = d3.interpolateNumber(p0[0], p1[0]);
return function(t){
var x = i(t),
y = m*x + b;
return "translate(" + x + "," + y + ")";
}
})
.on("end", function(){
if (transData.length <= 1) return;
iter++;
setTimeout(transition, pausePoints.indexOf(iter) !== -1 ? pauseTime : 0);
});
}
function polygon(d) {
return "M" + d.join("L") + "Z";
}
</script>
</body>

Wrapping multi line labels

I'm building a responsive zoomable treemap based on this project. The problem is that the labels I've are longer than the original visualization and end up not showing:
function text(text) {
text.selectAll("tspan")
.attr("x", function(d) { return x(d.x) + 6; })
text.attr("x", function(d) { return x(d.x) + 6; })
.attr("y", function(d) { return y(d.y) + 6; })
.style("opacity", function(d) { return this.getComputedTextLength() < x(d.x + d.dx) - x(d.x) ? 1 : 0; });
}
The problem I see is that the text needs to be showed completely in the first line, but I'd like to show it in multiple lines (inside the rect) instead.
There's a code by Mike Bostock which seems to solve this issue but I don't know how to apply it to the treemap.
Here's a quick a modification which wraps the parent text in that example:
<!DOCTYPE html>
<!--
Generic treemap, based on http://bost.ocks.org/mike/treemap/
-->
<html>
<head>
<meta charset="utf-8">
<title>Zoomable treemap template</title>
<style>
#chart {
background: #fff;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
.title {
font-weight: bold;
font-size: 24px;
text-align: center;
margin-top: 6px;
margin-bottom: 6px;
}
text {
pointer-events: none;
}
.grandparent text {
font-weight: bold;
}
rect {
fill: none;
stroke: #fff;
}
rect.parent,
.grandparent rect {
stroke-width: 2px;
}
rect.parent {
pointer-events: none;
}
.grandparent rect {
fill: orange;
}
.grandparent:hover rect {
fill: #ee9700;
}
.children rect.parent,
.grandparent rect {
cursor: pointer;
}
.children rect.parent {
fill: #bbb;
fill-opacity: .5;
}
.children:hover rect.child {
fill: #bbb;
}
</style>
</head>
<body>
<div id="chart"></div>
<script src="http://code.jquery.com/jquery-1.7.2.min.js"></script>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
window.addEventListener('message', function(e) {
var opts = e.data.opts,
data = e.data.data;
return main(opts, data);
});
var defaults = {
margin: {
top: 24,
right: 0,
bottom: 0,
left: 0
},
rootname: "TOP",
format: ",d",
title: "",
width: 960,
height: 500
};
function main(o, data) {
var root,
opts = $.extend(true, {}, defaults, o),
formatNumber = d3.format(opts.format),
rname = opts.rootname,
margin = opts.margin,
theight = 36 + 16;
$('#chart').width(opts.width).height(opts.height);
var width = opts.width - margin.left - margin.right,
height = opts.height - margin.top - margin.bottom - theight,
transitioning;
var color = d3.scale.category20c();
var x = d3.scale.linear()
.domain([0, width])
.range([0, width]);
var y = d3.scale.linear()
.domain([0, height])
.range([0, height]);
var treemap = d3.layout.treemap()
.children(function(d, depth) {
return depth ? null : d._children;
})
.sort(function(a, b) {
return a.value - b.value;
})
.ratio(height / width * 0.5 * (1 + Math.sqrt(5)))
.round(false);
var svg = d3.select("#chart").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.bottom + margin.top)
.style("margin-left", -margin.left + "px")
.style("margin.right", -margin.right + "px")
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.style("shape-rendering", "crispEdges");
var grandparent = svg.append("g")
.attr("class", "grandparent");
grandparent.append("rect")
.attr("y", -margin.top)
.attr("width", width)
.attr("height", margin.top);
grandparent.append("text")
.attr("x", 6)
.attr("y", 6 - margin.top)
.attr("dy", ".75em");
if (opts.title) {
$("#chart").prepend("<p class='title'>" + opts.title + "</p>");
}
if (data instanceof Array) {
root = {
key: rname,
values: data
};
} else {
root = data;
}
initialize(root);
accumulate(root);
layout(root);
console.log(root);
display(root);
if (window.parent !== window) {
var myheight = document.documentElement.scrollHeight || document.body.scrollHeight;
window.parent.postMessage({
height: myheight
}, '*');
}
function initialize(root) {
root.x = root.y = 0;
root.dx = width;
root.dy = height;
root.depth = 0;
}
// Aggregate the values for internal nodes. This is normally done by the
// treemap layout, but not here because of our custom implementation.
// We also take a snapshot of the original children (_children) to avoid
// the children being overwritten when when layout is computed.
function accumulate(d) {
return (d._children = d.values) ? d.value = d.values.reduce(function(p, v) {
return p + accumulate(v);
}, 0) : d.value;
}
// Compute the treemap layout recursively such that each group of siblings
// uses the same size (1×1) rather than the dimensions of the parent cell.
// This optimizes the layout for the current zoom state. Note that a wrapper
// object is created for the parent node for each group of siblings so that
// the parent’s dimensions are not discarded as we recurse. Since each group
// of sibling was laid out in 1×1, we must rescale to fit using absolute
// coordinates. This lets us use a viewport to zoom.
function layout(d) {
if (d._children) {
treemap.nodes({
_children: d._children
});
d._children.forEach(function(c) {
c.x = d.x + c.x * d.dx;
c.y = d.y + c.y * d.dy;
c.dx *= d.dx;
c.dy *= d.dy;
c.parent = d;
layout(c);
});
}
}
function display(d) {
grandparent
.datum(d.parent)
.on("click", transition)
.select("text")
.text(name(d));
var g1 = svg.insert("g", ".grandparent")
.datum(d)
.attr("class", "depth");
var g = g1.selectAll("g")
.data(d._children)
.enter().append("g");
g.filter(function(d) {
return d._children;
})
.classed("children", true)
.on("click", transition);
var children = g.selectAll(".child")
.data(function(d) {
return d._children || [d];
})
.enter().append("g");
children.append("rect")
.attr("class", "child")
.call(rect)
.append("title")
.text(function(d) {
return d.key + " (" + formatNumber(d.value) + ")";
});
children.append("text")
.attr("class", "ctext")
.text(function(d) {
return d.key;
})
.call(text2);
g.append("rect")
.attr("class", "parent")
.call(rect);
var t = g.append("text")
.attr("class", "ptext")
.attr("dy", ".75em")
t.append("tspan")
.text(function(d) {
return d.key;
});
t.append("tspan")
.attr("dy", "1.0em")
.text(function(d) {
return formatNumber(d.value);
});
t.call(text);
g.selectAll("rect")
.style("fill", function(d) {
return color(d.key);
});
function transition(d) {
if (transitioning || !d) return;
transitioning = true;
var g2 = display(d),
t1 = g1.transition().duration(750),
t2 = g2.transition().duration(750);
// Update the domain only after entering new elements.
x.domain([d.x, d.x + d.dx]);
y.domain([d.y, d.y + d.dy]);
// Enable anti-aliasing during the transition.
svg.style("shape-rendering", null);
// Draw child nodes on top of parent nodes.
svg.selectAll(".depth").sort(function(a, b) {
return a.depth - b.depth;
});
// Fade-in entering text.
g2.selectAll("text").style("fill-opacity", 0);
// Transition to the new view.
t1.selectAll(".ptext").call(text).style("fill-opacity", 0);
t1.selectAll(".ctext").call(text2).style("fill-opacity", 0);
t2.selectAll(".ptext").call(text).style("fill-opacity", 1);
t2.selectAll(".ctext").call(text2).style("fill-opacity", 1);
t1.selectAll("rect").call(rect);
t2.selectAll("rect").call(rect);
// Remove the old node when the transition is finished.
t1.remove().each("end", function() {
svg.style("shape-rendering", "crispEdges");
transitioning = false;
});
}
return g;
}
function text(text) {
text.selectAll("tspan")
.attr("x", function(d) {
return x(d.x) + 6;
})
text.attr("x", function(d) {
return x(d.x) + 6;
})
.attr("y", function(d) {
return y(d.y) + 6;
})
.each(function(d) {
var tspan = this.childNodes[0];
var w = x(d.x + d.dx) - x(d.x);
wrap(tspan, w, x(d.x) + 6);
})
}
function text2(text) {
text.attr("x", function(d) {
return x(d.x + d.dx) - this.getComputedTextLength() - 6;
})
.attr("y", function(d) {
return y(d.y + d.dy) - 6;
})
.style("opacity", function(d) {
return this.getComputedTextLength() < x(d.x + d.dx) - x(d.x) ? 1 : 0;
});
}
function rect(rect) {
rect.attr("x", function(d) {
return x(d.x);
})
.attr("y", function(d) {
return y(d.y);
})
.attr("width", function(d) {
return x(d.x + d.dx) - x(d.x);
})
.attr("height", function(d) {
return y(d.y + d.dy) - y(d.y);
});
}
function name(d) {
return d.parent ? name(d.parent) + " / " + d.key + " (" + formatNumber(d.value) + ")" : d.key + " (" + formatNumber(d.value) + ")";
}
}
if (window.location.hash === "") {
d3.json("https://jsonblob.com/api/7c30e101-da91-11e6-90ab-11c211a4b3d5", function(err, res) {
if (!err) {
console.log(res);
var data = d3.nest().key(function(d) {
return d.region;
}).key(function(d) {
return d.subregion;
}).entries(res);
main({
title: "World Population"
}, {
key: "World",
values: data
});
}
});
}
function wrap(tspan, width, x) {
var text = d3.select(tspan),
words = text.text().split(/\s+/).reverse(),
word,
line = [],
y = text.attr("y"),
dy = parseFloat(text.attr("dy")) || 0.4,
tspan = text.text(null).append("tspan").attr("x", x).attr("y", y).attr("dy", "0.75em");
while (word = words.pop()) {
line.push(word);
tspan.text(line.join(" "));
if (tspan.node().getComputedTextLength() > width) {
line.pop();
tspan.text(line.join(" "));
line = [word];
tspan = text.append("tspan").attr("x", x).attr("y", y).attr("dy", "1em").text(word);
}
}
}
</script>
</body>
</html>

Resources