Skip to content

Instantly share code, notes, and snippets.

@NelsonMinar
Created September 1, 2012 22:31
Show Gist options
  • Save NelsonMinar/3589712 to your computer and use it in GitHub Desktop.
Save NelsonMinar/3589712 to your computer and use it in GitHub Desktop.
windhistory.com D3 code
// This is the core Javascript code for http://windhistory.com/
// I haven't done a full open source release, but I figured I'd put the most important
// D3 code out there for people to learn from. --nelson@monkey.org
/** Common wind rose code **/
// Function to draw a single arc for the wind rose
// Input: Drawing options object containing
// width: degrees of width to draw (ie 5 or 15)
// from: integer, inner radius
// to: function returning the outer radius
// Output: a function that when called, generates SVG paths.
// It expects to be called via D3 with data objects from totalsToFrequences()
var arc = function(o) {
return d3.svg.arc()
.startAngle(function(d) { return (d.d - o.width) * Math.PI/180; })
.endAngle(function(d) { return (d.d + o.width) * Math.PI/180; })
.innerRadius(o.from)
.outerRadius(function(d) { return o.to(d) });
};
/** Code for data manipulation **/
// Convert a dictionary of {direction: total} to frequencies
// Output is an array of objects with three parameters:
// d: wind direction
// p: probability of the wind being in this direction
// s: average speed of the wind in this direction
function totalsToFrequencies(totals, speeds) {
var sum = 0;
// Sum all the values in the dictionary
for (var dir in totals) {
sum += totals[dir];
}
if (sum == 0) { // total hack to work around the case where no months are selected
sum = 1;
}
// Build up an object containing frequencies
var ret = {};
ret.dirs = []
ret.sum = sum;
for (var dir in totals) {
var freq = totals[dir] / sum;
var avgspeed;
if (totals[dir] > 0) {
avgspeed = speeds[dir] / totals[dir];
} else {
avgspeed = 0;
}
if (dir == "null") { // winds calm is a special case
ret.calm = { d: null, p: freq, s: null };
} else {
ret.dirs.push({ d: parseInt(dir), p: freq, s: avgspeed });
}
}
return ret;
}
// Filter input data, giving back frequencies for the selected month
function rollupForMonths(d, months) {
var totals = {}, speeds = {};
for (var i = 10; i < 361; i += 10) { totals[""+i] = 0; speeds[""+i] = 0 }
totals["null"] = 0; speeds["null"] = 0;
for (var key in d.data) {
var s = key.split(":")
if (s.length == 1) {
var direction = s[0];
} else {
var month = s[0];
var direction = s[1];
}
if (months && !months[month-1]) { continue; }
// count up all samples with this key
totals[direction] += d.data[key][0];
// add in the average speed * count from this key
speeds[direction] += d.data[key][0] * d.data[key][1];
}
return totalsToFrequencies(totals, speeds);
}
/** Code for big visualization **/
// Transformation to place a mark on top of an arc
function probArcTextT(d) {
var tr = probabilityToRadius(d);
return "translate(" + visWidth + "," + (visWidth-tr) + ")" +
"rotate(" + d.d + ",0," + tr + ")"; };
function speedArcTextT(d) {
var tr = speedToRadius(d);
return "translate(" + visWidth + "," + (visWidth-tr) + ")" +
"rotate(" + d.d + ",0," + tr + ")"; };
// Return a string representing the wind speed for this datum
function speedText(d) { return d.s < 10 ? "" : d.s.toFixed(0); };
// Return a string representing the probability of wind coming from this direction
function probabilityText(d) { return d.p < 0.02 ? "" : (100*d.p).toFixed(0); };
// Map a wind speed to a color
var speedToColorScale = d3.scale.linear()
.domain([5, 25])
.range(["hsl(220, 70%, 90%)", "hsl(220, 70%, 30%)"])
.interpolate(d3.interpolateHsl);
function speedToColor(d) { return speedToColorScale(d.s); }
// Map a wind probability to a color
var probabilityToColorScale = d3.scale.linear()
.domain([0, 0.2])
.range(["hsl(0, 70%, 99%)", "hsl(0, 70%, 40%)"])
.interpolate(d3.interpolateHsl);
function probabilityToColor(d) { return probabilityToColorScale(d.p); }
// Width of the whole visualization; used for centering
var visWidth = 200;
// Map a wind probability to an outer radius for the chart
var probabilityToRadiusScale = d3.scale.linear().domain([0, 0.15]).range([34, visWidth-20]).clamp(true);
function probabilityToRadius(d) { return probabilityToRadiusScale(d.p); }
// Map a wind speed to an outer radius for the chart
var speedToRadiusScale = d3.scale.linear().domain([0, 20]).range([34, visWidth-20]).clamp(true);
function speedToRadius(d) { return speedToRadiusScale(d.s); }
// Options for drawing the complex arc chart
var windroseArcOptions = {
width: 5,
from: 34,
to: probabilityToRadius
}
var windspeedArcOptions = {
width: 5,
from: 34,
to: speedToRadius
}
// Draw a complete wind rose visualization, including axes and center text
function drawComplexArcs(parent, plotData, colorFunc, arcTextFunc, complexArcOptions, arcTextT) {
// Draw the main wind rose arcs
parent.append("svg:g")
.attr("class", "arcs")
.selectAll("path")
.data(plotData.dirs)
.enter().append("svg:path")
.attr("d", arc(complexArcOptions))
.style("fill", colorFunc)
.attr("transform", "translate(" + visWidth + "," + visWidth + ")")
.append("svg:title")
.text(function(d) { return d.d + "\u00b0 " + (100*d.p).toFixed(1) + "% " + d.s.toFixed(0) + "kts" });
// Annotate the arcs with speed in text
if (false) { // disabled: just looks like chart junk
parent.append("svg:g")
.attr("class", "arctext")
.selectAll("text")
.data(plotData.dirs)
.enter().append("svg:text")
.text(arcTextFunc)
.attr("dy", "-3px")
.attr("transform", arcTextT);
}
// Add the calm wind probability in the center
var cw = parent.append("svg:g").attr("class", "calmwind")
.selectAll("text")
.data([plotData.calm.p])
.enter();
cw.append("svg:text")
.attr("transform", "translate(" + visWidth + "," + visWidth + ")")
.text(function(d) { return Math.round(d * 100) + "%" });
cw.append("svg:text")
.attr("transform", "translate(" + visWidth + "," + (visWidth+14) + ")")
.attr("class", "calmcaption")
.text("calm");
}
// Update the page text after the data has been loaded
// Lots of template substitution here
function updatePageText(d) {
if (!('info' in d)) {
// workaround for stations missing in the master list
d3.selectAll(".stationid").text("????")
d3.selectAll(".stationname").text("Unknown station");
return;
}
document.title = "Wind History for " + d.info.id;
d3.selectAll(".stationid").text(d.info.id);
d3.selectAll(".stationname").text(d.info.name.toLowerCase());
var mapurl = 'map.html#10.00/' + d.info.lat + "/" + d.info.lon;
d3.select("#maplink").html('<a href="' + mapurl + '">' + d.info.lat + ', ' + d.info.lon + '</a>');
d3.select("#whlink").attr("href", mapurl);
var wsurl = 'http://weatherspark.com/#!dashboard;loc=' + d.info.lat + ',' + d.info.lon + ';t0=01/01;t1=12/31';
d3.select("#wslink").attr("href", wsurl);
var wuurl = 'http://www.wunderground.com/cgi-bin/findweather/getForecast?query=' + d.info.id;
d3.select("#wulink").attr("href", wuurl);
var vmurl = 'http://vfrmap.com/?type=vfrc&lat=' + d.info.lat + '&lon=' + d.info.lon + '&zoom=10';
d3.select("#vmlink").attr("href", vmurl);
var rfurl = 'http://runwayfinder.com/?loc=' + d.info.id;
d3.select("#rflink").attr("href", rfurl);
var nmurl = 'http://www.navmonster.com/apt/' + d.info.id;
d3.select("#nmlink").attr("href", nmurl);
}
// Update all diagrams to the newly selected months
function updateWindVisDiagrams(d) {
updateBigWindrose(d, "#windrose");
updateBigWindrose(d, "#windspeed");
}
// Update a specific digram to the newly selected months
function updateBigWindrose(windroseData, container) {
var vis = d3.select(container).select("svg");
var rollup = rollupForMonths(windroseData, selectedMonthControl.selected());
if (container == "#windrose") {
updateComplexArcs(vis, rollup, speedToColor, speedText, windroseArcOptions, probArcTextT);
} else {
updateComplexArcs(vis, rollup, probabilityToColor, probabilityText, windspeedArcOptions, speedArcTextT);
}
}
// Update drawn arcs, etc to the newly selected months
function updateComplexArcs(parent, plotData, colorFunc, arcTextFunc, complexArcOptions, arcTextT) {
// Update the arcs' shape and color
parent.select("g.arcs").selectAll("path")
.data(plotData.dirs)
.transition().duration(200)
.style("fill", colorFunc)
.attr("d", arc(complexArcOptions));
// Update the arcs' title tooltip
parent.select("g.arcs").selectAll("path").select("title")
.text(function(d) { return d.d + "\u00b0 " + (100*d.p).toFixed(1) + "% " + d.s.toFixed(0) + "kts" });
// Update the calm wind probability in the center
parent.select("g.calmwind").select("text")
.data([plotData.calm.p])
.text(function(d) { return Math.round(d * 100) + "%" });
}
// Top level function to draw all station diagrams
function makeWindVis(station) {
var url = "data/" + station + ".json";
var stationData = null;
d3.json(url, function(d) {
stationData = d;
updatePageText(d);
drawBigWindrose(d, "#windrose", "Frequency by Direction");
drawBigWindrose(d, "#windspeed", "Average Speed by Direction");
selectedMonthControl.setCallback(function() { updateWindVisDiagrams(d); });
});
selectedMonthControl = new monthControl(null);
selectedMonthControl.install("#monthControlDiv");
}
// Draw a big wind rose, for the visualization
function drawBigWindrose(windroseData, container, captionText) {
// Various visualization size parameters
var w = 400,
h = 400,
r = Math.min(w, h) / 2, // center; probably broken if not square
p = 20, // padding on outside of major elements
ip = 34; // padding on inner circle
// The main SVG visualization element
var vis = d3.select(container)
.append("svg:svg")
.attr("width", w + "px").attr("height", (h + 30) + "px");
// Set up axes: circles whose radius represents probability or speed
if (container == "#windrose") {
var ticks = d3.range(0.025, 0.151, 0.025);
var tickmarks = d3.range(0.05,0.101,0.05);
var radiusFunction = probabilityToRadiusScale;
var tickLabel = function(d) { return "" + (d*100).toFixed(0) + "%"; }
} else {
var ticks = d3.range(5, 20.1, 5);
var tickmarks = d3.range(5, 15.1, 5);
var radiusFunction = speedToRadiusScale;
var tickLabel = function(d) { return "" + d + "kts"; }
}
// Circles representing chart ticks
vis.append("svg:g")
.attr("class", "axes")
.selectAll("circle")
.data(ticks)
.enter().append("svg:circle")
.attr("cx", r).attr("cy", r)
.attr("r", radiusFunction);
// Text representing chart tickmarks
vis.append("svg:g").attr("class", "tickmarks")
.selectAll("text")
.data(tickmarks)
.enter().append("svg:text")
.text(tickLabel)
.attr("dy", "-2px")
.attr("transform", function(d) {
var y = visWidth - radiusFunction(d);
return "translate(" + r + "," + y + ") " })
// Labels: degree markers
vis.append("svg:g")
.attr("class", "labels")
.selectAll("text")
.data(d3.range(30, 361, 30))
.enter().append("svg:text")
.attr("dy", "-4px")
.attr("transform", function(d) {
return "translate(" + r + "," + p + ") rotate(" + d + ",0," + (r-p) + ")"})
.text(function(dir) { return dir; });
var rollup = rollupForMonths(windroseData, selectedMonthControl.selected());
if (container == "#windrose") {
drawComplexArcs(vis, rollup, speedToColor, speedText, windroseArcOptions, probArcTextT);
} else {
drawComplexArcs(vis, rollup, probabilityToColor, probabilityText, windspeedArcOptions, speedArcTextT);
}
vis.append("svg:text")
.text(captionText)
.attr("class", "caption")
.attr("transform", "translate(" + w/2 + "," + (h + 20) + ")");
}
/** Code for small wind roses **/
// Plot a small wind rose with the specified percentage data
// parent: the SVG element to put the plot on
// plotData: a list of 12 months, each a list of 13 numbers. plotData[month][0] is winds calm percentage,
// plotData[month][1, 2, 3...] is percentage of winds at 30 degrees, 60, 90, ...
var smallArcScale = d3.scale.linear().domain([0, 0.15]).range([5, 30]).clamp(true)
var smallArcOptions = {
width: 15,
from: 5,
to: function(d) { return smallArcScale(d.p); }
}
function plotSmallRose(parent, plotData) {
var winds = [];
var months = selectedMonthControl.selected();
// For every wind direction (note: skip plotData[0], winds calm)
for (var dir = 1; dir < 13; dir++) {
// Calculate average probability for all selected months
var n = 0; sum = 0;
for (var month = 0; month < 12; month++) {
if (months[month]) {
n += 1;
sum += plotData[month][dir];
}
}
var avg = sum/n;
winds.push({d: dir * 30, p: avg / 100});
}
parent.append("svg:g")
.selectAll("path")
.data(winds)
.enter().append("svg:path")
.attr("d", arc(smallArcOptions));
parent.append("svg:circle")
.attr("r", smallArcOptions.from);
}
/** Map code **/
// Augment d3.geo with code that aids tiling data in Polymaps
function installGeoTiler() {
// Temporary code from http://bl.ocks.org/900050 (gist 900050)
if (d3.geo.tiler) {
console.log("d3.geo.tiler defined: not using ours. Good luck!");
} else {
d3.geo.tiler = function() {
var tiler = {},
points = [],
projection = d3.geo.mercator().scale(1).translate([.5, .5]),
location = Object, // identity function
zoom = 8,
root = null;
function build(points, x, y, z) {
if (z >= zoom) return points.map(d3_geo_tilerData);
var i = -1,
n = points.length,
c = [[], [], [], []],
k = 1 << z++,
p;
while (++i < n) {
p = points[i];
var x1 = (p[0] * k - x) >= .5,
y1 = (p[1] * k - y) >= .5;
c[x1 << 1 | y1].push(p);
}
x <<= 1;
y <<= 1;
return {
"0": c[0].length && build(c[0], x , y , z),
"1": c[1].length && build(c[1], x , y + 1, z),
"2": c[2].length && build(c[2], x + 1, y , z),
"3": c[3].length && build(c[3], x + 1, y + 1, z)
};
}
tiler.location = function(x) {
if (!arguments.length) return location;
location = x;
root = null; // reset
return tiler;
};
tiler.projection = function(x) {
if (!arguments.length) return projection;
projection = x;
root = null; // reset
return tiler;
};
tiler.zoom = function(x) {
if (!arguments.length) return zoom;
zoom = x;
root = null; // reset
return tiler;
};
tiler.points = function(x) {
if (!arguments.length) return points;
points = x;
root = null; // reset
return tiler;
};
tiler.tile = function(x, y, z) {
var results = [];
// Lazy initialization…
// Project the points to normalized coordinates in [0, 1].
if (!root) {
root = build(points.map(function(d, i) {
var point = projection(location.call(tiler, d, i));
point.data = d;
return point;
}), 0, 0, 0);
}
function search(node, x0, y0, z0) {
if (!node) return;
if (z0 < z) {
var k = Math.pow(2, z0 - z),
x1 = (x * k - x0) >= .5,
y1 = (y * k - y0) >= .5;
search(node[x1 << 1 | y1], x0 << 1 | x1, y0 << 1 | y1, z0 + 1);
} else {
accumulate(node);
}
}
function accumulate(node) {
if (node.length) {
for (var i = -1, n = node.length; ++i < n;) {
results.push(node[i]);
}
} else {
for (var i = -1; ++i < 4;) {
if (node[i]) accumulate(node[i]);
}
}
}
search(root, 0, 0, 0);
return results;
};
return tiler;
};
function d3_geo_tilerData(d) {
return d.data;
}
}
}
// A custom Polymaps layer for the wind roses.
// Copied and modified from Mike Bostock's gist 900050 http://bl.ocks.org/900050
function stations(url, callback) {
installGeoTiler();
// Create the tiler, for organizing our points into tile boundaries.
var tiler = d3.geo.tiler()
.zoom(12)
.location(function(d) { return d.value; });
// Create the base layer object, using our tile factory.
var layer = org.polymaps.layer(load);
layer.id("stations");
// Load the station data. When the data comes back, reload.
d3.json(url, function(json) {
callback(json);
tiler.points(d3.entries(json));
layer.reload();
});
// Custom tile implementation.
function load(tile, projection) {
projection = projection(tile).locationPoint;
// Add an svg:g for each station.
var g = d3.select(tile.element = po.svg("g")).selectAll("g")
.data(tiler.tile(tile.column, tile.row, tile.zoom))
.enter()
.append("svg:a")
.attr("xlink:href", function(d) { return "station.html?" + d.key })
.attr("target", "_blank")
.append("svg:g")
.attr("class", "station")
.attr("transform", transform);
// svg:title for each station, visible on hover
g.append("svg:title").text(function(d) { return d.value[2] });
// Draw the mark for each station. Just a dot if zoomed out too far.
if (tile.zoom > 6) {
g.each(function(d) {
plotSmallRose(d3.select(this), d.value[3]);
});
} else {
g.append("svg:circle").attr("r", 5).attr("class", "alone");
}
function transform(d) {
d = projection({lon: d.value[0], lat: d.value[1]});
return "translate(" + d.x + "," + d.y + ")";
}
}
return layer;
}
// Global state for the map view
var stationDb = null;
var map = null;
var po = (typeof org != "undefined") ? org.polymaps : null;
var selectedMonthControl = null;
// Top level function for making the map view
function makeMap() {
var mapLocationInUrl = location.hash != "";
// Construct our map
map = po.map()
.container(document.getElementById("map").appendChild(po.svg("svg")))
.add(po.dblclick())
.add(po.drag())
.add(po.arrow())
.add(po.wheel().smooth(false))
.add(po.hash())
.add(po.touch().rotate(false));
// Handle users who come with no location specified in the URL
if (!mapLocationInUrl) {
// Center the map on North America
map.center({lon: -95, lat: 36});
map.zoom(4);
// Fire off a geolocation request to center the user
if (!!navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
function (pos) {
map.center({lon: pos.coords.longitude, lat: pos.coords.latitude});
map.zoom(8);
},
function (code, message) { },
{ maximumAge: 1000*60*60, timeout: 1000*10, enableHighAccuracy: false});
}
}
// Add a basemap: OpenStreetMap rendered with Mapnik
map.add(po.image().id("osmMapnik")
.url(po.url("http://{S}tile.openstreetmap.org/{Z}/{X}/{Y}.png")
.hosts(["a.", "b.", "c.", ""])));
map.zoomRange([2,12]);
// Add a layer for all the stations
var stationLayer = stations("data/stations-md.json", function(d) { stationDb = d; });
map.add(stationLayer);
// Add the compass
map.add(po.compass().pan("none"));
// Add the month control
selectedMonthControl = new monthControl(stationLayer.reload);
selectedMonthControl.install("#monthControlDiv");
}
// Search for station: center and zoom
function searchMap(form) {
var q = document.forms[0].elements[0].value;
q = q.toUpperCase();
var s = stationDb[q];
if (s) {
map.center({lon: s[0], lat: s[1]});
if (map.zoom() < 8) {
map.zoom(8);
}
} else {
document.forms[0].elements[0].style["background-color"]="#d55";
setTimeout(function () { document.forms[0].elements[0].style.removeProperty("background-color"); },
150);
}
}
@sareeshkrishnan
Copy link

Can anyone provide a simple example of how to plot wind rose using d3.js library..?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment