UNPKG

sigma

Version:

A JavaScript library dedicated to graph drawing.

229 lines (228 loc) 11.2 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.edgeLabelsToDisplayFromNodes = exports.labelsToDisplayFromGrid = void 0; var camera_1 = __importDefault(require("./camera")); /** * Constants. */ // Dimensions of a normal cell var DEFAULT_CELL = { width: 250, height: 175, }; // Dimensions of an unzoomed cell. This one is usually larger than the normal // one to account for the fact that labels will more likely collide. var DEFAULT_UNZOOMED_CELL = { width: 400, height: 300, }; /** * Helpers. */ function collision(x1, y1, w1, h1, x2, y2, w2, h2) { return x1 < x2 + w2 && x1 + w1 > x2 && y1 < y2 + h2 && y1 + h1 > y2; } // TODO: cache camera position of selected nodes to avoid costly computations // in anti-collision step // TOOD: document a little bit more so future people can understand this mess /** * Label grid heuristic selecting labels to display. * * @param {object} params - Parameters: * @param {object} cache - Cache storing nodes' data. * @param {Camera} camera - The renderer's camera. * @param {Set} displayedLabels - Currently displayed labels. * @param {Array} visibleNodes - Nodes visible for this render. * @param {Graph} graph - The rendered graph. * @return {Array} - The selected labels. */ function labelsToDisplayFromGrid(params) { var cache = params.cache, camera = params.camera, userCell = params.cell, dimensions = params.dimensions, displayedLabels = params.displayedLabels, _a = params.fontSize, fontSize = _a === void 0 ? 14 : _a, graph = params.graph, _b = params.renderedSizeThreshold, renderedSizeThreshold = _b === void 0 ? -Infinity : _b, visibleNodes = params.visibleNodes; var cameraState = camera.getState(), previousCameraState = camera.getPreviousState(); var previousCamera = new camera_1.default(); previousCamera.setState(previousCameraState); // TODO: should factorize. This same code is used quite a lot throughout the codebase // TODO: POW RATIO is currently default 0.5 and harcoded var sizeRatio = Math.pow(cameraState.ratio, 0.5); // State var zooming = cameraState.ratio < previousCameraState.ratio, panning = cameraState.x !== previousCameraState.x || cameraState.y !== previousCameraState.y, unzooming = cameraState.ratio > previousCameraState.ratio, unzoomedPanning = panning && !zooming && !unzooming && cameraState.ratio >= 1, zoomedPanning = panning && displayedLabels.size && !zooming && !unzooming; // Trick to discretize unzooming if (unzooming && Math.trunc(cameraState.ratio * 100) % 5 !== 0) return Array.from(displayedLabels); // If panning while unzoomed, we shouldn't change label selection if (unzoomedPanning && displayedLabels.size !== 0) return Array.from(displayedLabels); // When unzoomed & zooming if (zooming && cameraState.ratio >= 1) return Array.from(displayedLabels); // Adapting cell dimensions var cell = userCell ? userCell : DEFAULT_CELL; if (cameraState.ratio >= 1.3) cell = DEFAULT_UNZOOMED_CELL; var cwr = dimensions.width % cell.width; var cellWidth = cell.width + cwr / Math.floor(dimensions.width / cell.width); var chr = dimensions.height % cell.height; var cellHeight = cell.height + chr / Math.floor(dimensions.height / cell.height); var adjustedWidth = dimensions.width + cellWidth, adjustedHeight = dimensions.height + cellHeight, adjustedX = -cellWidth, adjustedY = -cellHeight; var panningWidth = dimensions.width + cellWidth / 2, panningHeight = dimensions.height + cellHeight / 2, panningX = -(cellWidth / 2), panningY = -(cellHeight / 2); var worthyLabels = []; var grid = {}; var maxSize = -Infinity, biggestNode = null; for (var i = 0, l = visibleNodes.length; i < l; i++) { var node = visibleNodes[i], nodeData = cache[node]; // We filter hidden nodes if (nodeData.hidden) continue; // We filter nodes having a rendered size less than a certain thresold if (nodeData.size / sizeRatio < renderedSizeThreshold) continue; // Finding our node's cell in the grid var pos = camera.framedGraphToViewport(dimensions, nodeData); // Node is not actually visible on screen // NOTE: can optimize margin on the right side (only if we know where the labels go) if (pos.x < adjustedX || pos.x > adjustedWidth || pos.y < adjustedY || pos.y > adjustedHeight) continue; // Keeping track of the maximum node size for certain cases if (nodeData.size > maxSize) { maxSize = nodeData.size; biggestNode = node; } // If panning when zoomed, we consider only displayed labels and newly // visible nodes if (zoomedPanning) { var ppos = previousCamera.framedGraphToViewport(dimensions, nodeData); // Was node visible earlier? if (ppos.x >= panningX && ppos.x <= panningWidth && ppos.y >= panningY && ppos.y <= panningHeight) { // Was the label displayed? if (!displayedLabels.has(node)) continue; } } var xKey = Math.floor(pos.x / cellWidth), yKey = Math.floor(pos.y / cellHeight); var key = xKey + "\u00A7" + yKey; if (typeof grid[key] === "undefined") { // This cell is not yet occupied grid[key] = node; } else { // We must solve a conflict in this cell var currentNode = grid[key], currentNodeData = cache[currentNode]; // We prefer already displayed labels if (displayedLabels.size > 0) { var n1 = displayedLabels.has(node), n2 = displayedLabels.has(currentNode); if (!n1 && n2) { continue; } if (n1 && !n2) { grid[key] = node; continue; } if ((zoomedPanning || zooming) && n1 && n2) { worthyLabels.push(node); continue; } } // In case of size & degree equality, we use the node's key so that the // process remains deterministic var won = false; if (nodeData.size > currentNodeData.size) { won = true; } else if (nodeData.size === currentNodeData.size) { var nodeDegree = graph.degree(node), currentNodeDegree = graph.degree(currentNode); if (nodeDegree > currentNodeDegree) { won = true; } else if (nodeDegree === currentNodeDegree) { if (node > currentNode) won = true; } } if (won) grid[key] = node; } } // Compiling the labels var biggestNodeShown = worthyLabels.some(function (node) { return node === biggestNode; }); for (var key in grid) { var node = grid[key]; if (node === biggestNode) biggestNodeShown = true; worthyLabels.push(node); } // Always keeping biggest node shown on screen if (!biggestNodeShown && biggestNode) worthyLabels.push(biggestNode); // Basic anti-collision var collisions = new Set(); for (var i = 0, l = worthyLabels.length; i < l; i++) { var n1 = worthyLabels[i], d1 = cache[n1], p1 = camera.framedGraphToViewport(dimensions, d1); if (collisions.has(n1)) continue; for (var j = i + 1; j < l; j++) { var n2 = worthyLabels[j], d2 = cache[n2], p2 = camera.framedGraphToViewport(dimensions, d2); var c = collision( // First abstract bbox p1.x, p1.y, d1.label.length * 8, fontSize, // Second abstract bbox p2.x, p2.y, d2.label.length * 8, fontSize); if (c) { // NOTE: add degree as tie-breaker here if required in the future // NOTE: add final stable tie-breaker using node key if required if (d1.size < d2.size) collisions.add(n1); else collisions.add(n2); } } } // console.log(collisions) return worthyLabels.filter(function (l) { return !collisions.has(l); }); } exports.labelsToDisplayFromGrid = labelsToDisplayFromGrid; /** * Label heuristic selecting edge labels to display, based on displayed node * labels * * @param {object} params - Parameters: * @param {object} nodeDataCache - Cache storing nodes data. * @param {object} edgeDataCache - Cache storing edges data. * @param {Set} displayedNodeLabels - Currently displayed node labels. * @param {Set} highlightedNodes - Highlighted nodes. * @param {Graph} graph - The rendered graph. * @param {string} hoveredNode - Hovered node (optional) * @return {Array} - The selected labels. */ function edgeLabelsToDisplayFromNodes(params) { var nodeDataCache = params.nodeDataCache, edgeDataCache = params.edgeDataCache, graph = params.graph, hoveredNode = params.hoveredNode, highlightedNodes = params.highlightedNodes, displayedNodeLabels = params.displayedNodeLabels; var worthyEdges = new Set(); var displayedNodeLabelsArray = Array.from(displayedNodeLabels); // Each edge connecting a highlighted node has its label displayed if the other extremity is not hidden: var highlightedNodesArray = Array.from(highlightedNodes); if (hoveredNode && !highlightedNodes.has(hoveredNode)) highlightedNodesArray.push(hoveredNode); for (var i = 0; i < highlightedNodesArray.length; i++) { var key = highlightedNodesArray[i]; var edges = graph.edges(key); for (var j = 0; j < edges.length; j++) { var edgeKey = edges[j]; var extremities = graph.extremities(edgeKey), sourceData = nodeDataCache[extremities[0]], targetData = nodeDataCache[extremities[1]], edgeData = edgeDataCache[edgeKey]; if (edgeData.hidden && sourceData.hidden && targetData.hidden) { worthyEdges.add(edgeKey); } } } // Each edge connecting two nodes with visible labels has its label displayed: for (var i = 0; i < displayedNodeLabelsArray.length; i++) { var key = displayedNodeLabelsArray[i]; var edges = graph.outboundEdges(key); for (var j = 0; j < edges.length; j++) if (!edgeDataCache[edges[j]].hidden && displayedNodeLabels.has(graph.opposite(key, edges[j]))) worthyEdges.add(edges[j]); } return Array.from(worthyEdges); } exports.edgeLabelsToDisplayFromNodes = edgeLabelsToDisplayFromNodes;