UNPKG

three-forcegraph

Version:
1,271 lines (1,234 loc) 61.9 kB
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