UNPKG

sigma

Version:

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

386 lines (385 loc) 15 kB
"use strict"; var __read = (this && this.__read) || function (o, n) { var m = typeof Symbol === "function" && o[Symbol.iterator]; if (!m) return o; var i = m.call(o), r, ar = [], e; try { while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); } catch (error) { e = { error: error }; } finally { try { if (r && !r.done && (m = i["return"])) m.call(i); } finally { if (e) throw e.error; } } return ar; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.validateGraph = exports.canUse32BitsIndices = exports.extractPixel = exports.getMatrixImpact = exports.matrixFromCamera = exports.getCorrectionRatio = exports.floatColor = exports.floatArrayColor = exports.parseColor = exports.zIndexOrdering = exports.createNormalizationFunction = exports.graphExtent = exports.getPixelRatio = exports.createElement = exports.cancelFrame = exports.requestFrame = exports.assignDeep = exports.assign = exports.isPlainObject = void 0; var is_graph_1 = __importDefault(require("graphology-utils/is-graph")); var matrices_1 = require("./matrices"); var data_1 = require("./data"); /** * Checks whether the given value is a plain object. * * @param {mixed} value - Target value. * @return {boolean} */ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types function isPlainObject(value) { return typeof value === "object" && value !== null && value.constructor === Object; } exports.isPlainObject = isPlainObject; /** * Helper to use Object.assign with more than two objects. * * @param {object} target - First object. * @param {object} [...objects] - Objects to merge. * @return {object} */ function assign(target) { var objects = []; for (var _i = 1; _i < arguments.length; _i++) { objects[_i - 1] = arguments[_i]; } target = target || {}; for (var i = 0, l = objects.length; i < l; i++) { var o = objects[i]; if (!o) continue; Object.assign(target, o); } return target; } exports.assign = assign; /** * Very simple recursive Object.assign-like function. * * @param {object} target - First object. * @param {object} [...objects] - Objects to merge. * @return {object} */ function assignDeep(target) { var objects = []; for (var _i = 1; _i < arguments.length; _i++) { objects[_i - 1] = arguments[_i]; } target = target || {}; for (var i = 0, l = objects.length; i < l; i++) { var o = objects[i]; if (!o) continue; for (var k in o) { if (isPlainObject(o[k])) { target[k] = assignDeep(target[k], o[k]); } else { target[k] = o[k]; } } } return target; } exports.assignDeep = assignDeep; /** * Just some dirty trick to make requestAnimationFrame and cancelAnimationFrame "work" in Node.js, for unit tests: */ exports.requestFrame = typeof requestAnimationFrame !== "undefined" ? function (callback) { return requestAnimationFrame(callback); } : function (callback) { return setTimeout(callback, 0); }; exports.cancelFrame = typeof cancelAnimationFrame !== "undefined" ? function (requestID) { return cancelAnimationFrame(requestID); } : function (requestID) { return clearTimeout(requestID); }; /** * Function used to create DOM elements easily. * * @param {string} tag - Tag name of the element to create. * @param {object} style - Styles map. * @param {object} attributes - Attributes map. * @return {HTMLElement} */ 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; } exports.createElement = createElement; /** * Function returning the browser's pixel ratio. * * @return {number} */ function getPixelRatio() { if (typeof window.devicePixelRatio !== "undefined") return window.devicePixelRatio; return 1; } exports.getPixelRatio = getPixelRatio; /** * Function returning the graph's node extent in x & y. * * @param {Graph} * @return {object} */ 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] }; } exports.graphExtent = graphExtent; function createNormalizationFunction(extent) { var _a = __read(extent.x, 2), minX = _a[0], maxX = _a[1], _b = __read(extent.y, 2), minY = _b[0], maxY = _b[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 (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.createNormalizationFunction = createNormalizationFunction; /** * Function ordering the given elements in reverse z-order so they drawn * the correct way. * * @param {number} extent - [min, max] z values. * @param {function} getter - Z attribute getter function. * @param {array} elements - The array to sort. * @return {array} - The sorted array. */ 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 } exports.zIndexOrdering = zIndexOrdering; /** * WebGL utils * =========== */ /** * Memoized function returning a float-encoded color from various string * formats describing colors. */ var INT8 = new Int8Array(4); var INT32 = new Int32Array(INT8.buffer, 0, 1); var FLOAT32 = new Float32Array(INT8.buffer, 0, 1); var RGBA_TEST_REGEX = /^\s*rgba?\s*\(/; var RGBA_EXTRACT_REGEX = /^\s*rgba?\s*\(\s*([0-9]*)\s*,\s*([0-9]*)\s*,\s*([0-9]*)(?:\s*,\s*(.*)?)?\)\s*$/; function parseColor(val) { var r = 0; // byte var g = 0; // byte var b = 0; // byte var a = 1; // float // Handling hexadecimal notation if (val[0] === "#") { if (val.length === 4) { r = parseInt(val.charAt(1) + val.charAt(1), 16); g = parseInt(val.charAt(2) + val.charAt(2), 16); b = parseInt(val.charAt(3) + val.charAt(3), 16); } else { r = parseInt(val.charAt(1) + val.charAt(2), 16); g = parseInt(val.charAt(3) + val.charAt(4), 16); b = parseInt(val.charAt(5) + val.charAt(6), 16); } if (val.length === 9) { a = parseInt(val.charAt(7) + val.charAt(8), 16) / 255; } } // Handling rgb notation else if (RGBA_TEST_REGEX.test(val)) { var match = val.match(RGBA_EXTRACT_REGEX); if (match) { r = +match[1]; g = +match[2]; b = +match[3]; if (match[4]) a = +match[4]; } } return { r: r, g: g, b: b, a: a }; } exports.parseColor = parseColor; var FLOAT_COLOR_CACHE = {}; for (var htmlColor in data_1.HTML_COLORS) { FLOAT_COLOR_CACHE[htmlColor] = floatColor(data_1.HTML_COLORS[htmlColor]); // Replicating cache for hex values for free FLOAT_COLOR_CACHE[data_1.HTML_COLORS[htmlColor]] = FLOAT_COLOR_CACHE[htmlColor]; } function floatArrayColor(val) { val = data_1.HTML_COLORS[val] || val; // NOTE: this variant is not cached because it is mostly used for uniforms var _a = parseColor(val), r = _a.r, g = _a.g, b = _a.b, a = _a.a; return new Float32Array([r / 255, g / 255, b / 255, a]); } exports.floatArrayColor = floatArrayColor; function floatColor(val) { // If the color is already computed, we yield it if (typeof FLOAT_COLOR_CACHE[val] !== "undefined") return FLOAT_COLOR_CACHE[val]; var parsed = parseColor(val); var r = parsed.r, g = parsed.g, b = parsed.b; var a = parsed.a; a = (a * 255) | 0; INT32[0] = ((a << 24) | (b << 16) | (g << 8) | r) & 0xfeffffff; var color = FLOAT32[0]; FLOAT_COLOR_CACHE[val] = color; return color; } exports.floatColor = floatColor; /** * In sigma, the graph is normalized into a [0, 1], [0, 1] square, before being given to the various renderers. This * helps dealing with quadtree in particular. * But at some point, we need to rescale it so that it takes the best place in the screen, ie. 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" (ie. 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)); } exports.getCorrectionRatio = getCorrectionRatio; /** * Function returning a matrix from the current state of the camera. */ // TODO: it's possible to optimize this drastically! function matrixFromCamera(state, viewportDimensions, graphDimensions, padding, inverse) { var angle = state.angle, ratio = state.ratio, x = state.x, y = state.y; var width = viewportDimensions.width, height = viewportDimensions.height; var matrix = (0, matrices_1.identity)(); var smallestDimension = Math.min(width, height) - 2 * padding; var correctionRatio = getCorrectionRatio(viewportDimensions, graphDimensions); if (!inverse) { (0, matrices_1.multiply)(matrix, (0, matrices_1.scale)((0, matrices_1.identity)(), 2 * (smallestDimension / width) * correctionRatio, 2 * (smallestDimension / height) * correctionRatio)); (0, matrices_1.multiply)(matrix, (0, matrices_1.rotate)((0, matrices_1.identity)(), -angle)); (0, matrices_1.multiply)(matrix, (0, matrices_1.scale)((0, matrices_1.identity)(), 1 / ratio)); (0, matrices_1.multiply)(matrix, (0, matrices_1.translate)((0, matrices_1.identity)(), -x, -y)); } else { (0, matrices_1.multiply)(matrix, (0, matrices_1.translate)((0, matrices_1.identity)(), x, y)); (0, matrices_1.multiply)(matrix, (0, matrices_1.scale)((0, matrices_1.identity)(), ratio)); (0, matrices_1.multiply)(matrix, (0, matrices_1.rotate)((0, matrices_1.identity)(), angle)); (0, matrices_1.multiply)(matrix, (0, matrices_1.scale)((0, matrices_1.identity)(), width / smallestDimension / 2 / correctionRatio, height / smallestDimension / 2 / correctionRatio)); } return matrix; } exports.matrixFromCamera = matrixFromCamera; /** * 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 (ie. 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 of the fact that in WebGL, * the image is vertically swapped. */ function getMatrixImpact(matrix, cameraState, viewportDimensions) { var _a = (0, matrices_1.multiplyVec2)(matrix, { x: Math.cos(cameraState.angle), y: Math.sin(cameraState.angle) }, 0), x = _a.x, y = _a.y; return 1 / Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)) / viewportDimensions.width; } exports.getMatrixImpact = getMatrixImpact; /** * Function extracting the color at the given pixel. */ function extractPixel(gl, x, y, array) { var data = array || new Uint8Array(4); gl.readPixels(x, y, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, data); return data; } exports.extractPixel = extractPixel; /** * Function used to know whether given webgl context can use 32 bits indices. */ function canUse32BitsIndices(gl) { var webgl2 = typeof WebGL2RenderingContext !== "undefined" && gl instanceof WebGL2RenderingContext; return webgl2 || !!gl.getExtension("OES_element_index_uint"); } exports.canUse32BitsIndices = canUse32BitsIndices; /** * 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 (!(0, is_graph_1.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.")); } }); } exports.validateGraph = validateGraph;