sigma
Version:
A JavaScript library aimed at visualizing graphs of thousands of nodes and edges.
1,145 lines • 64.1 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 __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(require("@yomguithereal/helpers/extend"));
var camera_1 = __importDefault(require("./core/camera"));
var mouse_1 = __importDefault(require("./core/captors/mouse"));
var quadtree_1 = __importDefault(require("./core/quadtree"));
var types_1 = require("./types");
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 matrices_1 = require("./utils/matrices");
var edge_collisions_1 = require("./utils/edge-collisions");
/**
* 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.
*
* @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 returning the current graph's dimensions.
*
* @return {Dimensions}
*/
Sigma.prototype.getGraphDimensions = function () {
var extent = this.customBBox || this.nodeExtent;
return {
width: extent.x[1] - extent.x[0] || 1,
height: extent.y[1] - extent.y[0] || 1,
};
};
/**
* 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 {NodeDisplayData | undefined} A copy of the desired node's attribute or undefined if not found
*/
Sigma.prototype.getNodeDisplayData = 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 {EdgeDisplayData | undefined} A copy of the desired edge's attribute or undefined if not found
*/
Sigma.prototype.getEdgeDisplayData = function (key) {
var edge = this.edgeDataCache[key];
return edge ? Object.assign({}, edge) : undefined;
};
/**
* Method returning a copy of the settings collection.
*
* @return {Settings} A copy of the settings collection.
*/
Sigma.prototype.getSettings = function () {
return __assign({}, this.settings);
};
/**
* Method returning the current value for a given s