Source: diagram.js

/**
 * `dc_graph.diagram` is a dc.js-compatible network visualization component. It registers in
 * the dc.js chart registry and its nodes and edges are generated from crossfilter groups. It
 * logically derives from the dc.js
 * {@link https://github.com/dc-js/dc.js/blob/develop/web/docs/api-latest.md#dc.baseMixin baseMixin},
 * but it does not physically derive from it since so much is different about network
 * visualization versus conventional charts.
 * @class diagram
 * @memberof dc_graph
 * @param {String|node} parent - Any valid
 * {@link https://github.com/mbostock/d3/wiki/Selections#selecting-elements d3 single selector}
 * specifying a dom block element such as a div; or a dom element.
 * @param {String} [chartGroup] - The name of the dc.js chart group this diagram instance
 * should be placed in. Filter interaction with a diagram will only trigger events and redraws
 * within the diagram's group.
 * @return {dc_graph.diagram}
 **/
dc_graph.diagram = function (parent, chartGroup) {
    // different enough from regular dc charts that we don't use dc.baseMixin
    // but attempt to implement most of that interface, copying some of the most basic stuff
    var _diagram = dc.marginMixin({});
    _diagram.__dcFlag__ = dc.utils.uniqueId();
    _diagram.margins({left: 10, top: 10, right: 10, bottom: 10});
    var _dispatch = d3.dispatch('preDraw', 'data', 'end', 'start', 'render', 'drawn', 'receivedLayout', 'transitionsStarted', 'zoomed', 'reset');
    var _nodes = {}, _edges = {}; // hold state between runs
    var _ports = {}; // id = node|edge/id/name
    var _clusters = {};
    var _nodePorts; // ports sorted by node id
    var _stats = {};
    var _nodes_snapshot, _edges_snapshot;
    var _arrows = {};
    var _running = false; // for detecting concurrency issues
    var _anchor, _chartGroup;
    var _animateZoom;

    var _minWidth = 200;
    var _defaultWidthCalc = function (element) {
        var width = element && element.getBoundingClientRect && element.getBoundingClientRect().width;
        return (width && width > _minWidth) ? width : _minWidth;
    };
    var _widthCalc = _defaultWidthCalc;

    var _minHeight = 200;
    var _defaultHeightCalc = function (element) {
        var height = element && element.getBoundingClientRect && element.getBoundingClientRect().height;
        return (height && height > _minHeight) ? height : _minHeight;
    };
    var _heightCalc = _defaultHeightCalc;
    var _width, _height, _lastWidth, _lastHeight;

    function deprecate_layout_algo_parameter(name) {
        return function(value) {
            if(!_diagram.layoutEngine())
                _diagram.layoutAlgorithm('cola', true);
            var engine = _diagram.layoutEngine();
            if(engine.getEngine)
                engine = engine.getEngine();
            if(engine[name]) {
                console.warn('property is deprecated, call on layout engine instead: dc_graph.diagram.%c' + name,
                             'font-weight: bold');
                if(!arguments.length)
                    return engine[name]();
                engine[name](value);
            } else {
                console.warn('property is deprecated, and is not supported for Warning: dc_graph.diagram.<b>' + name + '</b> is deprecated, and it is not supported for the "' + engine.layoutAlgorithm() + '" layout algorithm: ignored.');
                if(!arguments.length)
                    return null;
            }
            return this;
        };
    }

    /**
     * Set or get the height attribute of the diagram. If a value is given, then the diagram is
     * returned for method chaining. If no value is given, then the current value of the height
     * attribute will be returned.
     *
     * The width and height are applied to the SVG element generated by the diagram on render, or
     * when `resizeSvg` is called.
     *
     * If the value is falsy or a function, the height will be calculated the first time it is
     * needed, using the provided function or default height calculator, and then cached. The
     * default calculator uses the client rect of the element specified when constructing the chart,
     * with a minimum of `minHeight`. A custom calculator will be passed the element.
     *
     * If the value is `'auto'`, the height will be calculated every time the diagram is drawn, and
     * it will not be set on the `<svg>` element. Instead, the element will be pinned to the same
     * rectangle as its containing div using CSS.
     *
     * @method height
     * @memberof dc_graph.diagram
     * @instance
     * @param {Number} [height=200]
     * @return {Number}
     * @return {dc_graph.diagram}
      **/
    _diagram.height = function (height) {
        if (!arguments.length) {
            if (!dc.utils.isNumber(_height)) {
                _lastHeight = _heightCalc(_diagram.root().node());
                if(_height === 'auto') // 'auto' => calculate every time
                    return _lastHeight;
                // null/undefined => calculate once only
                _height = _lastHeight;
            }
            return _height;
        }
        if(dc.utils.isNumber(height) || !height || height === 'auto')
            _height = height;
        else if(typeof height === 'function') {
            _heightCalc = height;
            _height = undefined;
        }
        else throw new Error("don't know what to do with height type " + typeof height + " value " + height);
        return _diagram;
    };
    _diagram.minHeight = function(height) {
        if(!arguments.length)
            return _minHeight;
        _minHeight = height;
        return _diagram;
    };
    /**
     * Set or get the width attribute of the diagram. If a value is given, then the diagram is
     * returned for method chaining. If no value is given, then the current value of the width
     * attribute will be returned.
     *
     * The width and height are applied to the SVG element generated by the diagram on render, or
     * when `resizeSvg` is called.
     *
     * If the value is falsy or a function, the width will be calculated the first time it is
     * needed, using the provided function or default width calculator, and then cached. The default
     * calculator uses the client rect of the element specified when constructing the chart, with a
     * minimum of `minWidth`. A custom calculator will be passed the element.
     *
     * If the value is `'auto'`, the width will be calculated every time the diagram is drawn, and
     * it will not be set on the `<svg>` element. Instead, the element will be pinned to the same
     * rectangle as its containing div using CSS.
     *
     * @method width
     * @memberof dc_graph.diagram
     * @instance
     * @param {Number} [width=200]
     * @return {Number}
     * @return {dc_graph.diagram}
     **/
    _diagram.width = function (width) {
        if (!arguments.length) {
            if (!dc.utils.isNumber(_width)) {
                _lastWidth = _widthCalc(_diagram.root().node());
                if(_width === 'auto') // 'auto' => calculate every time
                    return _lastWidth;
                // null/undefined => calculate once only
                _width = _lastWidth;
            }
            return _width;
        }
        if(dc.utils.isNumber(width) || !width || width === 'auto')
            _width = width;
        else if(typeof width === 'function') {
            _widthCalc = width;
            _width = undefined;
        }
        else throw new Error("don't know what to do with width type " + typeof width + " value " + width);
        return _diagram;
    };
    _diagram.minWidth = function(width) {
        if(!arguments.length)
            return _minWidth;
        _minWidth = width;
        return _diagram;
    };

    /**
     * Get or set the root element, which is usually the parent div. Normally the root is set
     * when the diagram is constructed; setting it later may have unexpected consequences.
     * @method root
     * @memberof dc_graph.diagram
     * @instance
     * @param {node} [root=null]
     * @return {node}
     * @return {dc_graph.diagram}
     **/
    _diagram.root = property(null).react(function(e) {
        if(e.empty())
            console.log('Warning: parent selector ' + parent + " doesn't seem to exist");
    });

    /**
     * Get or set whether mouse wheel rotation or touchpad gestures will zoom the diagram, and
     * whether dragging on the background pans the diagram.
     * @method mouseZoomable
     * @memberof dc_graph.diagram
     * @instance
     * @param {Boolean} [mouseZoomable=true]
     * @return {Boolean}
     * @return {dc_graph.diagram}
     **/
    _diagram.mouseZoomable = property(true);

    _diagram.zoomExtent = property([.1, 2]);

    /**
     * Whether zooming should only be enabled when the alt key is pressed.
     * @method altKeyZoom
     * @memberof dc_graph.diagram
     * @instance
     * @param {Boolean} [altKeyZoom=true]
     * @return {Boolean}
     * @return {dc_graph.diagram}
     **/
    _diagram.modKeyZoom = _diagram.altKeyZoom = property(false);

    /**
     * Set or get the fitting strategy for the canvas, which affects how the translate
     * and scale get calculated when `autoZoom` is triggered.
     *
     * * `'default'` - simulates the preserveAspectRatio behavior of `xMidYMid meet`, but
     *   with margins - the content is stretched or squished in the more constrained
     *   direction, and centered in the other direction
     * * `'vertical'` - fits the canvas vertically (with vertical margins) and centers
     *   it horizontally. If the canvas is taller than the viewport, it will meet
     *   vertically and there will be blank areas to the left and right. If the canvas
     *   is wider than the viewport, it will be sliced.
     * * `'horizontal'` - fits the canvas horizontally (with horizontal margins) and
     *   centers it vertically. If the canvas is wider than the viewport, it will meet
     *   horizontally and there will be blank areas above and below. If the canvas is
     *   taller than the viewport, it will be sliced.
     *
     * Other options
     * * `null` - no attempt is made to fit the content in the viewport
     * * `'zoom'` - does not scale the content, but attempts to bring as much content
     *   into view as possible, using using the same algorithm as `restrictPan`
     * * `'align_{tlbrc}[2]'` - does not scale; aligns up to two sides or centers them
     * @method fitStrategy
     * @memberof dc_graph.diagram
     * @instance
     * @param {String} [fitStrategy='default']
     * @return {String}
     * @return {dc_graph.diagram}
     **/
    _diagram.fitStrategy = property('default');

    /**
     * Do not allow panning (scrolling) to push the diagram out of the viewable area, if there
     * is space for it to be shown. */
    _diagram.restrictPan = property(false);

    /**
     * Auto-zoom behavior.
     * * `'always'` - zoom every time layout happens
     * * `'once'` - zoom the next time layout happens
     * * `null` - manual, call `zoomToFit` to fit
     * @method autoZoom
     * @memberof dc_graph.diagram
     * @instance
     * @param {String} [autoZoom=null]
     * @return {String}
     * @return {dc_graph.diagram}
     **/
    _diagram.autoZoom = property(null);
    _diagram.zoomToFit = function(animate) {
        // if(!(_nodeLayer && _edgeLayer))
        //     return;
        auto_zoom(animate);
    };
    _diagram.zoomDuration = property(500);

    /**
     * Set or get the crossfilter dimension which represents the nodes (vertices) in the
     * diagram. Typically there will be a crossfilter instance for the nodes, and another for
     * the edges.
     *
     * *Dimensions are included on the diagram for similarity to dc.js, however the diagram
     * itself does not use them - but {@link dc_graph.filter_selection filter_selection} will.*
     * @method nodeDimension
     * @memberof dc_graph.diagram
     * @instance
     * @param {crossfilter.dimension} [nodeDimension]
     * @return {crossfilter.dimension}
     * @return {dc_graph.diagram}
     **/
    _diagram.nodeDimension = property();

    /**
     * Set or get the crossfilter group which is the data source for the nodes in the
     * diagram. The diagram will use the group's `.all()` method to get an array of `{key,
     * value}` pairs, where the key is a unique identifier, and the value is usually an object
     * containing the node's attributes. All accessors work with these key/value pairs.
     *
     * If the group is changed or returns different values, the next call to `.redraw()` will
     * reflect the changes incrementally.
     *
     * It is possible to pass another object with the same `.all()` interface instead of a
     * crossfilter group.
     * @method nodeGroup
     * @memberof dc_graph.diagram
     * @instance
     * @param {crossfilter.group} [nodeGroup]
     * @return {crossfilter.group}
     * @return {dc_graph.diagram}
     **/
    _diagram.nodeGroup = property();

    /**
     * Set or get the crossfilter dimension which represents the edges in the
     * diagram. Typically there will be a crossfilter instance for the nodes, and another for
     * the edges.
     *
     * *Dimensions are included on the diagram for similarity to dc.js, however the diagram
     * itself does not use them - but {@link dc_graph.filter_selection filter_selection} will.*
     * @method edgeDimension
     * @memberof dc_graph.diagram
     * @instance
     * @param {crossfilter.dimension} [edgeDimension]
     * @return {crossfilter.dimension}
     * @return {dc_graph.diagram}
     **/
    _diagram.edgeDimension = property();

    /**
     * Set or get the crossfilter group which is the data source for the edges in the
     * diagram. See `.nodeGroup` above for the way data is loaded from a crossfilter group.
     *
     * The values in the key/value pairs returned by `diagram.edgeGroup().all()` need to
     * support, at a minimum, the {@link dc_graph.diagram#nodeSource nodeSource} and
     * {@link dc_graph.diagram#nodeTarget nodeTarget}, which should return the same
     * keys as the {@link dc_graph.diagram#nodeKey nodeKey}
     *
     * @method edgeGroup
     * @memberof dc_graph.diagram
     * @instance
     * @param {crossfilter.group} [edgeGroup]
     * @return {crossfilter.group}
     * @return {dc_graph.diagram}
     **/
    _diagram.edgeGroup = property();

    _diagram.edgesInFront = property(false);

    /**
     * Set or get the function which will be used to retrieve the unique key for each node. By
     * default, this accesses the `key` field of the object passed to it. The keys should match
     * the keys returned by the {@link dc_graph.diagram#edgeSource edgeSource} and
     * {@link dc_graph.diagram#edgeTarget edgeTarget}.
     *
     * @method nodeKey
     * @memberof dc_graph.diagram
     * @instance
     * @param {Function} [nodeKey=function(kv) { return kv.key }]
     * @return {Function}
     * @return {dc_graph.diagram}
     **/
    _diagram.nodeKey = _diagram.nodeKeyAccessor = property(function(kv) {
        return kv.key;
    });

    /**
     * Set or get the function which will be used to retrieve the unique key for each edge. By
     * default, this accesses the `key` field of the object passed to it.
     *
     * @method edgeKey
     * @memberof dc_graph.diagram
     * @instance
     * @param {Function} [edgeKey=function(kv) { return kv.key }]
     * @return {Function}
     * @return {dc_graph.diagram}
     **/
    _diagram.edgeKey = _diagram.edgeKeyAccessor = property(function(kv) {
        return kv.key;
    });

    /**
     * Set or get the function which will be used to retrieve the source (origin/tail) key of
     * the edge objects.  The key must equal the key returned by the `.nodeKey` for one of the
     * nodes; if it does not, or if the node is currently filtered out, the edge will not be
     * displayed. By default, looks for `.value.sourcename`.
     *
     * @method edgeSource
     * @memberof dc_graph.diagram
     * @instance
     * @param {Function} [edgeSource=function(kv) { return kv.value.sourcename; }]
     * @return {Function}
     * @return {dc_graph.diagram}
     **/
    _diagram.edgeSource = _diagram.sourceAccessor = property(function(kv) {
        return kv.value.sourcename;
    });

    /**
     * Set or get the function which will be used to retrieve the target (destination/head) key
     * of the edge objects.  The key must equal the key returned by the
     * {@link dc_graph.diagram#nodeKey nodeKey} for one of the nodes; if it does not, or if the node
     * is currently filtered out, the edge will not be displayed. By default, looks for
     * `.value.targetname`.
     * @method edgeTarget
     * @memberof dc_graph.diagram
     * @instance
     * @param {Function} [edgeTarget=function(kv) { return kv.value.targetname; }]
     * @return {Function}
     * @return {dc_graph.diagram}
     **/
    _diagram.edgeTarget = _diagram.targetAccessor = property(function(kv) {
        return kv.value.targetname;
    });

    _diagram.portDimension = property(null);
    _diagram.portGroup = property(null);
    _diagram.portNodeKey = property(null);
    _diagram.portEdgeKey = property(null);
    _diagram.portName = property(null);
    _diagram.portStyleName = property(null);
    _diagram.portElastic = property(true);

    _diagram.portStyle = named_children();

    _diagram.portBounds = property(null); // position limits, in radians

    _diagram.edgeSourcePortName = property(null);
    _diagram.edgeTargetPortName = property(null);

    /**
     * Set or get the crossfilter dimension which represents the edges in the
     * diagram. Typically there will be a crossfilter instance for the nodes, and another for
     * the edges.
     *
     * *As with node and edge dimensions, the diagram will itself not filter on cluster dimensions;
     * this is included for symmetry, and for modes which may want to filter clusters.*
     * @method clusterDimension
     * @memberof dc_graph.diagram
     * @instance
     * @param {crossfilter.dimension} [clusterDimension]
     * @return {crossfilter.dimension}
     * @return {dc_graph.diagram}
     **/
    _diagram.clusterDimension = property(null);

    /**
     * Set or get the crossfilter group which is the data source for clusters in the
     * diagram.
     *
     * The key/value pairs returned by `diagram.clusterGroup().all()` need to support, at a minimum,
     * the {@link dc_graph.diagram#clusterKey clusterKey} and {@link dc_graph.diagram#clusterParent clusterParent}
     * accessors, which should return keys in this group.
     *
     * @method clusterGroup
     * @memberof dc_graph.diagram
     * @instance
     * @param {crossfilter.group} [clusterGroup]
     * @return {crossfilter.group}
     * @return {dc_graph.diagram}
     **/
    _diagram.clusterGroup = property(null);

    // cluster accessors
    /**
     * Set or get the function which will be used to retrieve the unique key for each cluster. By
     * default, this accesses the `key` field of the object passed to it.
     *
     * @method clusterKey
     * @memberof dc_graph.diagram
     * @instance
     * @param {Function} [clusterKey=function(kv) { return kv.key }]
     * @return {Function}
     * @return {dc_graph.diagram}
     **/
    _diagram.clusterKey = property(dc.pluck('key'));

    /**
     * Set or get the function which will be used to retrieve the key of the parent of a cluster,
     * which is another cluster.
     *
     * @method clusterParent
     * @memberof dc_graph.diagram
     * @instance
     * @param {Function} [clusterParent=function(kv) { return kv.key }]
     * @return {Function}
     * @return {dc_graph.diagram}
     **/
    _diagram.clusterParent = property(null);

    /**
     * Set or get the function which will be used to retrieve the padding, in pixels, around a cluster.
     *
     * **To be implemented.** If a single value is returned, it will be used on all sides; if two
     * values are returned they will be interpreted as the vertical and horizontal padding.
     *
     * @method clusterPadding
     * @memberof dc_graph.diagram
     * @instance
     * @param {Function} [clusterPadding=function(kv) { return kv.key }]
     * @return {Function}
     * @return {dc_graph.diagram}
     **/
    _diagram.clusterPadding = property(8);

    // node accessor
    /**
     * Set or get the function which will be used to retrieve the parent cluster of a node, or
     * `null` if the node is not in a cluster.
     *
     * @method nodeParentCluster
     * @memberof dc_graph.diagram
     * @instance
     * @param {Function} [nodeParentCluster=function(kv) { return kv.key }]
     * @return {Function}
     * @return {dc_graph.diagram}
     **/
    _diagram.nodeParentCluster = property(null);

    /**
     * Set or get the function which will be used to retrieve the radius, in pixels, for each
     * node. This determines the height of nodes,and if `nodeFitLabel` is false, the width too.
     * @method nodeRadius
     * @memberof dc_graph.diagram
     * @instance
     * @param {Function|Number} [nodeRadius=25]
     * @return {Function|Number}
     * @return {dc_graph.diagram}
     **/
    _diagram.nodeRadius = _diagram.nodeRadiusAccessor = property(25);

    /**
     * Set or get the function which will be used to retrieve the stroke width, in pixels, for
     * drawing the outline of each node. According to the SVG specification, the outline will
     * be drawn half on top of the fill, and half outside. Default: 1
     * @method nodeStrokeWidth
     * @memberof dc_graph.diagram
     * @instance
     * @param {Function|Number} [nodeStrokeWidth=1]
     * @return {Function|Number}
     * @return {dc_graph.diagram}
     **/
    _diagram.nodeStrokeWidth = _diagram.nodeStrokeWidthAccessor = property(1);

    /**
     * Set or get the function which will be used to retrieve the stroke color for the outline
     * of each node.
     * @method nodeStroke
     * @memberof dc_graph.diagram
     * @instance
     * @param {Function|String} [nodeStroke='black']
     * @return {Function|String}
     * @return {dc_graph.diagram}
     **/
    _diagram.nodeStroke = _diagram.nodeStrokeAccessor = property('black');

    _diagram.nodeStrokeDashArray = property(null);

    /**
     * If set, the value returned from `nodeFill` will be processed through this
     * {@link https://github.com/mbostock/d3/wiki/Scales d3.scale}
     * to return the fill color. If falsy, uses the identity function (no scale).
     * @method nodeFillScale
     * @memberof dc_graph.diagram
     * @instance
     * @param {Function|d3.scale} [nodeFillScale]
     * @return {Function|d3.scale}
     * @return {dc_graph.diagram}
     **/
    _diagram.nodeFillScale = property(null);

    /**
     * Set or get the function which will be used to retrieve the fill color for the body of each
     * node.
     * @method nodeFill
     * @memberof dc_graph.diagram
     * @instance
     * @param {Function|String} [nodeFill='white']
     * @return {Function|String}
     * @return {dc_graph.diagram}
     **/
    _diagram.nodeFill = _diagram.nodeFillAccessor = property('white');

    /**
     * Set or get the function which will be used to retrieve the opacity of each node.
     * @method nodeOpacity
     * @memberof dc_graph.diagram
     * @instance
     * @param {Function|Number} [nodeOpacity=1]
     * @return {Function|Number}
     * @return {dc_graph.diagram}
     **/
    _diagram.nodeOpacity = property(1);

    /**
     * Set or get the padding or minimum distance, in pixels, for a node. (Will be distributed
     * to both sides of the node.)
     * @method nodePadding
     * @memberof dc_graph.diagram
     * @instance
     * @param {Function|Number} [nodePadding=6]
     * @return {Function|Number}
     * @return {dc_graph.diagram}
     **/
    _diagram.nodePadding = property(6);


    /**
     * Set or get the padding, in pixels, for a node's label. If an object, should contain fields
     * `x` and `y`. If a number, will be applied to both x and y.
     * @method nodeLabelPadding
     * @memberof dc_graph.diagram
     * @instance
     * @param {Function|Number|Object} [nodeLabelPadding=0]
     * @return {Function|Number}
     * @return {dc_graph.diagram}
     **/
    _diagram.nodeLabelPadding = property(0);

    /**
     * Set or get the line height for nodes with multiple lines of text, in ems.
     * @method nodeLineHeight
     * @memberof dc_graph.diagram
     * @instance
     * @param {Function|Number} [nodeLineHeight=1]
     * @return {Function|Number}
     * @return {dc_graph.diagram}
     **/
    _diagram.nodeLineHeight = property(1);

    /**
     * Set or get the function which will be used to retrieve the label text to display in each
     * node. By default, looks for a field `label` or `name` inside the `value` field.
     * @method nodeLabel
     * @memberof dc_graph.diagram
     * @instance
     * @param {Function|String} [nodeLabel]
     * @return {Function|String}
     * @example
     * // Default behavior
     * diagram.nodeLabel(function(kv) {
     *   return kv.value.label || kv.value.name;
     * });
     * @return {dc_graph.diagram}
     **/
    _diagram.nodeLabel = _diagram.nodeLabelAccessor = property(function(kv) {
        return kv.value.label || kv.value.name;
    });

    _diagram.nodeLabelAlignment = property('center');
    _diagram.nodeLabelDecoration = property(null);

    /**
     * Set or get the function which will be used to retrieve the label fill color. Default: null
     * @method nodeLabelFill
     * @memberof dc_graph.diagram
     * @instance
     * @param {Function|String} [nodeLabelFill=null]
     * @return {Function|String}
     * @return {dc_graph.diagram}
     **/
    _diagram.nodeLabelFill = _diagram.nodeLabelFillAccessor = property(null);

    /**
     * Whether to fit the node shape around the label
     * @method nodeFitLabel
     * @memberof dc_graph.diagram
     * @instance
     * @param {Function|Boolean} [nodeFitLabel=true]
     * @return {Function|Boolean}
     * @return {dc_graph.diagram}
     **/
    _diagram.nodeFitLabel = _diagram.nodeFitLabelAccessor = property(true);

    /**
     * The shape to use for drawing each node, specified as an object with at least the field
     * `shape`. The names of shapes are mostly taken
     * [from graphviz](http://www.graphviz.org/doc/info/shapes.html); currently ellipse, egg,
     * triangle, rectangle, diamond, trapezium, parallelogram, pentagon, hexagon, septagon, octagon,
     * invtriangle, invtrapezium, square, polygon are supported.
     *
     * If `shape = polygon`:
     * * `sides`: number of sides for a polygon
     * @method nodeShape
     * @memberof dc_graph.diagram
     * @instance
     * @param {Function|Object} [nodeShape={shape: 'ellipse'}]
     * @return {Function|Object}
     * @return {dc_graph.diagram}
     * @example
     * // set shape to diamond or parallelogram based on flag
     * diagram.nodeShape(function(kv) {
     *   return {shape: kv.value.flag ? 'diamond' : 'parallelogram'};
     * });
     **/
    _diagram.nodeShape = property(default_shape);

    // for defining custom (and standard) shapes
    _diagram.shape = named_children();

    _diagram.shape('nothing', dc_graph.no_shape());
    _diagram.shape('ellipse', dc_graph.ellipse_shape());
    _diagram.shape('polygon', dc_graph.polygon_shape());
    _diagram.shape('rounded-rect', dc_graph.rounded_rectangle_shape());
    _diagram.shape('elaborated-rect', dc_graph.elaborated_rectangle_shape());

    _diagram.nodeOutlineClip = property(null);

    _diagram.nodeContent = property('text');
    _diagram.content = named_children();
    _diagram.content('text', dc_graph.text_contents());

    // really looks like these should reside in an open namespace - this used only by an extension
    // but it's no less real than any other computed property
    _diagram.nodeIcon = property(null);

    /**
     * Set or get the function which will be used to retrieve the node title, usually rendered
     * as a tooltip. By default, uses the key of the node.
     * @method nodeTitle
     * @memberof dc_graph.diagram
     * @instance
     * @param {Function|String} [nodeTitle]
     * @return {Function|String}
     * @example
     * // Default behavior
     * diagram.nodeTitle(function(kv) {
     *   return _diagram.nodeKey()(kv);
     * });
     * @return {dc_graph.diagram}
     **/
    _diagram.nodeTitle = _diagram.nodeTitleAccessor = property(function(kv) {
        return _diagram.nodeKey()(kv);
    });

    /**
     * By default, nodes are added to the layout in the order that `.nodeGroup().all()` returns
     * them. If specified, `.nodeOrdering` provides an accessor that returns a key to sort the
     * nodes on.  *It would be better not to rely on ordering to affect layout, but it may
     * affect the layout in some cases.*
     * @method nodeOrdering
     * @memberof dc_graph.diagram
     * @instance
     * @param {Function} [nodeOrdering=null]
     * @return {Function}
     * @return {dc_graph.diagram}
     **/
    _diagram.nodeOrdering = property(null);

    /**
     * Specify an accessor that returns an {x,y} coordinate for a node that should be
     * {@link https://github.com/tgdwyer/WebCola/wiki/Fixed-Node-Positions fixed in place},
     * and returns falsy for other nodes.
     * @method nodeFixed
     * @memberof dc_graph.diagram
     * @instance
     * @param {Function|Object} [nodeFixed=null]
     * @return {Function|Object}
     * @return {dc_graph.diagram}
     **/
    _diagram.nodeFixed = _diagram.nodeFixedAccessor = property(null);


    /**
     * Set or get the function which will be used to retrieve the stroke color for the edges.
     * @method edgeStroke
     * @memberof dc_graph.diagram
     * @instance
     * @param {Function|String} [edgeStroke='black']
     * @return {Function|String}
     * @return {dc_graph.diagram}
     **/
    _diagram.edgeStroke = _diagram.edgeStrokeAccessor = property('black');

    /**
     * Set or get the function which will be used to retrieve the stroke width for the edges.
     * @method edgeStrokeWidth
     * @memberof dc_graph.diagram
     * @instance
     * @param {Function|Number} [edgeStrokeWidth=1]
     * @return {Function|Number}
     * @return {dc_graph.diagram}
     **/
    _diagram.edgeStrokeWidth = _diagram.edgeStrokeWidthAccessor = property(1);

    _diagram.edgeStrokeDashArray = property(null);

    /**
     * Set or get the function which will be used to retrieve the edge opacity, a number from 0
     * to 1.
     * @method edgeOpacity
     * @memberof dc_graph.diagram
     * @instance
     * @param {Function|Number} [edgeOpacity=1]
     * @return {Function|Number}
     * @return {dc_graph.diagram}
     **/
    _diagram.edgeOpacity = _diagram.edgeOpacityAccessor = property(1);

    /**
     * Set or get the function which will be used to retrieve the edge label text. The label is
     * displayed when an edge is hovered over. By default, uses the `edgeKey`.
     * @method edgeLabel
     * @memberof dc_graph.diagram
     * @instance
     * @param {Function|String} [edgeLabel]
     * @example
     * // Default behavior
     * diagram.edgeLabel(function(e) {
     *   return _diagram.edgeKey()(e);
     * });
     * @return {Function|String}
     * @return {dc_graph.diagram}
     **/
    _diagram.edgeLabel = _diagram.edgeLabelAccessor = property(function(e) {
        return _diagram.edgeKey()(e);
    });
    // vertical spacing when there are multiple lines of edge label
    _diagram.edgeLabelSpacing = property(12);

    /**
     * Set or get the function which will be used to retrieve the name of the arrowhead to use
     * for the target/ head/destination of the edge. Arrow symbols can be specified with
     * `.defineArrow()`. Return null to display no arrowhead.
     * @method edgeArrowhead
     * @memberof dc_graph.diagram
     * @instance
     * @param {Function|String} [edgeArrowhead='vee']
     * @return {Function|String}
     * @return {dc_graph.diagram}
     **/
    _diagram.edgeArrowhead = _diagram.edgeArrowheadAccessor = property('vee');

    /**
     * Set or get the function which will be used to retrieve the name of the arrow tail to use
     * for the tail/source of the edge. Arrow symbols can be specified with
     * `.defineArrow()`. Return null to display no arrowtail.
     * @method edgeArrowtail
     * @memberof dc_graph.diagram
     * @instance
     * @param {Function|String} [edgeArrowtail=null]
     * @return {Function|String}
     * @return {dc_graph.diagram}
     **/
    _diagram.edgeArrowtail = _diagram.edgeArrowtailAccessor = property(null);

    /**
     * Multiplier for arrow size.
     * @method edgeArrowSize
     * @memberof dc_graph.diagram
     * @instance
     * @param {Function|Number} [edgeArrowSize=1]
     * @return {Function|Number}
     * @return {dc_graph.diagram}
     **/
    _diagram.edgeArrowSize = property(1);

    /**
     * To draw an edge but not have it affect the layout, specify a function which returns
     * false for that edge.  By default, will return false if the `notLayout` field of the edge
     * value is truthy, true otherwise.
     * @method edgeIsLayout
     * @memberof dc_graph.diagram
     * @instance
     * @param {Function|Boolean} [edgeIsLayout]
     * @example
     * // Default behavior
     * diagram.edgeIsLayout(function(kv) {
     *   return !kv.value.notLayout;
     * });
     * @return {Function|Boolean}
     * @return {dc_graph.diagram}
     **/
    _diagram.edgeIsLayout = _diagram.edgeIsLayoutAccessor = property(function(kv) {
        return !kv.value.notLayout;
    });

    // if false, don't draw or layout the edge. this is not documented because it seems like
    // the interface could be better and this combined with edgeIsLayout. (currently there is
    // no way to layout but not draw an edge.)
    _diagram.edgeIsShown = property(true);

    /**
     * Currently, three strategies are supported for specifying the lengths of edges:
     * * 'individual' - uses the `edgeLength` for each edge. If it returns falsy, uses the
     * `baseLength`
     * * 'symmetric', 'jaccard' - compute the edge length based on the graph structure around
     * the edge. See
     * {@link https://github.com/tgdwyer/WebCola/wiki/link-lengths the cola.js wiki}
     * for more details.
     * 'none' - no edge lengths will be specified
     *
     * **Deprecated**: Use {@link dc_graph.cola_layout#lengthStrategy cola_layout.lengthStrategy} instead.
     * @method lengthStrategy
     * @memberof dc_graph.diagram
     * @instance
     * @param {Function|String} [lengthStrategy='symmetric']
     * @return {Function|String}
     * @return {dc_graph.diagram}
     **/
    _diagram.lengthStrategy = deprecate_layout_algo_parameter('lengthStrategy');

    /**
     * When the `.lengthStrategy` is 'individual', this accessor will be used to read the
     * length of each edge.  By default, reads the `distance` field of the edge. If the
     * distance is falsy, uses the `baseLength`.
     * @method edgeLength
     * @memberof dc_graph.diagram
     * @instance
     * @param {Function|Number} [edgeLength]
     * @example
     * // Default behavior
     * diagram.edgeLength(function(kv) {
     *   return kv.value.distance;
     * });
     * @return {Function|Number}
     * @return {dc_graph.diagram}
     **/
    _diagram.edgeLength = _diagram.edgeDistanceAccessor = property(function(kv) {
        return kv.value.distance;
    });

    /**
     * This should be equivalent to rankdir and ranksep in the dagre/graphviz nomenclature, but for
     * now it is separate.
     *
     * **Deprecated**: use {@link dc_graph.cola_layout#flowLayout cola_layout.flowLayout} instead.
     * @method flowLayout
     * @memberof dc_graph.diagram
     * @instance
     * @param {Object} [flowLayout]
     * @example
     * // No flow (default)
     * diagram.flowLayout(null)
     * // flow in x with min separation 200
     * diagram.flowLayout({axis: 'x', minSeparation: 200})
     **/
    _diagram.flowLayout = deprecate_layout_algo_parameter('flowLayout');

    /**
     * Direction to draw ranks. Currently for dagre and expand_collapse, but I think cola could be
     * generated from graphviz-style since it is more general.
     *
     * **Deprecated**: use {@link dc_graph.dagre_layout#rankdir dagre_layout.rankdir} instead.
     * @method rankdir
     * @memberof dc_graph.diagram
     * @instance
     * @param {String} [rankdir]
     **/
    _diagram.rankdir = deprecate_layout_algo_parameter('rankdir');

    /**
     * Gets or sets the default edge length (in pixels) when the `.lengthStrategy` is
     * 'individual', and the base value to be multiplied for 'symmetric' and 'jaccard' edge
     * lengths.
     *
     * **Deprecated**: use {@link dc_graph.cola_layout#baseLength cola_layout.baseLength} instead.
     * @method baseLength
     * @memberof dc_graph.diagram
     * @instance
     * @param {Number} [baseLength]
     * @return {Number}
     * @return {dc_graph.diagram}
     **/
    _diagram.baseLength = deprecate_layout_algo_parameter('baseLength');

    /**
     * Gets or sets the transition duration, the length of time each change to the diagram will
     * be animated.
     * @method transitionDuration
     * @memberof dc_graph.diagram
     * @instance
     * @param {Number} [transitionDuration=500]
     * @return {Number}
     * @return {dc_graph.diagram}
     **/
    _diagram.transitionDuration = property(500);

    /**
     * How transitions should be split into separate animations to emphasize
     * the delete, modify, and insert operations:
     * * `none`: modify and insert operations animate at the same time
     * * `modins`: modify operations happen before inserts
     * * `insmod`: insert operations happen before modifies
     *
     * Deletions always happen before/during layout computation.
     * @method stageTransitions
     * @memberof dc_graph.diagram
     * @instance
     * @param {String} [stageTransitions='none']
     * @return {String}
     * @return {dc_graph.diagram}
     **/
    _diagram.stageTransitions = property('none');

    /**
     * The delete transition happens simultaneously with layout, which can take longer
     * than the transition duration. Delaying it can bring it closer to the other
     * staged transitions.
     * @method deleteDelay
     * @memberof dc_graph.diagram
     * @instance
     * @param {Number} [deleteDelay=0]
     * @return {Number}
     * @return {dc_graph.diagram}
     **/
    _diagram.deleteDelay = property(0);

    /**
     * Whether to put connected components each in their own group, to stabilize layout.
     * @method groupConnected
     * @memberof dc_graph.diagram
     * @instance
     * @param {String} [groupConnected=false]
     * @return {String}
     * @return {dc_graph.diagram}
     **/
    _diagram.groupConnected = deprecate_layout_algo_parameter('groupConnected');

    /**
     * Gets or sets the maximum time spent doing layout for a render or redraw. Set to 0 for no
     * limit.
     * @method timeLimit
     * @memberof dc_graph.diagram
     * @instance
     * @param {Function|Number} [timeLimit=0]
     * @return {Function|Number}
     * @return {dc_graph.diagram}
     **/
    _diagram.timeLimit = property(0);

    /**
     * Gets or sets a function which will be called with the current nodes and edges on each
     * redraw in order to derive new layout constraints. The constraints are built from scratch
     * on each redraw.
     *
     * This can be used to generate alignment (rank) or axis constraints. By default, no
     * constraints will be added, although cola.js uses constraints internally to implement
     * flow and overlap prevention. See
     * {@link https://github.com/tgdwyer/WebCola/wiki/Constraints the cola.js wiki}
     * for more details.
     *
     * For convenience, dc.graph.js implements a other constraints on top of those implemented
     * by cola.js:
     * * 'ordering' - the nodes will be ordered on the specified `axis` according to the keys
     * returned by the `ordering` function, by creating separation constraints using the
     * specified `gap`.
     * * 'circle' - (experimental) the nodes will be placed in a circle using "wheel"
     * edge lengths similar to those described in
     * {@link http://www.csse.monash.edu.au/~tdwyer/Dwyer2009FastConstraints.pdf Scalable, Versatile, and Simple Constrained Graph Layout}
     * *Although this is not as performant or stable as might be desired, it may work for
     * simple cases. In particular, it should use edge length *constraints*, which don't yet
     * exist in cola.js.*
     *
     * Because it is tedious to write code to generate constraints for a graph, **dc.graph.js**
     * also includes a {@link #dc_graph+constraint_pattern constraint generator} to produce
     * this constrain function, specifying the constraints themselves in a graph.
     * @method constrain
     * @memberof dc_graph.diagram
     * @instance
     * @param {Function} [constrain]
     * @return {Function}
     * @return {dc_graph.diagram}
     **/
    _diagram.constrain = property(function(nodes, edges) {
        return [];
    });

    /**
     * If there are multiple edges between the same two nodes, start them this many pixels away
     * from the original so they don't overlap.
     * @method parallelEdgeOffset
     * @memberof dc_graph.diagram
     * @instance
     * @param {Number} [parallelEdgeOffset=10]
     * @return {Number}
     * @return {dc_graph.diagram}
     **/
    _diagram.parallelEdgeOffset = property(10);

    /**
     * By default, edges are added to the layout in the order that `.edgeGroup().all()` returns
     * them. If specified, `.edgeOrdering` provides an accessor that returns a key to sort the
     * edges on.
     *
     * *It would be better not to rely on ordering to affect layout, but it may affect the
     * layout in some cases. (Probably less than node ordering, but it does affect which
     * parallel edge is which.)*
     * @method edgeOrdering
     * @memberof dc_graph.diagram
     * @instance
     * @param {Function} [edgeOrdering=null]
     * @return {Function}
     * @return {dc_graph.diagram}
     **/
    _diagram.edgeOrdering = property(null);

    _diagram.edgeSort = property(null);

    _diagram.cascade = cascade(_diagram);

    /**
     * Currently there are some bugs when the same instance of cola.js is used multiple
     * times. (In particular, overlaps between nodes may not be eliminated
     * {@link https://github.com/tgdwyer/WebCola/issues/118 if cola is not reinitialized}
     * This flag can be set true to construct a new cola layout object on each redraw. However,
     * layout seems to be more stable if this is set false, so hopefully this will be fixed
     * soon.
     * @method initLayoutOnRedraw
     * @memberof dc_graph.diagram
     * @instance
     * @param {Boolean} [initLayoutOnRedraw=false]
     * @return {Boolean}
     * @return {dc_graph.diagram}
     **/
    _diagram.initLayoutOnRedraw = property(false);

    /**
     * Whether to perform layout when the data is unchanged from the last redraw.
     * @method layoutUnchanged
     * @memberof dc_graph.diagram
     * @instance
     * @param {Boolean} [layoutUnchanged=false]
     * @return {Boolean}
     * @return {dc_graph.diagram}
     **/
    _diagram.layoutUnchanged = property(false);
    _diagram.nodeChangeSelect = property(function() {
        if(_diagram.layoutEngine().supportsMoving && _diagram.layoutEngine().supportsMoving())
            return topology_node;
        else
            return basic_node;
    });
    _diagram.edgeChangeSelect = property(function() {
        if(_diagram.layoutEngine().supportsMoving && _diagram.layoutEngine().supportsMoving())
            return topology_edge;
        else
            return basic_edge;
    });

    /**
     * When `layoutUnchanged` is false, this will force layout to happen again. This may be needed
     * when changing a parameter but not changing the topology of the graph. (Yes, probably should
     * not be necessary.)
     * @method relayout
     * @memberof dc_graph.diagram
     * @instance
     * @return {dc_graph.diagram}
     **/
    _diagram.relayout = function() {
        _nodes_snapshot = _edges_snapshot = null;
        return this;
    };

    /**
     * Function to call to generate an initial layout. Takes (diagram, nodes, edges)
     *
     * **Deprecated**: The only layout that was using this was `tree_positions` and it never
     * worked as an initialization step for cola, as was originally intended. Now that
     * `tree_layout` is a layout algorithm, this should go away.
     *
     * In the future, there will be support for chaining layout algorithms. But that will be a
     * matter of composing them into a super-algorithm, not a special step like this was.
     * @method initialLayout
     * @memberof dc_graph.diagram
     * @instance
     * @param {Function} [initialLayout=null]
     * @return {Function}
     * @return {dc_graph.diagram}
     **/
    _diagram.initialLayout = deprecated_property('initialLayout is deprecated - use layout algorithms instead', null);

    _diagram.initialOnly = deprecated_property('initialOnly is deprecated - see the initialLayout deprecation notice in the documentation', false);

    /**
     * By default, all nodes are included, and edges are only included if both end-nodes are
     * visible.  If `.induceNodes` is set, then only nodes which have at least one edge will be
     * shown.
     * @method induceNodes
     * @memberof dc_graph.diagram
     * @instance
     * @param {Boolean} [induceNodes=false]
     * @return {Boolean}
     * @return {dc_graph.diagram}
     **/
    _diagram.induceNodes = property(false);

    /**
     * If this flag is true, the positions of nodes and will be updated while layout is
     * iterating. If false, the positions will only be updated once layout has
     * stabilized. Note: this may not be compatible with transitionDuration.
     * @method showLayoutSteps
     * @memberof dc_graph.diagram
     * @instance
     * @param {Boolean} [showLayoutSteps=false]
     * @return {Boolean}
     * @return {dc_graph.diagram}
     **/
    _diagram.showLayoutSteps = property(false);

    /**
     * Assigns a legend object which will be displayed within the same SVG element and
     * according to the visual encoding of this diagram.
     * @method legend
     * @memberof dc_graph.diagram
     * @instance
     * @param {Object} [legend=null]
     * @return {Object}
     * @return {dc_graph.diagram}
     **/
    // (pre-deprecated; see below)

    /**
     * Specifies another kind of child layer or interface. For example, this can
     * be used to display tooltips on nodes using `dc_graph.tip`.

     * The child needs to support a `parent` method, the diagram to modify.
     * @method child
     * @memberof dc_graph.diagram
     * @instance
     * @param {String} [id] - the name of the child to modify or add
     * @param {Object} [object] - the child object to add, or null to remove
     * @example
     * // Display tooltips on node hover, via the d3-tip library
     * var tip = dc_graph.tip()
     * tip.content(function(n, k) {
     *   // you can do an asynchronous call here, e.g. d3.json, if you need
     *   // to fetch data to show the tooltip - just call k() with the content
     *   k("This is <em>" + n.orig.value.name + "</em>");
     * });
     * diagram.child('tip', tip);
     * @return {dc_graph.diagram}
     **/
    _diagram.mode = _diagram.child = named_children();

    _diagram.mode.reject = function(id, object) {
        var rtype = _diagram.renderer().rendererType();
        if(!object)
            return false; // null is always a valid mode for any renderer
        if(!object.supportsRenderer)
            console.log('could not check if "' + id + '" is compatible with ' + rtype);
        else if(!object.supportsRenderer(rtype))
            return 'not installing "' + id + '" because it is not compatible with renderer ' + rtype;
        return false;
    };

    _diagram.legend = deprecate_function(".legend() is deprecated; use .child() for more control & multiple legends", function(_) {
        if(!arguments.length)
            return _diagram.child('node-legend');
        _diagram.child('node-legend', _);
        return _diagram;
    });

    /**
     * Specify 'cola' (the default) or 'dagre' as the Layout Algorithm and it will replace the
     * back-end.
     *
     * **Deprecated**: use {@link dc_graph.diagram#layoutEngine diagram.layoutEngine} with the engine
     * object instead
     * @method layoutAlgorithm
     * @memberof dc_graph.diagram
     * @instance
     * @param {String} [algo='cola'] - the name of the layout algorithm to use
     * @example
     * // use dagre for layout
     * diagram.layoutAlgorithm('dagre');
     * @return {dc_graph.diagram}
     **/
    _diagram.layoutAlgorithm = function(value, skipWarning) {
        if(!arguments.length)
            return _diagram.layoutEngine() ? _diagram.layoutEngine().layoutAlgorithm() : 'cola';
        if(!skipWarning)
            console.warn('dc.graph.diagram.layoutAlgorithm is deprecated - pass the layout engine object to dc_graph.diagram.layoutEngine instead');

        var engine;
        switch(value) {
        case 'cola':
            engine = dc_graph.cola_layout();
            break;
        case 'dagre':
            engine = dc_graph.dagre_layout();
        }
        engine = dc_graph.webworker_layout(engine);
        _diagram.layoutEngine(engine);
        return this;
    };

    /**
     * The layout engine determines positions of nodes and edges.
     * @method layoutEngine
     * @memberof dc_graph.diagram
     * @instance
     * @param {Object} [engine=null] - the layout engine to use
     * @example
     * // use cola with no webworker
     * diagram.layoutEngine(dc_graph.cola_layout());
     * // use dagre with a webworker
     * diagram.layoutEngine(dc_graph.webworker_layout(dc_graph.dagre_layout()));
     **/
    _diagram.layoutEngine = property(null).react(function(val) {
        if(val && val.parent)
            val.parent(_diagram);
        if(_diagram.renderer().isRendered()) {
            // remove any calculated points, if engine did that
            Object.keys(_edges).forEach(function(k) {
                _edges[k].cola.points = null;
            });
            // initialize engine
            initLayout(val);
        }
    });

    _diagram.renderer = property(dc_graph.render_svg().parent(_diagram)).react(function(r) {
        if(_diagram.renderer())
            _diagram.renderer().parent(null);
        r.parent(_diagram);
    });

    // S-spline any edges that are not going in this direction
    _diagram.enforceEdgeDirection = property(null);

    _diagram.tickSize = deprecate_layout_algo_parameter('tickSize');


    _diagram.uniqueId = function() {
        return _diagram.anchorName().replace(/[ .#=\[\]"]/g, '-');
    };

    _diagram.edgeId = function(e) {
        return 'edge-' + _diagram.edgeKey.eval(e).replace(/[^\w-_]/g, '-');
    };

    _diagram.arrowId = function(e, kind) {
        return 'arrow-' + kind + '-' + _diagram.uniqueId() + '-'  + _diagram.edgeId(e);
    };
    _diagram.textpathId = function(e) {
        return 'textpath-' + _diagram.uniqueId() + '-' + _diagram.edgeId(e);
    };

    // this kind of begs a (meta)graph ADT
    // instead of munging this into the diagram
    _diagram.getNode = function(id) {
        return _nodes[id] ? _nodes[id].orig : null;
    };

    _diagram.getWholeNode = function(id) {
        return _nodes[id] ? _nodes[id] : null;
    };

    _diagram.getEdge = function(id) {
        return _edges[id] ? _edges[id].orig : null;
    };

    _diagram.getWholeEdge = function(id) {
        return _edges[id] ? _edges[id] : null;
    };

    // again, awful, we need an ADT
    _diagram.getPort = function(nid, eid, name) {
        return _ports[port_name(nid, eid, name)];
    };

    _diagram.nodePorts = function() {
        return _nodePorts;
    };

    _diagram.getWholeCluster = function(id) {
        return _clusters[id] || null;
    };

    /**
     * Instructs cola.js to fit the connected components.
     *
     * **Deprecated**: Use
     * {@link dc_graph.cola_layout#handleDisconnected cola_layout.handleDisconnected} instead.
     * @method handleDisconnected
     * @memberof dc_graph.diagram
     * @instance
     * @param {Boolean} [handleDisconnected=true]
     * @return {Boolean}
     * @return {dc_graph.diagram}
     **/
    _diagram.handleDisconnected = deprecate_layout_algo_parameter('handleDisconnected');

    function initLayout(engine) {
        if(!_diagram.layoutEngine())
            _diagram.layoutAlgorithm('cola', true);
        (engine || _diagram.layoutEngine()).init({
            width: _diagram.width(),
            height: _diagram.height()
        });
    }

    _diagram.forEachChild = function(node, children, idf, f) {
        children.enum().forEach(function(key) {
            f(children(key),
              node.filter(function(n) { return idf(n) === key; }));
        });
    };
    _diagram.forEachShape = function(node, f) {
        _diagram.forEachChild(node, _diagram.shape, function(n) { return n.dcg_shape.shape; }, f);
    };
    _diagram.forEachContent = function(node, f) {
        _diagram.forEachChild(node, _diagram.content, _diagram.nodeContent.eval, f);
    };

    function has_source_and_target(e) {
        return !!e.source && !!e.target;
    }

    // three stages: delete before layout, and modify & insert split the transitionDuration
    _diagram.stagedDuration = function() {
        return (_diagram.stageTransitions() !== 'none') ?
            _diagram.transitionDuration() / 2 :
            _diagram.transitionDuration();
    };

    _diagram.stagedDelay = function(is_enter) {
        return _diagram.stageTransitions() === 'none' ||
            _diagram.stageTransitions() === 'modins' === !is_enter ?
            0 :
            _diagram.transitionDuration() / 2;
    };

    _diagram.isRunning = function() {
        return _running;
    };

    function svg_specific(name) {
        return trace_function('trace', name + '() is specific to the SVG renderer', function() {
            return _diagram.renderer()[name].apply(this, arguments);
        });
    }

    function call_on_renderer(name) {
        return trace_function('trace', 'calling ' + name + '() on renderer', function() {
            return _diagram.renderer()[name].apply(this, arguments);
        });
    }

    _diagram.svg = svg_specific('svg');
    _diagram.g = svg_specific('g');
    _diagram.select = svg_specific('select');
    _diagram.selectAll = svg_specific('selectAll');
    _diagram.addOrRemoveDef = svg_specific('addOrRemoveDef');
    _diagram.selectAllNodes = svg_specific('selectAllNodes');
    _diagram.selectAllEdges = svg_specific('selectAllEdges');
    _diagram.selectNodePortsOfStyle = svg_specific('selectNodePortsOfStyle');
    _diagram.zoom = svg_specific('zoom');
    _diagram.translate = svg_specific('translate');
    _diagram.scale = svg_specific('scale');

    function renderer_specific(name) {
        return trace_function('trace', name + '() will have renderer-specific arguments', function() {
            return _diagram.renderer()[name].apply(this, arguments);
        });
    }
    _diagram.renderNode = svg_specific('renderNode');
    _diagram.renderEdge = svg_specific('renderEdge');
    _diagram.redrawNode = svg_specific('redrawNode');
    _diagram.redrawEdge = svg_specific('redrawEdge');
    _diagram.reposition = call_on_renderer('reposition');


    /**
     * Standard dc.js
     * {@link https://github.com/dc-js/dc.js/blob/develop/web/docs/api-latest.md#dc.baseMixin baseMixin}
     * method. Computes a new layout based on the nodes and edges in the edge groups, and
     * displays the diagram.  To the extent possible, the diagram will minimize changes in
     * positions from the previous layout.  `.render()` must be called the first time, and
     * `.redraw()` can be called after that.
     *
     * `.redraw()` will be triggered by changes to the filters in any other charts in the same
     * dc.js chart group.
     *
     * Unlike in dc.js, `redraw` executes asynchronously, because drawing can be computationally
     * intensive, and the diagram will be drawn multiple times if
     * {@link #dc_graph.diagram+showLayoutSteps showLayoutSteps}
     * is enabled. Watch the {@link #dc_graph.diagram+on 'end'} event to know when layout is
     * complete.
     * @method redraw
     * @memberof dc_graph.diagram
     * @instance
     * @return {dc_graph.diagram}
     **/
    var _needsRedraw = false;
    _diagram.redraw = function () {
        // since dc.js can receive UI events and trigger redraws whenever it wants,
        // and cola absolutely will not tolerate being poked while it's doing layout,
        // we need to guard the startLayout call.
        if(_running) {
            _needsRedraw = true;
            return this;
        }
        else return _diagram.startLayout();
    };

    /**
     * Standard dc.js
     * {@link https://github.com/dc-js/dc.js/blob/develop/web/docs/api-latest.md#dc.baseMixin baseMixin}
     * method. Erases any existing SVG elements and draws the diagram from scratch. `.render()`
     * must be called the first time, and `.redraw()` can be called after that.
     * @method render
     * @memberof dc_graph.diagram
     * @instance
     * @return {dc_graph.diagram}
     **/
    _diagram.render = function() {
        if(_diagram.renderer().isRendered())
            _dispatch.reset();
        if(!_diagram.initLayoutOnRedraw())
            initLayout();

        _nodes = {};
        _edges = {};
        _ports = {};
        _clusters = {};

        // start out with 1:1 zoom
        _diagram.x(d3.scale.linear()
                   .domain([0, _diagram.width()])
                   .range([0, _diagram.width()]));
        _diagram.y(d3.scale.linear()
                   .domain([0, _diagram.height()])
                   .range([0, _diagram.height()]));
        _diagram.renderer().initializeDrawing();
        _dispatch.render();
        _diagram.redraw();
        return this;
    };

    _diagram.refresh = call_on_renderer('refresh');

    _diagram.width_is_automatic = function() {
        return _width === 'auto';
    };

    _diagram.height_is_automatic = function() {
        return _height === 'auto';
    };

    function detect_size_change() {
        var oldWidth = _lastWidth, oldHeight = _lastHeight;
        var newWidth = _diagram.width(), newHeight = _diagram.height();
        if(oldWidth !== newWidth || oldHeight !== newHeight)
            _diagram.renderer().rezoom(oldWidth, oldHeight, newWidth, newHeight);
    }

    // extract just the topology-related parts of nodes & edges to see if
    // graph has changed wrt layout. imperfect heuristic: assume that the original
    // data as well as all cola fields starting with dcg_ are related to topology
    function dcg_fields(cola) {
        var entries = Object.entries(cola)
            .filter(function(entry) { return /^dcg_/.test(entry[0]); });
        return entries.reduce(function(p, entry) {
            p[entry[0]] = entry[1];
            return p;
        }, {});
    }
    function topology_node(n) {
        return {orig: get_original(n), cola: dcg_fields(n.cola)};
    }
    function topology_edge(e) {
        return {orig: get_original(e), cola: dcg_fields(e.cola)};
    }
    function basic_node(n) {
        var n0 = get_original(n);
        return {
            orig: {
                key: n0.key,
                value: Object.fromEntries(
                    Object.entries(n0.value)
                        .filter(function(kv) { return kv[0] !== 'fixedPos'; }))
            }
        };
    }
    function basic_edge(e) {
        return {orig: get_original(e)};
    }

    _diagram.startLayout = function () {
        var nodes = _diagram.nodeGroup().all();
        var edges = _diagram.edgeGroup().all();
        var ports = _diagram.portGroup() ? _diagram.portGroup().all() : [];
        var clusters = _diagram.clusterGroup() ? _diagram.clusterGroup().all() : [];
        if(_running) {
            throw new Error('dc_graph.diagram.redraw already running!');
        }
        _running = true;

        if(_diagram.width_is_automatic() || _diagram.height_is_automatic())
            detect_size_change();
        else
            _diagram.renderer().resize();

        if(_diagram.initLayoutOnRedraw())
            initLayout();
        _diagram.layoutEngine().stop();
        _dispatch.preDraw();

        // ordering shouldn't matter, but we support ordering in case it does
        if(_diagram.nodeOrdering()) {
            nodes = crossfilter.quicksort.by(_diagram.nodeOrdering())(nodes.slice(0), 0, nodes.length);
        }
        if(_diagram.edgeOrdering()) {
            edges = crossfilter.quicksort.by(_diagram.edgeOrdering())(edges.slice(0), 0, edges.length);
        }

        var wnodes = regenerate_objects(_nodes, nodes, null, function(v) {
            return _diagram.nodeKey()(v);
        }, function(v1, v) {
            v1.orig = v;
            v1.cola = v1.cola || {};
            v1.cola.dcg_nodeKey = _diagram.nodeKey.eval(v1);
            v1.cola.dcg_nodeParentCluster = _diagram.nodeParentCluster.eval(v1);
            _diagram.layoutEngine().populateLayoutNode(v1.cola, v1);
        });
        var wedges = regenerate_objects(_edges, edges, null, function(e) {
            return _diagram.edgeKey()(e);
        }, function(e1, e) {
            e1.orig = e;
            e1.cola = e1.cola || {};
            e1.cola.dcg_edgeKey = _diagram.edgeKey.eval(e1);
            e1.cola.dcg_edgeSource = _diagram.edgeSource.eval(e1);
            e1.cola.dcg_edgeTarget = _diagram.edgeTarget.eval(e1);
            e1.source = _nodes[e1.cola.dcg_edgeSource];
            e1.target = _nodes[e1.cola.dcg_edgeTarget];
            e1.sourcePort = e1.sourcePort || {};
            e1.targetPort = e1.targetPort || {};
            _diagram.layoutEngine().populateLayoutEdge(e1.cola, e1);
        });

        // remove edges that don't have both end nodes
        wedges = wedges.filter(has_source_and_target);

        // remove self-edges (since we can't draw them - will be option later)
        wedges = wedges.filter(function(e) { return e.source !== e.target; });

        wedges = wedges.filter(_diagram.edgeIsShown.eval);

        // now we know which ports should exist
        var needports = wedges.map(function(e) {
            if(_diagram.edgeSourcePortName.eval(e))
                return port_name(_diagram.edgeSource.eval(e), null, _diagram.edgeSourcePortName.eval(e));
            else return port_name(null, _diagram.edgeKey.eval(e), 'source');
        });
        needports = needports.concat(wedges.map(function(e) {
            if(_diagram.edgeTargetPortName.eval(e))
                return port_name(_diagram.edgeTarget.eval(e), null, _diagram.edgeTargetPortName.eval(e));
            else return port_name(null, _diagram.edgeKey.eval(e), 'target');
        }));
        // remove any invalid ports so they don't crash in confusing ways later
        ports = ports.filter(function(p) {
            return _diagram.portNodeKey() && _diagram.portNodeKey()(p) ||
                _diagram.portEdgeKey() && _diagram.portEdgeKey()(p);
        });
        var wports = regenerate_objects(_ports, ports, needports, function(p) {
            return port_name(_diagram.portNodeKey() && _diagram.portNodeKey()(p),
                             _diagram.portEdgeKey() && _diagram.portEdgeKey()(p),
                             _diagram.portName()(p));
        }, function(p1, p) {
            p1.orig = p;
            if(p1.named)
                p1.edges = [];
        }, function(k, p) {
            console.assert(k, 'should have screened out invalid ports');
            // it's dumb to parse the id we just created. as usual, i blame the lack of metagraphs
            var parse = split_port_name(k);
            if(parse.nodeKey) {
                p.node = _nodes[parse.nodeKey];
                p.named = true;
            }
            else {
                var e = _edges[parse.edgeKey];
                p.node = e[parse.name];
                p.edges = [e];
                p.named = false;
            }
            p.name = parse.name;
        });
        // remove any ports where the end-node was not found, to avoid crashing elsewhere
        wports = wports.filter(function(p) { return p.node; });

        // find all edges for named ports
        wedges.forEach(function(e) {
            var name = _diagram.edgeSourcePortName.eval(e);
            if(name)
                _ports[port_name(_diagram.nodeKey.eval(e.source), null, name)].edges.push(e);
            name = _diagram.edgeTargetPortName.eval(e);
            if(name)
                _ports[port_name(_diagram.nodeKey.eval(e.target), null, name)].edges.push(e);
        });

        // optionally, delete nodes that have no edges
        if(_diagram.induceNodes()) {
            var keeps = {};
            wedges.forEach(function(e) {
                keeps[e.cola.dcg_edgeSource] = true;
                keeps[e.cola.dcg_edgeTarget] = true;
            });
            wnodes = wnodes.filter(function(n) { return keeps[n.cola.dcg_nodeKey]; });
            for(var k in _nodes)
                if(!keeps[k])
                    delete _nodes[k];
        }

        var needclusters = d3.set(wnodes.map(function(n) {
            return _diagram.nodeParentCluster.eval(n);
        }).filter(identity)).values();

        var wclusters = regenerate_objects(_clusters, clusters, needclusters, function(c) {
            return _diagram.clusterKey()(c);
        }, function(c1, c) { // assign
            c1.orig = c;
            c1.cola = c1.cola || {
                dcg_clusterKey: _diagram.clusterKey.eval(c1),
                dcg_clusterParent: _diagram.clusterParent.eval(c1)
            };
        }, function(k, c) { // create
        });

        wnodes.forEach(function(v, i) {
            v.index = i;
        });

        // announce new data
        _dispatch.data(_diagram, _nodes, wnodes, _edges, wedges, _ports, wports);
        _stats = {nnodes: wnodes.length, nedges: wedges.length};

        // fixed nodes may have been affected by .data() so calculate now
        wnodes.forEach(function(v) {
            if(_diagram.nodeFixed())
                v.cola.dcg_nodeFixed = _diagram.nodeFixed.eval(v);
        });

        // annotate parallel edges so we can draw them specially
        if(_diagram.parallelEdgeOffset()) {
            var em = new Array(wnodes.length);
            for(var i = 0; i < wnodes.length; ++i)
                em[i] = new Array(i);
            wedges.forEach(function(e) {
                e.pos = e.pos || {};
                var min, max, minattr, maxattr;
                if(e.source.index < e.target.index) {
                    min = e.source.index; max = e.target.index;
                    minattr = 'edgeSourcePortName'; maxattr = 'edgeTargetPortName';
                } else {
                    max = e.source.index; min = e.target.index;
                    maxattr = 'edgeSourcePortName'; minattr = 'edgeTargetPortName';
                }
                var minport = _diagram[minattr].eval(e) || 'no port',
                    maxport = _diagram[maxattr].eval(e) || 'no port';
                em[max][min] = em[max][min] || {};
                em[max][min][maxport] = em[max][min][maxport] || {};
                e.parallel = em[max][min][maxport][minport] = em[max][min][maxport][minport] || {
                    rev: [],
                    edges: []
                };
                e.parallel.edges.push(e);
                e.parallel.rev.push(min !== e.source.index);
            });
        }

        var drawState = _diagram.renderer().startRedraw(_dispatch, wnodes, wedges);

        // really we should have layout chaining like in the good old Dynagraph days
        // the ordering of this and the previous 4 statements is somewhat questionable
        if(_diagram.initialLayout())
            _diagram.initialLayout()(_diagram, wnodes, wedges);

        // no layout if the topology and layout parameters haven't changed
        var skip_layout = false;
        if(!_diagram.layoutUnchanged()) {
            var node_fields = _diagram.nodeChangeSelect()(),
                edge_fields = _diagram.edgeChangeSelect()();
            var nodes_snapshot = JSON.stringify(wnodes.map(node_fields));
            var edges_snapshot = JSON.stringify(wedges.map(edge_fields));
            if(nodes_snapshot === _nodes_snapshot && edges_snapshot === _edges_snapshot)
                skip_layout = true;
            _nodes_snapshot = nodes_snapshot;
            _edges_snapshot = edges_snapshot;
        }

        // edge lengths may be affected by node sizes
        wedges.forEach(function(e) {
            e.cola.dcg_edgeLength = _diagram.edgeLength.eval(e);
        });

        // cola constraints always use indices, but node references
        // are more friendly, so translate those

        // i am not satisfied with this constraint generation api...
        // https://github.com/dc-js/dc.graph.js/issues/10
        var constraints = _diagram.constrain()(_diagram, wnodes, wedges);

        // warn if there are any loops (before changing names to indices)
        // it would be better to do this in webcola
        // (for one thing, this duplicates logic in rectangle.ts)
        // but by that time it has lost the names of things,
        // so the output would be difficult to use
        var constraints_by_left = constraints.reduce(function(p, c) {
            if(c.type) {
                switch(c.type) {
                case 'alignment':
                    var left = c.offsets[0].node;
                    p[left] = p[left] || [];
                    c.offsets.slice(1).forEach(function(o) {
                        p[left].push({node: o.node, in_constraint: c});
                    });
                    break;
                }
            } else if(c.axis) {
                p[c.left] = p[c.left] || [];
                p[c.left].push({node: c.right, in_constraint: c});
            }
            return p;
        }, {});
        var touched = {};
        function find_constraint_loops(con, stack) {
            var left = con.node;
            stack = stack || [];
            var loop = stack.find(function(con) { return con.node === left; });
            stack = stack.concat([con]);
            if(loop)
                console.warn('found a loop in constraints', stack);
            if(touched[left])
                return;
            touched[left] = true;
            if(!constraints_by_left[left])
                return;
            constraints_by_left[left].forEach(function(right) {
                find_constraint_loops(right, stack);
            });
        }
        Object.keys(constraints_by_left).forEach(function(left) {
            if(!touched[left])
                find_constraint_loops({node: left, in_constraint: null});
        });

        // translate references from names to indices (ugly)
        var invalid_constraints = [];
        constraints.forEach(function(c) {
            if(c.type) {
                switch(c.type) {
                case 'alignment':
                    c.offsets.forEach(function(o) {
                        o.node = _nodes[o.node].index;
                    });
                    break;
                case 'circle':
                    c.nodes.forEach(function(n) {
                        n.node = _nodes[n.node].index;
                    });
                    break;
                }
            } else if(c.axis && c.left && c.right) {
                c.left = _nodes[c.left].index;
                c.right = _nodes[c.right].index;
            }
            else invalid_constraints.push(c);
        });

        if(invalid_constraints.length)
            console.warn(invalid_constraints.length + ' invalid constraints', invalid_constraints);

        // pseudo-cola.js features

        // 1. non-layout edges are drawn but not told to cola.js
        var layout_edges = wedges.filter(_diagram.edgeIsLayout.eval);
        var nonlayout_edges = wedges.filter(function(x) {
            return !_diagram.edgeIsLayout.eval(x);
        });

        // 2. type=circle constraints
        var circle_constraints = constraints.filter(function(c) {
            return c.type === 'circle';
        });
        constraints = constraints.filter(function(c) {
            return c.type !== 'circle';
        });
        circle_constraints.forEach(function(c) {
            var R = (c.distance || _diagram.baseLength()*4) / (2*Math.sin(Math.PI/c.nodes.length));
            var nindices = c.nodes.map(function(x) { return x.node; });
            var namef = function(i) {
                return _diagram.nodeKey.eval(wnodes[i]);
            };
            var wheel = dc_graph.wheel_edges(namef, nindices, R)
                    .map(function(e) {
                        var e1 = {internal: e};
                        e1.source = _nodes[e.sourcename];
                        e1.target = _nodes[e.targetname];
                        return e1;
                    });
            layout_edges = layout_edges.concat(wheel);
        });

        // 3. ordered alignment
        var ordered_constraints = constraints.filter(function(c) {
            return c.type === 'ordering';
        });
        constraints = constraints.filter(function(c) {
            return c.type !== 'ordering';
        });
        ordered_constraints.forEach(function(c) {
            var sorted = c.nodes.map(function(n) { return _nodes[n]; });
            if(c.ordering) {
                var sort = crossfilter.quicksort.by(param(c.ordering));
                sorted = sort(sorted, 0, sorted.length);
            }
            var left;
            sorted.forEach(function(n, i) {
                if(i===0)
                    left = n;
                else {
                    constraints.push({
                        left: left.index,
                        right: (left = n).index,
                        axis: c.axis,
                        gap: c.gap
                    });
                }
            });
        });
        if(skip_layout) {
            _running = false;
            // init_node_ports?
            _diagram.renderer().draw(drawState, true);
            _diagram.renderer().drawPorts(drawState);
            _diagram.renderer().fireTSEvent(_dispatch, drawState);
            check_zoom(drawState);
            return this;
        }
        var startTime = Date.now();

        function populate_cola(rnodes, redges, rclusters) {
            rnodes.forEach(function(rn) {
                var n = _nodes[rn.dcg_nodeKey];
                if(!n) {
                    console.warn('received node "' + rn.dcg_nodeKey + '" that we did not send, ignored');
                    return;
                }
                n.cola.x = rn.x;
                n.cola.y = rn.y;
                n.cola.z = rn.z;
            });
            redges.forEach(function(re) {
                var e = _edges[re.dcg_edgeKey];
                if(!e) {
                    console.warn('received edge "' + re.dcg_edgeKey + '" that we did not send, ignored');
                    return;
                }
                if(re.points)
                    e.cola.points = re.points;
            });
            wclusters.forEach(function(c) {
                c.cola.bounds = null;
            });
            if(rclusters)
                rclusters.forEach(function(rc) {
                    var c = _clusters[rc.dcg_clusterKey];
                    if(!c) {
                        console.warn('received cluster "' + rc.dcg_clusterKey + '" that we did not send, ignored');
                        return;
                    }
                    if(rc.bounds)
                        c.cola.bounds = rc.bounds;
                });
        }
        _diagram.layoutEngine()
            .on('tick.diagram', function(nodes, edges, clusters) {
                var elapsed = Date.now() - startTime;
                if(!_diagram.initialOnly())
                    populate_cola(nodes, edges, clusters);
                if(_diagram.showLayoutSteps()) {
                    init_node_ports(_nodes, wports);
                    _dispatch.receivedLayout(_diagram, _nodes, wnodes, _edges, wedges, _ports, wports);
                    propagate_port_positions(_nodes, wedges, _ports);
                    _diagram.renderer().draw(drawState, true);
                    _diagram.renderer().drawPorts(drawState);
                    // should do this only once
                    _diagram.renderer().fireTSEvent(_dispatch, drawState);
                }
                if(_needsRedraw || _diagram.timeLimit() && elapsed > _diagram.timeLimit()) {
                    console.log('cancelled');
                    _diagram.layoutEngine().stop();
                }
            })
            .on('end.diagram', function(nodes, edges, clusters) {
                if(!_diagram.showLayoutSteps()) {
                    if(!_diagram.initialOnly())
                        populate_cola(nodes, edges, clusters);
                    init_node_ports(_nodes, wports);
                    _dispatch.receivedLayout(_diagram, _nodes, wnodes, _edges, wedges, _ports, wports);
                    propagate_port_positions(_nodes, wedges, _ports);
                    _diagram.renderer().draw(drawState, true);
                    _diagram.renderer().drawPorts(drawState);
                    _diagram.renderer().fireTSEvent(_dispatch, drawState);
                }
                else _diagram.layoutDone(true);
                check_zoom(drawState);
            })
            .on('start.diagram', function() {
                console.log('algo ' + _diagram.layoutEngine().layoutAlgorithm() + ' started.');
                _dispatch.start();
            });

        if(_diagram.initialOnly())
            _diagram.layoutEngine().dispatch().end(wnodes, wedges);
        else {
            _dispatch.start(); // cola doesn't seem to fire this itself?
            var engine = _diagram.layoutEngine();
            engine.data(
                { width: _diagram.width(), height: _diagram.height() },
                wnodes.map(function(v) {
                    var lv = Object.assign({}, v.cola, v.dcg_shape);
                    if(engine.annotateNode)
                        engine.annotateNode(lv, v);
                    else if(engine.extractNodeAttrs)
                        Object.keys(engine.extractNodeAttrs()).forEach(function(key) {
                            lv[key] = engine.extractNodeAttrs()[key](v.orig);
                        });
                    return lv;
                }),
                layout_edges.map(function(e) {
                    var le = e.cola;
                    if(engine.annotateEdge)
                        engine.annotateEdge(le, e);
                    else if(engine.extractEdgeAttrs)
                        Object.keys(engine.extractEdgeAttrs()).forEach(function(key) {
                            le[key] = engine.extractEdgeAttrs()[key](e.orig);
                        });
                    return le;
                }),
                wclusters.map(function(c) {
                    return c.cola;
                }),
                constraints
            );
            engine.start();
        }
        return this;
    };

    function check_zoom(drawState) {
        var do_zoom, animate = true;
        if(_diagram.width_is_automatic() || _diagram.height_is_automatic())
            detect_size_change();
        switch(_diagram.autoZoom()) {
        case 'always-skipanimonce':
            animate = false;
            _diagram.autoZoom('always');
        case 'always':
            do_zoom = true;
            break;
        case 'once-noanim':
            animate = false;
        case 'once':
            do_zoom = true;
            _diagram.autoZoom(null);
            break;
        default:
            do_zoom = false;
        }
        calc_bounds(drawState);
        if(do_zoom)
            auto_zoom(animate);
    }

    function norm(v) {
        var len = Math.hypot(v[0], v[1]);
        return [v[0]/len, v[1]/len];
    }
    function edge_vec(n, e) {
        var dy = e.target.cola.y - e.source.cola.y,
            dx = e.target.cola.x - e.source.cola.x;
        if(dy === 0 && dx === 0)
            return [1, 0];
        if(e.source !== n)
            dy = -dy, dx = -dx;
        if(e.parallel && e.parallel.edges.length > 1 && e.source.index > e.target.index)
            dy = -dy, dx = -dx;
        return norm([dx, dy]);
    }
    function init_node_ports(nodes, wports) {
        _nodePorts = {};
        // assemble port-lists for nodes, again because we don't have a metagraph.
        wports.forEach(function(p) {
            var nid = _diagram.nodeKey.eval(p.node);
            var np = _nodePorts[nid] = _nodePorts[nid] || [];
            np.push(p);
        });
        for(var nid in _nodePorts) {
            var n = nodes[nid],
                nports = _nodePorts[nid];
            // initial positions: use average of edge vectors, if any, or existing position
            nports.forEach(function(p) {
                if(_diagram.portElastic.eval(p) && p.edges.length) {
                    var vecs = p.edges.map(edge_vec.bind(null, n));
                    p.vec = [
                        d3.sum(vecs, function(v) { return v[0]; })/vecs.length,
                        d3.sum(vecs, function(v) { return v[1]; })/vecs.length
                    ];
                } else p.vec = p.vec || undefined;
                p.pos = null;
            });
        }
    }
    function propagate_port_positions(nodes, wedges, ports) {
        // make sure we have projected vectors to positions
        for(var nid in _nodePorts) {
            var n = nodes[nid];
            _nodePorts[nid].forEach(function(p) {
                if(!p.pos)
                    project_port(_diagram, n, p);
            });
        }

        // propagate port positions to edge endpoints
        wedges.forEach(function(e) {
            var name = _diagram.edgeSourcePortName.eval(e);
            e.sourcePort.pos = name ? ports[port_name(_diagram.nodeKey.eval(e.source), null, name)].pos :
                ports[port_name(null, _diagram.edgeKey.eval(e), 'source')].pos;
            name = _diagram.edgeTargetPortName.eval(e);
            e.targetPort.pos = name ? ports[port_name(_diagram.nodeKey.eval(e.target), null, name)].pos :
                ports[port_name(null, _diagram.edgeKey.eval(e), 'target')].pos;
            console.assert(e.sourcePort.pos && e.targetPort.pos);
        });
    }

    _diagram.requestRefresh = function(durationOverride) {
        window.requestAnimationFrame(function() {
            var transdur;
            if(durationOverride !== undefined) {
                transdur = _diagram.transitionDuration();
                _diagram.transitionDuration(durationOverride);
            }
            _diagram.renderer().refresh();
            if(durationOverride !== undefined)
                _diagram.transitionDuration(transdur);
        });
    };

    _diagram.layoutDone = function(happens) {
        _dispatch.end(happens);
        _running = false;
        if(_needsRedraw) {
            _needsRedraw = false;
            window.setTimeout(function() {
                if(!_diagram.isRunning()) // someone else may already have started
                    _diagram.redraw();
            }, 0);
        }
    };

    function enforce_path_direction(path, spos, tpos) {
        var points = path.points, first = points[0], last = points[points.length-1];
        switch(_diagram.enforceEdgeDirection()) {
        case 'LR':
            if(spos.x >= tpos.x) {
                var dx = first.x - last.x;
                return {
                    points: [
                        first,
                        {x: first.x + dx, y: first.y - dx/2},
                        {x: last.x - dx, y: last.y - dx/2},
                        last
                    ],
                    bezDegree: 3,
                    sourcePort: path.sourcePort,
                    targetPort: path.targetPort
                };
            }
            break;
        case 'TB':
            if(spos.y >= tpos.y) {
                var dy = first.y - last.y;
                return {
                    points: [
                        first,
                        {x: first.x + dy/2, y: first.y + dy},
                        {x: last.x + dy/2, y: last.y - dy},
                        last
                    ],
                    bezDegree: 3,
                    sourcePort: path.sourcePort,
                    targetPort: path.targetPort
                };
            }
            break;
        }
        return path;
    }
    _diagram.calcEdgePath = function(e, age, sx, sy, tx, ty) {
        var parallel = e.parallel;
        var source = e.source, target = e.target;
        if(parallel.edges.length > 1 && e.source.index > e.target.index) {
            var t;
            t = target; target = source; source = t;
            t = tx; tx = sx; sx = t;
            t = ty; ty = sy; sy = t;
        }
        var source_padding = source.dcg_ry +
            _diagram.nodeStrokeWidth.eval(source) / 2,
            target_padding = target.dcg_ry +
            _diagram.nodeStrokeWidth.eval(target) / 2;
        for(var p = 0; p < parallel.edges.length; ++p) {
            // alternate parallel edges over, then under
            var dir = (!!(p%2) === (sx < tx)) ? -1 : 1,
                port = Math.floor((p+1)/2),
                last = port > 0 ? parallel.edges[p > 2 ? p - 2 : 0].pos[age].path : null;
            var path = draw_edge_to_shapes(_diagram, e, sx, sy, tx, ty,
                                           last, dir, _diagram.parallelEdgeOffset(),
                                           source_padding, target_padding
                                          );
            if(parallel.edges.length > 1 && parallel.rev[p])
                path.points.reverse();
            if(_diagram.enforceEdgeDirection())
                path = enforce_path_direction(path, source.cola, target.cola);
            var path0 = {
                points: path.points,
                bezDegree: path.bezDegree
            };
            var alengths = scaled_arrow_lengths(_diagram, parallel.edges[p]);
            path = clip_path_to_arrows(alengths.headLength, alengths.tailLength, path);
            var points = path.points, points0 = path0.points;
            parallel.edges[p].pos[age] = {
                path: path,
                full: path0,
                orienthead: angle_between_points(points[points.length-1], points0[points0.length-1]) + 'rad',
                orienttail: angle_between_points(points[0], points0[0]) + 'rad'
            };
        }
    };

    function node_bounds(n) {
        var bounds = {left: n.cola.x - n.dcg_rx, top: n.cola.y - n.dcg_ry,
                      right: n.cola.x + n.dcg_rx, bottom: n.cola.y + n.dcg_ry};
        if(_diagram.portStyle.enum().length) {
            var ports = _nodePorts[_diagram.nodeKey.eval(n)];
            if(ports)
                ports.forEach(function(p) {
                    var portStyle =_diagram.portStyleName.eval(p);
                    if(!portStyle || !_diagram.portStyle(portStyle))
                        return;
                    var pb = _diagram.portStyle(portStyle).portBounds(p);
                    pb.left += n.cola.x; pb.top += n.cola.y;
                    pb.right += n.cola.x; pb.bottom += n.cola.y;
                    bounds = union_bounds(bounds, pb);
                });
        }
        return bounds;
    }

    function union_bounds(b1, b2) {
        return {
            left: Math.min(b1.left, b2.left),
            top: Math.min(b1.top, b2.top),
            right: Math.max(b1.right, b2.right),
            bottom: Math.max(b1.bottom, b2.bottom)
        };
    }

    function point_to_bounds(p) {
        return {
            left: p.x,
            top: p.y,
            right: p.x,
            bottom: p.y
        };
    }

    function edge_bounds(e) {
        // assumption: edge must have some points
        var points = e.pos.new.path.points;
        return points.map(point_to_bounds).reduce(union_bounds);
    }

    _diagram.calculateBounds = function(ndata, edata) {
        // assumption: there can be no edges without nodes
        var bounds = ndata.map(node_bounds).reduce(union_bounds);
        return edata.map(edge_bounds).reduce(union_bounds, bounds);
    };
    var _bounds;
    function calc_bounds(drawState) {
        if((_diagram.fitStrategy() || _diagram.restrictPan())) {
            _bounds = _diagram.renderer().calculateBounds(drawState);
        }
    }

    _diagram.animateZoom = function(_) {
        if(!arguments.length)
            return _animateZoom;
        _animateZoom = _;
        return _diagram;
    };

    function auto_zoom(animate) {
        if(_diagram.fitStrategy()) {
            if(!_bounds)
                return;
            var vwidth = _bounds.right - _bounds.left, vheight = _bounds.bottom - _bounds.top,
                swidth =  _diagram.width() - _diagram.margins().left - _diagram.margins().right,
                sheight = _diagram.height() - _diagram.margins().top - _diagram.margins().bottom;
            var fitS = _diagram.fitStrategy(), translate = [0,0], scale = 1;
            if(['default', 'vertical', 'horizontal'].indexOf(fitS) >= 0) {
                var sAR = sheight / swidth, vAR = vheight / vwidth,
                    vrl = vAR<sAR, // view aspect ratio is less (wider)
                    amv = (fitS === 'default') ? !vrl : (fitS === 'vertical'); // align margins vertically
                scale = amv ? sheight / vheight : swidth / vwidth;
                scale = Math.max(_diagram.zoomExtent()[0], Math.min(_diagram.zoomExtent()[1], scale));
                translate = [_diagram.margins().left - _bounds.left*scale + (swidth - vwidth*scale) / 2,
                             _diagram.margins().top - _bounds.top*scale + (sheight - vheight*scale) / 2];
            }
            else if(typeof fitS === 'string' && fitS.match(/^align_/)) {
                var sides = fitS.split('_')[1].toLowerCase().split('');
                if(sides.length > 2)
                    throw new Error("align_ expecting 0-2 sides, not " + sides.length);
                var bounds = margined_bounds();
                translate = _diagram.renderer().translate();
                scale = _diagram.renderer().scale();
                var vertalign = false, horzalign = false;
                sides.forEach(function(s) {
                    switch(s) {
                    case 'l':
                        translate[0] = align_left(translate, bounds.left);
                        horzalign = true;
                        break;
                    case 't':
                        translate[1] = align_top(translate, bounds.top);
                        vertalign = true;
                        break;
                    case 'r':
                        translate[0] = align_right(translate, bounds.right);
                        horzalign = true;
                        break;
                    case 'b':
                        translate[1] = align_bottom(translate, bounds.bottom);
                        vertalign = true;
                        break;
                    case 'c': // handled below
                        break;
                    default:
                        throw new Error("align_ expecting l t r b or c, not '" + s + "'");
                    }
                });
                if(sides.includes('c')) {
                    if(!horzalign)
                        translate[0] = center_horizontally(translate, bounds);
                    if(!vertalign)
                        translate[1] = center_vertically(translate, bounds);
                }
            }
            else if(fitS === 'zoom') {
                scale = _diagram.renderer().scale();
                translate = bring_in_bounds(_diagram.renderer().translate());
            }
            else
                throw new Error('unknown fitStrategy type ' + typeof fitS);

            _animateZoom = animate;
            _diagram.renderer().translate(translate).scale(scale).commitTranslateScale();
            _animateZoom = false;
        }
    }
    function namespace_event_reducer(msg_fun) {
        return function(p, ev) {
            var namespace = {};
            p[ev] = function(ns) {
                return namespace[ns] = namespace[ns] || onetime_trace('trace', msg_fun(ns, ev));
            };
            return p;
        };
    }
    var renderer_specific_events = ['drawn', 'transitionsStarted', 'zoomed']
            .reduce(namespace_event_reducer(function(ns, ev) {
                return 'subscribing "' + ns + '" to event "' + ev + '" which takes renderer-specific parameters';
            }), {});
    var inconsistent_arguments = ['end']
            .reduce(namespace_event_reducer(function(ns, ev) {
                return 'subscribing "' + ns + '" to event "' + ev + '" which may receive inconsistent arguments';
            }), {});

    /**
     * Standard dc.js
     * {@link https://github.com/dc-js/dc.js/blob/develop/web/docs/api-latest.md#dc.baseMixin baseMixin}
     * method. Attaches an event handler to the diagram. The currently supported events are
     * * `start()` - layout is starting
     * * `drawn(nodes, edges)` - the node and edge elements have been rendered to the screen
     * and can be modified through the passed d3 selections.
     * * `end()` - diagram layout has completed.
     * @method on
     * @memberof dc_graph.diagram
     * @instance
     * @param {String} [event] - the event to subscribe to
     * @param {Function} [f] - the event handler
     * @return {dc_graph.diagram}
     **/
    _diagram.on = function(event, f) {
        if(arguments.length === 1)
            return _dispatch.on(event);
        var evns = event.split('.'),
            warning = renderer_specific_events[evns[0]] || inconsistent_arguments[evns[0]];
        if(warning)
            warning(evns[1] || '')();
        _dispatch.on(event, f);
        return this;
    };

    /**
     * Returns an object with current statistics on graph layout.
     * * `nnodes` - number of nodes displayed
     * * `nedges` - number of edges displayed
     * @method getStats
     * @memberof dc_graph.diagram
     * @instance
     * @return {}
     * @return {dc_graph.diagram}
     **/
    _diagram.getStats = function() {
        return _stats;
    };

    /**
     * Standard dc.js
     * {@link https://github.com/dc-js/dc.js/blob/develop/web/docs/api-latest.md#dc.baseMixin baseMixin}
     * method. Gets or sets the x scale.
     * @method x
     * @memberof dc_graph.diagram
     * @instance
     * @param {d3.scale} [scale]
     * @return {d3.scale}
     * @return {dc_graph.diagram}

     **/
    _diagram.x = property(null);

    /**
     * Standard dc.js
     * {@link https://github.com/dc-js/dc.js/blob/develop/web/docs/api-latest.md#dc.baseMixin baseMixin}
     * method. Gets or sets the y scale.
     * @method y
     * @memberof dc_graph.diagram
     * @instance
     * @param {d3.scale} [scale]
     * @return {d3.scale}
     * @return {dc_graph.diagram}

     **/
    _diagram.y = property(null);

    /**
     * Standard dc.js
     * {@link https://github.com/dc-js/dc.js/blob/develop/web/docs/api-latest.md#dc.baseMixin baseMixin}
     * method. Causes all charts in the chart group to be redrawn.
     * @method redrawGroup
     * @memberof dc_graph.diagram
     * @instance
     * @return {dc_graph.diagram}
     **/
    _diagram.redrawGroup = function () {
        dc.redrawAll(_chartGroup);
    };

    /**
     * Standard dc.js
     * {@link https://github.com/dc-js/dc.js/blob/develop/web/docs/api-latest.md#dc.baseMixin baseMixin}
     * method. Causes all charts in the chart group to be rendered.
     * @method renderGroup
     * @memberof dc_graph.diagram
     * @instance
     * @return {dc_graph.diagram}
     **/
    _diagram.renderGroup = function () {
        dc.renderAll(_chartGroup);
    };

    /**
     * Creates an svg marker definition for drawing edge arrow tails or heads.
     *
     * Sorry, this is not currently documented - please see
     * [arrows.js](https://github.com/dc-js/dc.graph.js/blob/develop/src/arrows.js)
     * for examples
     * @return {dc_graph.diagram}
     **/
    _diagram.defineArrow = function(name, defn) {
        if(typeof defn !== 'function')
            throw new Error('sorry, defineArrow no longer takes specific shape parameters, and the parameters have changed too much to convert them. it takes a name and a function returning a definition - please look at arrows.js for new format');
        _arrows[name] = defn;
        return _diagram;
    };

    // hmm
    _diagram.arrows = function() {
        return _arrows;
    };

    Object.keys(dc_graph.builtin_arrows).forEach(function(aname) {
        var defn = dc_graph.builtin_arrows[aname];
        _diagram.defineArrow(aname, defn);
    });

    function margined_bounds() {
        var bounds = _bounds || {left: 0, top: 0, right: 0, bottom: 0};
        var scale = _diagram.renderer().scale();
        return {
            left: bounds.left - _diagram.margins().left/scale,
            top: bounds.top - _diagram.margins().top/scale,
            right: bounds.right + _diagram.margins().right/scale,
            bottom: bounds.bottom + _diagram.margins().bottom/scale
        };
    }

    // with thanks to comments in https://github.com/d3/d3/issues/1084
    function align_left(translate, x) {
        return translate[0] - _diagram.x()(x) + _diagram.x().range()[0];
    }
    function align_top(translate, y) {
        return translate[1] - _diagram.y()(y) + _diagram.y().range()[0];
    }
    function align_right(translate, x) {
        return translate[0] - _diagram.x()(x) + _diagram.x().range()[1];
    }
    function align_bottom(translate, y) {
        return translate[1] - _diagram.y()(y) + _diagram.y().range()[1];;
    }
    function center_horizontally(translate, bounds) {
        return (align_left(translate, bounds.left) + align_right(translate, bounds.right))/2;
    }
    function center_vertically(translate, bounds) {
        return (align_top(translate, bounds.top) + align_bottom(translate, bounds.bottom))/2;
    }

    function bring_in_bounds(translate) {
        var xDomain = _diagram.x().domain(), yDomain = _diagram.y().domain();
        var bounds = margined_bounds();
        var less1 = bounds.left < xDomain[0], less2 = bounds.right < xDomain[1],
            lessExt = (bounds.right - bounds.left) < (xDomain[1] - xDomain[0]);
        var align, nothing = 0;
        if(less1 && less2)
            if(lessExt)
                align = 'left';
        else
            align = 'right';
        else if(!less1 && !less2)
            if(lessExt)
                align = 'right';
        else
            align = 'left';
        switch(align) {
        case 'left':
            translate[0] = align_left(translate, bounds.left);
            break;
        case 'right':
            translate[0] = align_right(translate, bounds.right);
            break;
        default:
            ++nothing;
        }
        less1 = bounds.top < yDomain[0]; less2 = bounds.bottom < yDomain[1];
        lessExt = (bounds.bottom - bounds.top) < (yDomain[1] - yDomain[0]);
        if(less1 && less2)
            if(lessExt)
                align = 'top';
        else
            align = 'bottom';
        else if(!less1 && !less2)
            if(lessExt)
                align = 'bottom';
        else
            align = 'top';
        switch(align) {
        case 'top':
            translate[1] = align_top(translate, bounds.top);
            break;
        case 'bottom':
            translate[1] = align_bottom(translate, bounds.bottom);
            break;
        default:
            ++nothing;
        }
        return translate;

    }

    _diagram.doZoom = function() {
        if(_diagram.width_is_automatic() || _diagram.height_is_automatic())
            detect_size_change();
        var translate, scale = d3.event.scale;
        if(_diagram.restrictPan())
            _diagram.renderer().translate(translate = bring_in_bounds(d3.event.translate));
        else translate = d3.event.translate;
        _diagram.renderer().globalTransform(translate, scale, _animateZoom);
        _dispatch.zoomed(translate, scale, _diagram.x().domain(), _diagram.y().domain());
    };

    _diagram.invertCoord = function(clientCoord) {
        return [
            _diagram.x().invert(clientCoord[0]),
            _diagram.y().invert(clientCoord[1])
        ];
    };

    /**
     * Set the root SVGElement to either be any valid [d3 single
     * selector](https://github.com/mbostock/d3/wiki/Selections#selecting-elements) specifying a dom
     * block element such as a div; or a dom element or d3 selection. This class is called
     * internally on diagram initialization, but be called again to relocate the diagram. However, it
     * will orphan any previously created SVGElements.
     * @method anchor
     * @memberof dc_graph.diagram
     * @instance
     * @param {anchorSelector|anchorNode|d3.selection} [parent]
     * @param {String} [chartGroup]
     * @return {String|node|d3.selection}
     * @return {dc_graph.diagram}
     */
    _diagram.anchor = function(parent, chartGroup) {
        if (!arguments.length) {
            return _anchor;
        }
        if (parent) {
            if (parent.select && parent.classed) { // detect d3 selection
                _anchor = parent.node();
            } else {
                _anchor = parent;
            }
            _diagram.root(d3.select(_anchor));
            _diagram.root().classed(dc_graph.constants.CHART_CLASS, true);
            dc.registerChart(_diagram, chartGroup);
        } else {
            throw new dc.errors.BadArgumentException('parent must be defined');
        }
        _chartGroup = chartGroup;
        return _diagram;
    };

    /**
     * Returns the internal numeric ID of the chart.
     * @method chartID
     * @memberof dc.baseMixin
     * @instance
     * @returns {String}
     */
    _diagram.chartID = function () {
        return _diagram.__dcFlag__;
    };

    /**
     * Returns the DOM id for the chart's anchored location.
     * @method anchorName
     * @memberof dc_graph.diagram
     * @instance
     * @return {String}
     */
    _diagram.anchorName = function () {
        var a = _diagram.anchor();
        if (a && a.id) {
            return a.id;
        }
        if (a && a.replace) {
            return a.replace('#', '');
        }
        return 'dc-graph' + _diagram.chartID();
    };

    return _diagram.anchor(parent, chartGroup);
};