Visualization of hourly data of the storm in The Netherlands from Januari 18, 2018. Data has been preprocessed: the original data only contains hourly wind data of the weather stations in NL; the data has been interpolated over a grid.
Data source: KNMI https://projects.knmi.nl/klimatologie/uurgegevens/selectie.cgi
forked from janwillemtulp's block: Storm in NL, January 18, 2018
xxxxxxxxxx
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<style>
body {
margin:0;
position:fixed;
top:0;
right:0;
bottom:0;
left:0;
background-color: black;
}
.time {
font-family: Arial;
font-size: 24px;
font-weight: bold;
fill: white;
}
.land {
opacity: 0.4;
}
</style>
</head>
<body>
<div>
<input id="hour" type="range" min="1" max="24" step="1" value="1" />
</div>
<span id="value"></span>
<script>
// data from: https://projects.knmi.nl/klimatologie/uurgegevens/selectie.cgi
// margins of the svg area
var margin = { top: 50, right: 50, bottom: 50, left: -30 };
// applying margin convention https://bl.ocks.org/mbostock/3019563
var WIDTH = 1000 - margin.left - margin.right;
var HEIGHT = 936 - margin.top - margin.bottom;
var STEP_SIZE = 0.1; // stepsize of lat/lon used to create the grid
// create an SVG element, using the margins convention
var svg = d3.select("body").append("svg")
.attr("width", WIDTH + margin.left + margin.right)
.attr("height", HEIGHT + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// group for wind
var wind = svg.append("g")
.attr("class", "wind");
// display of time
var time = svg.append("text")
.attr("class", "time")
.attr("dx", 50)
// scale for longitude
var x = d3.scaleLinear()
.range([0, WIDTH]);
// scale for latitude
var y = d3.scaleLinear()
.range([HEIGHT, 0]);
// scale for color
var c = d3.scaleLinear()
.domain([0, 100, 200, 250])
.range(["black", "lightblue", "gold", "red"]);
// scale for offset of lines
var o = d3.scaleLinear()
.range([1, 50])
// calculation of cell with and height
var cellWidth, cellHeight;
var data = []; // the original data
var hourData = []; // the data filtered by hour
var landCells = []; // cells that are NL land
var hour = 1; // current hour
var arrowHeight = 5; // height of the arrow
var arrowScaleFactor = 0.007; // amount of scaling applied to the arrow
var updateDelay = 500; // milliseconds before next hour will be rendered
// updates the data
function update() {
// filters the data based on the current hour
hourData = data.filter(function(d) { return d.hour === hour; })
}
// function to draw an arrow (path)
function drawArrow(d) {
var halfArrowHeight = ((arrowScaleFactor * d.wind_speed * arrowHeight) / 2);
var arrowLength = (4 * cellWidth);
return "M0," // M = move 'pen' to this position
+ halfArrowHeight
+ " L" // L = draw line to this position
+ arrowLength
+ ",0 L0,-"
+ halfArrowHeight;
}
// creates a random offset for each arrow
function getOffset(d) {
return (o(d.wind_speed) / 2) - (Math.random() * o(d.wind_speed));
}
// renders the data
function render() {
var land = wind.selectAll(".land")
.data(hourData.filter(function(d) { return d.land }))
land.enter().append("circle")
.attr("class", "land")
.attr("cx", function(d) { return x(d.lon) + (cellWidth / 2) })
.attr("cy", function(d) { return y(d.lat) + (cellHeight / 2) })
.attr("r", cellHeight / 2)
.merge(land)
.style("fill", function(d) { return c(d.wind_speed)})
// binds all the .cell's to the data filtered by hour
var cell = wind.selectAll(".cell")
.data(hourData)
// add a new cell group element
var cellEnter = cell.enter().append("g")
.attr("class", "cell")
.attr("transform", function(d, i) {
return "translate(" + x(d.lon) + ", " + y(d.lat) + ")";
})
// center of a cell
var cellCenter = cellEnter.append("g")
.attr("transform", "translate(" + (cellWidth / 2) + ", " + (cellHeight / 2) + ")")
// enter AND update of a cell
var cellMerge = cellEnter.merge(cell).transition()
.duration(updateDelay)
.ease(d3.easeLinear)
// append triangle path to cell center
cellCenter.append("path")
// enter AND update of triangle path
cellMerge.select("path")
.attr("transform", function(d) {
return "translate("
+ getOffset(d)
+ ", "
+ getOffset(d)
+ ") "
+ "rotate("
+ (d.wind_direction - 90 - 180)
+ ")";
})
.attr("d", drawArrow)
.style("fill", function(d) { return c(d.wind_speed); })
// set the time
time.text((hour < 10 ? "0" : "") + hour + ":00")
}
// load the data
d3.csv('storm_data.csv',
function(row) { // make sure numeric data is numeric, not strings
return {
lon: +row.lon,
lat: +row.lat,
hour: +row.hour,
wind_speed: +row.wind_speed,
wind_direction: +row.wind_direction,
land: row.land == "True",
}
},
function(result) {
data = result; // set the data variable
// landCells = data.filter(function(d) {
// return d.hour == 1 && d.land;
// })
// set the domain ranges
x.domain(d3.extent(data, function(d) { return d.lon; }));
y.domain(d3.extent(data, function(d) { return d.lat; }));
o.domain(d3.extent(data, function(d) { return d.wind_speed; }));
// calculate cell width and height
cellWidth = WIDTH / (x.domain()[1] - x.domain()[0]) * STEP_SIZE;
cellHeight = HEIGHT / (y.domain()[1] - y.domain()[0]) * STEP_SIZE;
update();
render();
})
// automatically update and render every 200 milliseconds (updateDelay)
// d3.interval(function() {
// if (hour == 24) { // if hour == 24 set it to 0 again
// hour = 0
// }
// hour++; // increase current hour by 1
// update()
// render()
// }, updateDelay);
// when slider changes
d3.select("#hour").on("input", function() {
hour = parseInt(this.value); // this.value will return a String, so it needs to be parsed
update();
render();
})
</script>
</body>
https://d3js.org/d3.v4.min.js