UNPKG

sigma

Version:

A JavaScript library dedicated to graph drawing.

986 lines (985 loc) 41 kB
"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 * ======== * @module */ var events_1 = require("events"); var extent_1 = __importDefault(require("graphology-metrics/extent")); var camera_1 = __importDefault(require("./core/camera")); var mouse_1 = __importDefault(require("./core/captors/mouse")); var quadtree_1 = __importDefault(require("./core/quadtree")); var utils_1 = require("./utils"); var labels_1 = require("./core/labels"); var settings_1 = require("./settings"); var touch_1 = __importDefault(require("./core/captors/touch")); var nodeExtent = extent_1.default.nodeExtent, edgeExtent = extent_1.default.edgeExtent; /** * Constants. */ var PIXEL_RATIO = utils_1.getPixelRatio(); var WEBGL_OVERSAMPLING_RATIO = utils_1.getPixelRatio(); /** * 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 \"" + 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 = ""; if (!data.size) data.size = 2; if (!data.hasOwnProperty("hidden")) data.hidden = false; if (!data.hasOwnProperty("highlighted")) data.highlighted = false; } 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; } /** * 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.nodeDataCache = {}; _this.edgeDataCache = {}; _this.nodeKeyToIndex = {}; _this.edgeKeyToIndex = {}; _this.nodeExtent = null; _this.edgeExtent = null; _this.normalizationFunction = utils_1.createNormalizationFunction({ x: [-Infinity, Infinity], y: [-Infinity, Infinity], }); // Starting dimensions _this.width = 0; _this.height = 0; // State _this.highlightedNodes = new Set(); _this.displayedLabels = new Set(); _this.hoveredNode = null; _this.renderFrame = null; _this.renderHighlightedNodesFrame = null; _this.needToProcess = false; _this.needToSoftProcess = false; // programs _this.nodePrograms = {}; _this.edgePrograms = {}; _this.settings = utils_1.assignDeep({}, settings_1.DEFAULT_SETTINGS, settings); // Validating settings_1.validateSettings(_this.settings); 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; _this.initializeCache(); // Initializing contexts _this.createWebGLContext("edges"); _this.createWebGLContext("nodes"); _this.createCanvasContext("edgeLabels"); _this.createCanvasContext("labels"); _this.createCanvasContext("hovers"); _this.createCanvasContext("mouse"); // Blending var gl = _this.webGLContexts.nodes; gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); gl.enable(gl.BLEND); gl = _this.webGLContexts.edges; 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); } for (var type in _this.settings.edgeProgramClasses) { var EdgeProgramClass = _this.settings.edgeProgramClasses[type]; _this.edgePrograms[type] = new EdgeProgramClass(_this.webGLContexts.edges); } // 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.camera); _this.touchCaptor = new touch_1.default(_this.elements.mouse, _this.camera); // Binding event handlers _this.bindEventHandlers(); // Binding graph handlers _this.bindGraphHandlers(); // 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 = utils_1.createElement("canvas", { position: "absolute", }, { class: "sigma-" + 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. * @return {Sigma} */ Sigma.prototype.createWebGLContext = function (id) { var canvas = this.createCanvas(id); var contextOptions = { preserveDrawingBuffer: false, antialias: false, }; 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 used to initialize display data cache. * * @return {Sigma} */ Sigma.prototype.initializeCache = function () { var _this = this; var graph = this.graph; // NOTE: the data caches are never reset to avoid paying a GC cost // But this could prove to be a bad decision. In which case just "reset" // them here. var i = 0; graph.forEachNode(function (key) { _this.nodeKeyToIndex[key] = i++; _this.nodeDataCache[key] = {}; }); i = 0; graph.forEachEdge(function (key) { _this.edgeKeyToIndex[key] = i++; _this.edgeDataCache[key] = {}; }); }; /** * 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 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); // Function checking if the mouse is on the given node var mouseIsOnNode = function (mouseX, mouseY, nodeX, nodeY, size) { return (mouseX > nodeX - size && mouseX < nodeX + size && mouseY > nodeY - size && mouseY < nodeY + size && Math.sqrt(Math.pow(mouseX - nodeX, 2) + Math.pow(mouseY - nodeY, 2)) < size); }; // Function returning the nodes in the mouse's quad var getQuadNodes = function (mouseX, mouseY) { var mouseGraphPosition = _this.camera.viewportToFramedGraph({ width: _this.width, height: _this.height }, { x: mouseX, y: mouseY }); // TODO: minus 1? lol return _this.quadtree.point(mouseGraphPosition.x, 1 - mouseGraphPosition.y); }; // Handling mouse move this.activeListeners.handleMove = function (e) { // NOTE: for the canvas renderer, testing the pixel's alpha should // give some boost but this slows things down for WebGL empirically. // TODO: this should be a method from the camera (or can be passed to graph to display somehow) var sizeRatio = Math.pow(_this.camera.getState().ratio, 0.5); var quadNodes = getQuadNodes(e.x, e.y); var dimensions = { width: _this.width, height: _this.height }; // We will hover the node whose center is closest to mouse var minDistance = Infinity, nodeToHover = null; for (var i = 0, l = quadNodes.length; i < l; i++) { var node = quadNodes[i]; var data = _this.nodeDataCache[node]; var pos = _this.camera.framedGraphToViewport(dimensions, data); var size = data.size / sizeRatio; if (mouseIsOnNode(e.x, e.y, pos.x, pos.y, size)) { var distance = Math.sqrt(Math.pow(e.x - pos.x, 2) + Math.pow(e.y - pos.y, 2)); // TODO: sort by min size also for cases where center is the same if (distance < minDistance) { minDistance = distance; nodeToHover = node; } } } if (nodeToHover && _this.hoveredNode !== nodeToHover && !_this.nodeDataCache[nodeToHover].hidden) { // Handling passing from one node to the other directly if (_this.hoveredNode) _this.emit("leaveNode", { node: _this.hoveredNode }); _this.hoveredNode = nodeToHover; _this.emit("enterNode", { 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.camera.framedGraphToViewport(dimensions, data); var size = data.size / sizeRatio; if (!mouseIsOnNode(e.x, e.y, pos.x, pos.y, size)) { var node = _this.hoveredNode; _this.hoveredNode = null; _this.emit("leaveNode", { node: node }); return _this.scheduleHighlightedNodesRender(); } } }; // Handling click var createClickListener = function (eventType) { return function (e) { var sizeRatio = Math.pow(_this.camera.getState().ratio, 0.5); var quadNodes = getQuadNodes(e.x, e.y); var dimensions = { width: _this.width, height: _this.height }; for (var i = 0, l = quadNodes.length; i < l; i++) { var node = quadNodes[i]; var data = _this.nodeDataCache[node]; var pos = _this.camera.framedGraphToViewport(dimensions, data); var size = data.size / sizeRatio; if (mouseIsOnNode(e.x, e.y, pos.x, pos.y, size)) return _this.emit(eventType + "Node", { node: node, captor: e, event: e }); } return _this.emit(eventType + "Stage", { event: e }); }; }; this.activeListeners.handleClick = createClickListener("click"); this.activeListeners.handleRightClick = createClickListener("rightClick"); this.activeListeners.handleDown = createClickListener("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("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.addNodeGraphUpdate = function (e) { // Adding entry to cache _this.nodeKeyToIndex[e.key] = graph.order - 1; _this.nodeDataCache[e.key] = {}; _this.activeListeners.graphUpdate(); }; this.activeListeners.addEdgeGraphUpdate = function (e) { // Adding entry to cache _this.nodeKeyToIndex[e.key] = graph.order - 1; _this.edgeDataCache[e.key] = {}; _this.activeListeners.graphUpdate(); }; // TODO: clean cache on drop! // TODO: bind this on composed state events // TODO: it could be possible to update only specific node etc. by holding // a fixed-size pool of updated items graph.on("nodeAdded", this.activeListeners.addNodeGraphUpdate); graph.on("nodeDropped", this.activeListeners.graphUpdate); graph.on("nodeAttributesUpdated", this.activeListeners.softGraphUpdate); graph.on("eachNodeAttributesUpdated", this.activeListeners.graphUpdate); graph.on("edgeAdded", this.activeListeners.addEdgeGraphUpdate); graph.on("edgeDropped", this.activeListeners.graphUpdate); graph.on("edgeAttributesUpdated", this.activeListeners.softGraphUpdate); graph.on("eachEdgeAttributesUpdated", this.activeListeners.graphUpdate); graph.on("edgesCleared", this.activeListeners.graphUpdate); graph.on("cleared", this.activeListeners.graphUpdate); return this; }; /** * Method used to process the whole graph's data. * * @return {Sigma} */ Sigma.prototype.process = function (keepArrays) { if (keepArrays === void 0) { keepArrays = false; } var graph = this.graph, settings = this.settings; // Clearing the quad this.quadtree.clear(); // Clear the highlightedNodes this.highlightedNodes = new Set(); // Computing extents var nodeExtentProperties = ["x", "y", "z"]; if (this.settings.zIndex) { nodeExtentProperties.push("z"); this.edgeExtent = edgeExtent(graph, ["z"]); } this.nodeExtent = nodeExtent(graph, nodeExtentProperties); // Rescaling function this.normalizationFunction = utils_1.createNormalizationFunction(this.nodeExtent); var nodeProgram = this.nodePrograms[this.settings.defaultNodeType]; if (!keepArrays) nodeProgram.allocate(graph.order); var nodes = graph.nodes(); // Handling node z-index // TODO: z-index needs us to compute display data before hand if (this.settings.zIndex) nodes = utils_1.zIndexOrdering(this.nodeExtent.z, function (node) { return graph.getNodeAttribute(node, "z"); }, 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 var data = graph.getNodeAttributes(node); if (settings.nodeReducer) data = settings.nodeReducer(node, data); // We shallow copy the data to avoid mutating both the graph and the reducer's result data = Object.assign(this.nodeDataCache[node], data); applyNodeDefaults(this.settings, node, data); this.normalizationFunction.applyTo(data); this.quadtree.add(node, data.x, 1 - data.y, data.size / this.width); nodeProgram.process(data, data.hidden, i); // Save the node in the highlighted set if needed if (data.highlighted && !data.hidden) this.highlightedNodes.add(node); this.nodeKeyToIndex[node] = i; } // TODO: maybe we should bind and buffer as part of rendering? // We also need to find when it is useful and when it's really not nodeProgram.bind(); nodeProgram.bufferData(); var edgeProgram = this.edgePrograms[this.settings.defaultEdgeType]; if (!keepArrays) edgeProgram.allocate(graph.size); var edges = graph.edges(); // Handling edge z-index if (this.settings.zIndex && this.edgeExtent) edges = utils_1.zIndexOrdering(this.edgeExtent.z, function (edge) { return graph.getEdgeAttribute(edge, "z"); }, 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 var data = graph.getEdgeAttributes(edge); if (settings.edgeReducer) data = settings.edgeReducer(edge, data); // We shallow copy the data to avoid mutating both the graph and the reducer's result data = Object.assign(this.edgeDataCache[edge], data); applyEdgeDefaults(this.settings, edge, data); var extremities = graph.extremities(edge), sourceData = this.nodeDataCache[extremities[0]], targetData = this.nodeDataCache[extremities[1]]; var hidden = data.hidden || sourceData.hidden || targetData.hidden; edgeProgram.process(sourceData, targetData, data, hidden, i); this.nodeKeyToIndex[edge] = i; } // Computing edge indices if necessary if (!keepArrays && typeof edgeProgram.computeIndices === "function") edgeProgram.computeIndices(); // TODO: maybe we should bind and buffer as part of rendering? // We also need to find when it is useful and when it's really not edgeProgram.bind(); edgeProgram.bufferData(); 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 = 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(); var dimensions = { width: this.width, height: this.height }; // Finding visible nodes to display their labels var visibleNodes; if (cameraState.ratio >= 1) { // Camera is unzoomed so no need to ask the quadtree for visible nodes visibleNodes = this.graph.nodes(); } else { // Let's ask the quadtree var viewRectangle = this.camera.viewRectangle(dimensions); visibleNodes = this.quadtree.rectangle(viewRectangle.x1, 1 - viewRectangle.y1, viewRectangle.x2, 1 - viewRectangle.y2, viewRectangle.height); } // Selecting labels to draw var gridSettings = this.settings.labelGrid; var labelsToDisplay = labels_1.labelsToDisplayFromGrid({ cache: this.nodeDataCache, camera: this.camera, cell: gridSettings.cell, dimensions: dimensions, displayedLabels: this.displayedLabels, fontSize: this.settings.labelSize, graph: this.graph, renderedSizeThreshold: gridSettings.renderedSizeThreshold, visibleNodes: visibleNodes, }); // Drawing labels var context = this.canvasContexts.labels; var sizeRatio = Math.pow(cameraState.ratio, 0.5); for (var i = 0, l = labelsToDisplay.length; i < l; i++) { var data = this.nodeDataCache[labelsToDisplay[i]]; var _a = this.camera.framedGraphToViewport(dimensions, data), x = _a.x, y = _a.y; // TODO: we can cache the labels we need to render until the camera's ratio changes // TODO: this should be computed in the canvas components? var size = data.size / sizeRatio; this.settings.labelRenderer(context, { key: labelsToDisplay[i], label: data.label, color: "#000", size: size, x: x, y: y, }, this.settings); } // Caching visible nodes and displayed labels this.displayedLabels = new Set(labelsToDisplay); 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 cameraState = this.camera.getState(); var sizeRatio = Math.pow(cameraState.ratio, 0.5); var context = this.canvasContexts.edgeLabels; var dimensions = { width: this.width, height: this.height }; // Clearing context.clearRect(0, 0, this.width, this.height); var edgeLabelsToDisplay = labels_1.edgeLabelsToDisplayFromNodes({ nodeDataCache: this.nodeDataCache, edgeDataCache: this.edgeDataCache, graph: this.graph, hoveredNode: this.hoveredNode, displayedNodeLabels: this.displayedLabels, highlightedNodes: this.highlightedNodes, }); 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[edgeLabelsToDisplay[i]]; var _a = this.camera.framedGraphToViewport(dimensions, sourceData), sourceX = _a.x, sourceY = _a.y; var _b = this.camera.framedGraphToViewport(dimensions, targetData), targetX = _b.x, targetY = _b.y; // TODO: we can cache the labels we need to render until the camera's ratio changes // TODO: this should be computed in the canvas components? var size = edgeData.size / sizeRatio; this.settings.edgeLabelRenderer(context, { key: edge, label: edgeData.label, color: edgeData.color, size: size, }, { key: extremities[0], x: sourceX, y: sourceY, }, { key: extremities[1], x: targetX, y: targetY, }, this.settings); } return this; }; /** * Method used to render the highlighted nodes. * * @return {Sigma} */ Sigma.prototype.renderHighlightedNodes = function () { var _this = this; var camera = this.camera; var sizeRatio = Math.pow(camera.getState().ratio, 0.5); 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 = camera.framedGraphToViewport({ width: _this.width, height: _this.height }, data), x = _a.x, y = _a.y; var size = data.size / sizeRatio; _this.settings.hoverRenderer(context, { key: node, label: data.label, color: data.color, size: size, x: x, y: y, }, _this.settings); }; if (this.hoveredNode && !this.nodeDataCache[this.hoveredNode].hidden) { render(this.hoveredNode); } this.highlightedNodes.forEach(render); }; /** * Method used to schedule a hover render. * */ Sigma.prototype.scheduleHighlightedNodesRender = function () { var _this = this; if (this.renderHighlightedNodesFrame || this.renderFrame) return; this.renderHighlightedNodesFrame = utils_1.requestFrame(function () { // Resetting state _this.renderHighlightedNodesFrame = null; // Rendering _this.renderHighlightedNodes(); _this.renderEdgeLabels(); }); }; /** * Method used to render. * * @return {Sigma} */ Sigma.prototype.render = function () { // If a render was scheduled, we cancel it if (this.renderFrame) { 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(); // If we have no nodes we can stop right there if (!this.graph.order) return this; // 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(), cameraMatrix = utils_1.matrixFromCamera(cameraState, { width: this.width, height: this.height, }); var program; // Drawing nodes program = this.nodePrograms[this.settings.defaultNodeType]; program.render({ matrix: cameraMatrix, width: this.width, height: this.height, ratio: cameraState.ratio, nodesPowRatio: 0.5, scalingRatio: WEBGL_OVERSAMPLING_RATIO, }); // Drawing edges if (!this.settings.hideEdgesOnMove || !moving) { program = this.edgePrograms[this.settings.defaultEdgeType]; program.render({ matrix: cameraMatrix, width: this.width, height: this.height, ratio: cameraState.ratio, edgesPowRatio: 0.5, scalingRatio: WEBGL_OVERSAMPLING_RATIO, }); } // Do not display labels on move per setting if (this.settings.hideLabelsOnMove && moving) return this; this.renderLabels(); this.renderEdgeLabels(); this.renderHighlightedNodes(); return this; }; /**--------------------------------------------------------------------------- * Public API. **--------------------------------------------------------------------------- */ /** * Method returning the renderer's camera. * * @return {Camera} */ Sigma.prototype.getCamera = function () { return this.camera; }; /** * Method returning the renderer's graph. * * @return {Graph} */ Sigma.prototype.getGraph = function () { return this.graph; }; /** * Method returning the mouse captor. * * @return {MouseCaptor} */ Sigma.prototype.getMouseCaptor = function () { return this.mouseCaptor; }; /** * Method returning the touch captor. * * @return {TouchCaptor} */ Sigma.prototype.getTouchCaptor = function () { return this.touchCaptor; }; /** * Method returning the current renderer's dimensions. * * @return {Dimensions} */ Sigma.prototype.getDimensions = function () { return { width: this.width, height: this.height }; }; /** * Method used to get all the sigma node attributes. * It's usefull for example to get the position of a node * and to get values that are set by the nodeReducer * * @param {string} key - The node's key. * @return {Partial<NodeAttributes>} A copy of the desired node's attribute or undefined if not found */ Sigma.prototype.getNodeAttributes = function (key) { var node = this.nodeDataCache[key]; return node ? Object.assign({}, node) : undefined; }; /** * Method used to get all the sigma edge attributes. * It's usefull for example to get values that are set by the edgeReducer. * * @param {string} key - The edge's key. * @return {Partial<EdgeAttributes> | undefined} A copy of the desired edge's attribute or undefined if not found */ Sigma.prototype.getEdgeAttributes = function (key) { var edge = this.edgeDataCache[key]; return edge ? Object.assign({}, edge) : undefined; }; /** * Method returning the current value for a given setting key. * * @param {string} key - The setting key to get. * @return {any} The value attached to this setting key or undefined if not found */ Sigma.prototype.getSetting = function (key) { return this.settings[key]; }; /** * Method setting the value of a given setting key. Note that this will schedule * a new render next frame. * * @param {string} key - The setting key to set. * @param {any} value - The value to set. * @return {Sigma} */ Sigma.prototype.setSetting = function (key, value) { this.settings[key] = value; settings_1.validateSettings(this.settings); this.needToProcess = true; // TODO: some keys may work with only needToSoftProcess or even nothing this._scheduleRefresh(); return this; }; /** * Method updating the value of a given setting key using the provided function. * Note that this will schedule a new render next frame. * * @param {string} key - The setting key to set. * @param {any} value - The value to set. * @return {Sigma} */ Sigma.prototype.updateSetting = function (key, updater) { this.settings[key] = updater(this.settings[key]); settings_1.validateSettings(this.settings); this.needToProcess = true; // TODO: some keys may work with only needToSoftProcess or even nothing this._scheduleRefresh(); return this; }; /** * Method used to resize the renderer. * * @return {Sigma} */ Sigma.prototype.resize = function () { var previousWidth = this.width, previousHeight = this.height; this.width = this.container.offsetWidth; this.height = this.container.offsetHeight; if (this.width === 0) throw new Error("Sigma: container has no width."); if (this.height === 0) throw new Error("Sigma: container has no height."); // If nothing has changed, we can stop right here if (previousWidth === this.width && previousHeight === this.height) return this; // Sizing dom elements for (var id in this.elements) { var element = this.elements[id]; element.style.width = this.width + "px"; element.style.height = this.height + "px"; } // Sizing canvas contexts for (var id in this.canvasContexts) { this.elements[id].setAttribute("width", this.width * PIXEL_RATIO + "px"); this.elements[id].setAttribute("height", this.height * PIXEL_RATIO + "px"); if (PIXEL_RATIO !== 1) this.canvasContexts[id].scale(PIXEL_RATIO, PIXEL_RATIO); } // Sizing WebGL contexts for (var id in this.webGLContexts) { this.elements[id].setAttribute("width", this.width * WEBGL_OVERSAMPLING_RATIO + "px"); this.elements[id].setAttribute("height", this.height * WEBGL_OVERSAMPLING_RATIO + "px"); this.webGLContexts[id].viewport(0, 0, this.width * WEBGL_OVERSAMPLING_RATIO, this.height * WEBGL_OVERSAMPLING_RATIO); } return this; }; /** * Method used to clear all the canvases. * * @return {Sigma} */ Sigma.prototype.clear = function () { this.webGLContexts.nodes.clear(this.webGLContexts.nodes.COLOR_BUFFER_BIT); this.webGLContexts.edges.clear(this.webGLContexts.edges.COLOR_BUFFER_BIT); this.canvasContexts.labels.clearRect(0, 0, this.width, this.height); this.canvasContexts.hovers.clearRect(0, 0, this.width, this.height); this.canvasContexts.edgeLabels.clearRect(0, 0, this.width, this.height); return this; }; /** * Method used to refresh all computed data. * * @return {Sigma} */ Sigma.prototype.refresh = function () { this.needToProcess = true; this._refresh(); return this; }; /** * Method used to refresh all computed data, at the next available frame. * If this method has already been called this frame, then it will only render once at the next available frame. * * @return {Sigma} */ Sigma.prototype.scheduleRefresh = function () { this.needToProcess = true; this._scheduleRefresh(); return this; }; /** * Method used to translate a point's coordinates from the viewport system (pixel distance from the top-left of the * stage) to the graph system (the reference system of data as they are in the given graph instance). * * @param {Coordinates} viewportPoint */ Sigma.prototype.viewportToGraph = function (viewportPoint) { return this.normalizationFunction.inverse(this.camera.viewportToFramedGraph(this.getDimensions(), viewportPoint)); }; /** * Method used to translate a point's coordinates from the graph system (the reference system of data as they are in * the given graph instance) to the viewport system (pixel distance from the top-left of the stage). * * @param {Coordinates} graphPoint */ Sigma.prototype.graphToViewport = function (graphPoint) { return this.camera.framedGraphToViewport(this.getDimensions(), this.normalizationFunction(graphPoint)); }; /** * Method used to shut the container & release event listeners. * * @return {undefined} */ Sigma.prototype.kill = function () { var graph = this.graph; // Emitting "kill" events so that plugins and such can cleanup this.emit("kill"); // Releasing events this.removeAllListeners(); // Releasing camera handlers this.camera.removeListener("updated", this.activeListeners.camera); // Releasing DOM events & captors window.removeEventListener("resize", this.activeListeners.handleResize); this.mouseCaptor.kill(); this.touchCaptor.kill(); // Releasing graph handlers graph.removeListener("nodeAdded", this.activeListeners.addNodeGraphUpdate); graph.removeListener("nodeDropped", this.activeListeners.graphUpdate); graph.removeListener("nodeAttributesUpdated", this.activeListeners.softGraphUpdate); graph.removeListener("eachNodeAttributesUpdated", this.activeListeners.graphUpdate); graph.removeListener("edgeAdded", this.activeListeners.addEdgeGraphUpdate); graph.removeListener("edgeDropped", this.activeListeners.graphUpdate); graph.removeListener("edgeAttributesUpdated", this.activeListeners.softGraphUpdate); graph.removeListener("eachEdgeAttributesUpdated", this.activeListeners.graphUpdate); graph.removeListener("edgesCleared", this.activeListeners.graphUpdate); graph.removeListener("cleared", this.activeListeners.graphUpdate); // Releasing cache & state this.quadtree = new quadtree_1.default(); this.nodeDataCache = {}; this.edgeDataCache = {}; this.highlightedNodes = new Set(); this.displayedLabels = new Set(); // Clearing frames if (this.renderFrame) { utils_1.cancelFrame(this.renderFrame); this.renderFrame = null; } if (this.renderHighlightedNodesFrame) { utils_1.cancelFrame(this.renderHighlightedNodesFrame); this.renderHighlightedNodesFrame = null; } // Destroying canvases var container = this.container; while (container.firstChild) container.removeChild(container.firstChild); }; return Sigma; }(events_1.EventEmitter)); exports.default = Sigma;