an updated look at README.md
files from bl.ocks.org that contain links to other bl.ocks with data gathered in early May 2016.
click on a node to view that bl.ock
community detection done with the jLouvain library
source data and scripts used to generate the graph data are at this repository. thanks to @mbostock for removing missing nodes, solitary nodes, self-links, and redundant links. The original dataset contains 2,101 nodes
and 8,617 links
, and is 1.2 MB
. The cleaned dataset contains 1,238 nodes
and 2,602 links
, and is only 281 KB
.
this example also sets the fill-opacity
for the nodes to 0.7
so it is more apparent that clusters are in fact comprised of many small, equal-radius individual nodes.
many bl.ocks informed this example:
xxxxxxxxxx
<html>
<head>
<title>Blocks Graph - Readme Mentions</title>
<meta charset='utf-8' />
<script src='https://d3js.org/d3.v3.min.js'></script>
<script src='https://npmcdn.com/babel-core@5.8.34/browser.min.js'></script>
<script src='jsLouvain.js'></script>
</head>
<body>
<script lang='babel' type='text/babel'>
/* Make the vis fill the page using CSS. */
d3.select('body')
.style({
margin: 0,
position: 'fixed',
top: 0,
right: 0,
bottom: 0,
left: 0
});
d3.select('body').append('div')
.attr('id', 'viz')
.style({
float: 'left',
width: '70%',
position: 'fixed',
top: 0,
right: 0,
bottom: 0,
left: 0
})
d3.select('body').append('div')
.attr('id', 'detail')
.style({
float: 'right',
width: '30%',
position: 'fixed',
top: 0,
right: 0,
bottom: 0,
left: '70%'
})
// Extract the width and height that was computed by CSS.
const vizDiv = document.getElementById('viz');
const width = vizDiv.clientWidth;
const height = vizDiv.clientHeight;
const minSide = d3.min([width, height]);
const xOffset = (width - minSide) / 2;
const yOffset = (height - minSide) / 2;
const detailDiv = document.getElementById('detail');
const detailHeight = detailDiv.clientHeight;
const detailWidth = detailDiv.clientWidth;
const detailSVG = d3.select(detailDiv)
.append('svg')
.attr('width', detailDiv.clientWidth)
.attr('height', detailDiv.clientHeight);
let cardsOnPage = [];
let cardsDisplayed = [];
const nodeHash = {};
// Use the extracted size to set the size of a Canvas element
d3.select(vizDiv).append('canvas')
.attr('width', width)
.attr('height', height)
.style('position', 'absolute');
// Use the extracted size to set the size of a SVG element
d3.select(vizDiv).append('svg')
.attr('width', width)
.attr('height', height)
.classed('main', true)
.style('position', 'absolute');
d3.json('graph.json', function (error, data) {
createNetwork(error, data);
});
// remove thumbnails url data loading for now
// queue()
// .defer(d3.json, 'readme-blocks-graph.json')
// .defer(d3.json, 'thumbnailsHash.json')
// .await((error, data1, data2) => createNetwork(error, data1, data2));
function onlyUnique(value, index, self) {
return self.indexOf(value) === index;
}
function createNetwork(error, graph) {
console.log('graph', graph);
const nodes = [];
const edges = [];
const edgelist = graph.links;
const nodelist = graph.nodes;
edgelist.forEach(edge => {
if (!nodeHash[edge.source]) {
nodeHash[edge.source] = {
id: edge.source,
label: edge.source
};
nodes.push(nodeHash[edge.source]);
}
if (!nodeHash[edge.target]) {
nodeHash[edge.target] = {
id: edge.target,
label: edge.target
};
nodes.push(nodeHash[edge.target]);
}
if (edge /* .weight == 5 */) {
edges.push({
id: `${nodeHash[edge.source].id}-${nodeHash[edge.target].id}`,
source: nodeHash[edge.source],
target: nodeHash[edge.target],
weight: 1 /* edge.weight */
});
}
});
// get some attributes from the nodelist (calculated elsewhere)
// and store them in the nodeHash
nodelist.forEach(node => {
if (nodeHash[node.id]) {
nodeHash[node.id].user = node.user;
nodeHash[node.id].createdAt = node.createdAt;
nodeHash[node.id].updatedAt = node.updatedAt;
nodeHash[node.id].description = node.description;
}
});
// take the attributes now in the nodeHash
// and hang them on the nodes (calculated here from the edgelist)
nodes.forEach(node => {
if (nodeHash[node.id]) {
node.user = nodeHash[node.id].user;
node.createdAt = nodeHash[node.id].createdAt;
node.updatedAt = nodeHash[node.id].updatedAt;
node.description = nodeHash[node.id].description;
}
});
console.log('nodes', nodes);
console.log('edges', edges);
console.log('nodeHash', nodeHash);
createForceNetwork(nodes, edges);
}
function modularityCensus(nodes, edges /* , moduleHash */) {
edges.forEach(edge => {
if (edge.source.module !== edge.target.module) {
edge.border = true;
} else {
edge.border = false;
}
});
nodes.forEach(node => {
const theseEdges = edges.filter(d => d.source === node || d.target === node);
const theseSourceModules = theseEdges.map(d => d.source.module).filter(onlyUnique);
const theseTargetModules = theseEdges.map(d => d.target.module).filter(onlyUnique);
if (theseSourceModules.length > 1 || theseTargetModules.length > 1) {
node.border = true;
} else {
node.border = false;
}
});
}
function createForceNetwork(nodes, edges) {
// create a network from an edgelist
// var colors = d3.scale.ordinal().domain([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]).range(['#996666', '#66CCCC', '#FFFF99', '#CC9999', '#666633', '#993300', '#999966', '#660000', '#996699', '#cc6633', '#ff9966', '#339999', '#6699cc', '#ffcc66', '#ff6600', '#00ccccc']);
const colors = d3.scale.category20();
const nodeData = nodes.map(d => d.id);
const edgeData = edges.map(function (d) {
return {
source: d.source.id,
target: d.target.id,
weight: 1
};
});
console.log('nodeData for jLouvain', nodeData, 'nodes', nodeData.length);
console.log('edgeData for jLouvain', edgeData, 'edges', edgeData.length);
const community = jLouvain().nodes(nodeData).edges(edgeData);
const result = community();
console.log('jLouvain result', result);
nodes.forEach(node => {
node.module = result[node.id];
// console.log('node.module', node.module)
});
modularityCensus(nodes, edges, result);
const force = d3.layout.force().nodes(nodes).links(edges)
.size([minSide, minSide]) // make a square // minSide, minSide
.charge(-100)
.chargeDistance(100)
.linkStrength(2)
.linkDistance(1)
.gravity(0.07);
const nodeEnter = d3.select('svg.main').selectAll('g.node')
.data(nodes, d => d.id)
.enter();
nodeEnter
.append('a')
.attr('xlink:href', d => `https://bl.ocks.org/${d.user}/${d.id}`)
.attr('target', '_blank')
.attr('id', d => d.id)
// .attr('target', '_blank')
.append('circle')
.attr('r', 3)
.attr('class', 'foreground')
.style('fill', d => colors(d.module))
.style('fill-opacity', 0.7)
.style('stroke-width', d => {
if (d.border) return '3px';
return '1px';
})
.on('mouseover', d => nodeMouseover(d))
.on('mouseout', () => nodeMouseout());
/*
// draw labels over nodes
nodeEnter.append('text')
.style('text-anchor', 'middle')
.attr('y', 3)
.style('stroke-width', '1px')
.style('stroke-opacity', 0.75)
.style('stroke', 'white')
.style('font-size', '8px')
.text(function (d) {return d.id})
.style('pointer-events', 'none')
nodeEnter.append('text')
.style('text-anchor', 'middle')
.attr('y', 3)
.style('font-size', '8px')
.text(function (d) {return d.id})
.style('pointer-events', 'none')
*/
force.start();
for (let i = 0; i < 200; i++) {
force.tick();
}
// draw the network at each tick
// force.on('tick', updateNetwork);
// after 200 iterations, we say the network
// has stabilized and we stop the
// force layout physics simulation
force.stop();
// now we draw the network
updateNetwork(edges);
// drawVoronoiOverlay();
function updateNetwork(edgesToUpdate) {
// // draw the links
// d3.select('svg.main').selectAll('line')
// .attr('x1', d => d.source.x)
// .attr('y1', d => d.source.y)
// .attr('x2', d => d.target.x)
// .attr('y2', d => d.target.y)
// .attr('transform', 'translate(' + xOffset + ',' + yOffset +')');
const context = d3.select('canvas').node().getContext('2d');
context.clearRect(0, 0, width, height);
context.translate(xOffset, yOffset);
// draw links (or edges, if you prefer)
context.strokeStyle = 'rgba(0, 0, 0, 0.5)';
context.beginPath();
edgesToUpdate.forEach(d => {
context.moveTo(d.source.x, d.source.y);
context.lineTo(d.target.x, d.target.y);
});
context.stroke();
// draw the nodes
d3.select('svg.main').selectAll('circle')
.attr('cx', d => d.x)
.attr('cy', d => d.y)
.attr('transform', `translate(${xOffset}, ${yOffset})`);
}
function drawVoronoiOverlay() {
const rawPoints = [];
nodes.forEach(d => {
rawPoints.push({
x: d.x,
y: d.y,
id: d.id,
user: d.user,
description: d.description
});
});
const voronoi = d3.geom.voronoi();
voronoi
.x(d => d.x)
.y(d => d.y)
.clipExtent([[-10 - xOffset, -10 - yOffset], [width + 10, height + 10]]);
voronoiData = voronoi(rawPoints);
voronoiData = voronoiData.map(function (d, i) {
return {
coordinates: d,
data: rawPoints[i]
};
});
console.log('voronoiData', voronoiData);
const voronoiPaths = d3.select('svg.main').selectAll('path.voronoi')
.data(voronoiData)
.enter()
.insert('path')
.attr('class', 'voronoi')
.style('stroke', 'none')
.style('stroke-width', '1px')
.style('fill', 'white')
.style('fill-opacity', 0)
.attr('d', d => `M${d.coordinates.join('L')}Z`)
.attr('transform', `translate(${xOffset}, ${yOffset})`);
voronoiPaths
.on('mouseover', d => nodeMouseover(d))
.on('click', d => nodeClick(d))
.on('mouseout', () => nodeMouseout());
}
function nodeMouseover(d) {
console.log('nodeHash', nodeHash);
// allow nodeMousever to be called in different contexts
// either from the node circle or from the Voronoi path
let id;
let user;
let description;
const dH = 30;
if (typeof d.data === 'undefined') {
id = d.id;
user = nodeHash[d.id].user;
description = nodeHash[d.id].description;
}
else {
id = d.data.id;
user = d.data.user;
d.data.description;
}
const blockUrl = `https://bl.ocks.org/${user}/${id}`
// generate nice text to display on the small canvas below the thumbnail image
let blockTitleText;
if (
['undefined', 'null'].indexOf(String(user)) === -1 &&
['undefined', 'null'].indexOf(String(description)) === -1
) {
blockTitleText = `${user} | ${description}`
}
if (
['undefined', 'null'].indexOf(String(user)) === -1 &&
['undefined', 'null'].indexOf(String(description)) > -1
) {
blockTitleText = user;
}
if (
['undefined', 'null'].indexOf(String(user)) > -1 &&
['undefined', 'null'].indexOf(String(description)) === -1
) {
blockTitleText = description;
}
if (
['undefined', 'null'].indexOf(String(user)) > -1 &&
['undefined', 'null'].indexOf(String(description)) > -1
) {
blockTitleText = '';
}
console.log(blockUrl);
console.log(blockTitleText);
// const cardDiv = d3.select(detailDiv).append('div')
// .attr('id', `card${id}`)
// .append('a')
// .attr('xlink:href', d => `https://bl.ocks.org/${user}/${id}`)
// .innerHTML(blockTitleText);
//if (cardsDisplayed.length > 10) {
// d3.selectAll('.cards')
// .attr('transform', `translate(0,${15 * (cardsOnPage.length-1)})`);
//}
// draw a rectangle
const detailG = detailSVG.append('a')
.attr('xlink:href', blockUrl)
.attr('target', '_blank')
.attr('id', `card${id}`)
.classed('cards', true)
.append('rect')
//.classed('cards', true)
.attr('id', `card${id}`)
.attr('x', 0)
.attr('y', 5 + dH * cardsOnPage.length)
.attr('height', dH)
.attr('width', detailDiv.clientWidth)
.style('stroke-width', 1)
.style('stroke', 'white')
.style('fill', 'lightgray')
.style('fill-opacity', .4)
.attr('rx', 4)
.attr('ry', 4);
// draw text on the screen
detailSVG.append('text')
.attr('x', 10)
.attr('y', 5 + dH * cardsOnPage.length)
.classed('cards', true)
.attr('id', `card${id}`)
.style('fill', 'black')
.style('font-size', '16px')
.attr('dy', '20px')
.attr('text-anchor', 'start')
.style('pointer-events', 'none')
.text(blockTitleText);
console.log('cardsOnPage', cardsOnPage);
console.log('cardsOnPage.length', cardsOnPage.length);
if (false/*cardsDisplayed.length > 10*/) {
const lastCardId = cardsOnPage[cardsOnPage.length - 1];
const firstCardId = cardsOnPage[0];
const cardSelector = `div#card${firstCardId}`.replace(/[,.]/g, '');
d3.select(cardSelector)
.remove();
// d3.selectAll('.cards')
// .attr('transform', `translate(0,-15)`);
//cardsOnPage.pop();
cardsOnPage.shift();
//if(cardsOnPage.indexOf(id) > -1) cardsOnPage = removeByValue(cardsOnPage, id);
}
cardsOnPage.push(id);
cardsDisplayed.push(id);
d3.select(vizDiv).select('svg').append('text')
.attr('x', 100)
.attr('y', 100)
.classed('titleText', true)
.style('fill', 'black')
.style('fill-opacity', 0.8)
.style('font-size', '30px')
.style('font-weight', 600)
.attr('dy', '0px')
.attr('text-anchor', 'start')
.style('pointer-events', 'none')
.text(blockTitleText);
}
function nodeMouseout() {
d3.selectAll('.titleText')
.remove();
}
// https://stackoverflow.com/a/11223909/1732222
function ImageExist(url)
{
var img = new Image();
img.src = url;
return img.height != 0;
}
// https://stackoverflow.com/a/3955096/1732222
function removeByValue(arr) {
var what, a = arguments, L = a.length, ax;
while (L > 1 && arr.length) {
what = a[--L];
while ((ax= arr.indexOf(what)) !== -1) {
arr.splice(ax, 1);
}
}
return arr;
}
function nodeClick(d) {
const user = d.data.user;
const id = d.data.id;
const blockUrl = `https://bl.ocks.org/${user}/${id}`
openInNewTab(blockUrl);
}
// https://stackoverflow.com/a/11384018/1732222
function openInNewTab(url) {
const win = window.open(url, '_blank');
win.focus();
}
}
</script>
</body>
</html>
https://d3js.org/d3.v3.min.js
https://npmcdn.com/babel-core@5.8.34/browser.min.js