UNPKG

717 kBJavaScriptView Raw
1/**
2 * vis-network
3 * https://visjs.github.io/vis-network/
4 *
5 * A dynamic, browser-based visualization library.
6 *
7 * @version 9.1.0
8 * @date 2021-08-29T08:43:14.666Z
9 *
10 * @copyright (c) 2011-2017 Almende B.V, http://almende.com
11 * @copyright (c) 2017-2019 visjs contributors, https://github.com/visjs
12 *
13 * @license
14 * vis.js is dual licensed under both
15 *
16 * 1. The Apache 2.0 License
17 * http://www.apache.org/licenses/LICENSE-2.0
18 *
19 * and
20 *
21 * 2. The MIT License
22 * http://opensource.org/licenses/MIT
23 *
24 * vis.js may be distributed under either license.
25 */
26
27import Emitter from 'component-emitter';
28import { topMost, forEach, deepExtend, overrideOpacity, bridgeObject, selectiveNotDeepExtend, parseColor, mergeOptions, fillIfDefined, VALIDATOR_PRINT_STYLE, selectiveDeepExtend, isString, Alea, HSVToHex, addEventListener, removeEventListener, Hammer, easingFunctions, getAbsoluteLeft, getAbsoluteTop, Popup, recursiveDOMDelete, Validator, Configurator, Activator } from 'vis-util/esnext/esm/vis-util.js';
29import { isDataViewLike, DataSet } from 'vis-data/esnext/esm/vis-data.js';
30import { v4 } from 'uuid';
31import keycharm from 'keycharm';
32import { __classPrivateFieldSet, __classPrivateFieldGet } from 'tslib';
33import TimSort, { sort } from 'timsort';
34
35/**
36 * Draw a circle.
37 *
38 * @param ctx - The context this shape will be rendered to.
39 * @param x - The position of the center on the x axis.
40 * @param y - The position of the center on the y axis.
41 * @param r - The radius of the circle.
42 */
43function drawCircle(ctx, x, y, r) {
44 ctx.beginPath();
45 ctx.arc(x, y, r, 0, 2 * Math.PI, false);
46 ctx.closePath();
47}
48/**
49 * Draw a square.
50 *
51 * @param ctx - The context this shape will be rendered to.
52 * @param x - The position of the center on the x axis.
53 * @param y - The position of the center on the y axis.
54 * @param r - Half of the width and height of the square.
55 */
56function drawSquare(ctx, x, y, r) {
57 ctx.beginPath();
58 ctx.rect(x - r, y - r, r * 2, r * 2);
59 ctx.closePath();
60}
61/**
62 * Draw an equilateral triangle standing on a side.
63 *
64 * @param ctx - The context this shape will be rendered to.
65 * @param x - The position of the center on the x axis.
66 * @param y - The position of the center on the y axis.
67 * @param r - Half of the length of the sides.
68 *
69 * @remarks
70 * http://en.wikipedia.org/wiki/Equilateral_triangle
71 */
72function drawTriangle(ctx, x, y, r) {
73 ctx.beginPath();
74 // the change in radius and the offset is here to center the shape
75 r *= 1.15;
76 y += 0.275 * r;
77 const s = r * 2;
78 const s2 = s / 2;
79 const ir = (Math.sqrt(3) / 6) * s; // radius of inner circle
80 const h = Math.sqrt(s * s - s2 * s2); // height
81 ctx.moveTo(x, y - (h - ir));
82 ctx.lineTo(x + s2, y + ir);
83 ctx.lineTo(x - s2, y + ir);
84 ctx.lineTo(x, y - (h - ir));
85 ctx.closePath();
86}
87/**
88 * Draw an equilateral triangle standing on a vertex.
89 *
90 * @param ctx - The context this shape will be rendered to.
91 * @param x - The position of the center on the x axis.
92 * @param y - The position of the center on the y axis.
93 * @param r - Half of the length of the sides.
94 *
95 * @remarks
96 * http://en.wikipedia.org/wiki/Equilateral_triangle
97 */
98function drawTriangleDown(ctx, x, y, r) {
99 ctx.beginPath();
100 // the change in radius and the offset is here to center the shape
101 r *= 1.15;
102 y -= 0.275 * r;
103 const s = r * 2;
104 const s2 = s / 2;
105 const ir = (Math.sqrt(3) / 6) * s; // radius of inner circle
106 const h = Math.sqrt(s * s - s2 * s2); // height
107 ctx.moveTo(x, y + (h - ir));
108 ctx.lineTo(x + s2, y - ir);
109 ctx.lineTo(x - s2, y - ir);
110 ctx.lineTo(x, y + (h - ir));
111 ctx.closePath();
112}
113/**
114 * Draw a star.
115 *
116 * @param ctx - The context this shape will be rendered to.
117 * @param x - The position of the center on the x axis.
118 * @param y - The position of the center on the y axis.
119 * @param r - The outer radius of the star.
120 */
121function drawStar(ctx, x, y, r) {
122 // http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/
123 ctx.beginPath();
124 // the change in radius and the offset is here to center the shape
125 r *= 0.82;
126 y += 0.1 * r;
127 for (let n = 0; n < 10; n++) {
128 const radius = n % 2 === 0 ? r * 1.3 : r * 0.5;
129 ctx.lineTo(x + radius * Math.sin((n * 2 * Math.PI) / 10), y - radius * Math.cos((n * 2 * Math.PI) / 10));
130 }
131 ctx.closePath();
132}
133/**
134 * Draw a diamond.
135 *
136 * @param ctx - The context this shape will be rendered to.
137 * @param x - The position of the center on the x axis.
138 * @param y - The position of the center on the y axis.
139 * @param r - Half of the width and height of the diamond.
140 *
141 * @remarks
142 * http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/
143 */
144function drawDiamond(ctx, x, y, r) {
145 ctx.beginPath();
146 ctx.lineTo(x, y + r);
147 ctx.lineTo(x + r, y);
148 ctx.lineTo(x, y - r);
149 ctx.lineTo(x - r, y);
150 ctx.closePath();
151}
152/**
153 * Draw a rectangle with rounded corners.
154 *
155 * @param ctx - The context this shape will be rendered to.
156 * @param x - The position of the center on the x axis.
157 * @param y - The position of the center on the y axis.
158 * @param w - The width of the rectangle.
159 * @param h - The height of the rectangle.
160 * @param r - The radius of the corners.
161 *
162 * @remarks
163 * http://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-on-html-canvas
164 */
165function drawRoundRect(ctx, x, y, w, h, r) {
166 const r2d = Math.PI / 180;
167 if (w - 2 * r < 0) {
168 r = w / 2;
169 } //ensure that the radius isn't too large for x
170 if (h - 2 * r < 0) {
171 r = h / 2;
172 } //ensure that the radius isn't too large for y
173 ctx.beginPath();
174 ctx.moveTo(x + r, y);
175 ctx.lineTo(x + w - r, y);
176 ctx.arc(x + w - r, y + r, r, r2d * 270, r2d * 360, false);
177 ctx.lineTo(x + w, y + h - r);
178 ctx.arc(x + w - r, y + h - r, r, 0, r2d * 90, false);
179 ctx.lineTo(x + r, y + h);
180 ctx.arc(x + r, y + h - r, r, r2d * 90, r2d * 180, false);
181 ctx.lineTo(x, y + r);
182 ctx.arc(x + r, y + r, r, r2d * 180, r2d * 270, false);
183 ctx.closePath();
184}
185/**
186 * Draw an ellipse.
187 *
188 * @param ctx - The context this shape will be rendered to.
189 * @param x - The position of the center on the x axis.
190 * @param y - The position of the center on the y axis.
191 * @param w - The width of the ellipse.
192 * @param h - The height of the ellipse.
193 *
194 * @remarks
195 * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
196 *
197 * Postfix '_vis' added to discern it from standard method ellipse().
198 */
199function drawEllipse(ctx, x, y, w, h) {
200 const kappa = 0.5522848, ox = (w / 2) * kappa, // control point offset horizontal
201 oy = (h / 2) * kappa, // control point offset vertical
202 xe = x + w, // x-end
203 ye = y + h, // y-end
204 xm = x + w / 2, // x-middle
205 ym = y + h / 2; // y-middle
206 ctx.beginPath();
207 ctx.moveTo(x, ym);
208 ctx.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
209 ctx.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
210 ctx.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
211 ctx.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
212 ctx.closePath();
213}
214/**
215 * Draw an isometric cylinder.
216 *
217 * @param ctx - The context this shape will be rendered to.
218 * @param x - The position of the center on the x axis.
219 * @param y - The position of the center on the y axis.
220 * @param w - The width of the database.
221 * @param h - The height of the database.
222 *
223 * @remarks
224 * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
225 */
226function drawDatabase(ctx, x, y, w, h) {
227 const f = 1 / 3;
228 const wEllipse = w;
229 const hEllipse = h * f;
230 const kappa = 0.5522848, ox = (wEllipse / 2) * kappa, // control point offset horizontal
231 oy = (hEllipse / 2) * kappa, // control point offset vertical
232 xe = x + wEllipse, // x-end
233 ye = y + hEllipse, // y-end
234 xm = x + wEllipse / 2, // x-middle
235 ym = y + hEllipse / 2, // y-middle
236 ymb = y + (h - hEllipse / 2), // y-midlle, bottom ellipse
237 yeb = y + h; // y-end, bottom ellipse
238 ctx.beginPath();
239 ctx.moveTo(xe, ym);
240 ctx.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
241 ctx.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
242 ctx.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
243 ctx.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
244 ctx.lineTo(xe, ymb);
245 ctx.bezierCurveTo(xe, ymb + oy, xm + ox, yeb, xm, yeb);
246 ctx.bezierCurveTo(xm - ox, yeb, x, ymb + oy, x, ymb);
247 ctx.lineTo(x, ym);
248}
249/**
250 * Draw a dashed line.
251 *
252 * @param ctx - The context this shape will be rendered to.
253 * @param x - The start position on the x axis.
254 * @param y - The start position on the y axis.
255 * @param x2 - The end position on the x axis.
256 * @param y2 - The end position on the y axis.
257 * @param pattern - List of lengths starting with line and then alternating between space and line.
258 *
259 * @author David Jordan
260 * @remarks
261 * date 2012-08-08
262 * http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas
263 */
264function drawDashedLine(ctx, x, y, x2, y2, pattern) {
265 ctx.beginPath();
266 ctx.moveTo(x, y);
267 const patternLength = pattern.length;
268 const dx = x2 - x;
269 const dy = y2 - y;
270 const slope = dy / dx;
271 let distRemaining = Math.sqrt(dx * dx + dy * dy);
272 let patternIndex = 0;
273 let draw = true;
274 let xStep = 0;
275 let dashLength = +pattern[0];
276 while (distRemaining >= 0.1) {
277 dashLength = +pattern[patternIndex++ % patternLength];
278 if (dashLength > distRemaining) {
279 dashLength = distRemaining;
280 }
281 xStep = Math.sqrt((dashLength * dashLength) / (1 + slope * slope));
282 xStep = dx < 0 ? -xStep : xStep;
283 x += xStep;
284 y += slope * xStep;
285 if (draw === true) {
286 ctx.lineTo(x, y);
287 }
288 else {
289 ctx.moveTo(x, y);
290 }
291 distRemaining -= dashLength;
292 draw = !draw;
293 }
294}
295/**
296 * Draw a hexagon.
297 *
298 * @param ctx - The context this shape will be rendered to.
299 * @param x - The position of the center on the x axis.
300 * @param y - The position of the center on the y axis.
301 * @param r - The radius of the hexagon.
302 */
303function drawHexagon(ctx, x, y, r) {
304 ctx.beginPath();
305 const sides = 6;
306 const a = (Math.PI * 2) / sides;
307 ctx.moveTo(x + r, y);
308 for (let i = 1; i < sides; i++) {
309 ctx.lineTo(x + r * Math.cos(a * i), y + r * Math.sin(a * i));
310 }
311 ctx.closePath();
312}
313const shapeMap = {
314 circle: drawCircle,
315 dashedLine: drawDashedLine,
316 database: drawDatabase,
317 diamond: drawDiamond,
318 ellipse: drawEllipse,
319 ellipse_vis: drawEllipse,
320 hexagon: drawHexagon,
321 roundRect: drawRoundRect,
322 square: drawSquare,
323 star: drawStar,
324 triangle: drawTriangle,
325 triangleDown: drawTriangleDown,
326};
327/**
328 * Returns either custom or native drawing function base on supplied name.
329 *
330 * @param name - The name of the function. Either the name of a
331 * CanvasRenderingContext2D property or an export from shapes.ts without the
332 * draw prefix.
333 *
334 * @returns The function that can be used for rendering. In case of native
335 * CanvasRenderingContext2D function the API is normalized to
336 * `(ctx: CanvasRenderingContext2D, ...originalArgs) => void`.
337 */
338function getShape(name) {
339 if (Object.prototype.hasOwnProperty.call(shapeMap, name)) {
340 return shapeMap[name];
341 }
342 else {
343 return function (ctx, ...args) {
344 CanvasRenderingContext2D.prototype[name].call(ctx, args);
345 };
346 }
347}
348
349/* eslint-disable no-prototype-builtins */
350/* eslint-disable no-unused-vars */
351/* eslint-disable no-var */
352
353/**
354 * Parse a text source containing data in DOT language into a JSON object.
355 * The object contains two lists: one with nodes and one with edges.
356 *
357 * DOT language reference: http://www.graphviz.org/doc/info/lang.html
358 *
359 * DOT language attributes: http://graphviz.org/content/attrs
360 *
361 * @param {string} data Text containing a graph in DOT-notation
362 * @returns {object} graph An object containing two parameters:
363 * {Object[]} nodes
364 * {Object[]} edges
365 *
366 * -------------------------------------------
367 * TODO
368 * ====
369 *
370 * For label handling, this is an incomplete implementation. From docs (quote #3015):
371 *
372 * > the escape sequences "\n", "\l" and "\r" divide the label into lines, centered,
373 * > left-justified, and right-justified, respectively.
374 *
375 * Source: http://www.graphviz.org/content/attrs#kescString
376 *
377 * > As another aid for readability, dot allows double-quoted strings to span multiple physical
378 * > lines using the standard C convention of a backslash immediately preceding a newline
379 * > character
380 * > In addition, double-quoted strings can be concatenated using a '+' operator.
381 * > As HTML strings can contain newline characters, which are used solely for formatting,
382 * > the language does not allow escaped newlines or concatenation operators to be used
383 * > within them.
384 *
385 * - Currently, only '\\n' is handled
386 * - Note that text explicitly says 'labels'; the dot parser currently handles escape
387 * sequences in **all** strings.
388 */
389function parseDOT(data) {
390 dot = data;
391 return parseGraph();
392}
393
394// mapping of attributes from DOT (the keys) to vis.js (the values)
395var NODE_ATTR_MAPPING = {
396 fontsize: "font.size",
397 fontcolor: "font.color",
398 labelfontcolor: "font.color",
399 fontname: "font.face",
400 color: ["color.border", "color.background"],
401 fillcolor: "color.background",
402 tooltip: "title",
403 labeltooltip: "title",
404};
405var EDGE_ATTR_MAPPING = Object.create(NODE_ATTR_MAPPING);
406EDGE_ATTR_MAPPING.color = "color.color";
407EDGE_ATTR_MAPPING.style = "dashes";
408
409// token types enumeration
410var TOKENTYPE = {
411 NULL: 0,
412 DELIMITER: 1,
413 IDENTIFIER: 2,
414 UNKNOWN: 3,
415};
416
417// map with all delimiters
418var DELIMITERS = {
419 "{": true,
420 "}": true,
421 "[": true,
422 "]": true,
423 ";": true,
424 "=": true,
425 ",": true,
426
427 "->": true,
428 "--": true,
429};
430
431var dot = ""; // current dot file
432var index = 0; // current index in dot file
433var c = ""; // current token character in expr
434var token = ""; // current token
435var tokenType = TOKENTYPE.NULL; // type of the token
436
437/**
438 * Get the first character from the dot file.
439 * The character is stored into the char c. If the end of the dot file is
440 * reached, the function puts an empty string in c.
441 */
442function first() {
443 index = 0;
444 c = dot.charAt(0);
445}
446
447/**
448 * Get the next character from the dot file.
449 * The character is stored into the char c. If the end of the dot file is
450 * reached, the function puts an empty string in c.
451 */
452function next() {
453 index++;
454 c = dot.charAt(index);
455}
456
457/**
458 * Preview the next character from the dot file.
459 *
460 * @returns {string} cNext
461 */
462function nextPreview() {
463 return dot.charAt(index + 1);
464}
465
466/**
467 * Test whether given character is alphabetic or numeric ( a-zA-Z_0-9.:# )
468 *
469 * @param {string} c
470 * @returns {boolean} isAlphaNumeric
471 */
472function isAlphaNumeric(c) {
473 var charCode = c.charCodeAt(0);
474
475 if (charCode < 47) {
476 // #.
477 return charCode === 35 || charCode === 46;
478 }
479 if (charCode < 59) {
480 // 0-9 and :
481 return charCode > 47;
482 }
483 if (charCode < 91) {
484 // A-Z
485 return charCode > 64;
486 }
487 if (charCode < 96) {
488 // _
489 return charCode === 95;
490 }
491 if (charCode < 123) {
492 // a-z
493 return charCode > 96;
494 }
495
496 return false;
497}
498
499/**
500 * Merge all options of object b into object b
501 *
502 * @param {object} a
503 * @param {object} b
504 * @returns {object} a
505 */
506function merge(a, b) {
507 if (!a) {
508 a = {};
509 }
510
511 if (b) {
512 for (var name in b) {
513 if (b.hasOwnProperty(name)) {
514 a[name] = b[name];
515 }
516 }
517 }
518 return a;
519}
520
521/**
522 * Set a value in an object, where the provided parameter name can be a
523 * path with nested parameters. For example:
524 *
525 * var obj = {a: 2};
526 * setValue(obj, 'b.c', 3); // obj = {a: 2, b: {c: 3}}
527 *
528 * @param {object} obj
529 * @param {string} path A parameter name or dot-separated parameter path,
530 * like "color.highlight.border".
531 * @param {*} value
532 */
533function setValue(obj, path, value) {
534 var keys = path.split(".");
535 var o = obj;
536 while (keys.length) {
537 var key = keys.shift();
538 if (keys.length) {
539 // this isn't the end point
540 if (!o[key]) {
541 o[key] = {};
542 }
543 o = o[key];
544 } else {
545 // this is the end point
546 o[key] = value;
547 }
548 }
549}
550
551/**
552 * Add a node to a graph object. If there is already a node with
553 * the same id, their attributes will be merged.
554 *
555 * @param {object} graph
556 * @param {object} node
557 */
558function addNode(graph, node) {
559 var i, len;
560 var current = null;
561
562 // find root graph (in case of subgraph)
563 var graphs = [graph]; // list with all graphs from current graph to root graph
564 var root = graph;
565 while (root.parent) {
566 graphs.push(root.parent);
567 root = root.parent;
568 }
569
570 // find existing node (at root level) by its id
571 if (root.nodes) {
572 for (i = 0, len = root.nodes.length; i < len; i++) {
573 if (node.id === root.nodes[i].id) {
574 current = root.nodes[i];
575 break;
576 }
577 }
578 }
579
580 if (!current) {
581 // this is a new node
582 current = {
583 id: node.id,
584 };
585 if (graph.node) {
586 // clone default attributes
587 current.attr = merge(current.attr, graph.node);
588 }
589 }
590
591 // add node to this (sub)graph and all its parent graphs
592 for (i = graphs.length - 1; i >= 0; i--) {
593 var g = graphs[i];
594
595 if (!g.nodes) {
596 g.nodes = [];
597 }
598 if (g.nodes.indexOf(current) === -1) {
599 g.nodes.push(current);
600 }
601 }
602
603 // merge attributes
604 if (node.attr) {
605 current.attr = merge(current.attr, node.attr);
606 }
607}
608
609/**
610 * Add an edge to a graph object
611 *
612 * @param {object} graph
613 * @param {object} edge
614 */
615function addEdge(graph, edge) {
616 if (!graph.edges) {
617 graph.edges = [];
618 }
619 graph.edges.push(edge);
620 if (graph.edge) {
621 var attr = merge({}, graph.edge); // clone default attributes
622 edge.attr = merge(attr, edge.attr); // merge attributes
623 }
624}
625
626/**
627 * Create an edge to a graph object
628 *
629 * @param {object} graph
630 * @param {string | number | object} from
631 * @param {string | number | object} to
632 * @param {string} type
633 * @param {object | null} attr
634 * @returns {object} edge
635 */
636function createEdge(graph, from, to, type, attr) {
637 var edge = {
638 from: from,
639 to: to,
640 type: type,
641 };
642
643 if (graph.edge) {
644 edge.attr = merge({}, graph.edge); // clone default attributes
645 }
646 edge.attr = merge(edge.attr || {}, attr); // merge attributes
647
648 // Move arrows attribute from attr to edge temporally created in
649 // parseAttributeList().
650 if (attr != null) {
651 if (attr.hasOwnProperty("arrows") && attr["arrows"] != null) {
652 edge["arrows"] = { to: { enabled: true, type: attr.arrows.type } };
653 attr["arrows"] = null;
654 }
655 }
656 return edge;
657}
658
659/**
660 * Get next token in the current dot file.
661 * The token and token type are available as token and tokenType
662 */
663function getToken() {
664 tokenType = TOKENTYPE.NULL;
665 token = "";
666
667 // skip over whitespaces
668 while (c === " " || c === "\t" || c === "\n" || c === "\r") {
669 // space, tab, enter
670 next();
671 }
672
673 do {
674 var isComment = false;
675
676 // skip comment
677 if (c === "#") {
678 // find the previous non-space character
679 var i = index - 1;
680 while (dot.charAt(i) === " " || dot.charAt(i) === "\t") {
681 i--;
682 }
683 if (dot.charAt(i) === "\n" || dot.charAt(i) === "") {
684 // the # is at the start of a line, this is indeed a line comment
685 while (c != "" && c != "\n") {
686 next();
687 }
688 isComment = true;
689 }
690 }
691 if (c === "/" && nextPreview() === "/") {
692 // skip line comment
693 while (c != "" && c != "\n") {
694 next();
695 }
696 isComment = true;
697 }
698 if (c === "/" && nextPreview() === "*") {
699 // skip block comment
700 while (c != "") {
701 if (c === "*" && nextPreview() === "/") {
702 // end of block comment found. skip these last two characters
703 next();
704 next();
705 break;
706 } else {
707 next();
708 }
709 }
710 isComment = true;
711 }
712
713 // skip over whitespaces
714 while (c === " " || c === "\t" || c === "\n" || c === "\r") {
715 // space, tab, enter
716 next();
717 }
718 } while (isComment);
719
720 // check for end of dot file
721 if (c === "") {
722 // token is still empty
723 tokenType = TOKENTYPE.DELIMITER;
724 return;
725 }
726
727 // check for delimiters consisting of 2 characters
728 var c2 = c + nextPreview();
729 if (DELIMITERS[c2]) {
730 tokenType = TOKENTYPE.DELIMITER;
731 token = c2;
732 next();
733 next();
734 return;
735 }
736
737 // check for delimiters consisting of 1 character
738 if (DELIMITERS[c]) {
739 tokenType = TOKENTYPE.DELIMITER;
740 token = c;
741 next();
742 return;
743 }
744
745 // check for an identifier (number or string)
746 // TODO: more precise parsing of numbers/strings (and the port separator ':')
747 if (isAlphaNumeric(c) || c === "-") {
748 token += c;
749 next();
750
751 while (isAlphaNumeric(c)) {
752 token += c;
753 next();
754 }
755 if (token === "false") {
756 token = false; // convert to boolean
757 } else if (token === "true") {
758 token = true; // convert to boolean
759 } else if (!isNaN(Number(token))) {
760 token = Number(token); // convert to number
761 }
762 tokenType = TOKENTYPE.IDENTIFIER;
763 return;
764 }
765
766 // check for a string enclosed by double quotes
767 if (c === '"') {
768 next();
769 while (c != "" && (c != '"' || (c === '"' && nextPreview() === '"'))) {
770 if (c === '"') {
771 // skip the escape character
772 token += c;
773 next();
774 } else if (c === "\\" && nextPreview() === "n") {
775 // Honor a newline escape sequence
776 token += "\n";
777 next();
778 } else {
779 token += c;
780 }
781 next();
782 }
783 if (c != '"') {
784 throw newSyntaxError('End of string " expected');
785 }
786 next();
787 tokenType = TOKENTYPE.IDENTIFIER;
788 return;
789 }
790
791 // something unknown is found, wrong characters, a syntax error
792 tokenType = TOKENTYPE.UNKNOWN;
793 while (c != "") {
794 token += c;
795 next();
796 }
797 throw new SyntaxError('Syntax error in part "' + chop(token, 30) + '"');
798}
799
800/**
801 * Parse a graph.
802 *
803 * @returns {object} graph
804 */
805function parseGraph() {
806 var graph = {};
807
808 first();
809 getToken();
810
811 // optional strict keyword
812 if (token === "strict") {
813 graph.strict = true;
814 getToken();
815 }
816
817 // graph or digraph keyword
818 if (token === "graph" || token === "digraph") {
819 graph.type = token;
820 getToken();
821 }
822
823 // optional graph id
824 if (tokenType === TOKENTYPE.IDENTIFIER) {
825 graph.id = token;
826 getToken();
827 }
828
829 // open angle bracket
830 if (token != "{") {
831 throw newSyntaxError("Angle bracket { expected");
832 }
833 getToken();
834
835 // statements
836 parseStatements(graph);
837
838 // close angle bracket
839 if (token != "}") {
840 throw newSyntaxError("Angle bracket } expected");
841 }
842 getToken();
843
844 // end of file
845 if (token !== "") {
846 throw newSyntaxError("End of file expected");
847 }
848 getToken();
849
850 // remove temporary default options
851 delete graph.node;
852 delete graph.edge;
853 delete graph.graph;
854
855 return graph;
856}
857
858/**
859 * Parse a list with statements.
860 *
861 * @param {object} graph
862 */
863function parseStatements(graph) {
864 while (token !== "" && token != "}") {
865 parseStatement(graph);
866 if (token === ";") {
867 getToken();
868 }
869 }
870}
871
872/**
873 * Parse a single statement. Can be a an attribute statement, node
874 * statement, a series of node statements and edge statements, or a
875 * parameter.
876 *
877 * @param {object} graph
878 */
879function parseStatement(graph) {
880 // parse subgraph
881 var subgraph = parseSubgraph(graph);
882 if (subgraph) {
883 // edge statements
884 parseEdge(graph, subgraph);
885
886 return;
887 }
888
889 // parse an attribute statement
890 var attr = parseAttributeStatement(graph);
891 if (attr) {
892 return;
893 }
894
895 // parse node
896 if (tokenType != TOKENTYPE.IDENTIFIER) {
897 throw newSyntaxError("Identifier expected");
898 }
899 var id = token; // id can be a string or a number
900 getToken();
901
902 if (token === "=") {
903 // id statement
904 getToken();
905 if (tokenType != TOKENTYPE.IDENTIFIER) {
906 throw newSyntaxError("Identifier expected");
907 }
908 graph[id] = token;
909 getToken();
910 // TODO: implement comma separated list with "a_list: ID=ID [','] [a_list] "
911 } else {
912 parseNodeStatement(graph, id);
913 }
914}
915
916/**
917 * Parse a subgraph
918 *
919 * @param {object} graph parent graph object
920 * @returns {object | null} subgraph
921 */
922function parseSubgraph(graph) {
923 var subgraph = null;
924
925 // optional subgraph keyword
926 if (token === "subgraph") {
927 subgraph = {};
928 subgraph.type = "subgraph";
929 getToken();
930
931 // optional graph id
932 if (tokenType === TOKENTYPE.IDENTIFIER) {
933 subgraph.id = token;
934 getToken();
935 }
936 }
937
938 // open angle bracket
939 if (token === "{") {
940 getToken();
941
942 if (!subgraph) {
943 subgraph = {};
944 }
945 subgraph.parent = graph;
946 subgraph.node = graph.node;
947 subgraph.edge = graph.edge;
948 subgraph.graph = graph.graph;
949
950 // statements
951 parseStatements(subgraph);
952
953 // close angle bracket
954 if (token != "}") {
955 throw newSyntaxError("Angle bracket } expected");
956 }
957 getToken();
958
959 // remove temporary default options
960 delete subgraph.node;
961 delete subgraph.edge;
962 delete subgraph.graph;
963 delete subgraph.parent;
964
965 // register at the parent graph
966 if (!graph.subgraphs) {
967 graph.subgraphs = [];
968 }
969 graph.subgraphs.push(subgraph);
970 }
971
972 return subgraph;
973}
974
975/**
976 * parse an attribute statement like "node [shape=circle fontSize=16]".
977 * Available keywords are 'node', 'edge', 'graph'.
978 * The previous list with default attributes will be replaced
979 *
980 * @param {object} graph
981 * @returns {string | null} keyword Returns the name of the parsed attribute
982 * (node, edge, graph), or null if nothing
983 * is parsed.
984 */
985function parseAttributeStatement(graph) {
986 // attribute statements
987 if (token === "node") {
988 getToken();
989
990 // node attributes
991 graph.node = parseAttributeList();
992 return "node";
993 } else if (token === "edge") {
994 getToken();
995
996 // edge attributes
997 graph.edge = parseAttributeList();
998 return "edge";
999 } else if (token === "graph") {
1000 getToken();
1001
1002 // graph attributes
1003 graph.graph = parseAttributeList();
1004 return "graph";
1005 }
1006
1007 return null;
1008}
1009
1010/**
1011 * parse a node statement
1012 *
1013 * @param {object} graph
1014 * @param {string | number} id
1015 */
1016function parseNodeStatement(graph, id) {
1017 // node statement
1018 var node = {
1019 id: id,
1020 };
1021 var attr = parseAttributeList();
1022 if (attr) {
1023 node.attr = attr;
1024 }
1025 addNode(graph, node);
1026
1027 // edge statements
1028 parseEdge(graph, id);
1029}
1030
1031/**
1032 * Parse an edge or a series of edges
1033 *
1034 * @param {object} graph
1035 * @param {string | number} from Id of the from node
1036 */
1037function parseEdge(graph, from) {
1038 while (token === "->" || token === "--") {
1039 var to;
1040 var type = token;
1041 getToken();
1042
1043 var subgraph = parseSubgraph(graph);
1044 if (subgraph) {
1045 to = subgraph;
1046 } else {
1047 if (tokenType != TOKENTYPE.IDENTIFIER) {
1048 throw newSyntaxError("Identifier or subgraph expected");
1049 }
1050 to = token;
1051 addNode(graph, {
1052 id: to,
1053 });
1054 getToken();
1055 }
1056
1057 // parse edge attributes
1058 var attr = parseAttributeList();
1059
1060 // create edge
1061 var edge = createEdge(graph, from, to, type, attr);
1062 addEdge(graph, edge);
1063
1064 from = to;
1065 }
1066}
1067
1068/**
1069 * Parse a set with attributes,
1070 * for example [label="1.000", shape=solid]
1071 *
1072 * @returns {object | null} attr
1073 */
1074function parseAttributeList() {
1075 var i;
1076 var attr = null;
1077
1078 // edge styles of dot and vis
1079 var edgeStyles = {
1080 dashed: true,
1081 solid: false,
1082 dotted: [1, 5],
1083 };
1084
1085 /**
1086 * Define arrow types.
1087 * vis currently supports types defined in 'arrowTypes'.
1088 * Details of arrow shapes are described in
1089 * http://www.graphviz.org/content/arrow-shapes
1090 */
1091 var arrowTypes = {
1092 dot: "circle",
1093 box: "box",
1094 crow: "crow",
1095 curve: "curve",
1096 icurve: "inv_curve",
1097 normal: "triangle",
1098 inv: "inv_triangle",
1099 diamond: "diamond",
1100 tee: "bar",
1101 vee: "vee",
1102 };
1103
1104 /**
1105 * 'attr_list' contains attributes for checking if some of them are affected
1106 * later. For instance, both of 'arrowhead' and 'dir' (edge style defined
1107 * in DOT) make changes to 'arrows' attribute in vis.
1108 */
1109 var attr_list = new Array();
1110 var attr_names = new Array(); // used for checking the case.
1111
1112 // parse attributes
1113 while (token === "[") {
1114 getToken();
1115 attr = {};
1116 while (token !== "" && token != "]") {
1117 if (tokenType != TOKENTYPE.IDENTIFIER) {
1118 throw newSyntaxError("Attribute name expected");
1119 }
1120 var name = token;
1121
1122 getToken();
1123 if (token != "=") {
1124 throw newSyntaxError("Equal sign = expected");
1125 }
1126 getToken();
1127
1128 if (tokenType != TOKENTYPE.IDENTIFIER) {
1129 throw newSyntaxError("Attribute value expected");
1130 }
1131 var value = token;
1132
1133 // convert from dot style to vis
1134 if (name === "style") {
1135 value = edgeStyles[value];
1136 }
1137
1138 var arrowType;
1139 if (name === "arrowhead") {
1140 arrowType = arrowTypes[value];
1141 name = "arrows";
1142 value = { to: { enabled: true, type: arrowType } };
1143 }
1144
1145 if (name === "arrowtail") {
1146 arrowType = arrowTypes[value];
1147 name = "arrows";
1148 value = { from: { enabled: true, type: arrowType } };
1149 }
1150
1151 attr_list.push({ attr: attr, name: name, value: value });
1152 attr_names.push(name);
1153
1154 getToken();
1155 if (token == ",") {
1156 getToken();
1157 }
1158 }
1159
1160 if (token != "]") {
1161 throw newSyntaxError("Bracket ] expected");
1162 }
1163 getToken();
1164 }
1165
1166 /**
1167 * As explained in [1], graphviz has limitations for combination of
1168 * arrow[head|tail] and dir. If attribute list includes 'dir',
1169 * following cases just be supported.
1170 * 1. both or none + arrowhead, arrowtail
1171 * 2. forward + arrowhead (arrowtail is not affedted)
1172 * 3. back + arrowtail (arrowhead is not affected)
1173 * [1] https://www.graphviz.org/doc/info/attrs.html#h:undir_note
1174 */
1175 if (attr_names.includes("dir")) {
1176 var idx = {}; // get index of 'arrows' and 'dir'
1177 idx.arrows = {};
1178 for (i = 0; i < attr_list.length; i++) {
1179 if (attr_list[i].name === "arrows") {
1180 if (attr_list[i].value.to != null) {
1181 idx.arrows.to = i;
1182 } else if (attr_list[i].value.from != null) {
1183 idx.arrows.from = i;
1184 } else {
1185 throw newSyntaxError("Invalid value of arrows");
1186 }
1187 } else if (attr_list[i].name === "dir") {
1188 idx.dir = i;
1189 }
1190 }
1191
1192 // first, add default arrow shape if it is not assigned to avoid error
1193 var dir_type = attr_list[idx.dir].value;
1194 if (!attr_names.includes("arrows")) {
1195 if (dir_type === "both") {
1196 attr_list.push({
1197 attr: attr_list[idx.dir].attr,
1198 name: "arrows",
1199 value: { to: { enabled: true } },
1200 });
1201 idx.arrows.to = attr_list.length - 1;
1202 attr_list.push({
1203 attr: attr_list[idx.dir].attr,
1204 name: "arrows",
1205 value: { from: { enabled: true } },
1206 });
1207 idx.arrows.from = attr_list.length - 1;
1208 } else if (dir_type === "forward") {
1209 attr_list.push({
1210 attr: attr_list[idx.dir].attr,
1211 name: "arrows",
1212 value: { to: { enabled: true } },
1213 });
1214 idx.arrows.to = attr_list.length - 1;
1215 } else if (dir_type === "back") {
1216 attr_list.push({
1217 attr: attr_list[idx.dir].attr,
1218 name: "arrows",
1219 value: { from: { enabled: true } },
1220 });
1221 idx.arrows.from = attr_list.length - 1;
1222 } else if (dir_type === "none") {
1223 attr_list.push({
1224 attr: attr_list[idx.dir].attr,
1225 name: "arrows",
1226 value: "",
1227 });
1228 idx.arrows.to = attr_list.length - 1;
1229 } else {
1230 throw newSyntaxError('Invalid dir type "' + dir_type + '"');
1231 }
1232 }
1233
1234 var from_type;
1235 var to_type;
1236 // update 'arrows' attribute from 'dir'.
1237 if (dir_type === "both") {
1238 // both of shapes of 'from' and 'to' are given
1239 if (idx.arrows.to && idx.arrows.from) {
1240 to_type = attr_list[idx.arrows.to].value.to.type;
1241 from_type = attr_list[idx.arrows.from].value.from.type;
1242 attr_list[idx.arrows.to] = {
1243 attr: attr_list[idx.arrows.to].attr,
1244 name: attr_list[idx.arrows.to].name,
1245 value: {
1246 to: { enabled: true, type: to_type },
1247 from: { enabled: true, type: from_type },
1248 },
1249 };
1250 attr_list.splice(idx.arrows.from, 1);
1251
1252 // shape of 'to' is assigned and use default to 'from'
1253 } else if (idx.arrows.to) {
1254 to_type = attr_list[idx.arrows.to].value.to.type;
1255 from_type = "arrow";
1256 attr_list[idx.arrows.to] = {
1257 attr: attr_list[idx.arrows.to].attr,
1258 name: attr_list[idx.arrows.to].name,
1259 value: {
1260 to: { enabled: true, type: to_type },
1261 from: { enabled: true, type: from_type },
1262 },
1263 };
1264
1265 // only shape of 'from' is assigned and use default for 'to'
1266 } else if (idx.arrows.from) {
1267 to_type = "arrow";
1268 from_type = attr_list[idx.arrows.from].value.from.type;
1269 attr_list[idx.arrows.from] = {
1270 attr: attr_list[idx.arrows.from].attr,
1271 name: attr_list[idx.arrows.from].name,
1272 value: {
1273 to: { enabled: true, type: to_type },
1274 from: { enabled: true, type: from_type },
1275 },
1276 };
1277 }
1278 } else if (dir_type === "back") {
1279 // given both of shapes, but use only 'from'
1280 if (idx.arrows.to && idx.arrows.from) {
1281 to_type = "";
1282 from_type = attr_list[idx.arrows.from].value.from.type;
1283 attr_list[idx.arrows.from] = {
1284 attr: attr_list[idx.arrows.from].attr,
1285 name: attr_list[idx.arrows.from].name,
1286 value: {
1287 to: { enabled: true, type: to_type },
1288 from: { enabled: true, type: from_type },
1289 },
1290 };
1291
1292 // given shape of 'to', but does not use it
1293 } else if (idx.arrows.to) {
1294 to_type = "";
1295 from_type = "arrow";
1296 idx.arrows.from = idx.arrows.to;
1297 attr_list[idx.arrows.from] = {
1298 attr: attr_list[idx.arrows.from].attr,
1299 name: attr_list[idx.arrows.from].name,
1300 value: {
1301 to: { enabled: true, type: to_type },
1302 from: { enabled: true, type: from_type },
1303 },
1304 };
1305
1306 // assign given 'from' shape
1307 } else if (idx.arrows.from) {
1308 to_type = "";
1309 from_type = attr_list[idx.arrows.from].value.from.type;
1310 attr_list[idx.arrows.to] = {
1311 attr: attr_list[idx.arrows.from].attr,
1312 name: attr_list[idx.arrows.from].name,
1313 value: {
1314 to: { enabled: true, type: to_type },
1315 from: { enabled: true, type: from_type },
1316 },
1317 };
1318 }
1319
1320 attr_list[idx.arrows.from] = {
1321 attr: attr_list[idx.arrows.from].attr,
1322 name: attr_list[idx.arrows.from].name,
1323 value: {
1324 from: {
1325 enabled: true,
1326 type: attr_list[idx.arrows.from].value.from.type,
1327 },
1328 },
1329 };
1330 } else if (dir_type === "none") {
1331 var idx_arrow;
1332 if (idx.arrows.to) {
1333 idx_arrow = idx.arrows.to;
1334 } else {
1335 idx_arrow = idx.arrows.from;
1336 }
1337
1338 attr_list[idx_arrow] = {
1339 attr: attr_list[idx_arrow].attr,
1340 name: attr_list[idx_arrow].name,
1341 value: "",
1342 };
1343 } else if (dir_type === "forward") {
1344 // given both of shapes, but use only 'to'
1345 if (idx.arrows.to && idx.arrows.from) {
1346 to_type = attr_list[idx.arrows.to].value.to.type;
1347 from_type = "";
1348 attr_list[idx.arrows.to] = {
1349 attr: attr_list[idx.arrows.to].attr,
1350 name: attr_list[idx.arrows.to].name,
1351 value: {
1352 to: { enabled: true, type: to_type },
1353 from: { enabled: true, type: from_type },
1354 },
1355 };
1356
1357 // assign given 'to' shape
1358 } else if (idx.arrows.to) {
1359 to_type = attr_list[idx.arrows.to].value.to.type;
1360 from_type = "";
1361 attr_list[idx.arrows.to] = {
1362 attr: attr_list[idx.arrows.to].attr,
1363 name: attr_list[idx.arrows.to].name,
1364 value: {
1365 to: { enabled: true, type: to_type },
1366 from: { enabled: true, type: from_type },
1367 },
1368 };
1369
1370 // given shape of 'from', but does not use it
1371 } else if (idx.arrows.from) {
1372 to_type = "arrow";
1373 from_type = "";
1374 idx.arrows.to = idx.arrows.from;
1375 attr_list[idx.arrows.to] = {
1376 attr: attr_list[idx.arrows.to].attr,
1377 name: attr_list[idx.arrows.to].name,
1378 value: {
1379 to: { enabled: true, type: to_type },
1380 from: { enabled: true, type: from_type },
1381 },
1382 };
1383 }
1384
1385 attr_list[idx.arrows.to] = {
1386 attr: attr_list[idx.arrows.to].attr,
1387 name: attr_list[idx.arrows.to].name,
1388 value: {
1389 to: { enabled: true, type: attr_list[idx.arrows.to].value.to.type },
1390 },
1391 };
1392 } else {
1393 throw newSyntaxError('Invalid dir type "' + dir_type + '"');
1394 }
1395
1396 // remove 'dir' attribute no need anymore
1397 attr_list.splice(idx.dir, 1);
1398 }
1399
1400 // parse 'penwidth'
1401 var nof_attr_list;
1402 if (attr_names.includes("penwidth")) {
1403 var tmp_attr_list = [];
1404
1405 nof_attr_list = attr_list.length;
1406 for (i = 0; i < nof_attr_list; i++) {
1407 // exclude 'width' from attr_list if 'penwidth' exists
1408 if (attr_list[i].name !== "width") {
1409 if (attr_list[i].name === "penwidth") {
1410 attr_list[i].name = "width";
1411 }
1412 tmp_attr_list.push(attr_list[i]);
1413 }
1414 }
1415 attr_list = tmp_attr_list;
1416 }
1417
1418 nof_attr_list = attr_list.length;
1419 for (i = 0; i < nof_attr_list; i++) {
1420 setValue(attr_list[i].attr, attr_list[i].name, attr_list[i].value);
1421 }
1422
1423 return attr;
1424}
1425
1426/**
1427 * Create a syntax error with extra information on current token and index.
1428 *
1429 * @param {string} message
1430 * @returns {SyntaxError} err
1431 */
1432function newSyntaxError(message) {
1433 return new SyntaxError(
1434 message + ', got "' + chop(token, 30) + '" (char ' + index + ")"
1435 );
1436}
1437
1438/**
1439 * Chop off text after a maximum length
1440 *
1441 * @param {string} text
1442 * @param {number} maxLength
1443 * @returns {string}
1444 */
1445function chop(text, maxLength) {
1446 return text.length <= maxLength ? text : text.substr(0, 27) + "...";
1447}
1448
1449/**
1450 * Execute a function fn for each pair of elements in two arrays
1451 *
1452 * @param {Array | *} array1
1453 * @param {Array | *} array2
1454 * @param {Function} fn
1455 */
1456function forEach2(array1, array2, fn) {
1457 if (Array.isArray(array1)) {
1458 array1.forEach(function (elem1) {
1459 if (Array.isArray(array2)) {
1460 array2.forEach(function (elem2) {
1461 fn(elem1, elem2);
1462 });
1463 } else {
1464 fn(elem1, array2);
1465 }
1466 });
1467 } else {
1468 if (Array.isArray(array2)) {
1469 array2.forEach(function (elem2) {
1470 fn(array1, elem2);
1471 });
1472 } else {
1473 fn(array1, array2);
1474 }
1475 }
1476}
1477
1478/**
1479 * Set a nested property on an object
1480 * When nested objects are missing, they will be created.
1481 * For example setProp({}, 'font.color', 'red') will return {font: {color: 'red'}}
1482 *
1483 * @param {object} object
1484 * @param {string} path A dot separated string like 'font.color'
1485 * @param {*} value Value for the property
1486 * @returns {object} Returns the original object, allows for chaining.
1487 */
1488function setProp(object, path, value) {
1489 var names = path.split(".");
1490 var prop = names.pop();
1491
1492 // traverse over the nested objects
1493 var obj = object;
1494 for (var i = 0; i < names.length; i++) {
1495 var name = names[i];
1496 if (!(name in obj)) {
1497 obj[name] = {};
1498 }
1499 obj = obj[name];
1500 }
1501
1502 // set the property value
1503 obj[prop] = value;
1504
1505 return object;
1506}
1507
1508/**
1509 * Convert an object with DOT attributes to their vis.js equivalents.
1510 *
1511 * @param {object} attr Object with DOT attributes
1512 * @param {object} mapping
1513 * @returns {object} Returns an object with vis.js attributes
1514 */
1515function convertAttr(attr, mapping) {
1516 var converted = {};
1517
1518 for (var prop in attr) {
1519 if (attr.hasOwnProperty(prop)) {
1520 var visProp = mapping[prop];
1521 if (Array.isArray(visProp)) {
1522 visProp.forEach(function (visPropI) {
1523 setProp(converted, visPropI, attr[prop]);
1524 });
1525 } else if (typeof visProp === "string") {
1526 setProp(converted, visProp, attr[prop]);
1527 } else {
1528 setProp(converted, prop, attr[prop]);
1529 }
1530 }
1531 }
1532
1533 return converted;
1534}
1535
1536/**
1537 * Convert a string containing a graph in DOT language into a map containing
1538 * with nodes and edges in the format of graph.
1539 *
1540 * @param {string} data Text containing a graph in DOT-notation
1541 * @returns {object} graphData
1542 */
1543function DOTToGraph(data) {
1544 // parse the DOT file
1545 var dotData = parseDOT(data);
1546 var graphData = {
1547 nodes: [],
1548 edges: [],
1549 options: {},
1550 };
1551
1552 // copy the nodes
1553 if (dotData.nodes) {
1554 dotData.nodes.forEach(function (dotNode) {
1555 var graphNode = {
1556 id: dotNode.id,
1557 label: String(dotNode.label || dotNode.id),
1558 };
1559 merge(graphNode, convertAttr(dotNode.attr, NODE_ATTR_MAPPING));
1560 if (graphNode.image) {
1561 graphNode.shape = "image";
1562 }
1563 graphData.nodes.push(graphNode);
1564 });
1565 }
1566
1567 // copy the edges
1568 if (dotData.edges) {
1569 /**
1570 * Convert an edge in DOT format to an edge with VisGraph format
1571 *
1572 * @param {object} dotEdge
1573 * @returns {object} graphEdge
1574 */
1575 var convertEdge = function (dotEdge) {
1576 var graphEdge = {
1577 from: dotEdge.from,
1578 to: dotEdge.to,
1579 };
1580 merge(graphEdge, convertAttr(dotEdge.attr, EDGE_ATTR_MAPPING));
1581
1582 // Add arrows attribute to default styled arrow.
1583 // The reason why default style is not added in parseAttributeList() is
1584 // because only default is cleared before here.
1585 if (graphEdge.arrows == null && dotEdge.type === "->") {
1586 graphEdge.arrows = "to";
1587 }
1588
1589 return graphEdge;
1590 };
1591
1592 dotData.edges.forEach(function (dotEdge) {
1593 var from, to;
1594 if (dotEdge.from instanceof Object) {
1595 from = dotEdge.from.nodes;
1596 } else {
1597 from = {
1598 id: dotEdge.from,
1599 };
1600 }
1601
1602 if (dotEdge.to instanceof Object) {
1603 to = dotEdge.to.nodes;
1604 } else {
1605 to = {
1606 id: dotEdge.to,
1607 };
1608 }
1609
1610 if (dotEdge.from instanceof Object && dotEdge.from.edges) {
1611 dotEdge.from.edges.forEach(function (subEdge) {
1612 var graphEdge = convertEdge(subEdge);
1613 graphData.edges.push(graphEdge);
1614 });
1615 }
1616
1617 forEach2(from, to, function (from, to) {
1618 var subEdge = createEdge(
1619 graphData,
1620 from.id,
1621 to.id,
1622 dotEdge.type,
1623 dotEdge.attr
1624 );
1625 var graphEdge = convertEdge(subEdge);
1626 graphData.edges.push(graphEdge);
1627 });
1628
1629 if (dotEdge.to instanceof Object && dotEdge.to.edges) {
1630 dotEdge.to.edges.forEach(function (subEdge) {
1631 var graphEdge = convertEdge(subEdge);
1632 graphData.edges.push(graphEdge);
1633 });
1634 }
1635 });
1636 }
1637
1638 // copy the options
1639 if (dotData.attr) {
1640 graphData.options = dotData.attr;
1641 }
1642
1643 return graphData;
1644}
1645
1646/* eslint-enable no-var */
1647/* eslint-enable no-unused-vars */
1648/* eslint-enable no-prototype-builtins */
1649
1650var dotparser = /*#__PURE__*/Object.freeze({
1651 __proto__: null,
1652 parseDOT: parseDOT,
1653 DOTToGraph: DOTToGraph
1654});
1655
1656/**
1657 * Convert Gephi to Vis.
1658 *
1659 * @param gephiJSON - The parsed JSON data in Gephi format.
1660 * @param optionsObj - Additional options.
1661 *
1662 * @returns The converted data ready to be used in Vis.
1663 */
1664function parseGephi(gephiJSON, optionsObj) {
1665 const options = {
1666 edges: {
1667 inheritColor: false,
1668 },
1669 nodes: {
1670 fixed: false,
1671 parseColor: false,
1672 },
1673 };
1674 if (optionsObj != null) {
1675 if (optionsObj.fixed != null) {
1676 options.nodes.fixed = optionsObj.fixed;
1677 }
1678 if (optionsObj.parseColor != null) {
1679 options.nodes.parseColor = optionsObj.parseColor;
1680 }
1681 if (optionsObj.inheritColor != null) {
1682 options.edges.inheritColor = optionsObj.inheritColor;
1683 }
1684 }
1685 const gEdges = gephiJSON.edges;
1686 const vEdges = gEdges.map((gEdge) => {
1687 const vEdge = {
1688 from: gEdge.source,
1689 id: gEdge.id,
1690 to: gEdge.target,
1691 };
1692 if (gEdge.attributes != null) {
1693 vEdge.attributes = gEdge.attributes;
1694 }
1695 if (gEdge.label != null) {
1696 vEdge.label = gEdge.label;
1697 }
1698 if (gEdge.attributes != null && gEdge.attributes.title != null) {
1699 vEdge.title = gEdge.attributes.title;
1700 }
1701 if (gEdge.type === "Directed") {
1702 vEdge.arrows = "to";
1703 }
1704 // edge['value'] = gEdge.attributes != null ? gEdge.attributes.Weight : undefined;
1705 // edge['width'] = edge['value'] != null ? undefined : edgegEdge.size;
1706 if (gEdge.color && options.edges.inheritColor === false) {
1707 vEdge.color = gEdge.color;
1708 }
1709 return vEdge;
1710 });
1711 const vNodes = gephiJSON.nodes.map((gNode) => {
1712 const vNode = {
1713 id: gNode.id,
1714 fixed: options.nodes.fixed && gNode.x != null && gNode.y != null,
1715 };
1716 if (gNode.attributes != null) {
1717 vNode.attributes = gNode.attributes;
1718 }
1719 if (gNode.label != null) {
1720 vNode.label = gNode.label;
1721 }
1722 if (gNode.size != null) {
1723 vNode.size = gNode.size;
1724 }
1725 if (gNode.attributes != null && gNode.attributes.title != null) {
1726 vNode.title = gNode.attributes.title;
1727 }
1728 if (gNode.title != null) {
1729 vNode.title = gNode.title;
1730 }
1731 if (gNode.x != null) {
1732 vNode.x = gNode.x;
1733 }
1734 if (gNode.y != null) {
1735 vNode.y = gNode.y;
1736 }
1737 if (gNode.color != null) {
1738 if (options.nodes.parseColor === true) {
1739 vNode.color = gNode.color;
1740 }
1741 else {
1742 vNode.color = {
1743 background: gNode.color,
1744 border: gNode.color,
1745 highlight: {
1746 background: gNode.color,
1747 border: gNode.color,
1748 },
1749 hover: {
1750 background: gNode.color,
1751 border: gNode.color,
1752 },
1753 };
1754 }
1755 }
1756 return vNode;
1757 });
1758 return { nodes: vNodes, edges: vEdges };
1759}
1760
1761var gephiParser = /*#__PURE__*/Object.freeze({
1762 __proto__: null,
1763 parseGephi: parseGephi
1764});
1765
1766// English
1767const en = {
1768 addDescription: "Click in an empty space to place a new node.",
1769 addEdge: "Add Edge",
1770 addNode: "Add Node",
1771 back: "Back",
1772 close: "Close",
1773 createEdgeError: "Cannot link edges to a cluster.",
1774 del: "Delete selected",
1775 deleteClusterError: "Clusters cannot be deleted.",
1776 edgeDescription: "Click on a node and drag the edge to another node to connect them.",
1777 edit: "Edit",
1778 editClusterError: "Clusters cannot be edited.",
1779 editEdge: "Edit Edge",
1780 editEdgeDescription: "Click on the control points and drag them to a node to connect to it.",
1781 editNode: "Edit Node",
1782};
1783// German
1784const de = {
1785 addDescription: "Klicke auf eine freie Stelle, um einen neuen Knoten zu plazieren.",
1786 addEdge: "Kante hinzuf\u00fcgen",
1787 addNode: "Knoten hinzuf\u00fcgen",
1788 back: "Zur\u00fcck",
1789 close: "Schließen",
1790 createEdgeError: "Es ist nicht m\u00f6glich, Kanten mit Clustern zu verbinden.",
1791 del: "L\u00f6sche Auswahl",
1792 deleteClusterError: "Cluster k\u00f6nnen nicht gel\u00f6scht werden.",
1793 edgeDescription: "Klicke auf einen Knoten und ziehe die Kante zu einem anderen Knoten, um diese zu verbinden.",
1794 edit: "Editieren",
1795 editClusterError: "Cluster k\u00f6nnen nicht editiert werden.",
1796 editEdge: "Kante editieren",
1797 editEdgeDescription: "Klicke auf die Verbindungspunkte und ziehe diese auf einen Knoten, um sie zu verbinden.",
1798 editNode: "Knoten editieren",
1799};
1800// Spanish
1801const es = {
1802 addDescription: "Haga clic en un lugar vac\u00edo para colocar un nuevo nodo.",
1803 addEdge: "A\u00f1adir arista",
1804 addNode: "A\u00f1adir nodo",
1805 back: "Atr\u00e1s",
1806 close: "Cerrar",
1807 createEdgeError: "No se puede conectar una arista a un grupo.",
1808 del: "Eliminar selecci\u00f3n",
1809 deleteClusterError: "No es posible eliminar grupos.",
1810 edgeDescription: "Haga clic en un nodo y arrastre la arista hacia otro nodo para conectarlos.",
1811 edit: "Editar",
1812 editClusterError: "No es posible editar grupos.",
1813 editEdge: "Editar arista",
1814 editEdgeDescription: "Haga clic en un punto de control y arrastrelo a un nodo para conectarlo.",
1815 editNode: "Editar nodo",
1816};
1817//Italiano
1818const it = {
1819 addDescription: "Clicca per aggiungere un nuovo nodo",
1820 addEdge: "Aggiungi un vertice",
1821 addNode: "Aggiungi un nodo",
1822 back: "Indietro",
1823 close: "Chiudere",
1824 createEdgeError: "Non si possono collegare vertici ad un cluster",
1825 del: "Cancella la selezione",
1826 deleteClusterError: "I cluster non possono essere cancellati",
1827 edgeDescription: "Clicca su un nodo e trascinalo ad un altro nodo per connetterli.",
1828 edit: "Modifica",
1829 editClusterError: "I clusters non possono essere modificati.",
1830 editEdge: "Modifica il vertice",
1831 editEdgeDescription: "Clicca sui Punti di controllo e trascinali ad un nodo per connetterli.",
1832 editNode: "Modifica il nodo",
1833};
1834// Dutch
1835const nl = {
1836 addDescription: "Klik op een leeg gebied om een nieuwe node te maken.",
1837 addEdge: "Link toevoegen",
1838 addNode: "Node toevoegen",
1839 back: "Terug",
1840 close: "Sluiten",
1841 createEdgeError: "Kan geen link maken naar een cluster.",
1842 del: "Selectie verwijderen",
1843 deleteClusterError: "Clusters kunnen niet worden verwijderd.",
1844 edgeDescription: "Klik op een node en sleep de link naar een andere node om ze te verbinden.",
1845 edit: "Wijzigen",
1846 editClusterError: "Clusters kunnen niet worden aangepast.",
1847 editEdge: "Link wijzigen",
1848 editEdgeDescription: "Klik op de verbindingspunten en sleep ze naar een node om daarmee te verbinden.",
1849 editNode: "Node wijzigen",
1850};
1851// Portuguese Brazil
1852const pt = {
1853 addDescription: "Clique em um espaço em branco para adicionar um novo nó",
1854 addEdge: "Adicionar aresta",
1855 addNode: "Adicionar nó",
1856 back: "Voltar",
1857 close: "Fechar",
1858 createEdgeError: "Não foi possível linkar arestas a um cluster.",
1859 del: "Remover selecionado",
1860 deleteClusterError: "Clusters não puderam ser removidos.",
1861 edgeDescription: "Clique em um nó e arraste a aresta até outro nó para conectá-los",
1862 edit: "Editar",
1863 editClusterError: "Clusters não puderam ser editados.",
1864 editEdge: "Editar aresta",
1865 editEdgeDescription: "Clique nos pontos de controle e os arraste para um nó para conectá-los",
1866 editNode: "Editar nó",
1867};
1868// Russian
1869const ru = {
1870 addDescription: "Кликните в свободное место, чтобы добавить новый узел.",
1871 addEdge: "Добавить ребро",
1872 addNode: "Добавить узел",
1873 back: "Назад",
1874 close: "Закрывать",
1875 createEdgeError: "Невозможно соединить ребра в кластер.",
1876 del: "Удалить выбранное",
1877 deleteClusterError: "Кластеры не могут быть удалены",
1878 edgeDescription: "Кликните на узел и протяните ребро к другому узлу, чтобы соединить их.",
1879 edit: "Редактировать",
1880 editClusterError: "Кластеры недоступны для редактирования.",
1881 editEdge: "Редактировать ребро",
1882 editEdgeDescription: "Кликните на контрольные точки и перетащите их в узел, чтобы подключиться к нему.",
1883 editNode: "Редактировать узел",
1884};
1885// Chinese
1886const cn = {
1887 addDescription: "单击空白处放置新节点。",
1888 addEdge: "添加连接线",
1889 addNode: "添加节点",
1890 back: "返回",
1891 close: "關閉",
1892 createEdgeError: "无法将连接线连接到群集。",
1893 del: "删除选定",
1894 deleteClusterError: "无法删除群集。",
1895 edgeDescription: "单击某个节点并将该连接线拖动到另一个节点以连接它们。",
1896 edit: "编辑",
1897 editClusterError: "无法编辑群集。",
1898 editEdge: "编辑连接线",
1899 editEdgeDescription: "单击控制节点并将它们拖到节点上连接。",
1900 editNode: "编辑节点",
1901};
1902// Ukrainian
1903const uk = {
1904 addDescription: "Kлікніть на вільне місце, щоб додати новий вузол.",
1905 addEdge: "Додати край",
1906 addNode: "Додати вузол",
1907 back: "Назад",
1908 close: "Закрити",
1909 createEdgeError: "Не можливо об'єднати краї в групу.",
1910 del: "Видалити обране",
1911 deleteClusterError: "Групи не можуть бути видалені.",
1912 edgeDescription: "Клікніть на вузол і перетягніть край до іншого вузла, щоб їх з'єднати.",
1913 edit: "Редагувати",
1914 editClusterError: "Групи недоступні для редагування.",
1915 editEdge: "Редагувати край",
1916 editEdgeDescription: "Клікніть на контрольні точки і перетягніть їх у вузол, щоб підключитися до нього.",
1917 editNode: "Редагувати вузол",
1918};
1919// French
1920const fr = {
1921 addDescription: "Cliquez dans un endroit vide pour placer un nœud.",
1922 addEdge: "Ajouter un lien",
1923 addNode: "Ajouter un nœud",
1924 back: "Retour",
1925 close: "Fermer",
1926 createEdgeError: "Impossible de créer un lien vers un cluster.",
1927 del: "Effacer la sélection",
1928 deleteClusterError: "Les clusters ne peuvent pas être effacés.",
1929 edgeDescription: "Cliquez sur un nœud et glissez le lien vers un autre nœud pour les connecter.",
1930 edit: "Éditer",
1931 editClusterError: "Les clusters ne peuvent pas être édités.",
1932 editEdge: "Éditer le lien",
1933 editEdgeDescription: "Cliquez sur les points de contrôle et glissez-les pour connecter un nœud.",
1934 editNode: "Éditer le nœud",
1935};
1936// Czech
1937const cs = {
1938 addDescription: "Kluknutím do prázdného prostoru můžete přidat nový vrchol.",
1939 addEdge: "Přidat hranu",
1940 addNode: "Přidat vrchol",
1941 back: "Zpět",
1942 close: "Zavřít",
1943 createEdgeError: "Nelze připojit hranu ke shluku.",
1944 del: "Smazat výběr",
1945 deleteClusterError: "Nelze mazat shluky.",
1946 edgeDescription: "Přetažením z jednoho vrcholu do druhého můžete spojit tyto vrcholy novou hranou.",
1947 edit: "Upravit",
1948 editClusterError: "Nelze upravovat shluky.",
1949 editEdge: "Upravit hranu",
1950 editEdgeDescription: "Přetažením kontrolního vrcholu hrany ji můžete připojit k jinému vrcholu.",
1951 editNode: "Upravit vrchol",
1952};
1953
1954var locales = /*#__PURE__*/Object.freeze({
1955 __proto__: null,
1956 en: en,
1957 de: de,
1958 es: es,
1959 it: it,
1960 nl: nl,
1961 pt: pt,
1962 ru: ru,
1963 cn: cn,
1964 uk: uk,
1965 fr: fr,
1966 cs: cs
1967});
1968
1969/**
1970 * Normalizes language code into the format used internally.
1971 *
1972 * @param locales - All the available locales.
1973 * @param rawCode - The original code as supplied by the user.
1974 *
1975 * @returns Language code in the format language-COUNTRY or language, eventually
1976 * fallbacks to en.
1977 */
1978function normalizeLanguageCode(locales, rawCode) {
1979 try {
1980 const [rawLanguage, rawCountry] = rawCode.split(/[-_ /]/, 2);
1981 const language = rawLanguage != null ? rawLanguage.toLowerCase() : null;
1982 const country = rawCountry != null ? rawCountry.toUpperCase() : null;
1983 if (language && country) {
1984 const code = language + "-" + country;
1985 if (Object.prototype.hasOwnProperty.call(locales, code)) {
1986 return code;
1987 }
1988 else {
1989 console.warn(`Unknown variant ${country} of language ${language}.`);
1990 }
1991 }
1992 if (language) {
1993 const code = language;
1994 if (Object.prototype.hasOwnProperty.call(locales, code)) {
1995 return code;
1996 }
1997 else {
1998 console.warn(`Unknown language ${language}`);
1999 }
2000 }
2001 console.warn(`Unknown locale ${rawCode}, falling back to English.`);
2002 return "en";
2003 }
2004 catch (error) {
2005 console.error(error);
2006 console.warn(`Unexpected error while normalizing locale ${rawCode}, falling back to English.`);
2007 return "en";
2008 }
2009}
2010
2011/**
2012 * Associates a canvas to a given image, containing a number of renderings
2013 * of the image at various sizes.
2014 *
2015 * This technique is known as 'mipmapping'.
2016 *
2017 * NOTE: Images can also be of type 'data:svg+xml`. This code also works
2018 * for svg, but the mipmapping may not be necessary.
2019 *
2020 * @param {Image} image
2021 */
2022class CachedImage {
2023 /**
2024 * @ignore
2025 */
2026 constructor() {
2027 this.NUM_ITERATIONS = 4; // Number of items in the coordinates array
2028
2029 this.image = new Image();
2030 this.canvas = document.createElement("canvas");
2031 }
2032
2033 /**
2034 * Called when the image has been successfully loaded.
2035 */
2036 init() {
2037 if (this.initialized()) return;
2038
2039 this.src = this.image.src; // For same interface with Image
2040 const w = this.image.width;
2041 const h = this.image.height;
2042
2043 // Ease external access
2044 this.width = w;
2045 this.height = h;
2046
2047 const h2 = Math.floor(h / 2);
2048 const h4 = Math.floor(h / 4);
2049 const h8 = Math.floor(h / 8);
2050 const h16 = Math.floor(h / 16);
2051
2052 const w2 = Math.floor(w / 2);
2053 const w4 = Math.floor(w / 4);
2054 const w8 = Math.floor(w / 8);
2055 const w16 = Math.floor(w / 16);
2056
2057 // Make canvas as small as possible
2058 this.canvas.width = 3 * w4;
2059 this.canvas.height = h2;
2060
2061 // Coordinates and sizes of images contained in the canvas
2062 // Values per row: [top x, left y, width, height]
2063
2064 this.coordinates = [
2065 [0, 0, w2, h2],
2066 [w2, 0, w4, h4],
2067 [w2, h4, w8, h8],
2068 [5 * w8, h4, w16, h16],
2069 ];
2070
2071 this._fillMipMap();
2072 }
2073
2074 /**
2075 * @returns {boolean} true if init() has been called, false otherwise.
2076 */
2077 initialized() {
2078 return this.coordinates !== undefined;
2079 }
2080
2081 /**
2082 * Redraw main image in various sizes to the context.
2083 *
2084 * The rationale behind this is to reduce artefacts due to interpolation
2085 * at differing zoom levels.
2086 *
2087 * Source: http://stackoverflow.com/q/18761404/1223531
2088 *
2089 * This methods takes the resizing out of the drawing loop, in order to
2090 * reduce performance overhead.
2091 *
2092 * TODO: The code assumes that a 2D context can always be gotten. This is
2093 * not necessarily true! OTOH, if not true then usage of this class
2094 * is senseless.
2095 *
2096 * @private
2097 */
2098 _fillMipMap() {
2099 const ctx = this.canvas.getContext("2d");
2100
2101 // First zoom-level comes from the image
2102 const to = this.coordinates[0];
2103 ctx.drawImage(this.image, to[0], to[1], to[2], to[3]);
2104
2105 // The rest are copy actions internal to the canvas/context
2106 for (let iterations = 1; iterations < this.NUM_ITERATIONS; iterations++) {
2107 const from = this.coordinates[iterations - 1];
2108 const to = this.coordinates[iterations];
2109
2110 ctx.drawImage(
2111 this.canvas,
2112 from[0],
2113 from[1],
2114 from[2],
2115 from[3],
2116 to[0],
2117 to[1],
2118 to[2],
2119 to[3]
2120 );
2121 }
2122 }
2123
2124 /**
2125 * Draw the image, using the mipmap if necessary.
2126 *
2127 * MipMap is only used if param factor > 2; otherwise, original bitmap
2128 * is resized. This is also used to skip mipmap usage, e.g. by setting factor = 1
2129 *
2130 * Credits to 'Alex de Mulder' for original implementation.
2131 *
2132 * @param {CanvasRenderingContext2D} ctx context on which to draw zoomed image
2133 * @param {Float} factor scale factor at which to draw
2134 * @param {number} left
2135 * @param {number} top
2136 * @param {number} width
2137 * @param {number} height
2138 */
2139 drawImageAtPosition(ctx, factor, left, top, width, height) {
2140 if (!this.initialized()) return; //can't draw image yet not intialized
2141
2142 if (factor > 2) {
2143 // Determine which zoomed image to use
2144 factor *= 0.5;
2145 let iterations = 0;
2146 while (factor > 2 && iterations < this.NUM_ITERATIONS) {
2147 factor *= 0.5;
2148 iterations += 1;
2149 }
2150
2151 if (iterations >= this.NUM_ITERATIONS) {
2152 iterations = this.NUM_ITERATIONS - 1;
2153 }
2154 //console.log("iterations: " + iterations);
2155
2156 const from = this.coordinates[iterations];
2157 ctx.drawImage(
2158 this.canvas,
2159 from[0],
2160 from[1],
2161 from[2],
2162 from[3],
2163 left,
2164 top,
2165 width,
2166 height
2167 );
2168 } else {
2169 // Draw image directly
2170 ctx.drawImage(this.image, left, top, width, height);
2171 }
2172 }
2173}
2174
2175/**
2176 * This callback is a callback that accepts an Image.
2177 *
2178 * @callback ImageCallback
2179 * @param {Image} image
2180 */
2181
2182/**
2183 * This class loads images and keeps them stored.
2184 *
2185 * @param {ImageCallback} callback
2186 */
2187class Images {
2188 /**
2189 * @param {ImageCallback} callback
2190 */
2191 constructor(callback) {
2192 this.images = {};
2193 this.imageBroken = {};
2194 this.callback = callback;
2195 }
2196
2197 /**
2198 * @param {string} url The original Url that failed to load, if the broken image is successfully loaded it will be added to the cache using this Url as the key so that subsequent requests for this Url will return the broken image
2199 * @param {string} brokenUrl Url the broken image to try and load
2200 * @param {Image} imageToLoadBrokenUrlOn The image object
2201 */
2202 _tryloadBrokenUrl(url, brokenUrl, imageToLoadBrokenUrlOn) {
2203 //If these parameters aren't specified then exit the function because nothing constructive can be done
2204 if (url === undefined || imageToLoadBrokenUrlOn === undefined) return;
2205 if (brokenUrl === undefined) {
2206 console.warn("No broken url image defined");
2207 return;
2208 }
2209
2210 //Clear the old subscription to the error event and put a new in place that only handle errors in loading the brokenImageUrl
2211 imageToLoadBrokenUrlOn.image.onerror = () => {
2212 console.error("Could not load brokenImage:", brokenUrl);
2213 // cache item will contain empty image, this should be OK for default
2214 };
2215
2216 //Set the source of the image to the brokenUrl, this is actually what kicks off the loading of the broken image
2217 imageToLoadBrokenUrlOn.image.src = brokenUrl;
2218 }
2219
2220 /**
2221 *
2222 * @param {vis.Image} imageToRedrawWith
2223 * @private
2224 */
2225 _redrawWithImage(imageToRedrawWith) {
2226 if (this.callback) {
2227 this.callback(imageToRedrawWith);
2228 }
2229 }
2230
2231 /**
2232 * @param {string} url Url of the image
2233 * @param {string} brokenUrl Url of an image to use if the url image is not found
2234 * @returns {Image} img The image object
2235 */
2236 load(url, brokenUrl) {
2237 //Try and get the image from the cache, if successful then return the cached image
2238 const cachedImage = this.images[url];
2239 if (cachedImage) return cachedImage;
2240
2241 //Create a new image
2242 const img = new CachedImage();
2243
2244 // Need to add to cache here, otherwise final return will spawn different copies of the same image,
2245 // Also, there will be multiple loads of the same image.
2246 this.images[url] = img;
2247
2248 //Subscribe to the event that is raised if the image loads successfully
2249 img.image.onload = () => {
2250 // Properly init the cached item and then request a redraw
2251 this._fixImageCoordinates(img.image);
2252 img.init();
2253 this._redrawWithImage(img);
2254 };
2255
2256 //Subscribe to the event that is raised if the image fails to load
2257 img.image.onerror = () => {
2258 console.error("Could not load image:", url);
2259 //Try and load the image specified by the brokenUrl using
2260 this._tryloadBrokenUrl(url, brokenUrl, img);
2261 };
2262
2263 //Set the source of the image to the url, this is what actually kicks off the loading of the image
2264 img.image.src = url;
2265
2266 //Return the new image
2267 return img;
2268 }
2269
2270 /**
2271 * IE11 fix -- thanks dponch!
2272 *
2273 * Local helper function
2274 *
2275 * @param {vis.Image} imageToCache
2276 * @private
2277 */
2278 _fixImageCoordinates(imageToCache) {
2279 if (imageToCache.width === 0) {
2280 document.body.appendChild(imageToCache);
2281 imageToCache.width = imageToCache.offsetWidth;
2282 imageToCache.height = imageToCache.offsetHeight;
2283 document.body.removeChild(imageToCache);
2284 }
2285 }
2286}
2287
2288/**
2289 * This class can store groups and options specific for groups.
2290 */
2291class Groups {
2292 /**
2293 * @ignore
2294 */
2295 constructor() {
2296 this.clear();
2297 this._defaultIndex = 0;
2298 this._groupIndex = 0;
2299
2300 this._defaultGroups = [
2301 {
2302 border: "#2B7CE9",
2303 background: "#97C2FC",
2304 highlight: { border: "#2B7CE9", background: "#D2E5FF" },
2305 hover: { border: "#2B7CE9", background: "#D2E5FF" },
2306 }, // 0: blue
2307 {
2308 border: "#FFA500",
2309 background: "#FFFF00",
2310 highlight: { border: "#FFA500", background: "#FFFFA3" },
2311 hover: { border: "#FFA500", background: "#FFFFA3" },
2312 }, // 1: yellow
2313 {
2314 border: "#FA0A10",
2315 background: "#FB7E81",
2316 highlight: { border: "#FA0A10", background: "#FFAFB1" },
2317 hover: { border: "#FA0A10", background: "#FFAFB1" },
2318 }, // 2: red
2319 {
2320 border: "#41A906",
2321 background: "#7BE141",
2322 highlight: { border: "#41A906", background: "#A1EC76" },
2323 hover: { border: "#41A906", background: "#A1EC76" },
2324 }, // 3: green
2325 {
2326 border: "#E129F0",
2327 background: "#EB7DF4",
2328 highlight: { border: "#E129F0", background: "#F0B3F5" },
2329 hover: { border: "#E129F0", background: "#F0B3F5" },
2330 }, // 4: magenta
2331 {
2332 border: "#7C29F0",
2333 background: "#AD85E4",
2334 highlight: { border: "#7C29F0", background: "#D3BDF0" },
2335 hover: { border: "#7C29F0", background: "#D3BDF0" },
2336 }, // 5: purple
2337 {
2338 border: "#C37F00",
2339 background: "#FFA807",
2340 highlight: { border: "#C37F00", background: "#FFCA66" },
2341 hover: { border: "#C37F00", background: "#FFCA66" },
2342 }, // 6: orange
2343 {
2344 border: "#4220FB",
2345 background: "#6E6EFD",
2346 highlight: { border: "#4220FB", background: "#9B9BFD" },
2347 hover: { border: "#4220FB", background: "#9B9BFD" },
2348 }, // 7: darkblue
2349 {
2350 border: "#FD5A77",
2351 background: "#FFC0CB",
2352 highlight: { border: "#FD5A77", background: "#FFD1D9" },
2353 hover: { border: "#FD5A77", background: "#FFD1D9" },
2354 }, // 8: pink
2355 {
2356 border: "#4AD63A",
2357 background: "#C2FABC",
2358 highlight: { border: "#4AD63A", background: "#E6FFE3" },
2359 hover: { border: "#4AD63A", background: "#E6FFE3" },
2360 }, // 9: mint
2361
2362 {
2363 border: "#990000",
2364 background: "#EE0000",
2365 highlight: { border: "#BB0000", background: "#FF3333" },
2366 hover: { border: "#BB0000", background: "#FF3333" },
2367 }, // 10:bright red
2368
2369 {
2370 border: "#FF6000",
2371 background: "#FF6000",
2372 highlight: { border: "#FF6000", background: "#FF6000" },
2373 hover: { border: "#FF6000", background: "#FF6000" },
2374 }, // 12: real orange
2375 {
2376 border: "#97C2FC",
2377 background: "#2B7CE9",
2378 highlight: { border: "#D2E5FF", background: "#2B7CE9" },
2379 hover: { border: "#D2E5FF", background: "#2B7CE9" },
2380 }, // 13: blue
2381 {
2382 border: "#399605",
2383 background: "#255C03",
2384 highlight: { border: "#399605", background: "#255C03" },
2385 hover: { border: "#399605", background: "#255C03" },
2386 }, // 14: green
2387 {
2388 border: "#B70054",
2389 background: "#FF007E",
2390 highlight: { border: "#B70054", background: "#FF007E" },
2391 hover: { border: "#B70054", background: "#FF007E" },
2392 }, // 15: magenta
2393 {
2394 border: "#AD85E4",
2395 background: "#7C29F0",
2396 highlight: { border: "#D3BDF0", background: "#7C29F0" },
2397 hover: { border: "#D3BDF0", background: "#7C29F0" },
2398 }, // 16: purple
2399 {
2400 border: "#4557FA",
2401 background: "#000EA1",
2402 highlight: { border: "#6E6EFD", background: "#000EA1" },
2403 hover: { border: "#6E6EFD", background: "#000EA1" },
2404 }, // 17: darkblue
2405 {
2406 border: "#FFC0CB",
2407 background: "#FD5A77",
2408 highlight: { border: "#FFD1D9", background: "#FD5A77" },
2409 hover: { border: "#FFD1D9", background: "#FD5A77" },
2410 }, // 18: pink
2411 {
2412 border: "#C2FABC",
2413 background: "#74D66A",
2414 highlight: { border: "#E6FFE3", background: "#74D66A" },
2415 hover: { border: "#E6FFE3", background: "#74D66A" },
2416 }, // 19: mint
2417
2418 {
2419 border: "#EE0000",
2420 background: "#990000",
2421 highlight: { border: "#FF3333", background: "#BB0000" },
2422 hover: { border: "#FF3333", background: "#BB0000" },
2423 }, // 20:bright red
2424 ];
2425
2426 this.options = {};
2427 this.defaultOptions = {
2428 useDefaultGroups: true,
2429 };
2430 Object.assign(this.options, this.defaultOptions);
2431 }
2432
2433 /**
2434 *
2435 * @param {object} options
2436 */
2437 setOptions(options) {
2438 const optionFields = ["useDefaultGroups"];
2439
2440 if (options !== undefined) {
2441 for (const groupName in options) {
2442 if (Object.prototype.hasOwnProperty.call(options, groupName)) {
2443 if (optionFields.indexOf(groupName) === -1) {
2444 const group = options[groupName];
2445 this.add(groupName, group);
2446 }
2447 }
2448 }
2449 }
2450 }
2451
2452 /**
2453 * Clear all groups
2454 */
2455 clear() {
2456 this._groups = new Map();
2457 this._groupNames = [];
2458 }
2459
2460 /**
2461 * Get group options of a groupname.
2462 * If groupname is not found, a new group may be created.
2463 *
2464 * @param {*} groupname Can be a number, string, Date, etc.
2465 * @param {boolean} [shouldCreate=true] If true, create a new group
2466 * @returns {object} The found or created group
2467 */
2468 get(groupname, shouldCreate = true) {
2469 let group = this._groups.get(groupname);
2470
2471 if (group === undefined && shouldCreate) {
2472 if (
2473 this.options.useDefaultGroups === false &&
2474 this._groupNames.length > 0
2475 ) {
2476 // create new group
2477 const index = this._groupIndex % this._groupNames.length;
2478 ++this._groupIndex;
2479 group = {};
2480 group.color = this._groups.get(this._groupNames[index]);
2481 this._groups.set(groupname, group);
2482 } else {
2483 // create new group
2484 const index = this._defaultIndex % this._defaultGroups.length;
2485 this._defaultIndex++;
2486 group = {};
2487 group.color = this._defaultGroups[index];
2488 this._groups.set(groupname, group);
2489 }
2490 }
2491
2492 return group;
2493 }
2494
2495 /**
2496 * Add custom group style.
2497 *
2498 * @param {string} groupName - The name of the group, a new group will be
2499 * created if a group with the same name doesn't exist, otherwise the old
2500 * groups style will be overwritten.
2501 * @param {object} style - An object containing borderColor, backgroundColor,
2502 * etc.
2503 * @returns {object} The created group object.
2504 */
2505 add(groupName, style) {
2506 // Only push group name once to prevent duplicates which would consume more
2507 // RAM and also skew the distribution towards more often updated groups,
2508 // neither of which is desirable.
2509 if (!this._groups.has(groupName)) {
2510 this._groupNames.push(groupName);
2511 }
2512 this._groups.set(groupName, style);
2513 return style;
2514 }
2515}
2516
2517/**
2518 * Helper functions for components
2519 */
2520
2521/**
2522 * Determine values to use for (sub)options of 'chosen'.
2523 *
2524 * This option is either a boolean or an object whose values should be examined further.
2525 * The relevant structures are:
2526 *
2527 * - chosen: <boolean value>
2528 * - chosen: { subOption: <boolean or function> }
2529 *
2530 * Where subOption is 'node', 'edge' or 'label'.
2531 *
2532 * The intention of this method appears to be to set a specific priority to the options;
2533 * Since most properties are either bridged or merged into the local options objects, there
2534 * is not much point in handling them separately.
2535 * TODO: examine if 'most' in previous sentence can be replaced with 'all'. In that case, we
2536 * should be able to get rid of this method.
2537 *
2538 * @param {string} subOption option within object 'chosen' to consider; either 'node', 'edge' or 'label'
2539 * @param {object} pile array of options objects to consider
2540 *
2541 * @returns {boolean | Function} value for passed subOption of 'chosen' to use
2542 */
2543function choosify(subOption, pile) {
2544 // allowed values for subOption
2545 const allowed = ["node", "edge", "label"];
2546 let value = true;
2547
2548 const chosen = topMost(pile, "chosen");
2549 if (typeof chosen === "boolean") {
2550 value = chosen;
2551 } else if (typeof chosen === "object") {
2552 if (allowed.indexOf(subOption) === -1) {
2553 throw new Error(
2554 "choosify: subOption '" +
2555 subOption +
2556 "' should be one of " +
2557 "'" +
2558 allowed.join("', '") +
2559 "'"
2560 );
2561 }
2562
2563 const chosenEdge = topMost(pile, ["chosen", subOption]);
2564 if (typeof chosenEdge === "boolean" || typeof chosenEdge === "function") {
2565 value = chosenEdge;
2566 }
2567 }
2568
2569 return value;
2570}
2571
2572/**
2573 * Check if the point falls within the given rectangle.
2574 *
2575 * @param {rect} rect
2576 * @param {point} point
2577 * @param {rotationPoint} [rotationPoint] if specified, the rotation that applies to the rectangle.
2578 * @returns {boolean} true if point within rectangle, false otherwise
2579 */
2580function pointInRect(rect, point, rotationPoint) {
2581 if (rect.width <= 0 || rect.height <= 0) {
2582 return false; // early out
2583 }
2584
2585 if (rotationPoint !== undefined) {
2586 // Rotate the point the same amount as the rectangle
2587 const tmp = {
2588 x: point.x - rotationPoint.x,
2589 y: point.y - rotationPoint.y,
2590 };
2591
2592 if (rotationPoint.angle !== 0) {
2593 // In order to get the coordinates the same, you need to
2594 // rotate in the reverse direction
2595 const angle = -rotationPoint.angle;
2596
2597 const tmp2 = {
2598 x: Math.cos(angle) * tmp.x - Math.sin(angle) * tmp.y,
2599 y: Math.sin(angle) * tmp.x + Math.cos(angle) * tmp.y,
2600 };
2601 point = tmp2;
2602 } else {
2603 point = tmp;
2604 }
2605
2606 // Note that if a rotation is specified, the rectangle coordinates
2607 // are **not* the full canvas coordinates. They are relative to the
2608 // rotationPoint. Hence, the point coordinates need not be translated
2609 // back in this case.
2610 }
2611
2612 const right = rect.x + rect.width;
2613 const bottom = rect.y + rect.width;
2614
2615 return (
2616 rect.left < point.x &&
2617 right > point.x &&
2618 rect.top < point.y &&
2619 bottom > point.y
2620 );
2621}
2622
2623/**
2624 * Check if given value is acceptable as a label text.
2625 *
2626 * @param {*} text value to check; can be anything at this point
2627 * @returns {boolean} true if valid label value, false otherwise
2628 */
2629function isValidLabel(text) {
2630 // Note that this is quite strict: types that *might* be converted to string are disallowed
2631 return typeof text === "string" && text !== "";
2632}
2633
2634/**
2635 * Returns x, y of self reference circle based on provided angle
2636 *
2637 * @param {object} ctx
2638 * @param {number} angle
2639 * @param {number} radius
2640 * @param {VisNode} node
2641 *
2642 * @returns {object} x and y coordinates
2643 */
2644function getSelfRefCoordinates(ctx, angle, radius, node) {
2645 let x = node.x;
2646 let y = node.y;
2647
2648 if (typeof node.distanceToBorder === "function") {
2649 //calculating opposite and adjacent
2650 //distaneToBorder becomes Hypotenuse.
2651 //Formulas sin(a) = Opposite / Hypotenuse and cos(a) = Adjacent / Hypotenuse
2652 const toBorderDist = node.distanceToBorder(ctx, angle);
2653 const yFromNodeCenter = Math.sin(angle) * toBorderDist;
2654 const xFromNodeCenter = Math.cos(angle) * toBorderDist;
2655 //xFromNodeCenter is basically x and if xFromNodeCenter equals to the distance to border then it means
2656 //that y does not need calculation because it is equal node.height / 2 or node.y
2657 //same thing with yFromNodeCenter and if yFromNodeCenter equals to the distance to border then it means
2658 //that x is equal node.width / 2 or node.x
2659 if (xFromNodeCenter === toBorderDist) {
2660 x += toBorderDist;
2661 y = node.y;
2662 } else if (yFromNodeCenter === toBorderDist) {
2663 x = node.x;
2664 y -= toBorderDist;
2665 } else {
2666 x += xFromNodeCenter;
2667 y -= yFromNodeCenter;
2668 }
2669 } else if (node.shape.width > node.shape.height) {
2670 x = node.x + node.shape.width * 0.5;
2671 y = node.y - radius;
2672 } else {
2673 x = node.x + radius;
2674 y = node.y - node.shape.height * 0.5;
2675 }
2676
2677 return { x, y };
2678}
2679
2680/**
2681 * Callback to determine text dimensions, using the parent label settings.
2682 *
2683 * @callback MeasureText
2684 * @param {text} text
2685 * @param {text} mod
2686 * @returns {object} { width, values} width in pixels and font attributes
2687 */
2688
2689/**
2690 * Helper class for Label which collects results of splitting labels into lines and blocks.
2691 *
2692 * @private
2693 */
2694class LabelAccumulator {
2695 /**
2696 * @param {MeasureText} measureText
2697 */
2698 constructor(measureText) {
2699 this.measureText = measureText;
2700 this.current = 0;
2701 this.width = 0;
2702 this.height = 0;
2703 this.lines = [];
2704 }
2705
2706 /**
2707 * Append given text to the given line.
2708 *
2709 * @param {number} l index of line to add to
2710 * @param {string} text string to append to line
2711 * @param {'bold'|'ital'|'boldital'|'mono'|'normal'} [mod='normal']
2712 * @private
2713 */
2714 _add(l, text, mod = "normal") {
2715 if (this.lines[l] === undefined) {
2716 this.lines[l] = {
2717 width: 0,
2718 height: 0,
2719 blocks: [],
2720 };
2721 }
2722
2723 // We still need to set a block for undefined and empty texts, hence return at this point
2724 // This is necessary because we don't know at this point if we're at the
2725 // start of an empty line or not.
2726 // To compensate, empty blocks are removed in `finalize()`.
2727 //
2728 // Empty strings should still have a height
2729 let tmpText = text;
2730 if (text === undefined || text === "") tmpText = " ";
2731
2732 // Determine width and get the font properties
2733 const result = this.measureText(tmpText, mod);
2734 const block = Object.assign({}, result.values);
2735 block.text = text;
2736 block.width = result.width;
2737 block.mod = mod;
2738
2739 if (text === undefined || text === "") {
2740 block.width = 0;
2741 }
2742
2743 this.lines[l].blocks.push(block);
2744
2745 // Update the line width. We need this for determining if a string goes over max width
2746 this.lines[l].width += block.width;
2747 }
2748
2749 /**
2750 * Returns the width in pixels of the current line.
2751 *
2752 * @returns {number}
2753 */
2754 curWidth() {
2755 const line = this.lines[this.current];
2756 if (line === undefined) return 0;
2757
2758 return line.width;
2759 }
2760
2761 /**
2762 * Add text in block to current line
2763 *
2764 * @param {string} text
2765 * @param {'bold'|'ital'|'boldital'|'mono'|'normal'} [mod='normal']
2766 */
2767 append(text, mod = "normal") {
2768 this._add(this.current, text, mod);
2769 }
2770
2771 /**
2772 * Add text in block to current line and start a new line
2773 *
2774 * @param {string} text
2775 * @param {'bold'|'ital'|'boldital'|'mono'|'normal'} [mod='normal']
2776 */
2777 newLine(text, mod = "normal") {
2778 this._add(this.current, text, mod);
2779 this.current++;
2780 }
2781
2782 /**
2783 * Determine and set the heights of all the lines currently contained in this instance
2784 *
2785 * Note that width has already been set.
2786 *
2787 * @private
2788 */
2789 determineLineHeights() {
2790 for (let k = 0; k < this.lines.length; k++) {
2791 const line = this.lines[k];
2792
2793 // Looking for max height of blocks in line
2794 let height = 0;
2795
2796 if (line.blocks !== undefined) {
2797 // Can happen if text contains e.g. '\n '
2798 for (let l = 0; l < line.blocks.length; l++) {
2799 const block = line.blocks[l];
2800
2801 if (height < block.height) {
2802 height = block.height;
2803 }
2804 }
2805 }
2806
2807 line.height = height;
2808 }
2809 }
2810
2811 /**
2812 * Determine the full size of the label text, as determined by current lines and blocks
2813 *
2814 * @private
2815 */
2816 determineLabelSize() {
2817 let width = 0;
2818 let height = 0;
2819 for (let k = 0; k < this.lines.length; k++) {
2820 const line = this.lines[k];
2821
2822 if (line.width > width) {
2823 width = line.width;
2824 }
2825 height += line.height;
2826 }
2827
2828 this.width = width;
2829 this.height = height;
2830 }
2831
2832 /**
2833 * Remove all empty blocks and empty lines we don't need
2834 *
2835 * This must be done after the width/height determination,
2836 * so that these are set properly for processing here.
2837 *
2838 * @returns {Array<Line>} Lines with empty blocks (and some empty lines) removed
2839 * @private
2840 */
2841 removeEmptyBlocks() {
2842 const tmpLines = [];
2843 for (let k = 0; k < this.lines.length; k++) {
2844 const line = this.lines[k];
2845
2846 // Note: an empty line in between text has width zero but is still relevant to layout.
2847 // So we can't use width for testing empty line here
2848 if (line.blocks.length === 0) continue;
2849
2850 // Discard final empty line always
2851 if (k === this.lines.length - 1) {
2852 if (line.width === 0) continue;
2853 }
2854
2855 const tmpLine = {};
2856 Object.assign(tmpLine, line);
2857 tmpLine.blocks = [];
2858
2859 let firstEmptyBlock;
2860 const tmpBlocks = [];
2861 for (let l = 0; l < line.blocks.length; l++) {
2862 const block = line.blocks[l];
2863 if (block.width !== 0) {
2864 tmpBlocks.push(block);
2865 } else {
2866 if (firstEmptyBlock === undefined) {
2867 firstEmptyBlock = block;
2868 }
2869 }
2870 }
2871
2872 // Ensure that there is *some* text present
2873 if (tmpBlocks.length === 0 && firstEmptyBlock !== undefined) {
2874 tmpBlocks.push(firstEmptyBlock);
2875 }
2876
2877 tmpLine.blocks = tmpBlocks;
2878
2879 tmpLines.push(tmpLine);
2880 }
2881
2882 return tmpLines;
2883 }
2884
2885 /**
2886 * Set the sizes for all lines and the whole thing.
2887 *
2888 * @returns {{width: (number|*), height: (number|*), lines: Array}}
2889 */
2890 finalize() {
2891 //console.log(JSON.stringify(this.lines, null, 2));
2892
2893 this.determineLineHeights();
2894 this.determineLabelSize();
2895 const tmpLines = this.removeEmptyBlocks();
2896
2897 // Return a simple hash object for further processing.
2898 return {
2899 width: this.width,
2900 height: this.height,
2901 lines: tmpLines,
2902 };
2903 }
2904}
2905
2906// Hash of prepared regexp's for tags
2907const tagPattern = {
2908 // HTML
2909 "<b>": /<b>/,
2910 "<i>": /<i>/,
2911 "<code>": /<code>/,
2912 "</b>": /<\/b>/,
2913 "</i>": /<\/i>/,
2914 "</code>": /<\/code>/,
2915 // Markdown
2916 "*": /\*/, // bold
2917 _: /_/, // ital
2918 "`": /`/, // mono
2919 afterBold: /[^*]/,
2920 afterItal: /[^_]/,
2921 afterMono: /[^`]/,
2922};
2923
2924/**
2925 * Internal helper class for parsing the markup tags for HTML and Markdown.
2926 *
2927 * NOTE: Sequences of tabs and spaces are reduced to single space.
2928 * Scan usage of `this.spacing` within method
2929 */
2930class MarkupAccumulator {
2931 /**
2932 * Create an instance
2933 *
2934 * @param {string} text text to parse for markup
2935 */
2936 constructor(text) {
2937 this.text = text;
2938 this.bold = false;
2939 this.ital = false;
2940 this.mono = false;
2941 this.spacing = false;
2942 this.position = 0;
2943 this.buffer = "";
2944 this.modStack = [];
2945
2946 this.blocks = [];
2947 }
2948
2949 /**
2950 * Return the mod label currently on the top of the stack
2951 *
2952 * @returns {string} label of topmost mod
2953 * @private
2954 */
2955 mod() {
2956 return this.modStack.length === 0 ? "normal" : this.modStack[0];
2957 }
2958
2959 /**
2960 * Return the mod label currently active
2961 *
2962 * @returns {string} label of active mod
2963 * @private
2964 */
2965 modName() {
2966 if (this.modStack.length === 0) return "normal";
2967 else if (this.modStack[0] === "mono") return "mono";
2968 else {
2969 if (this.bold && this.ital) {
2970 return "boldital";
2971 } else if (this.bold) {
2972 return "bold";
2973 } else if (this.ital) {
2974 return "ital";
2975 }
2976 }
2977 }
2978
2979 /**
2980 * @private
2981 */
2982 emitBlock() {
2983 if (this.spacing) {
2984 this.add(" ");
2985 this.spacing = false;
2986 }
2987 if (this.buffer.length > 0) {
2988 this.blocks.push({ text: this.buffer, mod: this.modName() });
2989 this.buffer = "";
2990 }
2991 }
2992
2993 /**
2994 * Output text to buffer
2995 *
2996 * @param {string} text text to add
2997 * @private
2998 */
2999 add(text) {
3000 if (text === " ") {
3001 this.spacing = true;
3002 }
3003 if (this.spacing) {
3004 this.buffer += " ";
3005 this.spacing = false;
3006 }
3007 if (text != " ") {
3008 this.buffer += text;
3009 }
3010 }
3011
3012 /**
3013 * Handle parsing of whitespace
3014 *
3015 * @param {string} ch the character to check
3016 * @returns {boolean} true if the character was processed as whitespace, false otherwise
3017 */
3018 parseWS(ch) {
3019 if (/[ \t]/.test(ch)) {
3020 if (!this.mono) {
3021 this.spacing = true;
3022 } else {
3023 this.add(ch);
3024 }
3025 return true;
3026 }
3027
3028 return false;
3029 }
3030
3031 /**
3032 * @param {string} tagName label for block type to set
3033 * @private
3034 */
3035 setTag(tagName) {
3036 this.emitBlock();
3037 this[tagName] = true;
3038 this.modStack.unshift(tagName);
3039 }
3040
3041 /**
3042 * @param {string} tagName label for block type to unset
3043 * @private
3044 */
3045 unsetTag(tagName) {
3046 this.emitBlock();
3047 this[tagName] = false;
3048 this.modStack.shift();
3049 }
3050
3051 /**
3052 * @param {string} tagName label for block type we are currently processing
3053 * @param {string|RegExp} tag string to match in text
3054 * @returns {boolean} true if the tag was processed, false otherwise
3055 */
3056 parseStartTag(tagName, tag) {
3057 // Note: if 'mono' passed as tagName, there is a double check here. This is OK
3058 if (!this.mono && !this[tagName] && this.match(tag)) {
3059 this.setTag(tagName);
3060 return true;
3061 }
3062
3063 return false;
3064 }
3065
3066 /**
3067 * @param {string|RegExp} tag
3068 * @param {number} [advance=true] if set, advance current position in text
3069 * @returns {boolean} true if match at given position, false otherwise
3070 * @private
3071 */
3072 match(tag, advance = true) {
3073 const [regExp, length] = this.prepareRegExp(tag);
3074 const matched = regExp.test(this.text.substr(this.position, length));
3075
3076 if (matched && advance) {
3077 this.position += length - 1;
3078 }
3079
3080 return matched;
3081 }
3082
3083 /**
3084 * @param {string} tagName label for block type we are currently processing
3085 * @param {string|RegExp} tag string to match in text
3086 * @param {RegExp} [nextTag] regular expression to match for characters *following* the current tag
3087 * @returns {boolean} true if the tag was processed, false otherwise
3088 */
3089 parseEndTag(tagName, tag, nextTag) {
3090 let checkTag = this.mod() === tagName;
3091 if (tagName === "mono") {
3092 // special handling for 'mono'
3093 checkTag = checkTag && this.mono;
3094 } else {
3095 checkTag = checkTag && !this.mono;
3096 }
3097
3098 if (checkTag && this.match(tag)) {
3099 if (nextTag !== undefined) {
3100 // Purpose of the following match is to prevent a direct unset/set of a given tag
3101 // E.g. '*bold **still bold*' => '*bold still bold*'
3102 if (
3103 this.position === this.text.length - 1 ||
3104 this.match(nextTag, false)
3105 ) {
3106 this.unsetTag(tagName);
3107 }
3108 } else {
3109 this.unsetTag(tagName);
3110 }
3111
3112 return true;
3113 }
3114
3115 return false;
3116 }
3117
3118 /**
3119 * @param {string|RegExp} tag string to match in text
3120 * @param {value} value string to replace tag with, if found at current position
3121 * @returns {boolean} true if the tag was processed, false otherwise
3122 */
3123 replace(tag, value) {
3124 if (this.match(tag)) {
3125 this.add(value);
3126 this.position += length - 1;
3127 return true;
3128 }
3129
3130 return false;
3131 }
3132
3133 /**
3134 * Create a regular expression for the tag if it isn't already one.
3135 *
3136 * The return value is an array `[RegExp, number]`, with exactly two value, where:
3137 * - RegExp is the regular expression to use
3138 * - number is the lenth of the input string to match
3139 *
3140 * @param {string|RegExp} tag string to match in text
3141 * @returns {Array} regular expression to use and length of input string to match
3142 * @private
3143 */
3144 prepareRegExp(tag) {
3145 let length;
3146 let regExp;
3147 if (tag instanceof RegExp) {
3148 regExp = tag;
3149 length = 1; // ASSUMPTION: regexp only tests one character
3150 } else {
3151 // use prepared regexp if present
3152 const prepared = tagPattern[tag];
3153 if (prepared !== undefined) {
3154 regExp = prepared;
3155 } else {
3156 regExp = new RegExp(tag);
3157 }
3158
3159 length = tag.length;
3160 }
3161
3162 return [regExp, length];
3163 }
3164}
3165
3166/**
3167 * Helper class for Label which explodes the label text into lines and blocks within lines
3168 *
3169 * @private
3170 */
3171class LabelSplitter {
3172 /**
3173 * @param {CanvasRenderingContext2D} ctx Canvas rendering context
3174 * @param {Label} parent reference to the Label instance using current instance
3175 * @param {boolean} selected
3176 * @param {boolean} hover
3177 */
3178 constructor(ctx, parent, selected, hover) {
3179 this.ctx = ctx;
3180 this.parent = parent;
3181 this.selected = selected;
3182 this.hover = hover;
3183
3184 /**
3185 * Callback to determine text width; passed to LabelAccumulator instance
3186 *
3187 * @param {string} text string to determine width of
3188 * @param {string} mod font type to use for this text
3189 * @returns {object} { width, values} width in pixels and font attributes
3190 */
3191 const textWidth = (text, mod) => {
3192 if (text === undefined) return 0;
3193
3194 // TODO: This can be done more efficiently with caching
3195 // This will set the ctx.font correctly, depending on selected/hover and mod - so that ctx.measureText() will be accurate.
3196 const values = this.parent.getFormattingValues(ctx, selected, hover, mod);
3197
3198 let width = 0;
3199 if (text !== "") {
3200 const measure = this.ctx.measureText(text);
3201 width = measure.width;
3202 }
3203
3204 return { width, values: values };
3205 };
3206
3207 this.lines = new LabelAccumulator(textWidth);
3208 }
3209
3210 /**
3211 * Split passed text of a label into lines and blocks.
3212 *
3213 * # NOTE
3214 *
3215 * The handling of spacing is option dependent:
3216 *
3217 * - if `font.multi : false`, all spaces are retained
3218 * - if `font.multi : true`, every sequence of spaces is compressed to a single space
3219 *
3220 * This might not be the best way to do it, but this is as it has been working till now.
3221 * In order not to break existing functionality, for the time being this behaviour will
3222 * be retained in any code changes.
3223 *
3224 * @param {string} text text to split
3225 * @returns {Array<line>}
3226 */
3227 process(text) {
3228 if (!isValidLabel(text)) {
3229 return this.lines.finalize();
3230 }
3231
3232 const font = this.parent.fontOptions;
3233
3234 // Normalize the end-of-line's to a single representation - order important
3235 text = text.replace(/\r\n/g, "\n"); // Dos EOL's
3236 text = text.replace(/\r/g, "\n"); // Mac EOL's
3237
3238 // Note that at this point, there can be no \r's in the text.
3239 // This is used later on splitStringIntoLines() to split multifont texts.
3240
3241 const nlLines = String(text).split("\n");
3242 const lineCount = nlLines.length;
3243
3244 if (font.multi) {
3245 // Multi-font case: styling tags active
3246 for (let i = 0; i < lineCount; i++) {
3247 const blocks = this.splitBlocks(nlLines[i], font.multi);
3248 // Post: Sequences of tabs and spaces are reduced to single space
3249
3250 if (blocks === undefined) continue;
3251
3252 if (blocks.length === 0) {
3253 this.lines.newLine("");
3254 continue;
3255 }
3256
3257 if (font.maxWdt > 0) {
3258 // widthConstraint.maximum defined
3259 //console.log('Running widthConstraint multi, max: ' + this.fontOptions.maxWdt);
3260 for (let j = 0; j < blocks.length; j++) {
3261 const mod = blocks[j].mod;
3262 const text = blocks[j].text;
3263 this.splitStringIntoLines(text, mod, true);
3264 }
3265 } else {
3266 // widthConstraint.maximum NOT defined
3267 for (let j = 0; j < blocks.length; j++) {
3268 const mod = blocks[j].mod;
3269 const text = blocks[j].text;
3270 this.lines.append(text, mod);
3271 }
3272 }
3273
3274 this.lines.newLine();
3275 }
3276 } else {
3277 // Single-font case
3278 if (font.maxWdt > 0) {
3279 // widthConstraint.maximum defined
3280 // console.log('Running widthConstraint normal, max: ' + this.fontOptions.maxWdt);
3281 for (let i = 0; i < lineCount; i++) {
3282 this.splitStringIntoLines(nlLines[i]);
3283 }
3284 } else {
3285 // widthConstraint.maximum NOT defined
3286 for (let i = 0; i < lineCount; i++) {
3287 this.lines.newLine(nlLines[i]);
3288 }
3289 }
3290 }
3291
3292 return this.lines.finalize();
3293 }
3294
3295 /**
3296 * normalize the markup system
3297 *
3298 * @param {boolean|'md'|'markdown'|'html'} markupSystem
3299 * @returns {string}
3300 */
3301 decodeMarkupSystem(markupSystem) {
3302 let system = "none";
3303 if (markupSystem === "markdown" || markupSystem === "md") {
3304 system = "markdown";
3305 } else if (markupSystem === true || markupSystem === "html") {
3306 system = "html";
3307 }
3308 return system;
3309 }
3310
3311 /**
3312 *
3313 * @param {string} text
3314 * @returns {Array}
3315 */
3316 splitHtmlBlocks(text) {
3317 const s = new MarkupAccumulator(text);
3318
3319 const parseEntities = (ch) => {
3320 if (/&/.test(ch)) {
3321 const parsed =
3322 s.replace(s.text, "&lt;", "<") || s.replace(s.text, "&amp;", "&");
3323
3324 if (!parsed) {
3325 s.add("&");
3326 }
3327
3328 return true;
3329 }
3330
3331 return false;
3332 };
3333
3334 while (s.position < s.text.length) {
3335 const ch = s.text.charAt(s.position);
3336
3337 const parsed =
3338 s.parseWS(ch) ||
3339 (/</.test(ch) &&
3340 (s.parseStartTag("bold", "<b>") ||
3341 s.parseStartTag("ital", "<i>") ||
3342 s.parseStartTag("mono", "<code>") ||
3343 s.parseEndTag("bold", "</b>") ||
3344 s.parseEndTag("ital", "</i>") ||
3345 s.parseEndTag("mono", "</code>"))) ||
3346 parseEntities(ch);
3347
3348 if (!parsed) {
3349 s.add(ch);
3350 }
3351 s.position++;
3352 }
3353 s.emitBlock();
3354 return s.blocks;
3355 }
3356
3357 /**
3358 *
3359 * @param {string} text
3360 * @returns {Array}
3361 */
3362 splitMarkdownBlocks(text) {
3363 const s = new MarkupAccumulator(text);
3364 let beginable = true;
3365
3366 const parseOverride = (ch) => {
3367 if (/\\/.test(ch)) {
3368 if (s.position < this.text.length + 1) {
3369 s.position++;
3370 ch = this.text.charAt(s.position);
3371 if (/ \t/.test(ch)) {
3372 s.spacing = true;
3373 } else {
3374 s.add(ch);
3375 beginable = false;
3376 }
3377 }
3378
3379 return true;
3380 }
3381
3382 return false;
3383 };
3384
3385 while (s.position < s.text.length) {
3386 const ch = s.text.charAt(s.position);
3387
3388 const parsed =
3389 s.parseWS(ch) ||
3390 parseOverride(ch) ||
3391 ((beginable || s.spacing) &&
3392 (s.parseStartTag("bold", "*") ||
3393 s.parseStartTag("ital", "_") ||
3394 s.parseStartTag("mono", "`"))) ||
3395 s.parseEndTag("bold", "*", "afterBold") ||
3396 s.parseEndTag("ital", "_", "afterItal") ||
3397 s.parseEndTag("mono", "`", "afterMono");
3398
3399 if (!parsed) {
3400 s.add(ch);
3401 beginable = false;
3402 }
3403 s.position++;
3404 }
3405 s.emitBlock();
3406 return s.blocks;
3407 }
3408
3409 /**
3410 * Explodes a piece of text into single-font blocks using a given markup
3411 *
3412 * @param {string} text
3413 * @param {boolean|'md'|'markdown'|'html'} markupSystem
3414 * @returns {Array.<{text: string, mod: string}>}
3415 * @private
3416 */
3417 splitBlocks(text, markupSystem) {
3418 const system = this.decodeMarkupSystem(markupSystem);
3419 if (system === "none") {
3420 return [
3421 {
3422 text: text,
3423 mod: "normal",
3424 },
3425 ];
3426 } else if (system === "markdown") {
3427 return this.splitMarkdownBlocks(text);
3428 } else if (system === "html") {
3429 return this.splitHtmlBlocks(text);
3430 }
3431 }
3432
3433 /**
3434 * @param {string} text
3435 * @returns {boolean} true if text length over the current max with
3436 * @private
3437 */
3438 overMaxWidth(text) {
3439 const width = this.ctx.measureText(text).width;
3440 return this.lines.curWidth() + width > this.parent.fontOptions.maxWdt;
3441 }
3442
3443 /**
3444 * Determine the longest part of the sentence which still fits in the
3445 * current max width.
3446 *
3447 * @param {Array} words Array of strings signifying a text lines
3448 * @returns {number} index of first item in string making string go over max
3449 * @private
3450 */
3451 getLongestFit(words) {
3452 let text = "";
3453 let w = 0;
3454
3455 while (w < words.length) {
3456 const pre = text === "" ? "" : " ";
3457 const newText = text + pre + words[w];
3458
3459 if (this.overMaxWidth(newText)) break;
3460 text = newText;
3461 w++;
3462 }
3463
3464 return w;
3465 }
3466
3467 /**
3468 * Determine the longest part of the string which still fits in the
3469 * current max width.
3470 *
3471 * @param {Array} words Array of strings signifying a text lines
3472 * @returns {number} index of first item in string making string go over max
3473 */
3474 getLongestFitWord(words) {
3475 let w = 0;
3476
3477 while (w < words.length) {
3478 if (this.overMaxWidth(words.slice(0, w))) break;
3479 w++;
3480 }
3481
3482 return w;
3483 }
3484
3485 /**
3486 * Split the passed text into lines, according to width constraint (if any).
3487 *
3488 * The method assumes that the input string is a single line, i.e. without lines break.
3489 *
3490 * This method retains spaces, if still present (case `font.multi: false`).
3491 * A space which falls on an internal line break, will be replaced by a newline.
3492 * There is no special handling of tabs; these go along with the flow.
3493 *
3494 * @param {string} str
3495 * @param {string} [mod='normal']
3496 * @param {boolean} [appendLast=false]
3497 * @private
3498 */
3499 splitStringIntoLines(str, mod = "normal", appendLast = false) {
3500 // Set the canvas context font, based upon the current selected/hover state
3501 // and the provided mod, so the text measurement performed by getLongestFit
3502 // will be accurate - and not just use the font of whoever last used the canvas.
3503 this.parent.getFormattingValues(this.ctx, this.selected, this.hover, mod);
3504
3505 // Still-present spaces are relevant, retain them
3506 str = str.replace(/^( +)/g, "$1\r");
3507 str = str.replace(/([^\r][^ ]*)( +)/g, "$1\r$2\r");
3508 let words = str.split("\r");
3509
3510 while (words.length > 0) {
3511 let w = this.getLongestFit(words);
3512
3513 if (w === 0) {
3514 // Special case: the first word is already larger than the max width.
3515 const word = words[0];
3516
3517 // Break the word to the largest part that fits the line
3518 const x = this.getLongestFitWord(word);
3519 this.lines.newLine(word.slice(0, x), mod);
3520
3521 // Adjust the word, so that the rest will be done next iteration
3522 words[0] = word.slice(x);
3523 } else {
3524 // skip any space that is replaced by a newline
3525 let newW = w;
3526 if (words[w - 1] === " ") {
3527 w--;
3528 } else if (words[newW] === " ") {
3529 newW++;
3530 }
3531
3532 const text = words.slice(0, w).join("");
3533
3534 if (w == words.length && appendLast) {
3535 this.lines.append(text, mod);
3536 } else {
3537 this.lines.newLine(text, mod);
3538 }
3539
3540 // Adjust the word, so that the rest will be done next iteration
3541 words = words.slice(newW);
3542 }
3543 }
3544 }
3545}
3546
3547/**
3548 * List of special styles for multi-fonts
3549 *
3550 * @private
3551 */
3552const multiFontStyle = ["bold", "ital", "boldital", "mono"];
3553
3554/**
3555 * A Label to be used for Nodes or Edges.
3556 */
3557class Label {
3558 /**
3559 * @param {object} body
3560 * @param {object} options
3561 * @param {boolean} [edgelabel=false]
3562 */
3563 constructor(body, options, edgelabel = false) {
3564 this.body = body;
3565 this.pointToSelf = false;
3566 this.baseSize = undefined;
3567 this.fontOptions = {}; // instance variable containing the *instance-local* font options
3568 this.setOptions(options);
3569 this.size = { top: 0, left: 0, width: 0, height: 0, yLine: 0 };
3570 this.isEdgeLabel = edgelabel;
3571 }
3572
3573 /**
3574 * @param {object} options the options of the parent Node-instance
3575 */
3576 setOptions(options) {
3577 this.elementOptions = options; // Reference to the options of the parent Node-instance
3578
3579 this.initFontOptions(options.font);
3580
3581 if (isValidLabel(options.label)) {
3582 this.labelDirty = true;
3583 } else {
3584 // Bad label! Change the option value to prevent bad stuff happening
3585 options.label = undefined;
3586 }
3587
3588 if (options.font !== undefined && options.font !== null) {
3589 // font options can be deleted at various levels
3590 if (typeof options.font === "string") {
3591 this.baseSize = this.fontOptions.size;
3592 } else if (typeof options.font === "object") {
3593 const size = options.font.size;
3594
3595 if (size !== undefined) {
3596 this.baseSize = size;
3597 }
3598 }
3599 }
3600 }
3601
3602 /**
3603 * Init the font Options structure.
3604 *
3605 * Member fontOptions serves as an accumulator for the current font options.
3606 * As such, it needs to be completely separated from the node options.
3607 *
3608 * @param {object} newFontOptions the new font options to process
3609 * @private
3610 */
3611 initFontOptions(newFontOptions) {
3612 // Prepare the multi-font option objects.
3613 // These will be filled in propagateFonts(), if required
3614 forEach(multiFontStyle, (style) => {
3615 this.fontOptions[style] = {};
3616 });
3617
3618 // Handle shorthand option, if present
3619 if (Label.parseFontString(this.fontOptions, newFontOptions)) {
3620 this.fontOptions.vadjust = 0;
3621 return;
3622 }
3623
3624 // Copy over the non-multifont options, if specified
3625 forEach(newFontOptions, (prop, n) => {
3626 if (prop !== undefined && prop !== null && typeof prop !== "object") {
3627 this.fontOptions[n] = prop;
3628 }
3629 });
3630 }
3631
3632 /**
3633 * If in-variable is a string, parse it as a font specifier.
3634 *
3635 * Note that following is not done here and have to be done after the call:
3636 * - Not all font options are set (vadjust, mod)
3637 *
3638 * @param {object} outOptions out-parameter, object in which to store the parse results (if any)
3639 * @param {object} inOptions font options to parse
3640 * @returns {boolean} true if font parsed as string, false otherwise
3641 * @static
3642 */
3643 static parseFontString(outOptions, inOptions) {
3644 if (!inOptions || typeof inOptions !== "string") return false;
3645
3646 const newOptionsArray = inOptions.split(" ");
3647
3648 outOptions.size = +newOptionsArray[0].replace("px", "");
3649 outOptions.face = newOptionsArray[1];
3650 outOptions.color = newOptionsArray[2];
3651
3652 return true;
3653 }
3654
3655 /**
3656 * Set the width and height constraints based on 'nearest' value
3657 *
3658 * @param {Array} pile array of option objects to consider
3659 * @returns {object} the actual constraint values to use
3660 * @private
3661 */
3662 constrain(pile) {
3663 // NOTE: constrainWidth and constrainHeight never set!
3664 // NOTE: for edge labels, only 'maxWdt' set
3665 // Node labels can set all the fields
3666 const fontOptions = {
3667 constrainWidth: false,
3668 maxWdt: -1,
3669 minWdt: -1,
3670 constrainHeight: false,
3671 minHgt: -1,
3672 valign: "middle",
3673 };
3674
3675 const widthConstraint = topMost(pile, "widthConstraint");
3676 if (typeof widthConstraint === "number") {
3677 fontOptions.maxWdt = Number(widthConstraint);
3678 fontOptions.minWdt = Number(widthConstraint);
3679 } else if (typeof widthConstraint === "object") {
3680 const widthConstraintMaximum = topMost(pile, [
3681 "widthConstraint",
3682 "maximum",
3683 ]);
3684 if (typeof widthConstraintMaximum === "number") {
3685 fontOptions.maxWdt = Number(widthConstraintMaximum);
3686 }
3687 const widthConstraintMinimum = topMost(pile, [
3688 "widthConstraint",
3689 "minimum",
3690 ]);
3691 if (typeof widthConstraintMinimum === "number") {
3692 fontOptions.minWdt = Number(widthConstraintMinimum);
3693 }
3694 }
3695
3696 const heightConstraint = topMost(pile, "heightConstraint");
3697 if (typeof heightConstraint === "number") {
3698 fontOptions.minHgt = Number(heightConstraint);
3699 } else if (typeof heightConstraint === "object") {
3700 const heightConstraintMinimum = topMost(pile, [
3701 "heightConstraint",
3702 "minimum",
3703 ]);
3704 if (typeof heightConstraintMinimum === "number") {
3705 fontOptions.minHgt = Number(heightConstraintMinimum);
3706 }
3707 const heightConstraintValign = topMost(pile, [
3708 "heightConstraint",
3709 "valign",
3710 ]);
3711 if (typeof heightConstraintValign === "string") {
3712 if (
3713 heightConstraintValign === "top" ||
3714 heightConstraintValign === "bottom"
3715 ) {
3716 fontOptions.valign = heightConstraintValign;
3717 }
3718 }
3719 }
3720
3721 return fontOptions;
3722 }
3723
3724 /**
3725 * Set options and update internal state
3726 *
3727 * @param {object} options options to set
3728 * @param {Array} pile array of option objects to consider for option 'chosen'
3729 */
3730 update(options, pile) {
3731 this.setOptions(options, true);
3732 this.propagateFonts(pile);
3733 deepExtend(this.fontOptions, this.constrain(pile));
3734 this.fontOptions.chooser = choosify("label", pile);
3735 }
3736
3737 /**
3738 * When margins are set in an element, adjust sizes is called to remove them
3739 * from the width/height constraints. This must be done prior to label sizing.
3740 *
3741 * @param {{top: number, right: number, bottom: number, left: number}} margins
3742 */
3743 adjustSizes(margins) {
3744 const widthBias = margins ? margins.right + margins.left : 0;
3745 if (this.fontOptions.constrainWidth) {
3746 this.fontOptions.maxWdt -= widthBias;
3747 this.fontOptions.minWdt -= widthBias;
3748 }
3749 const heightBias = margins ? margins.top + margins.bottom : 0;
3750 if (this.fontOptions.constrainHeight) {
3751 this.fontOptions.minHgt -= heightBias;
3752 }
3753 }
3754
3755 /////////////////////////////////////////////////////////
3756 // Methods for handling options piles
3757 // Eventually, these will be moved to a separate class
3758 /////////////////////////////////////////////////////////
3759
3760 /**
3761 * Add the font members of the passed list of option objects to the pile.
3762 *
3763 * @param {Pile} dstPile pile of option objects add to
3764 * @param {Pile} srcPile pile of option objects to take font options from
3765 * @private
3766 */
3767 addFontOptionsToPile(dstPile, srcPile) {
3768 for (let i = 0; i < srcPile.length; ++i) {
3769 this.addFontToPile(dstPile, srcPile[i]);
3770 }
3771 }
3772
3773 /**
3774 * Add given font option object to the list of objects (the 'pile') to consider for determining
3775 * multi-font option values.
3776 *
3777 * @param {Pile} pile pile of option objects to use
3778 * @param {object} options instance to add to pile
3779 * @private
3780 */
3781 addFontToPile(pile, options) {
3782 if (options === undefined) return;
3783 if (options.font === undefined || options.font === null) return;
3784
3785 const item = options.font;
3786 pile.push(item);
3787 }
3788
3789 /**
3790 * Collect all own-property values from the font pile that aren't multi-font option objectss.
3791 *
3792 * @param {Pile} pile pile of option objects to use
3793 * @returns {object} object with all current own basic font properties
3794 * @private
3795 */
3796 getBasicOptions(pile) {
3797 const ret = {};
3798
3799 // Scans the whole pile to get all options present
3800 for (let n = 0; n < pile.length; ++n) {
3801 let fontOptions = pile[n];
3802
3803 // Convert shorthand if necessary
3804 const tmpShorthand = {};
3805 if (Label.parseFontString(tmpShorthand, fontOptions)) {
3806 fontOptions = tmpShorthand;
3807 }
3808
3809 forEach(fontOptions, (opt, name) => {
3810 if (opt === undefined) return; // multi-font option need not be present
3811 if (Object.prototype.hasOwnProperty.call(ret, name)) return; // Keep first value we encounter
3812
3813 if (multiFontStyle.indexOf(name) !== -1) {
3814 // Skip multi-font properties but we do need the structure
3815 ret[name] = {};
3816 } else {
3817 ret[name] = opt;
3818 }
3819 });
3820 }
3821
3822 return ret;
3823 }
3824
3825 /**
3826 * Return the value for given option for the given multi-font.
3827 *
3828 * All available option objects are trawled in the set order to construct the option values.
3829 *
3830 * ---------------------------------------------------------------------
3831 * ## Traversal of pile for multi-fonts
3832 *
3833 * The determination of multi-font option values is a special case, because any values not
3834 * present in the multi-font options should by definition be taken from the main font options,
3835 * i.e. from the current 'parent' object of the multi-font option.
3836 *
3837 * ### Search order for multi-fonts
3838 *
3839 * 'bold' used as example:
3840 *
3841 * - search in option group 'bold' in local properties
3842 * - search in main font option group in local properties
3843 *
3844 * ---------------------------------------------------------------------
3845 *
3846 * @param {Pile} pile pile of option objects to use
3847 * @param {MultiFontStyle} multiName sub path for the multi-font
3848 * @param {string} option the option to search for, for the given multi-font
3849 * @returns {string|number} the value for the given option
3850 * @private
3851 */
3852 getFontOption(pile, multiName, option) {
3853 let multiFont;
3854
3855 // Search multi font in local properties
3856 for (let n = 0; n < pile.length; ++n) {
3857 const fontOptions = pile[n];
3858
3859 if (Object.prototype.hasOwnProperty.call(fontOptions, multiName)) {
3860 multiFont = fontOptions[multiName];
3861 if (multiFont === undefined || multiFont === null) continue;
3862
3863 // Convert shorthand if necessary
3864 // TODO: inefficient to do this conversion every time; find a better way.
3865 const tmpShorthand = {};
3866 if (Label.parseFontString(tmpShorthand, multiFont)) {
3867 multiFont = tmpShorthand;
3868 }
3869
3870 if (Object.prototype.hasOwnProperty.call(multiFont, option)) {
3871 return multiFont[option];
3872 }
3873 }
3874 }
3875
3876 // Option is not mentioned in the multi font options; take it from the parent font options.
3877 // These have already been converted with getBasicOptions(), so use the converted values.
3878 if (Object.prototype.hasOwnProperty.call(this.fontOptions, option)) {
3879 return this.fontOptions[option];
3880 }
3881
3882 // A value **must** be found; you should never get here.
3883 throw new Error(
3884 "Did not find value for multi-font for property: '" + option + "'"
3885 );
3886 }
3887
3888 /**
3889 * Return all options values for the given multi-font.
3890 *
3891 * All available option objects are trawled in the set order to construct the option values.
3892 *
3893 * @param {Pile} pile pile of option objects to use
3894 * @param {MultiFontStyle} multiName sub path for the mod-font
3895 * @returns {MultiFontOptions}
3896 * @private
3897 */
3898 getFontOptions(pile, multiName) {
3899 const result = {};
3900 const optionNames = ["color", "size", "face", "mod", "vadjust"]; // List of allowed options per multi-font
3901
3902 for (let i = 0; i < optionNames.length; ++i) {
3903 const mod = optionNames[i];
3904 result[mod] = this.getFontOption(pile, multiName, mod);
3905 }
3906
3907 return result;
3908 }
3909
3910 /////////////////////////////////////////////////////////
3911 // End methods for handling options piles
3912 /////////////////////////////////////////////////////////
3913
3914 /**
3915 * Collapse the font options for the multi-font to single objects, from
3916 * the chain of option objects passed (the 'pile').
3917 *
3918 * @param {Pile} pile sequence of option objects to consider.
3919 * First item in list assumed to be the newly set options.
3920 */
3921 propagateFonts(pile) {
3922 const fontPile = []; // sequence of font objects to consider, order important
3923
3924 // Note that this.elementOptions is not used here.
3925 this.addFontOptionsToPile(fontPile, pile);
3926 this.fontOptions = this.getBasicOptions(fontPile);
3927
3928 // We set multifont values even if multi === false, for consistency (things break otherwise)
3929 for (let i = 0; i < multiFontStyle.length; ++i) {
3930 const mod = multiFontStyle[i];
3931 const modOptions = this.fontOptions[mod];
3932 const tmpMultiFontOptions = this.getFontOptions(fontPile, mod);
3933
3934 // Copy over found values
3935 forEach(tmpMultiFontOptions, (option, n) => {
3936 modOptions[n] = option;
3937 });
3938
3939 modOptions.size = Number(modOptions.size);
3940 modOptions.vadjust = Number(modOptions.vadjust);
3941 }
3942 }
3943
3944 /**
3945 * Main function. This is called from anything that wants to draw a label.
3946 *
3947 * @param {CanvasRenderingContext2D} ctx
3948 * @param {number} x
3949 * @param {number} y
3950 * @param {boolean} selected
3951 * @param {boolean} hover
3952 * @param {string} [baseline='middle']
3953 */
3954 draw(ctx, x, y, selected, hover, baseline = "middle") {
3955 // if no label, return
3956 if (this.elementOptions.label === undefined) return;
3957
3958 // check if we have to render the label
3959 let viewFontSize = this.fontOptions.size * this.body.view.scale;
3960 if (
3961 this.elementOptions.label &&
3962 viewFontSize < this.elementOptions.scaling.label.drawThreshold - 1
3963 )
3964 return;
3965
3966 // This ensures that there will not be HUGE letters on screen
3967 // by setting an upper limit on the visible text size (regardless of zoomLevel)
3968 if (viewFontSize >= this.elementOptions.scaling.label.maxVisible) {
3969 viewFontSize =
3970 Number(this.elementOptions.scaling.label.maxVisible) /
3971 this.body.view.scale;
3972 }
3973
3974 // update the size cache if required
3975 this.calculateLabelSize(ctx, selected, hover, x, y, baseline);
3976 this._drawBackground(ctx);
3977 this._drawText(ctx, x, this.size.yLine, baseline, viewFontSize);
3978 }
3979
3980 /**
3981 * Draws the label background
3982 *
3983 * @param {CanvasRenderingContext2D} ctx
3984 * @private
3985 */
3986 _drawBackground(ctx) {
3987 if (
3988 this.fontOptions.background !== undefined &&
3989 this.fontOptions.background !== "none"
3990 ) {
3991 ctx.fillStyle = this.fontOptions.background;
3992 const size = this.getSize();
3993 ctx.fillRect(size.left, size.top, size.width, size.height);
3994 }
3995 }
3996
3997 /**
3998 *
3999 * @param {CanvasRenderingContext2D} ctx
4000 * @param {number} x
4001 * @param {number} y
4002 * @param {string} [baseline='middle']
4003 * @param {number} viewFontSize
4004 * @private
4005 */
4006 _drawText(ctx, x, y, baseline = "middle", viewFontSize) {
4007 [x, y] = this._setAlignment(ctx, x, y, baseline);
4008
4009 ctx.textAlign = "left";
4010 x = x - this.size.width / 2; // Shift label 1/2-distance to the left
4011 if (this.fontOptions.valign && this.size.height > this.size.labelHeight) {
4012 if (this.fontOptions.valign === "top") {
4013 y -= (this.size.height - this.size.labelHeight) / 2;
4014 }
4015 if (this.fontOptions.valign === "bottom") {
4016 y += (this.size.height - this.size.labelHeight) / 2;
4017 }
4018 }
4019
4020 // draw the text
4021 for (let i = 0; i < this.lineCount; i++) {
4022 const line = this.lines[i];
4023 if (line && line.blocks) {
4024 let width = 0;
4025 if (this.isEdgeLabel || this.fontOptions.align === "center") {
4026 width += (this.size.width - line.width) / 2;
4027 } else if (this.fontOptions.align === "right") {
4028 width += this.size.width - line.width;
4029 }
4030 for (let j = 0; j < line.blocks.length; j++) {
4031 const block = line.blocks[j];
4032 ctx.font = block.font;
4033 const [fontColor, strokeColor] = this._getColor(
4034 block.color,
4035 viewFontSize,
4036 block.strokeColor
4037 );
4038 if (block.strokeWidth > 0) {
4039 ctx.lineWidth = block.strokeWidth;
4040 ctx.strokeStyle = strokeColor;
4041 ctx.lineJoin = "round";
4042 }
4043 ctx.fillStyle = fontColor;
4044
4045 if (block.strokeWidth > 0) {
4046 ctx.strokeText(block.text, x + width, y + block.vadjust);
4047 }
4048 ctx.fillText(block.text, x + width, y + block.vadjust);
4049 width += block.width;
4050 }
4051 y += line.height;
4052 }
4053 }
4054 }
4055
4056 /**
4057 *
4058 * @param {CanvasRenderingContext2D} ctx
4059 * @param {number} x
4060 * @param {number} y
4061 * @param {string} baseline
4062 * @returns {Array.<number>}
4063 * @private
4064 */
4065 _setAlignment(ctx, x, y, baseline) {
4066 // check for label alignment (for edges)
4067 // TODO: make alignment for nodes
4068 if (
4069 this.isEdgeLabel &&
4070 this.fontOptions.align !== "horizontal" &&
4071 this.pointToSelf === false
4072 ) {
4073 x = 0;
4074 y = 0;
4075
4076 const lineMargin = 2;
4077 if (this.fontOptions.align === "top") {
4078 ctx.textBaseline = "alphabetic";
4079 y -= 2 * lineMargin; // distance from edge, required because we use alphabetic. Alphabetic has less difference between browsers
4080 } else if (this.fontOptions.align === "bottom") {
4081 ctx.textBaseline = "hanging";
4082 y += 2 * lineMargin; // distance from edge, required because we use hanging. Hanging has less difference between browsers
4083 } else {
4084 ctx.textBaseline = "middle";
4085 }
4086 } else {
4087 ctx.textBaseline = baseline;
4088 }
4089 return [x, y];
4090 }
4091
4092 /**
4093 * fade in when relative scale is between threshold and threshold - 1.
4094 * If the relative scale would be smaller than threshold -1 the draw function would have returned before coming here.
4095 *
4096 * @param {string} color The font color to use
4097 * @param {number} viewFontSize
4098 * @param {string} initialStrokeColor
4099 * @returns {Array.<string>} An array containing the font color and stroke color
4100 * @private
4101 */
4102 _getColor(color, viewFontSize, initialStrokeColor) {
4103 let fontColor = color || "#000000";
4104 let strokeColor = initialStrokeColor || "#ffffff";
4105 if (viewFontSize <= this.elementOptions.scaling.label.drawThreshold) {
4106 const opacity = Math.max(
4107 0,
4108 Math.min(
4109 1,
4110 1 - (this.elementOptions.scaling.label.drawThreshold - viewFontSize)
4111 )
4112 );
4113 fontColor = overrideOpacity(fontColor, opacity);
4114 strokeColor = overrideOpacity(strokeColor, opacity);
4115 }
4116 return [fontColor, strokeColor];
4117 }
4118
4119 /**
4120 *
4121 * @param {CanvasRenderingContext2D} ctx
4122 * @param {boolean} selected
4123 * @param {boolean} hover
4124 * @returns {{width: number, height: number}}
4125 */
4126 getTextSize(ctx, selected = false, hover = false) {
4127 this._processLabel(ctx, selected, hover);
4128 return {
4129 width: this.size.width,
4130 height: this.size.height,
4131 lineCount: this.lineCount,
4132 };
4133 }
4134
4135 /**
4136 * Get the current dimensions of the label
4137 *
4138 * @returns {rect}
4139 */
4140 getSize() {
4141 const lineMargin = 2;
4142 let x = this.size.left; // default values which might be overridden below
4143 let y = this.size.top - 0.5 * lineMargin; // idem
4144
4145 if (this.isEdgeLabel) {
4146 const x2 = -this.size.width * 0.5;
4147
4148 switch (this.fontOptions.align) {
4149 case "middle":
4150 x = x2;
4151 y = -this.size.height * 0.5;
4152 break;
4153 case "top":
4154 x = x2;
4155 y = -(this.size.height + lineMargin);
4156 break;
4157 case "bottom":
4158 x = x2;
4159 y = lineMargin;
4160 break;
4161 }
4162 }
4163
4164 const ret = {
4165 left: x,
4166 top: y,
4167 width: this.size.width,
4168 height: this.size.height,
4169 };
4170
4171 return ret;
4172 }
4173
4174 /**
4175 *
4176 * @param {CanvasRenderingContext2D} ctx
4177 * @param {boolean} selected
4178 * @param {boolean} hover
4179 * @param {number} [x=0]
4180 * @param {number} [y=0]
4181 * @param {'middle'|'hanging'} [baseline='middle']
4182 */
4183 calculateLabelSize(ctx, selected, hover, x = 0, y = 0, baseline = "middle") {
4184 this._processLabel(ctx, selected, hover);
4185 this.size.left = x - this.size.width * 0.5;
4186 this.size.top = y - this.size.height * 0.5;
4187 this.size.yLine = y + (1 - this.lineCount) * 0.5 * this.fontOptions.size;
4188 if (baseline === "hanging") {
4189 this.size.top += 0.5 * this.fontOptions.size;
4190 this.size.top += 4; // distance from node, required because we use hanging. Hanging has less difference between browsers
4191 this.size.yLine += 4; // distance from node
4192 }
4193 }
4194
4195 /**
4196 *
4197 * @param {CanvasRenderingContext2D} ctx
4198 * @param {boolean} selected
4199 * @param {boolean} hover
4200 * @param {string} mod
4201 * @returns {{color, size, face, mod, vadjust, strokeWidth: *, strokeColor: (*|string|allOptions.edges.font.strokeColor|{string}|allOptions.nodes.font.strokeColor|Array)}}
4202 */
4203 getFormattingValues(ctx, selected, hover, mod) {
4204 const getValue = function (fontOptions, mod, option) {
4205 if (mod === "normal") {
4206 if (option === "mod") return "";
4207 return fontOptions[option];
4208 }
4209
4210 if (fontOptions[mod][option] !== undefined) {
4211 // Grumbl leaving out test on undefined equals false for ""
4212 return fontOptions[mod][option];
4213 } else {
4214 // Take from parent font option
4215 return fontOptions[option];
4216 }
4217 };
4218
4219 const values = {
4220 color: getValue(this.fontOptions, mod, "color"),
4221 size: getValue(this.fontOptions, mod, "size"),
4222 face: getValue(this.fontOptions, mod, "face"),
4223 mod: getValue(this.fontOptions, mod, "mod"),
4224 vadjust: getValue(this.fontOptions, mod, "vadjust"),
4225 strokeWidth: this.fontOptions.strokeWidth,
4226 strokeColor: this.fontOptions.strokeColor,
4227 };
4228 if (selected || hover) {
4229 if (
4230 mod === "normal" &&
4231 this.fontOptions.chooser === true &&
4232 this.elementOptions.labelHighlightBold
4233 ) {
4234 values.mod = "bold";
4235 } else {
4236 if (typeof this.fontOptions.chooser === "function") {
4237 this.fontOptions.chooser(
4238 values,
4239 this.elementOptions.id,
4240 selected,
4241 hover
4242 );
4243 }
4244 }
4245 }
4246
4247 let fontString = "";
4248 if (values.mod !== undefined && values.mod !== "") {
4249 // safeguard for undefined - this happened
4250 fontString += values.mod + " ";
4251 }
4252 fontString += values.size + "px " + values.face;
4253
4254 ctx.font = fontString.replace(/"/g, "");
4255 values.font = ctx.font;
4256 values.height = values.size;
4257 return values;
4258 }
4259
4260 /**
4261 *
4262 * @param {boolean} selected
4263 * @param {boolean} hover
4264 * @returns {boolean}
4265 */
4266 differentState(selected, hover) {
4267 return selected !== this.selectedState || hover !== this.hoverState;
4268 }
4269
4270 /**
4271 * This explodes the passed text into lines and determines the width, height and number of lines.
4272 *
4273 * @param {CanvasRenderingContext2D} ctx
4274 * @param {boolean} selected
4275 * @param {boolean} hover
4276 * @param {string} inText the text to explode
4277 * @returns {{width, height, lines}|*}
4278 * @private
4279 */
4280 _processLabelText(ctx, selected, hover, inText) {
4281 const splitter = new LabelSplitter(ctx, this, selected, hover);
4282 return splitter.process(inText);
4283 }
4284
4285 /**
4286 * This explodes the label string into lines and sets the width, height and number of lines.
4287 *
4288 * @param {CanvasRenderingContext2D} ctx
4289 * @param {boolean} selected
4290 * @param {boolean} hover
4291 * @private
4292 */
4293 _processLabel(ctx, selected, hover) {
4294 if (this.labelDirty === false && !this.differentState(selected, hover))
4295 return;
4296
4297 const state = this._processLabelText(
4298 ctx,
4299 selected,
4300 hover,
4301 this.elementOptions.label
4302 );
4303
4304 if (this.fontOptions.minWdt > 0 && state.width < this.fontOptions.minWdt) {
4305 state.width = this.fontOptions.minWdt;
4306 }
4307
4308 this.size.labelHeight = state.height;
4309 if (this.fontOptions.minHgt > 0 && state.height < this.fontOptions.minHgt) {
4310 state.height = this.fontOptions.minHgt;
4311 }
4312
4313 this.lines = state.lines;
4314 this.lineCount = state.lines.length;
4315 this.size.width = state.width;
4316 this.size.height = state.height;
4317 this.selectedState = selected;
4318 this.hoverState = hover;
4319
4320 this.labelDirty = false;
4321 }
4322
4323 /**
4324 * Check if this label is visible
4325 *
4326 * @returns {boolean} true if this label will be show, false otherwise
4327 */
4328 visible() {
4329 if (
4330 this.size.width === 0 ||
4331 this.size.height === 0 ||
4332 this.elementOptions.label === undefined
4333 ) {
4334 return false; // nothing to display
4335 }
4336
4337 const viewFontSize = this.fontOptions.size * this.body.view.scale;
4338 if (viewFontSize < this.elementOptions.scaling.label.drawThreshold - 1) {
4339 return false; // Too small or too far away to show
4340 }
4341
4342 return true;
4343 }
4344}
4345
4346/**
4347 * The Base class for all Nodes.
4348 */
4349class NodeBase {
4350 /**
4351 * @param {object} options
4352 * @param {object} body
4353 * @param {Label} labelModule
4354 */
4355 constructor(options, body, labelModule) {
4356 this.body = body;
4357 this.labelModule = labelModule;
4358 this.setOptions(options);
4359 this.top = undefined;
4360 this.left = undefined;
4361 this.height = undefined;
4362 this.width = undefined;
4363 this.radius = undefined;
4364 this.margin = undefined;
4365 this.refreshNeeded = true;
4366 this.boundingBox = { top: 0, left: 0, right: 0, bottom: 0 };
4367 }
4368
4369 /**
4370 *
4371 * @param {object} options
4372 */
4373 setOptions(options) {
4374 this.options = options;
4375 }
4376
4377 /**
4378 *
4379 * @param {Label} labelModule
4380 * @private
4381 */
4382 _setMargins(labelModule) {
4383 this.margin = {};
4384 if (this.options.margin) {
4385 if (typeof this.options.margin == "object") {
4386 this.margin.top = this.options.margin.top;
4387 this.margin.right = this.options.margin.right;
4388 this.margin.bottom = this.options.margin.bottom;
4389 this.margin.left = this.options.margin.left;
4390 } else {
4391 this.margin.top = this.options.margin;
4392 this.margin.right = this.options.margin;
4393 this.margin.bottom = this.options.margin;
4394 this.margin.left = this.options.margin;
4395 }
4396 }
4397 labelModule.adjustSizes(this.margin);
4398 }
4399
4400 /**
4401 *
4402 * @param {CanvasRenderingContext2D} ctx
4403 * @param {number} angle
4404 * @returns {number}
4405 * @private
4406 */
4407 _distanceToBorder(ctx, angle) {
4408 const borderWidth = this.options.borderWidth;
4409 if (ctx) {
4410 this.resize(ctx);
4411 }
4412 return (
4413 Math.min(
4414 Math.abs(this.width / 2 / Math.cos(angle)),
4415 Math.abs(this.height / 2 / Math.sin(angle))
4416 ) + borderWidth
4417 );
4418 }
4419
4420 /**
4421 *
4422 * @param {CanvasRenderingContext2D} ctx
4423 * @param {ArrowOptions} values
4424 */
4425 enableShadow(ctx, values) {
4426 if (values.shadow) {
4427 ctx.shadowColor = values.shadowColor;
4428 ctx.shadowBlur = values.shadowSize;
4429 ctx.shadowOffsetX = values.shadowX;
4430 ctx.shadowOffsetY = values.shadowY;
4431 }
4432 }
4433
4434 /**
4435 *
4436 * @param {CanvasRenderingContext2D} ctx
4437 * @param {ArrowOptions} values
4438 */
4439 disableShadow(ctx, values) {
4440 if (values.shadow) {
4441 ctx.shadowColor = "rgba(0,0,0,0)";
4442 ctx.shadowBlur = 0;
4443 ctx.shadowOffsetX = 0;
4444 ctx.shadowOffsetY = 0;
4445 }
4446 }
4447
4448 /**
4449 *
4450 * @param {CanvasRenderingContext2D} ctx
4451 * @param {ArrowOptions} values
4452 */
4453 enableBorderDashes(ctx, values) {
4454 if (values.borderDashes !== false) {
4455 if (ctx.setLineDash !== undefined) {
4456 let dashes = values.borderDashes;
4457 if (dashes === true) {
4458 dashes = [5, 15];
4459 }
4460 ctx.setLineDash(dashes);
4461 } else {
4462 console.warn(
4463 "setLineDash is not supported in this browser. The dashed borders cannot be used."
4464 );
4465 this.options.shapeProperties.borderDashes = false;
4466 values.borderDashes = false;
4467 }
4468 }
4469 }
4470
4471 /**
4472 *
4473 * @param {CanvasRenderingContext2D} ctx
4474 * @param {ArrowOptions} values
4475 */
4476 disableBorderDashes(ctx, values) {
4477 if (values.borderDashes !== false) {
4478 if (ctx.setLineDash !== undefined) {
4479 ctx.setLineDash([0]);
4480 } else {
4481 console.warn(
4482 "setLineDash is not supported in this browser. The dashed borders cannot be used."
4483 );
4484 this.options.shapeProperties.borderDashes = false;
4485 values.borderDashes = false;
4486 }
4487 }
4488 }
4489
4490 /**
4491 * Determine if the shape of a node needs to be recalculated.
4492 *
4493 * @param {boolean} selected
4494 * @param {boolean} hover
4495 * @returns {boolean}
4496 * @protected
4497 */
4498 needsRefresh(selected, hover) {
4499 if (this.refreshNeeded === true) {
4500 // This is probably not the best location to reset this member.
4501 // However, in the current logic, it is the most convenient one.
4502 this.refreshNeeded = false;
4503 return true;
4504 }
4505
4506 return (
4507 this.width === undefined ||
4508 this.labelModule.differentState(selected, hover)
4509 );
4510 }
4511
4512 /**
4513 *
4514 * @param {CanvasRenderingContext2D} ctx
4515 * @param {ArrowOptions} values
4516 */
4517 initContextForDraw(ctx, values) {
4518 const borderWidth = values.borderWidth / this.body.view.scale;
4519
4520 ctx.lineWidth = Math.min(this.width, borderWidth);
4521 ctx.strokeStyle = values.borderColor;
4522 ctx.fillStyle = values.color;
4523 }
4524
4525 /**
4526 *
4527 * @param {CanvasRenderingContext2D} ctx
4528 * @param {ArrowOptions} values
4529 */
4530 performStroke(ctx, values) {
4531 const borderWidth = values.borderWidth / this.body.view.scale;
4532
4533 //draw dashed border if enabled, save and restore is required for firefox not to crash on unix.
4534 ctx.save();
4535 // if borders are zero width, they will be drawn with width 1 by default. This prevents that
4536 if (borderWidth > 0) {
4537 this.enableBorderDashes(ctx, values);
4538 //draw the border
4539 ctx.stroke();
4540 //disable dashed border for other elements
4541 this.disableBorderDashes(ctx, values);
4542 }
4543 ctx.restore();
4544 }
4545
4546 /**
4547 *
4548 * @param {CanvasRenderingContext2D} ctx
4549 * @param {ArrowOptions} values
4550 */
4551 performFill(ctx, values) {
4552 ctx.save();
4553 ctx.fillStyle = values.color;
4554 // draw shadow if enabled
4555 this.enableShadow(ctx, values);
4556 // draw the background
4557 ctx.fill();
4558 // disable shadows for other elements.
4559 this.disableShadow(ctx, values);
4560
4561 ctx.restore();
4562 this.performStroke(ctx, values);
4563 }
4564
4565 /**
4566 *
4567 * @param {number} margin
4568 * @private
4569 */
4570 _addBoundingBoxMargin(margin) {
4571 this.boundingBox.left -= margin;
4572 this.boundingBox.top -= margin;
4573 this.boundingBox.bottom += margin;
4574 this.boundingBox.right += margin;
4575 }
4576
4577 /**
4578 * Actual implementation of this method call.
4579 *
4580 * Doing it like this makes it easier to override
4581 * in the child classes.
4582 *
4583 * @param {number} x width
4584 * @param {number} y height
4585 * @param {CanvasRenderingContext2D} ctx
4586 * @param {boolean} selected
4587 * @param {boolean} hover
4588 * @private
4589 */
4590 _updateBoundingBox(x, y, ctx, selected, hover) {
4591 if (ctx !== undefined) {
4592 this.resize(ctx, selected, hover);
4593 }
4594
4595 this.left = x - this.width / 2;
4596 this.top = y - this.height / 2;
4597
4598 this.boundingBox.left = this.left;
4599 this.boundingBox.top = this.top;
4600 this.boundingBox.bottom = this.top + this.height;
4601 this.boundingBox.right = this.left + this.width;
4602 }
4603
4604 /**
4605 * Default implementation of this method call.
4606 * This acts as a stub which can be overridden.
4607 *
4608 * @param {number} x width
4609 * @param {number} y height
4610 * @param {CanvasRenderingContext2D} ctx
4611 * @param {boolean} selected
4612 * @param {boolean} hover
4613 */
4614 updateBoundingBox(x, y, ctx, selected, hover) {
4615 this._updateBoundingBox(x, y, ctx, selected, hover);
4616 }
4617
4618 /**
4619 * Determine the dimensions to use for nodes with an internal label
4620 *
4621 * Currently, these are: Circle, Ellipse, Database, Box
4622 * The other nodes have external labels, and will not call this method
4623 *
4624 * If there is no label, decent default values are supplied.
4625 *
4626 * @param {CanvasRenderingContext2D} ctx
4627 * @param {boolean} [selected]
4628 * @param {boolean} [hover]
4629 * @returns {{width:number, height:number}}
4630 */
4631 getDimensionsFromLabel(ctx, selected, hover) {
4632 // NOTE: previously 'textSize' was not put in 'this' for Ellipse
4633 // TODO: examine the consequences.
4634 this.textSize = this.labelModule.getTextSize(ctx, selected, hover);
4635 let width = this.textSize.width;
4636 let height = this.textSize.height;
4637
4638 const DEFAULT_SIZE = 14;
4639 if (width === 0) {
4640 // This happens when there is no label text set
4641 width = DEFAULT_SIZE; // use a decent default
4642 height = DEFAULT_SIZE; // if width zero, then height also always zero
4643 }
4644
4645 return { width: width, height: height };
4646 }
4647}
4648
4649/**
4650 * A Box Node/Cluster shape.
4651 *
4652 * @augments NodeBase
4653 */
4654class Box$1 extends NodeBase {
4655 /**
4656 * @param {object} options
4657 * @param {object} body
4658 * @param {Label} labelModule
4659 */
4660 constructor(options, body, labelModule) {
4661 super(options, body, labelModule);
4662 this._setMargins(labelModule);
4663 }
4664
4665 /**
4666 *
4667 * @param {CanvasRenderingContext2D} ctx
4668 * @param {boolean} [selected]
4669 * @param {boolean} [hover]
4670 */
4671 resize(ctx, selected = this.selected, hover = this.hover) {
4672 if (this.needsRefresh(selected, hover)) {
4673 const dimensions = this.getDimensionsFromLabel(ctx, selected, hover);
4674
4675 this.width = dimensions.width + this.margin.right + this.margin.left;
4676 this.height = dimensions.height + this.margin.top + this.margin.bottom;
4677 this.radius = this.width / 2;
4678 }
4679 }
4680
4681 /**
4682 *
4683 * @param {CanvasRenderingContext2D} ctx
4684 * @param {number} x width
4685 * @param {number} y height
4686 * @param {boolean} selected
4687 * @param {boolean} hover
4688 * @param {ArrowOptions} values
4689 */
4690 draw(ctx, x, y, selected, hover, values) {
4691 this.resize(ctx, selected, hover);
4692 this.left = x - this.width / 2;
4693 this.top = y - this.height / 2;
4694
4695 this.initContextForDraw(ctx, values);
4696 drawRoundRect(
4697 ctx,
4698 this.left,
4699 this.top,
4700 this.width,
4701 this.height,
4702 values.borderRadius
4703 );
4704 this.performFill(ctx, values);
4705
4706 this.updateBoundingBox(x, y, ctx, selected, hover);
4707 this.labelModule.draw(
4708 ctx,
4709 this.left + this.textSize.width / 2 + this.margin.left,
4710 this.top + this.textSize.height / 2 + this.margin.top,
4711 selected,
4712 hover
4713 );
4714 }
4715
4716 /**
4717 *
4718 * @param {number} x width
4719 * @param {number} y height
4720 * @param {CanvasRenderingContext2D} ctx
4721 * @param {boolean} selected
4722 * @param {boolean} hover
4723 */
4724 updateBoundingBox(x, y, ctx, selected, hover) {
4725 this._updateBoundingBox(x, y, ctx, selected, hover);
4726
4727 const borderRadius = this.options.shapeProperties.borderRadius; // only effective for box
4728 this._addBoundingBoxMargin(borderRadius);
4729 }
4730
4731 /**
4732 *
4733 * @param {CanvasRenderingContext2D} ctx
4734 * @param {number} angle
4735 * @returns {number}
4736 */
4737 distanceToBorder(ctx, angle) {
4738 if (ctx) {
4739 this.resize(ctx);
4740 }
4741 const borderWidth = this.options.borderWidth;
4742
4743 return (
4744 Math.min(
4745 Math.abs(this.width / 2 / Math.cos(angle)),
4746 Math.abs(this.height / 2 / Math.sin(angle))
4747 ) + borderWidth
4748 );
4749 }
4750}
4751
4752/**
4753 * NOTE: This is a bad base class
4754 *
4755 * Child classes are:
4756 *
4757 * Image - uses *only* image methods
4758 * Circle - uses *only* _drawRawCircle
4759 * CircleImage - uses all
4760 *
4761 * TODO: Refactor, move _drawRawCircle to different module, derive Circle from NodeBase
4762 * Rename this to ImageBase
4763 * Consolidate common code in Image and CircleImage to base class
4764 *
4765 * @augments NodeBase
4766 */
4767class CircleImageBase extends NodeBase {
4768 /**
4769 * @param {object} options
4770 * @param {object} body
4771 * @param {Label} labelModule
4772 */
4773 constructor(options, body, labelModule) {
4774 super(options, body, labelModule);
4775 this.labelOffset = 0;
4776 this.selected = false;
4777 }
4778
4779 /**
4780 *
4781 * @param {object} options
4782 * @param {object} [imageObj]
4783 * @param {object} [imageObjAlt]
4784 */
4785 setOptions(options, imageObj, imageObjAlt) {
4786 this.options = options;
4787
4788 if (!(imageObj === undefined && imageObjAlt === undefined)) {
4789 this.setImages(imageObj, imageObjAlt);
4790 }
4791 }
4792
4793 /**
4794 * Set the images for this node.
4795 *
4796 * The images can be updated after the initial setting of options;
4797 * therefore, this method needs to be reentrant.
4798 *
4799 * For correct working in error cases, it is necessary to properly set
4800 * field 'nodes.brokenImage' in the options.
4801 *
4802 * @param {Image} imageObj required; main image to show for this node
4803 * @param {Image|undefined} imageObjAlt optional; image to show when node is selected
4804 */
4805 setImages(imageObj, imageObjAlt) {
4806 if (imageObjAlt && this.selected) {
4807 this.imageObj = imageObjAlt;
4808 this.imageObjAlt = imageObj;
4809 } else {
4810 this.imageObj = imageObj;
4811 this.imageObjAlt = imageObjAlt;
4812 }
4813 }
4814
4815 /**
4816 * Set selection and switch between the base and the selected image.
4817 *
4818 * Do the switch only if imageObjAlt exists.
4819 *
4820 * @param {boolean} selected value of new selected state for current node
4821 */
4822 switchImages(selected) {
4823 const selection_changed =
4824 (selected && !this.selected) || (!selected && this.selected);
4825 this.selected = selected; // Remember new selection
4826
4827 if (this.imageObjAlt !== undefined && selection_changed) {
4828 const imageTmp = this.imageObj;
4829 this.imageObj = this.imageObjAlt;
4830 this.imageObjAlt = imageTmp;
4831 }
4832 }
4833
4834 /**
4835 * Returns Image Padding from node options
4836 *
4837 * @returns {{top: number,left: number,bottom: number,right: number}} image padding inside this shape
4838 * @private
4839 */
4840 _getImagePadding() {
4841 const imgPadding = { top: 0, right: 0, bottom: 0, left: 0 };
4842 if (this.options.imagePadding) {
4843 const optImgPadding = this.options.imagePadding;
4844 if (typeof optImgPadding == "object") {
4845 imgPadding.top = optImgPadding.top;
4846 imgPadding.right = optImgPadding.right;
4847 imgPadding.bottom = optImgPadding.bottom;
4848 imgPadding.left = optImgPadding.left;
4849 } else {
4850 imgPadding.top = optImgPadding;
4851 imgPadding.right = optImgPadding;
4852 imgPadding.bottom = optImgPadding;
4853 imgPadding.left = optImgPadding;
4854 }
4855 }
4856
4857 return imgPadding;
4858 }
4859
4860 /**
4861 * Adjust the node dimensions for a loaded image.
4862 *
4863 * Pre: this.imageObj is valid
4864 */
4865 _resizeImage() {
4866 let width, height;
4867
4868 if (this.options.shapeProperties.useImageSize === false) {
4869 // Use the size property
4870 let ratio_width = 1;
4871 let ratio_height = 1;
4872
4873 // Only calculate the proper ratio if both width and height not zero
4874 if (this.imageObj.width && this.imageObj.height) {
4875 if (this.imageObj.width > this.imageObj.height) {
4876 ratio_width = this.imageObj.width / this.imageObj.height;
4877 } else {
4878 ratio_height = this.imageObj.height / this.imageObj.width;
4879 }
4880 }
4881
4882 width = this.options.size * 2 * ratio_width;
4883 height = this.options.size * 2 * ratio_height;
4884 } else {
4885 // Use the image size with image padding
4886 const imgPadding = this._getImagePadding();
4887 width = this.imageObj.width + imgPadding.left + imgPadding.right;
4888 height = this.imageObj.height + imgPadding.top + imgPadding.bottom;
4889 }
4890
4891 this.width = width;
4892 this.height = height;
4893 this.radius = 0.5 * this.width;
4894 }
4895
4896 /**
4897 *
4898 * @param {CanvasRenderingContext2D} ctx
4899 * @param {number} x width
4900 * @param {number} y height
4901 * @param {ArrowOptions} values
4902 * @private
4903 */
4904 _drawRawCircle(ctx, x, y, values) {
4905 this.initContextForDraw(ctx, values);
4906 drawCircle(ctx, x, y, values.size);
4907 this.performFill(ctx, values);
4908 }
4909
4910 /**
4911 *
4912 * @param {CanvasRenderingContext2D} ctx
4913 * @param {ArrowOptions} values
4914 * @private
4915 */
4916 _drawImageAtPosition(ctx, values) {
4917 if (this.imageObj.width != 0) {
4918 // draw the image
4919 ctx.globalAlpha = values.opacity !== undefined ? values.opacity : 1;
4920
4921 // draw shadow if enabled
4922 this.enableShadow(ctx, values);
4923
4924 let factor = 1;
4925 if (this.options.shapeProperties.interpolation === true) {
4926 factor = this.imageObj.width / this.width / this.body.view.scale;
4927 }
4928
4929 const imgPadding = this._getImagePadding();
4930
4931 const imgPosLeft = this.left + imgPadding.left;
4932 const imgPosTop = this.top + imgPadding.top;
4933 const imgWidth = this.width - imgPadding.left - imgPadding.right;
4934 const imgHeight = this.height - imgPadding.top - imgPadding.bottom;
4935 this.imageObj.drawImageAtPosition(
4936 ctx,
4937 factor,
4938 imgPosLeft,
4939 imgPosTop,
4940 imgWidth,
4941 imgHeight
4942 );
4943
4944 // disable shadows for other elements.
4945 this.disableShadow(ctx, values);
4946 }
4947 }
4948
4949 /**
4950 *
4951 * @param {CanvasRenderingContext2D} ctx
4952 * @param {number} x width
4953 * @param {number} y height
4954 * @param {boolean} selected
4955 * @param {boolean} hover
4956 * @private
4957 */
4958 _drawImageLabel(ctx, x, y, selected, hover) {
4959 let offset = 0;
4960
4961 if (this.height !== undefined) {
4962 offset = this.height * 0.5;
4963 const labelDimensions = this.labelModule.getTextSize(
4964 ctx,
4965 selected,
4966 hover
4967 );
4968 if (labelDimensions.lineCount >= 1) {
4969 offset += labelDimensions.height / 2;
4970 }
4971 }
4972
4973 const yLabel = y + offset;
4974
4975 if (this.options.label) {
4976 this.labelOffset = offset;
4977 }
4978 this.labelModule.draw(ctx, x, yLabel, selected, hover, "hanging");
4979 }
4980}
4981
4982/**
4983 * A Circle Node/Cluster shape.
4984 *
4985 * @augments CircleImageBase
4986 */
4987class Circle$1 extends CircleImageBase {
4988 /**
4989 * @param {object} options
4990 * @param {object} body
4991 * @param {Label} labelModule
4992 */
4993 constructor(options, body, labelModule) {
4994 super(options, body, labelModule);
4995 this._setMargins(labelModule);
4996 }
4997
4998 /**
4999 *
5000 * @param {CanvasRenderingContext2D} ctx
5001 * @param {boolean} [selected]
5002 * @param {boolean} [hover]
5003 */
5004 resize(ctx, selected = this.selected, hover = this.hover) {
5005 if (this.needsRefresh(selected, hover)) {
5006 const dimensions = this.getDimensionsFromLabel(ctx, selected, hover);
5007
5008 const diameter = Math.max(
5009 dimensions.width + this.margin.right + this.margin.left,
5010 dimensions.height + this.margin.top + this.margin.bottom
5011 );
5012
5013 this.options.size = diameter / 2; // NOTE: this size field only set here, not in Ellipse, Database, Box
5014 this.width = diameter;
5015 this.height = diameter;
5016 this.radius = this.width / 2;
5017 }
5018 }
5019
5020 /**
5021 *
5022 * @param {CanvasRenderingContext2D} ctx
5023 * @param {number} x width
5024 * @param {number} y height
5025 * @param {boolean} selected
5026 * @param {boolean} hover
5027 * @param {ArrowOptions} values
5028 */
5029 draw(ctx, x, y, selected, hover, values) {
5030 this.resize(ctx, selected, hover);
5031 this.left = x - this.width / 2;
5032 this.top = y - this.height / 2;
5033
5034 this._drawRawCircle(ctx, x, y, values);
5035
5036 this.updateBoundingBox(x, y);
5037 this.labelModule.draw(
5038 ctx,
5039 this.left + this.textSize.width / 2 + this.margin.left,
5040 y,
5041 selected,
5042 hover
5043 );
5044 }
5045
5046 /**
5047 *
5048 * @param {number} x width
5049 * @param {number} y height
5050 */
5051 updateBoundingBox(x, y) {
5052 this.boundingBox.top = y - this.options.size;
5053 this.boundingBox.left = x - this.options.size;
5054 this.boundingBox.right = x + this.options.size;
5055 this.boundingBox.bottom = y + this.options.size;
5056 }
5057
5058 /**
5059 *
5060 * @param {CanvasRenderingContext2D} ctx
5061 * @returns {number}
5062 */
5063 distanceToBorder(ctx) {
5064 if (ctx) {
5065 this.resize(ctx);
5066 }
5067 return this.width * 0.5;
5068 }
5069}
5070
5071/**
5072 * A CircularImage Node/Cluster shape.
5073 *
5074 * @augments CircleImageBase
5075 */
5076class CircularImage extends CircleImageBase {
5077 /**
5078 * @param {object} options
5079 * @param {object} body
5080 * @param {Label} labelModule
5081 * @param {Image} imageObj
5082 * @param {Image} imageObjAlt
5083 */
5084 constructor(options, body, labelModule, imageObj, imageObjAlt) {
5085 super(options, body, labelModule);
5086
5087 this.setImages(imageObj, imageObjAlt);
5088 }
5089
5090 /**
5091 *
5092 * @param {CanvasRenderingContext2D} ctx
5093 * @param {boolean} [selected]
5094 * @param {boolean} [hover]
5095 */
5096 resize(ctx, selected = this.selected, hover = this.hover) {
5097 const imageAbsent =
5098 this.imageObj.src === undefined ||
5099 this.imageObj.width === undefined ||
5100 this.imageObj.height === undefined;
5101
5102 if (imageAbsent) {
5103 const diameter = this.options.size * 2;
5104 this.width = diameter;
5105 this.height = diameter;
5106 this.radius = 0.5 * this.width;
5107 return;
5108 }
5109
5110 // At this point, an image is present, i.e. this.imageObj is valid.
5111 if (this.needsRefresh(selected, hover)) {
5112 this._resizeImage();
5113 }
5114 }
5115
5116 /**
5117 *
5118 * @param {CanvasRenderingContext2D} ctx
5119 * @param {number} x width
5120 * @param {number} y height
5121 * @param {boolean} selected
5122 * @param {boolean} hover
5123 * @param {ArrowOptions} values
5124 */
5125 draw(ctx, x, y, selected, hover, values) {
5126 this.switchImages(selected);
5127 this.resize();
5128
5129 let labelX = x,
5130 labelY = y;
5131
5132 if (this.options.shapeProperties.coordinateOrigin === "top-left") {
5133 this.left = x;
5134 this.top = y;
5135 labelX += this.width / 2;
5136 labelY += this.height / 2;
5137 } else {
5138 this.left = x - this.width / 2;
5139 this.top = y - this.height / 2;
5140 }
5141
5142 // draw the background circle. IMPORTANT: the stroke in this method is used by the clip method below.
5143 this._drawRawCircle(ctx, labelX, labelY, values);
5144
5145 // now we draw in the circle, we save so we can revert the clip operation after drawing.
5146 ctx.save();
5147 // clip is used to use the stroke in drawRawCircle as an area that we can draw in.
5148 ctx.clip();
5149 // draw the image
5150 this._drawImageAtPosition(ctx, values);
5151 // restore so we can again draw on the full canvas
5152 ctx.restore();
5153
5154 this._drawImageLabel(ctx, labelX, labelY, selected, hover);
5155
5156 this.updateBoundingBox(x, y);
5157 }
5158
5159 // TODO: compare with Circle.updateBoundingBox(), consolidate? More stuff is happening here
5160 /**
5161 *
5162 * @param {number} x width
5163 * @param {number} y height
5164 */
5165 updateBoundingBox(x, y) {
5166 if (this.options.shapeProperties.coordinateOrigin === "top-left") {
5167 this.boundingBox.top = y;
5168 this.boundingBox.left = x;
5169 this.boundingBox.right = x + this.options.size * 2;
5170 this.boundingBox.bottom = y + this.options.size * 2;
5171 } else {
5172 this.boundingBox.top = y - this.options.size;
5173 this.boundingBox.left = x - this.options.size;
5174 this.boundingBox.right = x + this.options.size;
5175 this.boundingBox.bottom = y + this.options.size;
5176 }
5177
5178 // TODO: compare with Image.updateBoundingBox(), consolidate?
5179 this.boundingBox.left = Math.min(
5180 this.boundingBox.left,
5181 this.labelModule.size.left
5182 );
5183 this.boundingBox.right = Math.max(
5184 this.boundingBox.right,
5185 this.labelModule.size.left + this.labelModule.size.width
5186 );
5187 this.boundingBox.bottom = Math.max(
5188 this.boundingBox.bottom,
5189 this.boundingBox.bottom + this.labelOffset
5190 );
5191 }
5192
5193 /**
5194 *
5195 * @param {CanvasRenderingContext2D} ctx
5196 * @returns {number}
5197 */
5198 distanceToBorder(ctx) {
5199 if (ctx) {
5200 this.resize(ctx);
5201 }
5202 return this.width * 0.5;
5203 }
5204}
5205
5206/**
5207 * Base class for constructing Node/Cluster Shapes.
5208 *
5209 * @augments NodeBase
5210 */
5211class ShapeBase extends NodeBase {
5212 /**
5213 * @param {object} options
5214 * @param {object} body
5215 * @param {Label} labelModule
5216 */
5217 constructor(options, body, labelModule) {
5218 super(options, body, labelModule);
5219 }
5220
5221 /**
5222 *
5223 * @param {CanvasRenderingContext2D} ctx
5224 * @param {boolean} [selected]
5225 * @param {boolean} [hover]
5226 * @param {object} [values={size: this.options.size}]
5227 */
5228 resize(
5229 ctx,
5230 selected = this.selected,
5231 hover = this.hover,
5232 values = { size: this.options.size }
5233 ) {
5234 if (this.needsRefresh(selected, hover)) {
5235 this.labelModule.getTextSize(ctx, selected, hover);
5236 const size = 2 * values.size;
5237 this.width = this.customSizeWidth ?? size;
5238 this.height = this.customSizeHeight ?? size;
5239 this.radius = 0.5 * this.width;
5240 }
5241 }
5242
5243 /**
5244 *
5245 * @param {CanvasRenderingContext2D} ctx
5246 * @param {string} shape
5247 * @param {number} sizeMultiplier - Unused! TODO: Remove next major release
5248 * @param {number} x
5249 * @param {number} y
5250 * @param {boolean} selected
5251 * @param {boolean} hover
5252 * @param {ArrowOptions} values
5253 * @private
5254 *
5255 * @returns {object} Callbacks to draw later on higher layers.
5256 */
5257 _drawShape(ctx, shape, sizeMultiplier, x, y, selected, hover, values) {
5258 this.resize(ctx, selected, hover, values);
5259 this.left = x - this.width / 2;
5260 this.top = y - this.height / 2;
5261
5262 this.initContextForDraw(ctx, values);
5263 getShape(shape)(ctx, x, y, values.size);
5264 this.performFill(ctx, values);
5265
5266 if (this.options.icon !== undefined) {
5267 if (this.options.icon.code !== undefined) {
5268 ctx.font =
5269 (selected ? "bold " : "") +
5270 this.height / 2 +
5271 "px " +
5272 (this.options.icon.face || "FontAwesome");
5273 ctx.fillStyle = this.options.icon.color || "black";
5274 ctx.textAlign = "center";
5275 ctx.textBaseline = "middle";
5276 ctx.fillText(this.options.icon.code, x, y);
5277 }
5278 }
5279
5280 return {
5281 drawExternalLabel: () => {
5282 if (this.options.label !== undefined) {
5283 // Need to call following here in order to ensure value for
5284 // `this.labelModule.size.height`.
5285 this.labelModule.calculateLabelSize(
5286 ctx,
5287 selected,
5288 hover,
5289 x,
5290 y,
5291 "hanging"
5292 );
5293 const yLabel =
5294 y + 0.5 * this.height + 0.5 * this.labelModule.size.height;
5295 this.labelModule.draw(ctx, x, yLabel, selected, hover, "hanging");
5296 }
5297
5298 this.updateBoundingBox(x, y);
5299 },
5300 };
5301 }
5302
5303 /**
5304 *
5305 * @param {number} x
5306 * @param {number} y
5307 */
5308 updateBoundingBox(x, y) {
5309 this.boundingBox.top = y - this.options.size;
5310 this.boundingBox.left = x - this.options.size;
5311 this.boundingBox.right = x + this.options.size;
5312 this.boundingBox.bottom = y + this.options.size;
5313
5314 if (this.options.label !== undefined && this.labelModule.size.width > 0) {
5315 this.boundingBox.left = Math.min(
5316 this.boundingBox.left,
5317 this.labelModule.size.left
5318 );
5319 this.boundingBox.right = Math.max(
5320 this.boundingBox.right,
5321 this.labelModule.size.left + this.labelModule.size.width
5322 );
5323 this.boundingBox.bottom = Math.max(
5324 this.boundingBox.bottom,
5325 this.boundingBox.bottom + this.labelModule.size.height
5326 );
5327 }
5328 }
5329}
5330
5331/**
5332 * A CustomShape Node/Cluster shape.
5333 *
5334 * @augments ShapeBase
5335 */
5336class CustomShape extends ShapeBase {
5337 /**
5338 * @param {object} options
5339 * @param {object} body
5340 * @param {Label} labelModule
5341 * @param {Function} ctxRenderer
5342
5343 */
5344 constructor(options, body, labelModule, ctxRenderer) {
5345 super(options, body, labelModule, ctxRenderer);
5346 this.ctxRenderer = ctxRenderer;
5347 }
5348
5349 /**
5350 *
5351 * @param {CanvasRenderingContext2D} ctx
5352 * @param {number} x width
5353 * @param {number} y height
5354 * @param {boolean} selected
5355 * @param {boolean} hover
5356 * @param {ArrowOptions} values
5357 *
5358 * @returns {object} Callbacks to draw later on different layers.
5359 */
5360 draw(ctx, x, y, selected, hover, values) {
5361 this.resize(ctx, selected, hover, values);
5362 this.left = x - this.width / 2;
5363 this.top = y - this.height / 2;
5364
5365 // Guard right away because someone may just draw in the function itself.
5366 ctx.save();
5367 const drawLater = this.ctxRenderer({
5368 ctx,
5369 id: this.options.id,
5370 x,
5371 y,
5372 state: { selected, hover },
5373 style: { ...values },
5374 label: this.options.label,
5375 });
5376 // Render the node shape bellow arrows.
5377 if (drawLater.drawNode != null) {
5378 drawLater.drawNode();
5379 }
5380 ctx.restore();
5381
5382 if (drawLater.drawExternalLabel) {
5383 // Guard the external label (above arrows) drawing function.
5384 const drawExternalLabel = drawLater.drawExternalLabel;
5385 drawLater.drawExternalLabel = () => {
5386 ctx.save();
5387 drawExternalLabel();
5388 ctx.restore();
5389 };
5390 }
5391
5392 if (drawLater.nodeDimensions) {
5393 this.customSizeWidth = drawLater.nodeDimensions.width;
5394 this.customSizeHeight = drawLater.nodeDimensions.height;
5395 }
5396
5397 return drawLater;
5398 }
5399
5400 /**
5401 *
5402 * @param {CanvasRenderingContext2D} ctx
5403 * @param {number} angle
5404 * @returns {number}
5405 */
5406 distanceToBorder(ctx, angle) {
5407 return this._distanceToBorder(ctx, angle);
5408 }
5409}
5410
5411/**
5412 * A Database Node/Cluster shape.
5413 *
5414 * @augments NodeBase
5415 */
5416class Database extends NodeBase {
5417 /**
5418 * @param {object} options
5419 * @param {object} body
5420 * @param {Label} labelModule
5421 */
5422 constructor(options, body, labelModule) {
5423 super(options, body, labelModule);
5424 this._setMargins(labelModule);
5425 }
5426
5427 /**
5428 *
5429 * @param {CanvasRenderingContext2D} ctx
5430 * @param {boolean} selected
5431 * @param {boolean} hover
5432 */
5433 resize(ctx, selected, hover) {
5434 if (this.needsRefresh(selected, hover)) {
5435 const dimensions = this.getDimensionsFromLabel(ctx, selected, hover);
5436 const size = dimensions.width + this.margin.right + this.margin.left;
5437
5438 this.width = size;
5439 this.height = size;
5440 this.radius = this.width / 2;
5441 }
5442 }
5443
5444 /**
5445 *
5446 * @param {CanvasRenderingContext2D} ctx
5447 * @param {number} x width
5448 * @param {number} y height
5449 * @param {boolean} selected
5450 * @param {boolean} hover
5451 * @param {ArrowOptions} values
5452 */
5453 draw(ctx, x, y, selected, hover, values) {
5454 this.resize(ctx, selected, hover);
5455 this.left = x - this.width / 2;
5456 this.top = y - this.height / 2;
5457
5458 this.initContextForDraw(ctx, values);
5459 drawDatabase(
5460 ctx,
5461 x - this.width / 2,
5462 y - this.height / 2,
5463 this.width,
5464 this.height
5465 );
5466 this.performFill(ctx, values);
5467
5468 this.updateBoundingBox(x, y, ctx, selected, hover);
5469 this.labelModule.draw(
5470 ctx,
5471 this.left + this.textSize.width / 2 + this.margin.left,
5472 this.top + this.textSize.height / 2 + this.margin.top,
5473 selected,
5474 hover
5475 );
5476 }
5477 /**
5478 *
5479 * @param {CanvasRenderingContext2D} ctx
5480 * @param {number} angle
5481 * @returns {number}
5482 */
5483 distanceToBorder(ctx, angle) {
5484 return this._distanceToBorder(ctx, angle);
5485 }
5486}
5487
5488/**
5489 * A Diamond Node/Cluster shape.
5490 *
5491 * @augments ShapeBase
5492 */
5493class Diamond$1 extends ShapeBase {
5494 /**
5495 * @param {object} options
5496 * @param {object} body
5497 * @param {Label} labelModule
5498 */
5499 constructor(options, body, labelModule) {
5500 super(options, body, labelModule);
5501 }
5502
5503 /**
5504 *
5505 * @param {CanvasRenderingContext2D} ctx
5506 * @param {number} x width
5507 * @param {number} y height
5508 * @param {boolean} selected
5509 * @param {boolean} hover
5510 * @param {ArrowOptions} values
5511 *
5512 * @returns {object} Callbacks to draw later on higher layers.
5513 */
5514 draw(ctx, x, y, selected, hover, values) {
5515 return this._drawShape(ctx, "diamond", 4, x, y, selected, hover, values);
5516 }
5517
5518 /**
5519 *
5520 * @param {CanvasRenderingContext2D} ctx
5521 * @param {number} angle
5522 * @returns {number}
5523 */
5524 distanceToBorder(ctx, angle) {
5525 return this._distanceToBorder(ctx, angle);
5526 }
5527}
5528
5529/**
5530 * A Dot Node/Cluster shape.
5531 *
5532 * @augments ShapeBase
5533 */
5534class Dot extends ShapeBase {
5535 /**
5536 * @param {object} options
5537 * @param {object} body
5538 * @param {Label} labelModule
5539 */
5540 constructor(options, body, labelModule) {
5541 super(options, body, labelModule);
5542 }
5543
5544 /**
5545 *
5546 * @param {CanvasRenderingContext2D} ctx
5547 * @param {number} x width
5548 * @param {number} y height
5549 * @param {boolean} selected
5550 * @param {boolean} hover
5551 * @param {ArrowOptions} values
5552 *
5553 * @returns {object} Callbacks to draw later on higher layers.
5554 */
5555 draw(ctx, x, y, selected, hover, values) {
5556 return this._drawShape(ctx, "circle", 2, x, y, selected, hover, values);
5557 }
5558
5559 /**
5560 *
5561 * @param {CanvasRenderingContext2D} ctx
5562 * @returns {number}
5563 */
5564 distanceToBorder(ctx) {
5565 if (ctx) {
5566 this.resize(ctx);
5567 }
5568 return this.options.size;
5569 }
5570}
5571
5572/**
5573 * Am Ellipse Node/Cluster shape.
5574 *
5575 * @augments NodeBase
5576 */
5577class Ellipse extends NodeBase {
5578 /**
5579 * @param {object} options
5580 * @param {object} body
5581 * @param {Label} labelModule
5582 */
5583 constructor(options, body, labelModule) {
5584 super(options, body, labelModule);
5585 }
5586
5587 /**
5588 *
5589 * @param {CanvasRenderingContext2D} ctx
5590 * @param {boolean} [selected]
5591 * @param {boolean} [hover]
5592 */
5593 resize(ctx, selected = this.selected, hover = this.hover) {
5594 if (this.needsRefresh(selected, hover)) {
5595 const dimensions = this.getDimensionsFromLabel(ctx, selected, hover);
5596
5597 this.height = dimensions.height * 2;
5598 this.width = dimensions.width + dimensions.height;
5599 this.radius = 0.5 * this.width;
5600 }
5601 }
5602
5603 /**
5604 *
5605 * @param {CanvasRenderingContext2D} ctx
5606 * @param {number} x width
5607 * @param {number} y height
5608 * @param {boolean} selected
5609 * @param {boolean} hover
5610 * @param {ArrowOptions} values
5611 */
5612 draw(ctx, x, y, selected, hover, values) {
5613 this.resize(ctx, selected, hover);
5614 this.left = x - this.width * 0.5;
5615 this.top = y - this.height * 0.5;
5616
5617 this.initContextForDraw(ctx, values);
5618 drawEllipse(ctx, this.left, this.top, this.width, this.height);
5619 this.performFill(ctx, values);
5620
5621 this.updateBoundingBox(x, y, ctx, selected, hover);
5622 this.labelModule.draw(ctx, x, y, selected, hover);
5623 }
5624
5625 /**
5626 *
5627 * @param {CanvasRenderingContext2D} ctx
5628 * @param {number} angle
5629 * @returns {number}
5630 */
5631 distanceToBorder(ctx, angle) {
5632 if (ctx) {
5633 this.resize(ctx);
5634 }
5635 const a = this.width * 0.5;
5636 const b = this.height * 0.5;
5637 const w = Math.sin(angle) * a;
5638 const h = Math.cos(angle) * b;
5639 return (a * b) / Math.sqrt(w * w + h * h);
5640 }
5641}
5642
5643/**
5644 * An icon replacement for the default Node shape.
5645 *
5646 * @augments NodeBase
5647 */
5648class Icon extends NodeBase {
5649 /**
5650 * @param {object} options
5651 * @param {object} body
5652 * @param {Label} labelModule
5653 */
5654 constructor(options, body, labelModule) {
5655 super(options, body, labelModule);
5656 this._setMargins(labelModule);
5657 }
5658
5659 /**
5660 *
5661 * @param {CanvasRenderingContext2D} ctx - Unused.
5662 * @param {boolean} [selected]
5663 * @param {boolean} [hover]
5664 */
5665 resize(ctx, selected, hover) {
5666 if (this.needsRefresh(selected, hover)) {
5667 this.iconSize = {
5668 width: Number(this.options.icon.size),
5669 height: Number(this.options.icon.size),
5670 };
5671 this.width = this.iconSize.width + this.margin.right + this.margin.left;
5672 this.height = this.iconSize.height + this.margin.top + this.margin.bottom;
5673 this.radius = 0.5 * this.width;
5674 }
5675 }
5676
5677 /**
5678 *
5679 * @param {CanvasRenderingContext2D} ctx
5680 * @param {number} x width
5681 * @param {number} y height
5682 * @param {boolean} selected
5683 * @param {boolean} hover
5684 * @param {ArrowOptions} values
5685 *
5686 * @returns {object} Callbacks to draw later on higher layers.
5687 */
5688 draw(ctx, x, y, selected, hover, values) {
5689 this.resize(ctx, selected, hover);
5690 this.options.icon.size = this.options.icon.size || 50;
5691
5692 this.left = x - this.width / 2;
5693 this.top = y - this.height / 2;
5694 this._icon(ctx, x, y, selected, hover, values);
5695
5696 return {
5697 drawExternalLabel: () => {
5698 if (this.options.label !== undefined) {
5699 const iconTextSpacing = 5;
5700 this.labelModule.draw(
5701 ctx,
5702 this.left + this.iconSize.width / 2 + this.margin.left,
5703 y + this.height / 2 + iconTextSpacing,
5704 selected
5705 );
5706 }
5707
5708 this.updateBoundingBox(x, y);
5709 },
5710 };
5711 }
5712
5713 /**
5714 *
5715 * @param {number} x
5716 * @param {number} y
5717 */
5718 updateBoundingBox(x, y) {
5719 this.boundingBox.top = y - this.options.icon.size * 0.5;
5720 this.boundingBox.left = x - this.options.icon.size * 0.5;
5721 this.boundingBox.right = x + this.options.icon.size * 0.5;
5722 this.boundingBox.bottom = y + this.options.icon.size * 0.5;
5723
5724 if (this.options.label !== undefined && this.labelModule.size.width > 0) {
5725 const iconTextSpacing = 5;
5726 this.boundingBox.left = Math.min(
5727 this.boundingBox.left,
5728 this.labelModule.size.left
5729 );
5730 this.boundingBox.right = Math.max(
5731 this.boundingBox.right,
5732 this.labelModule.size.left + this.labelModule.size.width
5733 );
5734 this.boundingBox.bottom = Math.max(
5735 this.boundingBox.bottom,
5736 this.boundingBox.bottom + this.labelModule.size.height + iconTextSpacing
5737 );
5738 }
5739 }
5740
5741 /**
5742 *
5743 * @param {CanvasRenderingContext2D} ctx
5744 * @param {number} x width
5745 * @param {number} y height
5746 * @param {boolean} selected
5747 * @param {boolean} hover - Unused
5748 * @param {ArrowOptions} values
5749 */
5750 _icon(ctx, x, y, selected, hover, values) {
5751 const iconSize = Number(this.options.icon.size);
5752
5753 if (this.options.icon.code !== undefined) {
5754 ctx.font = [
5755 this.options.icon.weight != null
5756 ? this.options.icon.weight
5757 : selected
5758 ? "bold"
5759 : "",
5760 // If the weight is forced (for example to make Font Awesome 5 work
5761 // properly) substitute slightly bigger size for bold font face.
5762 (this.options.icon.weight != null && selected ? 5 : 0) +
5763 iconSize +
5764 "px",
5765 this.options.icon.face,
5766 ].join(" ");
5767
5768 // draw icon
5769 ctx.fillStyle = this.options.icon.color || "black";
5770 ctx.textAlign = "center";
5771 ctx.textBaseline = "middle";
5772
5773 // draw shadow if enabled
5774 this.enableShadow(ctx, values);
5775 ctx.fillText(this.options.icon.code, x, y);
5776
5777 // disable shadows for other elements.
5778 this.disableShadow(ctx, values);
5779 } else {
5780 console.error(
5781 "When using the icon shape, you need to define the code in the icon options object. This can be done per node or globally."
5782 );
5783 }
5784 }
5785
5786 /**
5787 *
5788 * @param {CanvasRenderingContext2D} ctx
5789 * @param {number} angle
5790 * @returns {number}
5791 */
5792 distanceToBorder(ctx, angle) {
5793 return this._distanceToBorder(ctx, angle);
5794 }
5795}
5796
5797/**
5798 * An image-based replacement for the default Node shape.
5799 *
5800 * @augments CircleImageBase
5801 */
5802class Image$2 extends CircleImageBase {
5803 /**
5804 * @param {object} options
5805 * @param {object} body
5806 * @param {Label} labelModule
5807 * @param {Image} imageObj
5808 * @param {Image} imageObjAlt
5809 */
5810 constructor(options, body, labelModule, imageObj, imageObjAlt) {
5811 super(options, body, labelModule);
5812
5813 this.setImages(imageObj, imageObjAlt);
5814 }
5815
5816 /**
5817 *
5818 * @param {CanvasRenderingContext2D} ctx - Unused.
5819 * @param {boolean} [selected]
5820 * @param {boolean} [hover]
5821 */
5822 resize(ctx, selected = this.selected, hover = this.hover) {
5823 const imageAbsent =
5824 this.imageObj.src === undefined ||
5825 this.imageObj.width === undefined ||
5826 this.imageObj.height === undefined;
5827
5828 if (imageAbsent) {
5829 const side = this.options.size * 2;
5830 this.width = side;
5831 this.height = side;
5832 return;
5833 }
5834
5835 if (this.needsRefresh(selected, hover)) {
5836 this._resizeImage();
5837 }
5838 }
5839
5840 /**
5841 *
5842 * @param {CanvasRenderingContext2D} ctx
5843 * @param {number} x width
5844 * @param {number} y height
5845 * @param {boolean} selected
5846 * @param {boolean} hover
5847 * @param {ArrowOptions} values
5848 */
5849 draw(ctx, x, y, selected, hover, values) {
5850 ctx.save();
5851 this.switchImages(selected);
5852 this.resize();
5853
5854 let labelX = x,
5855 labelY = y;
5856
5857 if (this.options.shapeProperties.coordinateOrigin === "top-left") {
5858 this.left = x;
5859 this.top = y;
5860 labelX += this.width / 2;
5861 labelY += this.height / 2;
5862 } else {
5863 this.left = x - this.width / 2;
5864 this.top = y - this.height / 2;
5865 }
5866
5867 if (this.options.shapeProperties.useBorderWithImage === true) {
5868 const neutralborderWidth = this.options.borderWidth;
5869 const selectionLineWidth =
5870 this.options.borderWidthSelected || 2 * this.options.borderWidth;
5871 const borderWidth =
5872 (selected ? selectionLineWidth : neutralborderWidth) /
5873 this.body.view.scale;
5874 ctx.lineWidth = Math.min(this.width, borderWidth);
5875
5876 ctx.beginPath();
5877 let strokeStyle = selected
5878 ? this.options.color.highlight.border
5879 : hover
5880 ? this.options.color.hover.border
5881 : this.options.color.border;
5882 let fillStyle = selected
5883 ? this.options.color.highlight.background
5884 : hover
5885 ? this.options.color.hover.background
5886 : this.options.color.background;
5887
5888 if (values.opacity !== undefined) {
5889 strokeStyle = overrideOpacity(strokeStyle, values.opacity);
5890 fillStyle = overrideOpacity(fillStyle, values.opacity);
5891 }
5892 // setup the line properties.
5893 ctx.strokeStyle = strokeStyle;
5894
5895 // set a fillstyle
5896 ctx.fillStyle = fillStyle;
5897
5898 // draw a rectangle to form the border around. This rectangle is filled so the opacity of a picture (in future vis releases?) can be used to tint the image
5899 ctx.rect(
5900 this.left - 0.5 * ctx.lineWidth,
5901 this.top - 0.5 * ctx.lineWidth,
5902 this.width + ctx.lineWidth,
5903 this.height + ctx.lineWidth
5904 );
5905 ctx.fill();
5906
5907 this.performStroke(ctx, values);
5908
5909 ctx.closePath();
5910 }
5911
5912 this._drawImageAtPosition(ctx, values);
5913
5914 this._drawImageLabel(ctx, labelX, labelY, selected, hover);
5915
5916 this.updateBoundingBox(x, y);
5917 ctx.restore();
5918 }
5919
5920 /**
5921 *
5922 * @param {number} x
5923 * @param {number} y
5924 */
5925 updateBoundingBox(x, y) {
5926 this.resize();
5927
5928 if (this.options.shapeProperties.coordinateOrigin === "top-left") {
5929 this.left = x;
5930 this.top = y;
5931 } else {
5932 this.left = x - this.width / 2;
5933 this.top = y - this.height / 2;
5934 }
5935
5936 this.boundingBox.left = this.left;
5937 this.boundingBox.top = this.top;
5938 this.boundingBox.bottom = this.top + this.height;
5939 this.boundingBox.right = this.left + this.width;
5940
5941 if (this.options.label !== undefined && this.labelModule.size.width > 0) {
5942 this.boundingBox.left = Math.min(
5943 this.boundingBox.left,
5944 this.labelModule.size.left
5945 );
5946 this.boundingBox.right = Math.max(
5947 this.boundingBox.right,
5948 this.labelModule.size.left + this.labelModule.size.width
5949 );
5950 this.boundingBox.bottom = Math.max(
5951 this.boundingBox.bottom,
5952 this.boundingBox.bottom + this.labelOffset
5953 );
5954 }
5955 }
5956
5957 /**
5958 *
5959 * @param {CanvasRenderingContext2D} ctx
5960 * @param {number} angle
5961 * @returns {number}
5962 */
5963 distanceToBorder(ctx, angle) {
5964 return this._distanceToBorder(ctx, angle);
5965 }
5966}
5967
5968/**
5969 * A Square Node/Cluster shape.
5970 *
5971 * @augments ShapeBase
5972 */
5973class Square extends ShapeBase {
5974 /**
5975 * @param {object} options
5976 * @param {object} body
5977 * @param {Label} labelModule
5978 */
5979 constructor(options, body, labelModule) {
5980 super(options, body, labelModule);
5981 }
5982
5983 /**
5984 *
5985 * @param {CanvasRenderingContext2D} ctx
5986 * @param {number} x width
5987 * @param {number} y height
5988 * @param {boolean} selected
5989 * @param {boolean} hover
5990 * @param {ArrowOptions} values
5991 *
5992 * @returns {object} Callbacks to draw later on higher layers.
5993 */
5994 draw(ctx, x, y, selected, hover, values) {
5995 return this._drawShape(ctx, "square", 2, x, y, selected, hover, values);
5996 }
5997
5998 /**
5999 *
6000 * @param {CanvasRenderingContext2D} ctx
6001 * @param {number} angle
6002 * @returns {number}
6003 */
6004 distanceToBorder(ctx, angle) {
6005 return this._distanceToBorder(ctx, angle);
6006 }
6007}
6008
6009/**
6010 * A Hexagon Node/Cluster shape.
6011 *
6012 * @augments ShapeBase
6013 */
6014class Hexagon extends ShapeBase {
6015 /**
6016 * @param {object} options
6017 * @param {object} body
6018 * @param {Label} labelModule
6019 */
6020 constructor(options, body, labelModule) {
6021 super(options, body, labelModule);
6022 }
6023
6024 /**
6025 *
6026 * @param {CanvasRenderingContext2D} ctx
6027 * @param {number} x width
6028 * @param {number} y height
6029 * @param {boolean} selected
6030 * @param {boolean} hover
6031 * @param {ArrowOptions} values
6032 *
6033 * @returns {object} Callbacks to draw later on higher layers.
6034 */
6035 draw(ctx, x, y, selected, hover, values) {
6036 return this._drawShape(ctx, "hexagon", 4, x, y, selected, hover, values);
6037 }
6038
6039 /**
6040 *
6041 * @param {CanvasRenderingContext2D} ctx
6042 * @param {number} angle
6043 * @returns {number}
6044 */
6045 distanceToBorder(ctx, angle) {
6046 return this._distanceToBorder(ctx, angle);
6047 }
6048}
6049
6050/**
6051 * A Star Node/Cluster shape.
6052 *
6053 * @augments ShapeBase
6054 */
6055class Star extends ShapeBase {
6056 /**
6057 * @param {object} options
6058 * @param {object} body
6059 * @param {Label} labelModule
6060 */
6061 constructor(options, body, labelModule) {
6062 super(options, body, labelModule);
6063 }
6064
6065 /**
6066 *
6067 * @param {CanvasRenderingContext2D} ctx
6068 * @param {number} x width
6069 * @param {number} y height
6070 * @param {boolean} selected
6071 * @param {boolean} hover
6072 * @param {ArrowOptions} values
6073 *
6074 * @returns {object} Callbacks to draw later on higher layers.
6075 */
6076 draw(ctx, x, y, selected, hover, values) {
6077 return this._drawShape(ctx, "star", 4, x, y, selected, hover, values);
6078 }
6079
6080 /**
6081 *
6082 * @param {CanvasRenderingContext2D} ctx
6083 * @param {number} angle
6084 * @returns {number}
6085 */
6086 distanceToBorder(ctx, angle) {
6087 return this._distanceToBorder(ctx, angle);
6088 }
6089}
6090
6091/**
6092 * A text-based replacement for the default Node shape.
6093 *
6094 * @augments NodeBase
6095 */
6096class Text extends NodeBase {
6097 /**
6098 * @param {object} options
6099 * @param {object} body
6100 * @param {Label} labelModule
6101 */
6102 constructor(options, body, labelModule) {
6103 super(options, body, labelModule);
6104 this._setMargins(labelModule);
6105 }
6106
6107 /**
6108 *
6109 * @param {CanvasRenderingContext2D} ctx
6110 * @param {boolean} selected
6111 * @param {boolean} hover
6112 */
6113 resize(ctx, selected, hover) {
6114 if (this.needsRefresh(selected, hover)) {
6115 this.textSize = this.labelModule.getTextSize(ctx, selected, hover);
6116 this.width = this.textSize.width + this.margin.right + this.margin.left;
6117 this.height = this.textSize.height + this.margin.top + this.margin.bottom;
6118 this.radius = 0.5 * this.width;
6119 }
6120 }
6121
6122 /**
6123 *
6124 * @param {CanvasRenderingContext2D} ctx
6125 * @param {number} x width
6126 * @param {number} y height
6127 * @param {boolean} selected
6128 * @param {boolean} hover
6129 * @param {ArrowOptions} values
6130 */
6131 draw(ctx, x, y, selected, hover, values) {
6132 this.resize(ctx, selected, hover);
6133 this.left = x - this.width / 2;
6134 this.top = y - this.height / 2;
6135
6136 // draw shadow if enabled
6137 this.enableShadow(ctx, values);
6138 this.labelModule.draw(
6139 ctx,
6140 this.left + this.textSize.width / 2 + this.margin.left,
6141 this.top + this.textSize.height / 2 + this.margin.top,
6142 selected,
6143 hover
6144 );
6145
6146 // disable shadows for other elements.
6147 this.disableShadow(ctx, values);
6148
6149 this.updateBoundingBox(x, y, ctx, selected, hover);
6150 }
6151
6152 /**
6153 *
6154 * @param {CanvasRenderingContext2D} ctx
6155 * @param {number} angle
6156 * @returns {number}
6157 */
6158 distanceToBorder(ctx, angle) {
6159 return this._distanceToBorder(ctx, angle);
6160 }
6161}
6162
6163/**
6164 * A Triangle Node/Cluster shape.
6165 *
6166 * @augments ShapeBase
6167 */
6168class Triangle$1 extends ShapeBase {
6169 /**
6170 * @param {object} options
6171 * @param {object} body
6172 * @param {Label} labelModule
6173 */
6174 constructor(options, body, labelModule) {
6175 super(options, body, labelModule);
6176 }
6177
6178 /**
6179 *
6180 * @param {CanvasRenderingContext2D} ctx
6181 * @param {number} x
6182 * @param {number} y
6183 * @param {boolean} selected
6184 * @param {boolean} hover
6185 * @param {ArrowOptions} values
6186 *
6187 * @returns {object} Callbacks to draw later on higher layers.
6188 */
6189 draw(ctx, x, y, selected, hover, values) {
6190 return this._drawShape(ctx, "triangle", 3, x, y, selected, hover, values);
6191 }
6192
6193 /**
6194 *
6195 * @param {CanvasRenderingContext2D} ctx
6196 * @param {number} angle
6197 * @returns {number}
6198 */
6199 distanceToBorder(ctx, angle) {
6200 return this._distanceToBorder(ctx, angle);
6201 }
6202}
6203
6204/**
6205 * A downward facing Triangle Node/Cluster shape.
6206 *
6207 * @augments ShapeBase
6208 */
6209class TriangleDown extends ShapeBase {
6210 /**
6211 * @param {object} options
6212 * @param {object} body
6213 * @param {Label} labelModule
6214 */
6215 constructor(options, body, labelModule) {
6216 super(options, body, labelModule);
6217 }
6218
6219 /**
6220 *
6221 * @param {CanvasRenderingContext2D} ctx
6222 * @param {number} x
6223 * @param {number} y
6224 * @param {boolean} selected
6225 * @param {boolean} hover
6226 * @param {ArrowOptions} values
6227 *
6228 * @returns {object} Callbacks to draw later on higher layers.
6229 */
6230 draw(ctx, x, y, selected, hover, values) {
6231 return this._drawShape(
6232 ctx,
6233 "triangleDown",
6234 3,
6235 x,
6236 y,
6237 selected,
6238 hover,
6239 values
6240 );
6241 }
6242
6243 /**
6244 *
6245 * @param {CanvasRenderingContext2D} ctx
6246 * @param {number} angle
6247 * @returns {number}
6248 */
6249 distanceToBorder(ctx, angle) {
6250 return this._distanceToBorder(ctx, angle);
6251 }
6252}
6253
6254/**
6255 * A node. A node can be connected to other nodes via one or multiple edges.
6256 */
6257class Node {
6258 /**
6259 *
6260 * @param {object} options An object containing options for the node. All
6261 * options are optional, except for the id.
6262 * {number} id Id of the node. Required
6263 * {string} label Text label for the node
6264 * {number} x Horizontal position of the node
6265 * {number} y Vertical position of the node
6266 * {string} shape Node shape
6267 * {string} image An image url
6268 * {string} title A title text, can be HTML
6269 * {anytype} group A group name or number
6270 *
6271 * @param {object} body Shared state of current network instance
6272 * @param {Network.Images} imagelist A list with images. Only needed when the node has an image
6273 * @param {Groups} grouplist A list with groups. Needed for retrieving group options
6274 * @param {object} globalOptions Current global node options; these serve as defaults for the node instance
6275 * @param {object} defaultOptions Global default options for nodes; note that this is also the prototype
6276 * for parameter `globalOptions`.
6277 */
6278 constructor(
6279 options,
6280 body,
6281 imagelist,
6282 grouplist,
6283 globalOptions,
6284 defaultOptions
6285 ) {
6286 this.options = bridgeObject(globalOptions);
6287 this.globalOptions = globalOptions;
6288 this.defaultOptions = defaultOptions;
6289 this.body = body;
6290
6291 this.edges = []; // all edges connected to this node
6292
6293 // set defaults for the options
6294 this.id = undefined;
6295 this.imagelist = imagelist;
6296 this.grouplist = grouplist;
6297
6298 // state options
6299 this.x = undefined;
6300 this.y = undefined;
6301 this.baseSize = this.options.size;
6302 this.baseFontSize = this.options.font.size;
6303 this.predefinedPosition = false; // used to check if initial fit should just take the range or approximate
6304 this.selected = false;
6305 this.hover = false;
6306
6307 this.labelModule = new Label(
6308 this.body,
6309 this.options,
6310 false /* Not edge label */
6311 );
6312 this.setOptions(options);
6313 }
6314
6315 /**
6316 * Attach a edge to the node
6317 *
6318 * @param {Edge} edge
6319 */
6320 attachEdge(edge) {
6321 if (this.edges.indexOf(edge) === -1) {
6322 this.edges.push(edge);
6323 }
6324 }
6325
6326 /**
6327 * Detach a edge from the node
6328 *
6329 * @param {Edge} edge
6330 */
6331 detachEdge(edge) {
6332 const index = this.edges.indexOf(edge);
6333 if (index != -1) {
6334 this.edges.splice(index, 1);
6335 }
6336 }
6337
6338 /**
6339 * Set or overwrite options for the node
6340 *
6341 * @param {object} options an object with options
6342 * @returns {null|boolean}
6343 */
6344 setOptions(options) {
6345 const currentShape = this.options.shape;
6346
6347 if (!options) {
6348 return; // Note that the return value will be 'undefined'! This is OK.
6349 }
6350
6351 // Save the color for later.
6352 // This is necessary in order to prevent local color from being overwritten by group color.
6353 // TODO: To prevent such workarounds the way options are handled should be rewritten from scratch.
6354 // This is not the only problem with current options handling.
6355 if (typeof options.color !== "undefined") {
6356 this._localColor = options.color;
6357 }
6358
6359 // basic options
6360 if (options.id !== undefined) {
6361 this.id = options.id;
6362 }
6363
6364 if (this.id === undefined) {
6365 throw new Error("Node must have an id");
6366 }
6367
6368 Node.checkMass(options, this.id);
6369
6370 // set these options locally
6371 // clear x and y positions
6372 if (options.x !== undefined) {
6373 if (options.x === null) {
6374 this.x = undefined;
6375 this.predefinedPosition = false;
6376 } else {
6377 this.x = parseInt(options.x);
6378 this.predefinedPosition = true;
6379 }
6380 }
6381 if (options.y !== undefined) {
6382 if (options.y === null) {
6383 this.y = undefined;
6384 this.predefinedPosition = false;
6385 } else {
6386 this.y = parseInt(options.y);
6387 this.predefinedPosition = true;
6388 }
6389 }
6390 if (options.size !== undefined) {
6391 this.baseSize = options.size;
6392 }
6393 if (options.value !== undefined) {
6394 options.value = parseFloat(options.value);
6395 }
6396
6397 // this transforms all shorthands into fully defined options
6398 Node.parseOptions(
6399 this.options,
6400 options,
6401 true,
6402 this.globalOptions,
6403 this.grouplist
6404 );
6405
6406 const pile = [options, this.options, this.defaultOptions];
6407 this.chooser = choosify("node", pile);
6408
6409 this._load_images();
6410 this.updateLabelModule(options);
6411
6412 // Need to set local opacity after `this.updateLabelModule(options);` because `this.updateLabelModule(options);` overrites local opacity with group opacity
6413 if (options.opacity !== undefined && Node.checkOpacity(options.opacity)) {
6414 this.options.opacity = options.opacity;
6415 }
6416
6417 this.updateShape(currentShape);
6418
6419 return options.hidden !== undefined || options.physics !== undefined;
6420 }
6421
6422 /**
6423 * Load the images from the options, for the nodes that need them.
6424 *
6425 * Images are always loaded, even if they are not used in the current shape.
6426 * The user may switch to an image shape later on.
6427 *
6428 * @private
6429 */
6430 _load_images() {
6431 if (
6432 this.options.shape === "circularImage" ||
6433 this.options.shape === "image"
6434 ) {
6435 if (this.options.image === undefined) {
6436 throw new Error(
6437 "Option image must be defined for node type '" +
6438 this.options.shape +
6439 "'"
6440 );
6441 }
6442 }
6443
6444 if (this.options.image === undefined) {
6445 return;
6446 }
6447
6448 if (this.imagelist === undefined) {
6449 throw new Error("Internal Error: No images provided");
6450 }
6451
6452 if (typeof this.options.image === "string") {
6453 this.imageObj = this.imagelist.load(
6454 this.options.image,
6455 this.options.brokenImage,
6456 this.id
6457 );
6458 } else {
6459 if (this.options.image.unselected === undefined) {
6460 throw new Error("No unselected image provided");
6461 }
6462
6463 this.imageObj = this.imagelist.load(
6464 this.options.image.unselected,
6465 this.options.brokenImage,
6466 this.id
6467 );
6468
6469 if (this.options.image.selected !== undefined) {
6470 this.imageObjAlt = this.imagelist.load(
6471 this.options.image.selected,
6472 this.options.brokenImage,
6473 this.id
6474 );
6475 } else {
6476 this.imageObjAlt = undefined;
6477 }
6478 }
6479 }
6480
6481 /**
6482 * Check that opacity is only between 0 and 1
6483 *
6484 * @param {number} opacity
6485 * @returns {boolean}
6486 */
6487 static checkOpacity(opacity) {
6488 return 0 <= opacity && opacity <= 1;
6489 }
6490
6491 /**
6492 * Check that origin is 'center' or 'top-left'
6493 *
6494 * @param {string} origin
6495 * @returns {boolean}
6496 */
6497 static checkCoordinateOrigin(origin) {
6498 return origin === undefined || origin === "center" || origin === "top-left";
6499 }
6500
6501 /**
6502 * Copy group option values into the node options.
6503 *
6504 * The group options override the global node options, so the copy of group options
6505 * must happen *after* the global node options have been set.
6506 *
6507 * This method must also be called also if the global node options have changed and the group options did not.
6508 *
6509 * @param {object} parentOptions
6510 * @param {object} newOptions new values for the options, currently only passed in for check
6511 * @param {object} groupList
6512 */
6513 static updateGroupOptions(parentOptions, newOptions, groupList) {
6514 if (groupList === undefined) return; // No groups, nothing to do
6515
6516 const group = parentOptions.group;
6517
6518 // paranoia: the selected group is already merged into node options, check.
6519 if (
6520 newOptions !== undefined &&
6521 newOptions.group !== undefined &&
6522 group !== newOptions.group
6523 ) {
6524 throw new Error(
6525 "updateGroupOptions: group values in options don't match."
6526 );
6527 }
6528
6529 const hasGroup =
6530 typeof group === "number" || (typeof group === "string" && group != "");
6531 if (!hasGroup) return; // current node has no group, no need to merge
6532
6533 const groupObj = groupList.get(group);
6534
6535 if (groupObj.opacity !== undefined && newOptions.opacity === undefined) {
6536 if (!Node.checkOpacity(groupObj.opacity)) {
6537 console.error(
6538 "Invalid option for node opacity. Value must be between 0 and 1, found: " +
6539 groupObj.opacity
6540 );
6541 groupObj.opacity = undefined;
6542 }
6543 }
6544
6545 // Skip any new option to avoid them being overridden by the group options.
6546 const skipProperties = Object.getOwnPropertyNames(newOptions).filter(
6547 (p) => newOptions[p] != null
6548 );
6549 // Always skip merging group font options into parent; these are required to be distinct for labels
6550 skipProperties.push("font");
6551 selectiveNotDeepExtend(skipProperties, parentOptions, groupObj);
6552
6553 // the color object needs to be completely defined.
6554 // Since groups can partially overwrite the colors, we parse it again, just in case.
6555 parentOptions.color = parseColor(parentOptions.color);
6556 }
6557
6558 /**
6559 * This process all possible shorthands in the new options and makes sure that the parentOptions are fully defined.
6560 * Static so it can also be used by the handler.
6561 *
6562 * @param {object} parentOptions
6563 * @param {object} newOptions
6564 * @param {boolean} [allowDeletion=false]
6565 * @param {object} [globalOptions={}]
6566 * @param {object} [groupList]
6567 * @static
6568 */
6569 static parseOptions(
6570 parentOptions,
6571 newOptions,
6572 allowDeletion = false,
6573 globalOptions = {},
6574 groupList
6575 ) {
6576 const fields = ["color", "fixed", "shadow"];
6577 selectiveNotDeepExtend(fields, parentOptions, newOptions, allowDeletion);
6578
6579 Node.checkMass(newOptions);
6580
6581 if (parentOptions.opacity !== undefined) {
6582 if (!Node.checkOpacity(parentOptions.opacity)) {
6583 console.error(
6584 "Invalid option for node opacity. Value must be between 0 and 1, found: " +
6585 parentOptions.opacity
6586 );
6587 parentOptions.opacity = undefined;
6588 }
6589 }
6590
6591 if (newOptions.opacity !== undefined) {
6592 if (!Node.checkOpacity(newOptions.opacity)) {
6593 console.error(
6594 "Invalid option for node opacity. Value must be between 0 and 1, found: " +
6595 newOptions.opacity
6596 );
6597 newOptions.opacity = undefined;
6598 }
6599 }
6600
6601 if (
6602 newOptions.shapeProperties &&
6603 !Node.checkCoordinateOrigin(newOptions.shapeProperties.coordinateOrigin)
6604 ) {
6605 console.error(
6606 "Invalid option for node coordinateOrigin, found: " +
6607 newOptions.shapeProperties.coordinateOrigin
6608 );
6609 }
6610
6611 // merge the shadow options into the parent.
6612 mergeOptions(parentOptions, newOptions, "shadow", globalOptions);
6613
6614 // individual shape newOptions
6615 if (newOptions.color !== undefined && newOptions.color !== null) {
6616 const parsedColor = parseColor(newOptions.color);
6617 fillIfDefined(parentOptions.color, parsedColor);
6618 } else if (allowDeletion === true && newOptions.color === null) {
6619 parentOptions.color = bridgeObject(globalOptions.color); // set the object back to the global options
6620 }
6621
6622 // handle the fixed options
6623 if (newOptions.fixed !== undefined && newOptions.fixed !== null) {
6624 if (typeof newOptions.fixed === "boolean") {
6625 parentOptions.fixed.x = newOptions.fixed;
6626 parentOptions.fixed.y = newOptions.fixed;
6627 } else {
6628 if (
6629 newOptions.fixed.x !== undefined &&
6630 typeof newOptions.fixed.x === "boolean"
6631 ) {
6632 parentOptions.fixed.x = newOptions.fixed.x;
6633 }
6634 if (
6635 newOptions.fixed.y !== undefined &&
6636 typeof newOptions.fixed.y === "boolean"
6637 ) {
6638 parentOptions.fixed.y = newOptions.fixed.y;
6639 }
6640 }
6641 }
6642
6643 if (allowDeletion === true && newOptions.font === null) {
6644 parentOptions.font = bridgeObject(globalOptions.font); // set the object back to the global options
6645 }
6646
6647 Node.updateGroupOptions(parentOptions, newOptions, groupList);
6648
6649 // handle the scaling options, specifically the label part
6650 if (newOptions.scaling !== undefined) {
6651 mergeOptions(
6652 parentOptions.scaling,
6653 newOptions.scaling,
6654 "label",
6655 globalOptions.scaling
6656 );
6657 }
6658 }
6659
6660 /**
6661 *
6662 * @returns {{color: *, borderWidth: *, borderColor: *, size: *, borderDashes: (boolean|Array|allOptions.nodes.shapeProperties.borderDashes|{boolean, array}), borderRadius: (number|allOptions.nodes.shapeProperties.borderRadius|{number}|Array), shadow: *, shadowColor: *, shadowSize: *, shadowX: *, shadowY: *}}
6663 */
6664 getFormattingValues() {
6665 const values = {
6666 color: this.options.color.background,
6667 opacity: this.options.opacity,
6668 borderWidth: this.options.borderWidth,
6669 borderColor: this.options.color.border,
6670 size: this.options.size,
6671 borderDashes: this.options.shapeProperties.borderDashes,
6672 borderRadius: this.options.shapeProperties.borderRadius,
6673 shadow: this.options.shadow.enabled,
6674 shadowColor: this.options.shadow.color,
6675 shadowSize: this.options.shadow.size,
6676 shadowX: this.options.shadow.x,
6677 shadowY: this.options.shadow.y,
6678 };
6679 if (this.selected || this.hover) {
6680 if (this.chooser === true) {
6681 if (this.selected) {
6682 if (this.options.borderWidthSelected != null) {
6683 values.borderWidth = this.options.borderWidthSelected;
6684 } else {
6685 values.borderWidth *= 2;
6686 }
6687 values.color = this.options.color.highlight.background;
6688 values.borderColor = this.options.color.highlight.border;
6689 values.shadow = this.options.shadow.enabled;
6690 } else if (this.hover) {
6691 values.color = this.options.color.hover.background;
6692 values.borderColor = this.options.color.hover.border;
6693 values.shadow = this.options.shadow.enabled;
6694 }
6695 } else if (typeof this.chooser === "function") {
6696 this.chooser(values, this.options.id, this.selected, this.hover);
6697 if (values.shadow === false) {
6698 if (
6699 values.shadowColor !== this.options.shadow.color ||
6700 values.shadowSize !== this.options.shadow.size ||
6701 values.shadowX !== this.options.shadow.x ||
6702 values.shadowY !== this.options.shadow.y
6703 ) {
6704 values.shadow = true;
6705 }
6706 }
6707 }
6708 } else {
6709 values.shadow = this.options.shadow.enabled;
6710 }
6711 if (this.options.opacity !== undefined) {
6712 const opacity = this.options.opacity;
6713 values.borderColor = overrideOpacity(values.borderColor, opacity);
6714 values.color = overrideOpacity(values.color, opacity);
6715 values.shadowColor = overrideOpacity(values.shadowColor, opacity);
6716 }
6717 return values;
6718 }
6719
6720 /**
6721 *
6722 * @param {object} options
6723 */
6724 updateLabelModule(options) {
6725 if (this.options.label === undefined || this.options.label === null) {
6726 this.options.label = "";
6727 }
6728
6729 Node.updateGroupOptions(
6730 this.options,
6731 {
6732 ...options,
6733 color: (options && options.color) || this._localColor || undefined,
6734 },
6735 this.grouplist
6736 );
6737
6738 //
6739 // Note:The prototype chain for this.options is:
6740 //
6741 // this.options -> NodesHandler.options -> NodesHandler.defaultOptions
6742 // (also: this.globalOptions)
6743 //
6744 // Note that the prototypes are mentioned explicitly in the pile list below;
6745 // WE DON'T WANT THE ORDER OF THE PROTOTYPES!!!! At least, not for font handling of labels.
6746 // This is a good indication that the prototype usage of options is deficient.
6747 //
6748 const currentGroup = this.grouplist.get(this.options.group, false);
6749 const pile = [
6750 options, // new options
6751 this.options, // current node options, see comment above for prototype
6752 currentGroup, // group options, if any
6753 this.globalOptions, // Currently set global node options
6754 this.defaultOptions, // Default global node options
6755 ];
6756 this.labelModule.update(this.options, pile);
6757
6758 if (this.labelModule.baseSize !== undefined) {
6759 this.baseFontSize = this.labelModule.baseSize;
6760 }
6761 }
6762
6763 /**
6764 *
6765 * @param {string} currentShape
6766 */
6767 updateShape(currentShape) {
6768 if (currentShape === this.options.shape && this.shape) {
6769 this.shape.setOptions(this.options, this.imageObj, this.imageObjAlt);
6770 } else {
6771 // choose draw method depending on the shape
6772 switch (this.options.shape) {
6773 case "box":
6774 this.shape = new Box$1(this.options, this.body, this.labelModule);
6775 break;
6776 case "circle":
6777 this.shape = new Circle$1(this.options, this.body, this.labelModule);
6778 break;
6779 case "circularImage":
6780 this.shape = new CircularImage(
6781 this.options,
6782 this.body,
6783 this.labelModule,
6784 this.imageObj,
6785 this.imageObjAlt
6786 );
6787 break;
6788 case "custom":
6789 this.shape = new CustomShape(
6790 this.options,
6791 this.body,
6792 this.labelModule,
6793 this.options.ctxRenderer
6794 );
6795 break;
6796 case "database":
6797 this.shape = new Database(this.options, this.body, this.labelModule);
6798 break;
6799 case "diamond":
6800 this.shape = new Diamond$1(this.options, this.body, this.labelModule);
6801 break;
6802 case "dot":
6803 this.shape = new Dot(this.options, this.body, this.labelModule);
6804 break;
6805 case "ellipse":
6806 this.shape = new Ellipse(this.options, this.body, this.labelModule);
6807 break;
6808 case "icon":
6809 this.shape = new Icon(this.options, this.body, this.labelModule);
6810 break;
6811 case "image":
6812 this.shape = new Image$2(
6813 this.options,
6814 this.body,
6815 this.labelModule,
6816 this.imageObj,
6817 this.imageObjAlt
6818 );
6819 break;
6820 case "square":
6821 this.shape = new Square(this.options, this.body, this.labelModule);
6822 break;
6823 case "hexagon":
6824 this.shape = new Hexagon(this.options, this.body, this.labelModule);
6825 break;
6826 case "star":
6827 this.shape = new Star(this.options, this.body, this.labelModule);
6828 break;
6829 case "text":
6830 this.shape = new Text(this.options, this.body, this.labelModule);
6831 break;
6832 case "triangle":
6833 this.shape = new Triangle$1(this.options, this.body, this.labelModule);
6834 break;
6835 case "triangleDown":
6836 this.shape = new TriangleDown(
6837 this.options,
6838 this.body,
6839 this.labelModule
6840 );
6841 break;
6842 default:
6843 this.shape = new Ellipse(this.options, this.body, this.labelModule);
6844 break;
6845 }
6846 }
6847 this.needsRefresh();
6848 }
6849
6850 /**
6851 * select this node
6852 */
6853 select() {
6854 this.selected = true;
6855 this.needsRefresh();
6856 }
6857
6858 /**
6859 * unselect this node
6860 */
6861 unselect() {
6862 this.selected = false;
6863 this.needsRefresh();
6864 }
6865
6866 /**
6867 * Reset the calculated size of the node, forces it to recalculate its size
6868 */
6869 needsRefresh() {
6870 this.shape.refreshNeeded = true;
6871 }
6872
6873 /**
6874 * get the title of this node.
6875 *
6876 * @returns {string} title The title of the node, or undefined when no title
6877 * has been set.
6878 */
6879 getTitle() {
6880 return this.options.title;
6881 }
6882
6883 /**
6884 * Calculate the distance to the border of the Node
6885 *
6886 * @param {CanvasRenderingContext2D} ctx
6887 * @param {number} angle Angle in radians
6888 * @returns {number} distance Distance to the border in pixels
6889 */
6890 distanceToBorder(ctx, angle) {
6891 return this.shape.distanceToBorder(ctx, angle);
6892 }
6893
6894 /**
6895 * Check if this node has a fixed x and y position
6896 *
6897 * @returns {boolean} true if fixed, false if not
6898 */
6899 isFixed() {
6900 return this.options.fixed.x && this.options.fixed.y;
6901 }
6902
6903 /**
6904 * check if this node is selecte
6905 *
6906 * @returns {boolean} selected True if node is selected, else false
6907 */
6908 isSelected() {
6909 return this.selected;
6910 }
6911
6912 /**
6913 * Retrieve the value of the node. Can be undefined
6914 *
6915 * @returns {number} value
6916 */
6917 getValue() {
6918 return this.options.value;
6919 }
6920
6921 /**
6922 * Get the current dimensions of the label
6923 *
6924 * @returns {rect}
6925 */
6926 getLabelSize() {
6927 return this.labelModule.size();
6928 }
6929
6930 /**
6931 * Adjust the value range of the node. The node will adjust it's size
6932 * based on its value.
6933 *
6934 * @param {number} min
6935 * @param {number} max
6936 * @param {number} total
6937 */
6938 setValueRange(min, max, total) {
6939 if (this.options.value !== undefined) {
6940 const scale = this.options.scaling.customScalingFunction(
6941 min,
6942 max,
6943 total,
6944 this.options.value
6945 );
6946 const sizeDiff = this.options.scaling.max - this.options.scaling.min;
6947 if (this.options.scaling.label.enabled === true) {
6948 const fontDiff =
6949 this.options.scaling.label.max - this.options.scaling.label.min;
6950 this.options.font.size =
6951 this.options.scaling.label.min + scale * fontDiff;
6952 }
6953 this.options.size = this.options.scaling.min + scale * sizeDiff;
6954 } else {
6955 this.options.size = this.baseSize;
6956 this.options.font.size = this.baseFontSize;
6957 }
6958
6959 this.updateLabelModule();
6960 }
6961
6962 /**
6963 * Draw this node in the given canvas
6964 * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
6965 *
6966 * @param {CanvasRenderingContext2D} ctx
6967 *
6968 * @returns {object} Callbacks to draw later on higher layers.
6969 */
6970 draw(ctx) {
6971 const values = this.getFormattingValues();
6972 return (
6973 this.shape.draw(ctx, this.x, this.y, this.selected, this.hover, values) ||
6974 {}
6975 );
6976 }
6977
6978 /**
6979 * Update the bounding box of the shape
6980 *
6981 * @param {CanvasRenderingContext2D} ctx
6982 */
6983 updateBoundingBox(ctx) {
6984 this.shape.updateBoundingBox(this.x, this.y, ctx);
6985 }
6986
6987 /**
6988 * Recalculate the size of this node in the given canvas
6989 * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
6990 *
6991 * @param {CanvasRenderingContext2D} ctx
6992 */
6993 resize(ctx) {
6994 const values = this.getFormattingValues();
6995 this.shape.resize(ctx, this.selected, this.hover, values);
6996 }
6997
6998 /**
6999 * Determine all visual elements of this node instance, in which the given
7000 * point falls within the bounding shape.
7001 *
7002 * @param {point} point
7003 * @returns {Array.<nodeClickItem|nodeLabelClickItem>} list with the items which are on the point
7004 */
7005 getItemsOnPoint(point) {
7006 const ret = [];
7007
7008 if (this.labelModule.visible()) {
7009 if (pointInRect(this.labelModule.getSize(), point)) {
7010 ret.push({ nodeId: this.id, labelId: 0 });
7011 }
7012 }
7013
7014 if (pointInRect(this.shape.boundingBox, point)) {
7015 ret.push({ nodeId: this.id });
7016 }
7017
7018 return ret;
7019 }
7020
7021 /**
7022 * Check if this object is overlapping with the provided object
7023 *
7024 * @param {object} obj an object with parameters left, top, right, bottom
7025 * @returns {boolean} True if location is located on node
7026 */
7027 isOverlappingWith(obj) {
7028 return (
7029 this.shape.left < obj.right &&
7030 this.shape.left + this.shape.width > obj.left &&
7031 this.shape.top < obj.bottom &&
7032 this.shape.top + this.shape.height > obj.top
7033 );
7034 }
7035
7036 /**
7037 * Check if this object is overlapping with the provided object
7038 *
7039 * @param {object} obj an object with parameters left, top, right, bottom
7040 * @returns {boolean} True if location is located on node
7041 */
7042 isBoundingBoxOverlappingWith(obj) {
7043 return (
7044 this.shape.boundingBox.left < obj.right &&
7045 this.shape.boundingBox.right > obj.left &&
7046 this.shape.boundingBox.top < obj.bottom &&
7047 this.shape.boundingBox.bottom > obj.top
7048 );
7049 }
7050
7051 /**
7052 * Check valid values for mass
7053 *
7054 * The mass may not be negative or zero. If it is, reset to 1
7055 *
7056 * @param {object} options
7057 * @param {Node.id} id
7058 * @static
7059 */
7060 static checkMass(options, id) {
7061 if (options.mass !== undefined && options.mass <= 0) {
7062 let strId = "";
7063 if (id !== undefined) {
7064 strId = " in node id: " + id;
7065 }
7066 console.error(
7067 "%cNegative or zero mass disallowed" + strId + ", setting mass to 1.",
7068 VALIDATOR_PRINT_STYLE
7069 );
7070 options.mass = 1;
7071 }
7072 }
7073}
7074
7075/**
7076 * Handler for Nodes
7077 */
7078class NodesHandler {
7079 /**
7080 * @param {object} body
7081 * @param {Images} images
7082 * @param {Array.<Group>} groups
7083 * @param {LayoutEngine} layoutEngine
7084 */
7085 constructor(body, images, groups, layoutEngine) {
7086 this.body = body;
7087 this.images = images;
7088 this.groups = groups;
7089 this.layoutEngine = layoutEngine;
7090
7091 // create the node API in the body container
7092 this.body.functions.createNode = this.create.bind(this);
7093
7094 this.nodesListeners = {
7095 add: (event, params) => {
7096 this.add(params.items);
7097 },
7098 update: (event, params) => {
7099 this.update(params.items, params.data, params.oldData);
7100 },
7101 remove: (event, params) => {
7102 this.remove(params.items);
7103 },
7104 };
7105
7106 this.defaultOptions = {
7107 borderWidth: 1,
7108 borderWidthSelected: undefined,
7109 brokenImage: undefined,
7110 color: {
7111 border: "#2B7CE9",
7112 background: "#97C2FC",
7113 highlight: {
7114 border: "#2B7CE9",
7115 background: "#D2E5FF",
7116 },
7117 hover: {
7118 border: "#2B7CE9",
7119 background: "#D2E5FF",
7120 },
7121 },
7122 opacity: undefined, // number between 0 and 1
7123 fixed: {
7124 x: false,
7125 y: false,
7126 },
7127 font: {
7128 color: "#343434",
7129 size: 14, // px
7130 face: "arial",
7131 background: "none",
7132 strokeWidth: 0, // px
7133 strokeColor: "#ffffff",
7134 align: "center",
7135 vadjust: 0,
7136 multi: false,
7137 bold: {
7138 mod: "bold",
7139 },
7140 boldital: {
7141 mod: "bold italic",
7142 },
7143 ital: {
7144 mod: "italic",
7145 },
7146 mono: {
7147 mod: "",
7148 size: 15, // px
7149 face: "monospace",
7150 vadjust: 2,
7151 },
7152 },
7153 group: undefined,
7154 hidden: false,
7155 icon: {
7156 face: "FontAwesome", //'FontAwesome',
7157 code: undefined, //'\uf007',
7158 size: 50, //50,
7159 color: "#2B7CE9", //'#aa00ff'
7160 },
7161 image: undefined, // --> URL
7162 imagePadding: {
7163 // only for image shape
7164 top: 0,
7165 right: 0,
7166 bottom: 0,
7167 left: 0,
7168 },
7169 label: undefined,
7170 labelHighlightBold: true,
7171 level: undefined,
7172 margin: {
7173 top: 5,
7174 right: 5,
7175 bottom: 5,
7176 left: 5,
7177 },
7178 mass: 1,
7179 physics: true,
7180 scaling: {
7181 min: 10,
7182 max: 30,
7183 label: {
7184 enabled: false,
7185 min: 14,
7186 max: 30,
7187 maxVisible: 30,
7188 drawThreshold: 5,
7189 },
7190 customScalingFunction: function (min, max, total, value) {
7191 if (max === min) {
7192 return 0.5;
7193 } else {
7194 const scale = 1 / (max - min);
7195 return Math.max(0, (value - min) * scale);
7196 }
7197 },
7198 },
7199 shadow: {
7200 enabled: false,
7201 color: "rgba(0,0,0,0.5)",
7202 size: 10,
7203 x: 5,
7204 y: 5,
7205 },
7206 shape: "ellipse",
7207 shapeProperties: {
7208 borderDashes: false, // only for borders
7209 borderRadius: 6, // only for box shape
7210 interpolation: true, // only for image and circularImage shapes
7211 useImageSize: false, // only for image and circularImage shapes
7212 useBorderWithImage: false, // only for image shape
7213 coordinateOrigin: "center", // only for image and circularImage shapes
7214 },
7215 size: 25,
7216 title: undefined,
7217 value: undefined,
7218 x: undefined,
7219 y: undefined,
7220 };
7221
7222 // Protect from idiocy
7223 if (this.defaultOptions.mass <= 0) {
7224 throw "Internal error: mass in defaultOptions of NodesHandler may not be zero or negative";
7225 }
7226
7227 this.options = bridgeObject(this.defaultOptions);
7228
7229 this.bindEventListeners();
7230 }
7231
7232 /**
7233 * Binds event listeners
7234 */
7235 bindEventListeners() {
7236 // refresh the nodes. Used when reverting from hierarchical layout
7237 this.body.emitter.on("refreshNodes", this.refresh.bind(this));
7238 this.body.emitter.on("refresh", this.refresh.bind(this));
7239 this.body.emitter.on("destroy", () => {
7240 forEach(this.nodesListeners, (callback, event) => {
7241 if (this.body.data.nodes) this.body.data.nodes.off(event, callback);
7242 });
7243 delete this.body.functions.createNode;
7244 delete this.nodesListeners.add;
7245 delete this.nodesListeners.update;
7246 delete this.nodesListeners.remove;
7247 delete this.nodesListeners;
7248 });
7249 }
7250
7251 /**
7252 *
7253 * @param {object} options
7254 */
7255 setOptions(options) {
7256 if (options !== undefined) {
7257 Node.parseOptions(this.options, options);
7258
7259 // Need to set opacity here because Node.parseOptions is also used for groups,
7260 // if you set opacity in Node.parseOptions it overwrites group opacity.
7261 if (options.opacity !== undefined) {
7262 if (
7263 Number.isNaN(options.opacity) ||
7264 !Number.isFinite(options.opacity) ||
7265 options.opacity < 0 ||
7266 options.opacity > 1
7267 ) {
7268 console.error(
7269 "Invalid option for node opacity. Value must be between 0 and 1, found: " +
7270 options.opacity
7271 );
7272 } else {
7273 this.options.opacity = options.opacity;
7274 }
7275 }
7276
7277 // update the shape in all nodes
7278 if (options.shape !== undefined) {
7279 for (const nodeId in this.body.nodes) {
7280 if (Object.prototype.hasOwnProperty.call(this.body.nodes, nodeId)) {
7281 this.body.nodes[nodeId].updateShape();
7282 }
7283 }
7284 }
7285
7286 // Update the labels of nodes if any relevant options changed.
7287 if (
7288 typeof options.font !== "undefined" ||
7289 typeof options.widthConstraint !== "undefined" ||
7290 typeof options.heightConstraint !== "undefined"
7291 ) {
7292 for (const nodeId of Object.keys(this.body.nodes)) {
7293 this.body.nodes[nodeId].updateLabelModule();
7294 this.body.nodes[nodeId].needsRefresh();
7295 }
7296 }
7297
7298 // update the shape size in all nodes
7299 if (options.size !== undefined) {
7300 for (const nodeId in this.body.nodes) {
7301 if (Object.prototype.hasOwnProperty.call(this.body.nodes, nodeId)) {
7302 this.body.nodes[nodeId].needsRefresh();
7303 }
7304 }
7305 }
7306
7307 // update the state of the variables if needed
7308 if (options.hidden !== undefined || options.physics !== undefined) {
7309 this.body.emitter.emit("_dataChanged");
7310 }
7311 }
7312 }
7313
7314 /**
7315 * Set a data set with nodes for the network
7316 *
7317 * @param {Array | DataSet | DataView} nodes The data containing the nodes.
7318 * @param {boolean} [doNotEmit=false] - Suppress data changed event.
7319 * @private
7320 */
7321 setData(nodes, doNotEmit = false) {
7322 const oldNodesData = this.body.data.nodes;
7323
7324 if (isDataViewLike("id", nodes)) {
7325 this.body.data.nodes = nodes;
7326 } else if (Array.isArray(nodes)) {
7327 this.body.data.nodes = new DataSet();
7328 this.body.data.nodes.add(nodes);
7329 } else if (!nodes) {
7330 this.body.data.nodes = new DataSet();
7331 } else {
7332 throw new TypeError("Array or DataSet expected");
7333 }
7334
7335 if (oldNodesData) {
7336 // unsubscribe from old dataset
7337 forEach(this.nodesListeners, function (callback, event) {
7338 oldNodesData.off(event, callback);
7339 });
7340 }
7341
7342 // remove drawn nodes
7343 this.body.nodes = {};
7344
7345 if (this.body.data.nodes) {
7346 // subscribe to new dataset
7347 const me = this;
7348 forEach(this.nodesListeners, function (callback, event) {
7349 me.body.data.nodes.on(event, callback);
7350 });
7351
7352 // draw all new nodes
7353 const ids = this.body.data.nodes.getIds();
7354 this.add(ids, true);
7355 }
7356
7357 if (doNotEmit === false) {
7358 this.body.emitter.emit("_dataChanged");
7359 }
7360 }
7361
7362 /**
7363 * Add nodes
7364 *
7365 * @param {number[] | string[]} ids
7366 * @param {boolean} [doNotEmit=false]
7367 * @private
7368 */
7369 add(ids, doNotEmit = false) {
7370 let id;
7371 const newNodes = [];
7372 for (let i = 0; i < ids.length; i++) {
7373 id = ids[i];
7374 const properties = this.body.data.nodes.get(id);
7375 const node = this.create(properties);
7376 newNodes.push(node);
7377 this.body.nodes[id] = node; // note: this may replace an existing node
7378 }
7379
7380 this.layoutEngine.positionInitially(newNodes);
7381
7382 if (doNotEmit === false) {
7383 this.body.emitter.emit("_dataChanged");
7384 }
7385 }
7386
7387 /**
7388 * Update existing nodes, or create them when not yet existing
7389 *
7390 * @param {number[] | string[]} ids id's of changed nodes
7391 * @param {Array} changedData array with changed data
7392 * @param {Array|undefined} oldData optional; array with previous data
7393 * @private
7394 */
7395 update(ids, changedData, oldData) {
7396 const nodes = this.body.nodes;
7397 let dataChanged = false;
7398 for (let i = 0; i < ids.length; i++) {
7399 const id = ids[i];
7400 let node = nodes[id];
7401 const data = changedData[i];
7402 if (node !== undefined) {
7403 // update node
7404 if (node.setOptions(data)) {
7405 dataChanged = true;
7406 }
7407 } else {
7408 dataChanged = true;
7409 // create node
7410 node = this.create(data);
7411 nodes[id] = node;
7412 }
7413 }
7414
7415 if (!dataChanged && oldData !== undefined) {
7416 // Check for any changes which should trigger a layout recalculation
7417 // For now, this is just 'level' for hierarchical layout
7418 // Assumption: old and new data arranged in same order; at time of writing, this holds.
7419 dataChanged = changedData.some(function (newValue, index) {
7420 const oldValue = oldData[index];
7421 return oldValue && oldValue.level !== newValue.level;
7422 });
7423 }
7424
7425 if (dataChanged === true) {
7426 this.body.emitter.emit("_dataChanged");
7427 } else {
7428 this.body.emitter.emit("_dataUpdated");
7429 }
7430 }
7431
7432 /**
7433 * Remove existing nodes. If nodes do not exist, the method will just ignore it.
7434 *
7435 * @param {number[] | string[]} ids
7436 * @private
7437 */
7438 remove(ids) {
7439 const nodes = this.body.nodes;
7440
7441 for (let i = 0; i < ids.length; i++) {
7442 const id = ids[i];
7443 delete nodes[id];
7444 }
7445
7446 this.body.emitter.emit("_dataChanged");
7447 }
7448
7449 /**
7450 * create a node
7451 *
7452 * @param {object} properties
7453 * @param {class} [constructorClass=Node.default]
7454 * @returns {*}
7455 */
7456 create(properties, constructorClass = Node) {
7457 return new constructorClass(
7458 properties,
7459 this.body,
7460 this.images,
7461 this.groups,
7462 this.options,
7463 this.defaultOptions
7464 );
7465 }
7466
7467 /**
7468 *
7469 * @param {boolean} [clearPositions=false]
7470 */
7471 refresh(clearPositions = false) {
7472 forEach(this.body.nodes, (node, nodeId) => {
7473 const data = this.body.data.nodes.get(nodeId);
7474 if (data !== undefined) {
7475 if (clearPositions === true) {
7476 node.setOptions({ x: null, y: null });
7477 }
7478 node.setOptions({ fixed: false });
7479 node.setOptions(data);
7480 }
7481 });
7482 }
7483
7484 /**
7485 * Returns the positions of the nodes.
7486 *
7487 * @param {Array.<Node.id> | string} [ids] --> optional, can be array of nodeIds, can be string
7488 * @returns {{}}
7489 */
7490 getPositions(ids) {
7491 const dataArray = {};
7492 if (ids !== undefined) {
7493 if (Array.isArray(ids) === true) {
7494 for (let i = 0; i < ids.length; i++) {
7495 if (this.body.nodes[ids[i]] !== undefined) {
7496 const node = this.body.nodes[ids[i]];
7497 dataArray[ids[i]] = {
7498 x: Math.round(node.x),
7499 y: Math.round(node.y),
7500 };
7501 }
7502 }
7503 } else {
7504 if (this.body.nodes[ids] !== undefined) {
7505 const node = this.body.nodes[ids];
7506 dataArray[ids] = { x: Math.round(node.x), y: Math.round(node.y) };
7507 }
7508 }
7509 } else {
7510 for (let i = 0; i < this.body.nodeIndices.length; i++) {
7511 const node = this.body.nodes[this.body.nodeIndices[i]];
7512 dataArray[this.body.nodeIndices[i]] = {
7513 x: Math.round(node.x),
7514 y: Math.round(node.y),
7515 };
7516 }
7517 }
7518 return dataArray;
7519 }
7520
7521 /**
7522 * Retrieves the x y position of a specific id.
7523 *
7524 * @param {string} id The id to retrieve.
7525 *
7526 * @throws {TypeError} If no id is included.
7527 * @throws {ReferenceError} If an invalid id is provided.
7528 *
7529 * @returns {{ x: number, y: number }} Returns X, Y canvas position of the node with given id.
7530 */
7531 getPosition(id) {
7532 if (id == undefined) {
7533 throw new TypeError("No id was specified for getPosition method.");
7534 } else if (this.body.nodes[id] == undefined) {
7535 throw new ReferenceError(
7536 `NodeId provided for getPosition does not exist. Provided: ${id}`
7537 );
7538 } else {
7539 return {
7540 x: Math.round(this.body.nodes[id].x),
7541 y: Math.round(this.body.nodes[id].y),
7542 };
7543 }
7544 }
7545
7546 /**
7547 * Load the XY positions of the nodes into the dataset.
7548 */
7549 storePositions() {
7550 // todo: add support for clusters and hierarchical.
7551 const dataArray = [];
7552 const dataset = this.body.data.nodes.getDataSet();
7553
7554 for (const dsNode of dataset.get()) {
7555 const id = dsNode.id;
7556 const bodyNode = this.body.nodes[id];
7557 const x = Math.round(bodyNode.x);
7558 const y = Math.round(bodyNode.y);
7559
7560 if (dsNode.x !== x || dsNode.y !== y) {
7561 dataArray.push({ id, x, y });
7562 }
7563 }
7564
7565 dataset.update(dataArray);
7566 }
7567
7568 /**
7569 * get the bounding box of a node.
7570 *
7571 * @param {Node.id} nodeId
7572 * @returns {j|*}
7573 */
7574 getBoundingBox(nodeId) {
7575 if (this.body.nodes[nodeId] !== undefined) {
7576 return this.body.nodes[nodeId].shape.boundingBox;
7577 }
7578 }
7579
7580 /**
7581 * Get the Ids of nodes connected to this node.
7582 *
7583 * @param {Node.id} nodeId
7584 * @param {'to'|'from'|undefined} direction values 'from' and 'to' select respectively parent and child nodes only.
7585 * Any other value returns both parent and child nodes.
7586 * @returns {Array}
7587 */
7588 getConnectedNodes(nodeId, direction) {
7589 const nodeList = [];
7590 if (this.body.nodes[nodeId] !== undefined) {
7591 const node = this.body.nodes[nodeId];
7592 const nodeObj = {}; // used to quickly check if node already exists
7593 for (let i = 0; i < node.edges.length; i++) {
7594 const edge = node.edges[i];
7595 if (direction !== "to" && edge.toId == node.id) {
7596 // these are double equals since ids can be numeric or string
7597 if (nodeObj[edge.fromId] === undefined) {
7598 nodeList.push(edge.fromId);
7599 nodeObj[edge.fromId] = true;
7600 }
7601 } else if (direction !== "from" && edge.fromId == node.id) {
7602 // these are double equals since ids can be numeric or string
7603 if (nodeObj[edge.toId] === undefined) {
7604 nodeList.push(edge.toId);
7605 nodeObj[edge.toId] = true;
7606 }
7607 }
7608 }
7609 }
7610 return nodeList;
7611 }
7612
7613 /**
7614 * Get the ids of the edges connected to this node.
7615 *
7616 * @param {Node.id} nodeId
7617 * @returns {*}
7618 */
7619 getConnectedEdges(nodeId) {
7620 const edgeList = [];
7621 if (this.body.nodes[nodeId] !== undefined) {
7622 const node = this.body.nodes[nodeId];
7623 for (let i = 0; i < node.edges.length; i++) {
7624 edgeList.push(node.edges[i].id);
7625 }
7626 } else {
7627 console.error(
7628 "NodeId provided for getConnectedEdges does not exist. Provided: ",
7629 nodeId
7630 );
7631 }
7632 return edgeList;
7633 }
7634
7635 /**
7636 * Move a node.
7637 *
7638 * @param {Node.id} nodeId
7639 * @param {number} x
7640 * @param {number} y
7641 */
7642 moveNode(nodeId, x, y) {
7643 if (this.body.nodes[nodeId] !== undefined) {
7644 this.body.nodes[nodeId].x = Number(x);
7645 this.body.nodes[nodeId].y = Number(y);
7646 setTimeout(() => {
7647 this.body.emitter.emit("startSimulation");
7648 }, 0);
7649 } else {
7650 console.error(
7651 "Node id supplied to moveNode does not exist. Provided: ",
7652 nodeId
7653 );
7654 }
7655 }
7656}
7657
7658/** ============================================================================
7659 * Location of all the endpoint drawing routines.
7660 *
7661 * Every endpoint has its own drawing routine, which contains an endpoint definition.
7662 *
7663 * The endpoint definitions must have the following properies:
7664 *
7665 * - (0,0) is the connection point to the node it attaches to
7666 * - The endpoints are orientated to the positive x-direction
7667 * - The length of the endpoint is at most 1
7668 *
7669 * As long as the endpoint classes remain simple and not too numerous, they will
7670 * be contained within this module.
7671 * All classes here except `EndPoints` should be considered as private to this module.
7672 *
7673 * -----------------------------------------------------------------------------
7674 * ### Further Actions
7675 *
7676 * After adding a new endpoint here, you also need to do the following things:
7677 *
7678 * - Add the new endpoint name to `network/options.js` in array `endPoints`.
7679 * - Add the new endpoint name to the documentation.
7680 * Scan for 'arrows.to.type` and add it to the description.
7681 * - Add the endpoint to the examples. At the very least, add it to example
7682 * `edgeStyles/arrowTypes`.
7683 * ============================================================================= */
7684/**
7685 * Common methods for endpoints
7686 *
7687 * @class
7688 */
7689class EndPoint {
7690 /**
7691 * Apply transformation on points for display.
7692 *
7693 * The following is done:
7694 * - rotate by the specified angle
7695 * - multiply the (normalized) coordinates by the passed length
7696 * - offset by the target coordinates
7697 *
7698 * @param points - The point(s) to be transformed.
7699 * @param arrowData - The data determining the result of the transformation.
7700 */
7701 static transform(points, arrowData) {
7702 if (!Array.isArray(points)) {
7703 points = [points];
7704 }
7705 const x = arrowData.point.x;
7706 const y = arrowData.point.y;
7707 const angle = arrowData.angle;
7708 const length = arrowData.length;
7709 for (let i = 0; i < points.length; ++i) {
7710 const p = points[i];
7711 const xt = p.x * Math.cos(angle) - p.y * Math.sin(angle);
7712 const yt = p.x * Math.sin(angle) + p.y * Math.cos(angle);
7713 p.x = x + length * xt;
7714 p.y = y + length * yt;
7715 }
7716 }
7717 /**
7718 * Draw a closed path using the given real coordinates.
7719 *
7720 * @param ctx - The path will be rendered into this context.
7721 * @param points - The points of the path.
7722 */
7723 static drawPath(ctx, points) {
7724 ctx.beginPath();
7725 ctx.moveTo(points[0].x, points[0].y);
7726 for (let i = 1; i < points.length; ++i) {
7727 ctx.lineTo(points[i].x, points[i].y);
7728 }
7729 ctx.closePath();
7730 }
7731}
7732/**
7733 * Drawing methods for the arrow endpoint.
7734 */
7735class Image$1 extends EndPoint {
7736 /**
7737 * Draw this shape at the end of a line.
7738 *
7739 * @param ctx - The shape will be rendered into this context.
7740 * @param arrowData - The data determining the shape.
7741 *
7742 * @returns False as there is no way to fill an image.
7743 */
7744 static draw(ctx, arrowData) {
7745 if (arrowData.image) {
7746 ctx.save();
7747 ctx.translate(arrowData.point.x, arrowData.point.y);
7748 ctx.rotate(Math.PI / 2 + arrowData.angle);
7749 const width = arrowData.imageWidth != null
7750 ? arrowData.imageWidth
7751 : arrowData.image.width;
7752 const height = arrowData.imageHeight != null
7753 ? arrowData.imageHeight
7754 : arrowData.image.height;
7755 arrowData.image.drawImageAtPosition(ctx, 1, // scale
7756 -width / 2, // x
7757 0, // y
7758 width, height);
7759 ctx.restore();
7760 }
7761 return false;
7762 }
7763}
7764/**
7765 * Drawing methods for the arrow endpoint.
7766 */
7767class Arrow extends EndPoint {
7768 /**
7769 * Draw this shape at the end of a line.
7770 *
7771 * @param ctx - The shape will be rendered into this context.
7772 * @param arrowData - The data determining the shape.
7773 *
7774 * @returns True because ctx.fill() can be used to fill the arrow.
7775 */
7776 static draw(ctx, arrowData) {
7777 // Normalized points of closed path, in the order that they should be drawn.
7778 // (0, 0) is the attachment point, and the point around which should be rotated
7779 const points = [
7780 { x: 0, y: 0 },
7781 { x: -1, y: 0.3 },
7782 { x: -0.9, y: 0 },
7783 { x: -1, y: -0.3 },
7784 ];
7785 EndPoint.transform(points, arrowData);
7786 EndPoint.drawPath(ctx, points);
7787 return true;
7788 }
7789}
7790/**
7791 * Drawing methods for the crow endpoint.
7792 */
7793class Crow {
7794 /**
7795 * Draw this shape at the end of a line.
7796 *
7797 * @param ctx - The shape will be rendered into this context.
7798 * @param arrowData - The data determining the shape.
7799 *
7800 * @returns True because ctx.fill() can be used to fill the arrow.
7801 */
7802 static draw(ctx, arrowData) {
7803 // Normalized points of closed path, in the order that they should be drawn.
7804 // (0, 0) is the attachment point, and the point around which should be rotated
7805 const points = [
7806 { x: -1, y: 0 },
7807 { x: 0, y: 0.3 },
7808 { x: -0.4, y: 0 },
7809 { x: 0, y: -0.3 },
7810 ];
7811 EndPoint.transform(points, arrowData);
7812 EndPoint.drawPath(ctx, points);
7813 return true;
7814 }
7815}
7816/**
7817 * Drawing methods for the curve endpoint.
7818 */
7819class Curve {
7820 /**
7821 * Draw this shape at the end of a line.
7822 *
7823 * @param ctx - The shape will be rendered into this context.
7824 * @param arrowData - The data determining the shape.
7825 *
7826 * @returns True because ctx.fill() can be used to fill the arrow.
7827 */
7828 static draw(ctx, arrowData) {
7829 // Normalized points of closed path, in the order that they should be drawn.
7830 // (0, 0) is the attachment point, and the point around which should be rotated
7831 const point = { x: -0.4, y: 0 };
7832 EndPoint.transform(point, arrowData);
7833 // Update endpoint style for drawing transparent arc.
7834 ctx.strokeStyle = ctx.fillStyle;
7835 ctx.fillStyle = "rgba(0, 0, 0, 0)";
7836 // Define curve endpoint as semicircle.
7837 const pi = Math.PI;
7838 const startAngle = arrowData.angle - pi / 2;
7839 const endAngle = arrowData.angle + pi / 2;
7840 ctx.beginPath();
7841 ctx.arc(point.x, point.y, arrowData.length * 0.4, startAngle, endAngle, false);
7842 ctx.stroke();
7843 return true;
7844 }
7845}
7846/**
7847 * Drawing methods for the inverted curve endpoint.
7848 */
7849class InvertedCurve {
7850 /**
7851 * Draw this shape at the end of a line.
7852 *
7853 * @param ctx - The shape will be rendered into this context.
7854 * @param arrowData - The data determining the shape.
7855 *
7856 * @returns True because ctx.fill() can be used to fill the arrow.
7857 */
7858 static draw(ctx, arrowData) {
7859 // Normalized points of closed path, in the order that they should be drawn.
7860 // (0, 0) is the attachment point, and the point around which should be rotated
7861 const point = { x: -0.3, y: 0 };
7862 EndPoint.transform(point, arrowData);
7863 // Update endpoint style for drawing transparent arc.
7864 ctx.strokeStyle = ctx.fillStyle;
7865 ctx.fillStyle = "rgba(0, 0, 0, 0)";
7866 // Define inverted curve endpoint as semicircle.
7867 const pi = Math.PI;
7868 const startAngle = arrowData.angle + pi / 2;
7869 const endAngle = arrowData.angle + (3 * pi) / 2;
7870 ctx.beginPath();
7871 ctx.arc(point.x, point.y, arrowData.length * 0.4, startAngle, endAngle, false);
7872 ctx.stroke();
7873 return true;
7874 }
7875}
7876/**
7877 * Drawing methods for the trinagle endpoint.
7878 */
7879class Triangle {
7880 /**
7881 * Draw this shape at the end of a line.
7882 *
7883 * @param ctx - The shape will be rendered into this context.
7884 * @param arrowData - The data determining the shape.
7885 *
7886 * @returns True because ctx.fill() can be used to fill the arrow.
7887 */
7888 static draw(ctx, arrowData) {
7889 // Normalized points of closed path, in the order that they should be drawn.
7890 // (0, 0) is the attachment point, and the point around which should be rotated
7891 const points = [
7892 { x: 0.02, y: 0 },
7893 { x: -1, y: 0.3 },
7894 { x: -1, y: -0.3 },
7895 ];
7896 EndPoint.transform(points, arrowData);
7897 EndPoint.drawPath(ctx, points);
7898 return true;
7899 }
7900}
7901/**
7902 * Drawing methods for the inverted trinagle endpoint.
7903 */
7904class InvertedTriangle {
7905 /**
7906 * Draw this shape at the end of a line.
7907 *
7908 * @param ctx - The shape will be rendered into this context.
7909 * @param arrowData - The data determining the shape.
7910 *
7911 * @returns True because ctx.fill() can be used to fill the arrow.
7912 */
7913 static draw(ctx, arrowData) {
7914 // Normalized points of closed path, in the order that they should be drawn.
7915 // (0, 0) is the attachment point, and the point around which should be rotated
7916 const points = [
7917 { x: 0, y: 0.3 },
7918 { x: 0, y: -0.3 },
7919 { x: -1, y: 0 },
7920 ];
7921 EndPoint.transform(points, arrowData);
7922 EndPoint.drawPath(ctx, points);
7923 return true;
7924 }
7925}
7926/**
7927 * Drawing methods for the circle endpoint.
7928 */
7929class Circle {
7930 /**
7931 * Draw this shape at the end of a line.
7932 *
7933 * @param ctx - The shape will be rendered into this context.
7934 * @param arrowData - The data determining the shape.
7935 *
7936 * @returns True because ctx.fill() can be used to fill the arrow.
7937 */
7938 static draw(ctx, arrowData) {
7939 const point = { x: -0.4, y: 0 };
7940 EndPoint.transform(point, arrowData);
7941 drawCircle(ctx, point.x, point.y, arrowData.length * 0.4);
7942 return true;
7943 }
7944}
7945/**
7946 * Drawing methods for the bar endpoint.
7947 */
7948class Bar {
7949 /**
7950 * Draw this shape at the end of a line.
7951 *
7952 * @param ctx - The shape will be rendered into this context.
7953 * @param arrowData - The data determining the shape.
7954 *
7955 * @returns True because ctx.fill() can be used to fill the arrow.
7956 */
7957 static draw(ctx, arrowData) {
7958 /*
7959 var points = [
7960 {x:0, y:0.5},
7961 {x:0, y:-0.5}
7962 ];
7963
7964 EndPoint.transform(points, arrowData);
7965 ctx.beginPath();
7966 ctx.moveTo(points[0].x, points[0].y);
7967 ctx.lineTo(points[1].x, points[1].y);
7968 ctx.stroke();
7969 */
7970 const points = [
7971 { x: 0, y: 0.5 },
7972 { x: 0, y: -0.5 },
7973 { x: -0.15, y: -0.5 },
7974 { x: -0.15, y: 0.5 },
7975 ];
7976 EndPoint.transform(points, arrowData);
7977 EndPoint.drawPath(ctx, points);
7978 return true;
7979 }
7980}
7981/**
7982 * Drawing methods for the box endpoint.
7983 */
7984class Box {
7985 /**
7986 * Draw this shape at the end of a line.
7987 *
7988 * @param ctx - The shape will be rendered into this context.
7989 * @param arrowData - The data determining the shape.
7990 *
7991 * @returns True because ctx.fill() can be used to fill the arrow.
7992 */
7993 static draw(ctx, arrowData) {
7994 const points = [
7995 { x: 0, y: 0.3 },
7996 { x: 0, y: -0.3 },
7997 { x: -0.6, y: -0.3 },
7998 { x: -0.6, y: 0.3 },
7999 ];
8000 EndPoint.transform(points, arrowData);
8001 EndPoint.drawPath(ctx, points);
8002 return true;
8003 }
8004}
8005/**
8006 * Drawing methods for the diamond endpoint.
8007 */
8008class Diamond {
8009 /**
8010 * Draw this shape at the end of a line.
8011 *
8012 * @param ctx - The shape will be rendered into this context.
8013 * @param arrowData - The data determining the shape.
8014 *
8015 * @returns True because ctx.fill() can be used to fill the arrow.
8016 */
8017 static draw(ctx, arrowData) {
8018 const points = [
8019 { x: 0, y: 0 },
8020 { x: -0.5, y: -0.3 },
8021 { x: -1, y: 0 },
8022 { x: -0.5, y: 0.3 },
8023 ];
8024 EndPoint.transform(points, arrowData);
8025 EndPoint.drawPath(ctx, points);
8026 return true;
8027 }
8028}
8029/**
8030 * Drawing methods for the vee endpoint.
8031 */
8032class Vee {
8033 /**
8034 * Draw this shape at the end of a line.
8035 *
8036 * @param ctx - The shape will be rendered into this context.
8037 * @param arrowData - The data determining the shape.
8038 *
8039 * @returns True because ctx.fill() can be used to fill the arrow.
8040 */
8041 static draw(ctx, arrowData) {
8042 // Normalized points of closed path, in the order that they should be drawn.
8043 // (0, 0) is the attachment point, and the point around which should be rotated
8044 const points = [
8045 { x: -1, y: 0.3 },
8046 { x: -0.5, y: 0 },
8047 { x: -1, y: -0.3 },
8048 { x: 0, y: 0 },
8049 ];
8050 EndPoint.transform(points, arrowData);
8051 EndPoint.drawPath(ctx, points);
8052 return true;
8053 }
8054}
8055/**
8056 * Drawing methods for the endpoints.
8057 */
8058class EndPoints {
8059 /**
8060 * Draw an endpoint.
8061 *
8062 * @param ctx - The shape will be rendered into this context.
8063 * @param arrowData - The data determining the shape.
8064 *
8065 * @returns True if ctx.fill() can be used to fill the arrow, false otherwise.
8066 */
8067 static draw(ctx, arrowData) {
8068 let type;
8069 if (arrowData.type) {
8070 type = arrowData.type.toLowerCase();
8071 }
8072 switch (type) {
8073 case "image":
8074 return Image$1.draw(ctx, arrowData);
8075 case "circle":
8076 return Circle.draw(ctx, arrowData);
8077 case "box":
8078 return Box.draw(ctx, arrowData);
8079 case "crow":
8080 return Crow.draw(ctx, arrowData);
8081 case "curve":
8082 return Curve.draw(ctx, arrowData);
8083 case "diamond":
8084 return Diamond.draw(ctx, arrowData);
8085 case "inv_curve":
8086 return InvertedCurve.draw(ctx, arrowData);
8087 case "triangle":
8088 return Triangle.draw(ctx, arrowData);
8089 case "inv_triangle":
8090 return InvertedTriangle.draw(ctx, arrowData);
8091 case "bar":
8092 return Bar.draw(ctx, arrowData);
8093 case "vee":
8094 return Vee.draw(ctx, arrowData);
8095 case "arrow": // fall-through
8096 default:
8097 return Arrow.draw(ctx, arrowData);
8098 }
8099 }
8100}
8101
8102/**
8103 * The Base Class for all edges.
8104 */
8105class EdgeBase {
8106 /**
8107 * Create a new instance.
8108 *
8109 * @param options - The options object of given edge.
8110 * @param _body - The body of the network.
8111 * @param _labelModule - Label module.
8112 */
8113 constructor(options, _body, _labelModule) {
8114 this._body = _body;
8115 this._labelModule = _labelModule;
8116 this.color = {};
8117 this.colorDirty = true;
8118 this.hoverWidth = 1.5;
8119 this.selectionWidth = 2;
8120 this.setOptions(options);
8121 this.fromPoint = this.from;
8122 this.toPoint = this.to;
8123 }
8124 /** @inheritDoc */
8125 connect() {
8126 this.from = this._body.nodes[this.options.from];
8127 this.to = this._body.nodes[this.options.to];
8128 }
8129 /** @inheritDoc */
8130 cleanup() {
8131 return false;
8132 }
8133 /**
8134 * Set new edge options.
8135 *
8136 * @param options - The new edge options object.
8137 */
8138 setOptions(options) {
8139 this.options = options;
8140 this.from = this._body.nodes[this.options.from];
8141 this.to = this._body.nodes[this.options.to];
8142 this.id = this.options.id;
8143 }
8144 /** @inheritDoc */
8145 drawLine(ctx, values, _selected, _hover, viaNode = this.getViaNode()) {
8146 // set style
8147 ctx.strokeStyle = this.getColor(ctx, values);
8148 ctx.lineWidth = values.width;
8149 if (values.dashes !== false) {
8150 this._drawDashedLine(ctx, values, viaNode);
8151 }
8152 else {
8153 this._drawLine(ctx, values, viaNode);
8154 }
8155 }
8156 /**
8157 * Draw a line with given style between two nodes through supplied node(s).
8158 *
8159 * @param ctx - The context that will be used for rendering.
8160 * @param values - Formatting values like color, opacity or shadow.
8161 * @param viaNode - Additional control point(s) for the edge.
8162 * @param fromPoint - TODO: Seems ignored, remove?
8163 * @param toPoint - TODO: Seems ignored, remove?
8164 */
8165 _drawLine(ctx, values, viaNode, fromPoint, toPoint) {
8166 if (this.from != this.to) {
8167 // draw line
8168 this._line(ctx, values, viaNode, fromPoint, toPoint);
8169 }
8170 else {
8171 const [x, y, radius] = this._getCircleData(ctx);
8172 this._circle(ctx, values, x, y, radius);
8173 }
8174 }
8175 /**
8176 * Draw a dashed line with given style between two nodes through supplied node(s).
8177 *
8178 * @param ctx - The context that will be used for rendering.
8179 * @param values - Formatting values like color, opacity or shadow.
8180 * @param viaNode - Additional control point(s) for the edge.
8181 * @param _fromPoint - Ignored (TODO: remove in the future).
8182 * @param _toPoint - Ignored (TODO: remove in the future).
8183 */
8184 _drawDashedLine(ctx, values, viaNode, _fromPoint, _toPoint) {
8185 ctx.lineCap = "round";
8186 const pattern = Array.isArray(values.dashes) ? values.dashes : [5, 5];
8187 // only firefox and chrome support this method, else we use the legacy one.
8188 if (ctx.setLineDash !== undefined) {
8189 ctx.save();
8190 // set dash settings for chrome or firefox
8191 ctx.setLineDash(pattern);
8192 ctx.lineDashOffset = 0;
8193 // draw the line
8194 if (this.from != this.to) {
8195 // draw line
8196 this._line(ctx, values, viaNode);
8197 }
8198 else {
8199 const [x, y, radius] = this._getCircleData(ctx);
8200 this._circle(ctx, values, x, y, radius);
8201 }
8202 // restore the dash settings.
8203 ctx.setLineDash([0]);
8204 ctx.lineDashOffset = 0;
8205 ctx.restore();
8206 }
8207 else {
8208 // unsupporting smooth lines
8209 if (this.from != this.to) {
8210 // draw line
8211 drawDashedLine(ctx, this.from.x, this.from.y, this.to.x, this.to.y, pattern);
8212 }
8213 else {
8214 const [x, y, radius] = this._getCircleData(ctx);
8215 this._circle(ctx, values, x, y, radius);
8216 }
8217 // draw shadow if enabled
8218 this.enableShadow(ctx, values);
8219 ctx.stroke();
8220 // disable shadows for other elements.
8221 this.disableShadow(ctx, values);
8222 }
8223 }
8224 /**
8225 * Find the intersection between the border of the node and the edge.
8226 *
8227 * @param node - The node (either from or to node of the edge).
8228 * @param ctx - The context that will be used for rendering.
8229 * @param options - Additional options.
8230 *
8231 * @returns Cartesian coordinates of the intersection between the border of the node and the edge.
8232 */
8233 findBorderPosition(node, ctx, options) {
8234 if (this.from != this.to) {
8235 return this._findBorderPosition(node, ctx, options);
8236 }
8237 else {
8238 return this._findBorderPositionCircle(node, ctx, options);
8239 }
8240 }
8241 /** @inheritDoc */
8242 findBorderPositions(ctx) {
8243 if (this.from != this.to) {
8244 return {
8245 from: this._findBorderPosition(this.from, ctx),
8246 to: this._findBorderPosition(this.to, ctx),
8247 };
8248 }
8249 else {
8250 const [x, y] = this._getCircleData(ctx).slice(0, 2);
8251 return {
8252 from: this._findBorderPositionCircle(this.from, ctx, {
8253 x,
8254 y,
8255 low: 0.25,
8256 high: 0.6,
8257 direction: -1,
8258 }),
8259 to: this._findBorderPositionCircle(this.from, ctx, {
8260 x,
8261 y,
8262 low: 0.6,
8263 high: 0.8,
8264 direction: 1,
8265 }),
8266 };
8267 }
8268 }
8269 /**
8270 * Compute the center point and radius of an edge connected to the same node at both ends.
8271 *
8272 * @param ctx - The context that will be used for rendering.
8273 *
8274 * @returns `[x, y, radius]`
8275 */
8276 _getCircleData(ctx) {
8277 const radius = this.options.selfReference.size;
8278 if (ctx !== undefined) {
8279 if (this.from.shape.width === undefined) {
8280 this.from.shape.resize(ctx);
8281 }
8282 }
8283 // get circle coordinates
8284 const coordinates = getSelfRefCoordinates(ctx, this.options.selfReference.angle, radius, this.from);
8285 return [coordinates.x, coordinates.y, radius];
8286 }
8287 /**
8288 * Get a point on a circle.
8289 *
8290 * @param x - Center of the circle on the x axis.
8291 * @param y - Center of the circle on the y axis.
8292 * @param radius - Radius of the circle.
8293 * @param position - Value between 0 (line start) and 1 (line end).
8294 *
8295 * @returns Cartesian coordinates of requested point on the circle.
8296 */
8297 _pointOnCircle(x, y, radius, position) {
8298 const angle = position * 2 * Math.PI;
8299 return {
8300 x: x + radius * Math.cos(angle),
8301 y: y - radius * Math.sin(angle),
8302 };
8303 }
8304 /**
8305 * Find the intersection between the border of the node and the edge.
8306 *
8307 * @remarks
8308 * This function uses binary search to look for the point where the circle crosses the border of the node.
8309 *
8310 * @param nearNode - The node (either from or to node of the edge).
8311 * @param ctx - The context that will be used for rendering.
8312 * @param options - Additional options.
8313 *
8314 * @returns Cartesian coordinates of the intersection between the border of the node and the edge.
8315 */
8316 _findBorderPositionCircle(nearNode, ctx, options) {
8317 const x = options.x;
8318 const y = options.y;
8319 let low = options.low;
8320 let high = options.high;
8321 const direction = options.direction;
8322 const maxIterations = 10;
8323 const radius = this.options.selfReference.size;
8324 const threshold = 0.05;
8325 let pos;
8326 let middle = (low + high) * 0.5;
8327 let endPointOffset = 0;
8328 if (this.options.arrowStrikethrough === true) {
8329 if (direction === -1) {
8330 endPointOffset = this.options.endPointOffset.from;
8331 }
8332 else if (direction === 1) {
8333 endPointOffset = this.options.endPointOffset.to;
8334 }
8335 }
8336 let iteration = 0;
8337 do {
8338 middle = (low + high) * 0.5;
8339 pos = this._pointOnCircle(x, y, radius, middle);
8340 const angle = Math.atan2(nearNode.y - pos.y, nearNode.x - pos.x);
8341 const distanceToBorder = nearNode.distanceToBorder(ctx, angle) + endPointOffset;
8342 const distanceToPoint = Math.sqrt(Math.pow(pos.x - nearNode.x, 2) + Math.pow(pos.y - nearNode.y, 2));
8343 const difference = distanceToBorder - distanceToPoint;
8344 if (Math.abs(difference) < threshold) {
8345 break; // found
8346 }
8347 else if (difference > 0) {
8348 // distance to nodes is larger than distance to border --> t needs to be bigger if we're looking at the to node.
8349 if (direction > 0) {
8350 low = middle;
8351 }
8352 else {
8353 high = middle;
8354 }
8355 }
8356 else {
8357 if (direction > 0) {
8358 high = middle;
8359 }
8360 else {
8361 low = middle;
8362 }
8363 }
8364 ++iteration;
8365 } while (low <= high && iteration < maxIterations);
8366 return {
8367 ...pos,
8368 t: middle,
8369 };
8370 }
8371 /**
8372 * Get the line width of the edge. Depends on width and whether one of the connected nodes is selected.
8373 *
8374 * @param selected - Determines wheter the line is selected.
8375 * @param hover - Determines wheter the line is being hovered, only applies if selected is false.
8376 *
8377 * @returns The width of the line.
8378 */
8379 getLineWidth(selected, hover) {
8380 if (selected === true) {
8381 return Math.max(this.selectionWidth, 0.3 / this._body.view.scale);
8382 }
8383 else if (hover === true) {
8384 return Math.max(this.hoverWidth, 0.3 / this._body.view.scale);
8385 }
8386 else {
8387 return Math.max(this.options.width, 0.3 / this._body.view.scale);
8388 }
8389 }
8390 /**
8391 * Compute the color or gradient for given edge.
8392 *
8393 * @param ctx - The context that will be used for rendering.
8394 * @param values - Formatting values like color, opacity or shadow.
8395 * @param _selected - Ignored (TODO: remove in the future).
8396 * @param _hover - Ignored (TODO: remove in the future).
8397 *
8398 * @returns Color string if single color is inherited or gradient if two.
8399 */
8400 getColor(ctx, values) {
8401 if (values.inheritsColor !== false) {
8402 // when this is a loop edge, just use the 'from' method
8403 if (values.inheritsColor === "both" && this.from.id !== this.to.id) {
8404 const grd = ctx.createLinearGradient(this.from.x, this.from.y, this.to.x, this.to.y);
8405 let fromColor = this.from.options.color.highlight.border;
8406 let toColor = this.to.options.color.highlight.border;
8407 if (this.from.selected === false && this.to.selected === false) {
8408 fromColor = overrideOpacity(this.from.options.color.border, values.opacity);
8409 toColor = overrideOpacity(this.to.options.color.border, values.opacity);
8410 }
8411 else if (this.from.selected === true && this.to.selected === false) {
8412 toColor = this.to.options.color.border;
8413 }
8414 else if (this.from.selected === false && this.to.selected === true) {
8415 fromColor = this.from.options.color.border;
8416 }
8417 grd.addColorStop(0, fromColor);
8418 grd.addColorStop(1, toColor);
8419 // -------------------- this returns -------------------- //
8420 return grd;
8421 }
8422 if (values.inheritsColor === "to") {
8423 return overrideOpacity(this.to.options.color.border, values.opacity);
8424 }
8425 else {
8426 // "from"
8427 return overrideOpacity(this.from.options.color.border, values.opacity);
8428 }
8429 }
8430 else {
8431 return overrideOpacity(values.color, values.opacity);
8432 }
8433 }
8434 /**
8435 * Draw a line from a node to itself, a circle.
8436 *
8437 * @param ctx - The context that will be used for rendering.
8438 * @param values - Formatting values like color, opacity or shadow.
8439 * @param x - Center of the circle on the x axis.
8440 * @param y - Center of the circle on the y axis.
8441 * @param radius - Radius of the circle.
8442 */
8443 _circle(ctx, values, x, y, radius) {
8444 // draw shadow if enabled
8445 this.enableShadow(ctx, values);
8446 //full circle
8447 let angleFrom = 0;
8448 let angleTo = Math.PI * 2;
8449 if (!this.options.selfReference.renderBehindTheNode) {
8450 //render only parts which are not overlaping with parent node
8451 //need to find x,y of from point and x,y to point
8452 //calculating radians
8453 const low = this.options.selfReference.angle;
8454 const high = this.options.selfReference.angle + Math.PI;
8455 const pointTFrom = this._findBorderPositionCircle(this.from, ctx, {
8456 x,
8457 y,
8458 low,
8459 high,
8460 direction: -1,
8461 });
8462 const pointTTo = this._findBorderPositionCircle(this.from, ctx, {
8463 x,
8464 y,
8465 low,
8466 high,
8467 direction: 1,
8468 });
8469 angleFrom = Math.atan2(pointTFrom.y - y, pointTFrom.x - x);
8470 angleTo = Math.atan2(pointTTo.y - y, pointTTo.x - x);
8471 }
8472 // draw a circle
8473 ctx.beginPath();
8474 ctx.arc(x, y, radius, angleFrom, angleTo, false);
8475 ctx.stroke();
8476 // disable shadows for other elements.
8477 this.disableShadow(ctx, values);
8478 }
8479 /**
8480 * @inheritDoc
8481 *
8482 * @remarks
8483 * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment
8484 */
8485 getDistanceToEdge(x1, y1, x2, y2, x3, y3) {
8486 if (this.from != this.to) {
8487 return this._getDistanceToEdge(x1, y1, x2, y2, x3, y3);
8488 }
8489 else {
8490 const [x, y, radius] = this._getCircleData(undefined);
8491 const dx = x - x3;
8492 const dy = y - y3;
8493 return Math.abs(Math.sqrt(dx * dx + dy * dy) - radius);
8494 }
8495 }
8496 /**
8497 * Calculate the distance between a point (x3, y3) and a line segment from (x1, y1) to (x2, y2).
8498 *
8499 * @param x1 - First end of the line segment on the x axis.
8500 * @param y1 - First end of the line segment on the y axis.
8501 * @param x2 - Second end of the line segment on the x axis.
8502 * @param y2 - Second end of the line segment on the y axis.
8503 * @param x3 - Position of the point on the x axis.
8504 * @param y3 - Position of the point on the y axis.
8505 *
8506 * @returns The distance between the line segment and the point.
8507 */
8508 _getDistanceToLine(x1, y1, x2, y2, x3, y3) {
8509 const px = x2 - x1;
8510 const py = y2 - y1;
8511 const something = px * px + py * py;
8512 let u = ((x3 - x1) * px + (y3 - y1) * py) / something;
8513 if (u > 1) {
8514 u = 1;
8515 }
8516 else if (u < 0) {
8517 u = 0;
8518 }
8519 const x = x1 + u * px;
8520 const y = y1 + u * py;
8521 const dx = x - x3;
8522 const dy = y - y3;
8523 //# Note: If the actual distance does not matter,
8524 //# if you only want to compare what this function
8525 //# returns to other results of this function, you
8526 //# can just return the squared distance instead
8527 //# (i.e. remove the sqrt) to gain a little performance
8528 return Math.sqrt(dx * dx + dy * dy);
8529 }
8530 /** @inheritDoc */
8531 getArrowData(ctx, position, viaNode, _selected, _hover, values) {
8532 // set lets
8533 let angle;
8534 let arrowPoint;
8535 let node1;
8536 let node2;
8537 let reversed;
8538 let scaleFactor;
8539 let type;
8540 const lineWidth = values.width;
8541 if (position === "from") {
8542 node1 = this.from;
8543 node2 = this.to;
8544 reversed = values.fromArrowScale < 0;
8545 scaleFactor = Math.abs(values.fromArrowScale);
8546 type = values.fromArrowType;
8547 }
8548 else if (position === "to") {
8549 node1 = this.to;
8550 node2 = this.from;
8551 reversed = values.toArrowScale < 0;
8552 scaleFactor = Math.abs(values.toArrowScale);
8553 type = values.toArrowType;
8554 }
8555 else {
8556 node1 = this.to;
8557 node2 = this.from;
8558 reversed = values.middleArrowScale < 0;
8559 scaleFactor = Math.abs(values.middleArrowScale);
8560 type = values.middleArrowType;
8561 }
8562 const length = 15 * scaleFactor + 3 * lineWidth; // 3* lineWidth is the width of the edge.
8563 // if not connected to itself
8564 if (node1 != node2) {
8565 const approximateEdgeLength = Math.hypot(node1.x - node2.x, node1.y - node2.y);
8566 const relativeLength = length / approximateEdgeLength;
8567 if (position !== "middle") {
8568 // draw arrow head
8569 if (this.options.smooth.enabled === true) {
8570 const pointT = this._findBorderPosition(node1, ctx, { via: viaNode });
8571 const guidePos = this.getPoint(pointT.t + relativeLength * (position === "from" ? 1 : -1), viaNode);
8572 angle = Math.atan2(pointT.y - guidePos.y, pointT.x - guidePos.x);
8573 arrowPoint = pointT;
8574 }
8575 else {
8576 angle = Math.atan2(node1.y - node2.y, node1.x - node2.x);
8577 arrowPoint = this._findBorderPosition(node1, ctx);
8578 }
8579 }
8580 else {
8581 // Negative half length reverses arrow direction.
8582 const halfLength = (reversed ? -relativeLength : relativeLength) / 2;
8583 const guidePos1 = this.getPoint(0.5 + halfLength, viaNode);
8584 const guidePos2 = this.getPoint(0.5 - halfLength, viaNode);
8585 angle = Math.atan2(guidePos1.y - guidePos2.y, guidePos1.x - guidePos2.x);
8586 arrowPoint = this.getPoint(0.5, viaNode);
8587 }
8588 }
8589 else {
8590 // draw circle
8591 const [x, y, radius] = this._getCircleData(ctx);
8592 if (position === "from") {
8593 const low = this.options.selfReference.angle;
8594 const high = this.options.selfReference.angle + Math.PI;
8595 const pointT = this._findBorderPositionCircle(this.from, ctx, {
8596 x,
8597 y,
8598 low,
8599 high,
8600 direction: -1,
8601 });
8602 angle = pointT.t * -2 * Math.PI + 1.5 * Math.PI + 0.1 * Math.PI;
8603 arrowPoint = pointT;
8604 }
8605 else if (position === "to") {
8606 const low = this.options.selfReference.angle;
8607 const high = this.options.selfReference.angle + Math.PI;
8608 const pointT = this._findBorderPositionCircle(this.from, ctx, {
8609 x,
8610 y,
8611 low,
8612 high,
8613 direction: 1,
8614 });
8615 angle = pointT.t * -2 * Math.PI + 1.5 * Math.PI - 1.1 * Math.PI;
8616 arrowPoint = pointT;
8617 }
8618 else {
8619 const pos = this.options.selfReference.angle / (2 * Math.PI);
8620 arrowPoint = this._pointOnCircle(x, y, radius, pos);
8621 angle = pos * -2 * Math.PI + 1.5 * Math.PI + 0.1 * Math.PI;
8622 }
8623 }
8624 const xi = arrowPoint.x - length * 0.9 * Math.cos(angle);
8625 const yi = arrowPoint.y - length * 0.9 * Math.sin(angle);
8626 const arrowCore = { x: xi, y: yi };
8627 return {
8628 point: arrowPoint,
8629 core: arrowCore,
8630 angle: angle,
8631 length: length,
8632 type: type,
8633 };
8634 }
8635 /** @inheritDoc */
8636 drawArrowHead(ctx, values, _selected, _hover, arrowData) {
8637 // set style
8638 ctx.strokeStyle = this.getColor(ctx, values);
8639 ctx.fillStyle = ctx.strokeStyle;
8640 ctx.lineWidth = values.width;
8641 const canFill = EndPoints.draw(ctx, arrowData);
8642 if (canFill) {
8643 // draw shadow if enabled
8644 this.enableShadow(ctx, values);
8645 ctx.fill();
8646 // disable shadows for other elements.
8647 this.disableShadow(ctx, values);
8648 }
8649 }
8650 /**
8651 * Set the shadow formatting values in the context if enabled, do nothing otherwise.
8652 *
8653 * @param ctx - The context that will be used for rendering.
8654 * @param values - Formatting values for the shadow.
8655 */
8656 enableShadow(ctx, values) {
8657 if (values.shadow === true) {
8658 ctx.shadowColor = values.shadowColor;
8659 ctx.shadowBlur = values.shadowSize;
8660 ctx.shadowOffsetX = values.shadowX;
8661 ctx.shadowOffsetY = values.shadowY;
8662 }
8663 }
8664 /**
8665 * Reset the shadow formatting values in the context if enabled, do nothing otherwise.
8666 *
8667 * @param ctx - The context that will be used for rendering.
8668 * @param values - Formatting values for the shadow.
8669 */
8670 disableShadow(ctx, values) {
8671 if (values.shadow === true) {
8672 ctx.shadowColor = "rgba(0,0,0,0)";
8673 ctx.shadowBlur = 0;
8674 ctx.shadowOffsetX = 0;
8675 ctx.shadowOffsetY = 0;
8676 }
8677 }
8678 /**
8679 * Render the background according to the formatting values.
8680 *
8681 * @param ctx - The context that will be used for rendering.
8682 * @param values - Formatting values for the background.
8683 */
8684 drawBackground(ctx, values) {
8685 if (values.background !== false) {
8686 // save original line attrs
8687 const origCtxAttr = {
8688 strokeStyle: ctx.strokeStyle,
8689 lineWidth: ctx.lineWidth,
8690 dashes: ctx.dashes,
8691 };
8692 ctx.strokeStyle = values.backgroundColor;
8693 ctx.lineWidth = values.backgroundSize;
8694 this.setStrokeDashed(ctx, values.backgroundDashes);
8695 ctx.stroke();
8696 // restore original line attrs
8697 ctx.strokeStyle = origCtxAttr.strokeStyle;
8698 ctx.lineWidth = origCtxAttr.lineWidth;
8699 ctx.dashes = origCtxAttr.dashes;
8700 this.setStrokeDashed(ctx, values.dashes);
8701 }
8702 }
8703 /**
8704 * Set the line dash pattern if supported. Logs a warning to the console if it isn't supported.
8705 *
8706 * @param ctx - The context that will be used for rendering.
8707 * @param dashes - The pattern [line, space, line…], true for default dashed line or false for normal line.
8708 */
8709 setStrokeDashed(ctx, dashes) {
8710 if (dashes !== false) {
8711 if (ctx.setLineDash !== undefined) {
8712 const pattern = Array.isArray(dashes) ? dashes : [5, 5];
8713 ctx.setLineDash(pattern);
8714 }
8715 else {
8716 console.warn("setLineDash is not supported in this browser. The dashed stroke cannot be used.");
8717 }
8718 }
8719 else {
8720 if (ctx.setLineDash !== undefined) {
8721 ctx.setLineDash([]);
8722 }
8723 else {
8724 console.warn("setLineDash is not supported in this browser. The dashed stroke cannot be used.");
8725 }
8726 }
8727 }
8728}
8729
8730/**
8731 * The Base Class for all Bezier edges.
8732 * Bezier curves are used to model smooth gradual curves in paths between nodes.
8733 */
8734class BezierEdgeBase extends EdgeBase {
8735 /**
8736 * Create a new instance.
8737 *
8738 * @param options - The options object of given edge.
8739 * @param body - The body of the network.
8740 * @param labelModule - Label module.
8741 */
8742 constructor(options, body, labelModule) {
8743 super(options, body, labelModule);
8744 }
8745 /**
8746 * Find the intersection between the border of the node and the edge.
8747 *
8748 * @remarks
8749 * This function uses binary search to look for the point where the bezier curve crosses the border of the node.
8750 *
8751 * @param nearNode - The node (either from or to node of the edge).
8752 * @param ctx - The context that will be used for rendering.
8753 * @param viaNode - Additional node(s) the edge passes through.
8754 *
8755 * @returns Cartesian coordinates of the intersection between the border of the node and the edge.
8756 */
8757 _findBorderPositionBezier(nearNode, ctx, viaNode = this._getViaCoordinates()) {
8758 const maxIterations = 10;
8759 const threshold = 0.2;
8760 let from = false;
8761 let high = 1;
8762 let low = 0;
8763 let node = this.to;
8764 let pos;
8765 let middle;
8766 let endPointOffset = this.options.endPointOffset
8767 ? this.options.endPointOffset.to
8768 : 0;
8769 if (nearNode.id === this.from.id) {
8770 node = this.from;
8771 from = true;
8772 endPointOffset = this.options.endPointOffset
8773 ? this.options.endPointOffset.from
8774 : 0;
8775 }
8776 if (this.options.arrowStrikethrough === false) {
8777 endPointOffset = 0;
8778 }
8779 let iteration = 0;
8780 do {
8781 middle = (low + high) * 0.5;
8782 pos = this.getPoint(middle, viaNode);
8783 const angle = Math.atan2(node.y - pos.y, node.x - pos.x);
8784 const distanceToBorder = node.distanceToBorder(ctx, angle) + endPointOffset;
8785 const distanceToPoint = Math.sqrt(Math.pow(pos.x - node.x, 2) + Math.pow(pos.y - node.y, 2));
8786 const difference = distanceToBorder - distanceToPoint;
8787 if (Math.abs(difference) < threshold) {
8788 break; // found
8789 }
8790 else if (difference < 0) {
8791 // distance to nodes is larger than distance to border --> t needs to be bigger if we're looking at the to node.
8792 if (from === false) {
8793 low = middle;
8794 }
8795 else {
8796 high = middle;
8797 }
8798 }
8799 else {
8800 if (from === false) {
8801 high = middle;
8802 }
8803 else {
8804 low = middle;
8805 }
8806 }
8807 ++iteration;
8808 } while (low <= high && iteration < maxIterations);
8809 return {
8810 ...pos,
8811 t: middle,
8812 };
8813 }
8814 /**
8815 * Calculate the distance between a point (x3,y3) and a line segment from (x1,y1) to (x2,y2).
8816 *
8817 * @remarks
8818 * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment
8819 *
8820 * @param x1 - First end of the line segment on the x axis.
8821 * @param y1 - First end of the line segment on the y axis.
8822 * @param x2 - Second end of the line segment on the x axis.
8823 * @param y2 - Second end of the line segment on the y axis.
8824 * @param x3 - Position of the point on the x axis.
8825 * @param y3 - Position of the point on the y axis.
8826 * @param via - The control point for the edge.
8827 *
8828 * @returns The distance between the line segment and the point.
8829 */
8830 _getDistanceToBezierEdge(x1, y1, x2, y2, x3, y3, via) {
8831 // x3,y3 is the point
8832 let minDistance = 1e9;
8833 let distance;
8834 let i, t, x, y;
8835 let lastX = x1;
8836 let lastY = y1;
8837 for (i = 1; i < 10; i++) {
8838 t = 0.1 * i;
8839 x =
8840 Math.pow(1 - t, 2) * x1 + 2 * t * (1 - t) * via.x + Math.pow(t, 2) * x2;
8841 y =
8842 Math.pow(1 - t, 2) * y1 + 2 * t * (1 - t) * via.y + Math.pow(t, 2) * y2;
8843 if (i > 0) {
8844 distance = this._getDistanceToLine(lastX, lastY, x, y, x3, y3);
8845 minDistance = distance < minDistance ? distance : minDistance;
8846 }
8847 lastX = x;
8848 lastY = y;
8849 }
8850 return minDistance;
8851 }
8852 /**
8853 * Render a bezier curve between two nodes.
8854 *
8855 * @remarks
8856 * The method accepts zero, one or two control points.
8857 * Passing zero control points just draws a straight line.
8858 *
8859 * @param ctx - The context that will be used for rendering.
8860 * @param values - Style options for edge drawing.
8861 * @param viaNode1 - First control point for curve drawing.
8862 * @param viaNode2 - Second control point for curve drawing.
8863 */
8864 _bezierCurve(ctx, values, viaNode1, viaNode2) {
8865 ctx.beginPath();
8866 ctx.moveTo(this.fromPoint.x, this.fromPoint.y);
8867 if (viaNode1 != null && viaNode1.x != null) {
8868 if (viaNode2 != null && viaNode2.x != null) {
8869 ctx.bezierCurveTo(viaNode1.x, viaNode1.y, viaNode2.x, viaNode2.y, this.toPoint.x, this.toPoint.y);
8870 }
8871 else {
8872 ctx.quadraticCurveTo(viaNode1.x, viaNode1.y, this.toPoint.x, this.toPoint.y);
8873 }
8874 }
8875 else {
8876 // fallback to normal straight edge
8877 ctx.lineTo(this.toPoint.x, this.toPoint.y);
8878 }
8879 // draw a background
8880 this.drawBackground(ctx, values);
8881 // draw shadow if enabled
8882 this.enableShadow(ctx, values);
8883 ctx.stroke();
8884 this.disableShadow(ctx, values);
8885 }
8886 /** @inheritDoc */
8887 getViaNode() {
8888 return this._getViaCoordinates();
8889 }
8890}
8891
8892/**
8893 * A Dynamic Bezier Edge. Bezier curves are used to model smooth gradual
8894 * curves in paths between nodes. The Dynamic piece refers to how the curve
8895 * reacts to physics changes.
8896 *
8897 * @augments BezierEdgeBase
8898 */
8899class BezierEdgeDynamic extends BezierEdgeBase {
8900 /**
8901 * Create a new instance.
8902 *
8903 * @param options - The options object of given edge.
8904 * @param body - The body of the network.
8905 * @param labelModule - Label module.
8906 */
8907 constructor(options, body, labelModule) {
8908 //this.via = undefined; // Here for completeness but not allowed to defined before super() is invoked.
8909 super(options, body, labelModule); // --> this calls the setOptions below
8910 this.via = this.via; // constructor → super → super → setOptions → setupSupportNode
8911 this._boundFunction = () => {
8912 this.positionBezierNode();
8913 };
8914 this._body.emitter.on("_repositionBezierNodes", this._boundFunction);
8915 }
8916 /** @inheritDoc */
8917 setOptions(options) {
8918 super.setOptions(options);
8919 // check if the physics has changed.
8920 let physicsChange = false;
8921 if (this.options.physics !== options.physics) {
8922 physicsChange = true;
8923 }
8924 // set the options and the to and from nodes
8925 this.options = options;
8926 this.id = this.options.id;
8927 this.from = this._body.nodes[this.options.from];
8928 this.to = this._body.nodes[this.options.to];
8929 // setup the support node and connect
8930 this.setupSupportNode();
8931 this.connect();
8932 // when we change the physics state of the edge, we reposition the support node.
8933 if (physicsChange === true) {
8934 this.via.setOptions({ physics: this.options.physics });
8935 this.positionBezierNode();
8936 }
8937 }
8938 /** @inheritDoc */
8939 connect() {
8940 this.from = this._body.nodes[this.options.from];
8941 this.to = this._body.nodes[this.options.to];
8942 if (this.from === undefined ||
8943 this.to === undefined ||
8944 this.options.physics === false) {
8945 this.via.setOptions({ physics: false });
8946 }
8947 else {
8948 // fix weird behaviour where a self referencing node has physics enabled
8949 if (this.from.id === this.to.id) {
8950 this.via.setOptions({ physics: false });
8951 }
8952 else {
8953 this.via.setOptions({ physics: true });
8954 }
8955 }
8956 }
8957 /** @inheritDoc */
8958 cleanup() {
8959 this._body.emitter.off("_repositionBezierNodes", this._boundFunction);
8960 if (this.via !== undefined) {
8961 delete this._body.nodes[this.via.id];
8962 this.via = undefined;
8963 return true;
8964 }
8965 return false;
8966 }
8967 /**
8968 * Create and add a support node if not already present.
8969 *
8970 * @remarks
8971 * Bezier curves require an anchor point to calculate the smooth flow.
8972 * These points are nodes.
8973 * These nodes are invisible but are used for the force calculation.
8974 *
8975 * The changed data is not called, if needed, it is returned by the main edge constructor.
8976 */
8977 setupSupportNode() {
8978 if (this.via === undefined) {
8979 const nodeId = "edgeId:" + this.id;
8980 const node = this._body.functions.createNode({
8981 id: nodeId,
8982 shape: "circle",
8983 physics: true,
8984 hidden: true,
8985 });
8986 this._body.nodes[nodeId] = node;
8987 this.via = node;
8988 this.via.parentEdgeId = this.id;
8989 this.positionBezierNode();
8990 }
8991 }
8992 /**
8993 * Position bezier node.
8994 */
8995 positionBezierNode() {
8996 if (this.via !== undefined &&
8997 this.from !== undefined &&
8998 this.to !== undefined) {
8999 this.via.x = 0.5 * (this.from.x + this.to.x);
9000 this.via.y = 0.5 * (this.from.y + this.to.y);
9001 }
9002 else if (this.via !== undefined) {
9003 this.via.x = 0;
9004 this.via.y = 0;
9005 }
9006 }
9007 /** @inheritDoc */
9008 _line(ctx, values, viaNode) {
9009 this._bezierCurve(ctx, values, viaNode);
9010 }
9011 /** @inheritDoc */
9012 _getViaCoordinates() {
9013 return this.via;
9014 }
9015 /** @inheritDoc */
9016 getViaNode() {
9017 return this.via;
9018 }
9019 /** @inheritDoc */
9020 getPoint(position, viaNode = this.via) {
9021 if (this.from === this.to) {
9022 const [cx, cy, cr] = this._getCircleData();
9023 const a = 2 * Math.PI * (1 - position);
9024 return {
9025 x: cx + cr * Math.sin(a),
9026 y: cy + cr - cr * (1 - Math.cos(a)),
9027 };
9028 }
9029 else {
9030 return {
9031 x: Math.pow(1 - position, 2) * this.fromPoint.x +
9032 2 * position * (1 - position) * viaNode.x +
9033 Math.pow(position, 2) * this.toPoint.x,
9034 y: Math.pow(1 - position, 2) * this.fromPoint.y +
9035 2 * position * (1 - position) * viaNode.y +
9036 Math.pow(position, 2) * this.toPoint.y,
9037 };
9038 }
9039 }
9040 /** @inheritDoc */
9041 _findBorderPosition(nearNode, ctx) {
9042 return this._findBorderPositionBezier(nearNode, ctx, this.via);
9043 }
9044 /** @inheritDoc */
9045 _getDistanceToEdge(x1, y1, x2, y2, x3, y3) {
9046 // x3,y3 is the point
9047 return this._getDistanceToBezierEdge(x1, y1, x2, y2, x3, y3, this.via);
9048 }
9049}
9050
9051/**
9052 * A Static Bezier Edge. Bezier curves are used to model smooth gradual curves in paths between nodes.
9053 */
9054class BezierEdgeStatic extends BezierEdgeBase {
9055 /**
9056 * Create a new instance.
9057 *
9058 * @param options - The options object of given edge.
9059 * @param body - The body of the network.
9060 * @param labelModule - Label module.
9061 */
9062 constructor(options, body, labelModule) {
9063 super(options, body, labelModule);
9064 }
9065 /** @inheritDoc */
9066 _line(ctx, values, viaNode) {
9067 this._bezierCurve(ctx, values, viaNode);
9068 }
9069 /** @inheritDoc */
9070 getViaNode() {
9071 return this._getViaCoordinates();
9072 }
9073 /**
9074 * Compute the coordinates of the via node.
9075 *
9076 * @remarks
9077 * We do not use the to and fromPoints here to make the via nodes the same as edges without arrows.
9078 *
9079 * @returns Cartesian coordinates of the via node.
9080 */
9081 _getViaCoordinates() {
9082 // Assumption: x/y coordinates in from/to always defined
9083 const factor = this.options.smooth.roundness;
9084 const type = this.options.smooth.type;
9085 let dx = Math.abs(this.from.x - this.to.x);
9086 let dy = Math.abs(this.from.y - this.to.y);
9087 if (type === "discrete" || type === "diagonalCross") {
9088 let stepX;
9089 let stepY;
9090 if (dx <= dy) {
9091 stepX = stepY = factor * dy;
9092 }
9093 else {
9094 stepX = stepY = factor * dx;
9095 }
9096 if (this.from.x > this.to.x) {
9097 stepX = -stepX;
9098 }
9099 if (this.from.y >= this.to.y) {
9100 stepY = -stepY;
9101 }
9102 let xVia = this.from.x + stepX;
9103 let yVia = this.from.y + stepY;
9104 if (type === "discrete") {
9105 if (dx <= dy) {
9106 xVia = dx < factor * dy ? this.from.x : xVia;
9107 }
9108 else {
9109 yVia = dy < factor * dx ? this.from.y : yVia;
9110 }
9111 }
9112 return { x: xVia, y: yVia };
9113 }
9114 else if (type === "straightCross") {
9115 let stepX = (1 - factor) * dx;
9116 let stepY = (1 - factor) * dy;
9117 if (dx <= dy) {
9118 // up - down
9119 stepX = 0;
9120 if (this.from.y < this.to.y) {
9121 stepY = -stepY;
9122 }
9123 }
9124 else {
9125 // left - right
9126 if (this.from.x < this.to.x) {
9127 stepX = -stepX;
9128 }
9129 stepY = 0;
9130 }
9131 return {
9132 x: this.to.x + stepX,
9133 y: this.to.y + stepY,
9134 };
9135 }
9136 else if (type === "horizontal") {
9137 let stepX = (1 - factor) * dx;
9138 if (this.from.x < this.to.x) {
9139 stepX = -stepX;
9140 }
9141 return {
9142 x: this.to.x + stepX,
9143 y: this.from.y,
9144 };
9145 }
9146 else if (type === "vertical") {
9147 let stepY = (1 - factor) * dy;
9148 if (this.from.y < this.to.y) {
9149 stepY = -stepY;
9150 }
9151 return {
9152 x: this.from.x,
9153 y: this.to.y + stepY,
9154 };
9155 }
9156 else if (type === "curvedCW") {
9157 dx = this.to.x - this.from.x;
9158 dy = this.from.y - this.to.y;
9159 const radius = Math.sqrt(dx * dx + dy * dy);
9160 const pi = Math.PI;
9161 const originalAngle = Math.atan2(dy, dx);
9162 const myAngle = (originalAngle + (factor * 0.5 + 0.5) * pi) % (2 * pi);
9163 return {
9164 x: this.from.x + (factor * 0.5 + 0.5) * radius * Math.sin(myAngle),
9165 y: this.from.y + (factor * 0.5 + 0.5) * radius * Math.cos(myAngle),
9166 };
9167 }
9168 else if (type === "curvedCCW") {
9169 dx = this.to.x - this.from.x;
9170 dy = this.from.y - this.to.y;
9171 const radius = Math.sqrt(dx * dx + dy * dy);
9172 const pi = Math.PI;
9173 const originalAngle = Math.atan2(dy, dx);
9174 const myAngle = (originalAngle + (-factor * 0.5 + 0.5) * pi) % (2 * pi);
9175 return {
9176 x: this.from.x + (factor * 0.5 + 0.5) * radius * Math.sin(myAngle),
9177 y: this.from.y + (factor * 0.5 + 0.5) * radius * Math.cos(myAngle),
9178 };
9179 }
9180 else {
9181 // continuous
9182 let stepX;
9183 let stepY;
9184 if (dx <= dy) {
9185 stepX = stepY = factor * dy;
9186 }
9187 else {
9188 stepX = stepY = factor * dx;
9189 }
9190 if (this.from.x > this.to.x) {
9191 stepX = -stepX;
9192 }
9193 if (this.from.y >= this.to.y) {
9194 stepY = -stepY;
9195 }
9196 let xVia = this.from.x + stepX;
9197 let yVia = this.from.y + stepY;
9198 if (dx <= dy) {
9199 if (this.from.x <= this.to.x) {
9200 xVia = this.to.x < xVia ? this.to.x : xVia;
9201 }
9202 else {
9203 xVia = this.to.x > xVia ? this.to.x : xVia;
9204 }
9205 }
9206 else {
9207 if (this.from.y >= this.to.y) {
9208 yVia = this.to.y > yVia ? this.to.y : yVia;
9209 }
9210 else {
9211 yVia = this.to.y < yVia ? this.to.y : yVia;
9212 }
9213 }
9214 return { x: xVia, y: yVia };
9215 }
9216 }
9217 /** @inheritDoc */
9218 _findBorderPosition(nearNode, ctx, options = {}) {
9219 return this._findBorderPositionBezier(nearNode, ctx, options.via);
9220 }
9221 /** @inheritDoc */
9222 _getDistanceToEdge(x1, y1, x2, y2, x3, y3, viaNode = this._getViaCoordinates()) {
9223 // x3,y3 is the point
9224 return this._getDistanceToBezierEdge(x1, y1, x2, y2, x3, y3, viaNode);
9225 }
9226 /** @inheritDoc */
9227 getPoint(position, viaNode = this._getViaCoordinates()) {
9228 const t = position;
9229 const x = Math.pow(1 - t, 2) * this.fromPoint.x +
9230 2 * t * (1 - t) * viaNode.x +
9231 Math.pow(t, 2) * this.toPoint.x;
9232 const y = Math.pow(1 - t, 2) * this.fromPoint.y +
9233 2 * t * (1 - t) * viaNode.y +
9234 Math.pow(t, 2) * this.toPoint.y;
9235 return { x: x, y: y };
9236 }
9237}
9238
9239/**
9240 * A Base Class for all Cubic Bezier Edges. Bezier curves are used to model
9241 * smooth gradual curves in paths between nodes.
9242 *
9243 * @augments BezierEdgeBase
9244 */
9245class CubicBezierEdgeBase extends BezierEdgeBase {
9246 /**
9247 * Create a new instance.
9248 *
9249 * @param options - The options object of given edge.
9250 * @param body - The body of the network.
9251 * @param labelModule - Label module.
9252 */
9253 constructor(options, body, labelModule) {
9254 super(options, body, labelModule);
9255 }
9256 /**
9257 * Calculate the distance between a point (x3,y3) and a line segment from (x1,y1) to (x2,y2).
9258 *
9259 * @remarks
9260 * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment
9261 * https://en.wikipedia.org/wiki/B%C3%A9zier_curve
9262 *
9263 * @param x1 - First end of the line segment on the x axis.
9264 * @param y1 - First end of the line segment on the y axis.
9265 * @param x2 - Second end of the line segment on the x axis.
9266 * @param y2 - Second end of the line segment on the y axis.
9267 * @param x3 - Position of the point on the x axis.
9268 * @param y3 - Position of the point on the y axis.
9269 * @param via1 - The first point this edge passes through.
9270 * @param via2 - The second point this edge passes through.
9271 *
9272 * @returns The distance between the line segment and the point.
9273 */
9274 _getDistanceToBezierEdge2(x1, y1, x2, y2, x3, y3, via1, via2) {
9275 // x3,y3 is the point
9276 let minDistance = 1e9;
9277 let lastX = x1;
9278 let lastY = y1;
9279 const vec = [0, 0, 0, 0];
9280 for (let i = 1; i < 10; i++) {
9281 const t = 0.1 * i;
9282 vec[0] = Math.pow(1 - t, 3);
9283 vec[1] = 3 * t * Math.pow(1 - t, 2);
9284 vec[2] = 3 * Math.pow(t, 2) * (1 - t);
9285 vec[3] = Math.pow(t, 3);
9286 const x = vec[0] * x1 + vec[1] * via1.x + vec[2] * via2.x + vec[3] * x2;
9287 const y = vec[0] * y1 + vec[1] * via1.y + vec[2] * via2.y + vec[3] * y2;
9288 if (i > 0) {
9289 const distance = this._getDistanceToLine(lastX, lastY, x, y, x3, y3);
9290 minDistance = distance < minDistance ? distance : minDistance;
9291 }
9292 lastX = x;
9293 lastY = y;
9294 }
9295 return minDistance;
9296 }
9297}
9298
9299/**
9300 * A Cubic Bezier Edge. Bezier curves are used to model smooth gradual curves in paths between nodes.
9301 */
9302class CubicBezierEdge extends CubicBezierEdgeBase {
9303 /**
9304 * Create a new instance.
9305 *
9306 * @param options - The options object of given edge.
9307 * @param body - The body of the network.
9308 * @param labelModule - Label module.
9309 */
9310 constructor(options, body, labelModule) {
9311 super(options, body, labelModule);
9312 }
9313 /** @inheritDoc */
9314 _line(ctx, values, viaNodes) {
9315 // get the coordinates of the support points.
9316 const via1 = viaNodes[0];
9317 const via2 = viaNodes[1];
9318 this._bezierCurve(ctx, values, via1, via2);
9319 }
9320 /**
9321 * Compute the additional points the edge passes through.
9322 *
9323 * @returns Cartesian coordinates of the points the edge passes through.
9324 */
9325 _getViaCoordinates() {
9326 const dx = this.from.x - this.to.x;
9327 const dy = this.from.y - this.to.y;
9328 let x1;
9329 let y1;
9330 let x2;
9331 let y2;
9332 const roundness = this.options.smooth.roundness;
9333 // horizontal if x > y or if direction is forced or if direction is horizontal
9334 if ((Math.abs(dx) > Math.abs(dy) ||
9335 this.options.smooth.forceDirection === true ||
9336 this.options.smooth.forceDirection === "horizontal") &&
9337 this.options.smooth.forceDirection !== "vertical") {
9338 y1 = this.from.y;
9339 y2 = this.to.y;
9340 x1 = this.from.x - roundness * dx;
9341 x2 = this.to.x + roundness * dx;
9342 }
9343 else {
9344 y1 = this.from.y - roundness * dy;
9345 y2 = this.to.y + roundness * dy;
9346 x1 = this.from.x;
9347 x2 = this.to.x;
9348 }
9349 return [
9350 { x: x1, y: y1 },
9351 { x: x2, y: y2 },
9352 ];
9353 }
9354 /** @inheritDoc */
9355 getViaNode() {
9356 return this._getViaCoordinates();
9357 }
9358 /** @inheritDoc */
9359 _findBorderPosition(nearNode, ctx) {
9360 return this._findBorderPositionBezier(nearNode, ctx);
9361 }
9362 /** @inheritDoc */
9363 _getDistanceToEdge(x1, y1, x2, y2, x3, y3, [via1, via2] = this._getViaCoordinates()) {
9364 // x3,y3 is the point
9365 return this._getDistanceToBezierEdge2(x1, y1, x2, y2, x3, y3, via1, via2);
9366 }
9367 /** @inheritDoc */
9368 getPoint(position, [via1, via2] = this._getViaCoordinates()) {
9369 const t = position;
9370 const vec = [
9371 Math.pow(1 - t, 3),
9372 3 * t * Math.pow(1 - t, 2),
9373 3 * Math.pow(t, 2) * (1 - t),
9374 Math.pow(t, 3),
9375 ];
9376 const x = vec[0] * this.fromPoint.x +
9377 vec[1] * via1.x +
9378 vec[2] * via2.x +
9379 vec[3] * this.toPoint.x;
9380 const y = vec[0] * this.fromPoint.y +
9381 vec[1] * via1.y +
9382 vec[2] * via2.y +
9383 vec[3] * this.toPoint.y;
9384 return { x: x, y: y };
9385 }
9386}
9387
9388/**
9389 * A Straight Edge.
9390 */
9391class StraightEdge extends EdgeBase {
9392 /**
9393 * Create a new instance.
9394 *
9395 * @param options - The options object of given edge.
9396 * @param body - The body of the network.
9397 * @param labelModule - Label module.
9398 */
9399 constructor(options, body, labelModule) {
9400 super(options, body, labelModule);
9401 }
9402 /** @inheritDoc */
9403 _line(ctx, values) {
9404 // draw a straight line
9405 ctx.beginPath();
9406 ctx.moveTo(this.fromPoint.x, this.fromPoint.y);
9407 ctx.lineTo(this.toPoint.x, this.toPoint.y);
9408 // draw shadow if enabled
9409 this.enableShadow(ctx, values);
9410 ctx.stroke();
9411 this.disableShadow(ctx, values);
9412 }
9413 /** @inheritDoc */
9414 getViaNode() {
9415 return undefined;
9416 }
9417 /** @inheritDoc */
9418 getPoint(position) {
9419 return {
9420 x: (1 - position) * this.fromPoint.x + position * this.toPoint.x,
9421 y: (1 - position) * this.fromPoint.y + position * this.toPoint.y,
9422 };
9423 }
9424 /** @inheritDoc */
9425 _findBorderPosition(nearNode, ctx) {
9426 let node1 = this.to;
9427 let node2 = this.from;
9428 if (nearNode.id === this.from.id) {
9429 node1 = this.from;
9430 node2 = this.to;
9431 }
9432 const angle = Math.atan2(node1.y - node2.y, node1.x - node2.x);
9433 const dx = node1.x - node2.x;
9434 const dy = node1.y - node2.y;
9435 const edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
9436 const toBorderDist = nearNode.distanceToBorder(ctx, angle);
9437 const toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength;
9438 return {
9439 x: (1 - toBorderPoint) * node2.x + toBorderPoint * node1.x,
9440 y: (1 - toBorderPoint) * node2.y + toBorderPoint * node1.y,
9441 t: 0,
9442 };
9443 }
9444 /** @inheritDoc */
9445 _getDistanceToEdge(x1, y1, x2, y2, x3, y3) {
9446 // x3,y3 is the point
9447 return this._getDistanceToLine(x1, y1, x2, y2, x3, y3);
9448 }
9449}
9450
9451/**
9452 * An edge connects two nodes and has a specific direction.
9453 */
9454class Edge {
9455 /**
9456 * @param {object} options values specific to this edge, must contain at least 'from' and 'to'
9457 * @param {object} body shared state from Network instance
9458 * @param {Network.Images} imagelist A list with images. Only needed when the edge has image arrows.
9459 * @param {object} globalOptions options from the EdgesHandler instance
9460 * @param {object} defaultOptions default options from the EdgeHandler instance. Value and reference are constant
9461 */
9462 constructor(options, body, imagelist, globalOptions, defaultOptions) {
9463 if (body === undefined) {
9464 throw new Error("No body provided");
9465 }
9466
9467 // Since globalOptions is constant in values as well as reference,
9468 // Following needs to be done only once.
9469
9470 this.options = bridgeObject(globalOptions);
9471 this.globalOptions = globalOptions;
9472 this.defaultOptions = defaultOptions;
9473 this.body = body;
9474 this.imagelist = imagelist;
9475
9476 // initialize variables
9477 this.id = undefined;
9478 this.fromId = undefined;
9479 this.toId = undefined;
9480 this.selected = false;
9481 this.hover = false;
9482 this.labelDirty = true;
9483
9484 this.baseWidth = this.options.width;
9485 this.baseFontSize = this.options.font.size;
9486
9487 this.from = undefined; // a node
9488 this.to = undefined; // a node
9489
9490 this.edgeType = undefined;
9491
9492 this.connected = false;
9493
9494 this.labelModule = new Label(
9495 this.body,
9496 this.options,
9497 true /* It's an edge label */
9498 );
9499 this.setOptions(options);
9500 }
9501
9502 /**
9503 * Set or overwrite options for the edge
9504 *
9505 * @param {object} options an object with options
9506 * @returns {undefined|boolean} undefined if no options, true if layout affecting data changed, false otherwise.
9507 */
9508 setOptions(options) {
9509 if (!options) {
9510 return;
9511 }
9512
9513 // Following options if changed affect the layout.
9514 let affectsLayout =
9515 (typeof options.physics !== "undefined" &&
9516 this.options.physics !== options.physics) ||
9517 (typeof options.hidden !== "undefined" &&
9518 (this.options.hidden || false) !== (options.hidden || false)) ||
9519 (typeof options.from !== "undefined" &&
9520 this.options.from !== options.from) ||
9521 (typeof options.to !== "undefined" && this.options.to !== options.to);
9522
9523 Edge.parseOptions(this.options, options, true, this.globalOptions);
9524
9525 if (options.id !== undefined) {
9526 this.id = options.id;
9527 }
9528 if (options.from !== undefined) {
9529 this.fromId = options.from;
9530 }
9531 if (options.to !== undefined) {
9532 this.toId = options.to;
9533 }
9534 if (options.title !== undefined) {
9535 this.title = options.title;
9536 }
9537 if (options.value !== undefined) {
9538 options.value = parseFloat(options.value);
9539 }
9540
9541 const pile = [options, this.options, this.defaultOptions];
9542 this.chooser = choosify("edge", pile);
9543
9544 // update label Module
9545 this.updateLabelModule(options);
9546
9547 // Update edge type, this if changed affects the layout.
9548 affectsLayout = this.updateEdgeType() || affectsLayout;
9549
9550 // if anything has been updates, reset the selection width and the hover width
9551 this._setInteractionWidths();
9552
9553 // A node is connected when it has a from and to node that both exist in the network.body.nodes.
9554 this.connect();
9555
9556 return affectsLayout;
9557 }
9558
9559 /**
9560 *
9561 * @param {object} parentOptions
9562 * @param {object} newOptions
9563 * @param {boolean} [allowDeletion=false]
9564 * @param {object} [globalOptions={}]
9565 * @param {boolean} [copyFromGlobals=false]
9566 */
9567 static parseOptions(
9568 parentOptions,
9569 newOptions,
9570 allowDeletion = false,
9571 globalOptions = {},
9572 copyFromGlobals = false
9573 ) {
9574 const fields = [
9575 "endPointOffset",
9576 "arrowStrikethrough",
9577 "id",
9578 "from",
9579 "hidden",
9580 "hoverWidth",
9581 "labelHighlightBold",
9582 "length",
9583 "line",
9584 "opacity",
9585 "physics",
9586 "scaling",
9587 "selectionWidth",
9588 "selfReferenceSize",
9589 "selfReference",
9590 "to",
9591 "title",
9592 "value",
9593 "width",
9594 "font",
9595 "chosen",
9596 "widthConstraint",
9597 ];
9598
9599 // only deep extend the items in the field array. These do not have shorthand.
9600 selectiveDeepExtend(fields, parentOptions, newOptions, allowDeletion);
9601
9602 // Only use endPointOffset values (from and to) if it's valid values
9603 if (
9604 newOptions.endPointOffset !== undefined &&
9605 newOptions.endPointOffset.from !== undefined
9606 ) {
9607 if (Number.isFinite(newOptions.endPointOffset.from)) {
9608 parentOptions.endPointOffset.from = newOptions.endPointOffset.from;
9609 } else {
9610 parentOptions.endPointOffset.from =
9611 globalOptions.endPointOffset.from !== undefined
9612 ? globalOptions.endPointOffset.from
9613 : 0;
9614 console.error("endPointOffset.from is not a valid number");
9615 }
9616 }
9617
9618 if (
9619 newOptions.endPointOffset !== undefined &&
9620 newOptions.endPointOffset.to !== undefined
9621 ) {
9622 if (Number.isFinite(newOptions.endPointOffset.to)) {
9623 parentOptions.endPointOffset.to = newOptions.endPointOffset.to;
9624 } else {
9625 parentOptions.endPointOffset.to =
9626 globalOptions.endPointOffset.to !== undefined
9627 ? globalOptions.endPointOffset.to
9628 : 0;
9629 console.error("endPointOffset.to is not a valid number");
9630 }
9631 }
9632
9633 // Only copy label if it's a legal value.
9634 if (isValidLabel(newOptions.label)) {
9635 parentOptions.label = newOptions.label;
9636 } else if (!isValidLabel(parentOptions.label)) {
9637 parentOptions.label = undefined;
9638 }
9639
9640 mergeOptions(parentOptions, newOptions, "smooth", globalOptions);
9641 mergeOptions(parentOptions, newOptions, "shadow", globalOptions);
9642 mergeOptions(parentOptions, newOptions, "background", globalOptions);
9643
9644 if (newOptions.dashes !== undefined && newOptions.dashes !== null) {
9645 parentOptions.dashes = newOptions.dashes;
9646 } else if (allowDeletion === true && newOptions.dashes === null) {
9647 parentOptions.dashes = Object.create(globalOptions.dashes); // this sets the pointer of the option back to the global option.
9648 }
9649
9650 // set the scaling newOptions
9651 if (newOptions.scaling !== undefined && newOptions.scaling !== null) {
9652 if (newOptions.scaling.min !== undefined) {
9653 parentOptions.scaling.min = newOptions.scaling.min;
9654 }
9655 if (newOptions.scaling.max !== undefined) {
9656 parentOptions.scaling.max = newOptions.scaling.max;
9657 }
9658 mergeOptions(
9659 parentOptions.scaling,
9660 newOptions.scaling,
9661 "label",
9662 globalOptions.scaling
9663 );
9664 } else if (allowDeletion === true && newOptions.scaling === null) {
9665 parentOptions.scaling = Object.create(globalOptions.scaling); // this sets the pointer of the option back to the global option.
9666 }
9667
9668 // handle multiple input cases for arrows
9669 if (newOptions.arrows !== undefined && newOptions.arrows !== null) {
9670 if (typeof newOptions.arrows === "string") {
9671 const arrows = newOptions.arrows.toLowerCase();
9672 parentOptions.arrows.to.enabled = arrows.indexOf("to") != -1;
9673 parentOptions.arrows.middle.enabled = arrows.indexOf("middle") != -1;
9674 parentOptions.arrows.from.enabled = arrows.indexOf("from") != -1;
9675 } else if (typeof newOptions.arrows === "object") {
9676 mergeOptions(
9677 parentOptions.arrows,
9678 newOptions.arrows,
9679 "to",
9680 globalOptions.arrows
9681 );
9682 mergeOptions(
9683 parentOptions.arrows,
9684 newOptions.arrows,
9685 "middle",
9686 globalOptions.arrows
9687 );
9688 mergeOptions(
9689 parentOptions.arrows,
9690 newOptions.arrows,
9691 "from",
9692 globalOptions.arrows
9693 );
9694 } else {
9695 throw new Error(
9696 "The arrow newOptions can only be an object or a string. Refer to the documentation. You used:" +
9697 JSON.stringify(newOptions.arrows)
9698 );
9699 }
9700 } else if (allowDeletion === true && newOptions.arrows === null) {
9701 parentOptions.arrows = Object.create(globalOptions.arrows); // this sets the pointer of the option back to the global option.
9702 }
9703
9704 // handle multiple input cases for color
9705 if (newOptions.color !== undefined && newOptions.color !== null) {
9706 const fromColor = isString(newOptions.color)
9707 ? {
9708 color: newOptions.color,
9709 highlight: newOptions.color,
9710 hover: newOptions.color,
9711 inherit: false,
9712 opacity: 1,
9713 }
9714 : newOptions.color;
9715 const toColor = parentOptions.color;
9716
9717 // If passed, fill in values from default options - required in the case of no prototype bridging
9718 if (copyFromGlobals) {
9719 deepExtend(toColor, globalOptions.color, false, allowDeletion);
9720 } else {
9721 // Clear local properties - need to do it like this in order to retain prototype bridges
9722 for (const i in toColor) {
9723 if (Object.prototype.hasOwnProperty.call(toColor, i)) {
9724 delete toColor[i];
9725 }
9726 }
9727 }
9728
9729 if (isString(toColor)) {
9730 toColor.color = toColor;
9731 toColor.highlight = toColor;
9732 toColor.hover = toColor;
9733 toColor.inherit = false;
9734 if (fromColor.opacity === undefined) {
9735 toColor.opacity = 1.0; // set default
9736 }
9737 } else {
9738 let colorsDefined = false;
9739 if (fromColor.color !== undefined) {
9740 toColor.color = fromColor.color;
9741 colorsDefined = true;
9742 }
9743 if (fromColor.highlight !== undefined) {
9744 toColor.highlight = fromColor.highlight;
9745 colorsDefined = true;
9746 }
9747 if (fromColor.hover !== undefined) {
9748 toColor.hover = fromColor.hover;
9749 colorsDefined = true;
9750 }
9751 if (fromColor.inherit !== undefined) {
9752 toColor.inherit = fromColor.inherit;
9753 }
9754 if (fromColor.opacity !== undefined) {
9755 toColor.opacity = Math.min(1, Math.max(0, fromColor.opacity));
9756 }
9757
9758 if (colorsDefined === true) {
9759 toColor.inherit = false;
9760 } else {
9761 if (toColor.inherit === undefined) {
9762 toColor.inherit = "from"; // Set default
9763 }
9764 }
9765 }
9766 } else if (allowDeletion === true && newOptions.color === null) {
9767 parentOptions.color = bridgeObject(globalOptions.color); // set the object back to the global options
9768 }
9769
9770 if (allowDeletion === true && newOptions.font === null) {
9771 parentOptions.font = bridgeObject(globalOptions.font); // set the object back to the global options
9772 }
9773
9774 if (Object.prototype.hasOwnProperty.call(newOptions, "selfReferenceSize")) {
9775 console.warn(
9776 "The selfReferenceSize property has been deprecated. Please use selfReference property instead. The selfReference can be set like thise selfReference:{size:30, angle:Math.PI / 4}"
9777 );
9778 parentOptions.selfReference.size = newOptions.selfReferenceSize;
9779 }
9780 }
9781
9782 /**
9783 *
9784 * @returns {ArrowOptions}
9785 */
9786 getFormattingValues() {
9787 const toArrow =
9788 this.options.arrows.to === true ||
9789 this.options.arrows.to.enabled === true;
9790 const fromArrow =
9791 this.options.arrows.from === true ||
9792 this.options.arrows.from.enabled === true;
9793 const middleArrow =
9794 this.options.arrows.middle === true ||
9795 this.options.arrows.middle.enabled === true;
9796 const inheritsColor = this.options.color.inherit;
9797 const values = {
9798 toArrow: toArrow,
9799 toArrowScale: this.options.arrows.to.scaleFactor,
9800 toArrowType: this.options.arrows.to.type,
9801 toArrowSrc: this.options.arrows.to.src,
9802 toArrowImageWidth: this.options.arrows.to.imageWidth,
9803 toArrowImageHeight: this.options.arrows.to.imageHeight,
9804 middleArrow: middleArrow,
9805 middleArrowScale: this.options.arrows.middle.scaleFactor,
9806 middleArrowType: this.options.arrows.middle.type,
9807 middleArrowSrc: this.options.arrows.middle.src,
9808 middleArrowImageWidth: this.options.arrows.middle.imageWidth,
9809 middleArrowImageHeight: this.options.arrows.middle.imageHeight,
9810 fromArrow: fromArrow,
9811 fromArrowScale: this.options.arrows.from.scaleFactor,
9812 fromArrowType: this.options.arrows.from.type,
9813 fromArrowSrc: this.options.arrows.from.src,
9814 fromArrowImageWidth: this.options.arrows.from.imageWidth,
9815 fromArrowImageHeight: this.options.arrows.from.imageHeight,
9816 arrowStrikethrough: this.options.arrowStrikethrough,
9817 color: inheritsColor ? undefined : this.options.color.color,
9818 inheritsColor: inheritsColor,
9819 opacity: this.options.color.opacity,
9820 hidden: this.options.hidden,
9821 length: this.options.length,
9822 shadow: this.options.shadow.enabled,
9823 shadowColor: this.options.shadow.color,
9824 shadowSize: this.options.shadow.size,
9825 shadowX: this.options.shadow.x,
9826 shadowY: this.options.shadow.y,
9827 dashes: this.options.dashes,
9828 width: this.options.width,
9829 background: this.options.background.enabled,
9830 backgroundColor: this.options.background.color,
9831 backgroundSize: this.options.background.size,
9832 backgroundDashes: this.options.background.dashes,
9833 };
9834 if (this.selected || this.hover) {
9835 if (this.chooser === true) {
9836 if (this.selected) {
9837 const selectedWidth = this.options.selectionWidth;
9838 if (typeof selectedWidth === "function") {
9839 values.width = selectedWidth(values.width);
9840 } else if (typeof selectedWidth === "number") {
9841 values.width += selectedWidth;
9842 }
9843 values.width = Math.max(values.width, 0.3 / this.body.view.scale);
9844 values.color = this.options.color.highlight;
9845 values.shadow = this.options.shadow.enabled;
9846 } else if (this.hover) {
9847 const hoverWidth = this.options.hoverWidth;
9848 if (typeof hoverWidth === "function") {
9849 values.width = hoverWidth(values.width);
9850 } else if (typeof hoverWidth === "number") {
9851 values.width += hoverWidth;
9852 }
9853 values.width = Math.max(values.width, 0.3 / this.body.view.scale);
9854 values.color = this.options.color.hover;
9855 values.shadow = this.options.shadow.enabled;
9856 }
9857 } else if (typeof this.chooser === "function") {
9858 this.chooser(values, this.options.id, this.selected, this.hover);
9859 if (values.color !== undefined) {
9860 values.inheritsColor = false;
9861 }
9862 if (values.shadow === false) {
9863 if (
9864 values.shadowColor !== this.options.shadow.color ||
9865 values.shadowSize !== this.options.shadow.size ||
9866 values.shadowX !== this.options.shadow.x ||
9867 values.shadowY !== this.options.shadow.y
9868 ) {
9869 values.shadow = true;
9870 }
9871 }
9872 }
9873 } else {
9874 values.shadow = this.options.shadow.enabled;
9875 values.width = Math.max(values.width, 0.3 / this.body.view.scale);
9876 }
9877 return values;
9878 }
9879
9880 /**
9881 * update the options in the label module
9882 *
9883 * @param {object} options
9884 */
9885 updateLabelModule(options) {
9886 const pile = [
9887 options,
9888 this.options,
9889 this.globalOptions, // Currently set global edge options
9890 this.defaultOptions,
9891 ];
9892
9893 this.labelModule.update(this.options, pile);
9894
9895 if (this.labelModule.baseSize !== undefined) {
9896 this.baseFontSize = this.labelModule.baseSize;
9897 }
9898 }
9899
9900 /**
9901 * update the edge type, set the options
9902 *
9903 * @returns {boolean}
9904 */
9905 updateEdgeType() {
9906 const smooth = this.options.smooth;
9907 let dataChanged = false;
9908 let changeInType = true;
9909 if (this.edgeType !== undefined) {
9910 if (
9911 (this.edgeType instanceof BezierEdgeDynamic &&
9912 smooth.enabled === true &&
9913 smooth.type === "dynamic") ||
9914 (this.edgeType instanceof CubicBezierEdge &&
9915 smooth.enabled === true &&
9916 smooth.type === "cubicBezier") ||
9917 (this.edgeType instanceof BezierEdgeStatic &&
9918 smooth.enabled === true &&
9919 smooth.type !== "dynamic" &&
9920 smooth.type !== "cubicBezier") ||
9921 (this.edgeType instanceof StraightEdge && smooth.type.enabled === false)
9922 ) {
9923 changeInType = false;
9924 }
9925 if (changeInType === true) {
9926 dataChanged = this.cleanup();
9927 }
9928 }
9929 if (changeInType === true) {
9930 if (smooth.enabled === true) {
9931 if (smooth.type === "dynamic") {
9932 dataChanged = true;
9933 this.edgeType = new BezierEdgeDynamic(
9934 this.options,
9935 this.body,
9936 this.labelModule
9937 );
9938 } else if (smooth.type === "cubicBezier") {
9939 this.edgeType = new CubicBezierEdge(
9940 this.options,
9941 this.body,
9942 this.labelModule
9943 );
9944 } else {
9945 this.edgeType = new BezierEdgeStatic(
9946 this.options,
9947 this.body,
9948 this.labelModule
9949 );
9950 }
9951 } else {
9952 this.edgeType = new StraightEdge(
9953 this.options,
9954 this.body,
9955 this.labelModule
9956 );
9957 }
9958 } else {
9959 // if nothing changes, we just set the options.
9960 this.edgeType.setOptions(this.options);
9961 }
9962 return dataChanged;
9963 }
9964
9965 /**
9966 * Connect an edge to its nodes
9967 */
9968 connect() {
9969 this.disconnect();
9970
9971 this.from = this.body.nodes[this.fromId] || undefined;
9972 this.to = this.body.nodes[this.toId] || undefined;
9973 this.connected = this.from !== undefined && this.to !== undefined;
9974
9975 if (this.connected === true) {
9976 this.from.attachEdge(this);
9977 this.to.attachEdge(this);
9978 } else {
9979 if (this.from) {
9980 this.from.detachEdge(this);
9981 }
9982 if (this.to) {
9983 this.to.detachEdge(this);
9984 }
9985 }
9986
9987 this.edgeType.connect();
9988 }
9989
9990 /**
9991 * Disconnect an edge from its nodes
9992 */
9993 disconnect() {
9994 if (this.from) {
9995 this.from.detachEdge(this);
9996 this.from = undefined;
9997 }
9998 if (this.to) {
9999 this.to.detachEdge(this);
10000 this.to = undefined;
10001 }
10002
10003 this.connected = false;
10004 }
10005
10006 /**
10007 * get the title of this edge.
10008 *
10009 * @returns {string} title The title of the edge, or undefined when no title
10010 * has been set.
10011 */
10012 getTitle() {
10013 return this.title;
10014 }
10015
10016 /**
10017 * check if this node is selecte
10018 *
10019 * @returns {boolean} selected True if node is selected, else false
10020 */
10021 isSelected() {
10022 return this.selected;
10023 }
10024
10025 /**
10026 * Retrieve the value of the edge. Can be undefined
10027 *
10028 * @returns {number} value
10029 */
10030 getValue() {
10031 return this.options.value;
10032 }
10033
10034 /**
10035 * Adjust the value range of the edge. The edge will adjust it's width
10036 * based on its value.
10037 *
10038 * @param {number} min
10039 * @param {number} max
10040 * @param {number} total
10041 */
10042 setValueRange(min, max, total) {
10043 if (this.options.value !== undefined) {
10044 const scale = this.options.scaling.customScalingFunction(
10045 min,
10046 max,
10047 total,
10048 this.options.value
10049 );
10050 const widthDiff = this.options.scaling.max - this.options.scaling.min;
10051 if (this.options.scaling.label.enabled === true) {
10052 const fontDiff =
10053 this.options.scaling.label.max - this.options.scaling.label.min;
10054 this.options.font.size =
10055 this.options.scaling.label.min + scale * fontDiff;
10056 }
10057 this.options.width = this.options.scaling.min + scale * widthDiff;
10058 } else {
10059 this.options.width = this.baseWidth;
10060 this.options.font.size = this.baseFontSize;
10061 }
10062
10063 this._setInteractionWidths();
10064 this.updateLabelModule();
10065 }
10066
10067 /**
10068 *
10069 * @private
10070 */
10071 _setInteractionWidths() {
10072 if (typeof this.options.hoverWidth === "function") {
10073 this.edgeType.hoverWidth = this.options.hoverWidth(this.options.width);
10074 } else {
10075 this.edgeType.hoverWidth = this.options.hoverWidth + this.options.width;
10076 }
10077 if (typeof this.options.selectionWidth === "function") {
10078 this.edgeType.selectionWidth = this.options.selectionWidth(
10079 this.options.width
10080 );
10081 } else {
10082 this.edgeType.selectionWidth =
10083 this.options.selectionWidth + this.options.width;
10084 }
10085 }
10086
10087 /**
10088 * Redraw a edge
10089 * Draw this edge in the given canvas
10090 * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
10091 *
10092 * @param {CanvasRenderingContext2D} ctx
10093 */
10094 draw(ctx) {
10095 const values = this.getFormattingValues();
10096 if (values.hidden) {
10097 return;
10098 }
10099
10100 // get the via node from the edge type
10101 const viaNode = this.edgeType.getViaNode();
10102
10103 // draw line and label
10104 this.edgeType.drawLine(ctx, values, this.selected, this.hover, viaNode);
10105 this.drawLabel(ctx, viaNode);
10106 }
10107
10108 /**
10109 * Redraw arrows
10110 * Draw this arrows in the given canvas
10111 * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
10112 *
10113 * @param {CanvasRenderingContext2D} ctx
10114 */
10115 drawArrows(ctx) {
10116 const values = this.getFormattingValues();
10117 if (values.hidden) {
10118 return;
10119 }
10120
10121 // get the via node from the edge type
10122 const viaNode = this.edgeType.getViaNode();
10123 const arrowData = {};
10124
10125 // restore edge targets to defaults
10126 this.edgeType.fromPoint = this.edgeType.from;
10127 this.edgeType.toPoint = this.edgeType.to;
10128
10129 // from and to arrows give a different end point for edges. we set them here
10130 if (values.fromArrow) {
10131 arrowData.from = this.edgeType.getArrowData(
10132 ctx,
10133 "from",
10134 viaNode,
10135 this.selected,
10136 this.hover,
10137 values
10138 );
10139 if (values.arrowStrikethrough === false)
10140 this.edgeType.fromPoint = arrowData.from.core;
10141 if (values.fromArrowSrc) {
10142 arrowData.from.image = this.imagelist.load(values.fromArrowSrc);
10143 }
10144 if (values.fromArrowImageWidth) {
10145 arrowData.from.imageWidth = values.fromArrowImageWidth;
10146 }
10147 if (values.fromArrowImageHeight) {
10148 arrowData.from.imageHeight = values.fromArrowImageHeight;
10149 }
10150 }
10151 if (values.toArrow) {
10152 arrowData.to = this.edgeType.getArrowData(
10153 ctx,
10154 "to",
10155 viaNode,
10156 this.selected,
10157 this.hover,
10158 values
10159 );
10160 if (values.arrowStrikethrough === false)
10161 this.edgeType.toPoint = arrowData.to.core;
10162 if (values.toArrowSrc) {
10163 arrowData.to.image = this.imagelist.load(values.toArrowSrc);
10164 }
10165 if (values.toArrowImageWidth) {
10166 arrowData.to.imageWidth = values.toArrowImageWidth;
10167 }
10168 if (values.toArrowImageHeight) {
10169 arrowData.to.imageHeight = values.toArrowImageHeight;
10170 }
10171 }
10172
10173 // the middle arrow depends on the line, which can depend on the to and from arrows so we do this one lastly.
10174 if (values.middleArrow) {
10175 arrowData.middle = this.edgeType.getArrowData(
10176 ctx,
10177 "middle",
10178 viaNode,
10179 this.selected,
10180 this.hover,
10181 values
10182 );
10183
10184 if (values.middleArrowSrc) {
10185 arrowData.middle.image = this.imagelist.load(values.middleArrowSrc);
10186 }
10187 if (values.middleArrowImageWidth) {
10188 arrowData.middle.imageWidth = values.middleArrowImageWidth;
10189 }
10190 if (values.middleArrowImageHeight) {
10191 arrowData.middle.imageHeight = values.middleArrowImageHeight;
10192 }
10193 }
10194
10195 if (values.fromArrow) {
10196 this.edgeType.drawArrowHead(
10197 ctx,
10198 values,
10199 this.selected,
10200 this.hover,
10201 arrowData.from
10202 );
10203 }
10204 if (values.middleArrow) {
10205 this.edgeType.drawArrowHead(
10206 ctx,
10207 values,
10208 this.selected,
10209 this.hover,
10210 arrowData.middle
10211 );
10212 }
10213 if (values.toArrow) {
10214 this.edgeType.drawArrowHead(
10215 ctx,
10216 values,
10217 this.selected,
10218 this.hover,
10219 arrowData.to
10220 );
10221 }
10222 }
10223
10224 /**
10225 *
10226 * @param {CanvasRenderingContext2D} ctx
10227 * @param {Node} viaNode
10228 */
10229 drawLabel(ctx, viaNode) {
10230 if (this.options.label !== undefined) {
10231 // set style
10232 const node1 = this.from;
10233 const node2 = this.to;
10234
10235 if (this.labelModule.differentState(this.selected, this.hover)) {
10236 this.labelModule.getTextSize(ctx, this.selected, this.hover);
10237 }
10238
10239 let point;
10240 if (node1.id != node2.id) {
10241 this.labelModule.pointToSelf = false;
10242 point = this.edgeType.getPoint(0.5, viaNode);
10243 ctx.save();
10244
10245 const rotationPoint = this._getRotation(ctx);
10246 if (rotationPoint.angle != 0) {
10247 ctx.translate(rotationPoint.x, rotationPoint.y);
10248 ctx.rotate(rotationPoint.angle);
10249 }
10250
10251 // draw the label
10252 this.labelModule.draw(ctx, point.x, point.y, this.selected, this.hover);
10253
10254 /*
10255 // Useful debug code: draw a border around the label
10256 // This should **not** be enabled in production!
10257 var size = this.labelModule.getSize();; // ;; intentional so lint catches it
10258 ctx.strokeStyle = "#ff0000";
10259 ctx.strokeRect(size.left, size.top, size.width, size.height);
10260 // End debug code
10261*/
10262
10263 ctx.restore();
10264 } else {
10265 // Ignore the orientations.
10266 this.labelModule.pointToSelf = true;
10267
10268 // get circle coordinates
10269 const coordinates = getSelfRefCoordinates(
10270 ctx,
10271 this.options.selfReference.angle,
10272 this.options.selfReference.size,
10273 node1
10274 );
10275
10276 point = this._pointOnCircle(
10277 coordinates.x,
10278 coordinates.y,
10279 this.options.selfReference.size,
10280 this.options.selfReference.angle
10281 );
10282
10283 this.labelModule.draw(ctx, point.x, point.y, this.selected, this.hover);
10284 }
10285 }
10286 }
10287
10288 /**
10289 * Determine all visual elements of this edge instance, in which the given
10290 * point falls within the bounding shape.
10291 *
10292 * @param {point} point
10293 * @returns {Array.<edgeClickItem|edgeLabelClickItem>} list with the items which are on the point
10294 */
10295 getItemsOnPoint(point) {
10296 const ret = [];
10297
10298 if (this.labelModule.visible()) {
10299 const rotationPoint = this._getRotation();
10300 if (pointInRect(this.labelModule.getSize(), point, rotationPoint)) {
10301 ret.push({ edgeId: this.id, labelId: 0 });
10302 }
10303 }
10304
10305 const obj = {
10306 left: point.x,
10307 top: point.y,
10308 };
10309
10310 if (this.isOverlappingWith(obj)) {
10311 ret.push({ edgeId: this.id });
10312 }
10313
10314 return ret;
10315 }
10316
10317 /**
10318 * Check if this object is overlapping with the provided object
10319 *
10320 * @param {object} obj an object with parameters left, top
10321 * @returns {boolean} True if location is located on the edge
10322 */
10323 isOverlappingWith(obj) {
10324 if (this.connected) {
10325 const distMax = 10;
10326 const xFrom = this.from.x;
10327 const yFrom = this.from.y;
10328 const xTo = this.to.x;
10329 const yTo = this.to.y;
10330 const xObj = obj.left;
10331 const yObj = obj.top;
10332
10333 const dist = this.edgeType.getDistanceToEdge(
10334 xFrom,
10335 yFrom,
10336 xTo,
10337 yTo,
10338 xObj,
10339 yObj
10340 );
10341
10342 return dist < distMax;
10343 } else {
10344 return false;
10345 }
10346 }
10347
10348 /**
10349 * Determine the rotation point, if any.
10350 *
10351 * @param {CanvasRenderingContext2D} [ctx] if passed, do a recalculation of the label size
10352 * @returns {rotationPoint} the point to rotate around and the angle in radians to rotate
10353 * @private
10354 */
10355 _getRotation(ctx) {
10356 const viaNode = this.edgeType.getViaNode();
10357 const point = this.edgeType.getPoint(0.5, viaNode);
10358
10359 if (ctx !== undefined) {
10360 this.labelModule.calculateLabelSize(
10361 ctx,
10362 this.selected,
10363 this.hover,
10364 point.x,
10365 point.y
10366 );
10367 }
10368
10369 const ret = {
10370 x: point.x,
10371 y: this.labelModule.size.yLine,
10372 angle: 0,
10373 };
10374
10375 if (!this.labelModule.visible()) {
10376 return ret; // Don't even bother doing the atan2, there's nothing to draw
10377 }
10378
10379 if (this.options.font.align === "horizontal") {
10380 return ret; // No need to calculate angle
10381 }
10382
10383 const dy = this.from.y - this.to.y;
10384 const dx = this.from.x - this.to.x;
10385 let angle = Math.atan2(dy, dx); // radians
10386
10387 // rotate so that label is readable
10388 if ((angle < -1 && dx < 0) || (angle > 0 && dx < 0)) {
10389 angle += Math.PI;
10390 }
10391 ret.angle = angle;
10392
10393 return ret;
10394 }
10395
10396 /**
10397 * Get a point on a circle
10398 *
10399 * @param {number} x
10400 * @param {number} y
10401 * @param {number} radius
10402 * @param {number} angle
10403 * @returns {object} point
10404 * @private
10405 */
10406 _pointOnCircle(x, y, radius, angle) {
10407 return {
10408 x: x + radius * Math.cos(angle),
10409 y: y - radius * Math.sin(angle),
10410 };
10411 }
10412
10413 /**
10414 * Sets selected state to true
10415 */
10416 select() {
10417 this.selected = true;
10418 }
10419
10420 /**
10421 * Sets selected state to false
10422 */
10423 unselect() {
10424 this.selected = false;
10425 }
10426
10427 /**
10428 * cleans all required things on delete
10429 *
10430 * @returns {*}
10431 */
10432 cleanup() {
10433 return this.edgeType.cleanup();
10434 }
10435
10436 /**
10437 * Remove edge from the list and perform necessary cleanup.
10438 */
10439 remove() {
10440 this.cleanup();
10441 this.disconnect();
10442 delete this.body.edges[this.id];
10443 }
10444
10445 /**
10446 * Check if both connecting nodes exist
10447 *
10448 * @returns {boolean}
10449 */
10450 endPointsValid() {
10451 return (
10452 this.body.nodes[this.fromId] !== undefined &&
10453 this.body.nodes[this.toId] !== undefined
10454 );
10455 }
10456}
10457
10458/**
10459 * Handler for Edges
10460 */
10461class EdgesHandler {
10462 /**
10463 * @param {object} body
10464 * @param {Array.<Image>} images
10465 * @param {Array.<Group>} groups
10466 */
10467 constructor(body, images, groups) {
10468 this.body = body;
10469 this.images = images;
10470 this.groups = groups;
10471
10472 // create the edge API in the body container
10473 this.body.functions.createEdge = this.create.bind(this);
10474
10475 this.edgesListeners = {
10476 add: (event, params) => {
10477 this.add(params.items);
10478 },
10479 update: (event, params) => {
10480 this.update(params.items);
10481 },
10482 remove: (event, params) => {
10483 this.remove(params.items);
10484 },
10485 };
10486
10487 this.options = {};
10488 this.defaultOptions = {
10489 arrows: {
10490 to: { enabled: false, scaleFactor: 1, type: "arrow" }, // boolean / {arrowScaleFactor:1} / {enabled: false, arrowScaleFactor:1}
10491 middle: { enabled: false, scaleFactor: 1, type: "arrow" },
10492 from: { enabled: false, scaleFactor: 1, type: "arrow" },
10493 },
10494 endPointOffset: {
10495 from: 0,
10496 to: 0,
10497 },
10498 arrowStrikethrough: true,
10499 color: {
10500 color: "#848484",
10501 highlight: "#848484",
10502 hover: "#848484",
10503 inherit: "from",
10504 opacity: 1.0,
10505 },
10506 dashes: false,
10507 font: {
10508 color: "#343434",
10509 size: 14, // px
10510 face: "arial",
10511 background: "none",
10512 strokeWidth: 2, // px
10513 strokeColor: "#ffffff",
10514 align: "horizontal",
10515 multi: false,
10516 vadjust: 0,
10517 bold: {
10518 mod: "bold",
10519 },
10520 boldital: {
10521 mod: "bold italic",
10522 },
10523 ital: {
10524 mod: "italic",
10525 },
10526 mono: {
10527 mod: "",
10528 size: 15, // px
10529 face: "courier new",
10530 vadjust: 2,
10531 },
10532 },
10533 hidden: false,
10534 hoverWidth: 1.5,
10535 label: undefined,
10536 labelHighlightBold: true,
10537 length: undefined,
10538 physics: true,
10539 scaling: {
10540 min: 1,
10541 max: 15,
10542 label: {
10543 enabled: true,
10544 min: 14,
10545 max: 30,
10546 maxVisible: 30,
10547 drawThreshold: 5,
10548 },
10549 customScalingFunction: function (min, max, total, value) {
10550 if (max === min) {
10551 return 0.5;
10552 } else {
10553 const scale = 1 / (max - min);
10554 return Math.max(0, (value - min) * scale);
10555 }
10556 },
10557 },
10558 selectionWidth: 1.5,
10559 selfReference: {
10560 size: 20,
10561 angle: Math.PI / 4,
10562 renderBehindTheNode: true,
10563 },
10564 shadow: {
10565 enabled: false,
10566 color: "rgba(0,0,0,0.5)",
10567 size: 10,
10568 x: 5,
10569 y: 5,
10570 },
10571 background: {
10572 enabled: false,
10573 color: "rgba(111,111,111,1)",
10574 size: 10,
10575 dashes: false,
10576 },
10577 smooth: {
10578 enabled: true,
10579 type: "dynamic",
10580 forceDirection: "none",
10581 roundness: 0.5,
10582 },
10583 title: undefined,
10584 width: 1,
10585 value: undefined,
10586 };
10587
10588 deepExtend(this.options, this.defaultOptions);
10589
10590 this.bindEventListeners();
10591 }
10592
10593 /**
10594 * Binds event listeners
10595 */
10596 bindEventListeners() {
10597 // this allows external modules to force all dynamic curves to turn static.
10598 this.body.emitter.on("_forceDisableDynamicCurves", (type, emit = true) => {
10599 if (type === "dynamic") {
10600 type = "continuous";
10601 }
10602 let dataChanged = false;
10603 for (const edgeId in this.body.edges) {
10604 if (Object.prototype.hasOwnProperty.call(this.body.edges, edgeId)) {
10605 const edge = this.body.edges[edgeId];
10606 const edgeData = this.body.data.edges.get(edgeId);
10607
10608 // only forcibly remove the smooth curve if the data has been set of the edge has the smooth curves defined.
10609 // this is because a change in the global would not affect these curves.
10610 if (edgeData != null) {
10611 const smoothOptions = edgeData.smooth;
10612 if (smoothOptions !== undefined) {
10613 if (
10614 smoothOptions.enabled === true &&
10615 smoothOptions.type === "dynamic"
10616 ) {
10617 if (type === undefined) {
10618 edge.setOptions({ smooth: false });
10619 } else {
10620 edge.setOptions({ smooth: { type: type } });
10621 }
10622 dataChanged = true;
10623 }
10624 }
10625 }
10626 }
10627 }
10628 if (emit === true && dataChanged === true) {
10629 this.body.emitter.emit("_dataChanged");
10630 }
10631 });
10632
10633 // this is called when options of EXISTING nodes or edges have changed.
10634 //
10635 // NOTE: Not true, called when options have NOT changed, for both existing as well as new nodes.
10636 // See update() for logic.
10637 // TODO: Verify and examine the consequences of this. It might still trigger when
10638 // non-option fields have changed, but then reconnecting edges is still useless.
10639 // Alternatively, it might also be called when edges are removed.
10640 //
10641 this.body.emitter.on("_dataUpdated", () => {
10642 this.reconnectEdges();
10643 });
10644
10645 // refresh the edges. Used when reverting from hierarchical layout
10646 this.body.emitter.on("refreshEdges", this.refresh.bind(this));
10647 this.body.emitter.on("refresh", this.refresh.bind(this));
10648 this.body.emitter.on("destroy", () => {
10649 forEach(this.edgesListeners, (callback, event) => {
10650 if (this.body.data.edges) this.body.data.edges.off(event, callback);
10651 });
10652 delete this.body.functions.createEdge;
10653 delete this.edgesListeners.add;
10654 delete this.edgesListeners.update;
10655 delete this.edgesListeners.remove;
10656 delete this.edgesListeners;
10657 });
10658 }
10659
10660 /**
10661 *
10662 * @param {object} options
10663 */
10664 setOptions(options) {
10665 if (options !== undefined) {
10666 // use the parser from the Edge class to fill in all shorthand notations
10667 Edge.parseOptions(this.options, options, true, this.defaultOptions, true);
10668
10669 // update smooth settings in all edges
10670 let dataChanged = false;
10671 if (options.smooth !== undefined) {
10672 for (const edgeId in this.body.edges) {
10673 if (Object.prototype.hasOwnProperty.call(this.body.edges, edgeId)) {
10674 dataChanged =
10675 this.body.edges[edgeId].updateEdgeType() || dataChanged;
10676 }
10677 }
10678 }
10679
10680 // update fonts in all edges
10681 if (options.font !== undefined) {
10682 for (const edgeId in this.body.edges) {
10683 if (Object.prototype.hasOwnProperty.call(this.body.edges, edgeId)) {
10684 this.body.edges[edgeId].updateLabelModule();
10685 }
10686 }
10687 }
10688
10689 // update the state of the variables if needed
10690 if (
10691 options.hidden !== undefined ||
10692 options.physics !== undefined ||
10693 dataChanged === true
10694 ) {
10695 this.body.emitter.emit("_dataChanged");
10696 }
10697 }
10698 }
10699
10700 /**
10701 * Load edges by reading the data table
10702 *
10703 * @param {Array | DataSet | DataView} edges The data containing the edges.
10704 * @param {boolean} [doNotEmit=false] - Suppress data changed event.
10705 * @private
10706 */
10707 setData(edges, doNotEmit = false) {
10708 const oldEdgesData = this.body.data.edges;
10709
10710 if (isDataViewLike("id", edges)) {
10711 this.body.data.edges = edges;
10712 } else if (Array.isArray(edges)) {
10713 this.body.data.edges = new DataSet();
10714 this.body.data.edges.add(edges);
10715 } else if (!edges) {
10716 this.body.data.edges = new DataSet();
10717 } else {
10718 throw new TypeError("Array or DataSet expected");
10719 }
10720
10721 // TODO: is this null or undefined or false?
10722 if (oldEdgesData) {
10723 // unsubscribe from old dataset
10724 forEach(this.edgesListeners, (callback, event) => {
10725 oldEdgesData.off(event, callback);
10726 });
10727 }
10728
10729 // remove drawn edges
10730 this.body.edges = {};
10731
10732 // TODO: is this null or undefined or false?
10733 if (this.body.data.edges) {
10734 // subscribe to new dataset
10735 forEach(this.edgesListeners, (callback, event) => {
10736 this.body.data.edges.on(event, callback);
10737 });
10738
10739 // draw all new nodes
10740 const ids = this.body.data.edges.getIds();
10741 this.add(ids, true);
10742 }
10743
10744 this.body.emitter.emit("_adjustEdgesForHierarchicalLayout");
10745 if (doNotEmit === false) {
10746 this.body.emitter.emit("_dataChanged");
10747 }
10748 }
10749
10750 /**
10751 * Add edges
10752 *
10753 * @param {number[] | string[]} ids
10754 * @param {boolean} [doNotEmit=false]
10755 * @private
10756 */
10757 add(ids, doNotEmit = false) {
10758 const edges = this.body.edges;
10759 const edgesData = this.body.data.edges;
10760
10761 for (let i = 0; i < ids.length; i++) {
10762 const id = ids[i];
10763
10764 const oldEdge = edges[id];
10765 if (oldEdge) {
10766 oldEdge.disconnect();
10767 }
10768
10769 const data = edgesData.get(id, { showInternalIds: true });
10770 edges[id] = this.create(data);
10771 }
10772
10773 this.body.emitter.emit("_adjustEdgesForHierarchicalLayout");
10774
10775 if (doNotEmit === false) {
10776 this.body.emitter.emit("_dataChanged");
10777 }
10778 }
10779
10780 /**
10781 * Update existing edges, or create them when not yet existing
10782 *
10783 * @param {number[] | string[]} ids
10784 * @private
10785 */
10786 update(ids) {
10787 const edges = this.body.edges;
10788 const edgesData = this.body.data.edges;
10789 let dataChanged = false;
10790 for (let i = 0; i < ids.length; i++) {
10791 const id = ids[i];
10792 const data = edgesData.get(id);
10793 const edge = edges[id];
10794 if (edge !== undefined) {
10795 // update edge
10796 edge.disconnect();
10797 dataChanged = edge.setOptions(data) || dataChanged; // if a support node is added, data can be changed.
10798 edge.connect();
10799 } else {
10800 // create edge
10801 this.body.edges[id] = this.create(data);
10802 dataChanged = true;
10803 }
10804 }
10805
10806 if (dataChanged === true) {
10807 this.body.emitter.emit("_adjustEdgesForHierarchicalLayout");
10808 this.body.emitter.emit("_dataChanged");
10809 } else {
10810 this.body.emitter.emit("_dataUpdated");
10811 }
10812 }
10813
10814 /**
10815 * Remove existing edges. Non existing ids will be ignored
10816 *
10817 * @param {number[] | string[]} ids
10818 * @param {boolean} [emit=true]
10819 * @private
10820 */
10821 remove(ids, emit = true) {
10822 if (ids.length === 0) return; // early out
10823
10824 const edges = this.body.edges;
10825 forEach(ids, (id) => {
10826 const edge = edges[id];
10827 if (edge !== undefined) {
10828 edge.remove();
10829 }
10830 });
10831
10832 if (emit) {
10833 this.body.emitter.emit("_dataChanged");
10834 }
10835 }
10836
10837 /**
10838 * Refreshes Edge Handler
10839 */
10840 refresh() {
10841 forEach(this.body.edges, (edge, edgeId) => {
10842 const data = this.body.data.edges.get(edgeId);
10843 if (data !== undefined) {
10844 edge.setOptions(data);
10845 }
10846 });
10847 }
10848
10849 /**
10850 *
10851 * @param {object} properties
10852 * @returns {Edge}
10853 */
10854 create(properties) {
10855 return new Edge(
10856 properties,
10857 this.body,
10858 this.images,
10859 this.options,
10860 this.defaultOptions
10861 );
10862 }
10863
10864 /**
10865 * Reconnect all edges
10866 *
10867 * @private
10868 */
10869 reconnectEdges() {
10870 let id;
10871 const nodes = this.body.nodes;
10872 const edges = this.body.edges;
10873
10874 for (id in nodes) {
10875 if (Object.prototype.hasOwnProperty.call(nodes, id)) {
10876 nodes[id].edges = [];
10877 }
10878 }
10879
10880 for (id in edges) {
10881 if (Object.prototype.hasOwnProperty.call(edges, id)) {
10882 const edge = edges[id];
10883 edge.from = null;
10884 edge.to = null;
10885 edge.connect();
10886 }
10887 }
10888 }
10889
10890 /**
10891 *
10892 * @param {Edge.id} edgeId
10893 * @returns {Array}
10894 */
10895 getConnectedNodes(edgeId) {
10896 const nodeList = [];
10897 if (this.body.edges[edgeId] !== undefined) {
10898 const edge = this.body.edges[edgeId];
10899 if (edge.fromId !== undefined) {
10900 nodeList.push(edge.fromId);
10901 }
10902 if (edge.toId !== undefined) {
10903 nodeList.push(edge.toId);
10904 }
10905 }
10906 return nodeList;
10907 }
10908
10909 /**
10910 * There is no direct relation between the nodes and the edges DataSet,
10911 * so the right place to do call this is in the handler for event `_dataUpdated`.
10912 */
10913 _updateState() {
10914 this._addMissingEdges();
10915 this._removeInvalidEdges();
10916 }
10917
10918 /**
10919 * Scan for missing nodes and remove corresponding edges, if any.
10920 *
10921 * @private
10922 */
10923 _removeInvalidEdges() {
10924 const edgesToDelete = [];
10925
10926 forEach(this.body.edges, (edge, id) => {
10927 const toNode = this.body.nodes[edge.toId];
10928 const fromNode = this.body.nodes[edge.fromId];
10929
10930 // Skip clustering edges here, let the Clustering module handle those
10931 if (
10932 (toNode !== undefined && toNode.isCluster === true) ||
10933 (fromNode !== undefined && fromNode.isCluster === true)
10934 ) {
10935 return;
10936 }
10937
10938 if (toNode === undefined || fromNode === undefined) {
10939 edgesToDelete.push(id);
10940 }
10941 });
10942
10943 this.remove(edgesToDelete, false);
10944 }
10945
10946 /**
10947 * add all edges from dataset that are not in the cached state
10948 *
10949 * @private
10950 */
10951 _addMissingEdges() {
10952 const edgesData = this.body.data.edges;
10953 if (edgesData === undefined || edgesData === null) {
10954 return; // No edges DataSet yet; can happen on startup
10955 }
10956
10957 const edges = this.body.edges;
10958 const addIds = [];
10959
10960 edgesData.forEach((edgeData, edgeId) => {
10961 const edge = edges[edgeId];
10962 if (edge === undefined) {
10963 addIds.push(edgeId);
10964 }
10965 });
10966
10967 this.add(addIds, true);
10968 }
10969}
10970
10971/**
10972 * Barnes Hut Solver
10973 */
10974class BarnesHutSolver {
10975 /**
10976 * @param {object} body
10977 * @param {{physicsNodeIndices: Array, physicsEdgeIndices: Array, forces: {}, velocities: {}}} physicsBody
10978 * @param {object} options
10979 */
10980 constructor(body, physicsBody, options) {
10981 this.body = body;
10982 this.physicsBody = physicsBody;
10983 this.barnesHutTree;
10984 this.setOptions(options);
10985 this._rng = Alea("BARNES HUT SOLVER");
10986
10987 // debug: show grid
10988 // this.body.emitter.on("afterDrawing", (ctx) => {this._debug(ctx,'#ff0000')})
10989 }
10990
10991 /**
10992 *
10993 * @param {object} options
10994 */
10995 setOptions(options) {
10996 this.options = options;
10997 this.thetaInversed = 1 / this.options.theta;
10998
10999 // if 1 then min distance = 0.5, if 0.5 then min distance = 0.5 + 0.5*node.shape.radius
11000 this.overlapAvoidanceFactor =
11001 1 - Math.max(0, Math.min(1, this.options.avoidOverlap));
11002 }
11003
11004 /**
11005 * This function calculates the forces the nodes apply on each other based on a gravitational model.
11006 * The Barnes Hut method is used to speed up this N-body simulation.
11007 *
11008 * @private
11009 */
11010 solve() {
11011 if (
11012 this.options.gravitationalConstant !== 0 &&
11013 this.physicsBody.physicsNodeIndices.length > 0
11014 ) {
11015 let node;
11016 const nodes = this.body.nodes;
11017 const nodeIndices = this.physicsBody.physicsNodeIndices;
11018 const nodeCount = nodeIndices.length;
11019
11020 // create the tree
11021 const barnesHutTree = this._formBarnesHutTree(nodes, nodeIndices);
11022
11023 // for debugging
11024 this.barnesHutTree = barnesHutTree;
11025
11026 // place the nodes one by one recursively
11027 for (let i = 0; i < nodeCount; i++) {
11028 node = nodes[nodeIndices[i]];
11029 if (node.options.mass > 0) {
11030 // starting with root is irrelevant, it never passes the BarnesHutSolver condition
11031 this._getForceContributions(barnesHutTree.root, node);
11032 }
11033 }
11034 }
11035 }
11036
11037 /**
11038 * @param {object} parentBranch
11039 * @param {Node} node
11040 * @private
11041 */
11042 _getForceContributions(parentBranch, node) {
11043 this._getForceContribution(parentBranch.children.NW, node);
11044 this._getForceContribution(parentBranch.children.NE, node);
11045 this._getForceContribution(parentBranch.children.SW, node);
11046 this._getForceContribution(parentBranch.children.SE, node);
11047 }
11048
11049 /**
11050 * This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass.
11051 * If a region contains a single node, we check if it is not itself, then we apply the force.
11052 *
11053 * @param {object} parentBranch
11054 * @param {Node} node
11055 * @private
11056 */
11057 _getForceContribution(parentBranch, node) {
11058 // we get no force contribution from an empty region
11059 if (parentBranch.childrenCount > 0) {
11060 // get the distance from the center of mass to the node.
11061 const dx = parentBranch.centerOfMass.x - node.x;
11062 const dy = parentBranch.centerOfMass.y - node.y;
11063 const distance = Math.sqrt(dx * dx + dy * dy);
11064
11065 // BarnesHutSolver condition
11066 // original condition : s/d < theta = passed === d/s > 1/theta = passed
11067 // calcSize = 1/s --> d * 1/s > 1/theta = passed
11068 if (distance * parentBranch.calcSize > this.thetaInversed) {
11069 this._calculateForces(distance, dx, dy, node, parentBranch);
11070 } else {
11071 // Did not pass the condition, go into children if available
11072 if (parentBranch.childrenCount === 4) {
11073 this._getForceContributions(parentBranch, node);
11074 } else {
11075 // parentBranch must have only one node, if it was empty we wouldnt be here
11076 if (parentBranch.children.data.id != node.id) {
11077 // if it is not self
11078 this._calculateForces(distance, dx, dy, node, parentBranch);
11079 }
11080 }
11081 }
11082 }
11083 }
11084
11085 /**
11086 * Calculate the forces based on the distance.
11087 *
11088 * @param {number} distance
11089 * @param {number} dx
11090 * @param {number} dy
11091 * @param {Node} node
11092 * @param {object} parentBranch
11093 * @private
11094 */
11095 _calculateForces(distance, dx, dy, node, parentBranch) {
11096 if (distance === 0) {
11097 distance = 0.1;
11098 dx = distance;
11099 }
11100
11101 if (this.overlapAvoidanceFactor < 1 && node.shape.radius) {
11102 distance = Math.max(
11103 0.1 + this.overlapAvoidanceFactor * node.shape.radius,
11104 distance - node.shape.radius
11105 );
11106 }
11107
11108 // the dividing by the distance cubed instead of squared allows us to get the fx and fy components without sines and cosines
11109 // it is shorthand for gravityforce with distance squared and fx = dx/distance * gravityForce
11110 const gravityForce =
11111 (this.options.gravitationalConstant *
11112 parentBranch.mass *
11113 node.options.mass) /
11114 Math.pow(distance, 3);
11115 const fx = dx * gravityForce;
11116 const fy = dy * gravityForce;
11117
11118 this.physicsBody.forces[node.id].x += fx;
11119 this.physicsBody.forces[node.id].y += fy;
11120 }
11121
11122 /**
11123 * This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes.
11124 *
11125 * @param {Array.<Node>} nodes
11126 * @param {Array.<number>} nodeIndices
11127 * @returns {{root: {centerOfMass: {x: number, y: number}, mass: number, range: {minX: number, maxX: number, minY: number, maxY: number}, size: number, calcSize: number, children: {data: null}, maxWidth: number, level: number, childrenCount: number}}} BarnesHutTree
11128 * @private
11129 */
11130 _formBarnesHutTree(nodes, nodeIndices) {
11131 let node;
11132 const nodeCount = nodeIndices.length;
11133
11134 let minX = nodes[nodeIndices[0]].x;
11135 let minY = nodes[nodeIndices[0]].y;
11136 let maxX = nodes[nodeIndices[0]].x;
11137 let maxY = nodes[nodeIndices[0]].y;
11138
11139 // get the range of the nodes
11140 for (let i = 1; i < nodeCount; i++) {
11141 const node = nodes[nodeIndices[i]];
11142 const x = node.x;
11143 const y = node.y;
11144 if (node.options.mass > 0) {
11145 if (x < minX) {
11146 minX = x;
11147 }
11148 if (x > maxX) {
11149 maxX = x;
11150 }
11151 if (y < minY) {
11152 minY = y;
11153 }
11154 if (y > maxY) {
11155 maxY = y;
11156 }
11157 }
11158 }
11159 // make the range a square
11160 const sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y
11161 if (sizeDiff > 0) {
11162 minY -= 0.5 * sizeDiff;
11163 maxY += 0.5 * sizeDiff;
11164 } // xSize > ySize
11165 else {
11166 minX += 0.5 * sizeDiff;
11167 maxX -= 0.5 * sizeDiff;
11168 } // xSize < ySize
11169
11170 const minimumTreeSize = 1e-5;
11171 const rootSize = Math.max(minimumTreeSize, Math.abs(maxX - minX));
11172 const halfRootSize = 0.5 * rootSize;
11173 const centerX = 0.5 * (minX + maxX),
11174 centerY = 0.5 * (minY + maxY);
11175
11176 // construct the barnesHutTree
11177 const barnesHutTree = {
11178 root: {
11179 centerOfMass: { x: 0, y: 0 },
11180 mass: 0,
11181 range: {
11182 minX: centerX - halfRootSize,
11183 maxX: centerX + halfRootSize,
11184 minY: centerY - halfRootSize,
11185 maxY: centerY + halfRootSize,
11186 },
11187 size: rootSize,
11188 calcSize: 1 / rootSize,
11189 children: { data: null },
11190 maxWidth: 0,
11191 level: 0,
11192 childrenCount: 4,
11193 },
11194 };
11195 this._splitBranch(barnesHutTree.root);
11196
11197 // place the nodes one by one recursively
11198 for (let i = 0; i < nodeCount; i++) {
11199 node = nodes[nodeIndices[i]];
11200 if (node.options.mass > 0) {
11201 this._placeInTree(barnesHutTree.root, node);
11202 }
11203 }
11204
11205 // make global
11206 return barnesHutTree;
11207 }
11208
11209 /**
11210 * this updates the mass of a branch. this is increased by adding a node.
11211 *
11212 * @param {object} parentBranch
11213 * @param {Node} node
11214 * @private
11215 */
11216 _updateBranchMass(parentBranch, node) {
11217 const centerOfMass = parentBranch.centerOfMass;
11218 const totalMass = parentBranch.mass + node.options.mass;
11219 const totalMassInv = 1 / totalMass;
11220
11221 centerOfMass.x =
11222 centerOfMass.x * parentBranch.mass + node.x * node.options.mass;
11223 centerOfMass.x *= totalMassInv;
11224
11225 centerOfMass.y =
11226 centerOfMass.y * parentBranch.mass + node.y * node.options.mass;
11227 centerOfMass.y *= totalMassInv;
11228
11229 parentBranch.mass = totalMass;
11230 const biggestSize = Math.max(
11231 Math.max(node.height, node.radius),
11232 node.width
11233 );
11234 parentBranch.maxWidth =
11235 parentBranch.maxWidth < biggestSize ? biggestSize : parentBranch.maxWidth;
11236 }
11237
11238 /**
11239 * determine in which branch the node will be placed.
11240 *
11241 * @param {object} parentBranch
11242 * @param {Node} node
11243 * @param {boolean} skipMassUpdate
11244 * @private
11245 */
11246 _placeInTree(parentBranch, node, skipMassUpdate) {
11247 if (skipMassUpdate != true || skipMassUpdate === undefined) {
11248 // update the mass of the branch.
11249 this._updateBranchMass(parentBranch, node);
11250 }
11251
11252 const range = parentBranch.children.NW.range;
11253 let region;
11254 if (range.maxX > node.x) {
11255 // in NW or SW
11256 if (range.maxY > node.y) {
11257 region = "NW";
11258 } else {
11259 region = "SW";
11260 }
11261 } else {
11262 // in NE or SE
11263 if (range.maxY > node.y) {
11264 region = "NE";
11265 } else {
11266 region = "SE";
11267 }
11268 }
11269
11270 this._placeInRegion(parentBranch, node, region);
11271 }
11272
11273 /**
11274 * actually place the node in a region (or branch)
11275 *
11276 * @param {object} parentBranch
11277 * @param {Node} node
11278 * @param {'NW'| 'NE' | 'SW' | 'SE'} region
11279 * @private
11280 */
11281 _placeInRegion(parentBranch, node, region) {
11282 const children = parentBranch.children[region];
11283
11284 switch (children.childrenCount) {
11285 case 0: // place node here
11286 children.children.data = node;
11287 children.childrenCount = 1;
11288 this._updateBranchMass(children, node);
11289 break;
11290 case 1: // convert into children
11291 // if there are two nodes exactly overlapping (on init, on opening of cluster etc.)
11292 // we move one node a little bit and we do not put it in the tree.
11293 if (
11294 children.children.data.x === node.x &&
11295 children.children.data.y === node.y
11296 ) {
11297 node.x += this._rng();
11298 node.y += this._rng();
11299 } else {
11300 this._splitBranch(children);
11301 this._placeInTree(children, node);
11302 }
11303 break;
11304 case 4: // place in branch
11305 this._placeInTree(children, node);
11306 break;
11307 }
11308 }
11309
11310 /**
11311 * this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch
11312 * after the split is complete.
11313 *
11314 * @param {object} parentBranch
11315 * @private
11316 */
11317 _splitBranch(parentBranch) {
11318 // if the branch is shaded with a node, replace the node in the new subset.
11319 let containedNode = null;
11320 if (parentBranch.childrenCount === 1) {
11321 containedNode = parentBranch.children.data;
11322 parentBranch.mass = 0;
11323 parentBranch.centerOfMass.x = 0;
11324 parentBranch.centerOfMass.y = 0;
11325 }
11326 parentBranch.childrenCount = 4;
11327 parentBranch.children.data = null;
11328 this._insertRegion(parentBranch, "NW");
11329 this._insertRegion(parentBranch, "NE");
11330 this._insertRegion(parentBranch, "SW");
11331 this._insertRegion(parentBranch, "SE");
11332
11333 if (containedNode != null) {
11334 this._placeInTree(parentBranch, containedNode);
11335 }
11336 }
11337
11338 /**
11339 * This function subdivides the region into four new segments.
11340 * Specifically, this inserts a single new segment.
11341 * It fills the children section of the parentBranch
11342 *
11343 * @param {object} parentBranch
11344 * @param {'NW'| 'NE' | 'SW' | 'SE'} region
11345 * @private
11346 */
11347 _insertRegion(parentBranch, region) {
11348 let minX, maxX, minY, maxY;
11349 const childSize = 0.5 * parentBranch.size;
11350 switch (region) {
11351 case "NW":
11352 minX = parentBranch.range.minX;
11353 maxX = parentBranch.range.minX + childSize;
11354 minY = parentBranch.range.minY;
11355 maxY = parentBranch.range.minY + childSize;
11356 break;
11357 case "NE":
11358 minX = parentBranch.range.minX + childSize;
11359 maxX = parentBranch.range.maxX;
11360 minY = parentBranch.range.minY;
11361 maxY = parentBranch.range.minY + childSize;
11362 break;
11363 case "SW":
11364 minX = parentBranch.range.minX;
11365 maxX = parentBranch.range.minX + childSize;
11366 minY = parentBranch.range.minY + childSize;
11367 maxY = parentBranch.range.maxY;
11368 break;
11369 case "SE":
11370 minX = parentBranch.range.minX + childSize;
11371 maxX = parentBranch.range.maxX;
11372 minY = parentBranch.range.minY + childSize;
11373 maxY = parentBranch.range.maxY;
11374 break;
11375 }
11376
11377 parentBranch.children[region] = {
11378 centerOfMass: { x: 0, y: 0 },
11379 mass: 0,
11380 range: { minX: minX, maxX: maxX, minY: minY, maxY: maxY },
11381 size: 0.5 * parentBranch.size,
11382 calcSize: 2 * parentBranch.calcSize,
11383 children: { data: null },
11384 maxWidth: 0,
11385 level: parentBranch.level + 1,
11386 childrenCount: 0,
11387 };
11388 }
11389
11390 //--------------------------- DEBUGGING BELOW ---------------------------//
11391
11392 /**
11393 * This function is for debugging purposed, it draws the tree.
11394 *
11395 * @param {CanvasRenderingContext2D} ctx
11396 * @param {string} color
11397 * @private
11398 */
11399 _debug(ctx, color) {
11400 if (this.barnesHutTree !== undefined) {
11401 ctx.lineWidth = 1;
11402
11403 this._drawBranch(this.barnesHutTree.root, ctx, color);
11404 }
11405 }
11406
11407 /**
11408 * This function is for debugging purposes. It draws the branches recursively.
11409 *
11410 * @param {object} branch
11411 * @param {CanvasRenderingContext2D} ctx
11412 * @param {string} color
11413 * @private
11414 */
11415 _drawBranch(branch, ctx, color) {
11416 if (color === undefined) {
11417 color = "#FF0000";
11418 }
11419
11420 if (branch.childrenCount === 4) {
11421 this._drawBranch(branch.children.NW, ctx);
11422 this._drawBranch(branch.children.NE, ctx);
11423 this._drawBranch(branch.children.SE, ctx);
11424 this._drawBranch(branch.children.SW, ctx);
11425 }
11426 ctx.strokeStyle = color;
11427 ctx.beginPath();
11428 ctx.moveTo(branch.range.minX, branch.range.minY);
11429 ctx.lineTo(branch.range.maxX, branch.range.minY);
11430 ctx.stroke();
11431
11432 ctx.beginPath();
11433 ctx.moveTo(branch.range.maxX, branch.range.minY);
11434 ctx.lineTo(branch.range.maxX, branch.range.maxY);
11435 ctx.stroke();
11436
11437 ctx.beginPath();
11438 ctx.moveTo(branch.range.maxX, branch.range.maxY);
11439 ctx.lineTo(branch.range.minX, branch.range.maxY);
11440 ctx.stroke();
11441
11442 ctx.beginPath();
11443 ctx.moveTo(branch.range.minX, branch.range.maxY);
11444 ctx.lineTo(branch.range.minX, branch.range.minY);
11445 ctx.stroke();
11446
11447 /*
11448 if (branch.mass > 0) {
11449 ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass);
11450 ctx.stroke();
11451 }
11452 */
11453 }
11454}
11455
11456/**
11457 * Repulsion Solver
11458 */
11459class RepulsionSolver {
11460 /**
11461 * @param {object} body
11462 * @param {{physicsNodeIndices: Array, physicsEdgeIndices: Array, forces: {}, velocities: {}}} physicsBody
11463 * @param {object} options
11464 */
11465 constructor(body, physicsBody, options) {
11466 this._rng = Alea("REPULSION SOLVER");
11467
11468 this.body = body;
11469 this.physicsBody = physicsBody;
11470 this.setOptions(options);
11471 }
11472
11473 /**
11474 *
11475 * @param {object} options
11476 */
11477 setOptions(options) {
11478 this.options = options;
11479 }
11480
11481 /**
11482 * Calculate the forces the nodes apply on each other based on a repulsion field.
11483 * This field is linearly approximated.
11484 *
11485 * @private
11486 */
11487 solve() {
11488 let dx, dy, distance, fx, fy, repulsingForce, node1, node2;
11489
11490 const nodes = this.body.nodes;
11491 const nodeIndices = this.physicsBody.physicsNodeIndices;
11492 const forces = this.physicsBody.forces;
11493
11494 // repulsing forces between nodes
11495 const nodeDistance = this.options.nodeDistance;
11496
11497 // approximation constants
11498 const a = -2 / 3 / nodeDistance;
11499 const b = 4 / 3;
11500
11501 // we loop from i over all but the last entree in the array
11502 // j loops from i+1 to the last. This way we do not double count any of the indices, nor i === j
11503 for (let i = 0; i < nodeIndices.length - 1; i++) {
11504 node1 = nodes[nodeIndices[i]];
11505 for (let j = i + 1; j < nodeIndices.length; j++) {
11506 node2 = nodes[nodeIndices[j]];
11507
11508 dx = node2.x - node1.x;
11509 dy = node2.y - node1.y;
11510 distance = Math.sqrt(dx * dx + dy * dy);
11511
11512 // same condition as BarnesHutSolver, making sure nodes are never 100% overlapping.
11513 if (distance === 0) {
11514 distance = 0.1 * this._rng();
11515 dx = distance;
11516 }
11517
11518 if (distance < 2 * nodeDistance) {
11519 if (distance < 0.5 * nodeDistance) {
11520 repulsingForce = 1.0;
11521 } else {
11522 repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / nodeDistance - 1) * steepness))
11523 }
11524 repulsingForce = repulsingForce / distance;
11525
11526 fx = dx * repulsingForce;
11527 fy = dy * repulsingForce;
11528
11529 forces[node1.id].x -= fx;
11530 forces[node1.id].y -= fy;
11531 forces[node2.id].x += fx;
11532 forces[node2.id].y += fy;
11533 }
11534 }
11535 }
11536 }
11537}
11538
11539/**
11540 * Hierarchical Repulsion Solver
11541 */
11542class HierarchicalRepulsionSolver {
11543 /**
11544 * @param {object} body
11545 * @param {{physicsNodeIndices: Array, physicsEdgeIndices: Array, forces: {}, velocities: {}}} physicsBody
11546 * @param {object} options
11547 */
11548 constructor(body, physicsBody, options) {
11549 this.body = body;
11550 this.physicsBody = physicsBody;
11551 this.setOptions(options);
11552 }
11553
11554 /**
11555 *
11556 * @param {object} options
11557 */
11558 setOptions(options) {
11559 this.options = options;
11560 this.overlapAvoidanceFactor = Math.max(
11561 0,
11562 Math.min(1, this.options.avoidOverlap || 0)
11563 );
11564 }
11565
11566 /**
11567 * Calculate the forces the nodes apply on each other based on a repulsion field.
11568 * This field is linearly approximated.
11569 *
11570 * @private
11571 */
11572 solve() {
11573 const nodes = this.body.nodes;
11574 const nodeIndices = this.physicsBody.physicsNodeIndices;
11575 const forces = this.physicsBody.forces;
11576
11577 // repulsing forces between nodes
11578 const nodeDistance = this.options.nodeDistance;
11579
11580 // we loop from i over all but the last entree in the array
11581 // j loops from i+1 to the last. This way we do not double count any of the indices, nor i === j
11582 for (let i = 0; i < nodeIndices.length - 1; i++) {
11583 const node1 = nodes[nodeIndices[i]];
11584 for (let j = i + 1; j < nodeIndices.length; j++) {
11585 const node2 = nodes[nodeIndices[j]];
11586
11587 // nodes only affect nodes on their level
11588 if (node1.level === node2.level) {
11589 const theseNodesDistance =
11590 nodeDistance +
11591 this.overlapAvoidanceFactor *
11592 ((node1.shape.radius || 0) / 2 + (node2.shape.radius || 0) / 2);
11593
11594 const dx = node2.x - node1.x;
11595 const dy = node2.y - node1.y;
11596 const distance = Math.sqrt(dx * dx + dy * dy);
11597
11598 const steepness = 0.05;
11599 let repulsingForce;
11600 if (distance < theseNodesDistance) {
11601 repulsingForce =
11602 -Math.pow(steepness * distance, 2) +
11603 Math.pow(steepness * theseNodesDistance, 2);
11604 } else {
11605 repulsingForce = 0;
11606 }
11607 // normalize force with
11608 if (distance !== 0) {
11609 repulsingForce = repulsingForce / distance;
11610 }
11611 const fx = dx * repulsingForce;
11612 const fy = dy * repulsingForce;
11613
11614 forces[node1.id].x -= fx;
11615 forces[node1.id].y -= fy;
11616 forces[node2.id].x += fx;
11617 forces[node2.id].y += fy;
11618 }
11619 }
11620 }
11621 }
11622}
11623
11624/**
11625 * Spring Solver
11626 */
11627class SpringSolver {
11628 /**
11629 * @param {object} body
11630 * @param {{physicsNodeIndices: Array, physicsEdgeIndices: Array, forces: {}, velocities: {}}} physicsBody
11631 * @param {object} options
11632 */
11633 constructor(body, physicsBody, options) {
11634 this.body = body;
11635 this.physicsBody = physicsBody;
11636 this.setOptions(options);
11637 }
11638
11639 /**
11640 *
11641 * @param {object} options
11642 */
11643 setOptions(options) {
11644 this.options = options;
11645 }
11646
11647 /**
11648 * This function calculates the springforces on the nodes, accounting for the support nodes.
11649 *
11650 * @private
11651 */
11652 solve() {
11653 let edgeLength, edge;
11654 const edgeIndices = this.physicsBody.physicsEdgeIndices;
11655 const edges = this.body.edges;
11656 let node1, node2, node3;
11657
11658 // forces caused by the edges, modelled as springs
11659 for (let i = 0; i < edgeIndices.length; i++) {
11660 edge = edges[edgeIndices[i]];
11661 if (edge.connected === true && edge.toId !== edge.fromId) {
11662 // only calculate forces if nodes are in the same sector
11663 if (
11664 this.body.nodes[edge.toId] !== undefined &&
11665 this.body.nodes[edge.fromId] !== undefined
11666 ) {
11667 if (edge.edgeType.via !== undefined) {
11668 edgeLength =
11669 edge.options.length === undefined
11670 ? this.options.springLength
11671 : edge.options.length;
11672 node1 = edge.to;
11673 node2 = edge.edgeType.via;
11674 node3 = edge.from;
11675
11676 this._calculateSpringForce(node1, node2, 0.5 * edgeLength);
11677 this._calculateSpringForce(node2, node3, 0.5 * edgeLength);
11678 } else {
11679 // the * 1.5 is here so the edge looks as large as a smooth edge. It does not initially because the smooth edges use
11680 // the support nodes which exert a repulsive force on the to and from nodes, making the edge appear larger.
11681 edgeLength =
11682 edge.options.length === undefined
11683 ? this.options.springLength * 1.5
11684 : edge.options.length;
11685 this._calculateSpringForce(edge.from, edge.to, edgeLength);
11686 }
11687 }
11688 }
11689 }
11690 }
11691
11692 /**
11693 * This is the code actually performing the calculation for the function above.
11694 *
11695 * @param {Node} node1
11696 * @param {Node} node2
11697 * @param {number} edgeLength
11698 * @private
11699 */
11700 _calculateSpringForce(node1, node2, edgeLength) {
11701 const dx = node1.x - node2.x;
11702 const dy = node1.y - node2.y;
11703 const distance = Math.max(Math.sqrt(dx * dx + dy * dy), 0.01);
11704
11705 // the 1/distance is so the fx and fy can be calculated without sine or cosine.
11706 const springForce =
11707 (this.options.springConstant * (edgeLength - distance)) / distance;
11708
11709 const fx = dx * springForce;
11710 const fy = dy * springForce;
11711
11712 // handle the case where one node is not part of the physcis
11713 if (this.physicsBody.forces[node1.id] !== undefined) {
11714 this.physicsBody.forces[node1.id].x += fx;
11715 this.physicsBody.forces[node1.id].y += fy;
11716 }
11717
11718 if (this.physicsBody.forces[node2.id] !== undefined) {
11719 this.physicsBody.forces[node2.id].x -= fx;
11720 this.physicsBody.forces[node2.id].y -= fy;
11721 }
11722 }
11723}
11724
11725/**
11726 * Hierarchical Spring Solver
11727 */
11728class HierarchicalSpringSolver {
11729 /**
11730 * @param {object} body
11731 * @param {{physicsNodeIndices: Array, physicsEdgeIndices: Array, forces: {}, velocities: {}}} physicsBody
11732 * @param {object} options
11733 */
11734 constructor(body, physicsBody, options) {
11735 this.body = body;
11736 this.physicsBody = physicsBody;
11737 this.setOptions(options);
11738 }
11739
11740 /**
11741 *
11742 * @param {object} options
11743 */
11744 setOptions(options) {
11745 this.options = options;
11746 }
11747
11748 /**
11749 * This function calculates the springforces on the nodes, accounting for the support nodes.
11750 *
11751 * @private
11752 */
11753 solve() {
11754 let edgeLength, edge;
11755 let dx, dy, fx, fy, springForce, distance;
11756 const edges = this.body.edges;
11757 const factor = 0.5;
11758
11759 const edgeIndices = this.physicsBody.physicsEdgeIndices;
11760 const nodeIndices = this.physicsBody.physicsNodeIndices;
11761 const forces = this.physicsBody.forces;
11762
11763 // initialize the spring force counters
11764 for (let i = 0; i < nodeIndices.length; i++) {
11765 const nodeId = nodeIndices[i];
11766 forces[nodeId].springFx = 0;
11767 forces[nodeId].springFy = 0;
11768 }
11769
11770 // forces caused by the edges, modelled as springs
11771 for (let i = 0; i < edgeIndices.length; i++) {
11772 edge = edges[edgeIndices[i]];
11773 if (edge.connected === true) {
11774 edgeLength =
11775 edge.options.length === undefined
11776 ? this.options.springLength
11777 : edge.options.length;
11778
11779 dx = edge.from.x - edge.to.x;
11780 dy = edge.from.y - edge.to.y;
11781 distance = Math.sqrt(dx * dx + dy * dy);
11782 distance = distance === 0 ? 0.01 : distance;
11783
11784 // the 1/distance is so the fx and fy can be calculated without sine or cosine.
11785 springForce =
11786 (this.options.springConstant * (edgeLength - distance)) / distance;
11787
11788 fx = dx * springForce;
11789 fy = dy * springForce;
11790
11791 if (edge.to.level != edge.from.level) {
11792 if (forces[edge.toId] !== undefined) {
11793 forces[edge.toId].springFx -= fx;
11794 forces[edge.toId].springFy -= fy;
11795 }
11796 if (forces[edge.fromId] !== undefined) {
11797 forces[edge.fromId].springFx += fx;
11798 forces[edge.fromId].springFy += fy;
11799 }
11800 } else {
11801 if (forces[edge.toId] !== undefined) {
11802 forces[edge.toId].x -= factor * fx;
11803 forces[edge.toId].y -= factor * fy;
11804 }
11805 if (forces[edge.fromId] !== undefined) {
11806 forces[edge.fromId].x += factor * fx;
11807 forces[edge.fromId].y += factor * fy;
11808 }
11809 }
11810 }
11811 }
11812
11813 // normalize spring forces
11814 springForce = 1;
11815 let springFx, springFy;
11816 for (let i = 0; i < nodeIndices.length; i++) {
11817 const nodeId = nodeIndices[i];
11818 springFx = Math.min(
11819 springForce,
11820 Math.max(-springForce, forces[nodeId].springFx)
11821 );
11822 springFy = Math.min(
11823 springForce,
11824 Math.max(-springForce, forces[nodeId].springFy)
11825 );
11826
11827 forces[nodeId].x += springFx;
11828 forces[nodeId].y += springFy;
11829 }
11830
11831 // retain energy balance
11832 let totalFx = 0;
11833 let totalFy = 0;
11834 for (let i = 0; i < nodeIndices.length; i++) {
11835 const nodeId = nodeIndices[i];
11836 totalFx += forces[nodeId].x;
11837 totalFy += forces[nodeId].y;
11838 }
11839 const correctionFx = totalFx / nodeIndices.length;
11840 const correctionFy = totalFy / nodeIndices.length;
11841
11842 for (let i = 0; i < nodeIndices.length; i++) {
11843 const nodeId = nodeIndices[i];
11844 forces[nodeId].x -= correctionFx;
11845 forces[nodeId].y -= correctionFy;
11846 }
11847 }
11848}
11849
11850/**
11851 * Central Gravity Solver
11852 */
11853class CentralGravitySolver {
11854 /**
11855 * @param {object} body
11856 * @param {{physicsNodeIndices: Array, physicsEdgeIndices: Array, forces: {}, velocities: {}}} physicsBody
11857 * @param {object} options
11858 */
11859 constructor(body, physicsBody, options) {
11860 this.body = body;
11861 this.physicsBody = physicsBody;
11862 this.setOptions(options);
11863 }
11864
11865 /**
11866 *
11867 * @param {object} options
11868 */
11869 setOptions(options) {
11870 this.options = options;
11871 }
11872
11873 /**
11874 * Calculates forces for each node
11875 */
11876 solve() {
11877 let dx, dy, distance, node;
11878 const nodes = this.body.nodes;
11879 const nodeIndices = this.physicsBody.physicsNodeIndices;
11880 const forces = this.physicsBody.forces;
11881
11882 for (let i = 0; i < nodeIndices.length; i++) {
11883 const nodeId = nodeIndices[i];
11884 node = nodes[nodeId];
11885 dx = -node.x;
11886 dy = -node.y;
11887 distance = Math.sqrt(dx * dx + dy * dy);
11888
11889 this._calculateForces(distance, dx, dy, forces, node);
11890 }
11891 }
11892
11893 /**
11894 * Calculate the forces based on the distance.
11895 *
11896 * @param {number} distance
11897 * @param {number} dx
11898 * @param {number} dy
11899 * @param {object<Node.id, vis.Node>} forces
11900 * @param {Node} node
11901 * @private
11902 */
11903 _calculateForces(distance, dx, dy, forces, node) {
11904 const gravityForce =
11905 distance === 0 ? 0 : this.options.centralGravity / distance;
11906 forces[node.id].x = dx * gravityForce;
11907 forces[node.id].y = dy * gravityForce;
11908 }
11909}
11910
11911/**
11912 * @augments BarnesHutSolver
11913 */
11914class ForceAtlas2BasedRepulsionSolver extends BarnesHutSolver {
11915 /**
11916 * @param {object} body
11917 * @param {{physicsNodeIndices: Array, physicsEdgeIndices: Array, forces: {}, velocities: {}}} physicsBody
11918 * @param {object} options
11919 */
11920 constructor(body, physicsBody, options) {
11921 super(body, physicsBody, options);
11922
11923 this._rng = Alea("FORCE ATLAS 2 BASED REPULSION SOLVER");
11924 }
11925
11926 /**
11927 * Calculate the forces based on the distance.
11928 *
11929 * @param {number} distance
11930 * @param {number} dx
11931 * @param {number} dy
11932 * @param {Node} node
11933 * @param {object} parentBranch
11934 * @private
11935 */
11936 _calculateForces(distance, dx, dy, node, parentBranch) {
11937 if (distance === 0) {
11938 distance = 0.1 * this._rng();
11939 dx = distance;
11940 }
11941
11942 if (this.overlapAvoidanceFactor < 1 && node.shape.radius) {
11943 distance = Math.max(
11944 0.1 + this.overlapAvoidanceFactor * node.shape.radius,
11945 distance - node.shape.radius
11946 );
11947 }
11948
11949 const degree = node.edges.length + 1;
11950 // the dividing by the distance cubed instead of squared allows us to get the fx and fy components without sines and cosines
11951 // it is shorthand for gravityforce with distance squared and fx = dx/distance * gravityForce
11952 const gravityForce =
11953 (this.options.gravitationalConstant *
11954 parentBranch.mass *
11955 node.options.mass *
11956 degree) /
11957 Math.pow(distance, 2);
11958 const fx = dx * gravityForce;
11959 const fy = dy * gravityForce;
11960
11961 this.physicsBody.forces[node.id].x += fx;
11962 this.physicsBody.forces[node.id].y += fy;
11963 }
11964}
11965
11966/**
11967 * @augments CentralGravitySolver
11968 */
11969class ForceAtlas2BasedCentralGravitySolver extends CentralGravitySolver {
11970 /**
11971 * @param {object} body
11972 * @param {{physicsNodeIndices: Array, physicsEdgeIndices: Array, forces: {}, velocities: {}}} physicsBody
11973 * @param {object} options
11974 */
11975 constructor(body, physicsBody, options) {
11976 super(body, physicsBody, options);
11977 }
11978
11979 /**
11980 * Calculate the forces based on the distance.
11981 *
11982 * @param {number} distance
11983 * @param {number} dx
11984 * @param {number} dy
11985 * @param {object<Node.id, Node>} forces
11986 * @param {Node} node
11987 * @private
11988 */
11989 _calculateForces(distance, dx, dy, forces, node) {
11990 if (distance > 0) {
11991 const degree = node.edges.length + 1;
11992 const gravityForce =
11993 this.options.centralGravity * degree * node.options.mass;
11994 forces[node.id].x = dx * gravityForce;
11995 forces[node.id].y = dy * gravityForce;
11996 }
11997 }
11998}
11999
12000/**
12001 * The physics engine
12002 */
12003class PhysicsEngine {
12004 /**
12005 * @param {object} body
12006 */
12007 constructor(body) {
12008 this.body = body;
12009 this.physicsBody = {
12010 physicsNodeIndices: [],
12011 physicsEdgeIndices: [],
12012 forces: {},
12013 velocities: {},
12014 };
12015
12016 this.physicsEnabled = true;
12017 this.simulationInterval = 1000 / 60;
12018 this.requiresTimeout = true;
12019 this.previousStates = {};
12020 this.referenceState = {};
12021 this.freezeCache = {};
12022 this.renderTimer = undefined;
12023
12024 // parameters for the adaptive timestep
12025 this.adaptiveTimestep = false;
12026 this.adaptiveTimestepEnabled = false;
12027 this.adaptiveCounter = 0;
12028 this.adaptiveInterval = 3;
12029
12030 this.stabilized = false;
12031 this.startedStabilization = false;
12032 this.stabilizationIterations = 0;
12033 this.ready = false; // will be set to true if the stabilize
12034
12035 // default options
12036 this.options = {};
12037 this.defaultOptions = {
12038 enabled: true,
12039 barnesHut: {
12040 theta: 0.5,
12041 gravitationalConstant: -2000,
12042 centralGravity: 0.3,
12043 springLength: 95,
12044 springConstant: 0.04,
12045 damping: 0.09,
12046 avoidOverlap: 0,
12047 },
12048 forceAtlas2Based: {
12049 theta: 0.5,
12050 gravitationalConstant: -50,
12051 centralGravity: 0.01,
12052 springConstant: 0.08,
12053 springLength: 100,
12054 damping: 0.4,
12055 avoidOverlap: 0,
12056 },
12057 repulsion: {
12058 centralGravity: 0.2,
12059 springLength: 200,
12060 springConstant: 0.05,
12061 nodeDistance: 100,
12062 damping: 0.09,
12063 avoidOverlap: 0,
12064 },
12065 hierarchicalRepulsion: {
12066 centralGravity: 0.0,
12067 springLength: 100,
12068 springConstant: 0.01,
12069 nodeDistance: 120,
12070 damping: 0.09,
12071 },
12072 maxVelocity: 50,
12073 minVelocity: 0.75, // px/s
12074 solver: "barnesHut",
12075 stabilization: {
12076 enabled: true,
12077 iterations: 1000, // maximum number of iteration to stabilize
12078 updateInterval: 50,
12079 onlyDynamicEdges: false,
12080 fit: true,
12081 },
12082 timestep: 0.5,
12083 adaptiveTimestep: true,
12084 wind: { x: 0, y: 0 },
12085 };
12086 Object.assign(this.options, this.defaultOptions);
12087 this.timestep = 0.5;
12088 this.layoutFailed = false;
12089
12090 this.bindEventListeners();
12091 }
12092
12093 /**
12094 * Binds event listeners
12095 */
12096 bindEventListeners() {
12097 this.body.emitter.on("initPhysics", () => {
12098 this.initPhysics();
12099 });
12100 this.body.emitter.on("_layoutFailed", () => {
12101 this.layoutFailed = true;
12102 });
12103 this.body.emitter.on("resetPhysics", () => {
12104 this.stopSimulation();
12105 this.ready = false;
12106 });
12107 this.body.emitter.on("disablePhysics", () => {
12108 this.physicsEnabled = false;
12109 this.stopSimulation();
12110 });
12111 this.body.emitter.on("restorePhysics", () => {
12112 this.setOptions(this.options);
12113 if (this.ready === true) {
12114 this.startSimulation();
12115 }
12116 });
12117 this.body.emitter.on("startSimulation", () => {
12118 if (this.ready === true) {
12119 this.startSimulation();
12120 }
12121 });
12122 this.body.emitter.on("stopSimulation", () => {
12123 this.stopSimulation();
12124 });
12125 this.body.emitter.on("destroy", () => {
12126 this.stopSimulation(false);
12127 this.body.emitter.off();
12128 });
12129 this.body.emitter.on("_dataChanged", () => {
12130 // Nodes and/or edges have been added or removed, update shortcut lists.
12131 this.updatePhysicsData();
12132 });
12133
12134 // debug: show forces
12135 // this.body.emitter.on("afterDrawing", (ctx) => {this._drawForces(ctx);});
12136 }
12137
12138 /**
12139 * set the physics options
12140 *
12141 * @param {object} options
12142 */
12143 setOptions(options) {
12144 if (options !== undefined) {
12145 if (options === false) {
12146 this.options.enabled = false;
12147 this.physicsEnabled = false;
12148 this.stopSimulation();
12149 } else if (options === true) {
12150 this.options.enabled = true;
12151 this.physicsEnabled = true;
12152 this.startSimulation();
12153 } else {
12154 this.physicsEnabled = true;
12155 selectiveNotDeepExtend(["stabilization"], this.options, options);
12156 mergeOptions(this.options, options, "stabilization");
12157
12158 if (options.enabled === undefined) {
12159 this.options.enabled = true;
12160 }
12161
12162 if (this.options.enabled === false) {
12163 this.physicsEnabled = false;
12164 this.stopSimulation();
12165 }
12166
12167 const wind = this.options.wind;
12168 if (wind) {
12169 if (typeof wind.x !== "number" || Number.isNaN(wind.x)) {
12170 wind.x = 0;
12171 }
12172 if (typeof wind.y !== "number" || Number.isNaN(wind.y)) {
12173 wind.y = 0;
12174 }
12175 }
12176
12177 // set the timestep
12178 this.timestep = this.options.timestep;
12179 }
12180 }
12181 this.init();
12182 }
12183
12184 /**
12185 * configure the engine.
12186 */
12187 init() {
12188 let options;
12189 if (this.options.solver === "forceAtlas2Based") {
12190 options = this.options.forceAtlas2Based;
12191 this.nodesSolver = new ForceAtlas2BasedRepulsionSolver(
12192 this.body,
12193 this.physicsBody,
12194 options
12195 );
12196 this.edgesSolver = new SpringSolver(this.body, this.physicsBody, options);
12197 this.gravitySolver = new ForceAtlas2BasedCentralGravitySolver(
12198 this.body,
12199 this.physicsBody,
12200 options
12201 );
12202 } else if (this.options.solver === "repulsion") {
12203 options = this.options.repulsion;
12204 this.nodesSolver = new RepulsionSolver(this.body, this.physicsBody, options);
12205 this.edgesSolver = new SpringSolver(this.body, this.physicsBody, options);
12206 this.gravitySolver = new CentralGravitySolver(
12207 this.body,
12208 this.physicsBody,
12209 options
12210 );
12211 } else if (this.options.solver === "hierarchicalRepulsion") {
12212 options = this.options.hierarchicalRepulsion;
12213 this.nodesSolver = new HierarchicalRepulsionSolver(
12214 this.body,
12215 this.physicsBody,
12216 options
12217 );
12218 this.edgesSolver = new HierarchicalSpringSolver(
12219 this.body,
12220 this.physicsBody,
12221 options
12222 );
12223 this.gravitySolver = new CentralGravitySolver(
12224 this.body,
12225 this.physicsBody,
12226 options
12227 );
12228 } else {
12229 // barnesHut
12230 options = this.options.barnesHut;
12231 this.nodesSolver = new BarnesHutSolver(
12232 this.body,
12233 this.physicsBody,
12234 options
12235 );
12236 this.edgesSolver = new SpringSolver(this.body, this.physicsBody, options);
12237 this.gravitySolver = new CentralGravitySolver(
12238 this.body,
12239 this.physicsBody,
12240 options
12241 );
12242 }
12243
12244 this.modelOptions = options;
12245 }
12246
12247 /**
12248 * initialize the engine
12249 */
12250 initPhysics() {
12251 if (this.physicsEnabled === true && this.options.enabled === true) {
12252 if (this.options.stabilization.enabled === true) {
12253 this.stabilize();
12254 } else {
12255 this.stabilized = false;
12256 this.ready = true;
12257 this.body.emitter.emit("fit", {}, this.layoutFailed); // if the layout failed, we use the approximation for the zoom
12258 this.startSimulation();
12259 }
12260 } else {
12261 this.ready = true;
12262 this.body.emitter.emit("fit");
12263 }
12264 }
12265
12266 /**
12267 * Start the simulation
12268 */
12269 startSimulation() {
12270 if (this.physicsEnabled === true && this.options.enabled === true) {
12271 this.stabilized = false;
12272
12273 // when visible, adaptivity is disabled.
12274 this.adaptiveTimestep = false;
12275
12276 // this sets the width of all nodes initially which could be required for the avoidOverlap
12277 this.body.emitter.emit("_resizeNodes");
12278 if (this.viewFunction === undefined) {
12279 this.viewFunction = this.simulationStep.bind(this);
12280 this.body.emitter.on("initRedraw", this.viewFunction);
12281 this.body.emitter.emit("_startRendering");
12282 }
12283 } else {
12284 this.body.emitter.emit("_redraw");
12285 }
12286 }
12287
12288 /**
12289 * Stop the simulation, force stabilization.
12290 *
12291 * @param {boolean} [emit=true]
12292 */
12293 stopSimulation(emit = true) {
12294 this.stabilized = true;
12295 if (emit === true) {
12296 this._emitStabilized();
12297 }
12298 if (this.viewFunction !== undefined) {
12299 this.body.emitter.off("initRedraw", this.viewFunction);
12300 this.viewFunction = undefined;
12301 if (emit === true) {
12302 this.body.emitter.emit("_stopRendering");
12303 }
12304 }
12305 }
12306
12307 /**
12308 * The viewFunction inserts this step into each render loop. It calls the physics tick and handles the cleanup at stabilized.
12309 *
12310 */
12311 simulationStep() {
12312 // check if the physics have settled
12313 const startTime = Date.now();
12314 this.physicsTick();
12315 const physicsTime = Date.now() - startTime;
12316
12317 // run double speed if it is a little graph
12318 if (
12319 (physicsTime < 0.4 * this.simulationInterval ||
12320 this.runDoubleSpeed === true) &&
12321 this.stabilized === false
12322 ) {
12323 this.physicsTick();
12324
12325 // this makes sure there is no jitter. The decision is taken once to run it at double speed.
12326 this.runDoubleSpeed = true;
12327 }
12328
12329 if (this.stabilized === true) {
12330 this.stopSimulation();
12331 }
12332 }
12333
12334 /**
12335 * trigger the stabilized event.
12336 *
12337 * @param {number} [amountOfIterations=this.stabilizationIterations]
12338 * @private
12339 */
12340 _emitStabilized(amountOfIterations = this.stabilizationIterations) {
12341 if (
12342 this.stabilizationIterations > 1 ||
12343 this.startedStabilization === true
12344 ) {
12345 setTimeout(() => {
12346 this.body.emitter.emit("stabilized", {
12347 iterations: amountOfIterations,
12348 });
12349 this.startedStabilization = false;
12350 this.stabilizationIterations = 0;
12351 }, 0);
12352 }
12353 }
12354
12355 /**
12356 * Calculate the forces for one physics iteration and move the nodes.
12357 *
12358 * @private
12359 */
12360 physicsStep() {
12361 this.gravitySolver.solve();
12362 this.nodesSolver.solve();
12363 this.edgesSolver.solve();
12364 this.moveNodes();
12365 }
12366
12367 /**
12368 * Make dynamic adjustments to the timestep, based on current state.
12369 *
12370 * Helper function for physicsTick().
12371 *
12372 * @private
12373 */
12374 adjustTimeStep() {
12375 const factor = 1.2; // Factor for increasing the timestep on success.
12376
12377 // we compare the two steps. if it is acceptable we double the step.
12378 if (this._evaluateStepQuality() === true) {
12379 this.timestep = factor * this.timestep;
12380 } else {
12381 // if not, we decrease the step to a minimum of the options timestep.
12382 // if the decreased timestep is smaller than the options step, we do not reset the counter
12383 // we assume that the options timestep is stable enough.
12384 if (this.timestep / factor < this.options.timestep) {
12385 this.timestep = this.options.timestep;
12386 } else {
12387 // if the timestep was larger than 2 times the option one we check the adaptivity again to ensure
12388 // that large instabilities do not form.
12389 this.adaptiveCounter = -1; // check again next iteration
12390 this.timestep = Math.max(this.options.timestep, this.timestep / factor);
12391 }
12392 }
12393 }
12394
12395 /**
12396 * A single simulation step (or 'tick') in the physics simulation
12397 *
12398 * @private
12399 */
12400 physicsTick() {
12401 this._startStabilizing(); // this ensures that there is no start event when the network is already stable.
12402 if (this.stabilized === true) return;
12403
12404 // adaptivity means the timestep adapts to the situation, only applicable for stabilization
12405 if (
12406 this.adaptiveTimestep === true &&
12407 this.adaptiveTimestepEnabled === true
12408 ) {
12409 // timestep remains stable for "interval" iterations.
12410 const doAdaptive = this.adaptiveCounter % this.adaptiveInterval === 0;
12411
12412 if (doAdaptive) {
12413 // first the big step and revert.
12414 this.timestep = 2 * this.timestep;
12415 this.physicsStep();
12416 this.revert(); // saves the reference state
12417
12418 // now the normal step. Since this is the last step, it is the more stable one and we will take this.
12419 this.timestep = 0.5 * this.timestep;
12420
12421 // since it's half the step, we do it twice.
12422 this.physicsStep();
12423 this.physicsStep();
12424
12425 this.adjustTimeStep();
12426 } else {
12427 this.physicsStep(); // normal step, keeping timestep constant
12428 }
12429
12430 this.adaptiveCounter += 1;
12431 } else {
12432 // case for the static timestep, we reset it to the one in options and take a normal step.
12433 this.timestep = this.options.timestep;
12434 this.physicsStep();
12435 }
12436
12437 if (this.stabilized === true) this.revert();
12438 this.stabilizationIterations++;
12439 }
12440
12441 /**
12442 * Nodes and edges can have the physics toggles on or off. A collection of indices is created here so we can skip the check all the time.
12443 *
12444 * @private
12445 */
12446 updatePhysicsData() {
12447 this.physicsBody.forces = {};
12448 this.physicsBody.physicsNodeIndices = [];
12449 this.physicsBody.physicsEdgeIndices = [];
12450 const nodes = this.body.nodes;
12451 const edges = this.body.edges;
12452
12453 // get node indices for physics
12454 for (const nodeId in nodes) {
12455 if (Object.prototype.hasOwnProperty.call(nodes, nodeId)) {
12456 if (nodes[nodeId].options.physics === true) {
12457 this.physicsBody.physicsNodeIndices.push(nodes[nodeId].id);
12458 }
12459 }
12460 }
12461
12462 // get edge indices for physics
12463 for (const edgeId in edges) {
12464 if (Object.prototype.hasOwnProperty.call(edges, edgeId)) {
12465 if (edges[edgeId].options.physics === true) {
12466 this.physicsBody.physicsEdgeIndices.push(edges[edgeId].id);
12467 }
12468 }
12469 }
12470
12471 // get the velocity and the forces vector
12472 for (let i = 0; i < this.physicsBody.physicsNodeIndices.length; i++) {
12473 const nodeId = this.physicsBody.physicsNodeIndices[i];
12474 this.physicsBody.forces[nodeId] = { x: 0, y: 0 };
12475
12476 // forces can be reset because they are recalculated. Velocities have to persist.
12477 if (this.physicsBody.velocities[nodeId] === undefined) {
12478 this.physicsBody.velocities[nodeId] = { x: 0, y: 0 };
12479 }
12480 }
12481
12482 // clean deleted nodes from the velocity vector
12483 for (const nodeId in this.physicsBody.velocities) {
12484 if (nodes[nodeId] === undefined) {
12485 delete this.physicsBody.velocities[nodeId];
12486 }
12487 }
12488 }
12489
12490 /**
12491 * Revert the simulation one step. This is done so after stabilization, every new start of the simulation will also say stabilized.
12492 */
12493 revert() {
12494 const nodeIds = Object.keys(this.previousStates);
12495 const nodes = this.body.nodes;
12496 const velocities = this.physicsBody.velocities;
12497 this.referenceState = {};
12498
12499 for (let i = 0; i < nodeIds.length; i++) {
12500 const nodeId = nodeIds[i];
12501 if (nodes[nodeId] !== undefined) {
12502 if (nodes[nodeId].options.physics === true) {
12503 this.referenceState[nodeId] = {
12504 positions: { x: nodes[nodeId].x, y: nodes[nodeId].y },
12505 };
12506 velocities[nodeId].x = this.previousStates[nodeId].vx;
12507 velocities[nodeId].y = this.previousStates[nodeId].vy;
12508 nodes[nodeId].x = this.previousStates[nodeId].x;
12509 nodes[nodeId].y = this.previousStates[nodeId].y;
12510 }
12511 } else {
12512 delete this.previousStates[nodeId];
12513 }
12514 }
12515 }
12516
12517 /**
12518 * This compares the reference state to the current state
12519 *
12520 * @returns {boolean}
12521 * @private
12522 */
12523 _evaluateStepQuality() {
12524 let dx, dy, dpos;
12525 const nodes = this.body.nodes;
12526 const reference = this.referenceState;
12527 const posThreshold = 0.3;
12528
12529 for (const nodeId in this.referenceState) {
12530 if (
12531 Object.prototype.hasOwnProperty.call(this.referenceState, nodeId) &&
12532 nodes[nodeId] !== undefined
12533 ) {
12534 dx = nodes[nodeId].x - reference[nodeId].positions.x;
12535 dy = nodes[nodeId].y - reference[nodeId].positions.y;
12536
12537 dpos = Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2));
12538
12539 if (dpos > posThreshold) {
12540 return false;
12541 }
12542 }
12543 }
12544 return true;
12545 }
12546
12547 /**
12548 * move the nodes one timestep and check if they are stabilized
12549 */
12550 moveNodes() {
12551 const nodeIndices = this.physicsBody.physicsNodeIndices;
12552 let maxNodeVelocity = 0;
12553 let averageNodeVelocity = 0;
12554
12555 // the velocity threshold (energy in the system) for the adaptivity toggle
12556 const velocityAdaptiveThreshold = 5;
12557
12558 for (let i = 0; i < nodeIndices.length; i++) {
12559 const nodeId = nodeIndices[i];
12560 const nodeVelocity = this._performStep(nodeId);
12561 // stabilized is true if stabilized is true and velocity is smaller than vmin --> all nodes must be stabilized
12562 maxNodeVelocity = Math.max(maxNodeVelocity, nodeVelocity);
12563 averageNodeVelocity += nodeVelocity;
12564 }
12565
12566 // evaluating the stabilized and adaptiveTimestepEnabled conditions
12567 this.adaptiveTimestepEnabled =
12568 averageNodeVelocity / nodeIndices.length < velocityAdaptiveThreshold;
12569 this.stabilized = maxNodeVelocity < this.options.minVelocity;
12570 }
12571
12572 /**
12573 * Calculate new velocity for a coordinate direction
12574 *
12575 * @param {number} v velocity for current coordinate
12576 * @param {number} f regular force for current coordinate
12577 * @param {number} m mass of current node
12578 * @returns {number} new velocity for current coordinate
12579 * @private
12580 */
12581 calculateComponentVelocity(v, f, m) {
12582 const df = this.modelOptions.damping * v; // damping force
12583 const a = (f - df) / m; // acceleration
12584
12585 v += a * this.timestep;
12586
12587 // Put a limit on the velocities if it is really high
12588 const maxV = this.options.maxVelocity || 1e9;
12589 if (Math.abs(v) > maxV) {
12590 v = v > 0 ? maxV : -maxV;
12591 }
12592
12593 return v;
12594 }
12595
12596 /**
12597 * Perform the actual step
12598 *
12599 * @param {Node.id} nodeId
12600 * @returns {number} the new velocity of given node
12601 * @private
12602 */
12603 _performStep(nodeId) {
12604 const node = this.body.nodes[nodeId];
12605 const force = this.physicsBody.forces[nodeId];
12606
12607 if (this.options.wind) {
12608 force.x += this.options.wind.x;
12609 force.y += this.options.wind.y;
12610 }
12611
12612 const velocity = this.physicsBody.velocities[nodeId];
12613
12614 // store the state so we can revert
12615 this.previousStates[nodeId] = {
12616 x: node.x,
12617 y: node.y,
12618 vx: velocity.x,
12619 vy: velocity.y,
12620 };
12621
12622 if (node.options.fixed.x === false) {
12623 velocity.x = this.calculateComponentVelocity(
12624 velocity.x,
12625 force.x,
12626 node.options.mass
12627 );
12628 node.x += velocity.x * this.timestep;
12629 } else {
12630 force.x = 0;
12631 velocity.x = 0;
12632 }
12633
12634 if (node.options.fixed.y === false) {
12635 velocity.y = this.calculateComponentVelocity(
12636 velocity.y,
12637 force.y,
12638 node.options.mass
12639 );
12640 node.y += velocity.y * this.timestep;
12641 } else {
12642 force.y = 0;
12643 velocity.y = 0;
12644 }
12645
12646 const totalVelocity = Math.sqrt(
12647 Math.pow(velocity.x, 2) + Math.pow(velocity.y, 2)
12648 );
12649 return totalVelocity;
12650 }
12651
12652 /**
12653 * When initializing and stabilizing, we can freeze nodes with a predefined position.
12654 * This greatly speeds up stabilization because only the supportnodes for the smoothCurves have to settle.
12655 *
12656 * @private
12657 */
12658 _freezeNodes() {
12659 const nodes = this.body.nodes;
12660 for (const id in nodes) {
12661 if (Object.prototype.hasOwnProperty.call(nodes, id)) {
12662 if (nodes[id].x && nodes[id].y) {
12663 const fixed = nodes[id].options.fixed;
12664 this.freezeCache[id] = { x: fixed.x, y: fixed.y };
12665 fixed.x = true;
12666 fixed.y = true;
12667 }
12668 }
12669 }
12670 }
12671
12672 /**
12673 * Unfreezes the nodes that have been frozen by _freezeDefinedNodes.
12674 *
12675 * @private
12676 */
12677 _restoreFrozenNodes() {
12678 const nodes = this.body.nodes;
12679 for (const id in nodes) {
12680 if (Object.prototype.hasOwnProperty.call(nodes, id)) {
12681 if (this.freezeCache[id] !== undefined) {
12682 nodes[id].options.fixed.x = this.freezeCache[id].x;
12683 nodes[id].options.fixed.y = this.freezeCache[id].y;
12684 }
12685 }
12686 }
12687 this.freezeCache = {};
12688 }
12689
12690 /**
12691 * Find a stable position for all nodes
12692 *
12693 * @param {number} [iterations=this.options.stabilization.iterations]
12694 */
12695 stabilize(iterations = this.options.stabilization.iterations) {
12696 if (typeof iterations !== "number") {
12697 iterations = this.options.stabilization.iterations;
12698 console.error(
12699 "The stabilize method needs a numeric amount of iterations. Switching to default: ",
12700 iterations
12701 );
12702 }
12703
12704 if (this.physicsBody.physicsNodeIndices.length === 0) {
12705 this.ready = true;
12706 return;
12707 }
12708
12709 // enable adaptive timesteps
12710 this.adaptiveTimestep = this.options.adaptiveTimestep;
12711
12712 // this sets the width of all nodes initially which could be required for the avoidOverlap
12713 this.body.emitter.emit("_resizeNodes");
12714
12715 this.stopSimulation(); // stop the render loop
12716 this.stabilized = false;
12717
12718 // block redraw requests
12719 this.body.emitter.emit("_blockRedraw");
12720 this.targetIterations = iterations;
12721
12722 // start the stabilization
12723 if (this.options.stabilization.onlyDynamicEdges === true) {
12724 this._freezeNodes();
12725 }
12726 this.stabilizationIterations = 0;
12727
12728 setTimeout(() => this._stabilizationBatch(), 0);
12729 }
12730
12731 /**
12732 * If not already stabilizing, start it and emit a start event.
12733 *
12734 * @returns {boolean} true if stabilization started with this call
12735 * @private
12736 */
12737 _startStabilizing() {
12738 if (this.startedStabilization === true) return false;
12739
12740 this.body.emitter.emit("startStabilizing");
12741 this.startedStabilization = true;
12742 return true;
12743 }
12744
12745 /**
12746 * One batch of stabilization
12747 *
12748 * @private
12749 */
12750 _stabilizationBatch() {
12751 const running = () =>
12752 this.stabilized === false &&
12753 this.stabilizationIterations < this.targetIterations;
12754
12755 const sendProgress = () => {
12756 this.body.emitter.emit("stabilizationProgress", {
12757 iterations: this.stabilizationIterations,
12758 total: this.targetIterations,
12759 });
12760 };
12761
12762 if (this._startStabilizing()) {
12763 sendProgress(); // Ensure that there is at least one start event.
12764 }
12765
12766 let count = 0;
12767 while (running() && count < this.options.stabilization.updateInterval) {
12768 this.physicsTick();
12769 count++;
12770 }
12771
12772 sendProgress();
12773
12774 if (running()) {
12775 setTimeout(this._stabilizationBatch.bind(this), 0);
12776 } else {
12777 this._finalizeStabilization();
12778 }
12779 }
12780
12781 /**
12782 * Wrap up the stabilization, fit and emit the events.
12783 *
12784 * @private
12785 */
12786 _finalizeStabilization() {
12787 this.body.emitter.emit("_allowRedraw");
12788 if (this.options.stabilization.fit === true) {
12789 this.body.emitter.emit("fit");
12790 }
12791
12792 if (this.options.stabilization.onlyDynamicEdges === true) {
12793 this._restoreFrozenNodes();
12794 }
12795
12796 this.body.emitter.emit("stabilizationIterationsDone");
12797 this.body.emitter.emit("_requestRedraw");
12798
12799 if (this.stabilized === true) {
12800 this._emitStabilized();
12801 } else {
12802 this.startSimulation();
12803 }
12804
12805 this.ready = true;
12806 }
12807
12808 //--------------------------- DEBUGGING BELOW ---------------------------//
12809
12810 /**
12811 * Debug function that display arrows for the forces currently active in the network.
12812 *
12813 * Use this when debugging only.
12814 *
12815 * @param {CanvasRenderingContext2D} ctx
12816 * @private
12817 */
12818 _drawForces(ctx) {
12819 for (let i = 0; i < this.physicsBody.physicsNodeIndices.length; i++) {
12820 const index = this.physicsBody.physicsNodeIndices[i];
12821 const node = this.body.nodes[index];
12822 const force = this.physicsBody.forces[index];
12823 const factor = 20;
12824 const colorFactor = 0.03;
12825 const forceSize = Math.sqrt(Math.pow(force.x, 2) + Math.pow(force.x, 2));
12826
12827 const size = Math.min(Math.max(5, forceSize), 15);
12828 const arrowSize = 3 * size;
12829
12830 const color = HSVToHex(
12831 (180 - Math.min(1, Math.max(0, colorFactor * forceSize)) * 180) / 360,
12832 1,
12833 1
12834 );
12835
12836 const point = {
12837 x: node.x + factor * force.x,
12838 y: node.y + factor * force.y,
12839 };
12840
12841 ctx.lineWidth = size;
12842 ctx.strokeStyle = color;
12843 ctx.beginPath();
12844 ctx.moveTo(node.x, node.y);
12845 ctx.lineTo(point.x, point.y);
12846 ctx.stroke();
12847
12848 const angle = Math.atan2(force.y, force.x);
12849 ctx.fillStyle = color;
12850 EndPoints.draw(ctx, {
12851 type: "arrow",
12852 point: point,
12853 angle: angle,
12854 length: arrowSize,
12855 });
12856 ctx.fill();
12857 }
12858 }
12859}
12860
12861/**
12862 * Utility Class
12863 */
12864class NetworkUtil {
12865 /**
12866 * @ignore
12867 */
12868 constructor() {}
12869
12870 /**
12871 * Find the center position of the network considering the bounding boxes
12872 *
12873 * @param {Array.<Node>} allNodes
12874 * @param {Array.<Node>} [specificNodes=[]]
12875 * @returns {{minX: number, maxX: number, minY: number, maxY: number}}
12876 * @static
12877 */
12878 static getRange(allNodes, specificNodes = []) {
12879 let minY = 1e9,
12880 maxY = -1e9,
12881 minX = 1e9,
12882 maxX = -1e9,
12883 node;
12884 if (specificNodes.length > 0) {
12885 for (let i = 0; i < specificNodes.length; i++) {
12886 node = allNodes[specificNodes[i]];
12887 if (minX > node.shape.boundingBox.left) {
12888 minX = node.shape.boundingBox.left;
12889 }
12890 if (maxX < node.shape.boundingBox.right) {
12891 maxX = node.shape.boundingBox.right;
12892 }
12893 if (minY > node.shape.boundingBox.top) {
12894 minY = node.shape.boundingBox.top;
12895 } // top is negative, bottom is positive
12896 if (maxY < node.shape.boundingBox.bottom) {
12897 maxY = node.shape.boundingBox.bottom;
12898 } // top is negative, bottom is positive
12899 }
12900 }
12901
12902 if (minX === 1e9 && maxX === -1e9 && minY === 1e9 && maxY === -1e9) {
12903 (minY = 0), (maxY = 0), (minX = 0), (maxX = 0);
12904 }
12905 return { minX: minX, maxX: maxX, minY: minY, maxY: maxY };
12906 }
12907
12908 /**
12909 * Find the center position of the network
12910 *
12911 * @param {Array.<Node>} allNodes
12912 * @param {Array.<Node>} [specificNodes=[]]
12913 * @returns {{minX: number, maxX: number, minY: number, maxY: number}}
12914 * @static
12915 */
12916 static getRangeCore(allNodes, specificNodes = []) {
12917 let minY = 1e9,
12918 maxY = -1e9,
12919 minX = 1e9,
12920 maxX = -1e9,
12921 node;
12922 if (specificNodes.length > 0) {
12923 for (let i = 0; i < specificNodes.length; i++) {
12924 node = allNodes[specificNodes[i]];
12925 if (minX > node.x) {
12926 minX = node.x;
12927 }
12928 if (maxX < node.x) {
12929 maxX = node.x;
12930 }
12931 if (minY > node.y) {
12932 minY = node.y;
12933 } // top is negative, bottom is positive
12934 if (maxY < node.y) {
12935 maxY = node.y;
12936 } // top is negative, bottom is positive
12937 }
12938 }
12939
12940 if (minX === 1e9 && maxX === -1e9 && minY === 1e9 && maxY === -1e9) {
12941 (minY = 0), (maxY = 0), (minX = 0), (maxX = 0);
12942 }
12943 return { minX: minX, maxX: maxX, minY: minY, maxY: maxY };
12944 }
12945
12946 /**
12947 * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
12948 * @returns {{x: number, y: number}}
12949 * @static
12950 */
12951 static findCenter(range) {
12952 return {
12953 x: 0.5 * (range.maxX + range.minX),
12954 y: 0.5 * (range.maxY + range.minY),
12955 };
12956 }
12957
12958 /**
12959 * This returns a clone of the options or options of the edge or node to be used for construction of new edges or check functions for new nodes.
12960 *
12961 * @param {vis.Item} item
12962 * @param {'node'|undefined} type
12963 * @returns {{}}
12964 * @static
12965 */
12966 static cloneOptions(item, type) {
12967 const clonedOptions = {};
12968 if (type === undefined || type === "node") {
12969 deepExtend(clonedOptions, item.options, true);
12970 clonedOptions.x = item.x;
12971 clonedOptions.y = item.y;
12972 clonedOptions.amountOfConnections = item.edges.length;
12973 } else {
12974 deepExtend(clonedOptions, item.options, true);
12975 }
12976 return clonedOptions;
12977 }
12978}
12979
12980/**
12981 * A Cluster is a special Node that allows a group of Nodes positioned closely together
12982 * to be represented by a single Cluster Node.
12983 *
12984 * @augments Node
12985 */
12986class Cluster extends Node {
12987 /**
12988 * @param {object} options
12989 * @param {object} body
12990 * @param {Array.<HTMLImageElement>}imagelist
12991 * @param {Array} grouplist
12992 * @param {object} globalOptions
12993 * @param {object} defaultOptions Global default options for nodes
12994 */
12995 constructor(
12996 options,
12997 body,
12998 imagelist,
12999 grouplist,
13000 globalOptions,
13001 defaultOptions
13002 ) {
13003 super(options, body, imagelist, grouplist, globalOptions, defaultOptions);
13004
13005 this.isCluster = true;
13006 this.containedNodes = {};
13007 this.containedEdges = {};
13008 }
13009
13010 /**
13011 * Transfer child cluster data to current and disconnect the child cluster.
13012 *
13013 * Please consult the header comment in 'Clustering.js' for the fields set here.
13014 *
13015 * @param {string|number} childClusterId id of child cluster to open
13016 */
13017 _openChildCluster(childClusterId) {
13018 const childCluster = this.body.nodes[childClusterId];
13019 if (this.containedNodes[childClusterId] === undefined) {
13020 throw new Error(
13021 "node with id: " + childClusterId + " not in current cluster"
13022 );
13023 }
13024 if (!childCluster.isCluster) {
13025 throw new Error("node with id: " + childClusterId + " is not a cluster");
13026 }
13027
13028 // Disconnect child cluster from current cluster
13029 delete this.containedNodes[childClusterId];
13030 forEach(childCluster.edges, (edge) => {
13031 delete this.containedEdges[edge.id];
13032 });
13033
13034 // Transfer nodes and edges
13035 forEach(childCluster.containedNodes, (node, nodeId) => {
13036 this.containedNodes[nodeId] = node;
13037 });
13038 childCluster.containedNodes = {};
13039
13040 forEach(childCluster.containedEdges, (edge, edgeId) => {
13041 this.containedEdges[edgeId] = edge;
13042 });
13043 childCluster.containedEdges = {};
13044
13045 // Transfer edges within cluster edges which are clustered
13046 forEach(childCluster.edges, (clusterEdge) => {
13047 forEach(this.edges, (parentClusterEdge) => {
13048 // Assumption: a clustered edge can only be present in a single clustering edge
13049 // Not tested here
13050 const index = parentClusterEdge.clusteringEdgeReplacingIds.indexOf(
13051 clusterEdge.id
13052 );
13053 if (index === -1) return;
13054
13055 forEach(clusterEdge.clusteringEdgeReplacingIds, (srcId) => {
13056 parentClusterEdge.clusteringEdgeReplacingIds.push(srcId);
13057
13058 // Maintain correct bookkeeping for transferred edge
13059 this.body.edges[srcId].edgeReplacedById = parentClusterEdge.id;
13060 });
13061
13062 // Remove cluster edge from parent cluster edge
13063 parentClusterEdge.clusteringEdgeReplacingIds.splice(index, 1);
13064 });
13065 });
13066 childCluster.edges = [];
13067 }
13068}
13069
13070/* ===========================================================================
13071
13072# TODO
13073
13074- `edgeReplacedById` not cleaned up yet on cluster edge removal
13075- allowSingleNodeCluster could be a global option as well; currently needs to always
13076 be passed to clustering methods
13077
13078----------------------------------------------
13079
13080# State Model for Clustering
13081
13082The total state for clustering is non-trivial. It is useful to have a model
13083available as to how it works. The following documents the relevant state items.
13084
13085
13086## Network State
13087
13088The following `network`-members are relevant to clustering:
13089
13090- `body.nodes` - all nodes actively participating in the network
13091- `body.edges` - same for edges
13092- `body.nodeIndices` - id's of nodes that are visible at a given moment
13093- `body.edgeIndices` - same for edges
13094
13095This includes:
13096
13097- helper nodes for dragging in `manipulation`
13098- helper nodes for edge type `dynamic`
13099- cluster nodes and edges
13100- there may be more than this.
13101
13102A node/edge may be missing in the `Indices` member if:
13103
13104- it is a helper node
13105- the node or edge state has option `hidden` set
13106- It is not visible due to clustering
13107
13108
13109## Clustering State
13110
13111For the hashes, the id's of the nodes/edges are used as key.
13112
13113Member `network.clustering` contains the following items:
13114
13115- `clusteredNodes` - hash with values: { clusterId: <id of cluster>, node: <node instance>}
13116- `clusteredEdges` - hash with values: restore information for given edge
13117
13118
13119Due to nesting of clusters, these members can contain cluster nodes and edges as well.
13120
13121The important thing to note here, is that the clustered nodes and edges also
13122appear in the members of the cluster nodes. For data update, it is therefore
13123important to scan these lists as well as the cluster nodes.
13124
13125
13126### Cluster Node
13127
13128A cluster node has the following extra fields:
13129
13130- `isCluster : true` - indication that this is a cluster node
13131- `containedNodes` - hash of nodes contained in this cluster
13132- `containedEdges` - same for edges
13133- `edges` - array of cluster edges for this node
13134
13135
13136**NOTE:**
13137
13138- `containedEdges` can also contain edges which are not clustered; e.g. an edge
13139 connecting two nodes in the same cluster.
13140
13141
13142### Cluster Edge
13143
13144These are the items in the `edges` member of a clustered node. They have the
13145following relevant members:
13146
13147- 'clusteringEdgeReplacingIds` - array of id's of edges replaced by this edge
13148
13149Note that it's possible to nest clusters, so that `clusteringEdgeReplacingIds`
13150can contain edge id's of other clusters.
13151
13152
13153### Clustered Edge
13154
13155This is any edge contained by a cluster edge. It gets the following additional
13156member:
13157
13158- `edgeReplacedById` - id of the cluster edge in which current edge is clustered
13159
13160
13161 =========================================================================== */
13162
13163/**
13164 * The clustering engine
13165 */
13166class ClusterEngine {
13167 /**
13168 * @param {object} body
13169 */
13170 constructor(body) {
13171 this.body = body;
13172 this.clusteredNodes = {}; // key: node id, value: { clusterId: <id of cluster>, node: <node instance>}
13173 this.clusteredEdges = {}; // key: edge id, value: restore information for given edge
13174
13175 this.options = {};
13176 this.defaultOptions = {};
13177 Object.assign(this.options, this.defaultOptions);
13178
13179 this.body.emitter.on("_resetData", () => {
13180 this.clusteredNodes = {};
13181 this.clusteredEdges = {};
13182 });
13183 }
13184
13185 /**
13186 *
13187 * @param {number} hubsize
13188 * @param {object} options
13189 */
13190 clusterByHubsize(hubsize, options) {
13191 if (hubsize === undefined) {
13192 hubsize = this._getHubSize();
13193 } else if (typeof hubsize === "object") {
13194 options = this._checkOptions(hubsize);
13195 hubsize = this._getHubSize();
13196 }
13197
13198 const nodesToCluster = [];
13199 for (let i = 0; i < this.body.nodeIndices.length; i++) {
13200 const node = this.body.nodes[this.body.nodeIndices[i]];
13201 if (node.edges.length >= hubsize) {
13202 nodesToCluster.push(node.id);
13203 }
13204 }
13205
13206 for (let i = 0; i < nodesToCluster.length; i++) {
13207 this.clusterByConnection(nodesToCluster[i], options, true);
13208 }
13209
13210 this.body.emitter.emit("_dataChanged");
13211 }
13212
13213 /**
13214 * loop over all nodes, check if they adhere to the condition and cluster if needed.
13215 *
13216 * @param {object} options
13217 * @param {boolean} [refreshData=true]
13218 */
13219 cluster(options = {}, refreshData = true) {
13220 if (options.joinCondition === undefined) {
13221 throw new Error(
13222 "Cannot call clusterByNodeData without a joinCondition function in the options."
13223 );
13224 }
13225
13226 // check if the options object is fine, append if needed
13227 options = this._checkOptions(options);
13228
13229 const childNodesObj = {};
13230 const childEdgesObj = {};
13231
13232 // collect the nodes that will be in the cluster
13233 forEach(this.body.nodes, (node, nodeId) => {
13234 if (node.options && options.joinCondition(node.options) === true) {
13235 childNodesObj[nodeId] = node;
13236
13237 // collect the edges that will be in the cluster
13238 forEach(node.edges, (edge) => {
13239 if (this.clusteredEdges[edge.id] === undefined) {
13240 childEdgesObj[edge.id] = edge;
13241 }
13242 });
13243 }
13244 });
13245
13246 this._cluster(childNodesObj, childEdgesObj, options, refreshData);
13247 }
13248
13249 /**
13250 * Cluster all nodes in the network that have only X edges
13251 *
13252 * @param {number} edgeCount
13253 * @param {object} options
13254 * @param {boolean} [refreshData=true]
13255 */
13256 clusterByEdgeCount(edgeCount, options, refreshData = true) {
13257 options = this._checkOptions(options);
13258 const clusters = [];
13259 const usedNodes = {};
13260 let edge, edges, relevantEdgeCount;
13261 // collect the nodes that will be in the cluster
13262 for (let i = 0; i < this.body.nodeIndices.length; i++) {
13263 const childNodesObj = {};
13264 const childEdgesObj = {};
13265 const nodeId = this.body.nodeIndices[i];
13266 const node = this.body.nodes[nodeId];
13267
13268 // if this node is already used in another cluster this session, we do not have to re-evaluate it.
13269 if (usedNodes[nodeId] === undefined) {
13270 relevantEdgeCount = 0;
13271 edges = [];
13272 for (let j = 0; j < node.edges.length; j++) {
13273 edge = node.edges[j];
13274 if (this.clusteredEdges[edge.id] === undefined) {
13275 if (edge.toId !== edge.fromId) {
13276 relevantEdgeCount++;
13277 }
13278 edges.push(edge);
13279 }
13280 }
13281
13282 // this node qualifies, we collect its neighbours to start the clustering process.
13283 if (relevantEdgeCount === edgeCount) {
13284 const checkJoinCondition = function (node) {
13285 if (
13286 options.joinCondition === undefined ||
13287 options.joinCondition === null
13288 ) {
13289 return true;
13290 }
13291
13292 const clonedOptions = NetworkUtil.cloneOptions(node);
13293 return options.joinCondition(clonedOptions);
13294 };
13295
13296 let gatheringSuccessful = true;
13297 for (let j = 0; j < edges.length; j++) {
13298 edge = edges[j];
13299 const childNodeId = this._getConnectedId(edge, nodeId);
13300 // add the nodes to the list by the join condition.
13301 if (checkJoinCondition(node)) {
13302 childEdgesObj[edge.id] = edge;
13303 childNodesObj[nodeId] = node;
13304 childNodesObj[childNodeId] = this.body.nodes[childNodeId];
13305 usedNodes[nodeId] = true;
13306 } else {
13307 // this node does not qualify after all.
13308 gatheringSuccessful = false;
13309 break;
13310 }
13311 }
13312
13313 // add to the cluster queue
13314 if (
13315 Object.keys(childNodesObj).length > 0 &&
13316 Object.keys(childEdgesObj).length > 0 &&
13317 gatheringSuccessful === true
13318 ) {
13319 /**
13320 * Search for cluster data that contains any of the node id's
13321 *
13322 * @returns {boolean} true if no joinCondition, otherwise return value of joinCondition
13323 */
13324 const findClusterData = function () {
13325 for (let n = 0; n < clusters.length; ++n) {
13326 // Search for a cluster containing any of the node id's
13327 for (const m in childNodesObj) {
13328 if (clusters[n].nodes[m] !== undefined) {
13329 return clusters[n];
13330 }
13331 }
13332 }
13333
13334 return undefined;
13335 };
13336
13337 // If any of the found nodes is part of a cluster found in this method,
13338 // add the current values to that cluster
13339 const foundCluster = findClusterData();
13340 if (foundCluster !== undefined) {
13341 // Add nodes to found cluster if not present
13342 for (const m in childNodesObj) {
13343 if (foundCluster.nodes[m] === undefined) {
13344 foundCluster.nodes[m] = childNodesObj[m];
13345 }
13346 }
13347
13348 // Add edges to found cluster, if not present
13349 for (const m in childEdgesObj) {
13350 if (foundCluster.edges[m] === undefined) {
13351 foundCluster.edges[m] = childEdgesObj[m];
13352 }
13353 }
13354 } else {
13355 // Create a new cluster group
13356 clusters.push({ nodes: childNodesObj, edges: childEdgesObj });
13357 }
13358 }
13359 }
13360 }
13361 }
13362
13363 for (let i = 0; i < clusters.length; i++) {
13364 this._cluster(clusters[i].nodes, clusters[i].edges, options, false);
13365 }
13366
13367 if (refreshData === true) {
13368 this.body.emitter.emit("_dataChanged");
13369 }
13370 }
13371
13372 /**
13373 * Cluster all nodes in the network that have only 1 edge
13374 *
13375 * @param {object} options
13376 * @param {boolean} [refreshData=true]
13377 */
13378 clusterOutliers(options, refreshData = true) {
13379 this.clusterByEdgeCount(1, options, refreshData);
13380 }
13381
13382 /**
13383 * Cluster all nodes in the network that have only 2 edge
13384 *
13385 * @param {object} options
13386 * @param {boolean} [refreshData=true]
13387 */
13388 clusterBridges(options, refreshData = true) {
13389 this.clusterByEdgeCount(2, options, refreshData);
13390 }
13391
13392 /**
13393 * suck all connected nodes of a node into the node.
13394 *
13395 * @param {Node.id} nodeId
13396 * @param {object} options
13397 * @param {boolean} [refreshData=true]
13398 */
13399 clusterByConnection(nodeId, options, refreshData = true) {
13400 // kill conditions
13401 if (nodeId === undefined) {
13402 throw new Error("No nodeId supplied to clusterByConnection!");
13403 }
13404 if (this.body.nodes[nodeId] === undefined) {
13405 throw new Error(
13406 "The nodeId given to clusterByConnection does not exist!"
13407 );
13408 }
13409
13410 const node = this.body.nodes[nodeId];
13411 options = this._checkOptions(options, node);
13412 if (options.clusterNodeProperties.x === undefined) {
13413 options.clusterNodeProperties.x = node.x;
13414 }
13415 if (options.clusterNodeProperties.y === undefined) {
13416 options.clusterNodeProperties.y = node.y;
13417 }
13418 if (options.clusterNodeProperties.fixed === undefined) {
13419 options.clusterNodeProperties.fixed = {};
13420 options.clusterNodeProperties.fixed.x = node.options.fixed.x;
13421 options.clusterNodeProperties.fixed.y = node.options.fixed.y;
13422 }
13423
13424 const childNodesObj = {};
13425 const childEdgesObj = {};
13426 const parentNodeId = node.id;
13427 const parentClonedOptions = NetworkUtil.cloneOptions(node);
13428 childNodesObj[parentNodeId] = node;
13429
13430 // collect the nodes that will be in the cluster
13431 for (let i = 0; i < node.edges.length; i++) {
13432 const edge = node.edges[i];
13433 if (this.clusteredEdges[edge.id] === undefined) {
13434 const childNodeId = this._getConnectedId(edge, parentNodeId);
13435
13436 // if the child node is not in a cluster
13437 if (this.clusteredNodes[childNodeId] === undefined) {
13438 if (childNodeId !== parentNodeId) {
13439 if (options.joinCondition === undefined) {
13440 childEdgesObj[edge.id] = edge;
13441 childNodesObj[childNodeId] = this.body.nodes[childNodeId];
13442 } else {
13443 // clone the options and insert some additional parameters that could be interesting.
13444 const childClonedOptions = NetworkUtil.cloneOptions(
13445 this.body.nodes[childNodeId]
13446 );
13447 if (
13448 options.joinCondition(
13449 parentClonedOptions,
13450 childClonedOptions
13451 ) === true
13452 ) {
13453 childEdgesObj[edge.id] = edge;
13454 childNodesObj[childNodeId] = this.body.nodes[childNodeId];
13455 }
13456 }
13457 } else {
13458 // swallow the edge if it is self-referencing.
13459 childEdgesObj[edge.id] = edge;
13460 }
13461 }
13462 }
13463 }
13464 const childNodeIDs = Object.keys(childNodesObj).map(function (childNode) {
13465 return childNodesObj[childNode].id;
13466 });
13467
13468 for (const childNodeKey in childNodesObj) {
13469 if (!Object.prototype.hasOwnProperty.call(childNodesObj, childNodeKey))
13470 continue;
13471
13472 const childNode = childNodesObj[childNodeKey];
13473 for (let y = 0; y < childNode.edges.length; y++) {
13474 const childEdge = childNode.edges[y];
13475 if (
13476 childNodeIDs.indexOf(this._getConnectedId(childEdge, childNode.id)) >
13477 -1
13478 ) {
13479 childEdgesObj[childEdge.id] = childEdge;
13480 }
13481 }
13482 }
13483 this._cluster(childNodesObj, childEdgesObj, options, refreshData);
13484 }
13485
13486 /**
13487 * This function creates the edges that will be attached to the cluster
13488 * It looks for edges that are connected to the nodes from the "outside' of the cluster.
13489 *
13490 * @param {{Node.id: vis.Node}} childNodesObj
13491 * @param {{vis.Edge.id: vis.Edge}} childEdgesObj
13492 * @param {object} clusterNodeProperties
13493 * @param {object} clusterEdgeProperties
13494 * @private
13495 */
13496 _createClusterEdges(
13497 childNodesObj,
13498 childEdgesObj,
13499 clusterNodeProperties,
13500 clusterEdgeProperties
13501 ) {
13502 let edge, childNodeId, childNode, toId, fromId, otherNodeId;
13503
13504 // loop over all child nodes and their edges to find edges going out of the cluster
13505 // these edges will be replaced by clusterEdges.
13506 const childKeys = Object.keys(childNodesObj);
13507 const createEdges = [];
13508 for (let i = 0; i < childKeys.length; i++) {
13509 childNodeId = childKeys[i];
13510 childNode = childNodesObj[childNodeId];
13511
13512 // construct new edges from the cluster to others
13513 for (let j = 0; j < childNode.edges.length; j++) {
13514 edge = childNode.edges[j];
13515 // we only handle edges that are visible to the system, not the disabled ones from the clustering process.
13516 if (this.clusteredEdges[edge.id] === undefined) {
13517 // self-referencing edges will be added to the "hidden" list
13518 if (edge.toId == edge.fromId) {
13519 childEdgesObj[edge.id] = edge;
13520 } else {
13521 // set up the from and to.
13522 if (edge.toId == childNodeId) {
13523 // this is a double equals because ints and strings can be interchanged here.
13524 toId = clusterNodeProperties.id;
13525 fromId = edge.fromId;
13526 otherNodeId = fromId;
13527 } else {
13528 toId = edge.toId;
13529 fromId = clusterNodeProperties.id;
13530 otherNodeId = toId;
13531 }
13532 }
13533
13534 // Only edges from the cluster outwards are being replaced.
13535 if (childNodesObj[otherNodeId] === undefined) {
13536 createEdges.push({ edge: edge, fromId: fromId, toId: toId });
13537 }
13538 }
13539 }
13540 }
13541
13542 //
13543 // Here we actually create the replacement edges.
13544 //
13545 // We could not do this in the loop above as the creation process
13546 // would add an edge to the edges array we are iterating over.
13547 //
13548 // NOTE: a clustered edge can have multiple base edges!
13549 //
13550 const newEdges = [];
13551
13552 /**
13553 * Find a cluster edge which matches the given created edge.
13554 *
13555 * @param {vis.Edge} createdEdge
13556 * @returns {vis.Edge}
13557 */
13558 const getNewEdge = function (createdEdge) {
13559 for (let j = 0; j < newEdges.length; j++) {
13560 const newEdge = newEdges[j];
13561
13562 // We replace both to and from edges with a single cluster edge
13563 const matchToDirection =
13564 createdEdge.fromId === newEdge.fromId &&
13565 createdEdge.toId === newEdge.toId;
13566 const matchFromDirection =
13567 createdEdge.fromId === newEdge.toId &&
13568 createdEdge.toId === newEdge.fromId;
13569
13570 if (matchToDirection || matchFromDirection) {
13571 return newEdge;
13572 }
13573 }
13574
13575 return null;
13576 };
13577
13578 for (let j = 0; j < createEdges.length; j++) {
13579 const createdEdge = createEdges[j];
13580 const edge = createdEdge.edge;
13581 let newEdge = getNewEdge(createdEdge);
13582
13583 if (newEdge === null) {
13584 // Create a clustered edge for this connection
13585 newEdge = this._createClusteredEdge(
13586 createdEdge.fromId,
13587 createdEdge.toId,
13588 edge,
13589 clusterEdgeProperties
13590 );
13591
13592 newEdges.push(newEdge);
13593 } else {
13594 newEdge.clusteringEdgeReplacingIds.push(edge.id);
13595 }
13596
13597 // also reference the new edge in the old edge
13598 this.body.edges[edge.id].edgeReplacedById = newEdge.id;
13599
13600 // hide the replaced edge
13601 this._backupEdgeOptions(edge);
13602 edge.setOptions({ physics: false });
13603 }
13604 }
13605
13606 /**
13607 * This function checks the options that can be supplied to the different cluster functions
13608 * for certain fields and inserts defaults if needed
13609 *
13610 * @param {object} options
13611 * @returns {*}
13612 * @private
13613 */
13614 _checkOptions(options = {}) {
13615 if (options.clusterEdgeProperties === undefined) {
13616 options.clusterEdgeProperties = {};
13617 }
13618 if (options.clusterNodeProperties === undefined) {
13619 options.clusterNodeProperties = {};
13620 }
13621
13622 return options;
13623 }
13624
13625 /**
13626 *
13627 * @param {object} childNodesObj | object with node objects, id as keys, same as childNodes except it also contains a source node
13628 * @param {object} childEdgesObj | object with edge objects, id as keys
13629 * @param {Array} options | object with {clusterNodeProperties, clusterEdgeProperties, processProperties}
13630 * @param {boolean} refreshData | when true, do not wrap up
13631 * @private
13632 */
13633 _cluster(childNodesObj, childEdgesObj, options, refreshData = true) {
13634 // Remove nodes which are already clustered
13635 const tmpNodesToRemove = [];
13636 for (const nodeId in childNodesObj) {
13637 if (Object.prototype.hasOwnProperty.call(childNodesObj, nodeId)) {
13638 if (this.clusteredNodes[nodeId] !== undefined) {
13639 tmpNodesToRemove.push(nodeId);
13640 }
13641 }
13642 }
13643
13644 for (let n = 0; n < tmpNodesToRemove.length; ++n) {
13645 delete childNodesObj[tmpNodesToRemove[n]];
13646 }
13647
13648 // kill condition: no nodes don't bother
13649 if (Object.keys(childNodesObj).length == 0) {
13650 return;
13651 }
13652
13653 // allow clusters of 1 if options allow
13654 if (
13655 Object.keys(childNodesObj).length == 1 &&
13656 options.clusterNodeProperties.allowSingleNodeCluster != true
13657 ) {
13658 return;
13659 }
13660
13661 let clusterNodeProperties = deepExtend({}, options.clusterNodeProperties);
13662
13663 // construct the clusterNodeProperties
13664 if (options.processProperties !== undefined) {
13665 // get the childNode options
13666 const childNodesOptions = [];
13667 for (const nodeId in childNodesObj) {
13668 if (Object.prototype.hasOwnProperty.call(childNodesObj, nodeId)) {
13669 const clonedOptions = NetworkUtil.cloneOptions(childNodesObj[nodeId]);
13670 childNodesOptions.push(clonedOptions);
13671 }
13672 }
13673
13674 // get cluster properties based on childNodes
13675 const childEdgesOptions = [];
13676 for (const edgeId in childEdgesObj) {
13677 if (Object.prototype.hasOwnProperty.call(childEdgesObj, edgeId)) {
13678 // these cluster edges will be removed on creation of the cluster.
13679 if (edgeId.substr(0, 12) !== "clusterEdge:") {
13680 const clonedOptions = NetworkUtil.cloneOptions(
13681 childEdgesObj[edgeId],
13682 "edge"
13683 );
13684 childEdgesOptions.push(clonedOptions);
13685 }
13686 }
13687 }
13688
13689 clusterNodeProperties = options.processProperties(
13690 clusterNodeProperties,
13691 childNodesOptions,
13692 childEdgesOptions
13693 );
13694 if (!clusterNodeProperties) {
13695 throw new Error(
13696 "The processProperties function does not return properties!"
13697 );
13698 }
13699 }
13700
13701 // check if we have an unique id;
13702 if (clusterNodeProperties.id === undefined) {
13703 clusterNodeProperties.id = "cluster:" + v4();
13704 }
13705 const clusterId = clusterNodeProperties.id;
13706
13707 if (clusterNodeProperties.label === undefined) {
13708 clusterNodeProperties.label = "cluster";
13709 }
13710
13711 // give the clusterNode a position if it does not have one.
13712 let pos = undefined;
13713 if (clusterNodeProperties.x === undefined) {
13714 pos = this._getClusterPosition(childNodesObj);
13715 clusterNodeProperties.x = pos.x;
13716 }
13717 if (clusterNodeProperties.y === undefined) {
13718 if (pos === undefined) {
13719 pos = this._getClusterPosition(childNodesObj);
13720 }
13721 clusterNodeProperties.y = pos.y;
13722 }
13723
13724 // force the ID to remain the same
13725 clusterNodeProperties.id = clusterId;
13726
13727 // create the cluster Node
13728 // Note that allowSingleNodeCluster, if present, is stored in the options as well
13729 const clusterNode = this.body.functions.createNode(
13730 clusterNodeProperties,
13731 Cluster
13732 );
13733 clusterNode.containedNodes = childNodesObj;
13734 clusterNode.containedEdges = childEdgesObj;
13735 // cache a copy from the cluster edge properties if we have to reconnect others later on
13736 clusterNode.clusterEdgeProperties = options.clusterEdgeProperties;
13737
13738 // finally put the cluster node into global
13739 this.body.nodes[clusterNodeProperties.id] = clusterNode;
13740
13741 this._clusterEdges(
13742 childNodesObj,
13743 childEdgesObj,
13744 clusterNodeProperties,
13745 options.clusterEdgeProperties
13746 );
13747
13748 // set ID to undefined so no duplicates arise
13749 clusterNodeProperties.id = undefined;
13750
13751 // wrap up
13752 if (refreshData === true) {
13753 this.body.emitter.emit("_dataChanged");
13754 }
13755 }
13756
13757 /**
13758 *
13759 * @param {Edge} edge
13760 * @private
13761 */
13762 _backupEdgeOptions(edge) {
13763 if (this.clusteredEdges[edge.id] === undefined) {
13764 this.clusteredEdges[edge.id] = { physics: edge.options.physics };
13765 }
13766 }
13767
13768 /**
13769 *
13770 * @param {Edge} edge
13771 * @private
13772 */
13773 _restoreEdge(edge) {
13774 const originalOptions = this.clusteredEdges[edge.id];
13775 if (originalOptions !== undefined) {
13776 edge.setOptions({ physics: originalOptions.physics });
13777 delete this.clusteredEdges[edge.id];
13778 }
13779 }
13780
13781 /**
13782 * Check if a node is a cluster.
13783 *
13784 * @param {Node.id} nodeId
13785 * @returns {*}
13786 */
13787 isCluster(nodeId) {
13788 if (this.body.nodes[nodeId] !== undefined) {
13789 return this.body.nodes[nodeId].isCluster === true;
13790 } else {
13791 console.error("Node does not exist.");
13792 return false;
13793 }
13794 }
13795
13796 /**
13797 * get the position of the cluster node based on what's inside
13798 *
13799 * @param {object} childNodesObj | object with node objects, id as keys
13800 * @returns {{x: number, y: number}}
13801 * @private
13802 */
13803 _getClusterPosition(childNodesObj) {
13804 const childKeys = Object.keys(childNodesObj);
13805 let minX = childNodesObj[childKeys[0]].x;
13806 let maxX = childNodesObj[childKeys[0]].x;
13807 let minY = childNodesObj[childKeys[0]].y;
13808 let maxY = childNodesObj[childKeys[0]].y;
13809 let node;
13810 for (let i = 1; i < childKeys.length; i++) {
13811 node = childNodesObj[childKeys[i]];
13812 minX = node.x < minX ? node.x : minX;
13813 maxX = node.x > maxX ? node.x : maxX;
13814 minY = node.y < minY ? node.y : minY;
13815 maxY = node.y > maxY ? node.y : maxY;
13816 }
13817
13818 return { x: 0.5 * (minX + maxX), y: 0.5 * (minY + maxY) };
13819 }
13820
13821 /**
13822 * Open a cluster by calling this function.
13823 *
13824 * @param {vis.Edge.id} clusterNodeId | the ID of the cluster node
13825 * @param {object} options
13826 * @param {boolean} refreshData | wrap up afterwards if not true
13827 */
13828 openCluster(clusterNodeId, options, refreshData = true) {
13829 // kill conditions
13830 if (clusterNodeId === undefined) {
13831 throw new Error("No clusterNodeId supplied to openCluster.");
13832 }
13833
13834 const clusterNode = this.body.nodes[clusterNodeId];
13835
13836 if (clusterNode === undefined) {
13837 throw new Error(
13838 "The clusterNodeId supplied to openCluster does not exist."
13839 );
13840 }
13841 if (
13842 clusterNode.isCluster !== true ||
13843 clusterNode.containedNodes === undefined ||
13844 clusterNode.containedEdges === undefined
13845 ) {
13846 throw new Error("The node:" + clusterNodeId + " is not a valid cluster.");
13847 }
13848
13849 // Check if current cluster is clustered itself
13850 const stack = this.findNode(clusterNodeId);
13851 const parentIndex = stack.indexOf(clusterNodeId) - 1;
13852 if (parentIndex >= 0) {
13853 // Current cluster is clustered; transfer contained nodes and edges to parent
13854 const parentClusterNodeId = stack[parentIndex];
13855 const parentClusterNode = this.body.nodes[parentClusterNodeId];
13856
13857 // clustering.clusteredNodes and clustering.clusteredEdges remain unchanged
13858 parentClusterNode._openChildCluster(clusterNodeId);
13859
13860 // All components of child cluster node have been transferred. It can die now.
13861 delete this.body.nodes[clusterNodeId];
13862 if (refreshData === true) {
13863 this.body.emitter.emit("_dataChanged");
13864 }
13865
13866 return;
13867 }
13868
13869 // main body
13870 const containedNodes = clusterNode.containedNodes;
13871 const containedEdges = clusterNode.containedEdges;
13872
13873 // allow the user to position the nodes after release.
13874 if (
13875 options !== undefined &&
13876 options.releaseFunction !== undefined &&
13877 typeof options.releaseFunction === "function"
13878 ) {
13879 const positions = {};
13880 const clusterPosition = { x: clusterNode.x, y: clusterNode.y };
13881 for (const nodeId in containedNodes) {
13882 if (Object.prototype.hasOwnProperty.call(containedNodes, nodeId)) {
13883 const containedNode = this.body.nodes[nodeId];
13884 positions[nodeId] = { x: containedNode.x, y: containedNode.y };
13885 }
13886 }
13887 const newPositions = options.releaseFunction(clusterPosition, positions);
13888
13889 for (const nodeId in containedNodes) {
13890 if (Object.prototype.hasOwnProperty.call(containedNodes, nodeId)) {
13891 const containedNode = this.body.nodes[nodeId];
13892 if (newPositions[nodeId] !== undefined) {
13893 containedNode.x =
13894 newPositions[nodeId].x === undefined
13895 ? clusterNode.x
13896 : newPositions[nodeId].x;
13897 containedNode.y =
13898 newPositions[nodeId].y === undefined
13899 ? clusterNode.y
13900 : newPositions[nodeId].y;
13901 }
13902 }
13903 }
13904 } else {
13905 // copy the position from the cluster
13906 forEach(containedNodes, function (containedNode) {
13907 // inherit position
13908 if (containedNode.options.fixed.x === false) {
13909 containedNode.x = clusterNode.x;
13910 }
13911 if (containedNode.options.fixed.y === false) {
13912 containedNode.y = clusterNode.y;
13913 }
13914 });
13915 }
13916
13917 // release nodes
13918 for (const nodeId in containedNodes) {
13919 if (Object.prototype.hasOwnProperty.call(containedNodes, nodeId)) {
13920 const containedNode = this.body.nodes[nodeId];
13921
13922 // inherit speed
13923 containedNode.vx = clusterNode.vx;
13924 containedNode.vy = clusterNode.vy;
13925
13926 containedNode.setOptions({ physics: true });
13927
13928 delete this.clusteredNodes[nodeId];
13929 }
13930 }
13931
13932 // copy the clusterNode edges because we cannot iterate over an object that we add or remove from.
13933 const edgesToBeDeleted = [];
13934 for (let i = 0; i < clusterNode.edges.length; i++) {
13935 edgesToBeDeleted.push(clusterNode.edges[i]);
13936 }
13937
13938 // actually handling the deleting.
13939 for (let i = 0; i < edgesToBeDeleted.length; i++) {
13940 const edge = edgesToBeDeleted[i];
13941 const otherNodeId = this._getConnectedId(edge, clusterNodeId);
13942 const otherNode = this.clusteredNodes[otherNodeId];
13943
13944 for (let j = 0; j < edge.clusteringEdgeReplacingIds.length; j++) {
13945 const transferId = edge.clusteringEdgeReplacingIds[j];
13946 const transferEdge = this.body.edges[transferId];
13947 if (transferEdge === undefined) continue;
13948
13949 // if the other node is in another cluster, we transfer ownership of this edge to the other cluster
13950 if (otherNode !== undefined) {
13951 // transfer ownership:
13952 const otherCluster = this.body.nodes[otherNode.clusterId];
13953 otherCluster.containedEdges[transferEdge.id] = transferEdge;
13954
13955 // delete local reference
13956 delete containedEdges[transferEdge.id];
13957
13958 // get to and from
13959 let fromId = transferEdge.fromId;
13960 let toId = transferEdge.toId;
13961 if (transferEdge.toId == otherNodeId) {
13962 toId = otherNode.clusterId;
13963 } else {
13964 fromId = otherNode.clusterId;
13965 }
13966
13967 // create new cluster edge from the otherCluster
13968 this._createClusteredEdge(
13969 fromId,
13970 toId,
13971 transferEdge,
13972 otherCluster.clusterEdgeProperties,
13973 { hidden: false, physics: true }
13974 );
13975 } else {
13976 this._restoreEdge(transferEdge);
13977 }
13978 }
13979
13980 edge.remove();
13981 }
13982
13983 // handle the releasing of the edges
13984 for (const edgeId in containedEdges) {
13985 if (Object.prototype.hasOwnProperty.call(containedEdges, edgeId)) {
13986 this._restoreEdge(containedEdges[edgeId]);
13987 }
13988 }
13989
13990 // remove clusterNode
13991 delete this.body.nodes[clusterNodeId];
13992
13993 if (refreshData === true) {
13994 this.body.emitter.emit("_dataChanged");
13995 }
13996 }
13997
13998 /**
13999 *
14000 * @param {Cluster.id} clusterId
14001 * @returns {Array.<Node.id>}
14002 */
14003 getNodesInCluster(clusterId) {
14004 const nodesArray = [];
14005 if (this.isCluster(clusterId) === true) {
14006 const containedNodes = this.body.nodes[clusterId].containedNodes;
14007 for (const nodeId in containedNodes) {
14008 if (Object.prototype.hasOwnProperty.call(containedNodes, nodeId)) {
14009 nodesArray.push(this.body.nodes[nodeId].id);
14010 }
14011 }
14012 }
14013
14014 return nodesArray;
14015 }
14016
14017 /**
14018 * Get the stack clusterId's that a certain node resides in. cluster A -> cluster B -> cluster C -> node
14019 *
14020 * If a node can't be found in the chain, return an empty array.
14021 *
14022 * @param {string|number} nodeId
14023 * @returns {Array}
14024 */
14025 findNode(nodeId) {
14026 const stack = [];
14027 const max = 100;
14028 let counter = 0;
14029 let node;
14030
14031 while (this.clusteredNodes[nodeId] !== undefined && counter < max) {
14032 node = this.body.nodes[nodeId];
14033 if (node === undefined) return [];
14034 stack.push(node.id);
14035
14036 nodeId = this.clusteredNodes[nodeId].clusterId;
14037 counter++;
14038 }
14039
14040 node = this.body.nodes[nodeId];
14041 if (node === undefined) return [];
14042 stack.push(node.id);
14043
14044 stack.reverse();
14045 return stack;
14046 }
14047
14048 /**
14049 * Using a clustered nodeId, update with the new options
14050 *
14051 * @param {Node.id} clusteredNodeId
14052 * @param {object} newOptions
14053 */
14054 updateClusteredNode(clusteredNodeId, newOptions) {
14055 if (clusteredNodeId === undefined) {
14056 throw new Error("No clusteredNodeId supplied to updateClusteredNode.");
14057 }
14058 if (newOptions === undefined) {
14059 throw new Error("No newOptions supplied to updateClusteredNode.");
14060 }
14061 if (this.body.nodes[clusteredNodeId] === undefined) {
14062 throw new Error(
14063 "The clusteredNodeId supplied to updateClusteredNode does not exist."
14064 );
14065 }
14066
14067 this.body.nodes[clusteredNodeId].setOptions(newOptions);
14068 this.body.emitter.emit("_dataChanged");
14069 }
14070
14071 /**
14072 * Using a base edgeId, update all related clustered edges with the new options
14073 *
14074 * @param {vis.Edge.id} startEdgeId
14075 * @param {object} newOptions
14076 */
14077 updateEdge(startEdgeId, newOptions) {
14078 if (startEdgeId === undefined) {
14079 throw new Error("No startEdgeId supplied to updateEdge.");
14080 }
14081 if (newOptions === undefined) {
14082 throw new Error("No newOptions supplied to updateEdge.");
14083 }
14084 if (this.body.edges[startEdgeId] === undefined) {
14085 throw new Error("The startEdgeId supplied to updateEdge does not exist.");
14086 }
14087
14088 const allEdgeIds = this.getClusteredEdges(startEdgeId);
14089 for (let i = 0; i < allEdgeIds.length; i++) {
14090 const edge = this.body.edges[allEdgeIds[i]];
14091 edge.setOptions(newOptions);
14092 }
14093 this.body.emitter.emit("_dataChanged");
14094 }
14095
14096 /**
14097 * Get a stack of clusterEdgeId's (+base edgeid) that a base edge is the same as. cluster edge C -> cluster edge B -> cluster edge A -> base edge(edgeId)
14098 *
14099 * @param {vis.Edge.id} edgeId
14100 * @returns {Array.<vis.Edge.id>}
14101 */
14102 getClusteredEdges(edgeId) {
14103 const stack = [];
14104 const max = 100;
14105 let counter = 0;
14106
14107 while (
14108 edgeId !== undefined &&
14109 this.body.edges[edgeId] !== undefined &&
14110 counter < max
14111 ) {
14112 stack.push(this.body.edges[edgeId].id);
14113 edgeId = this.body.edges[edgeId].edgeReplacedById;
14114 counter++;
14115 }
14116 stack.reverse();
14117 return stack;
14118 }
14119
14120 /**
14121 * Get the base edge id of clusterEdgeId. cluster edge (clusteredEdgeId) -> cluster edge B -> cluster edge C -> base edge
14122 *
14123 * @param {vis.Edge.id} clusteredEdgeId
14124 * @returns {vis.Edge.id} baseEdgeId
14125 *
14126 * TODO: deprecate in 5.0.0. Method getBaseEdges() is the correct one to use.
14127 */
14128 getBaseEdge(clusteredEdgeId) {
14129 // Just kludge this by returning the first base edge id found
14130 return this.getBaseEdges(clusteredEdgeId)[0];
14131 }
14132
14133 /**
14134 * Get all regular edges for this clustered edge id.
14135 *
14136 * @param {vis.Edge.id} clusteredEdgeId
14137 * @returns {Array.<vis.Edge.id>} all baseEdgeId's under this clustered edge
14138 */
14139 getBaseEdges(clusteredEdgeId) {
14140 const IdsToHandle = [clusteredEdgeId];
14141 const doneIds = [];
14142 const foundIds = [];
14143 const max = 100;
14144 let counter = 0;
14145
14146 while (IdsToHandle.length > 0 && counter < max) {
14147 const nextId = IdsToHandle.pop();
14148 if (nextId === undefined) continue; // Paranoia here and onwards
14149 const nextEdge = this.body.edges[nextId];
14150 if (nextEdge === undefined) continue;
14151 counter++;
14152
14153 const replacingIds = nextEdge.clusteringEdgeReplacingIds;
14154 if (replacingIds === undefined) {
14155 // nextId is a base id
14156 foundIds.push(nextId);
14157 } else {
14158 // Another cluster edge, unravel this one as well
14159 for (let i = 0; i < replacingIds.length; ++i) {
14160 const replacingId = replacingIds[i];
14161
14162 // Don't add if already handled
14163 // TODO: never triggers; find a test-case which does
14164 if (
14165 IdsToHandle.indexOf(replacingIds) !== -1 ||
14166 doneIds.indexOf(replacingIds) !== -1
14167 ) {
14168 continue;
14169 }
14170
14171 IdsToHandle.push(replacingId);
14172 }
14173 }
14174
14175 doneIds.push(nextId);
14176 }
14177
14178 return foundIds;
14179 }
14180
14181 /**
14182 * Get the Id the node is connected to
14183 *
14184 * @param {vis.Edge} edge
14185 * @param {Node.id} nodeId
14186 * @returns {*}
14187 * @private
14188 */
14189 _getConnectedId(edge, nodeId) {
14190 if (edge.toId != nodeId) {
14191 return edge.toId;
14192 } else if (edge.fromId != nodeId) {
14193 return edge.fromId;
14194 } else {
14195 return edge.fromId;
14196 }
14197 }
14198
14199 /**
14200 * We determine how many connections denote an important hub.
14201 * We take the mean + 2*std as the important hub size. (Assuming a normal distribution of data, ~2.2%)
14202 *
14203 * @returns {number}
14204 * @private
14205 */
14206 _getHubSize() {
14207 let average = 0;
14208 let averageSquared = 0;
14209 let hubCounter = 0;
14210 let largestHub = 0;
14211
14212 for (let i = 0; i < this.body.nodeIndices.length; i++) {
14213 const node = this.body.nodes[this.body.nodeIndices[i]];
14214 if (node.edges.length > largestHub) {
14215 largestHub = node.edges.length;
14216 }
14217 average += node.edges.length;
14218 averageSquared += Math.pow(node.edges.length, 2);
14219 hubCounter += 1;
14220 }
14221 average = average / hubCounter;
14222 averageSquared = averageSquared / hubCounter;
14223
14224 const variance = averageSquared - Math.pow(average, 2);
14225 const standardDeviation = Math.sqrt(variance);
14226
14227 let hubThreshold = Math.floor(average + 2 * standardDeviation);
14228
14229 // always have at least one to cluster
14230 if (hubThreshold > largestHub) {
14231 hubThreshold = largestHub;
14232 }
14233
14234 return hubThreshold;
14235 }
14236
14237 /**
14238 * Create an edge for the cluster representation.
14239 *
14240 * @param {Node.id} fromId
14241 * @param {Node.id} toId
14242 * @param {vis.Edge} baseEdge
14243 * @param {object} clusterEdgeProperties
14244 * @param {object} extraOptions
14245 * @returns {Edge} newly created clustered edge
14246 * @private
14247 */
14248 _createClusteredEdge(
14249 fromId,
14250 toId,
14251 baseEdge,
14252 clusterEdgeProperties,
14253 extraOptions
14254 ) {
14255 // copy the options of the edge we will replace
14256 const clonedOptions = NetworkUtil.cloneOptions(baseEdge, "edge");
14257 // make sure the properties of clusterEdges are superimposed on it
14258 deepExtend(clonedOptions, clusterEdgeProperties);
14259
14260 // set up the edge
14261 clonedOptions.from = fromId;
14262 clonedOptions.to = toId;
14263 clonedOptions.id = "clusterEdge:" + v4();
14264
14265 // apply the edge specific options to it if specified
14266 if (extraOptions !== undefined) {
14267 deepExtend(clonedOptions, extraOptions);
14268 }
14269
14270 const newEdge = this.body.functions.createEdge(clonedOptions);
14271 newEdge.clusteringEdgeReplacingIds = [baseEdge.id];
14272 newEdge.connect();
14273
14274 // Register the new edge
14275 this.body.edges[newEdge.id] = newEdge;
14276
14277 return newEdge;
14278 }
14279
14280 /**
14281 * Add the passed child nodes and edges to the given cluster node.
14282 *
14283 * @param {object | Node} childNodes hash of nodes or single node to add in cluster
14284 * @param {object | Edge} childEdges hash of edges or single edge to take into account when clustering
14285 * @param {Node} clusterNode cluster node to add nodes and edges to
14286 * @param {object} [clusterEdgeProperties]
14287 * @private
14288 */
14289 _clusterEdges(childNodes, childEdges, clusterNode, clusterEdgeProperties) {
14290 if (childEdges instanceof Edge) {
14291 const edge = childEdges;
14292 const obj = {};
14293 obj[edge.id] = edge;
14294 childEdges = obj;
14295 }
14296
14297 if (childNodes instanceof Node) {
14298 const node = childNodes;
14299 const obj = {};
14300 obj[node.id] = node;
14301 childNodes = obj;
14302 }
14303
14304 if (clusterNode === undefined || clusterNode === null) {
14305 throw new Error("_clusterEdges: parameter clusterNode required");
14306 }
14307
14308 if (clusterEdgeProperties === undefined) {
14309 // Take the required properties from the cluster node
14310 clusterEdgeProperties = clusterNode.clusterEdgeProperties;
14311 }
14312
14313 // create the new edges that will connect to the cluster.
14314 // All self-referencing edges will be added to childEdges here.
14315 this._createClusterEdges(
14316 childNodes,
14317 childEdges,
14318 clusterNode,
14319 clusterEdgeProperties
14320 );
14321
14322 // disable the childEdges
14323 for (const edgeId in childEdges) {
14324 if (Object.prototype.hasOwnProperty.call(childEdges, edgeId)) {
14325 if (this.body.edges[edgeId] !== undefined) {
14326 const edge = this.body.edges[edgeId];
14327 // cache the options before changing
14328 this._backupEdgeOptions(edge);
14329 // disable physics and hide the edge
14330 edge.setOptions({ physics: false });
14331 }
14332 }
14333 }
14334
14335 // disable the childNodes
14336 for (const nodeId in childNodes) {
14337 if (Object.prototype.hasOwnProperty.call(childNodes, nodeId)) {
14338 this.clusteredNodes[nodeId] = {
14339 clusterId: clusterNode.id,
14340 node: this.body.nodes[nodeId],
14341 };
14342 this.body.nodes[nodeId].setOptions({ physics: false });
14343 }
14344 }
14345 }
14346
14347 /**
14348 * Determine in which cluster given nodeId resides.
14349 *
14350 * If not in cluster, return undefined.
14351 *
14352 * NOTE: If you know a cleaner way to do this, please enlighten me (wimrijnders).
14353 *
14354 * @param {Node.id} nodeId
14355 * @returns {Node|undefined} Node instance for cluster, if present
14356 * @private
14357 */
14358 _getClusterNodeForNode(nodeId) {
14359 if (nodeId === undefined) return undefined;
14360 const clusteredNode = this.clusteredNodes[nodeId];
14361
14362 // NOTE: If no cluster info found, it should actually be an error
14363 if (clusteredNode === undefined) return undefined;
14364 const clusterId = clusteredNode.clusterId;
14365 if (clusterId === undefined) return undefined;
14366
14367 return this.body.nodes[clusterId];
14368 }
14369
14370 /**
14371 * Internal helper function for conditionally removing items in array
14372 *
14373 * Done like this because Array.filter() is not fully supported by all IE's.
14374 *
14375 * @param {Array} arr
14376 * @param {Function} callback
14377 * @returns {Array}
14378 * @private
14379 */
14380 _filter(arr, callback) {
14381 const ret = [];
14382
14383 forEach(arr, (item) => {
14384 if (callback(item)) {
14385 ret.push(item);
14386 }
14387 });
14388
14389 return ret;
14390 }
14391
14392 /**
14393 * Scan all edges for changes in clustering and adjust this if necessary.
14394 *
14395 * Call this (internally) after there has been a change in node or edge data.
14396 *
14397 * Pre: States of this.body.nodes and this.body.edges consistent
14398 * Pre: this.clusteredNodes and this.clusteredEdge consistent with containedNodes and containedEdges
14399 * of cluster nodes.
14400 */
14401 _updateState() {
14402 let nodeId;
14403 const deletedNodeIds = [];
14404 const deletedEdgeIds = {};
14405
14406 /**
14407 * Utility function to iterate over clustering nodes only
14408 *
14409 * @param {Function} callback function to call for each cluster node
14410 */
14411 const eachClusterNode = (callback) => {
14412 forEach(this.body.nodes, (node) => {
14413 if (node.isCluster === true) {
14414 callback(node);
14415 }
14416 });
14417 };
14418
14419 //
14420 // Remove deleted regular nodes from clustering
14421 //
14422
14423 // Determine the deleted nodes
14424 for (nodeId in this.clusteredNodes) {
14425 if (!Object.prototype.hasOwnProperty.call(this.clusteredNodes, nodeId))
14426 continue;
14427 const node = this.body.nodes[nodeId];
14428
14429 if (node === undefined) {
14430 deletedNodeIds.push(nodeId);
14431 }
14432 }
14433
14434 // Remove nodes from cluster nodes
14435 eachClusterNode(function (clusterNode) {
14436 for (let n = 0; n < deletedNodeIds.length; n++) {
14437 delete clusterNode.containedNodes[deletedNodeIds[n]];
14438 }
14439 });
14440
14441 // Remove nodes from cluster list
14442 for (let n = 0; n < deletedNodeIds.length; n++) {
14443 delete this.clusteredNodes[deletedNodeIds[n]];
14444 }
14445
14446 //
14447 // Remove deleted edges from clustering
14448 //
14449
14450 // Add the deleted clustered edges to the list
14451 forEach(this.clusteredEdges, (edgeId) => {
14452 const edge = this.body.edges[edgeId];
14453 if (edge === undefined || !edge.endPointsValid()) {
14454 deletedEdgeIds[edgeId] = edgeId;
14455 }
14456 });
14457
14458 // Cluster nodes can also contain edges which are not clustered,
14459 // i.e. nodes 1-2 within cluster with an edge in between.
14460 // So the cluster nodes also need to be scanned for invalid edges
14461 eachClusterNode(function (clusterNode) {
14462 forEach(clusterNode.containedEdges, (edge, edgeId) => {
14463 if (!edge.endPointsValid() && !deletedEdgeIds[edgeId]) {
14464 deletedEdgeIds[edgeId] = edgeId;
14465 }
14466 });
14467 });
14468
14469 // Also scan for cluster edges which need to be removed in the active list.
14470 // Regular edges have been removed beforehand, so this only picks up the cluster edges.
14471 forEach(this.body.edges, (edge, edgeId) => {
14472 // Explicitly scan the contained edges for validity
14473 let isValid = true;
14474 const replacedIds = edge.clusteringEdgeReplacingIds;
14475 if (replacedIds !== undefined) {
14476 let numValid = 0;
14477
14478 forEach(replacedIds, (containedEdgeId) => {
14479 const containedEdge = this.body.edges[containedEdgeId];
14480
14481 if (containedEdge !== undefined && containedEdge.endPointsValid()) {
14482 numValid += 1;
14483 }
14484 });
14485
14486 isValid = numValid > 0;
14487 }
14488
14489 if (!edge.endPointsValid() || !isValid) {
14490 deletedEdgeIds[edgeId] = edgeId;
14491 }
14492 });
14493
14494 // Remove edges from cluster nodes
14495 eachClusterNode((clusterNode) => {
14496 forEach(deletedEdgeIds, (deletedEdgeId) => {
14497 delete clusterNode.containedEdges[deletedEdgeId];
14498
14499 forEach(clusterNode.edges, (edge, m) => {
14500 if (edge.id === deletedEdgeId) {
14501 clusterNode.edges[m] = null; // Don't want to directly delete here, because in the loop
14502 return;
14503 }
14504
14505 edge.clusteringEdgeReplacingIds = this._filter(
14506 edge.clusteringEdgeReplacingIds,
14507 function (id) {
14508 return !deletedEdgeIds[id];
14509 }
14510 );
14511 });
14512
14513 // Clean up the nulls
14514 clusterNode.edges = this._filter(clusterNode.edges, function (item) {
14515 return item !== null;
14516 });
14517 });
14518 });
14519
14520 // Remove from cluster list
14521 forEach(deletedEdgeIds, (edgeId) => {
14522 delete this.clusteredEdges[edgeId];
14523 });
14524
14525 // Remove cluster edges from active list (this.body.edges).
14526 // deletedEdgeIds still contains id of regular edges, but these should all
14527 // be gone when you reach here.
14528 forEach(deletedEdgeIds, (edgeId) => {
14529 delete this.body.edges[edgeId];
14530 });
14531
14532 //
14533 // Check changed cluster state of edges
14534 //
14535
14536 // Iterating over keys here, because edges may be removed in the loop
14537 const ids = Object.keys(this.body.edges);
14538 forEach(ids, (edgeId) => {
14539 const edge = this.body.edges[edgeId];
14540
14541 const shouldBeClustered =
14542 this._isClusteredNode(edge.fromId) || this._isClusteredNode(edge.toId);
14543 if (shouldBeClustered === this._isClusteredEdge(edge.id)) {
14544 return; // all is well
14545 }
14546
14547 if (shouldBeClustered) {
14548 // add edge to clustering
14549 const clusterFrom = this._getClusterNodeForNode(edge.fromId);
14550 if (clusterFrom !== undefined) {
14551 this._clusterEdges(this.body.nodes[edge.fromId], edge, clusterFrom);
14552 }
14553
14554 const clusterTo = this._getClusterNodeForNode(edge.toId);
14555 if (clusterTo !== undefined) {
14556 this._clusterEdges(this.body.nodes[edge.toId], edge, clusterTo);
14557 }
14558
14559 // TODO: check that it works for both edges clustered
14560 // (This might be paranoia)
14561 } else {
14562 delete this._clusterEdges[edgeId];
14563 this._restoreEdge(edge);
14564 // This should not be happening, the state should
14565 // be properly updated at this point.
14566 //
14567 // If it *is* reached during normal operation, then we have to implement
14568 // undo clustering for this edge here.
14569 // throw new Error('remove edge from clustering not implemented!')
14570 }
14571 });
14572
14573 // Clusters may be nested to any level. Keep on opening until nothing to open
14574 let changed = false;
14575 let continueLoop = true;
14576 while (continueLoop) {
14577 const clustersToOpen = [];
14578
14579 // Determine the id's of clusters that need opening
14580 eachClusterNode(function (clusterNode) {
14581 const numNodes = Object.keys(clusterNode.containedNodes).length;
14582 const allowSingle = clusterNode.options.allowSingleNodeCluster === true;
14583 if ((allowSingle && numNodes < 1) || (!allowSingle && numNodes < 2)) {
14584 clustersToOpen.push(clusterNode.id);
14585 }
14586 });
14587
14588 // Open them
14589 for (let n = 0; n < clustersToOpen.length; ++n) {
14590 this.openCluster(
14591 clustersToOpen[n],
14592 {},
14593 false /* Don't refresh, we're in an refresh/update already */
14594 );
14595 }
14596
14597 continueLoop = clustersToOpen.length > 0;
14598 changed = changed || continueLoop;
14599 }
14600
14601 if (changed) {
14602 this._updateState(); // Redo this method (recursion possible! should be safe)
14603 }
14604 }
14605
14606 /**
14607 * Determine if node with given id is part of a cluster.
14608 *
14609 * @param {Node.id} nodeId
14610 * @returns {boolean} true if part of a cluster.
14611 */
14612 _isClusteredNode(nodeId) {
14613 return this.clusteredNodes[nodeId] !== undefined;
14614 }
14615
14616 /**
14617 * Determine if edge with given id is not visible due to clustering.
14618 *
14619 * An edge is considered clustered if:
14620 * - it is directly replaced by a clustering edge
14621 * - any of its connecting nodes is in a cluster
14622 *
14623 * @param {vis.Edge.id} edgeId
14624 * @returns {boolean} true if part of a cluster.
14625 */
14626 _isClusteredEdge(edgeId) {
14627 return this.clusteredEdges[edgeId] !== undefined;
14628 }
14629}
14630
14631/**
14632 * Initializes window.requestAnimationFrame() to a usable form.
14633 *
14634 * Specifically, set up this method for the case of running on node.js with jsdom enabled.
14635 *
14636 * NOTES:
14637 *
14638 * * On node.js, when calling this directly outside of this class, `window` is not defined.
14639 * This happens even if jsdom is used.
14640 * * For node.js + jsdom, `window` is available at the moment the constructor is called.
14641 * For this reason, the called is placed within the constructor.
14642 * * Even then, `window.requestAnimationFrame()` is not defined, so it still needs to be added.
14643 * * During unit testing, it happens that the window object is reset during execution, causing
14644 * a runtime error due to missing `requestAnimationFrame()`. This needs to be compensated for,
14645 * see `_requestNextFrame()`.
14646 * * Since this is a global object, it may affect other modules besides `Network`. With normal
14647 * usage, this does not cause any problems. During unit testing, errors may occur. These have
14648 * been compensated for, see comment block in _requestNextFrame().
14649 *
14650 * @private
14651 */
14652function _initRequestAnimationFrame() {
14653 let func;
14654
14655 if (window !== undefined) {
14656 func =
14657 window.requestAnimationFrame ||
14658 window.mozRequestAnimationFrame ||
14659 window.webkitRequestAnimationFrame ||
14660 window.msRequestAnimationFrame;
14661 }
14662
14663 if (func === undefined) {
14664 // window or method not present, setting mock requestAnimationFrame
14665 window.requestAnimationFrame = function (callback) {
14666 //console.log("Called mock requestAnimationFrame");
14667 callback();
14668 };
14669 } else {
14670 window.requestAnimationFrame = func;
14671 }
14672}
14673
14674/**
14675 * The canvas renderer
14676 */
14677class CanvasRenderer {
14678 /**
14679 * @param {object} body
14680 * @param {Canvas} canvas
14681 */
14682 constructor(body, canvas) {
14683 _initRequestAnimationFrame();
14684 this.body = body;
14685 this.canvas = canvas;
14686
14687 this.redrawRequested = false;
14688 this.renderTimer = undefined;
14689 this.requiresTimeout = true;
14690 this.renderingActive = false;
14691 this.renderRequests = 0;
14692 this.allowRedraw = true;
14693
14694 this.dragging = false;
14695 this.zooming = false;
14696 this.options = {};
14697 this.defaultOptions = {
14698 hideEdgesOnDrag: false,
14699 hideEdgesOnZoom: false,
14700 hideNodesOnDrag: false,
14701 };
14702 Object.assign(this.options, this.defaultOptions);
14703
14704 this._determineBrowserMethod();
14705 this.bindEventListeners();
14706 }
14707
14708 /**
14709 * Binds event listeners
14710 */
14711 bindEventListeners() {
14712 this.body.emitter.on("dragStart", () => {
14713 this.dragging = true;
14714 });
14715 this.body.emitter.on("dragEnd", () => {
14716 this.dragging = false;
14717 });
14718 this.body.emitter.on("zoom", () => {
14719 this.zooming = true;
14720 window.clearTimeout(this.zoomTimeoutId);
14721 this.zoomTimeoutId = window.setTimeout(() => {
14722 this.zooming = false;
14723 this._requestRedraw.bind(this)();
14724 }, 250);
14725 });
14726 this.body.emitter.on("_resizeNodes", () => {
14727 this._resizeNodes();
14728 });
14729 this.body.emitter.on("_redraw", () => {
14730 if (this.renderingActive === false) {
14731 this._redraw();
14732 }
14733 });
14734 this.body.emitter.on("_blockRedraw", () => {
14735 this.allowRedraw = false;
14736 });
14737 this.body.emitter.on("_allowRedraw", () => {
14738 this.allowRedraw = true;
14739 this.redrawRequested = false;
14740 });
14741 this.body.emitter.on("_requestRedraw", this._requestRedraw.bind(this));
14742 this.body.emitter.on("_startRendering", () => {
14743 this.renderRequests += 1;
14744 this.renderingActive = true;
14745 this._startRendering();
14746 });
14747 this.body.emitter.on("_stopRendering", () => {
14748 this.renderRequests -= 1;
14749 this.renderingActive = this.renderRequests > 0;
14750 this.renderTimer = undefined;
14751 });
14752 this.body.emitter.on("destroy", () => {
14753 this.renderRequests = 0;
14754 this.allowRedraw = false;
14755 this.renderingActive = false;
14756 if (this.requiresTimeout === true) {
14757 clearTimeout(this.renderTimer);
14758 } else {
14759 window.cancelAnimationFrame(this.renderTimer);
14760 }
14761 this.body.emitter.off();
14762 });
14763 }
14764
14765 /**
14766 *
14767 * @param {object} options
14768 */
14769 setOptions(options) {
14770 if (options !== undefined) {
14771 const fields = ["hideEdgesOnDrag", "hideEdgesOnZoom", "hideNodesOnDrag"];
14772 selectiveDeepExtend(fields, this.options, options);
14773 }
14774 }
14775
14776 /**
14777 * Prepare the drawing of the next frame.
14778 *
14779 * Calls the callback when the next frame can or will be drawn.
14780 *
14781 * @param {Function} callback
14782 * @param {number} delay - timeout case only, wait this number of milliseconds
14783 * @returns {Function | undefined}
14784 * @private
14785 */
14786 _requestNextFrame(callback, delay) {
14787 // During unit testing, it happens that the mock window object is reset while
14788 // the next frame is still pending. Then, either 'window' is not present, or
14789 // 'requestAnimationFrame()' is not present because it is not defined on the
14790 // mock window object.
14791 //
14792 // As a consequence, unrelated unit tests may appear to fail, even if the problem
14793 // described happens in the current unit test.
14794 //
14795 // This is not something that will happen in normal operation, but we still need
14796 // to take it into account.
14797 //
14798 if (typeof window === "undefined") return; // Doing `if (window === undefined)` does not work here!
14799
14800 let timer;
14801
14802 const myWindow = window; // Grab a reference to reduce the possibility that 'window' is reset
14803 // while running this method.
14804
14805 if (this.requiresTimeout === true) {
14806 // wait given number of milliseconds and perform the animation step function
14807 timer = myWindow.setTimeout(callback, delay);
14808 } else {
14809 if (myWindow.requestAnimationFrame) {
14810 timer = myWindow.requestAnimationFrame(callback);
14811 }
14812 }
14813
14814 return timer;
14815 }
14816
14817 /**
14818 *
14819 * @private
14820 */
14821 _startRendering() {
14822 if (this.renderingActive === true) {
14823 if (this.renderTimer === undefined) {
14824 this.renderTimer = this._requestNextFrame(
14825 this._renderStep.bind(this),
14826 this.simulationInterval
14827 );
14828 }
14829 }
14830 }
14831
14832 /**
14833 *
14834 * @private
14835 */
14836 _renderStep() {
14837 if (this.renderingActive === true) {
14838 // reset the renderTimer so a new scheduled animation step can be set
14839 this.renderTimer = undefined;
14840
14841 if (this.requiresTimeout === true) {
14842 // this schedules a new simulation step
14843 this._startRendering();
14844 }
14845
14846 this._redraw();
14847
14848 if (this.requiresTimeout === false) {
14849 // this schedules a new simulation step
14850 this._startRendering();
14851 }
14852 }
14853 }
14854
14855 /**
14856 * Redraw the network with the current data
14857 * chart will be resized too.
14858 */
14859 redraw() {
14860 this.body.emitter.emit("setSize");
14861 this._redraw();
14862 }
14863
14864 /**
14865 * Redraw the network with the current data
14866 *
14867 * @private
14868 */
14869 _requestRedraw() {
14870 if (
14871 this.redrawRequested !== true &&
14872 this.renderingActive === false &&
14873 this.allowRedraw === true
14874 ) {
14875 this.redrawRequested = true;
14876 this._requestNextFrame(() => {
14877 this._redraw(false);
14878 }, 0);
14879 }
14880 }
14881
14882 /**
14883 * Redraw the network with the current data
14884 *
14885 * @param {boolean} [hidden=false] | Used to get the first estimate of the node sizes.
14886 * Only the nodes are drawn after which they are quickly drawn over.
14887 * @private
14888 */
14889 _redraw(hidden = false) {
14890 if (this.allowRedraw === true) {
14891 this.body.emitter.emit("initRedraw");
14892
14893 this.redrawRequested = false;
14894
14895 const drawLater = {
14896 drawExternalLabels: null,
14897 };
14898
14899 // when the container div was hidden, this fixes it back up!
14900 if (
14901 this.canvas.frame.canvas.width === 0 ||
14902 this.canvas.frame.canvas.height === 0
14903 ) {
14904 this.canvas.setSize();
14905 }
14906
14907 this.canvas.setTransform();
14908
14909 const ctx = this.canvas.getContext();
14910
14911 // clear the canvas
14912 const w = this.canvas.frame.canvas.clientWidth;
14913 const h = this.canvas.frame.canvas.clientHeight;
14914 ctx.clearRect(0, 0, w, h);
14915
14916 // if the div is hidden, we stop the redraw here for performance.
14917 if (this.canvas.frame.clientWidth === 0) {
14918 return;
14919 }
14920
14921 // set scaling and translation
14922 ctx.save();
14923 ctx.translate(this.body.view.translation.x, this.body.view.translation.y);
14924 ctx.scale(this.body.view.scale, this.body.view.scale);
14925
14926 ctx.beginPath();
14927 this.body.emitter.emit("beforeDrawing", ctx);
14928 ctx.closePath();
14929
14930 if (hidden === false) {
14931 if (
14932 (this.dragging === false ||
14933 (this.dragging === true &&
14934 this.options.hideEdgesOnDrag === false)) &&
14935 (this.zooming === false ||
14936 (this.zooming === true && this.options.hideEdgesOnZoom === false))
14937 ) {
14938 this._drawEdges(ctx);
14939 }
14940 }
14941
14942 if (
14943 this.dragging === false ||
14944 (this.dragging === true && this.options.hideNodesOnDrag === false)
14945 ) {
14946 const { drawExternalLabels } = this._drawNodes(ctx, hidden);
14947 drawLater.drawExternalLabels = drawExternalLabels;
14948 }
14949
14950 // draw the arrows last so they will be at the top
14951 if (hidden === false) {
14952 if (
14953 (this.dragging === false ||
14954 (this.dragging === true &&
14955 this.options.hideEdgesOnDrag === false)) &&
14956 (this.zooming === false ||
14957 (this.zooming === true && this.options.hideEdgesOnZoom === false))
14958 ) {
14959 this._drawArrows(ctx);
14960 }
14961 }
14962
14963 if (drawLater.drawExternalLabels != null) {
14964 drawLater.drawExternalLabels();
14965 }
14966
14967 if (hidden === false) {
14968 this._drawSelectionBox(ctx);
14969 }
14970
14971 ctx.beginPath();
14972 this.body.emitter.emit("afterDrawing", ctx);
14973 ctx.closePath();
14974
14975 // restore original scaling and translation
14976 ctx.restore();
14977 if (hidden === true) {
14978 ctx.clearRect(0, 0, w, h);
14979 }
14980 }
14981 }
14982
14983 /**
14984 * Redraw all nodes
14985 *
14986 * @param {CanvasRenderingContext2D} ctx
14987 * @param {boolean} [alwaysShow]
14988 * @private
14989 */
14990 _resizeNodes() {
14991 this.canvas.setTransform();
14992 const ctx = this.canvas.getContext();
14993 ctx.save();
14994 ctx.translate(this.body.view.translation.x, this.body.view.translation.y);
14995 ctx.scale(this.body.view.scale, this.body.view.scale);
14996
14997 const nodes = this.body.nodes;
14998 let node;
14999
15000 // resize all nodes
15001 for (const nodeId in nodes) {
15002 if (Object.prototype.hasOwnProperty.call(nodes, nodeId)) {
15003 node = nodes[nodeId];
15004 node.resize(ctx);
15005 node.updateBoundingBox(ctx, node.selected);
15006 }
15007 }
15008
15009 // restore original scaling and translation
15010 ctx.restore();
15011 }
15012
15013 /**
15014 * Redraw all nodes
15015 *
15016 * @param {CanvasRenderingContext2D} ctx 2D context of a HTML canvas
15017 * @param {boolean} [alwaysShow]
15018 * @private
15019 *
15020 * @returns {object} Callbacks to draw later on higher layers.
15021 */
15022 _drawNodes(ctx, alwaysShow = false) {
15023 const nodes = this.body.nodes;
15024 const nodeIndices = this.body.nodeIndices;
15025 let node;
15026 const selected = [];
15027 const hovered = [];
15028 const margin = 20;
15029 const topLeft = this.canvas.DOMtoCanvas({ x: -margin, y: -margin });
15030 const bottomRight = this.canvas.DOMtoCanvas({
15031 x: this.canvas.frame.canvas.clientWidth + margin,
15032 y: this.canvas.frame.canvas.clientHeight + margin,
15033 });
15034 const viewableArea = {
15035 top: topLeft.y,
15036 left: topLeft.x,
15037 bottom: bottomRight.y,
15038 right: bottomRight.x,
15039 };
15040
15041 const drawExternalLabels = [];
15042
15043 // draw unselected nodes;
15044 for (let i = 0; i < nodeIndices.length; i++) {
15045 node = nodes[nodeIndices[i]];
15046 // set selected and hovered nodes aside
15047 if (node.hover) {
15048 hovered.push(nodeIndices[i]);
15049 } else if (node.isSelected()) {
15050 selected.push(nodeIndices[i]);
15051 } else {
15052 if (alwaysShow === true) {
15053 const drawLater = node.draw(ctx);
15054 if (drawLater.drawExternalLabel != null) {
15055 drawExternalLabels.push(drawLater.drawExternalLabel);
15056 }
15057 } else if (node.isBoundingBoxOverlappingWith(viewableArea) === true) {
15058 const drawLater = node.draw(ctx);
15059 if (drawLater.drawExternalLabel != null) {
15060 drawExternalLabels.push(drawLater.drawExternalLabel);
15061 }
15062 } else {
15063 node.updateBoundingBox(ctx, node.selected);
15064 }
15065 }
15066 }
15067
15068 let i;
15069 const selectedLength = selected.length;
15070 const hoveredLength = hovered.length;
15071
15072 // draw the selected nodes on top
15073 for (i = 0; i < selectedLength; i++) {
15074 node = nodes[selected[i]];
15075 const drawLater = node.draw(ctx);
15076 if (drawLater.drawExternalLabel != null) {
15077 drawExternalLabels.push(drawLater.drawExternalLabel);
15078 }
15079 }
15080
15081 // draw hovered nodes above everything else: fixes https://github.com/visjs/vis-network/issues/226
15082 for (i = 0; i < hoveredLength; i++) {
15083 node = nodes[hovered[i]];
15084 const drawLater = node.draw(ctx);
15085 if (drawLater.drawExternalLabel != null) {
15086 drawExternalLabels.push(drawLater.drawExternalLabel);
15087 }
15088 }
15089
15090 return {
15091 drawExternalLabels: () => {
15092 for (const draw of drawExternalLabels) {
15093 draw();
15094 }
15095 },
15096 };
15097 }
15098
15099 /**
15100 * Redraw all edges
15101 *
15102 * @param {CanvasRenderingContext2D} ctx 2D context of a HTML canvas
15103 * @private
15104 */
15105 _drawEdges(ctx) {
15106 const edges = this.body.edges;
15107 const edgeIndices = this.body.edgeIndices;
15108
15109 for (let i = 0; i < edgeIndices.length; i++) {
15110 const edge = edges[edgeIndices[i]];
15111 if (edge.connected === true) {
15112 edge.draw(ctx);
15113 }
15114 }
15115 }
15116
15117 /**
15118 * Redraw all arrows
15119 *
15120 * @param {CanvasRenderingContext2D} ctx 2D context of a HTML canvas
15121 * @private
15122 */
15123 _drawArrows(ctx) {
15124 const edges = this.body.edges;
15125 const edgeIndices = this.body.edgeIndices;
15126
15127 for (let i = 0; i < edgeIndices.length; i++) {
15128 const edge = edges[edgeIndices[i]];
15129 if (edge.connected === true) {
15130 edge.drawArrows(ctx);
15131 }
15132 }
15133 }
15134
15135 /**
15136 * Determine if the browser requires a setTimeout or a requestAnimationFrame. This was required because
15137 * some implementations (safari and IE9) did not support requestAnimationFrame
15138 *
15139 * @private
15140 */
15141 _determineBrowserMethod() {
15142 if (typeof window !== "undefined") {
15143 const browserType = navigator.userAgent.toLowerCase();
15144 this.requiresTimeout = false;
15145 if (browserType.indexOf("msie 9.0") != -1) {
15146 // IE 9
15147 this.requiresTimeout = true;
15148 } else if (browserType.indexOf("safari") != -1) {
15149 // safari
15150 if (browserType.indexOf("chrome") <= -1) {
15151 this.requiresTimeout = true;
15152 }
15153 }
15154 } else {
15155 this.requiresTimeout = true;
15156 }
15157 }
15158
15159 /**
15160 * Redraw selection box
15161 *
15162 * @param {CanvasRenderingContext2D} ctx 2D context of a HTML canvas
15163 * @private
15164 */
15165 _drawSelectionBox(ctx) {
15166 if (this.body.selectionBox.show) {
15167 ctx.beginPath();
15168 const width =
15169 this.body.selectionBox.position.end.x -
15170 this.body.selectionBox.position.start.x;
15171 const height =
15172 this.body.selectionBox.position.end.y -
15173 this.body.selectionBox.position.start.y;
15174 ctx.rect(
15175 this.body.selectionBox.position.start.x,
15176 this.body.selectionBox.position.start.y,
15177 width,
15178 height
15179 );
15180 ctx.fillStyle = "rgba(151, 194, 252, 0.2)";
15181 ctx.fillRect(
15182 this.body.selectionBox.position.start.x,
15183 this.body.selectionBox.position.start.y,
15184 width,
15185 height
15186 );
15187 ctx.strokeStyle = "rgba(151, 194, 252, 1)";
15188 ctx.stroke();
15189 } else {
15190 ctx.closePath();
15191 }
15192 }
15193}
15194
15195/**
15196 * Register a touch event, taking place before a gesture
15197 *
15198 * @param {Hammer} hammer A hammer instance
15199 * @param {Function} callback Callback, called as callback(event)
15200 */
15201function onTouch(hammer, callback) {
15202 callback.inputHandler = function (event) {
15203 if (event.isFirst) {
15204 callback(event);
15205 }
15206 };
15207
15208 hammer.on("hammer.input", callback.inputHandler);
15209}
15210
15211/**
15212 * Register a release event, taking place after a gesture
15213 *
15214 * @param {Hammer} hammer A hammer instance
15215 * @param {Function} callback Callback, called as callback(event)
15216 * @returns {*}
15217 */
15218function onRelease(hammer, callback) {
15219 callback.inputHandler = function (event) {
15220 if (event.isFinal) {
15221 callback(event);
15222 }
15223 };
15224
15225 return hammer.on("hammer.input", callback.inputHandler);
15226}
15227
15228/**
15229 * Create the main frame for the Network.
15230 * This function is executed once when a Network object is created. The frame
15231 * contains a canvas, and this canvas contains all objects like the axis and
15232 * nodes.
15233 */
15234class Canvas {
15235 /**
15236 * @param {object} body
15237 */
15238 constructor(body) {
15239 this.body = body;
15240 this.pixelRatio = 1;
15241 this.cameraState = {};
15242 this.initialized = false;
15243 this.canvasViewCenter = {};
15244 this._cleanupCallbacks = [];
15245
15246 this.options = {};
15247 this.defaultOptions = {
15248 autoResize: true,
15249 height: "100%",
15250 width: "100%",
15251 };
15252 Object.assign(this.options, this.defaultOptions);
15253
15254 this.bindEventListeners();
15255 }
15256
15257 /**
15258 * Binds event listeners
15259 */
15260 bindEventListeners() {
15261 // bind the events
15262 this.body.emitter.once("resize", (obj) => {
15263 if (obj.width !== 0) {
15264 this.body.view.translation.x = obj.width * 0.5;
15265 }
15266 if (obj.height !== 0) {
15267 this.body.view.translation.y = obj.height * 0.5;
15268 }
15269 });
15270 this.body.emitter.on("setSize", this.setSize.bind(this));
15271 this.body.emitter.on("destroy", () => {
15272 this.hammerFrame.destroy();
15273 this.hammer.destroy();
15274 this._cleanUp();
15275 });
15276 }
15277
15278 /**
15279 * @param {object} options
15280 */
15281 setOptions(options) {
15282 if (options !== undefined) {
15283 const fields = ["width", "height", "autoResize"];
15284 selectiveDeepExtend(fields, this.options, options);
15285 }
15286
15287 // Automatically adapt to changing size of the container element.
15288 this._cleanUp();
15289 if (this.options.autoResize === true) {
15290 if (window.ResizeObserver) {
15291 // decent browsers, immediate reactions
15292 const observer = new ResizeObserver(() => {
15293 const changed = this.setSize();
15294 if (changed === true) {
15295 this.body.emitter.emit("_requestRedraw");
15296 }
15297 });
15298 const { frame } = this;
15299
15300 observer.observe(frame);
15301 this._cleanupCallbacks.push(() => {
15302 observer.unobserve(frame);
15303 });
15304 } else {
15305 // IE11, continous polling
15306 const resizeTimer = setInterval(() => {
15307 const changed = this.setSize();
15308 if (changed === true) {
15309 this.body.emitter.emit("_requestRedraw");
15310 }
15311 }, 1000);
15312 this._cleanupCallbacks.push(() => {
15313 clearInterval(resizeTimer);
15314 });
15315 }
15316
15317 // Automatically adapt to changing size of the browser.
15318 const resizeFunction = this._onResize.bind(this);
15319 addEventListener(window, "resize", resizeFunction);
15320 this._cleanupCallbacks.push(() => {
15321 removeEventListener(window, "resize", resizeFunction);
15322 });
15323 }
15324 }
15325
15326 /**
15327 * @private
15328 */
15329 _cleanUp() {
15330 this._cleanupCallbacks
15331 .splice(0)
15332 .reverse()
15333 .forEach((callback) => {
15334 try {
15335 callback();
15336 } catch (error) {
15337 console.error(error);
15338 }
15339 });
15340 }
15341
15342 /**
15343 * @private
15344 */
15345 _onResize() {
15346 this.setSize();
15347 this.body.emitter.emit("_redraw");
15348 }
15349
15350 /**
15351 * Get and store the cameraState
15352 *
15353 * @param {number} [pixelRatio=this.pixelRatio]
15354 * @private
15355 */
15356 _getCameraState(pixelRatio = this.pixelRatio) {
15357 if (this.initialized === true) {
15358 this.cameraState.previousWidth = this.frame.canvas.width / pixelRatio;
15359 this.cameraState.previousHeight = this.frame.canvas.height / pixelRatio;
15360 this.cameraState.scale = this.body.view.scale;
15361 this.cameraState.position = this.DOMtoCanvas({
15362 x: (0.5 * this.frame.canvas.width) / pixelRatio,
15363 y: (0.5 * this.frame.canvas.height) / pixelRatio,
15364 });
15365 }
15366 }
15367
15368 /**
15369 * Set the cameraState
15370 *
15371 * @private
15372 */
15373 _setCameraState() {
15374 if (
15375 this.cameraState.scale !== undefined &&
15376 this.frame.canvas.clientWidth !== 0 &&
15377 this.frame.canvas.clientHeight !== 0 &&
15378 this.pixelRatio !== 0 &&
15379 this.cameraState.previousWidth > 0 &&
15380 this.cameraState.previousHeight > 0
15381 ) {
15382 const widthRatio =
15383 this.frame.canvas.width /
15384 this.pixelRatio /
15385 this.cameraState.previousWidth;
15386 const heightRatio =
15387 this.frame.canvas.height /
15388 this.pixelRatio /
15389 this.cameraState.previousHeight;
15390 let newScale = this.cameraState.scale;
15391
15392 if (widthRatio != 1 && heightRatio != 1) {
15393 newScale = this.cameraState.scale * 0.5 * (widthRatio + heightRatio);
15394 } else if (widthRatio != 1) {
15395 newScale = this.cameraState.scale * widthRatio;
15396 } else if (heightRatio != 1) {
15397 newScale = this.cameraState.scale * heightRatio;
15398 }
15399
15400 this.body.view.scale = newScale;
15401 // this comes from the view module.
15402 const currentViewCenter = this.DOMtoCanvas({
15403 x: 0.5 * this.frame.canvas.clientWidth,
15404 y: 0.5 * this.frame.canvas.clientHeight,
15405 });
15406
15407 const distanceFromCenter = {
15408 // offset from view, distance view has to change by these x and y to center the node
15409 x: currentViewCenter.x - this.cameraState.position.x,
15410 y: currentViewCenter.y - this.cameraState.position.y,
15411 };
15412 this.body.view.translation.x +=
15413 distanceFromCenter.x * this.body.view.scale;
15414 this.body.view.translation.y +=
15415 distanceFromCenter.y * this.body.view.scale;
15416 }
15417 }
15418
15419 /**
15420 *
15421 * @param {number|string} value
15422 * @returns {string}
15423 * @private
15424 */
15425 _prepareValue(value) {
15426 if (typeof value === "number") {
15427 return value + "px";
15428 } else if (typeof value === "string") {
15429 if (value.indexOf("%") !== -1 || value.indexOf("px") !== -1) {
15430 return value;
15431 } else if (value.indexOf("%") === -1) {
15432 return value + "px";
15433 }
15434 }
15435 throw new Error(
15436 "Could not use the value supplied for width or height:" + value
15437 );
15438 }
15439
15440 /**
15441 * Create the HTML
15442 */
15443 _create() {
15444 // remove all elements from the container element.
15445 while (this.body.container.hasChildNodes()) {
15446 this.body.container.removeChild(this.body.container.firstChild);
15447 }
15448
15449 this.frame = document.createElement("div");
15450 this.frame.className = "vis-network";
15451 this.frame.style.position = "relative";
15452 this.frame.style.overflow = "hidden";
15453 this.frame.tabIndex = 0; // tab index is required for keycharm to bind keystrokes to the div instead of the window
15454
15455 //////////////////////////////////////////////////////////////////
15456
15457 this.frame.canvas = document.createElement("canvas");
15458 this.frame.canvas.style.position = "relative";
15459 this.frame.appendChild(this.frame.canvas);
15460
15461 if (!this.frame.canvas.getContext) {
15462 const noCanvas = document.createElement("DIV");
15463 noCanvas.style.color = "red";
15464 noCanvas.style.fontWeight = "bold";
15465 noCanvas.style.padding = "10px";
15466 noCanvas.innerText = "Error: your browser does not support HTML canvas";
15467 this.frame.canvas.appendChild(noCanvas);
15468 } else {
15469 this._setPixelRatio();
15470 this.setTransform();
15471 }
15472
15473 // add the frame to the container element
15474 this.body.container.appendChild(this.frame);
15475
15476 this.body.view.scale = 1;
15477 this.body.view.translation = {
15478 x: 0.5 * this.frame.canvas.clientWidth,
15479 y: 0.5 * this.frame.canvas.clientHeight,
15480 };
15481
15482 this._bindHammer();
15483 }
15484
15485 /**
15486 * This function binds hammer, it can be repeated over and over due to the uniqueness check.
15487 *
15488 * @private
15489 */
15490 _bindHammer() {
15491 if (this.hammer !== undefined) {
15492 this.hammer.destroy();
15493 }
15494 this.drag = {};
15495 this.pinch = {};
15496
15497 // init hammer
15498 this.hammer = new Hammer(this.frame.canvas);
15499 this.hammer.get("pinch").set({ enable: true });
15500 // enable to get better response, todo: test on mobile.
15501 this.hammer
15502 .get("pan")
15503 .set({ threshold: 5, direction: Hammer.DIRECTION_ALL });
15504
15505 onTouch(this.hammer, (event) => {
15506 this.body.eventListeners.onTouch(event);
15507 });
15508 this.hammer.on("tap", (event) => {
15509 this.body.eventListeners.onTap(event);
15510 });
15511 this.hammer.on("doubletap", (event) => {
15512 this.body.eventListeners.onDoubleTap(event);
15513 });
15514 this.hammer.on("press", (event) => {
15515 this.body.eventListeners.onHold(event);
15516 });
15517 this.hammer.on("panstart", (event) => {
15518 this.body.eventListeners.onDragStart(event);
15519 });
15520 this.hammer.on("panmove", (event) => {
15521 this.body.eventListeners.onDrag(event);
15522 });
15523 this.hammer.on("panend", (event) => {
15524 this.body.eventListeners.onDragEnd(event);
15525 });
15526 this.hammer.on("pinch", (event) => {
15527 this.body.eventListeners.onPinch(event);
15528 });
15529
15530 // TODO: neatly cleanup these handlers when re-creating the Canvas, IF these are done with hammer, event.stopPropagation will not work?
15531 this.frame.canvas.addEventListener("wheel", (event) => {
15532 this.body.eventListeners.onMouseWheel(event);
15533 });
15534
15535 this.frame.canvas.addEventListener("mousemove", (event) => {
15536 this.body.eventListeners.onMouseMove(event);
15537 });
15538 this.frame.canvas.addEventListener("contextmenu", (event) => {
15539 this.body.eventListeners.onContext(event);
15540 });
15541
15542 this.hammerFrame = new Hammer(this.frame);
15543 onRelease(this.hammerFrame, (event) => {
15544 this.body.eventListeners.onRelease(event);
15545 });
15546 }
15547
15548 /**
15549 * Set a new size for the network
15550 *
15551 * @param {string} width Width in pixels or percentage (for example '800px'
15552 * or '50%')
15553 * @param {string} height Height in pixels or percentage (for example '400px'
15554 * or '30%')
15555 * @returns {boolean}
15556 */
15557 setSize(width = this.options.width, height = this.options.height) {
15558 width = this._prepareValue(width);
15559 height = this._prepareValue(height);
15560
15561 let emitEvent = false;
15562 const oldWidth = this.frame.canvas.width;
15563 const oldHeight = this.frame.canvas.height;
15564
15565 // update the pixel ratio
15566 //
15567 // NOTE: Comment in following is rather inconsistent; this is the ONLY place in the code
15568 // where it is assumed that the pixel ratio could change at runtime.
15569 // The only way I can think of this happening is a rotating screen or tablet; but then
15570 // there should be a mechanism for reloading the data (TODO: check if this is present).
15571 //
15572 // If the assumption is true (i.e. pixel ratio can change at runtime), then *all* usage
15573 // of pixel ratio must be overhauled for this.
15574 //
15575 // For the time being, I will humor the assumption here, and in the rest of the code assume it is
15576 // constant.
15577 const previousRatio = this.pixelRatio; // we cache this because the camera state storage needs the old value
15578 this._setPixelRatio();
15579
15580 if (
15581 width != this.options.width ||
15582 height != this.options.height ||
15583 this.frame.style.width != width ||
15584 this.frame.style.height != height
15585 ) {
15586 this._getCameraState(previousRatio);
15587
15588 this.frame.style.width = width;
15589 this.frame.style.height = height;
15590
15591 this.frame.canvas.style.width = "100%";
15592 this.frame.canvas.style.height = "100%";
15593
15594 this.frame.canvas.width = Math.round(
15595 this.frame.canvas.clientWidth * this.pixelRatio
15596 );
15597 this.frame.canvas.height = Math.round(
15598 this.frame.canvas.clientHeight * this.pixelRatio
15599 );
15600
15601 this.options.width = width;
15602 this.options.height = height;
15603
15604 this.canvasViewCenter = {
15605 x: 0.5 * this.frame.clientWidth,
15606 y: 0.5 * this.frame.clientHeight,
15607 };
15608
15609 emitEvent = true;
15610 } else {
15611 // this would adapt the width of the canvas to the width from 100% if and only if
15612 // there is a change.
15613
15614 const newWidth = Math.round(
15615 this.frame.canvas.clientWidth * this.pixelRatio
15616 );
15617 const newHeight = Math.round(
15618 this.frame.canvas.clientHeight * this.pixelRatio
15619 );
15620
15621 // store the camera if there is a change in size.
15622 if (
15623 this.frame.canvas.width !== newWidth ||
15624 this.frame.canvas.height !== newHeight
15625 ) {
15626 this._getCameraState(previousRatio);
15627 }
15628
15629 if (this.frame.canvas.width !== newWidth) {
15630 this.frame.canvas.width = newWidth;
15631 emitEvent = true;
15632 }
15633 if (this.frame.canvas.height !== newHeight) {
15634 this.frame.canvas.height = newHeight;
15635 emitEvent = true;
15636 }
15637 }
15638
15639 if (emitEvent === true) {
15640 this.body.emitter.emit("resize", {
15641 width: Math.round(this.frame.canvas.width / this.pixelRatio),
15642 height: Math.round(this.frame.canvas.height / this.pixelRatio),
15643 oldWidth: Math.round(oldWidth / this.pixelRatio),
15644 oldHeight: Math.round(oldHeight / this.pixelRatio),
15645 });
15646
15647 // restore the camera on change.
15648 this._setCameraState();
15649 }
15650
15651 // set initialized so the get and set camera will work from now on.
15652 this.initialized = true;
15653 return emitEvent;
15654 }
15655
15656 /**
15657 *
15658 * @returns {CanvasRenderingContext2D}
15659 */
15660 getContext() {
15661 return this.frame.canvas.getContext("2d");
15662 }
15663
15664 /**
15665 * Determine the pixel ratio for various browsers.
15666 *
15667 * @returns {number}
15668 * @private
15669 */
15670 _determinePixelRatio() {
15671 const ctx = this.getContext();
15672 if (ctx === undefined) {
15673 throw new Error("Could not get canvax context");
15674 }
15675
15676 let numerator = 1;
15677 if (typeof window !== "undefined") {
15678 // (window !== undefined) doesn't work here!
15679 // Protection during unit tests, where 'window' can be missing
15680 numerator = window.devicePixelRatio || 1;
15681 }
15682
15683 const denominator =
15684 ctx.webkitBackingStorePixelRatio ||
15685 ctx.mozBackingStorePixelRatio ||
15686 ctx.msBackingStorePixelRatio ||
15687 ctx.oBackingStorePixelRatio ||
15688 ctx.backingStorePixelRatio ||
15689 1;
15690
15691 return numerator / denominator;
15692 }
15693
15694 /**
15695 * Lazy determination of pixel ratio.
15696 *
15697 * @private
15698 */
15699 _setPixelRatio() {
15700 this.pixelRatio = this._determinePixelRatio();
15701 }
15702
15703 /**
15704 * Set the transform in the contained context, based on its pixelRatio
15705 */
15706 setTransform() {
15707 const ctx = this.getContext();
15708 if (ctx === undefined) {
15709 throw new Error("Could not get canvax context");
15710 }
15711
15712 ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0);
15713 }
15714
15715 /**
15716 * Convert the X coordinate in DOM-space (coordinate point in browser relative to the container div) to
15717 * the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon)
15718 *
15719 * @param {number} x
15720 * @returns {number}
15721 * @private
15722 */
15723 _XconvertDOMtoCanvas(x) {
15724 return (x - this.body.view.translation.x) / this.body.view.scale;
15725 }
15726
15727 /**
15728 * Convert the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon) to
15729 * the X coordinate in DOM-space (coordinate point in browser relative to the container div)
15730 *
15731 * @param {number} x
15732 * @returns {number}
15733 * @private
15734 */
15735 _XconvertCanvasToDOM(x) {
15736 return x * this.body.view.scale + this.body.view.translation.x;
15737 }
15738
15739 /**
15740 * Convert the Y coordinate in DOM-space (coordinate point in browser relative to the container div) to
15741 * the Y coordinate in canvas-space (the simulation sandbox, which the camera looks upon)
15742 *
15743 * @param {number} y
15744 * @returns {number}
15745 * @private
15746 */
15747 _YconvertDOMtoCanvas(y) {
15748 return (y - this.body.view.translation.y) / this.body.view.scale;
15749 }
15750
15751 /**
15752 * Convert the Y coordinate in canvas-space (the simulation sandbox, which the camera looks upon) to
15753 * the Y coordinate in DOM-space (coordinate point in browser relative to the container div)
15754 *
15755 * @param {number} y
15756 * @returns {number}
15757 * @private
15758 */
15759 _YconvertCanvasToDOM(y) {
15760 return y * this.body.view.scale + this.body.view.translation.y;
15761 }
15762
15763 /**
15764 * @param {point} pos
15765 * @returns {point}
15766 */
15767 canvasToDOM(pos) {
15768 return {
15769 x: this._XconvertCanvasToDOM(pos.x),
15770 y: this._YconvertCanvasToDOM(pos.y),
15771 };
15772 }
15773
15774 /**
15775 *
15776 * @param {point} pos
15777 * @returns {point}
15778 */
15779 DOMtoCanvas(pos) {
15780 return {
15781 x: this._XconvertDOMtoCanvas(pos.x),
15782 y: this._YconvertDOMtoCanvas(pos.y),
15783 };
15784 }
15785}
15786
15787/**
15788 * Validate the fit options, replace missing optional values by defaults etc.
15789 *
15790 * @param rawOptions - The raw options.
15791 * @param allNodeIds - All node ids that will be used if nodes are omitted in
15792 * the raw options.
15793 *
15794 * @returns Options with everything filled in and validated.
15795 */
15796function normalizeFitOptions(rawOptions, allNodeIds) {
15797 const options = Object.assign({
15798 nodes: allNodeIds,
15799 minZoomLevel: Number.MIN_VALUE,
15800 maxZoomLevel: 1,
15801 }, rawOptions ?? {});
15802 if (!Array.isArray(options.nodes)) {
15803 throw new TypeError("Nodes has to be an array of ids.");
15804 }
15805 if (options.nodes.length === 0) {
15806 options.nodes = allNodeIds;
15807 }
15808 if (!(typeof options.minZoomLevel === "number" && options.minZoomLevel > 0)) {
15809 throw new TypeError("Min zoom level has to be a number higher than zero.");
15810 }
15811 if (!(typeof options.maxZoomLevel === "number" &&
15812 options.minZoomLevel <= options.maxZoomLevel)) {
15813 throw new TypeError("Max zoom level has to be a number higher than min zoom level.");
15814 }
15815 return options;
15816}
15817
15818/**
15819 * The view
15820 */
15821class View {
15822 /**
15823 * @param {object} body
15824 * @param {Canvas} canvas
15825 */
15826 constructor(body, canvas) {
15827 this.body = body;
15828 this.canvas = canvas;
15829
15830 this.animationSpeed = 1 / this.renderRefreshRate;
15831 this.animationEasingFunction = "easeInOutQuint";
15832 this.easingTime = 0;
15833 this.sourceScale = 0;
15834 this.targetScale = 0;
15835 this.sourceTranslation = 0;
15836 this.targetTranslation = 0;
15837 this.lockedOnNodeId = undefined;
15838 this.lockedOnNodeOffset = undefined;
15839 this.touchTime = 0;
15840
15841 this.viewFunction = undefined;
15842
15843 this.body.emitter.on("fit", this.fit.bind(this));
15844 this.body.emitter.on("animationFinished", () => {
15845 this.body.emitter.emit("_stopRendering");
15846 });
15847 this.body.emitter.on("unlockNode", this.releaseNode.bind(this));
15848 }
15849
15850 /**
15851 *
15852 * @param {object} [options={}]
15853 */
15854 setOptions(options = {}) {
15855 this.options = options;
15856 }
15857
15858 /**
15859 * This function zooms out to fit all data on screen based on amount of nodes
15860 *
15861 * @param {object} [options={{nodes=Array}}]
15862 * @param options
15863 * @param {boolean} [initialZoom=false] | zoom based on fitted formula or range, true = fitted, default = false;
15864 */
15865 fit(options, initialZoom = false) {
15866 options = normalizeFitOptions(options, this.body.nodeIndices);
15867
15868 const canvasWidth = this.canvas.frame.canvas.clientWidth;
15869 const canvasHeight = this.canvas.frame.canvas.clientHeight;
15870
15871 let range;
15872 let zoomLevel;
15873 if (canvasWidth === 0 || canvasHeight === 0) {
15874 // There's no point in trying to fit into zero sized canvas. This could
15875 // potentially even result in invalid values being computed. For example
15876 // for network without nodes and zero sized canvas the zoom level would
15877 // end up being computed as 0/0 which results in NaN. In any other case
15878 // this would be 0/something which is again pointless to compute.
15879 zoomLevel = 1;
15880
15881 range = NetworkUtil.getRange(this.body.nodes, options.nodes);
15882 } else if (initialZoom === true) {
15883 // check if more than half of the nodes have a predefined position. If so, we use the range, not the approximation.
15884 let positionDefined = 0;
15885 for (const nodeId in this.body.nodes) {
15886 if (Object.prototype.hasOwnProperty.call(this.body.nodes, nodeId)) {
15887 const node = this.body.nodes[nodeId];
15888 if (node.predefinedPosition === true) {
15889 positionDefined += 1;
15890 }
15891 }
15892 }
15893 if (positionDefined > 0.5 * this.body.nodeIndices.length) {
15894 this.fit(options, false);
15895 return;
15896 }
15897
15898 range = NetworkUtil.getRange(this.body.nodes, options.nodes);
15899
15900 const numberOfNodes = this.body.nodeIndices.length;
15901 zoomLevel = 12.662 / (numberOfNodes + 7.4147) + 0.0964822; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
15902
15903 // correct for larger canvasses.
15904 const factor = Math.min(canvasWidth / 600, canvasHeight / 600);
15905 zoomLevel *= factor;
15906 } else {
15907 this.body.emitter.emit("_resizeNodes");
15908 range = NetworkUtil.getRange(this.body.nodes, options.nodes);
15909
15910 const xDistance = Math.abs(range.maxX - range.minX) * 1.1;
15911 const yDistance = Math.abs(range.maxY - range.minY) * 1.1;
15912
15913 const xZoomLevel = canvasWidth / xDistance;
15914 const yZoomLevel = canvasHeight / yDistance;
15915
15916 zoomLevel = xZoomLevel <= yZoomLevel ? xZoomLevel : yZoomLevel;
15917 }
15918
15919 if (zoomLevel > options.maxZoomLevel) {
15920 zoomLevel = options.maxZoomLevel;
15921 } else if (zoomLevel < options.minZoomLevel) {
15922 zoomLevel = options.minZoomLevel;
15923 }
15924
15925 const center = NetworkUtil.findCenter(range);
15926 const animationOptions = {
15927 position: center,
15928 scale: zoomLevel,
15929 animation: options.animation,
15930 };
15931 this.moveTo(animationOptions);
15932 }
15933
15934 // animation
15935
15936 /**
15937 * Center a node in view.
15938 *
15939 * @param {number} nodeId
15940 * @param {number} [options]
15941 */
15942 focus(nodeId, options = {}) {
15943 if (this.body.nodes[nodeId] !== undefined) {
15944 const nodePosition = {
15945 x: this.body.nodes[nodeId].x,
15946 y: this.body.nodes[nodeId].y,
15947 };
15948 options.position = nodePosition;
15949 options.lockedOnNode = nodeId;
15950
15951 this.moveTo(options);
15952 } else {
15953 console.error("Node: " + nodeId + " cannot be found.");
15954 }
15955 }
15956
15957 /**
15958 *
15959 * @param {object} options | options.offset = {x:number, y:number} // offset from the center in DOM pixels
15960 * | options.scale = number // scale to move to
15961 * | options.position = {x:number, y:number} // position to move to
15962 * | options.animation = {duration:number, easingFunction:String} || Boolean // position to move to
15963 */
15964 moveTo(options) {
15965 if (options === undefined) {
15966 options = {};
15967 return;
15968 }
15969
15970 if (options.offset != null) {
15971 if (options.offset.x != null) {
15972 // Coerce and verify that x is valid.
15973 options.offset.x = +options.offset.x;
15974 if (!Number.isFinite(options.offset.x)) {
15975 throw new TypeError(
15976 'The option "offset.x" has to be a finite number.'
15977 );
15978 }
15979 } else {
15980 options.offset.x = 0;
15981 }
15982
15983 if (options.offset.y != null) {
15984 // Coerce and verify that y is valid.
15985 options.offset.y = +options.offset.y;
15986 if (!Number.isFinite(options.offset.y)) {
15987 throw new TypeError(
15988 'The option "offset.y" has to be a finite number.'
15989 );
15990 }
15991 } else {
15992 options.offset.x = 0;
15993 }
15994 } else {
15995 options.offset = {
15996 x: 0,
15997 y: 0,
15998 };
15999 }
16000
16001 if (options.position != null) {
16002 if (options.position.x != null) {
16003 // Coerce and verify that x is valid.
16004 options.position.x = +options.position.x;
16005 if (!Number.isFinite(options.position.x)) {
16006 throw new TypeError(
16007 'The option "position.x" has to be a finite number.'
16008 );
16009 }
16010 } else {
16011 options.position.x = 0;
16012 }
16013
16014 if (options.position.y != null) {
16015 // Coerce and verify that y is valid.
16016 options.position.y = +options.position.y;
16017 if (!Number.isFinite(options.position.y)) {
16018 throw new TypeError(
16019 'The option "position.y" has to be a finite number.'
16020 );
16021 }
16022 } else {
16023 options.position.x = 0;
16024 }
16025 } else {
16026 options.position = this.getViewPosition();
16027 }
16028
16029 if (options.scale != null) {
16030 // Coerce and verify that the scale is valid.
16031 options.scale = +options.scale;
16032 if (!(options.scale > 0)) {
16033 throw new TypeError(
16034 'The option "scale" has to be a number greater than zero.'
16035 );
16036 }
16037 } else {
16038 options.scale = this.body.view.scale;
16039 }
16040
16041 if (options.animation === undefined) {
16042 options.animation = { duration: 0 };
16043 }
16044 if (options.animation === false) {
16045 options.animation = { duration: 0 };
16046 }
16047 if (options.animation === true) {
16048 options.animation = {};
16049 }
16050 if (options.animation.duration === undefined) {
16051 options.animation.duration = 1000;
16052 } // default duration
16053 if (options.animation.easingFunction === undefined) {
16054 options.animation.easingFunction = "easeInOutQuad";
16055 } // default easing function
16056
16057 this.animateView(options);
16058 }
16059
16060 /**
16061 *
16062 * @param {object} options | options.offset = {x:number, y:number} // offset from the center in DOM pixels
16063 * | options.time = number // animation time in milliseconds
16064 * | options.scale = number // scale to animate to
16065 * | options.position = {x:number, y:number} // position to animate to
16066 * | options.easingFunction = String // linear, easeInQuad, easeOutQuad, easeInOutQuad,
16067 * // easeInCubic, easeOutCubic, easeInOutCubic,
16068 * // easeInQuart, easeOutQuart, easeInOutQuart,
16069 * // easeInQuint, easeOutQuint, easeInOutQuint
16070 */
16071 animateView(options) {
16072 if (options === undefined) {
16073 return;
16074 }
16075 this.animationEasingFunction = options.animation.easingFunction;
16076 // release if something focussed on the node
16077 this.releaseNode();
16078 if (options.locked === true) {
16079 this.lockedOnNodeId = options.lockedOnNode;
16080 this.lockedOnNodeOffset = options.offset;
16081 }
16082
16083 // forcefully complete the old animation if it was still running
16084 if (this.easingTime != 0) {
16085 this._transitionRedraw(true); // by setting easingtime to 1, we finish the animation.
16086 }
16087
16088 this.sourceScale = this.body.view.scale;
16089 this.sourceTranslation = this.body.view.translation;
16090 this.targetScale = options.scale;
16091
16092 // set the scale so the viewCenter is based on the correct zoom level. This is overridden in the transitionRedraw
16093 // but at least then we'll have the target transition
16094 this.body.view.scale = this.targetScale;
16095 const viewCenter = this.canvas.DOMtoCanvas({
16096 x: 0.5 * this.canvas.frame.canvas.clientWidth,
16097 y: 0.5 * this.canvas.frame.canvas.clientHeight,
16098 });
16099
16100 const distanceFromCenter = {
16101 // offset from view, distance view has to change by these x and y to center the node
16102 x: viewCenter.x - options.position.x,
16103 y: viewCenter.y - options.position.y,
16104 };
16105 this.targetTranslation = {
16106 x:
16107 this.sourceTranslation.x +
16108 distanceFromCenter.x * this.targetScale +
16109 options.offset.x,
16110 y:
16111 this.sourceTranslation.y +
16112 distanceFromCenter.y * this.targetScale +
16113 options.offset.y,
16114 };
16115
16116 // if the time is set to 0, don't do an animation
16117 if (options.animation.duration === 0) {
16118 if (this.lockedOnNodeId != undefined) {
16119 this.viewFunction = this._lockedRedraw.bind(this);
16120 this.body.emitter.on("initRedraw", this.viewFunction);
16121 } else {
16122 this.body.view.scale = this.targetScale;
16123 this.body.view.translation = this.targetTranslation;
16124 this.body.emitter.emit("_requestRedraw");
16125 }
16126 } else {
16127 this.animationSpeed =
16128 1 / (60 * options.animation.duration * 0.001) || 1 / 60; // 60 for 60 seconds, 0.001 for milli's
16129 this.animationEasingFunction = options.animation.easingFunction;
16130
16131 this.viewFunction = this._transitionRedraw.bind(this);
16132 this.body.emitter.on("initRedraw", this.viewFunction);
16133 this.body.emitter.emit("_startRendering");
16134 }
16135 }
16136
16137 /**
16138 * used to animate smoothly by hijacking the redraw function.
16139 *
16140 * @private
16141 */
16142 _lockedRedraw() {
16143 const nodePosition = {
16144 x: this.body.nodes[this.lockedOnNodeId].x,
16145 y: this.body.nodes[this.lockedOnNodeId].y,
16146 };
16147 const viewCenter = this.canvas.DOMtoCanvas({
16148 x: 0.5 * this.canvas.frame.canvas.clientWidth,
16149 y: 0.5 * this.canvas.frame.canvas.clientHeight,
16150 });
16151 const distanceFromCenter = {
16152 // offset from view, distance view has to change by these x and y to center the node
16153 x: viewCenter.x - nodePosition.x,
16154 y: viewCenter.y - nodePosition.y,
16155 };
16156 const sourceTranslation = this.body.view.translation;
16157 const targetTranslation = {
16158 x:
16159 sourceTranslation.x +
16160 distanceFromCenter.x * this.body.view.scale +
16161 this.lockedOnNodeOffset.x,
16162 y:
16163 sourceTranslation.y +
16164 distanceFromCenter.y * this.body.view.scale +
16165 this.lockedOnNodeOffset.y,
16166 };
16167
16168 this.body.view.translation = targetTranslation;
16169 }
16170
16171 /**
16172 * Resets state of a locked on Node
16173 */
16174 releaseNode() {
16175 if (this.lockedOnNodeId !== undefined && this.viewFunction !== undefined) {
16176 this.body.emitter.off("initRedraw", this.viewFunction);
16177 this.lockedOnNodeId = undefined;
16178 this.lockedOnNodeOffset = undefined;
16179 }
16180 }
16181
16182 /**
16183 * @param {boolean} [finished=false]
16184 * @private
16185 */
16186 _transitionRedraw(finished = false) {
16187 this.easingTime += this.animationSpeed;
16188 this.easingTime = finished === true ? 1.0 : this.easingTime;
16189
16190 const progress = easingFunctions[this.animationEasingFunction](
16191 this.easingTime
16192 );
16193
16194 this.body.view.scale =
16195 this.sourceScale + (this.targetScale - this.sourceScale) * progress;
16196 this.body.view.translation = {
16197 x:
16198 this.sourceTranslation.x +
16199 (this.targetTranslation.x - this.sourceTranslation.x) * progress,
16200 y:
16201 this.sourceTranslation.y +
16202 (this.targetTranslation.y - this.sourceTranslation.y) * progress,
16203 };
16204
16205 // cleanup
16206 if (this.easingTime >= 1.0) {
16207 this.body.emitter.off("initRedraw", this.viewFunction);
16208 this.easingTime = 0;
16209 if (this.lockedOnNodeId != undefined) {
16210 this.viewFunction = this._lockedRedraw.bind(this);
16211 this.body.emitter.on("initRedraw", this.viewFunction);
16212 }
16213 this.body.emitter.emit("animationFinished");
16214 }
16215 }
16216
16217 /**
16218 *
16219 * @returns {number}
16220 */
16221 getScale() {
16222 return this.body.view.scale;
16223 }
16224
16225 /**
16226 *
16227 * @returns {{x: number, y: number}}
16228 */
16229 getViewPosition() {
16230 return this.canvas.DOMtoCanvas({
16231 x: 0.5 * this.canvas.frame.canvas.clientWidth,
16232 y: 0.5 * this.canvas.frame.canvas.clientHeight,
16233 });
16234 }
16235}
16236
16237/**
16238 * Navigation Handler
16239 */
16240class NavigationHandler {
16241 /**
16242 * @param {object} body
16243 * @param {Canvas} canvas
16244 */
16245 constructor(body, canvas) {
16246 this.body = body;
16247 this.canvas = canvas;
16248
16249 this.iconsCreated = false;
16250 this.navigationHammers = [];
16251 this.boundFunctions = {};
16252 this.touchTime = 0;
16253 this.activated = false;
16254
16255 this.body.emitter.on("activate", () => {
16256 this.activated = true;
16257 this.configureKeyboardBindings();
16258 });
16259 this.body.emitter.on("deactivate", () => {
16260 this.activated = false;
16261 this.configureKeyboardBindings();
16262 });
16263 this.body.emitter.on("destroy", () => {
16264 if (this.keycharm !== undefined) {
16265 this.keycharm.destroy();
16266 }
16267 });
16268
16269 this.options = {};
16270 }
16271
16272 /**
16273 *
16274 * @param {object} options
16275 */
16276 setOptions(options) {
16277 if (options !== undefined) {
16278 this.options = options;
16279 this.create();
16280 }
16281 }
16282
16283 /**
16284 * Creates or refreshes navigation and sets key bindings
16285 */
16286 create() {
16287 if (this.options.navigationButtons === true) {
16288 if (this.iconsCreated === false) {
16289 this.loadNavigationElements();
16290 }
16291 } else if (this.iconsCreated === true) {
16292 this.cleanNavigation();
16293 }
16294
16295 this.configureKeyboardBindings();
16296 }
16297
16298 /**
16299 * Cleans up previous navigation items
16300 */
16301 cleanNavigation() {
16302 // clean hammer bindings
16303 if (this.navigationHammers.length != 0) {
16304 for (let i = 0; i < this.navigationHammers.length; i++) {
16305 this.navigationHammers[i].destroy();
16306 }
16307 this.navigationHammers = [];
16308 }
16309
16310 // clean up previous navigation items
16311 if (
16312 this.navigationDOM &&
16313 this.navigationDOM["wrapper"] &&
16314 this.navigationDOM["wrapper"].parentNode
16315 ) {
16316 this.navigationDOM["wrapper"].parentNode.removeChild(
16317 this.navigationDOM["wrapper"]
16318 );
16319 }
16320
16321 this.iconsCreated = false;
16322 }
16323
16324 /**
16325 * Creation of the navigation controls nodes. They are drawn over the rest of the nodes and are not affected by scale and translation
16326 * they have a triggerFunction which is called on click. If the position of the navigation controls is dependent
16327 * on this.frame.canvas.clientWidth or this.frame.canvas.clientHeight, we flag horizontalAlignLeft and verticalAlignTop false.
16328 * This means that the location will be corrected by the _relocateNavigation function on a size change of the canvas.
16329 *
16330 * @private
16331 */
16332 loadNavigationElements() {
16333 this.cleanNavigation();
16334
16335 this.navigationDOM = {};
16336 const navigationDivs = [
16337 "up",
16338 "down",
16339 "left",
16340 "right",
16341 "zoomIn",
16342 "zoomOut",
16343 "zoomExtends",
16344 ];
16345 const navigationDivActions = [
16346 "_moveUp",
16347 "_moveDown",
16348 "_moveLeft",
16349 "_moveRight",
16350 "_zoomIn",
16351 "_zoomOut",
16352 "_fit",
16353 ];
16354
16355 this.navigationDOM["wrapper"] = document.createElement("div");
16356 this.navigationDOM["wrapper"].className = "vis-navigation";
16357 this.canvas.frame.appendChild(this.navigationDOM["wrapper"]);
16358
16359 for (let i = 0; i < navigationDivs.length; i++) {
16360 this.navigationDOM[navigationDivs[i]] = document.createElement("div");
16361 this.navigationDOM[navigationDivs[i]].className =
16362 "vis-button vis-" + navigationDivs[i];
16363 this.navigationDOM["wrapper"].appendChild(
16364 this.navigationDOM[navigationDivs[i]]
16365 );
16366
16367 const hammer = new Hammer(this.navigationDOM[navigationDivs[i]]);
16368 if (navigationDivActions[i] === "_fit") {
16369 onTouch(hammer, this._fit.bind(this));
16370 } else {
16371 onTouch(hammer, this.bindToRedraw.bind(this, navigationDivActions[i]));
16372 }
16373
16374 this.navigationHammers.push(hammer);
16375 }
16376
16377 // use a hammer for the release so we do not require the one used in the rest of the network
16378 // the one the rest uses can be overloaded by the manipulation system.
16379 const hammerFrame = new Hammer(this.canvas.frame);
16380 onRelease(hammerFrame, () => {
16381 this._stopMovement();
16382 });
16383 this.navigationHammers.push(hammerFrame);
16384
16385 this.iconsCreated = true;
16386 }
16387
16388 /**
16389 *
16390 * @param {string} action
16391 */
16392 bindToRedraw(action) {
16393 if (this.boundFunctions[action] === undefined) {
16394 this.boundFunctions[action] = this[action].bind(this);
16395 this.body.emitter.on("initRedraw", this.boundFunctions[action]);
16396 this.body.emitter.emit("_startRendering");
16397 }
16398 }
16399
16400 /**
16401 *
16402 * @param {string} action
16403 */
16404 unbindFromRedraw(action) {
16405 if (this.boundFunctions[action] !== undefined) {
16406 this.body.emitter.off("initRedraw", this.boundFunctions[action]);
16407 this.body.emitter.emit("_stopRendering");
16408 delete this.boundFunctions[action];
16409 }
16410 }
16411
16412 /**
16413 * this stops all movement induced by the navigation buttons
16414 *
16415 * @private
16416 */
16417 _fit() {
16418 if (new Date().valueOf() - this.touchTime > 700) {
16419 // TODO: fix ugly hack to avoid hammer's double fireing of event (because we use release?)
16420 this.body.emitter.emit("fit", { duration: 700 });
16421 this.touchTime = new Date().valueOf();
16422 }
16423 }
16424
16425 /**
16426 * this stops all movement induced by the navigation buttons
16427 *
16428 * @private
16429 */
16430 _stopMovement() {
16431 for (const boundAction in this.boundFunctions) {
16432 if (
16433 Object.prototype.hasOwnProperty.call(this.boundFunctions, boundAction)
16434 ) {
16435 this.body.emitter.off("initRedraw", this.boundFunctions[boundAction]);
16436 this.body.emitter.emit("_stopRendering");
16437 }
16438 }
16439 this.boundFunctions = {};
16440 }
16441 /**
16442 *
16443 * @private
16444 */
16445 _moveUp() {
16446 this.body.view.translation.y += this.options.keyboard.speed.y;
16447 }
16448 /**
16449 *
16450 * @private
16451 */
16452 _moveDown() {
16453 this.body.view.translation.y -= this.options.keyboard.speed.y;
16454 }
16455 /**
16456 *
16457 * @private
16458 */
16459 _moveLeft() {
16460 this.body.view.translation.x += this.options.keyboard.speed.x;
16461 }
16462 /**
16463 *
16464 * @private
16465 */
16466 _moveRight() {
16467 this.body.view.translation.x -= this.options.keyboard.speed.x;
16468 }
16469 /**
16470 *
16471 * @private
16472 */
16473 _zoomIn() {
16474 const scaleOld = this.body.view.scale;
16475 const scale = this.body.view.scale * (1 + this.options.keyboard.speed.zoom);
16476 const translation = this.body.view.translation;
16477 const scaleFrac = scale / scaleOld;
16478 const tx =
16479 (1 - scaleFrac) * this.canvas.canvasViewCenter.x +
16480 translation.x * scaleFrac;
16481 const ty =
16482 (1 - scaleFrac) * this.canvas.canvasViewCenter.y +
16483 translation.y * scaleFrac;
16484
16485 this.body.view.scale = scale;
16486 this.body.view.translation = { x: tx, y: ty };
16487 this.body.emitter.emit("zoom", {
16488 direction: "+",
16489 scale: this.body.view.scale,
16490 pointer: null,
16491 });
16492 }
16493
16494 /**
16495 *
16496 * @private
16497 */
16498 _zoomOut() {
16499 const scaleOld = this.body.view.scale;
16500 const scale = this.body.view.scale / (1 + this.options.keyboard.speed.zoom);
16501 const translation = this.body.view.translation;
16502 const scaleFrac = scale / scaleOld;
16503 const tx =
16504 (1 - scaleFrac) * this.canvas.canvasViewCenter.x +
16505 translation.x * scaleFrac;
16506 const ty =
16507 (1 - scaleFrac) * this.canvas.canvasViewCenter.y +
16508 translation.y * scaleFrac;
16509
16510 this.body.view.scale = scale;
16511 this.body.view.translation = { x: tx, y: ty };
16512 this.body.emitter.emit("zoom", {
16513 direction: "-",
16514 scale: this.body.view.scale,
16515 pointer: null,
16516 });
16517 }
16518
16519 /**
16520 * bind all keys using keycharm.
16521 */
16522 configureKeyboardBindings() {
16523 if (this.keycharm !== undefined) {
16524 this.keycharm.destroy();
16525 }
16526
16527 if (this.options.keyboard.enabled === true) {
16528 if (this.options.keyboard.bindToWindow === true) {
16529 this.keycharm = keycharm({ container: window, preventDefault: true });
16530 } else {
16531 this.keycharm = keycharm({
16532 container: this.canvas.frame,
16533 preventDefault: true,
16534 });
16535 }
16536
16537 this.keycharm.reset();
16538
16539 if (this.activated === true) {
16540 this.keycharm.bind(
16541 "up",
16542 () => {
16543 this.bindToRedraw("_moveUp");
16544 },
16545 "keydown"
16546 );
16547 this.keycharm.bind(
16548 "down",
16549 () => {
16550 this.bindToRedraw("_moveDown");
16551 },
16552 "keydown"
16553 );
16554 this.keycharm.bind(
16555 "left",
16556 () => {
16557 this.bindToRedraw("_moveLeft");
16558 },
16559 "keydown"
16560 );
16561 this.keycharm.bind(
16562 "right",
16563 () => {
16564 this.bindToRedraw("_moveRight");
16565 },
16566 "keydown"
16567 );
16568 this.keycharm.bind(
16569 "=",
16570 () => {
16571 this.bindToRedraw("_zoomIn");
16572 },
16573 "keydown"
16574 );
16575 this.keycharm.bind(
16576 "num+",
16577 () => {
16578 this.bindToRedraw("_zoomIn");
16579 },
16580 "keydown"
16581 );
16582 this.keycharm.bind(
16583 "num-",
16584 () => {
16585 this.bindToRedraw("_zoomOut");
16586 },
16587 "keydown"
16588 );
16589 this.keycharm.bind(
16590 "-",
16591 () => {
16592 this.bindToRedraw("_zoomOut");
16593 },
16594 "keydown"
16595 );
16596 this.keycharm.bind(
16597 "[",
16598 () => {
16599 this.bindToRedraw("_zoomOut");
16600 },
16601 "keydown"
16602 );
16603 this.keycharm.bind(
16604 "]",
16605 () => {
16606 this.bindToRedraw("_zoomIn");
16607 },
16608 "keydown"
16609 );
16610 this.keycharm.bind(
16611 "pageup",
16612 () => {
16613 this.bindToRedraw("_zoomIn");
16614 },
16615 "keydown"
16616 );
16617 this.keycharm.bind(
16618 "pagedown",
16619 () => {
16620 this.bindToRedraw("_zoomOut");
16621 },
16622 "keydown"
16623 );
16624
16625 this.keycharm.bind(
16626 "up",
16627 () => {
16628 this.unbindFromRedraw("_moveUp");
16629 },
16630 "keyup"
16631 );
16632 this.keycharm.bind(
16633 "down",
16634 () => {
16635 this.unbindFromRedraw("_moveDown");
16636 },
16637 "keyup"
16638 );
16639 this.keycharm.bind(
16640 "left",
16641 () => {
16642 this.unbindFromRedraw("_moveLeft");
16643 },
16644 "keyup"
16645 );
16646 this.keycharm.bind(
16647 "right",
16648 () => {
16649 this.unbindFromRedraw("_moveRight");
16650 },
16651 "keyup"
16652 );
16653 this.keycharm.bind(
16654 "=",
16655 () => {
16656 this.unbindFromRedraw("_zoomIn");
16657 },
16658 "keyup"
16659 );
16660 this.keycharm.bind(
16661 "num+",
16662 () => {
16663 this.unbindFromRedraw("_zoomIn");
16664 },
16665 "keyup"
16666 );
16667 this.keycharm.bind(
16668 "num-",
16669 () => {
16670 this.unbindFromRedraw("_zoomOut");
16671 },
16672 "keyup"
16673 );
16674 this.keycharm.bind(
16675 "-",
16676 () => {
16677 this.unbindFromRedraw("_zoomOut");
16678 },
16679 "keyup"
16680 );
16681 this.keycharm.bind(
16682 "[",
16683 () => {
16684 this.unbindFromRedraw("_zoomOut");
16685 },
16686 "keyup"
16687 );
16688 this.keycharm.bind(
16689 "]",
16690 () => {
16691 this.unbindFromRedraw("_zoomIn");
16692 },
16693 "keyup"
16694 );
16695 this.keycharm.bind(
16696 "pageup",
16697 () => {
16698 this.unbindFromRedraw("_zoomIn");
16699 },
16700 "keyup"
16701 );
16702 this.keycharm.bind(
16703 "pagedown",
16704 () => {
16705 this.unbindFromRedraw("_zoomOut");
16706 },
16707 "keyup"
16708 );
16709 }
16710 }
16711 }
16712}
16713
16714/**
16715 * Handler for interactions
16716 */
16717class InteractionHandler {
16718 /**
16719 * @param {object} body
16720 * @param {Canvas} canvas
16721 * @param {SelectionHandler} selectionHandler
16722 */
16723 constructor(body, canvas, selectionHandler) {
16724 this.body = body;
16725 this.canvas = canvas;
16726 this.selectionHandler = selectionHandler;
16727 this.navigationHandler = new NavigationHandler(body, canvas);
16728
16729 // bind the events from hammer to functions in this object
16730 this.body.eventListeners.onTap = this.onTap.bind(this);
16731 this.body.eventListeners.onTouch = this.onTouch.bind(this);
16732 this.body.eventListeners.onDoubleTap = this.onDoubleTap.bind(this);
16733 this.body.eventListeners.onHold = this.onHold.bind(this);
16734 this.body.eventListeners.onDragStart = this.onDragStart.bind(this);
16735 this.body.eventListeners.onDrag = this.onDrag.bind(this);
16736 this.body.eventListeners.onDragEnd = this.onDragEnd.bind(this);
16737 this.body.eventListeners.onMouseWheel = this.onMouseWheel.bind(this);
16738 this.body.eventListeners.onPinch = this.onPinch.bind(this);
16739 this.body.eventListeners.onMouseMove = this.onMouseMove.bind(this);
16740 this.body.eventListeners.onRelease = this.onRelease.bind(this);
16741 this.body.eventListeners.onContext = this.onContext.bind(this);
16742
16743 this.touchTime = 0;
16744 this.drag = {};
16745 this.pinch = {};
16746 this.popup = undefined;
16747 this.popupObj = undefined;
16748 this.popupTimer = undefined;
16749
16750 this.body.functions.getPointer = this.getPointer.bind(this);
16751
16752 this.options = {};
16753 this.defaultOptions = {
16754 dragNodes: true,
16755 dragView: true,
16756 hover: false,
16757 keyboard: {
16758 enabled: false,
16759 speed: { x: 10, y: 10, zoom: 0.02 },
16760 bindToWindow: true,
16761 autoFocus: true,
16762 },
16763 navigationButtons: false,
16764 tooltipDelay: 300,
16765 zoomView: true,
16766 zoomSpeed: 1,
16767 };
16768 Object.assign(this.options, this.defaultOptions);
16769
16770 this.bindEventListeners();
16771 }
16772
16773 /**
16774 * Binds event listeners
16775 */
16776 bindEventListeners() {
16777 this.body.emitter.on("destroy", () => {
16778 clearTimeout(this.popupTimer);
16779 delete this.body.functions.getPointer;
16780 });
16781 }
16782
16783 /**
16784 *
16785 * @param {object} options
16786 */
16787 setOptions(options) {
16788 if (options !== undefined) {
16789 // extend all but the values in fields
16790 const fields = [
16791 "hideEdgesOnDrag",
16792 "hideEdgesOnZoom",
16793 "hideNodesOnDrag",
16794 "keyboard",
16795 "multiselect",
16796 "selectable",
16797 "selectConnectedEdges",
16798 ];
16799 selectiveNotDeepExtend(fields, this.options, options);
16800
16801 // merge the keyboard options in.
16802 mergeOptions(this.options, options, "keyboard");
16803
16804 if (options.tooltip) {
16805 Object.assign(this.options.tooltip, options.tooltip);
16806 if (options.tooltip.color) {
16807 this.options.tooltip.color = parseColor(options.tooltip.color);
16808 }
16809 }
16810 }
16811
16812 this.navigationHandler.setOptions(this.options);
16813 }
16814
16815 /**
16816 * Get the pointer location from a touch location
16817 *
16818 * @param {{x: number, y: number}} touch
16819 * @returns {{x: number, y: number}} pointer
16820 * @private
16821 */
16822 getPointer(touch) {
16823 return {
16824 x: touch.x - getAbsoluteLeft(this.canvas.frame.canvas),
16825 y: touch.y - getAbsoluteTop(this.canvas.frame.canvas),
16826 };
16827 }
16828
16829 /**
16830 * On start of a touch gesture, store the pointer
16831 *
16832 * @param {Event} event The event
16833 * @private
16834 */
16835 onTouch(event) {
16836 if (new Date().valueOf() - this.touchTime > 50) {
16837 this.drag.pointer = this.getPointer(event.center);
16838 this.drag.pinched = false;
16839 this.pinch.scale = this.body.view.scale;
16840 // to avoid double fireing of this event because we have two hammer instances. (on canvas and on frame)
16841 this.touchTime = new Date().valueOf();
16842 }
16843 }
16844
16845 /**
16846 * handle tap/click event: select/unselect a node
16847 *
16848 * @param {Event} event
16849 * @private
16850 */
16851 onTap(event) {
16852 const pointer = this.getPointer(event.center);
16853 const multiselect =
16854 this.selectionHandler.options.multiselect &&
16855 (event.changedPointers[0].ctrlKey || event.changedPointers[0].metaKey);
16856
16857 this.checkSelectionChanges(pointer, multiselect);
16858
16859 this.selectionHandler.commitAndEmit(pointer, event);
16860 this.selectionHandler.generateClickEvent("click", event, pointer);
16861 }
16862
16863 /**
16864 * handle doubletap event
16865 *
16866 * @param {Event} event
16867 * @private
16868 */
16869 onDoubleTap(event) {
16870 const pointer = this.getPointer(event.center);
16871 this.selectionHandler.generateClickEvent("doubleClick", event, pointer);
16872 }
16873
16874 /**
16875 * handle long tap event: multi select nodes
16876 *
16877 * @param {Event} event
16878 * @private
16879 */
16880 onHold(event) {
16881 const pointer = this.getPointer(event.center);
16882 const multiselect = this.selectionHandler.options.multiselect;
16883
16884 this.checkSelectionChanges(pointer, multiselect);
16885
16886 this.selectionHandler.commitAndEmit(pointer, event);
16887 this.selectionHandler.generateClickEvent("click", event, pointer);
16888 this.selectionHandler.generateClickEvent("hold", event, pointer);
16889 }
16890
16891 /**
16892 * handle the release of the screen
16893 *
16894 * @param {Event} event
16895 * @private
16896 */
16897 onRelease(event) {
16898 if (new Date().valueOf() - this.touchTime > 10) {
16899 const pointer = this.getPointer(event.center);
16900 this.selectionHandler.generateClickEvent("release", event, pointer);
16901 // to avoid double fireing of this event because we have two hammer instances. (on canvas and on frame)
16902 this.touchTime = new Date().valueOf();
16903 }
16904 }
16905
16906 /**
16907 *
16908 * @param {Event} event
16909 */
16910 onContext(event) {
16911 const pointer = this.getPointer({ x: event.clientX, y: event.clientY });
16912 this.selectionHandler.generateClickEvent("oncontext", event, pointer);
16913 }
16914
16915 /**
16916 * Select and deselect nodes depending current selection change.
16917 *
16918 * @param {{x: number, y: number}} pointer
16919 * @param {boolean} [add=false]
16920 */
16921 checkSelectionChanges(pointer, add = false) {
16922 if (add === true) {
16923 this.selectionHandler.selectAdditionalOnPoint(pointer);
16924 } else {
16925 this.selectionHandler.selectOnPoint(pointer);
16926 }
16927 }
16928
16929 /**
16930 * Remove all node and edge id's from the first set that are present in the second one.
16931 *
16932 * @param {{nodes: Array.<Node>, edges: Array.<vis.Edge>}} firstSet
16933 * @param {{nodes: Array.<Node>, edges: Array.<vis.Edge>}} secondSet
16934 * @returns {{nodes: Array.<Node>, edges: Array.<vis.Edge>}}
16935 * @private
16936 */
16937 _determineDifference(firstSet, secondSet) {
16938 const arrayDiff = function (firstArr, secondArr) {
16939 const result = [];
16940
16941 for (let i = 0; i < firstArr.length; i++) {
16942 const value = firstArr[i];
16943 if (secondArr.indexOf(value) === -1) {
16944 result.push(value);
16945 }
16946 }
16947
16948 return result;
16949 };
16950
16951 return {
16952 nodes: arrayDiff(firstSet.nodes, secondSet.nodes),
16953 edges: arrayDiff(firstSet.edges, secondSet.edges),
16954 };
16955 }
16956
16957 /**
16958 * This function is called by onDragStart.
16959 * It is separated out because we can then overload it for the datamanipulation system.
16960 *
16961 * @param {Event} event
16962 * @private
16963 */
16964 onDragStart(event) {
16965 // if already dragging, do not start
16966 // this can happen on touch screens with multiple fingers
16967 if (this.drag.dragging) {
16968 return;
16969 }
16970
16971 //in case the touch event was triggered on an external div, do the initial touch now.
16972 if (this.drag.pointer === undefined) {
16973 this.onTouch(event);
16974 }
16975
16976 // note: drag.pointer is set in onTouch to get the initial touch location
16977 const node = this.selectionHandler.getNodeAt(this.drag.pointer);
16978
16979 this.drag.dragging = true;
16980 this.drag.selection = [];
16981 this.drag.translation = Object.assign({}, this.body.view.translation); // copy the object
16982 this.drag.nodeId = undefined;
16983
16984 if (event.srcEvent.shiftKey) {
16985 this.body.selectionBox.show = true;
16986 const pointer = this.getPointer(event.center);
16987
16988 this.body.selectionBox.position.start = {
16989 x: this.canvas._XconvertDOMtoCanvas(pointer.x),
16990 y: this.canvas._YconvertDOMtoCanvas(pointer.y),
16991 };
16992 this.body.selectionBox.position.end = {
16993 x: this.canvas._XconvertDOMtoCanvas(pointer.x),
16994 y: this.canvas._YconvertDOMtoCanvas(pointer.y),
16995 };
16996 }
16997
16998 if (node !== undefined && this.options.dragNodes === true) {
16999 this.drag.nodeId = node.id;
17000 // select the clicked node if not yet selected
17001 if (node.isSelected() === false) {
17002 this.selectionHandler.unselectAll();
17003 this.selectionHandler.selectObject(node);
17004 }
17005
17006 // after select to contain the node
17007 this.selectionHandler.generateClickEvent(
17008 "dragStart",
17009 event,
17010 this.drag.pointer
17011 );
17012
17013 // create an array with the selected nodes and their original location and status
17014 for (const node of this.selectionHandler.getSelectedNodes()) {
17015 const s = {
17016 id: node.id,
17017 node: node,
17018
17019 // store original x, y, xFixed and yFixed, make the node temporarily Fixed
17020 x: node.x,
17021 y: node.y,
17022 xFixed: node.options.fixed.x,
17023 yFixed: node.options.fixed.y,
17024 };
17025
17026 node.options.fixed.x = true;
17027 node.options.fixed.y = true;
17028
17029 this.drag.selection.push(s);
17030 }
17031 } else {
17032 // fallback if no node is selected and thus the view is dragged.
17033 this.selectionHandler.generateClickEvent(
17034 "dragStart",
17035 event,
17036 this.drag.pointer,
17037 undefined,
17038 true
17039 );
17040 }
17041 }
17042
17043 /**
17044 * handle drag event
17045 *
17046 * @param {Event} event
17047 * @private
17048 */
17049 onDrag(event) {
17050 if (this.drag.pinched === true) {
17051 return;
17052 }
17053
17054 // remove the focus on node if it is focussed on by the focusOnNode
17055 this.body.emitter.emit("unlockNode");
17056
17057 const pointer = this.getPointer(event.center);
17058
17059 const selection = this.drag.selection;
17060 if (selection && selection.length && this.options.dragNodes === true) {
17061 this.selectionHandler.generateClickEvent("dragging", event, pointer);
17062
17063 // calculate delta's and new location
17064 const deltaX = pointer.x - this.drag.pointer.x;
17065 const deltaY = pointer.y - this.drag.pointer.y;
17066
17067 // update position of all selected nodes
17068 selection.forEach((selection) => {
17069 const node = selection.node;
17070 // only move the node if it was not fixed initially
17071 if (selection.xFixed === false) {
17072 node.x = this.canvas._XconvertDOMtoCanvas(
17073 this.canvas._XconvertCanvasToDOM(selection.x) + deltaX
17074 );
17075 }
17076 // only move the node if it was not fixed initially
17077 if (selection.yFixed === false) {
17078 node.y = this.canvas._YconvertDOMtoCanvas(
17079 this.canvas._YconvertCanvasToDOM(selection.y) + deltaY
17080 );
17081 }
17082 });
17083
17084 // start the simulation of the physics
17085 this.body.emitter.emit("startSimulation");
17086 } else {
17087 // create selection box
17088 if (event.srcEvent.shiftKey) {
17089 this.selectionHandler.generateClickEvent(
17090 "dragging",
17091 event,
17092 pointer,
17093 undefined,
17094 true
17095 );
17096
17097 // if the drag was not started properly because the click started outside the network div, start it now.
17098 if (this.drag.pointer === undefined) {
17099 this.onDragStart(event);
17100 return;
17101 }
17102
17103 this.body.selectionBox.position.end = {
17104 x: this.canvas._XconvertDOMtoCanvas(pointer.x),
17105 y: this.canvas._YconvertDOMtoCanvas(pointer.y),
17106 };
17107 this.body.emitter.emit("_requestRedraw");
17108 }
17109
17110 // move the network
17111 if (this.options.dragView === true && !event.srcEvent.shiftKey) {
17112 this.selectionHandler.generateClickEvent(
17113 "dragging",
17114 event,
17115 pointer,
17116 undefined,
17117 true
17118 );
17119
17120 // if the drag was not started properly because the click started outside the network div, start it now.
17121 if (this.drag.pointer === undefined) {
17122 this.onDragStart(event);
17123 return;
17124 }
17125
17126 const diffX = pointer.x - this.drag.pointer.x;
17127 const diffY = pointer.y - this.drag.pointer.y;
17128
17129 this.body.view.translation = {
17130 x: this.drag.translation.x + diffX,
17131 y: this.drag.translation.y + diffY,
17132 };
17133 this.body.emitter.emit("_requestRedraw");
17134 }
17135 }
17136 }
17137
17138 /**
17139 * handle drag start event
17140 *
17141 * @param {Event} event
17142 * @private
17143 */
17144 onDragEnd(event) {
17145 this.drag.dragging = false;
17146
17147 if (this.body.selectionBox.show) {
17148 this.body.selectionBox.show = false;
17149 const selectionBoxPosition = this.body.selectionBox.position;
17150 const selectionBoxPositionMinMax = {
17151 minX: Math.min(
17152 selectionBoxPosition.start.x,
17153 selectionBoxPosition.end.x
17154 ),
17155 minY: Math.min(
17156 selectionBoxPosition.start.y,
17157 selectionBoxPosition.end.y
17158 ),
17159 maxX: Math.max(
17160 selectionBoxPosition.start.x,
17161 selectionBoxPosition.end.x
17162 ),
17163 maxY: Math.max(
17164 selectionBoxPosition.start.y,
17165 selectionBoxPosition.end.y
17166 ),
17167 };
17168
17169 const toBeSelectedNodes = this.body.nodeIndices.filter((nodeId) => {
17170 const node = this.body.nodes[nodeId];
17171 return (
17172 node.x >= selectionBoxPositionMinMax.minX &&
17173 node.x <= selectionBoxPositionMinMax.maxX &&
17174 node.y >= selectionBoxPositionMinMax.minY &&
17175 node.y <= selectionBoxPositionMinMax.maxY
17176 );
17177 });
17178
17179 toBeSelectedNodes.forEach((nodeId) =>
17180 this.selectionHandler.selectObject(this.body.nodes[nodeId])
17181 );
17182
17183 const pointer = this.getPointer(event.center);
17184 this.selectionHandler.commitAndEmit(pointer, event);
17185 this.selectionHandler.generateClickEvent(
17186 "dragEnd",
17187 event,
17188 this.getPointer(event.center),
17189 undefined,
17190 true
17191 );
17192 this.body.emitter.emit("_requestRedraw");
17193 } else {
17194 const selection = this.drag.selection;
17195 if (selection && selection.length) {
17196 selection.forEach(function (s) {
17197 // restore original xFixed and yFixed
17198 s.node.options.fixed.x = s.xFixed;
17199 s.node.options.fixed.y = s.yFixed;
17200 });
17201 this.selectionHandler.generateClickEvent(
17202 "dragEnd",
17203 event,
17204 this.getPointer(event.center)
17205 );
17206 this.body.emitter.emit("startSimulation");
17207 } else {
17208 this.selectionHandler.generateClickEvent(
17209 "dragEnd",
17210 event,
17211 this.getPointer(event.center),
17212 undefined,
17213 true
17214 );
17215 this.body.emitter.emit("_requestRedraw");
17216 }
17217 }
17218 }
17219
17220 /**
17221 * Handle pinch event
17222 *
17223 * @param {Event} event The event
17224 * @private
17225 */
17226 onPinch(event) {
17227 const pointer = this.getPointer(event.center);
17228
17229 this.drag.pinched = true;
17230 if (this.pinch["scale"] === undefined) {
17231 this.pinch.scale = 1;
17232 }
17233
17234 // TODO: enabled moving while pinching?
17235 const scale = this.pinch.scale * event.scale;
17236 this.zoom(scale, pointer);
17237 }
17238
17239 /**
17240 * Zoom the network in or out
17241 *
17242 * @param {number} scale a number around 1, and between 0.01 and 10
17243 * @param {{x: number, y: number}} pointer Position on screen
17244 * @private
17245 */
17246 zoom(scale, pointer) {
17247 if (this.options.zoomView === true) {
17248 const scaleOld = this.body.view.scale;
17249 if (scale < 0.00001) {
17250 scale = 0.00001;
17251 }
17252 if (scale > 10) {
17253 scale = 10;
17254 }
17255
17256 let preScaleDragPointer = undefined;
17257 if (this.drag !== undefined) {
17258 if (this.drag.dragging === true) {
17259 preScaleDragPointer = this.canvas.DOMtoCanvas(this.drag.pointer);
17260 }
17261 }
17262 // + this.canvas.frame.canvas.clientHeight / 2
17263 const translation = this.body.view.translation;
17264
17265 const scaleFrac = scale / scaleOld;
17266 const tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac;
17267 const ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac;
17268
17269 this.body.view.scale = scale;
17270 this.body.view.translation = { x: tx, y: ty };
17271
17272 if (preScaleDragPointer != undefined) {
17273 const postScaleDragPointer =
17274 this.canvas.canvasToDOM(preScaleDragPointer);
17275 this.drag.pointer.x = postScaleDragPointer.x;
17276 this.drag.pointer.y = postScaleDragPointer.y;
17277 }
17278
17279 this.body.emitter.emit("_requestRedraw");
17280
17281 if (scaleOld < scale) {
17282 this.body.emitter.emit("zoom", {
17283 direction: "+",
17284 scale: this.body.view.scale,
17285 pointer: pointer,
17286 });
17287 } else {
17288 this.body.emitter.emit("zoom", {
17289 direction: "-",
17290 scale: this.body.view.scale,
17291 pointer: pointer,
17292 });
17293 }
17294 }
17295 }
17296
17297 /**
17298 * Event handler for mouse wheel event, used to zoom the timeline
17299 * See http://adomas.org/javascript-mouse-wheel/
17300 * https://github.com/EightMedia/hammer.js/issues/256
17301 *
17302 * @param {MouseEvent} event
17303 * @private
17304 */
17305 onMouseWheel(event) {
17306 if (this.options.zoomView === true) {
17307 // If delta is nonzero, handle it.
17308 // Basically, delta is now positive if wheel was scrolled up,
17309 // and negative, if wheel was scrolled down.
17310 if (event.deltaY !== 0) {
17311 // calculate the new scale
17312 let scale = this.body.view.scale;
17313 scale *=
17314 1 + (event.deltaY < 0 ? 1 : -1) * (this.options.zoomSpeed * 0.1);
17315
17316 // calculate the pointer location
17317 const pointer = this.getPointer({ x: event.clientX, y: event.clientY });
17318
17319 // apply the new scale
17320 this.zoom(scale, pointer);
17321 }
17322
17323 // Prevent default actions caused by mouse wheel.
17324 event.preventDefault();
17325 }
17326 }
17327
17328 /**
17329 * Mouse move handler for checking whether the title moves over a node with a title.
17330 *
17331 * @param {Event} event
17332 * @private
17333 */
17334 onMouseMove(event) {
17335 const pointer = this.getPointer({ x: event.clientX, y: event.clientY });
17336 let popupVisible = false;
17337
17338 // check if the previously selected node is still selected
17339 if (this.popup !== undefined) {
17340 if (this.popup.hidden === false) {
17341 this._checkHidePopup(pointer);
17342 }
17343
17344 // if the popup was not hidden above
17345 if (this.popup.hidden === false) {
17346 popupVisible = true;
17347 this.popup.setPosition(pointer.x + 3, pointer.y - 5);
17348 this.popup.show();
17349 }
17350 }
17351
17352 // if we bind the keyboard to the div, we have to highlight it to use it. This highlights it on mouse over.
17353 if (
17354 this.options.keyboard.autoFocus &&
17355 this.options.keyboard.bindToWindow === false &&
17356 this.options.keyboard.enabled === true
17357 ) {
17358 this.canvas.frame.focus();
17359 }
17360
17361 // start a timeout that will check if the mouse is positioned above an element
17362 if (popupVisible === false) {
17363 if (this.popupTimer !== undefined) {
17364 clearInterval(this.popupTimer); // stop any running calculationTimer
17365 this.popupTimer = undefined;
17366 }
17367 if (!this.drag.dragging) {
17368 this.popupTimer = setTimeout(
17369 () => this._checkShowPopup(pointer),
17370 this.options.tooltipDelay
17371 );
17372 }
17373 }
17374
17375 // adding hover highlights
17376 if (this.options.hover === true) {
17377 this.selectionHandler.hoverObject(event, pointer);
17378 }
17379 }
17380
17381 /**
17382 * Check if there is an element on the given position in the network
17383 * (a node or edge). If so, and if this element has a title,
17384 * show a popup window with its title.
17385 *
17386 * @param {{x:number, y:number}} pointer
17387 * @private
17388 */
17389 _checkShowPopup(pointer) {
17390 const x = this.canvas._XconvertDOMtoCanvas(pointer.x);
17391 const y = this.canvas._YconvertDOMtoCanvas(pointer.y);
17392 const pointerObj = {
17393 left: x,
17394 top: y,
17395 right: x,
17396 bottom: y,
17397 };
17398
17399 const previousPopupObjId =
17400 this.popupObj === undefined ? undefined : this.popupObj.id;
17401 let nodeUnderCursor = false;
17402 let popupType = "node";
17403
17404 // check if a node is under the cursor.
17405 if (this.popupObj === undefined) {
17406 // search the nodes for overlap, select the top one in case of multiple nodes
17407 const nodeIndices = this.body.nodeIndices;
17408 const nodes = this.body.nodes;
17409 let node;
17410 const overlappingNodes = [];
17411 for (let i = 0; i < nodeIndices.length; i++) {
17412 node = nodes[nodeIndices[i]];
17413 if (node.isOverlappingWith(pointerObj) === true) {
17414 nodeUnderCursor = true;
17415 if (node.getTitle() !== undefined) {
17416 overlappingNodes.push(nodeIndices[i]);
17417 }
17418 }
17419 }
17420
17421 if (overlappingNodes.length > 0) {
17422 // if there are overlapping nodes, select the last one, this is the one which is drawn on top of the others
17423 this.popupObj = nodes[overlappingNodes[overlappingNodes.length - 1]];
17424 // if you hover over a node, the title of the edge is not supposed to be shown.
17425 nodeUnderCursor = true;
17426 }
17427 }
17428
17429 if (this.popupObj === undefined && nodeUnderCursor === false) {
17430 // search the edges for overlap
17431 const edgeIndices = this.body.edgeIndices;
17432 const edges = this.body.edges;
17433 let edge;
17434 const overlappingEdges = [];
17435 for (let i = 0; i < edgeIndices.length; i++) {
17436 edge = edges[edgeIndices[i]];
17437 if (edge.isOverlappingWith(pointerObj) === true) {
17438 if (edge.connected === true && edge.getTitle() !== undefined) {
17439 overlappingEdges.push(edgeIndices[i]);
17440 }
17441 }
17442 }
17443
17444 if (overlappingEdges.length > 0) {
17445 this.popupObj = edges[overlappingEdges[overlappingEdges.length - 1]];
17446 popupType = "edge";
17447 }
17448 }
17449
17450 if (this.popupObj !== undefined) {
17451 // show popup message window
17452 if (this.popupObj.id !== previousPopupObjId) {
17453 if (this.popup === undefined) {
17454 this.popup = new Popup(this.canvas.frame);
17455 }
17456
17457 this.popup.popupTargetType = popupType;
17458 this.popup.popupTargetId = this.popupObj.id;
17459
17460 // adjust a small offset such that the mouse cursor is located in the
17461 // bottom left location of the popup, and you can easily move over the
17462 // popup area
17463 this.popup.setPosition(pointer.x + 3, pointer.y - 5);
17464 this.popup.setText(this.popupObj.getTitle());
17465 this.popup.show();
17466 this.body.emitter.emit("showPopup", this.popupObj.id);
17467 }
17468 } else {
17469 if (this.popup !== undefined) {
17470 this.popup.hide();
17471 this.body.emitter.emit("hidePopup");
17472 }
17473 }
17474 }
17475
17476 /**
17477 * Check if the popup must be hidden, which is the case when the mouse is no
17478 * longer hovering on the object
17479 *
17480 * @param {{x:number, y:number}} pointer
17481 * @private
17482 */
17483 _checkHidePopup(pointer) {
17484 const pointerObj = this.selectionHandler._pointerToPositionObject(pointer);
17485
17486 let stillOnObj = false;
17487 if (this.popup.popupTargetType === "node") {
17488 if (this.body.nodes[this.popup.popupTargetId] !== undefined) {
17489 stillOnObj =
17490 this.body.nodes[this.popup.popupTargetId].isOverlappingWith(
17491 pointerObj
17492 );
17493
17494 // if the mouse is still one the node, we have to check if it is not also on one that is drawn on top of it.
17495 // we initially only check stillOnObj because this is much faster.
17496 if (stillOnObj === true) {
17497 const overNode = this.selectionHandler.getNodeAt(pointer);
17498 stillOnObj =
17499 overNode === undefined
17500 ? false
17501 : overNode.id === this.popup.popupTargetId;
17502 }
17503 }
17504 } else {
17505 if (this.selectionHandler.getNodeAt(pointer) === undefined) {
17506 if (this.body.edges[this.popup.popupTargetId] !== undefined) {
17507 stillOnObj =
17508 this.body.edges[this.popup.popupTargetId].isOverlappingWith(
17509 pointerObj
17510 );
17511 }
17512 }
17513 }
17514
17515 if (stillOnObj === false) {
17516 this.popupObj = undefined;
17517 this.popup.hide();
17518 this.body.emitter.emit("hidePopup");
17519 }
17520 }
17521}
17522
17523var _SingleTypeSelectionAccumulator_previousSelection, _SingleTypeSelectionAccumulator_selection, _SelectionAccumulator_nodes, _SelectionAccumulator_edges, _SelectionAccumulator_commitHandler;
17524/**
17525 * @param prev
17526 * @param next
17527 */
17528function diffSets(prev, next) {
17529 const diff = new Set();
17530 for (const item of next) {
17531 if (!prev.has(item)) {
17532 diff.add(item);
17533 }
17534 }
17535 return diff;
17536}
17537class SingleTypeSelectionAccumulator {
17538 constructor() {
17539 _SingleTypeSelectionAccumulator_previousSelection.set(this, new Set());
17540 _SingleTypeSelectionAccumulator_selection.set(this, new Set());
17541 }
17542 get size() {
17543 return __classPrivateFieldGet(this, _SingleTypeSelectionAccumulator_selection, "f").size;
17544 }
17545 add(...items) {
17546 for (const item of items) {
17547 __classPrivateFieldGet(this, _SingleTypeSelectionAccumulator_selection, "f").add(item);
17548 }
17549 }
17550 delete(...items) {
17551 for (const item of items) {
17552 __classPrivateFieldGet(this, _SingleTypeSelectionAccumulator_selection, "f").delete(item);
17553 }
17554 }
17555 clear() {
17556 __classPrivateFieldGet(this, _SingleTypeSelectionAccumulator_selection, "f").clear();
17557 }
17558 getSelection() {
17559 return [...__classPrivateFieldGet(this, _SingleTypeSelectionAccumulator_selection, "f")];
17560 }
17561 getChanges() {
17562 return {
17563 added: [...diffSets(__classPrivateFieldGet(this, _SingleTypeSelectionAccumulator_previousSelection, "f"), __classPrivateFieldGet(this, _SingleTypeSelectionAccumulator_selection, "f"))],
17564 deleted: [...diffSets(__classPrivateFieldGet(this, _SingleTypeSelectionAccumulator_selection, "f"), __classPrivateFieldGet(this, _SingleTypeSelectionAccumulator_previousSelection, "f"))],
17565 previous: [...new Set(__classPrivateFieldGet(this, _SingleTypeSelectionAccumulator_previousSelection, "f"))],
17566 current: [...new Set(__classPrivateFieldGet(this, _SingleTypeSelectionAccumulator_selection, "f"))],
17567 };
17568 }
17569 commit() {
17570 const changes = this.getChanges();
17571 __classPrivateFieldSet(this, _SingleTypeSelectionAccumulator_previousSelection, __classPrivateFieldGet(this, _SingleTypeSelectionAccumulator_selection, "f"), "f");
17572 __classPrivateFieldSet(this, _SingleTypeSelectionAccumulator_selection, new Set(__classPrivateFieldGet(this, _SingleTypeSelectionAccumulator_previousSelection, "f")), "f");
17573 for (const item of changes.added) {
17574 item.select();
17575 }
17576 for (const item of changes.deleted) {
17577 item.unselect();
17578 }
17579 return changes;
17580 }
17581}
17582_SingleTypeSelectionAccumulator_previousSelection = new WeakMap(), _SingleTypeSelectionAccumulator_selection = new WeakMap();
17583class SelectionAccumulator {
17584 constructor(commitHandler = () => { }) {
17585 _SelectionAccumulator_nodes.set(this, new SingleTypeSelectionAccumulator());
17586 _SelectionAccumulator_edges.set(this, new SingleTypeSelectionAccumulator());
17587 _SelectionAccumulator_commitHandler.set(this, void 0);
17588 __classPrivateFieldSet(this, _SelectionAccumulator_commitHandler, commitHandler, "f");
17589 }
17590 get sizeNodes() {
17591 return __classPrivateFieldGet(this, _SelectionAccumulator_nodes, "f").size;
17592 }
17593 get sizeEdges() {
17594 return __classPrivateFieldGet(this, _SelectionAccumulator_edges, "f").size;
17595 }
17596 getNodes() {
17597 return __classPrivateFieldGet(this, _SelectionAccumulator_nodes, "f").getSelection();
17598 }
17599 getEdges() {
17600 return __classPrivateFieldGet(this, _SelectionAccumulator_edges, "f").getSelection();
17601 }
17602 addNodes(...nodes) {
17603 __classPrivateFieldGet(this, _SelectionAccumulator_nodes, "f").add(...nodes);
17604 }
17605 addEdges(...edges) {
17606 __classPrivateFieldGet(this, _SelectionAccumulator_edges, "f").add(...edges);
17607 }
17608 deleteNodes(node) {
17609 __classPrivateFieldGet(this, _SelectionAccumulator_nodes, "f").delete(node);
17610 }
17611 deleteEdges(edge) {
17612 __classPrivateFieldGet(this, _SelectionAccumulator_edges, "f").delete(edge);
17613 }
17614 clear() {
17615 __classPrivateFieldGet(this, _SelectionAccumulator_nodes, "f").clear();
17616 __classPrivateFieldGet(this, _SelectionAccumulator_edges, "f").clear();
17617 }
17618 commit(...rest) {
17619 const summary = {
17620 nodes: __classPrivateFieldGet(this, _SelectionAccumulator_nodes, "f").commit(),
17621 edges: __classPrivateFieldGet(this, _SelectionAccumulator_edges, "f").commit(),
17622 };
17623 __classPrivateFieldGet(this, _SelectionAccumulator_commitHandler, "f").call(this, summary, ...rest);
17624 return summary;
17625 }
17626}
17627_SelectionAccumulator_nodes = new WeakMap(), _SelectionAccumulator_edges = new WeakMap(), _SelectionAccumulator_commitHandler = new WeakMap();
17628
17629/**
17630 * The handler for selections
17631 */
17632class SelectionHandler {
17633 /**
17634 * @param {object} body
17635 * @param {Canvas} canvas
17636 */
17637 constructor(body, canvas) {
17638 this.body = body;
17639 this.canvas = canvas;
17640 // TODO: Consider firing an event on any change to the selection, not
17641 // only those caused by clicks and taps. It would be easy to implement
17642 // now and (at least to me) it seems like something that could be
17643 // quite useful.
17644 this._selectionAccumulator = new SelectionAccumulator();
17645 this.hoverObj = { nodes: {}, edges: {} };
17646
17647 this.options = {};
17648 this.defaultOptions = {
17649 multiselect: false,
17650 selectable: true,
17651 selectConnectedEdges: true,
17652 hoverConnectedEdges: true,
17653 };
17654 Object.assign(this.options, this.defaultOptions);
17655
17656 this.body.emitter.on("_dataChanged", () => {
17657 this.updateSelection();
17658 });
17659 }
17660
17661 /**
17662 *
17663 * @param {object} [options]
17664 */
17665 setOptions(options) {
17666 if (options !== undefined) {
17667 const fields = [
17668 "multiselect",
17669 "hoverConnectedEdges",
17670 "selectable",
17671 "selectConnectedEdges",
17672 ];
17673 selectiveDeepExtend(fields, this.options, options);
17674 }
17675 }
17676
17677 /**
17678 * handles the selection part of the tap;
17679 *
17680 * @param {{x: number, y: number}} pointer
17681 * @returns {boolean}
17682 */
17683 selectOnPoint(pointer) {
17684 let selected = false;
17685 if (this.options.selectable === true) {
17686 const obj = this.getNodeAt(pointer) || this.getEdgeAt(pointer);
17687
17688 // unselect after getting the objects in order to restore width and height.
17689 this.unselectAll();
17690
17691 if (obj !== undefined) {
17692 selected = this.selectObject(obj);
17693 }
17694 this.body.emitter.emit("_requestRedraw");
17695 }
17696 return selected;
17697 }
17698
17699 /**
17700 *
17701 * @param {{x: number, y: number}} pointer
17702 * @returns {boolean}
17703 */
17704 selectAdditionalOnPoint(pointer) {
17705 let selectionChanged = false;
17706 if (this.options.selectable === true) {
17707 const obj = this.getNodeAt(pointer) || this.getEdgeAt(pointer);
17708
17709 if (obj !== undefined) {
17710 selectionChanged = true;
17711 if (obj.isSelected() === true) {
17712 this.deselectObject(obj);
17713 } else {
17714 this.selectObject(obj);
17715 }
17716
17717 this.body.emitter.emit("_requestRedraw");
17718 }
17719 }
17720 return selectionChanged;
17721 }
17722
17723 /**
17724 * Create an object containing the standard fields for an event.
17725 *
17726 * @param {Event} event
17727 * @param {{x: number, y: number}} pointer Object with the x and y screen coordinates of the mouse
17728 * @returns {{}}
17729 * @private
17730 */
17731 _initBaseEvent(event, pointer) {
17732 const properties = {};
17733
17734 properties["pointer"] = {
17735 DOM: { x: pointer.x, y: pointer.y },
17736 canvas: this.canvas.DOMtoCanvas(pointer),
17737 };
17738 properties["event"] = event;
17739
17740 return properties;
17741 }
17742
17743 /**
17744 * Generate an event which the user can catch.
17745 *
17746 * This adds some extra data to the event with respect to cursor position and
17747 * selected nodes and edges.
17748 *
17749 * @param {string} eventType Name of event to send
17750 * @param {Event} event
17751 * @param {{x: number, y: number}} pointer Object with the x and y screen coordinates of the mouse
17752 * @param {object | undefined} oldSelection If present, selection state before event occured
17753 * @param {boolean|undefined} [emptySelection=false] Indicate if selection data should be passed
17754 */
17755 generateClickEvent(
17756 eventType,
17757 event,
17758 pointer,
17759 oldSelection,
17760 emptySelection = false
17761 ) {
17762 const properties = this._initBaseEvent(event, pointer);
17763
17764 if (emptySelection === true) {
17765 properties.nodes = [];
17766 properties.edges = [];
17767 } else {
17768 const tmp = this.getSelection();
17769 properties.nodes = tmp.nodes;
17770 properties.edges = tmp.edges;
17771 }
17772
17773 if (oldSelection !== undefined) {
17774 properties["previousSelection"] = oldSelection;
17775 }
17776
17777 if (eventType == "click") {
17778 // For the time being, restrict this functionality to
17779 // just the click event.
17780 properties.items = this.getClickedItems(pointer);
17781 }
17782
17783 if (event.controlEdge !== undefined) {
17784 properties.controlEdge = event.controlEdge;
17785 }
17786
17787 this.body.emitter.emit(eventType, properties);
17788 }
17789
17790 /**
17791 *
17792 * @param {object} obj
17793 * @param {boolean} [highlightEdges=this.options.selectConnectedEdges]
17794 * @returns {boolean}
17795 */
17796 selectObject(obj, highlightEdges = this.options.selectConnectedEdges) {
17797 if (obj !== undefined) {
17798 if (obj instanceof Node) {
17799 if (highlightEdges === true) {
17800 this._selectionAccumulator.addEdges(...obj.edges);
17801 }
17802 this._selectionAccumulator.addNodes(obj);
17803 } else {
17804 this._selectionAccumulator.addEdges(obj);
17805 }
17806 return true;
17807 }
17808 return false;
17809 }
17810
17811 /**
17812 *
17813 * @param {object} obj
17814 */
17815 deselectObject(obj) {
17816 if (obj.isSelected() === true) {
17817 obj.selected = false;
17818 this._removeFromSelection(obj);
17819 }
17820 }
17821
17822 /**
17823 * retrieve all nodes overlapping with given object
17824 *
17825 * @param {object} object An object with parameters left, top, right, bottom
17826 * @returns {number[]} An array with id's of the overlapping nodes
17827 * @private
17828 */
17829 _getAllNodesOverlappingWith(object) {
17830 const overlappingNodes = [];
17831 const nodes = this.body.nodes;
17832 for (let i = 0; i < this.body.nodeIndices.length; i++) {
17833 const nodeId = this.body.nodeIndices[i];
17834 if (nodes[nodeId].isOverlappingWith(object)) {
17835 overlappingNodes.push(nodeId);
17836 }
17837 }
17838 return overlappingNodes;
17839 }
17840
17841 /**
17842 * Return a position object in canvasspace from a single point in screenspace
17843 *
17844 * @param {{x: number, y: number}} pointer
17845 * @returns {{left: number, top: number, right: number, bottom: number}}
17846 * @private
17847 */
17848 _pointerToPositionObject(pointer) {
17849 const canvasPos = this.canvas.DOMtoCanvas(pointer);
17850 return {
17851 left: canvasPos.x - 1,
17852 top: canvasPos.y + 1,
17853 right: canvasPos.x + 1,
17854 bottom: canvasPos.y - 1,
17855 };
17856 }
17857
17858 /**
17859 * Get the top node at the passed point (like a click)
17860 *
17861 * @param {{x: number, y: number}} pointer
17862 * @param {boolean} [returnNode=true]
17863 * @returns {Node | undefined} node
17864 */
17865 getNodeAt(pointer, returnNode = true) {
17866 // we first check if this is an navigation controls element
17867 const positionObject = this._pointerToPositionObject(pointer);
17868 const overlappingNodes = this._getAllNodesOverlappingWith(positionObject);
17869 // if there are overlapping nodes, select the last one, this is the
17870 // one which is drawn on top of the others
17871 if (overlappingNodes.length > 0) {
17872 if (returnNode === true) {
17873 return this.body.nodes[overlappingNodes[overlappingNodes.length - 1]];
17874 } else {
17875 return overlappingNodes[overlappingNodes.length - 1];
17876 }
17877 } else {
17878 return undefined;
17879 }
17880 }
17881
17882 /**
17883 * retrieve all edges overlapping with given object, selector is around center
17884 *
17885 * @param {object} object An object with parameters left, top, right, bottom
17886 * @param {number[]} overlappingEdges An array with id's of the overlapping nodes
17887 * @private
17888 */
17889 _getEdgesOverlappingWith(object, overlappingEdges) {
17890 const edges = this.body.edges;
17891 for (let i = 0; i < this.body.edgeIndices.length; i++) {
17892 const edgeId = this.body.edgeIndices[i];
17893 if (edges[edgeId].isOverlappingWith(object)) {
17894 overlappingEdges.push(edgeId);
17895 }
17896 }
17897 }
17898
17899 /**
17900 * retrieve all nodes overlapping with given object
17901 *
17902 * @param {object} object An object with parameters left, top, right, bottom
17903 * @returns {number[]} An array with id's of the overlapping nodes
17904 * @private
17905 */
17906 _getAllEdgesOverlappingWith(object) {
17907 const overlappingEdges = [];
17908 this._getEdgesOverlappingWith(object, overlappingEdges);
17909 return overlappingEdges;
17910 }
17911
17912 /**
17913 * Get the edges nearest to the passed point (like a click)
17914 *
17915 * @param {{x: number, y: number}} pointer
17916 * @param {boolean} [returnEdge=true]
17917 * @returns {Edge | undefined} node
17918 */
17919 getEdgeAt(pointer, returnEdge = true) {
17920 // Iterate over edges, pick closest within 10
17921 const canvasPos = this.canvas.DOMtoCanvas(pointer);
17922 let mindist = 10;
17923 let overlappingEdge = null;
17924 const edges = this.body.edges;
17925 for (let i = 0; i < this.body.edgeIndices.length; i++) {
17926 const edgeId = this.body.edgeIndices[i];
17927 const edge = edges[edgeId];
17928 if (edge.connected) {
17929 const xFrom = edge.from.x;
17930 const yFrom = edge.from.y;
17931 const xTo = edge.to.x;
17932 const yTo = edge.to.y;
17933 const dist = edge.edgeType.getDistanceToEdge(
17934 xFrom,
17935 yFrom,
17936 xTo,
17937 yTo,
17938 canvasPos.x,
17939 canvasPos.y
17940 );
17941 if (dist < mindist) {
17942 overlappingEdge = edgeId;
17943 mindist = dist;
17944 }
17945 }
17946 }
17947 if (overlappingEdge !== null) {
17948 if (returnEdge === true) {
17949 return this.body.edges[overlappingEdge];
17950 } else {
17951 return overlappingEdge;
17952 }
17953 } else {
17954 return undefined;
17955 }
17956 }
17957
17958 /**
17959 * Add object to the selection array.
17960 *
17961 * @param {object} obj
17962 * @private
17963 */
17964 _addToHover(obj) {
17965 if (obj instanceof Node) {
17966 this.hoverObj.nodes[obj.id] = obj;
17967 } else {
17968 this.hoverObj.edges[obj.id] = obj;
17969 }
17970 }
17971
17972 /**
17973 * Remove a single option from selection.
17974 *
17975 * @param {object} obj
17976 * @private
17977 */
17978 _removeFromSelection(obj) {
17979 if (obj instanceof Node) {
17980 this._selectionAccumulator.deleteNodes(obj);
17981 this._selectionAccumulator.deleteEdges(...obj.edges);
17982 } else {
17983 this._selectionAccumulator.deleteEdges(obj);
17984 }
17985 }
17986
17987 /**
17988 * Unselect all nodes and edges.
17989 */
17990 unselectAll() {
17991 this._selectionAccumulator.clear();
17992 }
17993
17994 /**
17995 * return the number of selected nodes
17996 *
17997 * @returns {number}
17998 */
17999 getSelectedNodeCount() {
18000 return this._selectionAccumulator.sizeNodes;
18001 }
18002
18003 /**
18004 * return the number of selected edges
18005 *
18006 * @returns {number}
18007 */
18008 getSelectedEdgeCount() {
18009 return this._selectionAccumulator.sizeEdges;
18010 }
18011
18012 /**
18013 * select the edges connected to the node that is being selected
18014 *
18015 * @param {Node} node
18016 * @private
18017 */
18018 _hoverConnectedEdges(node) {
18019 for (let i = 0; i < node.edges.length; i++) {
18020 const edge = node.edges[i];
18021 edge.hover = true;
18022 this._addToHover(edge);
18023 }
18024 }
18025
18026 /**
18027 * Remove the highlight from a node or edge, in response to mouse movement
18028 *
18029 * @param {Event} event
18030 * @param {{x: number, y: number}} pointer object with the x and y screen coordinates of the mouse
18031 * @param {Node|vis.Edge} object
18032 * @private
18033 */
18034 emitBlurEvent(event, pointer, object) {
18035 const properties = this._initBaseEvent(event, pointer);
18036
18037 if (object.hover === true) {
18038 object.hover = false;
18039 if (object instanceof Node) {
18040 properties.node = object.id;
18041 this.body.emitter.emit("blurNode", properties);
18042 } else {
18043 properties.edge = object.id;
18044 this.body.emitter.emit("blurEdge", properties);
18045 }
18046 }
18047 }
18048
18049 /**
18050 * Create the highlight for a node or edge, in response to mouse movement
18051 *
18052 * @param {Event} event
18053 * @param {{x: number, y: number}} pointer object with the x and y screen coordinates of the mouse
18054 * @param {Node|vis.Edge} object
18055 * @returns {boolean} hoverChanged
18056 * @private
18057 */
18058 emitHoverEvent(event, pointer, object) {
18059 const properties = this._initBaseEvent(event, pointer);
18060 let hoverChanged = false;
18061
18062 if (object.hover === false) {
18063 object.hover = true;
18064 this._addToHover(object);
18065 hoverChanged = true;
18066 if (object instanceof Node) {
18067 properties.node = object.id;
18068 this.body.emitter.emit("hoverNode", properties);
18069 } else {
18070 properties.edge = object.id;
18071 this.body.emitter.emit("hoverEdge", properties);
18072 }
18073 }
18074
18075 return hoverChanged;
18076 }
18077
18078 /**
18079 * Perform actions in response to a mouse movement.
18080 *
18081 * @param {Event} event
18082 * @param {{x: number, y: number}} pointer | object with the x and y screen coordinates of the mouse
18083 */
18084 hoverObject(event, pointer) {
18085 let object = this.getNodeAt(pointer);
18086 if (object === undefined) {
18087 object = this.getEdgeAt(pointer);
18088 }
18089
18090 let hoverChanged = false;
18091 // remove all node hover highlights
18092 for (const nodeId in this.hoverObj.nodes) {
18093 if (Object.prototype.hasOwnProperty.call(this.hoverObj.nodes, nodeId)) {
18094 if (
18095 object === undefined ||
18096 (object instanceof Node && object.id != nodeId) ||
18097 object instanceof Edge
18098 ) {
18099 this.emitBlurEvent(event, pointer, this.hoverObj.nodes[nodeId]);
18100 delete this.hoverObj.nodes[nodeId];
18101 hoverChanged = true;
18102 }
18103 }
18104 }
18105
18106 // removing all edge hover highlights
18107 for (const edgeId in this.hoverObj.edges) {
18108 if (Object.prototype.hasOwnProperty.call(this.hoverObj.edges, edgeId)) {
18109 // if the hover has been changed here it means that the node has been hovered over or off
18110 // we then do not use the emitBlurEvent method here.
18111 if (hoverChanged === true) {
18112 this.hoverObj.edges[edgeId].hover = false;
18113 delete this.hoverObj.edges[edgeId];
18114 }
18115 // if the blur remains the same and the object is undefined (mouse off) or another
18116 // edge has been hovered, or another node has been hovered we blur the edge.
18117 else if (
18118 object === undefined ||
18119 (object instanceof Edge && object.id != edgeId) ||
18120 (object instanceof Node && !object.hover)
18121 ) {
18122 this.emitBlurEvent(event, pointer, this.hoverObj.edges[edgeId]);
18123 delete this.hoverObj.edges[edgeId];
18124 hoverChanged = true;
18125 }
18126 }
18127 }
18128
18129 if (object !== undefined) {
18130 const hoveredEdgesCount = Object.keys(this.hoverObj.edges).length;
18131 const hoveredNodesCount = Object.keys(this.hoverObj.nodes).length;
18132 const newOnlyHoveredEdge =
18133 object instanceof Edge &&
18134 hoveredEdgesCount === 0 &&
18135 hoveredNodesCount === 0;
18136 const newOnlyHoveredNode =
18137 object instanceof Node &&
18138 hoveredEdgesCount === 0 &&
18139 hoveredNodesCount === 0;
18140
18141 if (hoverChanged || newOnlyHoveredEdge || newOnlyHoveredNode) {
18142 hoverChanged = this.emitHoverEvent(event, pointer, object);
18143 }
18144
18145 if (object instanceof Node && this.options.hoverConnectedEdges === true) {
18146 this._hoverConnectedEdges(object);
18147 }
18148 }
18149
18150 if (hoverChanged === true) {
18151 this.body.emitter.emit("_requestRedraw");
18152 }
18153 }
18154
18155 /**
18156 * Commit the selection changes but don't emit any events.
18157 */
18158 commitWithoutEmitting() {
18159 this._selectionAccumulator.commit();
18160 }
18161
18162 /**
18163 * Select and deselect nodes depending current selection change.
18164 *
18165 * For changing nodes, select/deselect events are fired.
18166 *
18167 * NOTE: For a given edge, if one connecting node is deselected and with the
18168 * same click the other node is selected, no events for the edge will fire. It
18169 * was selected and it will remain selected.
18170 *
18171 * @param {{x: number, y: number}} pointer - The x and y coordinates of the
18172 * click, tap, dragend… that triggered this.
18173 * @param {UIEvent} event - The event that triggered this.
18174 */
18175 commitAndEmit(pointer, event) {
18176 let selected = false;
18177
18178 const selectionChanges = this._selectionAccumulator.commit();
18179 const previousSelection = {
18180 nodes: selectionChanges.nodes.previous,
18181 edges: selectionChanges.edges.previous,
18182 };
18183
18184 if (selectionChanges.edges.deleted.length > 0) {
18185 this.generateClickEvent(
18186 "deselectEdge",
18187 event,
18188 pointer,
18189 previousSelection
18190 );
18191 selected = true;
18192 }
18193
18194 if (selectionChanges.nodes.deleted.length > 0) {
18195 this.generateClickEvent(
18196 "deselectNode",
18197 event,
18198 pointer,
18199 previousSelection
18200 );
18201 selected = true;
18202 }
18203
18204 if (selectionChanges.nodes.added.length > 0) {
18205 this.generateClickEvent("selectNode", event, pointer);
18206 selected = true;
18207 }
18208
18209 if (selectionChanges.edges.added.length > 0) {
18210 this.generateClickEvent("selectEdge", event, pointer);
18211 selected = true;
18212 }
18213
18214 // fire the select event if anything has been selected or deselected
18215 if (selected === true) {
18216 // select or unselect
18217 this.generateClickEvent("select", event, pointer);
18218 }
18219 }
18220
18221 /**
18222 * Retrieve the currently selected node and edge ids.
18223 *
18224 * @returns {{nodes: Array.<string>, edges: Array.<string>}} Arrays with the
18225 * ids of the selected nodes and edges.
18226 */
18227 getSelection() {
18228 return {
18229 nodes: this.getSelectedNodeIds(),
18230 edges: this.getSelectedEdgeIds(),
18231 };
18232 }
18233
18234 /**
18235 * Retrieve the currently selected nodes.
18236 *
18237 * @returns {Array} An array with selected nodes.
18238 */
18239 getSelectedNodes() {
18240 return this._selectionAccumulator.getNodes();
18241 }
18242
18243 /**
18244 * Retrieve the currently selected edges.
18245 *
18246 * @returns {Array} An array with selected edges.
18247 */
18248 getSelectedEdges() {
18249 return this._selectionAccumulator.getEdges();
18250 }
18251
18252 /**
18253 * Retrieve the currently selected node ids.
18254 *
18255 * @returns {Array} An array with the ids of the selected nodes.
18256 */
18257 getSelectedNodeIds() {
18258 return this._selectionAccumulator.getNodes().map((node) => node.id);
18259 }
18260
18261 /**
18262 * Retrieve the currently selected edge ids.
18263 *
18264 * @returns {Array} An array with the ids of the selected edges.
18265 */
18266 getSelectedEdgeIds() {
18267 return this._selectionAccumulator.getEdges().map((edge) => edge.id);
18268 }
18269
18270 /**
18271 * Updates the current selection
18272 *
18273 * @param {{nodes: Array.<string>, edges: Array.<string>}} selection
18274 * @param {object} options Options
18275 */
18276 setSelection(selection, options = {}) {
18277 if (!selection || (!selection.nodes && !selection.edges)) {
18278 throw new TypeError(
18279 "Selection must be an object with nodes and/or edges properties"
18280 );
18281 }
18282
18283 // first unselect any selected node, if option is true or undefined
18284 if (options.unselectAll || options.unselectAll === undefined) {
18285 this.unselectAll();
18286 }
18287 if (selection.nodes) {
18288 for (const id of selection.nodes) {
18289 const node = this.body.nodes[id];
18290 if (!node) {
18291 throw new RangeError('Node with id "' + id + '" not found');
18292 }
18293 // don't select edges with it
18294 this.selectObject(node, options.highlightEdges);
18295 }
18296 }
18297
18298 if (selection.edges) {
18299 for (const id of selection.edges) {
18300 const edge = this.body.edges[id];
18301 if (!edge) {
18302 throw new RangeError('Edge with id "' + id + '" not found');
18303 }
18304 this.selectObject(edge);
18305 }
18306 }
18307 this.body.emitter.emit("_requestRedraw");
18308 this._selectionAccumulator.commit();
18309 }
18310
18311 /**
18312 * select zero or more nodes with the option to highlight edges
18313 *
18314 * @param {number[] | string[]} selection An array with the ids of the
18315 * selected nodes.
18316 * @param {boolean} [highlightEdges]
18317 */
18318 selectNodes(selection, highlightEdges = true) {
18319 if (!selection || selection.length === undefined)
18320 throw "Selection must be an array with ids";
18321
18322 this.setSelection({ nodes: selection }, { highlightEdges: highlightEdges });
18323 }
18324
18325 /**
18326 * select zero or more edges
18327 *
18328 * @param {number[] | string[]} selection An array with the ids of the
18329 * selected nodes.
18330 */
18331 selectEdges(selection) {
18332 if (!selection || selection.length === undefined)
18333 throw "Selection must be an array with ids";
18334
18335 this.setSelection({ edges: selection });
18336 }
18337
18338 /**
18339 * Validate the selection: remove ids of nodes which no longer exist
18340 *
18341 * @private
18342 */
18343 updateSelection() {
18344 for (const node in this._selectionAccumulator.getNodes()) {
18345 if (!Object.prototype.hasOwnProperty.call(this.body.nodes, node.id)) {
18346 this._selectionAccumulator.deleteNodes(node);
18347 }
18348 }
18349 for (const edge in this._selectionAccumulator.getEdges()) {
18350 if (!Object.prototype.hasOwnProperty.call(this.body.edges, edge.id)) {
18351 this._selectionAccumulator.deleteEdges(edge);
18352 }
18353 }
18354 }
18355
18356 /**
18357 * Determine all the visual elements clicked which are on the given point.
18358 *
18359 * All elements are returned; this includes nodes, edges and their labels.
18360 * The order returned is from highest to lowest, i.e. element 0 of the return
18361 * value is the topmost item clicked on.
18362 *
18363 * The return value consists of an array of the following possible elements:
18364 *
18365 * - `{nodeId:number}` - node with given id clicked on
18366 * - `{nodeId:number, labelId:0}` - label of node with given id clicked on
18367 * - `{edgeId:number}` - edge with given id clicked on
18368 * - `{edge:number, labelId:0}` - label of edge with given id clicked on
18369 *
18370 * ## NOTES
18371 *
18372 * - Currently, there is only one label associated with a node or an edge,
18373 * but this is expected to change somewhere in the future.
18374 * - Since there is no z-indexing yet, it is not really possible to set the nodes and
18375 * edges in the correct order. For the time being, nodes come first.
18376 *
18377 * @param {point} pointer mouse position in screen coordinates
18378 * @returns {Array.<nodeClickItem|nodeLabelClickItem|edgeClickItem|edgeLabelClickItem>}
18379 * @private
18380 */
18381 getClickedItems(pointer) {
18382 const point = this.canvas.DOMtoCanvas(pointer);
18383 const items = [];
18384
18385 // Note reverse order; we want the topmost clicked items to be first in the array
18386 // Also note that selected nodes are disregarded here; these normally display on top
18387 const nodeIndices = this.body.nodeIndices;
18388 const nodes = this.body.nodes;
18389 for (let i = nodeIndices.length - 1; i >= 0; i--) {
18390 const node = nodes[nodeIndices[i]];
18391 const ret = node.getItemsOnPoint(point);
18392 items.push.apply(items, ret); // Append the return value to the running list.
18393 }
18394
18395 const edgeIndices = this.body.edgeIndices;
18396 const edges = this.body.edges;
18397 for (let i = edgeIndices.length - 1; i >= 0; i--) {
18398 const edge = edges[edgeIndices[i]];
18399 const ret = edge.getItemsOnPoint(point);
18400 items.push.apply(items, ret); // Append the return value to the running list.
18401 }
18402
18403 return items;
18404 }
18405}
18406
18407/**
18408 * Helper classes for LayoutEngine.
18409 *
18410 * Strategy pattern for usage of direction methods for hierarchical layouts.
18411 */
18412
18413/**
18414 * Interface definition for direction strategy classes.
18415 *
18416 * This class describes the interface for the Strategy
18417 * pattern classes used to differentiate horizontal and vertical
18418 * direction of hierarchical results.
18419 *
18420 * For a given direction, one coordinate will be 'fixed', meaning that it is
18421 * determined by level.
18422 * The other coordinate is 'unfixed', meaning that the nodes on a given level
18423 * can still move along that coordinate. So:
18424 *
18425 * - `vertical` layout: `x` unfixed, `y` fixed per level
18426 * - `horizontal` layout: `x` fixed per level, `y` unfixed
18427 *
18428 * The local methods are stubs and should be regarded as abstract.
18429 * Derived classes **must** implement all the methods themselves.
18430 *
18431 * @private
18432 */
18433class DirectionInterface {
18434 /**
18435 * @ignore
18436 */
18437 abstract() {
18438 throw new Error("Can't instantiate abstract class!");
18439 }
18440
18441 /**
18442 * This is a dummy call which is used to suppress the jsdoc errors of type:
18443 *
18444 * "'param' is assigned a value but never used"
18445 *
18446 * @ignore
18447 **/
18448 fake_use() {
18449 // Do nothing special
18450 }
18451
18452 /**
18453 * Type to use to translate dynamic curves to, in the case of hierarchical layout.
18454 * Dynamic curves do not work for these.
18455 *
18456 * The value should be perpendicular to the actual direction of the layout.
18457 *
18458 * @returns {string} Direction, either 'vertical' or 'horizontal'
18459 */
18460 curveType() {
18461 return this.abstract();
18462 }
18463
18464 /**
18465 * Return the value of the coordinate that is not fixed for this direction.
18466 *
18467 * @param {Node} node The node to read
18468 * @returns {number} Value of the unfixed coordinate
18469 */
18470 getPosition(node) {
18471 this.fake_use(node);
18472 return this.abstract();
18473 }
18474
18475 /**
18476 * Set the value of the coordinate that is not fixed for this direction.
18477 *
18478 * @param {Node} node The node to adjust
18479 * @param {number} position
18480 * @param {number} [level] if specified, the hierarchy level that this node should be fixed to
18481 */
18482 setPosition(node, position, level = undefined) {
18483 this.fake_use(node, position, level);
18484 this.abstract();
18485 }
18486
18487 /**
18488 * Get the width of a tree.
18489 *
18490 * A `tree` here is a subset of nodes within the network which are not connected to other nodes,
18491 * only among themselves. In essence, it is a sub-network.
18492 *
18493 * @param {number} index The index number of a tree
18494 * @returns {number} the width of a tree in the view coordinates
18495 */
18496 getTreeSize(index) {
18497 this.fake_use(index);
18498 return this.abstract();
18499 }
18500
18501 /**
18502 * Sort array of nodes on the unfixed coordinates.
18503 *
18504 * **Note:** chrome has non-stable sorting implementation, which
18505 * has a tendency to change the order of the array items,
18506 * even if the custom sort function returns 0.
18507 *
18508 * For this reason, an external sort implementation is used,
18509 * which has the added benefit of being faster than the standard
18510 * platforms implementation. This has been verified on `node.js`,
18511 * `firefox` and `chrome` (all linux).
18512 *
18513 * @param {Array.<Node>} nodeArray array of nodes to sort
18514 */
18515 sort(nodeArray) {
18516 this.fake_use(nodeArray);
18517 this.abstract();
18518 }
18519
18520 /**
18521 * Assign the fixed coordinate of the node to the given level
18522 *
18523 * @param {Node} node The node to adjust
18524 * @param {number} level The level to fix to
18525 */
18526 fix(node, level) {
18527 this.fake_use(node, level);
18528 this.abstract();
18529 }
18530
18531 /**
18532 * Add an offset to the unfixed coordinate of the given node.
18533 *
18534 * @param {NodeId} nodeId Id of the node to adjust
18535 * @param {number} diff Offset to add to the unfixed coordinate
18536 */
18537 shift(nodeId, diff) {
18538 this.fake_use(nodeId, diff);
18539 this.abstract();
18540 }
18541}
18542
18543/**
18544 * Vertical Strategy
18545 *
18546 * Coordinate `y` is fixed on levels, coordinate `x` is unfixed.
18547 *
18548 * @augments DirectionInterface
18549 * @private
18550 */
18551class VerticalStrategy extends DirectionInterface {
18552 /**
18553 * Constructor
18554 *
18555 * @param {object} layout reference to the parent LayoutEngine instance.
18556 */
18557 constructor(layout) {
18558 super();
18559 this.layout = layout;
18560 }
18561
18562 /** @inheritDoc */
18563 curveType() {
18564 return "horizontal";
18565 }
18566
18567 /** @inheritDoc */
18568 getPosition(node) {
18569 return node.x;
18570 }
18571
18572 /** @inheritDoc */
18573 setPosition(node, position, level = undefined) {
18574 if (level !== undefined) {
18575 this.layout.hierarchical.addToOrdering(node, level);
18576 }
18577 node.x = position;
18578 }
18579
18580 /** @inheritDoc */
18581 getTreeSize(index) {
18582 const res = this.layout.hierarchical.getTreeSize(
18583 this.layout.body.nodes,
18584 index
18585 );
18586 return { min: res.min_x, max: res.max_x };
18587 }
18588
18589 /** @inheritDoc */
18590 sort(nodeArray) {
18591 sort(nodeArray, function (a, b) {
18592 return a.x - b.x;
18593 });
18594 }
18595
18596 /** @inheritDoc */
18597 fix(node, level) {
18598 node.y = this.layout.options.hierarchical.levelSeparation * level;
18599 node.options.fixed.y = true;
18600 }
18601
18602 /** @inheritDoc */
18603 shift(nodeId, diff) {
18604 this.layout.body.nodes[nodeId].x += diff;
18605 }
18606}
18607
18608/**
18609 * Horizontal Strategy
18610 *
18611 * Coordinate `x` is fixed on levels, coordinate `y` is unfixed.
18612 *
18613 * @augments DirectionInterface
18614 * @private
18615 */
18616class HorizontalStrategy extends DirectionInterface {
18617 /**
18618 * Constructor
18619 *
18620 * @param {object} layout reference to the parent LayoutEngine instance.
18621 */
18622 constructor(layout) {
18623 super();
18624 this.layout = layout;
18625 }
18626
18627 /** @inheritDoc */
18628 curveType() {
18629 return "vertical";
18630 }
18631
18632 /** @inheritDoc */
18633 getPosition(node) {
18634 return node.y;
18635 }
18636
18637 /** @inheritDoc */
18638 setPosition(node, position, level = undefined) {
18639 if (level !== undefined) {
18640 this.layout.hierarchical.addToOrdering(node, level);
18641 }
18642 node.y = position;
18643 }
18644
18645 /** @inheritDoc */
18646 getTreeSize(index) {
18647 const res = this.layout.hierarchical.getTreeSize(
18648 this.layout.body.nodes,
18649 index
18650 );
18651 return { min: res.min_y, max: res.max_y };
18652 }
18653
18654 /** @inheritDoc */
18655 sort(nodeArray) {
18656 sort(nodeArray, function (a, b) {
18657 return a.y - b.y;
18658 });
18659 }
18660
18661 /** @inheritDoc */
18662 fix(node, level) {
18663 node.x = this.layout.options.hierarchical.levelSeparation * level;
18664 node.options.fixed.x = true;
18665 }
18666
18667 /** @inheritDoc */
18668 shift(nodeId, diff) {
18669 this.layout.body.nodes[nodeId].y += diff;
18670 }
18671}
18672
18673/**
18674 * Try to assign levels to nodes according to their positions in the cyclic “hierarchy”.
18675 *
18676 * @param nodes - Visible nodes of the graph.
18677 * @param levels - If present levels will be added to it, if not a new object will be created.
18678 *
18679 * @returns Populated node levels.
18680 */
18681function fillLevelsByDirectionCyclic(nodes, levels) {
18682 const edges = new Set();
18683 nodes.forEach((node) => {
18684 node.edges.forEach((edge) => {
18685 if (edge.connected) {
18686 edges.add(edge);
18687 }
18688 });
18689 });
18690 edges.forEach((edge) => {
18691 const fromId = edge.from.id;
18692 const toId = edge.to.id;
18693 if (levels[fromId] == null) {
18694 levels[fromId] = 0;
18695 }
18696 if (levels[toId] == null || levels[fromId] >= levels[toId]) {
18697 levels[toId] = levels[fromId] + 1;
18698 }
18699 });
18700 return levels;
18701}
18702/**
18703 * Assign levels to nodes according to their positions in the hierarchy. Leaves will be lined up at the bottom and all other nodes as close to their children as possible.
18704 *
18705 * @param nodes - Visible nodes of the graph.
18706 *
18707 * @returns Populated node levels.
18708 */
18709function fillLevelsByDirectionLeaves(nodes) {
18710 return fillLevelsByDirection(
18711 // Pick only leaves (nodes without children).
18712 (node) => node.edges
18713 // Take only visible nodes into account.
18714 .filter((edge) => nodes.has(edge.toId))
18715 // Check that all edges lead to this node (leaf).
18716 .every((edge) => edge.to === node),
18717 // Use the lowest level.
18718 (newLevel, oldLevel) => oldLevel > newLevel,
18719 // Go against the direction of the edges.
18720 "from", nodes);
18721}
18722/**
18723 * Assign levels to nodes according to their positions in the hierarchy. Roots will be lined up at the top and all nodes as close to their parents as possible.
18724 *
18725 * @param nodes - Visible nodes of the graph.
18726 *
18727 * @returns Populated node levels.
18728 */
18729function fillLevelsByDirectionRoots(nodes) {
18730 return fillLevelsByDirection(
18731 // Pick only roots (nodes without parents).
18732 (node) => node.edges
18733 // Take only visible nodes into account.
18734 .filter((edge) => nodes.has(edge.toId))
18735 // Check that all edges lead from this node (root).
18736 .every((edge) => edge.from === node),
18737 // Use the highest level.
18738 (newLevel, oldLevel) => oldLevel < newLevel,
18739 // Go in the direction of the edges.
18740 "to", nodes);
18741}
18742/**
18743 * Assign levels to nodes according to their positions in the hierarchy.
18744 *
18745 * @param isEntryNode - Checks and return true if the graph should be traversed from this node.
18746 * @param shouldLevelBeReplaced - Checks and returns true if the level of given node should be updated to the new value.
18747 * @param direction - Wheter the graph should be traversed in the direction of the edges `"to"` or in the other way `"from"`.
18748 * @param nodes - Visible nodes of the graph.
18749 *
18750 * @returns Populated node levels.
18751 */
18752function fillLevelsByDirection(isEntryNode, shouldLevelBeReplaced, direction, nodes) {
18753 const levels = Object.create(null);
18754 // If acyclic, the graph can be walked through with (most likely way) fewer
18755 // steps than the number bellow. The exact value isn't too important as long
18756 // as it's quick to compute (doesn't impact acyclic graphs too much), is
18757 // higher than the number of steps actually needed (doesn't cut off before
18758 // acyclic graph is walked through) and prevents infinite loops (cuts off for
18759 // cyclic graphs).
18760 const limit = [...nodes.values()].reduce((acc, node) => acc + 1 + node.edges.length, 0);
18761 const edgeIdProp = (direction + "Id");
18762 const newLevelDiff = direction === "to" ? 1 : -1;
18763 for (const [entryNodeId, entryNode] of nodes) {
18764 if (
18765 // Skip if the node is not visible.
18766 !nodes.has(entryNodeId) ||
18767 // Skip if the node is not an entry node.
18768 !isEntryNode(entryNode)) {
18769 continue;
18770 }
18771 // Line up all the entry nodes on level 0.
18772 levels[entryNodeId] = 0;
18773 const stack = [entryNode];
18774 let done = 0;
18775 let node;
18776 while ((node = stack.pop())) {
18777 if (!nodes.has(entryNodeId)) {
18778 // Skip if the node is not visible.
18779 continue;
18780 }
18781 const newLevel = levels[node.id] + newLevelDiff;
18782 node.edges
18783 .filter((edge) =>
18784 // Ignore disconnected edges.
18785 edge.connected &&
18786 // Ignore circular edges.
18787 edge.to !== edge.from &&
18788 // Ignore edges leading to the node that's currently being processed.
18789 edge[direction] !== node &&
18790 // Ignore edges connecting to an invisible node.
18791 nodes.has(edge.toId) &&
18792 // Ignore edges connecting from an invisible node.
18793 nodes.has(edge.fromId))
18794 .forEach((edge) => {
18795 const targetNodeId = edge[edgeIdProp];
18796 const oldLevel = levels[targetNodeId];
18797 if (oldLevel == null || shouldLevelBeReplaced(newLevel, oldLevel)) {
18798 levels[targetNodeId] = newLevel;
18799 stack.push(edge[direction]);
18800 }
18801 });
18802 if (done > limit) {
18803 // This would run forever on a cyclic graph.
18804 return fillLevelsByDirectionCyclic(nodes, levels);
18805 }
18806 else {
18807 ++done;
18808 }
18809 }
18810 }
18811 return levels;
18812}
18813
18814/**
18815 * There's a mix-up with terms in the code. Following are the formal definitions:
18816 *
18817 * tree - a strict hierarchical network, i.e. every node has at most one parent
18818 * forest - a collection of trees. These distinct trees are thus not connected.
18819 *
18820 * So:
18821 * - in a network that is not a tree, there exist nodes with multiple parents.
18822 * - a network consisting of unconnected sub-networks, of which at least one
18823 * is not a tree, is not a forest.
18824 *
18825 * In the code, the definitions are:
18826 *
18827 * tree - any disconnected sub-network, strict hierarchical or not.
18828 * forest - a bunch of these sub-networks
18829 *
18830 * The difference between tree and not-tree is important in the code, notably within
18831 * to the block-shifting algorithm. The algorithm assumes formal trees and fails
18832 * for not-trees, often in a spectacular manner (search for 'exploding network' in the issues).
18833 *
18834 * In order to distinguish the definitions in the following code, the adjective 'formal' is
18835 * used. If 'formal' is absent, you must assume the non-formal definition.
18836 *
18837 * ----------------------------------------------------------------------------------
18838 * NOTES
18839 * =====
18840 *
18841 * A hierarchical layout is a different thing from a hierarchical network.
18842 * The layout is a way to arrange the nodes in the view; this can be done
18843 * on non-hierarchical networks as well. The converse is also possible.
18844 */
18845
18846/**
18847 * Container for derived data on current network, relating to hierarchy.
18848 *
18849 * @private
18850 */
18851class HierarchicalStatus {
18852 /**
18853 * @ignore
18854 */
18855 constructor() {
18856 this.childrenReference = {}; // child id's per node id
18857 this.parentReference = {}; // parent id's per node id
18858 this.trees = {}; // tree id per node id; i.e. to which tree does given node id belong
18859
18860 this.distributionOrdering = {}; // The nodes per level, in the display order
18861 this.levels = {}; // hierarchy level per node id
18862 this.distributionIndex = {}; // The position of the node in the level sorting order, per node id.
18863
18864 this.isTree = false; // True if current network is a formal tree
18865 this.treeIndex = -1; // Highest tree id in current network.
18866 }
18867
18868 /**
18869 * Add the relation between given nodes to the current state.
18870 *
18871 * @param {Node.id} parentNodeId
18872 * @param {Node.id} childNodeId
18873 */
18874 addRelation(parentNodeId, childNodeId) {
18875 if (this.childrenReference[parentNodeId] === undefined) {
18876 this.childrenReference[parentNodeId] = [];
18877 }
18878 this.childrenReference[parentNodeId].push(childNodeId);
18879
18880 if (this.parentReference[childNodeId] === undefined) {
18881 this.parentReference[childNodeId] = [];
18882 }
18883 this.parentReference[childNodeId].push(parentNodeId);
18884 }
18885
18886 /**
18887 * Check if the current state is for a formal tree or formal forest.
18888 *
18889 * This is the case if every node has at most one parent.
18890 *
18891 * Pre: parentReference init'ed properly for current network
18892 */
18893 checkIfTree() {
18894 for (const i in this.parentReference) {
18895 if (this.parentReference[i].length > 1) {
18896 this.isTree = false;
18897 return;
18898 }
18899 }
18900
18901 this.isTree = true;
18902 }
18903
18904 /**
18905 * Return the number of separate trees in the current network.
18906 *
18907 * @returns {number}
18908 */
18909 numTrees() {
18910 return this.treeIndex + 1; // This assumes the indexes are assigned consecitively
18911 }
18912
18913 /**
18914 * Assign a tree id to a node
18915 *
18916 * @param {Node} node
18917 * @param {string|number} treeId
18918 */
18919 setTreeIndex(node, treeId) {
18920 if (treeId === undefined) return; // Don't bother
18921
18922 if (this.trees[node.id] === undefined) {
18923 this.trees[node.id] = treeId;
18924 this.treeIndex = Math.max(treeId, this.treeIndex);
18925 }
18926 }
18927
18928 /**
18929 * Ensure level for given id is defined.
18930 *
18931 * Sets level to zero for given node id if not already present
18932 *
18933 * @param {Node.id} nodeId
18934 */
18935 ensureLevel(nodeId) {
18936 if (this.levels[nodeId] === undefined) {
18937 this.levels[nodeId] = 0;
18938 }
18939 }
18940
18941 /**
18942 * get the maximum level of a branch.
18943 *
18944 * TODO: Never entered; find a test case to test this!
18945 *
18946 * @param {Node.id} nodeId
18947 * @returns {number}
18948 */
18949 getMaxLevel(nodeId) {
18950 const accumulator = {};
18951
18952 const _getMaxLevel = (nodeId) => {
18953 if (accumulator[nodeId] !== undefined) {
18954 return accumulator[nodeId];
18955 }
18956 let level = this.levels[nodeId];
18957 if (this.childrenReference[nodeId]) {
18958 const children = this.childrenReference[nodeId];
18959 if (children.length > 0) {
18960 for (let i = 0; i < children.length; i++) {
18961 level = Math.max(level, _getMaxLevel(children[i]));
18962 }
18963 }
18964 }
18965 accumulator[nodeId] = level;
18966 return level;
18967 };
18968
18969 return _getMaxLevel(nodeId);
18970 }
18971
18972 /**
18973 *
18974 * @param {Node} nodeA
18975 * @param {Node} nodeB
18976 */
18977 levelDownstream(nodeA, nodeB) {
18978 if (this.levels[nodeB.id] === undefined) {
18979 // set initial level
18980 if (this.levels[nodeA.id] === undefined) {
18981 this.levels[nodeA.id] = 0;
18982 }
18983 // set level
18984 this.levels[nodeB.id] = this.levels[nodeA.id] + 1;
18985 }
18986 }
18987
18988 /**
18989 * Small util method to set the minimum levels of the nodes to zero.
18990 *
18991 * @param {Array.<Node>} nodes
18992 */
18993 setMinLevelToZero(nodes) {
18994 let minLevel = 1e9;
18995 // get the minimum level
18996 for (const nodeId in nodes) {
18997 if (Object.prototype.hasOwnProperty.call(nodes, nodeId)) {
18998 if (this.levels[nodeId] !== undefined) {
18999 minLevel = Math.min(this.levels[nodeId], minLevel);
19000 }
19001 }
19002 }
19003
19004 // subtract the minimum from the set so we have a range starting from 0
19005 for (const nodeId in nodes) {
19006 if (Object.prototype.hasOwnProperty.call(nodes, nodeId)) {
19007 if (this.levels[nodeId] !== undefined) {
19008 this.levels[nodeId] -= minLevel;
19009 }
19010 }
19011 }
19012 }
19013
19014 /**
19015 * Get the min and max xy-coordinates of a given tree
19016 *
19017 * @param {Array.<Node>} nodes
19018 * @param {number} index
19019 * @returns {{min_x: number, max_x: number, min_y: number, max_y: number}}
19020 */
19021 getTreeSize(nodes, index) {
19022 let min_x = 1e9;
19023 let max_x = -1e9;
19024 let min_y = 1e9;
19025 let max_y = -1e9;
19026
19027 for (const nodeId in this.trees) {
19028 if (Object.prototype.hasOwnProperty.call(this.trees, nodeId)) {
19029 if (this.trees[nodeId] === index) {
19030 const node = nodes[nodeId];
19031 min_x = Math.min(node.x, min_x);
19032 max_x = Math.max(node.x, max_x);
19033 min_y = Math.min(node.y, min_y);
19034 max_y = Math.max(node.y, max_y);
19035 }
19036 }
19037 }
19038
19039 return {
19040 min_x: min_x,
19041 max_x: max_x,
19042 min_y: min_y,
19043 max_y: max_y,
19044 };
19045 }
19046
19047 /**
19048 * Check if two nodes have the same parent(s)
19049 *
19050 * @param {Node} node1
19051 * @param {Node} node2
19052 * @returns {boolean} true if the two nodes have a same ancestor node, false otherwise
19053 */
19054 hasSameParent(node1, node2) {
19055 const parents1 = this.parentReference[node1.id];
19056 const parents2 = this.parentReference[node2.id];
19057 if (parents1 === undefined || parents2 === undefined) {
19058 return false;
19059 }
19060
19061 for (let i = 0; i < parents1.length; i++) {
19062 for (let j = 0; j < parents2.length; j++) {
19063 if (parents1[i] == parents2[j]) {
19064 return true;
19065 }
19066 }
19067 }
19068 return false;
19069 }
19070
19071 /**
19072 * Check if two nodes are in the same tree.
19073 *
19074 * @param {Node} node1
19075 * @param {Node} node2
19076 * @returns {boolean} true if this is so, false otherwise
19077 */
19078 inSameSubNetwork(node1, node2) {
19079 return this.trees[node1.id] === this.trees[node2.id];
19080 }
19081
19082 /**
19083 * Get a list of the distinct levels in the current network
19084 *
19085 * @returns {Array}
19086 */
19087 getLevels() {
19088 return Object.keys(this.distributionOrdering);
19089 }
19090
19091 /**
19092 * Add a node to the ordering per level
19093 *
19094 * @param {Node} node
19095 * @param {number} level
19096 */
19097 addToOrdering(node, level) {
19098 if (this.distributionOrdering[level] === undefined) {
19099 this.distributionOrdering[level] = [];
19100 }
19101
19102 let isPresent = false;
19103 const curLevel = this.distributionOrdering[level];
19104 for (const n in curLevel) {
19105 //if (curLevel[n].id === node.id) {
19106 if (curLevel[n] === node) {
19107 isPresent = true;
19108 break;
19109 }
19110 }
19111
19112 if (!isPresent) {
19113 this.distributionOrdering[level].push(node);
19114 this.distributionIndex[node.id] =
19115 this.distributionOrdering[level].length - 1;
19116 }
19117 }
19118}
19119
19120/**
19121 * The Layout Engine
19122 */
19123class LayoutEngine {
19124 /**
19125 * @param {object} body
19126 */
19127 constructor(body) {
19128 this.body = body;
19129
19130 // Make sure there always is some RNG because the setOptions method won't
19131 // set it unless there's a seed for it.
19132 this._resetRNG(Math.random() + ":" + Date.now());
19133
19134 this.setPhysics = false;
19135 this.options = {};
19136 this.optionsBackup = { physics: {} };
19137
19138 this.defaultOptions = {
19139 randomSeed: undefined,
19140 improvedLayout: true,
19141 clusterThreshold: 150,
19142 hierarchical: {
19143 enabled: false,
19144 levelSeparation: 150,
19145 nodeSpacing: 100,
19146 treeSpacing: 200,
19147 blockShifting: true,
19148 edgeMinimization: true,
19149 parentCentralization: true,
19150 direction: "UD", // UD, DU, LR, RL
19151 sortMethod: "hubsize", // hubsize, directed
19152 },
19153 };
19154 Object.assign(this.options, this.defaultOptions);
19155 this.bindEventListeners();
19156 }
19157
19158 /**
19159 * Binds event listeners
19160 */
19161 bindEventListeners() {
19162 this.body.emitter.on("_dataChanged", () => {
19163 this.setupHierarchicalLayout();
19164 });
19165 this.body.emitter.on("_dataLoaded", () => {
19166 this.layoutNetwork();
19167 });
19168 this.body.emitter.on("_resetHierarchicalLayout", () => {
19169 this.setupHierarchicalLayout();
19170 });
19171 this.body.emitter.on("_adjustEdgesForHierarchicalLayout", () => {
19172 if (this.options.hierarchical.enabled !== true) {
19173 return;
19174 }
19175 // get the type of static smooth curve in case it is required
19176 const type = this.direction.curveType();
19177
19178 // force all edges into static smooth curves.
19179 this.body.emitter.emit("_forceDisableDynamicCurves", type, false);
19180 });
19181 }
19182
19183 /**
19184 *
19185 * @param {object} options
19186 * @param {object} allOptions
19187 * @returns {object}
19188 */
19189 setOptions(options, allOptions) {
19190 if (options !== undefined) {
19191 const hierarchical = this.options.hierarchical;
19192 const prevHierarchicalState = hierarchical.enabled;
19193 selectiveDeepExtend(
19194 ["randomSeed", "improvedLayout", "clusterThreshold"],
19195 this.options,
19196 options
19197 );
19198 mergeOptions(this.options, options, "hierarchical");
19199
19200 if (options.randomSeed !== undefined) {
19201 this._resetRNG(options.randomSeed);
19202 }
19203
19204 if (hierarchical.enabled === true) {
19205 if (prevHierarchicalState === true) {
19206 // refresh the overridden options for nodes and edges.
19207 this.body.emitter.emit("refresh", true);
19208 }
19209
19210 // make sure the level separation is the right way up
19211 if (
19212 hierarchical.direction === "RL" ||
19213 hierarchical.direction === "DU"
19214 ) {
19215 if (hierarchical.levelSeparation > 0) {
19216 hierarchical.levelSeparation *= -1;
19217 }
19218 } else {
19219 if (hierarchical.levelSeparation < 0) {
19220 hierarchical.levelSeparation *= -1;
19221 }
19222 }
19223
19224 this.setDirectionStrategy();
19225
19226 this.body.emitter.emit("_resetHierarchicalLayout");
19227 // because the hierarchical system needs it's own physics and smooth curve settings,
19228 // we adapt the other options if needed.
19229 return this.adaptAllOptionsForHierarchicalLayout(allOptions);
19230 } else {
19231 if (prevHierarchicalState === true) {
19232 // refresh the overridden options for nodes and edges.
19233 this.body.emitter.emit("refresh");
19234 return deepExtend(allOptions, this.optionsBackup);
19235 }
19236 }
19237 }
19238 return allOptions;
19239 }
19240
19241 /**
19242 * Reset the random number generator with given seed.
19243 *
19244 * @param {any} seed - The seed that will be forwarded the the RNG.
19245 */
19246 _resetRNG(seed) {
19247 this.initialRandomSeed = seed;
19248 this._rng = Alea(this.initialRandomSeed);
19249 }
19250
19251 /**
19252 *
19253 * @param {object} allOptions
19254 * @returns {object}
19255 */
19256 adaptAllOptionsForHierarchicalLayout(allOptions) {
19257 if (this.options.hierarchical.enabled === true) {
19258 const backupPhysics = this.optionsBackup.physics;
19259
19260 // set the physics
19261 if (allOptions.physics === undefined || allOptions.physics === true) {
19262 allOptions.physics = {
19263 enabled:
19264 backupPhysics.enabled === undefined ? true : backupPhysics.enabled,
19265 solver: "hierarchicalRepulsion",
19266 };
19267 backupPhysics.enabled =
19268 backupPhysics.enabled === undefined ? true : backupPhysics.enabled;
19269 backupPhysics.solver = backupPhysics.solver || "barnesHut";
19270 } else if (typeof allOptions.physics === "object") {
19271 backupPhysics.enabled =
19272 allOptions.physics.enabled === undefined
19273 ? true
19274 : allOptions.physics.enabled;
19275 backupPhysics.solver = allOptions.physics.solver || "barnesHut";
19276 allOptions.physics.solver = "hierarchicalRepulsion";
19277 } else if (allOptions.physics !== false) {
19278 backupPhysics.solver = "barnesHut";
19279 allOptions.physics = { solver: "hierarchicalRepulsion" };
19280 }
19281
19282 // get the type of static smooth curve in case it is required
19283 let type = this.direction.curveType();
19284
19285 // disable smooth curves if nothing is defined. If smooth curves have been turned on,
19286 // turn them into static smooth curves.
19287 if (allOptions.edges === undefined) {
19288 this.optionsBackup.edges = {
19289 smooth: { enabled: true, type: "dynamic" },
19290 };
19291 allOptions.edges = { smooth: false };
19292 } else if (allOptions.edges.smooth === undefined) {
19293 this.optionsBackup.edges = {
19294 smooth: { enabled: true, type: "dynamic" },
19295 };
19296 allOptions.edges.smooth = false;
19297 } else {
19298 if (typeof allOptions.edges.smooth === "boolean") {
19299 this.optionsBackup.edges = { smooth: allOptions.edges.smooth };
19300 allOptions.edges.smooth = {
19301 enabled: allOptions.edges.smooth,
19302 type: type,
19303 };
19304 } else {
19305 const smooth = allOptions.edges.smooth;
19306
19307 // allow custom types except for dynamic
19308 if (smooth.type !== undefined && smooth.type !== "dynamic") {
19309 type = smooth.type;
19310 }
19311
19312 // TODO: this is options merging; see if the standard routines can be used here.
19313 this.optionsBackup.edges = {
19314 smooth: {
19315 enabled: smooth.enabled === undefined ? true : smooth.enabled,
19316 type: smooth.type === undefined ? "dynamic" : smooth.type,
19317 roundness:
19318 smooth.roundness === undefined ? 0.5 : smooth.roundness,
19319 forceDirection:
19320 smooth.forceDirection === undefined
19321 ? false
19322 : smooth.forceDirection,
19323 },
19324 };
19325
19326 // NOTE: Copying an object to self; this is basically setting defaults for undefined variables
19327 allOptions.edges.smooth = {
19328 enabled: smooth.enabled === undefined ? true : smooth.enabled,
19329 type: type,
19330 roundness: smooth.roundness === undefined ? 0.5 : smooth.roundness,
19331 forceDirection:
19332 smooth.forceDirection === undefined
19333 ? false
19334 : smooth.forceDirection,
19335 };
19336 }
19337 }
19338
19339 // Force all edges into static smooth curves.
19340 // Only applies to edges that do not use the global options for smooth.
19341 this.body.emitter.emit("_forceDisableDynamicCurves", type);
19342 }
19343
19344 return allOptions;
19345 }
19346
19347 /**
19348 *
19349 * @param {Array.<Node>} nodesArray
19350 */
19351 positionInitially(nodesArray) {
19352 if (this.options.hierarchical.enabled !== true) {
19353 this._resetRNG(this.initialRandomSeed);
19354 const radius = nodesArray.length + 50;
19355 for (let i = 0; i < nodesArray.length; i++) {
19356 const node = nodesArray[i];
19357 const angle = 2 * Math.PI * this._rng();
19358 if (node.x === undefined) {
19359 node.x = radius * Math.cos(angle);
19360 }
19361 if (node.y === undefined) {
19362 node.y = radius * Math.sin(angle);
19363 }
19364 }
19365 }
19366 }
19367
19368 /**
19369 * Use Kamada Kawai to position nodes. This is quite a heavy algorithm so if there are a lot of nodes we
19370 * cluster them first to reduce the amount.
19371 */
19372 layoutNetwork() {
19373 if (
19374 this.options.hierarchical.enabled !== true &&
19375 this.options.improvedLayout === true
19376 ) {
19377 const indices = this.body.nodeIndices;
19378
19379 // first check if we should Kamada Kawai to layout. The threshold is if less than half of the visible
19380 // nodes have predefined positions we use this.
19381 let positionDefined = 0;
19382 for (let i = 0; i < indices.length; i++) {
19383 const node = this.body.nodes[indices[i]];
19384 if (node.predefinedPosition === true) {
19385 positionDefined += 1;
19386 }
19387 }
19388
19389 // if less than half of the nodes have a predefined position we continue
19390 if (positionDefined < 0.5 * indices.length) {
19391 const MAX_LEVELS = 10;
19392 let level = 0;
19393 const clusterThreshold = this.options.clusterThreshold;
19394
19395 //
19396 // Define the options for the hidden cluster nodes
19397 // These options don't propagate outside the clustering phase.
19398 //
19399 // Some options are explicitly disabled, because they may be set in group or default node options.
19400 // The clusters are never displayed, so most explicit settings here serve as performance optimizations.
19401 //
19402 // The explicit setting of 'shape' is to avoid `shape: 'image'`; images are not passed to the hidden
19403 // cluster nodes, leading to an exception on creation.
19404 //
19405 // All settings here are performance related, except when noted otherwise.
19406 //
19407 const clusterOptions = {
19408 clusterNodeProperties: {
19409 shape: "ellipse", // Bugfix: avoid type 'image', no images supplied
19410 label: "", // avoid label handling
19411 group: "", // avoid group handling
19412 font: { multi: false }, // avoid font propagation
19413 },
19414 clusterEdgeProperties: {
19415 label: "", // avoid label handling
19416 font: { multi: false }, // avoid font propagation
19417 smooth: {
19418 enabled: false, // avoid drawing penalty for complex edges
19419 },
19420 },
19421 };
19422
19423 // if there are a lot of nodes, we cluster before we run the algorithm.
19424 // NOTE: this part fails to find clusters for large scale-free networks, which should
19425 // be easily clusterable.
19426 // TODO: examine why this is so
19427 if (indices.length > clusterThreshold) {
19428 const startLength = indices.length;
19429 while (indices.length > clusterThreshold && level <= MAX_LEVELS) {
19430 //console.time("clustering")
19431 level += 1;
19432 const before = indices.length;
19433 // if there are many nodes we do a hubsize cluster
19434 if (level % 3 === 0) {
19435 this.body.modules.clustering.clusterBridges(clusterOptions);
19436 } else {
19437 this.body.modules.clustering.clusterOutliers(clusterOptions);
19438 }
19439 const after = indices.length;
19440 if (before == after && level % 3 !== 0) {
19441 this._declusterAll();
19442 this.body.emitter.emit("_layoutFailed");
19443 console.info(
19444 "This network could not be positioned by this version of the improved layout algorithm." +
19445 " Please disable improvedLayout for better performance."
19446 );
19447 return;
19448 }
19449 //console.timeEnd("clustering")
19450 //console.log(before,level,after);
19451 }
19452 // increase the size of the edges
19453 this.body.modules.kamadaKawai.setOptions({
19454 springLength: Math.max(150, 2 * startLength),
19455 });
19456 }
19457 if (level > MAX_LEVELS) {
19458 console.info(
19459 "The clustering didn't succeed within the amount of interations allowed," +
19460 " progressing with partial result."
19461 );
19462 }
19463
19464 // position the system for these nodes and edges
19465 this.body.modules.kamadaKawai.solve(
19466 indices,
19467 this.body.edgeIndices,
19468 true
19469 );
19470
19471 // shift to center point
19472 this._shiftToCenter();
19473
19474 // perturb the nodes a little bit to force the physics to kick in
19475 const offset = 70;
19476 for (let i = 0; i < indices.length; i++) {
19477 // Only perturb the nodes that aren't fixed
19478 const node = this.body.nodes[indices[i]];
19479 if (node.predefinedPosition === false) {
19480 node.x += (0.5 - this._rng()) * offset;
19481 node.y += (0.5 - this._rng()) * offset;
19482 }
19483 }
19484
19485 // uncluster all clusters
19486 this._declusterAll();
19487
19488 // reposition all bezier nodes.
19489 this.body.emitter.emit("_repositionBezierNodes");
19490 }
19491 }
19492 }
19493
19494 /**
19495 * Move all the nodes towards to the center so gravitational pull wil not move the nodes away from view
19496 *
19497 * @private
19498 */
19499 _shiftToCenter() {
19500 const range = NetworkUtil.getRangeCore(
19501 this.body.nodes,
19502 this.body.nodeIndices
19503 );
19504 const center = NetworkUtil.findCenter(range);
19505 for (let i = 0; i < this.body.nodeIndices.length; i++) {
19506 const node = this.body.nodes[this.body.nodeIndices[i]];
19507 node.x -= center.x;
19508 node.y -= center.y;
19509 }
19510 }
19511
19512 /**
19513 * Expands all clusters
19514 *
19515 * @private
19516 */
19517 _declusterAll() {
19518 let clustersPresent = true;
19519 while (clustersPresent === true) {
19520 clustersPresent = false;
19521 for (let i = 0; i < this.body.nodeIndices.length; i++) {
19522 if (this.body.nodes[this.body.nodeIndices[i]].isCluster === true) {
19523 clustersPresent = true;
19524 this.body.modules.clustering.openCluster(
19525 this.body.nodeIndices[i],
19526 {},
19527 false
19528 );
19529 }
19530 }
19531 if (clustersPresent === true) {
19532 this.body.emitter.emit("_dataChanged");
19533 }
19534 }
19535 }
19536
19537 /**
19538 *
19539 * @returns {number|*}
19540 */
19541 getSeed() {
19542 return this.initialRandomSeed;
19543 }
19544
19545 /**
19546 * This is the main function to layout the nodes in a hierarchical way.
19547 * It checks if the node details are supplied correctly
19548 *
19549 * @private
19550 */
19551 setupHierarchicalLayout() {
19552 if (
19553 this.options.hierarchical.enabled === true &&
19554 this.body.nodeIndices.length > 0
19555 ) {
19556 // get the size of the largest hubs and check if the user has defined a level for a node.
19557 let node, nodeId;
19558 let definedLevel = false;
19559 let undefinedLevel = false;
19560 this.lastNodeOnLevel = {};
19561 this.hierarchical = new HierarchicalStatus();
19562
19563 for (nodeId in this.body.nodes) {
19564 if (Object.prototype.hasOwnProperty.call(this.body.nodes, nodeId)) {
19565 node = this.body.nodes[nodeId];
19566 if (node.options.level !== undefined) {
19567 definedLevel = true;
19568 this.hierarchical.levels[nodeId] = node.options.level;
19569 } else {
19570 undefinedLevel = true;
19571 }
19572 }
19573 }
19574
19575 // if the user defined some levels but not all, alert and run without hierarchical layout
19576 if (undefinedLevel === true && definedLevel === true) {
19577 throw new Error(
19578 "To use the hierarchical layout, nodes require either no predefined levels" +
19579 " or levels have to be defined for all nodes."
19580 );
19581 } else {
19582 // define levels if undefined by the users. Based on hubsize.
19583 if (undefinedLevel === true) {
19584 const sortMethod = this.options.hierarchical.sortMethod;
19585 if (sortMethod === "hubsize") {
19586 this._determineLevelsByHubsize();
19587 } else if (sortMethod === "directed") {
19588 this._determineLevelsDirected();
19589 } else if (sortMethod === "custom") {
19590 this._determineLevelsCustomCallback();
19591 }
19592 }
19593
19594 // fallback for cases where there are nodes but no edges
19595 for (const nodeId in this.body.nodes) {
19596 if (Object.prototype.hasOwnProperty.call(this.body.nodes, nodeId)) {
19597 this.hierarchical.ensureLevel(nodeId);
19598 }
19599 }
19600 // check the distribution of the nodes per level.
19601 const distribution = this._getDistribution();
19602
19603 // get the parent children relations.
19604 this._generateMap();
19605
19606 // place the nodes on the canvas.
19607 this._placeNodesByHierarchy(distribution);
19608
19609 // condense the whitespace.
19610 this._condenseHierarchy();
19611
19612 // shift to center so gravity does not have to do much
19613 this._shiftToCenter();
19614 }
19615 }
19616 }
19617
19618 /**
19619 * @private
19620 */
19621 _condenseHierarchy() {
19622 // Global var in this scope to define when the movement has stopped.
19623 let stillShifting = false;
19624 const branches = {};
19625 // first we have some methods to help shifting trees around.
19626 // the main method to shift the trees
19627 const shiftTrees = () => {
19628 const treeSizes = getTreeSizes();
19629 let shiftBy = 0;
19630 for (let i = 0; i < treeSizes.length - 1; i++) {
19631 const diff = treeSizes[i].max - treeSizes[i + 1].min;
19632 shiftBy += diff + this.options.hierarchical.treeSpacing;
19633 shiftTree(i + 1, shiftBy);
19634 }
19635 };
19636
19637 // shift a single tree by an offset
19638 const shiftTree = (index, offset) => {
19639 const trees = this.hierarchical.trees;
19640
19641 for (const nodeId in trees) {
19642 if (Object.prototype.hasOwnProperty.call(trees, nodeId)) {
19643 if (trees[nodeId] === index) {
19644 this.direction.shift(nodeId, offset);
19645 }
19646 }
19647 }
19648 };
19649
19650 // get the width of all trees
19651 const getTreeSizes = () => {
19652 const treeWidths = [];
19653 for (let i = 0; i < this.hierarchical.numTrees(); i++) {
19654 treeWidths.push(this.direction.getTreeSize(i));
19655 }
19656 return treeWidths;
19657 };
19658
19659 // get a map of all nodes in this branch
19660 const getBranchNodes = (source, map) => {
19661 if (map[source.id]) {
19662 return;
19663 }
19664 map[source.id] = true;
19665 if (this.hierarchical.childrenReference[source.id]) {
19666 const children = this.hierarchical.childrenReference[source.id];
19667 if (children.length > 0) {
19668 for (let i = 0; i < children.length; i++) {
19669 getBranchNodes(this.body.nodes[children[i]], map);
19670 }
19671 }
19672 }
19673 };
19674
19675 // get a min max width as well as the maximum movement space it has on either sides
19676 // we use min max terminology because width and height can interchange depending on the direction of the layout
19677 const getBranchBoundary = (branchMap, maxLevel = 1e9) => {
19678 let minSpace = 1e9;
19679 let maxSpace = 1e9;
19680 let min = 1e9;
19681 let max = -1e9;
19682 for (const branchNode in branchMap) {
19683 if (Object.prototype.hasOwnProperty.call(branchMap, branchNode)) {
19684 const node = this.body.nodes[branchNode];
19685 const level = this.hierarchical.levels[node.id];
19686 const position = this.direction.getPosition(node);
19687
19688 // get the space around the node.
19689 const [minSpaceNode, maxSpaceNode] = this._getSpaceAroundNode(
19690 node,
19691 branchMap
19692 );
19693 minSpace = Math.min(minSpaceNode, minSpace);
19694 maxSpace = Math.min(maxSpaceNode, maxSpace);
19695
19696 // the width is only relevant for the levels two nodes have in common. This is why we filter on this.
19697 if (level <= maxLevel) {
19698 min = Math.min(position, min);
19699 max = Math.max(position, max);
19700 }
19701 }
19702 }
19703
19704 return [min, max, minSpace, maxSpace];
19705 };
19706
19707 // check what the maximum level is these nodes have in common.
19708 const getCollisionLevel = (node1, node2) => {
19709 const maxLevel1 = this.hierarchical.getMaxLevel(node1.id);
19710 const maxLevel2 = this.hierarchical.getMaxLevel(node2.id);
19711 return Math.min(maxLevel1, maxLevel2);
19712 };
19713
19714 /**
19715 * Condense elements. These can be nodes or branches depending on the callback.
19716 *
19717 * @param {Function} callback
19718 * @param {Array.<number>} levels
19719 * @param {*} centerParents
19720 */
19721 const shiftElementsCloser = (callback, levels, centerParents) => {
19722 const hier = this.hierarchical;
19723
19724 for (let i = 0; i < levels.length; i++) {
19725 const level = levels[i];
19726 const levelNodes = hier.distributionOrdering[level];
19727 if (levelNodes.length > 1) {
19728 for (let j = 0; j < levelNodes.length - 1; j++) {
19729 const node1 = levelNodes[j];
19730 const node2 = levelNodes[j + 1];
19731
19732 // NOTE: logic maintained as it was; if nodes have same ancestor,
19733 // then of course they are in the same sub-network.
19734 if (
19735 hier.hasSameParent(node1, node2) &&
19736 hier.inSameSubNetwork(node1, node2)
19737 ) {
19738 callback(node1, node2, centerParents);
19739 }
19740 }
19741 }
19742 }
19743 };
19744
19745 // callback for shifting branches
19746 const branchShiftCallback = (node1, node2, centerParent = false) => {
19747 //window.CALLBACKS.push(() => {
19748 const pos1 = this.direction.getPosition(node1);
19749 const pos2 = this.direction.getPosition(node2);
19750 const diffAbs = Math.abs(pos2 - pos1);
19751 const nodeSpacing = this.options.hierarchical.nodeSpacing;
19752 //console.log("NOW CHECKING:", node1.id, node2.id, diffAbs);
19753 if (diffAbs > nodeSpacing) {
19754 const branchNodes1 = {};
19755 const branchNodes2 = {};
19756
19757 getBranchNodes(node1, branchNodes1);
19758 getBranchNodes(node2, branchNodes2);
19759
19760 // check the largest distance between the branches
19761 const maxLevel = getCollisionLevel(node1, node2);
19762 const branchNodeBoundary1 = getBranchBoundary(branchNodes1, maxLevel);
19763 const branchNodeBoundary2 = getBranchBoundary(branchNodes2, maxLevel);
19764 const max1 = branchNodeBoundary1[1];
19765 const min2 = branchNodeBoundary2[0];
19766 const minSpace2 = branchNodeBoundary2[2];
19767
19768 //console.log(node1.id, getBranchBoundary(branchNodes1, maxLevel), node2.id,
19769 // getBranchBoundary(branchNodes2, maxLevel), maxLevel);
19770 const diffBranch = Math.abs(max1 - min2);
19771 if (diffBranch > nodeSpacing) {
19772 let offset = max1 - min2 + nodeSpacing;
19773 if (offset < -minSpace2 + nodeSpacing) {
19774 offset = -minSpace2 + nodeSpacing;
19775 //console.log("RESETTING OFFSET", max1 - min2 + this.options.hierarchical.nodeSpacing, -minSpace2, offset);
19776 }
19777 if (offset < 0) {
19778 //console.log("SHIFTING", node2.id, offset);
19779 this._shiftBlock(node2.id, offset);
19780 stillShifting = true;
19781
19782 if (centerParent === true) this._centerParent(node2);
19783 }
19784 }
19785 }
19786 //this.body.emitter.emit("_redraw");})
19787 };
19788
19789 const minimizeEdgeLength = (iterations, node) => {
19790 //window.CALLBACKS.push(() => {
19791 // console.log("ts",node.id);
19792 const nodeId = node.id;
19793 const allEdges = node.edges;
19794 const nodeLevel = this.hierarchical.levels[node.id];
19795
19796 // gather constants
19797 const C2 =
19798 this.options.hierarchical.levelSeparation *
19799 this.options.hierarchical.levelSeparation;
19800 const referenceNodes = {};
19801 const aboveEdges = [];
19802 for (let i = 0; i < allEdges.length; i++) {
19803 const edge = allEdges[i];
19804 if (edge.toId != edge.fromId) {
19805 const otherNode = edge.toId == nodeId ? edge.from : edge.to;
19806 referenceNodes[allEdges[i].id] = otherNode;
19807 if (this.hierarchical.levels[otherNode.id] < nodeLevel) {
19808 aboveEdges.push(edge);
19809 }
19810 }
19811 }
19812
19813 // differentiated sum of lengths based on only moving one node over one axis
19814 const getFx = (point, edges) => {
19815 let sum = 0;
19816 for (let i = 0; i < edges.length; i++) {
19817 if (referenceNodes[edges[i].id] !== undefined) {
19818 const a =
19819 this.direction.getPosition(referenceNodes[edges[i].id]) - point;
19820 sum += a / Math.sqrt(a * a + C2);
19821 }
19822 }
19823 return sum;
19824 };
19825
19826 // doubly differentiated sum of lengths based on only moving one node over one axis
19827 const getDFx = (point, edges) => {
19828 let sum = 0;
19829 for (let i = 0; i < edges.length; i++) {
19830 if (referenceNodes[edges[i].id] !== undefined) {
19831 const a =
19832 this.direction.getPosition(referenceNodes[edges[i].id]) - point;
19833 sum -= C2 * Math.pow(a * a + C2, -1.5);
19834 }
19835 }
19836 return sum;
19837 };
19838
19839 const getGuess = (iterations, edges) => {
19840 let guess = this.direction.getPosition(node);
19841 // Newton's method for optimization
19842 const guessMap = {};
19843 for (let i = 0; i < iterations; i++) {
19844 const fx = getFx(guess, edges);
19845 const dfx = getDFx(guess, edges);
19846
19847 // we limit the movement to avoid instability.
19848 const limit = 40;
19849 const ratio = Math.max(-limit, Math.min(limit, Math.round(fx / dfx)));
19850 guess = guess - ratio;
19851 // reduce duplicates
19852 if (guessMap[guess] !== undefined) {
19853 break;
19854 }
19855 guessMap[guess] = i;
19856 }
19857 return guess;
19858 };
19859
19860 const moveBranch = (guess) => {
19861 // position node if there is space
19862 const nodePosition = this.direction.getPosition(node);
19863
19864 // check movable area of the branch
19865 if (branches[node.id] === undefined) {
19866 const branchNodes = {};
19867 getBranchNodes(node, branchNodes);
19868 branches[node.id] = branchNodes;
19869 }
19870 const branchBoundary = getBranchBoundary(branches[node.id]);
19871 const minSpaceBranch = branchBoundary[2];
19872 const maxSpaceBranch = branchBoundary[3];
19873
19874 const diff = guess - nodePosition;
19875
19876 // check if we are allowed to move the node:
19877 let branchOffset = 0;
19878 if (diff > 0) {
19879 branchOffset = Math.min(
19880 diff,
19881 maxSpaceBranch - this.options.hierarchical.nodeSpacing
19882 );
19883 } else if (diff < 0) {
19884 branchOffset = -Math.min(
19885 -diff,
19886 minSpaceBranch - this.options.hierarchical.nodeSpacing
19887 );
19888 }
19889
19890 if (branchOffset != 0) {
19891 //console.log("moving branch:",branchOffset, maxSpaceBranch, minSpaceBranch)
19892 this._shiftBlock(node.id, branchOffset);
19893 //this.body.emitter.emit("_redraw");
19894 stillShifting = true;
19895 }
19896 };
19897
19898 const moveNode = (guess) => {
19899 const nodePosition = this.direction.getPosition(node);
19900
19901 // position node if there is space
19902 const [minSpace, maxSpace] = this._getSpaceAroundNode(node);
19903 const diff = guess - nodePosition;
19904 // check if we are allowed to move the node:
19905 let newPosition = nodePosition;
19906 if (diff > 0) {
19907 newPosition = Math.min(
19908 nodePosition + (maxSpace - this.options.hierarchical.nodeSpacing),
19909 guess
19910 );
19911 } else if (diff < 0) {
19912 newPosition = Math.max(
19913 nodePosition - (minSpace - this.options.hierarchical.nodeSpacing),
19914 guess
19915 );
19916 }
19917
19918 if (newPosition !== nodePosition) {
19919 //console.log("moving Node:",diff, minSpace, maxSpace);
19920 this.direction.setPosition(node, newPosition);
19921 //this.body.emitter.emit("_redraw");
19922 stillShifting = true;
19923 }
19924 };
19925
19926 let guess = getGuess(iterations, aboveEdges);
19927 moveBranch(guess);
19928 guess = getGuess(iterations, allEdges);
19929 moveNode(guess);
19930 //})
19931 };
19932
19933 // method to remove whitespace between branches. Because we do bottom up, we can center the parents.
19934 const minimizeEdgeLengthBottomUp = (iterations) => {
19935 let levels = this.hierarchical.getLevels();
19936 levels = levels.reverse();
19937 for (let i = 0; i < iterations; i++) {
19938 stillShifting = false;
19939 for (let j = 0; j < levels.length; j++) {
19940 const level = levels[j];
19941 const levelNodes = this.hierarchical.distributionOrdering[level];
19942 for (let k = 0; k < levelNodes.length; k++) {
19943 minimizeEdgeLength(1000, levelNodes[k]);
19944 }
19945 }
19946 if (stillShifting !== true) {
19947 //console.log("FINISHED minimizeEdgeLengthBottomUp IN " + i);
19948 break;
19949 }
19950 }
19951 };
19952
19953 // method to remove whitespace between branches. Because we do bottom up, we can center the parents.
19954 const shiftBranchesCloserBottomUp = (iterations) => {
19955 let levels = this.hierarchical.getLevels();
19956 levels = levels.reverse();
19957 for (let i = 0; i < iterations; i++) {
19958 stillShifting = false;
19959 shiftElementsCloser(branchShiftCallback, levels, true);
19960 if (stillShifting !== true) {
19961 //console.log("FINISHED shiftBranchesCloserBottomUp IN " + (i+1));
19962 break;
19963 }
19964 }
19965 };
19966
19967 // center all parents
19968 const centerAllParents = () => {
19969 for (const nodeId in this.body.nodes) {
19970 if (Object.prototype.hasOwnProperty.call(this.body.nodes, nodeId))
19971 this._centerParent(this.body.nodes[nodeId]);
19972 }
19973 };
19974
19975 // center all parents
19976 const centerAllParentsBottomUp = () => {
19977 let levels = this.hierarchical.getLevels();
19978 levels = levels.reverse();
19979 for (let i = 0; i < levels.length; i++) {
19980 const level = levels[i];
19981 const levelNodes = this.hierarchical.distributionOrdering[level];
19982 for (let j = 0; j < levelNodes.length; j++) {
19983 this._centerParent(levelNodes[j]);
19984 }
19985 }
19986 };
19987
19988 // the actual work is done here.
19989 if (this.options.hierarchical.blockShifting === true) {
19990 shiftBranchesCloserBottomUp(5);
19991 centerAllParents();
19992 }
19993
19994 // minimize edge length
19995 if (this.options.hierarchical.edgeMinimization === true) {
19996 minimizeEdgeLengthBottomUp(20);
19997 }
19998
19999 if (this.options.hierarchical.parentCentralization === true) {
20000 centerAllParentsBottomUp();
20001 }
20002
20003 shiftTrees();
20004 }
20005
20006 /**
20007 * This gives the space around the node. IF a map is supplied, it will only check against nodes NOT in the map.
20008 * This is used to only get the distances to nodes outside of a branch.
20009 *
20010 * @param {Node} node
20011 * @param {{Node.id: vis.Node}} map
20012 * @returns {number[]}
20013 * @private
20014 */
20015 _getSpaceAroundNode(node, map) {
20016 let useMap = true;
20017 if (map === undefined) {
20018 useMap = false;
20019 }
20020 const level = this.hierarchical.levels[node.id];
20021 if (level !== undefined) {
20022 const index = this.hierarchical.distributionIndex[node.id];
20023 const position = this.direction.getPosition(node);
20024 const ordering = this.hierarchical.distributionOrdering[level];
20025 let minSpace = 1e9;
20026 let maxSpace = 1e9;
20027 if (index !== 0) {
20028 const prevNode = ordering[index - 1];
20029 if (
20030 (useMap === true && map[prevNode.id] === undefined) ||
20031 useMap === false
20032 ) {
20033 const prevPos = this.direction.getPosition(prevNode);
20034 minSpace = position - prevPos;
20035 }
20036 }
20037
20038 if (index != ordering.length - 1) {
20039 const nextNode = ordering[index + 1];
20040 if (
20041 (useMap === true && map[nextNode.id] === undefined) ||
20042 useMap === false
20043 ) {
20044 const nextPos = this.direction.getPosition(nextNode);
20045 maxSpace = Math.min(maxSpace, nextPos - position);
20046 }
20047 }
20048
20049 return [minSpace, maxSpace];
20050 } else {
20051 return [0, 0];
20052 }
20053 }
20054
20055 /**
20056 * We use this method to center a parent node and check if it does not cross other nodes when it does.
20057 *
20058 * @param {Node} node
20059 * @private
20060 */
20061 _centerParent(node) {
20062 if (this.hierarchical.parentReference[node.id]) {
20063 const parents = this.hierarchical.parentReference[node.id];
20064 for (let i = 0; i < parents.length; i++) {
20065 const parentId = parents[i];
20066 const parentNode = this.body.nodes[parentId];
20067 const children = this.hierarchical.childrenReference[parentId];
20068
20069 if (children !== undefined) {
20070 // get the range of the children
20071 const newPosition = this._getCenterPosition(children);
20072
20073 const position = this.direction.getPosition(parentNode);
20074 const [minSpace, maxSpace] = this._getSpaceAroundNode(parentNode);
20075 const diff = position - newPosition;
20076 if (
20077 (diff < 0 &&
20078 Math.abs(diff) <
20079 maxSpace - this.options.hierarchical.nodeSpacing) ||
20080 (diff > 0 &&
20081 Math.abs(diff) < minSpace - this.options.hierarchical.nodeSpacing)
20082 ) {
20083 this.direction.setPosition(parentNode, newPosition);
20084 }
20085 }
20086 }
20087 }
20088 }
20089
20090 /**
20091 * This function places the nodes on the canvas based on the hierarchial distribution.
20092 *
20093 * @param {object} distribution | obtained by the function this._getDistribution()
20094 * @private
20095 */
20096 _placeNodesByHierarchy(distribution) {
20097 this.positionedNodes = {};
20098 // start placing all the level 0 nodes first. Then recursively position their branches.
20099 for (const level in distribution) {
20100 if (Object.prototype.hasOwnProperty.call(distribution, level)) {
20101 // sort nodes in level by position:
20102 let nodeArray = Object.keys(distribution[level]);
20103 nodeArray = this._indexArrayToNodes(nodeArray);
20104 this.direction.sort(nodeArray);
20105 let handledNodeCount = 0;
20106
20107 for (let i = 0; i < nodeArray.length; i++) {
20108 const node = nodeArray[i];
20109 if (this.positionedNodes[node.id] === undefined) {
20110 const spacing = this.options.hierarchical.nodeSpacing;
20111 let pos = spacing * handledNodeCount;
20112 // We get the X or Y values we need and store them in pos and previousPos.
20113 // The get and set make sure we get X or Y
20114 if (handledNodeCount > 0) {
20115 pos = this.direction.getPosition(nodeArray[i - 1]) + spacing;
20116 }
20117 this.direction.setPosition(node, pos, level);
20118 this._validatePositionAndContinue(node, level, pos);
20119
20120 handledNodeCount++;
20121 }
20122 }
20123 }
20124 }
20125 }
20126
20127 /**
20128 * This is a recursively called function to enumerate the branches from the largest hubs and place the nodes
20129 * on a X position that ensures there will be no overlap.
20130 *
20131 * @param {Node.id} parentId
20132 * @param {number} parentLevel
20133 * @private
20134 */
20135 _placeBranchNodes(parentId, parentLevel) {
20136 const childRef = this.hierarchical.childrenReference[parentId];
20137
20138 // if this is not a parent, cancel the placing. This can happen with multiple parents to one child.
20139 if (childRef === undefined) {
20140 return;
20141 }
20142
20143 // get a list of childNodes
20144 const childNodes = [];
20145 for (let i = 0; i < childRef.length; i++) {
20146 childNodes.push(this.body.nodes[childRef[i]]);
20147 }
20148
20149 // use the positions to order the nodes.
20150 this.direction.sort(childNodes);
20151
20152 // position the childNodes
20153 for (let i = 0; i < childNodes.length; i++) {
20154 const childNode = childNodes[i];
20155 const childNodeLevel = this.hierarchical.levels[childNode.id];
20156 // check if the child node is below the parent node and if it has already been positioned.
20157 if (
20158 childNodeLevel > parentLevel &&
20159 this.positionedNodes[childNode.id] === undefined
20160 ) {
20161 // get the amount of space required for this node. If parent the width is based on the amount of children.
20162 const spacing = this.options.hierarchical.nodeSpacing;
20163 let pos;
20164
20165 // we get the X or Y values we need and store them in pos and previousPos.
20166 // The get and set make sure we get X or Y
20167 if (i === 0) {
20168 pos = this.direction.getPosition(this.body.nodes[parentId]);
20169 } else {
20170 pos = this.direction.getPosition(childNodes[i - 1]) + spacing;
20171 }
20172 this.direction.setPosition(childNode, pos, childNodeLevel);
20173 this._validatePositionAndContinue(childNode, childNodeLevel, pos);
20174 } else {
20175 return;
20176 }
20177 }
20178
20179 // center the parent nodes.
20180 const center = this._getCenterPosition(childNodes);
20181 this.direction.setPosition(this.body.nodes[parentId], center, parentLevel);
20182 }
20183
20184 /**
20185 * This method checks for overlap and if required shifts the branch. It also keeps records of positioned nodes.
20186 * Finally it will call _placeBranchNodes to place the branch nodes.
20187 *
20188 * @param {Node} node
20189 * @param {number} level
20190 * @param {number} pos
20191 * @private
20192 */
20193 _validatePositionAndContinue(node, level, pos) {
20194 // This method only works for formal trees and formal forests
20195 // Early exit if this is not the case
20196 if (!this.hierarchical.isTree) return;
20197
20198 // if overlap has been detected, we shift the branch
20199 if (this.lastNodeOnLevel[level] !== undefined) {
20200 const previousPos = this.direction.getPosition(
20201 this.body.nodes[this.lastNodeOnLevel[level]]
20202 );
20203 if (pos - previousPos < this.options.hierarchical.nodeSpacing) {
20204 const diff = previousPos + this.options.hierarchical.nodeSpacing - pos;
20205 const sharedParent = this._findCommonParent(
20206 this.lastNodeOnLevel[level],
20207 node.id
20208 );
20209 this._shiftBlock(sharedParent.withChild, diff);
20210 }
20211 }
20212
20213 this.lastNodeOnLevel[level] = node.id; // store change in position.
20214 this.positionedNodes[node.id] = true;
20215 this._placeBranchNodes(node.id, level);
20216 }
20217
20218 /**
20219 * Receives an array with node indices and returns an array with the actual node references.
20220 * Used for sorting based on node properties.
20221 *
20222 * @param {Array.<Node.id>} idArray
20223 * @returns {Array.<Node>}
20224 */
20225 _indexArrayToNodes(idArray) {
20226 const array = [];
20227 for (let i = 0; i < idArray.length; i++) {
20228 array.push(this.body.nodes[idArray[i]]);
20229 }
20230 return array;
20231 }
20232
20233 /**
20234 * This function get the distribution of levels based on hubsize
20235 *
20236 * @returns {object}
20237 * @private
20238 */
20239 _getDistribution() {
20240 const distribution = {};
20241 let nodeId, node;
20242
20243 // we fix Y because the hierarchy is vertical,
20244 // we fix X so we do not give a node an x position for a second time.
20245 // the fix of X is removed after the x value has been set.
20246 for (nodeId in this.body.nodes) {
20247 if (Object.prototype.hasOwnProperty.call(this.body.nodes, nodeId)) {
20248 node = this.body.nodes[nodeId];
20249 const level =
20250 this.hierarchical.levels[nodeId] === undefined
20251 ? 0
20252 : this.hierarchical.levels[nodeId];
20253 this.direction.fix(node, level);
20254 if (distribution[level] === undefined) {
20255 distribution[level] = {};
20256 }
20257 distribution[level][nodeId] = node;
20258 }
20259 }
20260 return distribution;
20261 }
20262
20263 /**
20264 * Return the active (i.e. visible) edges for this node
20265 *
20266 * @param {Node} node
20267 * @returns {Array.<vis.Edge>} Array of edge instances
20268 * @private
20269 */
20270 _getActiveEdges(node) {
20271 const result = [];
20272
20273 forEach(node.edges, (edge) => {
20274 if (this.body.edgeIndices.indexOf(edge.id) !== -1) {
20275 result.push(edge);
20276 }
20277 });
20278
20279 return result;
20280 }
20281
20282 /**
20283 * Get the hubsizes for all active nodes.
20284 *
20285 * @returns {number}
20286 * @private
20287 */
20288 _getHubSizes() {
20289 const hubSizes = {};
20290 const nodeIds = this.body.nodeIndices;
20291
20292 forEach(nodeIds, (nodeId) => {
20293 const node = this.body.nodes[nodeId];
20294 const hubSize = this._getActiveEdges(node).length;
20295 hubSizes[hubSize] = true;
20296 });
20297
20298 // Make an array of the size sorted descending
20299 const result = [];
20300 forEach(hubSizes, (size) => {
20301 result.push(Number(size));
20302 });
20303
20304 TimSort.sort(result, function (a, b) {
20305 return b - a;
20306 });
20307
20308 return result;
20309 }
20310
20311 /**
20312 * this function allocates nodes in levels based on the recursive branching from the largest hubs.
20313 *
20314 * @private
20315 */
20316 _determineLevelsByHubsize() {
20317 const levelDownstream = (nodeA, nodeB) => {
20318 this.hierarchical.levelDownstream(nodeA, nodeB);
20319 };
20320
20321 const hubSizes = this._getHubSizes();
20322
20323 for (let i = 0; i < hubSizes.length; ++i) {
20324 const hubSize = hubSizes[i];
20325 if (hubSize === 0) break;
20326
20327 forEach(this.body.nodeIndices, (nodeId) => {
20328 const node = this.body.nodes[nodeId];
20329
20330 if (hubSize === this._getActiveEdges(node).length) {
20331 this._crawlNetwork(levelDownstream, nodeId);
20332 }
20333 });
20334 }
20335 }
20336
20337 /**
20338 * TODO: release feature
20339 * TODO: Determine if this feature is needed at all
20340 *
20341 * @private
20342 */
20343 _determineLevelsCustomCallback() {
20344 const minLevel = 100000;
20345
20346 // TODO: this should come from options.
20347 // eslint-disable-next-line no-unused-vars -- This should eventually be implemented with these parameters used.
20348 const customCallback = function (nodeA, nodeB, edge) {};
20349
20350 // TODO: perhaps move to HierarchicalStatus.
20351 // But I currently don't see the point, this method is not used.
20352 const levelByDirection = (nodeA, nodeB, edge) => {
20353 let levelA = this.hierarchical.levels[nodeA.id];
20354 // set initial level
20355 if (levelA === undefined) {
20356 levelA = this.hierarchical.levels[nodeA.id] = minLevel;
20357 }
20358
20359 const diff = customCallback(
20360 NetworkUtil.cloneOptions(nodeA, "node"),
20361 NetworkUtil.cloneOptions(nodeB, "node"),
20362 NetworkUtil.cloneOptions(edge, "edge")
20363 );
20364
20365 this.hierarchical.levels[nodeB.id] = levelA + diff;
20366 };
20367
20368 this._crawlNetwork(levelByDirection);
20369 this.hierarchical.setMinLevelToZero(this.body.nodes);
20370 }
20371
20372 /**
20373 * Allocate nodes in levels based on the direction of the edges.
20374 *
20375 * @private
20376 */
20377 _determineLevelsDirected() {
20378 const nodes = this.body.nodeIndices.reduce((acc, id) => {
20379 acc.set(id, this.body.nodes[id]);
20380 return acc;
20381 }, new Map());
20382
20383 if (this.options.hierarchical.shakeTowards === "roots") {
20384 this.hierarchical.levels = fillLevelsByDirectionRoots(nodes);
20385 } else {
20386 this.hierarchical.levels = fillLevelsByDirectionLeaves(nodes);
20387 }
20388
20389 this.hierarchical.setMinLevelToZero(this.body.nodes);
20390 }
20391
20392 /**
20393 * Update the bookkeeping of parent and child.
20394 *
20395 * @private
20396 */
20397 _generateMap() {
20398 const fillInRelations = (parentNode, childNode) => {
20399 if (
20400 this.hierarchical.levels[childNode.id] >
20401 this.hierarchical.levels[parentNode.id]
20402 ) {
20403 this.hierarchical.addRelation(parentNode.id, childNode.id);
20404 }
20405 };
20406
20407 this._crawlNetwork(fillInRelations);
20408 this.hierarchical.checkIfTree();
20409 }
20410
20411 /**
20412 * Crawl over the entire network and use a callback on each node couple that is connected to each other.
20413 *
20414 * @param {Function} [callback=function(){}] | will receive nodeA, nodeB and the connecting edge. A and B are distinct.
20415 * @param {Node.id} startingNodeId
20416 * @private
20417 */
20418 _crawlNetwork(callback = function () {}, startingNodeId) {
20419 const progress = {};
20420
20421 const crawler = (node, tree) => {
20422 if (progress[node.id] === undefined) {
20423 this.hierarchical.setTreeIndex(node, tree);
20424
20425 progress[node.id] = true;
20426 let childNode;
20427 const edges = this._getActiveEdges(node);
20428 for (let i = 0; i < edges.length; i++) {
20429 const edge = edges[i];
20430 if (edge.connected === true) {
20431 if (edge.toId == node.id) {
20432 // Not '===' because id's can be string and numeric
20433 childNode = edge.from;
20434 } else {
20435 childNode = edge.to;
20436 }
20437
20438 if (node.id != childNode.id) {
20439 // Not '!==' because id's can be string and numeric
20440 callback(node, childNode, edge);
20441 crawler(childNode, tree);
20442 }
20443 }
20444 }
20445 }
20446 };
20447
20448 if (startingNodeId === undefined) {
20449 // Crawl over all nodes
20450 let treeIndex = 0; // Serves to pass a unique id for the current distinct tree
20451
20452 for (let i = 0; i < this.body.nodeIndices.length; i++) {
20453 const nodeId = this.body.nodeIndices[i];
20454
20455 if (progress[nodeId] === undefined) {
20456 const node = this.body.nodes[nodeId];
20457 crawler(node, treeIndex);
20458 treeIndex += 1;
20459 }
20460 }
20461 } else {
20462 // Crawl from the given starting node
20463 const node = this.body.nodes[startingNodeId];
20464 if (node === undefined) {
20465 console.error("Node not found:", startingNodeId);
20466 return;
20467 }
20468 crawler(node);
20469 }
20470 }
20471
20472 /**
20473 * Shift a branch a certain distance
20474 *
20475 * @param {Node.id} parentId
20476 * @param {number} diff
20477 * @private
20478 */
20479 _shiftBlock(parentId, diff) {
20480 const progress = {};
20481 const shifter = (parentId) => {
20482 if (progress[parentId]) {
20483 return;
20484 }
20485 progress[parentId] = true;
20486 this.direction.shift(parentId, diff);
20487
20488 const childRef = this.hierarchical.childrenReference[parentId];
20489 if (childRef !== undefined) {
20490 for (let i = 0; i < childRef.length; i++) {
20491 shifter(childRef[i]);
20492 }
20493 }
20494 };
20495 shifter(parentId);
20496 }
20497
20498 /**
20499 * Find a common parent between branches.
20500 *
20501 * @param {Node.id} childA
20502 * @param {Node.id} childB
20503 * @returns {{foundParent, withChild}}
20504 * @private
20505 */
20506 _findCommonParent(childA, childB) {
20507 const parents = {};
20508 const iterateParents = (parents, child) => {
20509 const parentRef = this.hierarchical.parentReference[child];
20510 if (parentRef !== undefined) {
20511 for (let i = 0; i < parentRef.length; i++) {
20512 const parent = parentRef[i];
20513 parents[parent] = true;
20514 iterateParents(parents, parent);
20515 }
20516 }
20517 };
20518 const findParent = (parents, child) => {
20519 const parentRef = this.hierarchical.parentReference[child];
20520 if (parentRef !== undefined) {
20521 for (let i = 0; i < parentRef.length; i++) {
20522 const parent = parentRef[i];
20523 if (parents[parent] !== undefined) {
20524 return { foundParent: parent, withChild: child };
20525 }
20526 const branch = findParent(parents, parent);
20527 if (branch.foundParent !== null) {
20528 return branch;
20529 }
20530 }
20531 }
20532 return { foundParent: null, withChild: child };
20533 };
20534
20535 iterateParents(parents, childA);
20536 return findParent(parents, childB);
20537 }
20538
20539 /**
20540 * Set the strategy pattern for handling the coordinates given the current direction.
20541 *
20542 * The individual instances contain all the operations and data specific to a layout direction.
20543 *
20544 * @param {Node} node
20545 * @param {{x: number, y: number}} position
20546 * @param {number} level
20547 * @param {boolean} [doNotUpdate=false]
20548 * @private
20549 */
20550 setDirectionStrategy() {
20551 const isVertical =
20552 this.options.hierarchical.direction === "UD" ||
20553 this.options.hierarchical.direction === "DU";
20554
20555 if (isVertical) {
20556 this.direction = new VerticalStrategy(this);
20557 } else {
20558 this.direction = new HorizontalStrategy(this);
20559 }
20560 }
20561
20562 /**
20563 * Determine the center position of a branch from the passed list of child nodes
20564 *
20565 * This takes into account the positions of all the child nodes.
20566 *
20567 * @param {Array.<Node|vis.Node.id>} childNodes Array of either child nodes or node id's
20568 * @returns {number}
20569 * @private
20570 */
20571 _getCenterPosition(childNodes) {
20572 let minPos = 1e9;
20573 let maxPos = -1e9;
20574
20575 for (let i = 0; i < childNodes.length; i++) {
20576 let childNode;
20577 if (childNodes[i].id !== undefined) {
20578 childNode = childNodes[i];
20579 } else {
20580 const childNodeId = childNodes[i];
20581 childNode = this.body.nodes[childNodeId];
20582 }
20583
20584 const position = this.direction.getPosition(childNode);
20585 minPos = Math.min(minPos, position);
20586 maxPos = Math.max(maxPos, position);
20587 }
20588
20589 return 0.5 * (minPos + maxPos);
20590 }
20591}
20592
20593/**
20594 * Clears the toolbar div element of children
20595 *
20596 * @private
20597 */
20598class ManipulationSystem {
20599 /**
20600 * @param {object} body
20601 * @param {Canvas} canvas
20602 * @param {SelectionHandler} selectionHandler
20603 * @param {InteractionHandler} interactionHandler
20604 */
20605 constructor(body, canvas, selectionHandler, interactionHandler) {
20606 this.body = body;
20607 this.canvas = canvas;
20608 this.selectionHandler = selectionHandler;
20609 this.interactionHandler = interactionHandler;
20610
20611 this.editMode = false;
20612 this.manipulationDiv = undefined;
20613 this.editModeDiv = undefined;
20614 this.closeDiv = undefined;
20615
20616 this._domEventListenerCleanupQueue = [];
20617 this.temporaryUIFunctions = {};
20618 this.temporaryEventFunctions = [];
20619
20620 this.touchTime = 0;
20621 this.temporaryIds = { nodes: [], edges: [] };
20622 this.guiEnabled = false;
20623 this.inMode = false;
20624 this.selectedControlNode = undefined;
20625
20626 this.options = {};
20627 this.defaultOptions = {
20628 enabled: false,
20629 initiallyActive: false,
20630 addNode: true,
20631 addEdge: true,
20632 editNode: undefined,
20633 editEdge: true,
20634 deleteNode: true,
20635 deleteEdge: true,
20636 controlNodeStyle: {
20637 shape: "dot",
20638 size: 6,
20639 color: {
20640 background: "#ff0000",
20641 border: "#3c3c3c",
20642 highlight: { background: "#07f968", border: "#3c3c3c" },
20643 },
20644 borderWidth: 2,
20645 borderWidthSelected: 2,
20646 },
20647 };
20648 Object.assign(this.options, this.defaultOptions);
20649
20650 this.body.emitter.on("destroy", () => {
20651 this._clean();
20652 });
20653 this.body.emitter.on("_dataChanged", this._restore.bind(this));
20654 this.body.emitter.on("_resetData", this._restore.bind(this));
20655 }
20656
20657 /**
20658 * If something changes in the data during editing, switch back to the initial datamanipulation state and close all edit modes.
20659 *
20660 * @private
20661 */
20662 _restore() {
20663 if (this.inMode !== false) {
20664 if (this.options.initiallyActive === true) {
20665 this.enableEditMode();
20666 } else {
20667 this.disableEditMode();
20668 }
20669 }
20670 }
20671
20672 /**
20673 * Set the Options
20674 *
20675 * @param {object} options
20676 * @param {object} allOptions
20677 * @param {object} globalOptions
20678 */
20679 setOptions(options, allOptions, globalOptions) {
20680 if (allOptions !== undefined) {
20681 if (allOptions.locale !== undefined) {
20682 this.options.locale = allOptions.locale;
20683 } else {
20684 this.options.locale = globalOptions.locale;
20685 }
20686 if (allOptions.locales !== undefined) {
20687 this.options.locales = allOptions.locales;
20688 } else {
20689 this.options.locales = globalOptions.locales;
20690 }
20691 }
20692
20693 if (options !== undefined) {
20694 if (typeof options === "boolean") {
20695 this.options.enabled = options;
20696 } else {
20697 this.options.enabled = true;
20698 deepExtend(this.options, options);
20699 }
20700 if (this.options.initiallyActive === true) {
20701 this.editMode = true;
20702 }
20703 this._setup();
20704 }
20705 }
20706
20707 /**
20708 * Enable or disable edit-mode. Draws the DOM required and cleans up after itself.
20709 *
20710 * @private
20711 */
20712 toggleEditMode() {
20713 if (this.editMode === true) {
20714 this.disableEditMode();
20715 } else {
20716 this.enableEditMode();
20717 }
20718 }
20719
20720 /**
20721 * Enables Edit Mode
20722 */
20723 enableEditMode() {
20724 this.editMode = true;
20725
20726 this._clean();
20727 if (this.guiEnabled === true) {
20728 this.manipulationDiv.style.display = "block";
20729 this.closeDiv.style.display = "block";
20730 this.editModeDiv.style.display = "none";
20731 this.showManipulatorToolbar();
20732 }
20733 }
20734
20735 /**
20736 * Disables Edit Mode
20737 */
20738 disableEditMode() {
20739 this.editMode = false;
20740
20741 this._clean();
20742 if (this.guiEnabled === true) {
20743 this.manipulationDiv.style.display = "none";
20744 this.closeDiv.style.display = "none";
20745 this.editModeDiv.style.display = "block";
20746 this._createEditButton();
20747 }
20748 }
20749
20750 /**
20751 * Creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar.
20752 *
20753 * @private
20754 */
20755 showManipulatorToolbar() {
20756 // restore the state of any bound functions or events, remove control nodes, restore physics
20757 this._clean();
20758
20759 // reset global variables
20760 this.manipulationDOM = {};
20761
20762 // if the gui is enabled, draw all elements.
20763 if (this.guiEnabled === true) {
20764 // a _restore will hide these menus
20765 this.editMode = true;
20766 this.manipulationDiv.style.display = "block";
20767 this.closeDiv.style.display = "block";
20768
20769 const selectedNodeCount = this.selectionHandler.getSelectedNodeCount();
20770 const selectedEdgeCount = this.selectionHandler.getSelectedEdgeCount();
20771 const selectedTotalCount = selectedNodeCount + selectedEdgeCount;
20772 const locale = this.options.locales[this.options.locale];
20773 let needSeperator = false;
20774
20775 if (this.options.addNode !== false) {
20776 this._createAddNodeButton(locale);
20777 needSeperator = true;
20778 }
20779 if (this.options.addEdge !== false) {
20780 if (needSeperator === true) {
20781 this._createSeperator(1);
20782 } else {
20783 needSeperator = true;
20784 }
20785 this._createAddEdgeButton(locale);
20786 }
20787
20788 if (
20789 selectedNodeCount === 1 &&
20790 typeof this.options.editNode === "function"
20791 ) {
20792 if (needSeperator === true) {
20793 this._createSeperator(2);
20794 } else {
20795 needSeperator = true;
20796 }
20797 this._createEditNodeButton(locale);
20798 } else if (
20799 selectedEdgeCount === 1 &&
20800 selectedNodeCount === 0 &&
20801 this.options.editEdge !== false
20802 ) {
20803 if (needSeperator === true) {
20804 this._createSeperator(3);
20805 } else {
20806 needSeperator = true;
20807 }
20808 this._createEditEdgeButton(locale);
20809 }
20810
20811 // remove buttons
20812 if (selectedTotalCount !== 0) {
20813 if (selectedNodeCount > 0 && this.options.deleteNode !== false) {
20814 if (needSeperator === true) {
20815 this._createSeperator(4);
20816 }
20817 this._createDeleteButton(locale);
20818 } else if (
20819 selectedNodeCount === 0 &&
20820 this.options.deleteEdge !== false
20821 ) {
20822 if (needSeperator === true) {
20823 this._createSeperator(4);
20824 }
20825 this._createDeleteButton(locale);
20826 }
20827 }
20828
20829 // bind the close button
20830 this._bindElementEvents(this.closeDiv, this.toggleEditMode.bind(this));
20831
20832 // refresh this bar based on what has been selected
20833 this._temporaryBindEvent(
20834 "select",
20835 this.showManipulatorToolbar.bind(this)
20836 );
20837 }
20838
20839 // redraw to show any possible changes
20840 this.body.emitter.emit("_redraw");
20841 }
20842
20843 /**
20844 * Create the toolbar for adding Nodes
20845 */
20846 addNodeMode() {
20847 // when using the gui, enable edit mode if it wasnt already.
20848 if (this.editMode !== true) {
20849 this.enableEditMode();
20850 }
20851
20852 // restore the state of any bound functions or events, remove control nodes, restore physics
20853 this._clean();
20854
20855 this.inMode = "addNode";
20856 if (this.guiEnabled === true) {
20857 const locale = this.options.locales[this.options.locale];
20858 this.manipulationDOM = {};
20859 this._createBackButton(locale);
20860 this._createSeperator();
20861 this._createDescription(
20862 locale["addDescription"] || this.options.locales["en"]["addDescription"]
20863 );
20864
20865 // bind the close button
20866 this._bindElementEvents(this.closeDiv, this.toggleEditMode.bind(this));
20867 }
20868
20869 this._temporaryBindEvent("click", this._performAddNode.bind(this));
20870 }
20871
20872 /**
20873 * call the bound function to handle the editing of the node. The node has to be selected.
20874 */
20875 editNode() {
20876 // when using the gui, enable edit mode if it wasnt already.
20877 if (this.editMode !== true) {
20878 this.enableEditMode();
20879 }
20880
20881 // restore the state of any bound functions or events, remove control nodes, restore physics
20882 this._clean();
20883 const node = this.selectionHandler.getSelectedNodes()[0];
20884 if (node !== undefined) {
20885 this.inMode = "editNode";
20886 if (typeof this.options.editNode === "function") {
20887 if (node.isCluster !== true) {
20888 const data = deepExtend({}, node.options, false);
20889 data.x = node.x;
20890 data.y = node.y;
20891
20892 if (this.options.editNode.length === 2) {
20893 this.options.editNode(data, (finalizedData) => {
20894 if (
20895 finalizedData !== null &&
20896 finalizedData !== undefined &&
20897 this.inMode === "editNode"
20898 ) {
20899 // if for whatever reason the mode has changes (due to dataset change) disregard the callback) {
20900 this.body.data.nodes.getDataSet().update(finalizedData);
20901 }
20902 this.showManipulatorToolbar();
20903 });
20904 } else {
20905 throw new Error(
20906 "The function for edit does not support two arguments (data, callback)"
20907 );
20908 }
20909 } else {
20910 alert(
20911 this.options.locales[this.options.locale]["editClusterError"] ||
20912 this.options.locales["en"]["editClusterError"]
20913 );
20914 }
20915 } else {
20916 throw new Error(
20917 "No function has been configured to handle the editing of nodes."
20918 );
20919 }
20920 } else {
20921 this.showManipulatorToolbar();
20922 }
20923 }
20924
20925 /**
20926 * create the toolbar to connect nodes
20927 */
20928 addEdgeMode() {
20929 // when using the gui, enable edit mode if it wasnt already.
20930 if (this.editMode !== true) {
20931 this.enableEditMode();
20932 }
20933
20934 // restore the state of any bound functions or events, remove control nodes, restore physics
20935 this._clean();
20936
20937 this.inMode = "addEdge";
20938 if (this.guiEnabled === true) {
20939 const locale = this.options.locales[this.options.locale];
20940 this.manipulationDOM = {};
20941 this._createBackButton(locale);
20942 this._createSeperator();
20943 this._createDescription(
20944 locale["edgeDescription"] ||
20945 this.options.locales["en"]["edgeDescription"]
20946 );
20947
20948 // bind the close button
20949 this._bindElementEvents(this.closeDiv, this.toggleEditMode.bind(this));
20950 }
20951
20952 // temporarily overload functions
20953 this._temporaryBindUI("onTouch", this._handleConnect.bind(this));
20954 this._temporaryBindUI("onDragEnd", this._finishConnect.bind(this));
20955 this._temporaryBindUI("onDrag", this._dragControlNode.bind(this));
20956 this._temporaryBindUI("onRelease", this._finishConnect.bind(this));
20957 this._temporaryBindUI("onDragStart", this._dragStartEdge.bind(this));
20958 this._temporaryBindUI("onHold", () => {});
20959 }
20960
20961 /**
20962 * create the toolbar to edit edges
20963 */
20964 editEdgeMode() {
20965 // when using the gui, enable edit mode if it wasn't already.
20966 if (this.editMode !== true) {
20967 this.enableEditMode();
20968 }
20969
20970 // restore the state of any bound functions or events, remove control nodes, restore physics
20971 this._clean();
20972
20973 this.inMode = "editEdge";
20974 if (
20975 typeof this.options.editEdge === "object" &&
20976 typeof this.options.editEdge.editWithoutDrag === "function"
20977 ) {
20978 this.edgeBeingEditedId = this.selectionHandler.getSelectedEdgeIds()[0];
20979 if (this.edgeBeingEditedId !== undefined) {
20980 const edge = this.body.edges[this.edgeBeingEditedId];
20981 this._performEditEdge(edge.from.id, edge.to.id);
20982 return;
20983 }
20984 }
20985 if (this.guiEnabled === true) {
20986 const locale = this.options.locales[this.options.locale];
20987 this.manipulationDOM = {};
20988 this._createBackButton(locale);
20989 this._createSeperator();
20990 this._createDescription(
20991 locale["editEdgeDescription"] ||
20992 this.options.locales["en"]["editEdgeDescription"]
20993 );
20994
20995 // bind the close button
20996 this._bindElementEvents(this.closeDiv, this.toggleEditMode.bind(this));
20997 }
20998
20999 this.edgeBeingEditedId = this.selectionHandler.getSelectedEdgeIds()[0];
21000 if (this.edgeBeingEditedId !== undefined) {
21001 const edge = this.body.edges[this.edgeBeingEditedId];
21002
21003 // create control nodes
21004 const controlNodeFrom = this._getNewTargetNode(edge.from.x, edge.from.y);
21005 const controlNodeTo = this._getNewTargetNode(edge.to.x, edge.to.y);
21006
21007 this.temporaryIds.nodes.push(controlNodeFrom.id);
21008 this.temporaryIds.nodes.push(controlNodeTo.id);
21009
21010 this.body.nodes[controlNodeFrom.id] = controlNodeFrom;
21011 this.body.nodeIndices.push(controlNodeFrom.id);
21012 this.body.nodes[controlNodeTo.id] = controlNodeTo;
21013 this.body.nodeIndices.push(controlNodeTo.id);
21014
21015 // temporarily overload UI functions, cleaned up automatically because of _temporaryBindUI
21016 this._temporaryBindUI("onTouch", this._controlNodeTouch.bind(this)); // used to get the position
21017 this._temporaryBindUI("onTap", () => {}); // disabled
21018 this._temporaryBindUI("onHold", () => {}); // disabled
21019 this._temporaryBindUI(
21020 "onDragStart",
21021 this._controlNodeDragStart.bind(this)
21022 ); // used to select control node
21023 this._temporaryBindUI("onDrag", this._controlNodeDrag.bind(this)); // used to drag control node
21024 this._temporaryBindUI("onDragEnd", this._controlNodeDragEnd.bind(this)); // used to connect or revert control nodes
21025 this._temporaryBindUI("onMouseMove", () => {}); // disabled
21026
21027 // create function to position control nodes correctly on movement
21028 // automatically cleaned up because we use the temporary bind
21029 this._temporaryBindEvent("beforeDrawing", (ctx) => {
21030 const positions = edge.edgeType.findBorderPositions(ctx);
21031 if (controlNodeFrom.selected === false) {
21032 controlNodeFrom.x = positions.from.x;
21033 controlNodeFrom.y = positions.from.y;
21034 }
21035 if (controlNodeTo.selected === false) {
21036 controlNodeTo.x = positions.to.x;
21037 controlNodeTo.y = positions.to.y;
21038 }
21039 });
21040
21041 this.body.emitter.emit("_redraw");
21042 } else {
21043 this.showManipulatorToolbar();
21044 }
21045 }
21046
21047 /**
21048 * delete everything in the selection
21049 */
21050 deleteSelected() {
21051 // when using the gui, enable edit mode if it wasnt already.
21052 if (this.editMode !== true) {
21053 this.enableEditMode();
21054 }
21055
21056 // restore the state of any bound functions or events, remove control nodes, restore physics
21057 this._clean();
21058
21059 this.inMode = "delete";
21060 const selectedNodes = this.selectionHandler.getSelectedNodeIds();
21061 const selectedEdges = this.selectionHandler.getSelectedEdgeIds();
21062 let deleteFunction = undefined;
21063 if (selectedNodes.length > 0) {
21064 for (let i = 0; i < selectedNodes.length; i++) {
21065 if (this.body.nodes[selectedNodes[i]].isCluster === true) {
21066 alert(
21067 this.options.locales[this.options.locale]["deleteClusterError"] ||
21068 this.options.locales["en"]["deleteClusterError"]
21069 );
21070 return;
21071 }
21072 }
21073
21074 if (typeof this.options.deleteNode === "function") {
21075 deleteFunction = this.options.deleteNode;
21076 }
21077 } else if (selectedEdges.length > 0) {
21078 if (typeof this.options.deleteEdge === "function") {
21079 deleteFunction = this.options.deleteEdge;
21080 }
21081 }
21082
21083 if (typeof deleteFunction === "function") {
21084 const data = { nodes: selectedNodes, edges: selectedEdges };
21085 if (deleteFunction.length === 2) {
21086 deleteFunction(data, (finalizedData) => {
21087 if (
21088 finalizedData !== null &&
21089 finalizedData !== undefined &&
21090 this.inMode === "delete"
21091 ) {
21092 // if for whatever reason the mode has changes (due to dataset change) disregard the callback) {
21093 this.body.data.edges.getDataSet().remove(finalizedData.edges);
21094 this.body.data.nodes.getDataSet().remove(finalizedData.nodes);
21095 this.body.emitter.emit("startSimulation");
21096 this.showManipulatorToolbar();
21097 } else {
21098 this.body.emitter.emit("startSimulation");
21099 this.showManipulatorToolbar();
21100 }
21101 });
21102 } else {
21103 throw new Error(
21104 "The function for delete does not support two arguments (data, callback)"
21105 );
21106 }
21107 } else {
21108 this.body.data.edges.getDataSet().remove(selectedEdges);
21109 this.body.data.nodes.getDataSet().remove(selectedNodes);
21110 this.body.emitter.emit("startSimulation");
21111 this.showManipulatorToolbar();
21112 }
21113 }
21114
21115 //********************************************** PRIVATE ***************************************//
21116
21117 /**
21118 * draw or remove the DOM
21119 *
21120 * @private
21121 */
21122 _setup() {
21123 if (this.options.enabled === true) {
21124 // Enable the GUI
21125 this.guiEnabled = true;
21126
21127 this._createWrappers();
21128 if (this.editMode === false) {
21129 this._createEditButton();
21130 } else {
21131 this.showManipulatorToolbar();
21132 }
21133 } else {
21134 this._removeManipulationDOM();
21135
21136 // disable the gui
21137 this.guiEnabled = false;
21138 }
21139 }
21140
21141 /**
21142 * create the div overlays that contain the DOM
21143 *
21144 * @private
21145 */
21146 _createWrappers() {
21147 // load the manipulator HTML elements. All styling done in css.
21148 if (this.manipulationDiv === undefined) {
21149 this.manipulationDiv = document.createElement("div");
21150 this.manipulationDiv.className = "vis-manipulation";
21151 if (this.editMode === true) {
21152 this.manipulationDiv.style.display = "block";
21153 } else {
21154 this.manipulationDiv.style.display = "none";
21155 }
21156 this.canvas.frame.appendChild(this.manipulationDiv);
21157 }
21158
21159 // container for the edit button.
21160 if (this.editModeDiv === undefined) {
21161 this.editModeDiv = document.createElement("div");
21162 this.editModeDiv.className = "vis-edit-mode";
21163 if (this.editMode === true) {
21164 this.editModeDiv.style.display = "none";
21165 } else {
21166 this.editModeDiv.style.display = "block";
21167 }
21168 this.canvas.frame.appendChild(this.editModeDiv);
21169 }
21170
21171 // container for the close div button
21172 if (this.closeDiv === undefined) {
21173 this.closeDiv = document.createElement("button");
21174 this.closeDiv.className = "vis-close";
21175 this.closeDiv.setAttribute(
21176 "aria-label",
21177 this.options.locales[this.options.locale]?.["close"] ??
21178 this.options.locales["en"]["close"]
21179 );
21180 this.closeDiv.style.display = this.manipulationDiv.style.display;
21181 this.canvas.frame.appendChild(this.closeDiv);
21182 }
21183 }
21184
21185 /**
21186 * generate a new target node. Used for creating new edges and editing edges
21187 *
21188 * @param {number} x
21189 * @param {number} y
21190 * @returns {Node}
21191 * @private
21192 */
21193 _getNewTargetNode(x, y) {
21194 const controlNodeStyle = deepExtend({}, this.options.controlNodeStyle);
21195
21196 controlNodeStyle.id = "targetNode" + v4();
21197 controlNodeStyle.hidden = false;
21198 controlNodeStyle.physics = false;
21199 controlNodeStyle.x = x;
21200 controlNodeStyle.y = y;
21201
21202 // we have to define the bounding box in order for the nodes to be drawn immediately
21203 const node = this.body.functions.createNode(controlNodeStyle);
21204 node.shape.boundingBox = { left: x, right: x, top: y, bottom: y };
21205
21206 return node;
21207 }
21208
21209 /**
21210 * Create the edit button
21211 */
21212 _createEditButton() {
21213 // restore everything to it's original state (if applicable)
21214 this._clean();
21215
21216 // reset the manipulationDOM
21217 this.manipulationDOM = {};
21218
21219 // empty the editModeDiv
21220 recursiveDOMDelete(this.editModeDiv);
21221
21222 // create the contents for the editMode button
21223 const locale = this.options.locales[this.options.locale];
21224 const button = this._createButton(
21225 "editMode",
21226 "vis-edit vis-edit-mode",
21227 locale["edit"] || this.options.locales["en"]["edit"]
21228 );
21229 this.editModeDiv.appendChild(button);
21230
21231 // bind a hammer listener to the button, calling the function toggleEditMode.
21232 this._bindElementEvents(button, this.toggleEditMode.bind(this));
21233 }
21234
21235 /**
21236 * this function cleans up after everything this module does. Temporary elements, functions and events are removed, physics restored, hammers removed.
21237 *
21238 * @private
21239 */
21240 _clean() {
21241 // not in mode
21242 this.inMode = false;
21243
21244 // _clean the divs
21245 if (this.guiEnabled === true) {
21246 recursiveDOMDelete(this.editModeDiv);
21247 recursiveDOMDelete(this.manipulationDiv);
21248
21249 // removes all the bindings and overloads
21250 this._cleanupDOMEventListeners();
21251 }
21252
21253 // remove temporary nodes and edges
21254 this._cleanupTemporaryNodesAndEdges();
21255
21256 // restore overloaded UI functions
21257 this._unbindTemporaryUIs();
21258
21259 // remove the temporaryEventFunctions
21260 this._unbindTemporaryEvents();
21261
21262 // restore the physics if required
21263 this.body.emitter.emit("restorePhysics");
21264 }
21265
21266 /**
21267 * Each dom element has it's own hammer. They are stored in this.manipulationHammers. This cleans them up.
21268 *
21269 * @private
21270 */
21271 _cleanupDOMEventListeners() {
21272 // _clean DOM event listener bindings
21273 for (const callback of this._domEventListenerCleanupQueue.splice(0)) {
21274 callback();
21275 }
21276 }
21277
21278 /**
21279 * Remove all DOM elements created by this module.
21280 *
21281 * @private
21282 */
21283 _removeManipulationDOM() {
21284 // removes all the bindings and overloads
21285 this._clean();
21286
21287 // empty the manipulation divs
21288 recursiveDOMDelete(this.manipulationDiv);
21289 recursiveDOMDelete(this.editModeDiv);
21290 recursiveDOMDelete(this.closeDiv);
21291
21292 // remove the manipulation divs
21293 if (this.manipulationDiv) {
21294 this.canvas.frame.removeChild(this.manipulationDiv);
21295 }
21296 if (this.editModeDiv) {
21297 this.canvas.frame.removeChild(this.editModeDiv);
21298 }
21299 if (this.closeDiv) {
21300 this.canvas.frame.removeChild(this.closeDiv);
21301 }
21302
21303 // set the references to undefined
21304 this.manipulationDiv = undefined;
21305 this.editModeDiv = undefined;
21306 this.closeDiv = undefined;
21307 }
21308
21309 /**
21310 * create a seperator line. the index is to differentiate in the manipulation dom
21311 *
21312 * @param {number} [index=1]
21313 * @private
21314 */
21315 _createSeperator(index = 1) {
21316 this.manipulationDOM["seperatorLineDiv" + index] =
21317 document.createElement("div");
21318 this.manipulationDOM["seperatorLineDiv" + index].className =
21319 "vis-separator-line";
21320 this.manipulationDiv.appendChild(
21321 this.manipulationDOM["seperatorLineDiv" + index]
21322 );
21323 }
21324
21325 // ---------------------- DOM functions for buttons --------------------------//
21326
21327 /**
21328 *
21329 * @param {Locale} locale
21330 * @private
21331 */
21332 _createAddNodeButton(locale) {
21333 const button = this._createButton(
21334 "addNode",
21335 "vis-add",
21336 locale["addNode"] || this.options.locales["en"]["addNode"]
21337 );
21338 this.manipulationDiv.appendChild(button);
21339 this._bindElementEvents(button, this.addNodeMode.bind(this));
21340 }
21341
21342 /**
21343 *
21344 * @param {Locale} locale
21345 * @private
21346 */
21347 _createAddEdgeButton(locale) {
21348 const button = this._createButton(
21349 "addEdge",
21350 "vis-connect",
21351 locale["addEdge"] || this.options.locales["en"]["addEdge"]
21352 );
21353 this.manipulationDiv.appendChild(button);
21354 this._bindElementEvents(button, this.addEdgeMode.bind(this));
21355 }
21356
21357 /**
21358 *
21359 * @param {Locale} locale
21360 * @private
21361 */
21362 _createEditNodeButton(locale) {
21363 const button = this._createButton(
21364 "editNode",
21365 "vis-edit",
21366 locale["editNode"] || this.options.locales["en"]["editNode"]
21367 );
21368 this.manipulationDiv.appendChild(button);
21369 this._bindElementEvents(button, this.editNode.bind(this));
21370 }
21371
21372 /**
21373 *
21374 * @param {Locale} locale
21375 * @private
21376 */
21377 _createEditEdgeButton(locale) {
21378 const button = this._createButton(
21379 "editEdge",
21380 "vis-edit",
21381 locale["editEdge"] || this.options.locales["en"]["editEdge"]
21382 );
21383 this.manipulationDiv.appendChild(button);
21384 this._bindElementEvents(button, this.editEdgeMode.bind(this));
21385 }
21386
21387 /**
21388 *
21389 * @param {Locale} locale
21390 * @private
21391 */
21392 _createDeleteButton(locale) {
21393 let deleteBtnClass;
21394 if (this.options.rtl) {
21395 deleteBtnClass = "vis-delete-rtl";
21396 } else {
21397 deleteBtnClass = "vis-delete";
21398 }
21399 const button = this._createButton(
21400 "delete",
21401 deleteBtnClass,
21402 locale["del"] || this.options.locales["en"]["del"]
21403 );
21404 this.manipulationDiv.appendChild(button);
21405 this._bindElementEvents(button, this.deleteSelected.bind(this));
21406 }
21407
21408 /**
21409 *
21410 * @param {Locale} locale
21411 * @private
21412 */
21413 _createBackButton(locale) {
21414 const button = this._createButton(
21415 "back",
21416 "vis-back",
21417 locale["back"] || this.options.locales["en"]["back"]
21418 );
21419 this.manipulationDiv.appendChild(button);
21420 this._bindElementEvents(button, this.showManipulatorToolbar.bind(this));
21421 }
21422
21423 /**
21424 *
21425 * @param {number|string} id
21426 * @param {string} className
21427 * @param {label} label
21428 * @param {string} labelClassName
21429 * @returns {HTMLElement}
21430 * @private
21431 */
21432 _createButton(id, className, label, labelClassName = "vis-label") {
21433 this.manipulationDOM[id + "Div"] = document.createElement("button");
21434 this.manipulationDOM[id + "Div"].className = "vis-button " + className;
21435 this.manipulationDOM[id + "Label"] = document.createElement("div");
21436 this.manipulationDOM[id + "Label"].className = labelClassName;
21437 this.manipulationDOM[id + "Label"].innerText = label;
21438 this.manipulationDOM[id + "Div"].appendChild(
21439 this.manipulationDOM[id + "Label"]
21440 );
21441 return this.manipulationDOM[id + "Div"];
21442 }
21443
21444 /**
21445 *
21446 * @param {Label} label
21447 * @private
21448 */
21449 _createDescription(label) {
21450 this.manipulationDOM["descriptionLabel"] = document.createElement("div");
21451 this.manipulationDOM["descriptionLabel"].className = "vis-none";
21452 this.manipulationDOM["descriptionLabel"].innerText = label;
21453 this.manipulationDiv.appendChild(this.manipulationDOM["descriptionLabel"]);
21454 }
21455
21456 // -------------------------- End of DOM functions for buttons ------------------------------//
21457
21458 /**
21459 * this binds an event until cleanup by the clean functions.
21460 *
21461 * @param {Event} event The event
21462 * @param {Function} newFunction
21463 * @private
21464 */
21465 _temporaryBindEvent(event, newFunction) {
21466 this.temporaryEventFunctions.push({
21467 event: event,
21468 boundFunction: newFunction,
21469 });
21470 this.body.emitter.on(event, newFunction);
21471 }
21472
21473 /**
21474 * this overrides an UI function until cleanup by the clean function
21475 *
21476 * @param {string} UIfunctionName
21477 * @param {Function} newFunction
21478 * @private
21479 */
21480 _temporaryBindUI(UIfunctionName, newFunction) {
21481 if (this.body.eventListeners[UIfunctionName] !== undefined) {
21482 this.temporaryUIFunctions[UIfunctionName] =
21483 this.body.eventListeners[UIfunctionName];
21484 this.body.eventListeners[UIfunctionName] = newFunction;
21485 } else {
21486 throw new Error(
21487 "This UI function does not exist. Typo? You tried: " +
21488 UIfunctionName +
21489 " possible are: " +
21490 JSON.stringify(Object.keys(this.body.eventListeners))
21491 );
21492 }
21493 }
21494
21495 /**
21496 * Restore the overridden UI functions to their original state.
21497 *
21498 * @private
21499 */
21500 _unbindTemporaryUIs() {
21501 for (const functionName in this.temporaryUIFunctions) {
21502 if (
21503 Object.prototype.hasOwnProperty.call(
21504 this.temporaryUIFunctions,
21505 functionName
21506 )
21507 ) {
21508 this.body.eventListeners[functionName] =
21509 this.temporaryUIFunctions[functionName];
21510 delete this.temporaryUIFunctions[functionName];
21511 }
21512 }
21513 this.temporaryUIFunctions = {};
21514 }
21515
21516 /**
21517 * Unbind the events created by _temporaryBindEvent
21518 *
21519 * @private
21520 */
21521 _unbindTemporaryEvents() {
21522 for (let i = 0; i < this.temporaryEventFunctions.length; i++) {
21523 const eventName = this.temporaryEventFunctions[i].event;
21524 const boundFunction = this.temporaryEventFunctions[i].boundFunction;
21525 this.body.emitter.off(eventName, boundFunction);
21526 }
21527 this.temporaryEventFunctions = [];
21528 }
21529
21530 /**
21531 * Bind an hammer instance to a DOM element.
21532 *
21533 * @param {Element} domElement
21534 * @param {Function} boundFunction
21535 */
21536 _bindElementEvents(domElement, boundFunction) {
21537 // Bind touch events.
21538 const hammer = new Hammer(domElement, {});
21539 onTouch(hammer, boundFunction);
21540 this._domEventListenerCleanupQueue.push(() => {
21541 hammer.destroy();
21542 });
21543
21544 // Bind keyboard events.
21545 const keyupListener = ({ keyCode, key }) => {
21546 if (key === "Enter" || key === " " || keyCode === 13 || keyCode === 32) {
21547 boundFunction();
21548 }
21549 };
21550 domElement.addEventListener("keyup", keyupListener, false);
21551 this._domEventListenerCleanupQueue.push(() => {
21552 domElement.removeEventListener("keyup", keyupListener, false);
21553 });
21554 }
21555
21556 /**
21557 * Neatly clean up temporary edges and nodes
21558 *
21559 * @private
21560 */
21561 _cleanupTemporaryNodesAndEdges() {
21562 // _clean temporary edges
21563 for (let i = 0; i < this.temporaryIds.edges.length; i++) {
21564 this.body.edges[this.temporaryIds.edges[i]].disconnect();
21565 delete this.body.edges[this.temporaryIds.edges[i]];
21566 const indexTempEdge = this.body.edgeIndices.indexOf(
21567 this.temporaryIds.edges[i]
21568 );
21569 if (indexTempEdge !== -1) {
21570 this.body.edgeIndices.splice(indexTempEdge, 1);
21571 }
21572 }
21573
21574 // _clean temporary nodes
21575 for (let i = 0; i < this.temporaryIds.nodes.length; i++) {
21576 delete this.body.nodes[this.temporaryIds.nodes[i]];
21577 const indexTempNode = this.body.nodeIndices.indexOf(
21578 this.temporaryIds.nodes[i]
21579 );
21580 if (indexTempNode !== -1) {
21581 this.body.nodeIndices.splice(indexTempNode, 1);
21582 }
21583 }
21584
21585 this.temporaryIds = { nodes: [], edges: [] };
21586 }
21587
21588 // ------------------------------------------ EDIT EDGE FUNCTIONS -----------------------------------------//
21589
21590 /**
21591 * the touch is used to get the position of the initial click
21592 *
21593 * @param {Event} event The event
21594 * @private
21595 */
21596 _controlNodeTouch(event) {
21597 this.selectionHandler.unselectAll();
21598 this.lastTouch = this.body.functions.getPointer(event.center);
21599 this.lastTouch.translation = Object.assign({}, this.body.view.translation); // copy the object
21600 }
21601
21602 /**
21603 * the drag start is used to mark one of the control nodes as selected.
21604 *
21605 * @private
21606 */
21607 _controlNodeDragStart() {
21608 const pointer = this.lastTouch;
21609 const pointerObj = this.selectionHandler._pointerToPositionObject(pointer);
21610 const from = this.body.nodes[this.temporaryIds.nodes[0]];
21611 const to = this.body.nodes[this.temporaryIds.nodes[1]];
21612 const edge = this.body.edges[this.edgeBeingEditedId];
21613 this.selectedControlNode = undefined;
21614
21615 const fromSelect = from.isOverlappingWith(pointerObj);
21616 const toSelect = to.isOverlappingWith(pointerObj);
21617
21618 if (fromSelect === true) {
21619 this.selectedControlNode = from;
21620 edge.edgeType.from = from;
21621 } else if (toSelect === true) {
21622 this.selectedControlNode = to;
21623 edge.edgeType.to = to;
21624 }
21625
21626 // we use the selection to find the node that is being dragged. We explicitly select it here.
21627 if (this.selectedControlNode !== undefined) {
21628 this.selectionHandler.selectObject(this.selectedControlNode);
21629 }
21630
21631 this.body.emitter.emit("_redraw");
21632 }
21633
21634 /**
21635 * dragging the control nodes or the canvas
21636 *
21637 * @param {Event} event The event
21638 * @private
21639 */
21640 _controlNodeDrag(event) {
21641 this.body.emitter.emit("disablePhysics");
21642 const pointer = this.body.functions.getPointer(event.center);
21643 const pos = this.canvas.DOMtoCanvas(pointer);
21644 if (this.selectedControlNode !== undefined) {
21645 this.selectedControlNode.x = pos.x;
21646 this.selectedControlNode.y = pos.y;
21647 } else {
21648 this.interactionHandler.onDrag(event);
21649 }
21650 this.body.emitter.emit("_redraw");
21651 }
21652
21653 /**
21654 * connecting or restoring the control nodes.
21655 *
21656 * @param {Event} event The event
21657 * @private
21658 */
21659 _controlNodeDragEnd(event) {
21660 const pointer = this.body.functions.getPointer(event.center);
21661 const pointerObj = this.selectionHandler._pointerToPositionObject(pointer);
21662 const edge = this.body.edges[this.edgeBeingEditedId];
21663 // if the node that was dragged is not a control node, return
21664 if (this.selectedControlNode === undefined) {
21665 return;
21666 }
21667
21668 // we use the selection to find the node that is being dragged. We explicitly DEselect the control node here.
21669 this.selectionHandler.unselectAll();
21670 const overlappingNodeIds =
21671 this.selectionHandler._getAllNodesOverlappingWith(pointerObj);
21672 let node = undefined;
21673 for (let i = overlappingNodeIds.length - 1; i >= 0; i--) {
21674 if (overlappingNodeIds[i] !== this.selectedControlNode.id) {
21675 node = this.body.nodes[overlappingNodeIds[i]];
21676 break;
21677 }
21678 }
21679 // perform the connection
21680 if (node !== undefined && this.selectedControlNode !== undefined) {
21681 if (node.isCluster === true) {
21682 alert(
21683 this.options.locales[this.options.locale]["createEdgeError"] ||
21684 this.options.locales["en"]["createEdgeError"]
21685 );
21686 } else {
21687 const from = this.body.nodes[this.temporaryIds.nodes[0]];
21688 if (this.selectedControlNode.id === from.id) {
21689 this._performEditEdge(node.id, edge.to.id);
21690 } else {
21691 this._performEditEdge(edge.from.id, node.id);
21692 }
21693 }
21694 } else {
21695 edge.updateEdgeType();
21696 this.body.emitter.emit("restorePhysics");
21697 }
21698
21699 this.body.emitter.emit("_redraw");
21700 }
21701
21702 // ------------------------------------ END OF EDIT EDGE FUNCTIONS -----------------------------------------//
21703
21704 // ------------------------------------------- ADD EDGE FUNCTIONS -----------------------------------------//
21705 /**
21706 * the function bound to the selection event. It checks if you want to connect a cluster and changes the description
21707 * to walk the user through the process.
21708 *
21709 * @param {Event} event
21710 * @private
21711 */
21712 _handleConnect(event) {
21713 // check to avoid double fireing of this function.
21714 if (new Date().valueOf() - this.touchTime > 100) {
21715 this.lastTouch = this.body.functions.getPointer(event.center);
21716 this.lastTouch.translation = Object.assign(
21717 {},
21718 this.body.view.translation
21719 ); // copy the object
21720
21721 this.interactionHandler.drag.pointer = this.lastTouch; // Drag pointer is not updated when adding edges
21722 this.interactionHandler.drag.translation = this.lastTouch.translation;
21723
21724 const pointer = this.lastTouch;
21725 const node = this.selectionHandler.getNodeAt(pointer);
21726
21727 if (node !== undefined) {
21728 if (node.isCluster === true) {
21729 alert(
21730 this.options.locales[this.options.locale]["createEdgeError"] ||
21731 this.options.locales["en"]["createEdgeError"]
21732 );
21733 } else {
21734 // create a node the temporary line can look at
21735 const targetNode = this._getNewTargetNode(node.x, node.y);
21736 this.body.nodes[targetNode.id] = targetNode;
21737 this.body.nodeIndices.push(targetNode.id);
21738
21739 // create a temporary edge
21740 const connectionEdge = this.body.functions.createEdge({
21741 id: "connectionEdge" + v4(),
21742 from: node.id,
21743 to: targetNode.id,
21744 physics: false,
21745 smooth: {
21746 enabled: true,
21747 type: "continuous",
21748 roundness: 0.5,
21749 },
21750 });
21751 this.body.edges[connectionEdge.id] = connectionEdge;
21752 this.body.edgeIndices.push(connectionEdge.id);
21753
21754 this.temporaryIds.nodes.push(targetNode.id);
21755 this.temporaryIds.edges.push(connectionEdge.id);
21756 }
21757 }
21758 this.touchTime = new Date().valueOf();
21759 }
21760 }
21761
21762 /**
21763 *
21764 * @param {Event} event
21765 * @private
21766 */
21767 _dragControlNode(event) {
21768 const pointer = this.body.functions.getPointer(event.center);
21769
21770 const pointerObj = this.selectionHandler._pointerToPositionObject(pointer);
21771 // remember the edge id
21772 let connectFromId = undefined;
21773 if (this.temporaryIds.edges[0] !== undefined) {
21774 connectFromId = this.body.edges[this.temporaryIds.edges[0]].fromId;
21775 }
21776
21777 // get the overlapping node but NOT the temporary node;
21778 const overlappingNodeIds =
21779 this.selectionHandler._getAllNodesOverlappingWith(pointerObj);
21780 let node = undefined;
21781 for (let i = overlappingNodeIds.length - 1; i >= 0; i--) {
21782 // if the node id is NOT a temporary node, accept the node.
21783 if (this.temporaryIds.nodes.indexOf(overlappingNodeIds[i]) === -1) {
21784 node = this.body.nodes[overlappingNodeIds[i]];
21785 break;
21786 }
21787 }
21788
21789 event.controlEdge = { from: connectFromId, to: node ? node.id : undefined };
21790 this.selectionHandler.generateClickEvent(
21791 "controlNodeDragging",
21792 event,
21793 pointer
21794 );
21795
21796 if (this.temporaryIds.nodes[0] !== undefined) {
21797 const targetNode = this.body.nodes[this.temporaryIds.nodes[0]]; // there is only one temp node in the add edge mode.
21798 targetNode.x = this.canvas._XconvertDOMtoCanvas(pointer.x);
21799 targetNode.y = this.canvas._YconvertDOMtoCanvas(pointer.y);
21800 this.body.emitter.emit("_redraw");
21801 } else {
21802 this.interactionHandler.onDrag(event);
21803 }
21804 }
21805
21806 /**
21807 * Connect the new edge to the target if one exists, otherwise remove temp line
21808 *
21809 * @param {Event} event The event
21810 * @private
21811 */
21812 _finishConnect(event) {
21813 const pointer = this.body.functions.getPointer(event.center);
21814 const pointerObj = this.selectionHandler._pointerToPositionObject(pointer);
21815
21816 // remember the edge id
21817 let connectFromId = undefined;
21818 if (this.temporaryIds.edges[0] !== undefined) {
21819 connectFromId = this.body.edges[this.temporaryIds.edges[0]].fromId;
21820 }
21821
21822 // get the overlapping node but NOT the temporary node;
21823 const overlappingNodeIds =
21824 this.selectionHandler._getAllNodesOverlappingWith(pointerObj);
21825 let node = undefined;
21826 for (let i = overlappingNodeIds.length - 1; i >= 0; i--) {
21827 // if the node id is NOT a temporary node, accept the node.
21828 if (this.temporaryIds.nodes.indexOf(overlappingNodeIds[i]) === -1) {
21829 node = this.body.nodes[overlappingNodeIds[i]];
21830 break;
21831 }
21832 }
21833
21834 // clean temporary nodes and edges.
21835 this._cleanupTemporaryNodesAndEdges();
21836
21837 // perform the connection
21838 if (node !== undefined) {
21839 if (node.isCluster === true) {
21840 alert(
21841 this.options.locales[this.options.locale]["createEdgeError"] ||
21842 this.options.locales["en"]["createEdgeError"]
21843 );
21844 } else {
21845 if (
21846 this.body.nodes[connectFromId] !== undefined &&
21847 this.body.nodes[node.id] !== undefined
21848 ) {
21849 this._performAddEdge(connectFromId, node.id);
21850 }
21851 }
21852 }
21853
21854 event.controlEdge = { from: connectFromId, to: node ? node.id : undefined };
21855 this.selectionHandler.generateClickEvent(
21856 "controlNodeDragEnd",
21857 event,
21858 pointer
21859 );
21860
21861 // No need to do _generateclickevent('dragEnd') here, the regular dragEnd event fires.
21862 this.body.emitter.emit("_redraw");
21863 }
21864
21865 /**
21866 *
21867 * @param {Event} event
21868 * @private
21869 */
21870 _dragStartEdge(event) {
21871 const pointer = this.lastTouch;
21872 this.selectionHandler.generateClickEvent(
21873 "dragStart",
21874 event,
21875 pointer,
21876 undefined,
21877 true
21878 );
21879 }
21880
21881 // --------------------------------------- END OF ADD EDGE FUNCTIONS -------------------------------------//
21882
21883 // ------------------------------ Performing all the actual data manipulation ------------------------//
21884
21885 /**
21886 * Adds a node on the specified location
21887 *
21888 * @param {object} clickData
21889 * @private
21890 */
21891 _performAddNode(clickData) {
21892 const defaultData = {
21893 id: v4(),
21894 x: clickData.pointer.canvas.x,
21895 y: clickData.pointer.canvas.y,
21896 label: "new",
21897 };
21898
21899 if (typeof this.options.addNode === "function") {
21900 if (this.options.addNode.length === 2) {
21901 this.options.addNode(defaultData, (finalizedData) => {
21902 if (
21903 finalizedData !== null &&
21904 finalizedData !== undefined &&
21905 this.inMode === "addNode"
21906 ) {
21907 // if for whatever reason the mode has changes (due to dataset change) disregard the callback
21908 this.body.data.nodes.getDataSet().add(finalizedData);
21909 }
21910 this.showManipulatorToolbar();
21911 });
21912 } else {
21913 this.showManipulatorToolbar();
21914 throw new Error(
21915 "The function for add does not support two arguments (data,callback)"
21916 );
21917 }
21918 } else {
21919 this.body.data.nodes.getDataSet().add(defaultData);
21920 this.showManipulatorToolbar();
21921 }
21922 }
21923
21924 /**
21925 * connect two nodes with a new edge.
21926 *
21927 * @param {Node.id} sourceNodeId
21928 * @param {Node.id} targetNodeId
21929 * @private
21930 */
21931 _performAddEdge(sourceNodeId, targetNodeId) {
21932 const defaultData = { from: sourceNodeId, to: targetNodeId };
21933 if (typeof this.options.addEdge === "function") {
21934 if (this.options.addEdge.length === 2) {
21935 this.options.addEdge(defaultData, (finalizedData) => {
21936 if (
21937 finalizedData !== null &&
21938 finalizedData !== undefined &&
21939 this.inMode === "addEdge"
21940 ) {
21941 // if for whatever reason the mode has changes (due to dataset change) disregard the callback
21942 this.body.data.edges.getDataSet().add(finalizedData);
21943 this.selectionHandler.unselectAll();
21944 this.showManipulatorToolbar();
21945 }
21946 });
21947 } else {
21948 throw new Error(
21949 "The function for connect does not support two arguments (data,callback)"
21950 );
21951 }
21952 } else {
21953 this.body.data.edges.getDataSet().add(defaultData);
21954 this.selectionHandler.unselectAll();
21955 this.showManipulatorToolbar();
21956 }
21957 }
21958
21959 /**
21960 * connect two nodes with a new edge.
21961 *
21962 * @param {Node.id} sourceNodeId
21963 * @param {Node.id} targetNodeId
21964 * @private
21965 */
21966 _performEditEdge(sourceNodeId, targetNodeId) {
21967 const defaultData = {
21968 id: this.edgeBeingEditedId,
21969 from: sourceNodeId,
21970 to: targetNodeId,
21971 label: this.body.data.edges.get(this.edgeBeingEditedId).label,
21972 };
21973 let eeFunct = this.options.editEdge;
21974 if (typeof eeFunct === "object") {
21975 eeFunct = eeFunct.editWithoutDrag;
21976 }
21977 if (typeof eeFunct === "function") {
21978 if (eeFunct.length === 2) {
21979 eeFunct(defaultData, (finalizedData) => {
21980 if (
21981 finalizedData === null ||
21982 finalizedData === undefined ||
21983 this.inMode !== "editEdge"
21984 ) {
21985 // if for whatever reason the mode has changes (due to dataset change) disregard the callback) {
21986 this.body.edges[defaultData.id].updateEdgeType();
21987 this.body.emitter.emit("_redraw");
21988 this.showManipulatorToolbar();
21989 } else {
21990 this.body.data.edges.getDataSet().update(finalizedData);
21991 this.selectionHandler.unselectAll();
21992 this.showManipulatorToolbar();
21993 }
21994 });
21995 } else {
21996 throw new Error(
21997 "The function for edit does not support two arguments (data, callback)"
21998 );
21999 }
22000 } else {
22001 this.body.data.edges.getDataSet().update(defaultData);
22002 this.selectionHandler.unselectAll();
22003 this.showManipulatorToolbar();
22004 }
22005 }
22006}
22007
22008/**
22009 * This object contains all possible options. It will check if the types are correct, if required if the option is one
22010 * of the allowed values.
22011 *
22012 * __any__ means that the name of the property does not matter.
22013 * __type__ is a required field for all objects and contains the allowed types of all objects
22014 */
22015const string = "string";
22016const bool = "boolean";
22017const number = "number";
22018const array = "array";
22019const object = "object"; // should only be in a __type__ property
22020const dom = "dom";
22021const any = "any";
22022// List of endpoints
22023const endPoints = [
22024 "arrow",
22025 "bar",
22026 "box",
22027 "circle",
22028 "crow",
22029 "curve",
22030 "diamond",
22031 "image",
22032 "inv_curve",
22033 "inv_triangle",
22034 "triangle",
22035 "vee",
22036];
22037/* eslint-disable @typescript-eslint/naming-convention -- The __*__ format is used to prevent collisions with actual option names. */
22038const nodeOptions = {
22039 borderWidth: { number },
22040 borderWidthSelected: { number, undefined: "undefined" },
22041 brokenImage: { string, undefined: "undefined" },
22042 chosen: {
22043 label: { boolean: bool, function: "function" },
22044 node: { boolean: bool, function: "function" },
22045 __type__: { object, boolean: bool },
22046 },
22047 color: {
22048 border: { string },
22049 background: { string },
22050 highlight: {
22051 border: { string },
22052 background: { string },
22053 __type__: { object, string },
22054 },
22055 hover: {
22056 border: { string },
22057 background: { string },
22058 __type__: { object, string },
22059 },
22060 __type__: { object, string },
22061 },
22062 opacity: { number, undefined: "undefined" },
22063 fixed: {
22064 x: { boolean: bool },
22065 y: { boolean: bool },
22066 __type__: { object, boolean: bool },
22067 },
22068 font: {
22069 align: { string },
22070 color: { string },
22071 size: { number },
22072 face: { string },
22073 background: { string },
22074 strokeWidth: { number },
22075 strokeColor: { string },
22076 vadjust: { number },
22077 multi: { boolean: bool, string },
22078 bold: {
22079 color: { string },
22080 size: { number },
22081 face: { string },
22082 mod: { string },
22083 vadjust: { number },
22084 __type__: { object, string },
22085 },
22086 boldital: {
22087 color: { string },
22088 size: { number },
22089 face: { string },
22090 mod: { string },
22091 vadjust: { number },
22092 __type__: { object, string },
22093 },
22094 ital: {
22095 color: { string },
22096 size: { number },
22097 face: { string },
22098 mod: { string },
22099 vadjust: { number },
22100 __type__: { object, string },
22101 },
22102 mono: {
22103 color: { string },
22104 size: { number },
22105 face: { string },
22106 mod: { string },
22107 vadjust: { number },
22108 __type__: { object, string },
22109 },
22110 __type__: { object, string },
22111 },
22112 group: { string, number, undefined: "undefined" },
22113 heightConstraint: {
22114 minimum: { number },
22115 valign: { string },
22116 __type__: { object, boolean: bool, number },
22117 },
22118 hidden: { boolean: bool },
22119 icon: {
22120 face: { string },
22121 code: { string },
22122 size: { number },
22123 color: { string },
22124 weight: { string, number },
22125 __type__: { object },
22126 },
22127 id: { string, number },
22128 image: {
22129 selected: { string, undefined: "undefined" },
22130 unselected: { string, undefined: "undefined" },
22131 __type__: { object, string },
22132 },
22133 imagePadding: {
22134 top: { number },
22135 right: { number },
22136 bottom: { number },
22137 left: { number },
22138 __type__: { object, number },
22139 },
22140 label: { string, undefined: "undefined" },
22141 labelHighlightBold: { boolean: bool },
22142 level: { number, undefined: "undefined" },
22143 margin: {
22144 top: { number },
22145 right: { number },
22146 bottom: { number },
22147 left: { number },
22148 __type__: { object, number },
22149 },
22150 mass: { number },
22151 physics: { boolean: bool },
22152 scaling: {
22153 min: { number },
22154 max: { number },
22155 label: {
22156 enabled: { boolean: bool },
22157 min: { number },
22158 max: { number },
22159 maxVisible: { number },
22160 drawThreshold: { number },
22161 __type__: { object, boolean: bool },
22162 },
22163 customScalingFunction: { function: "function" },
22164 __type__: { object },
22165 },
22166 shadow: {
22167 enabled: { boolean: bool },
22168 color: { string },
22169 size: { number },
22170 x: { number },
22171 y: { number },
22172 __type__: { object, boolean: bool },
22173 },
22174 shape: {
22175 string: [
22176 "custom",
22177 "ellipse",
22178 "circle",
22179 "database",
22180 "box",
22181 "text",
22182 "image",
22183 "circularImage",
22184 "diamond",
22185 "dot",
22186 "star",
22187 "triangle",
22188 "triangleDown",
22189 "square",
22190 "icon",
22191 "hexagon",
22192 ],
22193 },
22194 ctxRenderer: { function: "function" },
22195 shapeProperties: {
22196 borderDashes: { boolean: bool, array },
22197 borderRadius: { number },
22198 interpolation: { boolean: bool },
22199 useImageSize: { boolean: bool },
22200 useBorderWithImage: { boolean: bool },
22201 coordinateOrigin: { string: ["center", "top-left"] },
22202 __type__: { object },
22203 },
22204 size: { number },
22205 title: { string, dom, undefined: "undefined" },
22206 value: { number, undefined: "undefined" },
22207 widthConstraint: {
22208 minimum: { number },
22209 maximum: { number },
22210 __type__: { object, boolean: bool, number },
22211 },
22212 x: { number },
22213 y: { number },
22214 __type__: { object },
22215};
22216const allOptions = {
22217 configure: {
22218 enabled: { boolean: bool },
22219 filter: { boolean: bool, string, array, function: "function" },
22220 container: { dom },
22221 showButton: { boolean: bool },
22222 __type__: { object, boolean: bool, string, array, function: "function" },
22223 },
22224 edges: {
22225 arrows: {
22226 to: {
22227 enabled: { boolean: bool },
22228 scaleFactor: { number },
22229 type: { string: endPoints },
22230 imageHeight: { number },
22231 imageWidth: { number },
22232 src: { string },
22233 __type__: { object, boolean: bool },
22234 },
22235 middle: {
22236 enabled: { boolean: bool },
22237 scaleFactor: { number },
22238 type: { string: endPoints },
22239 imageWidth: { number },
22240 imageHeight: { number },
22241 src: { string },
22242 __type__: { object, boolean: bool },
22243 },
22244 from: {
22245 enabled: { boolean: bool },
22246 scaleFactor: { number },
22247 type: { string: endPoints },
22248 imageWidth: { number },
22249 imageHeight: { number },
22250 src: { string },
22251 __type__: { object, boolean: bool },
22252 },
22253 __type__: { string: ["from", "to", "middle"], object },
22254 },
22255 endPointOffset: {
22256 from: {
22257 number: number,
22258 },
22259 to: {
22260 number: number,
22261 },
22262 __type__: {
22263 object: object,
22264 number: number,
22265 },
22266 },
22267 arrowStrikethrough: { boolean: bool },
22268 background: {
22269 enabled: { boolean: bool },
22270 color: { string },
22271 size: { number },
22272 dashes: { boolean: bool, array },
22273 __type__: { object, boolean: bool },
22274 },
22275 chosen: {
22276 label: { boolean: bool, function: "function" },
22277 edge: { boolean: bool, function: "function" },
22278 __type__: { object, boolean: bool },
22279 },
22280 color: {
22281 color: { string },
22282 highlight: { string },
22283 hover: { string },
22284 inherit: { string: ["from", "to", "both"], boolean: bool },
22285 opacity: { number },
22286 __type__: { object, string },
22287 },
22288 dashes: { boolean: bool, array },
22289 font: {
22290 color: { string },
22291 size: { number },
22292 face: { string },
22293 background: { string },
22294 strokeWidth: { number },
22295 strokeColor: { string },
22296 align: { string: ["horizontal", "top", "middle", "bottom"] },
22297 vadjust: { number },
22298 multi: { boolean: bool, string },
22299 bold: {
22300 color: { string },
22301 size: { number },
22302 face: { string },
22303 mod: { string },
22304 vadjust: { number },
22305 __type__: { object, string },
22306 },
22307 boldital: {
22308 color: { string },
22309 size: { number },
22310 face: { string },
22311 mod: { string },
22312 vadjust: { number },
22313 __type__: { object, string },
22314 },
22315 ital: {
22316 color: { string },
22317 size: { number },
22318 face: { string },
22319 mod: { string },
22320 vadjust: { number },
22321 __type__: { object, string },
22322 },
22323 mono: {
22324 color: { string },
22325 size: { number },
22326 face: { string },
22327 mod: { string },
22328 vadjust: { number },
22329 __type__: { object, string },
22330 },
22331 __type__: { object, string },
22332 },
22333 hidden: { boolean: bool },
22334 hoverWidth: { function: "function", number },
22335 label: { string, undefined: "undefined" },
22336 labelHighlightBold: { boolean: bool },
22337 length: { number, undefined: "undefined" },
22338 physics: { boolean: bool },
22339 scaling: {
22340 min: { number },
22341 max: { number },
22342 label: {
22343 enabled: { boolean: bool },
22344 min: { number },
22345 max: { number },
22346 maxVisible: { number },
22347 drawThreshold: { number },
22348 __type__: { object, boolean: bool },
22349 },
22350 customScalingFunction: { function: "function" },
22351 __type__: { object },
22352 },
22353 selectionWidth: { function: "function", number },
22354 selfReferenceSize: { number },
22355 selfReference: {
22356 size: { number },
22357 angle: { number },
22358 renderBehindTheNode: { boolean: bool },
22359 __type__: { object },
22360 },
22361 shadow: {
22362 enabled: { boolean: bool },
22363 color: { string },
22364 size: { number },
22365 x: { number },
22366 y: { number },
22367 __type__: { object, boolean: bool },
22368 },
22369 smooth: {
22370 enabled: { boolean: bool },
22371 type: {
22372 string: [
22373 "dynamic",
22374 "continuous",
22375 "discrete",
22376 "diagonalCross",
22377 "straightCross",
22378 "horizontal",
22379 "vertical",
22380 "curvedCW",
22381 "curvedCCW",
22382 "cubicBezier",
22383 ],
22384 },
22385 roundness: { number },
22386 forceDirection: {
22387 string: ["horizontal", "vertical", "none"],
22388 boolean: bool,
22389 },
22390 __type__: { object, boolean: bool },
22391 },
22392 title: { string, undefined: "undefined" },
22393 width: { number },
22394 widthConstraint: {
22395 maximum: { number },
22396 __type__: { object, boolean: bool, number },
22397 },
22398 value: { number, undefined: "undefined" },
22399 __type__: { object },
22400 },
22401 groups: {
22402 useDefaultGroups: { boolean: bool },
22403 __any__: nodeOptions,
22404 __type__: { object },
22405 },
22406 interaction: {
22407 dragNodes: { boolean: bool },
22408 dragView: { boolean: bool },
22409 hideEdgesOnDrag: { boolean: bool },
22410 hideEdgesOnZoom: { boolean: bool },
22411 hideNodesOnDrag: { boolean: bool },
22412 hover: { boolean: bool },
22413 keyboard: {
22414 enabled: { boolean: bool },
22415 speed: {
22416 x: { number },
22417 y: { number },
22418 zoom: { number },
22419 __type__: { object },
22420 },
22421 bindToWindow: { boolean: bool },
22422 autoFocus: { boolean: bool },
22423 __type__: { object, boolean: bool },
22424 },
22425 multiselect: { boolean: bool },
22426 navigationButtons: { boolean: bool },
22427 selectable: { boolean: bool },
22428 selectConnectedEdges: { boolean: bool },
22429 hoverConnectedEdges: { boolean: bool },
22430 tooltipDelay: { number },
22431 zoomView: { boolean: bool },
22432 zoomSpeed: { number },
22433 __type__: { object },
22434 },
22435 layout: {
22436 randomSeed: { undefined: "undefined", number, string },
22437 improvedLayout: { boolean: bool },
22438 clusterThreshold: { number },
22439 hierarchical: {
22440 enabled: { boolean: bool },
22441 levelSeparation: { number },
22442 nodeSpacing: { number },
22443 treeSpacing: { number },
22444 blockShifting: { boolean: bool },
22445 edgeMinimization: { boolean: bool },
22446 parentCentralization: { boolean: bool },
22447 direction: { string: ["UD", "DU", "LR", "RL"] },
22448 sortMethod: { string: ["hubsize", "directed"] },
22449 shakeTowards: { string: ["leaves", "roots"] },
22450 __type__: { object, boolean: bool },
22451 },
22452 __type__: { object },
22453 },
22454 manipulation: {
22455 enabled: { boolean: bool },
22456 initiallyActive: { boolean: bool },
22457 addNode: { boolean: bool, function: "function" },
22458 addEdge: { boolean: bool, function: "function" },
22459 editNode: { function: "function" },
22460 editEdge: {
22461 editWithoutDrag: { function: "function" },
22462 __type__: { object, boolean: bool, function: "function" },
22463 },
22464 deleteNode: { boolean: bool, function: "function" },
22465 deleteEdge: { boolean: bool, function: "function" },
22466 controlNodeStyle: nodeOptions,
22467 __type__: { object, boolean: bool },
22468 },
22469 nodes: nodeOptions,
22470 physics: {
22471 enabled: { boolean: bool },
22472 barnesHut: {
22473 theta: { number },
22474 gravitationalConstant: { number },
22475 centralGravity: { number },
22476 springLength: { number },
22477 springConstant: { number },
22478 damping: { number },
22479 avoidOverlap: { number },
22480 __type__: { object },
22481 },
22482 forceAtlas2Based: {
22483 theta: { number },
22484 gravitationalConstant: { number },
22485 centralGravity: { number },
22486 springLength: { number },
22487 springConstant: { number },
22488 damping: { number },
22489 avoidOverlap: { number },
22490 __type__: { object },
22491 },
22492 repulsion: {
22493 centralGravity: { number },
22494 springLength: { number },
22495 springConstant: { number },
22496 nodeDistance: { number },
22497 damping: { number },
22498 __type__: { object },
22499 },
22500 hierarchicalRepulsion: {
22501 centralGravity: { number },
22502 springLength: { number },
22503 springConstant: { number },
22504 nodeDistance: { number },
22505 damping: { number },
22506 avoidOverlap: { number },
22507 __type__: { object },
22508 },
22509 maxVelocity: { number },
22510 minVelocity: { number },
22511 solver: {
22512 string: [
22513 "barnesHut",
22514 "repulsion",
22515 "hierarchicalRepulsion",
22516 "forceAtlas2Based",
22517 ],
22518 },
22519 stabilization: {
22520 enabled: { boolean: bool },
22521 iterations: { number },
22522 updateInterval: { number },
22523 onlyDynamicEdges: { boolean: bool },
22524 fit: { boolean: bool },
22525 __type__: { object, boolean: bool },
22526 },
22527 timestep: { number },
22528 adaptiveTimestep: { boolean: bool },
22529 wind: {
22530 x: { number },
22531 y: { number },
22532 __type__: { object },
22533 },
22534 __type__: { object, boolean: bool },
22535 },
22536 //globals :
22537 autoResize: { boolean: bool },
22538 clickToUse: { boolean: bool },
22539 locale: { string },
22540 locales: {
22541 __any__: { any },
22542 __type__: { object },
22543 },
22544 height: { string },
22545 width: { string },
22546 __type__: { object },
22547};
22548/* eslint-enable @typescript-eslint/naming-convention */
22549/**
22550 * This provides ranges, initial values, steps and dropdown menu choices for the
22551 * configuration.
22552 *
22553 * @remarks
22554 * Checkbox: `boolean`
22555 * The value supllied will be used as the initial value.
22556 *
22557 * Text field: `string`
22558 * The passed text will be used as the initial value. Any text will be
22559 * accepted afterwards.
22560 *
22561 * Number range: `[number, number, number, number]`
22562 * The meanings are `[initial value, min, max, step]`.
22563 *
22564 * Dropdown: `[Exclude<string, "color">, ...(string | number | boolean)[]]`
22565 * Translations for people with poor understanding of TypeScript: the first
22566 * value always has to be a string but never `"color"`, the rest can be any
22567 * combination of strings, numbers and booleans.
22568 *
22569 * Color picker: `["color", string]`
22570 * The first value says this will be a color picker not a dropdown menu. The
22571 * next value is the initial color.
22572 */
22573const configureOptions = {
22574 nodes: {
22575 borderWidth: [1, 0, 10, 1],
22576 borderWidthSelected: [2, 0, 10, 1],
22577 color: {
22578 border: ["color", "#2B7CE9"],
22579 background: ["color", "#97C2FC"],
22580 highlight: {
22581 border: ["color", "#2B7CE9"],
22582 background: ["color", "#D2E5FF"],
22583 },
22584 hover: {
22585 border: ["color", "#2B7CE9"],
22586 background: ["color", "#D2E5FF"],
22587 },
22588 },
22589 opacity: [0, 0, 1, 0.1],
22590 fixed: {
22591 x: false,
22592 y: false,
22593 },
22594 font: {
22595 color: ["color", "#343434"],
22596 size: [14, 0, 100, 1],
22597 face: ["arial", "verdana", "tahoma"],
22598 background: ["color", "none"],
22599 strokeWidth: [0, 0, 50, 1],
22600 strokeColor: ["color", "#ffffff"],
22601 },
22602 //group: 'string',
22603 hidden: false,
22604 labelHighlightBold: true,
22605 //icon: {
22606 // face: 'string', //'FontAwesome',
22607 // code: 'string', //'\uf007',
22608 // size: [50, 0, 200, 1], //50,
22609 // color: ['color','#2B7CE9'] //'#aa00ff'
22610 //},
22611 //image: 'string', // --> URL
22612 physics: true,
22613 scaling: {
22614 min: [10, 0, 200, 1],
22615 max: [30, 0, 200, 1],
22616 label: {
22617 enabled: false,
22618 min: [14, 0, 200, 1],
22619 max: [30, 0, 200, 1],
22620 maxVisible: [30, 0, 200, 1],
22621 drawThreshold: [5, 0, 20, 1],
22622 },
22623 },
22624 shadow: {
22625 enabled: false,
22626 color: "rgba(0,0,0,0.5)",
22627 size: [10, 0, 20, 1],
22628 x: [5, -30, 30, 1],
22629 y: [5, -30, 30, 1],
22630 },
22631 shape: [
22632 "ellipse",
22633 "box",
22634 "circle",
22635 "database",
22636 "diamond",
22637 "dot",
22638 "square",
22639 "star",
22640 "text",
22641 "triangle",
22642 "triangleDown",
22643 "hexagon",
22644 ],
22645 shapeProperties: {
22646 borderDashes: false,
22647 borderRadius: [6, 0, 20, 1],
22648 interpolation: true,
22649 useImageSize: false,
22650 },
22651 size: [25, 0, 200, 1],
22652 },
22653 edges: {
22654 arrows: {
22655 to: { enabled: false, scaleFactor: [1, 0, 3, 0.05], type: "arrow" },
22656 middle: { enabled: false, scaleFactor: [1, 0, 3, 0.05], type: "arrow" },
22657 from: { enabled: false, scaleFactor: [1, 0, 3, 0.05], type: "arrow" },
22658 },
22659 endPointOffset: {
22660 from: [0, -10, 10, 1],
22661 to: [0, -10, 10, 1],
22662 },
22663 arrowStrikethrough: true,
22664 color: {
22665 color: ["color", "#848484"],
22666 highlight: ["color", "#848484"],
22667 hover: ["color", "#848484"],
22668 inherit: ["from", "to", "both", true, false],
22669 opacity: [1, 0, 1, 0.05],
22670 },
22671 dashes: false,
22672 font: {
22673 color: ["color", "#343434"],
22674 size: [14, 0, 100, 1],
22675 face: ["arial", "verdana", "tahoma"],
22676 background: ["color", "none"],
22677 strokeWidth: [2, 0, 50, 1],
22678 strokeColor: ["color", "#ffffff"],
22679 align: ["horizontal", "top", "middle", "bottom"],
22680 },
22681 hidden: false,
22682 hoverWidth: [1.5, 0, 5, 0.1],
22683 labelHighlightBold: true,
22684 physics: true,
22685 scaling: {
22686 min: [1, 0, 100, 1],
22687 max: [15, 0, 100, 1],
22688 label: {
22689 enabled: true,
22690 min: [14, 0, 200, 1],
22691 max: [30, 0, 200, 1],
22692 maxVisible: [30, 0, 200, 1],
22693 drawThreshold: [5, 0, 20, 1],
22694 },
22695 },
22696 selectionWidth: [1.5, 0, 5, 0.1],
22697 selfReferenceSize: [20, 0, 200, 1],
22698 selfReference: {
22699 size: [20, 0, 200, 1],
22700 angle: [Math.PI / 2, -6 * Math.PI, 6 * Math.PI, Math.PI / 8],
22701 renderBehindTheNode: true,
22702 },
22703 shadow: {
22704 enabled: false,
22705 color: "rgba(0,0,0,0.5)",
22706 size: [10, 0, 20, 1],
22707 x: [5, -30, 30, 1],
22708 y: [5, -30, 30, 1],
22709 },
22710 smooth: {
22711 enabled: true,
22712 type: [
22713 "dynamic",
22714 "continuous",
22715 "discrete",
22716 "diagonalCross",
22717 "straightCross",
22718 "horizontal",
22719 "vertical",
22720 "curvedCW",
22721 "curvedCCW",
22722 "cubicBezier",
22723 ],
22724 forceDirection: ["horizontal", "vertical", "none"],
22725 roundness: [0.5, 0, 1, 0.05],
22726 },
22727 width: [1, 0, 30, 1],
22728 },
22729 layout: {
22730 //randomSeed: [0, 0, 500, 1],
22731 //improvedLayout: true,
22732 hierarchical: {
22733 enabled: false,
22734 levelSeparation: [150, 20, 500, 5],
22735 nodeSpacing: [100, 20, 500, 5],
22736 treeSpacing: [200, 20, 500, 5],
22737 blockShifting: true,
22738 edgeMinimization: true,
22739 parentCentralization: true,
22740 direction: ["UD", "DU", "LR", "RL"],
22741 sortMethod: ["hubsize", "directed"],
22742 shakeTowards: ["leaves", "roots"], // leaves, roots
22743 },
22744 },
22745 interaction: {
22746 dragNodes: true,
22747 dragView: true,
22748 hideEdgesOnDrag: false,
22749 hideEdgesOnZoom: false,
22750 hideNodesOnDrag: false,
22751 hover: false,
22752 keyboard: {
22753 enabled: false,
22754 speed: {
22755 x: [10, 0, 40, 1],
22756 y: [10, 0, 40, 1],
22757 zoom: [0.02, 0, 0.1, 0.005],
22758 },
22759 bindToWindow: true,
22760 autoFocus: true,
22761 },
22762 multiselect: false,
22763 navigationButtons: false,
22764 selectable: true,
22765 selectConnectedEdges: true,
22766 hoverConnectedEdges: true,
22767 tooltipDelay: [300, 0, 1000, 25],
22768 zoomView: true,
22769 zoomSpeed: [1, 0.1, 2, 0.1],
22770 },
22771 manipulation: {
22772 enabled: false,
22773 initiallyActive: false,
22774 },
22775 physics: {
22776 enabled: true,
22777 barnesHut: {
22778 theta: [0.5, 0.1, 1, 0.05],
22779 gravitationalConstant: [-2000, -30000, 0, 50],
22780 centralGravity: [0.3, 0, 10, 0.05],
22781 springLength: [95, 0, 500, 5],
22782 springConstant: [0.04, 0, 1.2, 0.005],
22783 damping: [0.09, 0, 1, 0.01],
22784 avoidOverlap: [0, 0, 1, 0.01],
22785 },
22786 forceAtlas2Based: {
22787 theta: [0.5, 0.1, 1, 0.05],
22788 gravitationalConstant: [-50, -500, 0, 1],
22789 centralGravity: [0.01, 0, 1, 0.005],
22790 springLength: [95, 0, 500, 5],
22791 springConstant: [0.08, 0, 1.2, 0.005],
22792 damping: [0.4, 0, 1, 0.01],
22793 avoidOverlap: [0, 0, 1, 0.01],
22794 },
22795 repulsion: {
22796 centralGravity: [0.2, 0, 10, 0.05],
22797 springLength: [200, 0, 500, 5],
22798 springConstant: [0.05, 0, 1.2, 0.005],
22799 nodeDistance: [100, 0, 500, 5],
22800 damping: [0.09, 0, 1, 0.01],
22801 },
22802 hierarchicalRepulsion: {
22803 centralGravity: [0.2, 0, 10, 0.05],
22804 springLength: [100, 0, 500, 5],
22805 springConstant: [0.01, 0, 1.2, 0.005],
22806 nodeDistance: [120, 0, 500, 5],
22807 damping: [0.09, 0, 1, 0.01],
22808 avoidOverlap: [0, 0, 1, 0.01],
22809 },
22810 maxVelocity: [50, 0, 150, 1],
22811 minVelocity: [0.1, 0.01, 0.5, 0.01],
22812 solver: [
22813 "barnesHut",
22814 "forceAtlas2Based",
22815 "repulsion",
22816 "hierarchicalRepulsion",
22817 ],
22818 timestep: [0.5, 0.01, 1, 0.01],
22819 wind: {
22820 x: [0, -10, 10, 0.1],
22821 y: [0, -10, 10, 0.1],
22822 },
22823 //adaptiveTimestep: true
22824 },
22825};
22826const configuratorHideOption = (parentPath, optionName, options) => {
22827 if (parentPath.includes("physics") &&
22828 configureOptions.physics.solver.includes(optionName) &&
22829 options.physics.solver !== optionName &&
22830 optionName !== "wind") {
22831 return true;
22832 }
22833 return false;
22834};
22835
22836var options = /*#__PURE__*/Object.freeze({
22837 __proto__: null,
22838 configuratorHideOption: configuratorHideOption,
22839 allOptions: allOptions,
22840 configureOptions: configureOptions
22841});
22842
22843/**
22844 * The Floyd–Warshall algorithm is an algorithm for finding shortest paths in
22845 * a weighted graph with positive or negative edge weights (but with no negative
22846 * cycles). - https://en.wikipedia.org/wiki/Floyd–Warshall_algorithm
22847 */
22848class FloydWarshall {
22849 /**
22850 * @ignore
22851 */
22852 constructor() {}
22853
22854 /**
22855 *
22856 * @param {object} body
22857 * @param {Array.<Node>} nodesArray
22858 * @param {Array.<Edge>} edgesArray
22859 * @returns {{}}
22860 */
22861 getDistances(body, nodesArray, edgesArray) {
22862 const D_matrix = {};
22863 const edges = body.edges;
22864
22865 // prepare matrix with large numbers
22866 for (let i = 0; i < nodesArray.length; i++) {
22867 const node = nodesArray[i];
22868 const cell = {};
22869 D_matrix[node] = cell;
22870 for (let j = 0; j < nodesArray.length; j++) {
22871 cell[nodesArray[j]] = i == j ? 0 : 1e9;
22872 }
22873 }
22874
22875 // put the weights for the edges in. This assumes unidirectionality.
22876 for (let i = 0; i < edgesArray.length; i++) {
22877 const edge = edges[edgesArray[i]];
22878 // edge has to be connected if it counts to the distances. If it is connected to inner clusters it will crash so we also check if it is in the D_matrix
22879 if (
22880 edge.connected === true &&
22881 D_matrix[edge.fromId] !== undefined &&
22882 D_matrix[edge.toId] !== undefined
22883 ) {
22884 D_matrix[edge.fromId][edge.toId] = 1;
22885 D_matrix[edge.toId][edge.fromId] = 1;
22886 }
22887 }
22888
22889 const nodeCount = nodesArray.length;
22890
22891 // Adapted FloydWarshall based on unidirectionality to greatly reduce complexity.
22892 for (let k = 0; k < nodeCount; k++) {
22893 const knode = nodesArray[k];
22894 const kcolm = D_matrix[knode];
22895 for (let i = 0; i < nodeCount - 1; i++) {
22896 const inode = nodesArray[i];
22897 const icolm = D_matrix[inode];
22898 for (let j = i + 1; j < nodeCount; j++) {
22899 const jnode = nodesArray[j];
22900 const jcolm = D_matrix[jnode];
22901
22902 const val = Math.min(icolm[jnode], icolm[knode] + kcolm[jnode]);
22903 icolm[jnode] = val;
22904 jcolm[inode] = val;
22905 }
22906 }
22907 }
22908
22909 return D_matrix;
22910 }
22911}
22912
22913// distance finding algorithm
22914
22915/**
22916 * KamadaKawai positions the nodes initially based on
22917 *
22918 * "AN ALGORITHM FOR DRAWING GENERAL UNDIRECTED GRAPHS"
22919 * -- Tomihisa KAMADA and Satoru KAWAI in 1989
22920 *
22921 * Possible optimizations in the distance calculation can be implemented.
22922 */
22923class KamadaKawai {
22924 /**
22925 * @param {object} body
22926 * @param {number} edgeLength
22927 * @param {number} edgeStrength
22928 */
22929 constructor(body, edgeLength, edgeStrength) {
22930 this.body = body;
22931 this.springLength = edgeLength;
22932 this.springConstant = edgeStrength;
22933 this.distanceSolver = new FloydWarshall();
22934 }
22935
22936 /**
22937 * Not sure if needed but can be used to update the spring length and spring constant
22938 *
22939 * @param {object} options
22940 */
22941 setOptions(options) {
22942 if (options) {
22943 if (options.springLength) {
22944 this.springLength = options.springLength;
22945 }
22946 if (options.springConstant) {
22947 this.springConstant = options.springConstant;
22948 }
22949 }
22950 }
22951
22952 /**
22953 * Position the system
22954 *
22955 * @param {Array.<Node>} nodesArray
22956 * @param {Array.<vis.Edge>} edgesArray
22957 * @param {boolean} [ignoreClusters=false]
22958 */
22959 solve(nodesArray, edgesArray, ignoreClusters = false) {
22960 // get distance matrix
22961 const D_matrix = this.distanceSolver.getDistances(
22962 this.body,
22963 nodesArray,
22964 edgesArray
22965 ); // distance matrix
22966
22967 // get the L Matrix
22968 this._createL_matrix(D_matrix);
22969
22970 // get the K Matrix
22971 this._createK_matrix(D_matrix);
22972
22973 // initial E Matrix
22974 this._createE_matrix();
22975
22976 // calculate positions
22977 const threshold = 0.01;
22978 const innerThreshold = 1;
22979 let iterations = 0;
22980 const maxIterations = Math.max(
22981 1000,
22982 Math.min(10 * this.body.nodeIndices.length, 6000)
22983 );
22984 const maxInnerIterations = 5;
22985
22986 let maxEnergy = 1e9;
22987 let highE_nodeId = 0,
22988 dE_dx = 0,
22989 dE_dy = 0,
22990 delta_m = 0,
22991 subIterations = 0;
22992
22993 while (maxEnergy > threshold && iterations < maxIterations) {
22994 iterations += 1;
22995 [highE_nodeId, maxEnergy, dE_dx, dE_dy] =
22996 this._getHighestEnergyNode(ignoreClusters);
22997 delta_m = maxEnergy;
22998 subIterations = 0;
22999 while (delta_m > innerThreshold && subIterations < maxInnerIterations) {
23000 subIterations += 1;
23001 this._moveNode(highE_nodeId, dE_dx, dE_dy);
23002 [delta_m, dE_dx, dE_dy] = this._getEnergy(highE_nodeId);
23003 }
23004 }
23005 }
23006
23007 /**
23008 * get the node with the highest energy
23009 *
23010 * @param {boolean} ignoreClusters
23011 * @returns {number[]}
23012 * @private
23013 */
23014 _getHighestEnergyNode(ignoreClusters) {
23015 const nodesArray = this.body.nodeIndices;
23016 const nodes = this.body.nodes;
23017 let maxEnergy = 0;
23018 let maxEnergyNodeId = nodesArray[0];
23019 let dE_dx_max = 0,
23020 dE_dy_max = 0;
23021
23022 for (let nodeIdx = 0; nodeIdx < nodesArray.length; nodeIdx++) {
23023 const m = nodesArray[nodeIdx];
23024 // by not evaluating nodes with predefined positions we should only move nodes that have no positions.
23025 if (
23026 nodes[m].predefinedPosition !== true ||
23027 (nodes[m].isCluster === true && ignoreClusters === true) ||
23028 nodes[m].options.fixed.x !== true ||
23029 nodes[m].options.fixed.y !== true
23030 ) {
23031 const [delta_m, dE_dx, dE_dy] = this._getEnergy(m);
23032 if (maxEnergy < delta_m) {
23033 maxEnergy = delta_m;
23034 maxEnergyNodeId = m;
23035 dE_dx_max = dE_dx;
23036 dE_dy_max = dE_dy;
23037 }
23038 }
23039 }
23040
23041 return [maxEnergyNodeId, maxEnergy, dE_dx_max, dE_dy_max];
23042 }
23043
23044 /**
23045 * calculate the energy of a single node
23046 *
23047 * @param {Node.id} m
23048 * @returns {number[]}
23049 * @private
23050 */
23051 _getEnergy(m) {
23052 const [dE_dx, dE_dy] = this.E_sums[m];
23053 const delta_m = Math.sqrt(dE_dx ** 2 + dE_dy ** 2);
23054 return [delta_m, dE_dx, dE_dy];
23055 }
23056
23057 /**
23058 * move the node based on it's energy
23059 * the dx and dy are calculated from the linear system proposed by Kamada and Kawai
23060 *
23061 * @param {number} m
23062 * @param {number} dE_dx
23063 * @param {number} dE_dy
23064 * @private
23065 */
23066 _moveNode(m, dE_dx, dE_dy) {
23067 const nodesArray = this.body.nodeIndices;
23068 const nodes = this.body.nodes;
23069 let d2E_dx2 = 0;
23070 let d2E_dxdy = 0;
23071 let d2E_dy2 = 0;
23072
23073 const x_m = nodes[m].x;
23074 const y_m = nodes[m].y;
23075 const km = this.K_matrix[m];
23076 const lm = this.L_matrix[m];
23077
23078 for (let iIdx = 0; iIdx < nodesArray.length; iIdx++) {
23079 const i = nodesArray[iIdx];
23080 if (i !== m) {
23081 const x_i = nodes[i].x;
23082 const y_i = nodes[i].y;
23083 const kmat = km[i];
23084 const lmat = lm[i];
23085 const denominator = 1.0 / ((x_m - x_i) ** 2 + (y_m - y_i) ** 2) ** 1.5;
23086 d2E_dx2 += kmat * (1 - lmat * (y_m - y_i) ** 2 * denominator);
23087 d2E_dxdy += kmat * (lmat * (x_m - x_i) * (y_m - y_i) * denominator);
23088 d2E_dy2 += kmat * (1 - lmat * (x_m - x_i) ** 2 * denominator);
23089 }
23090 }
23091 // make the variable names easier to make the solving of the linear system easier to read
23092 const A = d2E_dx2,
23093 B = d2E_dxdy,
23094 C = dE_dx,
23095 D = d2E_dy2,
23096 E = dE_dy;
23097
23098 // solve the linear system for dx and dy
23099 const dy = (C / A + E / B) / (B / A - D / B);
23100 const dx = -(B * dy + C) / A;
23101
23102 // move the node
23103 nodes[m].x += dx;
23104 nodes[m].y += dy;
23105
23106 // Recalculate E_matrix (should be incremental)
23107 this._updateE_matrix(m);
23108 }
23109
23110 /**
23111 * Create the L matrix: edge length times shortest path
23112 *
23113 * @param {object} D_matrix
23114 * @private
23115 */
23116 _createL_matrix(D_matrix) {
23117 const nodesArray = this.body.nodeIndices;
23118 const edgeLength = this.springLength;
23119
23120 this.L_matrix = [];
23121 for (let i = 0; i < nodesArray.length; i++) {
23122 this.L_matrix[nodesArray[i]] = {};
23123 for (let j = 0; j < nodesArray.length; j++) {
23124 this.L_matrix[nodesArray[i]][nodesArray[j]] =
23125 edgeLength * D_matrix[nodesArray[i]][nodesArray[j]];
23126 }
23127 }
23128 }
23129
23130 /**
23131 * Create the K matrix: spring constants times shortest path
23132 *
23133 * @param {object} D_matrix
23134 * @private
23135 */
23136 _createK_matrix(D_matrix) {
23137 const nodesArray = this.body.nodeIndices;
23138 const edgeStrength = this.springConstant;
23139
23140 this.K_matrix = [];
23141 for (let i = 0; i < nodesArray.length; i++) {
23142 this.K_matrix[nodesArray[i]] = {};
23143 for (let j = 0; j < nodesArray.length; j++) {
23144 this.K_matrix[nodesArray[i]][nodesArray[j]] =
23145 edgeStrength * D_matrix[nodesArray[i]][nodesArray[j]] ** -2;
23146 }
23147 }
23148 }
23149
23150 /**
23151 * Create matrix with all energies between nodes
23152 *
23153 * @private
23154 */
23155 _createE_matrix() {
23156 const nodesArray = this.body.nodeIndices;
23157 const nodes = this.body.nodes;
23158 this.E_matrix = {};
23159 this.E_sums = {};
23160 for (let mIdx = 0; mIdx < nodesArray.length; mIdx++) {
23161 this.E_matrix[nodesArray[mIdx]] = [];
23162 }
23163 for (let mIdx = 0; mIdx < nodesArray.length; mIdx++) {
23164 const m = nodesArray[mIdx];
23165 const x_m = nodes[m].x;
23166 const y_m = nodes[m].y;
23167 let dE_dx = 0;
23168 let dE_dy = 0;
23169 for (let iIdx = mIdx; iIdx < nodesArray.length; iIdx++) {
23170 const i = nodesArray[iIdx];
23171 if (i !== m) {
23172 const x_i = nodes[i].x;
23173 const y_i = nodes[i].y;
23174 const denominator =
23175 1.0 / Math.sqrt((x_m - x_i) ** 2 + (y_m - y_i) ** 2);
23176 this.E_matrix[m][iIdx] = [
23177 this.K_matrix[m][i] *
23178 (x_m - x_i - this.L_matrix[m][i] * (x_m - x_i) * denominator),
23179 this.K_matrix[m][i] *
23180 (y_m - y_i - this.L_matrix[m][i] * (y_m - y_i) * denominator),
23181 ];
23182 this.E_matrix[i][mIdx] = this.E_matrix[m][iIdx];
23183 dE_dx += this.E_matrix[m][iIdx][0];
23184 dE_dy += this.E_matrix[m][iIdx][1];
23185 }
23186 }
23187 //Store sum
23188 this.E_sums[m] = [dE_dx, dE_dy];
23189 }
23190 }
23191
23192 /**
23193 * Update method, just doing single column (rows are auto-updated) (update all sums)
23194 *
23195 * @param {number} m
23196 * @private
23197 */
23198 _updateE_matrix(m) {
23199 const nodesArray = this.body.nodeIndices;
23200 const nodes = this.body.nodes;
23201 const colm = this.E_matrix[m];
23202 const kcolm = this.K_matrix[m];
23203 const lcolm = this.L_matrix[m];
23204 const x_m = nodes[m].x;
23205 const y_m = nodes[m].y;
23206 let dE_dx = 0;
23207 let dE_dy = 0;
23208 for (let iIdx = 0; iIdx < nodesArray.length; iIdx++) {
23209 const i = nodesArray[iIdx];
23210 if (i !== m) {
23211 //Keep old energy value for sum modification below
23212 const cell = colm[iIdx];
23213 const oldDx = cell[0];
23214 const oldDy = cell[1];
23215
23216 //Calc new energy:
23217 const x_i = nodes[i].x;
23218 const y_i = nodes[i].y;
23219 const denominator =
23220 1.0 / Math.sqrt((x_m - x_i) ** 2 + (y_m - y_i) ** 2);
23221 const dx =
23222 kcolm[i] * (x_m - x_i - lcolm[i] * (x_m - x_i) * denominator);
23223 const dy =
23224 kcolm[i] * (y_m - y_i - lcolm[i] * (y_m - y_i) * denominator);
23225 colm[iIdx] = [dx, dy];
23226 dE_dx += dx;
23227 dE_dy += dy;
23228
23229 //add new energy to sum of each column
23230 const sum = this.E_sums[i];
23231 sum[0] += dx - oldDx;
23232 sum[1] += dy - oldDy;
23233 }
23234 }
23235 //Store sum at -1 index
23236 this.E_sums[m] = [dE_dx, dE_dy];
23237 }
23238}
23239
23240// Load custom shapes into CanvasRenderingContext2D
23241
23242/**
23243 * Create a network visualization, displaying nodes and edges.
23244 *
23245 * @param {Element} container The DOM element in which the Network will
23246 * be created. Normally a div element.
23247 * @param {object} data An object containing parameters
23248 * {Array} nodes
23249 * {Array} edges
23250 * @param {object} options Options
23251 * @class Network
23252 */
23253function Network(container, data, options) {
23254 if (!(this instanceof Network)) {
23255 throw new SyntaxError("Constructor must be called with the new operator");
23256 }
23257
23258 // set constant values
23259 this.options = {};
23260 this.defaultOptions = {
23261 locale: "en",
23262 locales: locales,
23263 clickToUse: false,
23264 };
23265 Object.assign(this.options, this.defaultOptions);
23266
23267 /**
23268 * Containers for nodes and edges.
23269 *
23270 * 'edges' and 'nodes' contain the full definitions of all the network elements.
23271 * 'nodeIndices' and 'edgeIndices' contain the id's of the active elements.
23272 *
23273 * The distinction is important, because a defined node need not be active, i.e.
23274 * visible on the canvas. This happens in particular when clusters are defined, in
23275 * that case there will be nodes and edges not displayed.
23276 * The bottom line is that all code with actions related to visibility, *must* use
23277 * 'nodeIndices' and 'edgeIndices', not 'nodes' and 'edges' directly.
23278 */
23279 this.body = {
23280 container: container,
23281
23282 // See comment above for following fields
23283 nodes: {},
23284 nodeIndices: [],
23285 edges: {},
23286 edgeIndices: [],
23287
23288 emitter: {
23289 on: this.on.bind(this),
23290 off: this.off.bind(this),
23291 emit: this.emit.bind(this),
23292 once: this.once.bind(this),
23293 },
23294 eventListeners: {
23295 onTap: function () {},
23296 onTouch: function () {},
23297 onDoubleTap: function () {},
23298 onHold: function () {},
23299 onDragStart: function () {},
23300 onDrag: function () {},
23301 onDragEnd: function () {},
23302 onMouseWheel: function () {},
23303 onPinch: function () {},
23304 onMouseMove: function () {},
23305 onRelease: function () {},
23306 onContext: function () {},
23307 },
23308 data: {
23309 nodes: null, // A DataSet or DataView
23310 edges: null, // A DataSet or DataView
23311 },
23312 functions: {
23313 createNode: function () {},
23314 createEdge: function () {},
23315 getPointer: function () {},
23316 },
23317 modules: {},
23318 view: {
23319 scale: 1,
23320 translation: { x: 0, y: 0 },
23321 },
23322 selectionBox: {
23323 show: false,
23324 position: {
23325 start: { x: 0, y: 0 },
23326 end: { x: 0, y: 0 },
23327 },
23328 },
23329 };
23330
23331 // bind the event listeners
23332 this.bindEventListeners();
23333
23334 // setting up all modules
23335 this.images = new Images(() => this.body.emitter.emit("_requestRedraw")); // object with images
23336 this.groups = new Groups(); // object with groups
23337 this.canvas = new Canvas(this.body); // DOM handler
23338 this.selectionHandler = new SelectionHandler(this.body, this.canvas); // Selection handler
23339 this.interactionHandler = new InteractionHandler(
23340 this.body,
23341 this.canvas,
23342 this.selectionHandler
23343 ); // Interaction handler handles all the hammer bindings (that are bound by canvas), key
23344 this.view = new View(this.body, this.canvas); // camera handler, does animations and zooms
23345 this.renderer = new CanvasRenderer(this.body, this.canvas); // renderer, starts renderloop, has events that modules can hook into
23346 this.physics = new PhysicsEngine(this.body); // physics engine, does all the simulations
23347 this.layoutEngine = new LayoutEngine(this.body); // layout engine for inital layout and hierarchical layout
23348 this.clustering = new ClusterEngine(this.body); // clustering api
23349 this.manipulation = new ManipulationSystem(
23350 this.body,
23351 this.canvas,
23352 this.selectionHandler,
23353 this.interactionHandler
23354 ); // data manipulation system
23355
23356 this.nodesHandler = new NodesHandler(
23357 this.body,
23358 this.images,
23359 this.groups,
23360 this.layoutEngine
23361 ); // Handle adding, deleting and updating of nodes as well as global options
23362 this.edgesHandler = new EdgesHandler(this.body, this.images, this.groups); // Handle adding, deleting and updating of edges as well as global options
23363
23364 this.body.modules["kamadaKawai"] = new KamadaKawai(this.body, 150, 0.05); // Layouting algorithm.
23365 this.body.modules["clustering"] = this.clustering;
23366
23367 // create the DOM elements
23368 this.canvas._create();
23369
23370 // apply options
23371 this.setOptions(options);
23372
23373 // load data (the disable start variable will be the same as the enabled clustering)
23374 this.setData(data);
23375}
23376
23377// Extend Network with an Emitter mixin
23378Emitter(Network.prototype);
23379
23380/**
23381 * Set options
23382 *
23383 * @param {object} options
23384 */
23385Network.prototype.setOptions = function (options) {
23386 if (options === null) {
23387 options = undefined; // This ensures that options handling doesn't crash in the handling
23388 }
23389
23390 if (options !== undefined) {
23391 const errorFound = Validator.validate(options, allOptions);
23392 if (errorFound === true) {
23393 console.error(
23394 "%cErrors have been found in the supplied options object.",
23395 VALIDATOR_PRINT_STYLE
23396 );
23397 }
23398
23399 // copy the global fields over
23400 const fields = ["locale", "locales", "clickToUse"];
23401 selectiveDeepExtend(fields, this.options, options);
23402
23403 // normalize the locale or use English
23404 if (options.locale !== undefined) {
23405 options.locale = normalizeLanguageCode(
23406 options.locales || this.options.locales,
23407 options.locale
23408 );
23409 }
23410
23411 // the hierarchical system can adapt the edges and the physics to it's own options because not all combinations work with the hierarichical system.
23412 options = this.layoutEngine.setOptions(options.layout, options);
23413
23414 this.canvas.setOptions(options); // options for canvas are in globals
23415
23416 // pass the options to the modules
23417 this.groups.setOptions(options.groups);
23418 this.nodesHandler.setOptions(options.nodes);
23419 this.edgesHandler.setOptions(options.edges);
23420 this.physics.setOptions(options.physics);
23421 this.manipulation.setOptions(options.manipulation, options, this.options); // manipulation uses the locales in the globals
23422
23423 this.interactionHandler.setOptions(options.interaction);
23424 this.renderer.setOptions(options.interaction); // options for rendering are in interaction
23425 this.selectionHandler.setOptions(options.interaction); // options for selection are in interaction
23426
23427 // reload the settings of the nodes to apply changes in groups that are not referenced by pointer.
23428 if (options.groups !== undefined) {
23429 this.body.emitter.emit("refreshNodes");
23430 }
23431 // these two do not have options at the moment, here for completeness
23432 //this.view.setOptions(options.view);
23433 //this.clustering.setOptions(options.clustering);
23434
23435 if ("configure" in options) {
23436 if (!this.configurator) {
23437 this.configurator = new Configurator(
23438 this,
23439 this.body.container,
23440 configureOptions,
23441 this.canvas.pixelRatio,
23442 configuratorHideOption
23443 );
23444 }
23445
23446 this.configurator.setOptions(options.configure);
23447 }
23448
23449 // if the configuration system is enabled, copy all options and put them into the config system
23450 if (this.configurator && this.configurator.options.enabled === true) {
23451 const networkOptions = {
23452 nodes: {},
23453 edges: {},
23454 layout: {},
23455 interaction: {},
23456 manipulation: {},
23457 physics: {},
23458 global: {},
23459 };
23460 deepExtend(networkOptions.nodes, this.nodesHandler.options);
23461 deepExtend(networkOptions.edges, this.edgesHandler.options);
23462 deepExtend(networkOptions.layout, this.layoutEngine.options);
23463 // load the selectionHandler and render default options in to the interaction group
23464 deepExtend(networkOptions.interaction, this.selectionHandler.options);
23465 deepExtend(networkOptions.interaction, this.renderer.options);
23466
23467 deepExtend(networkOptions.interaction, this.interactionHandler.options);
23468 deepExtend(networkOptions.manipulation, this.manipulation.options);
23469 deepExtend(networkOptions.physics, this.physics.options);
23470
23471 // load globals into the global object
23472 deepExtend(networkOptions.global, this.canvas.options);
23473 deepExtend(networkOptions.global, this.options);
23474
23475 this.configurator.setModuleOptions(networkOptions);
23476 }
23477
23478 // handle network global options
23479 if (options.clickToUse !== undefined) {
23480 if (options.clickToUse === true) {
23481 if (this.activator === undefined) {
23482 this.activator = new Activator(this.canvas.frame);
23483 this.activator.on("change", () => {
23484 this.body.emitter.emit("activate");
23485 });
23486 }
23487 } else {
23488 if (this.activator !== undefined) {
23489 this.activator.destroy();
23490 delete this.activator;
23491 }
23492 this.body.emitter.emit("activate");
23493 }
23494 } else {
23495 this.body.emitter.emit("activate");
23496 }
23497
23498 this.canvas.setSize();
23499 // start the physics simulation. Can be safely called multiple times.
23500 this.body.emitter.emit("startSimulation");
23501 }
23502};
23503
23504/**
23505 * Update the visible nodes and edges list with the most recent node state.
23506 *
23507 * Visible nodes are stored in this.body.nodeIndices.
23508 * Visible edges are stored in this.body.edgeIndices.
23509 * A node or edges is visible if it is not hidden or clustered.
23510 *
23511 * @private
23512 */
23513Network.prototype._updateVisibleIndices = function () {
23514 const nodes = this.body.nodes;
23515 const edges = this.body.edges;
23516 this.body.nodeIndices = [];
23517 this.body.edgeIndices = [];
23518
23519 for (const nodeId in nodes) {
23520 if (Object.prototype.hasOwnProperty.call(nodes, nodeId)) {
23521 if (
23522 !this.clustering._isClusteredNode(nodeId) &&
23523 nodes[nodeId].options.hidden === false
23524 ) {
23525 this.body.nodeIndices.push(nodes[nodeId].id);
23526 }
23527 }
23528 }
23529
23530 for (const edgeId in edges) {
23531 if (Object.prototype.hasOwnProperty.call(edges, edgeId)) {
23532 const edge = edges[edgeId];
23533
23534 // It can happen that this is executed *after* a node edge has been removed,
23535 // but *before* the edge itself has been removed. Taking this into account.
23536 const fromNode = nodes[edge.fromId];
23537 const toNode = nodes[edge.toId];
23538 const edgeNodesPresent = fromNode !== undefined && toNode !== undefined;
23539
23540 const isVisible =
23541 !this.clustering._isClusteredEdge(edgeId) &&
23542 edge.options.hidden === false &&
23543 edgeNodesPresent &&
23544 fromNode.options.hidden === false && // Also hidden if any of its connecting nodes are hidden
23545 toNode.options.hidden === false; // idem
23546
23547 if (isVisible) {
23548 this.body.edgeIndices.push(edge.id);
23549 }
23550 }
23551 }
23552};
23553
23554/**
23555 * Bind all events
23556 */
23557Network.prototype.bindEventListeners = function () {
23558 // This event will trigger a rebuilding of the cache everything.
23559 // Used when nodes or edges have been added or removed.
23560 this.body.emitter.on("_dataChanged", () => {
23561 this.edgesHandler._updateState();
23562 this.body.emitter.emit("_dataUpdated");
23563 });
23564
23565 // this is called when options of EXISTING nodes or edges have changed.
23566 this.body.emitter.on("_dataUpdated", () => {
23567 // Order important in following block
23568 this.clustering._updateState();
23569 this._updateVisibleIndices();
23570
23571 this._updateValueRange(this.body.nodes);
23572 this._updateValueRange(this.body.edges);
23573 // start simulation (can be called safely, even if already running)
23574 this.body.emitter.emit("startSimulation");
23575 this.body.emitter.emit("_requestRedraw");
23576 });
23577};
23578
23579/**
23580 * Set nodes and edges, and optionally options as well.
23581 *
23582 * @param {object} data Object containing parameters:
23583 * {Array | DataSet | DataView} [nodes] Array with nodes
23584 * {Array | DataSet | DataView} [edges] Array with edges
23585 * {String} [dot] String containing data in DOT format
23586 * {String} [gephi] String containing data in gephi JSON format
23587 * {Options} [options] Object with options
23588 */
23589Network.prototype.setData = function (data) {
23590 // reset the physics engine.
23591 this.body.emitter.emit("resetPhysics");
23592 this.body.emitter.emit("_resetData");
23593
23594 // unselect all to ensure no selections from old data are carried over.
23595 this.selectionHandler.unselectAll();
23596
23597 if (data && data.dot && (data.nodes || data.edges)) {
23598 throw new SyntaxError(
23599 'Data must contain either parameter "dot" or ' +
23600 ' parameter pair "nodes" and "edges", but not both.'
23601 );
23602 }
23603
23604 // set options
23605 this.setOptions(data && data.options);
23606 // set all data
23607 if (data && data.dot) {
23608 console.warn(
23609 "The dot property has been deprecated. Please use the static convertDot method to convert DOT into vis.network format and use the normal data format with nodes and edges. This converter is used like this: var data = vis.network.convertDot(dotString);"
23610 );
23611 // parse DOT file
23612 const dotData = DOTToGraph(data.dot);
23613 this.setData(dotData);
23614 return;
23615 } else if (data && data.gephi) {
23616 // parse DOT file
23617 console.warn(
23618 "The gephi property has been deprecated. Please use the static convertGephi method to convert gephi into vis.network format and use the normal data format with nodes and edges. This converter is used like this: var data = vis.network.convertGephi(gephiJson);"
23619 );
23620 const gephiData = parseGephi(data.gephi);
23621 this.setData(gephiData);
23622 return;
23623 } else {
23624 this.nodesHandler.setData(data && data.nodes, true);
23625 this.edgesHandler.setData(data && data.edges, true);
23626 }
23627
23628 // emit change in data
23629 this.body.emitter.emit("_dataChanged");
23630
23631 // emit data loaded
23632 this.body.emitter.emit("_dataLoaded");
23633
23634 // find a stable position or start animating to a stable position
23635 this.body.emitter.emit("initPhysics");
23636};
23637
23638/**
23639 * Cleans up all bindings of the network, removing it fully from the memory IF the variable is set to null after calling this function.
23640 * var network = new vis.Network(..);
23641 * network.destroy();
23642 * network = null;
23643 */
23644Network.prototype.destroy = function () {
23645 this.body.emitter.emit("destroy");
23646 // clear events
23647 this.body.emitter.off();
23648 this.off();
23649
23650 // delete modules
23651 delete this.groups;
23652 delete this.canvas;
23653 delete this.selectionHandler;
23654 delete this.interactionHandler;
23655 delete this.view;
23656 delete this.renderer;
23657 delete this.physics;
23658 delete this.layoutEngine;
23659 delete this.clustering;
23660 delete this.manipulation;
23661 delete this.nodesHandler;
23662 delete this.edgesHandler;
23663 delete this.configurator;
23664 delete this.images;
23665
23666 for (const nodeId in this.body.nodes) {
23667 if (!Object.prototype.hasOwnProperty.call(this.body.nodes, nodeId))
23668 continue;
23669 delete this.body.nodes[nodeId];
23670 }
23671
23672 for (const edgeId in this.body.edges) {
23673 if (!Object.prototype.hasOwnProperty.call(this.body.edges, edgeId))
23674 continue;
23675 delete this.body.edges[edgeId];
23676 }
23677
23678 // remove the container and everything inside it recursively
23679 recursiveDOMDelete(this.body.container);
23680};
23681
23682/**
23683 * Update the values of all object in the given array according to the current
23684 * value range of the objects in the array.
23685 *
23686 * @param {object} obj An object containing a set of Edges or Nodes
23687 * The objects must have a method getValue() and
23688 * setValueRange(min, max).
23689 * @private
23690 */
23691Network.prototype._updateValueRange = function (obj) {
23692 let id;
23693
23694 // determine the range of the objects
23695 let valueMin = undefined;
23696 let valueMax = undefined;
23697 let valueTotal = 0;
23698 for (id in obj) {
23699 if (Object.prototype.hasOwnProperty.call(obj, id)) {
23700 const value = obj[id].getValue();
23701 if (value !== undefined) {
23702 valueMin = valueMin === undefined ? value : Math.min(value, valueMin);
23703 valueMax = valueMax === undefined ? value : Math.max(value, valueMax);
23704 valueTotal += value;
23705 }
23706 }
23707 }
23708
23709 // adjust the range of all objects
23710 if (valueMin !== undefined && valueMax !== undefined) {
23711 for (id in obj) {
23712 if (Object.prototype.hasOwnProperty.call(obj, id)) {
23713 obj[id].setValueRange(valueMin, valueMax, valueTotal);
23714 }
23715 }
23716 }
23717};
23718
23719/**
23720 * Returns true when the Network is active.
23721 *
23722 * @returns {boolean}
23723 */
23724Network.prototype.isActive = function () {
23725 return !this.activator || this.activator.active;
23726};
23727
23728Network.prototype.setSize = function () {
23729 return this.canvas.setSize.apply(this.canvas, arguments);
23730};
23731Network.prototype.canvasToDOM = function () {
23732 return this.canvas.canvasToDOM.apply(this.canvas, arguments);
23733};
23734Network.prototype.DOMtoCanvas = function () {
23735 return this.canvas.DOMtoCanvas.apply(this.canvas, arguments);
23736};
23737
23738/**
23739 * Nodes can be in clusters. Clusters can also be in clusters. This function returns and array of
23740 * nodeIds showing where the node is.
23741 *
23742 * If any nodeId in the chain, especially the first passed in as a parameter, is not present in
23743 * the current nodes list, an empty array is returned.
23744 *
23745 * Example:
23746 * cluster 'A' contains cluster 'B',
23747 * cluster 'B' contains cluster 'C',
23748 * cluster 'C' contains node 'fred'.
23749 * `jsnetwork.clustering.findNode('fred')` will return `['A','B','C','fred']`.
23750 *
23751 * @param {string|number} nodeId
23752 * @returns {Array}
23753 */
23754Network.prototype.findNode = function () {
23755 return this.clustering.findNode.apply(this.clustering, arguments);
23756};
23757
23758Network.prototype.isCluster = function () {
23759 return this.clustering.isCluster.apply(this.clustering, arguments);
23760};
23761Network.prototype.openCluster = function () {
23762 return this.clustering.openCluster.apply(this.clustering, arguments);
23763};
23764Network.prototype.cluster = function () {
23765 return this.clustering.cluster.apply(this.clustering, arguments);
23766};
23767Network.prototype.getNodesInCluster = function () {
23768 return this.clustering.getNodesInCluster.apply(this.clustering, arguments);
23769};
23770Network.prototype.clusterByConnection = function () {
23771 return this.clustering.clusterByConnection.apply(this.clustering, arguments);
23772};
23773Network.prototype.clusterByHubsize = function () {
23774 return this.clustering.clusterByHubsize.apply(this.clustering, arguments);
23775};
23776Network.prototype.updateClusteredNode = function () {
23777 return this.clustering.updateClusteredNode.apply(this.clustering, arguments);
23778};
23779Network.prototype.getClusteredEdges = function () {
23780 return this.clustering.getClusteredEdges.apply(this.clustering, arguments);
23781};
23782Network.prototype.getBaseEdge = function () {
23783 return this.clustering.getBaseEdge.apply(this.clustering, arguments);
23784};
23785Network.prototype.getBaseEdges = function () {
23786 return this.clustering.getBaseEdges.apply(this.clustering, arguments);
23787};
23788Network.prototype.updateEdge = function () {
23789 return this.clustering.updateEdge.apply(this.clustering, arguments);
23790};
23791
23792/**
23793 * This method will cluster all nodes with 1 edge with their respective connected node.
23794 * The options object is explained in full <a data-scroll="" data-options="{ &quot;easing&quot;: &quot;easeInCubic&quot; }" href="#optionsObject">below</a>.
23795 *
23796 * @param {object} [options]
23797 * @returns {undefined}
23798 */
23799Network.prototype.clusterOutliers = function () {
23800 return this.clustering.clusterOutliers.apply(this.clustering, arguments);
23801};
23802
23803Network.prototype.getSeed = function () {
23804 return this.layoutEngine.getSeed.apply(this.layoutEngine, arguments);
23805};
23806Network.prototype.enableEditMode = function () {
23807 return this.manipulation.enableEditMode.apply(this.manipulation, arguments);
23808};
23809Network.prototype.disableEditMode = function () {
23810 return this.manipulation.disableEditMode.apply(this.manipulation, arguments);
23811};
23812Network.prototype.addNodeMode = function () {
23813 return this.manipulation.addNodeMode.apply(this.manipulation, arguments);
23814};
23815Network.prototype.editNode = function () {
23816 return this.manipulation.editNode.apply(this.manipulation, arguments);
23817};
23818Network.prototype.editNodeMode = function () {
23819 console.warn("Deprecated: Please use editNode instead of editNodeMode.");
23820 return this.manipulation.editNode.apply(this.manipulation, arguments);
23821};
23822Network.prototype.addEdgeMode = function () {
23823 return this.manipulation.addEdgeMode.apply(this.manipulation, arguments);
23824};
23825Network.prototype.editEdgeMode = function () {
23826 return this.manipulation.editEdgeMode.apply(this.manipulation, arguments);
23827};
23828Network.prototype.deleteSelected = function () {
23829 return this.manipulation.deleteSelected.apply(this.manipulation, arguments);
23830};
23831Network.prototype.getPositions = function () {
23832 return this.nodesHandler.getPositions.apply(this.nodesHandler, arguments);
23833};
23834Network.prototype.getPosition = function () {
23835 return this.nodesHandler.getPosition.apply(this.nodesHandler, arguments);
23836};
23837Network.prototype.storePositions = function () {
23838 return this.nodesHandler.storePositions.apply(this.nodesHandler, arguments);
23839};
23840Network.prototype.moveNode = function () {
23841 return this.nodesHandler.moveNode.apply(this.nodesHandler, arguments);
23842};
23843Network.prototype.getBoundingBox = function () {
23844 return this.nodesHandler.getBoundingBox.apply(this.nodesHandler, arguments);
23845};
23846Network.prototype.getConnectedNodes = function (objectId) {
23847 if (this.body.nodes[objectId] !== undefined) {
23848 return this.nodesHandler.getConnectedNodes.apply(
23849 this.nodesHandler,
23850 arguments
23851 );
23852 } else {
23853 return this.edgesHandler.getConnectedNodes.apply(
23854 this.edgesHandler,
23855 arguments
23856 );
23857 }
23858};
23859Network.prototype.getConnectedEdges = function () {
23860 return this.nodesHandler.getConnectedEdges.apply(
23861 this.nodesHandler,
23862 arguments
23863 );
23864};
23865Network.prototype.startSimulation = function () {
23866 return this.physics.startSimulation.apply(this.physics, arguments);
23867};
23868Network.prototype.stopSimulation = function () {
23869 return this.physics.stopSimulation.apply(this.physics, arguments);
23870};
23871Network.prototype.stabilize = function () {
23872 return this.physics.stabilize.apply(this.physics, arguments);
23873};
23874Network.prototype.getSelection = function () {
23875 return this.selectionHandler.getSelection.apply(
23876 this.selectionHandler,
23877 arguments
23878 );
23879};
23880Network.prototype.setSelection = function () {
23881 return this.selectionHandler.setSelection.apply(
23882 this.selectionHandler,
23883 arguments
23884 );
23885};
23886Network.prototype.getSelectedNodes = function () {
23887 return this.selectionHandler.getSelectedNodeIds.apply(
23888 this.selectionHandler,
23889 arguments
23890 );
23891};
23892Network.prototype.getSelectedEdges = function () {
23893 return this.selectionHandler.getSelectedEdgeIds.apply(
23894 this.selectionHandler,
23895 arguments
23896 );
23897};
23898Network.prototype.getNodeAt = function () {
23899 const node = this.selectionHandler.getNodeAt.apply(
23900 this.selectionHandler,
23901 arguments
23902 );
23903 if (node !== undefined && node.id !== undefined) {
23904 return node.id;
23905 }
23906 return node;
23907};
23908Network.prototype.getEdgeAt = function () {
23909 const edge = this.selectionHandler.getEdgeAt.apply(
23910 this.selectionHandler,
23911 arguments
23912 );
23913 if (edge !== undefined && edge.id !== undefined) {
23914 return edge.id;
23915 }
23916 return edge;
23917};
23918Network.prototype.selectNodes = function () {
23919 return this.selectionHandler.selectNodes.apply(
23920 this.selectionHandler,
23921 arguments
23922 );
23923};
23924Network.prototype.selectEdges = function () {
23925 return this.selectionHandler.selectEdges.apply(
23926 this.selectionHandler,
23927 arguments
23928 );
23929};
23930Network.prototype.unselectAll = function () {
23931 this.selectionHandler.unselectAll.apply(this.selectionHandler, arguments);
23932 this.selectionHandler.commitWithoutEmitting.apply(this.selectionHandler);
23933 this.redraw();
23934};
23935Network.prototype.redraw = function () {
23936 return this.renderer.redraw.apply(this.renderer, arguments);
23937};
23938Network.prototype.getScale = function () {
23939 return this.view.getScale.apply(this.view, arguments);
23940};
23941Network.prototype.getViewPosition = function () {
23942 return this.view.getViewPosition.apply(this.view, arguments);
23943};
23944Network.prototype.fit = function () {
23945 return this.view.fit.apply(this.view, arguments);
23946};
23947Network.prototype.moveTo = function () {
23948 return this.view.moveTo.apply(this.view, arguments);
23949};
23950Network.prototype.focus = function () {
23951 return this.view.focus.apply(this.view, arguments);
23952};
23953Network.prototype.releaseNode = function () {
23954 return this.view.releaseNode.apply(this.view, arguments);
23955};
23956Network.prototype.getOptionsFromConfigurator = function () {
23957 let options = {};
23958 if (this.configurator) {
23959 options = this.configurator.getOptions.apply(this.configurator);
23960 }
23961 return options;
23962};
23963
23964const parseDOTNetwork = DOTToGraph;
23965// DataSet, utils etc. can't be reexported here because that would cause stack
23966// overflow in UMD builds. They all export vis namespace therefore reexporting
23967// leads to loading vis to load vis to load vis…
23968
23969export { Network, Images as NetworkImages, dotparser as networkDOTParser, gephiParser as networkGephiParser, options as networkOptions, parseDOTNetwork, parseGephi as parseGephiNetwork };
23970//# sourceMappingURL=vis-network.js.map