three-forcegraph
Advanced tools
Comparing version 1.1.0 to 1.1.1
@@ -17,25 +17,25 @@ 'use strict'; | ||
var colorStr2Hex = function colorStr2Hex(str) { | ||
return isNaN(str) ? parseInt(tinyColor(str).toHex(), 16) : str; | ||
return isNaN(str) ? parseInt(tinyColor(str).toHex(), 16) : str; | ||
}; | ||
function autoColorNodes(nodes, colorByAccessor, colorField) { | ||
if (!colorByAccessor || typeof colorField !== 'string') return; | ||
if (!colorByAccessor || typeof colorField !== 'string') return; | ||
var colors = d3ScaleChromatic.schemePaired; // Paired color set from color brewer | ||
var colors = d3ScaleChromatic.schemePaired; // Paired color set from color brewer | ||
var uncoloredNodes = nodes.filter(function (node) { | ||
return !node[colorField]; | ||
}); | ||
var nodeGroups = {}; | ||
var uncoloredNodes = nodes.filter(function (node) { | ||
return !node[colorField]; | ||
}); | ||
var nodeGroups = {}; | ||
uncoloredNodes.forEach(function (node) { | ||
nodeGroups[colorByAccessor(node)] = null; | ||
}); | ||
Object.keys(nodeGroups).forEach(function (group, idx) { | ||
nodeGroups[group] = idx; | ||
}); | ||
uncoloredNodes.forEach(function (node) { | ||
nodeGroups[colorByAccessor(node)] = null; | ||
}); | ||
Object.keys(nodeGroups).forEach(function (group, idx) { | ||
nodeGroups[group] = idx; | ||
}); | ||
uncoloredNodes.forEach(function (node) { | ||
node[colorField] = colors[nodeGroups[colorByAccessor(node)] % colors.length]; | ||
}); | ||
uncoloredNodes.forEach(function (node) { | ||
node[colorField] = colors[nodeGroups[colorByAccessor(node)] % colors.length]; | ||
}); | ||
} | ||
@@ -49,243 +49,243 @@ | ||
props: { | ||
jsonUrl: {}, | ||
graphData: { | ||
default: { | ||
nodes: [], | ||
links: [] | ||
}, | ||
onChange: function onChange(_, state) { | ||
state.onFrame = null; | ||
} // Pause simulation | ||
props: { | ||
jsonUrl: {}, | ||
graphData: { | ||
default: { | ||
nodes: [], | ||
links: [] | ||
}, | ||
onChange: function onChange(_, state) { | ||
state.onFrame = null; | ||
} // Pause simulation | ||
}, | ||
numDimensions: { | ||
default: 3, | ||
onChange: function onChange(numDim, state) { | ||
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' + dim]; // velocity | ||
}); | ||
} | ||
} | ||
}, | ||
nodeRelSize: { default: 4 }, // volume per val unit | ||
autoColorBy: {}, | ||
nodeId: { default: 'id' }, | ||
nodeVal: { default: 'val' }, | ||
nodeResolution: { default: 8 }, // how many slice segments in the sphere's circumference | ||
nodeColor: { default: 'color' }, | ||
nodeThreeObject: {}, | ||
linkSource: { default: 'source' }, | ||
linkTarget: { default: 'target' }, | ||
linkColor: { default: 'color' }, | ||
linkOpacity: { default: 0.2 }, | ||
forceEngine: { default: 'd3' }, // d3 or ngraph | ||
d3AlphaDecay: { default: 0.0228 }, | ||
d3VelocityDecay: { default: 0.4 }, | ||
warmupTicks: { default: 0 }, // how many times to tick the force engine at init before starting to render | ||
cooldownTicks: { default: Infinity }, | ||
cooldownTime: { default: 15000 } // ms | ||
}, | ||
numDimensions: { | ||
default: 3, | ||
onChange: function onChange(numDim, state) { | ||
if (numDim < 3) { | ||
eraseDimension(state.graphData.nodes, 'z'); | ||
} | ||
if (numDim < 2) { | ||
eraseDimension(state.graphData.nodes, 'y'); | ||
} | ||
methods: { | ||
// Expose d3 forces for external manipulation | ||
d3Force: function d3Force(state, forceName, forceFn) { | ||
if (!state.initialised) { | ||
return null; // d3 force simulation object doesn't exist yet | ||
} | ||
if (forceFn === undefined) { | ||
return state.d3ForceLayout.force(forceName); // Force getter | ||
} | ||
state.d3ForceLayout.force(forceName, forceFn); // Force setter | ||
return this; | ||
}, | ||
tickFrame: function tickFrame(state) { | ||
if (state.onFrame) state.onFrame(); | ||
return this; | ||
function eraseDimension(nodes, dim) { | ||
nodes.forEach(function (d) { | ||
delete d[dim]; // position | ||
delete d['v' + dim]; // velocity | ||
}); | ||
} | ||
} | ||
}, | ||
nodeRelSize: { default: 4 }, // volume per val unit | ||
autoColorBy: {}, | ||
nodeId: { default: 'id' }, | ||
nodeVal: { default: 'val' }, | ||
nodeResolution: { default: 8 }, // how many slice segments in the sphere's circumference | ||
nodeColor: { default: 'color' }, | ||
nodeThreeObject: {}, | ||
linkSource: { default: 'source' }, | ||
linkTarget: { default: 'target' }, | ||
linkColor: { default: 'color' }, | ||
linkOpacity: { default: 0.2 }, | ||
forceEngine: { default: 'd3' }, // d3 or ngraph | ||
d3AlphaDecay: { default: 0.0228 }, | ||
d3VelocityDecay: { default: 0.4 }, | ||
warmupTicks: { default: 0 }, // how many times to tick the force engine at init before starting to render | ||
cooldownTicks: { default: Infinity }, | ||
cooldownTime: { default: 15000 } // ms | ||
}, | ||
init: function init(threeObj, state) { | ||
// Main three object to manipulate | ||
state.graphScene = threeObj; | ||
// Add D3 force-directed layout | ||
state.d3ForceLayout = d3Force3d.forceSimulation().force('link', d3Force3d.forceLink()).force('charge', d3Force3d.forceManyBody()).force('center', d3Force3d.forceCenter()).stop(); | ||
methods: { | ||
// 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; | ||
}, | ||
tickFrame: function tickFrame(state) { | ||
if (state.onFrame) state.onFrame(); | ||
return this; | ||
} | ||
}, | ||
update: function updateFn(state) { | ||
state.onFrame = null; // Pause simulation | ||
stateInit: function stateInit() { | ||
return { | ||
d3ForceLayout: d3Force3d.forceSimulation().force('link', d3Force3d.forceLink()).force('charge', d3Force3d.forceManyBody()).force('center', d3Force3d.forceCenter()).stop() | ||
}; | ||
}, | ||
if (state.graphData.nodes.length || state.graphData.links.length) { | ||
console.info('force-graph loading', state.graphData.nodes.length + ' nodes', state.graphData.links.length + ' links'); | ||
} | ||
init: function init(threeObj, state) { | ||
// Main three object to manipulate | ||
state.graphScene = threeObj; | ||
}, | ||
if (!state.fetchingJson && state.jsonUrl && !state.graphData.nodes.length && !state.graphData.links.length) { | ||
// (Re-)load data | ||
state.fetchingJson = true; | ||
qwest.get(state.jsonUrl).then(function (_, json) { | ||
state.fetchingJson = false; | ||
state.graphData = json; | ||
updateFn(state); // Force re-update | ||
}); | ||
} | ||
update: function updateFn(state) { | ||
state.onFrame = null; // Pause simulation | ||
if (state.autoColorBy !== null) { | ||
// Auto add color to uncolored nodes | ||
autoColorNodes(state.graphData.nodes, accessorFn(state.autoColorBy), state.nodeColor); | ||
} | ||
if (state.graphData.nodes.length || state.graphData.links.length) { | ||
console.info('force-graph loading', state.graphData.nodes.length + ' nodes', state.graphData.links.length + ' links'); | ||
} | ||
// parse links | ||
state.graphData.links.forEach(function (link) { | ||
link.source = link[state.linkSource]; | ||
link.target = link[state.linkTarget]; | ||
}); | ||
if (!state.fetchingJson && state.jsonUrl && !state.graphData.nodes.length && !state.graphData.links.length) { | ||
// (Re-)load data | ||
state.fetchingJson = true; | ||
qwest.get(state.jsonUrl).then(function (_, json) { | ||
state.fetchingJson = false; | ||
state.graphData = json; | ||
updateFn(state); // Force re-update | ||
}); | ||
} | ||
// Add WebGL objects | ||
while (state.graphScene.children.length) { | ||
state.graphScene.remove(state.graphScene.children[0]); | ||
} // Clear the place | ||
if (state.autoColorBy !== null) { | ||
// Auto add color to uncolored nodes | ||
autoColorNodes(state.graphData.nodes, accessorFn(state.autoColorBy), state.nodeColor); | ||
} | ||
var customNodeObjectAccessor = accessorFn(state.nodeThreeObject); | ||
var valAccessor = accessorFn(state.nodeVal); | ||
var colorAccessor = accessorFn(state.nodeColor); | ||
var sphereGeometries = {}; // indexed by node value | ||
var sphereMaterials = {}; // indexed by color | ||
state.graphData.nodes.forEach(function (node) { | ||
var customObj = customNodeObjectAccessor(node); | ||
// parse links | ||
state.graphData.links.forEach(function (link) { | ||
link.source = link[state.linkSource]; | ||
link.target = link[state.linkTarget]; | ||
}); | ||
var obj = void 0; | ||
if (customObj) { | ||
obj = customObj.clone(); | ||
} else { | ||
// Default object (sphere mesh) | ||
var val = valAccessor(node) || 1; | ||
if (!sphereGeometries.hasOwnProperty(val)) { | ||
sphereGeometries[val] = new three.SphereGeometry(Math.cbrt(val) * state.nodeRelSize, state.nodeResolution, state.nodeResolution); | ||
} | ||
// Add WebGL objects | ||
while (state.graphScene.children.length) { | ||
state.graphScene.remove(state.graphScene.children[0]); | ||
} // Clear the place | ||
var color = colorAccessor(node); | ||
if (!sphereMaterials.hasOwnProperty(color)) { | ||
sphereMaterials[color] = new three.MeshLambertMaterial({ | ||
color: colorStr2Hex(color || '#ffffaa'), | ||
transparent: true, | ||
opacity: 0.75 | ||
}); | ||
} | ||
var customNodeObjectAccessor = accessorFn(state.nodeThreeObject); | ||
var valAccessor = accessorFn(state.nodeVal); | ||
var colorAccessor = accessorFn(state.nodeColor); | ||
var sphereGeometries = {}; // indexed by node value | ||
var sphereMaterials = {}; // indexed by color | ||
state.graphData.nodes.forEach(function (node) { | ||
var customObj = customNodeObjectAccessor(node); | ||
obj = new three.Mesh(sphereGeometries[val], sphereMaterials[color]); | ||
} | ||
var obj = void 0; | ||
if (customObj) { | ||
obj = customObj.clone(); | ||
} else { | ||
// Default object (sphere mesh) | ||
var val = valAccessor(node) || 1; | ||
if (!sphereGeometries.hasOwnProperty(val)) { | ||
sphereGeometries[val] = new three.SphereGeometry(Math.cbrt(val) * state.nodeRelSize, state.nodeResolution, state.nodeResolution); | ||
} | ||
obj.__graphObjType = 'node'; // Add object type | ||
obj.__data = node; // Attach node data | ||
var color = colorAccessor(node); | ||
if (!sphereMaterials.hasOwnProperty(color)) { | ||
sphereMaterials[color] = new three.MeshLambertMaterial({ | ||
color: colorStr2Hex(color || '#ffffaa'), | ||
transparent: true, | ||
opacity: 0.75 | ||
}); | ||
} | ||
state.graphScene.add(node.__threeObj = obj); | ||
obj = new three.Mesh(sphereGeometries[val], sphereMaterials[color]); | ||
} | ||
obj.__graphObjType = 'node'; // Add object type | ||
obj.__data = node; // Attach node data | ||
state.graphScene.add(node.__threeObj = obj); | ||
}); | ||
var linkColorAccessor = accessorFn(state.linkColor); | ||
var lineMaterials = {}; // indexed by color | ||
state.graphData.links.forEach(function (link) { | ||
var color = linkColorAccessor(link); | ||
if (!lineMaterials.hasOwnProperty(color)) { | ||
lineMaterials[color] = new three.LineBasicMaterial({ | ||
color: colorStr2Hex(color || '#f0f0f0'), | ||
transparent: true, | ||
opacity: state.linkOpacity | ||
}); | ||
} | ||
var linkColorAccessor = accessorFn(state.linkColor); | ||
var lineMaterials = {}; // indexed by color | ||
state.graphData.links.forEach(function (link) { | ||
var color = linkColorAccessor(link); | ||
if (!lineMaterials.hasOwnProperty(color)) { | ||
lineMaterials[color] = new three.LineBasicMaterial({ | ||
color: colorStr2Hex(color || '#f0f0f0'), | ||
transparent: true, | ||
opacity: state.linkOpacity | ||
}); | ||
} | ||
var geometry = new three.BufferGeometry(); | ||
geometry.addAttribute('position', new three.BufferAttribute(new Float32Array(2 * 3), 3)); | ||
var lineMaterial = lineMaterials[color]; | ||
var line = new three.Line(geometry, lineMaterial); | ||
var geometry = new three.BufferGeometry(); | ||
geometry.addAttribute('position', new three.BufferAttribute(new Float32Array(2 * 3), 3)); | ||
var lineMaterial = lineMaterials[color]; | ||
var line = new three.Line(geometry, lineMaterial); | ||
line.renderOrder = 10; // Prevent visual glitches of dark lines on top of nodes by rendering them last | ||
line.renderOrder = 10; // Prevent visual glitches of dark lines on top of nodes by rendering them last | ||
line.__graphObjType = 'link'; // Add object type | ||
line.__graphObjType = 'link'; // Add object type | ||
state.graphScene.add(link.__lineObj = line); | ||
}); | ||
state.graphScene.add(link.__lineObj = line); | ||
}); | ||
// Feed data to force-directed layout | ||
var isD3Sim = state.forceEngine !== 'ngraph'; | ||
var layout = void 0; | ||
if (isD3Sim) { | ||
// D3-force | ||
(layout = state.d3ForceLayout).stop().alpha(1) // re-heat the simulation | ||
.alphaDecay(state.d3AlphaDecay).velocityDecay(state.d3VelocityDecay).numDimensions(state.numDimensions).nodes(state.graphData.nodes).force('link').id(function (d) { | ||
return d[state.nodeId]; | ||
}).links(state.graphData.links); | ||
} else { | ||
// ngraph | ||
var _graph = ngraph.graph(); | ||
state.graphData.nodes.forEach(function (node) { | ||
_graph.addNode(node[state.nodeId]); | ||
}); | ||
state.graphData.links.forEach(function (link) { | ||
_graph.addLink(link.source, link.target); | ||
}); | ||
layout = ngraph['forcelayout' + (state.numDimensions === 2 ? '' : '3d')](_graph); | ||
layout.graph = _graph; // Attach graph reference to layout | ||
} | ||
// Feed data to force-directed layout | ||
var isD3Sim = state.forceEngine !== 'ngraph'; | ||
var layout = void 0; | ||
if (isD3Sim) { | ||
// D3-force | ||
(layout = state.d3ForceLayout).stop().alpha(1) // re-heat the simulation | ||
.alphaDecay(state.d3AlphaDecay).velocityDecay(state.d3VelocityDecay).numDimensions(state.numDimensions).nodes(state.graphData.nodes).force('link').id(function (d) { | ||
return d[state.nodeId]; | ||
}).links(state.graphData.links); | ||
} else { | ||
// ngraph | ||
var _graph = ngraph.graph(); | ||
state.graphData.nodes.forEach(function (node) { | ||
_graph.addNode(node[state.nodeId]); | ||
}); | ||
state.graphData.links.forEach(function (link) { | ||
_graph.addLink(link.source, link.target); | ||
}); | ||
layout = ngraph['forcelayout' + (state.numDimensions === 2 ? '' : '3d')](_graph); | ||
layout.graph = _graph; // Attach graph reference to layout | ||
} | ||
for (var i = 0; i < state.warmupTicks; i++) { | ||
layout[isD3Sim ? 'tick' : 'step'](); | ||
} // Initial ticks before starting to render | ||
for (var i = 0; i < state.warmupTicks; i++) { | ||
layout[isD3Sim ? 'tick' : 'step'](); | ||
} // Initial ticks before starting to render | ||
var cntTicks = 0; | ||
var startTickTime = new Date(); | ||
state.onFrame = layoutTick; | ||
var cntTicks = 0; | ||
var startTickTime = new Date(); | ||
state.onFrame = layoutTick; | ||
// | ||
// | ||
function layoutTick() { | ||
if (++cntTicks > state.cooldownTicks || new Date() - startTickTime > state.cooldownTime) { | ||
state.onFrame = null; // Stop ticking graph | ||
} else { | ||
layout[isD3Sim ? 'tick' : 'step'](); // Tick it | ||
} | ||
function layoutTick() { | ||
if (++cntTicks > state.cooldownTicks || new Date() - startTickTime > state.cooldownTime) { | ||
state.onFrame = null; // Stop ticking graph | ||
} else { | ||
layout[isD3Sim ? 'tick' : 'step'](); // Tick it | ||
} | ||
// Update nodes position | ||
state.graphData.nodes.forEach(function (node) { | ||
var obj = node.__threeObj; | ||
if (!obj) return; | ||
// Update nodes position | ||
state.graphData.nodes.forEach(function (node) { | ||
var obj = node.__threeObj; | ||
if (!obj) return; | ||
var pos = isD3Sim ? node : layout.getNodePosition(node[state.nodeId]); | ||
var pos = isD3Sim ? node : layout.getNodePosition(node[state.nodeId]); | ||
obj.position.x = pos.x; | ||
obj.position.y = pos.y || 0; | ||
obj.position.z = pos.z || 0; | ||
}); | ||
obj.position.x = pos.x; | ||
obj.position.y = pos.y || 0; | ||
obj.position.z = pos.z || 0; | ||
}); | ||
// Update links position | ||
state.graphData.links.forEach(function (link) { | ||
var line = link.__lineObj; | ||
if (!line) return; | ||
// Update links position | ||
state.graphData.links.forEach(function (link) { | ||
var line = link.__lineObj; | ||
if (!line) return; | ||
var pos = isD3Sim ? link : layout.getLinkPosition(layout.graph.getLink(link.source, link.target).id), | ||
start = pos[isD3Sim ? 'source' : 'from'], | ||
end = pos[isD3Sim ? 'target' : 'to'], | ||
linePos = line.geometry.attributes.position; | ||
var pos = isD3Sim ? link : layout.getLinkPosition(layout.graph.getLink(link.source, link.target).id), | ||
start = pos[isD3Sim ? 'source' : 'from'], | ||
end = pos[isD3Sim ? 'target' : 'to'], | ||
linePos = line.geometry.attributes.position; | ||
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.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; | ||
line.geometry.computeBoundingSphere(); | ||
}); | ||
} | ||
linePos.needsUpdate = true; | ||
line.geometry.computeBoundingSphere(); | ||
}); | ||
} | ||
} | ||
}); | ||
@@ -292,0 +292,0 @@ |
@@ -13,25 +13,25 @@ import { BufferAttribute, BufferGeometry, Group, Line, LineBasicMaterial, Mesh, MeshLambertMaterial, SphereGeometry } from 'three'; | ||
var colorStr2Hex = function colorStr2Hex(str) { | ||
return isNaN(str) ? parseInt(tinyColor(str).toHex(), 16) : str; | ||
return isNaN(str) ? parseInt(tinyColor(str).toHex(), 16) : str; | ||
}; | ||
function autoColorNodes(nodes, colorByAccessor, colorField) { | ||
if (!colorByAccessor || typeof colorField !== 'string') return; | ||
if (!colorByAccessor || typeof colorField !== 'string') return; | ||
var colors = schemePaired; // Paired color set from color brewer | ||
var colors = schemePaired; // Paired color set from color brewer | ||
var uncoloredNodes = nodes.filter(function (node) { | ||
return !node[colorField]; | ||
}); | ||
var nodeGroups = {}; | ||
var uncoloredNodes = nodes.filter(function (node) { | ||
return !node[colorField]; | ||
}); | ||
var nodeGroups = {}; | ||
uncoloredNodes.forEach(function (node) { | ||
nodeGroups[colorByAccessor(node)] = null; | ||
}); | ||
Object.keys(nodeGroups).forEach(function (group, idx) { | ||
nodeGroups[group] = idx; | ||
}); | ||
uncoloredNodes.forEach(function (node) { | ||
nodeGroups[colorByAccessor(node)] = null; | ||
}); | ||
Object.keys(nodeGroups).forEach(function (group, idx) { | ||
nodeGroups[group] = idx; | ||
}); | ||
uncoloredNodes.forEach(function (node) { | ||
node[colorField] = colors[nodeGroups[colorByAccessor(node)] % colors.length]; | ||
}); | ||
uncoloredNodes.forEach(function (node) { | ||
node[colorField] = colors[nodeGroups[colorByAccessor(node)] % colors.length]; | ||
}); | ||
} | ||
@@ -45,243 +45,243 @@ | ||
props: { | ||
jsonUrl: {}, | ||
graphData: { | ||
default: { | ||
nodes: [], | ||
links: [] | ||
}, | ||
onChange: function onChange(_, state) { | ||
state.onFrame = null; | ||
} // Pause simulation | ||
props: { | ||
jsonUrl: {}, | ||
graphData: { | ||
default: { | ||
nodes: [], | ||
links: [] | ||
}, | ||
onChange: function onChange(_, state) { | ||
state.onFrame = null; | ||
} // Pause simulation | ||
}, | ||
numDimensions: { | ||
default: 3, | ||
onChange: function onChange(numDim, state) { | ||
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' + dim]; // velocity | ||
}); | ||
} | ||
} | ||
}, | ||
nodeRelSize: { default: 4 }, // volume per val unit | ||
autoColorBy: {}, | ||
nodeId: { default: 'id' }, | ||
nodeVal: { default: 'val' }, | ||
nodeResolution: { default: 8 }, // how many slice segments in the sphere's circumference | ||
nodeColor: { default: 'color' }, | ||
nodeThreeObject: {}, | ||
linkSource: { default: 'source' }, | ||
linkTarget: { default: 'target' }, | ||
linkColor: { default: 'color' }, | ||
linkOpacity: { default: 0.2 }, | ||
forceEngine: { default: 'd3' }, // d3 or ngraph | ||
d3AlphaDecay: { default: 0.0228 }, | ||
d3VelocityDecay: { default: 0.4 }, | ||
warmupTicks: { default: 0 }, // how many times to tick the force engine at init before starting to render | ||
cooldownTicks: { default: Infinity }, | ||
cooldownTime: { default: 15000 } // ms | ||
}, | ||
numDimensions: { | ||
default: 3, | ||
onChange: function onChange(numDim, state) { | ||
if (numDim < 3) { | ||
eraseDimension(state.graphData.nodes, 'z'); | ||
} | ||
if (numDim < 2) { | ||
eraseDimension(state.graphData.nodes, 'y'); | ||
} | ||
methods: { | ||
// Expose d3 forces for external manipulation | ||
d3Force: function d3Force(state, forceName, forceFn) { | ||
if (!state.initialised) { | ||
return null; // d3 force simulation object doesn't exist yet | ||
} | ||
if (forceFn === undefined) { | ||
return state.d3ForceLayout.force(forceName); // Force getter | ||
} | ||
state.d3ForceLayout.force(forceName, forceFn); // Force setter | ||
return this; | ||
}, | ||
tickFrame: function tickFrame(state) { | ||
if (state.onFrame) state.onFrame(); | ||
return this; | ||
function eraseDimension(nodes, dim) { | ||
nodes.forEach(function (d) { | ||
delete d[dim]; // position | ||
delete d['v' + dim]; // velocity | ||
}); | ||
} | ||
} | ||
}, | ||
nodeRelSize: { default: 4 }, // volume per val unit | ||
autoColorBy: {}, | ||
nodeId: { default: 'id' }, | ||
nodeVal: { default: 'val' }, | ||
nodeResolution: { default: 8 }, // how many slice segments in the sphere's circumference | ||
nodeColor: { default: 'color' }, | ||
nodeThreeObject: {}, | ||
linkSource: { default: 'source' }, | ||
linkTarget: { default: 'target' }, | ||
linkColor: { default: 'color' }, | ||
linkOpacity: { default: 0.2 }, | ||
forceEngine: { default: 'd3' }, // d3 or ngraph | ||
d3AlphaDecay: { default: 0.0228 }, | ||
d3VelocityDecay: { default: 0.4 }, | ||
warmupTicks: { default: 0 }, // how many times to tick the force engine at init before starting to render | ||
cooldownTicks: { default: Infinity }, | ||
cooldownTime: { default: 15000 } // ms | ||
}, | ||
init: function init(threeObj, state) { | ||
// Main three object to manipulate | ||
state.graphScene = threeObj; | ||
// Add D3 force-directed layout | ||
state.d3ForceLayout = forceSimulation().force('link', forceLink()).force('charge', forceManyBody()).force('center', forceCenter()).stop(); | ||
methods: { | ||
// 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; | ||
}, | ||
tickFrame: function tickFrame(state) { | ||
if (state.onFrame) state.onFrame(); | ||
return this; | ||
} | ||
}, | ||
update: function updateFn(state) { | ||
state.onFrame = null; // Pause simulation | ||
stateInit: function stateInit() { | ||
return { | ||
d3ForceLayout: forceSimulation().force('link', forceLink()).force('charge', forceManyBody()).force('center', forceCenter()).stop() | ||
}; | ||
}, | ||
if (state.graphData.nodes.length || state.graphData.links.length) { | ||
console.info('force-graph loading', state.graphData.nodes.length + ' nodes', state.graphData.links.length + ' links'); | ||
} | ||
init: function init(threeObj, state) { | ||
// Main three object to manipulate | ||
state.graphScene = threeObj; | ||
}, | ||
if (!state.fetchingJson && state.jsonUrl && !state.graphData.nodes.length && !state.graphData.links.length) { | ||
// (Re-)load data | ||
state.fetchingJson = true; | ||
qwest.get(state.jsonUrl).then(function (_, json) { | ||
state.fetchingJson = false; | ||
state.graphData = json; | ||
updateFn(state); // Force re-update | ||
}); | ||
} | ||
update: function updateFn(state) { | ||
state.onFrame = null; // Pause simulation | ||
if (state.autoColorBy !== null) { | ||
// Auto add color to uncolored nodes | ||
autoColorNodes(state.graphData.nodes, accessorFn(state.autoColorBy), state.nodeColor); | ||
} | ||
if (state.graphData.nodes.length || state.graphData.links.length) { | ||
console.info('force-graph loading', state.graphData.nodes.length + ' nodes', state.graphData.links.length + ' links'); | ||
} | ||
// parse links | ||
state.graphData.links.forEach(function (link) { | ||
link.source = link[state.linkSource]; | ||
link.target = link[state.linkTarget]; | ||
}); | ||
if (!state.fetchingJson && state.jsonUrl && !state.graphData.nodes.length && !state.graphData.links.length) { | ||
// (Re-)load data | ||
state.fetchingJson = true; | ||
qwest.get(state.jsonUrl).then(function (_, json) { | ||
state.fetchingJson = false; | ||
state.graphData = json; | ||
updateFn(state); // Force re-update | ||
}); | ||
} | ||
// Add WebGL objects | ||
while (state.graphScene.children.length) { | ||
state.graphScene.remove(state.graphScene.children[0]); | ||
} // Clear the place | ||
if (state.autoColorBy !== null) { | ||
// Auto add color to uncolored nodes | ||
autoColorNodes(state.graphData.nodes, accessorFn(state.autoColorBy), state.nodeColor); | ||
} | ||
var customNodeObjectAccessor = accessorFn(state.nodeThreeObject); | ||
var valAccessor = accessorFn(state.nodeVal); | ||
var colorAccessor = accessorFn(state.nodeColor); | ||
var sphereGeometries = {}; // indexed by node value | ||
var sphereMaterials = {}; // indexed by color | ||
state.graphData.nodes.forEach(function (node) { | ||
var customObj = customNodeObjectAccessor(node); | ||
// parse links | ||
state.graphData.links.forEach(function (link) { | ||
link.source = link[state.linkSource]; | ||
link.target = link[state.linkTarget]; | ||
}); | ||
var obj = void 0; | ||
if (customObj) { | ||
obj = customObj.clone(); | ||
} else { | ||
// Default object (sphere mesh) | ||
var val = valAccessor(node) || 1; | ||
if (!sphereGeometries.hasOwnProperty(val)) { | ||
sphereGeometries[val] = new SphereGeometry(Math.cbrt(val) * state.nodeRelSize, state.nodeResolution, state.nodeResolution); | ||
} | ||
// Add WebGL objects | ||
while (state.graphScene.children.length) { | ||
state.graphScene.remove(state.graphScene.children[0]); | ||
} // Clear the place | ||
var color = colorAccessor(node); | ||
if (!sphereMaterials.hasOwnProperty(color)) { | ||
sphereMaterials[color] = new MeshLambertMaterial({ | ||
color: colorStr2Hex(color || '#ffffaa'), | ||
transparent: true, | ||
opacity: 0.75 | ||
}); | ||
} | ||
var customNodeObjectAccessor = accessorFn(state.nodeThreeObject); | ||
var valAccessor = accessorFn(state.nodeVal); | ||
var colorAccessor = accessorFn(state.nodeColor); | ||
var sphereGeometries = {}; // indexed by node value | ||
var sphereMaterials = {}; // indexed by color | ||
state.graphData.nodes.forEach(function (node) { | ||
var customObj = customNodeObjectAccessor(node); | ||
obj = new Mesh(sphereGeometries[val], sphereMaterials[color]); | ||
} | ||
var obj = void 0; | ||
if (customObj) { | ||
obj = customObj.clone(); | ||
} else { | ||
// Default object (sphere mesh) | ||
var val = valAccessor(node) || 1; | ||
if (!sphereGeometries.hasOwnProperty(val)) { | ||
sphereGeometries[val] = new SphereGeometry(Math.cbrt(val) * state.nodeRelSize, state.nodeResolution, state.nodeResolution); | ||
} | ||
obj.__graphObjType = 'node'; // Add object type | ||
obj.__data = node; // Attach node data | ||
var color = colorAccessor(node); | ||
if (!sphereMaterials.hasOwnProperty(color)) { | ||
sphereMaterials[color] = new MeshLambertMaterial({ | ||
color: colorStr2Hex(color || '#ffffaa'), | ||
transparent: true, | ||
opacity: 0.75 | ||
}); | ||
} | ||
state.graphScene.add(node.__threeObj = obj); | ||
obj = new Mesh(sphereGeometries[val], sphereMaterials[color]); | ||
} | ||
obj.__graphObjType = 'node'; // Add object type | ||
obj.__data = node; // Attach node data | ||
state.graphScene.add(node.__threeObj = obj); | ||
}); | ||
var linkColorAccessor = accessorFn(state.linkColor); | ||
var lineMaterials = {}; // indexed by color | ||
state.graphData.links.forEach(function (link) { | ||
var color = linkColorAccessor(link); | ||
if (!lineMaterials.hasOwnProperty(color)) { | ||
lineMaterials[color] = new LineBasicMaterial({ | ||
color: colorStr2Hex(color || '#f0f0f0'), | ||
transparent: true, | ||
opacity: state.linkOpacity | ||
}); | ||
} | ||
var linkColorAccessor = accessorFn(state.linkColor); | ||
var lineMaterials = {}; // indexed by color | ||
state.graphData.links.forEach(function (link) { | ||
var color = linkColorAccessor(link); | ||
if (!lineMaterials.hasOwnProperty(color)) { | ||
lineMaterials[color] = new LineBasicMaterial({ | ||
color: colorStr2Hex(color || '#f0f0f0'), | ||
transparent: true, | ||
opacity: state.linkOpacity | ||
}); | ||
} | ||
var geometry = new BufferGeometry(); | ||
geometry.addAttribute('position', new BufferAttribute(new Float32Array(2 * 3), 3)); | ||
var lineMaterial = lineMaterials[color]; | ||
var line = new Line(geometry, lineMaterial); | ||
var geometry = new BufferGeometry(); | ||
geometry.addAttribute('position', new BufferAttribute(new Float32Array(2 * 3), 3)); | ||
var lineMaterial = lineMaterials[color]; | ||
var line = new Line(geometry, lineMaterial); | ||
line.renderOrder = 10; // Prevent visual glitches of dark lines on top of nodes by rendering them last | ||
line.renderOrder = 10; // Prevent visual glitches of dark lines on top of nodes by rendering them last | ||
line.__graphObjType = 'link'; // Add object type | ||
line.__graphObjType = 'link'; // Add object type | ||
state.graphScene.add(link.__lineObj = line); | ||
}); | ||
state.graphScene.add(link.__lineObj = line); | ||
}); | ||
// Feed data to force-directed layout | ||
var isD3Sim = state.forceEngine !== 'ngraph'; | ||
var layout = void 0; | ||
if (isD3Sim) { | ||
// D3-force | ||
(layout = state.d3ForceLayout).stop().alpha(1) // re-heat the simulation | ||
.alphaDecay(state.d3AlphaDecay).velocityDecay(state.d3VelocityDecay).numDimensions(state.numDimensions).nodes(state.graphData.nodes).force('link').id(function (d) { | ||
return d[state.nodeId]; | ||
}).links(state.graphData.links); | ||
} else { | ||
// ngraph | ||
var _graph = ngraph.graph(); | ||
state.graphData.nodes.forEach(function (node) { | ||
_graph.addNode(node[state.nodeId]); | ||
}); | ||
state.graphData.links.forEach(function (link) { | ||
_graph.addLink(link.source, link.target); | ||
}); | ||
layout = ngraph['forcelayout' + (state.numDimensions === 2 ? '' : '3d')](_graph); | ||
layout.graph = _graph; // Attach graph reference to layout | ||
} | ||
// Feed data to force-directed layout | ||
var isD3Sim = state.forceEngine !== 'ngraph'; | ||
var layout = void 0; | ||
if (isD3Sim) { | ||
// D3-force | ||
(layout = state.d3ForceLayout).stop().alpha(1) // re-heat the simulation | ||
.alphaDecay(state.d3AlphaDecay).velocityDecay(state.d3VelocityDecay).numDimensions(state.numDimensions).nodes(state.graphData.nodes).force('link').id(function (d) { | ||
return d[state.nodeId]; | ||
}).links(state.graphData.links); | ||
} else { | ||
// ngraph | ||
var _graph = ngraph.graph(); | ||
state.graphData.nodes.forEach(function (node) { | ||
_graph.addNode(node[state.nodeId]); | ||
}); | ||
state.graphData.links.forEach(function (link) { | ||
_graph.addLink(link.source, link.target); | ||
}); | ||
layout = ngraph['forcelayout' + (state.numDimensions === 2 ? '' : '3d')](_graph); | ||
layout.graph = _graph; // Attach graph reference to layout | ||
} | ||
for (var i = 0; i < state.warmupTicks; i++) { | ||
layout[isD3Sim ? 'tick' : 'step'](); | ||
} // Initial ticks before starting to render | ||
for (var i = 0; i < state.warmupTicks; i++) { | ||
layout[isD3Sim ? 'tick' : 'step'](); | ||
} // Initial ticks before starting to render | ||
var cntTicks = 0; | ||
var startTickTime = new Date(); | ||
state.onFrame = layoutTick; | ||
var cntTicks = 0; | ||
var startTickTime = new Date(); | ||
state.onFrame = layoutTick; | ||
// | ||
// | ||
function layoutTick() { | ||
if (++cntTicks > state.cooldownTicks || new Date() - startTickTime > state.cooldownTime) { | ||
state.onFrame = null; // Stop ticking graph | ||
} else { | ||
layout[isD3Sim ? 'tick' : 'step'](); // Tick it | ||
} | ||
function layoutTick() { | ||
if (++cntTicks > state.cooldownTicks || new Date() - startTickTime > state.cooldownTime) { | ||
state.onFrame = null; // Stop ticking graph | ||
} else { | ||
layout[isD3Sim ? 'tick' : 'step'](); // Tick it | ||
} | ||
// Update nodes position | ||
state.graphData.nodes.forEach(function (node) { | ||
var obj = node.__threeObj; | ||
if (!obj) return; | ||
// Update nodes position | ||
state.graphData.nodes.forEach(function (node) { | ||
var obj = node.__threeObj; | ||
if (!obj) return; | ||
var pos = isD3Sim ? node : layout.getNodePosition(node[state.nodeId]); | ||
var pos = isD3Sim ? node : layout.getNodePosition(node[state.nodeId]); | ||
obj.position.x = pos.x; | ||
obj.position.y = pos.y || 0; | ||
obj.position.z = pos.z || 0; | ||
}); | ||
obj.position.x = pos.x; | ||
obj.position.y = pos.y || 0; | ||
obj.position.z = pos.z || 0; | ||
}); | ||
// Update links position | ||
state.graphData.links.forEach(function (link) { | ||
var line = link.__lineObj; | ||
if (!line) return; | ||
// Update links position | ||
state.graphData.links.forEach(function (link) { | ||
var line = link.__lineObj; | ||
if (!line) return; | ||
var pos = isD3Sim ? link : layout.getLinkPosition(layout.graph.getLink(link.source, link.target).id), | ||
start = pos[isD3Sim ? 'source' : 'from'], | ||
end = pos[isD3Sim ? 'target' : 'to'], | ||
linePos = line.geometry.attributes.position; | ||
var pos = isD3Sim ? link : layout.getLinkPosition(layout.graph.getLink(link.source, link.target).id), | ||
start = pos[isD3Sim ? 'source' : 'from'], | ||
end = pos[isD3Sim ? 'target' : 'to'], | ||
linePos = line.geometry.attributes.position; | ||
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.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; | ||
line.geometry.computeBoundingSphere(); | ||
}); | ||
} | ||
linePos.needsUpdate = true; | ||
line.geometry.computeBoundingSphere(); | ||
}); | ||
} | ||
} | ||
}); | ||
@@ -288,0 +288,0 @@ |
{ | ||
"name": "three-forcegraph", | ||
"version": "1.1.0", | ||
"version": "1.1.1", | ||
"description": "Force-directed graph as a ThreeJS 3d object", | ||
@@ -41,3 +41,3 @@ "unpkg": "dist/three-forcegraph.min.js", | ||
"d3-scale-chromatic": "^1.1.1", | ||
"kapsule": "^1.7.8", | ||
"kapsule": "^1.8.1", | ||
"ngraph.forcelayout": "^0.2.1", | ||
@@ -58,3 +58,3 @@ "ngraph.forcelayout3d": "^0.0.16", | ||
"babel-preset-env": "^1.6.1", | ||
"rollup": "^0.53.0", | ||
"rollup": "^0.53.2", | ||
"rollup-plugin-babel": "^3.0.3", | ||
@@ -64,4 +64,4 @@ "rollup-plugin-commonjs": "^8.2.6", | ||
"rollup-watch": "^4.3.1", | ||
"uglify-js": "^3.3.3" | ||
"uglify-js": "^3.3.4" | ||
} | ||
} |
@@ -7,19 +7,19 @@ import resolve from 'rollup-plugin-node-resolve'; | ||
export default { | ||
external: ['three'], | ||
input: 'src/index.js', | ||
output: [ | ||
{ | ||
format: 'umd', | ||
name: 'ThreeForceGraph', | ||
globals: { three: 'THREE' }, | ||
file: `dist/${name}.js`, | ||
sourcemap: true, | ||
banner: `// Version ${version} ${name} - ${homepage}` | ||
} | ||
], | ||
plugins: [ | ||
resolve(), | ||
commonJs(), | ||
babel({ exclude: 'node_modules/**' }) | ||
] | ||
external: ['three'], | ||
input: 'src/index.js', | ||
output: [ | ||
{ | ||
format: 'umd', | ||
name: 'ThreeForceGraph', | ||
globals: { three: 'THREE' }, | ||
file: `dist/${name}.js`, | ||
sourcemap: true, | ||
banner: `// Version ${version} ${name} - ${homepage}` | ||
} | ||
], | ||
plugins: [ | ||
resolve(), | ||
commonJs(), | ||
babel({ exclude: 'node_modules/**' }) | ||
] | ||
}; |
@@ -5,17 +5,17 @@ import babel from 'rollup-plugin-babel'; | ||
export default { | ||
input: 'src/index.js', | ||
output: [ | ||
{ | ||
format: 'cjs', | ||
file: `dist/${name}.common.js` | ||
}, | ||
{ | ||
format: 'es', | ||
file: `dist/${name}.module.js` | ||
} | ||
], | ||
external: [...Object.keys(dependencies), ...Object.keys(peerDependencies)], | ||
plugins: [ | ||
babel() | ||
] | ||
input: 'src/index.js', | ||
output: [ | ||
{ | ||
format: 'cjs', | ||
file: `dist/${name}.common.js` | ||
}, | ||
{ | ||
format: 'es', | ||
file: `dist/${name}.module.js` | ||
} | ||
], | ||
external: [...Object.keys(dependencies), ...Object.keys(peerDependencies)], | ||
plugins: [ | ||
babel() | ||
] | ||
}; |
@@ -7,17 +7,17 @@ import { schemePaired } from 'd3-scale-chromatic'; | ||
function autoColorNodes(nodes, colorByAccessor, colorField) { | ||
if (!colorByAccessor || typeof colorField !== 'string') return; | ||
if (!colorByAccessor || typeof colorField !== 'string') return; | ||
const colors = schemePaired; // Paired color set from color brewer | ||
const colors = schemePaired; // Paired color set from color brewer | ||
const uncoloredNodes = nodes.filter(node => !node[colorField]); | ||
const nodeGroups = {}; | ||
const uncoloredNodes = nodes.filter(node => !node[colorField]); | ||
const nodeGroups = {}; | ||
uncoloredNodes.forEach(node => { nodeGroups[colorByAccessor(node)] = null }); | ||
Object.keys(nodeGroups).forEach((group, idx) => { nodeGroups[group] = idx }); | ||
uncoloredNodes.forEach(node => { nodeGroups[colorByAccessor(node)] = null }); | ||
Object.keys(nodeGroups).forEach((group, idx) => { nodeGroups[group] = idx }); | ||
uncoloredNodes.forEach(node => { | ||
node[colorField] = colors[nodeGroups[colorByAccessor(node)] % colors.length]; | ||
}); | ||
uncoloredNodes.forEach(node => { | ||
node[colorField] = colors[nodeGroups[colorByAccessor(node)] % colors.length]; | ||
}); | ||
} | ||
export { autoColorNodes, colorStr2Hex }; |
@@ -33,239 +33,237 @@ import { | ||
props: { | ||
jsonUrl: {}, | ||
graphData: { | ||
default: { | ||
nodes: [], | ||
links: [] | ||
}, | ||
onChange(_, state) { state.onFrame = null; } // Pause simulation | ||
}, | ||
numDimensions: { | ||
default: 3, | ||
onChange(numDim, state) { | ||
if (numDim < 3) { eraseDimension(state.graphData.nodes, 'z'); } | ||
if (numDim < 2) { eraseDimension(state.graphData.nodes, 'y'); } | ||
function eraseDimension(nodes, dim) { | ||
nodes.forEach(d => { | ||
delete d[dim]; // position | ||
delete d[`v${dim}`]; // velocity | ||
}); | ||
} | ||
} | ||
}, | ||
nodeRelSize: { default: 4 }, // volume per val unit | ||
autoColorBy: {}, | ||
nodeId: { default: 'id' }, | ||
nodeVal: { default: 'val' }, | ||
nodeResolution: { default: 8 }, // how many slice segments in the sphere's circumference | ||
nodeColor: { default: 'color' }, | ||
nodeThreeObject: {}, | ||
linkSource: { default: 'source' }, | ||
linkTarget: { default: 'target' }, | ||
linkColor: { default: 'color' }, | ||
linkOpacity: { default: 0.2 }, | ||
forceEngine: { default: 'd3' }, // d3 or ngraph | ||
d3AlphaDecay: { default: 0.0228 }, | ||
d3VelocityDecay: { default: 0.4 }, | ||
warmupTicks: { default: 0 }, // how many times to tick the force engine at init before starting to render | ||
cooldownTicks: { default: Infinity }, | ||
cooldownTime: { default: 15000 }, // ms | ||
props: { | ||
jsonUrl: {}, | ||
graphData: { | ||
default: { | ||
nodes: [], | ||
links: [] | ||
}, | ||
onChange(_, state) { state.onFrame = null; } // Pause simulation | ||
}, | ||
numDimensions: { | ||
default: 3, | ||
onChange(numDim, state) { | ||
if (numDim < 3) { eraseDimension(state.graphData.nodes, 'z'); } | ||
if (numDim < 2) { eraseDimension(state.graphData.nodes, 'y'); } | ||
methods: { | ||
// Expose d3 forces for external manipulation | ||
d3Force: function(state, forceName, forceFn) { | ||
if (!state.initialised) { | ||
return null; // d3 force simulation object doesn't exist yet | ||
} | ||
if (forceFn === undefined) { | ||
return state.d3ForceLayout.force(forceName); // Force getter | ||
} | ||
state.d3ForceLayout.force(forceName, forceFn); // Force setter | ||
return this; | ||
}, | ||
tickFrame: function(state) { | ||
if(state.onFrame) state.onFrame(); | ||
return this; | ||
function eraseDimension(nodes, dim) { | ||
nodes.forEach(d => { | ||
delete d[dim]; // position | ||
delete d[`v${dim}`]; // velocity | ||
}); | ||
} | ||
} | ||
}, | ||
nodeRelSize: { default: 4 }, // volume per val unit | ||
autoColorBy: {}, | ||
nodeId: { default: 'id' }, | ||
nodeVal: { default: 'val' }, | ||
nodeResolution: { default: 8 }, // how many slice segments in the sphere's circumference | ||
nodeColor: { default: 'color' }, | ||
nodeThreeObject: {}, | ||
linkSource: { default: 'source' }, | ||
linkTarget: { default: 'target' }, | ||
linkColor: { default: 'color' }, | ||
linkOpacity: { default: 0.2 }, | ||
forceEngine: { default: 'd3' }, // d3 or ngraph | ||
d3AlphaDecay: { default: 0.0228 }, | ||
d3VelocityDecay: { default: 0.4 }, | ||
warmupTicks: { default: 0 }, // how many times to tick the force engine at init before starting to render | ||
cooldownTicks: { default: Infinity }, | ||
cooldownTime: { default: 15000 }, // ms | ||
}, | ||
init: function(threeObj, state) { | ||
// Main three object to manipulate | ||
state.graphScene = threeObj; | ||
// Add D3 force-directed layout | ||
state.d3ForceLayout = d3ForceSimulation() | ||
.force('link', d3ForceLink()) | ||
.force('charge', d3ForceManyBody()) | ||
.force('center', d3ForceCenter()) | ||
.stop(); | ||
methods: { | ||
// Expose d3 forces for external manipulation | ||
d3Force: function(state, forceName, forceFn) { | ||
if (forceFn === undefined) { | ||
return state.d3ForceLayout.force(forceName); // Force getter | ||
} | ||
state.d3ForceLayout.force(forceName, forceFn); // Force setter | ||
return this; | ||
}, | ||
tickFrame: function(state) { | ||
if(state.onFrame) state.onFrame(); | ||
return this; | ||
} | ||
}, | ||
update: function updateFn(state) { | ||
state.onFrame = null; // Pause simulation | ||
stateInit: () => ({ | ||
d3ForceLayout: d3ForceSimulation() | ||
.force('link', d3ForceLink()) | ||
.force('charge', d3ForceManyBody()) | ||
.force('center', d3ForceCenter()) | ||
.stop() | ||
}), | ||
if (state.graphData.nodes.length || state.graphData.links.length) { | ||
console.info('force-graph loading', state.graphData.nodes.length + ' nodes', state.graphData.links.length + ' links'); | ||
} | ||
init: function(threeObj, state) { | ||
// Main three object to manipulate | ||
state.graphScene = threeObj; | ||
}, | ||
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 | ||
}); | ||
} | ||
update: function updateFn(state) { | ||
state.onFrame = null; // Pause simulation | ||
if (state.autoColorBy !== null) { | ||
// Auto add color to uncolored nodes | ||
autoColorNodes(state.graphData.nodes, accessorFn(state.autoColorBy), state.nodeColor); | ||
} | ||
if (state.graphData.nodes.length || state.graphData.links.length) { | ||
console.info('force-graph loading', state.graphData.nodes.length + ' nodes', state.graphData.links.length + ' links'); | ||
} | ||
// parse links | ||
state.graphData.links.forEach(link => { | ||
link.source = link[state.linkSource]; | ||
link.target = link[state.linkTarget]; | ||
}); | ||
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 | ||
}); | ||
} | ||
// Add WebGL objects | ||
while (state.graphScene.children.length) { state.graphScene.remove(state.graphScene.children[0]) } // Clear the place | ||
if (state.autoColorBy !== null) { | ||
// Auto add color to uncolored nodes | ||
autoColorNodes(state.graphData.nodes, accessorFn(state.autoColorBy), state.nodeColor); | ||
} | ||
const customNodeObjectAccessor = accessorFn(state.nodeThreeObject); | ||
const valAccessor = accessorFn(state.nodeVal); | ||
const colorAccessor = accessorFn(state.nodeColor); | ||
const sphereGeometries = {}; // indexed by node value | ||
const sphereMaterials = {}; // indexed by color | ||
state.graphData.nodes.forEach(node => { | ||
const customObj = customNodeObjectAccessor(node); | ||
// parse links | ||
state.graphData.links.forEach(link => { | ||
link.source = link[state.linkSource]; | ||
link.target = link[state.linkTarget]; | ||
}); | ||
let obj; | ||
if (customObj) { | ||
obj = customObj.clone(); | ||
} else { // Default object (sphere mesh) | ||
const val = valAccessor(node) || 1; | ||
if (!sphereGeometries.hasOwnProperty(val)) { | ||
sphereGeometries[val] = new SphereGeometry(Math.cbrt(val) * state.nodeRelSize, state.nodeResolution, state.nodeResolution); | ||
} | ||
// Add WebGL objects | ||
while (state.graphScene.children.length) { state.graphScene.remove(state.graphScene.children[0]) } // Clear the place | ||
const color = colorAccessor(node); | ||
if (!sphereMaterials.hasOwnProperty(color)) { | ||
sphereMaterials[color] = new MeshLambertMaterial({ | ||
color: colorStr2Hex(color || '#ffffaa'), | ||
transparent: true, | ||
opacity: 0.75 | ||
}); | ||
} | ||
const customNodeObjectAccessor = accessorFn(state.nodeThreeObject); | ||
const valAccessor = accessorFn(state.nodeVal); | ||
const colorAccessor = accessorFn(state.nodeColor); | ||
const sphereGeometries = {}; // indexed by node value | ||
const sphereMaterials = {}; // indexed by color | ||
state.graphData.nodes.forEach(node => { | ||
const customObj = customNodeObjectAccessor(node); | ||
obj = new Mesh(sphereGeometries[val], sphereMaterials[color]); | ||
} | ||
let obj; | ||
if (customObj) { | ||
obj = customObj.clone(); | ||
} else { // Default object (sphere mesh) | ||
const val = valAccessor(node) || 1; | ||
if (!sphereGeometries.hasOwnProperty(val)) { | ||
sphereGeometries[val] = new SphereGeometry(Math.cbrt(val) * state.nodeRelSize, state.nodeResolution, state.nodeResolution); | ||
} | ||
obj.__graphObjType = 'node'; // Add object type | ||
obj.__data = node; // Attach node data | ||
const color = colorAccessor(node); | ||
if (!sphereMaterials.hasOwnProperty(color)) { | ||
sphereMaterials[color] = new MeshLambertMaterial({ | ||
color: colorStr2Hex(color || '#ffffaa'), | ||
transparent: true, | ||
opacity: 0.75 | ||
}); | ||
} | ||
state.graphScene.add(node.__threeObj = obj); | ||
obj = new Mesh(sphereGeometries[val], sphereMaterials[color]); | ||
} | ||
obj.__graphObjType = 'node'; // Add object type | ||
obj.__data = node; // Attach node data | ||
state.graphScene.add(node.__threeObj = obj); | ||
}); | ||
const linkColorAccessor = accessorFn(state.linkColor); | ||
const lineMaterials = {}; // indexed by color | ||
state.graphData.links.forEach(link => { | ||
const color = linkColorAccessor(link); | ||
if (!lineMaterials.hasOwnProperty(color)) { | ||
lineMaterials[color] = new LineBasicMaterial({ | ||
color: colorStr2Hex(color || '#f0f0f0'), | ||
transparent: true, | ||
opacity: state.linkOpacity | ||
}); | ||
} | ||
const linkColorAccessor = accessorFn(state.linkColor); | ||
const lineMaterials = {}; // indexed by color | ||
state.graphData.links.forEach(link => { | ||
const color = linkColorAccessor(link); | ||
if (!lineMaterials.hasOwnProperty(color)) { | ||
lineMaterials[color] = new LineBasicMaterial({ | ||
color: colorStr2Hex(color || '#f0f0f0'), | ||
transparent: true, | ||
opacity: state.linkOpacity | ||
}); | ||
} | ||
const geometry = new BufferGeometry(); | ||
geometry.addAttribute('position', new BufferAttribute(new Float32Array(2 * 3), 3)); | ||
const lineMaterial = lineMaterials[color]; | ||
const line = new Line(geometry, lineMaterial); | ||
const geometry = new BufferGeometry(); | ||
geometry.addAttribute('position', new BufferAttribute(new Float32Array(2 * 3), 3)); | ||
const lineMaterial = lineMaterials[color]; | ||
const line = new Line(geometry, lineMaterial); | ||
line.renderOrder = 10; // Prevent visual glitches of dark lines on top of nodes by rendering them last | ||
line.renderOrder = 10; // Prevent visual glitches of dark lines on top of nodes by rendering them last | ||
line.__graphObjType = 'link'; // Add object type | ||
line.__graphObjType = 'link'; // Add object type | ||
state.graphScene.add(link.__lineObj = line); | ||
}); | ||
state.graphScene.add(link.__lineObj = line); | ||
}); | ||
// Feed data to force-directed layout | ||
const isD3Sim = state.forceEngine !== 'ngraph'; | ||
let layout; | ||
if (isD3Sim) { | ||
// D3-force | ||
(layout = state.d3ForceLayout) | ||
.stop() | ||
.alpha(1)// re-heat the simulation | ||
.alphaDecay(state.d3AlphaDecay) | ||
.velocityDecay(state.d3VelocityDecay) | ||
.numDimensions(state.numDimensions) | ||
.nodes(state.graphData.nodes) | ||
.force('link') | ||
.id(d => d[state.nodeId]) | ||
.links(state.graphData.links); | ||
} else { | ||
// ngraph | ||
const graph = ngraph.graph(); | ||
state.graphData.nodes.forEach(node => { graph.addNode(node[state.nodeId]); }); | ||
state.graphData.links.forEach(link => { graph.addLink(link.source, link.target); }); | ||
layout = ngraph['forcelayout' + (state.numDimensions === 2 ? '' : '3d')](graph); | ||
layout.graph = graph; // Attach graph reference to layout | ||
} | ||
// Feed data to force-directed layout | ||
const isD3Sim = state.forceEngine !== 'ngraph'; | ||
let layout; | ||
if (isD3Sim) { | ||
// D3-force | ||
(layout = state.d3ForceLayout) | ||
.stop() | ||
.alpha(1)// re-heat the simulation | ||
.alphaDecay(state.d3AlphaDecay) | ||
.velocityDecay(state.d3VelocityDecay) | ||
.numDimensions(state.numDimensions) | ||
.nodes(state.graphData.nodes) | ||
.force('link') | ||
.id(d => d[state.nodeId]) | ||
.links(state.graphData.links); | ||
} else { | ||
// ngraph | ||
const graph = ngraph.graph(); | ||
state.graphData.nodes.forEach(node => { graph.addNode(node[state.nodeId]); }); | ||
state.graphData.links.forEach(link => { graph.addLink(link.source, link.target); }); | ||
layout = ngraph['forcelayout' + (state.numDimensions === 2 ? '' : '3d')](graph); | ||
layout.graph = graph; // Attach graph reference to layout | ||
} | ||
for (let i=0; i<state.warmupTicks; i++) { layout[isD3Sim?'tick':'step'](); } // Initial ticks before starting to render | ||
for (let i=0; i<state.warmupTicks; i++) { layout[isD3Sim?'tick':'step'](); } // Initial ticks before starting to render | ||
let cntTicks = 0; | ||
const startTickTime = new Date(); | ||
state.onFrame = layoutTick; | ||
let cntTicks = 0; | ||
const startTickTime = new Date(); | ||
state.onFrame = layoutTick; | ||
// | ||
// | ||
function layoutTick() { | ||
if (++cntTicks > state.cooldownTicks || (new Date()) - startTickTime > state.cooldownTime) { | ||
state.onFrame = null; // Stop ticking graph | ||
} else { | ||
layout[isD3Sim ? 'tick' : 'step'](); // Tick it | ||
} | ||
function layoutTick() { | ||
if (++cntTicks > state.cooldownTicks || (new Date()) - startTickTime > state.cooldownTime) { | ||
state.onFrame = null; // Stop ticking graph | ||
} else { | ||
layout[isD3Sim ? 'tick' : 'step'](); // Tick it | ||
} | ||
// Update nodes position | ||
state.graphData.nodes.forEach(node => { | ||
const obj = node.__threeObj; | ||
if (!obj) return; | ||
// Update nodes position | ||
state.graphData.nodes.forEach(node => { | ||
const obj = node.__threeObj; | ||
if (!obj) return; | ||
const pos = isD3Sim ? node : layout.getNodePosition(node[state.nodeId]); | ||
const pos = isD3Sim ? node : layout.getNodePosition(node[state.nodeId]); | ||
obj.position.x = pos.x; | ||
obj.position.y = pos.y || 0; | ||
obj.position.z = pos.z || 0; | ||
}); | ||
obj.position.x = pos.x; | ||
obj.position.y = pos.y || 0; | ||
obj.position.z = pos.z || 0; | ||
}); | ||
// Update links position | ||
state.graphData.links.forEach(link => { | ||
const line = link.__lineObj; | ||
if (!line) return; | ||
// Update links position | ||
state.graphData.links.forEach(link => { | ||
const line = link.__lineObj; | ||
if (!line) return; | ||
const pos = isD3Sim | ||
? link | ||
: layout.getLinkPosition(layout.graph.getLink(link.source, link.target).id), | ||
start = pos[isD3Sim ? 'source' : 'from'], | ||
end = pos[isD3Sim ? 'target' : 'to'], | ||
linePos = line.geometry.attributes.position; | ||
const pos = isD3Sim | ||
? link | ||
: layout.getLinkPosition(layout.graph.getLink(link.source, link.target).id), | ||
start = pos[isD3Sim ? 'source' : 'from'], | ||
end = pos[isD3Sim ? 'target' : 'to'], | ||
linePos = line.geometry.attributes.position; | ||
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.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; | ||
line.geometry.computeBoundingSphere(); | ||
}); | ||
} | ||
linePos.needsUpdate = true; | ||
line.geometry.computeBoundingSphere(); | ||
}); | ||
} | ||
} | ||
}); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
1124059
27
Updatedkapsule@^1.8.1