UNPKG

vis-network

Version:

A dynamic, browser-based visualization library.

1,695 lines (1,559 loc) 801 kB
/** * 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; }