const canvas = document.querySelector('canvas'); const context = canvas.getContext('2d'); const width = canvas.width; const height = canvas.height; const searchRadius = 10; const color = d3.scaleOrdinal().range(d3.schemeCategory20); const simulation = d3 .forceSimulation() .force('charge', d3.forceManyBody().strength(-18)) .force('link', d3.forceLink().iterations(4).id(d => d.id)) .force('x', d3.forceX()) .force('y', d3.forceY()); const tooltip = d3 .select('body') .append('div') .attr('class', 'tooltip') .style('position', 'absolute') .style('z-index', '10') .style('visibility', 'hidden'); d3.json('graph.json', (error, graph) => { if (error) throw error; const users = d3 .nest() .key(d => d.user) .entries(graph.nodes) .sort((a, b) => b.values.length - a.values.length); color.domain(users.map(d => d.key)); simulation.nodes(graph.nodes).on('tick', ticked); simulation.force('link').links(graph.links); d3 .select(canvas) .on('mousemove', mousemoved) .call( d3 .drag() .container(canvas) .subject(dragsubject) .on('start', dragstarted) .on('drag', dragged) .on('end', dragended) ); function ticked() { context.clearRect(0, 0, width, height); context.save(); context.translate(width / 2, height / 2); context.beginPath(); graph.links.forEach(drawLink); context.strokeStyle = '#aaa'; context.stroke(); users.forEach(user => { context.beginPath(); user.values.forEach(drawNode); context.fillStyle = color(user.key); context.fill(); }); context.restore(); } function dragsubject() { return simulation.find( d3.event.x - width / 2, d3.event.y - height / 2, searchRadius ); } function mousemoved() { const a = this.parentNode; const m = d3.mouse(this); const d = simulation.find( m[0] - width / 2, m[1] - height / 2, searchRadius ); if (!d) return a.removeAttribute('href'); a.removeAttribute('title'); tooltip.style('visibility', 'hidden'); a.setAttribute( 'href', `http://bl.ocks.org/${d.user ? `${d.user}/` : ''}${d.id}` ); a.setAttribute( 'title', `${d.id}${d.user ? ` by ${d.user}` : ''}${d.description ? `\n${d.description}` : ''}` ); loadTooltipThumb(d); } }); function dragstarted() { if (!d3.event.active) simulation.alphaTarget(0.3).restart(); d3.event.subject.fx = d3.event.subject.x; d3.event.subject.fy = d3.event.subject.y; } function dragged() { d3.event.subject.fx = d3.event.x; d3.event.subject.fy = d3.event.y; } function dragended() { if (!d3.event.active) simulation.alphaTarget(0); d3.event.subject.fx = null; d3.event.subject.fy = null; } function drawLink(d) { context.moveTo(d.source.x, d.source.y); context.lineTo(d.target.x, d.target.y); } function drawNode(d) { context.moveTo(d.x + 3, d.y); context.arc(d.x, d.y, 3, 0, 2 * Math.PI); } function loadTooltipThumb(d) { tooltip.select('*').remove(); const thumbnailURL = `https://bl.ocks.org/${d.user ? `${d.user}/` : ''}raw/${d.id}/thumbnail.png`; const top = d3.event.pageY - 150; tooltip .style('top', `${top}px`) .style('left', `${d3.event.pageX + 40}px`) .style('visibility', 'visible') .append('img') .attr('src', thumbnailURL); }