|
<!DOCTYPE html> |
|
<html> |
|
<head> |
|
<meta charset="utf-8"> |
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.12/d3.min.js"></script> |
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.6/moment.min.js"></script> |
|
<style> |
|
body { |
|
font-family: futura; |
|
width: 960px; |
|
} |
|
|
|
h2.title { |
|
color: black; |
|
text-align: center; |
|
} |
|
|
|
.axis { |
|
font-family: arial; |
|
font-size: 0.8em; |
|
} |
|
|
|
.axis text { |
|
fill: black; |
|
stroke: none; |
|
} |
|
|
|
path { |
|
fill: none; |
|
stroke: black; |
|
stroke-width: 2px; |
|
} |
|
|
|
.tick { |
|
fill: none; |
|
stroke: black; |
|
} |
|
|
|
.line { |
|
fill: none; |
|
stroke: url(#temperature-gradient); |
|
stroke-width: 1px; |
|
} |
|
|
|
div.day_buttons { |
|
text-align: center; |
|
} |
|
|
|
div.day_buttons div { |
|
text-align: center; |
|
background-color: #438CCA; |
|
color: white; |
|
padding: 6px; |
|
margin: 5px; |
|
font-size: 0.8em; |
|
cursor: pointer; |
|
display: inline-block; |
|
} |
|
</style> |
|
|
|
<script type="text/javascript"> |
|
// D3 utility to convert from timestamp string to Date object |
|
var format = d3.time.format("%Y-%m-%dT%H:%M:%S.%LZ"); |
|
|
|
function draw(data) { |
|
"use strict"; |
|
|
|
// set margins according to Mike Bostock's margin conventions |
|
// http://bl.ocks.org/mbostock/3019563 |
|
var margin = {top: 25, right: 40, bottom: 25, left: 75}; |
|
|
|
// set height and width of chart |
|
var width = 960 - margin.left - margin.right, |
|
height = 350 - margin.top - margin.bottom; |
|
|
|
var field = 'temperature'; |
|
|
|
// Append the title for the graph |
|
d3.select("body") |
|
.append("h2") |
|
.text("Noe Valley Temperature") |
|
.attr('class', 'title'); |
|
|
|
// append the SVG tag with height and width to accommodate for margins |
|
var svg = d3.select("body") |
|
.append("svg") |
|
.attr("width", width + margin.left + margin.right) |
|
.attr("height", height + margin.top + margin.bottom) |
|
.append('g') |
|
.attr('class','chart') |
|
.attr("transform", "translate(" + margin.left + "," + margin.top + ")"); |
|
|
|
// object to keep track on animation state |
|
var animation = { |
|
"timeout": false, |
|
"animating": false |
|
}; |
|
|
|
// group data by day |
|
var nested = d3.nest() |
|
.key(function(d) { |
|
var ts = d['timestamp']; |
|
return (ts.getMonth() + 1).toString() + '/' + ts.getDate(); |
|
}) |
|
.entries(data); |
|
|
|
// remove the first 10 days of data (month of January) |
|
var day_data = nested.slice(10); |
|
|
|
// extract the days we are visualizing |
|
// (to be used as labels in buttons) |
|
var days = day_data.map(function(d, i) { |
|
return { |
|
key: d.key, |
|
selected: false, |
|
idx: i |
|
}; |
|
}); |
|
|
|
// dynamically create buttons to toggle days |
|
var buttons = d3.select("body") |
|
.append("div") |
|
.attr("class", "day_buttons") |
|
.selectAll("div") |
|
.data(days) |
|
.enter() |
|
.append("div") |
|
.text(function(d) { |
|
return d.key; |
|
}) |
|
.attr('id', function(d) { |
|
// convert mm/dd to mm_dd for more readable #id |
|
return 'd' + d.key.split('/').join('_'); |
|
}); |
|
|
|
// toggle button of first day |
|
d3.select('.day_buttons') |
|
.select('div:first-child') |
|
.transition() |
|
.duration(500) |
|
.style("background", "#F7977A") |
|
.style('color', 'black'); |
|
|
|
// Find min/max of temperature column |
|
var temp_extent = d3.extent(data, function(d) { |
|
return d[field]; |
|
}); |
|
|
|
console.log(nested); |
|
// get min/max times of first day of data |
|
// (we will need a new extent for each day) |
|
var time_extent = d3.extent(day_data[0].values, function(d){ |
|
return d['timestamp']; |
|
}); |
|
|
|
// Create x-axis scale mapping dates -> pixels |
|
var time_scale = d3.time.scale() |
|
.range([0, width]) |
|
.domain(time_extent); |
|
|
|
// Create y-axis scale mapping temperature -> pixels |
|
var temp_scale = d3.scale.linear() |
|
.range([height, 0]) |
|
.domain(temp_extent); |
|
|
|
// Create D3 axis object from time_scale for the x-axis |
|
var time_axis = d3.svg.axis() |
|
.scale(time_scale) |
|
.ticks(d3.time.hours, 2) |
|
.tickFormat(d3.time.format("%I:00 %p")); |
|
|
|
// Create D3 axis object from temp_scale for the y-axis |
|
var temp_axis = d3.svg.axis() |
|
.scale(temp_scale) |
|
.orient("left"); |
|
|
|
// determine thresholds for gradient stops |
|
var range = temp_extent[1] - temp_extent[0]; |
|
var interval = range / 3; |
|
var low_threshold = temp_extent[0] + interval; |
|
var high_threshold = temp_extent[0] + 2 * interval; |
|
|
|
// Append a Gradient to double encode temperature |
|
svg.append("linearGradient") |
|
.attr("id", "temperature-gradient") |
|
.attr("gradientUnits", "userSpaceOnUse") |
|
.attr("x1", 0).attr("y1", temp_scale(low_threshold)) |
|
.attr("x2", 0).attr("y2", temp_scale(high_threshold)) |
|
.selectAll("stop") |
|
.data([ |
|
{offset: "0%", color: "steelblue"}, |
|
{offset: "50%", color: "gray"}, |
|
{offset: "100%", color: "red"} |
|
]) |
|
.enter().append("stop") |
|
.attr("offset", function(d) { return d.offset; }) |
|
.attr("stop-color", function(d) { return d.color; }); |
|
|
|
// Append SVG to page corresponding to the x-axis |
|
svg.append('g') |
|
.attr('class', 'x axis') |
|
.attr('transform', "translate(0," + height + ")") |
|
.call(time_axis); |
|
|
|
// Append SVG to page corresponding to the y-axis |
|
svg.append('g') |
|
.attr('class', 'y axis') |
|
.call(temp_axis); |
|
|
|
// add label to y-axis |
|
d3.select(".y.axis") |
|
.append("text") |
|
.text("Degrees (Celsius)") |
|
.attr("transform", "rotate(-90, -43, 0) translate(-260)") |
|
.style('font-size', '1.2em'); |
|
|
|
// define the values to map for x and y position of the line |
|
var line = d3.svg.line() |
|
.x(function(d) { return time_scale(d['timestamp']); }) |
|
.y(function(d) { return temp_scale(d[field]); }); |
|
|
|
// append a SVG path that corresponds to the line chart |
|
var path = svg.append("path") |
|
.datum(day_data[0].values) |
|
.attr("class", "line") |
|
.attr("d", line); |
|
|
|
// create a timeout interval to animate over the days |
|
var interval_timeout = function(day_idx) { |
|
|
|
// set timeout to run callback function every 2000 milliseconds |
|
var day_interval = setInterval(function(){ |
|
// run the update function to update the bound data appropriately |
|
update(day_data[day_idx].key); |
|
|
|
// increment the day visualized by one |
|
day_idx++; |
|
|
|
if(day_idx >= day_data.length) { |
|
// clear callback if we have reached the end of days |
|
clearInterval(day_interval); |
|
} |
|
}, 2000); |
|
|
|
// change animation state on the global object to true |
|
animation.animating = true; |
|
|
|
// return a function we can use to stop the animation |
|
return function() { |
|
clearInterval(day_interval); |
|
}; |
|
}; |
|
|
|
// define a function to unselect all the buttons |
|
var button_unhighlight = function() { |
|
d3.select(".day_buttons") |
|
.selectAll("div") |
|
.datum(function(d) { |
|
// change the internal state of each button to unselected |
|
d.selected = false; |
|
return d; |
|
}) |
|
.transition() |
|
.duration(500) |
|
.style("color", "white") |
|
.style("background", '#438CCA'); |
|
}; |
|
|
|
// highlight button with a specified id |
|
var button_highlight = function(id) { |
|
d3.select('#' + 'd' + id.split('/').join('_')) |
|
.datum(function(d) { |
|
// change internal state to selected |
|
d.selected = true; |
|
return d; |
|
}) |
|
.transition() |
|
.duration(500) |
|
.style("background", "#F7977A") |
|
.style('color', 'black'); |
|
}; |
|
|
|
// add hover to buttons |
|
buttons.on('mouseover', function(d) { |
|
if (!d.selected) { |
|
d3.select(this) |
|
.transition() |
|
.duration(500) |
|
.style("background", "#F7977A") |
|
.style('color', 'black'); |
|
} |
|
}); |
|
|
|
// add hover to buttons |
|
buttons.on('mouseout', function(d) { |
|
if (!d.selected) { |
|
d3.select(this) |
|
.transition() |
|
.duration(500) |
|
.style("color", "white") |
|
.style("background", '#438CCA'); |
|
} else { |
|
d3.select(this) |
|
.transition() |
|
.duration(500) |
|
.style("background", "#F7977A") |
|
.style('color', 'black'); |
|
} |
|
}) |
|
|
|
|
|
// function to be run when on a clicked button |
|
buttons.on("click", function(d) { |
|
if(d.selected === true) { |
|
// if button is selected an the animation is stopped |
|
if (animation.animating === false) { |
|
// change internal state of this button |
|
d.selected = false; |
|
|
|
button_unhighlight(); |
|
|
|
// start the animation on the next button |
|
animation.timeout = interval_timeout(d.idx + 1); |
|
} else { |
|
// if the button is selected but animation |
|
// is running, stop the animation |
|
animation.timeout(); |
|
animation.animating = false; |
|
} |
|
} else { |
|
// if button is not selected, selected it |
|
d.selected = true; |
|
|
|
// stop the animation |
|
animation.animating = false; |
|
animation.timeout(); |
|
} |
|
|
|
// update the bound data for this button |
|
d3.select(this).datum(d); |
|
|
|
// run our update function for the line chart |
|
update(d.key); |
|
}); |
|
|
|
// function which updates the data bound to our chart |
|
function update(key) { |
|
// filter our data for the specified day in 'key' |
|
var data_subset = day_data.filter(function(d) { |
|
return d.key === key; |
|
})[0].values; |
|
|
|
// highlight buttons in sync with animation |
|
button_unhighlight(); |
|
button_highlight(key); |
|
|
|
var time_extent = d3.extent(data_subset, function(d) { |
|
return d['timestamp']; |
|
}); |
|
|
|
// Create x-axis scale mapping dates -> pixels |
|
var time_scale = d3.time.scale() |
|
.range([0, width]) |
|
.domain(time_extent); |
|
|
|
// define the values to map for x and y position of the line |
|
var line = d3.svg.line() |
|
.x(function(d) { return time_scale(d['timestamp']); }) |
|
.y(function(d) { return temp_scale(d[field]); }); |
|
|
|
// update data bound to path |
|
path.datum(data_subset); |
|
|
|
// transition the line to animate with the changed data |
|
path.transition().duration(1000).attr("d", line); |
|
}; |
|
|
|
// start initial animation for first day |
|
animation.timeout = interval_timeout(1); |
|
}; |
|
</script> |
|
</head> |
|
<body> |
|
<script type="text/javascript"> |
|
d3.csv("http://jay-oh-en.github.io/interactive-data-viz/data/datacanvas/noevalley.csv", function(d) { |
|
// convert from string to Date object |
|
var date = format.parse(d['timestamp']); |
|
|
|
// use moment.js to shift time back 8 hours (to PST) |
|
var mom = moment(date).subtract(8, 'hours'); |
|
d['timestamp'] = mom.toDate(); |
|
|
|
// coerce temperature from string to float |
|
d['temperature'] = +d['temperature']; |
|
return d; |
|
}, draw); |
|
</script> |
|
</body> |
|
</html> |