(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);