d3-force-straighten
Version:
A D3 force plugin that straightens chains of connected nodes into rigid lines.
287 lines (273 loc) • 9.82 kB
JavaScript
// Version 1.0.0 d3-force-straighten - https://github.com/vasturiano/d3-force-straighten
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.d3 = global.d3 || {}));
})(this, (function (exports) { 'use strict';
function _arrayLikeToArray(r, a) {
(null == a || a > r.length) && (a = r.length);
for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];
return n;
}
function _arrayWithoutHoles(r) {
if (Array.isArray(r)) return _arrayLikeToArray(r);
}
function _createForOfIteratorHelper(r, e) {
var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];
if (!t) {
if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e) {
t && (r = t);
var n = 0,
F = function () {};
return {
s: F,
n: function () {
return n >= r.length ? {
done: true
} : {
done: false,
value: r[n++]
};
},
e: function (r) {
throw r;
},
f: F
};
}
throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
var o,
a = true,
u = false;
return {
s: function () {
t = t.call(r);
},
n: function () {
var r = t.next();
return a = r.done, r;
},
e: function (r) {
u = true, o = r;
},
f: function () {
try {
a || null == t.return || t.return();
} finally {
if (u) throw o;
}
}
};
}
function _iterableToArray(r) {
if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r);
}
function _nonIterableSpread() {
throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
function _toConsumableArray(r) {
return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread();
}
function _typeof(o) {
"@babel/helpers - typeof";
return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) {
return typeof o;
} : function (o) {
return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
}, _typeof(o);
}
function _unsupportedIterableToArray(r, a) {
if (r) {
if ("string" == typeof r) return _arrayLikeToArray(r, a);
var t = {}.toString.call(r).slice(8, -1);
return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0;
}
}
function straighten () {
var links = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
var nDim,
nodes = [],
id = function id(node) {
return node.index;
},
strength = 1,
considerAlpha = false;
var segments = [];
function force(alpha) {
var k = strength * (considerAlpha ? alpha : 1);
segments.forEach(function (nodes) {
var lr = linearRegression(nodes, nDim);
nodes.forEach(function (node) {
var target = closestPointOnLine(lr, node, nDim);
node.vx += (target.x - node.x) * k;
node.vy += (target.y - node.y) * k;
});
});
}
function initialize() {
var nodesById = new Map(nodes.map(function (d, i) {
return [id(d, i, nodes), d];
}));
var linksByNode = new Map();
links.forEach(function (link) {
if (_typeof(link.source) !== "object") link.source = nodesById.get(link.source) || link.source;
if (_typeof(link.target) !== "object") link.target = nodesById.get(link.target) || link.target;
[link.source, link.target].forEach(function (node) {
!linksByNode.has(node) && linksByNode.set(node, []);
linksByNode.get(node).push(link);
});
});
segments.length = 0; // clear segments
var visited = new Set();
var _traverseSingleLinks = function traverseSingleLinks(node, dir) {
var accNodes = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : new Set();
accNodes.add(node);
var links = linksByNode.get(node);
if (new Set(links.map(function (l) {
return [l.source, l.target].flat();
})).size !== 2) return [node];
var nextNode = links.filter(function (l) {
return !visited.has(l);
}).map(function (link) {
visited.add(link);
return link[dir];
}).filter(function (node) {
return !accNodes.has(node);
})[0] || null;
return [node].concat(_toConsumableArray(nextNode ? _traverseSingleLinks(nextNode, dir, accNodes) : []));
};
links.forEach(function (link) {
if (visited.has(link)) return;
visited.add(link);
var segment = [].concat(_toConsumableArray(_traverseSingleLinks(link.source, 'source').reverse()), _toConsumableArray(_traverseSingleLinks(link.target, 'target')));
segment.length > 2 && new Set(segment).size === segment.length && segments.push(segment);
});
}
force.initialize = function (initNodes) {
nodes = initNodes;
for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
args[_key - 1] = arguments[_key];
}
nDim = args.find(function (arg) {
return [1, 2, 3].includes(arg);
}) || 2;
initialize();
};
force.links = function (_) {
return arguments.length ? (links = _, initialize(), force) : links;
};
force.id = function (_) {
return arguments.length ? (id = _, force) : id;
};
force.strength = function (_) {
return arguments.length ? (strength = _, force) : strength;
};
force.considerAlpha = function (_) {
return arguments.length ? (considerAlpha = _, force) : considerAlpha;
};
return force;
}
//
function linearRegression(pnts) {
var nDim = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 2;
if (pnts.length < 2) {
throw new Error("At least two points are required for linear regression.");
}
var dims = ['x', nDim > 1 ? 'y' : null, nDim > 2 ? 'z' : null].filter(function (d) {
return d;
});
// Compute mean point
var mean = Object.fromEntries(dims.map(function (dim) {
return [dim, pnts.map(function (p) {
return p[dim];
}).reduce(function (agg, n) {
return agg + n;
}, 0) / pnts.length];
}));
// Compute covariance matrix
var Sxx = 0,
Sxy = 0,
Sxz = 0;
var Syy = 0,
Syz = 0,
Szz = 0;
var _iterator = _createForOfIteratorHelper(pnts),
_step;
try {
var _loop2 = function _loop2() {
var p = _step.value;
var d = Object.fromEntries(dims.map(function (dim) {
return [dim, p[dim] - mean[dim]];
}));
Sxx += d.x * d.x;
if (nDim > 1) {
Sxy += d.x * d.y;
Syy += d.y * d.y;
}
if (nDim > 2) {
Sxz += d.x * d.z;
Syz += d.y * d.z;
Szz += d.z * d.z;
}
};
for (_iterator.s(); !(_step = _iterator.n()).done;) {
_loop2();
}
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}
var covarianceMatrix = [[Sxx, Sxy, Sxz], [Sxy, Syy, Syz], [Sxz, Syz, Szz]];
function powerIteration(A) {
var numIter = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 100;
var b = [1, 1, 1];
var _loop = function _loop() {
var Ab = [A[0][0] * b[0] + A[0][1] * b[1] + A[0][2] * b[2], A[1][0] * b[0] + A[1][1] * b[1] + A[1][2] * b[2], A[2][0] * b[0] + A[2][1] * b[1] + A[2][2] * b[2]];
var norm = Math.hypot.apply(Math, Ab);
b = Ab.map(function (val) {
return val / norm;
});
};
for (var k = 0; k < numIter; k++) {
_loop();
}
return b;
}
var direction = powerIteration(covarianceMatrix);
return {
point: mean,
// point on the line (center)
direction: Object.fromEntries(dims.map(function (dim, idx) {
return [dim, direction[idx]];
})) // direction vector (normalized)
};
}
function closestPointOnLine(line, point, nDim) {
var dims = ['x', nDim > 1 ? 'y' : null, nDim > 2 ? 'z' : null].filter(function (d) {
return d;
});
var p = line.point;
var d = line.direction;
var qMinusP = Object.fromEntries(dims.map(function (dim) {
return [dim, point[dim] - p[dim]];
}));
// Dot product (q - p) · d
var dotNumerator = dims.reduce(function (agg, dim) {
return agg + qMinusP[dim] * d[dim];
}, 0);
// Dot product d · d (magnitude squared)
var dotDenominator = dims.reduce(function (agg, dim) {
return agg + d[dim] * d[dim];
}, 0);
// Scalar projection t
var t = dotDenominator === 0 ? 0 : dotNumerator / dotDenominator;
// Closest point = p + t * d
return Object.fromEntries(dims.map(function (dim) {
return [dim, p[dim] + t * d[dim]];
}));
}
exports.forceStraighten = straighten;
}));
//# sourceMappingURL=d3-force-straighten.js.map