Related
I am looking for a way to add a button to a link connecting nodes in a TreeLayout in D3.
On click of this button I have to add another rhombus/rect node below it. The below image is just a UX visualization. The example could be a simple treelayout in D3.
Example D3 Treelayout:
var jsonData = getData();
var margin = {
top: 20,
right: 120,
bottom: 20,
left: 120
},
width = 960 - margin.right - margin.left,
height = 500 - margin.top - margin.bottom;
var i = 0,
duration = 750,
root;
var tree = d3.layout.tree()
.size([height, width]);
var diagonal = d3.svg.diagonal()
.projection(function(d) {
return [d.y, d.x];
});
var svg = d3.select("body").append("svg")
.attr("width", width + margin.right + margin.left)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
root = jsonData;
root.x0 = height / 2;
root.y0 = 0;
update(root);
//svg.style("height", "500px");
function update(source) {
// Compute the new tree layout.
var nodes = tree.nodes(root).reverse(),
links = tree.links(nodes);
// Normalize for fixed-depth.
nodes.forEach(function(d) {
d.y = d.depth * 180;
});
// Update the nodes…
var node = svg.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("r", 1e-6)
.attr('stroke', function(d) {
return d.color ? d.color : 'blue';
})
.style("fill", function(d) {
return d._children ? "#ccc" : "#fff";
});
nodeEnter.append("text")
.attr("x", function(d) {
return d.children || d._children ? -13 : 13;
})
.attr("dy", ".35em")
.attr("text-anchor", function(d) {
return d.children || d._children ? "end" : "start";
})
.text(function(d) {
return d.name;
})
.style("fill-opacity", 1e-6);
// Transition nodes to their new position.
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + d.y + "," + d.x + ")";
});
nodeUpdate.select("circle")
.attr("r", 10)
.style("fill", function(d) {
var collapseColor = d.color ? d.color : '#ccc';
return d._children ? collapseColor : "#fff";
});
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", 1e-6);
nodeExit.select("text")
.style("fill-opacity", 1e-6);
// Update the links…
var link = svg.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();
// Stash the old positions for transition.
nodes.forEach(function(d) {
d.x0 = d.x;
d.y0 = d.y;
});
}
// Toggle children on click.
function click(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
update(d);
}
function getData() {
return {
"name": "js",
"parent": "null",
"color": "green",
"children": [{
"name": "frameworks",
"parent": "js",
"color": "red",
"children": [{
"name": "Angular",
"parent": "frameworks",
"color": "red",
"size": 75
},
{
"name": "Backbone",
"parent": "frameworks",
"color": "red",
"size": 15
},
{
"name": "Ember",
"parent": "frameworks",
"color": "red",
"size": 5
},
{
"name": "Aurelia",
"parent": "frameworks",
"color": "red",
"size": 5
}
]
},
{
"name": "libraries",
"parent": "js",
"color": "blue",
"children": [{
"name": "jQuery",
"parent": "libraries",
"size": 70
},
{
"name": "YUI",
"parent": "libraries",
"size": 30
},
{
"name": "Dojo",
"parent": "libraries",
"size": 10
},
{
"name": "Prototype",
"parent": "libraries",
"size": 15
},
{
"name": "MooTools",
"parent": "libraries",
"size": 5
},
{
"name": "ExtJS",
"parent": "libraries",
"size": 5
}
]
}
]
};
};
.node {
cursor: pointer;
}
.node circle {
fill: #fff;
stroke-width: 3px;
}
.node text {
font: 12px sans-serif;
}
.link {
fill: none;
stroke: #ccc;
stroke-width: 2px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<div class="tree-diagram"></div>
When a user clicks ( or hover ? ) on any of the links a '+' button should be displayed. I came up with the SVG path to draw a + .
<svg height="400" width="450">
<path d= "M0 6 H12 M6 0 V12" stroke="blue" stroke-width="1.5" />
</svg>
But I am stuck on what needs to be done for converting this into a button and appending it to the link and making it clickable.
I added the button to your example, without making it do anything yet. Just, as soon as you hover on a link, the button is shown in the middle of that link, and when you click it, it logs to the console.
Now, there are probably a few things you'll want in the future:
Increase the hitbox of the link to make it easier to use;
Know which link was clicked, either by writing to a global variable clickedLink, or by re-assigning on('click', ...) inside the on('mouseenter') of the link;
Change the data object by adding a new node and calling update either on the entire data set (easy, but computationally expensive) or on the source node and its children.
var jsonData = getData();
var margin = {
top: 20,
right: 120,
bottom: 20,
left: 120
},
width = 960 - margin.right - margin.left,
height = 500 - margin.top - margin.bottom;
var i = 0,
duration = 750,
root;
var tree = d3.layout.tree()
.size([height, width]);
var diagonal = d3.svg.diagonal()
.projection(function(d) {
return [d.y, d.x];
});
var svg = d3.select("body").append("svg")
.attr("width", width + margin.right + margin.left)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// New part
var plusButton = svg
.append('g')
.classed('button', true)
.classed('hide', true)
.on('click', function() {
console.log("CLICKED");
});
plusButton
.append('rect')
.attr('transform', 'translate(-8, -8)') // center the button inside the `g`
.attr('width', 16)
.attr('height', 16)
.attr('rx', 2);
plusButton
.append('path')
.attr('d', 'M-6 0 H6 M0 -6 V6');
root = jsonData;
root.x0 = height / 2;
root.y0 = 0;
update(root);
//svg.style("height", "500px");
function update(source) {
// Compute the new tree layout.
var nodes = tree.nodes(root).reverse(),
links = tree.links(nodes);
// Normalize for fixed-depth.
nodes.forEach(function(d) {
d.y = d.depth * 180;
});
// Update the nodes…
var node = svg.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("r", 1e-6)
.attr('stroke', function(d) {
return d.color ? d.color : 'blue';
})
.style("fill", function(d) {
return d._children ? "#ccc" : "#fff";
});
nodeEnter.append("text")
.attr("x", function(d) {
return d.children || d._children ? -13 : 13;
})
.attr("dy", ".35em")
.attr("text-anchor", function(d) {
return d.children || d._children ? "end" : "start";
})
.text(function(d) {
return d.name;
})
.style("fill-opacity", 1e-6);
// Transition nodes to their new position.
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + d.y + "," + d.x + ")";
});
nodeUpdate.select("circle")
.attr("r", 10)
.style("fill", function(d) {
var collapseColor = d.color ? d.color : '#ccc';
return d._children ? collapseColor : "#fff";
});
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", 1e-6);
nodeExit.select("text")
.style("fill-opacity", 1e-6);
// Update the links…
var link = svg.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
});
})
// New part
.on('mouseenter', function(d, i) {
// Use the native SVG interface to get the bounding box to
// calculate the center of the path
var bbox = this.getBBox();
var x = bbox.x + bbox.width/2,
y = bbox.y + bbox.height/2;
plusButton
.attr('transform', 'translate(' + x + ', ' + y + ')')
.classed('hide', false);
})
.on('mouseleave', function(d, i) {
plusButton
.classed('hide', true);
});
// 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();
// Stash the old positions for transition.
nodes.forEach(function(d) {
d.x0 = d.x;
d.y0 = d.y;
});
}
// Toggle children on click.
function click(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
update(d);
}
function getData() {
return {
"name": "js",
"parent": "null",
"color": "green",
"children": [{
"name": "frameworks",
"parent": "js",
"color": "red",
"children": [{
"name": "Angular",
"parent": "frameworks",
"color": "red",
"size": 75
},
{
"name": "Backbone",
"parent": "frameworks",
"color": "red",
"size": 15
},
{
"name": "Ember",
"parent": "frameworks",
"color": "red",
"size": 5
},
{
"name": "Aurelia",
"parent": "frameworks",
"color": "red",
"size": 5
}
]
},
{
"name": "libraries",
"parent": "js",
"color": "blue",
"children": [{
"name": "jQuery",
"parent": "libraries",
"size": 70
},
{
"name": "YUI",
"parent": "libraries",
"size": 30
},
{
"name": "Dojo",
"parent": "libraries",
"size": 10
},
{
"name": "Prototype",
"parent": "libraries",
"size": 15
},
{
"name": "MooTools",
"parent": "libraries",
"size": 5
},
{
"name": "ExtJS",
"parent": "libraries",
"size": 5
}
]
}
]
};
};
.node {
cursor: pointer;
}
.node circle {
fill: #fff;
stroke-width: 3px;
}
.node text {
font: 12px sans-serif;
}
.link {
fill: none;
stroke: #ccc;
stroke-width: 2px;
}
/* New part */
.button > path {
stroke: blue;
stroke-width: 1.5;
}
.button > rect {
fill: #ddd;
stroke: grey;
stroke-width: 1px;
}
.hide {
opacity: 0 !important;
pointer-events: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<div class="tree-diagram"></div>
I am trying to run this demo shared by Igor from this post: Add custom html to nodes in d3 js tree. I've made some changes to reflect the new version of D3 below, but I get the above error. I can't find where the error is happening and was wondering if someone could help me figure it out :)
var treedata = {
"name": "PublisherNameLongName",
"id": "id1",
"type": "type0",
"addable": false,
"editable": false,
"removable": false,
"enableble": false,
"children": [{
"name": "Landing A",
"id": "id2",
"type": "type1",
"addable": true,
"editable": true,
"removable": true,
"enablable": true,
"enable": false,
"children": [{
"name": "Account 1",
"id": "id3",
"type": "type2",
"children": [{
"name": "tracking link 1",
"id": "id4",
"type": "type3",
"enablable": true,
"enable": true
}, {
"name": "tracking link 2",
"id": "id5",
"type": "type3",
"enablable": true,
"enable": true
}, {
"name": "tracking link 3",
"id": "id6",
"type": "type3",
"enablable": true,
"enable": false
}]
}, {
"name": "Account 2",
"id": "id7",
"type": "type2"
}, {
"name": "Account 3",
"id": "id9",
"type": "type2"
}]
}, {
"name": "Landing B",
"id": "id8",
"type": "type1",
"children": [{
"name": "Account 4",
"id": "id10",
"type": "type2"
}, {
"name": "Account 5",
"id": "id11",
"type": "type2"
}, {
"name": "Account 6",
"id": "id12",
"type": "type2"
}]
}, {
"name": "Landing C",
"id": "id13",
"type": "type1",
"children": [{
"name": "Subtopic 7",
"id": "id14",
"type": "type3"
}, {
"name": "Subtopic 8",
"id": "id15",
"type": "type3"
}, {
"name": "Subtopic 9",
"id": "id16",
"type": "type3"
}]
}]
}
var m = [20, 20, 20, 20],
w = 800 - m[1] - m[3],
h = 600 - m[0] - m[2],
i = 0,
r = 800,
x = d3.scaleLinear().domain([0, w]).range([0, w]),
y = d3.scaleLinear().domain([0, h]).range([0, h]),
root;
var vis = d3.select("#network")
.append("div").classed("svg-container", true)
.append("svg:svg")
.attr("preserveAspectRatio", "xMinYMin meet")
.attr("viewBox", "0 0 600 600")
.classed("svg-content-responsive", true)
.append("svg:g")
//.attr("pointer-events", "all")
.attr("transform", "translate(" + m[3] + "," + m[0] + ")")
//.call(d3.behavior.zoom().scaleExtent([1,8]).on("zoom",zoom));
//.call(d3.behavior.zoom().x(x).y(y).scaleExtent([1, 8]).on("zoom", zoom))
;
vis.append("rect")
.attr("class", "svg-content-responsive")
.attr("width", w + m[1] + m[3])
.attr("height", h + m[0] + m[2])
.attr("opacity", 0)
// draws edge
var diagonal = d3.linkVertical()
.x(function(d) {
return d.x;
})
.y(function(d) {
return d.y;
});
root = treedata;
root.x0 = h / 2;
root.y0 = 0;
// open or collaspe children of selected node
function toggleAll(d) {
if (d.children) {
d.children.forEach(toggleAll);
toggle(d);
}
};
console.log(root)
// initialize the display to show a few nodes.
root.children.forEach(toggleAll);
//toggle(root.children[1]);
//toggle(root.children[9]);
update(root);
function update(source) {
// how long animations last
var duration = d3.event && d3.event.altKey ? 5000 : 500;
// Compute the new tree layout.
var nodes = hierarchy(root).children
// Normalize for fixed-depth.
nodes.forEach(function(d) {
d.y = d.depth * 180;
});
// Update the nodes...
var node = vis.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("svg:g")
.attr("class", "node")
.attr("id", function(d) {
return "node-" + d.id;
})
.attr("transform", function(d) {
return "translate(" + source.y0 + "," + source.x0 + ")";
})
.on("click", function(d) {
toggle(d);
update(d);
// if (d['info']) {
// playvid(d['info']);
// }
});
nodeEnter.append("svg:circle")
.attr("r", 1e-6)
.style("fill", function(d) {
return d._children ? "lightsteelblue" : "#fff";
});
var nodeText = nodeEnter
.append("svg:foreignObject")
//.attr("y", function(d) { return d.children || d._children ? -10 : 10; })
//.attr("dx", ".35em")
//.attr("x", function(d) {
// return d.children || d._children ? -10 : 10;
//})
.attr("y", -30)
.attr("x", -5)
.attr("text-anchor", function(d) {
return d.children || d._children ? "end" : "start";
})
.style("fill-opacity", 1e-6)
.attr('width', 300)
.attr('height', 100)
.append('xhtml:div')
.attr("class", function(d) {
return "node-label" + " node-" + d.type
})
.classed("disabled", function(d) {
return d.enable !== undefined && !d.enable;
});
//Enable node button
nodeText.filter(function(d) {
return d.enablable;
})
.append("input", ".")
.attr("type", "checkbox")
.property("checked", function(d) {
return d.enable;
})
.on("change", toggleEnable, true)
.on("click", stopPropogation, true);
//Node label
nodeText.append("span")
.attr("class", "node-text")
.text(function(d) {
return d.name;
});
//Edit node button
nodeText.filter(function(d) {
return d.editable;
})
.append("a")
.attr("class", "node-edit")
.on("click", onEditNode, true)
.append("i")
.attr("class", "fa fa-pencil");
//Add node button
nodeText.filter(function(d) {
return d.addable;
})
.append("a")
.attr("class", "node-add")
.on("click", onAddNode, true)
.append("i")
.attr("class", "fa fa-plus");
//Remove node button
nodeText.filter(function(d) {
return d.removable;
})
.append("a")
.attr("class", "node-remove")
.on("click", onRemoveNode, true)
.append("i")
.attr("class", "fa fa-times");
// Transition nodes to their new position.
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + d.y + "," + d.x + ")";
});
nodeUpdate.select("circle")
.attr("r", 4.5)
.style("fill", function(d) {
return d._children ? "lightsteelblue" : "#fff";
});
nodeUpdate.select("text")
.style("fill-opacity", 1);
// Transition exiting ndoes 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", 1e-6);
nodeExit.select("text")
.style("fill-opacity", 1e-6);
// Update the links...
var link = vis.selectAll("path.link")
.data(hierarchy(root).links(), function(d) {
return d.target.id;
});
// Enter any new links at hte parent's previous position
link.enter().insert("svg:path", "g")
.attr("class", "link")
.attr("d", function(d) {
var o = {
x: source.x0,
y: source.y0
};
return diagonal({
source: o,
target: o
});
})
.transition()
.duration(duration)
.attr("d", diagonal);
// 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();
// Stash the old positions for transition.
nodes.forEach(function(d) {
d.x0 = d.x;
d.y0 = d.y;
});
}
// Toggle children
function toggle(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
}
// zoom in / out
function zoom(d) {
//vis.attr("transform", "transl3ate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
var nodes = vis.selectAll("g.node");
nodes.attr("transform", transform);
// Update the links...
var link = vis.selectAll("path.link");
link.attr("d", translate);
// Enter any new links at hte parent's previous position
//link.attr("d", function(d) {
// var o = {x: d.x0, y: d.y0};
// return diagonal({source: o, target: o});
// });
}
function transform(d) {
return "translate(" + x(d.y) + "," + y(d.x) + ")";
}
function translate(d) {
var sourceX = x(d.target.parent.y);
var sourceY = y(d.target.parent.x);
var targetX = x(d.target.y);
var targetY = (sourceX + targetX) / 2;
var linkTargetY = y(d.target.x0);
var result = "M" + sourceX + "," + sourceY + " C" + targetX + "," + sourceY + " " + targetY + "," + y(d.target.x0) + " " + targetX + "," + linkTargetY + "";
//console.log(result);
return result;
}
function onEditNode(d) {
var length = 9;
var id = Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, length);
addChildNode(d.id, {
"name": "new child node",
"id": id,
"type": "type2"
});
stopPropogation();
}
function onAddNode(d) {
var length = 9;
var id = Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, length);
addChildNode(d.id, {
"name": "new child node",
"id": id,
"type": "type2"
});
stopPropogation();
}
function onRemoveNode(d) {
var index = d.parent.children.indexOf(d);
if (index > -1) {
d.parent.children.splice(index, 1);
}
update(d.parent);
stopPropogation();
}
function addChildNode(parentId, newNode) {
var node = d3.select('#' + 'node-' + parentId);
var nodeData = node.datum();
if (nodeData.children === undefined && nodeData._children === undefined) {
nodeData.children = [newNode];
} else if (nodeData._children != null) {
nodeData._children.push(newNode);
toggle(nodeData);
} else if (nodeData.children != null) {
nodeData.children.push(newNode);
}
update(node);
stopPropogation();
}
function toggleEnable(d) {
d.enable = !d.enable;
var node = d3.select('#' + 'node-' + d.id + " .node-label")
.classed("disabled", !d.enable);
stopPropogation();
}
function stopPropogation() {
d3.event.stopPropagation();
}
body {
background-color: #fff;
}
.node circle {
cursor: pointer;
fill: #fff;
stroke: steelblue;
stroke-width: 1.5px;
}
.node-label {
font-size: 12px;
padding: 3px 5px;
display: inline-block;
word-wrap: break-word;
max-width: 160px;
background: #d0dee7;
border-radius: 5px;
}
.node a:hover {
cursor: pointer;
}
.node a {
font-size: 10px;
margin-left: 5px
}
a.node-remove {
color: red;
}
input+.node-text {
margin-left: 5px;
}
.node-label.node-type1 {
background: coral;
}
.node-label.node-type2 {
background: lightblue;
}
.node-label.node-type3 {
background: yellow;
}
.node-label.disabled {
background: #e9e9e9;
color: #838383
}
.node text {
font-size: 11px;
}
path.link {
fill: none;
stroke: #ccc;
stroke-width: 1.5px;
}
<script src="https://d3js.org/d3.v5.js"></script>
<div id="tree">
</div>
The problem was the diagonal function; I replaced it as well as other outdated chunks of code with code from this demo, https://bl.ocks.org/d3noob/43a860bc0024792f8803bba8ca0d5ecd.
I have a d3.js tree with descendant nodes receiving their level 2 ancestor's node colour. This is working from level 2 to level 3, but stops working at level 4 and on.
Relevant code:
var colourScale = d3.scale.ordinal()
.domain(["MD","Professional", "Leader", "Advocate", "Clinician", "Educator", "Scholar"])
.range(["#6695c8", "#cd3838","#d48440", "#a8ba5f", "#63b7c0", "#c97eb2", "#ccc136"]);
and
nodeUpdate.select("circle")
.attr("r", 10)
.attr("fill-opacity","0.7")
.attr("stroke-opacity","1")
.style("fill", function(d) {
return d.depth === 2 ? colourScale(d.parent.name) : colourScale(d.name);
})
.style("stroke", function(d) {
return d.depth === 2 ? colourScale(d.parent.name) : colourScale(d.name);
});
and
// Enter any new links at the parent's previous position.
link.enter().insert("path", "g")
.attr("class", "link")
.attr("stroke-width", function(d) {
return 1;
})
.attr("d", function(d) {
var o = {
x: source.x0,
y: source.y0
};
return diagonal({
source: o,
target: o
});
})
.attr("opacity","0.3")
.style("stroke", function(d) {
return d.target.depth === 2 ? colourScale(d.target.parent.name) : colourScale(d.target.name);
});
How can I set ALL descendants of a parent to the same colour (nodes and links)?
Fiddle
Use a recursive function to go up the hierarchy until you find a node with the correct depth. For instance:
function findParent(datum) {
if (datum.depth < 2) {
return datum.name
} else {
return findParent(datum.parent)
}
}
Or even shorter with a ternary:
function findParent(datum) {
return datum.depth < 2 ? datum.name : findParent(datum.parent);
};
Then, you can pass it to the colour scale:
.style("fill", function(d) {
return colourScale(findParent(d));
})
Here is the updated code:
var treeData = [{
"name": "MD",
"children": [{
"name": "Professional",
"children": [{
"name": "Third A",
"children": [{
"name": "Fourth A",
"children": [{
"name": "Fifth A"
}, {
"name": "Fifth B"
}, {
"name": "Fifth C"
}, {
"name": "Fifth D"
}]
}, {
"name": "Fourth B"
}, {
"name": "Fourth C"
}, {
"name": "Fourth D"
}]
}, {
"name": "Third B"
}]
}, {
"name": "Leader",
"children": [{
"name": "Third C"
}, {
"name": "Third D"
}]
}, {
"name": "Advocate",
"children": [{
"name": "Third E"
}, {
"name": "Third F"
}]
}, {
"name": "Clinician",
"children": [{
"name": "Third G"
}, {
"name": "Third H"
}, ]
}, ]
}];
var colourScale = d3.scale.ordinal()
.domain(["MD", "Professional", "Leader", "Advocate", "Clinician"])
.range(["#6695c8", "#cd3838", "#d48440", "#a8ba5f", "#63b7c0"]);
// ************** Generate the tree diagram *****************
var margin = {
top: 20,
right: 120,
bottom: 20,
left: 120
},
width = 1200 - margin.right - margin.left,
height = 650 - margin.top - margin.bottom;
var i = 0,
duration = 750,
root;
var tree = d3.layout.tree()
.size([height, width]);
var diagonal = d3.svg.diagonal()
.projection(function(d) {
return [d.y, d.x];
});
var svg = d3.select("body").append("svg")
.attr("width", width + margin.right + margin.left)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
root = treeData[0];
root.x0 = height / 2;
root.y0 = 0;
update(root);
d3.select(self.frameElement).style("height", "500px");
// Collapse after the second level
root.children.forEach(collapse);
update(root);
// Collapse the node and all it's children
function collapse(d) {
if (d.children) {
d._children = d.children
d._children.forEach(collapse)
d.children = null
}
}
function update(source) {
// Compute the new tree layout.
var nodes = tree.nodes(root).reverse(),
links = tree.links(nodes);
// Normalize for fixed-depth.
nodes.forEach(function(d) {
d.y = d.depth * 200;
});
// Update the nodes…
var node = svg.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("r", 1e-6)
.style("fill", function(d) {
return d._children ? "#C0C0C0" : "#fff";
});
nodeEnter.append("text")
.attr("x", function(d) {
return d.children || d._children ? -13 : 13;
})
.attr("dy", ".35em")
.attr("text-anchor", function(d) {
return d.children || d._children ? "end" : "start";
})
.text(function(d) {
return d.name;
})
.style("fill-opacity", 1e-6);
// Transition nodes to their new position.
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + d.y + "," + d.x + ")";
});
nodeUpdate.select("circle")
.attr("r", 10)
.attr("fill-opacity", "0.7")
.attr("stroke-opacity", "1")
.style("fill", function(d) {
return colourScale(findParent(d));
})
.style("stroke", function(d) {
return colourScale(findParent(d));
});
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", 1e-6);
nodeExit.select("text")
.style("fill-opacity", 1e-6);
// Update the links…
var link = svg.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("stroke-width", function(d) {
return 1;
})
.attr("d", function(d) {
var o = {
x: source.x0,
y: source.y0
};
return diagonal({
source: o,
target: o
});
})
.attr("opacity", "0.3")
.style("stroke", function(d) {
return colourScale(findParentLinks(d));
});
// 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();
// Stash the old positions for transition.
nodes.forEach(function(d) {
d.x0 = d.x;
d.y0 = d.y;
});
}
function findParent(datum) {
if (datum.depth < 2) {
return datum.name
} else {
return findParent(datum.parent)
}
}
function findParentLinks(datum) {
if (datum.target.depth < 2) {
return datum.target.name
} else {
return findParent(datum.target.parent)
}
}
// Toggle children on click.
function click(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
update(d);
}
.node {
cursor: pointer;
}
.node circle {
fill: #fff;
stroke: #C0C0C0;
stroke-width: 1.5px;
}
.node text {
font: 10px sans-serif;
}
.link {
fill: none;
stroke: #C0C0C0;
stroke-width: 1.5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
I need to plot bubble chart, where each bubble is a donut chart like in below image in d3 version 3. I am able to achieve something, but don't understand how to distribute the circles horizontally, as my widget will be rectangular.
Also, how to make the donut bubble like in the image below. Any help would be appreciated. Thanks.
Code:
let colorCircles = {
'a': '#59bcf9',
'b': '#faabab',
'd': '#ffde85'
};
let tooltip = d3.select("body")
.append("div")
.attr("class", "tooltip-inner")
.style("position", "absolute")
.style("min-width", "12rem")
.style("visibility", "hidden")
.style("color", "#627386")
.style("padding", "15px")
.style("stroke", '#b8bfca')
.style("fill", "none")
.style("stroke-width", 1)
.style("background-color", "#fff")
.style("border-radius", "6px")
.style("text-align", "center")
.text("");
let bubble = d3.layout.pack()
.sort(null)
.size([width, diameter])
.padding(15)
.value(function(d) {
return d[columnForRadius];
});
let svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", diameter)
.attr("class", "bubble");
let nodes = bubble.nodes({
children: dataset
}).filter(function(d) {
return !d.children;
});
let circles = svg.selectAll("circle")
.data(nodes)
.enter()
.append("circle")
.attr("r", function(d) {
return d.r;
})
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y - 20;
})
.style("fill", function(d) {
return colorCircles[d[columnForColors]]
})
.on("mouseover", function(d) {
tooltip.style("visibility", "visible");
tooltip.html('<p>' + d[columnForColors] + ": " + d[columnForText] + "</p><div class='font-bold displayInlineBlock'> $" + d[columnForRadius] + '</div>');
})
.on("mousemove", function() {
return tooltip.style("top", (d3.event.offsetY - 10) + "px").style("left", (d3.event.offsetX + 10) + "px");
})
// .on("mouseout", function() {
// return tooltip.style("visibility", "hidden");
// })
.attr("class", "node");
circles.transition()
.duration(1000)
.attr("r", function(d) {
return d.r;
})
.each('end', function() {
display_text();
});
function display_text() {
let text = svg
.selectAll(".text")
.data(nodes, function(d) {
return d[columnForText];
});
text.enter().append("text")
.attr("class", "graphText")
.attr("x", function(d) {
return d.x;
})
.attr("y", function(d) {
return d.y - 20;
})
.attr("dy", ".2em")
.attr("fill", "white")
.attr("font-size", function(d) {
return d.r / 5;
})
.attr("text-anchor", "middle")
.text(function(d) {
console.log(d)
return d[columnForText].substring(0, d.r / 3);
});
text.enter().append("text")
.attr("class", "graphText")
.attr("x", function(d) {
return d.x;
})
.attr("y", function(d) {
return d.y - 20;
})
.attr("dy", "1.3em")
.style("text-anchor", "middle")
.text(function(d) {
return '$' + d[columnForRadius];
})
.attr("font-size", function(d) {
return d.r / 5;
})
.attr("fill", "white");
}
function hide_text() {
let text = svg.selectAll(".text").remove();
}
d3.select(self.frameElement)
.style("height", diameter + "px");
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<script type="text/javascript">
var dataset = [
{ "Name": "Olives", "Count": 4319, "Category": "d" },
{ "Name": "Tea", "Count": 4159, "Category": "d" },
{ "Name": "Boiled Potatoes", "Count": 2074, "Category": "a" },
{ "Name": "Milk", "Count": 1894, "Category": "a" },
{ "Name": "Chicken Salad", "Count": 1809, "Category": "a" },
{ "Name": "Lettuce Salad", "Count": 1566, "Category": "a" },
{ "Name": "Lobster Salad", "Count": 1511, "Category": "a" },
{ "Name": "Chocolate", "Count": 1489, "Category": "b" }
];
var width = 300, diameter = 300;
var columnForText = 'Name',
columnForColors = 'Category',
columnForRadius = "Count";
</script>
Here's my fiddle: http://jsfiddle.net/71s86zL7/
I created a compound bubble pie chart and specified the inner radius in the pie chart.
var arc = d3.svg.arc()
.innerRadius(radius)
.outerRadius(radius);
.attr("d", function(d) {
arc.innerRadius(d.r+5);
arc.outerRadius(d.r);
return arc(d);
})
please let me know if there's any alternative solution to this problem.
I have a sorta hacky solution for this. What I did was:
to use the d3.layout.pie to get the startAngles and endAngles for arcs and create the arcs on top of the circles.
Give the circles a stroke line creating an effect of a donut chart.
And then I just had to adjust the startAngles and the endAngles so that all the arcs start from the same position.
Here's the fiddle:
let colorCircles = {
'a': '#59bcf9',
'b': '#faabab',
'd': '#ffde85'
};
let tooltip = d3.select("body")
.append("div")
.attr("class", "tooltip-inner")
.style("position", "absolute")
.style("min-width", "12rem")
.style("visibility", "hidden")
.style("color", "#627386")
.style("padding", "15px")
.style("stroke", '#b8bfca')
.style("fill", "none")
.style("stroke-width", 1)
.style("background-color", "#fff")
.style("border-radius", "6px")
.style("text-align", "center")
.text("");
let bubble = d3.layout.pack()
.sort(null)
.size([width, diameter])
.padding(15)
.value(function(d) {
return d[columnForRadius];
});
var pie = d3.layout.pie()
.sort(null)
.value(function(d) {
return d.Count;
});
var arc = d3.svg.arc()
let svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", diameter)
.attr("class", "bubble");
let nodes = bubble.nodes({
children: dataset
}).filter(function(d) {
return !d.children;
});
let g = svg.append('g')
let circles = g.selectAll("circle")
.data(nodes)
.enter()
.append("circle")
.attr("r", function(d) {
return d.r;
})
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y - 20;
})
.style("fill", function(d) {
return colorCircles[d[columnForColors]]
})
.attr("class", "node")
.on("mouseover", function(d) {
tooltip.style("visibility", "visible");
tooltip.html('<p>' + d[columnForColors] + ": " + d[columnForText] + "</p><div class='font-bold displayInlineBlock'> $" + d[columnForRadius] + '</div>');
})
.on("mousemove", function() {
return tooltip.style("top", (d3.event.offsetY - 10) + "px").style("left", (d3.event.offsetX + 10) + "px");
})
.on("mouseout", function() {
return tooltip.style("visibility", "hidden");
});
arcs = g.selectAll(".arc")
.data(pie(dataset))
.enter().append("g")
.attr("class", "arc");
arcs.append("path")
.attr('transform', function(d) {
return 'translate(' + d['data']['x'] + ',' + (d['data']['y'] - 20) + ')';
})
.attr("d", function(d) {
return arc({
startAngle: 0,
endAngle: d.startAngle - d.endAngle,
innerRadius: d['data']['r'] - 2,
outerRadius: d['data']['r'] + 2,
})
}).on("mouseover", function(d) {
tooltip.style("visibility", "visible");
tooltip.html('<p>' + d['data'][columnForColors] + ": " + d['data'][columnForText] + "</p><div class='font-bold displayInlineBlock'> $" + d['data'][columnForRadius] + '</div>');
})
.on("mousemove", function() {
return tooltip.style("top", (d3.event.offsetY - 10) + "px").style("left", (d3.event.offsetX + 10) + "px");
})
.on("mouseout", function() {
return tooltip.style("visibility", "hidden");
});
circles.transition()
.duration(1000)
.attr("r", function(d) {
return d.r;
})
.each('end', function() {
display_text();
});
function display_text() {
let text = svg
.selectAll(".text")
.data(nodes, function(d) {
return d[columnForText];
});
text.enter().append("text")
.attr("class", "graphText")
.attr("x", function(d) {
return d.x;
})
.attr("y", function(d) {
return d.y - 20;
})
.attr("dy", ".2em")
.attr("fill", "white")
.attr("font-size", function(d) {
return d.r / 3;
})
.attr("text-anchor", "middle")
.text(function(d) {
return d[columnForText].substring(0, d.r / 3);
});
text.enter().append("text")
.attr("class", "graphText")
.attr("x", function(d) {
return d.x;
})
.attr("y", function(d) {
return d.y - 20;
})
.attr("dy", "1.3em")
.style("text-anchor", "middle")
.text(function(d) {
return '$' + d[columnForRadius];
})
.attr("font-size", function(d) {
return d.r / 5;
})
.attr("fill", "white");
}
function hide_text() {
let text = svg.selectAll(".text").remove();
}
d3.select(self.frameElement)
.style("height", diameter + "px");
path {
fill: orange;
stroke-width: 1px;
stroke: crimson;
}
path:hover {
fill: yellow;
}
circle {
fill: white;
stroke: slategray;
stroke-width: 4px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.3.13/d3.min.js"></script>
<script type="text/javascript">
var dataset = [{
"Name": "Olives",
"Count": 4319,
"Category": "d"
},
{
"Name": "Tea",
"Count": 4159,
"Category": "d"
},
{
"Name": "Boiled Potatoes",
"Count": 2074,
"Category": "a"
},
{
"Name": "Milk",
"Count": 1894,
"Category": "a"
},
{
"Name": "Chicken Salad",
"Count": 1809,
"Category": "a"
},
{
"Name": "Lettuce Salad",
"Count": 1566,
"Category": "a"
},
{
"Name": "Lobster Salad",
"Count": 1511,
"Category": "a"
},
{
"Name": "Chocolate",
"Count": 1489,
"Category": "b"
}
];
var width = 300,
diameter = 300;
var columnForText = 'Name',
columnForColors = 'Category',
columnForRadius = "Count";
</script>
There is example how to have a label on the node in a D3 forced graph. What I try to do is to have a label on the line instead.
Example of the label on node: http://bl.ocks.org/mbostock/2706022
This code will display the text for the line up in the left corner. It seems that it takes the x, y cordinates from the canvas and not from my line. How to fix this?
var labelLine = olinks.append("text")
.attr("x", 12)
.attr("dy", ".35em")
.text("eeeeee");
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.node {
stroke: #fff;
stroke-width: 1.5px;
}
.link {
stroke: #999;
stroke-opacity: .6;
}
</style>
<body>
<script src="./Script/d3.v3/d3.v3.min.js"></script>
<script>
var graph = {
"nodes": [
{ "name": "App1-main", "group": 1 },
{ "name": "App2", "group": 1 },
{ "name": "App3", "group": 1 },
{ "name": "App4", "group": 1 },
{ "name": "Content-1", "group": 3 },
{ "name": "Content-1", "group": 3 },
{ "name": "Content-1", "group": 3 },
{ "name": "Content-1", "group": 3 },
{ "name": "Pontmercy", "group": 3 }
],
"links": [
{ "source": 1, "target": 0, "value": 1 },
{ "source": 2, "target": 0, "value": 1 },
{ "source": 0, "target": 3, "value": 1 }
]
};
var width = 960,
height = 500;
var color = d3.scale.category20();
var force = d3.layout.force()
.charge(-300)
.linkDistance(60)
.size([width, height]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var drawGraph = function (graph) {
force
.nodes(graph.nodes)
.links(graph.links)
.start();
var olinks = svg.selectAll("g.link")
.data(graph.links)
.enter().append("g")
.call(force.drag);
var link = olinks.append("line")
.attr("class", "link")
.style("stroke-width", function (d) { return Math.sqrt(d.value); });
var labelLine = olinks.append("text")
.attr("x", 12)
.attr("dy", ".35em")
.text("eeeeee");
var gnodes = svg.selectAll('g.gnode')
.data(graph.nodes)
.enter()
.append('g')
.classed('gnode', true);
var node = gnodes.append("circle")
.attr("class", "node")
.attr("r", 5)
.style("fill", function (d) { return color(d.group); })
.call(force.drag);
var labels = gnodes.append("text")
.attr("x", 12)
.attr("dy", ".35em")
.text(function (d) { return d.name; });
console.log(labels);
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; });
gnodes.attr("transform", function (d) {
return 'translate(' + [d.x, d.y] + ')';
});
});
};
drawGraph(graph);
</script>
This is an example that has correct behavior.
The key points are here:
1) You need to define link as SVG "g" element, so that you can define both lines and labels for each link, and that coordinates are computed correctly.
2) Label text must be centered horizontally (code: .attr("text-anchor", "middle")).
3) Inside tick(), you need to compute coordinate of the labels., as arithmetic mean between source and target node.
Hope this helps.
There also was another similar question recently.