/**
* `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)
onetime_trace('trace', '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 = nodes.slice(0).sort(function(a, b) {
return d3.ascending(_diagram.nodeOrdering()(a), _diagram.nodeOrdering()(b));
});
}
if(_diagram.edgeOrdering()) {
edges = edges.slice(0).sort(function(a, b) {
return d3.ascending(_diagram.edgeOrdering()(a), _diagram.edgeOrdering()(b));
});
}
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 orderingFn = param(c.ordering);
sorted = sorted.sort(function(a, b) {
return d3.ascending(orderingFn(a), orderingFn(b));
});
}
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);
};