UNPKG

sigma

Version:

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

361 lines (337 loc) 11 kB
import isGraph from 'graphology-utils/is-graph'; import { _ as _slicedToArray } from './colors-beb06eb2.esm.js'; 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(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 = _slicedToArray(extent.x, 2), minX = _extent$x[0], maxX = _extent$x[1], _extent$y = _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; } export { ANIMATE_DEFAULTS as A, getMatrixImpact as a, createElement as b, createNormalizationFunction as c, getPixelRatio as d, easings as e, multiplyVec2 as f, graphExtent as g, animateNodes as h, identity as i, getCorrectionRatio as j, quadraticOut as k, linear as l, matrixFromCamera as m, quadraticInOut as n, cubicIn as o, cubicOut as p, quadraticIn as q, cubicInOut as r, scale as s, rotate as t, translate as u, validateGraph as v, multiply as w, zIndexOrdering as z };