Skip to content

Instantly share code, notes, and snippets.

@emeeks
Last active August 19, 2022 14:22
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save emeeks/a666ed4846a7f8bb334d to your computer and use it in GitHub Desktop.
Save emeeks/a666ed4846a7f8bb334d to your computer and use it in GitHub Desktop.
Nested Timelines

Nested timelines in d3.layout.timeline.

You enable nested support by setting the timeline.children() accessor (which by default returns null) and pass the timeline either an array of time bands with child elements or a hierarchically structured JSON object (as in this example).

Child timeline heights are relative to the top-level parent(s).

Note that with nested timelines maxBandHeight only applies to the top-level parent (in the case of a single root node like this, it is the exact same as the height setting of timeline.size()).

{
"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},
{"label": "Expressionism",
"start": 1905,
"end": 1930},
{"label": "Die Brücke",
"start": 1905,
"end": 1913},
{"label": "Der Blaue Reiter",
"start": 1911,
"end": 1912},
{"label": "Bloomsbury Group",
"start": 1905,
"end": 1945}
]
},
{"label": "Contemporary Art",
"children": [
{"label": "Vienna School of Fantastic Realism",
"start": 1946,
"end": 1947},
{"label": "Neo-Dada",
"start": 1950,
"end": 1959},
{"label": "International Typographic Style",
"start": 1950,
"end": 1959},
{"label": "Soviet Nonconformist Art",
"start": 1953,
"end": 1986},
{"label": "Painters Eleven",
"start": 1954,
"end": 1960},
{"label": "Pop Art",
"start": 1953,
"end": 1957},
{"label": "Woodlands School",
"start": 1958,
"end": 1962},
{"label": "Situationism",
"start": 1957,
"end": 1973},
{"label": "New realism",
"start": 1960,
"end": 2015},
{"label": "Magic realism",
"start": 1960,
"end": 1969},
{"label": "Minimalism",
"start": 1960,
"end": 2015},
{"label": "Hard-edge painting",
"start": 1960,
"end": 1963},
{"label": "Fluxus",
"start": 1960,
"end": 1977},
{"label": "Happening",
"start": 1960,
"end": 2015}
]}
]
}
(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 = 1;
var projectedHierarchy = {id: "root", values: []};
function processTimelines(timelines, parentBand) {
if (!Array.isArray(timelines) && Array.isArray(childAccessor(timelines))) {
var rootnode = {id: 0, level: 0};
for (var x in timelines) {
if (timelines.hasOwnProperty(x)) {
rootnode[x] = timelines[x];
}
}
rootnode.id = 0;
rootnode.level = 0;
rootnode.values = [];
projectedHierarchy = rootnode;
processTimelines(childAccessor(timelines), rootnode);
rootnode.start = d3.min(rootnode.values, function (d) {return d.start});
rootnode.end = d3.max(rootnode.values, function (d) {return d.end});
processedTimelines.push(rootnode);
return;
}
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(projectedBand.values, function (d) {return d.start});
projectedBand.end = d3.max(projectedBand.values, function (d) {return d.end});
}
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;
projectedHierarchy = {id: "root", values: []};
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>Nested Timelines</title>
<meta charset="utf-8" />
<style type="text/css">
svg {
height: 1100px;
width: 1100px;
}
div.viz {
height: 1000px;
width: 1000px;
}
</style>
<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>
</head>
<body>
<div id="viz">
<svg style="background:white;" height=1100 width=1100>
</svg>
</div>
<footer>
</footer>
<script>
var colors = d3.scale.category10();
var timeline = d3.layout.timeline()
.size([1000,400])
.dateFormat(function (d) {return d})
.childAccessor(function (d) {return d.children})
.maxBandHeight(300)
.padding(2);
colors = d3.scale.ordinal()
.range(["#6798c1", "#677468", "#d7ce85", "#bf4730", "#9c5166"]);
d3.json("art_movements.json", function (json) {
timelineBands = timeline(json);
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("stroke-width", function (d) {return Math.max(0,(2 - d.level))})
.attr("rx", 3)
.style("fill", function (d) {return colors(d.level)})
.style("stroke", "black")
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", "14px")
})
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment