UNPKG

sigma

Version:

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

391 lines (364 loc) 11.6 kB
'use strict'; var isGraph = require('graphology-utils/is-graph'); var colors = require('./colors-ee2e2828.cjs.prod.js'); function _interopDefault (e) { return e && e.__esModule ? e : { 'default': e }; } var isGraph__default = /*#__PURE__*/_interopDefault(isGraph); var linear = function linear(k) { return k; }; var quadraticIn = function quadraticIn(k) { return k * k; }; var quadraticOut = function quadraticOut(k) { return k * (2 - k); }; var quadraticInOut = function quadraticInOut(k) { if ((k *= 2) < 1) return 0.5 * k * k; return -0.5 * (--k * (k - 2) - 1); }; var cubicIn = function cubicIn(k) { return k * k * k; }; var cubicOut = function cubicOut(k) { return --k * k * k + 1; }; var cubicInOut = function cubicInOut(k) { if ((k *= 2) < 1) return 0.5 * k * k * k; return 0.5 * ((k -= 2) * k * k + 2); }; var easings = { linear: linear, quadraticIn: quadraticIn, quadraticOut: quadraticOut, quadraticInOut: quadraticInOut, cubicIn: cubicIn, cubicOut: cubicOut, cubicInOut: cubicInOut }; /** * Defaults. */ var ANIMATE_DEFAULTS = { easing: "quadraticInOut", duration: 150 }; /** * Function used to animate the nodes. */ function animateNodes(graph, targets, opts, callback) { var options = Object.assign({}, ANIMATE_DEFAULTS, opts); var easing = typeof options.easing === "function" ? options.easing : easings[options.easing]; var start = Date.now(); var startPositions = {}; for (var node in targets) { var attrs = targets[node]; startPositions[node] = {}; for (var _k in attrs) startPositions[node][_k] = graph.getNodeAttribute(node, _k); } var frame = null; var _step = function step() { frame = null; var p = (Date.now() - start) / options.duration; if (p >= 1) { // Animation is done for (var _node in targets) { var _attrs = targets[_node]; // We use given values to avoid precision issues and for convenience for (var _k2 in _attrs) graph.setNodeAttribute(_node, _k2, _attrs[_k2]); } if (typeof callback === "function") callback(); return; } p = easing(p); for (var _node2 in targets) { var _attrs2 = targets[_node2]; var s = startPositions[_node2]; for (var _k3 in _attrs2) graph.setNodeAttribute(_node2, _k3, _attrs2[_k3] * p + s[_k3] * (1 - p)); } frame = requestAnimationFrame(_step); }; _step(); return function () { if (frame) cancelAnimationFrame(frame); }; } function identity() { return Float32Array.of(1, 0, 0, 0, 1, 0, 0, 0, 1); } // TODO: optimize function scale(m, x, y) { m[0] = x; m[4] = typeof y === "number" ? y : x; return m; } function rotate(m, r) { var s = Math.sin(r), c = Math.cos(r); m[0] = c; m[1] = s; m[3] = -s; m[4] = c; return m; } function translate(m, x, y) { m[6] = x; m[7] = y; return m; } function multiply(a, b) { var a00 = a[0], a01 = a[1], a02 = a[2]; var a10 = a[3], a11 = a[4], a12 = a[5]; var a20 = a[6], a21 = a[7], a22 = a[8]; var b00 = b[0], b01 = b[1], b02 = b[2]; var b10 = b[3], b11 = b[4], b12 = b[5]; var b20 = b[6], b21 = b[7], b22 = b[8]; a[0] = b00 * a00 + b01 * a10 + b02 * a20; a[1] = b00 * a01 + b01 * a11 + b02 * a21; a[2] = b00 * a02 + b01 * a12 + b02 * a22; a[3] = b10 * a00 + b11 * a10 + b12 * a20; a[4] = b10 * a01 + b11 * a11 + b12 * a21; a[5] = b10 * a02 + b11 * a12 + b12 * a22; a[6] = b20 * a00 + b21 * a10 + b22 * a20; a[7] = b20 * a01 + b21 * a11 + b22 * a21; a[8] = b20 * a02 + b21 * a12 + b22 * a22; return a; } function multiplyVec2(a, b) { var z = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 1; var a00 = a[0]; var a01 = a[1]; var a10 = a[3]; var a11 = a[4]; var a20 = a[6]; var a21 = a[7]; var b0 = b.x; var b1 = b.y; return { x: b0 * a00 + b1 * a10 + a20 * z, y: b0 * a01 + b1 * a11 + a21 * z }; } /** * In sigma, the graph is normalized into a [0, 1], [0, 1] square, before being given to the various renderers. This * helps to deal with quadtree in particular. * But at some point, we need to rescale it so that it takes the best place in the screen, i.e. we always want to see two * nodes "touching" opposite sides of the graph, with the camera being at its default state. * * This function determines this ratio. */ function getCorrectionRatio(viewportDimensions, graphDimensions) { var viewportRatio = viewportDimensions.height / viewportDimensions.width; var graphRatio = graphDimensions.height / graphDimensions.width; // If the stage and the graphs are in different directions (such as the graph being wider that tall while the stage // is taller than wide), we can stop here to have indeed nodes touching opposite sides: if (viewportRatio < 1 && graphRatio > 1 || viewportRatio > 1 && graphRatio < 1) { return 1; } // Else, we need to fit the graph inside the stage: // 1. If the graph is "squarer" (i.e. with a ratio closer to 1), we need to make the largest sides touch; // 2. If the stage is "squarer", we need to make the smallest sides touch. return Math.min(Math.max(graphRatio, 1 / graphRatio), Math.max(1 / viewportRatio, viewportRatio)); } /** * Function returning a matrix from the current state of the camera. */ function matrixFromCamera(state, viewportDimensions, graphDimensions, padding, inverse) { // TODO: it's possible to optimize this drastically! var angle = state.angle, ratio = state.ratio, x = state.x, y = state.y; var width = viewportDimensions.width, height = viewportDimensions.height; var matrix = identity(); var smallestDimension = Math.min(width, height) - 2 * padding; var correctionRatio = getCorrectionRatio(viewportDimensions, graphDimensions); if (!inverse) { multiply(matrix, scale(identity(), 2 * (smallestDimension / width) * correctionRatio, 2 * (smallestDimension / height) * correctionRatio)); multiply(matrix, rotate(identity(), -angle)); multiply(matrix, scale(identity(), 1 / ratio)); multiply(matrix, translate(identity(), -x, -y)); } else { multiply(matrix, translate(identity(), x, y)); multiply(matrix, scale(identity(), ratio)); multiply(matrix, rotate(identity(), angle)); multiply(matrix, scale(identity(), width / smallestDimension / 2 / correctionRatio, height / smallestDimension / 2 / correctionRatio)); } return matrix; } /** * All these transformations we apply on the matrix to get it rescale the graph * as we want make it very hard to get pixel-perfect distances in WebGL. This * function returns a factor that properly cancels the matrix effect on lengths. * * [jacomyal] * To be fully honest, I can't really explain happens here... I notice that the * following ratio works (i.e. it correctly compensates the matrix impact on all * camera states I could try): * > `R = size(V) / size(M * V) / W` * as long as `M * V` is in the direction of W (ie. parallel to (Ox)). It works * as well with H and a vector that transforms into something parallel to (Oy). * * Also, note that we use `angle` and not `-angle` (that would seem logical, * since we want to anticipate the rotation), because the image is vertically * swapped in WebGL. */ function getMatrixImpact(matrix, cameraState, viewportDimensions) { var _multiplyVec = multiplyVec2(matrix, { x: Math.cos(cameraState.angle), y: Math.sin(cameraState.angle) }, 0), x = _multiplyVec.x, y = _multiplyVec.y; return 1 / Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)) / viewportDimensions.width; } /** * Function returning the graph's node extent in x & y. */ function graphExtent(graph) { if (!graph.order) return { x: [0, 1], y: [0, 1] }; var xMin = Infinity; var xMax = -Infinity; var yMin = Infinity; var yMax = -Infinity; graph.forEachNode(function (_, attr) { var x = attr.x, y = attr.y; if (x < xMin) xMin = x; if (x > xMax) xMax = x; if (y < yMin) yMin = y; if (y > yMax) yMax = y; }); return { x: [xMin, xMax], y: [yMin, yMax] }; } /** * Check if the graph variable is a valid graph, and if sigma can render it. */ function validateGraph(graph) { // check if it's a valid graphology instance if (!isGraph__default["default"](graph)) throw new Error("Sigma: invalid graph instance."); // check if nodes have x/y attributes graph.forEachNode(function (key, attributes) { if (!Number.isFinite(attributes.x) || !Number.isFinite(attributes.y)) { throw new Error("Sigma: Coordinates of node ".concat(key, " are invalid. A node must have a numeric 'x' and 'y' attribute.")); } }); } /** * Function used to create DOM elements easily. */ function createElement(tag, style, attributes) { var element = document.createElement(tag); if (style) { for (var k in style) { element.style[k] = style[k]; } } if (attributes) { for (var _k in attributes) { element.setAttribute(_k, attributes[_k]); } } return element; } /** * Function returning the browser's pixel ratio. */ function getPixelRatio() { if (typeof window.devicePixelRatio !== "undefined") return window.devicePixelRatio; return 1; } /** * Function ordering the given elements in reverse z-order so they drawn * the correct way. */ function zIndexOrdering(_extent, getter, elements) { // If k is > n, we'll use a standard sort return elements.sort(function (a, b) { var zA = getter(a) || 0, zB = getter(b) || 0; if (zA < zB) return -1; if (zA > zB) return 1; return 0; }); // TODO: counting sort optimization } /** * Factory returning a function normalizing the given node's position & size. */ function createNormalizationFunction(extent) { var _extent$x = colors._slicedToArray(extent.x, 2), minX = _extent$x[0], maxX = _extent$x[1], _extent$y = colors._slicedToArray(extent.y, 2), minY = _extent$y[0], maxY = _extent$y[1]; var ratio = Math.max(maxX - minX, maxY - minY), dX = (maxX + minX) / 2, dY = (maxY + minY) / 2; if (ratio === 0 || Math.abs(ratio) === Infinity || isNaN(ratio)) ratio = 1; if (isNaN(dX)) dX = 0; if (isNaN(dY)) dY = 0; var fn = function fn(data) { return { x: 0.5 + (data.x - dX) / ratio, y: 0.5 + (data.y - dY) / ratio }; }; // TODO: possibility to apply this in batch over array of indices fn.applyTo = function (data) { data.x = 0.5 + (data.x - dX) / ratio; data.y = 0.5 + (data.y - dY) / ratio; }; fn.inverse = function (data) { return { x: dX + ratio * (data.x - 0.5), y: dY + ratio * (data.y - 0.5) }; }; fn.ratio = ratio; return fn; } exports.ANIMATE_DEFAULTS = ANIMATE_DEFAULTS; exports.animateNodes = animateNodes; exports.createElement = createElement; exports.createNormalizationFunction = createNormalizationFunction; exports.cubicIn = cubicIn; exports.cubicInOut = cubicInOut; exports.cubicOut = cubicOut; exports.easings = easings; exports.getCorrectionRatio = getCorrectionRatio; exports.getMatrixImpact = getMatrixImpact; exports.getPixelRatio = getPixelRatio; exports.graphExtent = graphExtent; exports.identity = identity; exports.linear = linear; exports.matrixFromCamera = matrixFromCamera; exports.multiply = multiply; exports.multiplyVec2 = multiplyVec2; exports.quadraticIn = quadraticIn; exports.quadraticInOut = quadraticInOut; exports.quadraticOut = quadraticOut; exports.rotate = rotate; exports.scale = scale; exports.translate = translate; exports.validateGraph = validateGraph; exports.zIndexOrdering = zIndexOrdering;