block by nitaku ba2a02bc93fe36e7b5b5

Weather wheel II

Full Screen

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.

index.js

// 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);

index.html

<!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>

d3-path.js

(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;

}));

index.coffee

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'
      

index.css

@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;
}

wu_get_history.php

<?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;
?>