Guys I am following this example problem I am facing when I added zoom functionality this code not drag and creating edges between nodes. The problem which I am thinking could be 'g' appended with SVG for zooming.
// set up SVG for D3
var width = 960,
height = 500,
colors = d3.scale.category10();
var svg = d3.select('body')
.append('svg')
.attr('oncontextmenu', 'return false;')
.attr('width', width)
.attr('height', height)
.attr("pointer-events", "all")
.call(d3.behavior.zoom().on("zoom",zoomed)).on("dblclick.zoom", null)
.append('svg:g');
zoomed() {
svg.attr("transform",
"translate(" + (<any>d3.event).translate + ")"
+ " scale(" + (<any>d3.event).scale + ")");
}
// set up initial nodes and links
// - nodes are known by 'id', not by index in array.
// - reflexive edges are indicated on the node (as a bold black circle).
// - links are always source < target; edge directions are set by 'left' and 'right'.
var nodes = [
{id: 0, reflexive: false},
{id: 1, reflexive: true },
{id: 2, reflexive: false}
],
lastNodeId = 2,
links = [
{source: nodes[0], target: nodes[1], left: false, right: true },
{source: nodes[1], target: nodes[2], left: false, right: true }
];
// init D3 force layout
var force = d3.layout.force()
.nodes(nodes)
.links(links)
.size([width, height])
.linkDistance(150)
.charge(-500)
.on('tick', tick)
// define arrow markers for graph links
svg.append('svg:defs').append('svg:marker')
.attr('id', 'end-arrow')
.attr('viewBox', '0 -5 10 10')
.attr('refX', 6)
.attr('markerWidth', 3)
.attr('markerHeight', 3)
.attr('orient', 'auto')
.append('svg:path')
.attr('d', 'M0,-5L10,0L0,5')
.attr('fill', '#000');
svg.append('svg:defs').append('svg:marker')
.attr('id', 'start-arrow')
.attr('viewBox', '0 -5 10 10')
.attr('refX', 4)
.attr('markerWidth', 3)
.attr('markerHeight', 3)
.attr('orient', 'auto')
.append('svg:path')
.attr('d', 'M10,-5L0,0L10,5')
.attr('fill', '#000');
// line displayed when dragging new nodes
var drag_line = svg.append('svg:path')
.attr('class', 'link dragline hidden')
.attr('d', 'M0,0L0,0');
// handles to link and node element groups
var path = svg.append('svg:g').selectAll('path'),
circle = svg.append('svg:g').selectAll('g');
// mouse event vars
var selected_node = null,
selected_link = null,
mousedown_link = null,
mousedown_node = null,
mouseup_node = null;
function resetMouseVars() {
mousedown_node = null;
mouseup_node = null;
mousedown_link = null;
}
// update force layout (called automatically each iteration)
function tick() {
// draw directed edges with proper padding from node centers
path.attr('d', function(d) {
var deltaX = d.target.x - d.source.x,
deltaY = d.target.y - d.source.y,
dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY),
normX = deltaX / dist,
normY = deltaY / dist,
sourcePadding = d.left ? 17 : 12,
targetPadding = d.right ? 17 : 12,
sourceX = d.source.x + (sourcePadding * normX),
sourceY = d.source.y + (sourcePadding * normY),
targetX = d.target.x - (targetPadding * normX),
targetY = d.target.y - (targetPadding * normY);
return 'M' + sourceX + ',' + sourceY + 'L' + targetX + ',' + targetY;
});
circle.attr('transform', function(d) {
return 'translate(' + d.x + ',' + d.y + ')';
});
}
// update graph (called when needed)
function restart() {
// path (link) group
path = path.data(links);
// update existing links
path.classed('selected', function(d) { return d === selected_link; })
.style('marker-start', function(d) { return d.left ? 'url(#start-arrow)' : ''; })
.style('marker-end', function(d) { return d.right ? 'url(#end-arrow)' : ''; });
// add new links
path.enter().append('svg:path')
.attr('class', 'link')
.classed('selected', function(d) { return d === selected_link; })
.style('marker-start', function(d) { return d.left ? 'url(#start-arrow)' : ''; })
.style('marker-end', function(d) { return d.right ? 'url(#end-arrow)' : ''; })
.on('mousedown', function(d) {
if(d3.event.ctrlKey) return;
// select link
mousedown_link = d;
if(mousedown_link === selected_link) selected_link = null;
else selected_link = mousedown_link;
selected_node = null;
restart();
});
// remove old links
path.exit().remove();
// circle (node) group
// NB: the function arg is crucial here! nodes are known by id, not by index!
circle = circle.data(nodes, function(d) { return d.id; });
// update existing nodes (reflexive & selected visual states)
circle.selectAll('circle')
.style('fill', function(d) { return (d === selected_node) ? d3.rgb(colors(d.id)).brighter().toString() : colors(d.id); })
.classed('reflexive', function(d) { return d.reflexive; });
// add new nodes
var g = circle.enter().append('svg:g');
g.append('svg:circle')
.attr('class', 'node')
.attr('r', 12)
.style('fill', function(d) { return (d === selected_node) ? d3.rgb(colors(d.id)).brighter().toString() : colors(d.id); })
.style('stroke', function(d) { return d3.rgb(colors(d.id)).darker().toString(); })
.classed('reflexive', function(d) { return d.reflexive; })
.on('mouseover', function(d) {
if(!mousedown_node || d === mousedown_node) return;
// enlarge target node
d3.select(this).attr('transform', 'scale(1.1)');
})
.on('mouseout', function(d) {
if(!mousedown_node || d === mousedown_node) return;
// unenlarge target node
d3.select(this).attr('transform', '');
})
.on('mousedown', function(d) {
if(d3.event.ctrlKey) return;
// select node
mousedown_node = d;
if(mousedown_node === selected_node) selected_node = null;
else selected_node = mousedown_node;
selected_link = null;
// reposition drag line
drag_line
.style('marker-end', 'url(#end-arrow)')
.classed('hidden', false)
.attr('d', 'M' + mousedown_node.x + ',' + mousedown_node.y + 'L' + mousedown_node.x + ',' + mousedown_node.y);
restart();
})
.on('mouseup', function(d) {
if(!mousedown_node) return;
// needed by FF
drag_line
.classed('hidden', true)
.style('marker-end', '');
// check for drag-to-self
mouseup_node = d;
if(mouseup_node === mousedown_node) { resetMouseVars(); return; }
// unenlarge target node
d3.select(this).attr('transform', '');
// add link to graph (update if exists)
// NB: links are strictly source < target; arrows separately specified by booleans
var source, target, direction;
if(mousedown_node.id < mouseup_node.id) {
source = mousedown_node;
target = mouseup_node;
direction = 'right';
} else {
source = mouseup_node;
target = mousedown_node;
direction = 'left';
}
var link;
link = links.filter(function(l) {
return (l.source === source && l.target === target);
})[0];
if(link) {
link[direction] = true;
} else {
link = {source: source, target: target, left: false, right: false};
link[direction] = true;
links.push(link);
}
// select new link
selected_link = link;
selected_node = null;
restart();
});
// show node IDs
g.append('svg:text')
.attr('x', 0)
.attr('y', 4)
.attr('class', 'id')
.text(function(d) { return d.id; });
// remove old nodes
circle.exit().remove();
// set the graph in motion
force.start();
}
function mousedown() {
// prevent I-bar on drag
//d3.event.preventDefault();
// because :active only works in WebKit?
svg.classed('active', true);
if(d3.event.ctrlKey || mousedown_node || mousedown_link) return;
// insert new node at point
var point = d3.mouse(this),
node = {id: ++lastNodeId, reflexive: false};
node.x = point[0];
node.y = point[1];
nodes.push(node);
restart();
}
function mousemove() {
if(!mousedown_node) return;
// update drag line
drag_line.attr('d', 'M' + mousedown_node.x + ',' + mousedown_node.y + 'L' + d3.mouse(this)[0] + ',' + d3.mouse(this)[1]);
restart();
}
function mouseup() {
if(mousedown_node) {
// hide drag line
drag_line
.classed('hidden', true)
.style('marker-end', '');
}
// because :active only works in WebKit?
svg.classed('active', false);
// clear mouse event vars
resetMouseVars();
}
function spliceLinksForNode(node) {
var toSplice = links.filter(function(l) {
return (l.source === node || l.target === node);
});
toSplice.map(function(l) {
links.splice(links.indexOf(l), 1);
});
}
// only respond once per keydown
var lastKeyDown = -1;
function keydown() {
d3.event.preventDefault();
if(lastKeyDown !== -1) return;
lastKeyDown = d3.event.keyCode;
// ctrl
if(d3.event.keyCode === 17) {
circle.call(force.drag);
svg.classed('ctrl', true);
}
if(!selected_node && !selected_link) return;
switch(d3.event.keyCode) {
case 8: // backspace
case 46: // delete
if(selected_node) {
nodes.splice(nodes.indexOf(selected_node), 1);
spliceLinksForNode(selected_node);
} else if(selected_link) {
links.splice(links.indexOf(selected_link), 1);
}
selected_link = null;
selected_node = null;
restart();
break;
case 66: // B
if(selected_link) {
// set link direction to both left and right
selected_link.left = true;
selected_link.right = true;
}
restart();
break;
case 76: // L
if(selected_link) {
// set link direction to left only
selected_link.left = true;
selected_link.right = false;
}
restart();
break;
case 82: // R
if(selected_node) {
// toggle node reflexivity
selected_node.reflexive = !selected_node.reflexive;
} else if(selected_link) {
// set link direction to right only
selected_link.left = false;
selected_link.right = true;
}
restart();
break;
}
}
function keyup() {
lastKeyDown = -1;
// ctrl
if(d3.event.keyCode === 17) {
circle
.on('mousedown.drag', null)
.on('touchstart.drag', null);
svg.classed('ctrl', false);
}
}
// app starts here
svg.on('mousedown', mousedown)
.on('mousemove', mousemove)
.on('mouseup', mouseup);
d3.select(window)
.on('keydown', keydown)
.on('keyup', keyup);
restart();
this is added for zooming and paning.
.attr("pointer-events", "all")
.call(d3.behavior.zoom().on("zoom",this.zoomed.bind(this))).on("dblclick.zoom", null)
.append('svg:g');
Related
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>
I want to place a marker in the middle of the links instead of placing it at the end as is done by my code.
Although I googled it, I am not able to find a solution for my code.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="../D3/d3.min.js"></script>
</head>
<body>
<style>
body {
background-color: #3a5795;
}
svg:not(.active):not(.ctrl) {
cursor: crosshair;
}
path.link {
fill: none;
stroke:floralwhite;
stroke-width: 4px;
cursor: default;
}
svg:not(.active):not(.ctrl) path.link {
cursor: pointer;
}
path.link.selected {
stroke-dasharray: 10,2;
}
path.link.dragline {
pointer-events: none;
}
path.link.hidden {
stroke-width: 0;
}
rect.node {
stroke-width: 1.5px;
cursor: pointer;
}
rect.node.reflexive {
stroke: #000 !important;
stroke-width: 2.5px;
}
text {
font: 12px sans-serif;
pointer-events: none;
}
text.id {
text-anchor: middle;
font-weight: bold;
}
</style>
<script type="text/javascript">
// set up SVG for D3
var width = 1400,
height = 800,
colors = d3.scale.category10();
var svg = d3.select('body')
.append('svg')
.attr('oncontextmenu', 'return false;')
.attr('width', width)
.attr('height', height);
// set up initial nodes and links
// - nodes are known by 'id', not by index in array.
// - reflexive edges are indicated on the node (as a bold black rect).
// - links are always source < target; edge directions are set by 'left' and 'right'.
var nodes = [
{
"id": "Component",
"description": "Component are the Containers",
"type":"wiring"
},
{
"id": "Form Design And Data Design",
"description": "In the Form Design and Data Design we can create form and data",
"type": "wiring"
},
{
"id": "Data and Property ",
"description": "All the Data has the Property and value Associated with It",
"type":"wiring"
},
{
"id": "Entity Query",
"description": "Entity Queries can be used to create Entity Relationship ",
"type": "wiring"
},
{
"id": "Entity Query and Entity Data",
"description": "Entity Data Can be used to create ",
"type": "wiring"
}
],
lastNodeId = 2,
links = [
];
// init D3 force layout
var force = d3.layout.force()
.nodes(nodes)
.links(links)
.size([width, height])
.linkDistance(250)
.charge(-1000)
.gravity(0.05)
.on('tick', tick)
//define arrow markers for graph links
svg.append('svg:defs').append('svg:marker')
.attr('id', 'end-arrow')
.attr('viewBox', '0 -5 10 10')
.attr('refX', 6)
.attr('markerWidth', 3)
.attr('markerHeight', 3)
.attr('orient', 'auto')
.append('svg:path')
.attr('d', 'M0,-5L10,0L0,5')
.attr('fill', '#000');
svg.append('svg:defs').append('svg:marker')
.attr('id', 'start-arrow')
.attr('viewBox', '0 -5 10 10')
.attr('refX', 4)
.attr('markerWidth', 6)
.attr('markerHeight', 5)
.attr('orient', 'auto')
.append('svg:path')
.attr('d', 'M10,-5L0,0L10,5')
.attr('fill', '#000');
// line displayed when dragging new nodes
var drag_line = svg.append('svg:path')
.attr('class', 'link dragline hidden')
.attr('d', 'M0,0L0,0');
// handles to link and node element groups
var path = svg.append('svg:g').selectAll('path'),
rect = svg.append('svg:g').selectAll('g');
// mouse event vars
var selected_node = null,
selected_link = null,
mousedown_link = null,
mousedown_node = null,
mouseup_node = null;
function wrapText(text, width) {
text.each(function () {
var textEl = d3.select(this),
words = textEl.text().split(/\s+/).reverse(),
word,
line = [],
linenumber = 0,
lineHeight = 1.1, // ems
y = textEl.attr('y'),
dx = parseFloat(textEl.attr('dx') || 0),
dy = parseFloat(textEl.attr('dy') || 0),
tspan = textEl.text(null).append('tspan').attr('x', 0).attr('y', y).attr('dy', dy + 'em');
while (word = words.pop()) {
line.push(word);
tspan.text(line.join(' '));
if (tspan.node().getComputedTextLength() > width) {
line.pop();
tspan.text(line.join(' '));
line = [word];
tspan = textEl.append('tspan').attr('x', 0).attr('y', y).attr('dx', dx).attr('dy', ++linenumber * lineHeight + dy + 'em').text(word);
}
}
});
}
function resetMouseVars() {
mousedown_node = null;
mouseup_node = null;
mousedown_link = null;
}
// update force layout (called automatically each iteration)
function tick() {
// draw directed edges with proper padding from node centers
path.attr('d', function (d) {
var deltaX = d.target.x - d.source.x,
deltaY = d.target.y - d.source.y,
dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY),
normX = deltaX / dist,
normY = deltaY / dist,
sourcePadding = d.left ? 17 : 12,
targetPadding = d.right ? 17 : 12,
sourceX = d.source.x + (sourcePadding * normX),
sourceY = d.source.y + (sourcePadding * normY),
targetX = d.target.x - (targetPadding * normX),
targetY = d.target.y - (targetPadding * normY);
return 'M' + sourceX + ',' + sourceY + 'L' + targetX + ',' + targetY;
});
rect.attr('transform', function (d) {
return 'translate(' + d.x + ',' + d.y + ')';
});
}
// update graph (called when needed)
function restart() {
// path (link) group
path = path.data(links);
// update existing links
path.classed('selected', function (d) { return d === selected_link; })
.style('marker-start', function (d) { return d.left ? 'url(#start-arrow)' : ''; })
.style('marker-end', function (d) { return d.right ? 'url(#end-arrow)' : ''; });
// add new links
path.enter().append('svg:path')
.attr('class', 'link')
.classed('selected', function (d) { return d === selected_link; })
.style('marker-start', function (d) { return d.left ? 'url(#start-arrow)' : ''; })
.style('marker-end', function (d) { return d.right ? 'url(#end-arrow)' : ''; })
.on('mousedown', function (d) {
if (d3.event.ctrlKey) return;
// select link
mousedown_link = d;
if (mousedown_link === selected_link) selected_link = null;
else selected_link = mousedown_link;
selected_node = null;
restart();
});
// remove old links
path.exit().remove();
// rect (node) group
// NB: the function arg is crucial here! nodes are known by id, not by index!
rect = rect.data(nodes, function (d) { return d.id; });
// update existing nodes (reflexive & selected visual states)
rect.selectAll('rect')
.style('fill', function (d) { return (d === selected_node) ? d3.rgb(colors(d.id)).brighter().toString() : colors(d.id); })
.classed('reflexive', function (d) { return d.reflexive; });
// add new nodes
var g = rect.enter().append('svg:g');
//g.append('svg:rect')
// .attr('class', 'node')
// .attr('r', 30)
g.append('svg:rect')
.attr('class', 'node')
.attr('width', 150)
.attr("height", 60)
.attr("rx", 30)
.attr("ry", 30)
.attr("x", -75)
.attr("y", -16.5)
.style('fill', function (d) { return (d === selected_node) ? d3.rgb(colors(d.id)).brighter().toString() : colors(d.id); })
.style('stroke', function (d) { return d3.rgb(colors(d.id)).darker().toString(); })
.classed('reflexive', function (d) { return d.reflexive; })
.on('mouseover', function (d) {
if (!mousedown_node || d === mousedown_node) return;
// enlarge target node
d3.select(this).attr('transform', 'scale(1.1)');
})
.on('mouseout', function (d) {
if (!mousedown_node || d === mousedown_node) return;
// unenlarge target node
d3.select(this).attr('transform', '');
})
.on('mousedown', function (d) {
if (d3.event.ctrlKey) return;
// select node
mousedown_node = d;
if (mousedown_node === selected_node) selected_node = null;
else selected_node = mousedown_node;
selected_link = null;
// reposition drag line
drag_line
.style('marker-end', 'url(#end-arrow)')
.classed('hidden', false)
.attr('d', 'M' + mousedown_node.x + ',' + mousedown_node.y + 'L' + mousedown_node.x + ',' + mousedown_node.y);
restart();
})
.on('mouseup', function (d) {
if (!mousedown_node) return;
// needed by FF
drag_line
.classed('hidden', true)
.style('marker-end', '');
// check for drag-to-self
mouseup_node = d;
if (mouseup_node === mousedown_node) { resetMouseVars(); return; }
// unenlarge target node
d3.select(this).attr('transform', '');
// add link to graph (update if exists)
// NB: links are strictly source < target; arrows separately specified by booleans
var source, target, direction;
if (mousedown_node.id < mouseup_node.id) {
source = mousedown_node;
target = mouseup_node;
direction = 'right';
} else {
source = mouseup_node;
target = mousedown_node;
direction = 'left';
}
var link;
link = links.filter(function (l) {
return (l.source === source && l.target === target);
})[0];
if (link) {
link[direction] = true;
} else {
link = { source: source, target: target, left: false, right: false };
link[direction] = true;
links.push(link);
}
// select new link
selected_link = link;
selected_node = null;
restart();
});
// show node IDs
g.append('svg:text')
.attr('x', 0)
.attr('y', 4)
.attr('class', 'id')
.text(function (d) { return d.id; })
.call(wrapText, 100);
// remove old nodes
rect.exit().remove();
// set the graph in motion
force.start();
}
function mousedown() {
// prevent I-bar on drag
//d3.event.preventDefault();
// because :active only works in WebKit?
svg.classed('active', true);
if (d3.event.ctrlKey || mousedown_node || mousedown_link) return;
// insert new node at point
var point = d3.mouse(this),
node = { id: ++lastNodeId, reflexive: false };
node.x = point[0];
node.y = point[1];
nodes.push(node);
restart();
}
function mousemove() {
if (!mousedown_node) return;
// update drag line
drag_line.attr('d', 'M' + mousedown_node.x + ',' + mousedown_node.y + 'L' + d3.mouse(this)[0] + ',' + d3.mouse(this)[1]);
restart();
}
function mouseup() {
if (mousedown_node) {
// hide drag line
drag_line
.classed('hidden', true)
.style('marker-end', '');
}
// because :active only works in WebKit?
svg.classed('active', false);
// clear mouse event vars
resetMouseVars();
}
function spliceLinksForNode(node) {
var toSplice = links.filter(function (l) {
return (l.source === node || l.target === node);
});
toSplice.map(function (l) {
links.splice(links.indexOf(l), 1);
});
}
// only respond once per keydown
var lastKeyDown = -1;
function keydown() {
//d3.event.preventDefault();
if (lastKeyDown !== -1) return;
lastKeyDown = d3.event.keyCode;
// ctrl
if (d3.event.keyCode === 17) {
rect.call(force.drag);
svg.classed('ctrl', true);
}
if (!selected_node && !selected_link) return;
switch (d3.event.keyCode) {
case 8: // backspace
case 46: // delete
if (selected_node) {
nodes.splice(nodes.indexOf(selected_node), 1);
spliceLinksForNode(selected_node);
} else if (selected_link) {
links.splice(links.indexOf(selected_link), 1);
}
selected_link = null;
selected_node = null;
restart();
break;
case 66: // B
if (selected_link) {
// set link direction to both left and right
selected_link.left = true;
selected_link.right = true;
}
restart();
break;
case 76: // L
if (selected_link) {
// set link direction to left only
selected_link.left = true;
selected_link.right = false;
}
restart();
break;
case 82: // R
if (selected_node) {
// toggle node reflexivity
selected_node.reflexive = !selected_node.reflexive;
} else if (selected_link) {
// set link direction to right only
selected_link.left = false;
selected_link.right = true;
}
restart();
break;
}
}
function keyup() {
lastKeyDown = -1;
// ctrl
if (d3.event.keyCode === 17) {
rect
.on('mousedown.drag', null)
.on('touchstart.drag', null);
svg.classed('ctrl', false);
}
}
// app starts here
svg.on('mousedown', mousedown)
.on('mousemove', mousemove)
.on('mouseup', mouseup);
d3.select(window)
.on('keydown', keydown)
.on('keyup', keyup);
restart();
</script>
</body>
</html>
To draw markers on the mid point of your links you can use marker-mid which works pretty much like marker-start and marker-end except that it inserts a marker element at the middle.
path.enter().append('svg:path')
.style('marker-mid', function (d) { return 'url(#start-arrow)'; })
For demonstration purposes I have just used the start-arrow here, which may of course be adjusted to your liking.
However, the marker will only be drawn, if there is a vertex at the mid point. This is not true for your code, because you are drawing a single straight line from source to target defining only start and end points. On the other hand, having a straight line comes in handy, because it is fairly easy to calculate the mid point and split up the straight line into two segements, thereby inserting a new vertex at the middle. There are already calculations going on in your tick() handler giving intermediate results to assist in finding the mid point:
// Coordinates of mid point on line to add new vertex.
midX = (targetX - sourceX) / 2 + sourceX;
midY = (targetY - sourceY) / 2 + sourceY;
// | v --- new vertex --- v |
return 'M' + sourceX + ',' + sourceY + 'L' + midX + ',' + midY + 'L' + targetX + ',' + targetY;
Have a look at the following code snippet for a working demo.
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="../D3/d3.min.js"></script>
</head>
<body>
<style>
body {
background-color: #3a5795;
}
svg:not(.active):not(.ctrl) {
cursor: crosshair;
}
path.link {
fill: none;
stroke:floralwhite;
stroke-width: 4px;
cursor: default;
}
svg:not(.active):not(.ctrl) path.link {
cursor: pointer;
}
path.link.selected {
stroke-dasharray: 10,2;
}
path.link.dragline {
pointer-events: none;
}
path.link.hidden {
stroke-width: 0;
}
rect.node {
stroke-width: 1.5px;
cursor: pointer;
}
rect.node.reflexive {
stroke: #000 !important;
stroke-width: 2.5px;
}
text {
font: 12px sans-serif;
pointer-events: none;
}
text.id {
text-anchor: middle;
font-weight: bold;
}
</style>
<script type="text/javascript">
// set up SVG for D3
var width = 1400,
height = 800,
colors = d3.scale.category10();
var svg = d3.select('body')
.append('svg')
.attr('oncontextmenu', 'return false;')
.attr('width', width)
.attr('height', height);
// set up initial nodes and links
// - nodes are known by 'id', not by index in array.
// - reflexive edges are indicated on the node (as a bold black rect).
// - links are always source < target; edge directions are set by 'left' and 'right'.
var nodes = [
{
"id": "Component",
"description": "Component are the Containers",
"type":"wiring"
},
{
"id": "Form Design And Data Design",
"description": "In the Form Design and Data Design we can create form and data",
"type": "wiring"
},
{
"id": "Data and Property ",
"description": "All the Data has the Property and value Associated with It",
"type":"wiring"
},
{
"id": "Entity Query",
"description": "Entity Queries can be used to create Entity Relationship ",
"type": "wiring"
},
{
"id": "Entity Query and Entity Data",
"description": "Entity Data Can be used to create ",
"type": "wiring"
}
],
lastNodeId = 2,
links = [
];
// init D3 force layout
var force = d3.layout.force()
.nodes(nodes)
.links(links)
.size([width, height])
.linkDistance(250)
.charge(-1000)
.gravity(0.05)
.on('tick', tick)
//define arrow markers for graph links
svg.append('svg:defs').append('svg:marker')
.attr('id', 'end-arrow')
.attr('viewBox', '0 -5 10 10')
.attr('refX', 6)
.attr('markerWidth', 3)
.attr('markerHeight', 3)
.attr('orient', 'auto')
.append('svg:path')
.attr('d', 'M0,-5L10,0L0,5')
.attr('fill', '#000');
svg.append('svg:defs').append('svg:marker')
.attr('id', 'start-arrow')
.attr('viewBox', '0 -5 10 10')
.attr('refX', 4)
.attr('markerWidth', 6)
.attr('markerHeight', 5)
.attr('orient', 'auto')
.append('svg:path')
.attr('d', 'M10,-5L0,0L10,5')
.attr('fill', '#000');
// line displayed when dragging new nodes
var drag_line = svg.append('svg:path')
.attr('class', 'link dragline hidden')
.attr('d', 'M0,0L0,0');
// handles to link and node element groups
var path = svg.append('svg:g').selectAll('path'),
rect = svg.append('svg:g').selectAll('g');
// mouse event vars
var selected_node = null,
selected_link = null,
mousedown_link = null,
mousedown_node = null,
mouseup_node = null;
function wrapText(text, width) {
text.each(function () {
var textEl = d3.select(this),
words = textEl.text().split(/\s+/).reverse(),
word,
line = [],
linenumber = 0,
lineHeight = 1.1, // ems
y = textEl.attr('y'),
dx = parseFloat(textEl.attr('dx') || 0),
dy = parseFloat(textEl.attr('dy') || 0),
tspan = textEl.text(null).append('tspan').attr('x', 0).attr('y', y).attr('dy', dy + 'em');
while (word = words.pop()) {
line.push(word);
tspan.text(line.join(' '));
if (tspan.node().getComputedTextLength() > width) {
line.pop();
tspan.text(line.join(' '));
line = [word];
tspan = textEl.append('tspan').attr('x', 0).attr('y', y).attr('dx', dx).attr('dy', ++linenumber * lineHeight + dy + 'em').text(word);
}
}
});
}
function resetMouseVars() {
mousedown_node = null;
mouseup_node = null;
mousedown_link = null;
}
// update force layout (called automatically each iteration)
function tick() {
console.log(path);
// draw directed edges with proper padding from node centers
path.attr('d', function (d) {
var deltaX = d.target.x - d.source.x,
deltaY = d.target.y - d.source.y,
dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY),
normX = deltaX / dist,
normY = deltaY / dist,
sourcePadding = d.left ? 17 : 12,
targetPadding = d.right ? 17 : 12,
sourceX = d.source.x + (sourcePadding * normX),
sourceY = d.source.y + (sourcePadding * normY),
targetX = d.target.x - (targetPadding * normX),
targetY = d.target.y - (targetPadding * normY),
midX = (targetX - sourceX) / 2 + sourceX,
midY = (targetY - sourceY) / 2 + sourceY;
return 'M' + sourceX + ',' + sourceY + 'L' + midX + ',' + midY + 'L' + targetX + ',' + targetY;
});
rect.attr('transform', function (d) {
return 'translate(' + d.x + ',' + d.y + ')';
});
}
// update graph (called when needed)
function restart() {
// path (link) group
path = path.data(links);
// update existing links
path.classed('selected', function (d) { return d === selected_link; })
.style('marker-start', function (d) { return d.left ? 'url(#start-arrow)' : ''; })
.style('marker-mid', function (d) { return 'url(#start-arrow)'; })
.style('marker-end', function (d) { return d.right ? 'url(#end-arrow)' : ''; });
// add new links
path.enter().append('svg:path')
.attr('class', 'link')
.classed('selected', function (d) { return d === selected_link; })
.style('marker-start', function (d) { return d.left ? 'url(#start-arrow)' : ''; })
.style('marker-mid', function (d) { return 'url(#start-arrow)'; })
.style('marker-end', function (d) { return d.right ? 'url(#end-arrow)' : ''; })
.on('mousedown', function (d) {
if (d3.event.ctrlKey) return;
// select link
mousedown_link = d;
if (mousedown_link === selected_link) selected_link = null;
else selected_link = mousedown_link;
selected_node = null;
restart();
});
// remove old links
path.exit().remove();
// rect (node) group
// NB: the function arg is crucial here! nodes are known by id, not by index!
rect = rect.data(nodes, function (d) { return d.id; });
// update existing nodes (reflexive & selected visual states)
rect.selectAll('rect')
.style('fill', function (d) { return (d === selected_node) ? d3.rgb(colors(d.id)).brighter().toString() : colors(d.id); })
.classed('reflexive', function (d) { return d.reflexive; });
// add new nodes
var g = rect.enter().append('svg:g');
//g.append('svg:rect')
// .attr('class', 'node')
// .attr('r', 30)
g.append('svg:rect')
.attr('class', 'node')
.attr('width', 150)
.attr("height", 60)
.attr("rx", 30)
.attr("ry", 30)
.attr("x", -75)
.attr("y", -16.5)
.style('fill', function (d) { return (d === selected_node) ? d3.rgb(colors(d.id)).brighter().toString() : colors(d.id); })
.style('stroke', function (d) { return d3.rgb(colors(d.id)).darker().toString(); })
.classed('reflexive', function (d) { return d.reflexive; })
.on('mouseover', function (d) {
if (!mousedown_node || d === mousedown_node) return;
// enlarge target node
d3.select(this).attr('transform', 'scale(1.1)');
})
.on('mouseout', function (d) {
if (!mousedown_node || d === mousedown_node) return;
// unenlarge target node
d3.select(this).attr('transform', '');
})
.on('mousedown', function (d) {
if (d3.event.ctrlKey) return;
// select node
mousedown_node = d;
if (mousedown_node === selected_node) selected_node = null;
else selected_node = mousedown_node;
selected_link = null;
// reposition drag line
drag_line
.style('marker-end', 'url(#end-arrow)')
.classed('hidden', false)
.attr('d', 'M' + mousedown_node.x + ',' + mousedown_node.y + 'L' + mousedown_node.x + ',' + mousedown_node.y);
restart();
})
.on('mouseup', function (d) {
if (!mousedown_node) return;
// needed by FF
drag_line
.classed('hidden', true)
.style('marker-end', '');
// check for drag-to-self
mouseup_node = d;
if (mouseup_node === mousedown_node) { resetMouseVars(); return; }
// unenlarge target node
d3.select(this).attr('transform', '');
// add link to graph (update if exists)
// NB: links are strictly source < target; arrows separately specified by booleans
var source, target, direction;
if (mousedown_node.id < mouseup_node.id) {
source = mousedown_node;
target = mouseup_node;
direction = 'right';
} else {
source = mouseup_node;
target = mousedown_node;
direction = 'left';
}
var link;
link = links.filter(function (l) {
return (l.source === source && l.target === target);
})[0];
if (link) {
link[direction] = true;
} else {
link = { source: source, target: target, left: false, right: false };
link[direction] = true;
links.push(link);
}
// select new link
selected_link = link;
selected_node = null;
restart();
});
// show node IDs
g.append('svg:text')
.attr('x', 0)
.attr('y', 4)
.attr('class', 'id')
.text(function (d) { return d.id; })
.call(wrapText, 100);
// remove old nodes
rect.exit().remove();
// set the graph in motion
force.start();
}
function mousedown() {
// prevent I-bar on drag
//d3.event.preventDefault();
// because :active only works in WebKit?
svg.classed('active', true);
if (d3.event.ctrlKey || mousedown_node || mousedown_link) return;
// insert new node at point
var point = d3.mouse(this),
node = { id: ++lastNodeId, reflexive: false };
node.x = point[0];
node.y = point[1];
nodes.push(node);
restart();
}
function mousemove() {
if (!mousedown_node) return;
// update drag line
drag_line.attr('d', 'M' + mousedown_node.x + ',' + mousedown_node.y + 'L' + d3.mouse(this)[0] + ',' + d3.mouse(this)[1]);
restart();
}
function mouseup() {
if (mousedown_node) {
// hide drag line
drag_line
.classed('hidden', true)
.style('marker-end', '');
}
// because :active only works in WebKit?
svg.classed('active', false);
// clear mouse event vars
resetMouseVars();
}
function spliceLinksForNode(node) {
var toSplice = links.filter(function (l) {
return (l.source === node || l.target === node);
});
toSplice.map(function (l) {
links.splice(links.indexOf(l), 1);
});
}
// only respond once per keydown
var lastKeyDown = -1;
function keydown() {
//d3.event.preventDefault();
if (lastKeyDown !== -1) return;
lastKeyDown = d3.event.keyCode;
// ctrl
if (d3.event.keyCode === 17) {
rect.call(force.drag);
svg.classed('ctrl', true);
}
if (!selected_node && !selected_link) return;
switch (d3.event.keyCode) {
case 8: // backspace
case 46: // delete
if (selected_node) {
nodes.splice(nodes.indexOf(selected_node), 1);
spliceLinksForNode(selected_node);
} else if (selected_link) {
links.splice(links.indexOf(selected_link), 1);
}
selected_link = null;
selected_node = null;
restart();
break;
case 66: // B
if (selected_link) {
// set link direction to both left and right
selected_link.left = true;
selected_link.right = true;
}
restart();
break;
case 76: // L
if (selected_link) {
// set link direction to left only
selected_link.left = true;
selected_link.right = false;
}
restart();
break;
case 82: // R
if (selected_node) {
// toggle node reflexivity
selected_node.reflexive = !selected_node.reflexive;
} else if (selected_link) {
// set link direction to right only
selected_link.left = false;
selected_link.right = true;
}
restart();
break;
}
}
function keyup() {
lastKeyDown = -1;
// ctrl
if (d3.event.keyCode === 17) {
rect
.on('mousedown.drag', null)
.on('touchstart.drag', null);
svg.classed('ctrl', false);
}
}
// app starts here
svg.on('mousedown', mousedown)
.on('mousemove', mousemove)
.on('mouseup', mouseup);
d3.select(window)
.on('keydown', keydown)
.on('keyup', keyup);
restart();
</script>
</body>
</html>
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.
I have my data formatted like flare.json that's used in this example :
I am just wondering what function the d3 zoomable chart uses to get the data in this format
In flare.json it's like this
{
name: "stuff",
children: [
....
]
}
and it's converted to this in the example. Which line does this?
{
children: Array[17]
depth: 1
dx: 0.6028744305756647
dy: 0.25
name: "A name would appear here"
parent: Object
value: 39850000.06
x: 0
y: 0.25
}
Chart
var total_revenue = json.total_revenue;
json = json.chart_data;
var width = 840,
height = width,
radius = width / 2,
x = d3.scale.linear().range([0, 2 * Math.PI]),
y = d3.scale.pow().exponent(1.3).domain([0, 1]).range([0, radius]),
padding = 5,
duration = 1000;
var div = d3.select("#chart_render");
div.select("img").remove();
var vis = div.append("svg")
.attr("width", width + padding * 2)
.attr("height", height + padding * 2)
.append("g")
.attr("transform", "translate(" + [radius + padding, radius + padding] + ")");
var partition = d3.layout.partition()
.value(function(d) { return d.size });
var arc = d3.svg.arc()
.startAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x))); })
.endAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x + d.dx))); })
.innerRadius(function(d) { return Math.max(0, d.y ? y(d.y) : d.y); })
.outerRadius(function(d) { return Math.max(0, y(d.y + d.dy)); });
console.log(json);
var nodes = partition.nodes({children: json});
var path = vis.selectAll("path").data(nodes);
path.enter().append("path")
.attr("id", function(d, i) { return "path-" + i; })
.attr("d", arc)
.attr("fill-rule", "evenodd")
.style("fill", colour)
.on("click", click);
var text = vis.selectAll("text").data(nodes);
var textEnter = text.enter().append("text")
.style("fill-opacity", function(d) {
var relative_percent = 0;
var relative_total = 0;
//console.log(d);
if (d.depth != 0) {
for(var i = 0; i < d.parent.children.length; i++) {
relative_total += d.parent.children[i].value;
}
//console.log(relative_total);
relative_percent = d.value/total_revenue*100;
if (relative_percent > 1) {
return '1';
} else {
return '0';
}
}
})
.style("fill", function(d) {
return "#fff";
})
.attr("text-anchor", function(d) {
return x(d.x + d.dx / 2) > Math.PI ? "end" : "start";
})
.attr("dy", ".2em")
.attr("transform", function(d) {
var multiline = (d.name || "").split(" ").length > 1,
angle = x(d.x + d.dx / 2) * 180 / Math.PI - 90,
rotate = angle + (multiline ? -.5 : 0);
return "rotate(" + rotate + ")translate(" + (y(d.y) + padding) + ")rotate(" + (angle > 90 ? -180 : 0) + ")";
})
.on("click", click);
textEnter.append("tspan")
.attr("x", 0)
.text(function(d) { return d.depth ? d.name.split(" ")[0] : ""; });
textEnter.append("tspan")
.attr("x", 0)
.attr("dy", "1em")
.text(function(d) { return d.depth ? d.name.split(" ")[1] || "" : ""; });
function click(d) {
path.transition()
.duration(duration)
.attrTween("d", arcTween(d));
// Somewhat of a hack as we rely on arcTween updating the scales.
text.style("visibility", function(e) {
return isParentOf(d, e) && e.value > 1500000 ? null : d3.select(this).style("visibility");
})
.transition()
.duration(duration)
.attrTween("text-anchor", function(d) {
return function() {
return x(d.x + d.dx / 2) > Math.PI ? "end" : "start";
};
})
.attrTween("transform", function(d) {
var multiline = (d.name || "").split(" ").length > 1;
return function() {
var angle = x(d.x + d.dx / 2) * 180 / Math.PI - 90,
rotate = angle + (multiline ? -.5 : 0);
return "rotate(" + rotate + ")translate(" + (y(d.y) + padding) + ")rotate(" + (angle > 90 ? -180 : 0) + ")";
};
})
.style("fill-opacity", function(e) { return isParentOf(d, e) ? 1 : 1e-6; })
.each("end", function(e) {
d3.select(this).style("visibility", function (d) {
// var relative_total = 0;
// var relative_percent = 0;
// for(var i = 0; i < d.parent.children.length; i++) {
// relative_total += d.parent.children[i].value;
// }
// console.log(relative_total);
// relative_percent = d.value/relative_total*100;
// console.log(relative_percent);
return isParentOf(d, e) && e.value > 1500000 ? null : "hidden";
})
});
}
function isParentOf(p, c) {
if (p === c) return true;
if (p.children) {
return p.children.some(function(d) {
return isParentOf(d, c);
});
}
return false;
}
function colour(d) {
if (d.depth == 0) {
return "rgb(250, 250, 250)";
} else if (d.depth == 1) {
return 'rgb(86, 135, 209)';
} else if (d.depth == 2) {
return 'rgb(222, 120, 59)';
} else if (d.depth == 3) {
return 'rgb(106, 185, 117)';
}
// if (d.children) {
// // There is a maximum of two children!
// var colours = d.children.map(colour),
// a = d3.hsl(colours[0]),
// b = d3.hsl(colours[1]);
// // L*a*b* might be better here...
// return d3.hsl((a.h + b.h) / 2, a.s * 1.2, a.l / 1.2);
// }
// return d.colour || "#fff";
}
// Interpolate the scales!
function arcTween(d) {
var my = maxY(d),
xd = d3.interpolate(x.domain(), [d.x, d.x + d.dx]),
yd = d3.interpolate(y.domain(), [d.y, my]),
yr = d3.interpolate(y.range(), [d.y ? 20 : 0, radius]);
return function(d) {
return function(t) { x.domain(xd(t)); y.domain(yd(t)).range(yr(t)); return arc(d); };
};
}
function maxY(d) {
return d.children ? Math.max.apply(Math, d.children.map(maxY)) : d.y + d.dy;
}
// http://www.w3.org/WAI/ER/WD-AERT/#color-contrast
function brightness(rgb) {
return rgb.r * .299 + rgb.g * .587 + rgb.b * .114;
}
This line:
var nodes = partition.nodes({children: json});
Explanation of code that sets up sunburst diagram
In D3 parlance, sunburst diagram is based on D3 "partition layout". Actually, D3 "partition layout" is in a way more general term, since it can be used for displaying not only sunburst diagram, but also others based on the idea of "partitioning" parents (hence the name "partition"). This is also a useful example for noticing difference between "layout" and "diagram" (in D3 mindset), but this is another story.
Following 2 lines are first steps in initializing partition layout:
var partition = d3.layout.partition()
.value(function(d) { return d.size });
This line does all calculations:
var nodes = partition.nodes({children: json});
Then variable nodes can be used for defining actual visual appearance of svg elements (arcs and labels):
var path = vis.selectAll("path").data(nodes);
and
var text = vis.selectAll("text").data(nodes);
These two lines represent something which is called "data binding" often. They enable programmers to use data to drive visual elements, like in the following line:
.text(function(d) { return d.depth ? d.name.split(" ")[0] : ""; });
Here, d.name originates from data, and d.depth is added by partition layout. They are both actually part of nodes.
I tried to explain in simple terms, but probably there are some confusing points to you - don't worry, it will be crystal clear to you soon, if you read the right docs and tutorials. ;)
** UPDATE **
Currently the first node loads up on the screen (very small) and I'm now getting the following error in the Chrome console
Error: Problem parsing d="M72.3168,NaNC235.618512,NaN 75.93263999999999,NaN 239.234352,NaN"
This function generates a tree with nodes that you can click, drag, and re-arrange.
In the very first line, instead of passing in the JSON file as "flare.json", instead I want to pass in a javascript object. How do I edit this function so that I can pass in an object?
treeJSON = d3.json("flare.json", function(error, treeData) {
// 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);
}
}
}
// 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([0.1, 3]).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", viewerWidth)
.attr("height", viewerHeight)
.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.dy;
d.y0 += d3.event.dx;
var node = d3.select(this);
node.attr("transform", "translate(" + d.y0 + "," + d.x0 + ")");
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) {
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: selectedNode.y0,
y: selectedNode.x0
},
target: {
x: draggingNode.y0,
y: draggingNode.x0
}
}];
}
var link = svgGroup.selectAll(".templink").data(data);
link.enter().append("path")
.attr("class", "templink")
.attr("d", d3.svg.diagonal())
.attr('pointer-events', 'none');
link.attr("d", d3.svg.diagonal());
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.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]);
}
// 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);
}
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 * 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("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);
// 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) {
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";
});
// Transition nodes to their new position.
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + d.y + "," + d.x + ")";
});
// Fade the text in
nodeUpdate.select("text")
.style("fill-opacity", 1);
// Transition exiting nodes to the parent's new position.
var nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + source.y + "," + source.x + ")";
})
.remove();
nodeExit.select("circle")
.attr("r", 0);
nodeExit.select("text")
.style("fill-opacity", 0);
// Update the links…
var link = svgGroup.selectAll("path.link")
.data(links, function(d) {
return d.target.id;
});
// Enter any new links at the parent's previous position.
link.enter().insert("path", "g")
.attr("class", "link")
.attr("d", function(d) {
var o = {
x: source.x0,
y: source.y0
};
return diagonal({
source: o,
target: o
});
});
// Transition links to their new position.
link.transition()
.duration(duration)
.attr("d", diagonal);
// Transition exiting nodes to the parent's new position.
link.exit().transition()
.duration(duration)
.attr("d", function(d) {
var o = {
x: source.x,
y: source.y
};
return diagonal({
source: o,
target: o
});
})
.remove();
// 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.
update(root);
centerNode(root);
});
var yourJSObjectWithData = { /* ... */ };
(function(treeData) {
// Calculate total nodes, max label length
var totalNodes = 0;
var maxLabelLength = 0;
// ...
// Layout the tree initially and center on the root node.
update(root);
centerNode(root);
})(yourJSObjectWithDataHere); // <-- Pass in your Data here
Instead of
treeJSON = d3.json("flare.json", function(error, treeData) {
// ...
}
try
var jsonObj = { }, // your JSON object here
treeJSON = function(treeData) {
// ... method body above here
}
Then call your treeJSON method with your jsonObj object something like
treeJSON(jsonObj);