-
Drgging a node and move it to another one will merge those two nodes.
-
Right click on blank canvas will generate a new node.
-
Right click on a node will remove this node and it's descendents nodes if it contains children.
-
rData is hierarchy tree object and it will changed accrodingly with this pack tree.
-
拖拽一个节点并靠近另一个将会使得这两个节点融合;
-
右键单击空白区域产生新的节点;
-
右键单击节点将删除该节点及其包含的子节点;
-
rData 是一个树对象,其会随着节点的增加和减少而变化。
Last active
September 30, 2019 08:34
-
-
Save 1Cr18Ni9/f3ffebcf9f97abb57c0a35d1bb92d03a to your computer and use it in GitHub Desktop.
dynamic pack
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
license: mit |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<head> | |
<meta charset="utf-8"> | |
<script src="https://d3js.org/d3.v4.min.js"></script> | |
<style> | |
.svg { | |
outline: 1px solid gray; | |
display: block; | |
margin: 1em auto; | |
font-size: 12px; | |
} | |
.svg__bg { | |
fill: rgba(209, 206, 206, 0.2); | |
pointer-events: all; | |
} | |
.svg__node { | |
stroke-width: 1; | |
cursor: pointer; | |
} | |
.svg__node:hover circle, | |
.svg__active circle { | |
stroke-width: 2; | |
stroke-dasharray: 2 6; | |
stroke: #e43939; | |
} | |
.svg__node-wrapper { | |
fill: #6f6fd3; | |
fill-opacity: 0.2; | |
stroke: white; | |
stroke-width: 1; | |
} | |
.svg__text { | |
text-anchor: middle; | |
fill: white; | |
pointer-events: none; | |
-webkit-user-select: none; | |
-moz-user-select: none; | |
-ms-user-select: none; | |
user-select: none; | |
} | |
textarea { | |
display:block; | |
width: 100%; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="app"></div> | |
<textarea id="tree" rows="5"></textarea> | |
<script> | |
var uuid = (function () { | |
var i = 0, prefix = "w", f; | |
f = function () { return prefix + "" + i++; }; | |
f.prefix = function (v) { prefix = v; return f; }; | |
return f; | |
})(); | |
var w = 800, h = 400, | |
svg, gMain, rectBg, | |
// dragedNode and mergeNode is only avaible on dragging events | |
dragedNode, mergeNode, isForceLayoutTriggered = false, | |
rData, root, nodes; | |
rData = { | |
"name": "ROOT", | |
"children": [ | |
{ | |
"name": "Wx2", | |
"children": [ | |
{ | |
name: "W__1", | |
children: [ | |
{ | |
"name": "N__1", | |
"value": 1, | |
color: "#e263fb", | |
data: {} | |
}, | |
{ | |
"name": "N__2", | |
"value": 1, | |
color: "#7400c3", | |
data: {} | |
} | |
] | |
}, | |
{ | |
"name": "Nx1", | |
"value": 1, | |
color: "#e263fb", | |
data: {} | |
}, | |
{ | |
"name": "Nx2", | |
"value": 1, | |
color: "#7400c3", | |
data: {} | |
}, | |
{ | |
"name": "Nx3", | |
"value": 1, | |
color: "#c649fb", | |
data: {} | |
} | |
] | |
}, | |
{ | |
"name": "Nx4", | |
"value": 1, | |
color: "#9904fc", | |
data: {} | |
} | |
] | |
}; | |
var tree = d3.select("#tree").node(); | |
var canvasDrag = d3.drag() | |
.subject(function () { | |
return gMain.datum(); | |
}) | |
.on("drag", function () { | |
var x = d3.event.x, y = d3.event.y; | |
gMain.datum({x: x, y: y}) | |
.attr("transform", "translate(" + [x, y] + ")"); | |
}); | |
svg = d3.select("#app").append("svg") | |
.attr("xmlns", "http://www.w3.org/2000/svg") | |
.attr("version", "1.1") | |
.attr("class", "svg") | |
.attr("width", w) | |
.attr("height", h); | |
rectBg = svg.append("rect") | |
.attr("class", "svg__bg") | |
.attr("width", w) | |
.attr("height", h) | |
.call(canvasDrag) | |
.on("contextmenu", function () { | |
d3.event.preventDefault(); | |
rData.children.push({ | |
name: uuid(), | |
data: {}, | |
color: "hsl(" + Math.floor(Math.random() * 360) + ", 85%, 80%)" | |
}); | |
iniRoot(rData); | |
nodesUpdate(nodes); | |
}); | |
gMain = svg.append("g") | |
.attr("class", "svg__main") | |
.datum({x: 0, y: 0}) | |
.attr("transform", "translate(0,0)"); | |
var iniRoot = (function () { | |
var p = d3.pack().radius(() => 45).padding(15).size([w, h]), | |
sum = function (d) { return d.value; }; | |
//sort = function (a, b) { return a.value - b.value; }; | |
return function (data) { | |
tree.value = JSON.stringify(data, null, 2); | |
root = d3.hierarchy(data).sum(sum); | |
nodes = root.descendants(); | |
p(root); | |
}; | |
})(); | |
var getDistance = function (c1, c2) { | |
return Math.sqrt((c1.x - c2.x) * (c1.x - c2.x) + | |
(c1.y - c2.y) * (c1.y - c2.y)); | |
}; | |
var nodeDrag = d3.drag() | |
.on("start", function () { | |
dragedNode = null; | |
mergeNode = null; | |
isForceLayoutTriggered = false; | |
}) | |
.on("drag", function () { | |
dragedNode = this; | |
var x = d3.event.x, y = d3.event.y, | |
dx = d3.event.dx, dy = d3.event.dy, | |
M = d3.select(this), D = M.datum(); | |
var children = [], | |
siblings = [], // siblings and ancestors | |
node, temp, | |
gNodes = gMain.selectAll(".svg__node").nodes(); | |
if (D.children && D.children.length) { | |
for (node of gNodes) { | |
if (node === this) { continue; } | |
temp = d3.select(node).datum(); | |
if (temp.ancestors().includes(D)) { | |
children.push(node); | |
} | |
else { | |
siblings.push(node); | |
} | |
} | |
} | |
else { | |
siblings = gNodes.filter(function (node) { | |
return node !== dragedNode; | |
}); | |
} | |
Object.assign(D, {x: x, y: y}); | |
M.classed("svg__active", true) | |
.attr("transform", "translate(" + [x, y] + ")"); | |
// children of the current node should move accordingly | |
children.forEach(function (n) { | |
var self = d3.select(n), d = self.datum(); | |
d.x += dx; | |
d.y += dy; | |
self.attr("transform", "translate(" + [d.x, d.y] + ")"); | |
}); | |
/* mergeNode requisitions: | |
1. have intersections with dragged node | |
2. choose the closest node(circle) | |
3. if mergeNode finded overwrite the preview one | |
*/ | |
siblings = siblings.map(function (node) { | |
var d = d3.select(node).datum(), | |
distance = getDistance(D, d), | |
// detect if dragNode is fenced by siblings | |
isAccommodated = ((d.r - D.r) > distance), | |
// detect if two node have intersection | |
isIntersected = (!isAccommodated && ((D.r + d.r) > distance)); | |
return { | |
node : node, | |
r : d.r, | |
distance : distance, | |
isIntersected : isIntersected, | |
isAccommodated: isAccommodated | |
}; | |
}) | |
.sort(function (a, b) { | |
return a.distance > b.distance; | |
}); | |
var intersetionNode, accommodatedNodes; | |
intersetionNode = siblings | |
.find(function (d) { return d.isIntersected; }); | |
accommodatedNodes = siblings | |
.filter(function (d) { return d.isAccommodated; }) | |
.sort(function (a, b) { return a.r - b.r; }); | |
// 1) intersection node come first, choose the closest one | |
if (intersetionNode) { | |
if (mergeNode) { d3.select(mergeNode).classed("svg__active", false); } | |
mergeNode = intersetionNode.node; | |
d3.select(mergeNode).classed("svg__active", true); | |
isForceLayoutTriggered = true; | |
} | |
// 2) seconde line shoule be some big ancestors node | |
else if (accommodatedNodes.length) { | |
if (mergeNode) { d3.select(mergeNode).classed("svg__active", false); } | |
mergeNode = accommodatedNodes[0].node; | |
d3.select(mergeNode).classed("svg__active", true); | |
} | |
// 3) must be moving under the root | |
else { | |
if (mergeNode) { d3.select(mergeNode).classed("svg__active", false); } | |
mergeNode = null; | |
} | |
}) | |
.on("end", function () { | |
gMain | |
.selectAll(".svg__active") | |
.classed("svg__active", false); | |
reCalculateAndLayout(); | |
}); | |
function onNodeContextmenu (d) { | |
d3.event.preventDefault(); | |
var clue = findItemFromHierarchy(rData, d); | |
clue.removedFromParent(); | |
iniRoot(rData); | |
nodesUpdate(nodes); | |
} | |
function nodesUpdate (nodes) { | |
var u = gMain.selectAll(".svg__node") | |
.data( | |
nodes.slice(1, nodes.length), // pharse out the root node | |
function (d) { return d.data.name; } | |
); | |
u.enter() | |
.append("g") | |
.attr("class", d => { return d.children ? "svg__node svg__node-wrapper" : "svg__node"; }) | |
.call(nodeDrag) | |
.on("contextmenu", onNodeContextmenu) | |
.each(function (d) { | |
var self = d3.select(this); | |
// add circle backgorund, circle is event-responsible | |
self.append("circle") | |
.attr("fill", d.children ? null : d.data.color) | |
.attr("r", d.r); | |
// add text content for leaf nodes | |
if (!d.children) { | |
self.append("text") | |
.attr("class", "svg__text") | |
.attr("y", -20) | |
.html(`<tspan>< ${d.data.name} ></tspan> | |
<tspan x="0" dy="20">性别=男</tspan> | |
<tspan x="0" dy="20">年龄>30</tspan> | |
<tspan x="0" dy="20">. . .</tspan>`); | |
} | |
}) | |
.attr("transform", "translate(" + [w / 2, h / 2] + ")") | |
.merge(u) | |
.sort(function (a, b) { return b.r - a.r; }) | |
.each(function (d) { | |
// data rebine and change apprence | |
d3.select(this).select("circle").datum(d).attr("r", d.r); | |
// TODO: text should rebine if neccessory....... | |
}) | |
.transition() | |
.duration(800) | |
.attr("transform", d => { return "translate(" + [d.x, d.y] + ")"; }); | |
u.exit() | |
.attr("transform", d => { return "translate(" + [d.x, d.y] + ") scale(1) rotate(0)"; }) | |
.transition() | |
.duration(400) | |
.attr("transform", d => { return "translate(" + [d.x, d.y] + ") scale(0.01) rotate(90)"; }) | |
.remove(); | |
} | |
function findItemFromHierarchy (hierarchyData, nodeData) { | |
// [currentNode, parentNode, grandParentNode, ... root] | |
var path = nodeData.ancestors(), | |
length = path.length, | |
i = length - 2, | |
child = hierarchyData, | |
parent, | |
name; | |
while (i >= 0) { | |
name = path[i].data.name; | |
grandParent = parent; | |
parent = child; | |
child = child.children.find(function (d) { return d.name === name; }); | |
i--; | |
} | |
return { | |
target: child, | |
targetParent: parent, | |
removedFromParent: function () { | |
var index = this.targetParent.children | |
.findIndex(function (d) { return d === child; }); | |
this.targetParent.children.splice(index, 1); | |
}, | |
addNodeToParent: function (node) { | |
this.targetParent.children.push(node); | |
}, | |
addNodeToSelf: function (node) { | |
this.target.children.push(node); | |
} | |
}; | |
} | |
function addItemsToHierarchy (itemArray, hierarchyData) { | |
Array.prototype.apply(hierarchyData.children, itemArray); | |
iniRoot(hierarchyData); | |
nodesUpdate(nodes); | |
} | |
function reCalculateAndLayout () { | |
/* dragedNode, mergeNode, isForceLayoutTriggered | |
1. isForceLayoutTriggered == false, nothing had happend | |
2. isForceLayoutTriggered == true, | |
-2.1 megeNode is null means dragedNode move under the root, | |
IF: dragedNode is the child of root let it alone, | |
OR: move the drgedNode to the root and relayout the whole chart. | |
-2.2 mergeNode have value. | |
checking the dragedNode and mergeNode relationship and do some accordingly. | |
this step could be tricky and should comply with the requirements. | |
*/ | |
if (!isForceLayoutTriggered) { return; } | |
var drageDatum = d3.select(dragedNode).datum(), | |
mergeDatum = mergeNode ? d3.select(mergeNode).datum() : null; | |
if (!mergeNode && (drageDatum.parent === root)) { return; } | |
if (!mergeNode) { | |
var clue = findItemFromHierarchy(rData, drageDatum); | |
clue.removedFromParent(); | |
rData.children.push(clue.target); | |
} | |
else { | |
var dragClue = findItemFromHierarchy(rData, drageDatum); | |
var mergeClue = findItemFromHierarchy(rData, mergeDatum); | |
// if both are parent node, merge dragNode to mergeNode | |
if (drageDatum.children && mergeDatum.children) { | |
dragClue.removedFromParent(); | |
mergeClue.addNodeToSelf(dragClue.target); | |
} | |
// if both are prime node, push dragNode to mergeNode's parent | |
else if (!drageDatum.children && !mergeDatum.children) { | |
dragClue.removedFromParent(); | |
mergeClue.removedFromParent(); | |
mergeClue.addNodeToParent({ | |
name: uuid(), | |
children: [dragClue.target, mergeClue.target] | |
}); | |
} | |
// one is parentNode the other is prime node | |
else { | |
if (drageDatum.children) { | |
mergeClue.removedFromParent(); | |
dragClue.addNodeToSelf(mergeClue.target); | |
} | |
if (mergeDatum.children) { | |
dragClue.removedFromParent(); | |
mergeClue.addNodeToSelf(dragClue.target); | |
} | |
} | |
} | |
iniRoot(rData); | |
nodesUpdate(nodes); | |
} | |
iniRoot(rData); | |
nodesUpdate(nodes); | |
</script> | |
</body> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment