# quaintitative

I write about my quantitative explorations in visualisation, data science, machine and deep learning here, as well as other random musings.

For more about me and my other interests, visit playgrd or socials below

## Growing a network garden in D3

Network analysis is a huge field of study in and of itself, but if we just need a quick easy way to visualise relationships, we can use D3 to quickly create a network diagram.

Other than a boring mess of lines and circles, I’ve used the ‘stroke-dasharray’ style property to create a network diagram that resembles a garden.

First we set up the constants.

// Color palette

var raspberry = '#8A1C59';
var magenta = '#FC60A8';
var bluejeans = '#5ECAE9';
var powderblue = '#BEE9E8';

var flowercol1 = '#F815CD';
var flowercol2 = '#EB0A9F';
var flowercol3 = '#BA00C9';

var margin = 0;
var width = 800 - 2*margin, height = 800 - 2*margin;
var selectedarea;

var alphaCoeff = 0.1

var textSizeOne = '22px';
var textSizeTwo = '12px';


Most of these should be familiar to you if you have looked at my previous posts, perhaps with the exception of alphaCoeff. This is just an input to a D3 function which determines the dampening for the network chart to settle down.

And then we run through the usual steps - creating the svg, textbook, etc.

var svg = d3.select('#d3canvas')
.append('svg')
.attr('width', width + 2*margin)
.attr('height', height + 2*margin)
var g = svg.append('g')
.attr('transform', 'translate(' + margin + ',' + margin + ')');

// Background color for the svg using a rect
g.append('rect')
.attr('width', width + 2*margin)
.attr('height', height + 2*margin)
.attr('fill', powderblue);

// Title text
var textBox = d3.select('#d3text')
.append('p');
textBox
.append('h1')
.attr('class', 'descriptor')
.attr('x', 0)
.attr('y', 0)
.text('network garden')
.style('color', magenta)
.style('opacity', 1.0)
.style("font-family", "sans-serif")
.style('font-weight', 'bold')
.style("font-size", "14px")
.style('text-anchor', 'middle');


We add in an event handler for mouse scroll events to allow the viewer to zoom in; and also a reset function to reset the view.

//add zoom capabilities
var zoom_handler = d3.zoom()
.on("zoom", zoom_actions);

zoom_handler(svg);

function zoom_actions(){
g.attr("transform", d3.event.transform)
}

d3.select("#reset")
.on("click", resetted);

function resetted() {
svg.transition()
.duration(750)
.call(zoom_handler.transform, d3.zoomIdentity);
}


Now, we read and munge the data.

d3.csv('/data/network.csv', function(error, links){
if (error) throw error;

// To set colors and size based on tags
}
})
}
})
var nodes = {};

// link source or target will equal NodeOne or NodeTwo if it exists
// OR it will create and assign a new node
// We also add attributes to NodeTwo

});

var nodes = d3.values(nodes); // Returns an array of nodes - i.e. change nodes from an object to an array


And with that, we can move on to the meat of the code that draws the network/force diagram to the screen.

function drawChart(nodes, links, area) {

// D3 simulation
var repelForce = d3.forceManyBody().strength(-200).distanceMax(500).distanceMin(100);

var simulation = d3.forceSimulation()
.force('charge', d3.forceManyBody())
.force('repel', repelForce)
.force('center', d3.forceCenter(width/2, height/2))

simulation.alphaTarget(alphaCoeff); // dampening for the force chart to settle down


Next we set up the nodes, links and text.

var nodetext = g.append('g')
.attr('class', 'nodestext')
.selectAll('circle')
.data(nodes)
.enter()
.append('text')
.attr('class', 'nodestext-text')
.text(function(d){return d.name;})
.style("font-size", textSizeOne)
.attr('font-weight', 'bold')
.attr('fill-opacity', 0.5)
.style('pointer-events', 'none')
.style('text-anchor', 'middle')
.on('click', connectedNodes)
// .on('mouseout', connectedNodes);

var node = g.append('g')
.attr('class', 'nodes')
.selectAll('circle')
.data(nodes)
.enter()
.append('circle')
.attr('fill', bluejeans)
.attr('fill-opacity', 0.01)
.attr('stroke', function(d){
return d.col;
})
.attr('stroke-width', function(d){
return 20*d.sizeMul;
})
.attr('stroke-opacity', 0.5)
.style('stroke-dasharray', '1,3')
.style('cursor', 'pointer')
.on('click', connectedNodes)
.on("touchstart", connectedNodes)
.call(d3.drag()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended))
// .on('mouseout', connectedNodes);

.selectAll('line')
.enter()
.append('line')
.attr('stroke', raspberry)
.attr('stroke-opacity', 0.5)
.attr('stroke-width', '2px')
.style('stroke-dasharray', '10');


Now, for what really makes this tick. We first define a tick function to update the positions of the nodes and links at each frame, and then call the force simulation. These functions working together are responsible for the network/force diagram moving around and settling down to their rightful locations on the screen.

And we also add in more functions to drag the nodes, as well and select and deselect nodes and their links.

// Function for dragging the nodes
function dragstarted(d){
if (!d3.event.active) simulation.alphaTarget(alphaCoeff).restart();
d.fx = d.x;
d.dy = d.y;
}

function dragged(d){
d.fx = d3.event.x;
d.fy = d3.event.y;
}

function dragended(d){
if (!d3.event.active) simulation.alphaTarget(alphaCoeff);
d.fx = null;
d.fy = null;
}

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

var toggle = 0;

function connectedNodes(){
// The tracker array is used for saving the positions of all the nodes that are connected in the current selection
tracker = [];
//  Toggle is needed here as we are using the same function for mouseover and mouseout

if (toggle==0){
d = d3.select(this).node().__data__;

node
.transition()
.duration(200)
.ease(d3.easeCubic)
.style('fill-opacity', function(o){
return d.index==o.index ? 0.2 : 0.05;
})
.style('stroke-opacity', function(o){
return d.index==o.index ? 0.2 : 0.05;
});

nodetext
.transition()
.duration(200)
.ease(d3.easeCubic)
.style('font-style', function(o){
return d.index==o.index ? 'bold' : 'normal';
})
.style('fill', function(o){
return d.index==o.index ? spacecadet : 'grey';
});

.transition()
.duration(200)
.ease(d3.easeCubic)
.style('opacity', function(o){
// If the index of d is equal to either the index of the source or target of the link
return d.index==o.source.index | d.index==o.target.index ? 0.8 : 0.5;
})
.style('stroke', function(o){
return d.index==o.source.index | d.index==o.target.index ? spacecadet : bluejeans;
})
.style('stroke-width', function(o){
return d.index==o.source.index | d.index==o.target.index ? '2px' : '0.5px';
});

toggle = 1;
} else {

// Note: Need to use styles instead of attr here for the change to take effect, probably because we used this to access the style
nodetext.transition().duration(300)
.style("font-size", textSizeOne)
.style('font-weight', 'bold')
.style('fill-opacity', 0.5)
.style('pointer-events', 'none')
.style('text-anchor', 'middle');

node.transition().duration(300)
.style('fill', bluejeans)
.style('fill-opacity', 0.01)
.style('stroke', magenta)
.style('stroke-width', '20')
.style('stroke-opacity', 0.5)
.style('stroke-dasharray', '1,3');

.style('stroke', raspberry)
.style('stroke-opacity', 0.5)
.style('stroke-width', '2px')
.style('stroke-dasharray', '10');

simulation.alphaTarget(0.05);
toggle = 0;
}
}


And that’s it.

The actual network/force diagram can be found here; and the code here.