sigma
Version:
A JavaScript library dedicated to graph drawing.
986 lines (985 loc) • 41 kB
JavaScript
"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;