3d-force-graph
Advanced tools
Comparing version 0.5.3 to 1.0.0
{ | ||
"name": "3d-force-graph", | ||
"version": "0.5.3", | ||
"version": "1.0.0", | ||
"description": "UI component for a 3D force-directed graph using ThreeJS and ngraph.forcelayout3d layout engine", | ||
@@ -34,4 +34,6 @@ "main": "dist/3d-force-graph.js", | ||
"dependencies": { | ||
"ngraph.forcelayout": "~0.1.2", | ||
"ngraph.forcelayout3d": "~0.0.16", | ||
"ngraph.graph": "~0.0.12", | ||
"qwest": "^4.4", | ||
"swc": "^0.1", | ||
@@ -38,0 +40,0 @@ "three": "~0.84" |
@@ -35,3 +35,3 @@ # 3D Force-Directed Graph | ||
``` | ||
<script src="/path/to/dist/3d-force-graph.js"></script> | ||
<script src="//unpkg.com/3d-force-graph/dist/3d-force-graph.js"></script> | ||
``` | ||
@@ -50,11 +50,17 @@ then | ||
.height(<px>) | ||
.jsonUrl(<URL of JSON file to load graph data directly from, as an alternative to specifying graphData directly>) | ||
.graphData(<data>) | ||
.numDimensions(<number of dimensions, between [1,3]. default: 3>) | ||
.nodeRelSize(<(number) node volume per value unit>) | ||
.lineOpacity(<between [0,1]>) | ||
.valAccessor(<function(node) to extract numeric value. default: node.val>) | ||
.nameAccessor(<function(node) to extract name string. default: node.name>) | ||
.colorAccessor(<function(node) to extract color hex number. default: node.color>) | ||
.warmUpTicks(<number of layout engine cycles to run before start rendering. default: 0>) | ||
.coolDownTicks(<# frames to stop engine. default: Infinity>) | ||
.coolDownTime(<ms to stop engine. default: 15000>) | ||
.autoColorBy(<node object accessor property name, only affects nodes without a color field>) | ||
.idField(<node object accessor property name. default: 'id'>) | ||
.valField(<node object accessor property name. default: 'val'>) | ||
.nameField(<node object accessor property name. default: 'name'>) | ||
.colorField(<node object accessor property name. default: 'color'>) | ||
.linkSourceField(<link object accessor property name. default: 'source'>) | ||
.linkTargetField(<link object accessor property name. default: 'target'>) | ||
.warmupTicks(<number of layout engine cycles to run before start rendering. default: 0>) | ||
.cooldownTicks(<# frames to stop engine. default: Infinity>) | ||
.cooldownTime(<ms to stop engine. default: 15000>) | ||
.resetProps() | ||
@@ -67,8 +73,10 @@ ``` | ||
{ | ||
nodes: { | ||
id1: { | ||
nodes: [ | ||
{ | ||
id: 'id1', | ||
name: "name1", | ||
val: 1 | ||
}, | ||
id2: { | ||
{ | ||
id: 'id2', | ||
name: "name2", | ||
@@ -78,5 +86,8 @@ val: 10 | ||
(...) | ||
}, | ||
], | ||
links: [ | ||
['id1', 'id2'], // [from, to] | ||
{ | ||
source: 'id1', | ||
target: 'id2' | ||
}, | ||
(...) | ||
@@ -83,0 +94,0 @@ ] |
@@ -7,4 +7,5 @@ import './3d-force-graph.css'; | ||
import graph from 'ngraph.graph'; | ||
import forcelayout from 'ngraph.forcelayout'; | ||
import forcelayout3d from 'ngraph.forcelayout3d'; | ||
const ngraph = { graph, forcelayout3d }; | ||
const ngraph = { graph, forcelayout, forcelayout3d }; | ||
@@ -22,14 +23,20 @@ import * as SWC from 'swc'; | ||
new SWC.Prop('height', window.innerHeight), | ||
new SWC.Prop('jsonUrl'), | ||
new SWC.Prop('graphData', { | ||
nodes: {}, | ||
links: [] // [from, to] | ||
nodes: [], | ||
links: [] | ||
}), | ||
new SWC.Prop('numDimensions', 3), | ||
new SWC.Prop('nodeRelSize', 4), // volume per val unit | ||
new SWC.Prop('lineOpacity', 0.2), | ||
new SWC.Prop('valAccessor', node => node.val), | ||
new SWC.Prop('nameAccessor', node => node.name), | ||
new SWC.Prop('colorAccessor', node => node.color), | ||
new SWC.Prop('warmUpTicks', 0), // how many times to tick the force engine at init before starting to render | ||
new SWC.Prop('coolDownTicks', Infinity), | ||
new SWC.Prop('coolDownTime', 15000) // ms | ||
new SWC.Prop('autoColorBy'), | ||
new SWC.Prop('idField', 'id'), | ||
new SWC.Prop('valField', 'val'), | ||
new SWC.Prop('nameField', 'name'), | ||
new SWC.Prop('colorField', 'color'), | ||
new SWC.Prop('linkSourceField', 'source'), | ||
new SWC.Prop('linkTargetField', 'target'), | ||
new SWC.Prop('warmupTicks', 0), // how many times to tick the force engine at init before starting to render | ||
new SWC.Prop('cooldownTicks', Infinity), | ||
new SWC.Prop('cooldownTime', 15000) // ms | ||
], | ||
@@ -42,6 +49,6 @@ | ||
// Add nav info section | ||
const navInfo = document.createElement('div'); | ||
navInfo.classList.add('graph-nav-info'); | ||
navInfo.innerHTML = "MOVE mouse & press LEFT/A: rotate, MIDDLE/S: zoom, RIGHT/D: pan"; | ||
domNode.appendChild(navInfo); | ||
let navInfo; | ||
domNode.appendChild(navInfo = document.createElement('div')); | ||
navInfo.className = 'graph-nav-info'; | ||
navInfo.textContent = "MOVE mouse & press LEFT/A: rotate, MIDDLE/S: zoom, RIGHT/D: pan"; | ||
@@ -80,14 +87,14 @@ // Setup tooltip | ||
// Setup renderer | ||
state.renderer = new THREE.WebGLRenderer(); | ||
domNode.appendChild(state.renderer.domElement); | ||
// Setup scene | ||
const scene = new THREE.Scene(); | ||
scene.add(state.graphScene = new THREE.Group()); | ||
// Setup camera | ||
state.camera = new THREE.PerspectiveCamera(); | ||
state.camera.far = 20000; | ||
state.camera.position.z = 1000; | ||
// Setup scene | ||
state.scene = new THREE.Scene(); | ||
// Setup renderer | ||
state.renderer = new THREE.WebGLRenderer(); | ||
domNode.appendChild(state.renderer.domElement); | ||
// Add camera interaction | ||
@@ -104,8 +111,9 @@ const tbControls = new THREE.TrackballControls(state.camera, state.renderer.domElement); | ||
raycaster.setFromCamera(mousePos, state.camera); | ||
const intersects = raycaster.intersectObjects(state.scene.children); | ||
toolTipElem.innerHTML = intersects.length ? intersects[0].object.name || '' : ''; | ||
const intersects = raycaster.intersectObjects(state.graphScene.children) | ||
.filter(o => o.object.name); // Check only objects with labels | ||
toolTipElem.textContent = intersects.length ? intersects[0].object.name : ''; | ||
// Frame cycle | ||
tbControls.update(); | ||
state.renderer.render(state.scene, state.camera); | ||
state.renderer.render(scene, state.camera); | ||
requestAnimationFrame(animate); | ||
@@ -115,53 +123,62 @@ })(); | ||
update: state => { | ||
update: function updateFn(state) { | ||
resizeCanvas(); | ||
state.onFrame = null; // Clear previous frame hook | ||
state.scene = new THREE.Scene(); // Clear the place | ||
state.onFrame = null; // Pause simulation | ||
// Build graph with data | ||
const graph = ngraph.graph(); | ||
for (let nodeId in state.graphData.nodes) { | ||
graph.addNode(nodeId, state.graphData.nodes[nodeId]); | ||
if (!state.fetchingJson && state.jsonUrl && !state.graphData.nodes.length && !state.graphData.links.length) { | ||
// (Re-)load data | ||
state.fetchingJson = true; | ||
qwest.get(state.jsonUrl).then((_, json) => { | ||
state.fetchingJson = false; | ||
state.graphData = json; | ||
updateFn(state); // Force re-update | ||
}); | ||
} | ||
for (let link of state.graphData.links) { | ||
graph.addLink(...link, {}); | ||
} | ||
// Auto add color to uncolored nodes | ||
autoColorNodes(state.graphData.nodes, state.autoColorBy, state.colorField); | ||
// parse links | ||
state.graphData.links.forEach(link => { | ||
link.source = link[state.linkSourceField]; | ||
link.target = link[state.linkTargetField]; | ||
}); | ||
// Add WebGL objects | ||
graph.forEachNode(node => { | ||
const nodeMaterial = new THREE.MeshBasicMaterial({ color: state.colorAccessor(node.data) || 0xffffaa, transparent: true }); | ||
while (state.graphScene.children.length) { state.graphScene.remove(state.graphScene.children[0]) } // Clear the place | ||
state.graphData.nodes.forEach(node => { | ||
const nodeMaterial = new THREE.MeshBasicMaterial({ color: node[state.colorField] || 0xffffaa, transparent: true }); | ||
nodeMaterial.opacity = 0.75; | ||
const sphere = new THREE.Mesh( | ||
new THREE.SphereGeometry(Math.cbrt(state.valAccessor(node.data) || 1) * state.nodeRelSize, 8, 8), | ||
new THREE.SphereGeometry(Math.cbrt(node[state.valField] || 1) * state.nodeRelSize, 8, 8), | ||
nodeMaterial | ||
); | ||
sphere.name = state.nameAccessor(node.data) || ''; | ||
state.scene.add(node.data.sphere = sphere) | ||
sphere.name = node[state.nameField]; // Add label | ||
state.graphScene.add(node.__sphere = sphere); | ||
}); | ||
const lineMaterial = new THREE.MeshBasicMaterial({ color: 0xf0f0f0, transparent: true }); | ||
const lineMaterial = new THREE.LineBasicMaterial({ color: 0xf0f0f0, transparent: true }); | ||
lineMaterial.opacity = state.lineOpacity; | ||
graph.forEachLink(link => { | ||
const line = new THREE.Line(new THREE.Geometry(), lineMaterial), | ||
fromName = getNodeName(link.fromId), | ||
toName = getNodeName(link.toId); | ||
if (fromName && toName) { line.name = `${fromName} > ${toName}`; } | ||
state.graphData.links.forEach(link => { | ||
const line = new THREE.Line(new THREE.Geometry(), lineMaterial); | ||
line.geometry.vertices=[new THREE.Vector3(0,0,0), new THREE.Vector3(0,0,0)]; | ||
state.scene.add(link.data.line = line); | ||
function getNodeName(nodeId) { | ||
return state.nameAccessor(graph.getNode(nodeId).data); | ||
} | ||
state.graphScene.add(link.__line = line); | ||
}); | ||
state.camera.lookAt(state.scene.position); | ||
state.camera.position.z = Math.cbrt(Object.keys(state.graphData.nodes).length) * CAMERA_DISTANCE2NODES_FACTOR; | ||
state.camera.lookAt(state.graphScene.position); | ||
state.camera.position.z = Math.cbrt(state.graphData.nodes.length) * CAMERA_DISTANCE2NODES_FACTOR; | ||
// Add force-directed layout | ||
const layout = ngraph.forcelayout3d(graph); | ||
const graph = ngraph.graph(); | ||
state.graphData.nodes.forEach(node => { graph.addNode(node[state.idField]); }); | ||
state.graphData.links.forEach(link => { graph.addLink(link.source, link.target); }); | ||
const layout = ngraph['forcelayout' + (state.numDimensions === 2 ? '' : '3d')](graph); | ||
for (let i=0; i<state.warmUpTicks; i++) { layout.step(); } // Initial ticks before starting to render | ||
for (let i=0; i<state.warmupTicks; i++) { layout.step(); } // Initial ticks before starting to render | ||
@@ -183,3 +200,3 @@ let cntTicks = 0; | ||
function layoutTick() { | ||
if (cntTicks++ > state.coolDownTicks || (new Date()) - startTickTime > state.coolDownTime) { | ||
if (cntTicks++ > state.cooldownTicks || (new Date()) - startTickTime > state.cooldownTime) { | ||
state.onFrame = null; // Stop ticking graph | ||
@@ -191,19 +208,18 @@ } | ||
// Update nodes position | ||
graph.forEachNode(node => { | ||
const sphere = node.data.sphere, | ||
pos = layout.getNodePosition(node.id); | ||
state.graphData.nodes.forEach(node => { | ||
const sphere = node.__sphere, | ||
pos = layout.getNodePosition(node[state.idField]); | ||
sphere.position.x = pos.x; | ||
sphere.position.y = pos.y; | ||
sphere.position.z = pos.z; | ||
sphere.position.y = pos.y || 0; | ||
sphere.position.z = pos.z || 0; | ||
}); | ||
// Update links position | ||
graph.forEachLink(link => { | ||
const line = link.data.line, | ||
pos = layout.getLinkPosition(link.id); | ||
state.graphData.links.forEach(link => { | ||
const line = link.__line, | ||
pos = layout.getLinkPosition(graph.getLink(link.source, link.target).id); | ||
line.geometry.vertices = [ | ||
new THREE.Vector3(pos.from.x, pos.from.y, pos.from.z), | ||
new THREE.Vector3(pos.to.x, pos.to.y, pos.to.z) | ||
new THREE.Vector3(pos.from.x, pos.from.y || 0, pos.from.z || 0), | ||
new THREE.Vector3(pos.to.x, pos.to.y || 0, pos.to.z || 0) | ||
]; | ||
@@ -215,4 +231,21 @@ | ||
} | ||
function autoColorNodes(nodes, colorBy, colorField) { | ||
if (!colorBy) return; | ||
// Color brewer paired set | ||
const colors = ['#a6cee3','#1f78b4','#b2df8a','#33a02c','#fb9a99','#e31a1c','#fdbf6f','#ff7f00','#cab2d6','#6a3d9a','#ffff99','#b15928']; | ||
const uncoloredNodes = nodes.filter(node => !node[colorField]), | ||
nodeGroups = {}; | ||
uncoloredNodes.forEach(node => { nodeGroups[node[colorBy]] = null }); | ||
Object.keys(nodeGroups).forEach((group, idx) => { nodeGroups[group] = idx }); | ||
uncoloredNodes.forEach(node => { | ||
node[colorField] = parseInt(colors[nodeGroups[node[colorBy]] % colors.length].slice(1), 16); | ||
}); | ||
} | ||
} | ||
}); | ||
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
1174429
29752
0
92
6
+ Addedngraph.forcelayout@~0.1.2
+ Addedqwest@^4.4
+ Addedjquery-param@0.1.2(transitive)
+ Addedngraph.forcelayout@0.1.2(transitive)
+ Addedngraph.physics.simulator@0.1.1(transitive)
+ Addedngraph.quadtreebh@0.0.4(transitive)
+ Addedpinkyswear@2.2.3(transitive)
+ Addedqwest@4.5.0(transitive)