vis-network
Version:
A dynamic, browser-based visualization library.
1,726 lines (1,592 loc) • 763 kB
JavaScript
/**
* vis-network
* https://visjs.github.io/vis-network/
*
* A dynamic, browser-based visualization library.
*
* @version 9.1.9
* @date 2023-11-03T01:42:27.418Z
*
* @copyright (c) 2011-2017 Almende B.V, http://almende.com
* @copyright (c) 2017-2019 visjs contributors, https://github.com/visjs
*
* @license
* vis.js is dual licensed under both
*
* 1. The Apache 2.0 License
* http://www.apache.org/licenses/LICENSE-2.0
*
* and
*
* 2. The MIT License
* http://opensource.org/licenses/MIT
*
* vis.js may be distributed under either license.
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('component-emitter'), require('vis-util/esnext/umd/vis-util.js'), require('vis-data/esnext/umd/vis-data.js'), require('uuid'), require('keycharm')) :
typeof define === 'function' && define.amd ? define(['exports', 'component-emitter', 'vis-util/esnext/umd/vis-util.js', 'vis-data/esnext/umd/vis-data.js', 'uuid', 'keycharm'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.vis = global.vis || {}, global.Emitter, global.vis, global.vis, global.uuid, global.keycharm));
})(this, (function (exports, Emitter, esnext, esnext$1, uuid, keycharm) {
/**
* Draw a circle.
*
* @param ctx - The context this shape will be rendered to.
* @param x - The position of the center on the x axis.
* @param y - The position of the center on the y axis.
* @param r - The radius of the circle.
*/
function drawCircle(ctx, x, y, r) {
ctx.beginPath();
ctx.arc(x, y, r, 0, 2 * Math.PI, false);
ctx.closePath();
}
/**
* Draw a square.
*
* @param ctx - The context this shape will be rendered to.
* @param x - The position of the center on the x axis.
* @param y - The position of the center on the y axis.
* @param r - Half of the width and height of the square.
*/
function drawSquare(ctx, x, y, r) {
ctx.beginPath();
ctx.rect(x - r, y - r, r * 2, r * 2);
ctx.closePath();
}
/**
* Draw an equilateral triangle standing on a side.
*
* @param ctx - The context this shape will be rendered to.
* @param x - The position of the center on the x axis.
* @param y - The position of the center on the y axis.
* @param r - Half of the length of the sides.
* @remarks
* http://en.wikipedia.org/wiki/Equilateral_triangle
*/
function drawTriangle(ctx, x, y, r) {
ctx.beginPath();
// the change in radius and the offset is here to center the shape
r *= 1.15;
y += 0.275 * r;
const s = r * 2;
const s2 = s / 2;
const ir = (Math.sqrt(3) / 6) * s; // radius of inner circle
const h = Math.sqrt(s * s - s2 * s2); // height
ctx.moveTo(x, y - (h - ir));
ctx.lineTo(x + s2, y + ir);
ctx.lineTo(x - s2, y + ir);
ctx.lineTo(x, y - (h - ir));
ctx.closePath();
}
/**
* Draw an equilateral triangle standing on a vertex.
*
* @param ctx - The context this shape will be rendered to.
* @param x - The position of the center on the x axis.
* @param y - The position of the center on the y axis.
* @param r - Half of the length of the sides.
* @remarks
* http://en.wikipedia.org/wiki/Equilateral_triangle
*/
function drawTriangleDown(ctx, x, y, r) {
ctx.beginPath();
// the change in radius and the offset is here to center the shape
r *= 1.15;
y -= 0.275 * r;
const s = r * 2;
const s2 = s / 2;
const ir = (Math.sqrt(3) / 6) * s; // radius of inner circle
const h = Math.sqrt(s * s - s2 * s2); // height
ctx.moveTo(x, y + (h - ir));
ctx.lineTo(x + s2, y - ir);
ctx.lineTo(x - s2, y - ir);
ctx.lineTo(x, y + (h - ir));
ctx.closePath();
}
/**
* Draw a star.
*
* @param ctx - The context this shape will be rendered to.
* @param x - The position of the center on the x axis.
* @param y - The position of the center on the y axis.
* @param r - The outer radius of the star.
*/
function drawStar(ctx, x, y, r) {
// http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/
ctx.beginPath();
// the change in radius and the offset is here to center the shape
r *= 0.82;
y += 0.1 * r;
for (let n = 0; n < 10; n++) {
const radius = n % 2 === 0 ? r * 1.3 : r * 0.5;
ctx.lineTo(x + radius * Math.sin((n * 2 * Math.PI) / 10), y - radius * Math.cos((n * 2 * Math.PI) / 10));
}
ctx.closePath();
}
/**
* Draw a diamond.
*
* @param ctx - The context this shape will be rendered to.
* @param x - The position of the center on the x axis.
* @param y - The position of the center on the y axis.
* @param r - Half of the width and height of the diamond.
* @remarks
* http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/
*/
function drawDiamond(ctx, x, y, r) {
ctx.beginPath();
ctx.lineTo(x, y + r);
ctx.lineTo(x + r, y);
ctx.lineTo(x, y - r);
ctx.lineTo(x - r, y);
ctx.closePath();
}
/**
* Draw a rectangle with rounded corners.
*
* @param ctx - The context this shape will be rendered to.
* @param x - The position of the center on the x axis.
* @param y - The position of the center on the y axis.
* @param w - The width of the rectangle.
* @param h - The height of the rectangle.
* @param r - The radius of the corners.
* @remarks
* http://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-on-html-canvas
*/
function drawRoundRect(ctx, x, y, w, h, r) {
const r2d = Math.PI / 180;
if (w - 2 * r < 0) {
r = w / 2;
} //ensure that the radius isn't too large for x
if (h - 2 * r < 0) {
r = h / 2;
} //ensure that the radius isn't too large for y
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.arc(x + w - r, y + r, r, r2d * 270, r2d * 360, false);
ctx.lineTo(x + w, y + h - r);
ctx.arc(x + w - r, y + h - r, r, 0, r2d * 90, false);
ctx.lineTo(x + r, y + h);
ctx.arc(x + r, y + h - r, r, r2d * 90, r2d * 180, false);
ctx.lineTo(x, y + r);
ctx.arc(x + r, y + r, r, r2d * 180, r2d * 270, false);
ctx.closePath();
}
/**
* Draw an ellipse.
*
* @param ctx - The context this shape will be rendered to.
* @param x - The position of the center on the x axis.
* @param y - The position of the center on the y axis.
* @param w - The width of the ellipse.
* @param h - The height of the ellipse.
* @remarks
* http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
*
* Postfix '_vis' added to discern it from standard method ellipse().
*/
function drawEllipse(ctx, x, y, w, h) {
const kappa = 0.5522848, ox = (w / 2) * kappa, // control point offset horizontal
oy = (h / 2) * kappa, // control point offset vertical
xe = x + w, // x-end
ye = y + h, // y-end
xm = x + w / 2, // x-middle
ym = y + h / 2; // y-middle
ctx.beginPath();
ctx.moveTo(x, ym);
ctx.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
ctx.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
ctx.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
ctx.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
ctx.closePath();
}
/**
* Draw an isometric cylinder.
*
* @param ctx - The context this shape will be rendered to.
* @param x - The position of the center on the x axis.
* @param y - The position of the center on the y axis.
* @param w - The width of the database.
* @param h - The height of the database.
* @remarks
* http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
*/
function drawDatabase(ctx, x, y, w, h) {
const f = 1 / 3;
const wEllipse = w;
const hEllipse = h * f;
const kappa = 0.5522848, ox = (wEllipse / 2) * kappa, // control point offset horizontal
oy = (hEllipse / 2) * kappa, // control point offset vertical
xe = x + wEllipse, // x-end
ye = y + hEllipse, // y-end
xm = x + wEllipse / 2, // x-middle
ym = y + hEllipse / 2, // y-middle
ymb = y + (h - hEllipse / 2), // y-midlle, bottom ellipse
yeb = y + h; // y-end, bottom ellipse
ctx.beginPath();
ctx.moveTo(xe, ym);
ctx.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
ctx.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
ctx.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
ctx.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
ctx.lineTo(xe, ymb);
ctx.bezierCurveTo(xe, ymb + oy, xm + ox, yeb, xm, yeb);
ctx.bezierCurveTo(xm - ox, yeb, x, ymb + oy, x, ymb);
ctx.lineTo(x, ym);
}
/**
* Draw a dashed line.
*
* @param ctx - The context this shape will be rendered to.
* @param x - The start position on the x axis.
* @param y - The start position on the y axis.
* @param x2 - The end position on the x axis.
* @param y2 - The end position on the y axis.
* @param pattern - List of lengths starting with line and then alternating between space and line.
* @author David Jordan
* @remarks
* date 2012-08-08
* http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas
*/
function drawDashedLine(ctx, x, y, x2, y2, pattern) {
ctx.beginPath();
ctx.moveTo(x, y);
const patternLength = pattern.length;
const dx = x2 - x;
const dy = y2 - y;
const slope = dy / dx;
let distRemaining = Math.sqrt(dx * dx + dy * dy);
let patternIndex = 0;
let draw = true;
let xStep = 0;
let dashLength = +pattern[0];
while (distRemaining >= 0.1) {
dashLength = +pattern[patternIndex++ % patternLength];
if (dashLength > distRemaining) {
dashLength = distRemaining;
}
xStep = Math.sqrt((dashLength * dashLength) / (1 + slope * slope));
xStep = dx < 0 ? -xStep : xStep;
x += xStep;
y += slope * xStep;
if (draw === true) {
ctx.lineTo(x, y);
}
else {
ctx.moveTo(x, y);
}
distRemaining -= dashLength;
draw = !draw;
}
}
/**
* Draw a hexagon.
*
* @param ctx - The context this shape will be rendered to.
* @param x - The position of the center on the x axis.
* @param y - The position of the center on the y axis.
* @param r - The radius of the hexagon.
*/
function drawHexagon(ctx, x, y, r) {
ctx.beginPath();
const sides = 6;
const a = (Math.PI * 2) / sides;
ctx.moveTo(x + r, y);
for (let i = 1; i < sides; i++) {
ctx.lineTo(x + r * Math.cos(a * i), y + r * Math.sin(a * i));
}
ctx.closePath();
}
const shapeMap = {
circle: drawCircle,
dashedLine: drawDashedLine,
database: drawDatabase,
diamond: drawDiamond,
ellipse: drawEllipse,
ellipse_vis: drawEllipse,
hexagon: drawHexagon,
roundRect: drawRoundRect,
square: drawSquare,
star: drawStar,
triangle: drawTriangle,
triangleDown: drawTriangleDown,
};
/**
* Returns either custom or native drawing function base on supplied name.
*
* @param name - The name of the function. Either the name of a
* CanvasRenderingContext2D property or an export from shapes.ts without the
* draw prefix.
* @returns The function that can be used for rendering. In case of native
* CanvasRenderingContext2D function the API is normalized to
* `(ctx: CanvasRenderingContext2D, ...originalArgs) => void`.
*/
function getShape(name) {
if (Object.prototype.hasOwnProperty.call(shapeMap, name)) {
return shapeMap[name];
}
else {
return function (ctx, ...args) {
CanvasRenderingContext2D.prototype[name].call(ctx, args);
};
}
}
/* eslint-disable no-prototype-builtins */
/* eslint-disable no-unused-vars */
/* eslint-disable no-var */
/**
* Parse a text source containing data in DOT language into a JSON object.
* The object contains two lists: one with nodes and one with edges.
*
* DOT language reference: http://www.graphviz.org/doc/info/lang.html
*
* DOT language attributes: http://graphviz.org/content/attrs
*
* @param {string} data Text containing a graph in DOT-notation
* @returns {object} graph An object containing two parameters:
* {Object[]} nodes
* {Object[]} edges
*
* -------------------------------------------
* TODO
* ====
*
* For label handling, this is an incomplete implementation. From docs (quote #3015):
*
* > the escape sequences "\n", "\l" and "\r" divide the label into lines, centered,
* > left-justified, and right-justified, respectively.
*
* Source: http://www.graphviz.org/content/attrs#kescString
*
* > As another aid for readability, dot allows double-quoted strings to span multiple physical
* > lines using the standard C convention of a backslash immediately preceding a newline
* > character
* > In addition, double-quoted strings can be concatenated using a '+' operator.
* > As HTML strings can contain newline characters, which are used solely for formatting,
* > the language does not allow escaped newlines or concatenation operators to be used
* > within them.
*
* - Currently, only '\\n' is handled
* - Note that text explicitly says 'labels'; the dot parser currently handles escape
* sequences in **all** strings.
*/
function parseDOT(data) {
dot = data;
return parseGraph();
}
// mapping of attributes from DOT (the keys) to vis.js (the values)
var NODE_ATTR_MAPPING = {
fontsize: "font.size",
fontcolor: "font.color",
labelfontcolor: "font.color",
fontname: "font.face",
color: ["color.border", "color.background"],
fillcolor: "color.background",
tooltip: "title",
labeltooltip: "title",
};
var EDGE_ATTR_MAPPING = Object.create(NODE_ATTR_MAPPING);
EDGE_ATTR_MAPPING.color = "color.color";
EDGE_ATTR_MAPPING.style = "dashes";
// token types enumeration
var TOKENTYPE = {
NULL: 0,
DELIMITER: 1,
IDENTIFIER: 2,
UNKNOWN: 3,
};
// map with all delimiters
var DELIMITERS = {
"{": true,
"}": true,
"[": true,
"]": true,
";": true,
"=": true,
",": true,
"->": true,
"--": true,
};
var dot = ""; // current dot file
var index = 0; // current index in dot file
var c = ""; // current token character in expr
var token = ""; // current token
var tokenType = TOKENTYPE.NULL; // type of the token
/**
* Get the first character from the dot file.
* The character is stored into the char c. If the end of the dot file is
* reached, the function puts an empty string in c.
*/
function first() {
index = 0;
c = dot.charAt(0);
}
/**
* Get the next character from the dot file.
* The character is stored into the char c. If the end of the dot file is
* reached, the function puts an empty string in c.
*/
function next() {
index++;
c = dot.charAt(index);
}
/**
* Preview the next character from the dot file.
*
* @returns {string} cNext
*/
function nextPreview() {
return dot.charAt(index + 1);
}
/**
* Test whether given character is alphabetic or numeric ( a-zA-Z_0-9.:# )
*
* @param {string} c
* @returns {boolean} isAlphaNumeric
*/
function isAlphaNumeric(c) {
var charCode = c.charCodeAt(0);
if (charCode < 47) {
// #.
return charCode === 35 || charCode === 46;
}
if (charCode < 59) {
// 0-9 and :
return charCode > 47;
}
if (charCode < 91) {
// A-Z
return charCode > 64;
}
if (charCode < 96) {
// _
return charCode === 95;
}
if (charCode < 123) {
// a-z
return charCode > 96;
}
return false;
}
/**
* Merge all options of object b into object b
*
* @param {object} a
* @param {object} b
* @returns {object} a
*/
function merge(a, b) {
if (!a) {
a = {};
}
if (b) {
for (var name in b) {
if (b.hasOwnProperty(name)) {
a[name] = b[name];
}
}
}
return a;
}
/**
* Set a value in an object, where the provided parameter name can be a
* path with nested parameters. For example:
*
* var obj = {a: 2};
* setValue(obj, 'b.c', 3); // obj = {a: 2, b: {c: 3}}
*
* @param {object} obj
* @param {string} path A parameter name or dot-separated parameter path,
* like "color.highlight.border".
* @param {*} value
*/
function setValue(obj, path, value) {
var keys = path.split(".");
var o = obj;
while (keys.length) {
var key = keys.shift();
if (keys.length) {
// this isn't the end point
if (!o[key]) {
o[key] = {};
}
o = o[key];
} else {
// this is the end point
o[key] = value;
}
}
}
/**
* Add a node to a graph object. If there is already a node with
* the same id, their attributes will be merged.
*
* @param {object} graph
* @param {object} node
*/
function addNode(graph, node) {
var i, len;
var current = null;
// find root graph (in case of subgraph)
var graphs = [graph]; // list with all graphs from current graph to root graph
var root = graph;
while (root.parent) {
graphs.push(root.parent);
root = root.parent;
}
// find existing node (at root level) by its id
if (root.nodes) {
for (i = 0, len = root.nodes.length; i < len; i++) {
if (node.id === root.nodes[i].id) {
current = root.nodes[i];
break;
}
}
}
if (!current) {
// this is a new node
current = {
id: node.id,
};
if (graph.node) {
// clone default attributes
current.attr = merge(current.attr, graph.node);
}
}
// add node to this (sub)graph and all its parent graphs
for (i = graphs.length - 1; i >= 0; i--) {
var g = graphs[i];
if (!g.nodes) {
g.nodes = [];
}
if (g.nodes.indexOf(current) === -1) {
g.nodes.push(current);
}
}
// merge attributes
if (node.attr) {
current.attr = merge(current.attr, node.attr);
}
}
/**
* Add an edge to a graph object
*
* @param {object} graph
* @param {object} edge
*/
function addEdge(graph, edge) {
if (!graph.edges) {
graph.edges = [];
}
graph.edges.push(edge);
if (graph.edge) {
var attr = merge({}, graph.edge); // clone default attributes
edge.attr = merge(attr, edge.attr); // merge attributes
}
}
/**
* Create an edge to a graph object
*
* @param {object} graph
* @param {string | number | object} from
* @param {string | number | object} to
* @param {string} type
* @param {object | null} attr
* @returns {object} edge
*/
function createEdge(graph, from, to, type, attr) {
var edge = {
from: from,
to: to,
type: type,
};
if (graph.edge) {
edge.attr = merge({}, graph.edge); // clone default attributes
}
edge.attr = merge(edge.attr || {}, attr); // merge attributes
// Move arrows attribute from attr to edge temporally created in
// parseAttributeList().
if (attr != null) {
if (attr.hasOwnProperty("arrows") && attr["arrows"] != null) {
edge["arrows"] = { to: { enabled: true, type: attr.arrows.type } };
attr["arrows"] = null;
}
}
return edge;
}
/**
* Get next token in the current dot file.
* The token and token type are available as token and tokenType
*/
function getToken() {
tokenType = TOKENTYPE.NULL;
token = "";
// skip over whitespaces
while (c === " " || c === "\t" || c === "\n" || c === "\r") {
// space, tab, enter
next();
}
do {
var isComment = false;
// skip comment
if (c === "#") {
// find the previous non-space character
var i = index - 1;
while (dot.charAt(i) === " " || dot.charAt(i) === "\t") {
i--;
}
if (dot.charAt(i) === "\n" || dot.charAt(i) === "") {
// the # is at the start of a line, this is indeed a line comment
while (c != "" && c != "\n") {
next();
}
isComment = true;
}
}
if (c === "/" && nextPreview() === "/") {
// skip line comment
while (c != "" && c != "\n") {
next();
}
isComment = true;
}
if (c === "/" && nextPreview() === "*") {
// skip block comment
while (c != "") {
if (c === "*" && nextPreview() === "/") {
// end of block comment found. skip these last two characters
next();
next();
break;
} else {
next();
}
}
isComment = true;
}
// skip over whitespaces
while (c === " " || c === "\t" || c === "\n" || c === "\r") {
// space, tab, enter
next();
}
} while (isComment);
// check for end of dot file
if (c === "") {
// token is still empty
tokenType = TOKENTYPE.DELIMITER;
return;
}
// check for delimiters consisting of 2 characters
var c2 = c + nextPreview();
if (DELIMITERS[c2]) {
tokenType = TOKENTYPE.DELIMITER;
token = c2;
next();
next();
return;
}
// check for delimiters consisting of 1 character
if (DELIMITERS[c]) {
tokenType = TOKENTYPE.DELIMITER;
token = c;
next();
return;
}
// check for an identifier (number or string)
// TODO: more precise parsing of numbers/strings (and the port separator ':')
if (isAlphaNumeric(c) || c === "-") {
token += c;
next();
while (isAlphaNumeric(c)) {
token += c;
next();
}
if (token === "false") {
token = false; // convert to boolean
} else if (token === "true") {
token = true; // convert to boolean
} else if (!isNaN(Number(token))) {
token = Number(token); // convert to number
}
tokenType = TOKENTYPE.IDENTIFIER;
return;
}
// check for a string enclosed by double quotes
if (c === '"') {
next();
while (c != "" && (c != '"' || (c === '"' && nextPreview() === '"'))) {
if (c === '"') {
// skip the escape character
token += c;
next();
} else if (c === "\\" && nextPreview() === "n") {
// Honor a newline escape sequence
token += "\n";
next();
} else {
token += c;
}
next();
}
if (c != '"') {
throw newSyntaxError('End of string " expected');
}
next();
tokenType = TOKENTYPE.IDENTIFIER;
return;
}
// something unknown is found, wrong characters, a syntax error
tokenType = TOKENTYPE.UNKNOWN;
while (c != "") {
token += c;
next();
}
throw new SyntaxError('Syntax error in part "' + chop(token, 30) + '"');
}
/**
* Parse a graph.
*
* @returns {object} graph
*/
function parseGraph() {
var graph = {};
first();
getToken();
// optional strict keyword
if (token === "strict") {
graph.strict = true;
getToken();
}
// graph or digraph keyword
if (token === "graph" || token === "digraph") {
graph.type = token;
getToken();
}
// optional graph id
if (tokenType === TOKENTYPE.IDENTIFIER) {
graph.id = token;
getToken();
}
// open angle bracket
if (token != "{") {
throw newSyntaxError("Angle bracket { expected");
}
getToken();
// statements
parseStatements(graph);
// close angle bracket
if (token != "}") {
throw newSyntaxError("Angle bracket } expected");
}
getToken();
// end of file
if (token !== "") {
throw newSyntaxError("End of file expected");
}
getToken();
// remove temporary default options
delete graph.node;
delete graph.edge;
delete graph.graph;
return graph;
}
/**
* Parse a list with statements.
*
* @param {object} graph
*/
function parseStatements(graph) {
while (token !== "" && token != "}") {
parseStatement(graph);
if (token === ";") {
getToken();
}
}
}
/**
* Parse a single statement. Can be a an attribute statement, node
* statement, a series of node statements and edge statements, or a
* parameter.
*
* @param {object} graph
*/
function parseStatement(graph) {
// parse subgraph
var subgraph = parseSubgraph(graph);
if (subgraph) {
// edge statements
parseEdge(graph, subgraph);
return;
}
// parse an attribute statement
var attr = parseAttributeStatement(graph);
if (attr) {
return;
}
// parse node
if (tokenType != TOKENTYPE.IDENTIFIER) {
throw newSyntaxError("Identifier expected");
}
var id = token; // id can be a string or a number
getToken();
if (token === "=") {
// id statement
getToken();
if (tokenType != TOKENTYPE.IDENTIFIER) {
throw newSyntaxError("Identifier expected");
}
graph[id] = token;
getToken();
// TODO: implement comma separated list with "a_list: ID=ID [','] [a_list] "
} else {
parseNodeStatement(graph, id);
}
}
/**
* Parse a subgraph
*
* @param {object} graph parent graph object
* @returns {object | null} subgraph
*/
function parseSubgraph(graph) {
var subgraph = null;
// optional subgraph keyword
if (token === "subgraph") {
subgraph = {};
subgraph.type = "subgraph";
getToken();
// optional graph id
if (tokenType === TOKENTYPE.IDENTIFIER) {
subgraph.id = token;
getToken();
}
}
// open angle bracket
if (token === "{") {
getToken();
if (!subgraph) {
subgraph = {};
}
subgraph.parent = graph;
subgraph.node = graph.node;
subgraph.edge = graph.edge;
subgraph.graph = graph.graph;
// statements
parseStatements(subgraph);
// close angle bracket
if (token != "}") {
throw newSyntaxError("Angle bracket } expected");
}
getToken();
// remove temporary default options
delete subgraph.node;
delete subgraph.edge;
delete subgraph.graph;
delete subgraph.parent;
// register at the parent graph
if (!graph.subgraphs) {
graph.subgraphs = [];
}
graph.subgraphs.push(subgraph);
}
return subgraph;
}
/**
* parse an attribute statement like "node [shape=circle fontSize=16]".
* Available keywords are 'node', 'edge', 'graph'.
* The previous list with default attributes will be replaced
*
* @param {object} graph
* @returns {string | null} keyword Returns the name of the parsed attribute
* (node, edge, graph), or null if nothing
* is parsed.
*/
function parseAttributeStatement(graph) {
// attribute statements
if (token === "node") {
getToken();
// node attributes
graph.node = parseAttributeList();
return "node";
} else if (token === "edge") {
getToken();
// edge attributes
graph.edge = parseAttributeList();
return "edge";
} else if (token === "graph") {
getToken();
// graph attributes
graph.graph = parseAttributeList();
return "graph";
}
return null;
}
/**
* parse a node statement
*
* @param {object} graph
* @param {string | number} id
*/
function parseNodeStatement(graph, id) {
// node statement
var node = {
id: id,
};
var attr = parseAttributeList();
if (attr) {
node.attr = attr;
}
addNode(graph, node);
// edge statements
parseEdge(graph, id);
}
/**
* Parse an edge or a series of edges
*
* @param {object} graph
* @param {string | number} from Id of the from node
*/
function parseEdge(graph, from) {
while (token === "->" || token === "--") {
var to;
var type = token;
getToken();
var subgraph = parseSubgraph(graph);
if (subgraph) {
to = subgraph;
} else {
if (tokenType != TOKENTYPE.IDENTIFIER) {
throw newSyntaxError("Identifier or subgraph expected");
}
to = token;
addNode(graph, {
id: to,
});
getToken();
}
// parse edge attributes
var attr = parseAttributeList();
// create edge
var edge = createEdge(graph, from, to, type, attr);
addEdge(graph, edge);
from = to;
}
}
/**
* Parse a set with attributes,
* for example [label="1.000", shape=solid]
*
* @returns {object | null} attr
*/
function parseAttributeList() {
var i;
var attr = null;
// edge styles of dot and vis
var edgeStyles = {
dashed: true,
solid: false,
dotted: [1, 5],
};
/**
* Define arrow types.
* vis currently supports types defined in 'arrowTypes'.
* Details of arrow shapes are described in
* http://www.graphviz.org/content/arrow-shapes
*/
var arrowTypes = {
dot: "circle",
box: "box",
crow: "crow",
curve: "curve",
icurve: "inv_curve",
normal: "triangle",
inv: "inv_triangle",
diamond: "diamond",
tee: "bar",
vee: "vee",
};
/**
* 'attr_list' contains attributes for checking if some of them are affected
* later. For instance, both of 'arrowhead' and 'dir' (edge style defined
* in DOT) make changes to 'arrows' attribute in vis.
*/
var attr_list = new Array();
var attr_names = new Array(); // used for checking the case.
// parse attributes
while (token === "[") {
getToken();
attr = {};
while (token !== "" && token != "]") {
if (tokenType != TOKENTYPE.IDENTIFIER) {
throw newSyntaxError("Attribute name expected");
}
var name = token;
getToken();
if (token != "=") {
throw newSyntaxError("Equal sign = expected");
}
getToken();
if (tokenType != TOKENTYPE.IDENTIFIER) {
throw newSyntaxError("Attribute value expected");
}
var value = token;
// convert from dot style to vis
if (name === "style") {
value = edgeStyles[value];
}
var arrowType;
if (name === "arrowhead") {
arrowType = arrowTypes[value];
name = "arrows";
value = { to: { enabled: true, type: arrowType } };
}
if (name === "arrowtail") {
arrowType = arrowTypes[value];
name = "arrows";
value = { from: { enabled: true, type: arrowType } };
}
attr_list.push({ attr: attr, name: name, value: value });
attr_names.push(name);
getToken();
if (token == ",") {
getToken();
}
}
if (token != "]") {
throw newSyntaxError("Bracket ] expected");
}
getToken();
}
/**
* As explained in [1], graphviz has limitations for combination of
* arrow[head|tail] and dir. If attribute list includes 'dir',
* following cases just be supported.
* 1. both or none + arrowhead, arrowtail
* 2. forward + arrowhead (arrowtail is not affedted)
* 3. back + arrowtail (arrowhead is not affected)
* [1] https://www.graphviz.org/doc/info/attrs.html#h:undir_note
*/
if (attr_names.includes("dir")) {
var idx = {}; // get index of 'arrows' and 'dir'
idx.arrows = {};
for (i = 0; i < attr_list.length; i++) {
if (attr_list[i].name === "arrows") {
if (attr_list[i].value.to != null) {
idx.arrows.to = i;
} else if (attr_list[i].value.from != null) {
idx.arrows.from = i;
} else {
throw newSyntaxError("Invalid value of arrows");
}
} else if (attr_list[i].name === "dir") {
idx.dir = i;
}
}
// first, add default arrow shape if it is not assigned to avoid error
var dir_type = attr_list[idx.dir].value;
if (!attr_names.includes("arrows")) {
if (dir_type === "both") {
attr_list.push({
attr: attr_list[idx.dir].attr,
name: "arrows",
value: { to: { enabled: true } },
});
idx.arrows.to = attr_list.length - 1;
attr_list.push({
attr: attr_list[idx.dir].attr,
name: "arrows",
value: { from: { enabled: true } },
});
idx.arrows.from = attr_list.length - 1;
} else if (dir_type === "forward") {
attr_list.push({
attr: attr_list[idx.dir].attr,
name: "arrows",
value: { to: { enabled: true } },
});
idx.arrows.to = attr_list.length - 1;
} else if (dir_type === "back") {
attr_list.push({
attr: attr_list[idx.dir].attr,
name: "arrows",
value: { from: { enabled: true } },
});
idx.arrows.from = attr_list.length - 1;
} else if (dir_type === "none") {
attr_list.push({
attr: attr_list[idx.dir].attr,
name: "arrows",
value: "",
});
idx.arrows.to = attr_list.length - 1;
} else {
throw newSyntaxError('Invalid dir type "' + dir_type + '"');
}
}
var from_type;
var to_type;
// update 'arrows' attribute from 'dir'.
if (dir_type === "both") {
// both of shapes of 'from' and 'to' are given
if (idx.arrows.to && idx.arrows.from) {
to_type = attr_list[idx.arrows.to].value.to.type;
from_type = attr_list[idx.arrows.from].value.from.type;
attr_list[idx.arrows.to] = {
attr: attr_list[idx.arrows.to].attr,
name: attr_list[idx.arrows.to].name,
value: {
to: { enabled: true, type: to_type },
from: { enabled: true, type: from_type },
},
};
attr_list.splice(idx.arrows.from, 1);
// shape of 'to' is assigned and use default to 'from'
} else if (idx.arrows.to) {
to_type = attr_list[idx.arrows.to].value.to.type;
from_type = "arrow";
attr_list[idx.arrows.to] = {
attr: attr_list[idx.arrows.to].attr,
name: attr_list[idx.arrows.to].name,
value: {
to: { enabled: true, type: to_type },
from: { enabled: true, type: from_type },
},
};
// only shape of 'from' is assigned and use default for 'to'
} else if (idx.arrows.from) {
to_type = "arrow";
from_type = attr_list[idx.arrows.from].value.from.type;
attr_list[idx.arrows.from] = {
attr: attr_list[idx.arrows.from].attr,
name: attr_list[idx.arrows.from].name,
value: {
to: { enabled: true, type: to_type },
from: { enabled: true, type: from_type },
},
};
}
} else if (dir_type === "back") {
// given both of shapes, but use only 'from'
if (idx.arrows.to && idx.arrows.from) {
to_type = "";
from_type = attr_list[idx.arrows.from].value.from.type;
attr_list[idx.arrows.from] = {
attr: attr_list[idx.arrows.from].attr,
name: attr_list[idx.arrows.from].name,
value: {
to: { enabled: true, type: to_type },
from: { enabled: true, type: from_type },
},
};
// given shape of 'to', but does not use it
} else if (idx.arrows.to) {
to_type = "";
from_type = "arrow";
idx.arrows.from = idx.arrows.to;
attr_list[idx.arrows.from] = {
attr: attr_list[idx.arrows.from].attr,
name: attr_list[idx.arrows.from].name,
value: {
to: { enabled: true, type: to_type },
from: { enabled: true, type: from_type },
},
};
// assign given 'from' shape
} else if (idx.arrows.from) {
to_type = "";
from_type = attr_list[idx.arrows.from].value.from.type;
attr_list[idx.arrows.to] = {
attr: attr_list[idx.arrows.from].attr,
name: attr_list[idx.arrows.from].name,
value: {
to: { enabled: true, type: to_type },
from: { enabled: true, type: from_type },
},
};
}
attr_list[idx.arrows.from] = {
attr: attr_list[idx.arrows.from].attr,
name: attr_list[idx.arrows.from].name,
value: {
from: {
enabled: true,
type: attr_list[idx.arrows.from].value.from.type,
},
},
};
} else if (dir_type === "none") {
var idx_arrow;
if (idx.arrows.to) {
idx_arrow = idx.arrows.to;
} else {
idx_arrow = idx.arrows.from;
}
attr_list[idx_arrow] = {
attr: attr_list[idx_arrow].attr,
name: attr_list[idx_arrow].name,
value: "",
};
} else if (dir_type === "forward") {
// given both of shapes, but use only 'to'
if (idx.arrows.to && idx.arrows.from) {
to_type = attr_list[idx.arrows.to].value.to.type;
from_type = "";
attr_list[idx.arrows.to] = {
attr: attr_list[idx.arrows.to].attr,
name: attr_list[idx.arrows.to].name,
value: {
to: { enabled: true, type: to_type },
from: { enabled: true, type: from_type },
},
};
// assign given 'to' shape
} else if (idx.arrows.to) {
to_type = attr_list[idx.arrows.to].value.to.type;
from_type = "";
attr_list[idx.arrows.to] = {
attr: attr_list[idx.arrows.to].attr,
name: attr_list[idx.arrows.to].name,
value: {
to: { enabled: true, type: to_type },
from: { enabled: true, type: from_type },
},
};
// given shape of 'from', but does not use it
} else if (idx.arrows.from) {
to_type = "arrow";
from_type = "";
idx.arrows.to = idx.arrows.from;
attr_list[idx.arrows.to] = {
attr: attr_list[idx.arrows.to].attr,
name: attr_list[idx.arrows.to].name,
value: {
to: { enabled: true, type: to_type },
from: { enabled: true, type: from_type },
},
};
}
attr_list[idx.arrows.to] = {
attr: attr_list[idx.arrows.to].attr,
name: attr_list[idx.arrows.to].name,
value: {
to: { enabled: true, type: attr_list[idx.arrows.to].value.to.type },
},
};
} else {
throw newSyntaxError('Invalid dir type "' + dir_type + '"');
}
// remove 'dir' attribute no need anymore
attr_list.splice(idx.dir, 1);
}
// parse 'penwidth'
var nof_attr_list;
if (attr_names.includes("penwidth")) {
var tmp_attr_list = [];
nof_attr_list = attr_list.length;
for (i = 0; i < nof_attr_list; i++) {
// exclude 'width' from attr_list if 'penwidth' exists
if (attr_list[i].name !== "width") {
if (attr_list[i].name === "penwidth") {
attr_list[i].name = "width";
}
tmp_attr_list.push(attr_list[i]);
}
}
attr_list = tmp_attr_list;
}
nof_attr_list = attr_list.length;
for (i = 0; i < nof_attr_list; i++) {
setValue(attr_list[i].attr, attr_list[i].name, attr_list[i].value);
}
return attr;
}
/**
* Create a syntax error with extra information on current token and index.
*
* @param {string} message
* @returns {SyntaxError} err
*/
function newSyntaxError(message) {
return new SyntaxError(
message + ', got "' + chop(token, 30) + '" (char ' + index + ")"
);
}
/**
* Chop off text after a maximum length
*
* @param {string} text
* @param {number} maxLength
* @returns {string}
*/
function chop(text, maxLength) {
return text.length <= maxLength ? text : text.substr(0, 27) + "...";
}
/**
* Execute a function fn for each pair of elements in two arrays
*
* @param {Array | *} array1
* @param {Array | *} array2
* @param {Function} fn
*/
function forEach2(array1, array2, fn) {
if (Array.isArray(array1)) {
array1.forEach(function (elem1) {
if (Array.isArray(array2)) {
array2.forEach(function (elem2) {
fn(elem1, elem2);
});
} else {
fn(elem1, array2);
}
});
} else {
if (Array.isArray(array2)) {
array2.forEach(function (elem2) {
fn(array1, elem2);
});
} else {
fn(array1, array2);
}
}
}
/**
* Set a nested property on an object
* When nested objects are missing, they will be created.
* For example setProp({}, 'font.color', 'red') will return {font: {color: 'red'}}
*
* @param {object} object
* @param {string} path A dot separated string like 'font.color'
* @param {*} value Value for the property
* @returns {object} Returns the original object, allows for chaining.
*/
function setProp(object, path, value) {
var names = path.split(".");
var prop = names.pop();
// traverse over the nested objects
var obj = object;
for (var i = 0; i < names.length; i++) {
var name = names[i];
if (!(name in obj)) {
obj[name] = {};
}
obj = obj[name];
}
// set the property value
obj[prop] = value;
return object;
}
/**
* Convert an object with DOT attributes to their vis.js equivalents.
*
* @param {object} attr Object with DOT attributes
* @param {object} mapping
* @returns {object} Returns an object with vis.js attributes
*/
function convertAttr(attr, mapping) {
var converted = {};
for (var prop in attr) {
if (attr.hasOwnProperty(prop)) {
var visProp = mapping[prop];
if (Array.isArray(visProp)) {
visProp.forEach(function (visPropI) {
setProp(converted, visPropI, attr[prop]);
});
} else if (typeof visProp === "string") {
setProp(converted, visProp, attr[prop]);
} else {
setProp(converted, prop, attr[prop]);
}
}
}
return converted;
}
/**
* Convert a string containing a graph in DOT language into a map containing
* with nodes and edges in the format of graph.
*
* @param {string} data Text containing a graph in DOT-notation
* @returns {object} graphData
*/
function DOTToGraph(data) {
// parse the DOT file
var dotData = parseDOT(data);
var graphData = {
nodes: [],
edges: [],
options: {},
};
// copy the nodes
if (dotData.nodes) {
dotData.nodes.forEach(function (dotNode) {
var graphNode = {
id: dotNode.id,
label: String(dotNode.label || dotNode.id),
};
merge(graphNode, convertAttr(dotNode.attr, NODE_ATTR_MAPPING));
if (graphNode.image) {
graphNode.shape = "image";
}
graphData.nodes.push(graphNode);
});
}
// copy the edges
if (dotData.edges) {
/**
* Convert an edge in DOT format to an edge with VisGraph format
*
* @param {object} dotEdge
* @returns {object} graphEdge
*/
var convertEdge = function (dotEdge) {
var graphEdge = {
from: dotEdge.from,
to: dotEdge.to,
};
merge(graphEdge, convertAttr(dotEdge.attr, EDGE_ATTR_MAPPING));
// Add arrows attribute to default styled arrow.
// The reason why default style is not added in parseAttributeList() is
// because only default is cleared before here.
if (graphEdge.arrows == null && dotEdge.type === "->") {
graphEdge.arrows = "to";
}
return graphEdge;
};
dotData.edges.forEach(function (dotEdge) {
var from, to;
if (dotEdge.from instanceof Object) {
from = dotEdge.from.nodes;
} else {
from = {
id: dotEdge.from,
};
}
if (dotEdge.to instanceof Object) {
to = dotEdge.to.nodes;
} else {
to = {
id: dotEdge.to,
};
}
if (dotEdge.from instanceof Object && dotEdge.from.edges) {
dotEdge.from.edges.forEach(function (subEdge) {
var graphEdge = convertEdge(subEdge);
graphData.edges.push(graphEdge);
});
}
forEach2(from, to, function (from, to) {
var subEdge = createEdge(
graphData,
from.id,
to.id,
dotEdge.type,
dotEdge.attr
);
var graphEdge = convertEdge(subEdge);
graphData.edges.push(graphEdge);
});
if (dotEdge.to instanceof Object && dotEdge.to.edges) {
dotEdge.to.edges.forEach(function (subEdge) {
var graphEdge = convertEdge(subEdge);
graphData.edges.push(graphEdge);
});
}
});
}
// copy the options
if (dotData.attr) {
graphData.options = dotData.attr;
}
return graphData;
}
/* eslint-enable no-var */
/* eslint-enable no-unused-vars */
/* eslint-enable no-prototype-builtins */
var dotparser = /*#__PURE__*/Object.freeze({
__proto__: null,
DOTToGraph: DOTToGraph,
parseDOT: parseDOT
});
/**
* Convert Gephi to Vis.
*
* @param gephiJSON - The parsed JSON data in Gephi format.
* @param optionsObj - Additional options.
* @returns The converted data ready to be used in Vis.
*/
function parseGephi(gephiJSON, optionsObj) {
const options = {
edges: {
inheritColor: false,
},
nodes: {
fixed: false,
parseColor: false,
},
};
if (optionsObj != null) {
if (optionsObj.fixed != null) {
options.nodes.fixed = optionsObj.fixed;
}
if (optionsObj.parseColor != null) {
options.nodes.parseColor = optionsObj.parseColor;
}
if (optionsObj.inheritColor != null) {
options.edges.inheritColor = optionsObj.inheritColor;
}
}
const gEdges = gephiJSON.edges;
const vEdges = gEdges.map((gEdge) => {
const vEdge = {
from: gEdge.source,
id: gEdge.id,
to: gEdge.target,
};
if (gEdge.attributes != null) {
vEdge.attributes = gEdge.attributes;
}
if (gEdge.label != null) {
vEdge.label = gEdge.label;
}
if (gEdge.attributes != null && gEdge.attributes.title != null) {
vEdge.title = gEdge.attributes.title;
}
if (gEdge.type === "Directed") {
vEdge.arrows = "to";
}
// edge['value'] = gEdge.attributes != null ? gEdge.attributes.Weight : undefined;
// edge['width'] = edge['value'] != null ? undefined : edgegEdge.size;
if (gEdge.color && options.edges.inheritColor === false) {
vEdge.color = gEdge.color;
}
return vEdge;
});
const vNodes = gephiJSON.nodes.map((gNode) => {
const vNode = {
id: gNode.id,
fixed: options.nodes.fixed && gNode.x != null && gNode.y != null,
};
if (gNode.attributes != null) {
vNode.attributes = gNode.attributes;
}
if (gNode.label != null) {
vNode.label = gNode.label;
}
if (gNode.size != null) {
vNode.size = gNode.size;
}
if (gNode.attributes != null && gNode.attributes.title != null) {
vNode.title = gNode.attributes.title;
}
if (gNode.title != null) {
vNode.title = gNode.title;
}
if (gNode.x != null) {
vNode.x = gNode.x;
}
if (gNode.y != null) {
vNode.y = gNode.y;
}
if (gNode.color != null) {
if (options.nodes.par