Skip to content

Instantly share code, notes, and snippets.

@sbhargava24
Last active April 16, 2016 04:49
Show Gist options
  • Save sbhargava24/2a567b81be88c6c136da2a42aeabcd48 to your computer and use it in GitHub Desktop.
Save sbhargava24/2a567b81be88c6c136da2a42aeabcd48 to your computer and use it in GitHub Desktop.
Simple Line Chart with Color Gradient
<!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
};
// Q1: group data by day
var data_sub = data.slice(1000, 10000);
var nested_data = d3.nest()
.key(function(dt) {
var tmp = dt['timestamp'];
return (tmp.getMonth()).toString() + '/' + tmp.getDate();
})
.entries(data);
// Q2: remove the first 10 days of data (month of January)
var day_new = nested_data.slice(10);
// Q3: extract the days we are visualizing
// (to be used as labels in buttons)
// var getKeys = function(arr) {
// var key, keys = [];
// for (i=0; i<arr.length; i++)
// for (key in arr[i]) {
// keys.push(key);
// }
// }
// return keys;
// };
// var days = getKeys(day_new);
var days = day_new.map(function(dt, x) {
return {
key: dt.key,
selected: false,
idx: x
};
});
// Q4: 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;
});
// Q5: toggle button of first day
d3.select('.day_buttons')
.select('div:first-child')
.transition()
.duration(500)
.style("background", "#FF0000")
.style('color', 'black');
// Find min/max of temperature column
var temp_extent = d3.extent(data_sub, function(d) {
return d[field];
});
// get min/max times of first day of data
// (we will need a new extent for each day)
var time_extent = d3.extent(data_sub, 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(data_sub)
.attr("class", "line")
.attr("d", line);
// Q6: 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(nested_data[day_idx].key);
// increment the day visualized by one
day_idx++;
if(day_idx >= nested_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);
};
};
// Q7: 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');
};
// Q8: 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", "#FF0000")
.style('color', 'black');
};
// Q9: add hover to buttons
buttons.on('mouseover', function(d) {
if (!d.selected) {
d3.select(this)
.transition()
.duration(500)
.style("background", "#FF0000")
.style('color', 'black');
}
});
// Q10: 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", "#FF0000")
.style('color', 'black');
}
})
// Q11: 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);
});
// Q12: function which updates the data bound to our chart
function update(key) {
// filter our data for the specified day in 'key'
var data_subset = nested_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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment