UNPKG

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