A weather wheel that queries Weather Underground APIs on the fly. LIRP is the ICAO code for Pisa. Temperatures, precipitation and cloud cover are depicted as bars. See also an older example for credits and more information.
// Generated by CoffeeScript 1.10.0
(function() {
var INNER_RADIUS, OUTER_OUTER_OUTER_RADIUS, OUTER_OUTER_RADIUS, OUTER_RADIUS, arc_generator, height, r_cloud, r_prec, r_temp, refs, refs_data, refs_labels_north, refs_labels_south, station, svg, temp_color, width, year;
svg = d3.select('svg');
width = svg.node().getBoundingClientRect().width;
height = svg.node().getBoundingClientRect().height;
svg.attr({
viewBox: (-width / 2) + " " + (-height / 2) + " " + width + " " + height
});
OUTER_OUTER_OUTER_RADIUS = 230;
OUTER_OUTER_RADIUS = 200;
OUTER_RADIUS = 180;
INNER_RADIUS = 40;
r_temp = d3.scale.linear().domain([-40, 40]).range([INNER_RADIUS, OUTER_RADIUS]);
r_prec = d3.scale.linear().domain([0, 30]).range([OUTER_OUTER_RADIUS, OUTER_RADIUS]);
r_cloud = d3.scale.linear().domain([0, 10]).range([OUTER_OUTER_RADIUS, OUTER_OUTER_OUTER_RADIUS]);
temp_color = d3.scale.linear().domain([-20, 0, 20, 40]).range([d3.hcl(290, 70, 15), d3.hcl(230, 70, 45), d3.hcl(80, 70, 75), d3.hcl(10, 70, 45)]).interpolate(d3.interpolateHcl);
arc_generator = d3_shape.arc();
refs_data = d3.range(-20, 40, 10);
refs = svg.selectAll('.ref').data(refs_data);
refs.enter().append('circle').attr({
"class": 'ref',
r: function(d) {
return r_temp(d);
},
stroke: function(d) {
if (d === 0) {
return '#CCC';
} else {
return '#EEE';
}
}
});
refs_labels_north = svg.selectAll('.ref_label_north').data(refs_data);
refs_labels_north.enter().append('text').text(function(d) {
return d + "°C";
}).attr({
"class": 'ref_label_north',
y: function(d) {
return -r_temp(d);
},
dy: '0.35em',
fill: function(d) {
if (d === 0) {
return '#BBB';
} else {
return '#DDD';
}
}
});
refs_labels_south = svg.selectAll('.ref_label_south').data(refs_data);
refs_labels_south.enter().append('text').text(function(d) {
return d + "°C";
}).attr({
"class": 'ref_label_south',
y: function(d) {
return r_temp(d);
},
dy: '0.35em',
fill: function(d) {
if (d === 0) {
return '#BBB';
} else {
return '#DDD';
}
}
});
svg.append('circle').attr({
"class": 'prec_ref',
r: OUTER_RADIUS
});
station = 'LIRP';
year = 2015;
d3.csv("wu_get_history.php?station=" + station + "&year=" + year, function(days) {
var angle, cloud_bars, enter_cloud_bars, enter_prec_bars, enter_temp_bars, prec_bars, temp_bars;
days.forEach(function(d) {
d.t = d['CET'];
d['MinTemperatureC'] = +d['MinTemperatureC'];
d['MaxTemperatureC'] = +d['MaxTemperatureC'];
d['MeanTemperatureC'] = +d['MeanTemperatureC'];
d['Precipitationmm'] = +d['Precipitationmm'];
return d['CloudCover'] = +d['CloudCover'];
});
svg.append('text').text(station).attr({
"class": 'title',
dy: '0.35em'
});
angle = d3.scale.ordinal().domain(days.map(function(d) {
return d.t;
})).range(d3.range(Math.PI / 2, 2 * Math.PI + Math.PI / 2, 2 * Math.PI / days.length));
temp_bars = svg.selectAll('.temp_bar').data(days);
enter_temp_bars = temp_bars.enter().append('path').attr({
"class": 'bar temp_bar',
d: function(d, i) {
return arc_generator({
innerRadius: r_temp(d['MinTemperatureC']),
outerRadius: r_temp(d['MaxTemperatureC']),
startAngle: i * 2 * Math.PI / days.length,
endAngle: (i + 1) * 2 * Math.PI / days.length
});
},
fill: function(d) {
return temp_color(d['MeanTemperatureC']);
}
});
enter_temp_bars.append('title').text(function(d) {
return (d3.time.format('%Y, %B %e')(new Date(d.t))) + "\nTemperature:\n Min: " + d['MinTemperatureC'] + " °C\n Max: " + d['MaxTemperatureC'] + " °C\n Mean: " + d['MeanTemperatureC'] + " °C";
});
prec_bars = svg.selectAll('.prec_bar').data(days);
enter_prec_bars = prec_bars.enter().append('path').attr({
"class": 'prec_bar',
d: function(d, i) {
return arc_generator({
innerRadius: r_prec(0),
outerRadius: r_prec(d['Precipitationmm']),
startAngle: i * 2 * Math.PI / days.length,
endAngle: (i + 1) * 2 * Math.PI / days.length
});
}
});
enter_prec_bars.append('title').text(function(d) {
return (d3.time.format('%Y, %B %e')(new Date(d.t))) + "\nPrecipitation: " + d['Precipitationmm'] + " mm";
});
cloud_bars = svg.selectAll('.cloud_bar').data(days);
enter_cloud_bars = cloud_bars.enter().append('path').attr({
"class": 'cloud_bar',
d: function(d, i) {
return arc_generator({
innerRadius: r_cloud(0),
outerRadius: r_cloud(d['CloudCover']),
startAngle: i * 2 * Math.PI / days.length,
endAngle: (i + 1) * 2 * Math.PI / days.length
});
}
});
enter_cloud_bars.append('title').text(function(d) {
return (d3.time.format('%Y, %B %e')(new Date(d.t))) + "\nCloud cover: " + d['CloudCover'];
});
svg.append('line').attr({
"class": 'hand',
x1: 0,
x2: 0,
y1: -INNER_RADIUS + 10,
y2: -OUTER_RADIUS - 40
});
return svg.append('text').text(year).attr({
"class": 'hand_label',
x: 4,
y: -OUTER_RADIUS - 40,
dy: '0.6em'
});
});
}).call(this);
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Weather wheel II</title>
<link rel="stylesheet" href="index.css">
<script src="//d3js.org/d3.v3.min.js"></script>
<script src="d3-path.js"></script>
<script src="d3-shape.js"></script>
</head>
<body>
<svg></svg>
<script src="index.js"></script>
</body>
</html>
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define('d3-path', ['exports'], factory) :
factory((global.d3_path = {}));
}(this, function (exports) { 'use strict';
var pi = Math.PI;
var tau = 2 * pi;
var epsilon = 1e-6;
var tauEpsilon = tau - epsilon;
function Path() {
this._x0 = this._y0 = // start of current subpath
this._x1 = this._y1 = null; // end of current subpath
this._ = [];
}
function path() {
return new Path;
}
Path.prototype = path.prototype = {
moveTo: function(x, y) {
this._.push("M", this._x0 = this._x1 = +x, ",", this._y0 = this._y1 = +y);
},
closePath: function() {
if (this._x1 !== null) {
this._x1 = this._x0, this._y1 = this._y0;
this._.push("Z");
}
},
lineTo: function(x, y) {
this._.push("L", this._x1 = +x, ",", this._y1 = +y);
},
quadraticCurveTo: function(x1, y1, x, y) {
this._.push("Q", +x1, ",", +y1, ",", this._x1 = +x, ",", this._y1 = +y);
},
bezierCurveTo: function(x1, y1, x2, y2, x, y) {
this._.push("C", +x1, ",", +y1, ",", +x2, ",", +y2, ",", this._x1 = +x, ",", this._y1 = +y);
},
arcTo: function(x1, y1, x2, y2, r) {
x1 = +x1, y1 = +y1, x2 = +x2, y2 = +y2, r = +r;
var x0 = this._x1,
y0 = this._y1,
x21 = x2 - x1,
y21 = y2 - y1,
x01 = x0 - x1,
y01 = y0 - y1,
l01_2 = x01 * x01 + y01 * y01;
// Is the radius negative? Error.
if (r < 0) throw new Error("negative radius: " + r);
// Is this path empty? Move to (x1,y1).
if (this._x1 === null) {
this._.push(
"M", this._x1 = x1, ",", this._y1 = y1
);
}
// Or, is (x1,y1) coincident with (x0,y0)? Do nothing.
else if (!(l01_2 > epsilon));
// Or, are (x0,y0), (x1,y1) and (x2,y2) collinear?
// Equivalently, is (x1,y1) coincident with (x2,y2)?
// Or, is the radius zero? Line to (x1,y1).
else if (!(Math.abs(y01 * x21 - y21 * x01) > epsilon) || !r) {
this._.push(
"L", this._x1 = x1, ",", this._y1 = y1
);
}
// Otherwise, draw an arc!
else {
var x20 = x2 - x0,
y20 = y2 - y0,
l21_2 = x21 * x21 + y21 * y21,
l20_2 = x20 * x20 + y20 * y20,
l21 = Math.sqrt(l21_2),
l01 = Math.sqrt(l01_2),
l = r * Math.tan((pi - Math.acos((l21_2 + l01_2 - l20_2) / (2 * l21 * l01))) / 2),
t01 = l / l01,
t21 = l / l21;
// If the start tangent is not coincident with (x0,y0), line to.
if (Math.abs(t01 - 1) > epsilon) {
this._.push(
"L", x1 + t01 * x01, ",", y1 + t01 * y01
);
}
this._.push(
"A", r, ",", r, ",0,0,", +(y01 * x20 > x01 * y20), ",", this._x1 = x1 + t21 * x21, ",", this._y1 = y1 + t21 * y21
);
}
},
arc: function(x, y, r, a0, a1, ccw) {
x = +x, y = +y, r = +r;
var dx = r * Math.cos(a0),
dy = r * Math.sin(a0),
x0 = x + dx,
y0 = y + dy,
cw = 1 ^ ccw,
da = ccw ? a0 - a1 : a1 - a0;
// Is the radius negative? Error.
if (r < 0) throw new Error("negative radius: " + r);
// Is this path empty? Move to (x0,y0).
if (this._x1 === null) {
this._.push(
"M", x0, ",", y0
);
}
// Or, is (x0,y0) not coincident with the previous point? Line to (x0,y0).
else if (Math.abs(this._x1 - x0) > epsilon || Math.abs(this._y1 - y0) > epsilon) {
this._.push(
"L", x0, ",", y0
);
}
// Is this arc empty? We’re done.
if (!r) return;
// Is this a complete circle? Draw two arcs to complete the circle.
if (da > tauEpsilon) {
this._.push(
"A", r, ",", r, ",0,1,", cw, ",", x - dx, ",", y - dy,
"A", r, ",", r, ",0,1,", cw, ",", this._x1 = x0, ",", this._y1 = y0
);
}
// Otherwise, draw an arc!
else {
if (da < 0) da = da % tau + tau;
this._.push(
"A", r, ",", r, ",0,", +(da >= pi), ",", cw, ",", this._x1 = x + r * Math.cos(a1), ",", this._y1 = y + r * Math.sin(a1)
);
}
},
rect: function(x, y, w, h) {
this._.push("M", this._x0 = this._x1 = +x, ",", this._y0 = this._y1 = +y, "h", +w, "v", +h, "h", -w, "Z");
},
toString: function() {
return this._.join("");
}
};
var version = "0.1.2";
exports.version = version;
exports.path = path;
}));
svg = d3.select('svg')
width = svg.node().getBoundingClientRect().width
height = svg.node().getBoundingClientRect().height
svg
.attr
viewBox: "#{-width/2} #{-height/2} #{width} #{height}"
OUTER_OUTER_OUTER_RADIUS = 230
OUTER_OUTER_RADIUS = 200
OUTER_RADIUS = 180
INNER_RADIUS = 40
r_temp = d3.scale.linear()
.domain([-40,40])
.range([INNER_RADIUS,OUTER_RADIUS])
r_prec = d3.scale.linear()
.domain([0,30])
.range([OUTER_OUTER_RADIUS,OUTER_RADIUS])
r_cloud = d3.scale.linear()
.domain([0,10])
.range([OUTER_OUTER_RADIUS,OUTER_OUTER_OUTER_RADIUS])
temp_color = d3.scale.linear()
.domain([-20,0,20,40])
.range([d3.hcl(290,70,15),d3.hcl(230,70,45),d3.hcl(80,70,75),d3.hcl(10,70,45)])
.interpolate(d3.interpolateHcl)
arc_generator = d3_shape.arc()
# reference and labels
refs_data = d3.range(-20,40,10)
refs = svg.selectAll '.ref'
.data refs_data
refs.enter().append 'circle'
.attr
class: 'ref'
r: (d) -> r_temp(d)
stroke: (d) -> if d is 0 then '#CCC' else '#EEE'
refs_labels_north = svg.selectAll '.ref_label_north'
.data refs_data
refs_labels_north.enter().append 'text'
.text (d) -> "#{d}°C"
.attr
class: 'ref_label_north'
y: (d) -> -r_temp(d)
dy: '0.35em'
fill: (d) -> if d is 0 then '#BBB' else '#DDD'
refs_labels_south = svg.selectAll '.ref_label_south'
.data refs_data
refs_labels_south.enter().append 'text'
.text (d) -> "#{d}°C"
.attr
class: 'ref_label_south'
y: (d) -> r_temp(d)
dy: '0.35em'
fill: (d) -> if d is 0 then '#BBB' else '#DDD'
svg.append 'circle'
.attr
class: 'prec_ref'
r: OUTER_RADIUS
station = 'LIRP'
year = 2015
d3.csv "wu_get_history.php?station=#{station}&year=#{year}", (days) ->
days.forEach (d) ->
d.t = d['CET']
d['MinTemperatureC'] = +d['MinTemperatureC']
d['MaxTemperatureC'] = +d['MaxTemperatureC']
d['MeanTemperatureC'] = +d['MeanTemperatureC']
d['Precipitationmm'] = +d['Precipitationmm']
d['CloudCover'] = +d['CloudCover']
svg.append 'text'
.text station
.attr
class: 'title'
dy: '0.35em'
angle = d3.scale.ordinal()
.domain(days.map (d) -> d.t)
.range(d3.range(Math.PI/2, 2*Math.PI+Math.PI/2, 2*Math.PI/days.length))
temp_bars = svg.selectAll '.temp_bar'
.data days
enter_temp_bars = temp_bars.enter().append 'path'
.attr
class: 'bar temp_bar'
d: (d,i) -> arc_generator
innerRadius: r_temp(d['MinTemperatureC'])
outerRadius: r_temp(d['MaxTemperatureC'])
startAngle: i*2*Math.PI/days.length # FIXME index-based positioning, should be day-based
endAngle: (i+1)*2*Math.PI/days.length
fill: (d) -> temp_color(d['MeanTemperatureC'])
enter_temp_bars.append 'title'
.text (d) -> "#{d3.time.format('%Y, %B %e')(new Date(d.t))}\nTemperature:\n Min: #{d['MinTemperatureC']} °C\n Max: #{d['MaxTemperatureC']} °C\n Mean: #{d['MeanTemperatureC']} °C"
prec_bars = svg.selectAll '.prec_bar'
.data days
enter_prec_bars = prec_bars.enter().append 'path'
.attr
class: 'prec_bar'
d: (d,i) -> arc_generator
innerRadius: r_prec(0)
outerRadius: r_prec(d['Precipitationmm'])
startAngle: i*2*Math.PI/days.length # FIXME index-based positioning, should be day-based
endAngle: (i+1)*2*Math.PI/days.length
enter_prec_bars.append 'title'
.text (d) -> "#{d3.time.format('%Y, %B %e')(new Date(d.t))}\nPrecipitation: #{d['Precipitationmm']} mm"
cloud_bars = svg.selectAll '.cloud_bar'
.data days
enter_cloud_bars = cloud_bars.enter().append 'path'
.attr
class: 'cloud_bar'
d: (d,i) -> arc_generator
innerRadius: r_cloud(0)
outerRadius: r_cloud(d['CloudCover'])
startAngle: i*2*Math.PI/days.length # FIXME index-based positioning, should be day-based
endAngle: (i+1)*2*Math.PI/days.length
enter_cloud_bars.append 'title'
.text (d) -> "#{d3.time.format('%Y, %B %e')(new Date(d.t))}\nCloud cover: #{d['CloudCover']}"
# draw the hand
svg.append 'line'
.attr
class: 'hand'
x1: 0
x2: 0
y1: -INNER_RADIUS+10
y2: -OUTER_RADIUS-40
svg.append 'text'
.text year
.attr
class: 'hand_label'
x: 4
y: -OUTER_RADIUS-40
dy: '0.6em'
@font-face {
font-family: "Lacuna";
src: url("lacuna.ttf");
}
@font-face {
font-family: "Antonio-Light";
src: url("Antonio-Light.ttf");
}
body, html {
padding: 0;
margin: 0;
width: 100%;
height: 100%;
font-family: sans-serif;
font-size: 12px;
overflow: hidden;
}
svg {
width: 100%;
height: 100%;
background: white;
}
.bar {
stroke-width: 0.5;
stroke: white;
fill-opacity: 0.8;
}
.bar:hover {
fill-opacity: 1;
}
.ref, .prec_ref {
fill: none;
}
.prec_ref {
stroke: steelblue;
opacity: 0.2;
}
.ref_label_north, .ref_label_south {
text-anchor: middle;
font-size: 8px;
text-shadow: -1px -1px white, -1px 1px white, 1px 1px white, 1px -1px white, -1px 0 white, 0 1px white, 1px 0 white, 0 -1px white;
font-family: "Lacuna";
}
.prec_bar {
fill: steelblue;
fill-opacity: 0.7;
}
.prec_bar:hover {
fill-opacity: 0.8;
}
.cloud_bar {
fill: #777;
fill-opacity: 0.5;
}
.cloud_bar:hover {
fill-opacity: 0.8;
}
.title {
font-size: 32px;
text-anchor: middle;
text-shadow: -1px -1px white, -1px 1px white, 1px 1px white, 1px -1px white, -1px 0 white, 0 1px white, 1px 0 white, 0 -1px white;
fill: #999;
font-family: "Antonio-Light";
}
.hand {
stroke: #777;
stroke-dasharray: 2 2;
fill: none;
}
.hand_label {
font-size: 9px;
font-weight: bold;
font-family: "Lacuna";
fill: #555;
}
<?php
$airport = $_GET['station'];
$year = $_GET['year'];
$url = "http://www.wunderground.com/history/airport/$airport/$year/1/1/CustomHistory.html?dayend=31&monthend=12&yearend=$year&req_city=&req_state=&req_statename=&reqdb.zip=&reqdb.magic=&reqdb.wmo=&MR=1&format=1";
$html = file_get_contents($url);
$csv = preg_replace('/^\n/', '', $html);
$lines = explode('<br />', $csv);
$lines[0] = str_replace(' ', '', $lines[0]);
$csv = implode('', $lines);
header('Content-Type: application/csv');
echo $csv;
?>