Published
Edited
Jan 12, 2019
3 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
allDelays = d3.tsv('https://raw.githubusercontent.com/alexmasselot/swiss-transport-data/master/data/SELECT_sm_operating_day_as_operating_day-'+selectedDay+'.tsv')
.then((rows)=>{
return _.chain(rows)
.filter((r) =>{
if(!railwayNetwork.connections[r.stop_id_0] || ! railwayNetwork.connections[r.stop_id_0][r.stop_id_1]){
//console.log(r.stop_name_0, r.stop_name_1)
//console.log(r.stop_id_0, r.stop_id_1)
return false
}
return true
})
.each((r,i)=>{
r.operating_day = r.operating_day?moment(parseInt(r.operating_day)*1000):undefined
r.arrival_time_0 = r.arrival_time_0?(parseInt(r.arrival_time_0)*1000):undefined
r.arrival_time_1 = r.arrival_time_1?(parseInt(r.arrival_time_1)*1000):undefined
r.departure_time_0 = r.departure_time_0?(parseInt(r.departure_time_0)*1000):undefined
r.departure_actual_0 = r.departure_actual_0?(parseInt(r.departure_actual_0)*1000):undefined
r.time_0 = r.arrival_time_0 || r.departure_time_0
r.arrival_actual_0 = r.arrival_actual_0?(parseInt(r.arrival_actual_0)*1000):undefined
r.actual_0 = r.arrival_actual_0 || r.departure_actual_0
r.arrival_actual_1 = r.arrival_actual_1?(parseInt(r.arrival_actual_1)*1000):undefined
r.lat_0 = parseFloat(r.lat_0)
r.lon_0 = parseFloat(r.lon_0)
r.lat_1 = parseFloat(r.lat_1)
r.lon_1 = parseFloat(r.lon_1)
r.path = railwayNetwork.connections[r.stop_id_0][r.stop_id_1]
r.arrival_delta_seconds_1 = parseFloat(r.arrival_delta_seconds_1)
r.key = i
r.is_cancelled = r.is_cancelled === 'true'
r.is_additional_driving = r.is_additional_driving === 'true'
})
.value()
})
Insert cell
Insert cell
Insert cell
d3Update = ()=>{
elClock.text(moment(selectedTimestamp).format('k:mm'))
const line = d3.line()
.x((p) => map.latLngToLayerPoint(L.latLng(p.lat, p.lon)).x)
.y((p)=> map.latLngToLayerPoint(L.latLng(p.lat, p.lon)).y)
gTrainPos.selectAll('path.segment')
.attr('d', (d) => line(d.curPos.schedPath))

gTrainPos.selectAll('circle.train')
.attr('cx', (d) => map.latLngToLayerPoint(L.latLng(d.curPos.lat, d.curPos.lon)).x)
.attr('cy', (d) => map.latLngToLayerPoint(L.latLng(d.curPos.lat, d.curPos.lon)).y)

gHisto.selectAll('rect.histo-bar')
.attr('y', (v) => {
return histoYScale(1-v.pc)
})
.attr('height', (v) => {
return histoYScale(v.pc)
})
elCancelled.text(stats.cancelled.count)
}
Insert cell
Insert cell
{
updateDelays(selectedTimestamp)
d3Enter()
d3Update()
}
Insert cell
Insert cell
map = {
let m = L.map(mapContainer,
{
zoomControl: false,
dragging: false,
boxZoom:false,
keyboard:false,
boxZoom:false,
scrollWheelZoom: false,
touchZoom:false
});
yield m

let osmLayer = L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}@2x.png', {
attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
}).addTo(m);
m.fitBounds([
[45.90,6.0141006],
[47.7247412,10.1079157]
]);
}
Insert cell
Insert cell
svg = d3.select(map.getPanes().overlayPane)
.append('svg')
.attr('height', map.getSize().y)
.attr('width', map.getSize().x)

Insert cell
Insert cell
Insert cell
dataLegend = [
{
delay:0,
text:'on time (<3 min)'
},
{
delay:4,
text:'slightly late (3-6 min)'
},
{
delay:7,
text:'late (>=6 min)'
}
]
Insert cell
elLegend = gLegend.selectAll('g.legend')
.data(dataLegend)
.enter()
.append('g')
.classed('legend', true)
.attr('transform', (undefined, i)=>'translate(0,'+(16*i)+')')

Insert cell
{
elLegend.append('line')
.attr('x1', 0 )
.attr('x2', (d) => d.delay*4)
.attr('y1',-3)
.attr('y2',-3)
.attr('r', (d) => radiusDelay(60*4))
.style('fill', 'black')
.attr('stroke', (d) => colorDelay(60*d.delay))
.attr('stroke-width', 2)

elLegend.append('circle')
.attr('cx', 0)
.attr('cy', -3)
.attr('r', (d) => radiusDelay(60*d.delay))
.style('fill', (d) => colorDelay(60*d.delay))
elLegend.append('text')
.attr('x', 36)
.text((d)=> d.text)

const gLine = gLegend.append('g')
.attr('transform', 'translate(0, '+(16*3)+')')

gLine.append('text')
.attr('x', -5)
.style('font-style', 'italic')
.text('line length: scheduled vs. actual lag')
}

Insert cell
Insert cell
gClock = svg
.append('g')
.classed('clock', true)
.attr('transform', 'translate('+(70+width/10+width/80)+', '+(width/15)+')')
Insert cell
elClock = gClock.append('text')
.style('font-size', 10+(width/40)+'pt')
.style('font-weight', 'bolder')
.style('opacity', 0.4)
.style('text-anchor', 'end')
Insert cell
Insert cell
stats = {
let h = []
_.times(15, (i) => h[i]={pc:0})
return {
histogramDelayPerMinute:h,
cancelled:{
count:0
}
}
}
Insert cell
Insert cell
gCancelled = svg
.append('g')
.classed('cancelles', true)
.attr('transform', 'translate('+(70+width/10+width/80)+', '+(width/8)+')')
Insert cell
elCancelled = {
const el = gCancelled.append('text')
.style('font-size', '400%')
.style('font-weight', 'bold')
.style('opacity', 0.5)
.style('fill', 'red')
.style('text-anchor', 'end')

yield el
gCancelled.append('text')
.attr('x',0)
.attr('y', 13)
.text('cancelled trains')
.style('font-weight', 'bold')
.style('opacity', 0.5)
.style('fill', 'red')
.style('text-anchor', 'end')
}
Insert cell
Insert cell
gHisto = svg.append('g')
.classed('histo', true)
.attr('transform', 'translate('+(width/80)+', -30)')
Insert cell
Insert cell
histoYScale = d3.scaleLinear()
.range([0, 200])
.domain([0, 1])
Insert cell
elHisto = {
const w = 70+width/10
const xScale = d3.scaleLinear()
.range([0, w])
.domain([0, stats.histogramDelayPerMinute.length+1])
gHisto.selectAll('rect.histo-bar').remove()
gHisto.selectAll('g.axis').remove()
const gAxis = gHisto.append("g")
.classed('axis', true)
.attr("transform", 'translate(0, ' + histoYScale(1)+ ')')
gAxis.call(d3.axisBottom(xScale)
.tickValues([0, 3, 6, 16])
)
gAxis.append('text')
.attr('x', w/2)
.attr('y', 30)
.style('fill','black')
.style('font-size', '10pt')
.text('delay distribution (per min)')
return gHisto.selectAll('rect.histo-bar')
.data(stats.histogramDelayPerMinute)
.enter()
.append('rect')
.classed('histo-bar', true)
.attr('x', (undefined, i) => xScale(i))
.attr('width', xScale(1))
.attr('fill', (undefined, i) => colorDelay(i*60))
}
Insert cell
Insert cell
Insert cell
gTrainPos = svg.append('g').classed('train-positions', true)
Insert cell
Insert cell
colorDelay=(delay)=>{
if(delay < 180){
return '#3182bd'
}
if(delay < 360){
return '#fec44f'
}
return '#d95f0e'
}
Insert cell
radiusDelay=(delay)=>{
if(delay < 180){
return 1+width/400
}
if(delay < 360){
return 1.5+width/300
}
return 2 +width/250
}
Insert cell
Insert cell
d3Enter = () => {
gTrainPos.selectAll('path.segment').remove();
gTrainPos.selectAll('path.segment')
.data(_.filter(currentDelays, (d)=> d.arrival_delta_seconds_1>180))
.enter()
.append('path')
.classed('segment', true)
.attr('stroke-width', 2)
.attr('stroke', (d) => colorDelay(d.arrival_delta_seconds_1))
.style('fill', 'none')
.exit()
.remove()
gTrainPos.selectAll('circle.train').remove()
gTrainPos.selectAll('circle.train')
.data(currentDelays)
.enter()
.append('circle')
.classed('train', true)
.attr('r', (d) => radiusDelay(d.arrival_delta_seconds_1))
.attr('fill', (d) => colorDelay(d.arrival_delta_seconds_1))
.exit()
.remove()
}
Insert cell
updateDelays = (ts)=>{
let interp = (x0, x1, t0, t1, ti) => {
if(x0 == x1){
return x0;
}
if(t0 == t1) {
return (x0+x1)/2
}
if(ti<t0){
ti=t0
}
if(ti>t1){
ti=t1
}
return x0 + (x1-x0)*(ti-t0)/(t1-t0)
}
currentDelays.length = 0
_.each(allDelays, (d)=>{
if(d.actual_0 <= ts && d.arrival_actual_1 >= ts){
currentDelays.push(d)
const ratioCur = (ts-d.actual_0)/(d.arrival_actual_1-d.actual_0)
const ratioSched = (ts+d.arrival_delta_seconds_1*1000-d.actual_0)/(d.arrival_actual_1-d.actual_0)
const pos = d.path.interpolatePosition(ratioCur)
const posSched = d.path.interpolatePosition(ratioSched)
d.curPos = {
lat : pos.lat,
lon : pos.lon,
schedPath : d.path.interpolatePartial(ratioCur, ratioSched)
}
}
})
const n = stats.histogramDelayPerMinute.length
_.times(n, (i)=> {stats.histogramDelayPerMinute[i].pc=0})
_.each(currentDelays, (d) => {
let i = Math.max(0, Math.floor(d.arrival_delta_seconds_1/60))
if(i>= n){
return
}
stats.histogramDelayPerMinute[i].pc+=1
})
const tot = currentDelays.length
_.times(n, (i)=> {stats.histogramDelayPerMinute[i].pc=stats.histogramDelayPerMinute[i].pc/tot});
stats.cancelled.count = _.chain(allDelays)
.filter('is_cancelled')
.filter((d) => d.time_0 <= selectedTimestamp && d.arrival_time_1 >= selectedTimestamp)
.size()
.value()
}
Insert cell
Insert cell
Insert cell
L = require('leaflet@1.3.4')
Insert cell
Insert cell
Insert cell
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more