Skip to content

Instantly share code, notes, and snippets.

@curran
Last active August 29, 2015 14:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save curran/015402cce2caa074551e to your computer and use it in GitHub Desktop.
Save curran/015402cce2caa074551e to your computer and use it in GitHub Desktop.
Data Canvas Part 2 - Line Chart
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<!-- Runs the main program found in main.js. -->
<script data-main="main.js" src="//cdnjs.cloudflare.com/ajax/libs/require.js/2.1.14/require.js"></script>
<!-- Configure paths for RequireJS modules. -->
<script>
requirejs.config({
paths: {
d3: "http://d3js.org/d3.v3.min",
jQuery: "//code.jquery.com/jquery-2.1.1.min",
}
});
</script>
<!-- Include CSS that styles the visualization. -->
<link rel="stylesheet" href="styles.css">
<title>Line Chart</title>
</head>
<body>
<!-- The visualization will be injected into this div. -->
<div id="container"></div>
</body>
</html>
// A reusable line chart module.
// Draws from D3 line chart example http://bl.ocks.org/mbostock/3883245
// Curran Kelleher March 2015
define(["d3", "model"], function (d3, Model) {
// The constructor function, accepting default values.
return function LineChart(defaults) {
// Create a Model instance for the line chart.
// This will serve as the line chart's public API.
var model = Model(defaults);
// Create the SVG element from the container DOM element.
model.when("container", function (container) {
model.svg = d3.select(container).append('svg');
});
// Adjust the size of the SVG based on the `box` property.
model.when(["svg", "box"], function (svg, box) {
svg.attr("width", box.width).attr("height", box.height);
});
// Create the SVG group that will contain the visualization.
model.when("svg", function (svg) {
model.g = svg.append("g");
});
// Adjust the translation of the group based on the margin.
model.when(["g", "margin"], function (g, margin) {
g.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
});
// Create the title text element.
model.when("g", function (g){
model.titleText = g.append("text").attr("class", "title-text");
});
// Center the title text when width changes.
model.when(["titleText", "width"], function (titleText, width) {
titleText.attr("x", width / 2);
});
// Update the title text based on the `title` property.
model.when(["titleText", "title"], function (titleText, title){
titleText.text(title);
});
// Update the title text offset.
model.when(["titleText", "titleOffset"], function (titleText, titleOffset){
titleText.attr("dy", titleOffset + "em");
});
// Compute the inner box from the outer box and margin.
// See Margin Convention http://bl.ocks.org/mbostock/3019563
model.when(["box", "margin"], function (box, margin) {
model.width = box.width - margin.left - margin.right;
model.height = box.height - margin.top - margin.bottom;
});
// Generate a function for getting the X value.
model.when(["data", "xAttribute"], function (data, xAttribute) {
model.getX = function (d) { return d[xAttribute]; };
});
// Compute the domain of the X attribute.
model.when(["data", "getX"], function (data, getX) {
model.xDomain = d3.extent(data, getX);
});
// Compute the X scale.
model.when(["data", "xDomain", "width"], function (data, xDomain, width) {
model.xScale = d3.time.scale().domain(xDomain).range([0, width]);
});
// Generate a function for getting the scaled X value.
model.when(["data", "xScale", "getX"], function (data, xScale, getX) {
model.getXScaled = function (d) { return xScale(getX(d)); };
});
// Set up the X axis.
model.when("g", function (g) {
model.xAxisG = g.append("g").attr("class", "x axis");
model.xAxisText = model.xAxisG.append("text").style("text-anchor", "middle");
});
// Move the X axis label based on its specified offset.
model.when(["xAxisText", "xAxisLabelOffset"], function (xAxisText, xAxisLabelOffset){
xAxisText.attr("dy", xAxisLabelOffset + "em");
});
// Update the X axis transform when height changes.
model.when(["xAxisG", "height"], function (xAxisG, height) {
xAxisG.attr("transform", "translate(0," + height + ")");
});
// Center the X axis label when width changes.
model.when(["xAxisText", "width"], function (xAxisText, width) {
xAxisText.attr("x", width / 2);
});
// Update the X axis based on the X scale.
model.when(["xAxisG", "xScale"], function (xAxisG, xScale) {
xAxisG.call(d3.svg.axis().orient("bottom").scale(xScale));
});
// Update X axis label.
model.when(["xAxisText", "xAxisLabel"], function (xAxisText, xAxisLabel) {
xAxisText.text(xAxisLabel);
});
// Generate a function for getting the Y value.
model.when(["data", "yAttribute"], function (data, yAttribute) {
model.getY = function (d) { return d[yAttribute]; };
});
// Compute the domain of the Y attribute.
model.when(["data", "getY"], function (data, getY) {
model.yDomain = d3.extent(data, getY);
});
// Compute the Y scale.
model.when(["data", "yDomain", "height"], function (data, yDomain, height) {
model.yScale = d3.scale.linear().domain(yDomain).range([height, 0]);
});
// Generate a function for getting the scaled Y value.
model.when(["data", "yScale", "getY"], function (data, yScale, getY) {
model.getYScaled = function (d) { return yScale(getY(d)); };
});
// Set up the Y axis.
model.when("g", function (g) {
model.yAxisG = g.append("g").attr("class", "y axis");
model.yAxisText = model.yAxisG.append("text")
.style("text-anchor", "middle")
.attr("transform", "rotate(-90)")
.attr("y", 0);
});
// Move the Y axis label based on its specified offset.
model.when(["yAxisText", "yAxisLabelOffset"], function (yAxisText, yAxisLabelOffset){
yAxisText.attr("dy", "-" + yAxisLabelOffset + "em")
});
// Center the Y axis label when height changes.
model.when(["yAxisText", "height"], function (yAxisText, height) {
yAxisText.attr("x", -height / 2);
});
// Update Y axis label.
model.when(["yAxisText", "yAxisLabel"], function (yAxisText, yAxisLabel) {
yAxisText.text(yAxisLabel);
});
// Update the Y axis based on the Y scale.
model.when(["yAxisG", "yScale"], function (yAxisG, yScale) {
yAxisG.call(d3.svg.axis().orient("left").scale(yScale));
});
// Adjust Y axis tick mark parameters.
// See https://github.com/mbostock/d3/wiki/Quantitative-Scales#linear_tickFormat
model.when(['yAxisNumTicks', 'yAxisTickFormat'], function (count, format) {
yAxis.ticks(count, format);
});
// Add an SVG group to contain the line.
model.when("g", function (g) {
model.lineG = g.append("g");
});
// Generate an SVG path element that will hold the line.
model.when("lineG", function (lineG) {
model.linePath = lineG.append('path').attr('class', 'line');
});
// Draw the line.
model.when(["linePath", "data", "getXScaled", "getYScaled"], function (linePath, data, getXScaled, getYScaled){
linePath.attr('d', d3.svg.line().x(getXScaled).y(getYScaled)(data));
});
return model;
};
});
// This is the main program that sets up a line chart
// to visualize data from the Data Canvas - Sense Your City API.
//
// Curran Kelleher March 2015
require(["jQuery", "lineChart"], function (jQuery, LineChart) {
// Initialize the line chart.
var lineChart = LineChart({
title: "Geneva",
xAttribute: "date",
xAxisLabel: "Time",
yAttribute: "temperature",
yAxisLabel: "Temperature (°C)",
// Tell the chart which DOM element to insert itself into.
container: d3.select("#container").node(),
// Specify the margin and text label offsets.
margin: {
top: 50,
right: 20,
bottom: 50,
left: 70
},
yAxisLabelOffset: 1.4, // Unit is CSS "em"s
xAxisLabelOffset: 1.6,
titleOffset: -0.2
});
// See API documentation at http://map.datacanvas.org/#!/data
var API_URL = "http://sensor-api.localdata.com/api/v1/aggregations.csv";
function getLatestData(){
// Use jQuery to fetch the data.
// jQuery is used here rather than D3 because of its nice parameter syntax.
$.get(API_URL, {
// Use averaging as the aggregation operator.
op: "mean",
// Include temperature only.
fields: "temperature", //,light,airquality_raw,sound,humidity,dust",
// Get data for every 5 minutes.
resolution: "5m",
// Average over all sensors in Geneva.
"over.city": "Geneva",
// Get data for the last 24 hours.
// 1000 milliseconds/second, 60 seconds/minute, 60 minutes/hour, 24 hours/day
from: new Date(Date.now() - 1000 * 60 * 60 * 24).toISOString(),
before: new Date().toISOString()
}, function(csv) {
// Parse the CSV string.
var data = d3.csv.parse(csv, function(d){
// Parse ISO date strings into Date objects.
d.date = new Date(d.timestamp);
// Parse strings into Numbers for numeric fields.
d.temperature = +d.temperature;
d.light = +d.light
d.airquality_raw = +d.airquality_raw
d.sound = +d.sound
d.humidity = +d.humidity
d.dust = +d.dust
return d;
});
// Pass the data into the line chart.
lineChart.data = data;
});
}
// Initialize the data.
getLatestData();
// Update the data every 5 minutes.
setInterval(getLatestData, 1000 * 60 * 5);
// Sets the `box` model property
// based on the size of the container,
function computeBox(){
lineChart.box = {
width: container.clientWidth,
height: container.clientHeight
};
}
// once to initialize `model.box`, and
computeBox();
// whenever the browser window resizes in the future.
window.addEventListener("resize", computeBox);
});
// Implements key-value models with a functional reactive `when` operator.
// See also https://github.com/curran/model
define([], function (){
// The constructor function, accepting default values.
return function Model(defaults){
// The returned public API object.
var model = {},
// The internal stored values for tracked properties. { property -> value }
values = {},
// The listeners for each tracked property. { property -> [callback] }
listeners = {},
// The set of tracked properties. { property -> true }
trackedProperties = {};
// The functional reactive "when" operator.
//
// * `properties` An array of property names (can also be a single property string).
// * `callback` A callback function that is called:
// * with property values as arguments, ordered corresponding to the properties array,
// * only if all specified properties have values,
// * once for initialization,
// * whenever one or more specified properties change,
// * on the next tick of the JavaScript event loop after properties change,
// * only once as a result of one or more synchronous changes to dependency properties.
function when(properties, callback){
// This function will trigger the callback to be invoked.
var triggerCallback = debounce(function (){
var args = properties.map(function(property){
return values[property];
});
if(allAreDefined(args)){
callback.apply(null, args);
}
});
// Handle either an array or a single string.
properties = (properties instanceof Array) ? properties : [properties];
// Trigger the callback once for initialization.
triggerCallback();
// Trigger the callback whenever specified properties change.
properties.forEach(function(property){
on(property, triggerCallback);
});
}
// Returns a debounced version of the given function.
// See http://underscorejs.org/#debounce
function debounce(callback){
var queued = false;
return function () {
if(!queued){
queued = true;
setTimeout(function () {
queued = false;
callback();
}, 0);
}
};
}
// Returns true if all elements of the given array are defined, false otherwise.
function allAreDefined(arr){
return !arr.some(function (d) {
return typeof d === 'undefined' || d === null;
});
}
// Adds a change listener for a given property with Backbone-like behavior.
// See http://backbonejs.org/#Events-on
function on(property, callback){
getListeners(property).push(callback);
track(property);
};
// Gets or creates the array of listener functions for a given property.
function getListeners(property){
return listeners[property] || (listeners[property] = []);
}
// Tracks a property if it is not already tracked.
function track(property){
if(!(property in trackedProperties)){
trackedProperties[property] = true;
values[property] = model[property];
Object.defineProperty(model, property, {
get: function () { return values[property]; },
set: function(value) {
values[property] = value;
getListeners(property).forEach(function(callback){
callback(value);
});
}
});
}
}
// Sets all of the given values on the model.
// Values is an object { property -> value }.
function set(values){
for(property in values){
model[property] = values[property];
}
}
// Transfer defaults passed into the constructor to the model.
set(defaults);
// Expose the public API.
model.when = when;
model.on = on;
model.set = set
return model;
}
});
/* Make the visualization container fill the page. */
#container {
position: fixed;
left: 0px;
right: 0px;
top: 0px;
bottom: 0px;
}
/* Style the visualization. Draws from http://bl.ocks.org/mbostock/3887118 */
/* Tick mark labels */
.axis .tick text {
font: 12pt sans-serif;
}
/* Axis labels */
.axis text {
font: 20pt sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.line {
fill: none;
stroke: black;
stroke-width: 1.5px;
}
.title-text {
text-anchor: middle;
font: 30pt sans-serif;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment