(This example contains server-side code. You need to run it from our WebVis lab rather than bl.ocks.org in order to use it.)
This example lets you compare one entire year of weather data with another one using two weather wheels. You can choose which year to display, and which weather station (by choosing a country and then a city).
You can zoom in either views by using your mouse wheel or touch gestures.
Each day of the year is assigned a circular slice of the diagram, proceeding clockwise. For each day, several measures are depicted:
The design has been heavily determined by reverse engineering this beautiful work by Raureif. Data is requested live from the Weather Underground Historical APIs.
// Generated by CoffeeScript 1.10.0
(function() {
d3.csv('airports.csv', function(airports) {
var airports_db;
airports_db = {};
airports_db.list = airports.filter(function(d) {
return d.icao !== '\\N' && d.icao !== '';
});
airports_db.index = {};
airports_db.list.forEach(function(d) {
return airports_db.index[d.icao] = d;
});
airports_db.countries = d3.nest().key(function(d) {
return d.country;
}).entries(airports_db.list);
airports_db.by_country = {};
airports_db.countries.forEach(function(d) {
return airports_db.by_country[d.key] = d.values;
});
return new AppView({
airports_db: airports_db,
parent: 'body'
});
});
}).call(this);
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Weather wheel III</title>
<link type="text/css" href="index.css" rel="stylesheet"/>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-selection-multi.v0.4.min.js"></script>
<script src="eye.js"></script>
<script src="Query.js"></script>
<script src="Weather.js"></script>
<script src="AppView.js"></script>
<script src="WeatherPanel.js"></script>
<script src="QueryView.js"></script>
<script src="WeatherWheel.js"></script>
</head>
<body>
<script src="index.js"></script>
</body>
</html>
window.AppView = class AppView extends View
constructor: (conf) ->
super(conf)
q1 = new Query
airports_db: conf.airports_db
q2 = new Query
airports_db: conf.airports_db
new WeatherPanel
query: q1
parent: this
new WeatherPanel
query: q2
parent: this
q1.set
year: 2015
country: 'Italy'
icao: 'LIRP' # Pisa
q2.set
year: 2015
country: 'Japan'
icao: 'RJTT' # Tokyo
// Generated by CoffeeScript 1.10.0
(function() {
var AppView,
extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
window.AppView = AppView = (function(superClass) {
extend(AppView, superClass);
function AppView(conf) {
var q1, q2;
AppView.__super__.constructor.call(this, conf);
q1 = new Query({
airports_db: conf.airports_db
});
q2 = new Query({
airports_db: conf.airports_db
});
new WeatherPanel({
query: q1,
parent: this
});
new WeatherPanel({
query: q2,
parent: this
});
q1.set({
year: 2015,
country: 'Italy',
icao: 'LIRP'
});
q2.set({
year: 2015,
country: 'Japan',
icao: 'RJTT'
});
}
return AppView;
})(View);
}).call(this);
window.Query = observable class Query
constructor: (conf) ->
@init
events: ['change', 'change_country']
@airports_db = conf.airports_db
get_years: () -> d3.range(2000,new Date().getFullYear()+1)
get_year: () -> @selected_year
set_year: (year) ->
@selected_year = year
@trigger 'change'
get_countries: () -> @airports_db.countries.map (d) -> d.key
set_country: (name) ->
@selected_country = name
@trigger 'change_country'
# also select the first airport
@set_airport @airports_db.by_country[@selected_country][0].icao
set_airport: (icao) ->
@selected_airport = @airports_db.index[icao]
@trigger 'change'
get_all: () -> @airports_db.by_country[@selected_country]
get_airport: () -> @selected_airport
get_country: () -> @selected_country
set: (conf) ->
@selected_year = conf.year
@selected_country = conf.country
@selected_airport = @airports_db.index[conf.icao]
@trigger 'change_country'
@trigger 'change'
// Generated by CoffeeScript 1.10.0
(function() {
var Query;
window.Query = observable(Query = (function() {
function Query(conf) {
this.init({
events: ['change', 'change_country']
});
this.airports_db = conf.airports_db;
}
Query.prototype.get_years = function() {
return d3.range(2000, new Date().getFullYear() + 1);
};
Query.prototype.get_year = function() {
return this.selected_year;
};
Query.prototype.set_year = function(year) {
this.selected_year = year;
return this.trigger('change');
};
Query.prototype.get_countries = function() {
return this.airports_db.countries.map(function(d) {
return d.key;
});
};
Query.prototype.set_country = function(name) {
this.selected_country = name;
this.trigger('change_country');
return this.set_airport(this.airports_db.by_country[this.selected_country][0].icao);
};
Query.prototype.set_airport = function(icao) {
this.selected_airport = this.airports_db.index[icao];
return this.trigger('change');
};
Query.prototype.get_all = function() {
return this.airports_db.by_country[this.selected_country];
};
Query.prototype.get_airport = function() {
return this.selected_airport;
};
Query.prototype.get_country = function() {
return this.selected_country;
};
Query.prototype.set = function(conf) {
this.selected_year = conf.year;
this.selected_country = conf.country;
this.selected_airport = this.airports_db.index[conf.icao];
this.trigger('change_country');
return this.trigger('change');
};
return Query;
})());
}).call(this);
window.QueryView = observer class QueryView extends View
constructor: (conf) ->
super(conf)
@init()
@query = conf.query
# Year select
@select_year_el = @d3el.append 'select'
year_options = @select_year_el.selectAll 'option'
.data @query.get_years().sort(), (d) -> d
year_options.enter().append 'option'
.text (d) -> d
.attrs
value: (d) -> d
@select_year_el.on 'change', () =>
@query.set_year @select_year_el.node().options[@select_year_el.node().selectedIndex].value
# Country select
@select_country_el = @d3el.append 'select'
country_options = @select_country_el.selectAll 'option'
.data @query.get_countries().sort(), (d) -> d
country_options.enter().append 'option'
.text (d) -> d
.attrs
value: (d) -> d
@select_country_el.on 'change', () =>
@query.set_country @select_country_el.node().options[@select_country_el.node().selectedIndex].value
# Airport select
@select_airport_el = @d3el.append 'select'
@select_airport_el.on 'change', () =>
@query.set_airport @select_airport_el.node().options[@select_airport_el.node().selectedIndex].value
# Listeners
@listen_to @query, 'change_country', () =>
@select_country_option @query.get_country()
@redraw()
@listen_to @query, 'change', () =>
@select_year_option @query.get_year()
@select_airport_option @query.get_airport().icao
select_year_option: (year) ->
@select_year_el.select "option[value='#{year}']"
.property 'selected', true
select_country_option: (name) ->
@select_country_el.select "option[value='#{name}']"
.property 'selected', true
select_airport_option: (icao) ->
@select_airport_el.select "option[value='#{icao}']"
.property 'selected', true
redraw: () ->
airport_options = @select_airport_el.selectAll 'option'
.data @query.get_all().sort( (a,b) -> d3.ascending(a.city, b.city) ), (d) -> d.icao
airport_options.enter().append 'option'
.text (d) -> "#{d.city} (#{d.icao})"
.attrs
value: (d) -> d.icao
airport_options.exit()
.remove()
// Generated by CoffeeScript 1.10.0
(function() {
var QueryView,
extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
window.QueryView = observer(QueryView = (function(superClass) {
extend(QueryView, superClass);
function QueryView(conf) {
var country_options, year_options;
QueryView.__super__.constructor.call(this, conf);
this.init();
this.query = conf.query;
this.select_year_el = this.d3el.append('select');
year_options = this.select_year_el.selectAll('option').data(this.query.get_years().sort(), function(d) {
return d;
});
year_options.enter().append('option').text(function(d) {
return d;
}).attrs({
value: function(d) {
return d;
}
});
this.select_year_el.on('change', (function(_this) {
return function() {
return _this.query.set_year(_this.select_year_el.node().options[_this.select_year_el.node().selectedIndex].value);
};
})(this));
this.select_country_el = this.d3el.append('select');
country_options = this.select_country_el.selectAll('option').data(this.query.get_countries().sort(), function(d) {
return d;
});
country_options.enter().append('option').text(function(d) {
return d;
}).attrs({
value: function(d) {
return d;
}
});
this.select_country_el.on('change', (function(_this) {
return function() {
return _this.query.set_country(_this.select_country_el.node().options[_this.select_country_el.node().selectedIndex].value);
};
})(this));
this.select_airport_el = this.d3el.append('select');
this.select_airport_el.on('change', (function(_this) {
return function() {
return _this.query.set_airport(_this.select_airport_el.node().options[_this.select_airport_el.node().selectedIndex].value);
};
})(this));
this.listen_to(this.query, 'change_country', (function(_this) {
return function() {
_this.select_country_option(_this.query.get_country());
return _this.redraw();
};
})(this));
this.listen_to(this.query, 'change', (function(_this) {
return function() {
_this.select_year_option(_this.query.get_year());
return _this.select_airport_option(_this.query.get_airport().icao);
};
})(this));
}
QueryView.prototype.select_year_option = function(year) {
return this.select_year_el.select("option[value='" + year + "']").property('selected', true);
};
QueryView.prototype.select_country_option = function(name) {
return this.select_country_el.select("option[value='" + name + "']").property('selected', true);
};
QueryView.prototype.select_airport_option = function(icao) {
return this.select_airport_el.select("option[value='" + icao + "']").property('selected', true);
};
QueryView.prototype.redraw = function() {
var airport_options;
airport_options = this.select_airport_el.selectAll('option').data(this.query.get_all().sort(function(a, b) {
return d3.ascending(a.city, b.city);
}), function(d) {
return d.icao;
});
airport_options.enter().append('option').text(function(d) {
return d.city + " (" + d.icao + ")";
}).attrs({
value: function(d) {
return d.icao;
}
});
return airport_options.exit().remove();
};
return QueryView;
})(View));
}).call(this);
window.Weather = observable class Weather
constructor: (conf) ->
@init
events: ['change']
query: (icao, year) ->
@icao = icao
@year = year
d3.csv "wu_get_history.php?station=#{icao}&year=#{year}", (days) =>
@days = days
@days.forEach (d) =>
d.t = d[@days.columns[0]]
d.date = new Date(d.t)
d.day = d.date.getDOY()
d.MinTemperatureC = +d.MinTemperatureC
d.MaxTemperatureC = +d.MaxTemperatureC
d.MeanTemperatureC = +d.MeanTemperatureC
d.Precipitationmm = +d.Precipitationmm or 0
d.CloudCover = Math.max 0, +d.CloudCover
d['MeanWindSpeedKm/h'] = +d['MeanWindSpeedKm/h']
d.WindDirDegrees = +d.WindDirDegrees
@trigger 'change'
`
// code by Joe Orost
// http://stackoverflow.com/questions/8619879/javascript-calculate-the-day-of-the-year-1-366
Date.prototype.isLeapYear = function() {
var year = this.getFullYear();
if((year & 3) != 0) return false;
return ((year % 100) != 0 || (year % 400) == 0);
};
// Get Day of Year
Date.prototype.getDOY = function() {
var dayCount = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
var mn = this.getMonth();
var dn = this.getDate();
var dayOfYear = dayCount[mn] + dn;
if(mn > 1 && this.isLeapYear()) dayOfYear++;
return dayOfYear;
};
`
// Generated by CoffeeScript 1.10.0
(function() {
var Weather;
window.Weather = observable(Weather = (function() {
function Weather(conf) {
this.init({
events: ['change']
});
}
Weather.prototype.query = function(icao, year) {
this.icao = icao;
this.year = year;
return d3.csv("wu_get_history.php?station=" + icao + "&year=" + year, (function(_this) {
return function(days) {
_this.days = days;
_this.days.forEach(function(d) {
d.t = d[_this.days.columns[0]];
d.date = new Date(d.t);
d.day = d.date.getDOY();
d.MinTemperatureC = +d.MinTemperatureC;
d.MaxTemperatureC = +d.MaxTemperatureC;
d.MeanTemperatureC = +d.MeanTemperatureC;
d.Precipitationmm = +d.Precipitationmm || 0;
d.CloudCover = Math.max(0, +d.CloudCover);
d['MeanWindSpeedKm/h'] = +d['MeanWindSpeedKm/h'];
return d.WindDirDegrees = +d.WindDirDegrees;
});
return _this.trigger('change');
};
})(this));
};
return Weather;
})());
// code by Joe Orost
// http://stackoverflow.com/questions/8619879/javascript-calculate-the-day-of-the-year-1-366
Date.prototype.isLeapYear = function() {
var year = this.getFullYear();
if((year & 3) != 0) return false;
return ((year % 100) != 0 || (year % 400) == 0);
};
// Get Day of Year
Date.prototype.getDOY = function() {
var dayCount = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
var mn = this.getMonth();
var dn = this.getDate();
var dayOfYear = dayCount[mn] + dn;
if(mn > 1 && this.isLeapYear()) dayOfYear++;
return dayOfYear;
};
;
}).call(this);
window.WeatherPanel = observer class WeatherPanel extends View
constructor: (conf) ->
super(conf)
@init()
@query = conf.query
@weather = new Weather
@listen_to @query, 'change', () =>
@weather.query @query.get_airport().icao, @query.get_year()
new QueryView
query: @query
parent: this
new WeatherWheel
weather: @weather
parent: this
// Generated by CoffeeScript 1.10.0
(function() {
var WeatherPanel,
extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
window.WeatherPanel = observer(WeatherPanel = (function(superClass) {
extend(WeatherPanel, superClass);
function WeatherPanel(conf) {
WeatherPanel.__super__.constructor.call(this, conf);
this.init();
this.query = conf.query;
this.weather = new Weather;
this.listen_to(this.query, 'change', (function(_this) {
return function() {
return _this.weather.query(_this.query.get_airport().icao, _this.query.get_year());
};
})(this));
new QueryView({
query: this.query,
parent: this
});
new WeatherWheel({
weather: this.weather,
parent: this
});
}
return WeatherPanel;
})(View));
}).call(this);
window.WeatherWheel = observer class WeatherWheel extends View
constructor: (conf) ->
super(conf)
@init()
@weather = conf.weather
@listen_to @weather, 'change', () => @redraw()
scale = 230
@svg = @d3el.append 'svg'
.attrs
viewBox: "#{-scale/2} #{-scale/2} #{scale} #{scale}"
@zoomable_layer = @svg.append 'g'
zoom = d3.zoom()
.scaleExtent([-Infinity,Infinity])
.on 'zoom', () =>
@zoomable_layer
.attrs
transform: d3.event.transform
@svg.call zoom
# Fixed scales
@temp2radius = d3.scaleLinear()
.domain [-40,40]
.range [10, 70]
@temp2color = d3.scaleLinear()
.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)
@prec2radius = d3.scaleLinear()
.domain [0,40]
.range [80, 70]
@cloud2radius = d3.scaleLinear()
.domain [0,8]
.range [80, 86]
@wind2dradius = d3.scaleLinear()
.domain [0,100]
.range [0,60]
# References
@zoomable_layer.append 'circle'
.attrs
class: 'ref_line temp_line'
r: @temp2radius(-20)
@zoomable_layer.append 'circle'
.attrs
class: 'ref_line temp_line emph'
r: @temp2radius(0)
@zoomable_layer.append 'circle'
.attrs
class: 'ref_line temp_line'
r: @temp2radius(20)
@zoomable_layer.append 'circle'
.attrs
class: 'ref_line prec_line'
r: @prec2radius(20)
@zoomable_layer.append 'circle'
.attrs
class: 'ref_line prec_line'
r: @prec2radius(40)
@zoomable_layer.append 'circle'
.attrs
class: 'ref_line wind_line'
r: @wind2dradius(20) + 87 # FIXME magic number - base for wind
@zoomable_layer.append 'path'
.attrs
class: 'ref_line year emph'
d: "M#{0} #{-scale*0.1} L #{0} #{-scale*0.5}"
redraw: () ->
EPSILON = 0.01
ANIMATION_DURATION = 1500
# check if leap year
ndays = if ((@weather.year % 4 is 0) and (@weather.year % 100 isnt 0)) or (@weather.year % 400 is 0) then 366 else 365
day2radians = d3.scaleLinear()
.domain [1, ndays+1]
.range [0, 2*Math.PI]
arc_generator = d3.arc()
# Temperature
temp_bars = @zoomable_layer.selectAll '.temp_bar'
.data @weather.days, (d) -> d.day
enter_temp_bars = temp_bars.enter().append 'path'
.attrs
class: 'temp_bar bar'
enter_temp_bars.append 'title'
all_temp_bars = enter_temp_bars.merge(temp_bars)
all_temp_bars.select 'title'
.text (d) -> "#{d3.timeFormat('%Y, %B %e')(d.date)}\nTemperature:\n Maximum: #{d.MaxTemperatureC} °C\n Mean: #{d.MeanTemperatureC} °C\n Minimum: #{d.MinTemperatureC} °C"
all_temp_bars
.transition().duration(ANIMATION_DURATION)
.attrs
d: (d) => arc_generator
startAngle: day2radians d.day
endAngle: day2radians (d.day+1)
innerRadius: @temp2radius(d.MinTemperatureC)
outerRadius: @temp2radius(d.MaxTemperatureC+EPSILON) # this is needed to avoid interpolation errors
fill: (d) => @temp2color(d.MeanTemperatureC)
temp_bars.exit()
.remove()
# Precipitation
prec_bars = @zoomable_layer.selectAll '.prec_bar'
.data @weather.days, (d) -> d.day
enter_prec_bars = prec_bars.enter().append 'path'
.attrs
class: 'prec_bar bar'
enter_prec_bars.append 'title'
all_prec_bars = enter_prec_bars.merge(prec_bars)
all_prec_bars.select 'title'
.text (d) -> "#{d3.timeFormat('%Y, %B %e')(d.date)}\nPrecipitation: #{d.Precipitationmm} mm"
all_prec_bars
.transition().duration(ANIMATION_DURATION)
.attrs
d: (d) => arc_generator
startAngle: day2radians d.day
endAngle: day2radians (d.day+1)
innerRadius: @prec2radius(d.Precipitationmm)
outerRadius: @prec2radius(-EPSILON) # this is needed to avoid interpolation errors
prec_bars.exit()
.remove()
# Cloud cover
cloud_bars = @zoomable_layer.selectAll '.cloud_bar'
.data @weather.days, (d) -> d.day
enter_cloud_bars = cloud_bars.enter().append 'path'
.attrs
class: 'cloud_bar bar'
enter_cloud_bars.append 'title'
all_cloud_bars = enter_cloud_bars.merge(cloud_bars)
all_cloud_bars.select 'title'
.text (d) -> "#{d3.timeFormat('%Y, %B %e')(d.date)}\nCloud cover: #{d.CloudCover}/8"
all_cloud_bars
.transition().duration(ANIMATION_DURATION)
.attrs
d: (d) => arc_generator
startAngle: day2radians d.day
endAngle: day2radians (d.day+1)
innerRadius: @cloud2radius(-EPSILON) # this is needed to avoid interpolation errors
outerRadius: @cloud2radius(d.CloudCover)
cloud_bars.exit()
.remove()
# Wind
wind_bars = @zoomable_layer.selectAll '.wind_bar'
.data @weather.days, (d) -> d.day
enter_wind_bars = wind_bars.enter().append 'path'
.attrs
class: 'wind_bar bar'
enter_wind_bars.append 'title'
all_wind_bars = enter_wind_bars.merge(wind_bars)
all_wind_bars.select 'title'
.text (d) -> "#{d3.timeFormat('%Y, %B %e')(d.date)}\nMean wind speed: #{d['MeanWindSpeedKm/h']} Km/h\nWind direction: #{d.WindDirDegrees} °"
all_wind_bars
.transition().duration(ANIMATION_DURATION)
.attrs
d: (d) =>
theta = day2radians((d.day+0.5)) - Math.PI/2
rho = 87
x = rho*Math.cos(theta)
y = rho*Math.sin(theta)
r = @wind2dradius(d['MeanWindSpeedKm/h'])
dx = r*Math.cos(theta)
dy = r*Math.sin(theta)
a = 2*Math.PI*(d.WindDirDegrees-90)/360
dax = 2*Math.cos(a)
day = 2*Math.sin(a)
return "M#{x+dax} #{y+day} l#{-dax} #{-day} l#{dx} #{dy}"
# stroke: (d) -> wind2color d.WindDirDegrees
stroke: 'teal'
wind_bars.exit()
.remove()
// Generated by CoffeeScript 1.10.0
(function() {
var WeatherWheel,
extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
window.WeatherWheel = observer(WeatherWheel = (function(superClass) {
extend(WeatherWheel, superClass);
function WeatherWheel(conf) {
var scale, zoom;
WeatherWheel.__super__.constructor.call(this, conf);
this.init();
this.weather = conf.weather;
this.listen_to(this.weather, 'change', (function(_this) {
return function() {
return _this.redraw();
};
})(this));
scale = 230;
this.svg = this.d3el.append('svg').attrs({
viewBox: (-scale / 2) + " " + (-scale / 2) + " " + scale + " " + scale
});
this.zoomable_layer = this.svg.append('g');
zoom = d3.zoom().scaleExtent([-Infinity, Infinity]).on('zoom', (function(_this) {
return function() {
return _this.zoomable_layer.attrs({
transform: d3.event.transform
});
};
})(this));
this.svg.call(zoom);
this.temp2radius = d3.scaleLinear().domain([-40, 40]).range([10, 70]);
this.temp2color = d3.scaleLinear().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);
this.prec2radius = d3.scaleLinear().domain([0, 40]).range([80, 70]);
this.cloud2radius = d3.scaleLinear().domain([0, 8]).range([80, 86]);
this.wind2dradius = d3.scaleLinear().domain([0, 100]).range([0, 60]);
this.zoomable_layer.append('circle').attrs({
"class": 'ref_line temp_line',
r: this.temp2radius(-20)
});
this.zoomable_layer.append('circle').attrs({
"class": 'ref_line temp_line emph',
r: this.temp2radius(0)
});
this.zoomable_layer.append('circle').attrs({
"class": 'ref_line temp_line',
r: this.temp2radius(20)
});
this.zoomable_layer.append('circle').attrs({
"class": 'ref_line prec_line',
r: this.prec2radius(20)
});
this.zoomable_layer.append('circle').attrs({
"class": 'ref_line prec_line',
r: this.prec2radius(40)
});
this.zoomable_layer.append('circle').attrs({
"class": 'ref_line wind_line',
r: this.wind2dradius(20) + 87
});
this.zoomable_layer.append('path').attrs({
"class": 'ref_line year emph',
d: "M" + 0. + " " + (-scale * 0.1) + " L " + 0. + " " + (-scale * 0.5)
});
}
WeatherWheel.prototype.redraw = function() {
var ANIMATION_DURATION, EPSILON, all_cloud_bars, all_prec_bars, all_temp_bars, all_wind_bars, arc_generator, cloud_bars, day2radians, enter_cloud_bars, enter_prec_bars, enter_temp_bars, enter_wind_bars, ndays, prec_bars, temp_bars, wind_bars;
EPSILON = 0.01;
ANIMATION_DURATION = 1500;
ndays = ((this.weather.year % 4 === 0) && (this.weather.year % 100 !== 0)) || (this.weather.year % 400 === 0) ? 366 : 365;
day2radians = d3.scaleLinear().domain([1, ndays + 1]).range([0, 2 * Math.PI]);
arc_generator = d3.arc();
temp_bars = this.zoomable_layer.selectAll('.temp_bar').data(this.weather.days, function(d) {
return d.day;
});
enter_temp_bars = temp_bars.enter().append('path').attrs({
"class": 'temp_bar bar'
});
enter_temp_bars.append('title');
all_temp_bars = enter_temp_bars.merge(temp_bars);
all_temp_bars.select('title').text(function(d) {
return (d3.timeFormat('%Y, %B %e')(d.date)) + "\nTemperature:\n Maximum: " + d.MaxTemperatureC + " °C\n Mean: " + d.MeanTemperatureC + " °C\n Minimum: " + d.MinTemperatureC + " °C";
});
all_temp_bars.transition().duration(ANIMATION_DURATION).attrs({
d: (function(_this) {
return function(d) {
return arc_generator({
startAngle: day2radians(d.day),
endAngle: day2radians(d.day + 1),
innerRadius: _this.temp2radius(d.MinTemperatureC),
outerRadius: _this.temp2radius(d.MaxTemperatureC + EPSILON)
});
};
})(this),
fill: (function(_this) {
return function(d) {
return _this.temp2color(d.MeanTemperatureC);
};
})(this)
});
temp_bars.exit().remove();
prec_bars = this.zoomable_layer.selectAll('.prec_bar').data(this.weather.days, function(d) {
return d.day;
});
enter_prec_bars = prec_bars.enter().append('path').attrs({
"class": 'prec_bar bar'
});
enter_prec_bars.append('title');
all_prec_bars = enter_prec_bars.merge(prec_bars);
all_prec_bars.select('title').text(function(d) {
return (d3.timeFormat('%Y, %B %e')(d.date)) + "\nPrecipitation: " + d.Precipitationmm + " mm";
});
all_prec_bars.transition().duration(ANIMATION_DURATION).attrs({
d: (function(_this) {
return function(d) {
return arc_generator({
startAngle: day2radians(d.day),
endAngle: day2radians(d.day + 1),
innerRadius: _this.prec2radius(d.Precipitationmm),
outerRadius: _this.prec2radius(-EPSILON)
});
};
})(this)
});
prec_bars.exit().remove();
cloud_bars = this.zoomable_layer.selectAll('.cloud_bar').data(this.weather.days, function(d) {
return d.day;
});
enter_cloud_bars = cloud_bars.enter().append('path').attrs({
"class": 'cloud_bar bar'
});
enter_cloud_bars.append('title');
all_cloud_bars = enter_cloud_bars.merge(cloud_bars);
all_cloud_bars.select('title').text(function(d) {
return (d3.timeFormat('%Y, %B %e')(d.date)) + "\nCloud cover: " + d.CloudCover + "/8";
});
all_cloud_bars.transition().duration(ANIMATION_DURATION).attrs({
d: (function(_this) {
return function(d) {
return arc_generator({
startAngle: day2radians(d.day),
endAngle: day2radians(d.day + 1),
innerRadius: _this.cloud2radius(-EPSILON),
outerRadius: _this.cloud2radius(d.CloudCover)
});
};
})(this)
});
cloud_bars.exit().remove();
wind_bars = this.zoomable_layer.selectAll('.wind_bar').data(this.weather.days, function(d) {
return d.day;
});
enter_wind_bars = wind_bars.enter().append('path').attrs({
"class": 'wind_bar bar'
});
enter_wind_bars.append('title');
all_wind_bars = enter_wind_bars.merge(wind_bars);
all_wind_bars.select('title').text(function(d) {
return (d3.timeFormat('%Y, %B %e')(d.date)) + "\nMean wind speed: " + d['MeanWindSpeedKm/h'] + " Km/h\nWind direction: " + d.WindDirDegrees + " °";
});
all_wind_bars.transition().duration(ANIMATION_DURATION).attrs({
d: (function(_this) {
return function(d) {
var a, dax, day, dx, dy, r, rho, theta, x, y;
theta = day2radians(d.day + 0.5) - Math.PI / 2;
rho = 87;
x = rho * Math.cos(theta);
y = rho * Math.sin(theta);
r = _this.wind2dradius(d['MeanWindSpeedKm/h']);
dx = r * Math.cos(theta);
dy = r * Math.sin(theta);
a = 2 * Math.PI * (d.WindDirDegrees - 90) / 360;
dax = 2 * Math.cos(a);
day = 2 * Math.sin(a);
return "M" + (x + dax) + " " + (y + day) + " l" + (-dax) + " " + (-day) + " l" + dx + " " + dy;
};
})(this),
stroke: 'teal'
});
return wind_bars.exit().remove();
};
return WeatherWheel;
})(View));
}).call(this);
// Generated by CoffeeScript 1.10.0
(function() {
var View, setup_init,
slice = [].slice;
setup_init = function(c, init) {
if (c.prototype.inits == null) {
c.prototype.inits = [];
}
c.prototype.inits.push(init);
return c.prototype.init = function(conf) {
var i, len, m, ref, results;
ref = this.inits;
results = [];
for (i = 0, len = ref.length; i < len; i++) {
m = ref[i];
results.push(m.call(this, conf));
}
return results;
};
};
window.observable = function(c) {
setup_init(c, function(config) {
this._dispatcher = d3.dispatch.apply(d3, config.events);
return this._next_id = 0;
});
c.prototype.on = function(event_type_ns, callback) {
var event_type, event_type_full, namespace, splitted_event_type_ns;
splitted_event_type_ns = event_type_ns.split('.');
event_type = splitted_event_type_ns[0];
if (splitted_event_type_ns.length > 1) {
namespace = splitted_event_type_ns[1];
} else {
namespace = this._next_id;
this._next_id += 1;
}
event_type_full = event_type + '.' + namespace;
this._dispatcher.on(event_type_full, callback);
return event_type_full;
};
c.prototype.trigger = function() {
var args, event_type;
event_type = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : [];
this._dispatcher.apply(event_type, this, args);
return this;
};
return c;
};
window.observer = function(c) {
setup_init(c, function() {
return this._bindings = [];
});
c.prototype.listen_to = function(observed, event, cb) {
return this._bindings.push({
observed: observed,
event_type: observed.on(event, cb)
});
};
c.prototype.stop_listening = function() {
return this._bindings.forEach((function(_this) {
return function(l) {
return l.observed.on(l.event_type, null);
};
})(this));
};
return c;
};
window.View = View = (function() {
function View(conf) {
if (conf.tag == null) {
conf.tag = 'div';
}
this.el = document.createElement(conf.tag);
this.d3el = d3.select(this.el);
this.d3el.classed(this.constructor.name, true);
if (conf.parent != null) {
this.append_to(conf.parent, conf.prepend);
}
}
View.prototype.append_to = function(parent, prepend) {
var p_el;
if (parent.el != null) {
p_el = parent.el;
} else {
if (parent.node != null) {
p_el = parent.node();
} else {
p_el = d3.select(parent).node();
}
}
if (prepend) {
return p_el.insertBefore(this.el, p_el.firstChild);
} else {
return p_el.appendChild(this.el);
}
};
View.prototype.compute_size = function() {
this.width = this.el.getBoundingClientRect().width;
return this.height = this.el.getBoundingClientRect().height;
};
return View;
})();
}).call(this);
d3.csv 'airports.csv', (airports) ->
# build the airports database in memory
airports_db = {}
airports_db.list = airports
# ICAO-only airports
.filter (d) -> d.icao isnt '\\N' and d.icao isnt ''
airports_db.index = {}
airports_db.list.forEach (d) ->
airports_db.index[d.icao] = d
airports_db.countries = d3.nest()
.key (d) -> d.country
.entries airports_db.list
airports_db.by_country = {}
airports_db.countries.forEach (d) ->
airports_db.by_country[d.key] = d.values
new AppView
airports_db: airports_db
parent: 'body'
body, html {
padding: 0;
margin: 0;
width: 100%;
height: 100%;
}
.AppView {
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
}
.AppView > * {
width: 0;
flex-grow: 1;
}
.AppView > *:not(:last-child) {
border-right: 1px solid #DDD;
}
.WeatherPanel {
display: flex;
flex-direction: column;
}
.WeatherWheel {
height: 0;
flex-grow: 1;
}
.WeatherWheel svg {
width: 100%;
height: 100%;
}
.WeatherWheel .bar {
opacity: 0.8;
}
.WeatherWheel .bar:hover {
opacity: 1;
}
.WeatherWheel .temp_bar {
stroke-width: 0.1;
stroke: white;
}
.WeatherWheel .prec_bar {
fill: steelblue;
}
.WeatherWheel .cloud_bar {
fill: #AAA;
}
.WeatherWheel .wind_bar {
stroke-width: 0.4;
opacity: 0.4;
fill: none;
}
.WeatherWheel .ref_line {
fill: none;
stroke-width: 0.3;
stroke: #555;
vector-effect: non-scaling-stroke;
opacity: 0.5;
}
.WeatherWheel .ref_line.emph {
stroke-width: 0.6;
}
.WeatherWheel .prec_line {
stroke: steelblue;
}
.WeatherWheel .wind_line {
stroke: teal;
stroke-dasharray: 3 3;
}
.QueryView {
padding: 4px;
width: 100%;
box-sizing: border-box;
}
.QueryView select {
padding: 4px;
width: 100%;
font-size: 16px;
}
.QueryView select:first-child {
margin-bottom: 4px;
}
<?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;
?>