Built with blockbuilder.org
Last active
June 14, 2020 20:54
-
-
Save GitNoise/4ec83d8edb8d5add7c9e to your computer and use it in GitHub Desktop.
Timeline with 2 streams and events
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
license: mit |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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