Displaying weather data from the Global Historical Climatology Network (GHCN)

In our previous blog posts, we downloaded and analyzed the GHCN weather data. That leads us to the next step:  Displaying the data!

Our goal is to display the climate change data so that the regional trends are clearly visible as well as the local weather history underpinning those trends. In order to do this we decided on the following graphics components:

  • displaying data on a map, both in the form of point locations (the weather stations) and bitmap overlays (regional weather trends). The Leaflet library of JavaScript routines handles this part.
  • displaying graphs of the trends in local weather data. The D3.js library handles this.
  • and displaying the data history for a given weather station as a heatmap. We draw directly to

    an HTML <canvas> for this.

Each of these approaches helped with a specific part of the final page; in this blog post we’ll give brief introductions to them.

The Leaflet library

Leaflet is a compact and well-organized set of Javascript routines for drawing map data. The most basic use is to set up a <div> to hold the map, then set up the map with a few Leaflet calls:

<div id="mapid" style="width: 600px; height: 400px;">&lt;/div>
<script>
    var attrib = 'Map tiles by Stamen Design, under CC BY 3.0.' +
                 ' Data by OpenStreetMap, under ODbL';
    var serverURL = "http://{s}.tile.stamen.com/toner-lite/{z}/{x}/{y}.png";

    var mymap = L.map('mapid').setView([42, -88], 6);
    L.tileLayer(serverURL, {attribution: attrib,}).addTo(mymap);
// at this point the map is set up and displays!  One can continue from here,
// adding other components to the map.

The critical components here are the L.map() call, which creates a map display area on the web page, and the addTo() call, which connects map tiles to that display area. The serverURL is a template for getting map tiles from stamen.com’s tile server (info here). To request a map tile from a tile server Leaflet needs to specify the x and y locations of the tile as well as its zoom level; Leaflet uses the serverURL pattern to translate those x, y, and z numbers into a URL appropriate for this tile server.

Adding markers to a map is straightforward:

    L.marker([41.8, -87.7]).addTo(mymap);

Adding a bitmap overlay is the same idea; the location is a rectangle instead of a point.

    var imageUrl = "rainfallTrend.png";
    var imageBounds = [[26.78, -123.88], [49.01, -69.85]];
    L.imageOverlay(imageUrl, imageBounds, {opacity:0.5}).addTo(mymap);

Leaflet maps have zoom and pan interactions built-in; adding other interaction, for instance click handling for the markers, can be as easy as

    var marker = L.marker([lat,lon],{icon: ourcircle});
    marker.on( 'click', markerClickHandlerFn, auxData );

The Leaflet site has excellent tutorials.

Here’s a full example including all the boilerplate:

<!DOCTYPE html>
<html>
<head>
	<title>Leaflet with overlay</title>
	<meta charset="utf-8" />
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<link rel="stylesheet" href="https://unpkg.com/leaflet@1.0.3/dist/leaflet.css" />
	<script src="https://unpkg.com/leaflet@1.0.3/dist/leaflet.js"></script>
</head>
<body>
<!-- attribution info: http://maps.stamen.com/#toner/12/37.7706/-122.3782 -->
<div id="mapid" style="width: 600px; height: 400px;"></div>
<script>
    var attrib = 'Map tiles by Stamen Design, under CC BY 3.0.' +
                 ' Data by OpenStreetMap, under ODbL';
    var serverURL = "http://{s}.tile.stamen.com/toner-lite/{z}/{x}/{y}.png";

    var mymap = L.map('mapid').setView([43, -107], 5);
    L.tileLayer(serverURL, {attribution: attrib,}).addTo(mymap);
    // put an image on the map to just match Wyoming
    var imageUrl = "https://upload.wikimedia.org/wikipedia/commons/f/f0/Willow_Flats_area_and_Teton_Range_in_Grand_Teton_National_Park.jpg";
    var imageBounds = [[41, -111.05], [45, -104.05]];  // Wyoming's bounds
    L.imageOverlay(imageUrl, imageBounds, {opacity:0.5}).addTo(mymap);
</script>
</body>
</html>

The D3 javascript library

D3 is a javascript library especially useful for making graphs. At its core it works not by providing new graphics APIs but by providing a wrapper around standard HTML graphics structures. For instance, instead of writing this HTML

<svg>
    <g>
        <rect x="20" y="30" width="45" height="25" fill="#956" />
    </g>
</svg>

you can write this block of Javascript using D3:

var svg = d3.select("svg");  // find an svg element in the HTML
svg.append("g").append("rect")  // now build the <g> tag and <rect> underneath it...
   .attr("x", 20)
   .attr("y", 30)
   .attr("width", 45)
   .attr("height", 25)
   .attr("fill","#658");

comparison of HTML and D3 rectangles

D3 helps with graphing specifically by providing utilities like axis scaling and tick marks, as well as some convenient looping and filtering constructs. Here is a full example of a small graph:

<!DOCTYPE html>
<html>
<head>
    <title>D3 plot example</title>

    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <link rel="stylesheet" type="text/css" href="css/d3graph.css">
    <script src="http://d3js.org/d3.v3.min.js"></script>
</head>
<body>

    <svg class="chart" width="200" height="200"> </svg>  <!-- the render area -->
    <script>
        var chartdim={width:200,height:200},
            plotmargin={top:0,left:35,bottom:20,right:0}, // axes at left,bottom
            plotdim={width: chartdim.width-(plotmargin.left+plotmargin.right),
                     height: chartdim.height-(plotmargin.top+plotmargin.bottom)};
        // note:  x, y, xAxis, and yAxis are functions; the declarations set up
        // some default arguments, and the functions get called later when
        // we need graphics put on the page.
        var x = d3.scale.linear()
                 .domain([-5,5])
                 .range([plotmargin.left, plotmargin.left+plotdim.width]);
        var y = d3.scale.linear()
                 .domain([-5,5])
                 .range([plotmargin.top+plotdim.height,plotmargin.top]);
        var xAxis = d3.svg.axis().scale(x).orient("bottom").ticks(5);  // uses the x scale
        var yAxis = d3.svg.axis().scale(y).orient("left").ticks(5);

        // fake data for now; ideally we'd read it from a .csv file
        var points =
             [{horiz: -3, vert: -4},
              {horiz: -4, vert: -3},
              {horiz: -2, vert: -1},
              {horiz: -2, vert:  3},
              {horiz: -2, vert:  2},
              {horiz: -1, vert:  0},
              {horiz:  0, vert: -4},
              {horiz:  1, vert: -1},
              {horiz:  1, vert:  3},
              {horiz:  3, vert: -2},
              {horiz:  4, vert:  1},
             ];
        var svg = d3.select("svg");  // the rendering area

        // Add the points
        svg.append("g")
            .selectAll("dot")	
            .data(points)
            .enter().append("circle") // a looping construct, to draw a circle for each data point.
            .attr("r", 4)		
            .attr("cx", function(d) { return x(d.horiz); })
            .attr("cy", function(d) { return y(d.vert); })
            .attr("fill","#888");  //  make dots gray

        // Add the X axis -- translate it to the bottom
        svg.append("g")
            .attr("transform", "translate(0," + (plotdim.height) + ")")
            .style({"stroke-width": "1px"})
            .style({"stroke": "#555"})
            .style({"fill": "none"})  // required for the axis line but affects text too...
            .call(xAxis);

        // Add the Y axis -- translated to the left edge
        svg.append("g")
            .attr("transform", "translate(" + (plotmargin.left) + "," + 0 + ")")
            .style({"stroke-width": "1px"})
            .style({"stroke": "#555"})
            .style({"fill": "none"})
            .call(yAxis);

        // ... reinstate fill for the axis text
        svg.selectAll("text").style({ "fill": "#555"});
    </script>
</body>
</html>

Note the distinctive “method chaining” style of D3 code. Functions in D3 generally return the same object they’re applied to; this lets you link one function call to the next without using variables. The style, customary in D3 work, can be very compact and readable. For more information on D3, see the huge number of examples and tutorials available.

Direct HTML drawing

HTML provides two main kinds of tags to use for graphics rendering: <canvas> and <svg>. SVG has the advantage of resizing: magnified graphics are still crisp-edged and don’t show pixelation. Canvas, on the other hand, gives you pixel-level control, at the cost of pixelation at high zooms. D3 can wrap either type of rendering area — though since we only need simple pixel operations on a bitmap it seems less overhead to skip D3 and work directly with the canvas.

The technique for editing a bitmap is

  1. request the bitmap’s memory array from the canvas
  2. set pixel colors by altering the values in the bitmap array
  3. replace the canvas memory array with your altered values

One further detail about the bitmap memory array: One might expect that you’d index into the array by [x,y], and at the [x,y] location you’d find the pixel’s R,G,B value. Instead, the array is a single long vector which scans the bitmap a row at a time: if the bitmap’s width is 100 pixels, the pixel for [x,y] is stored at [x + y*100]. Further, the RGB values are interleaved in the array: R,G,B,A,R,G,B,A,… where A is the “alpha” (opacity) of the pixel. For example, in a 100×100 bitmap, to turn pixel [5,8] red, you’d do this:

    imgData.data[ (5 + 8*100) * 4    ] = 255; // red gets 255 (full on)
    imgData.data[ (5 + 8*100) * 4 + 1] = 0;   // green gets 0 (no green)
    imgData.data[ (5 + 8*100) * 4 + 2] = 0;   // blue gets 0 (no blue)
    imgData.data[ (5 + 8*100) * 4 + 3] = 255; // the pixel is opaque

Here is a complete example:

<!DOCTYPE html>
<html>
<head>
    <title>Canvas plot example</title>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
    <canvas id="chart" width="200" height="200"> </canvas>  <!-- the render area -->
    <div></div>
    <script>
        // boilerplate to get access to the bitmap
        var canv = document.getElementById("chart");
        var ctx = canv.getContext("2d");

        // fill the render area with white
        ctx.fillStyle="white";
        ctx.fillRect(0,0,200,200);

        // get the bitmap memory array for the canvas
        var imgData = ctx.getImageData(0,0,canv.width, canv.height);

        // set the pixels to be dark at the center and lightening outward
        for ( i = 0; i < 200; i++ ) {
            for ( j = 0; j < 200; j++ ) {
                var rad = Math.sqrt( (j-100)*(j-100) + (i-100)*(i-100) );
                var whitefrac = rad / 150;
                idx = ( i+j*200) * 4;
                imgData.data[idx+0] = 255*whitefrac;
                imgData.data[idx+1] = 255*whitefrac;
                imgData.data[idx+2] = 255*whitefrac;
                imgData.data[idx+3] = 255;
            }
        }

        // and display the bitmap
        ctx.putImageData(imgData,0,0);
    </script>
</body>
</html>

A canvas provides not just per-pixel access to the display but also some higher-level rendering operations. Text, conveniently:

    ctx.font="12px Arial";
    ctx.fillStyle = "#000000";
    ctx.fillText("CaptionForGraph",45,90);

One advantage of using raw html is that there are no dependencies on external libraries like D3 or Leaflet, which can help make the page smaller and quicker to load. (This turns out not to be a significant advantage for the GHCN data, since the code size is tiny compared to the data we’re downloading.) For more information on drawing directly to a canvas, a good place to start is w3schools.

What’s next?

In most projects, visualizing the data is a first step, guiding either presentation or further research. In this case we want the data to speak for itself; we’re leaving the discovery phase, as much as we can, to site visitors — and, ideally, to people who decide to do this same experiment for themselves, downloading the data and writing their own styles of display.