sigma
Version:
A JavaScript library dedicated to graph drawing.
229 lines (228 loc) • 11.2 kB
JavaScript
"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;