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 {
}
.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;
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()));
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'; });
}
});
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();
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>