D3js - Trying to update links in a Force Layout - javascript

I'd like to update links and nodes of a force directed layout at run-time. But the behavior is strange, because sometimes it does not add new links and sometimes it does not remove old links. Do you have any suggestions?
Any help would be appreciated.
Here is my code:
network.js
// Network View size
var width = 1280,
height = 500
var radius = 200;
var robj = 8;
var scaled_radius = d3.scale.linear()
.domain([0, 7000000000000])
.range([10, 40]);
var svg_network = d3.select(document.createElementNS(d3.ns.prefix.svg, "svg"))
.attr("width", width)
.attr("height", height);
var force = d3.layout.force()
.gravity(1.0)
.distance(100)
.charge(-60)
.size([width, height]);
var timestamp_info = svg_network.append("text")
.attr("dx", 10)
.attr("dy", 10);
function graph_network_start(flowz)
{
/* ------------------------ */
/* DATA PREPROCESSING */
/* ------------------------ */
flowz = network_preprocess(flowz)
/* ------------------------ */
/* NODE POSITIONING */
/* ------------------------ */
nodePositioning(flowz.activeNodes);
var x = d3.scale.linear().domain([0, flowz.activeNodes.length]).range([0, 180]);
// Bind link and node data to DOM elements
var link = svg_network.selectAll(".link").data(flowz.flow, function(d) { return d.source + "-" + d.target; });
var node = svg_network.selectAll("g.node").data(flowz.activeNodes)//, function(d,i) {return i;});
/* ------------------------ */
/* UPDATE LINKS */
/* ------------------------ */
link.exit().transition().duration(10).remove();
link.enter().append("line")
.attr("id",function(d){return d.source.ID + "-" + d.target.ID;})
.attr("class", "link")
.style("opacity",0)
.transition()
.duration(1000)
.style("opacity",1);
/* ------------------------ */
/* UPDATE NODES */
/* ------------------------ */
node.exit().transition().duration(100).remove();
var newNode = node.enter().append("svg:g")
.attr("id", function(d) {return d.ID})
.attr("class", "node")
.call(force.drag).on("click", function(d){
if (nodes[d.ID].name == "Workstations Site 1") {
selectedSite=1; bubble_visualize(selectedTimestamp, selectedSite);showMode(2);
} else if (nodes[d.ID].name == "Workstations Site 2") {
selectedSite=2; bubble_visualize(selectedTimestamp, selectedSite);showMode(2);
} else if (nodes[d.ID].name == "Workstations Site 3") {
selectedSite=3; bubble_visualize(selectedTimestamp, selectedSite);showMode(2);
}})
.on("mouseover", fade(.1)).on("mouseout", fade(1))
newNode.append("circle")
.attr("r", 8) //function(d){return scaled_radius(d.output)})
.style("fill", color)
.style("stroke", "black" )
.style("opacity",0)
.transition()
.duration(1000)
.style("opacity",1);
newNode.append("text")
.attr("dx", robj + 2)
.attr("dy", ".1em")
.text(function(d) { return nodes[d.ID].name })
.style("opacity",0)
.transition()
.duration(1000)
.style("opacity",1);
//if (nodes[d.ID].name == "Workstations Site" || nodes[d.ID].name == "Workstations Site 2" || nodes[d.ID].name == "Workstations Site 3") return nodes[d.ID].name });
/* ------------------------ */
/* UPDATE INFOS */
/* ------------------------ */
timestamp_info.text(function(d) { return new Date(flowz.timestamp).toString()});
force
.nodes(flowz.activeNodes)
.links(flowz.flow)
.on("tick", tick);
force.start();
var linkedByIndex = {};
flowz.flow.forEach(function(d) {
linkedByIndex[d.source + "," + d.target] = 1;
});
function color(d){
return "steelblue"
}
function isConnected(a, b) {
return linkedByIndex[a.index + "," + b.index] || linkedByIndex[b.index + "," + a.index] || a.index == b.index;
}
function tick()
{
link.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
}
function fade(opacity) {
return function(d) {
node.style("stroke-opacity", function(o) {
thisOpacity = isConnected(d, o) ? 1 : opacity;
this.setAttribute('fill-opacity', thisOpacity);
return thisOpacity;
});
link.style("stroke-opacity", function(o) {
return o.source === d || o.target === d ? 1 : opacity;
});
};
}
function nodePositioning(nodesparam){
for (var i=0; i<nodesparam.length; i++)
{
if(nodes[nodesparam[i].ID].name == "Internet")
{
nodesparam[i].x=width/2;
nodesparam[i].y=30;
}
else if (nodes[nodesparam[i].ID].name == "Firewall")
{
nodesparam[i].x=width/2;
nodesparam[i].y=50;
}
else if (nodes[nodesparam[i].ID].name == "172.0.0.1")
{
nodesparam[i].x=width/2;
nodesparam[i].y=120;
}
else
{
nodesparam[i].x = width/2 + (radius - robj) * Math.cos(Math.PI + i * (Math.PI / nodesparam.length +1)) ;
nodesparam[i].y = height/2 + (radius - robj) * Math.sin(Math.PI + i * (Math.PI / nodesparam.length +1)) ;
}
nodesparam[i].fixed = true;
}
}
function positionX(node, index)
{
if(node.name == "Internet" || node.name == "Firewall" || node.name == "127.0.0.1")
{
return width/2;
}
else
{
return (width/2 + (radius - robj) * Math.cos(Math.PI + index * (Math.PI / nodes.length +1))) ;
}
}
function positionY(node, index)
{
if(node.name == "Internet")
{
return 30;
}
else if (node.name == "Firewall")
{
return 50;
}
else if (node.name == "172.0.0.1")
{
return 120;
}
else
{
return (height/2 + (radius - robj) * Math.sin(Math.PI + index * (Math.PI / nodes.length + 1))) ;;
}
}
function network_preprocess(flowz){
for(var i=+0; i<flowz.flow.length;i++)
{
for(var j=0; j<flowz.activeNodes.length;j++)
{
if(flowz.activeNodes[j].ID == flowz.flow[i].source)
flowz.flow[i].source = j;
if(flowz.activeNodes[j].ID == flowz.flow[i].target)
flowz.flow[i].target = j;
}
}
return flowz
}
}
links.json excerpt
[
{
"timestamp": 1364795760000,
"flow": [
{
"source": 0,
"target": 1,
"value": 15540
}
],
"activeNodes": [
{
"output": 15540,
"ID": 0
},
{
"output": 0,
"ID": 1
}
]
},
{
"timestamp": 1364795880000,
"flow": [
{
"source": 2,
"target": 1,
"value": 2960
},
{
"source": 0,
"target": 1,
"value": 14800
}
],
"activeNodes": [
{
"output": 14800,
"ID": 0
},
{
"output": 0,
"ID": 1
},
{
"output": 2960,
"ID": 2
}
]
}
]
EDIT: 1
I could narrow it down. The links.source and links.target elements are being destroyed by my code. For some reason they are changed from int to objects:
Output at time[0]:
Object {source: 3, target: 4, value: 213143231}
Object {source: 5, target: 4, value: 448560}
and when I go to another dataset and back to time[0]:
Object {source: Object, target: Object, value: "213143231"}
Object {source: Object, target: Object, value: "448560"}
Even the values get changed into strings. Maybe it happens inside of this code excerpt:
function graph_network_start(flowz)
{
flowz = network_preprocess(flowz);
...
var link = svg_network.selectAll(".link").data(flowz.flow, function(d) { console.log(d); return d.source + "-" + d.target; });
var node = svg_network.selectAll("g.node").data(flowz.activeNodes, function(d) {return d.ID})
...
force
.nodes(flowz.activeNodes)
.links(flowz.flow)
.on("tick", tick);
force.start();
function network_preprocess(flowz){
/*
We have to remove not existing nodes and add new nodes to our network nodes
*/
// Remove every node which does not exist anymore
var activeSet = new HashSet();
activeSet.addAll(flowz.activeNodes);
var toRemove = network_nodes.complement(activeSet).values();
for(var i=0; i<toRemove.length; i++)
{
network_nodes.remove(toRemove[i]);
}
// Add new nodes
var activenodes = flowz.activeNodes;
for(var i=0; i<activenodes.length; i++)
{
if (! network_nodes.contains(activenodes[i]) )
{
network_nodes.add(activenodes[i])
}
}
// Order nodes
var ufzuffu = network_nodes.values();
ufzuffu.sort(function(a,b){
var keya = a.ID;
var keyb = b.ID;
if(keya < keyb) return -1;
if(keya > keyb) return 1;
return 0;
});
flowz.activeNodes = ufzuffu;
// Edit links
for(var i=0; i<flowz.flow.length;i++)
{
for(var j=0; j<flowz.activeNodes.length;j++)
{
if(flowz.activeNodes[j].ID == flowz.flow[i].source)
{
flowz.flow[i].source = +j;
continue;
}
if(flowz.activeNodes[j].ID == flowz.flow[i].target)
{
flowz.flow[i].target = +j;
}
}
}
return flowz
}
}
EDIT 2
Well .. looks like I found a workaround:
var link = svg_network.selectAll(".link").data(flowz.flow, function(d) {
if(d.source.hasOwnProperty("ID"))
return d.source.index + "-" + d.target.index;
else
return d.source + "-" + d.target;
});
It works but I still dont know why it randomly(?) generates objects out of integers ..

Related

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

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>

Display a diagram (using d3.js)

I get the js file below and I have to do some changes on it,
This is a Javascript code using the d3.js lib, the goal is to get a diagram like this screenshot from JSON data.
First of all, I would like to display the diagram.
I know that the javascript code is working, the problem must be in my html file but i don't really understand why nothing is happening.
Thank you for your help !
var orgChart = (function() {
var oldSelNode = null,
oldSelID = null,
oldSelColor = null,
selNode = null,
_x = 0,
_y = 0,
nodeID = -1,
nodePID = -1;
var _margin = {
top: 20,
right: 20,
bottom: 20,
left: 20
},
_root = {},
_nodes = [],
_counter = 0,
_svgroot = null,
_svg = null,
_tree = null,
_diagonal = null,
_lineFunction = null,
_loadFunction = null,
/* Configuration */
_duration = 500,
/* Duration of the animations */
_rectW = 250,
/* Width of the rectangle */
_rectH = 50,
/* Height of the rectangle */
_rectSpacing = 20 /* Spacing between the rectangles */
_fixedDepth = 40, /* Height of the line for child nodes */
_mode = "diagonal", /* Choose the values "line" or "diagonal" */
_callerNode = null,
_callerMode = 0,
_scale = 0.3,
_zoomEnabled = false,
_dragEnabled = true,
defLinearGradient = function(id, x1, y1, x2, y2, stopsdata) {
var gradient = _svgroot.append("svg:defs")
.append("svg:linearGradient")
.attr("id", id)
.attr("x1", x1)
.attr("y1", y1)
.attr("x2", x2)
.attr("y2", y2)
.attr("spreadMethod", "pad");
$.each(stopsdata, function(index, value) {
gradient.append("svg:stop")
.attr("offset", value.offset)
.attr("stop-color", value.color)
.attr("stop-opacity", value.opacity);
});
},
defBoxShadow = function(id) {
var filter = _svgroot.append("svg:defs")
.append("svg:filter")
.attr("id", id).attr("height", "150%").attr("width", "150%");
filter.append("svg:feOffset")
.attr("dx", "2").attr("dy", "2").attr("result", "offOut"); // how much to offset
filter.append("svg:feGaussianBlur")
.attr("in", "offOut").attr("result", "blurOut").attr("stdDeviation", "2"); // stdDeviation is how much to blur
filter.append("svg:feBlend")
.attr("in", "SourceGraphic").attr("in2", "blurOut").attr("mode", "normal");
},
collapse = function(d) {
if (d.children) {
}
},
update = function(source) {
// Compute the new tree layout.
_nodes = _tree.nodes(_root).reverse();
var links = _tree.links(_nodes);
// Normalize for fixed-depth.
_nodes.forEach(function(d) {
d.y = d.depth * _fixedDepth;
});
// Update the nodes
var node = _svg.selectAll("g.node")
.data(_nodes, function(d) {
return d.id || (d.id = ++_counter);
});
// 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.x0 + "," + source.y0 + ")";
})
.on("click", nodeclick)
.on("contextmenu", contextmenuclick);
nodeEnter.append("rect")
.attr("width", _rectW)
.attr("height", _rectH)
.attr("fill", "#898989")
.attr("filter", "url(#boxShadow)");
nodeEnter.append("rect")
.attr("width", _rectW)
.attr("height", _rectH)
.attr("id", function(d) {
return d.id;
})
.attr("fill", function(d) {
return d.fill
})
.style("cursor", "pointer")
.attr("class", "box");
nodeEnter.append("text")
.attr("x", _rectW / 2)
.attr("y", _rectH / 2)
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.attr("fill", "#FFFF")
.style("cursor", "pointer")
.text(function(d) {
return d.desc;
});
// Transition nodes to their new position.
var nodeUpdate = node.transition()
.duration(_duration)
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
nodeUpdate.select("#_" + _canvasObj.nm + " rect.box")
.attr("fill", function(d) {
return (d.fill);
});
// 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();
// Update the links
var link = _svg.selectAll("path.link")
.data(links, function(d) {
return d.target.id;
});
if (_mode === "line") {
// Enter any new links at the parent's previous position.
link.enter().append("path", "g")
.attr("class", "link")
.attr("d", function(d) {
var u_line = (function(d) {
var u_linedata = [{
"x": d.source.x0 + parseInt(_rectW / 2),
"y": d.source.y0 + _rectH + 2
}, {
"x": d.source.x0 + parseInt(_rectW / 2),
"y": d.source.y0 + _rectH + 2
}, {
"x": d.source.x0 + parseInt(_rectW / 2),
"y": d.source.y0 + _rectH + 2
}, {
"x": d.source.x0 + parseInt(_rectW / 2),
"y": d.source.y0 + _rectH + 2
}];
return u_linedata;
})(d);
return _lineFunction(u_line);
});
// Transition links to their new position.
link.transition()
.duration(_duration)
.attr("d", function(d) {
var u_line = (function(d) {
var u_linedata = [{
"x": d.source.x + parseInt(_rectW / 2),
"y": d.source.y + _rectH
}, {
"x": d.source.x + parseInt(_rectW / 2),
"y": d.target.y - _margin.top / 2
}, {
"x": d.target.x + parseInt(_rectW / 2),
"y": d.target.y - _margin.top / 2
}, {
"x": d.target.x + parseInt(_rectW / 2),
"y": d.target.y
}];
return u_linedata;
})(d);
return _lineFunction(u_line);
});
// Transition exiting nodes to the parent's new position.
link.exit().transition()
.duration(_duration)
.attr("d", function(d) {
/* This is needed to draw the lines right back to the caller */
var u_line = (function(d) {
var u_linedata = [{
"x": _callerNode.x + parseInt(_rectW / 2),
"y": _callerNode.y + _rectH + 2
}, {
"x": _callerNode.x + parseInt(_rectW / 2),
"y": _callerNode.y + _rectH + 2
}, {
"x": _callerNode.x + parseInt(_rectW / 2),
"y": _callerNode.y + _rectH + 2
}, {
"x": _callerNode.x + parseInt(_rectW / 2),
"y": _callerNode.y + _rectH + 2
}];
return u_linedata;
})(d);
return _lineFunction(u_line);
}).each("end", function() {
_callerNode = null; /* After transition clear the caller node variable */
});
} else if (_mode === "diagonal") {
// Enter any new links at the parent's previous position.
link.enter().insert("path", "g")
.attr("class", "link")
.attr("x", _rectW / 2)
.attr("y", _rectH / 2)
.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.
nodeclick = function(d) {
selNode = d;
d3.selectAll("#_" + _canvasObj.nm + " rect.box").style("fill", function(d) {
return d.fill
});
d3.select(this.childNodes[1]).style("fill", "red");
oldSelNode = this.childNodes[1];
nodeID = d.id;
if (d.parent && d.parent.id) {
nodePID = d.parent.id
} else {
nodePID = -1
};
ajaxRequest(_canvasObj, "_nodeclick", ["nodeID=" + nodeID, "nodePID=" + nodePID, "nodeText=" + d.desc], false);
pn = d.parent;
dd = function(d) {
Ext.get(pn.id).dom.style.fill = "yellow";
}
while (pn) {
dd(pn);
pn = pn.parent;
}
},
// contextmenu
contextmenuclick = function(d) {
selNode = d;
d3.selectAll("#_" + _canvasObj.nm + " rect.box").style("fill", function(d) {
return d.fill
});
function getScreenCoords(x, y, ctm) {
var xn = ctm.e + x * ctm.a + y * ctm.c;
var yn = ctm.f + x * ctm.b + y * ctm.d;
return {
x: xn,
y: yn
};
}
var _node = this,
cx = +_node.getAttribute('cx'),
cy = +_node.getAttribute('cy'),
ctm = _node.getCTM(),
coords = getScreenCoords(cx, cy, ctm);
_x = parseInt(coords.x) + parseInt(_canvasObj.x) + parseInt(_rectW + 5),
_y = parseInt(coords.y) + parseInt(_rectH + 25);
d3.select(this.childNodes[1]).style("fill", "red");
oldSelNode = this.childNodes[1];
nodeID = d.id;
if (d.parent && d.parent.id) {
nodePID = d.parent.id
} else {
nodePID = -1
};
ajaxRequest(_canvasObj, "_contextmenu", ["x=" + _x, "y=" + _y, "nodeID=" + nodeID, "nodePID=" + nodePID, "nodeText=" + d.desc], false);
pn = d.parent;
dd = function(d) {
Ext.get(pn.id).dom.style.fill = "yellow";
}
while (pn) {
dd(pn);
pn = pn.parent;
}
},
// delete Node
deleteNode = function(event) {
if (selNode.parent) {
var tSelNode = selNode;
selNode = selNode.parent;
addRemoveNode(event, -1);
selNode = tSelNode;
simulateClick(selNode.parent);
}
}
// rename Node
renameNode = function(newValue) {
d = selNode;
nodeID = d.id;
d.desc = newValue;
d3.selectAll("#_" + _canvasObj.nm + " .node")[0].forEach(function(n) {
if (n.childNodes[1].id == nodeID) n.childNodes[2].textContent = newValue
})
}
// add Node
addNode = function(event, id) {
if (selNode.parent) {
var tSelNode = selNode;
selNode = selNode.parent;
addRemoveNode(event, id);
selNode = tSelNode;
}
}
// simulateClick
function simulateClick(elem, id) {
var evt = document.createEvent("MouseEvents");
evt.initMouseEvent(
"click", /* type */
true, /* canBubble */
true, /* cancelable */
window, /* view */
0, /* detail */
0, /* screenX */
0, /* screenY */
0, /* clientX */
0, /* clientY */
false, /* ctrlKey */
false, /* altKey */
false, /* shiftKey */
false, /* metaKey */
0, /* button */
null); /* relatedTarget */
if (elem != null) {
d3.selectAll("#_" + _canvasObj.nm + " .node")[0].forEach(function(n) {
if (n.childNodes[1].id == elem.id) n.dispatchEvent(evt)
});
} else {
d3.selectAll("#_" + _canvasObj.nm + " .node")[0].forEach(function(n) {
if (n.childNodes[1].id == id) n.dispatchEvent(evt)
})
}
}
// add Child Node
addRemoveNode = function(event, id) {
var xhrPopup = new XMLHttpRequest();
var formDataEmpty = new FormData();
d = selNode;
nodeID = d.id;
xhrPopup.open("POST", "/HandleEvent?Evt=" + event + "&IsEvent=1&Obj=" + _canvasObj.nm + "&" + _S_ID + "&nodeID=" + nodeID, false);
xhrPopup.onreadystatechange = function() {
if (xhrPopup.readyState == 4) {
var _res = xhrPopup.response;
var response = {
id: d.id,
desc: d.desc,
children: JSON.parse(_res)
};
response.children.forEach(function(child) {
if (!_tree.nodes(d)[0]._children) {
_tree.nodes(d)[0]._children = [];
}
child.x = d.x;
child.y = d.y;
child.x0 = d.x0;
child.y0 = d.y0;
_tree.nodes(d)[0]._children.push(child);
});
_callerNode = null;
_callerMode = 1; // Expand
d.children = d._children;
d._children = null;
update(d);
if (id != -1) {
simulateClick(null, id)
}
}
};
xhrPopup.send(formDataEmpty);
},
//refill
refill = function(v) {
selNode.fill = v;
},
//Redraw for zoom
redraw = function() {
_svg.attr("transform", "translate(" + d3.event.translate + ")" +
" scale(" + d3.event.scale.toFixed(_scale) + ")");
},
initTree = function(options) {
//oldSelColor = null;
//oldSelNode = null;
oldSelID = -1;
var u_opts = $.extend({
id: "",
data: {},
modus: "line",
canvasObj: null,
rectW: 250,
rectH: 50,
loadFunc: function() {}
},
options),
id = u_opts.id;
_loadFunction = u_opts.loadFunc;
_mode = u_opts.modus;
_root = u_opts.data;
_rectW = u_opts.rectW;
_rectH = u_opts.rectH;
_fixedDepth = _rectH * 2.5;
_canvasObj = u_opts.canvasObj;
/*if(_mode == "line") {
_fixedDepth = 80;
} else {
_fixedDepth = 110;
}*/
$(id).html(""); // Reset
var width = $(id).innerWidth() - _margin.left - _margin.right,
height = $(id).innerHeight() - _margin.top - _margin.bottom;
_tree = d3.layout.tree().nodeSize([_rectW + _rectSpacing, _rectH + _rectSpacing]);
/* Basic Setup for the diagonal function. _mode = "diagonal" */
_diagonal = d3.svg.diagonal()
.projection(function(d) {
return [d.x + _rectW / 2, d.y + _rectH / 2];
});
/* Basic setup for the line function. _mode = "line" */
_lineFunction = d3.svg.line()
.x(function(d) {
return d.x;
})
.y(function(d) {
return d.y;
})
.interpolate("linear");
var u_childwidth = parseInt((_root.children.length * _rectW) / 2);
_svgroot = d3.select(id).append("svg").attr("width", width).attr("height", height).attr("id", "_" + _canvasObj.nm) //.attr("viewBox", "0,0,150,420")
//.call(zm = d3.behavior.zoom().scaleExtent([0.15,3]).on("zoom", null));
.call(zm = d3.behavior.zoom().scaleExtent([0.15, 3]).on("zoom", redraw)).on('dblclick.zoom', null);;
_svg = _svgroot.append("g")
.attr("transform", "translate(" + parseInt(u_childwidth + ((width - u_childwidth * 2) / 2) - _margin.left / 2) + "," + 20 + ")");
var u_stops = [{
offset: "0%",
color: "#03A9F4",
opacity: 1
}, {
offset: "100%",
color: "#0288D1",
opacity: 1
}];
defLinearGradient("gradientnochilds", "0%", "0%", "0%", "100%", u_stops);
var u_stops = [{
offset: "0%",
color: "#8BC34A",
opacity: 1
}, {
offset: "100%",
color: "#689F38",
opacity: 1
}];
defLinearGradient("gradientchilds", "0%", "0%", "0%", "100%", u_stops);
defBoxShadow("boxShadow");
//necessary so that zoom knows where to zoom and unzoom from
zm.translate([parseInt(u_childwidth + ((width - u_childwidth * 2) / 2) - _margin.left / 2), 20]);
//zm.translate([0, 10]);
_root.x0 = 0; // the root is already centered
_root.y0 = 0; // draw & animate from center
_root.children.forEach(collapse);
update(_root);
d3.select(id).style("height", height + _margin.top + _margin.bottom);
};
return {
initTree: initTree,
addRemoveNode: addRemoveNode,
renameNode: renameNode,
addNode: addNode,
deleteNode: deleteNode,
refill: refill
};
})();
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Titre</title>
<script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
<script src="d3.v3.min.js"></script>
<script src="orgChart.js"></script>
<script type="text/javascript">
var canv = document.createElement('canvas');
canv.id = "CursorLayer";
canv.width = 1224;
canv.height = 768;
canv.style.zIndex = 8;
canv.style.position = "absolute";
canv.style.border = "1px solid";
var _jsonstr = JSON.parse("{\"id\":\"11\", \"desc\":\"5\", \"children\":[{\"id\":\"12\", \"desc\":\"5.1\"},{\"id\":\"13\", \"desc\":\"5.2\"},{\"id\":\"14\", \"desc\":\"5.3\", \"children\":[{\"id\":\"15\", \"desc\":\"5.3.1\"},{\"id\":\"16\", \"desc\":\"5.3.2\", \"children\":[{\"id\":\"17\", \"desc\":\"5.3.2.1\"},{\"id\":\"18\", \"desc\":\"5.3.2.2\"}]},{\"id\":\"19\", \"desc\":\"5.3.3\"}]},{\"id\":\"20\", \"desc\":\"5.4\", \"children\":[{\"id\":\"21\", \"desc\":\"5.4.1\"},{\"id\":\"22\", \"desc\":\"5.4.2\", \"children\":[{\"id\":\"23\", \"desc\":\"5.4.2.1\"},{\"id\":\"24\", \"desc\":\"5.4.2.2\", \"children\":[{\"id\":\"25\", \"desc\":\"5.4.2.2.1\"},{\"id\":\"26\", \"desc\":\"5.4.2.2.2\", \"children\":[{\"id\":\"27\", \"desc\":\"5.4.2.2.2.1\"}]}]},{\"id\":\"28\", \"desc\":\"5.4.2.3\"},{\"id\":\"29\", \"desc\":\"5.4.2.4\"}]},{\"id\":\"30\", \"desc\":\"5.4.3\"}]}]}");
orgChart.initTree({
id: "#O13_id-innerCt",
data: _jsonstr,
modus: "diagonal",
canvasObj: canv,
loadFunc: function() {}
});
</script>
</head>
<body>
</body>
</html>
Here are first steps to get something to show up (all changes are inside the last <script> tag):
1) Despite the name, it looks like canvasObj is expected to be some container element for <svg>. So, replace
var canv = document.createElement('canvas');
with
var canv = document.createElement('div');
2) By looking at the following line: d3.selectAll("#_" + _canvasObj.nm + " rect.box") we can infer, that canvas id must start with _ and .nm attribute must be equal to that id without _, so you can declare it like this:
canv.id = '_CursorLayer';
canv.nm = 'CursorLayer';
You also have to replace id: "#O13_id-innerCt", with id: "#_CursorLayer"
3) SVG size is taken from canv size. Since div does not has width and height attributes, you should specify them by css:
canv.style.width = '1224px';
canv.style.height = '768px';
4) We need to append canv to body to see it:
document.body.appendChild(canv);
In order for document.body to be available during the last <script> execution, I suggest to move the last <script> tag inside <body>.

Get visible root node in zoomable sunburst

I made a zoomable sunburst visualisation with labels (see in action, or check out the code). When clicking on an item, the innermost visible node has its label turned sideways. I'd like to fix the label just for this one node, but I haven't found a way to do this.
Is there a way to say "if (current node is the root of visible nodes)"? Any other ideas?
The full visualization:
Zoomed in after click. I'd like to make the 'Calm' node text horizontal:
While not perfect, this modified version of the code you were using adjusts the text of the currently selected node as it animates and makes it horizontal.
var width = 960,
height = 700,
radius = (Math.min(width, height) / 2) - 10;
var formatNumber = d3.format(",d");
var x = d3.scaleLinear()
.range([0, 2 * Math.PI]);
var y = d3.scaleLinear()
.range([0, radius]);
var color = d3.scaleOrdinal(d3.schemeCategory20);
var partition = d3.partition();
function startAngle(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x0))); }
function endAngle(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x1))); }
function innerRadius(d) { return Math.max(0, y(d.y0)); }
function outerRadius(d) { return Math.max(0, y(d.y1)); }
var arc = d3.arc()
.startAngle( function(d) { return startAngle(d); })
.endAngle( function(d) { return endAngle(d); })
.innerRadius(function(d) { return innerRadius(d); })
.outerRadius(function(d) { return outerRadius(d); })
var texttransform = function(d) {
var translation = y(d.y0);
var rotation = computeTextRotation(d);
if (rotation > 90 && rotation < 270) {
rotation = rotation + 180;
translation = -translation;
}
return (
"rotate(" + rotation + ")" +
"translate(" + translation + ",0)"
);
}
var transition = {};
function calcTransitionPercentage(){
var now = Date.now()-transition.clockNow;
if(!transition.delay || now > transition.delay){
return Math.min(1,(now-(transition.delay||0))/transition.duration);
}
return 0;
}
function computeTextRotation(d) {
if (d.depth === 0) {
return 0;
}
var current = x((d.x0 + d.x1)/2);
var angle = (current - Math.PI / 2) / Math.PI * 180;
if(transition.node === d){
angle -= 90 * calcTransitionPercentage();
}
return (angle > 90 || angle < 270) ? angle : 180 + angle ;
}
var textanchor = function(d) {
if (d.depth === 0) {
return "middle";
}
var rotation = computeTextRotation(d);
return (rotation > 90 && rotation < 270) ? "end" : "start";
}
var textdx = function(d) {
if (d.depth === 0) {
return 0;
}
var rotation = computeTextRotation(d);
return (rotation > 90 && rotation < 270) ? -6 : 6;
}
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + (height / 2) + ")");
function calcFontSize(d) {
const xFactor = 12, yFactor = 7.5 ; // stub
if (d.depth === 0) {
return "30px";
}
// use inner arc len as text height delimiter
var innerArc = (endAngle(d) - startAngle(d)) * 2 * Math.PI * innerRadius(d);
var len = (d.y1-d.y0) * radius;
return Math.min(innerArc / yFactor, len / d.data.textlen * xFactor) + "px";
}
function click(d = { x0: 0, x1: 1, y0: 0, y1: 1 }) {
transition = {clockNow: Date.now(), duration: 750, node: d }
var trans = svg.transition().duration(750);
trans.selectAll("path")
.attrTween("d", function(n) { return function() { return arc(n); }; })
.tween("scale", function() {
var xd = d3.interpolate(x.domain(), [d.x0, d.x1]),
yd = d3.interpolate(y.domain(), [d.y0, 1]),
yr = d3.interpolate(y.range(), [d.y0 ? 20 : 0, radius]);
return function(t) {
x.domain(xd(t));
y.domain(yd(t)).range(yr(t));
};
});
trans.selectAll("text")
.attrTween("transform", function(n) { return function() { return texttransform(n); }; })
.attrTween("text-anchor", function(n) { return function() { return textanchor(n); }; })
.attrTween("dx", function(n) { return function() { return textdx(n); }; })
.styleTween("font-size", function(n) { return function() { return calcFontSize(n); }; });
trans.selectAll("text")
.delay(400)
.attrTween("opacity", function(n) { return function() {
if (d === n || n.ancestors().includes(d)) {
return 1;
} else {
return 0;
}
}; });
}
d3.text('https://raw.githubusercontent.com/manooh/NVSee/master/data/feelings_EN.txt', function(error, raw){
if (error) throw error;
// replace two-space indentation with pipes
raw = raw.replace(new RegExp(' ', 'g'), '|');
//read pipe-delimited data
var dsv = d3.dsvFormat('|');
var flatData = dsv.parse(raw);
var rData = currentNode = tree(flatData);
rData = d3.hierarchy(rData);
var nodes = partition(rData
.sum(function(d) { return 1; }) // each leaf gets a size of 1
.sort(function(a, b) { d3.ascending(a.name, b.name) }) // not working?
)
.descendants();
g = svg.selectAll("path")
.data(nodes)
.enter().append("g");
path = g.append("path")
.attr("d", arc)
.style("fill", function(d, i) {
var c;
if (d.depth === 0) {
return "white";
} else if (d.depth === 1) {
c = color((d.children ? d : d.parent).data.name);
} else if (d.depth > 1) {
c = d3.color(d.parent.data.color).darker();
}
d.data.color = c;
return c;
})
.on("click", click)
.append("title")
.text(function(d) { return d.data.name });
text = g.append("text")
.style("fill", function(d) {
if (d.depth === 0) {
return "#CCC";
} else {
return "#FFF";
}})
.attr("class", "svglabel")
.attr("transform", texttransform)
.attr("text-anchor", textanchor)
.attr("dx", textdx)
.attr("dy", ".35em") // vertical-align
.text(function(d) { return d.data.name; })
.style("font-size", function(d) {
// hack. save text len as property to make accessible in transiton
d.data.textlen = this.getComputedTextLength();
return calcFontSize(d);
});
});
function tree(nodes) {
var curr, parent, root;
var lev = 1;
nodes.forEach(function(d) {
if (!root) {
// handle root (first node)
curr = {
name: d.d1,
children: []
};
root = curr;
parent = curr;
} else {
if (d['d' + (lev+1)]) {
// handle children
lev = lev+1;
parent = curr;
} else if (d['d' + (lev-1)]) {
// handle moving up the hierarchy
lev = lev-1;
parent = parent.parent;
} else if (!d['d' + lev]) {
// if it's neither child, nor moving up, nor a sibling, handle exception
throw "unhandled tree level";
}
curr = {
name: d['d' + lev],
children: []
};
curr.parent = parent;
parent.children.push(curr);
}
});
return root;
}
.svglabel {
font-family: sans-serif;
pointer-events: none;
}
body {
-webkit-touch-callout: none; /* iOS Safari */
-webkit-user-select: none; /* Safari */
-khtml-user-select: none; /* Konqueror HTML */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* Internet Explorer/Edge */
user-select: none; /* Non-prefixed version, currently
supported by Chrome and Opera */
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.js"></script>

d3 sankey diagram - how to set the y position

I am trying to set the position of nodes (by name) either on the top or the bottom of the Sankey Diagram. For example, if I had a node named "New" and another node named "Dropped", and I wanted to keep the New node at the top of the diagram always, and Dropped nodes at the bottom, how would I accomplish this?
I am looking for something similar to this jsFiddle for setting the x axis position, but for the y position:
//////////////////////// sankey.js /////////////////////////
d3.sankey = function() {
var sankey = {},
nodeWidth = 24,
nodePadding = 8,
size = [1, 1],
nodes = [],
links = [];
sankey.nodeWidth = function(_) {
if (!arguments.length) return nodeWidth;
nodeWidth = +_;
return sankey;
};
sankey.nodePadding = function(_) {
if (!arguments.length) return nodePadding;
nodePadding = +_;
return sankey;
};
sankey.nodes = function(_) {
if (!arguments.length) return nodes;
nodes = _;
return sankey;
};
sankey.links = function(_) {
if (!arguments.length) return links;
links = _;
return sankey;
};
sankey.size = function(_) {
if (!arguments.length) return size;
size = _;
return sankey;
};
sankey.layout = function(iterations) {
computeNodeLinks();
computeNodeValues();
computeNodeBreadths();
computeNodeDepths(iterations);
computeLinkDepths();
return sankey;
};
sankey.relayout = function() {
computeLinkDepths();
return sankey;
};
sankey.link = function() {
var curvature = .5;
function link(d) {
var x0 = d.source.x + d.source.dx,
x1 = d.target.x,
xi = d3.interpolateNumber(x0, x1),
x2 = xi(curvature),
x3 = xi(1 - curvature),
y0 = d.source.y + d.sy + d.dy / 2,
y1 = d.target.y + d.ty + d.dy / 2;
return "M" + x0 + "," + y0
+ "C" + x2 + "," + y0
+ " " + x3 + "," + y1
+ " " + x1 + "," + y1;
}
link.curvature = function(_) {
if (!arguments.length) return curvature;
curvature = +_;
return link;
};
return link;
};
// Populate the sourceLinks and targetLinks for each node.
// Also, if the source and target are not objects, assume they are indices.
function computeNodeLinks() {
nodes.forEach(function(node) {
node.sourceLinks = [];
node.targetLinks = [];
});
links.forEach(function(link) {
var source = link.source,
target = link.target;
if (typeof source === "number") source = link.source = nodes[link.source];
if (typeof target === "number") target = link.target = nodes[link.target];
source.sourceLinks.push(link);
target.targetLinks.push(link);
});
}
// Compute the value (size) of each node by summing the associated links.
function computeNodeValues() {
nodes.forEach(function(node) {
node.value = Math.max(
d3.sum(node.sourceLinks, value),
d3.sum(node.targetLinks, value)
);
});
}
// Iteratively assign the breadth (x-position) for each node.
// Nodes are assigned the maximum breadth of incoming neighbors plus one;
// nodes with no incoming links are assigned breadth zero, while
// nodes with no outgoing links are assigned the maximum breadth.
function computeNodeBreadths() {
var remainingNodes = nodes,
nextNodes,
x = 0;
while (remainingNodes.length) {
nextNodes = [];
remainingNodes.forEach(function(node) {
if (node.xPos)
node.x = node.xPos;
else
node.x = x;
node.dx = nodeWidth;
node.sourceLinks.forEach(function(link) {
nextNodes.push(link.target);
});
});
remainingNodes = nextNodes;
++x;
}
//
moveSinksRight(x);
scaleNodeBreadths((width - nodeWidth) / (x - 1));
}
function moveSourcesRight() {
nodes.forEach(function(node) {
if (!node.targetLinks.length) {
node.x = d3.min(node.sourceLinks, function(d) { return d.target.x; }) - 1;
}
});
}
function moveSinksRight(x) {
nodes.forEach(function(node) {
if (!node.sourceLinks.length) {
node.x = x - 1;
}
});
}
function scaleNodeBreadths(kx) {
nodes.forEach(function(node) {
node.x *= kx;
});
}
function computeNodeDepths(iterations) {
var nodesByBreadth = d3.nest()
.key(function(d) { return d.x; })
.sortKeys(d3.ascending)
.entries(nodes)
.map(function(d) { return d.values; });
//
initializeNodeDepth();
resolveCollisions();
for (var alpha = 1; iterations > 0; --iterations) {
relaxRightToLeft(alpha *= .99);
resolveCollisions();
relaxLeftToRight(alpha);
resolveCollisions();
}
function initializeNodeDepth() {
var ky = d3.min(nodesByBreadth, function(nodes) {
return (size[1] - (nodes.length - 1) * nodePadding) / d3.sum(nodes, value);
});
nodesByBreadth.forEach(function(nodes) {
nodes.forEach(function(node, i) {
node.y = i;
node.dy = node.value * ky;
});
});
links.forEach(function(link) {
link.dy = link.value * ky;
});
}
function relaxLeftToRight(alpha) {
nodesByBreadth.forEach(function(nodes, breadth) {
nodes.forEach(function(node) {
if (node.targetLinks.length) {
var y = d3.sum(node.targetLinks, weightedSource) / d3.sum(node.targetLinks, value);
node.y += (y - center(node)) * alpha;
}
});
});
function weightedSource(link) {
return center(link.source) * link.value;
}
}
function relaxRightToLeft(alpha) {
nodesByBreadth.slice().reverse().forEach(function(nodes) {
nodes.forEach(function(node) {
if (node.sourceLinks.length) {
var y = d3.sum(node.sourceLinks, weightedTarget) / d3.sum(node.sourceLinks, value);
node.y += (y - center(node)) * alpha;
}
});
});
function weightedTarget(link) {
return center(link.target) * link.value;
}
}
function resolveCollisions() {
nodesByBreadth.forEach(function(nodes) {
var node,
dy,
y0 = 0,
n = nodes.length,
i;
// Push any overlapping nodes down.
nodes.sort(ascendingDepth);
for (i = 0; i < n; ++i) {
node = nodes[i];
dy = y0 - node.y;
if (dy > 0) node.y += dy;
y0 = node.y + node.dy + nodePadding;
}
// If the bottommost node goes outside the bounds, push it back up.
dy = y0 - nodePadding - size[1];
if (dy > 0) {
y0 = node.y -= dy;
// Push any overlapping nodes back up.
for (i = n - 2; i >= 0; --i) {
node = nodes[i];
dy = node.y + node.dy + nodePadding - y0;
if (dy > 0) node.y -= dy;
y0 = node.y;
}
}
});
}
function ascendingDepth(a, b) {
return a.y - b.y;
}
}
function computeLinkDepths() {
nodes.forEach(function(node) {
node.sourceLinks.sort(ascendingTargetDepth);
node.targetLinks.sort(ascendingSourceDepth);
});
nodes.forEach(function(node) {
var sy = 0, ty = 0;
node.sourceLinks.forEach(function(link) {
link.sy = sy;
sy += link.dy;
});
node.targetLinks.forEach(function(link) {
link.ty = ty;
ty += link.dy;
});
});
function ascendingSourceDepth(a, b) {
return a.source.y - b.source.y;
}
function ascendingTargetDepth(a, b) {
return a.target.y - b.target.y;
}
}
function center(node) {
return node.y + node.dy / 2;
}
function value(link) {
return link.value;
}
return sankey;
};
///////////////////////////////////////////
function getData() {
return {
"nodes": [{
"node": 0,
"name": "node0"
}, {
"node": 1,
"name": "node1"
}, {
"node": 2,
"name": "node2",
"xPos": 1
}, {
"node": 3,
"name": "node3"
}],
"links": [{
"source": 0,
"target": 1,
"value": 5
}, {
"source": 1,
"target": 3,
"value": 2
}, {
"source": 2,
"target": 3,
"value": 3
}]};
}
var margin = {top: 1, right: 1, bottom: 6, left: 1},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var formatNumber = d3.format(",.0f"),
format = function(d) { return formatNumber(d) + " TWh"; },
color = d3.scale.category20();
var svg = d3.select("#chart").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var sankey = d3.sankey()
.nodeWidth(15)
.nodePadding(10)
.size([width, height]);
var path = sankey.link();
var renderSankey = function(energy) {
window.width = 500;
sankey
.nodes(energy.nodes)
.links(energy.links)
.layout(32);
var link = svg.append("g").selectAll(".link")
.data(energy.links)
.enter().append("path")
.attr("class", "link")
.attr("d", path)
.style("stroke-width", function(d) { return Math.max(1, d.dy); })
.sort(function(a, b) { return b.dy - a.dy; });
link.append("title")
.text(function(d) { return d.source.name + " → " + d.target.name + "\n" + format(d.value); });
var node = svg.append("g").selectAll(".node")
.data(energy.nodes)
.enter().append("g")
.attr("class", "node")
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })
.call(d3.behavior.drag()
.origin(function(d) { return d; })
.on("dragstart", function() { this.parentNode.appendChild(this); })
.on("drag", dragmove));
node.append("rect")
.attr("height", function(d) { return d.dy; })
.attr("width", sankey.nodeWidth())
.style("fill", function(d) { return d.color = color(d.name.replace(/ .*/, "")); })
.style("stroke", function(d) { return d3.rgb(d.color).darker(2); })
.append("title")
.text(function(d) { return d.name + "\n" + format(d.value); });
node.append("text")
.attr("x", -6)
.attr("y", function(d) { return d.dy / 2; })
.attr("dy", ".35em")
.attr("text-anchor", "end")
.attr("transform", null)
.text(function(d) { return d.name; })
.filter(function(d) { return d.x < width / 2; })
.attr("x", 6 + sankey.nodeWidth())
.attr("text-anchor", "start");
function dragmove(d) {
d3.select(this).attr("transform", "translate(" + d.x + "," + (d.y = Math.max(0, Math.min(height - d.dy, d3.event.y))) + ")");
sankey.relayout();
link.attr("d", path);
}
}
renderSankey(getData());
Try to change the target sort. Instead of function ascendingTargetDepth(a, b) change it to function ascendingTargetDepth(b, a). IE sort by name in descending order.
function ascendingTargetDepth(b, a) {
return a.target.y - b.target.y;
}

d3.js dougnut pie chart legend toggling

I'm developing a legend toggling d3.js pie chart application using this jsfiddle as my latest version http://jsfiddle.net/Qh9X5/3328/ .
I am aiming to get a streamlined working example where the legend can toggle the slices, trying to deactivate all slices - resorts in a reset which reactivates all the slices. Splitting up presentation and application layer logic.
Tweening needs improvement too - as the slices pop into existence then re-tween smoothly.
How do I improve/fix the various bugs in this code base?
onLegendClick: function(dt, i){
//_toggle rectangle in legend
var completeData = jQuery.extend(true, [], methods.currentDataSet);
newDataSet = completeData;
if(methods.manipulatedData){
newDataSet = methods.manipulatedData;
}
d3.selectAll('rect')
.data([dt], function(d) {
return d.data.label;
})
.style("fill-opacity", function(d, j) {
var isActive = Math.abs(1-d3.select(this).style("fill-opacity"));
if(isActive){
newDataSet[j].total = completeData[j].total;
}else{
newDataSet[j].total = 0;
}
return isActive;
});
//animate slices
methods.animateSlices(newDataSet);
//stash manipulated data
methods.manipulatedData = newDataSet;
}
Here is the entire js code - I've used the tidyup. I wasn't sure about using the shortcuts as I'm not sure the values will be correct. The latest fiddle - http://jsfiddle.net/Qh9X5/3340/
$(document).ready(function () {
var pieChart = {
el: "",
init: function (el, options) {
var clone = jQuery.extend(true, {}, options["data"]);
pieChart.el = el;
pieChart.setup(clone, options["width"], options["height"], options["r"], options["ir"]);
},
getArc: function (radius, innerradius) {
var arc = d3.svg.arc()
.innerRadius(innerradius)
.outerRadius(radius);
return arc;
},
setup: function (dataset, w, h, r, ir) {
var padding = 80;
this.width = w;
this.height = h;
this.radius = r
this.innerradius = ir;
this.color = d3.scale.category20();
this.pie = d3.layout.pie()
.sort(null)
.value(function (d) {
return d.total;
});
this.arc = this.getArc(this.radius, this.innerradius);
this.svg = d3.select(pieChart.el["selector"]).append("svg")
.attr("width", this.width + padding)
.attr("height", this.height + padding);
this.holder = this.svg.append("g")
.attr("transform", "translate(" + ((this.width / 2) + (padding / 2)) + "," + ((this.height / 2) + (padding / 2)) + ")");
this.piec = this.holder.append("g")
.attr("class", "piechart");
this.segments = this.holder.append("g")
.attr("class", "segments");
this.labels = this.holder.append("g")
.attr("class", "labels");
this.pointers = this.holder.append("g")
.attr("class", "pointers");
this.legend = this.svg.append("g")
.attr("class", "legend")
.attr("transform", "translate(" + -(this.width / 4) + "," + this.height + ")");
},
oldPieData: "",
pieTween: function (r, ir, d, i) {
var that = this;
var theOldDataInPie = pieChart.oldPieData;
// Interpolate the arcs in data space
var s0;
var e0;
if (theOldDataInPie[i]) {
s0 = theOldDataInPie[i].startAngle;
e0 = theOldDataInPie[i].endAngle;
} else if (!(theOldDataInPie[i]) && theOldDataInPie[i - 1]) {
s0 = theOldDataInPie[i - 1].endAngle;
e0 = theOldDataInPie[i - 1].endAngle;
} else if (!(theOldDataInPie[i - 1]) && theOldDataInPie.length > 0) {
s0 = theOldDataInPie[theOldDataInPie.length - 1].endAngle;
e0 = theOldDataInPie[theOldDataInPie.length - 1].endAngle;
} else {
s0 = 0;
e0 = 0;
}
var i = d3.interpolate({
startAngle: s0,
endAngle: e0
}, {
startAngle: d.startAngle,
endAngle: d.endAngle
});
return function (t) {
var b = i(t);
return pieChart.getArc(r, ir)(b);
};
},
removePieTween: function (r, ir, d, i) {
var that = this;
s0 = 2 * Math.PI;
e0 = 2 * Math.PI;
var i = d3.interpolate({
startAngle: d.startAngle,
endAngle: d.endAngle
}, {
startAngle: s0,
endAngle: e0
});
return function (t) {
var b = i(t);
return pieChart.getArc(r, ir)(b);
};
},
animateSlices: function (dataSet) {
var r = $(pieChart.el["selector"]).data("r");
var ir = $(pieChart.el["selector"]).data("ir");
this.piedata = pieChart.pie(dataSet);
//__slices
this.path = pieChart.segments.selectAll("path.pie")
.data(this.piedata, function (d) {
return d.data.label
});
this.path.enter().append("path")
.attr("class", "pie")
.attr("fill", function (d, i) {
return pieChart.color(i);
})
.attr("stroke", "#ffffff")
.transition()
.duration(300)
.attrTween("d", function (d, i) {
return pieChart.pieTween(r, ir, d, i);
});
this.path.transition()
.duration(300)
.attrTween("d", function (d, i) {
return pieChart.pieTween(r, ir, d, i);
});
this.path.exit()
.transition()
.duration(300)
.attrTween("d", function (d, i) {
return pieChart.removePieTween(r, ir, d, i);
})
.remove();
//__slices
//__labels
var labels = pieChart.labels.selectAll("text")
.data(this.piedata, function (d) {
return d.data.label
});
labels.enter()
.append("text")
.attr("text-anchor", "middle")
labels.attr("x", function (d) {
var a = d.startAngle + (d.endAngle - d.startAngle) / 2 - Math.PI / 2;
d.cx = Math.cos(a) * (ir + ((r - ir) / 2));
return d.x = Math.cos(a) * (r + 20);
})
.attr("y", function (d) {
var a = d.startAngle + (d.endAngle - d.startAngle) / 2 - Math.PI / 2;
d.cy = Math.sin(a) * (ir + ((r - ir) / 2));
return d.y = Math.sin(a) * (r + 20);
})
.attr("opacity", function (d) {
var opacityLevel = 1;
if (d.value == 0) {
opacityLevel = 0;
}
return opacityLevel;
})
.text(function (d) {
return d.data.label;
})
.each(function (d) {
var bbox = this.getBBox();
d.sx = d.x - bbox.width / 2 - 2;
d.ox = d.x + bbox.width / 2 + 2;
d.sy = d.oy = d.y + 5;
})
.transition()
.duration(300)
labels.transition()
.duration(300)
labels.exit().remove();
//__labels
//__pointers
pieChart.pointers.append("defs").append("marker")
.attr("id", "circ")
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("refX", 3)
.attr("refY", 3)
.append("circle")
.attr("cx", 3)
.attr("cy", 3)
.attr("r", 3);
var pointers = pieChart.pointers.selectAll("path.pointer")
.data(this.piedata, function (d) {
return d.data.label
});
pointers.enter()
.append("path")
.attr("class", "pointer")
.style("fill", "none")
.style("stroke", "black")
.attr("marker-end", "url(#circ)");
pointers.attr("d", function (d) {
if (d.cx > d.ox) {
return "M" + d.sx + "," + d.sy + "L" + d.ox + "," + d.oy + " " + d.cx + "," + d.cy;
} else {
return "M" + d.ox + "," + d.oy + "L" + d.sx + "," + d.sy + " " + d.cx + "," + d.cy;
}
})
.attr("opacity", function (d) {
var opacityLevel = 1;
if (d.value == 0) {
opacityLevel = 0;
}
return opacityLevel;
})
.transition()
.duration(300)
pointers.transition()
.duration(300)
pointers.exit().remove();
},
onToggle: function (sliceData, index) {
//_toggle rectangle in legend
//_toggle slice
var completeData = jQuery.extend(true, [], pieChart.currentDataSet);
var dataLength = completeData.length;
var newDataSet = completeData;
if (pieChart.manipulatedData) {
newDataSet = pieChart.manipulatedData;
}
d3.selectAll('rect')
.data([sliceData], function (d) {
return d.data.label;
})
.style("fill-opacity", function (d) {
var isActive = Math.abs(1 - d3.select(this).style("fill-opacity"));
if (isActive) {
newDataSet[index].total = completeData[index].total;
newDataSet[index].value = completeData[index].value;
} else {
newDataSet[index].total = 0;
newDataSet[index].value = 0;
}
return isActive;
});
//if all elements are to be not shown - reset to show all slices again.
//animate slices
pieChart.animateSlices(newDataSet);
//stash manipulated data
pieChart.manipulatedData = newDataSet;
},
update: function (el, dataSet) {
var that = this;
pieChart.el = el;
pieChart.svg = d3.select(pieChart.el["selector"] + " .piechart");
pieChart.segments = d3.select(pieChart.el["selector"] + " .segments");
pieChart.labels = d3.select(pieChart.el["selector"] + " .labels");
pieChart.pointers = d3.select(pieChart.el["selector"] + " .pointers");
pieChart.legend = d3.select(pieChart.el["selector"] + " .legend");
dataSet.forEach(function (d) {
d.total = +d.value;
});
pieChart.currentDataSet = dataSet;
pieChart.animateSlices(dataSet);
//__legends
var w = 200;
// add legend
var legend = pieChart.legend; //.append("g")
var legendRects = legend.selectAll('rect')
.data(this.piedata, function (d) {
return d.data.label
});
legendRects.enter()
.append("rect")
.attr("x", w - 65)
.attr("y", function (d, i) {
return i * 20;
})
.attr("width", 10)
.attr("height", 10)
.style("fill", function (d, i) {
return pieChart.color(i);
})
.style("stroke", function (d, i) {
return pieChart.color(i);
})
.on('click', function(d, i){
pieChart.onToggle(d, i);
})
.transition()
.duration(300)
legendRects.style("fill", function (d, i) {
return pieChart.color(i);
})
.style("stroke", function (d, i) {
return pieChart.color(i);
})
.transition()
.duration(300)
legendRects.exit().remove();
var legendText = legend.selectAll('text.label')
.data(this.piedata, function (d) {
return d.data.label
});
legendText.enter()
.append("text")
.attr("class", "label")
.attr("x", w - 52)
.attr("y", function (d, i) {
return i * 20 + 9;
})
.text(function (d) {
return d.data.label;
})
.transition()
.duration(300)
legendText.text(function (d) {
return d.data.label;
})
.transition()
.duration(300)
legendText.exit().remove();
var legendTextVals = legend.selectAll('text.vals')
.data(this.piedata, function (d) {
return d.data.label
});
legendTextVals.enter()
.append("text")
.attr("class", "vals")
.attr("x", w + 20)
.attr("y", function (d, i) {
return i * 20 + 9;
})
.text(function (d) {
return d.data.value;
})
.transition()
.duration(300)
legendTextVals.text(function (d) {
return d.data.value;
})
.transition()
.duration(300)
legendTextVals.exit().remove();
//__pointers
this.oldPieData = this.piedata;
}
};
var dataCharts = [{
"data": [{
"segments": [{
"label": "apple",
"value": 53245
}, {
"label": "cherry",
"value": 145
}, {
"label": "pear",
"value": 2245
}, {
"label": "bananana",
"value": 15325
}]
}]
}, {
"data": [{
"segments": [{
"label": "milk",
"value": 122
}, {
"label": "cheese",
"value": 44
}, {
"label": "grapes",
"value": 533
}]
}]
}, {
"data": [{
"segments": [{
"label": "pineapple",
"value": 1532
}, {
"label": "orange",
"value": 1435
}, {
"label": "grapes",
"value": 22
}]
}]
}, {
"data": [{
"segments": [{
"label": "lemons",
"value": 133
}, {
"label": "mango",
"value": 435
}, {
"label": "melon",
"value": 2122
}]
}]
}];
var clone = jQuery.extend(true, {}, dataCharts);
//__invoke concentric
$('[data-role="piechart"]').each(function (index) {
var selector = "piechart" + index;
$(this).attr("id", selector);
var options = {
data: clone[0].data,
width: $(this).data("width"),
height: $(this).data("height"),
r: $(this).data("r"),
ir: $(this).data("ir")
}
pieChart.init($("#" + selector), options);
pieChart.update($("#" + selector), clone[0].data[0].segments);
});
$(".testers a").on("click", function (e) {
e.preventDefault();
var clone = jQuery.extend(true, {}, dataCharts);
var min = 0;
var max = 3;
//__invoke pie chart
$('[data-role="piechart"]').each(function (index) {
pos = Math.floor(Math.random() * (max - min + 1)) + min;
pieChart.update($("#" + $(this).attr("id")), clone[pos].data[0].segments);
});
});
});

Categories