UNPKG

d3-graphviz

Version:

Graphviz DOT rendering and animated transitions for D3

398 lines (377 loc) 16.1 kB
import * as d3 from "d3-selection"; import {transition} from "d3-transition"; import {timeout} from "d3-timer"; import {interpolateTransformSvg} from "d3-interpolate"; import {zoomTransform} from "d3-zoom"; import {createElement, extractElementData, replaceElement} from "./element"; import {shallowCopyObject} from "./utils"; import {createZoomBehavior, getTranslatedZoomTransform, translateZoomBehaviorTransform} from "./zoom"; import {pathTween} from "./tweening"; import {isEdgeElement} from "./data"; import {isEdgeElementParent} from "./data"; export default function(callback) { if (this._busy) { this._queue.push(this.render.bind(this, callback)); return this; } this._dispatch.call('renderStart', this); if (this._transitionFactory) { timeout(function () { // Decouple from time spent. See https://github.com/d3/d3-timer/issues/27 this._transition = transition(this._transitionFactory()); _render.call(this, callback); }.bind(this), 0); } else { _render.call(this, callback); } return this; } function _render(callback) { var transitionInstance = this._transition; var fade = this._options.fade && transitionInstance != null; var tweenPaths = this._options.tweenPaths; var tweenShapes = this._options.tweenShapes; var convertEqualSidedPolygons = this._options.convertEqualSidedPolygons; var tweenPrecision = this._options.tweenPrecision; var growEnteringEdges = this._options.growEnteringEdges && transitionInstance != null; var attributer = this._attributer; var graphvizInstance = this; function insertChildren(element) { var children = element.selectAll(function () { return element.node().childNodes; }); children = children .data(function (d) { return d.children; }, function (d) { return d.key; }); var childrenEnter = children .enter() .append(function(d) { var element = createElement(d); if (d.tag == '#text' && fade) { element.nodeValue = d.text; } return element; }); if (fade || (growEnteringEdges && isEdgeElementParent(element.datum()))) { var childElementsEnter = childrenEnter .filter(function(d) { return d.tag[0] == '#' ? null : this; }) .each(function (d) { var childEnter = d3.select(this); for (var attributeName of Object.keys(d.attributes)) { var attributeValue = d.attributes[attributeName]; childEnter .attr(attributeName, attributeValue); } }); childElementsEnter .filter(function(d) { return d.tag == 'svg' || d.tag == 'g' ? null : this; }) .style("opacity", 0.0); } var childrenExit = children .exit(); if (attributer) { childrenExit.each(attributer); } if (transitionInstance) { childrenExit = childrenExit .transition(transitionInstance); if (fade) { childrenExit .filter(function(d) { return d.tag[0] == '#' ? null : this; }) .style("opacity", 0.0); } } childrenExit = childrenExit .remove() children = childrenEnter .merge(children); children.each(attributeElement); } function attributeElement(data) { var element = d3.select(this); if (data.tag == "svg") { var options = graphvizInstance._options; if (options.width != null || options.height != null) { var width = options.width; var height = options.height; if (width == null) { width = data.attributes.width.replace('pt', '') * 4 / 3; } else { element .attr("width", width); data.attributes.width = width; } if (height == null) { height = data.attributes.height.replace('pt', '') * 4 / 3; } else { element .attr("height", height); data.attributes.height = height; } if (!options.fit) { element .attr("viewBox", `0 0 ${width * 3 / 4 / options.scale} ${height * 3 / 4 / options.scale}`); data.attributes.viewBox = `0 0 ${width * 3 / 4 / options.scale} ${height * 3 / 4 / options.scale}`; } } if (options.scale != 1 && (options.fit || (options.width == null && options.height == null))) { width = data.attributes.viewBox.split(' ')[2]; height = data.attributes.viewBox.split(' ')[3]; element .attr("viewBox", `0 0 ${width / options.scale} ${height / options.scale}`); data.attributes.viewBox = `0 0 ${width / options.scale} ${height / options.scale}`; } } if (attributer) { element.each(attributer); } var tag = data.tag; var attributes = data.attributes; var currentAttributes = element.node().attributes; if (currentAttributes) { for (var i = 0; i < currentAttributes.length; i++) { var currentAttribute = currentAttributes[i]; var name = currentAttribute.name; if (name.split(':')[0] != 'xmlns' && currentAttribute.namespaceURI) { var namespaceURIParts = currentAttribute.namespaceURI.split('/'); var namespace = namespaceURIParts[namespaceURIParts.length - 1]; name = namespace + ':' + name; } if (!(name in attributes)) { attributes[name] = null; } } } var convertShape = false; var convertPrevShape = false; if (tweenShapes && transitionInstance) { if ((this.nodeName == 'polygon' || this.nodeName == 'ellipse') && data.alternativeOld) { convertPrevShape = true; } if ((tag == 'polygon' || tag == 'ellipse') && data.alternativeNew) { convertShape = true; } if (this.nodeName == 'polygon' && tag == 'polygon') { var prevData = extractElementData(element); var prevPoints = prevData.attributes.points; if (!convertEqualSidedPolygons) { var nPrevPoints = prevPoints.split(' ').length; var points = data.attributes.points; var nPoints = points.split(' ').length; if (nPoints == nPrevPoints) { convertShape = false; convertPrevShape = false; } } } if (convertPrevShape) { var prevPathData = data.alternativeOld; var pathElement = replaceElement(element, prevPathData); pathElement.data([data], function () { return data.key; }); element = pathElement; } if (convertShape) { var newPathData = data.alternativeNew; tag = 'path'; attributes = newPathData.attributes; } } var elementTransition = element; if (transitionInstance) { elementTransition = elementTransition .transition(transitionInstance); if (fade) { elementTransition .filter(function(d) { return d.tag[0] == '#' ? null : this; }) .style("opacity", 1.0); } elementTransition .filter(function(d) { return d.tag[0] == '#' ? null : this; }) .on("end", function() { d3.select(this) .attr('style', null); }); } var growThisPath = growEnteringEdges && tag == 'path' && data.offset; if (growThisPath) { var totalLength = data.totalLength; element .attr("stroke-dasharray", totalLength + " " + totalLength) .attr("stroke-dashoffset", totalLength) .attr('transform', 'translate(' + data.offset.x + ',' + data.offset.y + ')'); attributes["stroke-dashoffset"] = 0; attributes['transform'] = 'translate(0,0)'; elementTransition .attr("stroke-dashoffset", attributes["stroke-dashoffset"]) .attr('transform', attributes['transform']) .on("start", function() { d3.select(this) .style('opacity', null); }) .on("end", function() { d3.select(this) .attr('stroke-dashoffset', null) .attr('stroke-dasharray', null) .attr('transform', null); }); } var moveThisPolygon = growEnteringEdges && tag == 'polygon' && isEdgeElement(data) && data.offset; if (moveThisPolygon) { var edgePath = d3.select(element.node().parentNode.querySelector("path")); var p0 = edgePath.node().getPointAtLength(0); var p1 = edgePath.node().getPointAtLength(data.totalLength); var p2 = edgePath.node().getPointAtLength(data.totalLength - 1); var angle1 = Math.atan2(p1.y - p2.y, p1.x - p2.x) * 180 / Math.PI; var x = p0.x - p1.x + data.offset.x; var y = p0.y - p1.y + data.offset.y; element .attr('transform', 'translate(' + x + ',' + y + ')'); elementTransition .attrTween("transform", function () { return function (t) { var p = edgePath.node().getPointAtLength(data.totalLength * t); var p2 = edgePath.node().getPointAtLength(data.totalLength * t + 1); var angle = Math.atan2(p2.y - p.y, p2.x - p.x) * 180 / Math.PI - angle1; x = p.x - p1.x + data.offset.x * (1 - t); y = p.y - p1.y + data.offset.y * (1 - t); return 'translate(' + x + ',' + y + ') rotate(' + angle + ' ' + p1.x + ' ' + p1.y + ')'; } }) .on("start", function() { d3.select(this) .style('opacity', null); }) .on("end", function() { d3.select(this).attr('transform', null); }); } var tweenThisPath = tweenPaths && transitionInstance && tag == 'path' && element.attr('d') != null; for (var attributeName of Object.keys(attributes)) { var attributeValue = attributes[attributeName]; if (tweenThisPath && attributeName == 'd') { var points = (data.alternativeOld || data).points; if (points) { elementTransition .attrTween("d", pathTween(points, attributeValue)); } } else { if (attributeName == 'transform' && data.translation) { var onEnd = elementTransition.on("end"); elementTransition .on("start", function () { if (graphvizInstance._zoomBehavior) { // Update the transform to transition to, just before the transition starts // in order to catch changes between the transition scheduling to its start. elementTransition .tween("attr.transform", function() { var node = this; return function(t) { node.setAttribute("transform", interpolateTransformSvg(zoomTransform(graphvizInstance._zoomSelection.node()).toString(), getTranslatedZoomTransform.call(graphvizInstance, element).toString())(t)); }; }); } }) .on("end", function () { onEnd.call(this); // Update the zoom transform to the new translated transform if (graphvizInstance._zoomBehavior) { translateZoomBehaviorTransform.call(graphvizInstance, element); } }) } elementTransition .attr(attributeName, attributeValue); } } if (convertShape) { elementTransition .on("end", function (d, i, nodes) { pathElement = d3.select(this); var newElement = replaceElement(pathElement, d); newElement.data([d], function () { return d.key; }); }) } if (data.text) { elementTransition .text(data.text); } insertChildren(element); } var root = this._selection; if (transitionInstance != null) { // Ensure original SVG shape elements are restored after transition before rendering new graph var jobs = this._jobs; if (graphvizInstance._active) { jobs.push(null); return this; } else { root .transition(transitionInstance) .transition() .duration(0) .on("end" , function () { graphvizInstance._active = false; if (jobs.length != 0) { jobs.shift(); graphvizInstance.render(); } }); this._active = true; } } if (transitionInstance != null) { root .transition(transitionInstance) .on("start" , function () { graphvizInstance._dispatch.call('transitionStart', graphvizInstance); }) .on("end" , function () { graphvizInstance._dispatch.call('transitionEnd', graphvizInstance); }) .transition() .duration(0) .on("start" , function () { graphvizInstance._dispatch.call('restoreEnd', graphvizInstance); graphvizInstance._dispatch.call('end', graphvizInstance); if (callback) { callback.call(graphvizInstance); } }); } var data = this._data; var svg = root .selectAll("svg") .data([data], function (d) {return d.key}); svg = svg .enter() .append("svg") .merge(svg); attributeElement.call(svg.node(), data); if (this._options.zoom && !this._zoomBehavior) { createZoomBehavior.call(this); } graphvizInstance._dispatch.call('renderEnd', graphvizInstance); if (transitionInstance == null) { this._dispatch.call('end', this); if (callback) { callback.call(this); } } return this; };