This chart shows events, that have a defined start and/or end in the time continuum in form of a timeline or timechart. Events can be instants (one date only) or intervals (start date and end date).
The timeline consists of two bands.
The upper band shows the timeline items with data within the selected timeline interval. The lower band is the navigation band; it just shows the distribution of the items. The numbers in the lower band show the start, the length, and the end of the selected interval, respectively. Click on the lower band and drag to create a brush and select an interval.
Click on the brush and drag to move the interval. Click on the left or right border of the brush and drag to resize the interval. Click on the lower band outside the brush to restore the original view. Mouseover an item to show a tooltip.
This work was inspired by
‘Simile Timeline’ by ‘David François Huynh’ (http://www.simile-widgets.org/timeline/) and ‘Swimlane Chart using d3.js’ by Bill Bunkat (http://bl.ocks.org/bunkat/1962173).
Any csv data file with the following line structure will do:
start,end,label …,…,…
The first line contains the field names. The field names ‘start’, ‘end’, and ‘label’ in the first line are required. The following lines contain the data.
Intervals have a ‘start’, an ‘end’, and a label. The following example describes the lifespan of the philosopher Kant: 1724,1804,Kant
Instants have a ‘start’, an EMPTY ‘end’, and a label. The second comma is required to mark the ‘end’ field and to designate this event as a point. The following example gives the publication date of the ‘Critique of Pure Reason’ by Kant: 1781,,Critique of Pure Reason
Optional additional fields (i.e., ‘description’, ‘image’, ‘link’, etc.) are not used in this version, but will be in future versions.
‘start’ and ‘end’ either conform to the ISO data format YYYY-MM-DD or contain full years (that is: with century). BC dates and dates between 0..99 AD are handled correctly. For more information on ‘start’ and ‘end’ see the comment of the function ‘parseDate’. ‘label’ ist a plain string. Example:
start,end,label 800 BC,701 BC,Homer 4 BC,65,Seneca 55,135,Epictetus 1469,1527,Machiavelli 1781,,Critique of Pure Reason …
I have developed this version as an exercise to learn a little bit of d3 and as a proof-of-concept for doing timelines.
This timeline doesn’t look especially ‘pretty’. I have chosen theses items (some poets and philosophers, some philosophical works) to show how a relatively long time span is displayed and how BC dates are handled. The dates are from the English Wikipedia Shorter intervals make for ‘nicer’ timelines, as you can see for yourself, if you use the brush.
To create your own timeline, you need
A data file (see ‘The file structure’ above).
The file ‘timeline.js’; download and put into your working directory or on your path.
The file ‘timeline.css’; download and put into your working directory or on your path; change settings according to your preferences.
Use ‘index.html’ (without comments) as a template and put in your filenames and paths.
I’m still a d3 newbie. So feedback on doing things more the ‘d3 way’ is very welcome. Comments, suggestions, and bug reports are welcome, too.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="keywords" lang="de" content="Zeitleiste, Zeitlinie, Zeitkarte, Geschichte, Chronologie">
<meta name="keywords" lang="en" content="Timeline, Timemap, History, Chronology">
<title>Timeline - Proof-of-concept</title>
<!-- That's my local d3 path. When working locally, use your local path. -->
<!--<script src="../../../lib/d3/d3.v3.js"></script>-->
<!-- That's the 'official' path. Comment out, when working locally. -->
<script src="//d3js.org/d3.v3.min.js"></script>
<!-- Store these two files in your application directory or on your path. -->
<script src="timeline.js"></script>
<link href="timeline.css" rel="stylesheet" type="text/css" />
</head>
<body>
<div id="timeline"></div>
<script>
/* You need a domElement, a sourceFile and a timeline.
The domElement will contain your timeline.
Use the CSS convention for identifying elements,
i.e. "div", "p", ".className", or "#id".
The sourceFile will contain your data.
If you prefer, you can also use tsv, xml, or json files
and the corresponding d3 functions for your data.
A timeline can have the following components:
.band(bandName, sizeFactor
bandName - string; the name of the band for references
sizeFactor - percentage; height of the band relation to the total height
Defines an area for timeline items.
A timeline must have at least one band.
Two bands are necessary, to change the selected time interval.
Three and Bands are allowed.
.xAxis(bandName)
bandName - string; the name of the band the xAxis will be attached to
Defines an xAxis for a band to show the range of the band.
This is optional, but highly recommended.
.labels(bandName)
bandName - string; the name of the band the labels will be attached to
Shows the start, length and end of the range of the band.
This is optional.
.tooltips(bandName)
bandName - string; the name of the band the labels will be attached to
Shows current start, length, and end of the selected interval of the band.
This is optional.
.brush(parentBand, targetBands]
parentBand - string; the band that the brush will be attached to
targetBands - array; the bands that are controlled by the brush
Controls the time interval of the targetBand.
Required, if you want to control/change the selected time interval
of one of the other bands.
.redraw()
Shows the initial view of the timeline.
This is required.
To make yourself familiar with these components try to
- comment out components and see what happens.
- change the size factors (second arguments) of the bands.
- rearrange the definitions of the components.
*/
// Define domElement and sourceFile
var domElement = "#timeline";
var sourceFile = "philosophers.csv";
// Read in the data and construct the timeline
d3.csv(sourceFile, function(dataset) {
timeline(domElement)
.data(dataset)
.band("mainBand", 0.82)
.band("naviBand", 0.08)
.xAxis("mainBand")
.tooltips("mainBand")
.xAxis("naviBand")
.labels("mainBand")
.labels("naviBand")
.brush("naviBand", ["mainBand"])
.redraw();
});
</script>
</body>
</html>
start,end,label
800 BC,701 BC,Homer
750 BC,650 BC,Hesiod
624 BC,546 BC,Thales
610 BC,546 BC,Anaximander
585 BC,525 BC,Anaximenes
582 BC,496 BC,Pythagoras
470 BC,380 BC,Philolaus
428 BC,347 BC,Archytas
570 BC,470 BC,Xenophanes
510 BC,440 BC,Parmenides
535 BC,475 BC,Heraclitus
490 BC,430 BC,Empedocles
500 BC,428 BC,Anaxagoras
480 BC,420 BC,Leucippus
460 BC,370 BC,Democritus
490 BC,420 BC,Protagoras
487 BC,376 BC,Gorgias
371 BC,287 BC,Theophrastus
469 BC,399 BC,Socrates
450 BC,380 BC,Euclid of Megara
445 BC,360 BC,Antisthenes
435 BC,356 BC,Aristippus
428 BC,347 BC,Plato
405 BC,320 BC,Diogenes of Sinope
396 BC,314 BC,Xenocrates
384 BC,322 BC,Aristotle
341 BC,270 BC,Epicurus
287 BC,212 BC,Archimedes
280 BC,207 BC,Chrysippus
360 BC,280 BC,Stilpo
334 BC,262 BC,Zeno of Citium
187 BC,109 BC,Clitomachus
106 BC,43 BC,Cicero
94 BC,55 BC,Lucretius
4 BC,65,Seneca
30,100,Musonius Rufus
45,120,Plutarch
55,135,Epictetus
70,156,Polycarp
121,180,Marcus Aurelius
150,215,Clement of Alexandria
160,210,Sextus Empiricus
205,270,Plotinus
232,304,Porphyry
270,351,Nicolaus of Myra
295,373,Athanasius
315,367,Hilarius of Poitiers
317,388,Themistius
329,390,Gregorius of Nazianz
330,379,Basilius
335,394,Gregorius of Nyssa
339,397,Ambrosius
340,420,Hieronymus
354,407,Chrisostomos
381,444,Cyrill
411,485,Proclus
462,540,Damascius
472,524,Boethius
1033,1109,Anselm of Canterbury
1091,1153,Bernhard of Clairvaux
1135,1204,Maimonides
1221,1274,Bonaventura
1225,1274,Thomas of Aquin
1235,1315,Rymundus Lullus
1193,1280,Albertus Magnus
1265,1321,Dante
1266,1308,Duns Scotus
1287,1347,Occam
1396,1484,Trapezuntius
1401,1464,Cusanus
1443,1485,Agricola
1452,1498,Savonarola
1467,1536,Erasmus
1469,1527,Machiavelli
1478,1535,Morus
1473,1543,Kopernikus
1483,1546,Luther
1484,1531,Zwingli
1486,1535,Agrippa
1493,1541,Paracelsus
1497,1560,Melanchton
1509,1564,Calvin
1540,1609,Scaliger
1546,1601,Brahe
1560,1626,Bacon
1564,1642,Galilei
1588,1679,Hobbes
1596,1659,Descartes
1623,1662,Pascal
1632,1677,Spinoza
1646,1716,Leibniz
1685,1735,Berkeley
1711,1776,Hume
1724,1804,Kant
1743,1819,Jacobi
1748,1832,Bentham
1770,1831,Hegel
1775,1854,Schelling
1762,1814,Fichte
1788,1860,Schopenhauer
1818,1883,Marx
1833,1921,Dühring
1833,1911,Dilthey
1844,1900,Nietzsche
1848,1925,Frege
1859,1952,Dewey
1859,1941,Bergson
1872,1970,Russell
1873,1958,Moore
1874,1928,Scheler
1882,1936,Schlick
1885,1977,Bloch
1889,1951,Wittgenstein
1890,1963,Ajdukiewicz
1900,1976,Ryle
1902,1995,Bochenski
1902,1994,Popper
1903,1993,Jonas
1903,1969,Adorno
1908,2000,Quine
1910,1989,Ayer
1911,1960,Austin
1921,2002,Rawls
1923,1991,Stegmüller
1931-10-04,2007-06-08,Rorty
760 BC,,Iliad
740 BC,,Odyssey
700 BC,,Theogony
1781,,Critique of Pure Reason
1818,,The World as Will and Representation
1739,,A Treatise of Human Nature
1748,,An Enquiry Concerning Human Understanding
1807,,The Phenomenology of Spirit
1532,,The Prince
380 BC,,The Republic
1651,,Leviathan
1710,,A Treatise Concerning the Principles of Human Knowledge
1637,,Discourse on the Method
1883,,Thus Spoke Zarathustra
/* axis */
.axis { /* axis labels */
fill: #808080;
font-family: sans-serif;
font-size: 10px;
}
.axis line{ /* axis tick marks */
stroke-width : 1;
stroke: grey;
shape-rendering: crispEdges;
}
.axis path { /* axis line */
stroke-width : 1;
stroke: grey;
shape-rendering: crispEdges;
}
/* timeline band */
.band { /* band background */
fill: #FAFAFA;
}
/* labels */
.bandLabel {
fill: #F0F0F0;
font: 10px sans-serif;
font-weight: bold;
}
.bandMinMaxLabel {
fill: blue;
font: 10px sans-serif;
font-weight: bold;
}
.bandMidLabel {
fill: red;
font: 10px sans-serif;
font-style: italic;
font-weight: bold;
}
/* brush */
.brush .extent {
stroke: gray;
fill: blue;
fill-opacity: .1;
}
.chart {
fill: #EEEEEE;
}
.interval {
fill: #AAFFFF;
stroke-width: 6;
cursor : default;
pointer-events: true;
}
.instant {
fill: #FFAAFF;
stroke-width: 6;
cursor : default;
/*pointer-events: true;*/
}
.instantLabel {
fill : blue;
font: 10px sans-serif;
shape-rendering: crispEdges;
}
.intervalLabel {
fill : black;
font: 10px sans-serif;
shape-rendering: crispEdges;
}
.item {
cursor : default;
pointer-events: auto;
}
.svg {
border-style: solid;
border-width: 1px;
border-color: black;
background-color: #FFFFFF;
}
.tooltip {
width: auto;
position: absolute;
visibility: hidden;
color : black;
cursor:default;
background-color: #FFFFEE;
border: 1px solid;
padding: 4px;
shape-rendering: crispEdges;
pointer-events: none;
}
// A timeline component for d3
// version v0.1
function timeline(domElement) {
//--------------------------------------------------------------------------
//
// chart
//
// chart geometry
var margin = {top: 20, right: 20, bottom: 20, left: 20},
outerWidth = 960,
outerHeight = 500,
width = outerWidth - margin.left - margin.right,
height = outerHeight - margin.top - margin.bottom;
// global timeline variables
var timeline = {}, // The timeline
data = {}, // Container for the data
components = [], // All the components of the timeline for redrawing
bandGap = 25, // Arbitray gap between to consecutive bands
bands = {}, // Registry for all the bands in the timeline
bandY = 0, // Y-Position of the next band
bandNum = 0; // Count of bands for ids
// Create svg element
var svg = d3.select(domElement).append("svg")
.attr("class", "svg")
.attr("id", "svg")
.attr("width", outerWidth)
.attr("height", outerHeight)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
svg.append("clipPath")
.attr("id", "chart-area")
.append("rect")
.attr("width", width)
.attr("height", height);
var chart = svg.append("g")
.attr("class", "chart")
.attr("clip-path", "url(#chart-area)" );
var tooltip = d3.select("body")
.append("div")
.attr("class", "tooltip")
.style("visibility", "visible");
//--------------------------------------------------------------------------
//
// data
//
timeline.data = function(items) {
var today = new Date(),
tracks = [],
yearMillis = 31622400000,
instantOffset = 100 * yearMillis;
data.items = items;
function showItems(n) {
var count = 0, n = n || 10;
console.log("\n");
items.forEach(function (d) {
count++;
if (count > n) return;
console.log(toYear(d.start) + " - " + toYear(d.end) + ": " + d.label);
})
}
function compareAscending(item1, item2) {
// Every item must have two fields: 'start' and 'end'.
var result = item1.start - item2.start;
// earlier first
if (result < 0) { return -1; }
if (result > 0) { return 1; }
// longer first
result = item2.end - item1.end;
if (result < 0) { return -1; }
if (result > 0) { return 1; }
return 0;
}
function compareDescending(item1, item2) {
// Every item must have two fields: 'start' and 'end'.
var result = item1.start - item2.start;
// later first
if (result < 0) { return 1; }
if (result > 0) { return -1; }
// shorter first
result = item2.end - item1.end;
if (result < 0) { return 1; }
if (result > 0) { return -1; }
return 0;
}
function calculateTracks(items, sortOrder, timeOrder) {
var i, track;
sortOrder = sortOrder || "descending"; // "ascending", "descending"
timeOrder = timeOrder || "backward"; // "forward", "backward"
function sortBackward() {
// older items end deeper
items.forEach(function (item) {
for (i = 0, track = 0; i < tracks.length; i++, track++) {
if (item.end < tracks[i]) { break; }
}
item.track = track;
tracks[track] = item.start;
});
}
function sortForward() {
// younger items end deeper
items.forEach(function (item) {
for (i = 0, track = 0; i < tracks.length; i++, track++) {
if (item.start > tracks[i]) { break; }
}
item.track = track;
tracks[track] = item.end;
});
}
if (sortOrder === "ascending")
data.items.sort(compareAscending);
else
data.items.sort(compareDescending);
if (timeOrder === "forward")
sortForward();
else
sortBackward();
}
// Convert yearStrings into dates
data.items.forEach(function (item){
item.start = parseDate(item.start);
if (item.end == "") {
//console.log("1 item.start: " + item.start);
//console.log("2 item.end: " + item.end);
item.end = new Date(item.start.getTime() + instantOffset);
//console.log("3 item.end: " + item.end);
item.instant = true;
} else {
//console.log("4 item.end: " + item.end);
item.end = parseDate(item.end);
item.instant = false;
}
// The timeline never reaches into the future.
// This is an arbitrary decision.
// Comment out, if dates in the future should be allowed.
if (item.end > today) { item.end = today};
});
//calculateTracks(data.items);
// Show patterns
//calculateTracks(data.items, "ascending", "backward");
//calculateTracks(data.items, "descending", "forward");
// Show real data
calculateTracks(data.items, "descending", "backward");
//calculateTracks(data.items, "ascending", "forward");
data.nTracks = tracks.length;
data.minDate = d3.min(data.items, function (d) { return d.start; });
data.maxDate = d3.max(data.items, function (d) { return d.end; });
return timeline;
};
//----------------------------------------------------------------------
//
// band
//
timeline.band = function (bandName, sizeFactor) {
var band = {};
band.id = "band" + bandNum;
band.x = 0;
band.y = bandY;
band.w = width;
band.h = height * (sizeFactor || 1);
band.trackOffset = 4;
// Prevent tracks from getting too high
band.trackHeight = Math.min((band.h - band.trackOffset) / data.nTracks, 20);
band.itemHeight = band.trackHeight * 0.8,
band.parts = [],
band.instantWidth = 100; // arbitray value
band.xScale = d3.time.scale()
.domain([data.minDate, data.maxDate])
.range([0, band.w]);
band.yScale = function (track) {
return band.trackOffset + track * band.trackHeight;
};
band.g = chart.append("g")
.attr("id", band.id)
.attr("transform", "translate(0," + band.y + ")");
band.g.append("rect")
.attr("class", "band")
.attr("width", band.w)
.attr("height", band.h);
// Items
var items = band.g.selectAll("g")
.data(data.items)
.enter().append("svg")
.attr("y", function (d) { return band.yScale(d.track); })
.attr("height", band.itemHeight)
.attr("class", function (d) { return d.instant ? "part instant" : "part interval";});
var intervals = d3.select("#band" + bandNum).selectAll(".interval");
intervals.append("rect")
.attr("width", "100%")
.attr("height", "100%");
intervals.append("text")
.attr("class", "intervalLabel")
.attr("x", 1)
.attr("y", 10)
.text(function (d) { return d.label; });
var instants = d3.select("#band" + bandNum).selectAll(".instant");
instants.append("circle")
.attr("cx", band.itemHeight / 2)
.attr("cy", band.itemHeight / 2)
.attr("r", 5);
instants.append("text")
.attr("class", "instantLabel")
.attr("x", 15)
.attr("y", 10)
.text(function (d) { return d.label; });
band.addActions = function(actions) {
// actions - array: [[trigger, function], ...]
actions.forEach(function (action) {
items.on(action[0], action[1]);
})
};
band.redraw = function () {
items
.attr("x", function (d) { return band.xScale(d.start);})
.attr("width", function (d) {
return band.xScale(d.end) - band.xScale(d.start); });
band.parts.forEach(function(part) { part.redraw(); })
};
bands[bandName] = band;
components.push(band);
// Adjust values for next band
bandY += band.h + bandGap;
bandNum += 1;
return timeline;
};
//----------------------------------------------------------------------
//
// labels
//
timeline.labels = function (bandName) {
var band = bands[bandName],
labelWidth = 46,
labelHeight = 20,
labelTop = band.y + band.h - 10,
y = band.y + band.h + 1,
yText = 15;
var labelDefs = [
["start", "bandMinMaxLabel", 0, 4,
function(min, max) { return toYear(min); },
"Start of the selected interval", band.x + 30, labelTop],
["end", "bandMinMaxLabel", band.w - labelWidth, band.w - 4,
function(min, max) { return toYear(max); },
"End of the selected interval", band.x + band.w - 152, labelTop],
["middle", "bandMidLabel", (band.w - labelWidth) / 2, band.w / 2,
function(min, max) { return max.getUTCFullYear() - min.getUTCFullYear(); },
"Length of the selected interval", band.x + band.w / 2 - 75, labelTop]
];
var bandLabels = chart.append("g")
.attr("id", bandName + "Labels")
.attr("transform", "translate(0," + (band.y + band.h + 1) + ")")
.selectAll("#" + bandName + "Labels")
.data(labelDefs)
.enter().append("g")
.on("mouseover", function(d) {
tooltip.html(d[5])
.style("top", d[7] + "px")
.style("left", d[6] + "px")
.style("visibility", "visible");
})
.on("mouseout", function(){
tooltip.style("visibility", "hidden");
});
bandLabels.append("rect")
.attr("class", "bandLabel")
.attr("x", function(d) { return d[2];})
.attr("width", labelWidth)
.attr("height", labelHeight)
.style("opacity", 1);
var labels = bandLabels.append("text")
.attr("class", function(d) { return d[1];})
.attr("id", function(d) { return d[0];})
.attr("x", function(d) { return d[3];})
.attr("y", yText)
.attr("text-anchor", function(d) { return d[0];});
labels.redraw = function () {
var min = band.xScale.domain()[0],
max = band.xScale.domain()[1];
labels.text(function (d) { return d[4](min, max); })
};
band.parts.push(labels);
components.push(labels);
return timeline;
};
//----------------------------------------------------------------------
//
// tooltips
//
timeline.tooltips = function (bandName) {
var band = bands[bandName];
band.addActions([
// trigger, function
["mouseover", showTooltip],
["mouseout", hideTooltip]
]);
function getHtml(element, d) {
var html;
if (element.attr("class") == "interval") {
html = d.label + "<br>" + toYear(d.start) + " - " + toYear(d.end);
} else {
html = d.label + "<br>" + toYear(d.start);
}
return html;
}
function showTooltip (d) {
var x = event.pageX < band.x + band.w / 2
? event.pageX + 10
: event.pageX - 110,
y = event.pageY < band.y + band.h / 2
? event.pageY + 30
: event.pageY - 30;
tooltip
.html(getHtml(d3.select(this), d))
.style("top", y + "px")
.style("left", x + "px")
.style("visibility", "visible");
}
function hideTooltip () {
tooltip.style("visibility", "hidden");
}
return timeline;
};
//----------------------------------------------------------------------
//
// xAxis
//
timeline.xAxis = function (bandName, orientation) {
var band = bands[bandName];
var axis = d3.svg.axis()
.scale(band.xScale)
.orient(orientation || "bottom")
.tickSize(6, 0)
.tickFormat(function (d) { return toYear(d); });
var xAxis = chart.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + (band.y + band.h) + ")");
xAxis.redraw = function () {
xAxis.call(axis);
};
band.parts.push(xAxis); // for brush.redraw
components.push(xAxis); // for timeline.redraw
return timeline;
};
//----------------------------------------------------------------------
//
// brush
//
timeline.brush = function (bandName, targetNames) {
var band = bands[bandName];
var brush = d3.svg.brush()
.x(band.xScale.range([0, band.w]))
.on("brush", function() {
var domain = brush.empty()
? band.xScale.domain()
: brush.extent();
targetNames.forEach(function(d) {
bands[d].xScale.domain(domain);
bands[d].redraw();
});
});
var xBrush = band.g.append("svg")
.attr("class", "x brush")
.call(brush);
xBrush.selectAll("rect")
.attr("y", 4)
.attr("height", band.h - 4);
return timeline;
};
//----------------------------------------------------------------------
//
// redraw
//
timeline.redraw = function () {
components.forEach(function (component) {
component.redraw();
})
};
//--------------------------------------------------------------------------
//
// Utility functions
//
function parseDate(dateString) {
// 'dateString' must either conform to the ISO date format YYYY-MM-DD
// or be a full year without month and day.
// AD years may not contain letters, only digits '0'-'9'!
// Invalid AD years: '10 AD', '1234 AD', '500 CE', '300 n.Chr.'
// Valid AD years: '1', '99', '2013'
// BC years must contain letters or negative numbers!
// Valid BC years: '1 BC', '-1', '12 BCE', '10 v.Chr.', '-384'
// A dateString of '0' will be converted to '1 BC'.
// Because JavaScript can't define AD years between 0..99,
// these years require a special treatment.
var format = d3.time.format("%Y-%m-%d"),
date,
year;
date = format.parse(dateString);
if (date !== null) return date;
// BC yearStrings are not numbers!
if (isNaN(dateString)) { // Handle BC year
// Remove non-digits, convert to negative number
year = -(dateString.replace(/[^0-9]/g, ""));
} else { // Handle AD year
// Convert to positive number
year = +dateString;
}
if (year < 0 || year > 99) { // 'Normal' dates
date = new Date(year, 6, 1);
} else if (year == 0) { // Year 0 is '1 BC'
date = new Date (-1, 6, 1);
} else { // Create arbitrary year and then set the correct year
// For full years, I chose to set the date to mid year (1st of July).
date = new Date(year, 6, 1);
date.setUTCFullYear(("0000" + year).slice(-4));
}
// Finally create the date
return date;
}
function toYear(date, bcString) {
// bcString is the prefix or postfix for BC dates.
// If bcString starts with '-' (minus),
// if will be placed in front of the year.
bcString = bcString || " BC" // With blank!
var year = date.getUTCFullYear();
if (year > 0) return year.toString();
if (bcString[0] == '-') return bcString + (-year);
return (-year) + bcString;
}
return timeline;
}