UNPKG

@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
<!doctype 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> &middot; <a href="http://github.com/jonobr1/force-directed-graph">Source Code</a> &middot; <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>