vis-network
Version:
A dynamic, browser-based visualization library.
1,818 lines (1,665 loc) • 698 kB
JavaScript
/**
* vis-network
* https://visjs.github.io/vis-network/
*
* A dynamic, browser-based visualization library.
*
* @version 10.1.1
* @date 2026-08-07T17:51:09.571Z
*
* @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.
*/
import Emitter from 'component-emitter';
import { selectiveDeepExtend, Hammer, deepExtend, topMost, overrideOpacity, forEach, bridgeObject, mergeOptions, isString, selectiveNotDeepExtend, parseColor, fillIfDefined, VALIDATOR_PRINT_STYLE, getAbsoluteTop, getAbsoluteLeft, Popup, Alea, recursiveDOMDelete, HSVToHex, easingFunctions, Validator, Configurator, Activator } from 'vis-util/esnext/esm/vis-util.js';
import { v4 } from 'uuid';
import { isDataViewLike, DataSet } from 'vis-data/esnext/esm/vis-data.js';
import keycharm from '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-unused-vars */
// mapping of attributes from DOT (the keys) to vis.js (the values)
const 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",
};
const EDGE_ATTR_MAPPING = Object.create(NODE_ATTR_MAPPING);
EDGE_ATTR_MAPPING.color = "color.color";
EDGE_ATTR_MAPPING.style = "dashes";
// token types enumeration
const TOKENTYPE = {
NULL: 0,
DELIMITER: 1,
IDENTIFIER: 2,
UNKNOWN: 3,
};
// map with all delimiters
const DELIMITERS = {
"{": true,
"}": true,
"[": true,
"]": true,
";": true,
"=": true,
",": true,
"->": true,
"--": true,
};
let dot = ""; // current dot file
let index = 0; // current index in dot file
let c = ""; // current token character in expr
let token = ""; // current token
let 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) {
const 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 (const 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:
*
* const 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) {
const keys = path.split(".");
let o = obj;
while (keys.length) {
const 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) {
let i, len;
let current = null;
// find root graph (in case of subgraph)
const graphs = [graph]; // list with all graphs from current graph to root graph
let 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--) {
const 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) {
const 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) {
const 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();
}
let isComment;
do {
isComment = false;
// skip comment
if (c === "#") {
// find the previous non-space character
let 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
const 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() {
const 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
const subgraph = parseSubgraph(graph);
if (subgraph) {
// edge statements
parseEdge(graph, subgraph);
return;
}
// parse an attribute statement
const attr = parseAttributeStatement(graph);
if (attr) {
return;
}
// parse node
if (tokenType != TOKENTYPE.IDENTIFIER) {
throw newSyntaxError("Identifier expected");
}
const 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) {
let 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
const node = {
id: id,
};
const 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 === "--") {
let to;
const type = token;
getToken();
const 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
const attr = parseAttributeList();
// create edge
const 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() {
let i;
let attr = null;
// edge styles of dot and vis
const 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
*/
const 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.
*/
let attr_list = [];
const attr_names = []; // used for checking the case.
// parse attributes
while (token === "[") {
getToken();
attr = {};
while (token !== "" && token != "]") {
if (tokenType != TOKENTYPE.IDENTIFIER) {
throw newSyntaxError("Attribute name expected");
}
let name = token;
getToken();
if (token != "=") {
throw newSyntaxError("Equal sign = expected");
}
getToken();
if (tokenType != TOKENTYPE.IDENTIFIER) {
throw newSyntaxError("Attribute value expected");
}
let value = token;
// convert from dot style to vis
if (name === "style") {
value = edgeStyles[value];
}
let 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")) {
const idx = {}; // get index of 'arrows' and 'dir'
idx.arrows = {};
for (let 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
const 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 + '"');
}
}
let from_type;
let 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") {
let 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'
let nof_attr_list;
if (attr_names.includes("penwidth")) {
const 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) {
const names = path.split(".");
const prop = names.pop();
// traverse over the nested objects
let obj = object;
for (let i = 0; i < names.length; i++) {
const 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) {
const converted = {};
for (const prop in attr) {
if (attr.hasOwnProperty(prop)) {
const 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
const dotData = parseDOT(data);
const graphData = {
nodes: [],
edges: [],
options: {},
};
// copy the nodes
if (dotData.nodes) {
dotData.nodes.forEach(function (dotNode) {
const 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
*/
const convertEdge = function (dotEdge) {
const 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) {
let 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) {
const graphEdge = convertEdge(subEdge);
graphData.edges.push(graphEdge);
});
}
forEach2(from, to, function (from, to) {
const subEdge = createEdge(
graphData,
from.id,
to.id,
dotEdge.type,
dotEdge.attr,
);
const graphEdge = convertEdge(subEdge);
graphData.edges.push(graphEdge);
});
if (dotEdge.to instanceof Object && dotEdge.to.edges) {
dotEdge.to.edges.forEach(function (subEdge) {
const graphEdge = convertEdge(subEdge);
graphData.edges.push(graphEdge);
});
}
});
}
// copy the options
if (dotData.attr) {
graphData.options = dotData.attr;
}
return graphData;
}
/**
* 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();
}
/* eslint-enable no-unused-vars */
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.
* @param optionsObj.fixed - If true, nodes will be fixed at the positions defined in Gephi.
* @param optionsObj.inheritColor - If true, colors from Gephi will be ignored.
* @param optionsObj.parseColor - If true, the single Gephi color will be parsed into different shades for border, highlight, and hover.
* @returns The converted data ready to be used in Vis.
*/
function parseGephi(gephiJSON, { fixed: isNodePositionFixed = false, inheritColor: isEdgeColorInherited = false, parseColor: isNodeColorParsed = false, } = {}) {
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 && isEdgeColorInherited === false) {
vEdge.color = gEdge.color;
}
return vEdge;
});
const vNodes = gephiJSON.nodes.map((gNode) => {
const vNode = {
id: gNode.id,
fixed: isNodePositionFixed && 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 (isNodeColorParsed === true) {
vNode.color = gNode.color;
}
else {
vNode.color = {
background: gNode.color,
border: gNode.color,
highlight: {
background: gNode.color,
border: gNode.color,
},
hover: {
background: gNode.color,
border: gNode.color,
},
};
}
}
return vNode;
});
return { nodes: vNodes, edges: vEdges };
}
var gephiParser = /*#__PURE__*/Object.freeze({
__proto__: null,
parseGephi: parseGephi
});
/**
* Associates a canvas to a given image, containing a number of renderings
* of the image at various sizes.
*
* This technique is known as 'mipmapping'.
*
* NOTE: Images can also be of type 'data:svg+xml`. This code also works
* for svg, but the mipmapping may not be necessary.
* @param {Image} image
*/
class CachedImage {
/**
* @ignore
*/
constructor() {
this.NUM_ITERATIONS = 4; // Number of items in the coordinates array
this.image = new Image();
this.canvas = document.createElement("canvas");
}
/**
* Called when the image has been successfully loaded.
*/
init() {
if (this.initialized()) return;
this.src = this.image.src; // For same interface with Image
const w = this.image.width;
const h = this.image.height;
// Ease external access
this.width = w;
this.height = h;
const h2 = Math.floor(h / 2);
const h4 = Math.floor(h / 4);
const h8 = Math.floor(h / 8);
const h16 = Math.floor(h / 16);
const w2 = Math.floor(w / 2);
const w4 = Math.floor(w / 4);
const w8 = Math.floor(w / 8);
const w16 = Math.floor(w / 16);
// Make canvas as small as possible
this.canvas.width = 3 * w4;
this.canvas.height = h2;
// Coordinates and sizes of images contained in the canvas
// Values per row: [top x, left y, width, height]
this.coordinates = [
[0, 0, w2, h2],
[w2, 0, w4, h4],
[w2, h4, w8, h8],
[5 * w8, h4, w16, h16],
];
this._fillMipMap();
}
/**
* @returns {boolean} true if init() has been called, false otherwise.
*/
initialized() {
return this.coordinates !== undefined;
}
/**
* Redraw main image in various sizes to the context.
*
* The rationale behind this is to reduce artefacts due to interpolation
* at differing zoom levels.
*
* Source: http://stackoverflow.com/q/18761404/1223531
*
* This methods takes the resizing out of the drawing loop, in order to
* reduce performance overhead.
*
* TODO: The code assumes that a 2D context can always be gotten. This is
* not necessarily true! OTOH, if not true then usage of this class
* is senseless.
* @private
*/
_fillMipMap() {
const ctx = this.canvas.getContext("2d");
// First zoom-level comes from the image
const to = this.coordinates[0];
ctx.drawImage(this.image, to[0], to[1], to[2], to[3]);
// The rest are copy actions internal to the canvas/context
for (let iterations = 1; iterations < this.NUM_ITERATIONS; iterations++) {
const from = this.coordinates[iterations - 1];
const to = this.coordinates[iterations];
ctx.drawImage(
this.canvas,
from[0],
from[1],
from[2],
from[3],
to[0],
to[1],
to[2],
to[3],
);
}
}
/**
* Draw the image, using the mipmap if necessary.
*
* MipMap is only used if param factor > 2; otherwise, original bitmap
* is resized. This is also used to skip mipmap usage, e.g. by setting factor = 1
*
* Credits to 'Alex de Mulder' for original implementation.
* @param {CanvasRenderingContext2D} ctx context on which to draw zoomed image
* @param {Float} factor scale factor