@jonobr1/force-directed-graph
Version:
GPU supercharged attraction-graph visualizations for the web built on top of Three.js
346 lines (295 loc) • 11.4 kB
HTML
<html lang="en">
<head>
<meta charset="utf8">
<!-- Primary Meta Tags -->
<title>Force Directed Graph</title>
<meta name="title" content="FDG — Force Directed Graph">
<meta name="description" content="GPU supercharged attraction-graph visualizations for the web built on top of Three.js.">
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://jonobr1.com/force-directed-graph">
<meta property="og:title" content="FDG — Force Directed Graph">
<meta property="og:description" content="GPU supercharged attraction-graph visualizations for the web built on top of Three.js.">
<meta property="og:image" content="https://jonobr1.com/force-directed-graph/thumbnail.png">
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image">
<meta property="twitter:url" content="https://metatags.io/">
<meta property="twitter:title" content="FDG — Force Directed Graph">
<meta property="twitter:description" content="GPU supercharged attraction-graph visualizations for the web built on top of Three.js.">
<meta property="twitter:image" content="https://jonobr1.com/force-directed-graph/thumbnail.png">
<meta name="viewport" content="width=device-width, user-scalable=no">
<link rel="icon" type="image/png" href="./favicon.png" />
<style>
* { margin: 0; padding: 0; }
body {
font-family: "Lucida Bright", "Times New Roman", Times, serif;
font-size: 20px;
line-height: 1.5;
color: #333;
}
.symbols {
font-family: webdings;
}
svg, canvas {
display: block;
}
div#stage {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: hidden;
}
div.content {
padding: 20px;
padding-top: 80px;
pointer-events: none;
}
p {
padding-top: 20px;
}
ul li {
display: inline-block;
}
ul li + li:before {
content: " • ";
}
div.scripts {
display: none;
}
div.column {
display: inline-block;
position: relative;
vertical-align: top;
min-width: 300px;
}
a {
color: #333;
text-decoration: none;
border-bottom: 1px solid #111;
pointer-events: auto;
}
h1, h2, h3, h4, h5, h6 {
font-weight: 100;
}
p {
max-width: 600px;
}
p + p {
margin-top: 10px;
}
ul {
margin-bottom: 20px;
}
.explanation {
cursor: help;
}
.action {
font-style: italic;
cursor: pointer;
border-bottom: 1px solid #333;
}
.nota-bene {
font-style: italic;
border-left: 4px solid orange;
padding-left: 10px;
margin-left: -10px;
}
.hidden {
display: none;
}
</style>
</head>
<body>
<div id="stage"></div>
<div class="content column">
<h1 id="title">
Force Directed Graph
</h1>
<p id="links">
<a href="https://npmjs.com/package/@jonobr1/force-directed-graph">NPM</a> · <a href="http://github.com/jonobr1/force-directed-graph">Source Code</a> · <a href="https://github.com/jonobr1/force-directed-graph/blob/master/LICENSE" target="_blank">MIT</a>
</p>
<p>
GPU supercharged attraction-graph visualizations for the web built on top of <a href="http://threejs.org">Three.js</a>. Importable as an ES6 module. In this demo you can click and drag the visualization to rotate the camera.
</p>
<p>
Check out this demo with different particle counts:
<ul>
<li><a href="./?amount=250">250 Nodes</a></li>
<li><a href="./?amount=1000">1k Nodes</a></li>
<li><a href="./?amount=5000">5k Nodes</a></li>
<li><a href="./?amount=10000">10k Nodes</a></li>
<li><a href="https://codepen.io/collection/YyQjom">More examples</a></li>
</ul>
</p>
<p id="post-scriptum">
Created <span id="created-date"></span> and updated <span id="updated-date"></span>.
<br />
A free and open source tool by <a href="http://jono.fyi/" target="_blank">Jono Brandel</a>
<br />
</p>
<div id="gui"></div>
</div>
<div class="scripts">
<script async src="https://unpkg.com/es-module-shims@1.3.6/dist/es-module-shims.js"></script>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three/build/three.module.js",
"three/examples/jsm/misc/GPUComputationRenderer.js": "https://cdn.jsdelivr.net/npm/three/examples/jsm/misc/GPUComputationRenderer.js",
"three/examples/jsm/controls/OrbitControls.js": "https://cdn.jsdelivr.net/npm/three/examples/jsm/controls/OrbitControls.js",
"lil-gui": "https://cdn.jsdelivr.net/npm/lil-gui/dist/lil-gui.esm.js",
"@jonobr1/force-directed-graph": "./src/index.js"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { GUI } from 'lil-gui';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import { ForceDirectedGraph } from '@jonobr1/force-directed-graph';
const BLEND_MODES = {
Normal: THREE.NormalBlending,
Additive: THREE.AdditiveBlending,
Subtractive: THREE.SubtractiveBlending,
Multiply: THREE.MultiplyBlending
};
const renderer = new THREE.WebGLRenderer({ antialias: true });
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera();
const controls = new OrbitControls(camera, renderer.domElement);
const fog = new THREE.Fog(0xffffff, 200, 750);
const mouse = new THREE.Vector2(-2, -2);
const fdg = new ForceDirectedGraph(renderer);
scene.fog = fog;
controls.enableDamping = true;
camera.position.z = 250;
const qp = new URLSearchParams(window.location.search);
const amount = +(qp.get('amount') || 1000);
const data = {
nodes: [],
links: []
};
for (let i = 0; i < amount; i++) {
data.nodes.push({ id: i });
const target = Math.floor(Math.random() * i);
const source = i;
if (i > 0 && Math.random() > 0.5) {
data.links.push({ target: i, source: i - 1 });
} else if (target !== source) {
data.links.push({
target,
source
});
}
}
let gui;
fdg.set(data).then(setup);
function setup() {
gui = new GUI();
fdg.pointColor.setRGB(0.3, 0.3, 0.3);
fdg.linkColor.setRGB(0.9, 0.9, 0.9);
gui.close();
[
{ name: 'decay', min: 0, max: 1, step: 0.001 },
{ name: 'maxSpeed', min: 0, max: 25, step: 1 },
{ name: 'timeStep', min: 0, max: 2, step: 0.1 },
{ name: 'damping', min: 0, max: 1, step: 0.1 },
{ name: 'repulsion', min: -2, max: 2, step: 0.1 },
{ name: 'springLength', min: 0, max: 10, step: 0.5 },
{ name: 'stiffness', min: 0, max: 1, step: 0.1 },
{ name: 'nodeRadius', min: 0, max: 5, step: 0.5 },
{ name: 'nodeScale', min: 0, max: 50, step: 0.1 },
{ name: 'gravity', min: 0, max: 1, step: 0.1 },
{ name: 'opacity', min: 0, max: 1, step: 0.1 }
].forEach(function({ name, min, max, step }) {
gui.add(fdg, name, min, max, step).name(name).onChange(reset);
});
gui.add(fdg, 'sizeAttenuation').onChange(reset);
gui.add(fdg, 'is2D').name('2D').onChange(reset);
gui.add({ blending: THREE.NormalBlending }, 'blending', BLEND_MODES)
.onChange(function(v) {
fdg.blending = v;
reset();
});
const folders = {
points: gui.addFolder('Points'),
links: gui.addFolder('Links')
};
folders.points.add(fdg.points, 'visible').name('visible');
folders.points.add(fdg, 'pointsInheritColor').name('inheritColors');
folders.points.addColor(fdg, 'pointColor').name('color');
folders.links.add(fdg.links, 'visible').name('visible');
folders.links.add(fdg, 'linksInheritColor').name('inheritColor');
folders.links.addColor(fdg, 'linkColor').name('color');
scene.add(fdg);
updateStats();
renderer.setClearColor('#fff');
document.querySelector('#stage').appendChild(renderer.domElement);
window.addEventListener('resize', resize, false);
renderer.domElement.addEventListener('pointermove', pointermove, false);
resize();
renderer.setAnimationLoop(render);
}
function resize() {
const width = window.innerWidth;
const height = window.innerHeight;
renderer.setSize(width, height);
camera.aspect = width / height;
camera.updateProjectionMatrix();
}
function pointermove(e) {
const x = e.clientX / window.innerWidth;
const y = e.clientY / window.innerHeight;
mouse.set(x, y);
}
function render(elapsed) {
controls.update();
fdg.update(elapsed);
renderer.render(scene, camera);
}
function reset() {
fdg.alpha = 1;
}
function updateStats() {
var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.github.com/repos/jonobr1/force-directed-graph');
xhr.onreadystatechange = function() {
if (!(xhr.readyState === 4 && xhr.status === 200)) {
return;
}
var resp = JSON.parse(xhr.responseText);
document.querySelector('#updated-date').textContent = formatDate(resp.pushed_at);
document.querySelector('#created-date').textContent = formatDate(resp.created_at);
};
xhr.send();
}
function formatDate(time) {
var date = new Date(time);
var suffices = ['st', 'nd', 'rd'];
suffices.getIndex = function(n) {
var index = parseInt((n + '').slice(-1));
return index - 1;
};
var months = ['January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'];
return [
months[date.getMonth()],
date.getDate() + ',',
date.getFullYear()
].join(' ')
}
</script>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-C0Y38714D6"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-C0Y38714D6');
</script>
</div>
</body>
</html>