three-forcegraph
Version:
Force-directed graph as a ThreeJS 3d object
1,271 lines (1,234 loc) • 61.9 kB
JavaScript
import { BufferGeometry, Mesh, SphereGeometry, Color, MeshLambertMaterial, BufferAttribute, Line, Group, CylinderGeometry, Matrix4, Box3, CubicBezierCurve3, QuadraticBezierCurve3, LineBasicMaterial, ConeGeometry, TubeGeometry, Vector3 } from 'three';
import { forceRadial, forceSimulation, forceLink, forceManyBody, forceCenter } from 'd3-force-3d';
import graph from 'ngraph.graph';
import forcelayout from 'ngraph.forcelayout';
import Kapsule from 'kapsule';
import accessorFn from 'accessor-fn';
import { min, max } from 'd3-array';
import DataBindMapper from 'data-bind-mapper';
import { scaleOrdinal } from 'd3-scale';
import { schemePaired } from 'd3-scale-chromatic';
import tinyColor from 'tinycolor2';
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 _arrayWithHoles(r) {
if (Array.isArray(r)) return r;
}
function _arrayWithoutHoles(r) {
if (Array.isArray(r)) return _arrayLikeToArray(r);
}
function _assertClassBrand(e, t, n) {
if ("function" == typeof e ? e === t : e.has(t)) return arguments.length < 3 ? t : n;
throw new TypeError("Private element is not present on this object");
}
function _assertThisInitialized(e) {
if (void 0 === e) throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
return e;
}
function _callSuper(t, o, e) {
return o = _getPrototypeOf(o), _possibleConstructorReturn(t, _isNativeReflectConstruct() ? Reflect.construct(o, e || [], _getPrototypeOf(t).constructor) : o.apply(t, e));
}
function _checkPrivateRedeclaration(e, t) {
if (t.has(e)) throw new TypeError("Cannot initialize the same private elements twice on an object");
}
function _classCallCheck(a, n) {
if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function");
}
function _classPrivateFieldGet2(s, a) {
return s.get(_assertClassBrand(s, a));
}
function _classPrivateFieldInitSpec(e, t, a) {
_checkPrivateRedeclaration(e, t), t.set(e, a);
}
function _classPrivateFieldSet2(s, a, r) {
return s.set(_assertClassBrand(s, a), r), r;
}
function _construct(t, e, r) {
if (_isNativeReflectConstruct()) return Reflect.construct.apply(null, arguments);
var o = [null];
o.push.apply(o, e);
var p = new (t.bind.apply(t, o))();
return p;
}
function _defineProperties(e, r) {
for (var t = 0; t < r.length; t++) {
var o = r[t];
o.enumerable = o.enumerable || false, o.configurable = true, "value" in o && (o.writable = true), Object.defineProperty(e, _toPropertyKey(o.key), o);
}
}
function _createClass(e, r, t) {
return r && _defineProperties(e.prototype, r), Object.defineProperty(e, "prototype", {
writable: false
}), e;
}
function _defineProperty(e, r, t) {
return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
value: t,
enumerable: true,
configurable: true,
writable: true
}) : e[r] = t, e;
}
function _get() {
return _get = "undefined" != typeof Reflect && Reflect.get ? Reflect.get.bind() : function (e, t, r) {
var p = _superPropBase(e, t);
if (p) {
var n = Object.getOwnPropertyDescriptor(p, t);
return n.get ? n.get.call(arguments.length < 3 ? e : r) : n.value;
}
}, _get.apply(null, arguments);
}
function _getPrototypeOf(t) {
return _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function (t) {
return t.__proto__ || Object.getPrototypeOf(t);
}, _getPrototypeOf(t);
}
function _inherits(t, e) {
if ("function" != typeof e && null !== e) throw new TypeError("Super expression must either be null or a function");
t.prototype = Object.create(e && e.prototype, {
constructor: {
value: t,
writable: true,
configurable: true
}
}), Object.defineProperty(t, "prototype", {
writable: false
}), e && _setPrototypeOf(t, e);
}
function _isNativeReflectConstruct() {
try {
var t = !Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {}));
} catch (t) {}
return (_isNativeReflectConstruct = function () {
return !!t;
})();
}
function _iterableToArray(r) {
if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r);
}
function _iterableToArrayLimit(r, l) {
var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];
if (null != t) {
var e,
n,
i,
u,
a = [],
f = true,
o = false;
try {
if (i = (t = t.call(r)).next, 0 === l) ; else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0);
} catch (r) {
o = true, n = r;
} finally {
try {
if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return;
} finally {
if (o) throw n;
}
}
return a;
}
}
function _nonIterableRest() {
throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
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 ownKeys(e, r) {
var t = Object.keys(e);
if (Object.getOwnPropertySymbols) {
var o = Object.getOwnPropertySymbols(e);
r && (o = o.filter(function (r) {
return Object.getOwnPropertyDescriptor(e, r).enumerable;
})), t.push.apply(t, o);
}
return t;
}
function _objectSpread2(e) {
for (var r = 1; r < arguments.length; r++) {
var t = null != arguments[r] ? arguments[r] : {};
r % 2 ? ownKeys(Object(t), true).forEach(function (r) {
_defineProperty(e, r, t[r]);
}) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) {
Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r));
});
}
return e;
}
function _possibleConstructorReturn(t, e) {
if (e && ("object" == typeof e || "function" == typeof e)) return e;
if (void 0 !== e) throw new TypeError("Derived constructors may only return object or undefined");
return _assertThisInitialized(t);
}
function _setPrototypeOf(t, e) {
return _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function (t, e) {
return t.__proto__ = e, t;
}, _setPrototypeOf(t, e);
}
function _slicedToArray(r, e) {
return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest();
}
function _superPropBase(t, o) {
for (; !{}.hasOwnProperty.call(t, o) && null !== (t = _getPrototypeOf(t)););
return t;
}
function _superPropGet(t, o, e, r) {
var p = _get(_getPrototypeOf(t.prototype ), o, e);
return "function" == typeof p ? function (t) {
return p.apply(e, t);
} : p;
}
function _toConsumableArray(r) {
return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread();
}
function _toPrimitive(t, r) {
if ("object" != typeof t || !t) return t;
var e = t[Symbol.toPrimitive];
if (void 0 !== e) {
var i = e.call(t, r);
if ("object" != typeof i) return i;
throw new TypeError("@@toPrimitive must return a primitive value.");
}
return ("string" === r ? String : Number)(t);
}
function _toPropertyKey(t) {
var i = _toPrimitive(t, "string");
return "symbol" == typeof i ? i : i + "";
}
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;
}
}
var _materialDispose = function materialDispose(material) {
if (material instanceof Array) {
material.forEach(_materialDispose);
} else {
if (material.map) {
material.map.dispose();
}
material.dispose();
}
};
var _deallocate = function deallocate(obj) {
if (obj.geometry) {
obj.geometry.dispose();
}
if (obj.material) {
_materialDispose(obj.material);
}
if (obj.texture) {
obj.texture.dispose();
}
if (obj.children) {
obj.children.forEach(_deallocate);
}
};
var emptyObject = function emptyObject(obj) {
while (obj.children.length) {
var childObj = obj.children[0];
obj.remove(childObj);
_deallocate(childObj);
}
};
var _dataBindAttr = /*#__PURE__*/new WeakMap();
var _objBindAttr = /*#__PURE__*/new WeakMap();
var ThreeDigest = /*#__PURE__*/function (_DataBindMapper) {
function ThreeDigest(scene) {
var _this;
var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
_ref$dataBindAttr = _ref.dataBindAttr,
dataBindAttr = _ref$dataBindAttr === void 0 ? '__data' : _ref$dataBindAttr,
_ref$objBindAttr = _ref.objBindAttr,
objBindAttr = _ref$objBindAttr === void 0 ? '__threeObj' : _ref$objBindAttr;
_classCallCheck(this, ThreeDigest);
_this = _callSuper(this, ThreeDigest);
_defineProperty(_this, "scene", void 0);
_classPrivateFieldInitSpec(_this, _dataBindAttr, void 0);
_classPrivateFieldInitSpec(_this, _objBindAttr, void 0);
_this.scene = scene;
_classPrivateFieldSet2(_dataBindAttr, _this, dataBindAttr);
_classPrivateFieldSet2(_objBindAttr, _this, objBindAttr);
_this.onRemoveObj(function () {});
return _this;
}
_inherits(ThreeDigest, _DataBindMapper);
return _createClass(ThreeDigest, [{
key: "onCreateObj",
value: function onCreateObj(fn) {
var _this2 = this;
_superPropGet(ThreeDigest, "onCreateObj", this)([function (d) {
var obj = fn(d);
d[_classPrivateFieldGet2(_objBindAttr, _this2)] = obj;
obj[_classPrivateFieldGet2(_dataBindAttr, _this2)] = d;
_this2.scene.add(obj);
return obj;
}]);
return this;
}
}, {
key: "onRemoveObj",
value: function onRemoveObj(fn) {
var _this3 = this;
_superPropGet(ThreeDigest, "onRemoveObj", this)([function (obj, dId) {
var d = _superPropGet(ThreeDigest, "getData", _this3)([obj]);
fn(obj, dId);
_this3.scene.remove(obj);
emptyObject(obj);
delete d[_classPrivateFieldGet2(_objBindAttr, _this3)];
}]);
return this;
}
}]);
}(DataBindMapper);
var colorStr2Hex = function colorStr2Hex(str) {
return isNaN(str) ? parseInt(tinyColor(str).toHex(), 16) : str;
};
var colorAlpha = function colorAlpha(str) {
return isNaN(str) ? tinyColor(str).getAlpha() : 1;
};
var autoColorScale = scaleOrdinal(schemePaired);
// Autoset attribute colorField by colorByAccessor property
// If an object has already a color, don't set it
// Objects can be nodes or links
function autoColorObjects(objects, colorByAccessor, colorField) {
if (!colorByAccessor || typeof colorField !== 'string') return;
objects.filter(function (obj) {
return !obj[colorField];
}).forEach(function (obj) {
obj[colorField] = autoColorScale(colorByAccessor(obj));
});
}
function getDagDepths (_ref, idAccessor) {
var nodes = _ref.nodes,
links = _ref.links;
var _ref2 = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {},
_ref2$nodeFilter = _ref2.nodeFilter,
nodeFilter = _ref2$nodeFilter === void 0 ? function () {
return true;
} : _ref2$nodeFilter,
_ref2$onLoopError = _ref2.onLoopError,
onLoopError = _ref2$onLoopError === void 0 ? function (loopIds) {
throw "Invalid DAG structure! Found cycle in node path: ".concat(loopIds.join(' -> '), ".");
} : _ref2$onLoopError;
// linked graph
var graph = {};
nodes.forEach(function (node) {
return graph[idAccessor(node)] = {
data: node,
out: [],
depth: -1,
skip: !nodeFilter(node)
};
});
links.forEach(function (_ref3) {
var source = _ref3.source,
target = _ref3.target;
var sourceId = getNodeId(source);
var targetId = getNodeId(target);
if (!graph.hasOwnProperty(sourceId)) throw "Missing source node with id: ".concat(sourceId);
if (!graph.hasOwnProperty(targetId)) throw "Missing target node with id: ".concat(targetId);
var sourceNode = graph[sourceId];
var targetNode = graph[targetId];
sourceNode.out.push(targetNode);
function getNodeId(node) {
return _typeof(node) === 'object' ? idAccessor(node) : node;
}
});
var foundLoops = [];
traverse(Object.values(graph));
var nodeDepths = Object.assign.apply(Object, [{}].concat(_toConsumableArray(Object.entries(graph).filter(function (_ref4) {
var _ref5 = _slicedToArray(_ref4, 2),
node = _ref5[1];
return !node.skip;
}).map(function (_ref6) {
var _ref7 = _slicedToArray(_ref6, 2),
id = _ref7[0],
node = _ref7[1];
return _defineProperty({}, id, node.depth);
}))));
return nodeDepths;
function traverse(nodes) {
var nodeStack = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [];
var currentDepth = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0;
var _loop = function _loop() {
var node = nodes[i];
if (nodeStack.indexOf(node) !== -1) {
var loop = [].concat(_toConsumableArray(nodeStack.slice(nodeStack.indexOf(node))), [node]).map(function (d) {
return idAccessor(d.data);
});
if (!foundLoops.some(function (foundLoop) {
return foundLoop.length === loop.length && foundLoop.every(function (id, idx) {
return id === loop[idx];
});
})) {
foundLoops.push(loop);
onLoopError(loop);
}
return 1; // continue
}
if (currentDepth > node.depth) {
// Don't unnecessarily revisit chunks of the graph
node.depth = currentDepth;
traverse(node.out, [].concat(_toConsumableArray(nodeStack), [node]), currentDepth + (node.skip ? 0 : 1));
}
};
for (var i = 0, l = nodes.length; i < l; i++) {
if (_loop()) continue;
}
}
}
var three$1 = window.THREE ? window.THREE // Prefer consumption from global THREE, if exists
: {
Group: Group,
Mesh: Mesh,
MeshLambertMaterial: MeshLambertMaterial,
Color: Color,
BufferGeometry: BufferGeometry,
BufferAttribute: BufferAttribute,
Matrix4: Matrix4,
Vector3: Vector3,
SphereGeometry: SphereGeometry,
CylinderGeometry: CylinderGeometry,
TubeGeometry: TubeGeometry,
ConeGeometry: ConeGeometry,
Line: Line,
LineBasicMaterial: LineBasicMaterial,
QuadraticBezierCurve3: QuadraticBezierCurve3,
CubicBezierCurve3: CubicBezierCurve3,
Box3: Box3
};
var ngraph = {
graph: graph,
forcelayout: forcelayout
};
//
var DAG_LEVEL_NODE_RATIO = 2;
// support multiple method names for backwards threejs compatibility
var setAttributeFn = new three$1.BufferGeometry().setAttribute ? 'setAttribute' : 'addAttribute';
var applyMatrix4Fn = new three$1.BufferGeometry().applyMatrix4 ? 'applyMatrix4' : 'applyMatrix';
var ForceGraph = Kapsule({
props: {
jsonUrl: {
onChange: function onChange(jsonUrl, state) {
var _this = this;
if (jsonUrl && !state.fetchingJson) {
// Load data asynchronously
state.fetchingJson = true;
state.onLoading();
fetch(jsonUrl).then(function (r) {
return r.json();
}).then(function (json) {
state.fetchingJson = false;
state.onFinishLoading(json);
_this.graphData(json);
});
}
},
triggerUpdate: false
},
graphData: {
"default": {
nodes: [],
links: []
},
onChange: function onChange(graphData, state) {
state.engineRunning = false; // Pause simulation immediately
}
},
numDimensions: {
"default": 3,
onChange: function onChange(numDim, state) {
var chargeForce = state.d3ForceLayout.force('charge');
// Increase repulsion on 3D mode for improved spatial separation
if (chargeForce) {
chargeForce.strength(numDim > 2 ? -60 : -30);
}
if (numDim < 3) {
eraseDimension(state.graphData.nodes, 'z');
}
if (numDim < 2) {
eraseDimension(state.graphData.nodes, 'y');
}
function eraseDimension(nodes, dim) {
nodes.forEach(function (d) {
delete d[dim]; // position
delete d["v".concat(dim)]; // velocity
});
}
}
},
dagMode: {
onChange: function onChange(dagMode, state) {
// td, bu, lr, rl, zin, zout, radialin, radialout
!dagMode && state.forceEngine === 'd3' && (state.graphData.nodes || []).forEach(function (n) {
return n.fx = n.fy = n.fz = undefined;
}); // unfix nodes when disabling dag mode
}
},
dagLevelDistance: {},
dagNodeFilter: {
"default": function _default(node) {
return true;
}
},
onDagError: {
triggerUpdate: false
},
nodeRelSize: {
"default": 4
},
// volume per val unit
nodeId: {
"default": 'id'
},
nodeVal: {
"default": 'val'
},
nodeResolution: {
"default": 8
},
// how many slice segments in the sphere's circumference
nodeColor: {
"default": 'color'
},
nodeAutoColorBy: {},
nodeOpacity: {
"default": 0.75
},
nodeVisibility: {
"default": true
},
nodeThreeObject: {},
nodeThreeObjectExtend: {
"default": false
},
nodePositionUpdate: {
triggerUpdate: false
},
// custom function to call for updating the node's position. Signature: (threeObj, { x, y, z}, node). If the function returns a truthy value, the regular node position update will not run.
linkSource: {
"default": 'source'
},
linkTarget: {
"default": 'target'
},
linkVisibility: {
"default": true
},
linkColor: {
"default": 'color'
},
linkAutoColorBy: {},
linkOpacity: {
"default": 0.2
},
linkWidth: {},
// Rounded to nearest decimal. For falsy values use dimensionless line with 1px regardless of distance.
linkResolution: {
"default": 6
},
// how many radial segments in each line tube's geometry
linkCurvature: {
"default": 0,
triggerUpdate: false
},
// line curvature radius (0: straight, 1: semi-circle)
linkCurveRotation: {
"default": 0,
triggerUpdate: false
},
// line curve rotation along the line axis (0: interection with XY plane, PI: upside down)
linkMaterial: {},
linkThreeObject: {},
linkThreeObjectExtend: {
"default": false
},
linkPositionUpdate: {
triggerUpdate: false
},
// custom function to call for updating the link's position. Signature: (threeObj, { start: { x, y, z}, end: { x, y, z }}, link). If the function returns a truthy value, the regular link position update will not run.
linkDirectionalArrowLength: {
"default": 0
},
linkDirectionalArrowColor: {},
linkDirectionalArrowRelPos: {
"default": 0.5,
triggerUpdate: false
},
// value between 0<>1 indicating the relative pos along the (exposed) line
linkDirectionalArrowResolution: {
"default": 8
},
// how many slice segments in the arrow's conic circumference
linkDirectionalParticles: {
"default": 0
},
// animate photons travelling in the link direction
linkDirectionalParticleSpeed: {
"default": 0.01,
triggerUpdate: false
},
// in link length ratio per frame
linkDirectionalParticleOffset: {
"default": 0,
triggerUpdate: false
},
// starting position offset along the link's length, like a pre-delay. Values between [0, 1]
linkDirectionalParticleWidth: {
"default": 0.5
},
linkDirectionalParticleColor: {},
linkDirectionalParticleResolution: {
"default": 4
},
// how many slice segments in the particle sphere's circumference
linkDirectionalParticleThreeObject: {},
forceEngine: {
"default": 'd3'
},
// d3 or ngraph
d3AlphaMin: {
"default": 0
},
d3AlphaDecay: {
"default": 0.0228,
triggerUpdate: false,
onChange: function onChange(alphaDecay, state) {
state.d3ForceLayout.alphaDecay(alphaDecay);
}
},
d3AlphaTarget: {
"default": 0,
triggerUpdate: false,
onChange: function onChange(alphaTarget, state) {
state.d3ForceLayout.alphaTarget(alphaTarget);
}
},
d3VelocityDecay: {
"default": 0.4,
triggerUpdate: false,
onChange: function onChange(velocityDecay, state) {
state.d3ForceLayout.velocityDecay(velocityDecay);
}
},
ngraphPhysics: {
"default": {
// defaults from https://github.com/anvaka/ngraph.physics.simulator/blob/master/index.js
timeStep: 20,
gravity: -1.2,
theta: 0.8,
springLength: 30,
springCoefficient: 0.0008,
dragCoefficient: 0.02
}
},
warmupTicks: {
"default": 0,
triggerUpdate: false
},
// how many times to tick the force engine at init before starting to render
cooldownTicks: {
"default": Infinity,
triggerUpdate: false
},
cooldownTime: {
"default": 15000,
triggerUpdate: false
},
// ms
onLoading: {
"default": function _default() {},
triggerUpdate: false
},
onFinishLoading: {
"default": function _default() {},
triggerUpdate: false
},
onUpdate: {
"default": function _default() {},
triggerUpdate: false
},
onFinishUpdate: {
"default": function _default() {},
triggerUpdate: false
},
onEngineTick: {
"default": function _default() {},
triggerUpdate: false
},
onEngineStop: {
"default": function _default() {},
triggerUpdate: false
}
},
methods: {
refresh: function refresh(state) {
state._flushObjects = true;
state._rerender();
return this;
},
// Expose d3 forces for external manipulation
d3Force: function d3Force(state, forceName, forceFn) {
if (forceFn === undefined) {
return state.d3ForceLayout.force(forceName); // Force getter
}
state.d3ForceLayout.force(forceName, forceFn); // Force setter
return this;
},
d3ReheatSimulation: function d3ReheatSimulation(state) {
state.d3ForceLayout.alpha(1);
this.resetCountdown();
return this;
},
// reset cooldown state
resetCountdown: function resetCountdown(state) {
state.cntTicks = 0;
state.startTickTime = new Date();
state.engineRunning = true;
return this;
},
tickFrame: function tickFrame(state) {
var isD3Sim = state.forceEngine !== 'ngraph';
if (state.engineRunning) {
layoutTick();
}
updateArrows();
updatePhotons();
return this;
//
function layoutTick() {
if (++state.cntTicks > state.cooldownTicks || new Date() - state.startTickTime > state.cooldownTime || isD3Sim && state.d3AlphaMin > 0 && state.d3ForceLayout.alpha() < state.d3AlphaMin) {
state.engineRunning = false; // Stop ticking graph
state.onEngineStop();
} else {
state.layout[isD3Sim ? 'tick' : 'step'](); // Tick it
state.onEngineTick();
}
var nodeThreeObjectExtendAccessor = accessorFn(state.nodeThreeObjectExtend);
// Update nodes position
state.nodeDataMapper.entries().forEach(function (_ref) {
var _ref2 = _slicedToArray(_ref, 2),
node = _ref2[0],
obj = _ref2[1];
if (!obj) return;
var pos = isD3Sim ? node : state.layout.getNodePosition(node[state.nodeId]);
var extendedObj = nodeThreeObjectExtendAccessor(node);
if (!state.nodePositionUpdate || !state.nodePositionUpdate(extendedObj ? obj.children[0] : obj, {
x: pos.x,
y: pos.y,
z: pos.z
}, node) // pass child custom object if extending the default
|| extendedObj) {
obj.position.x = pos.x;
obj.position.y = pos.y || 0;
obj.position.z = pos.z || 0;
}
});
// Update links position
var linkWidthAccessor = accessorFn(state.linkWidth);
var linkCurvatureAccessor = accessorFn(state.linkCurvature);
var linkCurveRotationAccessor = accessorFn(state.linkCurveRotation);
var linkThreeObjectExtendAccessor = accessorFn(state.linkThreeObjectExtend);
state.linkDataMapper.entries().forEach(function (_ref3) {
var _ref4 = _slicedToArray(_ref3, 2),
link = _ref4[0],
lineObj = _ref4[1];
if (!lineObj) return;
var pos = isD3Sim ? link : state.layout.getLinkPosition(state.layout.graph.getLink(link.source, link.target).id);
var start = pos[isD3Sim ? 'source' : 'from'];
var end = pos[isD3Sim ? 'target' : 'to'];
if (!start || !end || !start.hasOwnProperty('x') || !end.hasOwnProperty('x')) return; // skip invalid link
calcLinkCurve(link); // calculate link curve for all links, including custom replaced, so it can be used in directional functionality
var extendedObj = linkThreeObjectExtendAccessor(link);
if (state.linkPositionUpdate && state.linkPositionUpdate(extendedObj ? lineObj.children[1] : lineObj,
// pass child custom object if extending the default
{
start: {
x: start.x,
y: start.y,
z: start.z
},
end: {
x: end.x,
y: end.y,
z: end.z
}
}, link) && !extendedObj) {
// exit if successfully custom updated position of non-extended obj
return;
}
var curveResolution = 30; // # line segments
var curve = link.__curve;
// select default line obj if it's an extended group
var line = lineObj.children.length ? lineObj.children[0] : lineObj;
if (line.type === 'Line') {
// Update line geometry
if (!curve) {
// straight line
var linePos = line.geometry.getAttribute('position');
if (!linePos || !linePos.array || linePos.array.length !== 6) {
line.geometry[setAttributeFn]('position', linePos = new three$1.BufferAttribute(new Float32Array(2 * 3), 3));
}
linePos.array[0] = start.x;
linePos.array[1] = start.y || 0;
linePos.array[2] = start.z || 0;
linePos.array[3] = end.x;
linePos.array[4] = end.y || 0;
linePos.array[5] = end.z || 0;
linePos.needsUpdate = true;
} else {
// bezier curve line
var curvePnts = curve.getPoints(curveResolution);
// resize buffer if needed
if (line.geometry.getAttribute('position').array.length !== curvePnts.length * 3) {
line.geometry[setAttributeFn]('position', new three$1.BufferAttribute(new Float32Array(curvePnts.length * 3), 3));
}
line.geometry.setFromPoints(curvePnts);
}
line.geometry.computeBoundingSphere();
} else if (line.type === 'Mesh') {
// Update cylinder geometry
if (!curve) {
// straight tube
if (!line.geometry.type.match(/^Cylinder(Buffer)?Geometry$/)) {
var linkWidth = Math.ceil(linkWidthAccessor(link) * 10) / 10;
var r = linkWidth / 2;
var geometry = new three$1.CylinderGeometry(r, r, 1, state.linkResolution, 1, false);
geometry[applyMatrix4Fn](new three$1.Matrix4().makeTranslation(0, 1 / 2, 0));
geometry[applyMatrix4Fn](new three$1.Matrix4().makeRotationX(Math.PI / 2));
line.geometry.dispose();
line.geometry = geometry;
}
var vStart = new three$1.Vector3(start.x, start.y || 0, start.z || 0);
var vEnd = new three$1.Vector3(end.x, end.y || 0, end.z || 0);
var distance = vStart.distanceTo(vEnd);
line.position.x = vStart.x;
line.position.y = vStart.y;
line.position.z = vStart.z;
line.scale.z = distance;
line.parent.localToWorld(vEnd); // lookAt requires world coords
line.lookAt(vEnd);
} else {
// curved tube
if (!line.geometry.type.match(/^Tube(Buffer)?Geometry$/)) {
// reset object positioning
line.position.set(0, 0, 0);
line.rotation.set(0, 0, 0);
line.scale.set(1, 1, 1);
}
var _linkWidth = Math.ceil(linkWidthAccessor(link) * 10) / 10;
var _r = _linkWidth / 2;
var _geometry = new three$1.TubeGeometry(curve, curveResolution, _r, state.linkResolution, false);
line.geometry.dispose();
line.geometry = _geometry;
}
}
});
//
function calcLinkCurve(link) {
var pos = isD3Sim ? link : state.layout.getLinkPosition(state.layout.graph.getLink(link.source, link.target).id);
var start = pos[isD3Sim ? 'source' : 'from'];
var end = pos[isD3Sim ? 'target' : 'to'];
if (!start || !end || !start.hasOwnProperty('x') || !end.hasOwnProperty('x')) return; // skip invalid link
var curvature = linkCurvatureAccessor(link);
if (!curvature) {
link.__curve = null; // Straight line
} else {
// bezier curve line (only for line types)
var vStart = new three$1.Vector3(start.x, start.y || 0, start.z || 0);
var vEnd = new three$1.Vector3(end.x, end.y || 0, end.z || 0);
var l = vStart.distanceTo(vEnd); // line length
var curve;
var curveRotation = linkCurveRotationAccessor(link);
if (l > 0) {
var dx = end.x - start.x;
var dy = end.y - start.y || 0;
var vLine = new three$1.Vector3().subVectors(vEnd, vStart);
var cp = vLine.clone().multiplyScalar(curvature).cross(dx !== 0 || dy !== 0 ? new three$1.Vector3(0, 0, 1) : new three$1.Vector3(0, 1, 0)) // avoid cross-product of parallel vectors (prefer Z, fallback to Y)
.applyAxisAngle(vLine.normalize(), curveRotation) // rotate along line axis according to linkCurveRotation
.add(new three$1.Vector3().addVectors(vStart, vEnd).divideScalar(2));
curve = new three$1.QuadraticBezierCurve3(vStart, cp, vEnd);
} else {
// Same point, draw a loop
var d = curvature * 70;
var endAngle = -curveRotation; // Rotate clockwise (from Z angle perspective)
var startAngle = endAngle + Math.PI / 2;
curve = new three$1.CubicBezierCurve3(vStart, new three$1.Vector3(d * Math.cos(startAngle), d * Math.sin(startAngle), 0).add(vStart), new three$1.Vector3(d * Math.cos(endAngle), d * Math.sin(endAngle), 0).add(vStart), vEnd);
}
link.__curve = curve;
}
}
}
function updateArrows() {
// update link arrow position
var arrowRelPosAccessor = accessorFn(state.linkDirectionalArrowRelPos);
var arrowLengthAccessor = accessorFn(state.linkDirectionalArrowLength);
var nodeValAccessor = accessorFn(state.nodeVal);
state.arrowDataMapper.entries().forEach(function (_ref5) {
var _ref6 = _slicedToArray(_ref5, 2),
link = _ref6[0],
arrowObj = _ref6[1];
if (!arrowObj) return;
var pos = isD3Sim ? link : state.layout.getLinkPosition(state.layout.graph.getLink(link.source, link.target).id);
var start = pos[isD3Sim ? 'source' : 'from'];
var end = pos[isD3Sim ? 'target' : 'to'];
if (!start || !end || !start.hasOwnProperty('x') || !end.hasOwnProperty('x')) return; // skip invalid link
var startR = Math.cbrt(Math.max(0, nodeValAccessor(start) || 1)) * state.nodeRelSize;
var endR = Math.cbrt(Math.max(0, nodeValAccessor(end) || 1)) * state.nodeRelSize;
var arrowLength = arrowLengthAccessor(link);
var arrowRelPos = arrowRelPosAccessor(link);
var getPosAlongLine = link.__curve ? function (t) {
return link.__curve.getPoint(t);
} // interpolate along bezier curve
: function (t) {
// straight line: interpolate linearly
var iplt = function iplt(dim, start, end, t) {
return start[dim] + (end[dim] - start[dim]) * t || 0;
};
return {
x: iplt('x', start, end, t),
y: iplt('y', start, end, t),
z: iplt('z', start, end, t)
};
};
var lineLen = link.__curve ? link.__curve.getLength() : Math.sqrt(['x', 'y', 'z'].map(function (dim) {
return Math.pow((end[dim] || 0) - (start[dim] || 0), 2);
}).reduce(function (acc, v) {
return acc + v;
}, 0));
var posAlongLine = startR + arrowLength + (lineLen - startR - endR - arrowLength) * arrowRelPos;
var arrowHead = getPosAlongLine(posAlongLine / lineLen);
var arrowTail = getPosAlongLine((posAlongLine - arrowLength) / lineLen);
['x', 'y', 'z'].forEach(function (dim) {
return arrowObj.position[dim] = arrowTail[dim];
});
var headVec = _construct(three$1.Vector3, _toConsumableArray(['x', 'y', 'z'].map(function (c) {
return arrowHead[c];
})));
arrowObj.parent.localToWorld(headVec); // lookAt requires world coords
arrowObj.lookAt(headVec);
});
}
function updatePhotons() {
// update link particle positions
var particleSpeedAccessor = accessorFn(state.linkDirectionalParticleSpeed);
var particleOffsetAccessor = accessorFn(state.linkDirectionalParticleOffset);
state.graphData.links.forEach(function (link) {
var photonsObj = state.particlesDataMapper.getObj(link);
var cyclePhotons = photonsObj && photonsObj.children;
var singleHopPhotons = link.__singleHopPhotonsObj && link.__singleHopPhotonsObj.children;
if ((!singleHopPhotons || !singleHopPhotons.length) && (!cyclePhotons || !cyclePhotons.length)) return;
var pos = isD3Sim ? link : state.layout.getLinkPosition(state.layout.graph.getLink(link.source, link.target).id);
var start = pos[isD3Sim ? 'source' : 'from'];
var end = pos[isD3Sim ? 'target' : 'to'];
if (!start || !end || !start.hasOwnProperty('x') || !end.hasOwnProperty('x')) return; // skip invalid link
var particleSpeed = particleSpeedAccessor(link);
var particleOffset = Math.abs(particleOffsetAccessor(link));
var getPhotonPos = link.__curve ? function (t) {
return link.__curve.getPoint(t);
} // interpolate along bezier curve
: function (t) {
// straight line: interpolate linearly
var iplt = function iplt(dim, start, end, t) {
return start[dim] + (end[dim] - start[dim]) * t || 0;
};
return {
x: iplt('x', start, end, t),
y: iplt('y', start, end, t),
z: iplt('z', start, end, t)
};
};
var photons = [].concat(_toConsumableArray(cyclePhotons || []), _toConsumableArray(singleHopPhotons || []));
photons.forEach(function (photon, idx) {
var singleHop = photon.parent.__linkThreeObjType === 'singleHopPhotons';
if (!photon.hasOwnProperty('__progressRatio')) {
photon.__progressRatio = singleHop ? 0 : (idx + particleOffset) / cyclePhotons.length;
}
photon.__progressRatio += particleSpeed;
if (photon.__progressRatio >= 1) {
if (!singleHop) {
photon.__progressRatio = photon.__progressRatio % 1;
} else {
// remove particle
photon.parent.remove(photon);
emptyObject(photon);
return;
}
}
var photonPosRatio = photon.__progressRatio;
var pos = getPhotonPos(photonPosRatio);
// Orient asymmetrical particles to target
photon.geometry.type !== 'SphereGeometry' && photon.lookAt(pos.x, pos.y, pos.z);
['x', 'y', 'z'].forEach(function (dim) {
return photon.position[dim] = pos[dim];
});
});
});
}
},
emitParticle: function emitParticle(state, link) {
if (link && state.graphData.links.includes(link)) {
if (!link.__singleHopPhotonsObj) {
var obj = new three$1.Group();
obj.__linkThreeObjType = 'singleHopPhotons';
link.__singleHopPhotonsObj = obj;
state.graphScene.add(obj);
}
var particleObj = accessorFn(state.linkDirectionalParticleThreeObject)(link);
if (particleObj && state.linkDirectionalParticleThreeObject === particleObj) {
// clone object if it's a shared object among all links
particleObj = particleObj.clone();
}
if (!particleObj) {
var particleWidthAccessor = accessorFn(state.linkDirectionalParticleWidth);
var photonR = Math.ceil(particleWidthAccessor(link) * 10) / 10 / 2;
var numSegments = state.linkDirectionalParticleResolution;
var particleGeometry = new three$1.SphereGeometry(photonR, numSegments, numSegments);
var linkColorAccessor = accessorFn(state.linkColor);
var particleColorAccessor = accessorFn(state.linkDirectionalParticleColor);
var photonColor = particleColorAccessor(link) || linkColorAccessor(link) || '#f0f0f0';
var materialColor = new three$1.Color(colorStr2Hex(photonColor));
var opacity = state.linkOpacity * 3;
var particleMaterial = new three$1.MeshLambertMaterial({
color: materialColor,
transparent: true,
opacity: opacity
});
particleObj = new three$1.Mesh(particleGeometry, particleMaterial);
}
// add a single hop particle
link.__singleHopPhotonsObj.add(particleObj);
}
return this;
},
getGraphBbox: function getGraphBbox(state) {
var nodeFilter = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : function () {
return true;
};
if (!state.initialised) return null;
// recursively collect all nested geometries bboxes
var bboxes = function getBboxes(obj) {
var bboxes = [];
if (obj.geometry) {
obj.geometry.computeBoundingBox();
var box = new three$1.Box3();
box.copy(obj.geometry.boundingBox).applyMatrix4(obj.matrixWorld);
bboxes.push(box);
}
return bboxes.concat.apply(bboxes, _toConsumableArray((obj.children || []).filter(function (obj) {
return !obj.hasOwnProperty('__graphObjType') || obj.__graphObjType === 'node' && nodeFilter(obj.__data);
} // exclude filtered out nodes
).map(getBboxes)));
}(state.graphScene);
if (!bboxes.length) return null;
// extract global x,y,z min/max
return Object.assign.apply(Object, _toConsumableArray(['x', 'y', 'z'].map(function (c) {
return _defineProperty({}, c, [min(bboxes, function (bb) {
return bb.min[c];
}), max(bboxes, function (bb) {
return bb.max[c];
})]);
})));
}
},
stateInit: function stateInit() {
return {
d3ForceLayout: forceSimulation().force('link', forceLink()).force('charge', forceManyBody()).force('center', forceCenter()).force('dagRadial', null).stop(),
engineRunning: false
};
},
init: function init(threeObj, state) {
// Main three object to manipulate
state.graphScene = threeObj;
state.nodeDataMapper = new ThreeDigest(threeObj, {
objBindAttr: '__threeObj'
});
state.linkDataMapper = new ThreeDigest(threeObj, {
objBindAttr: '__lineObj'
});
state.arrowDataMapper = new ThreeDigest(threeObj, {
objBindAttr: '__arrowObj'
});
state.particlesDataMapper = new ThreeDigest(threeObj, {
objBindAttr: '__photonsObj'
});
},
update: function update(state, changedProps) {
var hasAnyPropChanged = function hasAnyPropChanged(propList) {
return propList.some(function (p) {
return changedProps.hasOwnProperty(p);
});
};
state.engineRunning = false; // pause simulation
typeof state.onUpdate === "function" && state.onUpdate();
if (state.nodeAutoColorBy !== null && hasAnyPropChanged(['nodeAutoColorBy', 'graphData', 'nodeColor'])) {
// Auto add color to uncolored nodes
autoColorObjects(state.graphData.nodes, accessorFn(state.nodeAutoColorBy), state.nodeColor);
}
if (state.linkAutoColorBy !== null && hasAnyPropChanged(['linkAutoColorBy', 'graphData', 'linkColor'])) {
// Auto add color to uncolored links
autoColorObjects(state.graphData.links, accessorFn(state.linkAutoColorBy), state.linkColor);
}
// Digest nodes WebGL objects
if (state._flushObjects || hasAnyPropChanged(['graphData', 'nodeThreeObject', 'nodeThreeObjectExtend', 'nodeVal', 'nodeColor', 'nodeVisibility', 'nodeRelSize', 'nodeResolution', 'nodeOpacity'])) {
var customObjectAccessor = accessorFn(state.nodeThreeObject);
var customObjectExtendAccessor = accessorFn(state.nodeThreeObjectExtend);
var valAccessor = accessorFn(state.nodeVal);
var colorAccessor = accessorFn(state.nodeColor);
var visibilityAccessor = accessorFn(state.nodeVisibility);
var sphereGeometries = {}; // indexed by node value
var sphereMaterials = {}; // indexed by color
if (state._flushObjects || hasAnyPropChanged([
// recreate objects if any of these props have changed
'nodeThreeObject', 'nodeThreeObjectExtend'])) state.nodeDataMapper.clear();
state.nodeDataMapper.onCreateObj(function (node) {
var customObj = customObjectAccessor(node);
var extendObj = customObjectExtendAccessor(node);
if (customObj && state.nodeThreeObject === customObj) {
// clone object if it's a shared object among all nodes
customObj = customObj.clone();
}
var obj;
if (customObj && !extendObj) {
obj = customObj;
} else {
// Add default object (sphere mesh)
obj = new three$1.Mesh();
obj.__graphDefaultObj = true;
if (customObj && extendObj) {
obj.add(customObj); // extend default with custom
}
}
obj.__graphObjType = 'node'; // Add object type
return obj;
}).onUpdateObj(function (obj, node) {
if (obj.__graphDefaultObj) {
// bypass internal updates for custom node objects
var val = valAccessor(node) || 1;
var radius = Math.cbrt(val) * state.nodeRelSize;
var numSegments = state.nodeResolution;
if (!obj.geometry.type.match(/^Sphere(Buffer)?Geometry$/) || obj.geometry.parameters.radius !== radius || obj.geometry.parameters.widthSegments !== numSegments) {
if (!sphereGeometries.hasOwnProperty(val)) {
sphereGeometries[val] = new three$1.SphereGeometry(radius, numSegments, numSegments);
}
obj.geometry.dispose();
obj.geometry = sphereGeometries[val];
}
var color = colorAccessor(node);
var materialColor = new three$1.Color(colorStr2Hex(color || '#ffffaa'));
var opacity = state.nodeOpacity * colorAlpha(color);
if (obj.material.type !== 'MeshLambertMaterial' || !obj.material.color.equals(materialColor) || obj.material.opacity !== opacity) {
if (!sphereMaterials.hasOwnProperty(color)) {
sphereMaterials[color] = new three$1.MeshLambertMaterial({
color: materialColor,
transparent: true,
opacity: opacity
});
}
obj.material.dispose();
obj.material = sphereMaterials[color];
}
}
}).digest(state.graphData.nodes.filter(visibilityAccessor));
}
// Digest links WebGL objects
if (state._flushObjects || hasAnyPropChanged(['graphData', 'linkThreeObject', 'linkThreeObjectExtend', 'linkMaterial', 'linkColor', 'linkWidth', 'linkVisibility', 'linkResolution', 'linkOpacity', 'linkDirectionalArrowLength', 'linkDirectionalArrowColor', 'linkDirectionalArrowResolution', 'linkDirectionalParticles', 'linkDirectionalParticleWidth', 'linkDirectionalParticleColor', 'linkDirectionalParticleResolution', 'linkDirectionalParticleThreeObject'])) {
var _customObjectAccessor = accessorFn(state.linkThreeObject);
var _customObjectExtendAccessor = accessorFn(state.linkThreeObjectExtend);
var customMaterialAccessor = accessorFn(state.linkMaterial);
var _visibilityAccessor = accessorFn(state.linkVisibility);
var _colorAccessor = accessorFn(state.linkColor);
var widthAccessor = accessorFn(state.linkWidth);
var cylinderGeometries = {}; // indexed by link width
var lambertLineMaterials = {}; // for cylinder objects, indexed by link color
var basicLineMaterials = {}; // for line objects, indexed by link color
var visibleLinks = state.graphData.links.filter(_visibilityAccessor);
// lines digest cycle
if (state._flushObjects || hasAnyPropChanged([
// recreate objects if any of these props have changed
'linkThreeObject', 'linkThreeObjectExtend', 'linkWidth'])) state.linkDataMapper.clear();
state.linkDataMapper.onRemoveObj(function (obj) {
// remove trailing single photons
var singlePhotonsObj = obj.__data && obj.__data.__singleHopPhotonsObj;
if (singlePhotonsObj) {
singlePhotonsObj.parent.remove(singlePhotonsObj);
emptyObject(singlePhotonsObj);
delete obj.__data.__singleHopPhotonsObj;
}
}).onCreateObj(function (link) {
var customObj = _customObjectAccessor(link);
var extendObj = _customObjectExtendAccessor(link);
if (customObj && state.linkThreeObject === customObj) {
// clone object if it's a shared object among all links
customObj = customObj.clone();
}
var defaultObj;
if (!customObj || extendObj) {
// construct default line obj
var useCylinder = !!widthAccessor(link);
if (useCylinder) {
defaultObj = new three$1.Mesh();
} else {
// Use plain line (constant width)
var lineGeometry = new three$1.BufferGeometry();
lineGeometry[setAttributeFn]('position', new three$1.BufferAttribute(new Float32Array(2 * 3), 3));
defaultObj = new three$1.Line(lineGeometry);
}
}
var obj;
if (!customObj) {
obj = defaultObj;
obj.__graphDefaultObj = true;
} else {
if (!extendObj) {
// use custom object
obj = customObj;
} else {
// extend default with custom in a group
obj = new three$1.Group();
obj.__graphDefaultObj = true;
obj.add(defaultObj);
obj.add(customObj);
}
}
obj.renderOrder = 10; // Prevent visual glitches of dark lines on top of nodes by rendering them last
obj.__graphObjType = 'link'; // Add object type
return obj;
}).onUpdateObj(function (updObj, link) {
if (updObj.__graphDefaultObj) {
// bypass internal updates for custom link objects
// select default object if it's an extended group
var obj = updObj.children.length ? updObj.children[0] : updObj;
var linkWidth = Math.ceil(widthAccessor(link) * 10) / 10;
var useCylinder = !!linkWidth;
if (useCylinder) {
var r = linkWidth / 2;
var numSegments = state.linkResolution;
if (!obj.geometry.type.match(/^Cylinder(Buffer)?Geometry$/) || obj.geometry.parameters.radiusTop !== r || obj.geometry.parameters.radialSegments !== numSegments) {
if (!cylinderGeometries.hasOwnProperty(linkWidth)) {
var geometry = new three$1.CylinderGeometry(r, r, 1, numSegments, 1, false);
geometry[applyMatrix4Fn](new three$1.Matrix4().makeTranslation(0, 1 / 2, 0));
geometry[applyMatrix4Fn](new three$1.Matrix4().makeRotationX(Math.PI / 2));
cylinderGeometries[linkWidth] = geometry;
}
obj.geometry.dispose();
obj.geometry = cylinderGeometries[linkWidth];
}
}
var customMaterial = cust