D3.js child map failure ? Any gurus spot my error? - javascript

First the code can be found here: working - almost - code
I've had a few pointers and learnt "quite a bit" along to way trying to get this to work.
Basically I'm building, or trying to build a hierachy tree for office staff with some basic functions.
Its all going pretty well except one last issue and no matter how I look at it or approach it, I cannot see why it isnt working.
Problem:
If you hold the mouse over a node, 4 little pop up menus appear - the green and the red add and remove nodes - this works.
At the top of the canvas is a "Save" button, which I'm trying to get to go through all the nodes giving their parent-to-child relationship - again this works until you add a node and then another node, it will not see the a child of a new node.
If anyone knows the way to refresh the "child map" that Im using in the snippit below, it would be much appreciated:
d3.selectAll('g.node')
.each(function(p) {
p.children.map(function(c) {
alert(c.name + "(" + c.id + ")" + "- PARENT TO -" + p.name + "(" +
p.id + ")")
});
});

I don't know if I got your problem right, maybe I'm totally wrong with my assumption but your data is seems fine to me. The example you linked will throw an error when clicking on save for those nodes without children on line 25:
p.children.map(function(c) {
alert(c.name + "(" + c.id + ")" + "- PARENT TO -" + p.name + "(" + p.id + ")")
});
Since in some cases p.children is undefined (there is none) the loop will break.
Since the application is working visually, there is no reason to think the data bindings aren't correct. Just to be sure, I wrote a slightly modified version of your "save" loop. Add some children and check the console.
Now, a little bit out of scope to your question but may save you some headaches the next time: d3 just maps your data to your elements. When creating, destroying and updating stuff, leave all the DOM manipulation to d3 and concentrate on your model.
var diameter = 1000;
var height = diameter - 150;
var n = {
"name": "A",
"id": 1,
"target": 0,
"children": [{
"name": "B",
"id": 2,
"target": 1,
"children": [{
"name": "Cr",
"id": 8,
"target": 2,
"children": [{
"name": "D",
"id": 7,
"target": 2
}, {
"name": "E",
"id": 9,
"target": 8
}, {
"name": "F",
"id": 10,
"target": 8
}]
}]
},
{
"name": "G",
"id": 3,
"target": 0
}, {
"name": "H",
"id": 4,
"target": 0
}, {
"name": "I",
"id": 5,
"target": 0
}, {
"name": "J",
"id": 6,
"target": 0
}
]
}
var tree = d3.layout.tree()
.size([260, diameter / 2 - 120])
.separation(function(a, b) {
return (a.parent == b.parent ? 1 : 2) / a.depth;
});
var diagonal = d3.svg.diagonal.radial()
.projection(function(d) {
return [d.y, d.x / 180 * Math.PI];
});
var myZoom = d3.behavior.zoom()
.scaleExtent([.5, 10])
.on("zoom", zoom);
var container = d3.select("body").append("svg")
.attr("width", diameter)
.attr("height", height)
.style('border', '3px solid black')
.call(myZoom);
//I am centering my node here
var svg = container.append("g")
.attr("transform", "translate(" + diameter / 2 + "," + height / 2 + ")");
myZoom.translate([diameter / 2, height / 2]);
var init = true;
function zoom() {
svg.attr("transform", "translate(" + (d3.event.translate[0]) + "," + (d3.event.translate[1]) + ")scale(" + d3.event.scale + ")");
}
var nodes = tree(n);
//make sure to set the parent x and y for all nodes
nodes.forEach(function(node) {
if (node.id == 1) {
node.px = node.x = 500;
node.py = node.y = 304;
} else {
node.px = node.parent.x;
node.py = node.parent.y;
}
});
// Build a array for borken tree case
var myCords = d3.range(50);
buildSingleTreeData();
var id = ++nodes.length;
function update(root) {
var node = svg.selectAll(".node");
var link = svg.selectAll(".link");
nodes = tree.nodes(root);
if (checkBrokenTree(root)) {
if (!root.children || root.children.length == 0) {
id = 2;
} else {
var returnId = resetIds(root, 1);
id = nodes.length + 1;
}
singleNodeBuild(nodes);
}
links = tree.links(nodes);
/*This is a data join on all nodes and links
if a node is added a link will also be added
they are based parsing of the root*/
node = node.data(nodes, function(d) {
return d.id;
});
link = link.data(links, function(d) {
return d.source.id + "-" + d.target.id;
});
var enterNodes = node.enter().append("g")
.attr("class", "node")
.attr("transform", function(d) {
d.tx = (d.parent ? d.parent.x : d.px) - 90;
return "rotate(" + ((d.parent ? d.parent.x : d.px) - 90) +
")translate(" + d.py + ")";
})
enterNodes.append('g')
.attr('class', 'label')
.attr('transform', function(d) {
return 'rotate(' + -d.px + ')';
})
.append('text')
.attr("dx", "-1.6em")
.attr("dy", "2.5em")
.text(function(d) {
return d.name;
})
.call(make_editable, function(d) {
return d.name;
});
var circlesGroup = enterNodes.append('g')
.attr('class', 'circles');
var mainCircles = circlesGroup.append("circle")
.attr('class', 'main')
.attr("r", 9);
circlesGroup.append("circle")
.attr('class', 'delete')
.attr('cx', 0)
.attr('cy', 0)
.attr('fill', 'red')
.attr('opacity', 0.5)
.attr("r", 0);
circlesGroup.append("circle")
.attr('class', 'add')
.attr('cx', 0)
.attr('cy', 0)
.attr('fill', 'green')
.attr('opacity', 0.5)
.attr("r", 0);
circlesGroup.append("circle")
.attr('class', 'admin')
.attr('cx', 0)
.attr('cy', 0)
.attr('fill', 'blue')
.attr('opacity', 0.5)
.attr("r", 0);
circlesGroup.append("circle")
.attr('class', 'userid')
.attr('cx', 0)
.attr('cy', 0)
.attr('fill', 'yellow')
.attr('opacity', 0.5)
.attr("r", 0);
circlesGroup.on("mouseenter", function() {
var elem = this.__data__;
elem1 = d3.selectAll(".delete").filter(function(d, i) {
return elem.id == d.id ? this : null;
});
elem2 = d3.selectAll(".add").filter(function(d, i) {
return elem.id == d.id ? this : null;
});
elem3 = d3.selectAll(".admin").filter(function(d, i) {
return elem.id == d.id ? this : null;
});
elem4 = d3.selectAll(".userid").filter(function(d, i) {
return elem.id == d.id ? this : null;
});
elem2.transition()
.duration(duration)
.attr('cx', -20)
.attr('cy', 0)
.attr("r", 8);
elem1.transition()
.duration(duration)
.attr('cx', 20)
.attr('cy', 0)
.attr("r", 8);
elem3.transition()
.duration(duration)
.attr('cx', -10)
.attr('cy', -20)
.attr("r", 8);
elem4.transition()
.duration(duration)
.attr('cx', 10)
.attr('cy', -20)
.attr("r", 8);
});
circlesGroup.on("mouseleave", function() {
var elem = this.__data__;
elem1 = d3.selectAll(".delete").filter(function(d, i) {
return elem.id == d.id ? this : null;
});
elem2 = d3.selectAll(".add").filter(function(d, i) {
return elem.id == d.id ? this : null;
});
elem3 = d3.selectAll(".admin").filter(function(d, i) {
return elem.id == d.id ? this : null;
});
elem2.transition()
.duration(duration)
.attr('cy', 0)
.attr('cx', 0)
.attr("r", 0);
elem1.transition()
.duration(duration)
.attr('cy', 0)
.attr('cx', 0)
.attr("r", 0);
elem3.transition()
.duration(duration)
.attr('cy', 0)
.attr('cx', 0)
.attr("r", 0);
elem4.transition()
.duration(duration)
.attr('cy', 0)
.attr('cx', 0)
.attr("r", 0);
});
var linkEnter = link.enter()
.insert("path", '.node')
.attr("class", "link")
.attr("d", function(d) {
var o = {
x: d.source.px,
y: d.source.py
};
return diagonal({
source: o,
target: o
});
});
// UserID node event handeler
node.select('.userid').on('click', function() {
var p = this.__data__;
alert(p.name + " Userid (" + p.username + ")" + "-->" + p.id + "<--" + p.children);
});
// Admin node event handeler
node.select('.admin').on('click', function() {
var p = this.__data__;
alert(p.name + " password is (" + p.pword + ")");
});
// Delete node event handeler
node.select('.delete').on('click', function() {
var p = this.__data__;
if (p.id != 1) {
removeNode(p);
var childArr = p.parent.children;
childArr = childArr.splice(childArr.indexOf(p), 1);
update(n);
}
function removeNode(p) {
if (!p.children) {
if (p.id) {
p.id = null;
}
return p;
} else {
for (var i = 0; i < p.children.length; i++) {
p.children[i].id == null;
removeNode(p.children[i]);
}
p.children = null;
return p;
}
}
node.exit().remove();
link.exit().remove();
// alertify.alert(p.name + " has left the building..");
});
// The add node even handeler
node.select('.add').on('click', function() {
var p = this.__data__;
var aId = id++;
var d = {
name: 'name' + aId
};
d.id = aId;
if (p.children) {
p.children.push(d);
//top node add
} else {
p.children = [d];
//child of child
}
d.px = p.x;
d.py = p.x;
d3.event.preventDefault();
update(n)
node.exit().remove();
link.exit().remove();
});
/* this is the update section of the graph and nodes will be updated to their current positions*/
var duration = 700;
node.transition()
.duration(duration)
.attr("transform", function(d) {
d.utx = (d.x - 90);
return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")";
})
link.transition()
.duration(duration).attr("d", diagonal);
node.select('g')
.transition()
.duration(duration)
.attr('transform', function(d) {
return 'rotate(' + -d.utx + ')';
});
node.select('.circles').attr('transform', function(d) {
return 'rotate(' + -d.utx + ')';
});
node.exit().remove();
link.exit().remove();
}
update(n);
/** make a manual tree for when it is just
a linked list. For some reason the algorithm will break down
all nodes in tree only have one child.
*/
function buildSingleTreeData() {
myCords = d3.range(50);
var offset = 130;
myCords = myCords.map(function(n, i) {
return {
x: 90,
y: offset * i
};
});
}
/**
This function will build single node tree where every node
has 1 child. From testing this layout does not support
a layout for nodes tree less than size 3 so they must
be manually drawn. Also if evey node has one child then
the tree will also break down and as a result this fucntion
is there to manually build a singe tree up to 50 nodes
*/
function resetIds(aNode, aId) {
if (aNode.children) {
for (var i = 0; i < aNode.children.length; i++) {
aNode.children[i].id = ++aId;
resetIds(aNode.children[i], aId);
}
return aId;
}
}
/*
builds a liner tree D3 does not support this
and so it must be hard coded
*/
function singleNodeBuild(nodes) {
nodes.forEach(function(elem) {
var i = elem.id - 1;
elem.x = myCords[i].x;
elem.y = myCords[i].y;
});
}
/* D3 does not support operations
on where root nodes does not have atlest
2 children. this case need to be check for
and hard coded
*/
function checkBrokenTree(rootNode) {
var size = nodes.length;
var val = 0;
function recur(nod, i) {
if (nod.children) {
return recur(nod.children[0], i + 1);
} else {
return i + 1;
}
}
return recur(rootNode, val) == nodes.length;
}
/*
Credit https://gist.github.com/GerHobbelt/2653660
This funciton make a text node editable
*/
function make_editable(d, field) {
this
.on("mouseover", function() {
d3.select(this).style("fill", "red");
})
.on("mouseout", function() {
d3.select(this).style("fill", null);
})
.on("click", function(d) {
var p = this.parentNode;
//console.log(this, arguments);
// inject a HTML form to edit the content here...
// bug in the getBBox logic here, but don't know what I've done wrong here;
// anyhow, the coordinates are completely off & wrong. :-((
var xy = this.getBBox();
var p_xy = p.getBBox();
xy.x -= p_xy.x;
xy.y -= p_xy.y;
var el = d3.select(this);
var p_el = d3.select(p);
var frm = p_el.append("foreignObject");
var inp = frm
.attr("x", xy.x - 40)
.attr("y", xy.y + 40)
.attr("dx", "2em")
.attr("dy", "-3em")
.attr("width", 100)
.attr("height", 25)
.append("xhtml:form")
.append("input")
.attr("value", function() {
// nasty spot to place this call, but here we are sure that the <input> tag is available
// and is handily pointed at by 'this':
this.focus();
//console.log( d);
return d.name;
})
.attr({
maxlength: 16
})
.style({
width: "100px"
})
// make the form go away when you jump out (form looses focus) or hit ENTER:
.on("blur", function() {
//console.log("blur", this, arguments);
var txt = inp.node().value;
d.name = txt;
if (txt) {
el
.text(function(d) {
return d.name;
});
}
// Note to self: frm.remove() will remove the entire <g> group! Remember the D3 selection logic!
p_el.select("foreignObject").remove();
})
.on("keypress", function() {
// console.log("keypress", this, arguments);
// IE fix
if (!d3.event)
d3.event = window.event;
var e = d3.event;
if (e.keyCode == 13) {
if (typeof(e.cancelBubble) !== 'undefined') // IE
e.cancelBubble = true;
if (e.stopPropagation)
e.stopPropagation();
e.preventDefault();
var txt = inp.node().value;
if (txt) {
d.name = txt;
el
.text(function(d) {
return d.name;
});
}
// odd. Should work in Safari, but the debugger crashes on this instead.
// Anyway, it SHOULD be here and it doesn't hurt otherwise.
p_el.select("foreignObject").remove();
}
});
});
}
.node .main {
fill: #fff;
stroke: steelblue;
stroke-width: 1.5px;
}
h1 {
text-align: center;
}
.node .delete {
stroke-width: 1.5px;
}
.node {
font: 10px sans-serif;
}
.link {
fill: none;
stroke: #ccc;
stroke-width: 1.5px;
}
svg {
margin-left: auto;
margin-right: auto;
display: block;
}
text {
font: 10px "Helvetica Neue", Helvetica, Arial, sans-serif;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<html lang="en">
<head>
<meta charset="utf-8">
<title> Staff</title>
</head>
<h1> STAFF </h1>
<input type="button" id="button" value="Save" />
<body style="text-align:center">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
<script>
var onClick = function() {
d3.selectAll('g.node')
.each(function(p) {
if (p.children) {
console.log(`node ${p.name} has ${p.children.length} children: `, p.children.map(child => child.name));
[...p.children].forEach((child) => {
console.log(`${child.name} is child of ${child.parent.name}`);
});
} else {
console.log(`node ${p.name} has no children`);
}
console.log('----------------')
});
};
$('#button').click(onClick);
</script>
</body>
</head>

Related

Child elements not appearing in webpage

I'm learning how to use D3. After fixing up the code from this example (https://jsfiddle.net/tgsen1bc/
) to run the newest version of D3, I managed to get the links to show but not the nodes or their labels.
EDIT: I got the nodes to appear :) My problem now is that some child elements of the <g>'s do not appear so the labels for the nodes don't appear. For example, in Chrome Dev Tools, the child elements of <foreignObject> (a child of <g>) do not appear on the window. I tried making <foreignObject> extremely large but the child elements still do not appear. I also tried replacing the <foreignObject> with a <div>, but the <div> ended up disappearing as well. I checked the parent css properties and it doesn't seem like anything should be blocking the child elements from appearing. The only child element that appears are the circle <svg> and <foreignObject> The code section for the label is:
// Add labels for the nodes
nodeEnter.append('foreignObject')
.attr("y", -30)
.attr("x", -5)
.attr("text-anchor", function (d) {
return d.children || d._children ? "end" : "start";
})
.attr('width', 100)
.attr('height', 50)
.append('div') // doesn't show up on webpage
.attr("class", function (d) {
return "node-label" + " node-" + d.data.type
})
.classed("disabled", function (d) {
return d.enable !== undefined && !d.enable;
})
.append("span") // doesn't show up on webpage
.attr("class", "node-text")
.text(function (d) { // correct label in chrome dev tools
return d.data.name; // but does not show up on webpage
});
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": null
}]
}
// Set the dimensions and margins of the diagram
var margin = { top: 20, right: 20, bottom: 20, left: 20 },
width = 800 - margin.left - margin.right,
height = 600 - margin.top - margin.bottom,
i = 0,
x = d3.scaleLinear().domain([0, width]).range([0, width]),
y = d3.scaleLinear().domain([0, height]).range([0, height]),
root;
// append the svg object to the body of the page
// appends a 'group' element to 'svg'
// moves the 'group' element to the top left margin
var vis = d3.select("#root")
.append("svg")
.attr("width", width + margin.right + margin.left)
.attr("height", height + margin.top + margin.bottom)
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
;
vis.append("rect")
.attr("class", "overlay")
.attr("width", width + margin.right + margin.left)
.attr("height", height + margin.top + margin.bottom)
.attr("opacity", 0)
var tree = d3.tree().size([height, width]);
// Draws curved diagonal path from parent to child nodes
function diagonal(s, d) {
return `M ${s.y} ${s.x}
C ${(s.y + d.y) / 2} ${s.x},
${(s.y + d.y) / 2} ${d.x},
${d.y} ${d.x}`
}
root = d3.hierarchy(treedata, function (d) {
return d.children;
});
root.x0 = height / 2;
root.y0 = 0;
// open or collaspe children of selected node
function toggleAll(d) {
if (d.children) {
d.children.forEach(toggleAll);
toggle(d);
}
};
// Initialize the display to show a few nodes
// root.children.forEach(toggleAll);
update(root);
function update(source) {
// how long animations last
var duration = d3.event && d3.event.altKey ? 5000 : 500;
// Compute the new tree layout.
var treeObj = tree(root)
var nodes = treeObj.descendants(),
links = treeObj.descendants().slice(1);
// Normalize for fixed-depth.
nodes.forEach(function (d) {
d.y = d.depth * 180;
});
/********************* NODES SECTION *********************/
// 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("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);
});
// Add Circle for the nodes
nodeEnter.append("circle")
.attr("class", "circle-for-nodes")
.attr("r", 1e-6)
.style("fill", function (d) {
return d._children ? "lightsteelblue" : "#fff";
})
// Add labels for the nodes
nodeEnter.append('foreignObject')
.attr("y", -30)
.attr("x", -5)
.attr("text-anchor", function (d) {
return d.children || d._children ? "end" : "start";
})
.attr('width', 100)
.attr('height', 50)
.append('div')
.attr("class", function (d) {
return "node-label" + " node-" + d.data.type
})
.classed("disabled", function (d) {
return d.enable !== undefined && !d.enable;
})
.append("span")
.attr("class", "node-text")
.text(function (d) {
return d.data.name;
});
// Enable node button if enablable
nodeEnter.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);
// Edit node button if editable
nodeEnter.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 if addable
nodeEnter.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 if removable
nodeEnter.filter(function (d) {
return d.removable;
})
.append("a")
.attr("class", "node-remove")
.on("click", onRemoveNode, true)
.append("i")
.attr("class", "fa fa-times");
// UPDATE - merges all transitions together?
// var nodeUpdate = node;
var nodeUpdate = nodeEnter.merge(node);
// Transition nodes to their new position.
nodeUpdate.transition()
.duration(duration)
.attr("transform", function (d) {
return "translate(" + d.y + "," + d.x + ")";
});
// Display node
nodeUpdate.select("circle.circle-for-nodes")
.attr("r", 4.5)
.style("fill", function (d) {
return d._children ? "lightsteelblue" : "#fff";
})
// Display text
nodeUpdate.select(".node-text")
.style("fill-opacity", function (d) {
console.log(d)
return 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();
// On exit reduce the node circles size to 0
nodeExit.select("circle")
.attr("r", 1e-6);
// On exit reduce the opacity of text labels
nodeExit.select("text")
.style("fill-opacity", 1e-6);
/********************* LINKS SECTION *********************/
// Update the links...
var link = vis.selectAll("path.link")
.data(links, function (d) { return d.id; });
// Enter any new links at the parent's previous position
var linkEnter = link.enter().insert("path", "g")
.attr("class", "link")
.attr("d", function (d) {
var o = { x: source.x0, y: source.y0 };
return diagonal(o, o);
})
// UPDATE - merges all transitions together?
var linkUpdate = linkEnter.merge(link);
// Transition back to the parent element position.
linkUpdate.transition()
.duration(duration)
.attr("d", function (d) {
return diagonal(d, d.parent)
});
// Remove exiting links
var linkExit = link.exit().transition()
.duration(duration)
.attr("d", function (d) {
var o = { x: source.x, y: source.y };
return diagonal(o, o);
})
.remove();
// Stash the old positions for transition.
nodes.forEach(function (d) {
d.x0 = d.x;
d.y0 = d.y;
});
// End of function update()
}
// 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 + "";
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 {
height: 100vh;
width: 100vw;
margin: 0;
padding: 0;
}
.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;
}
<!DOCTYPE html>
<meta charset="utf-8" />
<script src="https://d3js.org/d3.v5.js"></script>
<body>
<div id="root"></div>
</body>
Found my answer here: HTML element inside SVG not displayed
To summarize what Christopher said, <div></div> is not recognized as a xhtml paragraph because the namespace xhtml is not included in the foreignObject context (a foreignObject might contain anything (xml formated data for example). To specify the input as html, I needed to .append("xhtml:div")

Javascript D3 not showing visualization

I am trying to replicate the visualization I see on this github profile
https://github.com/mojoaxel/d3-sunburst/tree/master/examples
I copied the following code and changed the path to the respective files.
suburst.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Sequences sunburst</title>
<link rel="stylesheet" type="text/css" href="sunburst.css"/>
<link rel="stylesheet" type="text/css" href="examples.css"/>
<script src="http://d3js.org/d3.v3.min.js" type="text/javascript"></script>
<script src="sunburst.js" type="text/javascript"></script>
</head>
<body>
<div id="main">
<div id="sunburst-breadcrumbs"></div>
<div id="sunburst-chart">
<div id="sunburst-description"></div>
</div>
</div>
<div id="sidebar">
<input type="checkbox" id="togglelegend"> Legend<br/>
<div id="sunburst-legend" style="visibility: hidden;"></div>
</div>
<script type="text/javascript">
(function() {
var sunburst = new Sunburst({
colors: {
"home": "#5687d1",
"product": "#7b615c",
"search": "#de783b",
"account": "#6ab975",
"other": "#a173d1",
"end": "#bbbbbb"
}
});
sunburst.loadCsv("/Users/Documents/data/visit-sequences.csv");
d3.select("#togglelegend").on("click", function() {
var legend = d3.select('#sunburst-legend');
if (legend.style("visibility") == "hidden") {
legend.style("visibility", "");
} else {
legend.style("visibility", "hidden");
}
});
})();
</script>
</body>
</html>
sunburst.js
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define(['d3'], factory);
} else {
root.Sunburst = factory(root.d3);
}
}(this, function (d3) {
var defaultOptions = {
// DOM Selectors
selectors: {
breadcrumbs: '#sunburst-breadcrumbs',
chart: '#sunburst-chart',
description: '#sunburst-description',
legend: '#sunburst-legend'
},
// Dimensions of sunburst.
width: 750,
height: 600,
// Mapping of step names to colors.
colors: {},
// If a color-name is missing this color-scale is used
colorScale: d3.scale.category20(),
colorScaleLength: 20,
// Breadcrumb dimensions: width, height, spacing, width of tip/tail.
breadcrumbs: {
w: 75,
h: 30,
s: 3,
t: 10
},
// parser settings
separator: '-'
};
/**
* This hashing function returns a number between 0 and 4294967295 (inclusive) from the given string.
* #see https://github.com/darkskyapp/string-hash
* #param {String} str
*/
function hash(str) {
var hash = 5381;
var i = str.length;
while(i) {
hash = (hash * 33) ^ str.charCodeAt(--i);
}
return hash >>> 0;
}
var Sunburst = function(options, data) {
this.opt = Object.assign({}, defaultOptions, options);
// Total size of all segments; we set this later, after loading the data.
this.totalSize = 0;
if (data) {
this.setData(data);
}
}
Sunburst.prototype.getColorByName = function(name) {
return this.opt.colors[name] || this.opt.colorScale(hash(name) % this.opt.colorScaleLength);
}
Sunburst.prototype.setData = function(data) {
var json = this.buildHierarchy(data);
this.createVisualization(json);
}
Sunburst.prototype.loadCsv = function(csvFile) {
// Use d3.text and d3.csv.parseRows so that we do not need to have a header
// row, and can receive the csv as an array of arrays.
d3.text(csvFile, function(text) {
var array = d3.csv.parseRows(text);
var json = this.buildHierarchy(array);
this.createVisualization(json);
}.bind(this));
}
// Main function to draw and set up the visualization, once we have the data.
Sunburst.prototype.createVisualization = function(json) {
var that = this;
var radius = Math.min(this.opt.width, this.opt.height) / 2
this.vis = d3.select(this.opt.selectors.chart).append("svg:svg")
.attr("width", this.opt.width)
.attr("height", this.opt.height)
.append("svg:g")
.attr("id", "sunburst-container")
.attr("transform", "translate(" + this.opt.width / 2 + "," + this.opt.height / 2 + ")");
var arc = d3.svg.arc()
.startAngle(function(d) { return d.x; })
.endAngle(function(d) { return d.x + d.dx; })
.innerRadius(function(d) { return Math.sqrt(d.y); })
.outerRadius(function(d) { return Math.sqrt(d.y + d.dy); });
var partition = d3.layout.partition()
.size([2 * Math.PI, radius * radius])
.value(function(d) { return d.size; });
// Basic setup of page elements.
this.initializeBreadcrumbTrail();
this.drawLegend();
// For efficiency, filter nodes to keep only those large enough to see.
var nodes = partition.nodes(json)
.filter(function(d) {
return (d.dx > 0.005); // 0.005 radians = 0.29 degrees
});
var all = this.vis.data([json])
.selectAll("path")
.data(nodes)
.enter();
all.append("svg:path")
.attr("display", function(d) { return d.depth ? null : "none"; })
.attr("d", arc)
.attr("fill-rule", "evenodd")
.style("fill", function(d) { return that.getColorByName(d.name); })
.style("opacity", 1)
.on("mouseover", that.mouseover.bind(this));
// some tests with text
/*
var arcText = d3.svg.arc()
.startAngle(function(d) { return d.x; })
.endAngle(function(d) { return d.x + d.dx; })
.innerRadius(function(d) { return Math.sqrt(d.y * 0.4); })
.outerRadius(function(d) { return Math.sqrt(d.y + d.dy * 0.4); })
var arcsText = arcs.append("svg:path")
.attr("d", arcText)
.style("fill", "none")
.attr("id", function(d, i){
return "s" + i;
});
var texts = all.append("svg:text")
.attr("dx", "0")
.attr("dy", "0")
.style("text-anchor","middle")
.append("textPath")
.attr("xlink:href", function(d, i){
return "#s" + i;
})
.attr("startOffset",function(d,i){return "25%";})
.text(function (d) {
return d.depth === 1 ? d.name : '';
});
*/
// Add the mouseleave handler to the bounding circle.
d3.select(this.opt.selectors.chart).on("mouseleave", that.mouseleave.bind(this));
// Get total size of the tree = value of root node from partition.
var node = all.node();
this.totalSize = node ? node.__data__.value : 0;
}
// Fade all but the current sequence, and show it in the breadcrumb trail.
Sunburst.prototype.mouseover = function(d) {
var percentage = (100 * d.value / this.totalSize).toPrecision(3);
var sequenceArray = this.getAncestors(d);
this.updateDescription(sequenceArray, d.value, percentage)
this.updateBreadcrumbs(sequenceArray, d.value, percentage);
// Fade all the segments.
this.vis.selectAll("path")
.style("opacity", 0.3);
// Then highlight only those that are an ancestor of the current segment.
this.vis.selectAll("path")
.filter(function(node) {
return (sequenceArray.indexOf(node) >= 0);
})
.style("opacity", 1);
}
// Restore everything to full opacity when moving off the visualization.
Sunburst.prototype.mouseleave = function(d) {
var that = this;
// Hide the breadcrumb trail
d3.select("#trail")
.style("visibility", "hidden");
// Deactivate all segments during transition.
d3.selectAll("path").on("mouseover", null);
// Transition each segment to full opacity and then reactivate it.
//TODO cancel this transition on mouseover
d3.selectAll("path")
.transition()
.duration(1000)
.style("opacity", 1)
.each("end", function() {
d3.select(this).on("mouseover", that.mouseover.bind(that));
});
d3.select(this.opt.selectors.description)
.style("visibility", "hidden");
}
// Given a node in a partition layout, return an array of all of its ancestor
// nodes, highest first, but excluding the root.
Sunburst.prototype.getAncestors = function(node) {
var path = [];
var current = node;
while (current.parent) {
path.unshift(current);
current = current.parent;
}
return path;
}
Sunburst.prototype.initializeBreadcrumbTrail = function() {
// Add the svg area.
var trail = d3.select(this.opt.selectors.breadcrumbs).append("svg:svg")
.attr("width", this.opt.width)
.attr("height", 50)
.attr("id", "trail");
// Add the label at the end, for the percentage.
trail.append("svg:text")
.attr("id", "endlabel")
.style("fill", "#000");
}
// Generate a string that describes the points of a breadcrumb polygon.
Sunburst.prototype.breadcrumbPoints = function(d, i) {
var points = [];
var b = this.opt.breadcrumbs;
points.push("0,0");
points.push(b.w + ",0");
points.push(b.w + b.t + "," + (b.h / 2));
points.push(b.w + "," + b.h);
points.push("0," + b.h);
if (i > 0) { // Leftmost breadcrumb; don't include 6th vertex.
points.push(b.t + "," + (b.h / 2));
}
return points.join(" ");
}
// format the description string in the middle of the chart
Sunburst.prototype.formatDescription = function(sequence, value, percentage) {
return percentage < 0.1 ? "< 0.1%" : percentage + '%';
}
Sunburst.prototype.updateDescription = function(sequence, value, percentage) {
d3.select(this.opt.selectors.description)
.html(this.formatDescription(sequence, value, percentage))
.style("visibility", "");
}
// format the text at the end of the breadcrumbs
Sunburst.prototype.formatBreadcrumbText = function(sequence, value, percentage) {
return value + " (" + (percentage < 0.1 ? "< 0.1%" : percentage + "%") + ")";
}
// Update the breadcrumb trail to show the current sequence and percentage.
Sunburst.prototype.updateBreadcrumbs = function(sequence, value, percentage) {
var that = this;
var b = this.opt.breadcrumbs;
// Data join; key function combines name and depth (= position in sequence).
var g = d3.select("#trail")
.selectAll("g")
.data(sequence, function(d) { return d.name + d.depth; });
// Add breadcrumb and label for entering nodes.
var entering = g.enter().append("svg:g");
entering.append("svg:polygon")
.attr("points", this.breadcrumbPoints.bind(that))
.style("fill", function(d) { return that.getColorByName(d.name); });
entering.append("svg:text")
.attr("x", (b.w + b.t) / 2)
.attr("y", b.h / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.text(function(d) { return d.name; });
// Set position for entering and updating nodes.
g.attr("transform", function(d, i) {
return "translate(" + i * (b.w + b.s) + ", 0)";
});
// Remove exiting nodes.
g.exit().remove();
// Now move and update the percentage at the end.
d3.select("#trail").select("#endlabel")
.attr("x", (sequence.length + 1) * (b.w + b.s))
.attr("y", b.h / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.html(this.formatBreadcrumbText(sequence, value, percentage));
// Make the breadcrumb trail visible, if it's hidden.
d3.select("#trail")
.style("visibility", "");
}
Sunburst.prototype.drawLegend = function() {
// Dimensions of legend item: width, height, spacing, radius of rounded rect.
var li = {
w: 75, h: 30, s: 3, r: 3
};
var legend = d3.select(this.opt.selectors.legend).append("svg:svg")
.attr("width", li.w)
.attr("height", d3.keys(this.opt.colors).length * (li.h + li.s));
var g = legend.selectAll("g")
.data(d3.entries(this.opt.colors))
.enter().append("svg:g")
.attr("transform", function(d, i) {
return "translate(0," + i * (li.h + li.s) + ")";
});
g.append("svg:rect")
.attr("rx", li.r)
.attr("ry", li.r)
.attr("width", li.w)
.attr("height", li.h)
.style("fill", function(d) { return d.value; });
g.append("svg:text")
.attr("x", li.w / 2)
.attr("y", li.h / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.text(function(d) { return d.key; });
}
// Take a 2-column CSV and transform it into a hierarchical structure suitable
// for a partition layout. The first column is a sequence of step names, from
// root to leaf, separated by hyphens. The second column is a count of how
// often that sequence occurred.
Sunburst.prototype.buildHierarchy = function(array) {
var root = {"name": "root", "children": []};
for (var i = 0; i < array.length; i++) {
var sequence = array[i][0];
var size = +array[i][1];
if (isNaN(size)) { // e.g. if this is a header row
continue;
}
var parts = sequence.split(this.opt.separator);
var currentNode = root;
for (var j = 0; j < parts.length; j++) {
var children = currentNode["children"] || [];
var nodeName = parts[j];
var childNode;
if (j + 1 < parts.length) {
// Not yet at the end of the sequence; move down the tree.
var foundChild = false;
for (var k = 0; k < children.length; k++) {
if (children[k]["name"] == nodeName) {
childNode = children[k];
foundChild = true;
break;
}
}
// If we don't already have a child node for this branch, create it.
if (!foundChild) {
childNode = {"name": nodeName, "children": []};
children.push(childNode);
}
currentNode = childNode;
} else {
// Reached the end of the sequence; create a leaf node.
childNode = {"name": nodeName, "size": size};
children.push(childNode);
}
}
}
return root;
}
return Sunburst;
}));
All the html, js and css files are stored in same folder.
But when I run it, I get the browser window like this with no visualization but just a legend box on the right.
What am I missing? The dataset is downloaded from the same github page.
Provided the path to visit-sequences.csv is correct, its local loading will be blocked by the browser (see CORS).
You can either:
run it via a (even local) webserver
start Chrome with the --allow-file-access-from-files flag (if you're using Chrome)
The problem is here
sunburst.loadCsv("/Users/i854319/Documents/Mike_UX_web/data/visit-sequences.csv");
Your browser cannot just load a file from your PC.

D3 mousedown event deleting the wrong node

I am trying to add a delete node feature in this jsfiddle
The refresh method is adding on("mousedown", mousedownNode) event to each circle, but when I click the node GW2 it deletes the DB node. I figured out that mousedownNode method deletes the correct node but, node = node.data(nodes); in the refresh method is messing up. I am not sure how to fix this problem.
function mousedownNode(d, i) {
nodes.splice(i, 1);
links = links.filter(function(l) {
return l.source !== d && l.target !== d;
});
d3.event.stopPropagation();
refresh();
}
EDIT
I have a map which is indexing the node name to index mapping :
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];
})
As you said, the correct nodes are getting deleted and the data bound to the <g class="node"></g> also seems correct BUT you're not UPDATING the node texts.
This is what you have:
nodeEnter
.append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.text(function(d) {
return d.id;
});
This is what I've changed it to:
nodeEnter
.append("text")
.attr("dx", 12)
.attr("dy", ".35em");
node.select('text')
.text(function(d) {
return d.id;
});
You can also change this by assigning the nodeText to a variable like this var nodeText = ...
But here's the point, when you call the refresh after say deleting a node, the exit() selection works BUT the existing nodes DO NOT update by themselves. Hope this makes sense. Read How Selections Work
JSFIDDLE and here's the snippet:
EDIT: Removing a draggable SVG element results in an error. To fix, change the exit selection line to:
node.exit().on('mousedown.drag', null).remove();
that removes the drag event bound before removing the element.
rect {
fill: none;
pointer-events: all;
}
.node {
fill: #000;
}
.cursor {
fill: green;
stroke: brown;
pointer-events: none;
}
.link {
stroke: #999;
}
.node text {
pointer-events: none;
font: 10px sans-serif;
}
path.link {
fill: none;
stroke: #666;
stroke-width: 1.5px;
}
<script src="https://d3js.org/d3.v3.min.js"></script>
<button id="ref" onclick="refresh()">refresh </button>
<script>
//setInterval(refresh, 15000);
function addNodeCanvas(nodeName,g) {
var node = { x: 900, y: 900, id: nodeName, grp:g };
var n = nodes.push(node);
console.log(node);
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 fill = d3.scale.category20();
var links = [{ source: "FH", target: "TP" }];
var nodes = [
{ id: "FH", x: 100, y: 110 },
{ id: "TP", x: 200, y: 110 },
{ id: "GW1", x: 200, y: 110 },
{ id: "GW", x: 200, y: 110 },
{ id: "GW2", x: 200, y: 110 },
{ id: "DB", x: 100, y: 110 }
]
var width = 600,
height = 400,
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()
.size([width, height])
.nodes(nodes) .links(links)
.linkDistance(50)
.charge(-100)
.on("tick", tick);
var svg = d3
.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
// build the arrow.
var arrows = svg
.append("svg:defs")
.selectAll("marker")
.data(["arrow"]) // Different link/path types can be defined here
.enter()
.append("svg:marker") // This section adds in the arrows
.attr("id", String)
.attr("viewBox", "0 -5 10 10")
.attr("refX", 10)
.attr("refY", -1)
.attr("markerWidth", 4)
.attr("markerHeight", 4)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5");
svg
.append("rect")
.attr("width", width)
.attr("height", height);
var nodes = force.nodes(),
links = force.links(),
node = svg.selectAll(".node"),
link = svg.selectAll(".link");
function tick() {
link.attr("d", function(d) {
var dx = d.target.x - d.source.x,
dy = d.target.y - d.source.y;
var angle = Math.atan2(dy, dx);
var offsetX = radius * Math.cos(angle);
var offsetY = radius * Math.sin(angle);
dr = Math.sqrt(dx * dx + dy * dy);
return (
"M" +
(d.source.x + offsetX) +
"," +
(d.source.y + offsetY) +
"A" +
dr +
"," +
dr +
" 0 0,1 " +
(d.target.x - offsetX) +
"," +
(d.target.y - offsetY)
);
});
node.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
}
function mousedownNode(d, i) {
nodes.splice(i, 1);
links = links.filter(function(l) {
return l.source !== d && l.target !== d;
});
d3.event.stopPropagation();
refresh();
}
refresh();
function refresh() {
link = link.data(links);
link
.enter()
.append("path")
.attr("class", "link")
.attr("marker-end", "url(#arrow)");
link.exit().remove();
node = node.data(nodes);
var nodeEnter = node
.enter()
.insert("g")
.attr("class", "node")
.on("mousedown", mousedownNode)
.call(force.drag);
nodeEnter
.append("image")
.attr("xlink:href", "https://github.com/favicon.ico")
.attr("x", -8)
.attr("y", -8)
.attr("width", 16)
.attr("height", 16);
nodeEnter
.append("text")
.attr("dx", 12)
.attr("dy", ".35em");
node.select('text')
.text(function(d) {
return d.id;
});
node.exit().on('mousedown.drag', null).remove();
force.start();
}
</script>
I didn't dig into this much, as I felt like the problem was quite obvious to me. I can't exactly explain why the GW node returns but it's something to do with your data-binding being incorrect I believe.
When you're doing your data join you're basically telling D3 to key on an index. Now the original GW node in the DOM is going to be re-used if it weren't the last in the index because the indexes will be offset by 1.
node = node.data(nodes);
If you change this to key on a field
node = node.data(nodes, d => d.id);
You'll find your problem goes away.

D3 Tree update color of nodes dynamically

So I'm trying to create a tree in D3 (Adapted from here) to show a series of nodes that are a specific color according to their value. The problem is that I am getting new data at set intervals that may change these values. I want the tree to update the colors accordingly when it recieves new information. I've tried a number of different methods that result in the entire tree redrawing itself, flying onto the screen, and auto expanding every nodes children. The desired effect that I am looking for is for the color of updated nodes to change, while the tree respects the status of collapsed/uncollapsed nodes that the user doesn't see the whole tree reset itself. Is this possible?
Here's what i have so far:
// Get JSON data
var treeData = {
"name": "rootAlert",
"alert": "true",
"children": [{
"name": "Child1",
"alert": "true",
"children": [{
"name": "Child1-1",
"alert": "false"
}, {
"name": "Child1-2",
"alert": "false"
}, {
"name": "Child1-3",
"alert": "true"
}]
}, {
"name": "Child2",
"alert": "false",
"children": [{
"name": "Child2-1",
"alert": "false"
}, {
"name": "Child2-2",
"alert": "false"
}, {
"name": "Child2-3",
"alert": "false"
}]
}, {
"name": "Child3",
"alert": "false"
}]
}
// Calculate total nodes, max label length
var totalNodes = 0;
var maxLabelLength = 0;
// variables for drag/drop
var selectedNode = null;
var draggingNode = null;
// panning variables
var panSpeed = 200;
var panBoundary = 20; // Within 20px from edges will pan when dragging.
// Misc. variables
var i = 0;
var duration = 750;
var root;
// size of the diagram
var viewerWidth = $(document).width();
var viewerHeight = $(document).height();
var tree = d3.layout.tree()
.size([viewerHeight, viewerWidth]);
// define a d3 diagonal projection for use by the node paths later on.
var diagonal = d3.svg.diagonal()
.projection(function(d) {
return [d.y, d.x];
});
// A recursive helper function for performing some setup by walking through all nodes
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);
}
}
}
function visit2(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);
}
}
}
// Call visit function to establish maxLabelLength
visit(treeData, function(d) {
totalNodes++;
maxLabelLength = Math.max(d.name.length, maxLabelLength);
}, function(d) {
return d.children && d.children.length > 0 ? d.children : null;
});
// TODO: Pan function, can be better implemented.
function pan(domNode, direction) {
var speed = panSpeed;
if (panTimer) {
clearTimeout(panTimer);
translateCoords = d3.transform(svgGroup.attr("transform"));
if (direction == 'left' || direction == 'right') {
translateX = direction == 'left' ? translateCoords.translate[0] + speed : translateCoords.translate[0] - speed;
translateY = translateCoords.translate[1];
} else if (direction == 'up' || direction == 'down') {
translateX = translateCoords.translate[0];
translateY = direction == 'up' ? translateCoords.translate[1] + speed : translateCoords.translate[1] - speed;
}
scaleX = translateCoords.scale[0];
scaleY = translateCoords.scale[1];
scale = zoomListener.scale();
svgGroup.transition().attr("transform", "translate(" + translateX + "," + translateY + ")scale(" + scale + ")");
d3.select(domNode).select('g.node').attr("transform", "translate(" + translateX + "," + translateY + ")");
zoomListener.scale(zoomListener.scale());
zoomListener.translate([translateX, translateY]);
panTimer = setTimeout(function() {
pan(domNode, speed, direction);
}, 50);
}
}
// Define the zoom function for the zoomable tree
function zoom() {
svgGroup.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
// define the zoomListener which calls the zoom function on the "zoom" event constrained within the scaleExtents
var zoomListener = d3.behavior.zoom().scaleExtent([0.1, 3]).on("zoom", zoom);
// define the baseSvg, attaching a class for styling and the zoomListener
var baseSvg = d3.select("#tree-container").append("svg")
.attr("width", viewerWidth)
.attr("height", viewerHeight)
.attr("class", "overlay")
.call(zoomListener);
// Function to center node when clicked/dropped so node doesn't get lost when collapsing/moving with large amount of children.
function centerNode(source) {
scale = zoomListener.scale();
x = -source.y0;
y = -source.x0;
x = x * scale + viewerWidth / 2;
y = y * scale + viewerHeight / 2;
d3.select('g').transition()
.duration(duration)
.attr("transform", "translate(" + x + "," + y + ")scale(" + scale + ")");
zoomListener.scale(scale);
zoomListener.translate([x, y]);
}
function leftAlignNode(source) {
scale = zoomListener.scale();
x = -source.y0;
y = -source.x0;
x = (x * scale) + 100;
y = y * scale + viewerHeight / 2;
d3.select('g').transition()
.duration(duration)
.attr("transform", "translate(" + x + "," + y + ")scale(" + scale + ")");
zoomListener.scale(scale);
zoomListener.translate([x, y]);
}
// Toggle children function
function toggleChildren(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else if (d._children) {
d.children = d._children;
d._children = null;
}
return d;
}
function toggle(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
}
// Toggle children on click.
function click(d) {
if (d3.event.defaultPrevented) return; // click suppressed
if (d._children != null) {
var isCollapsed = true
} else {
var isCollapsed = false;
}
d = toggleChildren(d);
update(d);
if (isCollapsed) {
leftAlignNode(d);
} else {
centerNode(d);
}
}
function update(source) {
// Compute the new height, function counts total children of root node and sets tree height accordingly.
// This prevents the layout looking squashed when new nodes are made visible or looking sparse when nodes are removed
// This makes the layout more consistent.
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; // 25 pixels per line
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 * 5)); //maxLabelLength * 10px
// alternatively to keep a fixed scale one can set a fixed depth per level
// Normalize for fixed-depth by commenting out below line
// d.y = (d.depth * 500); //500px per level.
});
// Update the nodes…
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")
//.call(dragListener)
.attr("class", "node")
.attr("transform", function(d) {
return "translate(" + source.y0 + "," + source.x0 + ")";
})
.on('click', click);
nodeEnter.append("circle")
.attr('class', 'nodeCircle')
.attr("r", 0)
.style("fill", function(d) {
return d._children ? "lightsteelblue" : "#fff";
});
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.name;
})
.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;
})
.attr("text-anchor", function(d) {
return d.children || d._children ? "end" : "start";
})
.text(function(d) {
return d.name;
});
// Change the circle fill depending on whether it has children and is collapsed
node.select("circle.nodeCircle")
.attr("r", 4.5)
.style("fill", function(d) {
return d._children ? "lightsteelblue" : "#fff";
});
// 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) { // alert(d.alert);
//console.log(d.name + ' is ' + d.alert)
if (d.alert == 'true') //if alert == true
return "red";
else return d._children ? "green" : "green";
});
// 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();
// Stash the old positions for transition.
nodes.forEach(function(d) {
d.x0 = d.x;
d.y0 = d.y;
});
}
// Append a group which holds all nodes and which the zoom Listener can act upon.
var svgGroup = baseSvg.append("g");
// Define the root
root = treeData;
root.x0 = viewerHeight / 2;
root.y0 = 0;
// Layout the tree initially and center on the root node.
tree.nodes(root).forEach(function(n) {
toggle(n);
});
update(root);
leftAlignNode(root);
setInterval(function() {
//update the color of each node
}, 2000);
.node {
cursor: pointer;
}
.overlay {
background-color: #EEE;
}
.node circle {
fill: #fff;
stroke: gray;
stroke-width: 1.5px;
}
.node text {
font: 10px sans-serif;
}
.link {
fill: none;
stroke: #ccc;
stroke-width: 1.5px;
}
.templink {
fill: none;
stroke: red;
stroke-width: 3px;
}
.ghostCircle.show {
display: block;
}
.ghostCircle,
.activeDrag .ghostCircle {
display: none;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div id="tree-container"></div>
I've found a great example here
It does basically exactly what I want. However, instead of updating the tree when a selection is made, I want it to update automatically when new data is recieved. I'd also like to see it not in AngularJS. I've attempted to implement the same type of update function from that example, but mine still pos in from the top left, while the example is so smooth!
It may be best to re-render the visualization when your data pipeline is updated.
You'll have to save the state of your visualization, preserving all information that will be needed to re-render the visualization, so that your users are none the wiser.

Don't rotate nodes in radial tree layout in d3.js

Fiddle Example
I can't figure out how to tweak the transform:rotate attribute for the nodes so that the pictures and text in the foreign objects don't rotate/ go upside down. I have tried tweaking with this block of code:
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function (d) {
return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")";
});
In the Chrome Console, I change a node from
<g transform="translate(226.5247584249853,-164.57987064189248)rotate(-36)">
<foreignObject.....></foreignObject></g>
to
transform="translate(226.5247584249853,-164.57987064189248)rotate(0)
and it works. But changing rotate to 0 in the code above would make every children node go to the left side., like this
Full code:
treeData = myJSON;
// Calculate total nodes, max label length
var totalNodes = 0;
var maxLabelLength = 0;
// variables for drag/drop
var selectedNode = null;
var draggingNode = null;
// panning variables
var panSpeed = 200;
var panBoundary = 20; // Within 20px from edges will pan when dragging.
// Misc. variables
var i = 0;
var duration = 750;
var root;
// size of the diagram
var width = $(document).width();
var height = $(document).height();
var diameter = 800;
var tree = d3.layout.tree().size([360, diameter / 2 - 120])
.separation(function (a, b) {
return (a.parent == b.parent ? 1 : 10) / a.depth;
});
// define a d3 diagonal projection for use by the node paths later on.
var diagonal = d3.svg.diagonal.radial()
.projection(function (d) {
return [d.y, d.x / 180 * Math.PI];
});
// Define the root
root = treeData;
root.x0 = height / 2;
root.y0 = 0;
// A recursive helper function for performing some setup by walking through all nodes
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);
}
}
}
// Call visit function to establish maxLabelLength
visit(treeData, function (d) {
totalNodes++;
maxLabelLength = Math.max(d.name.length, maxLabelLength);
}, function (d) {
return d.children && d.children.length > 0 ? d.children : null;
});
// sort the tree according to the node names
function sortTree() {
tree.sort(function (a, b) {
return b.name.toLowerCase() < a.name.toLowerCase() ? 1 : -1;
});
}
// Sort the tree initially incase the JSON isn't in a sorted order.
sortTree();
// TODO: Pan function, can be better implemented.
function pan(domNode, direction) {
var speed = panSpeed;
if (panTimer) {
clearTimeout(panTimer);
translateCoords = d3.transform(svgGroup.attr("transform"));
if (direction == 'left' || direction == 'right') {
translateX = direction == 'left' ? translateCoords.translate[0] + speed : translateCoords.translate[0] - speed;
translateY = translateCoords.translate[1];
} else if (direction == 'up' || direction == 'down') {
translateX = translateCoords.translate[0];
translateY = direction == 'up' ? translateCoords.translate[1] + speed : translateCoords.translate[1] - speed;
}
scaleX = translateCoords.scale[0];
scaleY = translateCoords.scale[1];
scale = zoomListener.scale();
svgGroup.transition().attr("transform", "translate(" + translateX + "," + translateY + ")scale(" + scale + ")");
d3.select(domNode).select('g.node').attr("transform", "translate(" + translateX + "," + translateY + ")");
zoomListener.scale(zoomListener.scale());
zoomListener.translate([translateX, translateY]);
panTimer = setTimeout(function () {
pan(domNode, speed, direction);
}, 50);
}
}
// Define the zoom function for the zoomable tree
function zoom() {
svgGroup.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
// define the zoomListener which calls the zoom function on the "zoom" event constrained within the scaleExtents
var zoomListener = d3.behavior.zoom().scaleExtent([1, 1]).on("zoom", zoom);
function initiateDrag(d, domNode) {
draggingNode = d;
d3.select(domNode).select('.ghostCircle').attr('pointer-events', 'none');
d3.selectAll('.ghostCircle').attr('class', 'ghostCircle show');
d3.select(domNode).attr('class', 'node activeDrag');
svgGroup.selectAll("g.node").sort(function (a, b) { // select the parent and sort the path's
if (a.id != draggingNode.id) return 1; // a is not the hovered element, send "a" to the back
else return -1; // a is the hovered element, bring "a" to the front
});
// if nodes has children, remove the links and nodes
if (nodes.length > 1) {
// remove link paths
links = tree.links(nodes);
nodePaths = svgGroup.selectAll("path.link")
.data(links, function (d) {
return d.target.id;
}).remove();
// remove child nodes
nodesExit = svgGroup.selectAll("g.node")
.data(nodes, function (d) {
return d.id;
}).filter(function (d, i) {
if (d.id == draggingNode.id) {
return false;
}
return true;
}).remove();
}
// remove parent link
parentLink = tree.links(tree.nodes(draggingNode.parent));
svgGroup.selectAll('path.link').filter(function (d, i) {
if (d.target.id == draggingNode.id) {
return true;
}
return false;
}).remove();
dragStarted = null;
}
// define the baseSvg, attaching a class for styling and the zoomListener
var baseSvg = d3.select("#tree-container").append("svg")
.attr("width", width)
.attr("height", height)
.attr("class", "overlay")
.call(zoomListener);
// Define the drag listeners for drag/drop behaviour of nodes.
dragListener = d3.behavior.drag()
.on("dragstart", function (d) {
if (d == root) {
return;
}
dragStarted = true;
nodes = tree.nodes(d);
d3.event.sourceEvent.stopPropagation();
// it's important that we suppress the mouseover event on the node being dragged. Otherwise it will absorb the mouseover event and the underlying node will not detect it d3.select(this).attr('pointer-events', 'none');
})
.on("drag", function (d) {
if (d == root) {
return;
}
if (dragStarted) {
domNode = this;
initiateDrag(d, domNode);
}
// get coords of mouseEvent relative to svg container to allow for panning
relCoords = d3.mouse($('svg').get(0));
if (relCoords[0] < panBoundary) {
panTimer = true;
pan(this, 'left');
} else if (relCoords[0] > ($('svg').width() - panBoundary)) {
panTimer = true;
pan(this, 'right');
} else if (relCoords[1] < panBoundary) {
panTimer = true;
pan(this, 'up');
} else if (relCoords[1] > ($('svg').height() - panBoundary)) {
panTimer = true;
pan(this, 'down');
} else {
try {
clearTimeout(panTimer);
} catch (e) {
}
}
d.x0 = d3.event.x;
d.y0 = d3.event.y;
var node = d3.select(this);
node.attr("transform", "translate(" + d.x0 + "," + (d.y0) + ")");
updateTempConnector();
})
.on("dragend", function (d) {
if (d == root) {
return;
}
domNode = this;
if (selectedNode) {
// now remove the element from the parent, and insert it into the new elements children
var index = draggingNode.parent.children.indexOf(draggingNode);
if (index > -1) {
draggingNode.parent.children.splice(index, 1);
}
if (typeof selectedNode.children !== 'undefined' || typeof selectedNode._children !== 'undefined') {
if (typeof selectedNode.children !== 'undefined') {
selectedNode.children.push(draggingNode);
} else {
selectedNode._children.push(draggingNode);
}
} else {
selectedNode.children = [];
selectedNode.children.push(draggingNode);
}
// Make sure that the node being added to is expanded so user can see added node is correctly moved
expand(selectedNode);
sortTree();
endDrag();
} else {
endDrag();
}
});
function endDrag() {
selectedNode = null;
d3.selectAll('.ghostCircle').attr('class', 'ghostCircle');
d3.select(domNode).attr('class', 'node');
// now restore the mouseover event or we won't be able to drag a 2nd time
d3.select(domNode).select('.ghostCircle').attr('pointer-events', '');
updateTempConnector();
if (draggingNode !== null) {
update(root);
//centerNode(draggingNode);
draggingNode = null;
}
}
// 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 expand(d) {
if (d._children) {
d.children = d._children;
d.children.forEach(expand);
d._children = null;
}
}
var overCircle = function (d) {
console.log(d);
selectedNode = d;
updateTempConnector();
};
var outCircle = function (d) {
selectedNode = null;
updateTempConnector();
};
// Function to update the temporary connector indicating dragging affiliation
var updateTempConnector = function () {
var data = [];
if (draggingNode !== null && selectedNode !== null) {
// have to flip the source coordinates since we did this for the existing connectors on the original tree
data = [{
source: {
x: $('svg g').first().offset().left + selectedNode.position.left,
y: selectedNode.position.top
},
target: {
x: draggingNode.x0,
y: draggingNode.y0
}
}];
}
var link = svgGroup.selectAll(".templink").data(data);
link.enter().append("path")
.attr("class", "templink")
.attr("d", d3.svg.diagonal.radial())
.attr('pointer-events', 'none');
link.attr("d", d3.svg.diagonal.radial());
link.exit().remove();
};
// Function to center node when clicked/dropped so node doesn't get lost when collapsing/moving with large amount of children.
function centerNode(source) {
scale = zoomListener.scale();
x = -source.x0;
y = -source.y0;
x = x * scale + width / 2;
y = y * scale + height / 2;
d3.select('g').transition()
.duration(duration)
.attr("transform", "translate(" + x + "," + y + ")scale(" + scale + ")");
zoomListener.scale(scale);
zoomListener.translate([x, y]);
}
// Toggle children function
function toggleChildren(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else if (d._children) {
d.children = d._children;
d._children = null;
}
return d;
}
// Toggle children on click.
function click(d) {
if (d3.event.defaultPrevented) return; // click suppressed
d = toggleChildren(d);
update(d);
//centerNode(d);
//dofocus([{ name : 'o_id' , value : d.o_id }]);
}
function update(source) {
// Compute the new height, function counts total children of root node and sets tree height accordingly.
// This prevents the layout looking squashed when new nodes are made visible or looking sparse when nodes are removed
// This makes the layout more consistent.
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; // 25 pixels per line
// tree = tree.size([newHeight, width]);
// 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 * 10)); //maxLabelLength * 10px
// // alternatively to keep a fixed scale one can set a fixed depth per level
// // Normalize for fixed-depth by commenting out below line
// // d.y = (d.depth * 500); //500px per level.
// });
// Update the nodes…
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")
.call(dragListener)
.attr("class", "node")
// .attr("transform", function(d) {
// return "translate(" + source.y0 + "," + source.x0 + ")";
// })
.on('click', click)
nodeEnter.append("foreignObject")
.attr("class", "smallcircle")
.attr("width", function (d) {
var f = document.createElement("span");
f.id = "hiddenText";
f.style.display = 'hidden';
f.style.padding = '0px';
f.innerHTML = d.name;
document.body.appendChild(f);
textWidth = f.offsetWidth;
var f1 = document.getElementById('hiddenText');
f1.parentNode.removeChild(f1);
return textWidth + 50;
})
.attr("overflow", "visible")
.attr("height", 50)
.attr("y", - 50 / 2)
.attr("x", - 50)
.append("xhtml:div").attr("class", "mainDiv")
.html(function (d) {
var htmlString = "";
htmlString += "<div class='userImage' style='border-color:red '><img src='https://www.gravatar.com/avatar/6d2db975d856b8799a6198bab4777aed?s=32&d=identicon&r=PG' width='50' height='50'></div>";
htmlString += "<div class='content' style='color:red;'>" + d.name + "</div>";
htmlString += "<div style='clear:both;'></div>";
return htmlString;
})
nodeEnter.append("text")
.text(function (d) {
return d.name;
})
.style("font", "8px serif")
.style("opacity", 0.9)
.style("fill-opacity", 0);
// phantom node to give us mouseover in a radius around it
nodeEnter.append("circle")
.attr('class', 'ghostCircle')
.attr("r", 30)
.attr("opacity", 0.2) // change this to zero to hide the target area
.style("fill", "red")
.attr('pointer-events', 'mouseover')
.on("mouseover", function (node) {
node.position = $(this).position();
node.offset = $(this).offset();
overCircle(node);
})
.on("mouseout", function (node) {
outCircle(node);
});
// 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;
// })
// .attr("text-anchor", function(d) {
// return d.children || d._children ? "end" : "start";
// })
// .text(function(d) {
// return d.name;
// });
// Change the circle fill depending on whether it has children and is collapsed
node.select("circle.nodeCircle")
.attr("r", 4.5)
.style("fill", function (d) {
return d._children ? "lightsteelblue" : "#fff";
});
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function (d) {
return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")";
});
nodeUpdate.select("circle")
.attr("r", 4.5)
.style("fill", function (d) {
return d._children ? "lightsteelblue" : "#fff";
});
// Fade the text in
// nodeUpdate.select("text")
// .style("fill-opacity", 1);
nodeUpdate.select("text")
.style("fill-opacity", 1)
// .attr("transform", function(d) { return d.x < 180 ? "translate(0)" : "rotate(180)translate(-" + (d.name.length + 50) + ")"; })
.attr("dy", ".35em")
.attr("text-anchor", function (d) {
return d.x < 180 ? "start" : "end";
})
.attr("transform", function (d) {
return d.x < 180 ? "translate(8)" : "rotate(180)translate(-8)";
});
// Transition exiting nodes to the parent's new position.
var nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function (d) {
return "translate(" + source.x + "," + source.y + ")";
})
.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();
// Stash the old positions for transition.
nodes.forEach(function (d) {
d.x0 = d.x;
d.y0 = d.y;
});
}
// Append a group which holds all nodes and which the zoom Listener can act upon.
var svgGroup = baseSvg.append("g").attr("transform", "translate(" + diameter / 2 + "," + diameter / 2 + ")");
// Collapse all children of roots children before rendering.
root.children.forEach(function (child) {
collapse(child);
});
// Layout the tree initially and center on the root node.
update(root);
d3.select(self.frameElement).style("height", width);
The rotation is set in line 1221 of your fiddle:
return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")";
The rotation here is required for the positioning, as you're rotating around the origin of the non-translated coordinate system. So simply removing the rotate(...) won't work. However, you can rotate the elements back after positioning:
return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")rotate(" + (-d.x + 90) + ")";
Complete fiddle here.

Categories