Skip to content

Instantly share code, notes, and snippets.

@GitNoise
Last active June 14, 2020 20:54
Show Gist options
  • Save GitNoise/4ec83d8edb8d5add7c9e to your computer and use it in GitHub Desktop.
Save GitNoise/4ec83d8edb8d5add7c9e to your computer and use it in GitHub Desktop.
Timeline with 2 streams and events
license: mit
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/rough.js/3.1.0/rough.js"/></script>
<style>
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; color: #222; }
text {
font-family: Comic Sans MS;
color: #222;
font-size: 14px;
alignment-baseline: middle;
fill-opacity: 0.9;
}
.axis path,
.axis line {
stroke: #222;
stroke-opacity: 1;
}
.axis .ticks {
stroke: red:
}
.axis text {
font-family: Comic Sans MS;
font-size: 12px;
text-anchor: end;
transform: translate(-16px, 18px) rotate(-45deg);
fill-opacity: 1;
}
.axis .domain {
display: none;
}
.events path {
stroke: #222;
stroke-opacity: 0.4;
}
.edu path, .work path, .events path {
stroke: #222;
stroke-opacity: 0.5;
}
.legend text {
font-weight: bold;
fill: #222;
fill-opacity: 1;
filter: url(#outline);
}
</style>
</head>
<body>
<svg>
<defs>
<filter id="outline">
<feMorphology
in="SourceAlpha"
result="DILATED"
operator="dilate"
radius="2" />
<feFlood
flood-color="#fff"
flood-opacity="1"
result="OUTLINECOLOUR" />
<feComposite
in="OUTLINECOLOUR"
in2="DILATED"
operator="in"
result="OUTLINE" />
<feMerge>
<feMergeNode in="OUTLINE" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
</defs>
</svg>
<script>
d3.selection.prototype.moveToFront = function() {
return this.each(function(){
this.parentNode.appendChild(this);
});
};
var eduData = [
{
start: new Date('2002-09-01'),
end: new Date('2003-09-01'),
text: 'MSc, Intelligent Systems'
},
{
start: new Date('2008-09-01'),
end: new Date('2012-05-01'),
text: 'Master, Disaster Management'
},
{
start: new Date('2019-05-01'),
end: new Date('2019-06-01'),
text: 'UN OCHA, PREP Course'
}
];
var workData = [
{
start: new Date('2004-02-01'),
end: new Date('2005-01-01'),
text: 'Stockholm Filmfestival',
type: 'Fullstack'
},
{
start: new Date('2005-02-01'),
end: new Date('2005-05-01'),
text: 'SödraFot',
type: 'Fullstack'
},
{
start: new Date('2005-05-15'),
end: new Date('2008-01-01'),
text: 'Kentor',
type: 'Fullstack'
},
{
start: new Date('2008-01-15'),
end: new Date('2008-08-01'),
text: 'Famento',
type: 'Fullstack'
},
{
start: new Date('2009-05-15'),
end: new Date('2011-10-01'),
text: 'UNOPS',
type: 'Fullstack'
},
{
start: new Date('2011-10-15'),
end: new Date('2014-10-01'),
text: 'Capgemini',
type: 'Fullstack'
},
{
start: new Date('2014-10-15'),
end: new Date('2016-08-01'),
text: 'Consid',
type: 'Fullstack'
},
{
start: new Date('2016-08-15'),
end: new Date(),
text: 'Aftonbladet',
type: 'Data visualization & FE'
}
];
var eventData = [
{ date: new Date('2006-01-01'), text: 'Moved to China' },
{ date: new Date('2008-08-01'), text: 'Moved to Denmark' },
{ date: new Date('2009-09-09'), text: 'First child' },
{ date: new Date('2009-11-01'), text: 'Joined SBTF' },
{ date: new Date('2011-10-01'), text: 'Moved to Sweden' },
{ date: new Date('2011-11-09'), text: 'Second child' },
{ date: new Date('2012-04-20'), text: 'MSB Disaster Response roster' },
{ date: new Date('2017-02-01'), text: 'Founded DataViz Stockholm meetup' },
{ date: new Date('2017-08-19'), text: 'Third child' },
];
// configuration
var config = {
left: 20,
right: 155,
width: 1200,
height: 400
};
var svg = d3.select("svg").attr({
width: `${config.width}px`,
height: `${config.height}px`,
});
var offset = 50;
var axisYPos = config.height/2;
var eventOffset = -140;
var eduWorkOffset = 32;
var workEduTextDist = 10;
const lines = 3;
const offsetFunc = i =>
32 + eduWorkOffset*(i%lines)
// Scales
var xScale = d3.time.scale()
.domain([new Date('2002-09-01'), new Date()])
.range([config.left, config.width - config.right]);
var yScale = d3.scale.linear()
.domain([0, eventData.length])
.range([config.top, axisYPos - 20]);
// Axes
var xAxis = d3.svg.axis()
.scale(xScale)
.orient("bottom");
// Rendering
var svg = d3.select("svg");
/** BANDS **/
const bands = svg.append('g')
.classed("bands", true);
// work
bands.append('g')
.classed("time", true)
.selectAll("rect.range")
.data(workData).enter()
.append("rect")
.classed("range", true).classed("work", true)
.attr({
x: d => xScale(d.start),
y: axisYPos - 18 + 4,
height: 18,
width: d => xScale(d.end) - xScale(d.start),
});
// education
bands.append('g').selectAll('.range')
.data(eduData)
.enter()
.append("rect")
.classed("range", true).classed("edu", true)
.attr({
x: d => xScale(d.start),
y: axisYPos - 4,
height: 18,
width: d => xScale(d.end) - xScale(d.start),
});
/** EVENTS **/
const eventYOffset = 32;
const eventYStart = 180;
const events = svg.append("g")
.classed('events', true)
const event = events.selectAll('event')
.data(eventData)
.enter()
.append('g')
.classed('event', true);
event
.append("circle")
.classed("start", true)
.attr({
cx: d => xScale(d.date),
cy: axisYPos,
r: 2
});
event.append('line')
.attr({
x1: d => xScale(d.date),
x2: d => xScale(d.date),
y1: axisYPos,
y2: (d,i) => axisYPos + eventYStart - (i+2)%3 * eventYOffset
});
var circleRadius = 5;
event.append("circle")
.classed("end", true)
.attr({
cx: d => xScale(d.date),
cy: (d,i) => axisYPos + eventYStart - (i+2)%3 * eventYOffset,
r: circleRadius
});
event.each(function(d, i) {
const el = d3.select(this.parentNode.parentNode)
el
.append("text")
.attr('filter', "url(#outline)")
.attr({
x: xScale(d.date) + circleRadius*2,
y: axisYPos + eventYStart - (i+2)%3 * eventYOffset + 1.5,
})
.text(d.text);
})
/** WORK **/
var work = svg.append("g")
.classed("work", true);
work.append("g")
.classed("workText", true)
.selectAll("text")
.data(workData).enter()
.append("text").attr({
x: (d,i) => xScale(d.start) + offsetFunc(i) + 8 + 3,
y: (d,i) => axisYPos - offsetFunc(i) - 14
})
.text(d => d.text)
.each(function(d) {
d.bbox = this.getBBox();
});
work.append("g")
.classed("txtBkgrnd", true)
.selectAll("rect")
.data(workData).enter()
.append("rect")
.attr({
x: d => d.bbox.x - 8,
y: d => d.bbox.y - 4,
width: d => d.bbox.width + 16,
height: d => d.bbox.height + 8
})
.each(function(d) {
d.rectBBox = this.getBBox();
});
var workLines = work.append("g")
.classed("workLines", true)
.selectAll("line")
.data(workData)
.enter()
workLines.append("line")
.attr({
x1: d => xScale(d.start) + 3,
x2: d => d.rectBBox.x,
y1: axisYPos - 14,
y2: d =>
d.rectBBox.y + d.rectBBox.height/2
})
/** EDUCATION **/
var edu = svg.append("g")
.classed("edu", true);
edu.append("g")
.classed("eduText", true)
.selectAll("text")
.data(eduData)
.enter()
.append("text")
.attr({
x: d => xScale(d.start) + 8 + 4 +
workEduTextDist * 2,
y: axisYPos +
workEduTextDist * 2 +
32 + 8 + 3
})
.text(d => d.text)
.each(function(d) {
d.bbox = this.getBBox();
});
// text background
edu.append("g")
.classed("txtBkgrnd", true)
.selectAll("rect")
.data(eduData).enter()
.append("rect")
.attr({
x: d => d.bbox.x - 8,
y: d => d.bbox.y - 4,
width: d => d.bbox.width + 16,
height: d => d.bbox.height + 8
})
.each(function(d) {
d.rectBBox = this.getBBox();
});
var eduLines = edu
.append("g")
.classed("eduLine", true)
.selectAll("g")
.data(eduData)
.enter()
.append("g");
eduLines.append("line")
.attr({
x1: d => xScale(d.start) + 3,
x2: d => d.rectBBox.x,
y1: axisYPos + 14,
y2: d =>
d.rectBBox.y + d.rectBBox.height/2
})
d3.selectAll(".edu .eduText")
.moveToFront();
/** AXIS **/
var axis = svg.append("g")
.classed("axis", true)
.call(xAxis)
axis.append('line')
.attr({
x1: xScale.range()[1],
x2: xScale.range()[0]
})
axis
.attr('transform', `translate(0,${axisYPos})`)
axis.selectAll('text')
.attr('filter', "url(#outline)")
const createLegendBox = (container, text, className) =>
{
const boxWidth = 100;
const textEl = container
.append('text')
.attr('x', boxWidth/2)
.attr('y', 12)
.style('text-anchor', 'middle')
.text(text)
const legendWorkBBox = textEl.node().getBBox();
container.append('rect')
.classed(className, true)
.attr('x', 0)
.attr('y', 0)
.attr('width', boxWidth)
.attr('height', legendWorkBBox.height + 4)
.style('fill', 'none')
.style('stroke', '#222')
}
const legendWork = svg
.append('g')
.classed('legend', true)
.attr('transform', `translate(${config.left}, ${config.height - 30})`)
const legendEducation = svg
.append('g')
.classed('legend', true)
.attr('transform', `translate(${config.left + 100 + 8}, ${config.height - 30})`)
createLegendBox(legendWork, 'Work', 'work')
createLegendBox(legendEducation, 'Education', 'edu')
const legendEvents = svg
.append('g')
.classed('legend', true)
.attr('transform', `translate(${config.left + 100*2 + 8*2}, ${config.height - 30})`)
legendEvents.append('circle')
.attr('cx', 2.5)
.attr('cy', 10)
.attr('r', 5)
legendEvents.append('text')
.attr('x', 12)
.attr('y', 12)
.text('Major events')
/*** MAKE IT ROUGH! ***/
const rc = rough.svg(svg.node());
// lines to rough lines
const allLines = d3.selectAll('line')
allLines.each(function() {
const aLine = d3.select(this);
const container = d3.select(this.parentNode);
//line (x1, y1, x2, y2 [, options])
const rLine = rc.line(
+aLine.attr("x1"),
+aLine.attr("y1"),
+aLine.attr("x2"),
+aLine.attr("y2"));
container.node().appendChild(rLine);
aLine.remove();
})
// rect to rough rect
const allRects = d3.selectAll('rect')
allRects.each(function() {
const aRect = d3.select(this);
const container = d3.select(this.parentNode);
let options = { fill: '#fff', fillStyle: 'solid' };
const aReactClass = aRect.attr('class')
if (aReactClass) {
options = {
fill: (aReactClass.indexOf('work') > -1 ? '#fec763': '#a992fa'),
fillStyle: 'zigzag'
}
}
const rRect = rc.rectangle(
+aRect.attr("x"),
+aRect.attr("y"),
+aRect.attr("width"),
+aRect.attr("height"),
options);
container.node().appendChild(rRect);
aRect.remove();
})
// circle to rough circle
const allCircles = d3.selectAll('circle.end, .legend circle')
allCircles.each(function() {
const aCircle = d3.select(this);
const container = d3.select(this.parentNode);
let options = { fill: '#fff', fillStyle: 'solid' };
const rCircle = rc.circle(
+aCircle.attr("cx"),
+aCircle.attr("cy"),
+aCircle.attr("r")*2,
options);
container.node().appendChild(rCircle);
aCircle.remove();
})
/**** REORDER ****/
d3.selectAll(".txtBkgrnd")
.moveToFront();
d3.selectAll(".workText")
.moveToFront();
d3.selectAll(".eduText")
.moveToFront();
d3.selectAll('.legend text')
.moveToFront();
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment