Skip to content

Instantly share code, notes, and snippets.

@1Cr18Ni9
Last active September 30, 2019 08:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save 1Cr18Ni9/f3ffebcf9f97abb57c0a35d1bb92d03a to your computer and use it in GitHub Desktop.
Save 1Cr18Ni9/f3ffebcf9f97abb57c0a35d1bb92d03a to your computer and use it in GitHub Desktop.
dynamic pack
license: mit
  • 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 是一个树对象,其会随着节点的增加和减少而变化。

<!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>&lt; ${d.data.name} &gt;</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