Tooling to visualise EBI content relationships using D3.
Use this explore how content interacts and the types of links between content.
Legend:
Features:
Previous version: Adaptive-content-model V0.2
xxxxxxxxxx
<meta charset="utf-8">
<script type="text/javascript" src="//code.jquery.com/jquery-1.11.3.min.js"></script>
<script src="//d3js.org/d3.v3.min.js"></script>
<script type="text/javascript" src="d3-ForceEdgeBundling.js"></script>
<script src="colorbrewer.js"></script>
<style src="colorbrewer.css"></style>
<style>
body {
/*background: rgb(255,255,255);*/
background: rgb(55,55,55);
margin: 0;
}
body svg,
body .tooltip {
font: 9px verdana, sans-serif;
}
.link-5 {
fill: none;
stroke: #000;
stroke-dasharray: 2px 2px;
stroke-opacity: .4;
stroke-width: .8px;
}
.link-50 {
fill: none;
stroke: #666;
stroke-opacity: .4;
stroke-width: .9px;
}
.node circle:hover {
cursor: pointer;
opacity: .8;
}
.node circle.area-circle:hover {
cursor: -moz-grab; cursor: -webkit-grab;
}
.node text {
pointer-events: none;
position: relative;
z-index: 100;
color: #FFF;
text-shadow:
-1px -1px .1px #000,
1px -1px .1px #000,
-1px 1px .1px #000,
1px 1px .1px #000;
text-transform: uppercase;
}
h4 {
margin: 0;
text-transform: uppercase;
font-weight: normal;
}
.tooltip {
color: #fff;
background: #777;
padding: .5em;
}
.node-information {
width: 80%;
background: #fff;
position: fixed;
z-index: 100;
display: none;
margin: 8vw 9%;
padding: 1vw 1%;
border: 1px solid rgba(0,0,0,.5);
}
</style>
<body>
<div class="node-information"></div>
<script>
// About:
// This combines the basic D3 Force view with a newer "ForceBundle" graph.
// We first invoke the D3 Force to arange the nodes, and then ForceBundle to
// make it all pretty.
// info on force graphing
// https://bl.ocks.org/sathomas/1ca23ee9588580d768aa
// to do: experiment with specific force links: https://bl.ocks.org/sathomas/774d02a21dc1c714def8
// force layout settings https://github.com/mbostock/d3/wiki/Force-Layout#friction
// set the vis size
var width = window.innerWidth-15,
height = window.innerHeight-15;
// set the colors we wish to use
var color = d3.scale.ordinal()
.range(colorbrewer.Paired[9]);
// Showcase of colorbrewer pallets: https://bl.ocks.org/mhkeller/10504471
// set up the basic force direction graph
var force = d3.layout.force()
.linkDistance(function(node) {
return 30;
// return ((Math.abs(node.value) * Math.abs(node.value)) * 3) + 100;
})
.gravity(0.025)
.charge(function(node) {
// console.log(Math.abs(node.radius));
// return '-10';
return '-' + ( ((Math.abs(node.radius) * Math.abs(node.radius)) * 3) + 140 );
})
.linkStrength(function(node) {
return ( node.value + .5 ) * .5;
})
// .alpha(-.1)
// .friction(.5)
.size([width, height]);
// Append the SVG contianter for the vis
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var svgInner = svg.append("g");
// Append the tooltip
var tooltip = d3.select("body")
.append("div")
.attr('class','tooltip')
.style("position", "absolute")
.style("z-index", "10")
.style("visibility", "hidden")
.html("<a href='#'>a simple tooltip</a>");
// Load the data
d3.json("content-model.json", function(error, graph) {
if (error) console.log(error,graph);
var nodes = graph.nodes.slice(),
links = [],
bilinks = [];
// Setup the force direction graph
force
.nodes(nodes)
.links(graph.links);
// .start();
// Position certain points
// Assign variables to the position of each item in the array.
// It is equal to the "Row" column from the google doc minus 1 (1 = 0)
// https://docs.google.com/spreadsheets/d/1yikAVpZo4nXy7TZkHP7bDHCCOKs_mGsLUH-BwReFsK0/edit#gid=0
var nodeEMBL = 21,
nodeRESEARCH = 2,
nodeServices = 1,
nodeTraining = 3,
nodeNews = 7,
nodeAbout = 4,
nodeIntranet = 27,
nodeELIXIR = 6,
nodeSources = 55,
nodePeople = 47,
nodeExits = 56,
nodeIndustry = 5;
nodes[0].fixed = true; //front
nodes[0].x = width*.5;
nodes[0].y = height*.25;
// console.log(nodes[0].name);
// console.log(nodes);
nodes[nodeIndustry].fixed = true;
nodes[nodeIndustry].x = width*.35;
nodes[nodeIndustry].y = height*.75;
nodes[nodeEMBL].fixed = true;
nodes[nodeEMBL].x = width*.85;
nodes[nodeEMBL].y = height*.7;
nodes[nodePeople].fixed = true;
nodes[nodePeople].x = width*.6;
nodes[nodePeople].y = height*.2;
nodes[nodeELIXIR].fixed = true;
nodes[nodeELIXIR].x = width*.32;
nodes[nodeELIXIR].y = height*.9;
nodes[nodeRESEARCH].fixed = true;
nodes[nodeRESEARCH].x = width*.85;
nodes[nodeRESEARCH].y = height*.45;
nodes[nodeServices].fixed = true;
nodes[nodeServices].x = width*.15;
nodes[nodeServices].y = height*.55;
nodes[nodeTraining].fixed = true;
nodes[nodeTraining].x = width*.15;
nodes[nodeTraining].y = height*.3;
nodes[nodeAbout].fixed = true;
nodes[nodeAbout].x = width*.5;
nodes[nodeAbout].y = height*.35;
nodes[nodeNews].fixed = true;
nodes[nodeNews].x = width*.5;
nodes[nodeNews].y = height*.8;
nodes[nodeIntranet].fixed = true;
nodes[nodeIntranet].x = width*.75;
nodes[nodeIntranet].y = height*.1;
nodes[nodeSources].fixed = true;
nodes[nodeSources].x = width*.1;
nodes[nodeSources].y = height*.1;
nodes[nodeExits].fixed = true;
nodes[nodeExits].x = width*.9;
nodes[nodeExits].y = height*.1;
// Start the force graph
force.start();
// Add basic SVG containers and allow them to be drug about
var node = svgInner.selectAll(".node")
.data(graph.nodes)
.enter().append("g")
// .attr("d.x", 250)
// .attr("d.y", 250)
.attr("class", "node")
.call(force.drag);
// The basic D3 Force lines,
// they'll act as a little "loading" animation
var link = svgInner.selectAll(".link")
.data(graph.links)
.enter().append("line")
.style("stroke", function(d) { return color(d.target.group); })
.attr("class", function(d) { return "link-"+(d.value) * 100 })
.style("stroke-width", function(d) { return "0.1"; });
node
.append('text')
.attr("dx", function(d) { return 8 + d.radius * 1.4; })
// .attr("dx", function(d) { return ((d.radius * 2.5) + 4); })
.style("fill", function(d) { return 'rgba(255,255,255,.9)' })
.attr("charge", -500)
.attr("dy", ".35em")
.text(function(d) { return (d.name).replace(/^(.*[\\\/])/g,''); }); //Keep only the name after the trailing slash
// Render the ForceBundling visulisation
function invokeForceBundling() {
// We can't work directly from the data, and must generate a new marker ID object from the original data
var graphLinksNew = new Array(graph.links.length);
for (i=0;i<graph.links.length;i++) {
graphLinksNew[i] = { 'source' : Object.assign(graph.links[i].source.index),
'target' : Object.assign(graph.links[i].target.index)
};
}
var forceBundleInstantiation = d3.ForceEdgeBundling().step_size(0.1).nodes(nodes).edges(graphLinksNew);
var results = forceBundleInstantiation();
// Purge the old geometry
d3.selectAll("line").remove();
d3.selectAll("path").remove();
d3.selectAll("polygon").remove();
// Unfortunately, ForceBundle does not retaint the metada
// about the "value" of the line, so we
// use the target IDs to lookuo the thickness of the line
// from the original Force data
function findConnectionInfo(sourceID,targetID) {
for(var i = 0; i < graph.links.length; i++){
if (graph.links[i].source.index == sourceID && graph.links[i].target.index == targetID) {
return graph.links[i];
}
}
}
// calculate the lines
var d3line = d3.svg.line()
.x(function(d){return d.x;})
.y(function(d){return d.y;})
.interpolate("linear");
//plot the data
for (var i = 0; i < results.length; i++) {
var parentNode = results[i][results[i].length-1];
var parentConnection = findConnectionInfo(results[i][0].index,results[i][results[i].length-1].index);
// svg.insertBefore("path",svg).attr("d", d3line(results[i]))
svgInner.insert("path",':first-child').attr("d", d3line(results[i]))
.style("stroke-width", function(d) { return (parentConnection.value * 2) + .6 })
.style("stroke", function() { return 'rgba('+hexToRgb(color(nodes[parentNode.index].group))+',.85)'; })
.attr("class", function(d) { return "link-"+(parentConnection.value) * 100 })
.attr("data-parentnode", function(d) { return parentNode.index; })
.style("fill", "none");
// .style('stroke-opacity',0.4);
}
node.append('svg:polygon')
.attr('points', function(d) { return scaleMarker(d); })
.style("fill", function(d) { if (d.traffic === null) { return 'url(#diagonal-stripe-1)'; } return 'rgba(50,50,50,1)'; })
.style("stroke", function(d) { return 'rgba('+hexToRgb(color(d.group))+',.95)';})
// .attr('stroke', function(d,i) { return color(i)})
.on("mouseover", function(d){ connectionHighlight(d); return tooltip.style("visibility", "visible").html('<h4>'+d.name+'</h4>Connections: ' + d.radius + '<br/>Views: ' + d.traffic); })
.on("mousedown", function(d){ renderConnectionTable(d); svg.on('.zoom', null ); })
.on("mouseup", function(d){ zoomEnable(); })
.on("mousemove", function(){ return tooltip.style("top", (event.pageY-10)+"px").style("left",(event.pageX+10)+"px"); })
.on("mouseout", function(d){ connectionHighlightReset(d); return tooltip.style("visibility", "hidden"); });
}
// Set up zoom support
function zoomEnable() {
var zoom = d3.behavior.zoom()
.scaleExtent([1, 8])
.on("zoom", function() {
svgInner.attr("transform", "translate(" + d3.event.translate + ")" +
"scale(" + d3.event.scale + ")");
// darken the background as we zoom in or out
var backgoundRGBVal = Math.round(55 / d3.event.scale);
document.body.style.backgroundColor = 'rgb(' + backgoundRGBVal + ',' + backgoundRGBVal + ',' + backgoundRGBVal + ')';
});
svg.call(zoom);
}
zoomEnable();
// tick tick, the engine that plots the D3 Force layout
force.on("tick", tickFunction);
var tickCount = 0; // cap the number of tick runs
function tickFunction() {
tickCount++;
if (tickCount > 200) {
force.stop();
tickCount = 180; // so we can restart after a marker is moved
invokeForceBundling(); // draw the new ForceBundle data
}
// invokeForceBundling();
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("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
}
/* BEGIN
* Utility functions...
*/
// Scale the SVG marker according to it's importance
function scaleMarker (d) {
// var pointScale = ( (d.radius + 9) * Math.log(d.radius/.2 + 1) ) / 150;
// weight
// var pointScale = ( (d.radius + 9) * Math.log(d.radius/.2 + 1) ) / 150;
// traffic
if (d.traffic === null) {
var pointScale = .1;
} else {
var pointScale = Math.sqrt(Math.sqrt(d.traffic) * 1.5) / 50;
}
// pointScale = pointScale/ 10;
console.log(pointScale);
var toReturn = '';
var markerArray = '-28.1,-16.2 0,-32.4 28.1,-16.2 28.1,16.2 0,32.4 -28.1,16.2'.split(' '); //embl poloygon
for (x=0;x<markerArray.length;x++) {
var tempMarkerPoint = markerArray[x].split(',');
toReturn += tempMarkerPoint[0] * pointScale;
toReturn += ',';
toReturn += tempMarkerPoint[1] * pointScale;
toReturn += ' ';
}
return toReturn;
}
// Highlight the connected nodes on hvoer of a node
function connectionHighlight(d) {
// var tempLines = d3.selectAll("path")[0];
// d3.selectAll('path[data-parentnode="7"]').style("opacity", 0);
d3.selectAll('path').style("opacity",0);
d3.selectAll('path[data-parentnode="' + d.index + '"]').style("opacity",1);
// console.log('passed', d);
// data-parentnode
// for ( y = 0; y < tempLines.length; y++ ) {
// console.log(tempLines[y]);
// if ((tempLines[y].__data__.source.group != d.group) && (tempLines[y].__data__.target.group != d.group)) {
// d3.select(tempLines[y]).transition().style("opacity", 0);
// // tempData[y].style("strokeOpacity", function(d) { return '0'; });
// }
// }
}
// Reset the highlight on mouse out
function connectionHighlightReset(d) {
d3.selectAll('path').style("opacity",1);
// var tempLines = d3.selectAll("path")[0];
// for ( y = 0; y < tempLines.length; y++ ) {
// d3.select(tempLines[y]).transition().style("opacity", 1);
// }
}
// shade the spheres according to some metric/goal
function coloriseSphere() {
// for now, we fake it to select on of 10...
switch( Math.floor((Math.random() * 10) + 1) ) {
case 1:
return 'rgba(213, 60, 60,.8)';
case 2:
return 'rgba(213, 60, 60,.8)';
// case 3:
// return 'rgba(48, 171, 48,.8)';
default:
return 'rgba(255,255,255,.9)';
}
}
$('svg').mousedown( function(){
closeConnectionTable();
});
function renderConnectionTable(d) {
// $('.node-information').fadeIn();
var requestedContentModel = d.name.trim();
history.pushState(null, null, '#'+requestedContentModel);
// Fetch the associated core content description in a really crude fashion :P
$.get('/content_model_viewer/index.html').then(function(responseData) {
// $('.node-information').html('');
$('.node-information').html('<div class="close">X</div>' + responseData); // add a "close" button
$('.node-information .close').click( function(){
closeConnectionTable();
});
});
}
function closeConnectionTable() {
if ($('.node-information').css('opacity') == 1) {
$('.node-information').fadeOut();
}
}
function hexToRgb(hex) {
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? parseInt(result[1], 16) + "," + parseInt(result[2], 16) + "," + parseInt(result[3], 16) : null;
}
});
</script>
<!--
Handy pattern fills:
https://iros.github.io/patternfills/sample_d3.html
-->
<svg height="10" width="10" xmlns="https://www.w3.org/2000/svg" version="1.1"> <defs> <pattern id="diagonal-stripe-1" patternUnits="userSpaceOnUse" width="10" height="10"> <image xlink:href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPScxMCcgaGVpZ2h0PScxMCc+CiAgPHJlY3Qgd2lkdGg9JzEwJyBoZWlnaHQ9JzEwJyBmaWxsPSd3aGl0ZScvPgogIDxwYXRoIGQ9J00tMSwxIGwyLC0yCiAgICAgICAgICAgTTAsMTAgbDEwLC0xMAogICAgICAgICAgIE05LDExIGwyLC0yJyBzdHJva2U9J2JsYWNrJyBzdHJva2Utd2lkdGg9JzEnLz4KPC9zdmc+Cg==" x="0" y="0" width="10" height="10"> </image> </pattern> </defs> </svg>
</body>
https://code.jquery.com/jquery-1.11.3.min.js
https://d3js.org/d3.v3.min.js