|
<!DOCTYPE html> |
|
<meta charset='utf-8'> |
|
<style> |
|
body { |
|
background: black; |
|
} |
|
svg { |
|
font: 15px sans-serif; |
|
} |
|
.title { |
|
font: 20px sans-serif; |
|
} |
|
text { |
|
fill: white; |
|
} |
|
.progress rect { |
|
fill: #555; |
|
} |
|
.progress circle { |
|
fill: #ccc; |
|
} |
|
|
|
</style> |
|
<script src="//d3js.org/d3.v3.min.js" charset="utf-8"></script> |
|
<body> |
|
<script> |
|
// Basic dimensions. |
|
var margin = { top: 40, right: 40, bottom: 40, left: 40 }, |
|
html = document.documentElement, |
|
width = html.clientWidth - margin.left - margin.right, |
|
height = html.clientHeight - margin.top - margin.bottom, |
|
aspect = height / width; |
|
|
|
var datadate = d3.time.format("%Y-%m-%d"); |
|
var dateformat = d3.time.format("%b. %e, %Y"); |
|
|
|
var disc_radius = Math.min(width,height) * 0.25; |
|
var expire_time = 2500; |
|
var max_particles = 400; |
|
|
|
|
|
var leaves = []; |
|
var colors = { |
|
'-2': ['#A7B8B4', '#dddddd'], |
|
'-1': ['#03FBD5', '#A7B8B4'], |
|
'0': ['#1BD3F8', '#03FBD5'], |
|
'1': ['#03FBD5', '#C4E64B'], |
|
'2': ['#C4E64B', '#FED84B'], |
|
'3': ['#F29B5A', '#ED5363'], |
|
}; |
|
colors['-0'] = colors['0']; |
|
|
|
function addRandomPos ( obj, r ) { |
|
var a = Math.random(), |
|
b = Math.random(), |
|
t; |
|
if ( b < a ) { |
|
t = b; b = a; a = t; |
|
} |
|
obj.x = b * r * Math.cos( 2 * Math.PI * a / b ); |
|
obj.y = b * r * Math.sin( 2 * Math.PI * a / b ); |
|
return obj; |
|
} |
|
|
|
function makeParticle ( fact, static ) { |
|
var t = '' + Math.round(fact.temp / 5); |
|
return addRandomPos({ |
|
'birth': Date.now(), |
|
'static': Math.random() > .6, |
|
'weight': Math.random(), |
|
'color': colors[ t ][ Math.random() > .5 ? 1 : 0 ], |
|
'r': 1, |
|
}, disc_radius); |
|
} |
|
|
|
|
|
function parseRow ( d ) { |
|
return { |
|
'time': datadate.parse(d.GMT), |
|
'speed': +d[' Mean Wind SpeedKm/h'], |
|
'temp': +d['Mean TemperatureC'], |
|
'dir': +d.WindDirDegrees |
|
}; |
|
} |
|
|
|
var svg = d3.select( 'body' ).append( 'svg' ) |
|
.attr( 'width', width + margin.left + margin.right ) |
|
.attr( 'height', height + margin.top + margin.bottom ) |
|
.append( 'g' ) |
|
.attr( 'transform', `translate(${[ margin.left, margin.top ]})` ); |
|
|
|
// particles container |
|
var part = svg.append('g') |
|
.attr('transform', 'translate('+[width/2, height/2]+')'); |
|
|
|
// progess bar |
|
var progress = svg.append('g') |
|
.attr('transform', 'translate('+[0,height]+')') |
|
.attr('class', 'progress'); |
|
progress.append('rect') |
|
.attr('width', width) |
|
.attr('rx', 2) |
|
.attr('height', 5); |
|
var place = progress.append('circle') |
|
.attr('cx', 2.5) |
|
.attr('cy', 2.5) |
|
.attr('r', 2.5); |
|
|
|
// legend |
|
var legend = svg.append('g') |
|
.attr('class', 'legend'); |
|
legend.append('text') |
|
.attr('class', 'title') |
|
.attr('dy', '.5em') |
|
.attr('y', 0) |
|
.text('Reykjavík'); |
|
var clock = legend.append('text') |
|
.attr('class', 'line') |
|
.attr('dy', '.5em') |
|
.attr('y', 30); |
|
var line1 = legend.append('text') |
|
.attr('class', 'line') |
|
.attr('dy', '.5em') |
|
.attr('y', 50); |
|
var line2 = legend.append('text') |
|
.attr('class', 'line') |
|
.attr('dy', '.5em') |
|
.attr('y', 70); |
|
var line3 = legend.append('text') |
|
.attr('class', 'line') |
|
.attr('dy', '.5em') |
|
.attr('y', 90); |
|
|
|
|
|
d3.csv( 'data.csv', parseRow, data => { |
|
|
|
data.forEach((d, i) => { |
|
var pos = data[i - 1] || { x: 0, y: 0 }, |
|
a = ( d.dir - 90 - 180 ) * Math.PI / 180, |
|
r = d.speed, |
|
dx = r * Math.cos( a ), |
|
dy = r * Math.sin( a ); |
|
d.x = pos.x + dx; |
|
d.y = pos.y + (dy / aspect); |
|
}) |
|
data = data.filter( d => d.time.getUTCFullYear() === 2015 ); |
|
|
|
var x = d3.time.scale() |
|
.domain(d3.extent(data, d => d.time)) |
|
.nice(d3.time.date) |
|
.range([2.5, width-2.5]); |
|
|
|
var starttime = Date.now(); |
|
var frame = () => { |
|
var ctime = Date.now() - starttime; |
|
var currentPos = ~~( ctime / 1000 ); |
|
var fact = data[ currentPos % data.length ]; |
|
|
|
place.attr('cx', x(fact.time)); |
|
|
|
clock.text( dateformat( fact.time ) ); |
|
line1.text( 'Wind speed: ' + Math.round(fact.speed * 0.277778) + ' m/s' ); |
|
line2.text( 'Wind direction: ' + fact.dir + '°' ); |
|
line3.text( 'Temparature: ' + fact.temp + ' °C' ); |
|
|
|
leaves.forEach( d => { |
|
var t = ( Date.now() - d.birth ); |
|
if ( t > expire_time ) { d.dead = true; } |
|
d.r = Math.max( 0, (expire_time - t) / expire_time ); |
|
|
|
var a = ( Math.random() * 360 ) * Math.PI / 180, |
|
r = Math.random() * ( d.static ? 2 : 1 ), |
|
dx = r * Math.cos( a ), |
|
dy = r * Math.sin( a ); |
|
d.x = d.x + dx; |
|
d.y = d.y + dy; |
|
|
|
if ( !d.static ) { |
|
var smoothness = Math.round( 80 * d.weight ) || 1; |
|
|
|
if ( d.dir == null ) { d.dir = fact.dir; } |
|
d.dir = ( fact.dir + ( d.dir * (smoothness-1) ) ) / smoothness; |
|
|
|
if ( d.speed == null ) { d.speed = fact.speed; } |
|
d.speed = ( fact.speed + ( d.speed * (smoothness-1) ) ) / smoothness; |
|
|
|
var a = ( d.dir - 90 - 180 ) * Math.PI / 180, |
|
r = ( d.speed / 10 ) * d.weight, |
|
dx = r * Math.cos( a ), |
|
dy = r * Math.sin( a ); |
|
d.x = d.x + dx; |
|
d.y = d.y + dy; |
|
} |
|
}); |
|
|
|
leaves = leaves.filter( d => !d.dead ); |
|
|
|
if ( leaves.length < max_particles ) { |
|
// add only a few particles at a time |
|
leaves.push( makeParticle(fact) ); |
|
leaves.push( makeParticle(fact) ); |
|
leaves.push( makeParticle(fact) ); |
|
} |
|
|
|
var particles = part.selectAll('circle') |
|
.data( leaves ); |
|
particles.enter() |
|
.append('circle'); |
|
particles |
|
.attr('fill', d => d.color || '#fff' ) |
|
.attr('r', d => d.r * 2) |
|
.attr('cx', d => d.x) |
|
.attr('cy', d => d.y); |
|
particles.exit() |
|
.remove(); |
|
}; |
|
|
|
|
|
function loop () { |
|
frame(); |
|
requestAnimationFrame( loop ); |
|
} |
|
loop(); |
|
|
|
}); |
|
</script> |