(function (window, d3) {
"use strict";
if (window.PackTree) { return console.log("PackTree has been loaded!"); }
// svg filter
d3.select(document.body)
.append("svg")
.attr("class", "filter_only_svg")
.attr("style", "position:absolute;width:0;height:0;z-index:-5;")
.html(' \
\
\
\
\
\
\
\
\
'
);
function clearEmptyOrOnlyOneChildLogicalNode (node, parent) {
// checking if node is leaf node
if (node.type == "data") { return; }
var index;
(parent && (index = parent.children.findIndex(function (d) { return d === node; })));
if (parent && node.children.length == 0) {
parent.children.splice(index, 1);
}
else if (parent && node.children.length == 1) {
var nNode = node.children[0];
parent.children.push(nNode);
parent.children.splice(index, 1);
clearEmptyOrOnlyOneChildLogicalNode(nNode, parent);
}
else {
for (var i = 0, l = node.children.length; i < l; i++) {
clearEmptyOrOnlyOneChildLogicalNode(node.children[i], node);
}
}
}
var rootInit = (function () {
var loosePack = d3.pack().padding(15);
var sum = function (d) { return d.value; },
sort = function (a, b) { return b.r - a.r; },
andNodeOnly = function (node) { return node.data.type == "and"; },
nodeLocReassignment = function (node) {
// container origin is the offset for children
var x = node.x, y = node.y,
circle,
childNodes = node.children;
d3.packSiblings(childNodes);
childNodes.forEach(function (d) {
d.x += x;
d.y += y;
});
circle = d3.packEnclose(childNodes);
node.r = circle.r;
};
var f = function () {
clearEmptyOrOnlyOneChildLogicalNode(this._tree);
var root = d3.hierarchy(this._tree).sum(sum).sort(sort),
nodes = root.descendants();
loosePack.radius(() => this._r).size([this._w, this._h])(root);
// find all "and" container and relocat all data nodes
nodes
.filter(andNodeOnly)
.forEach(nodeLocReassignment);
return nodes;
};
return f;
})();
var uuid = (function () {
var i = 0, prefix = "w", f;
f = function () { return prefix + "" + i++; };
f.prefix = function (v) { prefix = v; return f; };
return f;
})();
// extract tspan text based on bind data
var appendText = function (selection, d, clipId) {
var f = [],
i = 0,
q = d.data.bind.querys,
y = {"4": -30, "3": -20, "2": -10, "1": 0};
f.push("[" + d.data.bind.collection + "]");
while (q[i] && i < 3) {
f.push(q[i].name + " " + q[i].logic + " " + q[i].query);
i++;
}
selection.select(".svg__text").remove();
selection.append("text")
.attr("class", "svg__text")
.attr("y", y[f.length])
.attr("clip-path", "url(#" + clipId + ")")
.selectAll("tspan")
.data(f)
.enter()
.append("tspan")
.attr("x", (j, i) => i ? 0 : null)
.attr("dy", (j, i) => i ? 20 : null)
.text(j => j);
};
// vm dialog callback function
var dataNodeUpdateFn = function (pack) {
/**
* _node: dom node
* _data: obj.bind
*/
var _node, _data, fn;
fn = function (bind) {
// update tree
var child = findItemFromHierarchy(pack._tree, _data).child;
child.bind = JSON.parse(JSON.stringify(bind));
// update node
var node = d3.select(_node);
node.datum().data.bind = bind;
node.call(appendText, node.datum(), pack._clipId);
node.select("circle")
.attr("r", 70)
.transition()
.duration(1000)
.ease(d3.easeBounceOut)
.attr("r", pack._r)
.attr("fill", d => d.data.bind.color);
_node = null;
_data = null;
};
fn.node = function (n) { _node = n; return fn; };
fn.data = function (d) { _data = d; return fn; };
return fn;
};
function TreeNode (child, parent, grandParent) {
this.child = child;
this.parent = parent;
this.grandParent = grandParent;
}
TreeNode.prototype = {
parentRemoveNode: function (childNode) {
var index = this.parent.children.findIndex(function (node) {
return node === childNode;
});
if (index < 0) {
return console.error("child node is NOT exist in parent node!");
}
this.parent.children.splice(index, 1);
},
removeChild: function () {
this.parentRemoveNode(this.child);
},
parentAdd: function (node) { // an node or an node array
if (node instanceof Array) {
Array.prototype.push.apply(this.parent.children, node);
}
else {
this.parent.children.push(node);
}
},
childAdd: function (node) { // an node or an node array
if (!this.child.children) {
return console.error("child is a prime node that can NOT contain any sub nodes.");
}
if (node instanceof Array) {
Array.prototype.push.apply(this.child.children, node);
}
else {
this.child.children.push(node);
}
}
};
function findItemFromHierarchy (hierarchyData, nodeData) {
// [currentNode, parentNode, grandParentNode, ... root]
var path = nodeData.ancestors(),
length = path.length,
i = length - 2,
child = hierarchyData,
grandParent,
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 new TreeNode(child, parent, grandParent);
}
// General pick for all PackTreee instance,
// only one PackTree is focused by PackTree.focusMe().
var ctrPick = (function () {
var pickedNodes = [],
pack = null;
// global keypress listener
var keyUp = function () {
// In case ctr is pressed while pressing other keys
if (d3.event.key.toUpperCase() !== "CONTROL") { return; }
// remove keyUp listener
d3.select(this).on("keyup.ctr", null);
var subChildren = [],
rootChildren = pack._tree.children;
// if picked node is less than 2 or all ROOT nodes are picked will do nothing
//if (pickedNodes.length < 2 || pickedNodes.length == rootChildren.length ) {
if (pickedNodes.length < 2) {
pack.gMain.selectAll(".svg__picked").classed("svg__picked", false);
return;
}
rootChildren.push({
name: uuid.prefix("o")(),
type: "or",
children: subChildren
});
pickedNodes.forEach(function (node) {
var name, index, target;
name = d3.select(node).datum().data.name;
index = rootChildren.findIndex(function (d) { return d.name === name; });
target = rootChildren.splice(index, 1)[0];
subChildren.push(target);
});
pack.gMain.selectAll(".svg__picked").classed("svg__picked", false);
pack.nodesUpdate(pack.rootInit());
};
var onKeydown = function () {
if (d3.event.key.toUpperCase() !== "CONTROL" || !pack) { return; }
pickedNodes = []; // clear content
d3.select(this).on("keyup.ctr", keyUp);
};
d3.select(window).on("keydown.ctr", onKeydown);
var click = function (d) {
/**
* 1) Ctrl key should be enabled
* 2) only dataNode is pickable
* 3) dataNode but its parent node is orNode and is not the ROOT
*/
if (
!d3.event.ctrlKey ||
d.data.type !== "data" ||
(d.parent.data.type == "or" && d.parent.data.name !== "ROOT")
) { return; }
var self = this, index;
if (d.parent.data.type == "and") {
self = pack.gMain
.selectAll(".svg__node")
.filter(function ($) { return $ === d.parent; })
.nodes()[0];
}
self.classList.toggle("svg__picked");
index = pickedNodes.findIndex(function (node) {
return node === self;
});
// if this node is included in pickNodes remove it,
// or push it into pickNodes.
index > -1 ? pickedNodes.splice(index, 1) : pickedNodes.push(self);
};
return {
clickFn: click,
clearNodes: function () {
pickedNodes = [];
return this;
},
setPack: function (v) {
if (!arguments.length) { return pack; }
pack = v;
return this;
}
};
})();
var nodeDragFn = function (pack) {
// nodeList is a live HTMLCollection object which contained all g-nodes under gMain
// nodesArray is an alternaltive to nodeList except needed repeately updated
var nodeList, nodesArray = [];
var targetNode = null, // node element, this element will comparing with matchNodes
triggeredNode = null, // one of "matchNodes" member which have insection with targetNode
relatedNodes = [], // targetNode's related nodes, if any.
matchNodes = []; // targetNode's match node array.
// calculation functions
var slice = Array.prototype.slice,
getDistance = function (c1, c2) {
return Math.sqrt((c1.x - c2.x) * (c1.x - c2.x) + (c1.y - c2.y) * (c1.y - c2.y));
},
innerReset = function () {
targetNode = null;
triggeredNode = null;
relatedNodes = [];
matchNodes = [];
nodesArray = [];
};
function onDragStart (d) {
// In case onEnd event don't trigged by user,
// reassure every thing on the same page.
innerReset();
// update nodesArray
nodeList = nodeList || pack.gMain.node().getElementsByClassName("svg__node");
nodesArray = slice.call(nodeList, 0);
var self = this, parent = d.parent, descendants;
if (parent.data.type == "or") {
targetNode = self;
descendants = d.descendants();
nodesArray.forEach(function (node) {
if (self === node) { return; }
var datum = d3.select(node).datum(),
children = parent.children; // or-Node's children
if (children.includes(datum)) { matchNodes.push(node); return; }
if (d.children && descendants.includes(datum)) { relatedNodes.push(node); }
});
}
else { // d.parent.data.type == "and"
targetNode = nodesArray.find(function (node) {
return d3.select(node).datum() === parent;
});
descendants = parent.descendants();
nodesArray.forEach(function (node) {
//if (self === node) { return; }
var datum = d3.select(node).datum(),
children = parent.parent.children; // or-Node's children
if (descendants.includes(datum)) { relatedNodes.push(node); return; }
if (children.includes(datum)) { matchNodes.push(node); }
});
}
} // onDragStart__End
function onDrag (d) {
var x = d3.event.x, y = d3.event.y,
dx = d3.event.dx, dy = d3.event.dy;
/**
* Detecting if targetNode moved outside of parentNode's domain,
* if it does stop coordinates updating by skipping onDrag loop.
* targetNode under the ROOT will be excepted.
*/
var moveCircle = (this === targetNode ? d : d3.select(targetNode).datum()),
fenceCircle = moveCircle.parent,
maxDis1 = fenceCircle.r - d.r - 1,
maxDis2 = fenceCircle.r - moveCircle.r - 1;
if (
(fenceCircle.data.name != "ROOT") &&
(
((moveCircle === d) && (getDistance(fenceCircle, {x: x, y: y}) > maxDis1)) ||
((moveCircle !== d) && (getDistance(fenceCircle, {x: moveCircle.x + dx, y: moveCircle.y + dy}) > maxDis2))
)
) {
return;
} // true: stop onDrag loop, code underbelow will not be excuted.
if (this === targetNode) {
Object.assign(d, {x: x, y: y});
d3.select(this).attr("transform", "translate(" + [x, y] + ")");
}
relatedNodes.forEach(function (node) {
var _node = d3.select(node), datum = _node.datum();
datum.x += dx;
datum.y += dy;
_node.attr("transform", "translate(" + [datum.x, datum.y] + ")");
});
/**
* This code block used for testing if targetNode have intersection with matchNodes.
* Find the triggeredNode which is the closest node to targetNode and should
* have intersection with it too.
*/
var wanted = matchNodes.map(function (node) {
var dm = d3.select(node).datum(),
dt = d3.select(targetNode).datum(),
distance = getDistance(dm, dt),
isIntersected = (distance <= Math.abs(dt.r + dm.r));
return {
node : node,
distance : distance,
isIntersected: isIntersected
};
})
.sort(function (a, b) { return a.distance - b.distance; })
.find(function (d) { return d.isIntersected; });
(triggeredNode && triggeredNode.classList.remove("svg__active"));
if (wanted) {
triggeredNode = wanted.node;
triggeredNode.classList.toggle("svg__active");
}
else {
triggeredNode = null;
}
d3.select(targetNode).classed("svg__active", true);
} // onDrag End
function onDragEnd (d) {
pack.gMain.selectAll(".svg__active").classed("svg__active", false);
if (!triggeredNode) { return innerReset(); }
var target = d3.select(targetNode).datum(),
trigger = d3.select(triggeredNode).datum(),
tt = findItemFromHierarchy(pack._tree, target),
tg = findItemFromHierarchy(pack._tree, trigger);
// 1) If two dataNodes come close merge them into a new andNode
if (target.data.type == "data" && trigger.data.type == "data") {
tt.removeChild();
tg.removeChild();
tt.parentAdd({
name: uuid.prefix("a")(),
type: "and",
children: [tt.child, tg.child]
});
}
// 2) If two andNodes come close tt grow into a bigger andNode and remove tg
else if (target.data.type == "and" && trigger.data.type == "and") {
tg.removeChild();
tt.childAdd(tg.child.children);
}
// 3) If two orNodes come close merge them into a new bigger orNode
else if (target.data.type == "or" && trigger.data.type == "or") {
tt.removeChild();
tg.removeChild();
tg.parentAdd({
name: uuid.prefix("o")(),
type: "or",
children: Array.prototype.concat([], tt.child.children, tg.child.children)
});
}
// 4) one node is dataNode and another is logic node(wrapper node)
// dataNode will be merged into that logic node
else if (
(target.data.type == "data" && trigger.data.type != "data") ||
(trigger.data.type == "data" && target.data.type != "data")
) {
if (target.data.type == "data") {
tt.removeChild();
tg.childAdd(tt.child);
}
else {
tg.removeChild();
tt.childAdd(tg.child);
}
}
// _______________ UNTESTED _______________
// 5) both are logic node but NOT the same type
// andNode will be merged into orNode
else {
if (target.data.type == "and" && trigger.data.type == "or") {
tt.removeChild();
tg.childAdd(tt.child);
}
else if (trigger.data.type == "and" && target.data.type == "or") {
tg.removeChild();
tt.childAdd(tg.child);
}
}
pack.nodesUpdate(pack.rootInit());
// release memory
innerReset();
} // onDragEnd End
return d3.drag()
.filter(function () {
return !d3.event.ctrlKey && !d3.event.button;
})
.on("start",onDragStart)
.on("drag", onDrag)
.on("end", onDragEnd);
}; // nodeDragFn END
function nodesUpdate (nodes) {
var u = this.gMain.selectAll(".svg__node")
.data(nodes.slice(1), function (d) { return d.data.name; });
var clipId = this._clipId;
u.enter()
.append("g")
.attr("class", d => { return "svg__node svg__node-" + d.data.type; })
.call(this.onNodeDrag)
.on("click", ctrPick.clickFn)
.on("dblclick", this.onDataNodeDBLClick)
.on("contextmenu", this.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.bind.color)
.attr("r", d.r);
// add text content for leaf nodes
if (!d.children) {
self.call(appendText, d, clipId);
}
})
.attr("transform", "translate(" + [this._w / 2, this._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 PackTree () {
this._tree = { name: "ROOT", type: "or", children: [] };
this._w = 1000;
this._h = 550;
this._r = 50;
this._clipId = uuid.prefix("clip")();
this._count = 0;
Object.defineProperty(this, "count", {
get: function () { return this._count; },
set: function (v) {
this._count = v;
if (this.onCount) { this.onCount(this._count); }
}
});
this.svg = null;
this.gMain = null;
this.vm = null; // pop window
var self = this;
this.dataNodeUpdate = dataNodeUpdateFn(this);
this.onNodeDrag = nodeDragFn(this);
this.onDataNodeDBLClick = function (d) {
if (d3.event.ctrlKey || d.data.type !== "data" || !self.vm) { return; }
self.vm.show(
self.dataNodeUpdate.data(d).node(this),
JSON.parse(JSON.stringify(d.data.bind))
);
}; // onDataNodeDBLClick END
this.onNodeContextmenu = function (d) {
d3.event.preventDefault();
var node = findItemFromHierarchy(self._tree, d);
// dataNode under the ROOT will be removed after user confirmed
if (node.parent.name == "ROOT" && node.child.type == "data") {
if (window.confirm("确认删除该节点?")) {
node.removeChild();
self.count -= 1;
return self.nodesUpdate(self.rootInit());
}
return;
}
// this code block will never be excuted as for
// orNode point-event is set to none.
if (node.child.type == "or") {
node.removeChild();
node.parentAdd(node.child.children);
return self.nodesUpdate(self.rootInit());
}
if (
(node.child.type == "data") &&
(node.parent.type == "and" || node.parent.type == "or")
) {
node.removeChild();
node.grandParent.children.push(node.child);
return self.nodesUpdate(self.rootInit());
}
}; // onNodeContextmenu END
}
// chainabel configure functions
PackTree.prototype.width = function (w) {
if (!arguments.length) { return this._w; }
this._w = w;
return this;
};
PackTree.prototype.height = function (h) {
if (!arguments.length) { return this._h; }
this._h = h;
return this;
};
PackTree.prototype.radius = function (r) {
if (!arguments.length) { return this._r; }
this._r = (r < 50 ? 50 : r);
return this;
};
PackTree.prototype.vForm = function (vm) {
if (!arguments.length) { return this.vm; }
this.vm = vm;
return this;
};
// Make ctrPick focus this instance so that subsequent
// ctr + mouse pick actions will be functional on this instance.
PackTree.prototype.focusMe = function () {
ctrPick.setPack(this);
return this;
};
PackTree.prototype.rootInit = rootInit;
PackTree.prototype.nodesUpdate = nodesUpdate;
PackTree.prototype.svgInit = function (id) {
var svg, gMain, vm = this.vm,
self = this,
clipId = this._clipId,
radius = this._r, w = this._w, h = this._h;
var formCallback = function (nodeData) {
self.addDataNode(nodeData);
};
var canvasDrag = d3.drag()
.subject(function () { return gMain.datum(); })
.on("drag", function () {
// if chart is empty diseabled drag event
if (!self._tree.children.length) { return; }
var x = d3.event.x, y = d3.event.y;
gMain.datum({x: x, y: y})
.attr("transform", "translate(" + [x, y] + ")");
});
svg = d3.select(id).append("svg")
.attr("xmlns", "http://www.w3.org/2000/svg")
.attr("version", "1.1")
.attr("tabIndex", 0) // focus: document.activeElement
.attr("class", "svg")
.attr("width", w)
.attr("height", h);
svg.append("defs")
.html('');
svg.append("rect")
.attr("class", "svg__bg")
.attr("width", w)
.attr("height", h)
.call(canvasDrag)
.on("contextmenu", function () {
d3.event.preventDefault();
if (vm) {
// vm.show(callbackFn, bind)
vm.show(formCallback);
}
});
gMain = svg.append("g")
.attr("class", "svg__main")
.datum({x: 0, y: 0})
.attr("transform", "translate(0,0)");
this.svg = svg;
this.gMain = gMain;
}; // svgInit END
PackTree.prototype.initialize = function (id, isFormPop) {
this.svgInit(id);
//this.rootInit()
if (isFormPop && this.vm && !this.vm.confirmCallbackFn) {
this.svg.select(".svg__bg").dispatch("contextmenu");
}
return this;
};
PackTree.prototype.addDataNode = function (bind) {
this._tree.children.push({
name: uuid.prefix("d")(),
type: "data",
bind: bind
});
this.count += 1;
this.nodesUpdate(this.rootInit());
};
PackTree.prototype.clearTree = function () {
this._tree.children = [];
this.count = 0;
this.nodesUpdate([]);
};
PackTree.prototype.toString = function (space) {
if (!space) { space = 0; }
return JSON.stringify(this._tree, null, space);
};
PackTree.prototype.toJSON = function () {
return JSON.parse(this.toString());
};
window.PackTree = PackTree;
})(window, d3);