@jonobr1/force-directed-graph
Version:
GPU supercharged attraction-graph visualizations for the web built on top of Three.js
613 lines (572 loc) • 17.5 kB
HTML
<html lang="en">
<head>
<meta charset="utf-8" />
<!-- 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;
}
@media screen and (max-width: 600px) {
body {
font-size: 15px;
}
}
@media screen and (orientation: landscape) and (max-height: 600px) {
#description + * {
display: none;
}
}
</style>
</head>
<body>
<div id="stage"></div>
<div id="info" 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">Github</a>
·
<a
href="https://github.com/jonobr1/force-directed-graph/blob/master/LICENSE"
target="_blank"
>MIT</a
>
</p>
<p id="description">
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>
<div>
<div>
<p>Check out this demo with different particle counts:</p>
<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>
</div>
<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>
<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: [],
};
const words = [
'Lorem',
'ipsum',
'dolor',
'sit',
'amet',
'consectetur',
'adipiscing',
'elit',
'Praesent',
'risus',
'elit',
'gravida',
'et',
'accumsan',
'sed',
'molestie',
'vitae',
'massa',
'In',
'efficitur',
'pharetra',
'vestibulum',
'Vivamus',
'tempus',
'nibh',
'quis',
'lorem',
'tristique',
'tristique',
'Morbi',
'imperdiet',
'tortor',
'sodales',
'lacus',
'venenatis',
'ac',
'dictum',
'arcu',
'pulvinar',
'Etiam',
'viverra',
'eget',
'odio',
'eget',
'pretium',
'Praesent',
'aliquet',
'enim',
'at',
'euismod',
'maximus',
'dolor',
'lectus',
'luctus',
'odio',
'nec',
'cursus',
'nisi',
'neque',
'at',
'libero',
'Nam',
'eu',
'justo',
'nec',
'dolor',
'malesuada',
'porta',
'Sed',
'felis',
'urna',
'luctus',
'vitae',
'finibus',
'non',
'aliquet',
'sed',
'est',
'Suspendisse',
'porta',
'nibh',
'eu',
'erat',
'sodales',
'ultricies',
'Integer',
'suscipit',
'urna',
'neque',
'non',
'tempus',
'justo',
'mattis',
'sed',
'Sed',
'leo',
'nibh',
'dictum',
'sit',
'amet',
'tellus',
'blandit',
'cursus',
'pellentesque',
'libero',
'Fusce',
'vestibulum',
'lorem',
'in',
'sollicitudin',
'cursus',
'nibh',
'risus',
'efficitur',
'ligula',
'at',
'posuere',
'ligula',
'dui',
'quis',
'purus',
'Fusce',
'gravida',
'urna',
'at',
'hendrerit',
'facilisis',
'Sed',
'accumsan',
'dolor',
'non',
'hendrerit',
'interdum',
];
for (let i = 0; i < amount; i++) {
const r = Math.floor(Math.random() * 255);
const g = Math.floor(Math.random() * 255);
const b = Math.floor(Math.random() * 255);
const label = words[Math.floor(Math.random() * words.length)];
data.nodes.push({
id: i,
color: `rgb(${r},${g},${b})`,
label,
});
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);
fdg.labelColor.setRGB(0, 0, 0);
fdg.obscurity = 0.9;
fdg.labels.visible = false;
fdg.pointsInheritColor = false;
gui.close();
gui
.add({ visible: true }, 'visible')
.name('domElement')
.onChange((v) => {
document.body.querySelector('#info').style.display = !!v
? 'block'
: 'none';
});
gui.add(fog, 'near', 0, 700, 1).name('fogNear');
gui.add(fog, 'far', 250, 1000).name('fogFar');
[
{ 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: 'gravity', min: 0, max: 1, step: 0.1 },
{ name: 'beginning', min: 0, max: 1, step: 0.01 },
{ name: 'ending', min: 0, max: 1, step: 0.01 },
{ 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').close(),
links: gui.addFolder('Links').close(),
labels: gui.addFolder('Labels').close(),
};
folders.points.add(fdg.points, 'visible').name('visible');
folders.points.add(fdg, 'pointsInheritColor').name('inheritColors');
folders.points.addColor(fdg, 'pointColor').name('color');
folders.points.add(fdg, 'nodeRadius', 0, 5, 0.5).name('nodeRadius');
folders.points.add(fdg, 'nodeScale', 0, 50, 0.1).name('nodeScale');
folders.links.add(fdg.links, 'visible').name('visible');
folders.links.add(fdg, 'linksInheritColor').name('inheritColor');
folders.links.addColor(fdg, 'linkColor').name('color');
folders.links.add(fdg, 'linewidth', 0.5, 16, 0.5).name('linewidth');
folders.links
.add(fdg, 'linecap', ['round', 'butt', 'square'])
.name('cap');
folders.labels.add(fdg.labels, 'visible').name('visible');
folders.labels.add(fdg, 'obscurity', 0, 1, 0.01).name('obscurity');
folders.labels.add(fdg, 'labelsInheritColor').name('inheritColor');
folders.labels.addColor(fdg, 'labelColor').name('color');
folders.labels
.add(fdg.labels, 'fontFamily', [
'Arial, sans-serif',
'Times, serif',
'monospace',
])
.name('fontFamily');
folders.labels
.add(fdg.labels, 'fontSize', 1, 100, 1)
.name('fontSize');
folders.labels
.add(fdg.labels, 'alignment', ['left', 'center', 'right'])
.name('alignment');
folders.labels
.add(fdg.labels, 'baseline', ['top', 'middle', 'bottom'])
.name('baseline');
folders.labels.add(fdg.labels, 'near', 0, 100, 0.1).name('near');
folders.labels
.add(fdg.labels.offset, 'x', -5, 5, 0.1)
.name('offsetX');
folders.labels
.add(fdg.labels.offset, 'y', -5, 5, 0.1)
.name('offsetY');
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>