This version also uses jQuery to manioulate screen elements, show the biography panel, and has some responsiveness built in.
A Pen by Peter Edwards on CodePen.
| <svg width="1000" height="1000"><rect width="100%" height="100%" class="bg"/></svg><div class="wkwpanel"><button class="wkwpanel-close">close</button><div class="wkwpanel-content"></div></div> |
| /** | |
| * Force-directed graph script for Who Knew Whom | |
| * Uses D3 and a custom Wordpress JSON API endpoint. | |
| */ | |
| (function($){ | |
| var width = $(window).width(), | |
| height = $(window).height(), | |
| breakpoint = 1024, | |
| svg = d3.select('svg'), | |
| min_zoom = 0.1, | |
| max_zoom = 7, | |
| api_url = 'https://culturalcartography.net/wkw-api/wkw/v1/', | |
| zoom, link, node, text, simulation; | |
| svg.attr('width', width).attr('height', height); | |
| //zoom = d3.behavior.zoom().scaleExtent([min_zoom,max_zoom]); | |
| var gl = svg.append("g").attr("class", "links"), | |
| gn = svg.append("g").attr("class", "nodes"), | |
| gt = svg.append("g").attr("class", "labels"); | |
| var t = d3.transition().duration(750); | |
| simulation = d3.forceSimulation() | |
| .force("link", d3.forceLink().distance(80).id(function(d) { return d.id; })) | |
| .force("charge", d3.forceManyBody().strength(-40)) | |
| .force("center", d3.forceCenter(width/2, height / 2)); | |
| function getData(id, callback){ | |
| d3.json(api_url + 'd3/' + id, function(error, graphdata){ | |
| if ( error ) throw error; | |
| callback(id, graphdata); | |
| }); | |
| } | |
| function update(id, graph){ | |
| link = gl.selectAll("line").data(graph.links); | |
| link.exit().remove(); | |
| link = link.enter().append("line").merge(link); | |
| node = gn.selectAll("circle").data(graph.nodes.reverse(), function(d) { return d.id; }); | |
| node.exit().transition() | |
| .attr("r", 0) | |
| .remove(); | |
| node = node.enter().append("circle") | |
| .merge(node) | |
| .attr("class", function(node) { hasbio = node.bio?' bio':'';return 'nodes level'+node.level+hasbio+' '+node.id+'-name';}) | |
| .attr("r", function(d) { return (30 / (d.level*d.level)); }) | |
| //.call(function(d) { d.transition().attr("r", (30 / (d.level*d.level))); }); | |
| .call(d3.drag() | |
| .on("start", dragstarted) | |
| .on("drag", dragged) | |
| .on("end", dragended)); | |
| node.on('click', function(d){getData(d.id, update);}); | |
| text = gt.selectAll("text").data(graph.nodes.reverse()); | |
| text.exit().remove(); | |
| text = text.enter().append("text").attr("text-anchor", "middle") | |
| .merge(text) | |
| .text(function(node) { return node.name }) | |
| .attr('id', function(node) { return node.id+'-text'}) | |
| .attr("class", function(node) { hasbio = node.bio?' bio':'';return 'texts level'+node.level+hasbio+' '+node.id+'-name';}) | |
| .attr("x", 0) | |
| .attr("dy", function(node) { return -(40/node.level);}); | |
| text.on('click', clickName); | |
| simulation | |
| .nodes(graph.nodes) | |
| .on("tick", ticked); | |
| simulation.force("link") | |
| .links(graph.links); | |
| simulation.alpha(1).restart(); | |
| showBio(id); | |
| } | |
| function ticked() { | |
| link | |
| .attr("x1", function(d) { return d.source.x; }) | |
| .attr("y1", function(d) { return d.source.y; }) | |
| .attr("x2", function(d) { return d.target.x; }) | |
| .attr("y2", function(d) { return d.target.y; }); | |
| node | |
| .attr("cx", function(d) { return d.x; }) | |
| .attr("cy", function(d) { return d.y; }); | |
| text | |
| .attr("x", function(d) { return d.x; }) | |
| .attr("y", function(d) { return d.y; }); | |
| } | |
| function clickName(selectedNode){ | |
| getData(selectedNode.id, update); | |
| } | |
| function showBio(id) | |
| { | |
| d3.json(api_url + 'name/' + id, function(error, data){ | |
| if ( error ) throw error; | |
| $('.wkwpanel-content').empty(); | |
| $('.wkwpanel-content').append($('<h1/>').text(data.name)); | |
| var bio = $('<div class="wkwpanel-content-bio"/>'); | |
| bio.append(data.content) | |
| if ( data.linked && data.linked.length ) { | |
| var list = $('<ul class="inline-list"/>'); | |
| for (var i = 0; i < data.linked.length; i++) { | |
| if (data.linked[i].url){ | |
| list.append('<li><a href="'+data.linked[i].url+'" data-nameid="'+data.linked[i].id+'">'+data.linked[i].name+'</a></li>'); | |
| } else { | |
| list.append('<li><span data-nameid="'+data.linked[i].id+'">'+data.linked[i].name+'</span></li>'); | |
| } | |
| } | |
| bio.append($('<h2/>').html(data.name+' knew…')).append(list); | |
| } | |
| $('.wkwpanel-content').append(bio); | |
| $('.wkwpanel').show(); | |
| moveGraph('left'); | |
| }); | |
| } | |
| function dragstarted(d) { | |
| if (!d3.event.active) simulation.alphaTarget(0.3).restart(); | |
| d.fx = d.x; | |
| d.fy = d.y; | |
| } | |
| function dragged(d) { | |
| d.fx = d3.event.x; | |
| d.fy = d3.event.y; | |
| } | |
| function dragended(d) { | |
| if (!d3.event.active) simulation.alphaTarget(0); | |
| d.fx = null; | |
| d.fy = null; | |
| } | |
| $('.wkwpanel').on('click', '.wkwpanel-close',function() { | |
| $('.wkwpanel').hide(); | |
| moveGraph(); | |
| }); | |
| $('.wkwpanel').on('click', 'a[data-nameid]',function(e) { | |
| getData($(this).data('nameid'), update); | |
| e.preventDefault(); | |
| }); | |
| $('.wkwpanel').on('mouseover', 'a[data-nameid]',function(e) { | |
| $('.'+$(this).data('nameid')+'-name').addClass('hover'); | |
| }); | |
| $('.wkwpanel').on('mouseout', 'a[data-nameid]',function(e) { | |
| $('.'+$(this).data('nameid')+'-name').removeClass('hover'); | |
| }); | |
| function moveGraph(dir) { | |
| if ( width > breakpoint ) { | |
| if (svg.attr('data-align') !== dir) { | |
| svg.attr('data-align', dir); | |
| d3.select({}).transition() | |
| .tween("center.move", function() { | |
| var i = (dir === 'left')? d3.interpolateArray([width/2, height/2],[width/3, height/2]): d3.interpolateArray([width/3, height/2],[width/2, height/2]); | |
| return function(t) { | |
| var c = i(t); | |
| simulation.force('center', d3.forceCenter(c[0], c[1])).restart(); | |
| }; | |
| }); | |
| } | |
| } | |
| } | |
| function resizeGraph() { | |
| width = $(window).width(); | |
| height = $(window).height(); | |
| svg.attr('width', width).attr('height', height); | |
| align = svg.attr('data-align'); | |
| if ( align === 'left' && width > breakpoint ) { | |
| simulation.force('center', d3.forceCenter(width/3, height/2)).restart(); | |
| } else { | |
| simulation.force('center', d3.forceCenter(width/2, height/2)).restart(); | |
| } | |
| } | |
| var resizeTimer; | |
| $(window).on('resize', function(e) { | |
| clearTimeout(resizeTimer); | |
| resizeTimer = setTimeout(function() { | |
| resizeGraph(); | |
| }, 250); | |
| }); | |
| getData(3717, update); | |
| })(jQuery); |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script> |
| $background:#19aa5a; | |
| $circle-fill:#0fc; | |
| $circle-stroke:#19aa5a; | |
| $circle-stroke-width:1.5px; | |
| $text-fill:#0fc; | |
| $text-stroke:#19aa5a; | |
| $line-colour:#ccc; | |
| $line-opacity:0.6; | |
| $level1-colour:#0fc; | |
| $level2-colour:mix($level1-colour,$background,66%); | |
| $level3-colour:mix($level1-colour,$background,33%); | |
| /* panel */ | |
| $panel-background:rgba(0,0,0,0.2); | |
| $panel-colour:#fff; | |
| $close-button-colour:#fff; | |
| $close-button-background:transparent; | |
| $close-button-border:none; | |
| $scrollbar-track:lighten($background, 1%); | |
| $scrollbar-thumb:darken($background, 5%); | |
| $scrollbar-thumb-hover:darken($background, 10%); | |
| /* Handsome Pro Regular font */ | |
| @font-face { | |
| font-family: 'HandsomeProRegular'; | |
| src: url('https://culturalcartography.net/wp-content/themes/wkw-theme/font/handsomepro-regular-webfont.eot'); | |
| src: url('https://culturalcartography.net/wp-content/themes/wkw-theme/font/handsomepro-regular-webfont.eot?#iefix') format('embedded-opentype'), | |
| url('https://culturalcartography.net/wp-content/themes/wkw-theme/font/handsomepro-regular-webfont.woff') format('woff'), | |
| url('https://culturalcartography.net/wp-content/themes/wkw-theme/font/handsomepro-regular-webfont.ttf') format('truetype'), | |
| url('https://culturalcartography.net/wp-content/themes/wkw-theme/font/handsomepro-regular-webfont.svg#HandsomeProRegular') format('svg'); | |
| font-weight: normal; | |
| font-style: normal; | |
| } | |
| :root { | |
| box-sizing: border-box; | |
| } | |
| *, | |
| ::before, | |
| ::after { | |
| box-sizing: inherit; | |
| } | |
| .bg { | |
| fill:$background; | |
| } | |
| .links line { | |
| stroke: $line-colour; | |
| stroke-opacity: $line-opacity; | |
| stroke-width:.5px; | |
| } | |
| .texts { | |
| font-family: 'HandsomeProRegular', sans-serif; | |
| font-size:1em; | |
| fill:$text-fill; | |
| stroke:$text-stroke; | |
| stroke-width:.01em; | |
| -webkit-user-select: none; | |
| -moz-user-select: none; | |
| -ms-user-select: none; | |
| user-select: none; | |
| } | |
| .texts.bio { | |
| cursor:pointer; | |
| } | |
| .texts.level1 { | |
| font-size:3em; | |
| fill:$level1-colour; | |
| opacity:1; | |
| } | |
| .texts.hover, | |
| .nodes circle.hover { | |
| fill:white; | |
| } | |
| .texts.level2 { | |
| font-size:1em; | |
| } | |
| .texts.level3 { | |
| display:none; | |
| } | |
| .nodes circle { | |
| stroke:$circle-stroke; | |
| stroke-width: $circle-stroke-width; | |
| opacity:1; | |
| } | |
| .nodes.level1 { | |
| fill:$level1-colour; | |
| } | |
| .nodes.level2 { | |
| fill:$level2-colour; | |
| cursor:move; | |
| } | |
| .nodes.level3 { | |
| fill:$level3-colour; | |
| } | |
| ul.inline-list { | |
| padding:0; | |
| margin:0; | |
| li { | |
| padding:0; | |
| margin:0; | |
| display: inline; | |
| list-style: none; | |
| &:after { | |
| content: ", "; | |
| } | |
| &:last-child:after { | |
| content: ""; | |
| } | |
| } | |
| } | |
| .wkwpanel { | |
| position:fixed; | |
| display:none; | |
| top:0; | |
| right:0; | |
| bottom:0; | |
| left:0; | |
| transition: left .5s; | |
| button.wkwpanel-close { | |
| background:$close-button-background; | |
| border:$close-button-border; | |
| margin:0; | |
| border-radius:.2em; | |
| color:$close-button-colour; | |
| position:absolute; | |
| right:1.5em; | |
| top:1.5em; | |
| cursor: pointer; | |
| font-size: 2em; | |
| height: 1em; | |
| width: 1em; | |
| text-indent: 10em; | |
| overflow: hidden; | |
| outline:none; | |
| &::after { | |
| position: absolute; | |
| line-height: 0.5; | |
| top: 0.22em; | |
| left: 0.18em; | |
| text-indent: 0; | |
| content: "\00D7"; | |
| } | |
| } | |
| .wkwpanel-content { | |
| margin:2em; | |
| padding:2em; | |
| border-radius:2em; | |
| background-color:$panel-background; | |
| color:$panel-colour; | |
| line-height:1.5; | |
| h1, h2 { | |
| font-family: 'HandsomeProRegular', sans-serif; | |
| line-height:1; | |
| margin:.3em 0; | |
| } | |
| h1 { | |
| font-size:3em; | |
| } | |
| h2 { | |
| font-size:2em; | |
| } | |
| a, a:hover, a:visited, a:visited:hover, a:active { | |
| color:#fff; | |
| text-decoration:none; | |
| } | |
| a:hover, a:visited:hover { | |
| text-decoration:underline; | |
| } | |
| .wkwpanel-content-bio { | |
| &::-webkit-scrollbar { | |
| width: 10px; | |
| } | |
| &::-webkit-scrollbar-track { | |
| background: $scrollbar-track; | |
| border-radius: 10px; | |
| } | |
| /* Handle */ | |
| &::-webkit-scrollbar-thumb { | |
| border-radius: 10px; | |
| background: $scrollbar-thumb; | |
| } | |
| /* Handle on hover */ | |
| &::-webkit-scrollbar-thumb:hover { | |
| background: $scrollbar-thumb-hover; | |
| } | |
| overflow:auto; | |
| padding-right:1em; | |
| height:calc(100vh - 12em); | |
| } | |
| } | |
| } | |
| @media screen and (min-width:1024px) { | |
| .wkwpanel { | |
| left:50%; | |
| } | |
| } | |
| @media screen and (min-width:1280px) { | |
| .wkwpanel { | |
| left:66%; | |
| } | |
| } |
This version also uses jQuery to manioulate screen elements, show the biography panel, and has some responsiveness built in.
A Pen by Peter Edwards on CodePen.