Skip to content

Instantly share code, notes, and snippets.

@markiaaan
Last active September 18, 2020 08:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save markiaaan/285e5940a3ab07a8baf306cc562bba40 to your computer and use it in GitHub Desktop.
Save markiaaan/285e5940a3ab07a8baf306cc562bba40 to your computer and use it in GitHub Desktop.
Graph to Timeline
license: mit
scrolling: no
border: yes

Experiment created in the context of the project SoNAR(IDH) by Mark-Jan Bludau(@markiaaan):

The visualization lets you switch between a graph and a timeline layout.In the timeline nodes afterwards are ordered by communities and date. As an example I used the Les Miserables dataset and randomly added values for dateStart & dateEnd to be exemplary used in the timeline layout along the x-axis. Nodes are colored by communities, detected by netClustering.js which uses the Clauset, Newman and Moore community detection algorithm, implemented by Robin W. Spencer and wrapped up by John A Guerra Gómez.

<!-- Experiment created in the context of the project SoNAR(IDH) (sonar.fh-potsdam.de) by Mark-Jan BLudau (@markiaaan)-->
<html>
<head>
<title>Graph2Timeline</title>
<meta charset="utf-8">
<style>
body {
font-family: sans-serif;
background-color: rgb(10, 3, 36);
color: white;
text-align: center;
overflow: hidden;
}
h1 {
position: absolute;
font-weight: lighter;
font-size: 30px;
letter-spacing: 5px;
margin-top: 20px;
margin-bottom: 5px;
margin-left: 20px;
text-align: left;
text-transform: uppercase;
pointer-events: none;
z-index: 20;
}
a:link {
color: white;
text-decoration: underline;
}
a:visited {
color: white;
text-decoration: underline;
}
a:hover {
color: white;
text-decoration: none;
}
a:active {
color: white;
text-decoration: underline;
}
#buttons {
position: absolute;
margin-top: 70px;
margin-left: 20px;
z-index: 500;
width: 250px;
text-align: left;
pointer-events: none;
}
.switchButton {
pointer-events: all;
}
button {
background-color: white;
border: none;
color: black;
padding: 5px 5px;
text-align: center;
text-decoration: none;
display: inline-block;
}
#details {
background-color: rgba(50, 50, 50, 0.63);
margin: 0px;
font-size: 0.8em;
position: fixed;
overflow-y: auto;
top: 0;
right: 0;
width: 400px;
height: 100%;
display: none;
text-align: left;
padding: 20px;
z-index: 10;
}
#graph {
position: fixed;
left: 0;
top: 0;
height: 100%;
width: 100%;
cursor: move;
z-index: 1;
}
#closedetails {
position: fixed;
right: 10;
top: 0;
font-size: 30;
color: white;
cursor: pointer;
display: none;
z-index: 30;
}
.detail_key {
font-weight: normal;
color: grey;
margin-bottom: 0;
}
.detail_value {
margin-top: 0;
font-weight: bold;
color: white;
}
.relations_value {
margin-top: 0;
margin-bottom: 5;
font-weight: bold;
}
</style>
<script src="https://d3js.org/d3.v5.min.js"></script>
<!-- netClustering.js (https://github.com/john-guerra/netClusteringJs) allows you to detect clusters in networks using the Clauset, Newman and Moore community detection algorithm directly from the browser -->
<script src="https://unpkg.com/netclustering@0.0.3/dist/netClustering.js"></script>
</head>
<body>
<svg id="graph"></svg>
<div id="details"></div>
<p id="closedetails">✕</p>
<h1>SONAR–Graph2Timeline</h1>
<div id="buttons">
<button class="switchButton" onclick="morphToTimeline();">timeline</button>
<button class="switchButton" onclick="morphToGraph();">graph</button>
</div>
<script type="text/javascript">
const margin = 20
let windowWidth = window.innerWidth - 2 * margin
let windowHeight = window.innerHeight - 2 * margin
let graph
let detailview = false;
///scale for the nodes
const nodeSize = d3.scaleLinear()
.domain([1, 40])
.range([5, 18]);
///colors for potential varying edge categories
const edgeColors = d3.scaleOrdinal()
.domain(["a", "b", "c"])
.range(["rgb(135, 179, 237)", "rgb(255, 212, 226)", "rgb(123, 236, 161)"])
///some potential colors for communities identfied by the community algorithm
const clusterColor = d3.scaleOrdinal(["rgb(0, 232, 255)", "rgb(255, 0, 199)", "rgb(255, 252, 0)", "rgb(46, 2117, 119)", "rgb(255, 122, 0)", "rgb(175, 68, 255)", "rgb(3, 82, 252)", "rgb(194, 252, 3)", "rgb(255, 255, 255)"]);
const temporalScale = d3.scaleLinear()
.domain([1600, 1950])
.range([0, window.innerWidth - 100]);
const svg = d3.select("#graph")
.attr("preserveAspectRatio", "xMidYMid")
.attr("viewBox", "0 0 " + windowWidth + " " + windowHeight)
.call(d3.zoom()
.scaleExtent([1 / 4, 3])
.on("zoom", zoomed))
const svgG = svg.append("g").attr("class", "svgG")
const link = svgG.append("g").attr("class", "linkG")
const node = svgG.append("g").attr("class", "nodeG")
function zoomed() {
svgG.attr("transform", d3.event.transform);
}
Promise.all([
d3.csv("les_mis_nodes.csv"), ///modified les miserables data. I randomly added values for dateStart & dateEnd for the timeline example
d3.csv("les_mis_edges.csv")
])
.then(([nodes, edges]) => {
nodes.forEach(function(d, i) {
d.dateStart = +d.dateStart;
d.dateEnd = +d.dateEnd;
})
graph = {
nodes: nodes,
links: edges
}
renderNetwork()
})
///force simulation
const simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d, i) {
return d.id;
}))
.force("charge", d3.forceManyBody())
.force('collision', d3.forceCollide())
.force("center", d3.forceCenter(windowWidth / 2, windowHeight / 2));
///function to render the graph
function renderNetwork() {
detailview = false;
simulation.alpha(1).restart();
simulation
.nodes(graph.nodes)
.on("tick", ticked);
simulation
.force("link")
.links(graph.links);
link.selectAll(".link")
.data(graph.links)
.join("path")
.style("fill", "none")
.attr("stroke-width", 1)
.attr("class", "link")
.style("stroke", function(d, i) {
return edgeColors(d.relationType)
})
.style("opacity", 0.3)
node.selectAll("rect")
.data(graph.nodes)
.join("rect")
.attr("class", "nodes")
.style("stroke", "rgb(10, 3, 36)")
.style("stroke-width", .5)
.attr("width", function(d) {
// add connectivity value for data here by counting the numbers of links for each node
d.connectivity = graph.links.filter(function(l) {
return l.source.id == d.id || l.target.id == d.id
}).length
return nodeSize(d.connectivity)
})
.attr("height", function(d, i) {
return nodeSize(d.connectivity)
})
.on("mouseover", function(d, i) {
if (detailview == false) {
mouseClick(d, i)
}
})
.on("click", function(d, i) {
detailview = true;
mouseClick(d, i)
})
.on("mouseout", function(d, i) {
mouseOut(d, i)
})
d3.selectAll("#closedetails")
.on("click", function(d, i) {
d3.select("#details")
.style("display", "none")
.selectAll("p")
.remove()
detailview = false;
mouseOut(d, i)
})
}
/////////////////////////////////
//ticked function
function ticked() {
if (simulation.alpha() < 0.01) {
simulation.stop()
//after simulation has cooled down use netclustering to identify clusters in the data. each node will get assigned a cluster property after that
netClustering.cluster(graph.nodes, graph.links);
graph.nodes.sort(function(a, b) {
return a.cluster - b.cluster || a.dateStart - b.dateStart
});
///order nodes by time and assigne an index for that for later use
graph.nodes.forEach(function(d, i) {
d.temporalIndex = i;
})
d3.selectAll(".link")
.transition()
.duration(2000)
.attr("d", function(d) {
let arcRadius = 100 //defines curvature of the links
return "M" + d.source.x + "," + d.source.y + "A" + arcRadius + "," + arcRadius + " 0 0,1 " + d.target.x + "," + d.target.y
})
d3.selectAll(".nodes")
.attr("x", function(d) {
return d.x - nodeSize(d.connectivity) / 2;
})
.attr("y", function(d) {
return d.y - nodeSize(d.connectivity) / 2;
})
.style("fill", function(d) {
return clusterColor(d.cluster);
})
}
}
/////////////////////////////////
//morph to graph function
function morphToGraph() {
d3.selectAll(".link").transition().duration(2000).attr("d", function(d) {
let arcRadius = 100 //defines curvature of the links
return "M" + d.source.x + "," + d.source.y + "A" + arcRadius + "," + arcRadius + " 0 0,1 " + d.target.x + "," + d.target.y
})
d3.selectAll(".nodes").transition().duration(2000)
.attr("x", function(d) {
return d.x - nodeSize(d.connectivity) / 2;
})
.attr("y", function(d) {
return d.y - nodeSize(d.connectivity) / 2;
})
.attr("width", function(d, i) {
return nodeSize(d.connectivity)
})
.attr("height", function(d, i) {
return nodeSize(d.connectivity)
})
.style("display", null)
}
/////////////////////////////////
//morph to timeline function
function morphToTimeline() {
d3.selectAll(".nodes")
.style("display", function(d) {
if (d.dateStart == undefined) {
return "none"
}
})
.transition()
.duration(2000)
.attr("x", function(d) {
return temporalScale(d.dateStart)
})
.attr("y", function(d, i) {
return (25 * d.temporalIndex)
})
.attr("width", function(d) {
return temporalScale(d.dateEnd) - temporalScale(d.dateStart)
})
d3.selectAll(".link")
.transition()
.duration(2000)
.attr("d", function(d) {
let arcRadius = 2
let nodeAx = temporalScale(d.source.dateStart)
let nodeAI = graph.nodes.filter(function(D, I) {
return D.id == d.source.id
})[0].temporalIndex
let nodeBx = temporalScale(d.target.dateStart)
let nodeBI = graph.nodes.filter(function(D, I) {
return D.id == d.target.id
})[0].temporalIndex
if (nodeAI < nodeBI) {
return "M" + nodeBx + "," + 25 * nodeBI + "A" + arcRadius + "," + arcRadius + " 0 0,1 " + nodeAx + "," + 25 * nodeAI
} else {
return "M" + nodeAx + "," + 25 * nodeAI + "A" + arcRadius + "," + arcRadius + " 0 0,1 " + nodeBx + "," + 25 * nodeBI
}
})
}
/////////////////////////////////
//mouseclick & mouseover function
function mouseClick(D, I) {
d3.select("#details").style("display", "none").selectAll("p").remove()
d3.select("#details").style("display", "none").selectAll("a").remove()
d3.select("#closedetails").style("display", "block")
d3.selectAll(".nodes").style("opacity", 0.1)
//identify connected nodes
let connections = graph.links.filter(function(E, G) {
return E.source.id == D.id ||
E.target.id == D.id
})
//highlight connected ndoes
connections.forEach(function(E, G) {
d3.selectAll(".nodes").filter(function(X, Y) {
return X.id == E.source.id || X.id == E.target.id
}).style("opacity", 1)
d3.selectAll(".nodes")
.filter(function(d, i) {
return D.id == d.id
})
.style("opacity", 1)
})
//highlight edges to connected nodes
d3.selectAll(".link").style("opacity", 0.1)
d3.selectAll(".link").filter(function(E, G) {
return E.source.id == D.id || E.target.id == D.id
})
.style("opacity", 1)
d3.select("#details").style("display", "block")
//fill detail view on right side with information
Object.entries(D).forEach(entry => {
console.log(entry)
let key = entry[0]
let value = entry[1]
d3.select("#details")
.append("p")
.attr("class", "detail_key")
.text(key)
d3.select("#details")
.append("p")
.attr("class", "detail_value")
.text(value)
});
d3.select("#details")
.append("p")
.attr("class", "relationsheadline, detail_key")
.text("Relations (" + D.connectivity + ")")
connections.forEach(function(connection, i) {
d3.select("#details")
.append("p")
.attr("class", "detail_value, relations_value")
.style("color", function() {
return edgeColors(connection.relationType)
}).text(function() {
return connection.source.id + " ↔ " + connection.target.id + " (" + connection.relationType + ")"
})
})
}
/////////////////////////////////
//mouseout function
function mouseOut(D, I) {
if (detailview != true) {
d3.select("#closedetails").style("display", "none")
d3.selectAll(".nodes").style("opacity", 1)
d3.selectAll(".link").style("opacity", 0.3)
d3.select("#details").style("display", "none").selectAll("p").remove()
}
}
</script>
</body>
</html>
source target value relationType
Napoleon Myriel 1 a
Mlle.Baptistine Myriel 8 b
Mme.Magloire Myriel 10 c
Mme.Magloire Mlle.Baptistine 6 a
CountessdeLo Myriel 1 a
Geborand Myriel 1 b
Champtercier Myriel 1 c
Cravatte Myriel 1 a
Count Myriel 2 a
OldMan Myriel 1 a
Valjean Labarre 1 c
Valjean Mme.Magloire 3 a
Valjean Mlle.Baptistine 3 a
Valjean Myriel 5 b
Marguerite Valjean 1 c
Mme.deR Valjean 1 a
Isabeau Valjean 1 a
Gervais Valjean 1 a
Listolier Tholomyes 4 a
Fameuil Tholomyes 4 b
Fameuil Listolier 4 c
Blacheville Tholomyes 4 a
Blacheville Listolier 4 a
Blacheville Fameuil 4 b
Favourite Tholomyes 3 a
Favourite Listolier 3 a
Favourite Fameuil 3 a
Favourite Blacheville 4 b
Dahlia Tholomyes 3 c
Dahlia Listolier 3 a
Dahlia Fameuil 3 a
Dahlia Blacheville 3 b
Dahlia Favourite 5 a
Zephine Tholomyes 3 a
Zephine Listolier 3 a
Zephine Fameuil 3 b
Zephine Blacheville 3 a
Zephine Favourite 4 b
Zephine Dahlia 4 c
Fantine Tholomyes 3 a
Fantine Listolier 3 a
Fantine Fameuil 3 a
Fantine Blacheville 3 c
Fantine Favourite 4 a
Fantine Dahlia 4 a
Fantine Zephine 4 b
Fantine Marguerite 2 c
Fantine Valjean 9 a
Mme.Thenardier Fantine 2 a
Mme.Thenardier Valjean 7 a
Thenardier Mme.Thenardier 13 a
Thenardier Fantine 1 a
Thenardier Valjean 12 a
Cosette Mme.Thenardier 4 b
Cosette Valjean 31 a
Cosette Tholomyes 1 b
Cosette Thenardier 1 c
Javert Valjean 17 a
Javert Fantine 5 a
Javert Thenardier 5 b
Javert Mme.Thenardier 1 c
Javert Cosette 1 a
Fauchelevent Valjean 8 a
Fauchelevent Javert 1 b
Bamatabois Fantine 1 c
Bamatabois Javert 1 a
Bamatabois Valjean 2 a
Perpetue Fantine 1 a
Simplice Perpetue 2 c
Simplice Valjean 3 a
Simplice Fantine 2 a
Simplice Javert 1 b
Scaufflaire Valjean 1 a
Woman1 Valjean 2 b
Woman1 Javert 1 c
Judge Valjean 3 a
Judge Bamatabois 2 a
Champmathieu Valjean 3 b
Champmathieu Judge 3 c
Champmathieu Bamatabois 2 a
Brevet Judge 2 a
Brevet Champmathieu 2 b
Brevet Valjean 2 a
Brevet Bamatabois 1 a
Chenildieu Judge 2 a
Chenildieu Champmathieu 2 b
Chenildieu Brevet 2 c
Chenildieu Valjean 2 a
Chenildieu Bamatabois 1 a
Cochepaille Judge 2 b
Cochepaille Champmathieu 2 a
Cochepaille Brevet 2 b
Cochepaille Chenildieu 2 a
Cochepaille Valjean 2 a
Cochepaille Bamatabois 1 a
Pontmercy Thenardier 1 b
Boulatruelle Thenardier 1 c
Eponine Mme.Thenardier 2 a
Eponine Thenardier 3 a
Anzelma Eponine 2 b
Anzelma Thenardier 2 c
Anzelma Mme.Thenardier 1 a
Woman2 Valjean 3 a
Woman2 Cosette 1 b
Woman2 Javert 1 a
MotherInnocent Fauchelevent 3 a
MotherInnocent Valjean 1 a
Gribier Fauchelevent 2 b
Mme.Burgon Jondrette 1 a
Gavroche Mme.Burgon 2 b
Gavroche Thenardier 1 c
Gavroche Javert 1 a
Gavroche Valjean 1 a
Gillenormand Cosette 3 a
Gillenormand Valjean 2 c
Magnon Gillenormand 1 a
Magnon Mme.Thenardier 1 a
Mlle.Gillenormand Gillenormand 9 b
Mlle.Gillenormand Cosette 2 c
Mlle.Gillenormand Valjean 2 a
Mme.Pontmercy Mlle.Gillenormand 1 a
Mme.Pontmercy Pontmercy 1 b
Mlle.Vaubois Mlle.Gillenormand 1 a
Lt.Gillenormand Mlle.Gillenormand 2 a
Lt.Gillenormand Gillenormand 1 a
Lt.Gillenormand Cosette 1 b
Marius Mlle.Gillenormand 6 a
Marius Gillenormand 12 b
Marius Pontmercy 1 c
Marius Lt.Gillenormand 1 a
Marius Cosette 21 a
Marius Valjean 19 b
Marius Tholomyes 1 c
Marius Thenardier 2 a
Marius Eponine 5 a
Marius Gavroche 4 b
BaronessT Gillenormand 1 c
BaronessT Marius 1 a
Mabeuf Marius 1 a
Mabeuf Eponine 1 b
Mabeuf Gavroche 1 c
Enjolras Marius 7 a
Enjolras Gavroche 7 a
Enjolras Javert 6 a
Enjolras Mabeuf 1 a
Enjolras Valjean 4 b
Combeferre Enjolras 15 c
Combeferre Marius 5 a
Combeferre Gavroche 6 a
Combeferre Mabeuf 2 b
Prouvaire Gavroche 1 c
Prouvaire Enjolras 4 a
Prouvaire Combeferre 2 a
Feuilly Gavroche 2 b
Feuilly Enjolras 6 c
Feuilly Prouvaire 2 a
Feuilly Combeferre 5 a
Feuilly Mabeuf 1 b
Feuilly Marius 1 c
Courfeyrac Marius 9 a
Courfeyrac Enjolras 17 a
Courfeyrac Combeferre 13 b
Courfeyrac Gavroche 7 a
Courfeyrac Mabeuf 2 b
Courfeyrac Eponine 1 c
Courfeyrac Feuilly 6 a
Courfeyrac Prouvaire 3 a
Bahorel Combeferre 5 b
Bahorel Gavroche 5 c
Bahorel Courfeyrac 6 a
Bahorel Mabeuf 2 a
Bahorel Enjolras 4 b
Bahorel Feuilly 3 c
Bahorel Prouvaire 2 a
Bahorel Marius 1 a
Bossuet Marius 5 a
Bossuet Courfeyrac 12 c
Bossuet Gavroche 5 a
Bossuet Bahorel 4 a
Bossuet Enjolras 10 a
Bossuet Feuilly 6 a
Bossuet Prouvaire 2 a
Bossuet Combeferre 9 c
Bossuet Mabeuf 1 a
Bossuet Valjean 1 a
Joly Bahorel 5 b
Joly Bossuet 7 c
Joly Gavroche 3 a
Joly Courfeyrac 5 a
Joly Enjolras 5 b
Joly Feuilly 5 c
Joly Prouvaire 2 a
Joly Combeferre 5 a
Joly Mabeuf 1 b
Joly Marius 2 c
Grantaire Bossuet 3 a
Grantaire Enjolras 3 a
Grantaire Combeferre 1 b
Grantaire Courfeyrac 2 a
Grantaire Joly 2 b
Grantaire Gavroche 1 c
Grantaire Bahorel 1 a
Grantaire Feuilly 1 a
Grantaire Prouvaire 1 b
MotherPlutarch Mabeuf 3 c
Gueulemer Thenardier 5 a
Gueulemer Valjean 1 a
Gueulemer Mme.Thenardier 1 b
Gueulemer Javert 1 c
Gueulemer Gavroche 1 a
Gueulemer Eponine 1 a
Babet Thenardier 6 b
Babet Gueulemer 6 a
Babet Valjean 1 a
Babet Mme.Thenardier 1 a
Babet Javert 2 b
Babet Gavroche 1 a
Babet Eponine 1 b
Claquesous Thenardier 4 c
Claquesous Babet 4 a
Claquesous Gueulemer 4 a
Claquesous Valjean 1 b
Claquesous Mme.Thenardier 1 c
Claquesous Javert 1 a
Claquesous Eponine 1 a
Claquesous Enjolras 1 b
Montparnasse Javert 1 c
Montparnasse Babet 2 a
Montparnasse Gueulemer 2 a
Montparnasse Claquesous 2 b
Montparnasse Valjean 1 a
Montparnasse Gavroche 1 a
Montparnasse Eponine 1 a
Montparnasse Thenardier 1 b
Toussaint Cosette 2 a
Toussaint Javert 1 b
Toussaint Valjean 1 c
Child1 Gavroche 2 a
Child2 Gavroche 2 a
Child2 Child1 3 a
Brujon Babet 3 c
Brujon Gueulemer 3 a
Brujon Thenardier 3 a
Brujon Gavroche 1 b
Brujon Eponine 1 c
Brujon Claquesous 1 a
Brujon Montparnasse 1 a
Mme.Hucheloup Bossuet 1 b
Mme.Hucheloup Joly 1 a
Mme.Hucheloup Grantaire 1 a
Mme.Hucheloup Bahorel 1 a
Mme.Hucheloup Courfeyrac 1 b
Mme.Hucheloup Gavroche 1 a
Mme.Hucheloup Enjolras 1 b
id dateStart dateEnd
Myriel 1815 1876
Napoleon 1710 1760
Mlle.Baptistine 1830 1873
Mme.Magloire 1833 1874
CountessdeLo 1850 1920
Geborand 1805 1837
Champtercier 1746 1799
Cravatte 1789 1832
Count 1862 1901
OldMan 1820 1867
Labarre 1879 1929
Valjean 1900 1974
Marguerite 1705 1740
Mme.deR 1788 1827
Isabeau 1800 1878
Gervais 1860 1901
Tholomyes 1690 1734
Listolier 1695 1765
Fameuil 1722 1756
Blacheville 1735 1770
Favourite 1754 1803
Dahlia 1780 1834
Zephine 1875 1924
Fantine 1836 1892
Mme.Thenardier 1782 1858
Thenardier 1835 1904
Cosette 1743 1792
Javert 1661 1737
Fauchelevent 1763 1840
Bamatabois 1871 1905
Perpetue 1799 1867
Simplice 1723 1783
Scaufflaire 1848 1926
Woman1 1824 1883
Judge 1686 1745
Champmathieu 1831 1883
Brevet 1865 1937
Chenildieu 1748 1795
Cochepaille 1764 1843
Pontmercy 1664 1738
Boulatruelle 1799 1864
Eponine 1754 1809
Anzelma 1699 1740
Woman2 1715 1752
MotherInnocent 1701 1737
Gribier 1690 1768
Jondrette 1807 1843
Mme.Burgon 1818 1853
Gavroche 1786 1824
Gillenormand 1784 1853
Magnon 1862 1931
Mlle.Gillenormand 1759 1825
Mme.Pontmercy 1662 1717
Mlle.Vaubois 1763 1794
Lt.Gillenormand 1744 1793
Marius 1842 1883
BaronessT 1860 1936
Mabeuf 1660 1706
Enjolras 1725 1776
Combeferre 1703 1764
Prouvaire 1771 1807
Feuilly 1692 1748
Courfeyrac 1863 1937
Bahorel 1870 1902
Bossuet 1849 1902
Joly 1794 1837
Grantaire 1726 1800
MotherPlutarch 1765 1817
Gueulemer 1703 1753
Babet 1792 1862
Claquesous 1785 1853
Montparnasse 1699 1761
Toussaint 1853 1884
Child1 1692 1743
Child2 1745 1797
Brujon 1817 1859
Mme.Hucheloup 1694 1757
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment