UNPKG

graphology-metrics

Version:

Miscellaneous graph metrics for graphology.

239 lines (202 loc) 6.7 kB
/** * Graphology Layout Quality - Connected-closeness * ================================================ * * Function computing the layout quality metric named "connected-closeness", * designed to provide a quantified statement about the mediation of the topology * by the node placement. * * [Article]: * Jacomy, M. (2023). Connected-closeness: A Visual Quantification of Distances in * Network Layouts. Journal of Graph Algorithms and Applications, 27(5), 341-404. * https://www.jgaa.info/index.php/jgaa/article/view/paper626 */ var isGraph = require('graphology-utils/is-graph'); function max(values, getter) { if (values.length < 1) throw new Error( 'graphology-metrics/layout-quality/connected-closeness.max: not enough values!' ); var m = undefined; var v; for (var i = 1, l = values.length; i < l; i++) { v = values[i]; if (getter !== undefined) v = getter(v); if (m === undefined || v > m) m = v; } return m; } function min(values, getter) { if (values.length < 1) throw new Error( 'graphology-metrics/layout-quality/connected-closeness.min: not enough values!' ); var m = undefined; var v; for (var i = 1, l = values.length; i < l; i++) { v = values[i]; if (getter !== undefined) v = getter(v); if (m === undefined || v < m) m = v; } return m; } module.exports = function connectedCloseness(g, settings) { if (!isGraph(g)) throw new Error( 'graphology-metrics/layout-quality/connected-closeness: given graph is not a valid graphology instance.' ); if (g.size < 2) return { deltaMax: undefined, ePercentOfDeltaMax: undefined, pPercentOfDeltaMax: undefined, pEdgeOfDeltaMax: undefined, cMax: indicatorsOfDeltaMax.C }; // Default settings // TODO: don't mutate the user's object // TODO: lint // TODO: fix directedness, infer types etc. // TODO: add non sampling option settings = settings || {}; settings.epsilon = settings.epsilon || 0.03; // 3% settings.gridSize = settings.gridSize || 10; // This is an optimization thing, it's not the graphical grid var rng = settings.rng || Math.random; var pairsOfNodesSampled = samplePairsOfNodes(); var connectedPairs = g.edges().map(function (eid) { var n1 = g.getNodeAttributes(g.source(eid)); var n2 = g.getNodeAttributes(g.target(eid)); var d = Math.sqrt(Math.pow(n1.x - n2.x, 2) + Math.pow(n1.y - n2.y, 2)); return d; }); // Grid search for CMax var range = [0, Math.max(max(pairsOfNodesSampled), max(connectedPairs) || 0)]; var CMax = 0; var distancesIndex = {}; var Delta, oldCMax, C, i, indicatorsOverDelta; var targetIndex = -1; do { for (i = 0; i <= settings.gridSize; i++) { Delta = range[0] + ((range[1] - range[0]) * i) / settings.gridSize; if (distancesIndex[Delta] === undefined) { distancesIndex[Delta] = computeIndicators( Delta, g, pairsOfNodesSampled, connectedPairs ); } } oldCMax = CMax; CMax = 0; indicatorsOverDelta = Object.values(distancesIndex); indicatorsOverDelta.forEach(function (indicators, i) { C = indicators.C; if (C > CMax) { CMax = C; targetIndex = i; } }); range = [ indicatorsOverDelta[Math.max(0, targetIndex - 1)].Delta, indicatorsOverDelta[ Math.min(indicatorsOverDelta.length - 1, targetIndex + 1) ].Delta ]; } while ((CMax - oldCMax) / CMax >= settings.epsilon / 10); var deltaMax = findDeltaMax(indicatorsOverDelta, settings.epsilon); var indicatorsOfDeltaMax = computeIndicators( deltaMax, g, pairsOfNodesSampled, connectedPairs ); // Resistance to misinterpretation if (indicatorsOfDeltaMax.C < 0.1) { return { deltaMax: undefined, ePercentOfDeltaMax: undefined, pPercentOfDeltaMax: undefined, pEdgeOfDeltaMax: undefined, cMax: indicatorsOfDeltaMax.C }; } else { return { deltaMax: deltaMax, ePercentOfDeltaMax: indicatorsOfDeltaMax.ePercent, pPercentOfDeltaMax: indicatorsOfDeltaMax.pPercent, pEdgeOfDeltaMax: indicatorsOfDeltaMax.pEdge, cMax: indicatorsOfDeltaMax.C }; } // Internal methods // Compute indicators given a distance Delta function computeIndicators(Delta, g, pairsOfNodesSampled, connectedPairs) { var connectedPairsBelowDelta = connectedPairs.filter(function (d) { return d <= Delta; }); var pairsBelowDelta = pairsOfNodesSampled.filter(function (d) { return d <= Delta; }); // Count of edges shorter than Delta // note: actual count var E = connectedPairsBelowDelta.length; // Proportion of edges shorter than Delta // note: actual count var ePercent = E / connectedPairs.length; // Count of node pairs closer than Delta // note: sampling-dependent var p = pairsBelowDelta.length; // Proportion of node pairs closer than Delta // note: sampling-dependent, but it cancels out var pPercent = p / pairsOfNodesSampled.length; // Connected closeness var C = ePercent - pPercent; // Probability that, considering two nodes closer than Delta, they are connected // note: p is sampling-dependent, so we have to normalize it here. var possibleEdgesPerPair = g.undirected ? 1 : 2; var pEdge = E / ((possibleEdgesPerPair * p * (g.order * (g.order - 1))) / pairsOfNodesSampled.length); return { Delta: Delta, ePercent: ePercent, pPercent: pPercent, pEdge: pEdge, // Note: pEdge is complentary information, not strictly necessary C: C }; } function samplePairsOfNodes() { if (g.order < 2) return []; var samples = []; var node1, node2, n1, n2, d, c; var samplesCount = g.size; // We want as many samples as edges if (samplesCount < 1) return []; for (var i = 0; i < samplesCount; i++) { node1 = g.nodes()[Math.floor(rng() * g.order)]; do { node2 = g.nodes()[Math.floor(rng() * g.order)]; } while (node1 === node2); n1 = g.getNodeAttributes(node1); n2 = g.getNodeAttributes(node2); d = Math.sqrt(Math.pow(n1.x - n2.x, 2) + Math.pow(n1.y - n2.y, 2)); samples.push(d); } return samples; } function findDeltaMax(indicatorsOverDelta, epsilon) { var CMax = max(indicatorsOverDelta, function (d) { return d.C; }); var deltaMax = min( indicatorsOverDelta.filter(function (d) { return d.C >= (1 - epsilon) * CMax; }), function (d) { return d.Delta; } ); return deltaMax; } };