/** * `dc_graph.diagram` is a dc.js-compatible network visualization component. It registers in * the dc.js chart registry and its nodes and edges are generated from crossfilter groups. It * logically derives from the dc.js * {@link https://github.com/dc-js/dc.js/blob/develop/web/docs/api-latest.md#dc.baseMixin baseMixin}, * but it does not physically derive from it since so much is different about network * visualization versus conventional charts. * @class diagram * @memberof dc_graph * @param {String|node} parent - Any valid * {@link https://github.com/mbostock/d3/wiki/Selections#selecting-elements d3 single selector} * specifying a dom block element such as a div; or a dom element. * @param {String} [chartGroup] - The name of the dc.js chart group this diagram instance * should be placed in. Filter interaction with a diagram will only trigger events and redraws * within the diagram's group. * @return {dc_graph.diagram} **/ dc_graph.diagram = function (parent, chartGroup) { // different enough from regular dc charts that we don't use dc.baseMixin // but attempt to implement most of that interface, copying some of the most basic stuff var _diagram = dc.marginMixin({}); _diagram.__dcFlag__ = dc.utils.uniqueId(); _diagram.margins({left: 10, top: 10, right: 10, bottom: 10}); var _dispatch = d3.dispatch('preDraw', 'data', 'end', 'start', 'render', 'drawn', 'receivedLayout', 'transitionsStarted', 'zoomed', 'reset'); var _nodes = {}, _edges = {}; // hold state between runs var _ports = {}; // id = node|edge/id/name var _clusters = {}; var _nodePorts; // ports sorted by node id var _stats = {}; var _nodes_snapshot, _edges_snapshot; var _arrows = {}; var _running = false; // for detecting concurrency issues var _anchor, _chartGroup; var _animateZoom; var _minWidth = 200; var _defaultWidthCalc = function (element) { var width = element && element.getBoundingClientRect && element.getBoundingClientRect().width; return (width && width > _minWidth) ? width : _minWidth; }; var _widthCalc = _defaultWidthCalc; var _minHeight = 200; var _defaultHeightCalc = function (element) { var height = element && element.getBoundingClientRect && element.getBoundingClientRect().height; return (height && height > _minHeight) ? height : _minHeight; }; var _heightCalc = _defaultHeightCalc; var _width, _height, _lastWidth, _lastHeight; function deprecate_layout_algo_parameter(name) { return function(value) { if(!_diagram.layoutEngine()) _diagram.layoutAlgorithm('cola', true); var engine = _diagram.layoutEngine(); if(engine.getEngine) engine = engine.getEngine(); if(engine[name]) { console.warn('property is deprecated, call on layout engine instead: dc_graph.diagram.%c' + name, 'font-weight: bold'); if(!arguments.length) return engine[name](); engine[name](value); } else { console.warn('property is deprecated, and is not supported for Warning: dc_graph.diagram.<b>' + name + '</b> is deprecated, and it is not supported for the "' + engine.layoutAlgorithm() + '" layout algorithm: ignored.'); if(!arguments.length) return null; } return this; }; } /** * Set or get the height attribute of the diagram. If a value is given, then the diagram is * returned for method chaining. If no value is given, then the current value of the height * attribute will be returned. * * The width and height are applied to the SVG element generated by the diagram on render, or * when `resizeSvg` is called. * * If the value is falsy or a function, the height will be calculated the first time it is * needed, using the provided function or default height calculator, and then cached. The * default calculator uses the client rect of the element specified when constructing the chart, * with a minimum of `minHeight`. A custom calculator will be passed the element. * * If the value is `'auto'`, the height will be calculated every time the diagram is drawn, and * it will not be set on the `<svg>` element. Instead, the element will be pinned to the same * rectangle as its containing div using CSS. * * @method height * @memberof dc_graph.diagram * @instance * @param {Number} [height=200] * @return {Number} * @return {dc_graph.diagram} **/ _diagram.height = function (height) { if (!arguments.length) { if (!dc.utils.isNumber(_height)) { _lastHeight = _heightCalc(_diagram.root().node()); if(_height === 'auto') // 'auto' => calculate every time return _lastHeight; // null/undefined => calculate once only _height = _lastHeight; } return _height; } if(dc.utils.isNumber(height) || !height || height === 'auto') _height = height; else if(typeof height === 'function') { _heightCalc = height; _height = undefined; } else throw new Error("don't know what to do with height type " + typeof height + " value " + height); return _diagram; }; _diagram.minHeight = function(height) { if(!arguments.length) return _minHeight; _minHeight = height; return _diagram; }; /** * Set or get the width attribute of the diagram. If a value is given, then the diagram is * returned for method chaining. If no value is given, then the current value of the width * attribute will be returned. * * The width and height are applied to the SVG element generated by the diagram on render, or * when `resizeSvg` is called. * * If the value is falsy or a function, the width will be calculated the first time it is * needed, using the provided function or default width calculator, and then cached. The default * calculator uses the client rect of the element specified when constructing the chart, with a * minimum of `minWidth`. A custom calculator will be passed the element. * * If the value is `'auto'`, the width will be calculated every time the diagram is drawn, and * it will not be set on the `<svg>` element. Instead, the element will be pinned to the same * rectangle as its containing div using CSS. * * @method width * @memberof dc_graph.diagram * @instance * @param {Number} [width=200] * @return {Number} * @return {dc_graph.diagram} **/ _diagram.width = function (width) { if (!arguments.length) { if (!dc.utils.isNumber(_width)) { _lastWidth = _widthCalc(_diagram.root().node()); if(_width === 'auto') // 'auto' => calculate every time return _lastWidth; // null/undefined => calculate once only _width = _lastWidth; } return _width; } if(dc.utils.isNumber(width) || !width || width === 'auto') _width = width; else if(typeof width === 'function') { _widthCalc = width; _width = undefined; } else throw new Error("don't know what to do with width type " + typeof width + " value " + width); return _diagram; }; _diagram.minWidth = function(width) { if(!arguments.length) return _minWidth; _minWidth = width; return _diagram; }; /** * Get or set the root element, which is usually the parent div. Normally the root is set * when the diagram is constructed; setting it later may have unexpected consequences. * @method root * @memberof dc_graph.diagram * @instance * @param {node} [root=null] * @return {node} * @return {dc_graph.diagram} **/ _diagram.root = property(null).react(function(e) { if(e.empty()) console.log('Warning: parent selector ' + parent + " doesn't seem to exist"); }); /** * Get or set whether mouse wheel rotation or touchpad gestures will zoom the diagram, and * whether dragging on the background pans the diagram. * @method mouseZoomable * @memberof dc_graph.diagram * @instance * @param {Boolean} [mouseZoomable=true] * @return {Boolean} * @return {dc_graph.diagram} **/ _diagram.mouseZoomable = property(true); _diagram.zoomExtent = property([.1, 2]); /** * Whether zooming should only be enabled when the alt key is pressed. * @method altKeyZoom * @memberof dc_graph.diagram * @instance * @param {Boolean} [altKeyZoom=true] * @return {Boolean} * @return {dc_graph.diagram} **/ _diagram.modKeyZoom = _diagram.altKeyZoom = property(false); /** * Set or get the fitting strategy for the canvas, which affects how the translate * and scale get calculated when `autoZoom` is triggered. * * * `'default'` - simulates the preserveAspectRatio behavior of `xMidYMid meet`, but * with margins - the content is stretched or squished in the more constrained * direction, and centered in the other direction * * `'vertical'` - fits the canvas vertically (with vertical margins) and centers * it horizontally. If the canvas is taller than the viewport, it will meet * vertically and there will be blank areas to the left and right. If the canvas * is wider than the viewport, it will be sliced. * * `'horizontal'` - fits the canvas horizontally (with horizontal margins) and * centers it vertically. If the canvas is wider than the viewport, it will meet * horizontally and there will be blank areas above and below. If the canvas is * taller than the viewport, it will be sliced. * * Other options * * `null` - no attempt is made to fit the content in the viewport * * `'zoom'` - does not scale the content, but attempts to bring as much content * into view as possible, using using the same algorithm as `restrictPan` * * `'align_{tlbrc}[2]'` - does not scale; aligns up to two sides or centers them * @method fitStrategy * @memberof dc_graph.diagram * @instance * @param {String} [fitStrategy='default'] * @return {String} * @return {dc_graph.diagram} **/ _diagram.fitStrategy = property('default'); /** * Do not allow panning (scrolling) to push the diagram out of the viewable area, if there * is space for it to be shown. */ _diagram.restrictPan = property(false); /** * Auto-zoom behavior. * * `'always'` - zoom every time layout happens * * `'once'` - zoom the next time layout happens * * `null` - manual, call `zoomToFit` to fit * @method autoZoom * @memberof dc_graph.diagram * @instance * @param {String} [autoZoom=null] * @return {String} * @return {dc_graph.diagram} **/ _diagram.autoZoom = property(null); _diagram.zoomToFit = function(animate) { // if(!(_nodeLayer && _edgeLayer)) // return; auto_zoom(animate); }; _diagram.zoomDuration = property(500); /** * Set or get the crossfilter dimension which represents the nodes (vertices) in the * diagram. Typically there will be a crossfilter instance for the nodes, and another for * the edges. * * *Dimensions are included on the diagram for similarity to dc.js, however the diagram * itself does not use them - but {@link dc_graph.filter_selection filter_selection} will.* * @method nodeDimension * @memberof dc_graph.diagram * @instance * @param {crossfilter.dimension} [nodeDimension] * @return {crossfilter.dimension} * @return {dc_graph.diagram} **/ _diagram.nodeDimension = property(); /** * Set or get the crossfilter group which is the data source for the nodes in the * diagram. The diagram will use the group's `.all()` method to get an array of `{key, * value}` pairs, where the key is a unique identifier, and the value is usually an object * containing the node's attributes. All accessors work with these key/value pairs. * * If the group is changed or returns different values, the next call to `.redraw()` will * reflect the changes incrementally. * * It is possible to pass another object with the same `.all()` interface instead of a * crossfilter group. * @method nodeGroup * @memberof dc_graph.diagram * @instance * @param {crossfilter.group} [nodeGroup] * @return {crossfilter.group} * @return {dc_graph.diagram} **/ _diagram.nodeGroup = property(); /** * Set or get the crossfilter dimension which represents the edges in the * diagram. Typically there will be a crossfilter instance for the nodes, and another for * the edges. * * *Dimensions are included on the diagram for similarity to dc.js, however the diagram * itself does not use them - but {@link dc_graph.filter_selection filter_selection} will.* * @method edgeDimension * @memberof dc_graph.diagram * @instance * @param {crossfilter.dimension} [edgeDimension] * @return {crossfilter.dimension} * @return {dc_graph.diagram} **/ _diagram.edgeDimension = property(); /** * Set or get the crossfilter group which is the data source for the edges in the * diagram. See `.nodeGroup` above for the way data is loaded from a crossfilter group. * * The values in the key/value pairs returned by `diagram.edgeGroup().all()` need to * support, at a minimum, the {@link dc_graph.diagram#nodeSource nodeSource} and * {@link dc_graph.diagram#nodeTarget nodeTarget}, which should return the same * keys as the {@link dc_graph.diagram#nodeKey nodeKey} * * @method edgeGroup * @memberof dc_graph.diagram * @instance * @param {crossfilter.group} [edgeGroup] * @return {crossfilter.group} * @return {dc_graph.diagram} **/ _diagram.edgeGroup = property(); _diagram.edgesInFront = property(false); /** * Set or get the function which will be used to retrieve the unique key for each node. By * default, this accesses the `key` field of the object passed to it. The keys should match * the keys returned by the {@link dc_graph.diagram#edgeSource edgeSource} and * {@link dc_graph.diagram#edgeTarget edgeTarget}. * * @method nodeKey * @memberof dc_graph.diagram * @instance * @param {Function} [nodeKey=function(kv) { return kv.key }] * @return {Function} * @return {dc_graph.diagram} **/ _diagram.nodeKey = _diagram.nodeKeyAccessor = property(function(kv) { return kv.key; }); /** * Set or get the function which will be used to retrieve the unique key for each edge. By * default, this accesses the `key` field of the object passed to it. * * @method edgeKey * @memberof dc_graph.diagram * @instance * @param {Function} [edgeKey=function(kv) { return kv.key }] * @return {Function} * @return {dc_graph.diagram} **/ _diagram.edgeKey = _diagram.edgeKeyAccessor = property(function(kv) { return kv.key; }); /** * Set or get the function which will be used to retrieve the source (origin/tail) key of * the edge objects. The key must equal the key returned by the `.nodeKey` for one of the * nodes; if it does not, or if the node is currently filtered out, the edge will not be * displayed. By default, looks for `.value.sourcename`. * * @method edgeSource * @memberof dc_graph.diagram * @instance * @param {Function} [edgeSource=function(kv) { return kv.value.sourcename; }] * @return {Function} * @return {dc_graph.diagram} **/ _diagram.edgeSource = _diagram.sourceAccessor = property(function(kv) { return kv.value.sourcename; }); /** * Set or get the function which will be used to retrieve the target (destination/head) key * of the edge objects. The key must equal the key returned by the * {@link dc_graph.diagram#nodeKey nodeKey} for one of the nodes; if it does not, or if the node * is currently filtered out, the edge will not be displayed. By default, looks for * `.value.targetname`. * @method edgeTarget * @memberof dc_graph.diagram * @instance * @param {Function} [edgeTarget=function(kv) { return kv.value.targetname; }] * @return {Function} * @return {dc_graph.diagram} **/ _diagram.edgeTarget = _diagram.targetAccessor = property(function(kv) { return kv.value.targetname; }); _diagram.portDimension = property(null); _diagram.portGroup = property(null); _diagram.portNodeKey = property(null); _diagram.portEdgeKey = property(null); _diagram.portName = property(null); _diagram.portStyleName = property(null); _diagram.portElastic = property(true); _diagram.portStyle = named_children(); _diagram.portBounds = property(null); // position limits, in radians _diagram.edgeSourcePortName = property(null); _diagram.edgeTargetPortName = property(null); /** * Set or get the crossfilter dimension which represents the edges in the * diagram. Typically there will be a crossfilter instance for the nodes, and another for * the edges. * * *As with node and edge dimensions, the diagram will itself not filter on cluster dimensions; * this is included for symmetry, and for modes which may want to filter clusters.* * @method clusterDimension * @memberof dc_graph.diagram * @instance * @param {crossfilter.dimension} [clusterDimension] * @return {crossfilter.dimension} * @return {dc_graph.diagram} **/ _diagram.clusterDimension = property(null); /** * Set or get the crossfilter group which is the data source for clusters in the * diagram. * * The key/value pairs returned by `diagram.clusterGroup().all()` need to support, at a minimum, * the {@link dc_graph.diagram#clusterKey clusterKey} and {@link dc_graph.diagram#clusterParent clusterParent} * accessors, which should return keys in this group. * * @method clusterGroup * @memberof dc_graph.diagram * @instance * @param {crossfilter.group} [clusterGroup] * @return {crossfilter.group} * @return {dc_graph.diagram} **/ _diagram.clusterGroup = property(null); // cluster accessors /** * Set or get the function which will be used to retrieve the unique key for each cluster. By * default, this accesses the `key` field of the object passed to it. * * @method clusterKey * @memberof dc_graph.diagram * @instance * @param {Function} [clusterKey=function(kv) { return kv.key }] * @return {Function} * @return {dc_graph.diagram} **/ _diagram.clusterKey = property(dc.pluck('key')); /** * Set or get the function which will be used to retrieve the key of the parent of a cluster, * which is another cluster. * * @method clusterParent * @memberof dc_graph.diagram * @instance * @param {Function} [clusterParent=function(kv) { return kv.key }] * @return {Function} * @return {dc_graph.diagram} **/ _diagram.clusterParent = property(null); /** * Set or get the function which will be used to retrieve the padding, in pixels, around a cluster. * * **To be implemented.** If a single value is returned, it will be used on all sides; if two * values are returned they will be interpreted as the vertical and horizontal padding. * * @method clusterPadding * @memberof dc_graph.diagram * @instance * @param {Function} [clusterPadding=function(kv) { return kv.key }] * @return {Function} * @return {dc_graph.diagram} **/ _diagram.clusterPadding = property(8); // node accessor /** * Set or get the function which will be used to retrieve the parent cluster of a node, or * `null` if the node is not in a cluster. * * @method nodeParentCluster * @memberof dc_graph.diagram * @instance * @param {Function} [nodeParentCluster=function(kv) { return kv.key }] * @return {Function} * @return {dc_graph.diagram} **/ _diagram.nodeParentCluster = property(null); /** * Set or get the function which will be used to retrieve the radius, in pixels, for each * node. This determines the height of nodes,and if `nodeFitLabel` is false, the width too. * @method nodeRadius * @memberof dc_graph.diagram * @instance * @param {Function|Number} [nodeRadius=25] * @return {Function|Number} * @return {dc_graph.diagram} **/ _diagram.nodeRadius = _diagram.nodeRadiusAccessor = property(25); /** * Set or get the function which will be used to retrieve the stroke width, in pixels, for * drawing the outline of each node. According to the SVG specification, the outline will * be drawn half on top of the fill, and half outside. Default: 1 * @method nodeStrokeWidth * @memberof dc_graph.diagram * @instance * @param {Function|Number} [nodeStrokeWidth=1] * @return {Function|Number} * @return {dc_graph.diagram} **/ _diagram.nodeStrokeWidth = _diagram.nodeStrokeWidthAccessor = property(1); /** * Set or get the function which will be used to retrieve the stroke color for the outline * of each node. * @method nodeStroke * @memberof dc_graph.diagram * @instance * @param {Function|String} [nodeStroke='black'] * @return {Function|String} * @return {dc_graph.diagram} **/ _diagram.nodeStroke = _diagram.nodeStrokeAccessor = property('black'); _diagram.nodeStrokeDashArray = property(null); /** * If set, the value returned from `nodeFill` will be processed through this * {@link https://github.com/mbostock/d3/wiki/Scales d3.scale} * to return the fill color. If falsy, uses the identity function (no scale). * @method nodeFillScale * @memberof dc_graph.diagram * @instance * @param {Function|d3.scale} [nodeFillScale] * @return {Function|d3.scale} * @return {dc_graph.diagram} **/ _diagram.nodeFillScale = property(null); /** * Set or get the function which will be used to retrieve the fill color for the body of each * node. * @method nodeFill * @memberof dc_graph.diagram * @instance * @param {Function|String} [nodeFill='white'] * @return {Function|String} * @return {dc_graph.diagram} **/ _diagram.nodeFill = _diagram.nodeFillAccessor = property('white'); /** * Set or get the function which will be used to retrieve the opacity of each node. * @method nodeOpacity * @memberof dc_graph.diagram * @instance * @param {Function|Number} [nodeOpacity=1] * @return {Function|Number} * @return {dc_graph.diagram} **/ _diagram.nodeOpacity = property(1); /** * Set or get the padding or minimum distance, in pixels, for a node. (Will be distributed * to both sides of the node.) * @method nodePadding * @memberof dc_graph.diagram * @instance * @param {Function|Number} [nodePadding=6] * @return {Function|Number} * @return {dc_graph.diagram} **/ _diagram.nodePadding = property(6); /** * Set or get the padding, in pixels, for a node's label. If an object, should contain fields * `x` and `y`. If a number, will be applied to both x and y. * @method nodeLabelPadding * @memberof dc_graph.diagram * @instance * @param {Function|Number|Object} [nodeLabelPadding=0] * @return {Function|Number} * @return {dc_graph.diagram} **/ _diagram.nodeLabelPadding = property(0); /** * Set or get the line height for nodes with multiple lines of text, in ems. * @method nodeLineHeight * @memberof dc_graph.diagram * @instance * @param {Function|Number} [nodeLineHeight=1] * @return {Function|Number} * @return {dc_graph.diagram} **/ _diagram.nodeLineHeight = property(1); /** * Set or get the function which will be used to retrieve the label text to display in each * node. By default, looks for a field `label` or `name` inside the `value` field. * @method nodeLabel * @memberof dc_graph.diagram * @instance * @param {Function|String} [nodeLabel] * @return {Function|String} * @example * // Default behavior * diagram.nodeLabel(function(kv) { * return kv.value.label || kv.value.name; * }); * @return {dc_graph.diagram} **/ _diagram.nodeLabel = _diagram.nodeLabelAccessor = property(function(kv) { return kv.value.label || kv.value.name; }); _diagram.nodeLabelAlignment = property('center'); _diagram.nodeLabelDecoration = property(null); /** * Set or get the function which will be used to retrieve the label fill color. Default: null * @method nodeLabelFill * @memberof dc_graph.diagram * @instance * @param {Function|String} [nodeLabelFill=null] * @return {Function|String} * @return {dc_graph.diagram} **/ _diagram.nodeLabelFill = _diagram.nodeLabelFillAccessor = property(null); /** * Whether to fit the node shape around the label * @method nodeFitLabel * @memberof dc_graph.diagram * @instance * @param {Function|Boolean} [nodeFitLabel=true] * @return {Function|Boolean} * @return {dc_graph.diagram} **/ _diagram.nodeFitLabel = _diagram.nodeFitLabelAccessor = property(true); /** * The shape to use for drawing each node, specified as an object with at least the field * `shape`. The names of shapes are mostly taken * [from graphviz](http://www.graphviz.org/doc/info/shapes.html); currently ellipse, egg, * triangle, rectangle, diamond, trapezium, parallelogram, pentagon, hexagon, septagon, octagon, * invtriangle, invtrapezium, square, polygon are supported. * * If `shape = polygon`: * * `sides`: number of sides for a polygon * @method nodeShape * @memberof dc_graph.diagram * @instance * @param {Function|Object} [nodeShape={shape: 'ellipse'}] * @return {Function|Object} * @return {dc_graph.diagram} * @example * // set shape to diamond or parallelogram based on flag * diagram.nodeShape(function(kv) { * return {shape: kv.value.flag ? 'diamond' : 'parallelogram'}; * }); **/ _diagram.nodeShape = property(default_shape); // for defining custom (and standard) shapes _diagram.shape = named_children(); _diagram.shape('nothing', dc_graph.no_shape()); _diagram.shape('ellipse', dc_graph.ellipse_shape()); _diagram.shape('polygon', dc_graph.polygon_shape()); _diagram.shape('rounded-rect', dc_graph.rounded_rectangle_shape()); _diagram.shape('elaborated-rect', dc_graph.elaborated_rectangle_shape()); _diagram.nodeOutlineClip = property(null); _diagram.nodeContent = property('text'); _diagram.content = named_children(); _diagram.content('text', dc_graph.text_contents()); // really looks like these should reside in an open namespace - this used only by an extension // but it's no less real than any other computed property _diagram.nodeIcon = property(null); /** * Set or get the function which will be used to retrieve the node title, usually rendered * as a tooltip. By default, uses the key of the node. * @method nodeTitle * @memberof dc_graph.diagram * @instance * @param {Function|String} [nodeTitle] * @return {Function|String} * @example * // Default behavior * diagram.nodeTitle(function(kv) { * return _diagram.nodeKey()(kv); * }); * @return {dc_graph.diagram} **/ _diagram.nodeTitle = _diagram.nodeTitleAccessor = property(function(kv) { return _diagram.nodeKey()(kv); }); /** * By default, nodes are added to the layout in the order that `.nodeGroup().all()` returns * them. If specified, `.nodeOrdering` provides an accessor that returns a key to sort the * nodes on. *It would be better not to rely on ordering to affect layout, but it may * affect the layout in some cases.* * @method nodeOrdering * @memberof dc_graph.diagram * @instance * @param {Function} [nodeOrdering=null] * @return {Function} * @return {dc_graph.diagram} **/ _diagram.nodeOrdering = property(null); /** * Specify an accessor that returns an {x,y} coordinate for a node that should be * {@link https://github.com/tgdwyer/WebCola/wiki/Fixed-Node-Positions fixed in place}, * and returns falsy for other nodes. * @method nodeFixed * @memberof dc_graph.diagram * @instance * @param {Function|Object} [nodeFixed=null] * @return {Function|Object} * @return {dc_graph.diagram} **/ _diagram.nodeFixed = _diagram.nodeFixedAccessor = property(null); /** * Set or get the function which will be used to retrieve the stroke color for the edges. * @method edgeStroke * @memberof dc_graph.diagram * @instance * @param {Function|String} [edgeStroke='black'] * @return {Function|String} * @return {dc_graph.diagram} **/ _diagram.edgeStroke = _diagram.edgeStrokeAccessor = property('black'); /** * Set or get the function which will be used to retrieve the stroke width for the edges. * @method edgeStrokeWidth * @memberof dc_graph.diagram * @instance * @param {Function|Number} [edgeStrokeWidth=1] * @return {Function|Number} * @return {dc_graph.diagram} **/ _diagram.edgeStrokeWidth = _diagram.edgeStrokeWidthAccessor = property(1); _diagram.edgeStrokeDashArray = property(null); /** * Set or get the function which will be used to retrieve the edge opacity, a number from 0 * to 1. * @method edgeOpacity * @memberof dc_graph.diagram * @instance * @param {Function|Number} [edgeOpacity=1] * @return {Function|Number} * @return {dc_graph.diagram} **/ _diagram.edgeOpacity = _diagram.edgeOpacityAccessor = property(1); /** * Set or get the function which will be used to retrieve the edge label text. The label is * displayed when an edge is hovered over. By default, uses the `edgeKey`. * @method edgeLabel * @memberof dc_graph.diagram * @instance * @param {Function|String} [edgeLabel] * @example * // Default behavior * diagram.edgeLabel(function(e) { * return _diagram.edgeKey()(e); * }); * @return {Function|String} * @return {dc_graph.diagram} **/ _diagram.edgeLabel = _diagram.edgeLabelAccessor = property(function(e) { return _diagram.edgeKey()(e); }); // vertical spacing when there are multiple lines of edge label _diagram.edgeLabelSpacing = property(12); /** * Set or get the function which will be used to retrieve the name of the arrowhead to use * for the target/ head/destination of the edge. Arrow symbols can be specified with * `.defineArrow()`. Return null to display no arrowhead. * @method edgeArrowhead * @memberof dc_graph.diagram * @instance * @param {Function|String} [edgeArrowhead='vee'] * @return {Function|String} * @return {dc_graph.diagram} **/ _diagram.edgeArrowhead = _diagram.edgeArrowheadAccessor = property('vee'); /** * Set or get the function which will be used to retrieve the name of the arrow tail to use * for the tail/source of the edge. Arrow symbols can be specified with * `.defineArrow()`. Return null to display no arrowtail. * @method edgeArrowtail * @memberof dc_graph.diagram * @instance * @param {Function|String} [edgeArrowtail=null] * @return {Function|String} * @return {dc_graph.diagram} **/ _diagram.edgeArrowtail = _diagram.edgeArrowtailAccessor = property(null); /** * Multiplier for arrow size. * @method edgeArrowSize * @memberof dc_graph.diagram * @instance * @param {Function|Number} [edgeArrowSize=1] * @return {Function|Number} * @return {dc_graph.diagram} **/ _diagram.edgeArrowSize = property(1); /** * To draw an edge but not have it affect the layout, specify a function which returns * false for that edge. By default, will return false if the `notLayout` field of the edge * value is truthy, true otherwise. * @method edgeIsLayout * @memberof dc_graph.diagram * @instance * @param {Function|Boolean} [edgeIsLayout] * @example * // Default behavior * diagram.edgeIsLayout(function(kv) { * return !kv.value.notLayout; * }); * @return {Function|Boolean} * @return {dc_graph.diagram} **/ _diagram.edgeIsLayout = _diagram.edgeIsLayoutAccessor = property(function(kv) { return !kv.value.notLayout; }); // if false, don't draw or layout the edge. this is not documented because it seems like // the interface could be better and this combined with edgeIsLayout. (currently there is // no way to layout but not draw an edge.) _diagram.edgeIsShown = property(true); /** * Currently, three strategies are supported for specifying the lengths of edges: * * 'individual' - uses the `edgeLength` for each edge. If it returns falsy, uses the * `baseLength` * * 'symmetric', 'jaccard' - compute the edge length based on the graph structure around * the edge. See * {@link https://github.com/tgdwyer/WebCola/wiki/link-lengths the cola.js wiki} * for more details. * 'none' - no edge lengths will be specified * * **Deprecated**: Use {@link dc_graph.cola_layout#lengthStrategy cola_layout.lengthStrategy} instead. * @method lengthStrategy * @memberof dc_graph.diagram * @instance * @param {Function|String} [lengthStrategy='symmetric'] * @return {Function|String} * @return {dc_graph.diagram} **/ _diagram.lengthStrategy = deprecate_layout_algo_parameter('lengthStrategy'); /** * When the `.lengthStrategy` is 'individual', this accessor will be used to read the * length of each edge. By default, reads the `distance` field of the edge. If the * distance is falsy, uses the `baseLength`. * @method edgeLength * @memberof dc_graph.diagram * @instance * @param {Function|Number} [edgeLength] * @example * // Default behavior * diagram.edgeLength(function(kv) { * return kv.value.distance; * }); * @return {Function|Number} * @return {dc_graph.diagram} **/ _diagram.edgeLength = _diagram.edgeDistanceAccessor = property(function(kv) { return kv.value.distance; }); /** * This should be equivalent to rankdir and ranksep in the dagre/graphviz nomenclature, but for * now it is separate. * * **Deprecated**: use {@link dc_graph.cola_layout#flowLayout cola_layout.flowLayout} instead. * @method flowLayout * @memberof dc_graph.diagram * @instance * @param {Object} [flowLayout] * @example * // No flow (default) * diagram.flowLayout(null) * // flow in x with min separation 200 * diagram.flowLayout({axis: 'x', minSeparation: 200}) **/ _diagram.flowLayout = deprecate_layout_algo_parameter('flowLayout'); /** * Direction to draw ranks. Currently for dagre and expand_collapse, but I think cola could be * generated from graphviz-style since it is more general. * * **Deprecated**: use {@link dc_graph.dagre_layout#rankdir dagre_layout.rankdir} instead. * @method rankdir * @memberof dc_graph.diagram * @instance * @param {String} [rankdir] **/ _diagram.rankdir = deprecate_layout_algo_parameter('rankdir'); /** * Gets or sets the default edge length (in pixels) when the `.lengthStrategy` is * 'individual', and the base value to be multiplied for 'symmetric' and 'jaccard' edge * lengths. * * **Deprecated**: use {@link dc_graph.cola_layout#baseLength cola_layout.baseLength} instead. * @method baseLength * @memberof dc_graph.diagram * @instance * @param {Number} [baseLength] * @return {Number} * @return {dc_graph.diagram} **/ _diagram.baseLength = deprecate_layout_algo_parameter('baseLength'); /** * Gets or sets the transition duration, the length of time each change to the diagram will * be animated. * @method transitionDuration * @memberof dc_graph.diagram * @instance * @param {Number} [transitionDuration=500] * @return {Number} * @return {dc_graph.diagram} **/ _diagram.transitionDuration = property(500); /** * How transitions should be split into separate animations to emphasize * the delete, modify, and insert operations: * * `none`: modify and insert operations animate at the same time * * `modins`: modify operations happen before inserts * * `insmod`: insert operations happen before modifies * * Deletions always happen before/during layout computation. * @method stageTransitions * @memberof dc_graph.diagram * @instance * @param {String} [stageTransitions='none'] * @return {String} * @return {dc_graph.diagram} **/ _diagram.stageTransitions = property('none'); /** * The delete transition happens simultaneously with layout, which can take longer * than the transition duration. Delaying it can bring it closer to the other * staged transitions. * @method deleteDelay * @memberof dc_graph.diagram * @instance * @param {Number} [deleteDelay=0] * @return {Number} * @return {dc_graph.diagram} **/ _diagram.deleteDelay = property(0); /** * Whether to put connected components each in their own group, to stabilize layout. * @method groupConnected * @memberof dc_graph.diagram * @instance * @param {String} [groupConnected=false] * @return {String} * @return {dc_graph.diagram} **/ _diagram.groupConnected = deprecate_layout_algo_parameter('groupConnected'); /** * Gets or sets the maximum time spent doing layout for a render or redraw. Set to 0 for no * limit. * @method timeLimit * @memberof dc_graph.diagram * @instance * @param {Function|Number} [timeLimit=0] * @return {Function|Number} * @return {dc_graph.diagram} **/ _diagram.timeLimit = property(0); /** * Gets or sets a function which will be called with the current nodes and edges on each * redraw in order to derive new layout constraints. The constraints are built from scratch * on each redraw. * * This can be used to generate alignment (rank) or axis constraints. By default, no * constraints will be added, although cola.js uses constraints internally to implement * flow and overlap prevention. See * {@link https://github.com/tgdwyer/WebCola/wiki/Constraints the cola.js wiki} * for more details. * * For convenience, dc.graph.js implements a other constraints on top of those implemented * by cola.js: * * 'ordering' - the nodes will be ordered on the specified `axis` according to the keys * returned by the `ordering` function, by creating separation constraints using the * specified `gap`. * * 'circle' - (experimental) the nodes will be placed in a circle using "wheel" * edge lengths similar to those described in * {@link http://www.csse.monash.edu.au/~tdwyer/Dwyer2009FastConstraints.pdf Scalable, Versatile, and Simple Constrained Graph Layout} * *Although this is not as performant or stable as might be desired, it may work for * simple cases. In particular, it should use edge length *constraints*, which don't yet * exist in cola.js.* * * Because it is tedious to write code to generate constraints for a graph, **dc.graph.js** * also includes a {@link #dc_graph+constraint_pattern constraint generator} to produce * this constrain function, specifying the constraints themselves in a graph. * @method constrain * @memberof dc_graph.diagram * @instance * @param {Function} [constrain] * @return {Function} * @return {dc_graph.diagram} **/ _diagram.constrain = property(function(nodes, edges) { return []; }); /** * If there are multiple edges between the same two nodes, start them this many pixels away * from the original so they don't overlap. * @method parallelEdgeOffset * @memberof dc_graph.diagram * @instance * @param {Number} [parallelEdgeOffset=10] * @return {Number} * @return {dc_graph.diagram} **/ _diagram.parallelEdgeOffset = property(10); /** * By default, edges are added to the layout in the order that `.edgeGroup().all()` returns * them. If specified, `.edgeOrdering` provides an accessor that returns a key to sort the * edges on. * * *It would be better not to rely on ordering to affect layout, but it may affect the * layout in some cases. (Probably less than node ordering, but it does affect which * parallel edge is which.)* * @method edgeOrdering * @memberof dc_graph.diagram * @instance * @param {Function} [edgeOrdering=null] * @return {Function} * @return {dc_graph.diagram} **/ _diagram.edgeOrdering = property(null); _diagram.edgeSort = property(null); _diagram.cascade = cascade(_diagram); /** * Currently there are some bugs when the same instance of cola.js is used multiple * times. (In particular, overlaps between nodes may not be eliminated * {@link https://github.com/tgdwyer/WebCola/issues/118 if cola is not reinitialized} * This flag can be set true to construct a new cola layout object on each redraw. However, * layout seems to be more stable if this is set false, so hopefully this will be fixed * soon. * @method initLayoutOnRedraw * @memberof dc_graph.diagram * @instance * @param {Boolean} [initLayoutOnRedraw=false] * @return {Boolean} * @return {dc_graph.diagram} **/ _diagram.initLayoutOnRedraw = property(false); /** * Whether to perform layout when the data is unchanged from the last redraw. * @method layoutUnchanged * @memberof dc_graph.diagram * @instance * @param {Boolean} [layoutUnchanged=false] * @return {Boolean} * @return {dc_graph.diagram} **/ _diagram.layoutUnchanged = property(false); _diagram.nodeChangeSelect = property(function() { if(_diagram.layoutEngine().supportsMoving && _diagram.layoutEngine().supportsMoving()) return topology_node; else return basic_node; }); _diagram.edgeChangeSelect = property(function() { if(_diagram.layoutEngine().supportsMoving && _diagram.layoutEngine().supportsMoving()) return topology_edge; else return basic_edge; }); /** * When `layoutUnchanged` is false, this will force layout to happen again. This may be needed * when changing a parameter but not changing the topology of the graph. (Yes, probably should * not be necessary.) * @method relayout * @memberof dc_graph.diagram * @instance * @return {dc_graph.diagram} **/ _diagram.relayout = function() { _nodes_snapshot = _edges_snapshot = null; return this; }; /** * Function to call to generate an initial layout. Takes (diagram, nodes, edges) * * **Deprecated**: The only layout that was using this was `tree_positions` and it never * worked as an initialization step for cola, as was originally intended. Now that * `tree_layout` is a layout algorithm, this should go away. * * In the future, there will be support for chaining layout algorithms. But that will be a * matter of composing them into a super-algorithm, not a special step like this was. * @method initialLayout * @memberof dc_graph.diagram * @instance * @param {Function} [initialLayout=null] * @return {Function} * @return {dc_graph.diagram} **/ _diagram.initialLayout = deprecated_property('initialLayout is deprecated - use layout algorithms instead', null); _diagram.initialOnly = deprecated_property('initialOnly is deprecated - see the initialLayout deprecation notice in the documentation', false); /** * By default, all nodes are included, and edges are only included if both end-nodes are * visible. If `.induceNodes` is set, then only nodes which have at least one edge will be * shown. * @method induceNodes * @memberof dc_graph.diagram * @instance * @param {Boolean} [induceNodes=false] * @return {Boolean} * @return {dc_graph.diagram} **/ _diagram.induceNodes = property(false); /** * If this flag is true, the positions of nodes and will be updated while layout is * iterating. If false, the positions will only be updated once layout has * stabilized. Note: this may not be compatible with transitionDuration. * @method showLayoutSteps * @memberof dc_graph.diagram * @instance * @param {Boolean} [showLayoutSteps=false] * @return {Boolean} * @return {dc_graph.diagram} **/ _diagram.showLayoutSteps = property(false); /** * Assigns a legend object which will be displayed within the same SVG element and * according to the visual encoding of this diagram. * @method legend * @memberof dc_graph.diagram * @instance * @param {Object} [legend=null] * @return {Object} * @return {dc_graph.diagram} **/ // (pre-deprecated; see below) /** * Specifies another kind of child layer or interface. For example, this can * be used to display tooltips on nodes using `dc_graph.tip`. * The child needs to support a `parent` method, the diagram to modify. * @method child * @memberof dc_graph.diagram * @instance * @param {String} [id] - the name of the child to modify or add * @param {Object} [object] - the child object to add, or null to remove * @example * // Display tooltips on node hover, via the d3-tip library * var tip = dc_graph.tip() * tip.content(function(n, k) { * // you can do an asynchronous call here, e.g. d3.json, if you need * // to fetch data to show the tooltip - just call k() with the content * k("This is <em>" + n.orig.value.name + "</em>"); * }); * diagram.child('tip', tip); * @return {dc_graph.diagram} **/ _diagram.mode = _diagram.child = named_children(); _diagram.mode.reject = function(id, object) { var rtype = _diagram.renderer().rendererType(); if(!object) return false; // null is always a valid mode for any renderer if(!object.supportsRenderer) console.log('could not check if "' + id + '" is compatible with ' + rtype); else if(!object.supportsRenderer(rtype)) return 'not installing "' + id + '" because it is not compatible with renderer ' + rtype; return false; }; _diagram.legend = deprecate_function(".legend() is deprecated; use .child() for more control & multiple legends", function(_) { if(!arguments.length) return _diagram.child('node-legend'); _diagram.child('node-legend', _); return _diagram; }); /** * Specify 'cola' (the default) or 'dagre' as the Layout Algorithm and it will replace the * back-end. * * **Deprecated**: use {@link dc_graph.diagram#layoutEngine diagram.layoutEngine} with the engine * object instead * @method layoutAlgorithm * @memberof dc_graph.diagram * @instance * @param {String} [algo='cola'] - the name of the layout algorithm to use * @example * // use dagre for layout * diagram.layoutAlgorithm('dagre'); * @return {dc_graph.diagram} **/ _diagram.layoutAlgorithm = function(value, skipWarning) { if(!arguments.length) return _diagram.layoutEngine() ? _diagram.layoutEngine().layoutAlgorithm() : 'cola'; if(!skipWarning) console.warn('dc.graph.diagram.layoutAlgorithm is deprecated - pass the layout engine object to dc_graph.diagram.layoutEngine instead'); var engine; switch(value) { case 'cola': engine = dc_graph.cola_layout(); break; case 'dagre': engine = dc_graph.dagre_layout(); } engine = dc_graph.webworker_layout(engine); _diagram.layoutEngine(engine); return this; }; /** * The layout engine determines positions of nodes and edges. * @method layoutEngine * @memberof dc_graph.diagram * @instance * @param {Object} [engine=null] - the layout engine to use * @example * // use cola with no webworker * diagram.layoutEngine(dc_graph.cola_layout()); * // use dagre with a webworker * diagram.layoutEngine(dc_graph.webworker_layout(dc_graph.dagre_layout())); **/ _diagram.layoutEngine = property(null).react(function(val) { if(val && val.parent) val.parent(_diagram); if(_diagram.renderer().isRendered()) { // remove any calculated points, if engine did that Object.keys(_edges).forEach(function(k) { _edges[k].cola.points = null; }); // initialize engine initLayout(val); } }); _diagram.renderer = property(dc_graph.render_svg().parent(_diagram)).react(function(r) { if(_diagram.renderer()) _diagram.renderer().parent(null); r.parent(_diagram); }); // S-spline any edges that are not going in this direction _diagram.enforceEdgeDirection = property(null); _diagram.tickSize = deprecate_layout_algo_parameter('tickSize'); _diagram.uniqueId = function() { return _diagram.anchorName().replace(/[ .#=\[\]"]/g, '-'); }; _diagram.edgeId = function(e) { return 'edge-' + _diagram.edgeKey.eval(e).replace(/[^\w-_]/g, '-'); }; _diagram.arrowId = function(e, kind) { return 'arrow-' + kind + '-' + _diagram.uniqueId() + '-' + _diagram.edgeId(e); }; _diagram.textpathId = function(e) { return 'textpath-' + _diagram.uniqueId() + '-' + _diagram.edgeId(e); }; // this kind of begs a (meta)graph ADT // instead of munging this into the diagram _diagram.getNode = function(id) { return _nodes[id] ? _nodes[id].orig : null; }; _diagram.getWholeNode = function(id) { return _nodes[id] ? _nodes[id] : null; }; _diagram.getEdge = function(id) { return _edges[id] ? _edges[id].orig : null; }; _diagram.getWholeEdge = function(id) { return _edges[id] ? _edges[id] : null; }; // again, awful, we need an ADT _diagram.getPort = function(nid, eid, name) { return _ports[port_name(nid, eid, name)]; }; _diagram.nodePorts = function() { return _nodePorts; }; _diagram.getWholeCluster = function(id) { return _clusters[id] || null; }; /** * Instructs cola.js to fit the connected components. * * **Deprecated**: Use * {@link dc_graph.cola_layout#handleDisconnected cola_layout.handleDisconnected} instead. * @method handleDisconnected * @memberof dc_graph.diagram * @instance * @param {Boolean} [handleDisconnected=true] * @return {Boolean} * @return {dc_graph.diagram} **/ _diagram.handleDisconnected = deprecate_layout_algo_parameter('handleDisconnected'); function initLayout(engine) { if(!_diagram.layoutEngine()) _diagram.layoutAlgorithm('cola', true); (engine || _diagram.layoutEngine()).init({ width: _diagram.width(), height: _diagram.height() }); } _diagram.forEachChild = function(node, children, idf, f) { children.enum().forEach(function(key) { f(children(key), node.filter(function(n) { return idf(n) === key; })); }); }; _diagram.forEachShape = function(node, f) { _diagram.forEachChild(node, _diagram.shape, function(n) { return n.dcg_shape.shape; }, f); }; _diagram.forEachContent = function(node, f) { _diagram.forEachChild(node, _diagram.content, _diagram.nodeContent.eval, f); }; function has_source_and_target(e) { return !!e.source && !!e.target; } // three stages: delete before layout, and modify & insert split the transitionDuration _diagram.stagedDuration = function() { return (_diagram.stageTransitions() !== 'none') ? _diagram.transitionDuration() / 2 : _diagram.transitionDuration(); }; _diagram.stagedDelay = function(is_enter) { return _diagram.stageTransitions() === 'none' || _diagram.stageTransitions() === 'modins' === !is_enter ? 0 : _diagram.transitionDuration() / 2; }; _diagram.isRunning = function() { return _running; }; function svg_specific(name) { return trace_function('trace', name + '() is specific to the SVG renderer', function() { return _diagram.renderer()[name].apply(this, arguments); }); } function call_on_renderer(name) { return trace_function('trace', 'calling ' + name + '() on renderer', function() { return _diagram.renderer()[name].apply(this, arguments); }); } _diagram.svg = svg_specific('svg'); _diagram.g = svg_specific('g'); _diagram.select = svg_specific('select'); _diagram.selectAll = svg_specific('selectAll'); _diagram.addOrRemoveDef = svg_specific('addOrRemoveDef'); _diagram.selectAllNodes = svg_specific('selectAllNodes'); _diagram.selectAllEdges = svg_specific('selectAllEdges'); _diagram.selectNodePortsOfStyle = svg_specific('selectNodePortsOfStyle'); _diagram.zoom = svg_specific('zoom'); _diagram.translate = svg_specific('translate'); _diagram.scale = svg_specific('scale'); function renderer_specific(name) { return trace_function('trace', name + '() will have renderer-specific arguments', function() { return _diagram.renderer()[name].apply(this, arguments); }); } _diagram.renderNode = svg_specific('renderNode'); _diagram.renderEdge = svg_specific('renderEdge'); _diagram.redrawNode = svg_specific('redrawNode'); _diagram.redrawEdge = svg_specific('redrawEdge'); _diagram.reposition = call_on_renderer('reposition'); /** * Standard dc.js * {@link https://github.com/dc-js/dc.js/blob/develop/web/docs/api-latest.md#dc.baseMixin baseMixin} * method. Computes a new layout based on the nodes and edges in the edge groups, and * displays the diagram. To the extent possible, the diagram will minimize changes in * positions from the previous layout. `.render()` must be called the first time, and * `.redraw()` can be called after that. * * `.redraw()` will be triggered by changes to the filters in any other charts in the same * dc.js chart group. * * Unlike in dc.js, `redraw` executes asynchronously, because drawing can be computationally * intensive, and the diagram will be drawn multiple times if * {@link #dc_graph.diagram+showLayoutSteps showLayoutSteps} * is enabled. Watch the {@link #dc_graph.diagram+on 'end'} event to know when layout is * complete. * @method redraw * @memberof dc_graph.diagram * @instance * @return {dc_graph.diagram} **/ var _needsRedraw = false; _diagram.redraw = function () { // since dc.js can receive UI events and trigger redraws whenever it wants, // and cola absolutely will not tolerate being poked while it's doing layout, // we need to guard the startLayout call. if(_running) { _needsRedraw = true; return this; } else return _diagram.startLayout(); }; /** * Standard dc.js * {@link https://github.com/dc-js/dc.js/blob/develop/web/docs/api-latest.md#dc.baseMixin baseMixin} * method. Erases any existing SVG elements and draws the diagram from scratch. `.render()` * must be called the first time, and `.redraw()` can be called after that. * @method render * @memberof dc_graph.diagram * @instance * @return {dc_graph.diagram} **/ _diagram.render = function() { if(_diagram.renderer().isRendered()) _dispatch.reset(); if(!_diagram.initLayoutOnRedraw()) initLayout(); _nodes = {}; _edges = {}; _ports = {}; _clusters = {}; // start out with 1:1 zoom _diagram.x(d3.scale.linear() .domain([0, _diagram.width()]) .range([0, _diagram.width()])); _diagram.y(d3.scale.linear() .domain([0, _diagram.height()]) .range([0, _diagram.height()])); _diagram.renderer().initializeDrawing(); _dispatch.render(); _diagram.redraw(); return this; }; _diagram.refresh = call_on_renderer('refresh'); _diagram.width_is_automatic = function() { return _width === 'auto'; }; _diagram.height_is_automatic = function() { return _height === 'auto'; }; function detect_size_change() { var oldWidth = _lastWidth, oldHeight = _lastHeight; var newWidth = _diagram.width(), newHeight = _diagram.height(); if(oldWidth !== newWidth || oldHeight !== newHeight) _diagram.renderer().rezoom(oldWidth, oldHeight, newWidth, newHeight); } // extract just the topology-related parts of nodes & edges to see if // graph has changed wrt layout. imperfect heuristic: assume that the original // data as well as all cola fields starting with dcg_ are related to topology function dcg_fields(cola) { var entries = Object.entries(cola) .filter(function(entry) { return /^dcg_/.test(entry[0]); }); return entries.reduce(function(p, entry) { p[entry[0]] = entry[1]; return p; }, {}); } function topology_node(n) { return {orig: get_original(n), cola: dcg_fields(n.cola)}; } function topology_edge(e) { return {orig: get_original(e), cola: dcg_fields(e.cola)}; } function basic_node(n) { var n0 = get_original(n); return { orig: { key: n0.key, value: Object.fromEntries( Object.entries(n0.value) .filter(function(kv) { return kv[0] !== 'fixedPos'; })) } }; } function basic_edge(e) { return {orig: get_original(e)}; } _diagram.startLayout = function () { var nodes = _diagram.nodeGroup().all(); var edges = _diagram.edgeGroup().all(); var ports = _diagram.portGroup() ? _diagram.portGroup().all() : []; var clusters = _diagram.clusterGroup() ? _diagram.clusterGroup().all() : []; if(_running) { throw new Error('dc_graph.diagram.redraw already running!'); } _running = true; if(_diagram.width_is_automatic() || _diagram.height_is_automatic()) detect_size_change(); else _diagram.renderer().resize(); if(_diagram.initLayoutOnRedraw()) initLayout(); _diagram.layoutEngine().stop(); _dispatch.preDraw(); // ordering shouldn't matter, but we support ordering in case it does if(_diagram.nodeOrdering()) { nodes = crossfilter.quicksort.by(_diagram.nodeOrdering())(nodes.slice(0), 0, nodes.length); } if(_diagram.edgeOrdering()) { edges = crossfilter.quicksort.by(_diagram.edgeOrdering())(edges.slice(0), 0, edges.length); } var wnodes = regenerate_objects(_nodes, nodes, null, function(v) { return _diagram.nodeKey()(v); }, function(v1, v) { v1.orig = v; v1.cola = v1.cola || {}; v1.cola.dcg_nodeKey = _diagram.nodeKey.eval(v1); v1.cola.dcg_nodeParentCluster = _diagram.nodeParentCluster.eval(v1); _diagram.layoutEngine().populateLayoutNode(v1.cola, v1); }); var wedges = regenerate_objects(_edges, edges, null, function(e) { return _diagram.edgeKey()(e); }, function(e1, e) { e1.orig = e; e1.cola = e1.cola || {}; e1.cola.dcg_edgeKey = _diagram.edgeKey.eval(e1); e1.cola.dcg_edgeSource = _diagram.edgeSource.eval(e1); e1.cola.dcg_edgeTarget = _diagram.edgeTarget.eval(e1); e1.source = _nodes[e1.cola.dcg_edgeSource]; e1.target = _nodes[e1.cola.dcg_edgeTarget]; e1.sourcePort = e1.sourcePort || {}; e1.targetPort = e1.targetPort || {}; _diagram.layoutEngine().populateLayoutEdge(e1.cola, e1); }); // remove edges that don't have both end nodes wedges = wedges.filter(has_source_and_target); // remove self-edges (since we can't draw them - will be option later) wedges = wedges.filter(function(e) { return e.source !== e.target; }); wedges = wedges.filter(_diagram.edgeIsShown.eval); // now we know which ports should exist var needports = wedges.map(function(e) { if(_diagram.edgeSourcePortName.eval(e)) return port_name(_diagram.edgeSource.eval(e), null, _diagram.edgeSourcePortName.eval(e)); else return port_name(null, _diagram.edgeKey.eval(e), 'source'); }); needports = needports.concat(wedges.map(function(e) { if(_diagram.edgeTargetPortName.eval(e)) return port_name(_diagram.edgeTarget.eval(e), null, _diagram.edgeTargetPortName.eval(e)); else return port_name(null, _diagram.edgeKey.eval(e), 'target'); })); // remove any invalid ports so they don't crash in confusing ways later ports = ports.filter(function(p) { return _diagram.portNodeKey() && _diagram.portNodeKey()(p) || _diagram.portEdgeKey() && _diagram.portEdgeKey()(p); }); var wports = regenerate_objects(_ports, ports, needports, function(p) { return port_name(_diagram.portNodeKey() && _diagram.portNodeKey()(p), _diagram.portEdgeKey() && _diagram.portEdgeKey()(p), _diagram.portName()(p)); }, function(p1, p) { p1.orig = p; if(p1.named) p1.edges = []; }, function(k, p) { console.assert(k, 'should have screened out invalid ports'); // it's dumb to parse the id we just created. as usual, i blame the lack of metagraphs var parse = split_port_name(k); if(parse.nodeKey) { p.node = _nodes[parse.nodeKey]; p.named = true; } else { var e = _edges[parse.edgeKey]; p.node = e[parse.name]; p.edges = [e]; p.named = false; } p.name = parse.name; }); // remove any ports where the end-node was not found, to avoid crashing elsewhere wports = wports.filter(function(p) { return p.node; }); // find all edges for named ports wedges.forEach(function(e) { var name = _diagram.edgeSourcePortName.eval(e); if(name) _ports[port_name(_diagram.nodeKey.eval(e.source), null, name)].edges.push(e); name = _diagram.edgeTargetPortName.eval(e); if(name) _ports[port_name(_diagram.nodeKey.eval(e.target), null, name)].edges.push(e); }); // optionally, delete nodes that have no edges if(_diagram.induceNodes()) { var keeps = {}; wedges.forEach(function(e) { keeps[e.cola.dcg_edgeSource] = true; keeps[e.cola.dcg_edgeTarget] = true; }); wnodes = wnodes.filter(function(n) { return keeps[n.cola.dcg_nodeKey]; }); for(var k in _nodes) if(!keeps[k]) delete _nodes[k]; } var needclusters = d3.set(wnodes.map(function(n) { return _diagram.nodeParentCluster.eval(n); }).filter(identity)).values(); var wclusters = regenerate_objects(_clusters, clusters, needclusters, function(c) { return _diagram.clusterKey()(c); }, function(c1, c) { // assign c1.orig = c; c1.cola = c1.cola || { dcg_clusterKey: _diagram.clusterKey.eval(c1), dcg_clusterParent: _diagram.clusterParent.eval(c1) }; }, function(k, c) { // create }); wnodes.forEach(function(v, i) { v.index = i; }); // announce new data _dispatch.data(_diagram, _nodes, wnodes, _edges, wedges, _ports, wports); _stats = {nnodes: wnodes.length, nedges: wedges.length}; // fixed nodes may have been affected by .data() so calculate now wnodes.forEach(function(v) { if(_diagram.nodeFixed()) v.cola.dcg_nodeFixed = _diagram.nodeFixed.eval(v); }); // annotate parallel edges so we can draw them specially if(_diagram.parallelEdgeOffset()) { var em = new Array(wnodes.length); for(var i = 0; i < wnodes.length; ++i) em[i] = new Array(i); wedges.forEach(function(e) { e.pos = e.pos || {}; var min, max, minattr, maxattr; if(e.source.index < e.target.index) { min = e.source.index; max = e.target.index; minattr = 'edgeSourcePortName'; maxattr = 'edgeTargetPortName'; } else { max = e.source.index; min = e.target.index; maxattr = 'edgeSourcePortName'; minattr = 'edgeTargetPortName'; } var minport = _diagram[minattr].eval(e) || 'no port', maxport = _diagram[maxattr].eval(e) || 'no port'; em[max][min] = em[max][min] || {}; em[max][min][maxport] = em[max][min][maxport] || {}; e.parallel = em[max][min][maxport][minport] = em[max][min][maxport][minport] || { rev: [], edges: [] }; e.parallel.edges.push(e); e.parallel.rev.push(min !== e.source.index); }); } var drawState = _diagram.renderer().startRedraw(_dispatch, wnodes, wedges); // really we should have layout chaining like in the good old Dynagraph days // the ordering of this and the previous 4 statements is somewhat questionable if(_diagram.initialLayout()) _diagram.initialLayout()(_diagram, wnodes, wedges); // no layout if the topology and layout parameters haven't changed var skip_layout = false; if(!_diagram.layoutUnchanged()) { var node_fields = _diagram.nodeChangeSelect()(), edge_fields = _diagram.edgeChangeSelect()(); var nodes_snapshot = JSON.stringify(wnodes.map(node_fields)); var edges_snapshot = JSON.stringify(wedges.map(edge_fields)); if(nodes_snapshot === _nodes_snapshot && edges_snapshot === _edges_snapshot) skip_layout = true; _nodes_snapshot = nodes_snapshot; _edges_snapshot = edges_snapshot; } // edge lengths may be affected by node sizes wedges.forEach(function(e) { e.cola.dcg_edgeLength = _diagram.edgeLength.eval(e); }); // cola constraints always use indices, but node references // are more friendly, so translate those // i am not satisfied with this constraint generation api... // https://github.com/dc-js/dc.graph.js/issues/10 var constraints = _diagram.constrain()(_diagram, wnodes, wedges); // warn if there are any loops (before changing names to indices) // it would be better to do this in webcola // (for one thing, this duplicates logic in rectangle.ts) // but by that time it has lost the names of things, // so the output would be difficult to use var constraints_by_left = constraints.reduce(function(p, c) { if(c.type) { switch(c.type) { case 'alignment': var left = c.offsets[0].node; p[left] = p[left] || []; c.offsets.slice(1).forEach(function(o) { p[left].push({node: o.node, in_constraint: c}); }); break; } } else if(c.axis) { p[c.left] = p[c.left] || []; p[c.left].push({node: c.right, in_constraint: c}); } return p; }, {}); var touched = {}; function find_constraint_loops(con, stack) { var left = con.node; stack = stack || []; var loop = stack.find(function(con) { return con.node === left; }); stack = stack.concat([con]); if(loop) console.warn('found a loop in constraints', stack); if(touched[left]) return; touched[left] = true; if(!constraints_by_left[left]) return; constraints_by_left[left].forEach(function(right) { find_constraint_loops(right, stack); }); } Object.keys(constraints_by_left).forEach(function(left) { if(!touched[left]) find_constraint_loops({node: left, in_constraint: null}); }); // translate references from names to indices (ugly) var invalid_constraints = []; constraints.forEach(function(c) { if(c.type) { switch(c.type) { case 'alignment': c.offsets.forEach(function(o) { o.node = _nodes[o.node].index; }); break; case 'circle': c.nodes.forEach(function(n) { n.node = _nodes[n.node].index; }); break; } } else if(c.axis && c.left && c.right) { c.left = _nodes[c.left].index; c.right = _nodes[c.right].index; } else invalid_constraints.push(c); }); if(invalid_constraints.length) console.warn(invalid_constraints.length + ' invalid constraints', invalid_constraints); // pseudo-cola.js features // 1. non-layout edges are drawn but not told to cola.js var layout_edges = wedges.filter(_diagram.edgeIsLayout.eval); var nonlayout_edges = wedges.filter(function(x) { return !_diagram.edgeIsLayout.eval(x); }); // 2. type=circle constraints var circle_constraints = constraints.filter(function(c) { return c.type === 'circle'; }); constraints = constraints.filter(function(c) { return c.type !== 'circle'; }); circle_constraints.forEach(function(c) { var R = (c.distance || _diagram.baseLength()*4) / (2*Math.sin(Math.PI/c.nodes.length)); var nindices = c.nodes.map(function(x) { return x.node; }); var namef = function(i) { return _diagram.nodeKey.eval(wnodes[i]); }; var wheel = dc_graph.wheel_edges(namef, nindices, R) .map(function(e) { var e1 = {internal: e}; e1.source = _nodes[e.sourcename]; e1.target = _nodes[e.targetname]; return e1; }); layout_edges = layout_edges.concat(wheel); }); // 3. ordered alignment var ordered_constraints = constraints.filter(function(c) { return c.type === 'ordering'; }); constraints = constraints.filter(function(c) { return c.type !== 'ordering'; }); ordered_constraints.forEach(function(c) { var sorted = c.nodes.map(function(n) { return _nodes[n]; }); if(c.ordering) { var sort = crossfilter.quicksort.by(param(c.ordering)); sorted = sort(sorted, 0, sorted.length); } var left; sorted.forEach(function(n, i) { if(i===0) left = n; else { constraints.push({ left: left.index, right: (left = n).index, axis: c.axis, gap: c.gap }); } }); }); if(skip_layout) { _running = false; // init_node_ports? _diagram.renderer().draw(drawState, true); _diagram.renderer().drawPorts(drawState); _diagram.renderer().fireTSEvent(_dispatch, drawState); check_zoom(drawState); return this; } var startTime = Date.now(); function populate_cola(rnodes, redges, rclusters) { rnodes.forEach(function(rn) { var n = _nodes[rn.dcg_nodeKey]; if(!n) { console.warn('received node "' + rn.dcg_nodeKey + '" that we did not send, ignored'); return; } n.cola.x = rn.x; n.cola.y = rn.y; n.cola.z = rn.z; }); redges.forEach(function(re) { var e = _edges[re.dcg_edgeKey]; if(!e) { console.warn('received edge "' + re.dcg_edgeKey + '" that we did not send, ignored'); return; } if(re.points) e.cola.points = re.points; }); wclusters.forEach(function(c) { c.cola.bounds = null; }); if(rclusters) rclusters.forEach(function(rc) { var c = _clusters[rc.dcg_clusterKey]; if(!c) { console.warn('received cluster "' + rc.dcg_clusterKey + '" that we did not send, ignored'); return; } if(rc.bounds) c.cola.bounds = rc.bounds; }); } _diagram.layoutEngine() .on('tick.diagram', function(nodes, edges, clusters) { var elapsed = Date.now() - startTime; if(!_diagram.initialOnly()) populate_cola(nodes, edges, clusters); if(_diagram.showLayoutSteps()) { init_node_ports(_nodes, wports); _dispatch.receivedLayout(_diagram, _nodes, wnodes, _edges, wedges, _ports, wports); propagate_port_positions(_nodes, wedges, _ports); _diagram.renderer().draw(drawState, true); _diagram.renderer().drawPorts(drawState); // should do this only once _diagram.renderer().fireTSEvent(_dispatch, drawState); } if(_needsRedraw || _diagram.timeLimit() && elapsed > _diagram.timeLimit()) { console.log('cancelled'); _diagram.layoutEngine().stop(); } }) .on('end.diagram', function(nodes, edges, clusters) { if(!_diagram.showLayoutSteps()) { if(!_diagram.initialOnly()) populate_cola(nodes, edges, clusters); init_node_ports(_nodes, wports); _dispatch.receivedLayout(_diagram, _nodes, wnodes, _edges, wedges, _ports, wports); propagate_port_positions(_nodes, wedges, _ports); _diagram.renderer().draw(drawState, true); _diagram.renderer().drawPorts(drawState); _diagram.renderer().fireTSEvent(_dispatch, drawState); } else _diagram.layoutDone(true); check_zoom(drawState); }) .on('start.diagram', function() { console.log('algo ' + _diagram.layoutEngine().layoutAlgorithm() + ' started.'); _dispatch.start(); }); if(_diagram.initialOnly()) _diagram.layoutEngine().dispatch().end(wnodes, wedges); else { _dispatch.start(); // cola doesn't seem to fire this itself? var engine = _diagram.layoutEngine(); engine.data( { width: _diagram.width(), height: _diagram.height() }, wnodes.map(function(v) { var lv = Object.assign({}, v.cola, v.dcg_shape); if(engine.annotateNode) engine.annotateNode(lv, v); else if(engine.extractNodeAttrs) Object.keys(engine.extractNodeAttrs()).forEach(function(key) { lv[key] = engine.extractNodeAttrs()[key](v.orig); }); return lv; }), layout_edges.map(function(e) { var le = e.cola; if(engine.annotateEdge) engine.annotateEdge(le, e); else if(engine.extractEdgeAttrs) Object.keys(engine.extractEdgeAttrs()).forEach(function(key) { le[key] = engine.extractEdgeAttrs()[key](e.orig); }); return le; }), wclusters.map(function(c) { return c.cola; }), constraints ); engine.start(); } return this; }; function check_zoom(drawState) { var do_zoom, animate = true; if(_diagram.width_is_automatic() || _diagram.height_is_automatic()) detect_size_change(); switch(_diagram.autoZoom()) { case 'always-skipanimonce': animate = false; _diagram.autoZoom('always'); case 'always': do_zoom = true; break; case 'once-noanim': animate = false; case 'once': do_zoom = true; _diagram.autoZoom(null); break; default: do_zoom = false; } calc_bounds(drawState); if(do_zoom) auto_zoom(animate); } function norm(v) { var len = Math.hypot(v[0], v[1]); return [v[0]/len, v[1]/len]; } function edge_vec(n, e) { var dy = e.target.cola.y - e.source.cola.y, dx = e.target.cola.x - e.source.cola.x; if(dy === 0 && dx === 0) return [1, 0]; if(e.source !== n) dy = -dy, dx = -dx; if(e.parallel && e.parallel.edges.length > 1 && e.source.index > e.target.index) dy = -dy, dx = -dx; return norm([dx, dy]); } function init_node_ports(nodes, wports) { _nodePorts = {}; // assemble port-lists for nodes, again because we don't have a metagraph. wports.forEach(function(p) { var nid = _diagram.nodeKey.eval(p.node); var np = _nodePorts[nid] = _nodePorts[nid] || []; np.push(p); }); for(var nid in _nodePorts) { var n = nodes[nid], nports = _nodePorts[nid]; // initial positions: use average of edge vectors, if any, or existing position nports.forEach(function(p) { if(_diagram.portElastic.eval(p) && p.edges.length) { var vecs = p.edges.map(edge_vec.bind(null, n)); p.vec = [ d3.sum(vecs, function(v) { return v[0]; })/vecs.length, d3.sum(vecs, function(v) { return v[1]; })/vecs.length ]; } else p.vec = p.vec || undefined; p.pos = null; }); } } function propagate_port_positions(nodes, wedges, ports) { // make sure we have projected vectors to positions for(var nid in _nodePorts) { var n = nodes[nid]; _nodePorts[nid].forEach(function(p) { if(!p.pos) project_port(_diagram, n, p); }); } // propagate port positions to edge endpoints wedges.forEach(function(e) { var name = _diagram.edgeSourcePortName.eval(e); e.sourcePort.pos = name ? ports[port_name(_diagram.nodeKey.eval(e.source), null, name)].pos : ports[port_name(null, _diagram.edgeKey.eval(e), 'source')].pos; name = _diagram.edgeTargetPortName.eval(e); e.targetPort.pos = name ? ports[port_name(_diagram.nodeKey.eval(e.target), null, name)].pos : ports[port_name(null, _diagram.edgeKey.eval(e), 'target')].pos; console.assert(e.sourcePort.pos && e.targetPort.pos); }); } _diagram.requestRefresh = function(durationOverride) { window.requestAnimationFrame(function() { var transdur; if(durationOverride !== undefined) { transdur = _diagram.transitionDuration(); _diagram.transitionDuration(durationOverride); } _diagram.renderer().refresh(); if(durationOverride !== undefined) _diagram.transitionDuration(transdur); }); }; _diagram.layoutDone = function(happens) { _dispatch.end(happens); _running = false; if(_needsRedraw) { _needsRedraw = false; window.setTimeout(function() { if(!_diagram.isRunning()) // someone else may already have started _diagram.redraw(); }, 0); } }; function enforce_path_direction(path, spos, tpos) { var points = path.points, first = points[0], last = points[points.length-1]; switch(_diagram.enforceEdgeDirection()) { case 'LR': if(spos.x >= tpos.x) { var dx = first.x - last.x; return { points: [ first, {x: first.x + dx, y: first.y - dx/2}, {x: last.x - dx, y: last.y - dx/2}, last ], bezDegree: 3, sourcePort: path.sourcePort, targetPort: path.targetPort }; } break; case 'TB': if(spos.y >= tpos.y) { var dy = first.y - last.y; return { points: [ first, {x: first.x + dy/2, y: first.y + dy}, {x: last.x + dy/2, y: last.y - dy}, last ], bezDegree: 3, sourcePort: path.sourcePort, targetPort: path.targetPort }; } break; } return path; } _diagram.calcEdgePath = function(e, age, sx, sy, tx, ty) { var parallel = e.parallel; var source = e.source, target = e.target; if(parallel.edges.length > 1 && e.source.index > e.target.index) { var t; t = target; target = source; source = t; t = tx; tx = sx; sx = t; t = ty; ty = sy; sy = t; } var source_padding = source.dcg_ry + _diagram.nodeStrokeWidth.eval(source) / 2, target_padding = target.dcg_ry + _diagram.nodeStrokeWidth.eval(target) / 2; for(var p = 0; p < parallel.edges.length; ++p) { // alternate parallel edges over, then under var dir = (!!(p%2) === (sx < tx)) ? -1 : 1, port = Math.floor((p+1)/2), last = port > 0 ? parallel.edges[p > 2 ? p - 2 : 0].pos[age].path : null; var path = draw_edge_to_shapes(_diagram, e, sx, sy, tx, ty, last, dir, _diagram.parallelEdgeOffset(), source_padding, target_padding ); if(parallel.edges.length > 1 && parallel.rev[p]) path.points.reverse(); if(_diagram.enforceEdgeDirection()) path = enforce_path_direction(path, source.cola, target.cola); var path0 = { points: path.points, bezDegree: path.bezDegree }; var alengths = scaled_arrow_lengths(_diagram, parallel.edges[p]); path = clip_path_to_arrows(alengths.headLength, alengths.tailLength, path); var points = path.points, points0 = path0.points; parallel.edges[p].pos[age] = { path: path, full: path0, orienthead: angle_between_points(points[points.length-1], points0[points0.length-1]) + 'rad', orienttail: angle_between_points(points[0], points0[0]) + 'rad' }; } }; function node_bounds(n) { var bounds = {left: n.cola.x - n.dcg_rx, top: n.cola.y - n.dcg_ry, right: n.cola.x + n.dcg_rx, bottom: n.cola.y + n.dcg_ry}; if(_diagram.portStyle.enum().length) { var ports = _nodePorts[_diagram.nodeKey.eval(n)]; if(ports) ports.forEach(function(p) { var portStyle =_diagram.portStyleName.eval(p); if(!portStyle || !_diagram.portStyle(portStyle)) return; var pb = _diagram.portStyle(portStyle).portBounds(p); pb.left += n.cola.x; pb.top += n.cola.y; pb.right += n.cola.x; pb.bottom += n.cola.y; bounds = union_bounds(bounds, pb); }); } return bounds; } function union_bounds(b1, b2) { return { left: Math.min(b1.left, b2.left), top: Math.min(b1.top, b2.top), right: Math.max(b1.right, b2.right), bottom: Math.max(b1.bottom, b2.bottom) }; } function point_to_bounds(p) { return { left: p.x, top: p.y, right: p.x, bottom: p.y }; } function edge_bounds(e) { // assumption: edge must have some points var points = e.pos.new.path.points; return points.map(point_to_bounds).reduce(union_bounds); } _diagram.calculateBounds = function(ndata, edata) { // assumption: there can be no edges without nodes var bounds = ndata.map(node_bounds).reduce(union_bounds); return edata.map(edge_bounds).reduce(union_bounds, bounds); }; var _bounds; function calc_bounds(drawState) { if((_diagram.fitStrategy() || _diagram.restrictPan())) { _bounds = _diagram.renderer().calculateBounds(drawState); } } _diagram.animateZoom = function(_) { if(!arguments.length) return _animateZoom; _animateZoom = _; return _diagram; }; function auto_zoom(animate) { if(_diagram.fitStrategy()) { if(!_bounds) return; var vwidth = _bounds.right - _bounds.left, vheight = _bounds.bottom - _bounds.top, swidth = _diagram.width() - _diagram.margins().left - _diagram.margins().right, sheight = _diagram.height() - _diagram.margins().top - _diagram.margins().bottom; var fitS = _diagram.fitStrategy(), translate = [0,0], scale = 1; if(['default', 'vertical', 'horizontal'].indexOf(fitS) >= 0) { var sAR = sheight / swidth, vAR = vheight / vwidth, vrl = vAR<sAR, // view aspect ratio is less (wider) amv = (fitS === 'default') ? !vrl : (fitS === 'vertical'); // align margins vertically scale = amv ? sheight / vheight : swidth / vwidth; scale = Math.max(_diagram.zoomExtent()[0], Math.min(_diagram.zoomExtent()[1], scale)); translate = [_diagram.margins().left - _bounds.left*scale + (swidth - vwidth*scale) / 2, _diagram.margins().top - _bounds.top*scale + (sheight - vheight*scale) / 2]; } else if(typeof fitS === 'string' && fitS.match(/^align_/)) { var sides = fitS.split('_')[1].toLowerCase().split(''); if(sides.length > 2) throw new Error("align_ expecting 0-2 sides, not " + sides.length); var bounds = margined_bounds(); translate = _diagram.renderer().translate(); scale = _diagram.renderer().scale(); var vertalign = false, horzalign = false; sides.forEach(function(s) { switch(s) { case 'l': translate[0] = align_left(translate, bounds.left); horzalign = true; break; case 't': translate[1] = align_top(translate, bounds.top); vertalign = true; break; case 'r': translate[0] = align_right(translate, bounds.right); horzalign = true; break; case 'b': translate[1] = align_bottom(translate, bounds.bottom); vertalign = true; break; case 'c': // handled below break; default: throw new Error("align_ expecting l t r b or c, not '" + s + "'"); } }); if(sides.includes('c')) { if(!horzalign) translate[0] = center_horizontally(translate, bounds); if(!vertalign) translate[1] = center_vertically(translate, bounds); } } else if(fitS === 'zoom') { scale = _diagram.renderer().scale(); translate = bring_in_bounds(_diagram.renderer().translate()); } else throw new Error('unknown fitStrategy type ' + typeof fitS); _animateZoom = animate; _diagram.renderer().translate(translate).scale(scale).commitTranslateScale(); _animateZoom = false; } } function namespace_event_reducer(msg_fun) { return function(p, ev) { var namespace = {}; p[ev] = function(ns) { return namespace[ns] = namespace[ns] || onetime_trace('trace', msg_fun(ns, ev)); }; return p; }; } var renderer_specific_events = ['drawn', 'transitionsStarted', 'zoomed'] .reduce(namespace_event_reducer(function(ns, ev) { return 'subscribing "' + ns + '" to event "' + ev + '" which takes renderer-specific parameters'; }), {}); var inconsistent_arguments = ['end'] .reduce(namespace_event_reducer(function(ns, ev) { return 'subscribing "' + ns + '" to event "' + ev + '" which may receive inconsistent arguments'; }), {}); /** * Standard dc.js * {@link https://github.com/dc-js/dc.js/blob/develop/web/docs/api-latest.md#dc.baseMixin baseMixin} * method. Attaches an event handler to the diagram. The currently supported events are * * `start()` - layout is starting * * `drawn(nodes, edges)` - the node and edge elements have been rendered to the screen * and can be modified through the passed d3 selections. * * `end()` - diagram layout has completed. * @method on * @memberof dc_graph.diagram * @instance * @param {String} [event] - the event to subscribe to * @param {Function} [f] - the event handler * @return {dc_graph.diagram} **/ _diagram.on = function(event, f) { if(arguments.length === 1) return _dispatch.on(event); var evns = event.split('.'), warning = renderer_specific_events[evns[0]] || inconsistent_arguments[evns[0]]; if(warning) warning(evns[1] || '')(); _dispatch.on(event, f); return this; }; /** * Returns an object with current statistics on graph layout. * * `nnodes` - number of nodes displayed * * `nedges` - number of edges displayed * @method getStats * @memberof dc_graph.diagram * @instance * @return {} * @return {dc_graph.diagram} **/ _diagram.getStats = function() { return _stats; }; /** * Standard dc.js * {@link https://github.com/dc-js/dc.js/blob/develop/web/docs/api-latest.md#dc.baseMixin baseMixin} * method. Gets or sets the x scale. * @method x * @memberof dc_graph.diagram * @instance * @param {d3.scale} [scale] * @return {d3.scale} * @return {dc_graph.diagram} **/ _diagram.x = property(null); /** * Standard dc.js * {@link https://github.com/dc-js/dc.js/blob/develop/web/docs/api-latest.md#dc.baseMixin baseMixin} * method. Gets or sets the y scale. * @method y * @memberof dc_graph.diagram * @instance * @param {d3.scale} [scale] * @return {d3.scale} * @return {dc_graph.diagram} **/ _diagram.y = property(null); /** * Standard dc.js * {@link https://github.com/dc-js/dc.js/blob/develop/web/docs/api-latest.md#dc.baseMixin baseMixin} * method. Causes all charts in the chart group to be redrawn. * @method redrawGroup * @memberof dc_graph.diagram * @instance * @return {dc_graph.diagram} **/ _diagram.redrawGroup = function () { dc.redrawAll(_chartGroup); }; /** * Standard dc.js * {@link https://github.com/dc-js/dc.js/blob/develop/web/docs/api-latest.md#dc.baseMixin baseMixin} * method. Causes all charts in the chart group to be rendered. * @method renderGroup * @memberof dc_graph.diagram * @instance * @return {dc_graph.diagram} **/ _diagram.renderGroup = function () { dc.renderAll(_chartGroup); }; /** * Creates an svg marker definition for drawing edge arrow tails or heads. * * Sorry, this is not currently documented - please see * [arrows.js](https://github.com/dc-js/dc.graph.js/blob/develop/src/arrows.js) * for examples * @return {dc_graph.diagram} **/ _diagram.defineArrow = function(name, defn) { if(typeof defn !== 'function') throw new Error('sorry, defineArrow no longer takes specific shape parameters, and the parameters have changed too much to convert them. it takes a name and a function returning a definition - please look at arrows.js for new format'); _arrows[name] = defn; return _diagram; }; // hmm _diagram.arrows = function() { return _arrows; }; Object.keys(dc_graph.builtin_arrows).forEach(function(aname) { var defn = dc_graph.builtin_arrows[aname]; _diagram.defineArrow(aname, defn); }); function margined_bounds() { var bounds = _bounds || {left: 0, top: 0, right: 0, bottom: 0}; var scale = _diagram.renderer().scale(); return { left: bounds.left - _diagram.margins().left/scale, top: bounds.top - _diagram.margins().top/scale, right: bounds.right + _diagram.margins().right/scale, bottom: bounds.bottom + _diagram.margins().bottom/scale }; } // with thanks to comments in https://github.com/d3/d3/issues/1084 function align_left(translate, x) { return translate[0] - _diagram.x()(x) + _diagram.x().range()[0]; } function align_top(translate, y) { return translate[1] - _diagram.y()(y) + _diagram.y().range()[0]; } function align_right(translate, x) { return translate[0] - _diagram.x()(x) + _diagram.x().range()[1]; } function align_bottom(translate, y) { return translate[1] - _diagram.y()(y) + _diagram.y().range()[1];; } function center_horizontally(translate, bounds) { return (align_left(translate, bounds.left) + align_right(translate, bounds.right))/2; } function center_vertically(translate, bounds) { return (align_top(translate, bounds.top) + align_bottom(translate, bounds.bottom))/2; } function bring_in_bounds(translate) { var xDomain = _diagram.x().domain(), yDomain = _diagram.y().domain(); var bounds = margined_bounds(); var less1 = bounds.left < xDomain[0], less2 = bounds.right < xDomain[1], lessExt = (bounds.right - bounds.left) < (xDomain[1] - xDomain[0]); var align, nothing = 0; if(less1 && less2) if(lessExt) align = 'left'; else align = 'right'; else if(!less1 && !less2) if(lessExt) align = 'right'; else align = 'left'; switch(align) { case 'left': translate[0] = align_left(translate, bounds.left); break; case 'right': translate[0] = align_right(translate, bounds.right); break; default: ++nothing; } less1 = bounds.top < yDomain[0]; less2 = bounds.bottom < yDomain[1]; lessExt = (bounds.bottom - bounds.top) < (yDomain[1] - yDomain[0]); if(less1 && less2) if(lessExt) align = 'top'; else align = 'bottom'; else if(!less1 && !less2) if(lessExt) align = 'bottom'; else align = 'top'; switch(align) { case 'top': translate[1] = align_top(translate, bounds.top); break; case 'bottom': translate[1] = align_bottom(translate, bounds.bottom); break; default: ++nothing; } return translate; } _diagram.doZoom = function() { if(_diagram.width_is_automatic() || _diagram.height_is_automatic()) detect_size_change(); var translate, scale = d3.event.scale; if(_diagram.restrictPan()) _diagram.renderer().translate(translate = bring_in_bounds(d3.event.translate)); else translate = d3.event.translate; _diagram.renderer().globalTransform(translate, scale, _animateZoom); _dispatch.zoomed(translate, scale, _diagram.x().domain(), _diagram.y().domain()); }; _diagram.invertCoord = function(clientCoord) { return [ _diagram.x().invert(clientCoord[0]), _diagram.y().invert(clientCoord[1]) ]; }; /** * Set the root SVGElement to either be any valid [d3 single * selector](https://github.com/mbostock/d3/wiki/Selections#selecting-elements) specifying a dom * block element such as a div; or a dom element or d3 selection. This class is called * internally on diagram initialization, but be called again to relocate the diagram. However, it * will orphan any previously created SVGElements. * @method anchor * @memberof dc_graph.diagram * @instance * @param {anchorSelector|anchorNode|d3.selection} [parent] * @param {String} [chartGroup] * @return {String|node|d3.selection} * @return {dc_graph.diagram} */ _diagram.anchor = function(parent, chartGroup) { if (!arguments.length) { return _anchor; } if (parent) { if (parent.select && parent.classed) { // detect d3 selection _anchor = parent.node(); } else { _anchor = parent; } _diagram.root(d3.select(_anchor)); _diagram.root().classed(dc_graph.constants.CHART_CLASS, true); dc.registerChart(_diagram, chartGroup); } else { throw new dc.errors.BadArgumentException('parent must be defined'); } _chartGroup = chartGroup; return _diagram; }; /** * Returns the internal numeric ID of the chart. * @method chartID * @memberof dc.baseMixin * @instance * @returns {String} */ _diagram.chartID = function () { return _diagram.__dcFlag__; }; /** * Returns the DOM id for the chart's anchored location. * @method anchorName * @memberof dc_graph.diagram * @instance * @return {String} */ _diagram.anchorName = function () { var a = _diagram.anchor(); if (a && a.id) { return a.id; } if (a && a.replace) { return a.replace('#', ''); } return 'dc-graph' + _diagram.chartID(); }; return _diagram.anchor(parent, chartGroup); };