This program makes a line chart from data in the Data Canvas - Sense Your City API.
It updates to the latest data automatically every 5 minutes.
Based on Data Canvas Part 1 - Data
This program makes a line chart from data in the Data Canvas - Sense Your City API.
It updates to the latest data automatically every 5 minutes.
Based on Data Canvas Part 1 - Data
<!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; | |
} |