UNPKG

dc.graph

Version:

Graph visualizations integrated with crossfilter and dc.js

1,635 lines (1,515 loc) 578 kB
/*! * dc.graph 0.9.8 * http://dc-js.github.io/dc.graph.js/ * Copyright 2015-2019 AT&T Intellectual Property & the dc.graph.js Developers * https://github.com/dc-js/dc.graph.js/blob/master/AUTHORS * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ (function() { function _dc_graph(d3, crossfilter, dc) { 'use strict'; /** * The entire dc.graph.js library is scoped under the **dc_graph** name space. It does not introduce * anything else into the global name space. * * Like in dc.js and most libraries built on d3, most `dc_graph` functions are designed to allow function chaining, meaning they return the current diagram * instance whenever it is appropriate. The getter forms of functions do not participate in function * chaining because they return values that are not the diagram. * @namespace dc_graph * @version 0.9.8 * @example * // Example chaining * diagram.width(600) * .height(400) * .nodeDimension(nodeDim) * .nodeGroup(nodeGroup); */ var dc_graph = { version: '0.9.8', constants: { CHART_CLASS: 'dc-graph' } }; function get_original(x) { return x.orig; } function identity(x) { return x; }; var property = function (defaultValue, unwrap) { if(unwrap === undefined) unwrap = get_original; else if(unwrap === false) unwrap = identity; var value = defaultValue, react = null; var cascade = []; var ret = function (_) { if (!arguments.length) { return value; } if(react) react(_); value = _; return this; }; ret.cascade = function (n, f) { for(var i = 0; i<cascade.length; ++i) { if(cascade[i].n === n) { if(f) cascade[i].f = f; else cascade.splice(i, 1); return ret; } else if(cascade[i].n > n) { cascade.splice(i, 0, {n: n, f: f}); return ret; } } cascade.push({n: n, f: f}); return ret; }; ret._eval = function(o, n) { if(n===0 || !cascade.length) return dc_graph.functor_wrap(ret(), unwrap)(o); else { var last = cascade[n-1]; return last.f(o, function() { return ret._eval(o, n-1); }); } }; ret.eval = function(o) { return ret._eval(o, cascade.length); }; ret.react = function(_) { if (!arguments.length) { return react; } react = _; return this; }; return ret; }; function named_children() { var _children = {}; var f = function(id, object) { if(arguments.length === 1) return _children[id]; if(f.reject) { var reject = f.reject(id, object); if(reject) { console.groupCollapsed(reject); console.trace(); console.groupEnd(); return this; } } // do not notify unnecessarily if(_children[id] === object) return this; if(_children[id]) _children[id].parent(null); _children[id] = object; if(object) object.parent(this); return this; }; f.enum = function() { return Object.keys(_children); }; f.nameOf = function(o) { var found = Object.entries(_children).find(function(kv) { return kv[1] == o; }); return found ? found[0] : null; }; return f; } function deprecated_property(message, defaultValue) { var prop = property(defaultValue); var ret = function() { if(arguments.length) { console.warn(message); prop.apply(property, arguments); return this; } return prop(); }; ['cascade', '_eval', 'eval', 'react'].forEach(function(method) { ret[method] = prop[method]; }); return ret; } function onetime_trace(level, message) { var said = false; return function() { if(said) return; if(level === 'trace') { console.groupCollapsed(message); console.trace(); console.groupEnd(); } else console[level](message); said = true; }; } function deprecation_warning(message) { return onetime_trace('warn', message); } function trace_function(level, message, f) { var dep = onetime_trace(level, message); return function() { dep(); return f.apply(this, arguments); }; } function deprecate_function(message, f) { return trace_function('warn', message, f); } // http://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript function uuid() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); return v.toString(16); }); } function is_ie() { var ua = window.navigator.userAgent; return(ua.indexOf('MSIE ') > 0 || ua.indexOf('Trident/') > 0 || ua.indexOf('Edge/') > 0); } function is_safari() { return /^((?!chrome|android).)*safari/i.test(navigator.userAgent); } // polyfill Object.assign for IE // it's just too useful to do without if (typeof Object.assign != 'function') { // Must be writable: true, enumerable: false, configurable: true Object.defineProperty(Object, "assign", { value: function assign(target, varArgs) { // .length of function is 2 'use strict'; if (target == null) { // TypeError if undefined or null throw new TypeError('Cannot convert undefined or null to object'); } var to = Object(target); for (var index = 1; index < arguments.length; index++) { var nextSource = arguments[index]; if (nextSource != null) { // Skip over if undefined or null for (var nextKey in nextSource) { // Avoid bugs when hasOwnProperty is shadowed if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { to[nextKey] = nextSource[nextKey]; } } } } return to; }, writable: true, configurable: true }); } // https://tc39.github.io/ecma262/#sec-array.prototype.includes if (!Array.prototype.includes) { Object.defineProperty(Array.prototype, 'includes', { value: function(valueToFind, fromIndex) { if (this == null) { throw new TypeError('"this" is null or not defined'); } // 1. Let O be ? ToObject(this value). var o = Object(this); // 2. Let len be ? ToLength(? Get(O, "length")). var len = o.length >>> 0; // 3. If len is 0, return false. if (len === 0) { return false; } // 4. Let n be ? ToInteger(fromIndex). // (If fromIndex is undefined, this step produces the value 0.) var n = fromIndex | 0; // 5. If n >= 0, then // a. Let k be n. // 6. Else n < 0, // a. Let k be len + n. // b. If k < 0, let k be 0. var k = Math.max(n >= 0 ? n : len - Math.abs(n), 0); function sameValueZero(x, y) { return x === y || (typeof x === 'number' && typeof y === 'number' && isNaN(x) && isNaN(y)); } // 7. Repeat, while k < len while (k < len) { // a. Let elementK be the result of ? Get(O, ! ToString(k)). // b. If SameValueZero(valueToFind, elementK) is true, return true. if (sameValueZero(o[k], valueToFind)) { return true; } // c. Increase k by 1. k++; } // 8. Return false return false; } }); } if (!Object.entries) { Object.entries = function( obj ){ var ownProps = Object.keys( obj ), i = ownProps.length, resArray = new Array(i); // preallocate the Array while (i--) resArray[i] = [ownProps[i], obj[ownProps[i]]]; return resArray; }; } // https://github.com/KhaledElAnsari/Object.values Object.values = Object.values ? Object.values : function(obj) { var allowedTypes = ["[object String]", "[object Object]", "[object Array]", "[object Function]"]; var objType = Object.prototype.toString.call(obj); if(obj === null || typeof obj === "undefined") { throw new TypeError("Cannot convert undefined or null to object"); } else if(!~allowedTypes.indexOf(objType)) { return []; } else { // if ES6 is supported if (Object.keys) { return Object.keys(obj).map(function (key) { return obj[key]; }); } var result = []; for (var prop in obj) { if (obj.hasOwnProperty(prop)) { result.push(obj[prop]); } } return result; } }; function getBBoxNoThrow(elem) { // firefox seems to have issues with some of my texts // just catch for now try { return elem.getBBox(); } catch(xep) { return {x: 0, y: 0, width:0, height: 0}; } } function property_if(pred, curr) { return function(o, last) { return pred(o) ? curr(o) : last(); }; } function property_interpolate(value, curr) { return function(o, last) { return d3.interpolate(last(o), curr(o))(value(o)); }; } function multiply_properties(pred, props, blend) { var props2 = {}; for(var p in props) props2[p] = blend(pred, param(props[p])); return props2; } function conditional_properties(pred, props) { return multiply_properties(pred, props, property_if); } function node_edge_conditions(npred, epred, props) { var nprops = {}, eprops = {}, badprops = []; for(var p in props) { if(/^node/.test(p)) nprops[p] = props[p]; else if(/^edge/.test(p)) eprops[p] = props[p]; else badprops.push(p); } if(badprops.length) console.error('only know how to deal with properties that start with "node" or "edge"', badprops); var props2 = npred ? conditional_properties(npred, nprops) : {}; if(epred) Object.assign(props2, conditional_properties(epred, eprops)); return props2; } function cascade(parent) { return function(level, add, props) { for(var p in props) { if(!parent[p]) throw new Error('unknown attribute ' + p); parent[p].cascade(level, add ? props[p] : null); } return parent; }; } function compose(f, g) { return function() { return f(g.apply(null, arguments)); }; } // version of d3.functor that optionally wraps the function with another // one, if the parameter is a function dc_graph.functor_wrap = function (v, wrap) { if(typeof v === "function") { return wrap ? function(x) { return v(wrap(x)); } : v; } else return function() { return v; }; }; // we want to allow either values or functions to be passed to specify parameters. // if a function, the function needs a preprocessor to extract the original key/value // pair from the wrapper object we put it in. function param(v) { return dc_graph.functor_wrap(v, get_original); } // http://jsperf.com/cloning-an-object/101 function clone(obj) { var target = {}; for(var i in obj) { if(obj.hasOwnProperty(i)) { target[i] = obj[i]; } } return target; } // because i don't think we need to bind edge point data (yet!) var bez_cmds = { 1: 'L', 2: 'Q', 3: 'C' }; function generate_path(pts, bezDegree, close) { var cats = ['M', pts[0].x, ',', pts[0].y], remain = bezDegree; var hasNaN = false; for(var i = 1; i < pts.length; ++i) { if(isNaN(pts[i].x) || isNaN(pts[i].y)) hasNaN = true; cats.push(remain===bezDegree ? bez_cmds[bezDegree] : ' ', pts[i].x, ',', pts[i].y); if(--remain===0) remain = bezDegree; } if(remain!=bezDegree) console.log("warning: pts.length didn't match bezian degree", pts, bezDegree); if(close) cats.push('Z'); return cats.join(''); } // for IE (do we care really?) Math.hypot = Math.hypot || function() { var y = 0; var length = arguments.length; for (var i = 0; i < length; i++) { if (arguments[i] === Infinity || arguments[i] === -Infinity) { return Infinity; } y += arguments[i] * arguments[i]; } return Math.sqrt(y); }; // outputs the array with adjacent identical lines collapsed to one function uniq(a) { var ret = []; a.forEach(function(x, i) { if(i === 0 || x !== a[i-1]) ret.push(x); }); return ret; } // https://tc39.github.io/ecma262/#sec-array.prototype.find if (!Array.prototype.find) { Object.defineProperty(Array.prototype, 'find', { value: function(predicate) { // 1. Let O be ? ToObject(this value). if (this == null) { throw new TypeError('"this" is null or not defined'); } var o = Object(this); // 2. Let len be ? ToLength(? Get(O, "length")). var len = o.length >>> 0; // 3. If IsCallable(predicate) is false, throw a TypeError exception. if (typeof predicate !== 'function') { throw new TypeError('predicate must be a function'); } // 4. If thisArg was supplied, let T be thisArg; else let T be undefined. var thisArg = arguments[1]; // 5. Let k be 0. var k = 0; // 6. Repeat, while k < len while (k < len) { // a. Let Pk be ! ToString(k). // b. Let kValue be ? Get(O, Pk). // c. Let testResult be ToBoolean(? Call(predicate, T, << kValue, k, O >>)). // d. If testResult is true, return kValue. var kValue = o[k]; if (predicate.call(thisArg, kValue, k, o)) { return kValue; } // e. Increase k by 1. k++; } // 7. Return undefined. return undefined; } }); } var script_path = function() { var _path; return function() { if(_path === undefined) { // adapted from http://stackoverflow.com/a/18283141/676195 _path = null; // only try once var filename = 'dc.graph.js'; var scripts = document.getElementsByTagName('script'); if (scripts && scripts.length > 0) { for (var i in scripts) { if (scripts[i].src && scripts[i].src.match(new RegExp(filename+'$'))) { _path = scripts[i].src.replace(new RegExp('(.*)'+filename+'$'), '$1'); break; } } } } return _path; }; }(); dc_graph.event_coords = function(diagram) { var bound = diagram.root().node().getBoundingClientRect(); return diagram.invertCoord([d3.event.clientX - bound.left, d3.event.clientY - bound.top]); }; function promise_identity(x) { return Promise.resolve(x); } // http://stackoverflow.com/questions/7044944/jquery-javascript-to-detect-os-without-a-plugin var is_a_mac = navigator.platform.toUpperCase().indexOf('MAC')!==-1; // https://stackoverflow.com/questions/16863917/check-if-class-exists-somewhere-in-parent-vanilla-js function ancestor_has_class(element, classname) { if(d3.select(element).classed(classname)) return true; return element.parentElement && ancestor_has_class(element.parentElement, classname); } if (typeof SVGElement.prototype.contains == 'undefined') { SVGElement.prototype.contains = HTMLDivElement.prototype.contains; } // arguably depth first search is a stupid algorithm to modularize - // there are many, many interesting moments to insert a behavior // and those end up being almost bigger than the function itself // this is an argument for providing a graph API which could make it // easy to just write a recursive function instead of using this dc_graph.depth_first_traversal = function(callbacks) { // {[init, root, row, tree, place, sib, push, pop, skip,] finish, nodeid, sourceid, targetid} return function(nodes, edges) { callbacks.init && callbacks.init(); if(callbacks.tree) edges = edges.filter(function(e) { return callbacks.tree(e); }); var indegree = {}; var outmap = edges.reduce(function(m, e) { var tail = callbacks.sourceid(e), head = callbacks.targetid(e); if(!m[tail]) m[tail] = []; m[tail].push(e); indegree[head] = (indegree[head] || 0) + 1; return m; }, {}); var nmap = nodes.reduce(function(m, n) { var key = callbacks.nodeid(n); m[key] = n; return m; }, {}); var rows = []; var placed = {}; function place_tree(n, r) { var key = callbacks.nodeid(n); if(placed[key]) { callbacks.skip && callbacks.skip(n, indegree[key]); return; } if(!rows[r]) rows[r] = []; callbacks.place && callbacks.place(n, r, rows[r]); rows[r].push(n); placed[key] = true; if(outmap[key]) outmap[key].forEach(function(e, ei) { var target = nmap[callbacks.targetid(e)]; if(ei && callbacks.sib) callbacks.sib(false, nmap[callbacks.targetid(outmap[key][ei-1])], target); callbacks.push && callbacks.push(); place_tree(target, r+1); }); callbacks.pop && callbacks.pop(n); } var roots; if(callbacks.root) roots = nodes.filter(function(n) { return callbacks.root(n); }); else { roots = nodes.filter(function(n) { return !indegree[callbacks.nodeid(n)]; }); if(nodes.length && !roots.length) // all nodes are in a cycle roots = [nodes[0]]; } roots.forEach(function(n, ni) { if(ni && callbacks.sib) callbacks.sib(true, roots[ni-1], n); callbacks.push && callbacks.push(); place_tree(n, callbacks.row && callbacks.row(n) || 0); }); callbacks.finish(rows); }; }; // basically, see if it's any simpler if we start from scratch // (well, of course it's simpler because we have less callbacks) // same caveats as above dc_graph.undirected_dfs = function(callbacks) { // {[comp, node], nodeid, sourceid, targetid} return function(nodes, edges) { var adjacencies = edges.reduce(function(m, e) { var tail = callbacks.sourceid(e), head = callbacks.targetid(e); if(!m[tail]) m[tail] = []; if(!m[head]) m[head] = []; m[tail].push(head); m[head].push(tail); return m; }, {}); var nmap = nodes.reduce(function(m, n) { var key = callbacks.nodeid(n); m[key] = n; return m; }, {}); var found = {}; function recurse(n) { var nid = callbacks.nodeid(n); callbacks.node(compid, n); found[nid] = true; if(adjacencies[nid]) adjacencies[nid].forEach(function(adj) { if(!found[adj]) recurse(nmap[adj]); }); } var compid = 0; nodes.forEach(function(n) { if(!found[callbacks.nodeid(n)]) { callbacks.comp && callbacks.comp(compid); recurse(n); ++compid; } }); }; }; // create or re-use objects in a map, delete the ones that were not reused function regenerate_objects(preserved, list, need, key, assign, create, destroy) { if(!create) create = function(k, o) { }; if(!destroy) destroy = function(k) { }; var keep = {}; function wrap(o) { var k = key(o); if(!preserved[k]) create(k, preserved[k] = {}, o); var o1 = preserved[k]; assign(o1, o); keep[k] = true; return o1; } var wlist = list.map(wrap); if(need) need.forEach(function(k) { if(!preserved[k]) { // hasn't been created, needs to be create(k, preserved[k] = {}, null); assign(preserved[k], null); } if(!keep[k]) { // wasn't in list, should be wlist.push(preserved[k]); keep[k] = true; } }); // delete any objects from last round that are no longer used for(var k in preserved) if(!keep[k]) { destroy(k, preserved[k]); delete preserved[k]; } return wlist; } function point_on_ellipse(A, B, dx, dy) { var tansq = Math.tan(Math.atan2(dy, dx)); tansq = tansq*tansq; // why is this not just dy*dy/dx*dx ? ? var ret = {x: A*B/Math.sqrt(B*B + A*A*tansq), y: A*B/Math.sqrt(A*A + B*B/tansq)}; if(dx<0) ret.x = -ret.x; if(dy<0) ret.y = -ret.y; return ret; } var eps = 0.0000001; function between(a, b, c) { return a-eps <= b && b <= c+eps; } // Adapted from http://stackoverflow.com/questions/563198/how-do-you-detect-where-two-line-segments-intersect/1968345#1968345 function segment_intersection(x1,y1,x2,y2, x3,y3,x4,y4) { var x=((x1*y2-y1*x2)*(x3-x4)-(x1-x2)*(x3*y4-y3*x4)) / ((x1-x2)*(y3-y4)-(y1-y2)*(x3-x4)); var y=((x1*y2-y1*x2)*(y3-y4)-(y1-y2)*(x3*y4-y3*x4)) / ((x1-x2)*(y3-y4)-(y1-y2)*(x3-x4)); if (isNaN(x)||isNaN(y)) { return false; } else { if (x1>=x2) { if (!between(x2, x, x1)) {return false;} } else { if (!between(x1, x, x2)) {return false;} } if (y1>=y2) { if (!between(y2, y, y1)) {return false;} } else { if (!between(y1, y, y2)) {return false;} } if (x3>=x4) { if (!between(x4, x, x3)) {return false;} } else { if (!between(x3, x, x4)) {return false;} } if (y3>=y4) { if (!between(y4, y, y3)) {return false;} } else { if (!between(y3, y, y4)) {return false;} } } return {x: x, y: y}; } function point_on_polygon(points, x0, y0, x1, y1) { for(var i = 0; i < points.length; ++i) { var next = i===points.length-1 ? 0 : i+1; var isect = segment_intersection(points[i].x, points[i].y, points[next].x, points[next].y, x0, y0, x1, y1); if(isect) return isect; } return null; } // as many as we can get from // http://www.graphviz.org/doc/info/shapes.html dc_graph.shape_presets = { egg: { // not really: an ovoid should be two half-ellipses stuck together // https://en.wikipedia.org/wiki/Oval generator: 'polygon', preset: function() { return {sides: 100, distortion: -0.25}; } }, triangle: { generator: 'polygon', preset: function() { return {sides: 3}; } }, rectangle: { generator: 'polygon', preset: function() { return {sides: 4}; } }, diamond: { generator: 'polygon', preset: function() { return {sides: 4, rotation: 45}; } }, trapezium: { generator: 'polygon', preset: function() { return {sides: 4, distortion: -0.5}; } }, parallelogram: { generator: 'polygon', preset: function() { return {sides: 4, skew: 0.5}; } }, pentagon: { generator: 'polygon', preset: function() { return {sides: 5}; } }, hexagon: { generator: 'polygon', preset: function() { return {sides: 6}; } }, septagon: { generator: 'polygon', preset: function() { return {sides: 7}; } }, octagon: { generator: 'polygon', preset: function() { return {sides: 8}; } }, invtriangle: { generator: 'polygon', preset: function() { return {sides: 3, rotation: 180}; } }, invtrapezium: { generator: 'polygon', preset: function() { return {sides: 4, distortion: 0.5}; } }, square: { generator: 'polygon', preset: function() { return { sides: 4, regular: true }; } }, plain: { generator: 'rounded-rect', preset: function() { return { noshape: true }; } }, house: { generator: 'elaborated-rect', preset: function() { return { get_points: function(rx, ry) { return [ {x: rx, y: ry*2/3}, {x: rx, y: -ry/2}, {x: 0, y: -ry}, {x: -rx, y: -ry/2}, {x: -rx, y: ry*2/3} ]; }, minrx: 30 }; } }, invhouse: { generator: 'elaborated-rect', preset: function() { return { get_points: function(rx, ry) { return [ {x: rx, y: ry/2}, {x: rx, y: -ry*2/3}, {x: -rx, y: -ry*2/3}, {x: -rx, y: ry/2}, {x: 0, y: ry} ]; }, minrx: 30 }; } }, rarrow: { generator: 'elaborated-rect', preset: function() { return { get_points: function(rx, ry) { return [ {x: rx, y: ry}, {x: rx, y: ry*1.5}, {x: rx + ry*1.5, y: 0}, {x: rx, y: -ry*1.5}, {x: rx, y: -ry}, {x: -rx, y: -ry}, {x: -rx, y: ry} ]; }, minrx: 30 }; } }, larrow: { generator: 'elaborated-rect', preset: function() { return { get_points: function(rx, ry) { return [ {x: -rx, y: ry}, {x: -rx, y: ry*1.5}, {x: -rx - ry*1.5, y: 0}, {x: -rx, y: -ry*1.5}, {x: -rx, y: -ry}, {x: rx, y: -ry}, {x: rx, y: ry} ]; }, minrx: 30 }; } }, rpromoter: { generator: 'elaborated-rect', preset: function() { return { get_points: function(rx, ry) { return [ {x: rx, y: ry}, {x: rx, y: ry*1.5}, {x: rx + ry*1.5, y: 0}, {x: rx, y: -ry*1.5}, {x: rx, y: -ry}, {x: -rx, y: -ry}, {x: -rx, y: ry*1.5}, {x: 0, y: ry*1.5}, {x: 0, y: ry}, ]; }, minrx: 30 }; } }, lpromoter: { generator: 'elaborated-rect', preset: function() { return { get_points: function(rx, ry) { return [ {x: -rx, y: ry}, {x: -rx, y: ry*1.5}, {x: -rx - ry*1.5, y: 0}, {x: -rx, y: -ry*1.5}, {x: -rx, y: -ry}, {x: rx, y: -ry}, {x: rx, y: ry*1.5}, {x: 0, y: ry*1.5}, {x: 0, y: ry} ]; }, minrx: 30 }; } }, cds: { generator: 'elaborated-rect', preset: function() { return { get_points: function(rx, ry) { return [ {x: rx, y: ry}, {x: rx + ry, y: 0}, {x: rx, y: -ry}, {x: -rx, y: -ry}, {x: -rx, y: ry} ]; }, minrx: 30 }; } }, }; dc_graph.shape_presets.box = dc_graph.shape_presets.rect = dc_graph.shape_presets.rectangle; dc_graph.available_shapes = function() { var shapes = Object.keys(dc_graph.shape_presets); return shapes.slice(0, shapes.length-1); // not including polygon }; var default_shape = {shape: 'ellipse'}; function normalize_shape_def(diagram, n) { var def = diagram.nodeShape.eval(n); if(!def) return default_shape; if(typeof def === 'string') return {shape: def}; return def; } function elaborate_shape(diagram, def) { var shape = def.shape, def2 = Object.assign({}, def); delete def2.shape; if(shape === 'random') { var available = dc_graph.available_shapes(); // could include diagram.shape !== ellipse, polygon shape = available[Math.floor(Math.random()*available.length)]; } else if(diagram.shape.enum().indexOf(shape) !== -1) return diagram.shape(shape).elaborate({shape: shape}, def2); if(!dc_graph.shape_presets[shape]) { console.warn('unknown shape ', shape); return default_shape; } var preset = dc_graph.shape_presets[shape].preset(def2); preset.shape = dc_graph.shape_presets[shape].generator; return diagram.shape(preset.shape).elaborate(preset, def2); } function infer_shape(diagram) { return function(n) { var def = normalize_shape_def(diagram, n); n.dcg_shape = elaborate_shape(diagram, def); n.dcg_shape.abstract = def; }; } function shape_changed(diagram) { return function(n) { var def = normalize_shape_def(diagram, n); var old = n.dcg_shape.abstract; if(def.shape !== old.shape) return true; else if(def.shape === 'polygon') { return def.shape.sides !== old.sides || def.shape.skew !== old.skew || def.shape.distortion !== old.distortion || def.shape.rotation !== old.rotation; } else return false; }; } function node_label_padding(diagram, n) { var nlp = diagram.nodeLabelPadding.eval(n); if(typeof nlp === 'number' || typeof nlp === 'string') return {x: +nlp, y: +nlp}; else return nlp; } function fit_shape(shape, diagram) { return function(content) { content.each(function(n) { var bbox = null; if((!shape.useTextSize || shape.useTextSize(n.dcg_shape)) && diagram.nodeFitLabel.eval(n)) { bbox = getBBoxNoThrow(this); bbox = {x: bbox.x, y: bbox.y, width: bbox.width, height: bbox.height}; var padding; var content = diagram.nodeContent.eval(n); if(content && diagram.content(content).padding) padding = diagram.content(content).padding(n); else { var padding2 = node_label_padding(diagram, n); padding = { x: padding2.x*2, y: padding2.y*2 }; } bbox.width += padding.x; bbox.height += padding.y; n.bbox = bbox; } var r = 0, radii; if(!shape.useRadius || shape.useRadius(n.dcg_shape)) r = diagram.nodeRadius.eval(n); if(bbox && bbox.width && bbox.height || shape.useTextSize && !shape.useTextSize(n.dcg_shape)) radii = shape.calc_radii(n, r, bbox); else radii = {rx: r, ry: r}; n.dcg_rx = radii.rx; n.dcg_ry = radii.ry; var w = radii.rx*2, h = radii.ry*2; // fixme: this is only consistent if regular || !squeeze // but we'd need to calculate polygon first in order to find out // (not a bad idea, just no time right now) // if(w<h) w = h; if(!shape.usePaddingAndStroke || shape.usePaddingAndStroke(n.dcg_shape)) { var pands = diagram.nodePadding.eval(n) + diagram.nodeStrokeWidth.eval(n); w += pands; h += pands; } n.cola.width = w; n.cola.height = h; }); }; } function ellipse_attrs(diagram) { return { rx: function(n) { return n.dcg_rx; }, ry: function(n) { return n.dcg_ry; } }; } function polygon_attrs(diagram, n) { return { d: function(n) { var rx = n.dcg_rx, ry = n.dcg_ry, def = n.dcg_shape, sides = def.sides || 4, skew = def.skew || 0, distortion = def.distortion || 0, rotation = def.rotation || 0, align = (sides%2 ? 0 : 0.5), // even-sided horizontal top, odd pointy top angles = []; rotation = rotation/360 + 0.25; // start at y axis not x for(var i = 0; i<sides; ++i) { var theta = -((i+align)/sides + rotation)*Math.PI*2; // svg is up-negative angles.push({x: Math.cos(theta), y: Math.sin(theta)}); } var yext = d3.extent(angles, function(theta) { return theta.y; }); if(def.regular) rx = ry = Math.max(rx, ry); else if(rx < ry && !def.squeeze) rx = ry; else ry = ry / Math.min(-yext[0], yext[1]); n.dcg_points = angles.map(function(theta) { var x = rx*theta.x, y = ry*theta.y; x *= 1 + distortion*((ry-y)/ry - 1); x -= skew*y/2; return {x: x, y: y}; }); return generate_path(n.dcg_points, 1, true); } }; } function binary_search(f, a, b) { var patience = 100; if(f(a).val >= 0) throw new Error("f(a) must be less than 0"); if(f(b).val <= 0) throw new Error("f(b) must be greater than 0"); while(true) { if(!--patience) throw new Error("patience ran out"); var c = (a+b)/2, f_c = f(c), fv = f_c.val; if(Math.abs(fv) < 0.5) return f_c; if(fv > 0) b = c; else a = c; } } function draw_edge_to_shapes(diagram, e, sx, sy, tx, ty, neighbor, dir, offset, source_padding, target_padding) { var deltaX, deltaY, sp, tp, points, bezDegree, headAng, retPath; if(!neighbor) { sp = e.sourcePort.pos; tp = e.targetPort.pos; if(!sp) sp = {x: 0, y: 0}; if(!tp) tp = {x: 0, y: 0}; points = [{ x: sx + sp.x, y: sy + sp.y }, { x: tx + tp.x, y: ty + tp.y }]; bezDegree = 1; } else { var p_on_s = function(node, ang) { return diagram.shape(node.dcg_shape.shape).intersect_vec(node, Math.cos(ang)*1000, Math.sin(ang)*1000); }; var compare_dist = function(node, port0, goal) { return function(ang) { var port = p_on_s(node, ang); if(!port) return { port: {x: 0, y: 0}, val: 0, ang: ang }; else return { port: port, val: Math.hypot(port.x - port0.x, port.y - port0.y) - goal, ang: ang }; }; }; var srcang = Math.atan2(neighbor.sourcePort.y, neighbor.sourcePort.x), tarang = Math.atan2(neighbor.targetPort.y, neighbor.targetPort.x); var bss, bst; // don't like this but throwing is unacceptable try { bss = binary_search(compare_dist(e.source, neighbor.sourcePort, offset), srcang, srcang + 2 * dir * offset / source_padding); } catch(x) { bss = {ang: srcang, port: neighbor.sourcePort}; } try { bst = binary_search(compare_dist(e.target, neighbor.targetPort, offset), tarang, tarang - 2 * dir * offset / source_padding); } catch(x) { bst = {ang: tarang, port: neighbor.targetPort}; } sp = bss.port; tp = bst.port; var sdist = Math.hypot(sp.x, sp.y), tdist = Math.hypot(tp.x, tp.y), c1dist = sdist+source_padding/2, c2dist = tdist+target_padding/2; var c1X = sx + c1dist * Math.cos(bss.ang), c1Y = sy + c1dist * Math.sin(bss.ang), c2X = tx + c2dist * Math.cos(bst.ang), c2Y = ty + c2dist * Math.sin(bst.ang); points = [ {x: sx + sp.x, y: sy + sp.y}, {x: c1X, y: c1Y}, {x: c2X, y: c2Y}, {x: tx + tp.x, y: ty + tp.y} ]; bezDegree = 3; } return { sourcePort: sp, targetPort: tp, points: points, bezDegree: bezDegree }; } function is_one_segment(path) { return path.bezDegree === 1 && path.points.length === 2 || path.bezDegree === 3 && path.points.length === 4; } function as_bezier3(path) { var p = path.points; if(path.bezDegree === 3) return p; else if(path.bezDegree === 1) return [ { x: p[0].x, y: p[0].y }, { x: p[0].x + (p[1].x - p[0].x)/3, y: p[0].y + (p[1].y - p[0].y)/3 }, { x: p[0].x + 2*(p[1].x - p[0].x)/3, y: p[0].y + 2*(p[1].y - p[0].y)/3 }, { x: p[1].x, y: p[1].y } ]; else throw new Error('unknown bezDegree ' + path.bezDegree); } // from https://www.jasondavies.com/animated-bezier/ function interpolate(d, p) { var r = []; for (var i=1; i<d.length; i++) { var d0 = d[i-1], d1 = d[i]; r.push({x: d0.x + (d1.x - d0.x) * p, y: d0.y + (d1.y - d0.y) * p}); } return r; } function getLevels(points, t_) { var x = [points]; for (var i=1; i<points.length; i++) { x.push(interpolate(x[x.length-1], t_)); } return x; } // get a point on a bezier segment, where 0 <= t <= 1 function bezier_point(points, t_) { var q = getLevels(points, t_); return q[q.length-1][0]; } // from https://stackoverflow.com/questions/8369488/splitting-a-bezier-curve#8405756 // somewhat redundant with the above but different objective function split_bezier(p, t) { var x1 = p[0].x, y1 = p[0].y, x2 = p[1].x, y2 = p[1].y, x3 = p[2].x, y3 = p[2].y, x4 = p[3].x, y4 = p[3].y, x12 = (x2-x1)*t+x1, y12 = (y2-y1)*t+y1, x23 = (x3-x2)*t+x2, y23 = (y3-y2)*t+y2, x34 = (x4-x3)*t+x3, y34 = (y4-y3)*t+y3, x123 = (x23-x12)*t+x12, y123 = (y23-y12)*t+y12, x234 = (x34-x23)*t+x23, y234 = (y34-y23)*t+y23, x1234 = (x234-x123)*t+x123, y1234 = (y234-y123)*t+y123; return [ [{x: x1, y: y1}, {x: x12, y: y12}, {x: x123, y: y123}, {x: x1234, y: y1234}], [{x: x1234, y: y1234}, {x: x234, y: y234}, {x: x34, y: y34}, {x: x4, y: y4}] ]; } function split_bezier_n(p, n) { var ret = []; while(n > 1) { var parts = split_bezier(p, 1/n); ret.push(parts[0][0], parts[0][1], parts[0][2]); p = parts[1]; --n; } ret.push.apply(ret, p); return ret; } // binary search for a point along a bezier that is a certain distance from one of the end points // return the bezier cut at that point. function chop_bezier(points, end, dist) { var EPS = 0.1, dist2 = dist*dist; var ref, dir, segment; if(end === 'head') { ref = points[points.length-1]; segment = points.slice(points.length-4); dir = -1; } else { ref = points[0]; segment = points.slice(0, 4); dir = 1; } var parts, d2, t = 0.5, dt = 0.5, dx, dy; do { parts = split_bezier(segment, t); dx = ref.x - parts[1][0].x; dy = ref.y - parts[1][0].y; d2 = dx*dx + dy*dy; dt /= 2; if(d2 > dist2) t -= dt*dir; else t += dt*dir; //console.log('dist', dist, 'dir', dir, 'd', d, 't', t, 'dt', dt); } while(dt > 0.0000001 && Math.abs(d2 - dist2) > EPS); points = points.slice(); if(end === 'head') return points.slice(0, points.length-4).concat(parts[0]); else return parts[1].concat(points.slice(4)); } function angle_between_points(p0, p1) { return Math.atan2(p1.y - p0.y, p1.x - p0.x); } dc_graph.no_shape = function() { var _shape = { parent: property(null), elaborate: function(preset, def) { return Object.assign(preset, def); }, useTextSize: function() { return false; }, useRadius: function() { return false; }, usePaddingAndStroke: function() { return false; }, intersect_vec: function(n, deltaX, deltaY) { return {x: 0, y: 0}; }, calc_radii: function(n, ry, bbox) { return {rx: 0, ry: 0}; }, create: function(nodeEnter) { }, replace: function(nodeChanged) { }, update: function(node) { } }; return _shape; }; dc_graph.ellipse_shape = function() { var _shape = { parent: property(null), elaborate: function(preset, def) { return Object.assign(preset, def); }, intersect_vec: function(n, deltaX, deltaY) { return point_on_ellipse(n.dcg_rx, n.dcg_ry, deltaX, deltaY); }, calc_radii: function(n, ry, bbox) { // make sure we can fit height in r ry = Math.max(ry, bbox.height/2 + 5); var rx = bbox.width/2; // solve (x/A)^2 + (y/B)^2) = 1 for A, with B=r, to fit text in ellipse // http://stackoverflow.com/a/433438/676195 var y_over_B = bbox.height/2/ry; rx = rx/Math.sqrt(1 - y_over_B*y_over_B); rx = Math.max(rx, ry); return {rx: rx, ry: ry}; }, create: function(nodeEnter) { nodeEnter.insert('ellipse', ':first-child') .attr('class', 'node-shape'); }, update: function(node) { node.select('ellipse.node-shape') .attr(ellipse_attrs(_shape.parent())); } }; return _shape; }; dc_graph.polygon_shape = function() { var _shape = { parent: property(null), elaborate: function(preset, def) { return Object.assign(preset, def); }, intersect_vec: function(n, deltaX, deltaY) { return point_on_polygon(n.dcg_points, 0, 0, deltaX, deltaY); }, calc_radii: function(n, ry, bbox) { // make sure we can fit height in r ry = Math.max(ry, bbox.height/2 + 5); var rx = bbox.width/2; // this is cribbed from graphviz but there is much i don't understand // and any errors are mine // https://github.com/ellson/graphviz/blob/6acd566eab716c899ef3c4ddc87eceb9b428b627/lib/common/shapes.c#L1996 rx = rx*Math.sqrt(2)/Math.cos(Math.PI/(n.dcg_shape.sides||4)); return {rx: rx, ry: ry}; }, create: function(nodeEnter) { nodeEnter.insert('path', ':first-child') .attr('class', 'node-shape'); }, update: function(node) { node.select('path.node-shape') .attr(polygon_attrs(_shape.parent())); } }; return _shape; }; dc_graph.rounded_rectangle_shape = function() { var _shape = { parent: property(null), elaborate: function(preset, def) { preset = Object.assign({rx: 10, ry: 10}, preset); return Object.assign(preset, def); }, intersect_vec: function(n, deltaX, deltaY) { var points = [ {x: n.dcg_rx, y: n.dcg_ry}, {x: n.dcg_rx, y: -n.dcg_ry}, {x: -n.dcg_rx, y: -n.dcg_ry}, {x: -n.dcg_rx, y: n.dcg_ry} ]; return point_on_polygon(points, 0, 0, deltaX, deltaY); // not rounded }, useRadius: function(shape) { return !shape.noshape; }, calc_radii: function(n, ry, bbox) { var fity = bbox.height/2; // fixme: fudge to make sure text is not too tall for node if(!n.dcg_shape.noshape) fity += 5; return { rx: bbox.width / 2, ry: Math.max(ry, fity) }; }, create: function(nodeEnter) { nodeEnter.filter(function(n) { return !n.dcg_shape.noshape; }).insert('rect', ':first-child') .attr('class', 'node-shape'); }, update: function(node) { node.select('rect.node-shape') .attr({ x: function(n) { return -n.dcg_rx; }, y: function(n) { return -n.dcg_ry; }, width: function(n) { return 2*n.dcg_rx; }, height: function(n) { return 2*n.dcg_ry; }, rx: function(n) { return n.dcg_shape.rx + 'px'; }, ry: function(n) { return n.dcg_shape.ry + 'px'; } }); } }; return _shape; }; // this is not all that accurate - idea is that arrows, houses, etc, are rectangles // in terms of sizing, but elaborated drawing & clipping. refine until done. dc_graph.elaborated_rectangle_shape = function() { var _shape = dc_graph.rounded_rectangle_shape(); _shape.intersect_vec = function(n, deltaX, deltaY) { var points = n.dcg_shape.get_points(n.dcg_rx, n.dcg_ry); return point_on_polygon(points, 0, 0, deltaX, deltaY); }; delete _shape.useRadius; var orig_radii = _shape.calc_radii; _shape.calc_radii = function(n, ry, bbox) { var ret = orig_radii(n, ry, bbox); return { rx: Math.max(ret.rx, n.dcg_shape.minrx), ry: ret.ry }; }; _shape.create = function(nodeEnter) { nodeEnter.insert('path', ':first-child') .attr('class', 'node-shape'); }; _shape.update = function(node) { node.select('path.node-shape') .attr('d', function(n) { return generate_path(n.dcg_shape.get_points(n.dcg_rx, n.dcg_ry), 1, true); }); }; return _shape; }; function offsetx(ofsx) { return function(p) { return {x: p.x + ofsx, y: p.y}; }; } dc_graph.builtin_arrows = { box: function(open, side) { if(!open) return { frontRef: [8,0], drawFunction: function(marker, ofs, stemWidth) { marker.append('rect') .attr({ x: ofs[0], y: side==='right' ? -stemWidth/2 : -4, width: 8, height: side ? 4+stemWidth/2 : 8, 'stroke-width': 0 }); } }; else return { frontRef: [8,0], drawFunction: function(marker, ofs, stemWidth) { marker.append('rect') .attr({ x: ofs[0] + 0.5, y: side==='right' ? 0 : -3.5, width: 7, height: side ? 3.5 : 7, 'stroke-width': 1, fill: 'none' }); if(side) marker.append('svg:path') .attr({ d: ['M', ofs[0], 0, 'h',8].join(' '), 'stroke-width': stemWidth, fill: 'none' }); } }; }, curve: function(open, side) { return { stems: [true,false], kernstems: [0, 0.25], frontRef: [8,0], drawFunction: function(marker, ofs, stemWidth) { var instrs = []; instrs.pus