d3-graphviz
Version:
Graphviz DOT rendering and animated transitions for D3
367 lines (340 loc) • 14 kB
JavaScript
import { Graphviz } from "@hpcc-js/wasm/graphviz";
import * as d3 from "d3-selection";
import {extractAllElementsData, extractElementData, createElementWithAttributes} from "./element.js";
import {convertToPathData} from "./svg.js";
import {pathTweenPoints} from "./tweening.js";
import {isEdgeElement} from "./data.js";
import {getEdgeTitle} from "./data.js";
export function initViz() {
// force JIT compilation of @hpcc-js/wasm
try {
Graphviz.load().then(graphviz => {
graphviz.layout("", "svg", "dot");
this.layoutSync = graphviz.layout.bind(graphviz);
if (this._worker == null) {
this._dispatch.call("initEnd", this);
}
if (this._afterInit) {
this._afterInit();
}
});
// after the port to ESM modules, we don't know how to trigger this so
// we just disable it from coverage
/* c8 ignore start */
} catch(error) {
// we end up here when the the script tag type used to load
// the "@hpcc-js/wasm" script is not "application/javascript"
// or "text/javascript", but typically "javascript/worker". In
// this case the browser does not load the script since it's
// unnecessary because it's loaded by the web worker
// instead. This is expected so we just ignore the error.
}
/* c8 ignore stop */
if (this._worker != null) {
var vizURL = this._vizURL;
var graphvizInstance = this;
this._workerPort.onmessage = function(event) {
var callback = graphvizInstance._workerCallbacks.shift();
callback.call(graphvizInstance, event);
}
if (!vizURL.match(/^https?:\/\/|^\/\//i)) {
// Local URL. Prepend with local domain to be usable in web worker
vizURL = (new window.URL(vizURL, document.location.href)).href;
}
postMessage.call(this, {type: "layout", dot: "", engine: 'dot', vizURL: vizURL}, function (event) {
switch (event.data.type) {
case "init":
break;
}
});
postMessage.call(this, { type: "version" }, function (event) {
switch (event.data.type) {
case "version":
graphvizInstance._graphvizVersion = event.data.version;
graphvizInstance._dispatch.call("initEnd", this);
break;
}
});
}
}
function postMessage(message, callback) {
this._workerCallbacks.push(callback);
this._workerPort.postMessage(message);
}
export function layout(src, engine, vizOptions, callback) {
if (this._worker) {
postMessage.call(this, {
type: "layout",
dot: src,
engine: engine,
options: vizOptions,
}, function (event) {
callback.call(this, event.data);
});
} else {
try {
var svgDoc = this.layoutSync(src, "svg", engine, vizOptions);
callback.call(this, {type: 'done', svg: svgDoc});
}
catch(error) {
callback.call(this, {type: 'error', error: error.message});
}
}
}
export default function(src, callback) {
var graphvizInstance = this;
var worker = this._worker;
var engine = this._options.engine;
var images = this._images;
this._dispatch.call("start", this);
this._busy = true;
this._dispatch.call("layoutStart", this);
var vizOptions = {
images: images,
};
if (!this._worker && this.layoutSync == null) {
this._afterInit = this.dot.bind(this, src, callback);
return this;
}
this.layout(src, engine, vizOptions, function (data) {
switch (data.type) {
case "error":
if (graphvizInstance._onerror) {
graphvizInstance._onerror(data.error);
} else {
throw data.error.message
}
break;
case "done":
var svgDoc = data.svg;
layoutDone.call(this, svgDoc, callback);
break;
}
});
return this;
};
function layoutDone(svgDoc, callback) {
var keyMode = this._options.keyMode;
var tweenPaths = this._options.tweenPaths;
var tweenShapes = this._options.tweenShapes;
if (typeof this._options.tweenPrecision == 'string' && this._options.tweenPrecision.includes('%')) {
var tweenPrecision = +this._options.tweenPrecision.split('%')[0] / 100;
var tweenPrecisionIsRelative = this._options.tweenPrecision.includes('%');
} else {
var tweenPrecision = this._options.tweenPrecision;
var tweenPrecisionIsRelative = false;
}
var growEnteringEdges = this._options.growEnteringEdges;
var dictionary = {};
var prevDictionary = this._dictionary || {};
var nodeDictionary = {};
var prevNodeDictionary = this._nodeDictionary || {};
function setKey(datum, index) {
var tag = datum.tag;
if (keyMode == 'index') {
datum.key = index;
} else if (tag[0] != '#') {
if (keyMode == 'id') {
datum.key = datum.attributes.id;
} else if (keyMode == 'title') {
var title = datum.children.find(function (childData) {
return childData.tag == 'title';
});
if (title) {
if (title.children.length > 0) {
datum.key = title.children[0].text;
} else {
datum.key = '';
}
}
}
}
if (datum.key == null) {
if (tweenShapes) {
if (tag == 'ellipse' || tag == 'polygon') {
tag = 'path';
}
}
datum.key = tag + '-' + index;
}
}
function setId(datum, parentData) {
var id = (parentData ? parentData.id + '.' : '') + datum.key;
datum.id = id;
}
function addToDictionary(datum) {
dictionary[datum.id] = datum;
}
function calculateAlternativeShapeData(datum, prevDatum) {
if (tweenShapes && datum.id in prevDictionary) {
if ((prevDatum.tag == 'polygon' || prevDatum.tag == 'ellipse' || prevDatum.tag == 'path') && (prevDatum.tag != datum.tag || datum.tag == 'polygon')) {
if (prevDatum.tag != 'path') {
datum.alternativeOld = convertToPathData(prevDatum, datum);
}
if (datum.tag != 'path') {
datum.alternativeNew = convertToPathData(datum, prevDatum);
}
}
}
}
function calculatePathTweenPoints(datum, prevDatum) {
if (tweenPaths && prevDatum && (prevDatum.tag == 'path' || (datum.alternativeOld && datum.alternativeOld.tag == 'path'))) {
var attribute_d = (datum.alternativeNew || datum).attributes.d;
if (datum.alternativeOld) {
var oldNode = createElementWithAttributes(datum.alternativeOld);
} else {
var oldNode = createElementWithAttributes(prevDatum);
}
(datum.alternativeOld || (datum.alternativeOld = {})).points = pathTweenPoints(oldNode, attribute_d, tweenPrecision, tweenPrecisionIsRelative);
}
}
function postProcessDataPass1Local(datum, index=0, parentData) {
setKey(datum, index);
setId(datum, parentData);
var id = datum.id;
var prevDatum = prevDictionary[id];
addToDictionary(datum);
calculateAlternativeShapeData(datum, prevDatum);
calculatePathTweenPoints(datum, prevDatum);
var childTagIndexes = {};
datum.children.forEach(function (childData) {
var childTag = childData.tag;
if (childTag == 'ellipse' || childTag == 'polygon') {
childTag = 'path';
}
if (childTagIndexes[childTag] == null) {
childTagIndexes[childTag] = 0;
}
var childIndex = childTagIndexes[childTag]++;
postProcessDataPass1Local(childData, childIndex, datum);
});
}
function addToNodeDictionary(datum) {
var tag = datum.tag;
if (growEnteringEdges && datum.parent) {
if (datum.parent.attributes.class == 'node') {
if (tag == 'title') {
if (datum.children.length > 0) {
var child = datum.children[0];
var nodeId = child.text;
} else {
var nodeId = '';
}
nodeDictionary[nodeId] = datum.parent;
}
}
}
}
function extractGrowingEdgesData(datum) {
var id = datum.id;
var tag = datum.tag;
var prevDatum = prevDictionary[id];
if (growEnteringEdges && !prevDatum && datum.parent) {
if (isEdgeElement(datum)) {
if (tag == 'path' || tag == 'polygon') {
if (tag == 'polygon') {
var path = datum.parent.children.find(function (e) {
return e.tag == 'path';
});
if (path) {
datum.totalLength = path.totalLength;
}
}
var title = getEdgeTitle(datum);
var child = title.children[0];
var nodeIds = child.text.split('->');
if (nodeIds.length != 2) {
nodeIds = child.text.split('--');
}
var startNodeId = nodeIds[0];
var startNode = nodeDictionary[startNodeId];
if (Object.hasOwn(prevNodeDictionary, startNodeId)) {
var prevStartNode = prevNodeDictionary[startNodeId];
var i = startNode.children.findIndex(function (element, index) {
return element.tag == 'g';
});
if (i >= 0) {
var j = startNode.children[i].children.findIndex(function (element, index) {
return element.tag == 'a';
});
startNode = startNode.children[i].children[j];
}
var i = prevStartNode.children.findIndex(function (element, index) {
return element.tag == 'g';
});
if (i >= 0) {
var j = prevStartNode.children[i].children.findIndex(function (element, index) {
return element.tag == 'a';
});
prevStartNode = prevStartNode.children[i].children[j];
}
var startShapes = startNode.children;
for (var i = 0; i < startShapes.length; i++) {
if (startShapes[i].tag == 'polygon' || startShapes[i].tag == 'ellipse' || startShapes[i].tag == 'path' || startShapes[i].tag == 'text') {
var startShape = startShapes[i];
break;
}
}
var prevStartShapes = prevStartNode.children;
for (var i = 0; i < prevStartShapes.length; i++) {
if (prevStartShapes[i].tag == 'polygon' || prevStartShapes[i].tag == 'ellipse' || prevStartShapes[i].tag == 'path' || prevStartShapes[i].tag == 'text') {
var prevStartShape = prevStartShapes[i];
break;
}
}
if (prevStartShape && startShape) {
datum.offset = {
x: prevStartShape.center.x - startShape.center.x,
y: prevStartShape.center.y - startShape.center.y,
}
} else {
datum.offset = {x: 0, y: 0};
}
}
}
}
}
}
function postProcessDataPass2Global(datum) {
addToNodeDictionary(datum);
extractGrowingEdgesData(datum);
datum.children.forEach(function (childData) {
postProcessDataPass2Global(childData);
});
}
this._dispatch.call("layoutEnd", this);
var newDoc = d3.select(document.createDocumentFragment())
.append('div');
var parser = new window.DOMParser();
var doc = parser.parseFromString(svgDoc, "image/svg+xml");
newDoc
.append(function() {
return doc.documentElement;
});
var newSvg = newDoc
.select('svg');
var data = extractAllElementsData(newSvg);
this._dispatch.call('dataExtractEnd', this);
postProcessDataPass1Local(data);
this._dispatch.call('dataProcessPass1End', this);
postProcessDataPass2Global(data);
this._dispatch.call('dataProcessPass2End', this);
this._data = data;
this._dictionary = dictionary;
this._nodeDictionary = nodeDictionary;
this._extractData = function (element, childIndex, parentData) {
var data = extractAllElementsData(element);
postProcessDataPass1Local(data, childIndex, parentData);
postProcessDataPass2Global(data);
return data;
}
this._busy = false;
this._dispatch.call('dataProcessEnd', this);
if (callback) {
callback.call(this);
}
if (this._queue.length > 0) {
var job = this._queue.shift();
job.call(this);
}
}