UNPKG

sigma

Version:

A JavaScript library aimed at visualizing graphs of thousands of nodes and edges.

1,145 lines (1,139 loc) 229 kB
var Sigma; /******/ (() => { // webpackBootstrap /******/ var __webpack_modules__ = ([ /* 0 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { if (typeof b !== "function" && b !== null) throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", ({ value: true })); /** * Sigma.js Bundle Endpoint * ======================== * * The library endpoint. * Will be built so that it exports a global `Sigma` class, that also exposes * useful classes as static properties. * @module */ var sigma_1 = __importDefault(__webpack_require__(1)); var camera_1 = __importDefault(__webpack_require__(3)); var quadtree_1 = __importDefault(__webpack_require__(14)); var mouse_1 = __importDefault(__webpack_require__(12)); var Sigma = /** @class */ (function (_super) { __extends(Sigma, _super); function Sigma() { return _super !== null && _super.apply(this, arguments) || this; } Sigma.Camera = camera_1.default; Sigma.QuadTree = quadtree_1.default; Sigma.MouseCaptor = mouse_1.default; Sigma.Sigma = sigma_1.default; return Sigma; }(sigma_1.default)); module.exports = Sigma; /***/ }), /* 1 */ /***/ (function(__unused_webpack_module, exports, __webpack_require__) { "use strict"; var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { if (typeof b !== "function" && b !== null) throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __values = (this && this.__values) || function(o) { var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; if (m) return m.call(o); if (o && typeof o.length === "number") return { next: function () { if (o && i >= o.length) o = void 0; return { value: o && o[i++], done: !o }; } }; throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", ({ value: true })); var extend_1 = __importDefault(__webpack_require__(2)); var camera_1 = __importDefault(__webpack_require__(3)); var mouse_1 = __importDefault(__webpack_require__(12)); var quadtree_1 = __importDefault(__webpack_require__(14)); var types_1 = __webpack_require__(10); var utils_1 = __webpack_require__(5); var labels_1 = __webpack_require__(15); var settings_1 = __webpack_require__(16); var touch_1 = __importDefault(__webpack_require__(36)); var matrices_1 = __webpack_require__(7); var edge_collisions_1 = __webpack_require__(37); /** * Constants. */ var X_LABEL_MARGIN = 150; var Y_LABEL_MARGIN = 50; /** * Important functions. */ function applyNodeDefaults(settings, key, data) { if (!data.hasOwnProperty("x") || !data.hasOwnProperty("y")) throw new Error("Sigma: could not find a valid position (x, y) for node \"".concat(key, "\". All your nodes must have a number \"x\" and \"y\". Maybe your forgot to apply a layout or your \"nodeReducer\" is not returning the correct data?")); if (!data.color) data.color = settings.defaultNodeColor; if (!data.label && data.label !== "") data.label = null; if (data.label !== undefined && data.label !== null) data.label = "" + data.label; else data.label = null; if (!data.size) data.size = 2; if (!data.hasOwnProperty("hidden")) data.hidden = false; if (!data.hasOwnProperty("highlighted")) data.highlighted = false; if (!data.hasOwnProperty("forceLabel")) data.forceLabel = false; if (!data.type || data.type === "") data.type = settings.defaultNodeType; if (!data.zIndex) data.zIndex = 0; return data; } function applyEdgeDefaults(settings, key, data) { if (!data.color) data.color = settings.defaultEdgeColor; if (!data.label) data.label = ""; if (!data.size) data.size = 0.5; if (!data.hasOwnProperty("hidden")) data.hidden = false; if (!data.hasOwnProperty("forceLabel")) data.forceLabel = false; if (!data.type || data.type === "") data.type = settings.defaultEdgeType; if (!data.zIndex) data.zIndex = 0; return data; } /** * Main class. * * @constructor * @param {Graph} graph - Graph to render. * @param {HTMLElement} container - DOM container in which to render. * @param {object} settings - Optional settings. */ var Sigma = /** @class */ (function (_super) { __extends(Sigma, _super); function Sigma(graph, container, settings) { if (settings === void 0) { settings = {}; } var _this = _super.call(this) || this; _this.elements = {}; _this.canvasContexts = {}; _this.webGLContexts = {}; _this.activeListeners = {}; _this.quadtree = new quadtree_1.default(); _this.labelGrid = new labels_1.LabelGrid(); _this.nodeDataCache = {}; _this.edgeDataCache = {}; _this.nodesWithForcedLabels = []; _this.edgesWithForcedLabels = []; _this.nodeExtent = { x: [0, 1], y: [0, 1] }; _this.matrix = (0, matrices_1.identity)(); _this.invMatrix = (0, matrices_1.identity)(); _this.correctionRatio = 1; _this.customBBox = null; _this.normalizationFunction = (0, utils_1.createNormalizationFunction)({ x: [0, 1], y: [0, 1], }); // Cache: _this.cameraSizeRatio = 1; // Starting dimensions and pixel ratio _this.width = 0; _this.height = 0; _this.pixelRatio = (0, utils_1.getPixelRatio)(); // State _this.displayedLabels = new Set(); _this.highlightedNodes = new Set(); _this.hoveredNode = null; _this.hoveredEdge = null; _this.renderFrame = null; _this.renderHighlightedNodesFrame = null; _this.needToProcess = false; _this.needToSoftProcess = false; _this.checkEdgesEventsFrame = null; // Programs _this.nodePrograms = {}; _this.nodeHoverPrograms = {}; _this.edgePrograms = {}; // Resolving settings _this.settings = (0, settings_1.resolveSettings)(settings); // Validating (0, settings_1.validateSettings)(_this.settings); (0, utils_1.validateGraph)(graph); if (!(container instanceof HTMLElement)) throw new Error("Sigma: container should be an html element."); // Properties _this.graph = graph; _this.container = container; // Initializing contexts _this.createWebGLContext("edges", { preserveDrawingBuffer: true }); _this.createCanvasContext("edgeLabels"); _this.createWebGLContext("nodes"); _this.createCanvasContext("labels"); _this.createCanvasContext("hovers"); _this.createWebGLContext("hoverNodes"); _this.createCanvasContext("mouse"); // Blending for (var key in _this.webGLContexts) { var gl = _this.webGLContexts[key]; gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); gl.enable(gl.BLEND); } // Loading programs for (var type in _this.settings.nodeProgramClasses) { var NodeProgramClass = _this.settings.nodeProgramClasses[type]; _this.nodePrograms[type] = new NodeProgramClass(_this.webGLContexts.nodes, _this); var NodeHoverProgram = NodeProgramClass; if (type in _this.settings.nodeHoverProgramClasses) { NodeHoverProgram = _this.settings.nodeHoverProgramClasses[type]; } _this.nodeHoverPrograms[type] = new NodeHoverProgram(_this.webGLContexts.hoverNodes, _this); } for (var type in _this.settings.edgeProgramClasses) { var EdgeProgramClass = _this.settings.edgeProgramClasses[type]; _this.edgePrograms[type] = new EdgeProgramClass(_this.webGLContexts.edges, _this); } // Initial resize _this.resize(); // Initializing the camera _this.camera = new camera_1.default(); // Binding camera events _this.bindCameraHandlers(); // Initializing captors _this.mouseCaptor = new mouse_1.default(_this.elements.mouse, _this); _this.touchCaptor = new touch_1.default(_this.elements.mouse, _this); // Binding event handlers _this.bindEventHandlers(); // Binding graph handlers _this.bindGraphHandlers(); // Trigger eventual settings-related things _this.handleSettingsUpdate(); // Processing data for the first time & render _this.process(); _this.render(); return _this; } /**--------------------------------------------------------------------------- * Internal methods. **--------------------------------------------------------------------------- */ /** * Internal function used to create a canvas element. * @param {string} id - Context's id. * @return {Sigma} */ Sigma.prototype.createCanvas = function (id) { var canvas = (0, utils_1.createElement)("canvas", { position: "absolute", }, { class: "sigma-".concat(id), }); this.elements[id] = canvas; this.container.appendChild(canvas); return canvas; }; /** * Internal function used to create a canvas context and add the relevant * DOM elements. * * @param {string} id - Context's id. * @return {Sigma} */ Sigma.prototype.createCanvasContext = function (id) { var canvas = this.createCanvas(id); var contextOptions = { preserveDrawingBuffer: false, antialias: false, }; this.canvasContexts[id] = canvas.getContext("2d", contextOptions); return this; }; /** * Internal function used to create a canvas context and add the relevant * DOM elements. * * @param {string} id - Context's id. * @param {object?} options - #getContext params to override (optional) * @return {Sigma} */ Sigma.prototype.createWebGLContext = function (id, options) { var canvas = this.createCanvas(id); var contextOptions = __assign({ preserveDrawingBuffer: false, antialias: false }, (options || {})); var context; // First we try webgl2 for an easy performance boost context = canvas.getContext("webgl2", contextOptions); // Else we fall back to webgl if (!context) context = canvas.getContext("webgl", contextOptions); // Edge, I am looking right at you... if (!context) context = canvas.getContext("experimental-webgl", contextOptions); this.webGLContexts[id] = context; return this; }; /** * Method binding camera handlers. * * @return {Sigma} */ Sigma.prototype.bindCameraHandlers = function () { var _this = this; this.activeListeners.camera = function () { _this._scheduleRefresh(); }; this.camera.on("updated", this.activeListeners.camera); return this; }; /** * Method that checks whether or not a node collides with a given position. */ Sigma.prototype.mouseIsOnNode = function (_a, _b, size) { var x = _a.x, y = _a.y; var nodeX = _b.x, nodeY = _b.y; return (x > nodeX - size && x < nodeX + size && y > nodeY - size && y < nodeY + size && Math.sqrt(Math.pow(x - nodeX, 2) + Math.pow(y - nodeY, 2)) < size); }; /** * Method that returns all nodes in quad at a given position. */ Sigma.prototype.getQuadNodes = function (position) { var mouseGraphPosition = this.viewportToFramedGraph(position); return this.quadtree.point(mouseGraphPosition.x, 1 - mouseGraphPosition.y); }; /** * Method that returns the closest node to a given position. */ Sigma.prototype.getNodeAtPosition = function (position) { var x = position.x, y = position.y; var quadNodes = this.getQuadNodes(position); // We will hover the node whose center is closest to mouse var minDistance = Infinity, nodeAtPosition = null; for (var i = 0, l = quadNodes.length; i < l; i++) { var node = quadNodes[i]; var data = this.nodeDataCache[node]; var nodePosition = this.framedGraphToViewport(data); var size = this.scaleSize(data.size); if (!data.hidden && this.mouseIsOnNode(position, nodePosition, size)) { var distance = Math.sqrt(Math.pow(x - nodePosition.x, 2) + Math.pow(y - nodePosition.y, 2)); // TODO: sort by min size also for cases where center is the same if (distance < minDistance) { minDistance = distance; nodeAtPosition = node; } } } return nodeAtPosition; }; /** * Method binding event handlers. * * @return {Sigma} */ Sigma.prototype.bindEventHandlers = function () { var _this = this; // Handling window resize this.activeListeners.handleResize = function () { _this.needToSoftProcess = true; _this._scheduleRefresh(); }; window.addEventListener("resize", this.activeListeners.handleResize); // Handling mouse move this.activeListeners.handleMove = function (e) { var baseEvent = { event: e, preventSigmaDefault: function () { e.preventSigmaDefault(); }, }; var nodeToHover = _this.getNodeAtPosition(e); if (nodeToHover && _this.hoveredNode !== nodeToHover && !_this.nodeDataCache[nodeToHover].hidden) { // Handling passing from one node to the other directly if (_this.hoveredNode) _this.emit("leaveNode", __assign(__assign({}, baseEvent), { node: _this.hoveredNode })); _this.hoveredNode = nodeToHover; _this.emit("enterNode", __assign(__assign({}, baseEvent), { node: nodeToHover })); _this.scheduleHighlightedNodesRender(); return; } // Checking if the hovered node is still hovered if (_this.hoveredNode) { var data = _this.nodeDataCache[_this.hoveredNode]; var pos = _this.framedGraphToViewport(data); var size = _this.scaleSize(data.size); if (!_this.mouseIsOnNode(e, pos, size)) { var node = _this.hoveredNode; _this.hoveredNode = null; _this.emit("leaveNode", __assign(__assign({}, baseEvent), { node: node })); _this.scheduleHighlightedNodesRender(); return; } } if (_this.settings.enableEdgeHoverEvents === true) { _this.checkEdgeHoverEvents(baseEvent); } else if (_this.settings.enableEdgeHoverEvents === "debounce") { if (!_this.checkEdgesEventsFrame) _this.checkEdgesEventsFrame = (0, utils_1.requestFrame)(function () { _this.checkEdgeHoverEvents(baseEvent); _this.checkEdgesEventsFrame = null; }); } }; // Handling click var createMouseListener = function (eventType) { return function (e) { var baseEvent = { event: e, preventSigmaDefault: function () { e.preventSigmaDefault(); }, }; var isFakeSigmaMouseEvent = e.original.isFakeSigmaMouseEvent; var nodeAtPosition = isFakeSigmaMouseEvent ? _this.getNodeAtPosition(e) : _this.hoveredNode; if (nodeAtPosition) return _this.emit("".concat(eventType, "Node"), __assign(__assign({}, baseEvent), { node: nodeAtPosition })); if (eventType === "wheel" ? _this.settings.enableEdgeWheelEvents : _this.settings.enableEdgeClickEvents) { var edge = _this.getEdgeAtPoint(e.x, e.y); if (edge) return _this.emit("".concat(eventType, "Edge"), __assign(__assign({}, baseEvent), { edge: edge })); } return _this.emit("".concat(eventType, "Stage"), baseEvent); }; }; this.activeListeners.handleClick = createMouseListener("click"); this.activeListeners.handleRightClick = createMouseListener("rightClick"); this.activeListeners.handleDoubleClick = createMouseListener("doubleClick"); this.activeListeners.handleWheel = createMouseListener("wheel"); this.activeListeners.handleDown = createMouseListener("down"); this.mouseCaptor.on("mousemove", this.activeListeners.handleMove); this.mouseCaptor.on("click", this.activeListeners.handleClick); this.mouseCaptor.on("rightClick", this.activeListeners.handleRightClick); this.mouseCaptor.on("doubleClick", this.activeListeners.handleDoubleClick); this.mouseCaptor.on("wheel", this.activeListeners.handleWheel); this.mouseCaptor.on("mousedown", this.activeListeners.handleDown); // TODO // Deal with Touch captor events return this; }; /** * Method binding graph handlers * * @return {Sigma} */ Sigma.prototype.bindGraphHandlers = function () { var _this = this; var graph = this.graph; this.activeListeners.graphUpdate = function () { _this.needToProcess = true; _this._scheduleRefresh(); }; this.activeListeners.softGraphUpdate = function () { _this.needToSoftProcess = true; _this._scheduleRefresh(); }; this.activeListeners.dropNodeGraphUpdate = function (e) { delete _this.nodeDataCache[e.key]; if (_this.hoveredNode === e.key) _this.hoveredNode = null; _this.activeListeners.graphUpdate(); }; this.activeListeners.dropEdgeGraphUpdate = function (e) { delete _this.edgeDataCache[e.key]; if (_this.hoveredEdge === e.key) _this.hoveredEdge = null; _this.activeListeners.graphUpdate(); }; this.activeListeners.clearEdgesGraphUpdate = function () { _this.edgeDataCache = {}; _this.hoveredEdge = null; _this.activeListeners.graphUpdate(); }; this.activeListeners.clearGraphUpdate = function () { _this.nodeDataCache = {}; _this.hoveredNode = null; _this.activeListeners.clearEdgesGraphUpdate(); }; graph.on("nodeAdded", this.activeListeners.graphUpdate); graph.on("nodeDropped", this.activeListeners.dropNodeGraphUpdate); graph.on("nodeAttributesUpdated", this.activeListeners.softGraphUpdate); graph.on("eachNodeAttributesUpdated", this.activeListeners.graphUpdate); graph.on("edgeAdded", this.activeListeners.graphUpdate); graph.on("edgeDropped", this.activeListeners.dropEdgeGraphUpdate); graph.on("edgeAttributesUpdated", this.activeListeners.softGraphUpdate); graph.on("eachEdgeAttributesUpdated", this.activeListeners.graphUpdate); graph.on("edgesCleared", this.activeListeners.clearEdgesGraphUpdate); graph.on("cleared", this.activeListeners.clearGraphUpdate); return this; }; /** * Method used to unbind handlers from the graph. * * @return {undefined} */ Sigma.prototype.unbindGraphHandlers = function () { var graph = this.graph; graph.removeListener("nodeAdded", this.activeListeners.graphUpdate); graph.removeListener("nodeDropped", this.activeListeners.dropNodeGraphUpdate); graph.removeListener("nodeAttributesUpdated", this.activeListeners.softGraphUpdate); graph.removeListener("eachNodeAttributesUpdated", this.activeListeners.graphUpdate); graph.removeListener("edgeAdded", this.activeListeners.graphUpdate); graph.removeListener("edgeDropped", this.activeListeners.dropEdgeGraphUpdate); graph.removeListener("edgeAttributesUpdated", this.activeListeners.softGraphUpdate); graph.removeListener("eachEdgeAttributesUpdated", this.activeListeners.graphUpdate); graph.removeListener("edgesCleared", this.activeListeners.clearEdgesGraphUpdate); graph.removeListener("cleared", this.activeListeners.clearGraphUpdate); }; /** * Method dealing with "leaveEdge" and "enterEdge" events. * * @return {Sigma} */ Sigma.prototype.checkEdgeHoverEvents = function (payload) { var edgeToHover = this.hoveredNode ? null : this.getEdgeAtPoint(payload.event.x, payload.event.y); if (edgeToHover !== this.hoveredEdge) { if (this.hoveredEdge) this.emit("leaveEdge", __assign(__assign({}, payload), { edge: this.hoveredEdge })); if (edgeToHover) this.emit("enterEdge", __assign(__assign({}, payload), { edge: edgeToHover })); this.hoveredEdge = edgeToHover; } return this; }; /** * Method looking for an edge colliding with a given point at (x, y). Returns * the key of the edge if any, or null else. */ Sigma.prototype.getEdgeAtPoint = function (x, y) { var e_1, _a; var _this = this; var _b = this, edgeDataCache = _b.edgeDataCache, nodeDataCache = _b.nodeDataCache; // Check first that pixel is colored: // Note that mouse positions must be corrected by pixel ratio to correctly // index the drawing buffer. if (!(0, edge_collisions_1.isPixelColored)(this.webGLContexts.edges, x * this.pixelRatio, y * this.pixelRatio)) return null; // Check for each edge if it collides with the point: var _c = this.viewportToGraph({ x: x, y: y }), graphX = _c.x, graphY = _c.y; // To translate edge thicknesses to the graph system, we observe by how much // the length of a non-null edge is transformed to between the graph system // and the viewport system: var transformationRatio = 0; this.graph.someEdge(function (key, _, sourceId, targetId, _a, _b) { var xs = _a.x, ys = _a.y; var xt = _b.x, yt = _b.y; if (edgeDataCache[key].hidden || nodeDataCache[sourceId].hidden || nodeDataCache[targetId].hidden) return false; if (xs !== xt || ys !== yt) { var graphLength = Math.sqrt(Math.pow(xt - xs, 2) + Math.pow(yt - ys, 2)); var _c = _this.graphToViewport({ x: xs, y: ys }), vp_xs = _c.x, vp_ys = _c.y; var _d = _this.graphToViewport({ x: xt, y: yt }), vp_xt = _d.x, vp_yt = _d.y; var viewportLength = Math.sqrt(Math.pow(vp_xt - vp_xs, 2) + Math.pow(vp_yt - vp_ys, 2)); transformationRatio = graphLength / viewportLength; return true; } }); // If no non-null edge has been found, return null: if (!transformationRatio) return null; // Now we can look for matching edges: var edges = this.graph.filterEdges(function (key, edgeAttributes, sourceId, targetId, sourcePosition, targetPosition) { if (edgeDataCache[key].hidden || nodeDataCache[sourceId].hidden || nodeDataCache[targetId].hidden) return false; if ((0, edge_collisions_1.doEdgeCollideWithPoint)(graphX, graphY, sourcePosition.x, sourcePosition.y, targetPosition.x, targetPosition.y, // Adapt the edge size to the zoom ratio: (edgeDataCache[key].size * transformationRatio) / _this.cameraSizeRatio)) { return true; } }); if (edges.length === 0) return null; // no edges found // if none of the edges have a zIndex, selected the most recently created one to match the rendering order var selectedEdge = edges[edges.length - 1]; // otherwise select edge with highest zIndex var highestZIndex = -Infinity; try { for (var edges_1 = __values(edges), edges_1_1 = edges_1.next(); !edges_1_1.done; edges_1_1 = edges_1.next()) { var edge = edges_1_1.value; var zIndex = this.graph.getEdgeAttribute(edge, "zIndex"); if (zIndex >= highestZIndex) { selectedEdge = edge; highestZIndex = zIndex; } } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (edges_1_1 && !edges_1_1.done && (_a = edges_1.return)) _a.call(edges_1); } finally { if (e_1) throw e_1.error; } } return selectedEdge; }; /** * Method used to process the whole graph's data. * * @return {Sigma} */ Sigma.prototype.process = function (keepArrays) { var _this = this; if (keepArrays === void 0) { keepArrays = false; } var graph = this.graph; var settings = this.settings; var dimensions = this.getDimensions(); var nodeZExtent = [Infinity, -Infinity]; var edgeZExtent = [Infinity, -Infinity]; // Clearing the quad this.quadtree.clear(); // Resetting the label grid // TODO: it's probably better to do this explicitly or on resizes for layout and anims this.labelGrid.resizeAndClear(dimensions, settings.labelGridCellSize); // Clear the highlightedNodes this.highlightedNodes = new Set(); // Computing extents this.nodeExtent = (0, utils_1.graphExtent)(graph); // Resetting `forceLabel` indices this.nodesWithForcedLabels = []; this.edgesWithForcedLabels = []; // NOTE: it is important to compute this matrix after computing the node's extent // because #.getGraphDimensions relies on it var nullCamera = new camera_1.default(); var nullCameraMatrix = (0, utils_1.matrixFromCamera)(nullCamera.getState(), this.getDimensions(), this.getGraphDimensions(), this.getSetting("stagePadding") || 0); // Rescaling function this.normalizationFunction = (0, utils_1.createNormalizationFunction)(this.customBBox || this.nodeExtent); var nodesPerPrograms = {}; var nodes = graph.nodes(); for (var i = 0, l = nodes.length; i < l; i++) { var node = nodes[i]; // Node display data resolution: // 1. First we get the node's attributes // 2. We optionally reduce them using the function provided by the user // Note that this function must return a total object and won't be merged // 3. We apply our defaults, while running some vital checks // 4. We apply the normalization function // We shallow copy node data to avoid dangerous behaviors from reducers var attr = Object.assign({}, graph.getNodeAttributes(node)); if (settings.nodeReducer) attr = settings.nodeReducer(node, attr); var data = applyNodeDefaults(this.settings, node, attr); nodesPerPrograms[data.type] = (nodesPerPrograms[data.type] || 0) + 1; this.nodeDataCache[node] = data; this.normalizationFunction.applyTo(data); if (data.forceLabel) this.nodesWithForcedLabels.push(node); if (this.settings.zIndex) { if (data.zIndex < nodeZExtent[0]) nodeZExtent[0] = data.zIndex; if (data.zIndex > nodeZExtent[1]) nodeZExtent[1] = data.zIndex; } } for (var type in this.nodePrograms) { if (!this.nodePrograms.hasOwnProperty(type)) { throw new Error("Sigma: could not find a suitable program for node type \"".concat(type, "\"!")); } if (!keepArrays) this.nodePrograms[type].allocate(nodesPerPrograms[type] || 0); // We reset that count here, so that we can reuse it while calling the Program#process methods: nodesPerPrograms[type] = 0; } // Handling node z-index // TODO: z-index needs us to compute display data before hand if (this.settings.zIndex && nodeZExtent[0] !== nodeZExtent[1]) nodes = (0, utils_1.zIndexOrdering)(nodeZExtent, function (node) { return _this.nodeDataCache[node].zIndex; }, nodes); for (var i = 0, l = nodes.length; i < l; i++) { var node = nodes[i]; var data = this.nodeDataCache[node]; this.quadtree.add(node, data.x, 1 - data.y, data.size / this.width); if (typeof data.label === "string" && !data.hidden) this.labelGrid.add(node, data.size, this.framedGraphToViewport(data, { matrix: nullCameraMatrix })); var nodeProgram = this.nodePrograms[data.type]; if (!nodeProgram) throw new Error("Sigma: could not find a suitable program for node type \"".concat(data.type, "\"!")); nodeProgram.process(data, data.hidden, nodesPerPrograms[data.type]++); // Save the node in the highlighted set if needed if (data.highlighted && !data.hidden) this.highlightedNodes.add(node); } this.labelGrid.organize(); var edgesPerPrograms = {}; var edges = graph.edges(); for (var i = 0, l = edges.length; i < l; i++) { var edge = edges[i]; // Edge display data resolution: // 1. First we get the edge's attributes // 2. We optionally reduce them using the function provided by the user // Note that this function must return a total object and won't be merged // 3. We apply our defaults, while running some vital checks // We shallow copy edge data to avoid dangerous behaviors from reducers var attr = Object.assign({}, graph.getEdgeAttributes(edge)); if (settings.edgeReducer) attr = settings.edgeReducer(edge, attr); var data = applyEdgeDefaults(this.settings, edge, attr); edgesPerPrograms[data.type] = (edgesPerPrograms[data.type] || 0) + 1; this.edgeDataCache[edge] = data; if (data.forceLabel && !data.hidden) this.edgesWithForcedLabels.push(edge); if (this.settings.zIndex) { if (data.zIndex < edgeZExtent[0]) edgeZExtent[0] = data.zIndex; if (data.zIndex > edgeZExtent[1]) edgeZExtent[1] = data.zIndex; } } for (var type in this.edgePrograms) { if (!this.edgePrograms.hasOwnProperty(type)) { throw new Error("Sigma: could not find a suitable program for edge type \"".concat(type, "\"!")); } if (!keepArrays) this.edgePrograms[type].allocate(edgesPerPrograms[type] || 0); // We reset that count here, so that we can reuse it while calling the Program#process methods: edgesPerPrograms[type] = 0; } // Handling edge z-index if (this.settings.zIndex && edgeZExtent[0] !== edgeZExtent[1]) edges = (0, utils_1.zIndexOrdering)(edgeZExtent, function (edge) { return _this.edgeDataCache[edge].zIndex; }, edges); for (var i = 0, l = edges.length; i < l; i++) { var edge = edges[i]; var data = this.edgeDataCache[edge]; var extremities = graph.extremities(edge), sourceData = this.nodeDataCache[extremities[0]], targetData = this.nodeDataCache[extremities[1]]; var hidden = data.hidden || sourceData.hidden || targetData.hidden; this.edgePrograms[data.type].process(sourceData, targetData, data, hidden, edgesPerPrograms[data.type]++); } for (var type in this.edgePrograms) { var program = this.edgePrograms[type]; if (!keepArrays && typeof program.computeIndices === "function") program.computeIndices(); } return this; }; /** * Method that backports potential settings updates where it's needed. * @private */ Sigma.prototype.handleSettingsUpdate = function () { this.camera.minRatio = this.settings.minCameraRatio; this.camera.maxRatio = this.settings.maxCameraRatio; this.camera.setState(this.camera.validateState(this.camera.getState())); return this; }; /** * Method that decides whether to reprocess graph or not, and then render the * graph. * * @return {Sigma} */ Sigma.prototype._refresh = function () { // Do we need to process data? if (this.needToProcess) { this.process(); } else if (this.needToSoftProcess) { this.process(true); } // Resetting state this.needToProcess = false; this.needToSoftProcess = false; // Rendering this.render(); return this; }; /** * Method that schedules a `_refresh` call if none has been scheduled yet. It * will then be processed next available frame. * * @return {Sigma} */ Sigma.prototype._scheduleRefresh = function () { var _this = this; if (!this.renderFrame) { this.renderFrame = (0, utils_1.requestFrame)(function () { _this._refresh(); _this.renderFrame = null; }); } return this; }; /** * Method used to render labels. * * @return {Sigma} */ Sigma.prototype.renderLabels = function () { if (!this.settings.renderLabels) return this; var cameraState = this.camera.getState(); // Selecting labels to draw var labelsToDisplay = this.labelGrid.getLabelsToDisplay(cameraState.ratio, this.settings.labelDensity); (0, extend_1.default)(labelsToDisplay, this.nodesWithForcedLabels); this.displayedLabels = new Set(); // Drawing labels var context = this.canvasContexts.labels; for (var i = 0, l = labelsToDisplay.length; i < l; i++) { var node = labelsToDisplay[i]; var data = this.nodeDataCache[node]; // If the node was already drawn (like if it is eligible AND has // `forceLabel`), we don't want to draw it again // NOTE: we can do better probably if (this.displayedLabels.has(node)) continue; // If the node is hidden, we don't need to display its label obviously if (data.hidden) continue; var _a = this.framedGraphToViewport(data), x = _a.x, y = _a.y; // NOTE: we can cache the labels we need to render until the camera's ratio changes var size = this.scaleSize(data.size); // Is node big enough? if (!data.forceLabel && size < this.settings.labelRenderedSizeThreshold) continue; // Is node actually on screen (with some margin) // NOTE: we used to rely on the quadtree for this, but the coordinates // conversion make it unreliable and at that point we already converted // to viewport coordinates and since the label grid already culls the // number of potential labels to display this looks like a good // performance compromise. // NOTE: labelGrid.getLabelsToDisplay could probably optimize by not // considering cells obviously outside of the range of the current // view rectangle. if (x < -X_LABEL_MARGIN || x > this.width + X_LABEL_MARGIN || y < -Y_LABEL_MARGIN || y > this.height + Y_LABEL_MARGIN) continue; // Because displayed edge labels depend directly on actually rendered node // labels, we need to only add to this.displayedLabels nodes whose label // is rendered. // This makes this.displayedLabels depend on viewport, which might become // an issue once we start memoizing getLabelsToDisplay. this.displayedLabels.add(node); this.settings.labelRenderer(context, __assign(__assign({ key: node }, data), { size: size, x: x, y: y }), this.settings); } return this; }; /** * Method used to render edge labels, based on which node labels were * rendered. * * @return {Sigma} */ Sigma.prototype.renderEdgeLabels = function () { if (!this.settings.renderEdgeLabels) return this; var context = this.canvasContexts.edgeLabels; // Clearing context.clearRect(0, 0, this.width, this.height); var edgeLabelsToDisplay = (0, labels_1.edgeLabelsToDisplayFromNodes)({ graph: this.graph, hoveredNode: this.hoveredNode, displayedNodeLabels: this.displayedLabels, highlightedNodes: this.highlightedNodes, }).concat(this.edgesWithForcedLabels); var displayedLabels = new Set(); for (var i = 0, l = edgeLabelsToDisplay.length; i < l; i++) { var edge = edgeLabelsToDisplay[i], extremities = this.graph.extremities(edge), sourceData = this.nodeDataCache[extremities[0]], targetData = this.nodeDataCache[extremities[1]], edgeData = this.edgeDataCache[edge]; // If the edge was already drawn (like if it is eligible AND has // `forceLabel`), we don't want to draw it again if (displayedLabels.has(edge)) continue; // If the edge is hidden we don't need to display its label // NOTE: the test on sourceData & targetData is probably paranoid at this point? if (edgeData.hidden || sourceData.hidden || targetData.hidden) { continue; } this.settings.edgeLabelRenderer(context, __assign(__assign({ key: edge }, edgeData), { size: this.scaleSize(edgeData.size) }), __assign(__assign(__assign({ key: extremities[0] }, sourceData), this.framedGraphToViewport(sourceData)), { size: this.scaleSize(sourceData.size) }), __assign(__assign(__assign({ key: extremities[1] }, targetData), this.framedGraphToViewport(targetData)), { size: this.scaleSize(targetData.size) }), this.settings); displayedLabels.add(edge); } return this; }; /** * Method used to render the highlighted nodes. * * @return {Sigma} */ Sigma.prototype.renderHighlightedNodes = function () { var _this = this; var context = this.canvasContexts.hovers; // Clearing context.clearRect(0, 0, this.width, this.height); // Rendering var render = function (node) { var data = _this.nodeDataCache[node]; var _a = _this.framedGraphToViewport(data), x = _a.x, y = _a.y; var size = _this.scaleSize(data.size); _this.settings.hoverRenderer(context, __assign(__assign({ key: node }, data), { size: size, x: x, y: y }), _this.settings); }; var nodesToRender = []; if (this.hoveredNode && !this.nodeDataCache[this.hoveredNode].hidden) { nodesToRender.push(this.hoveredNode); } this.highlightedNodes.forEach(function (node) { // The hovered node has already been highlighted if (node !== _this.hoveredNode) nodesToRender.push(node); }); // Draw labels: nodesToRender.forEach(function (node) { return render(node); }); // Draw WebGL nodes on top of the labels: var nodesPerPrograms = {}; // 1. Count nodes per type: nodesToRender.forEach(function (node) { var type = _this.nodeDataCache[node].type; nodesPerPrograms[type] = (nodesPerPrograms[type] || 0) + 1; }); // 2. Allocate for each type for the proper number of nodes for (var type in this.nodeHoverPrograms) { this.nodeHoverPrograms[type].allocate(nodesPerPrograms[type] || 0); // Also reset count, to use when rendering: nodesPerPrograms[type] = 0; } // 3. Process all nodes to render: nodesToRender.forEach(function (node) { var data = _this.nodeDataCache[node]; _this.nodeHoverPrograms[data.type].process(data, data.hidden, nodesPerPrograms[data.type]++); }); // 4. Clear hovered nodes layer: this.webGLContexts.hoverNodes.clear(this.webGLContexts.hoverNodes.COLOR_BUFFER_BIT); // 5. Render: for (var type in this.nodeHoverPrograms) { var program = this.nodeHoverPrograms[type]; program.bind(); program.bufferData(); program.render({ matrix: this.matrix, width: this.width, height: this.height, ratio: this.camera.ratio, correctionRatio: this.correctionRatio / this.camera.ratio, scalingRatio: this.pixelRatio, }); } }; /** * Method used to schedule a hover render. * */ Sigma.prototype.scheduleHighlightedNodesRender = function () { var _this = this; if (this.renderHighlightedNodesFrame || this.renderFrame) return; this.renderHighlightedNodesFrame = (0, utils_1.requestFrame)(function () { // Resetting state _this.renderHighlightedNodesFrame = null; // Rendering _this.renderHighlightedNodes(); _this.renderEdgeLabels(); }); }; /** * Method used to render. * * @return {Sigma} */ Sigma.prototype.render = function () { var _this = this; this.emit("beforeRender"); var exitRender = function () { _this.emit("afterRender"); return _this; }; // If a render was scheduled, we cancel it if (this.renderFrame) { (0, utils_1.cancelFrame)(this.renderFrame); this.renderFrame = null; this.needToProcess = false; this.needToSoftProcess = false; } // First we need to resize this.resize(); // Clearing the canvases this.clear(); // Recomputing useful camera-related values: this.updateCachedValues(); // If we have no nodes we can stop right there if (!this.graph.order) return exitRender(); // TODO: improve this heuristic or move to the captor itself? // TODO: deal with the touch captor here as well var mouseCaptor = this.mouseCaptor; var moving = this.camera.isAnimated() || mouseCaptor.isMoving || mouseCaptor.draggedEvents || mouseCaptor.currentWheelDirection; // Then we need to extract a matrix from the camera var cameraState = this.camera.getState(); var viewportDimensions = this.getDimensions(); var graphDimensions = this.getGraphDimensions(); var padding = this.getSetting("stagePadding") || 0; this.matrix = (0, utils_1.matrixFromCamera)(cameraState, viewportDimensions, graphDimensions, padding); this.invMatrix = (0, utils_1.matrixFromCamera)(cameraState, viewportDimensions, graphDimensions, padding, true); this.correctionRatio = (0, utils_1.getMatrixImpact)(this.matrix, cameraState, viewportDimensions); // Drawing nodes for (var type in this.nodePrograms) { var program = this.nodePrograms[type]; program.bind(); program.bufferData(); program.render({ matrix: this.matrix, width: this.width, height: this.height, ratio: cameraState.ratio, correctionRatio: this.correctionRatio / cameraState.ratio, scalingRatio: this.pixelRatio, }); } // Drawing edges if (!this.settings.hideEdgesOnMove || !moving) { for (var type in this.edgePrograms) { var program = this.edgePrograms[type]; program.bind(); program.bufferData(); program.render({ matrix: this.matrix, width: this.width, height: this.height, ratio: cameraState.ratio, correctionRatio: this.correctionRatio / cameraState.ratio, scalingRatio: this.pixelRatio, }); } } // Do not display labels on move per setting if (this.settings.hideLabelsOnMove && moving) return exitRender(); this.renderLabels(); this.renderEdgeLabels(); this.renderHighlightedNodes(); return exitRender(); }; /** * Internal method used to update expensive and therefore cached values * each time the camera state is updated. */ Sigma.prototype.updateCachedValues = function () { var ratio = this.camera.getState().ratio; this.cameraSizeRatio = Math.sqrt(ratio); }; /**--------------------------------------------------------------------------- * Public API. **--------------------------------------------------------------------------- */ /** * Method returning the renderer's camera. * * @return {Camera} */ Sigma.prototype.getCamera = function () { return this.camera; }; /** * Method returning the container DOM element. * * @return {HTMLElement} */ Sigma.prototype.getContainer = function () { return this.container; }; /** * Method returning the renderer's graph. * * @return {Graph} */ Sigma.prototype.getGraph = function () { return this.graph; }; /** * Method used to set the renderer's graph. * * @return {Graph} */ Sigma.prototype.setGraph = function (graph) { if (graph === this.graph) return; // Unbinding handlers on the current graph this.unbindGraphHandlers(); // Clearing the graph data caches this.nodeDataCache = {}; this.edgeDataCache = {}; // Cleaning renderer state tied to the current graph this.displayedLabels.clear(); this.highlightedNodes.clear(); this.hoveredNode = null; this.hoveredEdge = null; this.nodesWithForcedLabels.length = 0; this.edgesWithForcedLabels.length = 0; if (this.checkEdgesEventsFrame !== null) { (0, utils_1.cancelFrame)(this.checkEdgesEventsFrame); this.checkEdgesEventsFrame = null; } // Installing new graph this.graph = graph; // Binding new handlers this.bindGraphHandlers(); // Re-rendering now to avoid discrepancies from now to next frame this.process(); this.render(); }; /** * Method returning the mouse captor. * * @return {MouseCaptor} */ Sigma.prototype.getMouseCaptor = function () { return this.mouseCaptor; }; /** * Method returning the touch captor.