Published
Edited
Mar 22, 2020
1 fork
Importers
11 stars
Insert cell
Insert cell
md`A [group-in-a-box layout](http://hcil2.cs.umd.edu/trs/2011-24/2011-24.pdf) showing interactions between Les Miserables characters, where the characters are grouped by their communities (determined using the Louvain modularity community detection algorithm). Within each group, nodes are positioned using a [force-directed layout](https://bl.ocks.org/rpgove/1eb246918cd9d80fd84708fef0432128).

In contrast with a [pure force-directed layout](https://bl.ocks.org/rpgove/1eb246918cd9d80fd84708fef0432128), group-in-a-box layouts can clearly show connections within and between clusters. [Matrix diagrams](https://bl.ocks.org/rpgove/bf44631829eaa6512518005697649cb4) can also be used for this purpose, but group-in-a-box can be more effective for showing connections within and between categories of nodes that do not form clear clusters.`
Insert cell
chart = {
var width = 600;
var height = 300;
var nodeRadius = d3.scaleSqrt().range([4, 10]);
var linkWidth = d3.scaleLinear().range([1, 2 * nodeRadius.range()[0]]);
var drag = d3
.drag()
.on('start', dragStart)
.on('drag', dragging)
.on('end', dragEnd);

var svgDOM = d3
.select(DOM.svg(width, height))
.attr('class', 'groupbox')
.attr('width', "100%")
.attr('height', "auto");

var svg = svgDOM.append('g');
// .attr('transform', 'translate(1,1)');

var groupingForce = forceInABox()
.strength(0.1)
.template('treemap')
.groupBy('community')
.size([width, height]);
var forceSim = d3
.forceSimulation()
.force(
'link',
d3
.forceLink()
.id(function(d) {
return d.id;
})
.distance(50)
.strength(groupingForce.getLinkStrength)
)
.force('group', groupingForce)
.force('charge', d3.forceManyBody())
.force('center', d3.forceCenter(width / 2, height / 2))
.force('x', d3.forceX(width / 2).strength(0.02))
.force('y', d3.forceY(height / 2).strength(0.04));

// Make sure small nodes are drawn on top of larger nodes
data.nodes.sort(function(a, b) {
return b.chapters.length - a.chapters.length;
});
nodeRadius.domain([
data.nodes[data.nodes.length - 1].chapters.length,
data.nodes[0].chapters.length
]);
linkWidth.domain(
d3.extent(data.links, function(d) {
return d.chapters.length;
})
);
forceSim.nodes(data.nodes).on('tick', tick);
forceSim.force('link').links(data.links);
groupingForce.links(data.links).drawTreemap(svg);
var link = svg
.append('g')
.attr('class', 'links')
.selectAll('line')
.data(data.links)
.enter()
.append('line')
.attr('stroke-width', function(d) {
return linkWidth(d.chapters.length);
});
var node = svg
.append('g')
.attr('class', 'nodes')
.selectAll('circle')
.data(data.nodes)
.enter()
.append('circle')
.attr('r', function(d) {
return nodeRadius(d.chapters.length);
})
.call(drag);
node.append('title').text(function(d) {
return d.name;
});
function tick() {
link
.attr('x1', function(d) {
return d.source.x;
})
.attr('x2', function(d) {
return d.target.x;
})
.attr('y1', function(d) {
return d.source.y;
})
.attr('y2', function(d) {
return d.target.y;
});
node
.attr('cx', function(d) {
return d.x;
})
.attr('cy', function(d) {
return d.y;
});
}

function dragStart(d) {
if (!d3.event.active) forceSim.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragging(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragEnd(d) {
if (!d3.event.active) forceSim.alphaTarget(0);
d.fx = null;
d.fy = null;
}
return svgDOM.node();
}
Insert cell
html`
<style>
svg.groupbox rect.cell {
fill: none;
stroke: #ddd;
stroke-width: 2px;
}
svg.groupbox .links line {
stroke: #999;
stroke-opacity: 0.6;
}
svg.groupbox .nodes circle {
fill: #23d6af;
stroke: #fff;
stroke-width: 0.2px;
}
</style>
`
Insert cell
data = d3.json("https://gist.githubusercontent.com/cbuie/9a9e93805a6d8f403e2ba99289dbd0c2/raw/41a170544b8a1350cbb78389ef7437a8058c5209/lesMiserablesCharacters")
Insert cell
function forceInABox(alpha) {
function index(d) {
return d.index;
}

var id = index,
nodes,
links, //needed for the force version
tree,
size = [100,100],
nodeSize = 1, // The expected node size used for computing the cluster node
forceCharge = -2,
foci = {},
// oldStart = force.start,
linkStrengthIntraCluster = 0.1,
linkStrengthInterCluster = 0.01,
// oldGravity = force.gravity(),
templateNodes = [],
offset = [0,0],
templateForce,
templateNodesSel,
groupBy = function (d) { return d.cluster; },
template = "treemap",
enableGrouping = true,
strength = 0.1;
// showingTemplate = false;


function force(alpha) {
if (!enableGrouping) {
return force;
}
if (template==="force") {
//Do the tick of the template force and get the new focis
templateForce.tick();
getFocisFromTemplate();
}

for (var i = 0, n = nodes.length, node, k = alpha * strength; i < n; ++i) {
node = nodes[i];
node.vx += (foci[groupBy(node)].x - node.x) * k;
node.vy += (foci[groupBy(node)].y - node.y) * k;
}

}

function initialize() {
if (!nodes) return;

// var i,
// n = nodes.length,
// m = links.length,
// nodeById = map(nodes, id),
// link;

if (template==="treemap") {
initializeWithTreemap();
} else {
initializeWithForce();
}


}

force.initialize = function(_) {
nodes = _;
initialize();
};

function getLinkKey(l) {
var sourceID = groupBy(l.source),
targetID = groupBy(l.target);

return sourceID <= targetID ?
sourceID + "~" + targetID :
targetID + "~" + sourceID;
}

function computeClustersNodeCounts(nodes) {
var clustersCounts = d3.map();

nodes.forEach(function (d) {
if (!clustersCounts.has(groupBy(d))) {
clustersCounts.set(groupBy(d), 0);
}
});

nodes.forEach(function (d) {
// if (!d.show) { return; }
clustersCounts.set(groupBy(d), clustersCounts.get(groupBy(d)) + 1);
});

return clustersCounts;
}

//Returns
function computeClustersLinkCounts(links) {
var dClusterLinks = d3.map(),
clusterLinks = [];
links.forEach(function (l) {
var key = getLinkKey(l), count;
if (dClusterLinks.has(key)) {
count = dClusterLinks.get(key);
} else {
count = 0;
}
count += 1;
dClusterLinks.set(key, count);
});

dClusterLinks.entries().forEach(function (d) {
var source, target;
source = d.key.split("~")[0];
target = d.key.split("~")[1];
clusterLinks.push({
"source":source,
"target":target,
"count":d.value,
});
});
return clusterLinks;
}

//Returns the metagraph of the clusters
function getGroupsGraph() {
var gnodes = [],
glinks = [],
// edges = [],
dNodes = d3.map(),
// totalSize = 0,
clustersList,
c, i, size,
clustersCounts,
clustersLinks;

clustersCounts = computeClustersNodeCounts(nodes);
clustersLinks = computeClustersLinkCounts(links);

//map.keys() is really slow, it's crucial to have it outside the loop
clustersList = clustersCounts.keys();
for (i = 0; i< clustersList.length ; i+=1) {
c = clustersList[i];
size = clustersCounts.get(c);
gnodes.push({id : c, size :size });
dNodes.set(c, i);
// totalSize += size;
}

clustersLinks.forEach(function (l) {
glinks.push({
"source":dNodes.get(l.source),
"target":dNodes.get(l.target),
"count":l.count
});
});


return {nodes: gnodes, links: glinks};
}


function getGroupsTree() {
var children = [],
totalSize = 0,
clustersList,
c, i, size, clustersCounts;

clustersCounts = computeClustersNodeCounts(force.nodes());

//map.keys() is really slow, it's crucial to have it outside the loop
clustersList = clustersCounts.keys();
for (i = 0; i< clustersList.length ; i+=1) {
c = clustersList[i];
size = clustersCounts.get(c);
children.push({id : c, size :size });
totalSize += size;
}
// return {id: "clustersTree", size: totalSize, children : children};
return {id: "clustersTree", children : children};
}


function getFocisFromTemplate() {
//compute foci
foci.none = {x : 0, y : 0};
templateNodes.forEach(function (d) {
if (template==="treemap") {
foci[d.data.id] = {
x : (d.x0 + (d.x1-d.x0) / 2) - offset[0],
y : (d.y0 + (d.y1-d.y0) / 2) - offset[1]
};
} else {
foci[d.id] = {x : d.x - offset[0] , y : d.y - offset[1]};
}
});
}
function initializeWithTreemap() {
var treemap = d3.treemap()
.size(force.size());

tree = d3.hierarchy(getGroupsTree())
// .sort(function (p, q) { return d3.ascending(p.size, q.size); })
// .count()
.sum(function (d) { return d.size; })
.sort(function(a, b) {
return b.height - a.height || b.value - a.value; })
;


templateNodes = treemap(tree).leaves();

getFocisFromTemplate();
}

function checkLinksAsObjects() {
// Check if links come in the format of indexes instead of objects
var linkCount = 0;
if (nodes.length===0) return;

links.forEach(function (link) {
var source, target;
if (!nodes) return;
source = link.source;
target = link.target;
if (typeof link.source !== "object") source = nodes[link.source];
if (typeof link.target !== "object") target = nodes[link.target];
if (source === undefined || target === undefined) {
console.log(link);
throw Error("Error setting links, couldn't find nodes for a link (see it on the console)" );
}
link.source = source; link.target = target;
link.index = linkCount++;
});
}

function initializeWithForce() {
var net;

if (nodes && nodes.length>0) {
if (groupBy(nodes[0])===undefined) {
throw Error("Couldn't find the grouping attribute for the nodes. Make sure to set it up with forceInABox.groupBy('attr') before calling .links()");
}
}

checkLinksAsObjects();

net = getGroupsGraph();
templateForce = d3.forceSimulation(net.nodes)
.force("x", d3.forceX(size[0]/2).strength(0.5))
.force("y", d3.forceY(size[1]/2).strength(0.5))
.force("collide", d3.forceCollide(function (d) { return d.size*nodeSize; }))
.force("charge", d3.forceManyBody().strength(function (d) { return forceCharge * d.size; }))
.force("links", d3.forceLink(!net.nodes ? net.links :[]))

templateNodes = templateForce.nodes();

getFocisFromTemplate();
}


function drawTreemap(container) {
container.selectAll(".cell").remove();
container.selectAll("cell")
.data(templateNodes)
.enter().append("svg:rect")
.attr("class", "cell")
.attr("x", function (d) { return d.x0; })
.attr("y", function (d) { return d.y0; })
.attr("width", function (d) { return d.x1-d.x0; })
.attr("height", function (d) { return d.y1-d.y0; });

}

function drawGraph(container) {
container.selectAll(".cell").remove();
templateNodesSel = container.selectAll("cell")
.data(templateNodes);
templateNodesSel
.enter().append("svg:circle")
.attr("class", "cell")
.attr("cx", function (d) { return d.x; })
.attr("cy", function (d) { return d.y; })
.attr("r", function (d) { return d.size*nodeSize; });

}

force.drawTemplate = function (container) {
// showingTemplate = true;
if (template === "treemap") {
drawTreemap(container);
} else {
drawGraph(container);
}
return force;
};

//Backwards compatibility
force.drawTreemap = force.drawTemplate;

force.deleteTemplate = function (container) {
// showingTemplate = false;
container.selectAll(".cell").remove();

return force;
};


force.template = function (x) {
if (!arguments.length) return template;
template = x;
initialize();
return force;
};

force.groupBy = function (x) {
if (!arguments.length) return groupBy;
if (typeof x === "string") {
groupBy = function (d) {return d[x]; };
return force;
}
groupBy = x;
return force;
};


force.enableGrouping = function (x) {
if (!arguments.length) return enableGrouping;
enableGrouping = x;
// update();
return force;
};

force.strength = function (x) {
if (!arguments.length) return strength;
strength = x;
return force;
};


force.getLinkStrength = function (e) {
if(enableGrouping) {
if (groupBy(e.source) === groupBy(e.target)) {
if (typeof(linkStrengthIntraCluster)==="function") {
return linkStrengthIntraCluster(e);
} else {
return linkStrengthIntraCluster;
}
} else {
if (typeof(linkStrengthInterCluster)==="function") {
return linkStrengthInterCluster(e);
} else {
return linkStrengthInterCluster;
}
}
} else {
// Not grouping return the intracluster
if (typeof(linkStrengthIntraCluster)==="function") {
return linkStrengthIntraCluster(e);
} else {
return linkStrengthIntraCluster;
}

}
};


force.id = function(_) {
return arguments.length ? (id = _, force) : id;
};

force.size = function(_) {
return arguments.length ? (size = _, force) : size;
};

force.linkStrengthInterCluster = function(_) {
return arguments.length ? (linkStrengthInterCluster = _, force) : linkStrengthInterCluster;
};

force.linkStrengthIntraCluster = function(_) {
return arguments.length ? (linkStrengthIntraCluster = _, force) : linkStrengthIntraCluster;
};

force.nodes = function(_) {
return arguments.length ? (nodes = _, force) : nodes;
};

force.links = function(_) {
if (!arguments.length)
return links;
if (_ === null) links = [];
else links = _;
return force;
};

force.nodeSize = function(_) {
return arguments.length ? (nodeSize = _, force) : nodeSize;
};

force.forceCharge = function(_) {
return arguments.length ? (forceCharge = _, force) : forceCharge;
};

force.offset = function(_) {
return arguments.length ? (offset = _, force) : offset;
};

return force;
}
Insert cell
d3 = require("https://d3js.org/d3.v5.min.js")
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more