@graphty/layout
Version:
graph layout algorithms based on networkx
194 lines • 6.75 kB
JavaScript
/**
* Layout rescaling utilities
*/
/**
* Returns scaled position array/dict to (-scale, scale) in all axes.
*
* @param pos - Position dictionary or array
* @param scale - Scale factor for positions
* @param center - Coordinate pair around which to center the layout
* @returns Rescaled positions dictionary
*/
export function rescaleLayout(pos, scale = 1, center = null) {
// Check if pos is empty
if (Array.isArray(pos)) {
if (pos.length === 0)
return [];
}
else {
if (Object.keys(pos).length === 0)
return {};
}
// Extract position values
const posValues = Array.isArray(pos) ? pos : Object.values(pos);
const dim = posValues[0].length;
// Fix Bug #1: Default center should match dimensionality
if (!center) {
center = Array(dim).fill(0);
}
// Use the maximum dimension between positions and center
const targetDim = Math.max(dim, center.length);
// Calculate center of positions, handling NaN values (Bug #3)
const posCenter = Array(dim).fill(0);
const counts = Array(dim).fill(0);
for (const p of posValues) {
for (let i = 0; i < dim; i++) {
if (!isNaN(p[i])) {
posCenter[i] += p[i];
counts[i]++;
}
}
}
// Average by actual count, not total length
for (let i = 0; i < dim; i++) {
posCenter[i] = counts[i] > 0 ? posCenter[i] / counts[i] : 0;
}
// Center positions
let centeredPos = {};
if (Array.isArray(pos)) {
centeredPos = pos.map(p => {
const centered = Array(targetDim).fill(0);
for (let i = 0; i < targetDim; i++) {
centered[i] = (i < p.length ? p[i] : 0) - (i < posCenter.length ? posCenter[i] : 0);
}
return centered;
});
}
else {
for (const [node, p] of Object.entries(pos)) {
const centered = Array(targetDim).fill(0);
for (let i = 0; i < targetDim; i++) {
centered[i] = (i < p.length ? p[i] : 0) - (i < posCenter.length ? posCenter[i] : 0);
}
centeredPos[node] = centered;
}
}
// Find maximum distance from center, handling NaN values (Bug #3)
let maxDistance = 0;
const centeredValues = Array.isArray(centeredPos) ? centeredPos : Object.values(centeredPos);
for (const p of centeredValues) {
// Calculate distance, treating NaN as 0 for distance calculation
let sumSquares = 0;
for (const val of p) {
if (!isNaN(val)) {
sumSquares += val * val;
}
}
const distance = Math.sqrt(sumSquares);
if (!isNaN(distance)) {
maxDistance = Math.max(maxDistance, distance);
}
}
// Rescale
let scaledPos = Array.isArray(pos) ? [] : {};
if (maxDistance > 0) {
const scaleFactor = scale / maxDistance;
if (Array.isArray(pos)) {
scaledPos = centeredPos.map((p, idx) => p.map((val, i) => {
// Check if this dimension existed in the original position
const origPos = pos[idx];
if (i >= origPos.length) {
// This dimension was added, use center value
return center[i];
}
// Preserve NaN values (Bug #3)
if (isNaN(val))
return NaN;
return val * scaleFactor + center[i];
}));
}
else {
for (const [node, p] of Object.entries(centeredPos)) {
const origPos = pos[node];
scaledPos[node] = p.map((val, i) => {
// Check if this dimension existed in the original position
if (i >= origPos.length) {
// This dimension was added, use center value
return center[i];
}
// Preserve NaN values (Bug #3)
if (isNaN(val))
return NaN;
return val * scaleFactor + center[i];
});
}
}
}
else {
// All nodes at the same position - extend to target dimensionality
if (Array.isArray(pos)) {
scaledPos = pos.map(p => {
const result = Array(targetDim);
for (let i = 0; i < targetDim; i++) {
if (i < p.length) {
result[i] = isNaN(p[i]) ? NaN : center[i];
}
else {
result[i] = center[i];
}
}
return result;
});
}
else {
for (const [node, p] of Object.entries(pos)) {
const result = Array(targetDim);
for (let i = 0; i < targetDim; i++) {
if (i < p.length) {
result[i] = isNaN(p[i]) ? NaN : center[i];
}
else {
result[i] = center[i];
}
}
scaledPos[node] = result;
}
}
}
return scaledPos;
}
/**
* Return a dictionary of scaled positions centered at (0, 0).
*
* @param pos - Dictionary of positions
* @param scale - Scale factor for positions
* @returns Dictionary of scaled positions
*/
export function rescaleLayoutDict(pos, scale = 1) {
if (Object.keys(pos).length === 0) {
return {};
}
// Extract positions as array
const posArray = Object.values(pos);
// Find center of positions
const center = [];
for (let d = 0; d < posArray[0].length; d++) {
center[d] = posArray.reduce((sum, p) => sum + p[d], 0) / posArray.length;
}
// Center positions
const centeredPos = {};
for (const [node, p] of Object.entries(pos)) {
centeredPos[node] = p.map((val, d) => val - center[d]);
}
// Find maximum distance from center
let maxDist = 0;
for (const p of Object.values(centeredPos)) {
const dist = Math.sqrt(p.reduce((sum, val) => sum + val * val, 0));
maxDist = Math.max(maxDist, dist);
}
// Scale positions
const scaledPos = {};
if (maxDist > 0) {
for (const [node, p] of Object.entries(centeredPos)) {
scaledPos[node] = p.map(val => val * scale / maxDist);
}
}
else {
// All points at the center
for (const node of Object.keys(centeredPos)) {
scaledPos[node] = Array(centeredPos[node].length).fill(0);
}
}
return scaledPos;
}
//# sourceMappingURL=rescale.js.map