3d-force-graph
Version:
UI component for a 3D force-directed graph using ThreeJS and ngraph.forcelayout3d layout engine
362 lines (273 loc) • 11.2 kB
JavaScript
function __$styleInject(css, returnValue) {
if (typeof document === 'undefined') {
return returnValue;
}
css = css || '';
var head = document.head || document.getElementsByTagName('head')[0];
var style = document.createElement('style');
style.type = 'text/css';
head.appendChild(style);
if (style.styleSheet){
style.styleSheet.cssText = css;
} else {
style.appendChild(document.createTextNode(css));
}
return returnValue;
}
import { AmbientLight, Color, DirectionalLight, PerspectiveCamera, Raycaster, Scene, Vector2, WebGLRenderer } from 'three';
import ThreeTrackballControls from 'three-trackballcontrols';
import ThreeForceGraph from 'three-forcegraph';
import accessorFn from 'accessor-fn';
import Kapsule from 'kapsule';
import tinyColor from 'tinycolor2';
__$styleInject(".graph-nav-info {\n bottom: 5px;\n width: 100%;\n text-align: center;\n color: slategrey;\n opacity: 0.7;\n font-size: 10px;\n}\n\n.graph-info-msg {\n top: 50%;\n width: 100%;\n text-align: center;\n color: lavender;\n opacity: 0.7;\n font-size: 22px;\n}\n\n.graph-tooltip {\n color: lavender;\n font-size: 18px;\n}\n\n.graph-info-msg, .graph-nav-info, .graph-tooltip {\n position: absolute;\n font-family: Sans-serif;\n}", undefined);
function linkKapsule (kapsulePropName, kapsuleType) {
var dummyK = new kapsuleType(); // To extract defaults
return {
linkProp: function linkProp(prop) {
// link property config
return {
default: dummyK[prop](),
onChange: function onChange(v, state) {
state[kapsulePropName][prop](v);
},
triggerUpdate: false
};
},
linkMethod: function linkMethod(method) {
// link method pass-through
return function (state) {
var kapsuleInstance = state[kapsulePropName];
for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
args[_key - 1] = arguments[_key];
}
var returnVal = kapsuleInstance[method].apply(kapsuleInstance, args);
return returnVal === kapsuleInstance ? this // chain based on the parent object, not the inner kapsule
: returnVal;
};
}
};
}
var colorStr2Hex = (function (str) {
return isNaN(str) ? parseInt(tinyColor(str).toHex(), 16) : str;
});
var defineProperty = function (obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
});
} else {
obj[key] = value;
}
return obj;
};
var _extends = Object.assign || function (target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
}
return target;
};
var toConsumableArray = function (arr) {
if (Array.isArray(arr)) {
for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) arr2[i] = arr[i];
return arr2;
} else {
return Array.from(arr);
}
};
var three$1 = window.THREE ? window.THREE // Prefer consumption from global THREE, if exists
: {
WebGLRenderer: WebGLRenderer,
Scene: Scene,
PerspectiveCamera: PerspectiveCamera,
AmbientLight: AmbientLight,
DirectionalLight: DirectionalLight,
Raycaster: Raycaster,
Vector2: Vector2,
Color: Color
};
//
var CAMERA_DISTANCE2NODES_FACTOR = 150;
//
// Expose config from forceGraph
var bindFG = linkKapsule('forceGraph', ThreeForceGraph);
var linkedFGProps = Object.assign.apply(Object, toConsumableArray(['jsonUrl', 'graphData', 'numDimensions', 'nodeRelSize', 'nodeId', 'nodeVal', 'nodeResolution', 'nodeColor', 'nodeAutoColorBy', 'nodeOpacity', 'nodeThreeObject', 'linkSource', 'linkTarget', 'linkColor', 'linkAutoColorBy', 'linkOpacity', 'forceEngine', 'd3AlphaDecay', 'd3VelocityDecay', 'warmupTicks', 'cooldownTicks', 'cooldownTime'].map(function (p) {
return defineProperty({}, p, bindFG.linkProp(p));
})));
var linkedFGMethods = Object.assign.apply(Object, toConsumableArray(['d3Force'].map(function (p) {
return defineProperty({}, p, bindFG.linkMethod(p));
})));
//
var _3dForceGraph$1 = Kapsule({
props: _extends({
width: { default: window.innerWidth },
height: { default: window.innerHeight },
backgroundColor: {
default: '#000011',
onChange: function onChange(bckgColor, state) {
state.scene.background = new three$1.Color(colorStr2Hex(bckgColor));
},
triggerUpdate: false
},
showNavInfo: { default: true },
nodeLabel: { default: 'name', triggerUpdate: false },
linkLabel: { default: 'name', triggerUpdate: false },
linkHoverPrecision: { default: 1, triggerUpdate: false },
enablePointerInteraction: { default: true, onChange: function onChange(_, state) {
state.onHover = null;
},
triggerUpdate: false },
onNodeClick: { default: function _default() {}, triggerUpdate: false },
onNodeHover: { default: function _default() {}, triggerUpdate: false },
onLinkClick: { default: function _default() {}, triggerUpdate: false },
onLinkHover: { default: function _default() {}, triggerUpdate: false }
}, linkedFGProps),
aliases: { // Prop names supported for backwards compatibility
nameField: 'nodeLabel',
idField: 'nodeId',
valField: 'nodeVal',
colorField: 'nodeColor',
autoColorBy: 'nodeAutoColorBy',
linkSourceField: 'linkSource',
linkTargetField: 'linkTarget',
linkColorField: 'linkColor',
lineOpacity: 'linkOpacity'
},
methods: _extends({}, linkedFGMethods),
stateInit: function stateInit() {
return {
renderer: new three$1.WebGLRenderer(),
scene: new three$1.Scene(),
camera: new three$1.PerspectiveCamera(),
lastSetCameraZ: 0,
forceGraph: new ThreeForceGraph()
};
},
init: function init(domNode, state) {
// Wipe DOM
domNode.innerHTML = '';
// Add nav info section
domNode.appendChild(state.navInfo = document.createElement('div'));
state.navInfo.className = 'graph-nav-info';
state.navInfo.textContent = "MOVE mouse & press LEFT/A: rotate, MIDDLE/S: zoom, RIGHT/D: pan";
// Add info space
var infoElem = void 0;
domNode.appendChild(infoElem = document.createElement('div'));
infoElem.className = 'graph-info-msg';
infoElem.textContent = '';
state.forceGraph.onLoading(function () {
infoElem.textContent = 'Loading...';
});
state.forceGraph.onFinishLoading(function () {
infoElem.textContent = '';
// re-aim camera, if still in default position (not user modified)
if (state.camera.position.x === 0 && state.camera.position.y === 0 && state.camera.position.z === state.lastSetCameraZ) {
state.camera.lookAt(state.forceGraph.position);
state.lastSetCameraZ = state.camera.position.z = Math.cbrt(state.forceGraph.graphData().nodes.length) * CAMERA_DISTANCE2NODES_FACTOR;
}
});
// Setup tooltip
var toolTipElem = document.createElement('div');
toolTipElem.classList.add('graph-tooltip');
domNode.appendChild(toolTipElem);
// Capture mouse coords on move
var raycaster = new three$1.Raycaster();
var mousePos = new three$1.Vector2();
mousePos.x = -2; // Initialize off canvas
mousePos.y = -2;
domNode.addEventListener("mousemove", function (ev) {
// update the mouse pos
var offset = getOffset(domNode),
relPos = {
x: ev.pageX - offset.left,
y: ev.pageY - offset.top
};
mousePos.x = relPos.x / state.width * 2 - 1;
mousePos.y = -(relPos.y / state.height) * 2 + 1;
// Move tooltip
toolTipElem.style.top = relPos.y - 40 + 'px';
toolTipElem.style.left = relPos.x - 20 + 'px';
function getOffset(el) {
var rect = el.getBoundingClientRect(),
scrollLeft = window.pageXOffset || document.documentElement.scrollLeft,
scrollTop = window.pageYOffset || document.documentElement.scrollTop;
return { top: rect.top + scrollTop, left: rect.left + scrollLeft };
}
}, false);
// Handle click events on nodes
domNode.addEventListener("click", function (ev) {
if (state.hoverObj) {
state['on' + (state.hoverObj.__graphObjType === 'node' ? 'Node' : 'Link') + 'Click'](state.hoverObj.__data);
}
}, false);
// Setup renderer, camera and controls
domNode.appendChild(state.renderer.domElement);
var tbControls = new ThreeTrackballControls(state.camera, state.renderer.domElement);
state.renderer.setSize(state.width, state.height);
state.camera.far = 20000;
// Populate scene
state.scene.add(state.forceGraph);
state.scene.add(new three$1.AmbientLight(0xbbbbbb));
state.scene.add(new three$1.DirectionalLight(0xffffff, 0.6));
//
// Kick-off renderer
(function animate() {
// IIFE
if (state.enablePointerInteraction) {
// Update tooltip and trigger onHover events
raycaster.linePrecision = state.linkHoverPrecision;
raycaster.setFromCamera(mousePos, state.camera);
var intersects = raycaster.intersectObjects(state.forceGraph.children).filter(function (o) {
return ['node', 'link'].indexOf(o.object.__graphObjType) !== -1;
}) // Check only node/link objects
.sort(function (a, b) {
// Prioritize nodes over links
var isNode = function isNode(o) {
return o.object.__graphObjType === 'node';
};
return isNode(b) - isNode(a);
});
var topObject = intersects.length ? intersects[0].object : null;
if (topObject !== state.hoverObj) {
var prevObjType = state.hoverObj ? state.hoverObj.__graphObjType : null;
var prevObjData = state.hoverObj ? state.hoverObj.__data : null;
var objType = topObject ? topObject.__graphObjType : null;
var objData = topObject ? topObject.__data : null;
if (prevObjType && prevObjType !== objType) {
// Hover out
state['on' + (prevObjType === 'node' ? 'Node' : 'Link') + 'Hover'](null, prevObjData);
}
if (objType) {
// Hover in
state['on' + (objType === 'node' ? 'Node' : 'Link') + 'Hover'](objData, prevObjType === objType ? prevObjData : null);
}
toolTipElem.textContent = topObject ? accessorFn(state[objType + 'Label'])(objData) : '';
state.hoverObj = topObject;
}
}
// Frame cycle
state.forceGraph.tickFrame();
tbControls.update();
state.renderer.render(state.scene, state.camera);
requestAnimationFrame(animate);
})();
},
update: function updateFn(state) {
// resize canvas
if (state.width && state.height) {
state.renderer.setSize(state.width, state.height);
state.camera.aspect = state.width / state.height;
state.camera.updateProjectionMatrix();
}
state.navInfo.style.display = state.showNavInfo ? null : 'none';
}
});
export { _3dForceGraph$1 as default };