Skip to content

Instantly share code, notes, and snippets.

@hyponymous
Created April 12, 2020 00:35
Show Gist options
  • Save hyponymous/3e19f760a4215b9b18fbcc58fdd9e7b1 to your computer and use it in GitHub Desktop.
Save hyponymous/3e19f760a4215b9b18fbcc58fdd9e7b1 to your computer and use it in GitHub Desktop.
Multilane Timeline in d3
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"> </script>
<script src="https://d3js.org/d3.v5.min.js"></script>
<style type="text/css">
.timeline {
pointer-events: all;
}
.label {
font-family: "Helvetica Neue", sans-serif;
font-size: 9px;
}
.tick {
font-family: "Helvetica Neue", sans-serif;
font-size: 11px;
}
.gridline line {
stroke: lightgrey;
stroke-opacity: 0.7;
shape-rendering: crispEdges;
}
.gridline path {
stroke-width: 0;
}
</style>
</head>
<body>
<script>
const parseDate = dStr => dStr ? Date.parse(dStr) : new Date();
const isInterval = d => d.type === 'interval'
const genTimeline = (parent, data) => {
const flattenedData = _.flatMap(data, lane => lane.events.map(d => ({
...d,
swimlane: lane.swimlane,
})))
const allDates = _.chain(flattenedData)
.filter(isInterval)
.flatMap(i => [i.start, i.end])
.compact()
.map(parseDate)
.sortBy()
.value()
const totalWidth = d3.select(parent)
.node()
.getBoundingClientRect().width;
// set the dimensions and margins of the graph
const margin = {top: 20, right: 20, bottom: 30, left: 40},
width = totalWidth - margin.left - margin.right,
height = 80 * data.length - margin.top - margin.bottom;
// append the svg object to the body of the page
// append a 'group' element to 'svg'
// moves the 'group' element to the top left margin
const svg = d3.select(parent).append('svg')
.attr('class', 'timeline')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform',
'translate(' + margin.left + ',' + margin.top + ')');
const timeRange = new Date() - allDates[0];
const x_ = d3.scaleTime()
.domain([allDates[0] - 0.005 * timeRange, new Date()])
.range([0, width]);
const y_ = d3.scaleBand()
.domain(data.map(d => d.swimlane))
.range([0, height])
.padding(0.82);
let x = x_
let y = y_
// x gridlines;
const xGridlines = svg.append('g')
.attr('class', 'gridline')
.attr('transform', 'translate(0,' + height + ')');
const interval = svg.selectAll('.interval')
.data(flattenedData.filter(isInterval))
.enter().append('g')
.attr('class', 'interval');
const bar = interval.append('rect')
.attr('height', y.bandwidth())
.style('fill', (d, i) => d.color || d3.schemeCategory10[i % 10])
interval.append('text')
.attr('class', 'label')
.attr('dx', 0)
.attr('dy', 2 * y.bandwidth())
.text(d => d.label)
// add the x Axis
const xAxis = svg.append('g')
.attr('transform', 'translate(0,' + height + ')');
// add the y Axis
svg.append('g')
.call(d3.axisLeft(y));
const draw = () => {
interval
.attr('transform', d => `translate(${
x(parseDate(d.start))
}, ${
y(d.swimlane)
})`)
bar
.attr('width', d => x(parseDate(d.end)) - x(parseDate(d.start)))
xGridlines.call(d3.axisBottom(x)
.tickSize(-height)
.tickFormat(''))
xAxis.call(d3.axisBottom(x)
.tickArguments(d3.timeYear.every(1)))
}
draw();
svg.call(
d3.zoom()
.scaleExtent([1.0, 100.0])
.on('zoom', () => {
const transform = d3.event.transform;
x = transform.rescaleX(x_)
draw();
}))
}
genTimeline('body', [
{
"swimlane": "live",
"events": [
{
"type": "comment",
"value": "this is where I (used to) live"
},
{
"type": "interval",
"start": "2010/01/15",
"end": "2012/12/19",
"label": "Truffle Parlor",
"color": "#61813c"
},
{
"type": "interval",
"start": "2012/12/20",
"end": "2016/03/31",
"label": "Gumdrop Backwater",
"color": "#ead8e4"
},
{
"type": "interval",
"start": "2016/04/01",
"label": "The Obtrusive Saloon",
"color": "#908e72"
}
]
},
{
"swimlane": "work",
"events": [
{
"type": "comment",
"value": "this is where I (used to) work"
},
{
"type": "interval",
"start": "2010/01/18",
"end": "2010/11/31",
"label": "Pencil Fax Co."
},
{
"type": "interval",
"start": "2010/12/01",
"end": "2013/10/07",
"label": "Overbill Chatroom Ltd."
},
{
"type": "interval",
"start": "2014/05/26",
"label": "Spousal Recapture Inc."
}
]
}
]);
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment