dc.graph
Version:
Graph visualizations integrated with crossfilter and dc.js
1,283 lines (1,189 loc) • 103 kB
JavaScript
/**
* `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.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
* @exa