Skip to content

Instantly share code, notes, and snippets.

@emeeks
Last active March 16, 2016 15:46
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 emeeks/720351b9ea5bbe62ece2 to your computer and use it in GitHub Desktop.
Save emeeks/720351b9ea5bbe62ece2 to your computer and use it in GitHub Desktop.
Hierarchical Timeline Visualization

Experimental support for nested timelines in d3.layout.timeline. Positions and sizes nested timelines inside parents and derives the parent start and end date from the min and max start and end dates of the children.

This is not currently supported in the published version.

Hover over bands to see labels.

{
"label": "Art Movements",
"children": [
{"label": "Renaissance",
"children": [
{"label": "Italian Renaissance",
"start": 1275,
"end": 1602},
{"label": "Renaissance Classicism",
"start": 1475,
"end": 1602},
{"label": "Early Netherlandish Painting",
"start": 1400,
"end": 1500}
]
},
{"label": "Renaissance to Neoclassicism",
"children": [
{"label": "Mannerism and Late Renaissance",
"start": 1520,
"end": 1600},
{"label": "Boroque",
"start": 1600,
"end": 1730},
{"label": "Dutch Golden Age Painting",
"start": 1702,
"end": 1730},
{"label": "Flemish Boroque Painting",
"start": 1585,
"end": 1700},
{"label": "Rococo",
"start": 1720,
"end": 1780},
{"label": "Neoclassicism",
"start": 1750,
"end": 1830}
]
},
{"label": "Romanticism",
"children": [
{"label": "Nazarene Movement",
"start": 1820,
"end": 1847},
{"label": "The Ancients",
"start": 1825,
"end": 1845},
{"label": "Purismo",
"start": 1825,
"end": 1845},
{"label": "Düsseldorf school",
"start": 1825,
"end": 1865},
{"label": "Hudson River school",
"start": 1855,
"end": 1880},
{"label": "Luminism",
"start": 1855,
"end": 1875}
]
},
{"label": "Romanticism to Modern Art",
"children": [
{"label": "Norwich school",
"start": 1803,
"end": 1833},
{"label": "Biedermeier",
"start": 1815,
"end": 1848},
{"label": "Photography",
"start": 1830,
"end": 2015},
{"label": "Realism",
"start": 1830,
"end": 1870},
{"label": "Barbizon school ",
"start": 1830,
"end": 1870},
{"label": "Peredvizhniki",
"start": 1870,
"end": 1923},
{"label": "Hague School",
"start": 1870,
"end": 1900},
{"label": "American Barbizon school",
"start": 1853,
"end": 1895},
{"label": "Spanish Eclecticism",
"start": 1845,
"end": 1890},
{"label": "Macchiaioli",
"start": 1850,
"end": 1859},
{"label": "Pre-Raphaelite Brotherhood",
"start": 1848,
"end": 1854}
]
},
{"label": "Modern Art",
"children": [
{"label": "Impressionism",
"start": 1860,
"end": 1890,
"children": [
{"label": "American Impressionism",
"start": 1880,
"end": 1890},
{"label": "Cos Cob Art Colony",
"start": 1890,
"end": 1899},
{"label": "Heidelberg School",
"start": 1887,
"end": 1890}
]},
{"label": "Arts and Crafts",
"start": 1880,
"end": 1910},
{"label": "Tonalism",
"start": 1880,
"end": 1920},
{"label": "Symbolism",
"start": 1880,
"end": 1910,
"children": [
{"label": "Russian Symbolism",
"start": 1884,
"end": 1910},
{"label": "Aesthetic Movement",
"start": 1868,
"end": 1901}
]},
{"label": "Post-Impressionism",
"start": 1886,
"end": 1905,
"children": [
{"label": "Les Nabis",
"start": 1888,
"end": 1900},
{"label": "Cloisonnism",
"start": 1884,
"end": 1886},
{"label": "Synthetism",
"start": 1887,
"end": 1893}
]},
{"label": "Neo-Impressionism",
"start": 1886,
"end": 1906,
"children": [
{"label": "Pointillism",
"start": 1879,
"end": 1906},
{"label": "Divisionism",
"start": 1880,
"end": 1880}
]},
{"label": "Art Nouveau",
"start": 1890,
"end": 1914,
"children": [
{"label": "Vienna Secession",
"start": 1897,
"end": 1914},
{"label": "Jugendstil",
"start": 1890,
"end": 1914},
{"label": "Modernisme",
"start": 1890,
"end": 1910}
]},
{"label": "Russian avant-garde",
"start": 1890,
"end": 1930},
{"label": "Art à la Rue",
"start": 1890,
"end": 1905},
{"label": "Young Poland",
"start": 1890,
"end": 1918},
{"label": "Mir iskusstva",
"start": 1889,
"end": 1900},
{"label": "Hagenbund",
"start": 1900,
"end": 1930},
{"label": "Fauvism",
"start": 1904,
"end": 1909}
]
}
]
}
(function() {
d3.layout.timeline = function() {
var timelines = [];
var dateAccessor = function (d) {return new Date(d)};
var processedTimelines = [];
var startAccessor = function (d) {return d.start};
var endAccessor = function (d) {return d.end};
var size = [500,100];
var timelineExtent = [-Infinity, Infinity];
var setExtent = [];
var displayScale = d3.scale.linear();
var swimlanes = {root: []};
var swimlaneNumber = 1;
var padding = 0;
var fixedExtent = false;
var maximumHeight = Infinity;
var childAccessor = function (d) {return null};
var bandID = 0;
var projectedHierarchy = {id: "root", values: []};
function processTimelines(timelines, parentBand) {
timelines.forEach(function (band) {
var projectedBand = {level: 0, id: bandID};
if (parentBand !== undefined) {
projectedBand.parent = parentBand;
}
bandID++;
for (var x in band) {
if (band.hasOwnProperty(x)) {
projectedBand[x] = band[x];
}
}
if (Array.isArray(childAccessor(band))) {
processTimelines(childAccessor(band), projectedBand);
projectedBand.start = d3.min(childAccessor(band), function (d) {return dateAccessor(startAccessor(d))});
projectedBand.end = d3.max(childAccessor(band), function (d) {return dateAccessor(endAccessor(d))});
}
else {
projectedBand.start = dateAccessor(startAccessor(band));
projectedBand.end = dateAccessor(endAccessor(band));
}
projectedBand.lane = 0;
processedTimelines.push(projectedBand);
if (parentBand) {
if (!parentBand.values) {
parentBand.values = [];
}
parentBand.values.push(projectedBand);
}
if (parentBand === undefined) {
projectedHierarchy.values.push(projectedBand);
}
});
}
function projectTimelines() {
if (fixedExtent === false) {
var minStart = d3.min(processedTimelines, function (d) {return d.start});
var maxEnd = d3.max(processedTimelines, function (d) {return d.end});
timelineExtent = [minStart,maxEnd];
}
else {
timelineExtent = [dateAccessor(setExtent[0]), dateAccessor(setExtent[1])];
}
displayScale.domain(timelineExtent).range([0,size[0]]);
processedTimelines.forEach(function (band) {
band.originalStart = band.start;
band.originalEnd = band.end;
band.start = displayScale(band.start);
band.end = displayScale(band.end);
});
}
function fitsIn(lane, band) {
if (lane.end < band.start || lane.start > band.end) {
return true;
}
var filteredLane = lane.filter(function (d) {return d.start <= band.end && d.end >= band.start});
if (filteredLane.length === 0) {
return true;
}
return false;
}
function findlane(band) {
//make the first array
var swimlane = swimlanes["root"];
if (band.parent) {
swimlane = swimlanes[band.parent.id];
}
if (swimlane === undefined) {
swimlanes[band.parent.id] = [[band]];
swimlane = swimlanes[band.parent.id];
swimlaneNumber++;
return;
}
var l = swimlane.length - 1;
var x = 0;
while (x <= l) {
if (fitsIn(swimlane[x], band)) {
swimlane[x].push(band);
return;
}
x++;
}
swimlane[x] = [band];
return;
}
function timeline(data) {
if (!arguments.length) return timeline;
processedTimelines = [];
swimlanes = {root: []};
processTimelines(data);
projectTimelines(data);
processedTimelines.forEach(function (band) {
findlane(band);
});
for (var x in swimlanes) {
swimlanes[x].forEach(function (lane, i) {
var height = size[1] / swimlanes[x].length;
height = Math.min(height, maximumHeight);
lane.forEach(function (band) {
band.y = i * (height) + (padding / 2);
band.dy = height - padding;
band.lane = i;
band.dyp = 1 / swimlanes[x].length;
});
});
}
projectedHierarchy.values.forEach(relativePosition);
processedTimelines.sort(function (a, b) {
if (a.level > b.level) {
return 1;
}
if (a.level < b.level) {
return -1;
}
return 1;
});
return processedTimelines;
}
function relativePosition(band, i) {
if (!band.parent) {
band.level = 0;
}
else {
band.level = band.parent.level + 1;
var height = band.dyp * band.parent.dy;
band.y = band.parent.y + (band.lane * height) + (padding / 2);
band.dy = Math.max(1, height - padding);
}
if (band.values) {
band.values.forEach(relativePosition);
}
}
timeline.childAccessor = function (_x) {
if (!arguments.length) return childAccessor;
childAccessor = _x;
return timeline;
}
timeline.dateFormat = function (_x) {
if (!arguments.length) return dateAccessor;
dateAccessor = _x;
return timeline;
}
timeline.bandStart = function (_x) {
if (!arguments.length) return startAccessor;
startAccessor = _x;
return timeline;
}
timeline.bandEnd = function (_x) {
if (!arguments.length) return endAccessor;
endAccessor = _x;
return timeline;
}
timeline.size = function (_x) {
if (!arguments.length) return size;
size = _x;
return timeline;
}
timeline.padding = function (_x) {
if (!arguments.length) return padding;
padding = _x;
return timeline;
}
timeline.extent = function (_x) {
if (!arguments.length) return timelineExtent;
fixedExtent = true;
setExtent = _x;
if (_x.length === 0) {
fixedExtent = false;
}
return timeline;
}
timeline.maxBandHeight = function (_x) {
if (!arguments.length) return maximumHeight;
maximumHeight = _x;
return timeline;
}
return timeline;
}
})();
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Timeline with Dates</title>
<meta charset="utf-8" />
<style type="text/css">
svg {
height: 1100px;
width: 1100px;
}
div.viz {
height: 1000px;
width: 1000px;
}
</style>
</head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.16/d3.min.js" charset="utf-8" type="text/javascript"></script>
<script src="d3.layout.timeline.js" charset="utf-8" type="text/javascript"></script>
<script>
var colors = d3.scale.ordinal().range(["#f5e0b7", "#5b1e37", "#b9e3c5", "#827abf", "#f62150", "#6f89b6"]);
var timeline = d3.layout.timeline()
.size([1000,400])
.dateFormat(function (d) {return d})
.childAccessor(function (d) {return d.children})
.padding(1);
colorScale = d3.scale.ordinal()
.domain(["European","Native","Colonial","Latin America","Internal"])
.range(["#96abb1", "#313746", "#b0909d", "#687a97", "#292014"]);
d3.json("art_movements.json", function (json) {
timelineBands = timeline(json.children);
d3.select("svg").selectAll("g")
.data(timelineBands)
.enter()
.append("g")
.attr("class", "band")
.on("mouseover", function (d) {d3.selectAll("text").style("opacity", function (p) {return p.label === d.label ? 1 : 0})})
.on("mouseout", function (d) {d3.selectAll("text").style("opacity", 0)})
d3.selectAll("g.band")
.append("rect")
.attr("x", function (d) {return d.start})
.attr("y", function (d) {return d.y})
.attr("height", function (d) {return d.dy})
.attr("width", function (d) {return d.end - d.start})
.style("fill", function (d) {return colorScale(d.sphere)})
.style("stroke", "black")
.style("stroke-width", 1)
.style("opacity", 0.5)
.style("fill", function (d) {return colors(d.level)})
d3.select("svg")
.selectAll("text")
.data(timelineBands)
.enter()
.append("text")
.attr("x", function (d) {return (d.start + d.end) / 2})
.attr("y", function (d) {return d.y + (d.dy / 2)})
.text(function(d) {return d.label})
.style("opacity", 0)
.style("pointer-events", "none")
.style("text-anchor", "middle")
.style("font-size", function (d) {return (d.dy / d.label.length) * 4})
})
</script>
<body>
<div id="viz">
<svg style="background:white;" height=1100 width=1100>
</svg>
</div>
<footer>
</footer>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment