I am trying to migrate this v3 project to v5: http://bl.ocks.org/stevenZlai/cf14ba9b6372bddd2b4661beb95fbee1
a sample of my code is here https://codepen.io/sancelot/pen/daajJr
I am facing many problems in order to have a working versions:
- Text nodes are not displaying. - now working
- over circle, the path templink does not appear. now working
- when moving a node, the parent link is removed, but does not appear anymore now working
- when moving a node, the children does not move now working
- when moving a node to another node, the link does not appear
main code :
var data = {
"name":"Goal: Become a Pilot",
"nodeNo": 0,
"value": 1,
"status": "In",
"type": "MainNode",
"mainRoot": "Root1",
"nodeBefore": "null",
"linkWidth": 0,
"children": [{
"name": "Node1",
"nodeNo": 1,
"value": 5,
"status": "In",
"type": "Small",
"mainRoot": "Root1",
"nodeBefore": "Node1",
"linkWidth": 10
}, {
"name": "Node2",
"nodeNo": 2,
"value": 10,
"status": "In",
"type": "Medium",
"mainRoot": "Root1",
"nodeBefore": "Node2",
"linkWidth": 15,
"children" : [{
"name" : "Node2.1",
},
{
"name" : "Node2.2",
},
]
}, {
"name": "Node3",
"nodeNo": 3,
"value": 5,
"status": "Out",
"type": "Large",
"mainRoot": "Root1",
"nodeBefore": "Node3",
"linkWidth": 50
}
]
}
// dragging variables
var panBoundary = 20; // Within 20px from edges will pan when dragging.
var panSpeed = 200;
// Misc. variables
var i = 0;
var duration = 750;
// Calculate total nodes, max label length
var totalNodes = 0;
var maxLabelLength = 0;
// size of the diagram
var viewerWidth = 400;
var viewerHeight = 400;
console.log(viewerWidth, viewerHeight);
var treeLayout = d3.tree()
.size([viewerWidth-20, viewerHeight-20]);
var root = d3.hierarchy(data,function(d) { return d.children; });
console.log("tree is ",root);
treeLayout(root);// Nodes
/* Obsolete, remplacé par link linkRadial
// 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];
});
*/
// Creates a curved (diagonal) path from parent to the child nodes
function diagonal(source, target) {
path = `M ${source.y} ${source.x}
C ${(source.y + target.y) / 2} ${source.x},
${(source.y + target.y) / 2} ${target.x},
${target.y} ${target.x}`
return path
}
// 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(data, function(d) {
totalNodes++;
maxLabelLength = Math.max(d.name.length, maxLabelLength);
}, function(d) {
return d.children && d.children.length > 0 ? d.children : null;
});
//retourne les transformations x,y d un element
//https://stackoverflow.com/questions/38224875/replacing-d3-transform-in-d3-v4
function getTranslation(transform) {
// Create a dummy g for calculation purposes only. This will never
// be appended to the DOM and will be discarded once this function
// returns.
var g = document.createElementNS("http://www.w3.org/2000/svg", "g");
// Set the transform attribute to the provided string value.
g.setAttributeNS(null, "transform", transform);
// consolidate the SVGTransformList containing all transformations
// to a single SVGTransform of type SVG_TRANSFORM_MATRIX and get
// its SVGMatrix.
var matrix = g.transform.baseVal.consolidate().matrix;
// As per definition values e and f are the ones for the translation.
return [matrix.e, matrix.f];
}
// TODO: Pan function, can be better implemented.
function pan(domNode, direction) {
var speed = panSpeed;
if (panTimer) {
clearTimeout(panTimer);
translateCoords = getTranslation(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);
}
}
/*** ************ ZOOM ******/
// https://github.com/d3/d3-zoom
function zoom() {
baseSvg.attr("transform", "translate(" + d3.event.transform.x+","+d3.event.transform.y + ") scale(" + d3.event.transform.k + ")");
}
// define the zoomListener which calls the zoom function on the "zoom" event constrained within the scaleExtents
var zoomListener = d3.zoom().scaleExtent([0.1, 3]).on("zoom", zoom);
/*******************************/
/************* DRAG ***********/
// variables for drag/drop
var selectedNode = null;
var draggingNode = null;
var dragStarted = false;
function initiateDrag(d, domNode) {
console.log('initiatedrah');
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)
if (nodes.length > 1) {
// remove link paths
links = root.links(nodes);
var nodePaths = svgGroup.selectAll("path.link")
.data(links, function(d) {
return d.target.id;
}).remove();
// remove child nodes
var 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 = root.links(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 drag listeners for drag/drop behaviour of nodes.
dragListener = d3.drag()
.on("start", function(d) {
console.log("dragstart");
if (d == root) {
return;
}
dragStarted = true;
console.log("drag data : ",d.data);
console.log(d.children);
// A QUOI CA SERT ????? :
// nodes = root.data.nodes(d);
nodes = d.children;
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) {
console.log("drag");
if (d == root) {
return;
}
if (dragStarted) {
domNode = this;
initiateDrag(d, domNode);
}
console.log("drag()");
// 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("end", function(d) {
console.log("drag end");
if (d == root) {
return;
}
domNode = this;
console.log("selectedNode ",selectedNode);
if (selectedNode) {
// now remove the element from the parent, and insert it into the new elements children
var index = draggingNode.parent.children.indexOf(draggingNode);
console.log("index us ",index);
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() {
console.log("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;
}
}
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) {
console.log("update Temp Connector draggingNode && selectedNode ok");
console.log(selectedNode,draggingNode);
// have to flip the source coordinates since we did this for the existing connectors on the original tree
data = [{
source: {
'x': selectedNode.x0,
'y': selectedNode.y0
},
target: {
'x': draggingNode.x0,
'y': draggingNode.y0
}
}];
}
// console.log("data is ",data);
console.log("v3",data);
var link_ = svgGroup.selectAll("path.templink").data(data);
console.log("link is",link_);
var linkEnter = link_.enter().append("path")
.attr("class", "templink")
.attr("d", function(d) { console.log("ENTER ",d);return diagonal(d.source,d.target);})
.attr('pointer-events', 'none');
link_.attr("d", function(d) { console.log("UPD ",d);return diagonal(d.source,d.target);})
link_.exit().remove();
};
/********************************/
// 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);
// 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) {
console.log("clicked") ;
if (d3.event.defaultPrevented) return; // click suppressed
d = toggleChildren(d);
update(d);
//centerNode(d);
}
/* =================== */
function update(source) {
console.log("update() source is s",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);
});
}
};
console.log("l302",levelWidth);
childCount(0, root);
var newHeight = d3.max(levelWidth) * 25; // 25 pixels per line
treeLayout = treeLayout.size([newHeight, viewerWidth]);
// Assigns the x and y position for the nodes
var treeData = treeLayout(root);
// Compute the new tree layout.
var nodes = root.descendants(root).reverse(),
links = root.links(nodes);
/* var nodes = treeData.descendants(),
links = treeData.descendants().slice(1); */
console.log("maxLabelLength", maxLabelLength);
// 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.
});
var i= 0; // FIXME: debile !
// Update the nodes…
node = svgGroup.selectAll("g.node")
.data(nodes, function(d) {
console.log(d);
var x = d.id || (d.id = ++i);
console.log("select g.node ",x," name",d.data.name);
return x;
});
console.log("node is :",node);
// 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) {
console.log("transform 1");
var t = "translate(" + source.y0 + "," + source.x0 + ")";
console.log(t);
return(t);
})
.on('click', click);
// adding popup dialogue for changing/adding/deleting nodes for text captions too
nodeEnter.append("circle")
.attr('class', 'nodeCircle')
.attr("r", 0)
.style("fill", function(d) {
return d._children ? "lightsteelblue" : "#fff";
});
//.on('contextmenu', d3.contextMenu(menu));
// adding popup dialogue for changing/adding/deleting nodes to circles
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) {
console.log(" nodeEnter name ",d.data.name);
return d.data.name;
})
.style("fill-opacity", 0);
//.on('contextmenu', d3.contextMenu(menu));
// adding popup dialogue for changing/adding/deleting nodes for text captions too
// 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);
});
node = svgGroup.selectAll("g.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) {
console.log("node txt",d.data.name)
return d.data.name;
});
// Change the circle fill depending on whether it has children and is collapsed
node.select("circle.nodeCircle")
.attr("r",function(d) { return 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.
var linkEnter = link.enter().insert("path", "g")
.attr("class", "link")
.attr("d", function(d) {
var o = {
x: source.x0,
y: source.y0
};
return diagonal(o,o);
});
// UPDATE
var linkUpdate = linkEnter.merge(link);
// Transition links to their new position.
linkUpdate.transition()
.duration(duration)
.attr("d", function(d){ return diagonal(d.source, d.target); });
// Remove any exiting links
var linkExit = link.exit().transition()
.duration(duration)
.attr('d', function(d) {
var o = {x: source.x, y: source.y}
return diagonal(o, o);
})
.remove();
// Stash the old positions for transition.
nodes.forEach(function(d) {
d.x0 = d.x;
d.y0 = d.y;
});
}
/* end update */
// Append a group which holds all nodes and which the zoom Listener can act upon.
var svgGroup = baseSvg.append("g").attr("class","nodes");
var templink = baseSvg.append("path")
.attr("class","templink")
.attr('pointer-events', 'none');
//var svgLinks = baseSvg.append("g").attr("class","links");
/* d3.select('svg g.nodes')
.selectAll('circle.node')
.data(root.descendants())
.enter()
.append('rect')
.classed('node', true)
.attr('x', function(d) {return d.y;})
.attr('y', function(d) {return d.x;})
.attr('width', 24)
.attr('height', 24);
*/
// Links
/* d3.select('svg g.links')
.selectAll('line.link')
.data(root.links())
.enter()
.append('line')
.classed('link', true)
.attr('x1', function(d) {return d.source.y;})
.attr('y1', function(d) {return d.source.x;})
.attr('x2', function(d) {return d.target.y;})
.attr('y2', function(d) {return d.target.x;});
*/
console.log(root);
console.log(JSON.stringify(root.data));
root.x0 = viewerHeight / 2;
root.y0 = 0;
// Layout the tree initially and center on the root node.
update(root);