block by armollica a3629bcf68f46f16b76dc92650fedf7b

Wind speed

Full Screen

Source: NOAA - Great Lakes Environmental Research Laboratory.

index.html

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link href="https://fonts.googleapis.com/css?family=Inconsolata:400,700" rel="stylesheet">
<style>

html,
body {
    font-family: 'Inconsolata', monospace;   
}

.hidden {
    /* display: none; */
}

.container {
    position: relative;
}

.container canvas,
.container svg {
    position: absolute;
    left: 0;
    top: 0;
}

.container .tooltip {
    position: absolute;
    top: 0px;
    right: 0px;
}

.tick text {
    font-family: 'Inconsolata', monospace;
}

.line {
    fill: none;
    stroke: #000;
}

.axis text {
    font-size: 14px;
    fill: #999;
    text-shadow: -1px -1px 1px #fff,
                 -1px 0px 1px #fff,
                 -1px 1px 1px #fff,
                 0px -1px 1px #fff,
                 0px 1px 1px #fff,
                 1px -1px 1px #fff,
                 1px 0px 1px #fff,
                 1px 1px 1px #fff;
}

.axis .domain {
    display: none;
}

.axis .title {
    font-family: 'Inconsolata', monospace;
    font-size: 16px;
    fill: #434343;
}

.axis--x.axis--under .tick text {
    display: none;
}

.axis--x.axis--over .tick line {
    display: none;
}

.axis--x .tick line {
    stroke: #ddd;
}

.axis--x.axis--extra .tick line {
    display: none;
}

.axis--y .tick line {
    stroke: #ccc;
}

.axis--y .tick.midnight line {
    stroke: #666;
}

.axis--y .tick.midnight text.date {
    font-size: 20px;
    font-weight: bold;
    fill: #434343;
}

.tooltip .left,
.tooltip .right {
    height: 60px;
}

.tooltip .left {
    float: left;
    width: 100px;
}

.tooltip .right {
    float: right;
    width: 50px;
}

.tooltip .right .title {
    text-anchor: middle;
    text-transform: uppercase;
    font-size: 9px;
    font-weight: bold;
    fill: #999;
}

.tooltip .right svg {
    position: static;
    width: 100%;
    height: 100%;
}

.tooltip svg line {
    stroke: #555;
    stroke-width: 4px;
}

.tooltip svg #arrow {
    fill: #555;
}

.tooltip .date {
    font-size: 14px;
    color: #999;
}

.tooltip .time {
    font-size: 16px;
    font-weight: bold;
    color: #434343;
    margin-bottom: 5px;
}

.tooltip .temp {
    font-size: 14px;
    color: #666;
}

.tooltip--svg line {
    stroke: #434343;
    stroke-dasharray: 2, 2;
}

.tooltip--svg .marker text {
    text-shadow: -1px -1px 1px #fff,
                 -1px 0px 1px #fff,
                 -1px 1px 1px #fff,
                 0px -1px 1px #fff,
                 0px 1px 1px #fff,
                 1px -1px 1px #fff,
                 1px 0px 1px #fff,
                 1px 1px 1px #fff;
}

.tooltip--svg .marker .label {
    font-size: 10px;
    font-weight: bold;
    fill: #999;
    text-transform: uppercase;
}

.tooltip--svg .marker .value {
    font-size: 12px;
    fill: #434343;
}

.tooltip--svg .marker .tick {
    stroke: #434343;
    stroke-dasharray: none;
}

</style>
</head>
<body>

<div class="container">
    <svg class="layer--under"></svg>
    <canvas></canvas>
    <div class="tooltip">
        <div class="left">
            <div class="date"></div>
            <div class="time"></div>
            <div class="temp"></div>
        </div>
        <div class="right">
            <svg class="right" viewBox="0 0 50 50" preserveAspectRatio="xMidYMid meet">
                <defs>
                    <marker id="arrow" refX="2" refY="2" markerWidth="5" markerHeight="5" orient="auto">
                        <path d="M 0 0 L 0 4 L 4 2 z" />
                    </marker>
                </defs>
                <g transform="translate(25, 25)">
                    <line x1="15" x2="-15" marker-end="url(#arrow)"></line>
                </g>
                <text class="title" x="25" y="0" dy="0.33em">Wind dir.</text>
            </svg>
        </div>
    </div>
    <svg class="layer--over"></svg>
</div>

<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
<script>

var margin = { top: 30, right: 10, bottom: 10, left: 75 },
    width = 960 - margin.left - margin.right,
    height = 30000 - margin.top - margin.bottom;

var container = d3.select('.container')
    .style('width', (width + margin.left + margin.right) + 'px')
    .style('height', (height + margin.top + margin.bottom) + 'px');

var canvas = container.select('canvas')
    .attr('width', width) 
    .attr('height', height)
    .style('left', margin.left + 'px')
    .style('top', margin.top + 'px');

var context = canvas.node().getContext('2d');

var gUnder = container.select('svg.layer--under')
        .attr('width', width + margin.left + margin.right)
        .attr('height', height + margin.top + margin.bottom)
    .append('g')
        .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');

var gOver = container.select('svg.layer--over')
        .attr('width', width + margin.left + margin.right)
        .attr('height', height + margin.top + margin.bottom)
    .append('g')
        .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');

var tooltip = container.select('.tooltip');

var gTooltip = gOver.append('g')
    .attr('class', 'tooltip--svg');

gTooltip.append('line')
    .attr('class', 'marker marker--divider')
    .attr('x0', 0)
    .attr('x1', width);

var markerMax = gTooltip.append('g')
    .attr('class', 'marker marker--max');

markerMax.append('text')
    .attr('class', 'label')
    .attr('x', -5)
    .attr('y', -5)
    .attr('dy', '-1.67em')
    .text('Max.');

markerMax.append('text')
    .attr('class', 'value')
    .attr('x', -5)
    .attr('y', -5)
    .attr('dy', '-0.33em');

markerMax.append('line')
    .attr('class', 'tick')
    .attr('y1', 0)
    .attr('y2', -5);

var markerAvg = gTooltip.append('g')
    .attr('class', 'marker marker--avg');

markerAvg.append('text')
    .attr('class', 'label')
    .attr('x', -5)
    .attr('y', -5)
    .attr('dy', '-1.67em')
    .text('Avg.');

markerAvg.append('text')
    .attr('class', 'value')
    .attr('x', -5)
    .attr('y', -5)
    .attr('dy', '-0.33em');

markerAvg.append('line')
    .attr('class', 'tick')
    .attr('y1', 0)
    .attr('y2', -5);

var backgroundRect = gOver.append('rect')
    .attr('width', width)
    .attr('height', height)
    .style('fill-opacity', 0);

var gXAxisUnder = gUnder.append('g').attr('class', 'axis axis--x axis--under'),
    gXAxisOver = gOver.append('g').attr('class', 'axis axis--x axis--over'),
    gYAxis = gOver.append('g').attr('class', 'axis axis--y axis--over');

var bisectDate = d3.bisector(function(d) { return d.datetime; }).left;

var xScale = d3.scaleLinear().range([0, width]),
    xAxis = d3.axisTop(xScale);

function formatTick(d) {
    var hour = d.getHours(),
        midnight = hour === 0,
        noon = hour === 12;
    if (midnight) return d3.timeFormat('%A, %b %_d')(d);
    else if (noon) return 'Noon';
    return d3.timeFormat('%_I %p')(d);
}
var yScale = d3.scaleTime().range([0, height]),
    yAxis = d3.axisLeft(yScale)
        .ticks(d3.timeHour)
        .tickFormat(formatTick);

var colorScale = d3.scaleSequential(d3.interpolateBuPu);

var area = d3.area()
    .x0(0)
    .y(function(d) { return yScale(d.datetime); })
    .curve(d3.curveStep)
    .context(context);

var parseDatetime = d3.timeParse('%Y-%m-%d %H:%M');

function row(d) {
    return {
        datetime: parseDatetime(d.datetime),
        avg_wind_speed: +d.avg_wind_speed,
        max_wind_speed: +d.max_wind_speed,
        wind_direction: +d.wind_direction,
        temp: +d.temp
    };
}

d3.csv('wind.csv', row, function(error, wind) {
    if (error) throw error;

    // ----
    // Updates scales based on wind data

    xScale.domain([0, d3.max(wind, function(d) { return d.max_wind_speed; })]);
    yScale.domain(d3.extent(wind, function(d) { return d.datetime; }));
    colorScale.domain(d3.extent(xScale.ticks()));

    // ----
    // Draw axes

    gXAxisUnder.call(xAxis)
        .selectAll('.tick line')
            .attr('y1', height);

    gXAxisOver.call(xAxis);

    gXAxisOver.selectAll('.tick text')
        .attr('dx', 5)
        .attr('dy', '1em')
        .attr('text-anchor', 'start')
        .filter(function(d) {
            var lastTick = xScale.ticks().slice(-1)[0];
            return d === lastTick;
        })
        .text(function(d) { return d + ' mph'; });
    
    gXAxisOver.append('text')
        .attr('class', 'title')
        .attr('x', width)
        .attr('dx', -5)
        .attr('y', 0)
        .attr('dy', '-1em')
        .attr('text-anchor', 'end')
        .text('Wind speed');
    
    gYAxis.call(yAxis)
        .selectAll('.tick')
        .classed('midnight', function(d) { return d.getHours() === 0; })
        .classed('noon', function(d) { return d.getHours() === 12; })
        .each(function(d) {
            var hour = d.getHours(),
                midnight = hour === 0,
                noon = hour === 12,
                line = d3.select(this).select('line'),
                text = d3.select(this).select('text');
            if (midnight) {
                line.attr('x1', width);
                text
                    .attr('class', 'date')
                    .attr('x', width)
                    .attr('dx', -5)
                    .attr('dy', '-0.67em');

                d3.select(this).append('text')
                    .attr('x', -8)
                    .attr('dy', '0.33em')
                    .attr('text-anchor', 'end')
                    .text('Midnight');
            } else if (noon) {
                line.attr('x1', width);
            }

            if (midnight || noon) {
                d3.select(this).append('g')
                        .attr('class', 'axis axis--x axis--extra')
                        .call(xAxis)
                    .selectAll('.tick text')
                        .attr('dx', 5)
                        .attr('y', 0)
                        .attr('dy', '1em')
                        .attr('text-anchor', 'start')
                        .filter(function(d) {
                            var lastTick = xScale.ticks().slice(-1)[0];
                            return d === lastTick;
                        })
                        .text(function(d) { return d + ' mph'; });
            }
        });

    // ----
    // Draw specks for max wind speed

    area.x1(function(d) { return xScale(d.max_wind_speed); });

    context.save();
    wind.forEach(function(d) {
        var x = xScale(d.max_wind_speed),
            y = yScale(d.datetime);
        context.beginPath();
        context.fillStyle = colorScale(d.max_wind_speed);
        context.fillRect(x - 1, y - 1, 2, 2);
    });
    context.restore();

    // ----
    // Draw area slices for average wind speed

    area.x1(function(d) { return xScale(d.avg_wind_speed); });
    
    xScale.ticks()
        .slice(1)
        .reverse()
        .forEach(function(cap) {
            area.x1(function(d) {
                if (d.avg_wind_speed > cap) return xScale(cap);
                return xScale(d.avg_wind_speed);
            });
            context.save();
            context.beginPath();
            area(wind);
            context.fillStyle = colorScale(cap);
            context.fill();
            context.restore();
        });
    
    function updateTooltip(y) {
        var y0 = yScale.invert(y),
            i = bisectDate(wind, y0, 1),
            d0 = wind[i - 1],
            d1 = wind[i],
            d = y0 - d0.datetime > d1.datetime - y0 ? d1 : d0;
        
        var y = yScale(d.datetime);

        gTooltip
            .attr('transform', 'translate(0,' + y + ')');

        markerAvg
            .attr('transform', 'translate(' + xScale(d.avg_wind_speed) + ',0)');
    
        markerAvg.select('.value')
            .text(d3.format('.0f')(d.avg_wind_speed));

        markerMax
            .attr('transform', 'translate(' + xScale(d.max_wind_speed) + ',0)');
        
        markerMax.select('.value')
            .text(d3.format('.0f')(d.max_wind_speed) + ' mph');

        tooltip
            .style('top', (y + margin.top + 5) + 'px');
    
        var time = d3.timeFormat('%_I:%M %p')(d.datetime),
            date = d3.timeFormat('%b %_d, %Y')(d.datetime),
            fahrenheit = d3.format('.0f')(celsiusToFahrenheit(d.temp)),
            celsius = d3.format('.0f')(d.temp),
            temp = fahrenheit + ' °F (' + celsius + ' °C)';

        tooltip.select('.time').text(time);
        tooltip.select('.date').text(date);
        tooltip.select('.temp').text(temp);

        tooltip.select('svg line')
            .attr('transform', 'rotate(' + d.wind_direction + ')');
    }

    function mousemove() {
        var y = d3.mouse(this)[1];
        updateTooltip(y);
    }

    function mouseenter() {
        gTooltip.classed('hidden', false);
        tooltip.classed('hidden', false);
    }

    function mouseleave() {
        gTooltip.classed('hidden', true);
        tooltip.classed('hidden', true);
    }

    function wheel() {
        var transform = gTooltip.attr('transform');
        if (transform) {
            var y0 = +transform.split(',')[1].replace(')', ''),
                y = y0 + d3.event.deltaY;
            updateTooltip(y);
        }
    }

    backgroundRect
        .on('mouseenter', mouseenter)
        .on('mouseleave', mouseleave)
        .on('mousemove', mousemove);    

    d3.select(window)
        .on('wheel', wheel);
});

function celsiusToFahrenheit(celsius) {
    return celsius * 9 / 5 + 32;
}

</script>
</body>
</html>

Makefile


SHELL=/bin/bash

wind.csv:
	node gimme-wind.js -d 2017-12-01 > wind-1.csv
	node gimme-wind.js -d 2017-12-02 > wind-2.csv
	node gimme-wind.js -d 2017-12-03 > wind-3.csv
	node gimme-wind.js -d 2017-12-04 > wind-4.csv
	node gimme-wind.js -d 2017-12-05 > wind-5.csv
	node gimme-wind.js -d 2017-12-06 > wind-6.csv
	node gimme-wind.js -d 2017-12-07 > wind-7.csv
	node gimme-wind.js -d 2017-12-08 > wind-8.csv
	node gimme-wind.js -d 2017-12-09 > wind-9.csv
	node gimme-wind.js -d 2017-12-10 > wind-10.csv
	node gimme-wind.js -d 2017-12-11 > wind-11.csv
	node gimme-wind.js -d 2017-12-12 > wind-12.csv
	node gimme-wind.js -d 2017-12-13 > wind-13.csv
	node gimme-wind.js -d 2017-12-14 > wind-14.csv
	node gimme-wind.js -d 2017-12-15 > wind-15.csv
	node gimme-wind.js -d 2017-12-16 > wind-16.csv
	node gimme-wind.js -d 2017-12-16 > wind-17.csv
	cat wind-1.csv \
		<(tail -n +2 wind-2.csv) \
		<(tail -n +2 wind-3.csv) \
		<(tail -n +2 wind-4.csv) \
		<(tail -n +2 wind-5.csv) \
		<(tail -n +2 wind-6.csv) \
		<(tail -n +2 wind-7.csv) \
		<(tail -n +2 wind-8.csv) \
		<(tail -n +2 wind-9.csv) \
		<(tail -n +2 wind-10.csv) \
		<(tail -n +2 wind-11.csv) \
		<(tail -n +2 wind-12.csv) \
		<(tail -n +2 wind-13.csv) \
		<(tail -n +2 wind-14.csv) \
		<(tail -n +2 wind-15.csv) \
		<(tail -n +2 wind-16.csv) \
		<(tail -n +2 wind-17.csv) \
		> $@
	rm wind-*.csv

gimme-wind.js

#!/usr/bin/env node

const os = require('os');
const fs = require('fs');
const d3 = require('d3');
const request = require('request');
const commander = require('commander');

// Parse from UTC
function parseDatetime(year, day_of_year, time) {
    return d3.utcParse('%Y %j %H%M')(year + ' ' + day_of_year + ' ' + time);
}

// Format in CST
const formatDatetime = d3.timeFormat('%Y-%m-%d %H:%M');

function parseValue(d) {
    const value = parseFloat(d);
    if (value == -99) return null;
    return value;
}

//            N   E   S   W
// GLERL:     0  90 180 270
// SVG/CSS: 270   0  90 180
function parseWindDirection(d) {
    var direction = parseValue(d) - 90;
    if (direction < 0) return direction + 360
    return direction;
}

// 2.23694 mph == 1 m/s
function parseWindSpeed(d) {
    return parseValue(d) * 2.23694;
}

function parseRow(row) {
    const id = parseInt(row.slice(0, 2));

    const year = row.slice(3, 7);
    const day_of_year = row.slice(8, 11);
    const time = row.slice(12, 16);
    const datetime = formatDatetime(parseDatetime(year, day_of_year, time));

    const temp = parseValue(row.slice(17, 23));
    const avg_wind_speed = parseWindSpeed(row.slice(24, 31));
    const max_wind_speed = parseWindSpeed(row.slice(32, 39));

    const wind_direction = parseWindDirection(row.slice(40));
    
    return {
        datetime,
        temp,
        avg_wind_speed,
        max_wind_speed,
        wind_direction
    };
}

function formatUrl(date) {
    const stub = d3.timeFormat('%Y%m%d')(date);
    return `https://www.glerl.noaa.gov/metdata/mil/2017/${stub}.01t.txt`;
}

commander
    .version('1.0.0')
    .description('Download wind data for Milwaukee from GLERL')
    .option('-d, --date <date>', 'Date from 2017: YYYY-MM-DD')
    .parse(process.argv);

if (commander.date === undefined) {
    throw new Error('No date provided');
}

const date = d3.timeParse('%Y-%m-%d')(commander.date);

const url = formatUrl(date);

request(url, (error, response, raw) => {
    if (error) throw error;

    const rows = raw
        .split(os.EOL) // split lines into array
        .slice(2)      // drop top two header rows
        .slice(0, -1)  // drop empty last row
        .map(parseRow);
    
    const csv = d3.csvFormat(rows);

    process.stdout.write(csv + os.EOL);
});

package.json

{
  "name": "wind",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "commander": "^2.12.2",
    "d3": "^4.12.0",
    "request": "^2.83.0"
  }
}