vis-network
Version:
A dynamic, browser-based visualization library.
1,695 lines (1,559 loc) • 801 kB
JavaScript
/**
* vis-network
* https://visjs.github.io/vis-network/
*
* A dynamic, browser-based visualization library.
*
* @version 8.2.0
* @date 2020-08-13T21:43:47.994Z
*
* @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('keycharm'), require('@egjs/hammerjs'), require('vis-data/esnext/umd/vis-data.js'), require('uuid'), require('timsort')) :
typeof define === 'function' && define.amd ? define(['exports', 'component-emitter', 'vis-util/esnext/umd/vis-util.js', 'keycharm', '@egjs/hammerjs', 'vis-data/esnext/umd/vis-data.js', 'uuid', 'timsort'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.vis = global.vis || {}, global.Emitter, global.vis, global.keycharm, global.Hammer, global.vis, global.uuid, global.timsort));
}(this, (function (exports, Emitter, esnext, keycharm, hammerjs, esnext$1, uuid, TimSort) {
Emitter = Emitter && Object.prototype.hasOwnProperty.call(Emitter, 'default') ? Emitter['default'] : Emitter;
keycharm = keycharm && Object.prototype.hasOwnProperty.call(keycharm, 'default') ? keycharm['default'] : keycharm;
hammerjs = hammerjs && Object.prototype.hasOwnProperty.call(hammerjs, 'default') ? hammerjs['default'] : hammerjs;
var TimSort__default = 'default' in TimSort ? TimSort['default'] : TimSort;
/**
* 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
* @date 2012-08-08
* @remarks
* 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 max-statements */
/* 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
* @return {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.
* @return {string} cNext
*/
function nextPreview() {
return dot.charAt(index + 1);
}
var regexAlphaNumeric = /[a-zA-Z_0-9.:#]/;
/**
* Test whether given character is alphabetic or numeric
* @param {string} c
* @return {Boolean} isAlphaNumeric
*/
function isAlphaNumeric(c) {
return regexAlphaNumeric.test(c);
}
/**
* Merge all options of object b into object b
* @param {Object} a
* @param {Object} b
* @return {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
* @return {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
* @return {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]
* @return {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
* @return {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
* @return {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
* @return {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;
}
var dotparser = /*#__PURE__*/Object.freeze({
__proto__: null,
parseDOT: parseDOT,
DOTToGraph: DOTToGraph
});
/**
* 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;
}