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/3b811f05a0ce39d0d7cd to your computer and use it in GitHub Desktop.
Save curran/3b811f05a0ce39d0d7cd to your computer and use it in GitHub Desktop.
Data Canvas Part 4 - Colors
// A reusable bar chart module.
// Draws from D3 bar chart example http://bl.ocks.org/mbostock/3885304
// Curran Kelleher March 2015
define(["d3", "model", "lodash"], function (d3, Model, _) {
// A representation for an optional Model property that is not specified.
// This allows the "when" approach to support optional properties.
// Inspired by Scala"s Option type.
// See http://alvinalexander.com/scala/using-scala-option-some-none-idiom-function-java-null
var None = "__none__";
// The constructor function, accepting default values.
return function BarChart(defaults) {
// Create a Model instance for the bar chart.
// This will serve as the public API for the visualization.
var model = Model();
// 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", "xColumn"], function (data, xColumn) {
model.getX = function (d) { return d[xColumn]; };
});
// Handle sorting.
model.when(["sortColumn", "sortOrder", "data"], function (sortColumn, sortOrder, data){
var sortedData = _.sortBy(data, sortColumn);
if(sortOrder === "descending"){
sortedData.reverse();
}
model.sortedData = sortedData;
});
// Compute the domain of the X attribute.
model.when(["sortedData", "getX"], function (sortedData, getX) {
model.xDomain = sortedData.map(getX);
});
// Compute the X scale.
model.when(["xDomain", "width", "barPadding"], function (xDomain, width, padding) {
model.xScale = d3.scale.ordinal().domain(xDomain).rangeRoundBands([0, width], padding);
});
// 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", "yColumn"], function (data, yColumn) {
model.getY = function (d) { return d[yColumn]; };
});
// Compute the domain of the Y attribute.
// Allow the API client to optionally specify fixed min and max values.
model.yDomainMin = None;
model.yDomainMax = None;
model.when(["data", "getY", "yDomainMin", "yDomainMax"],
function (data, getY, yDomainMin, yDomainMax) {
if(yDomainMin === None && yDomainMax === None){
model.yDomain = d3.extent(data, getY);
} else {
if(yDomainMin === None){
yDomainMin = d3.min(data, getY);
}
if(yDomainMax === None){
yDomainMax = d3.max(data, getY);
}
model.yDomain = [yDomainMin, yDomainMax]
}
});
// 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));
});
// Compute the Color scale.
model.when(["colorDomain", "colorRange"], function(colorDomain, colorRange){
model.colorScale = d3.scale.ordinal().domain(colorDomain).range(colorRange);
});
model.when(["colorScale", "colorColumn"], function(colorScale, colorColumn){
model.getColorScaled = function(d) { return colorScale(d[colorColumn]); };
});
// Add an SVG group to contain the line.
model.when("g", function (g) {
model.barsG = g.append("g");
});
// Draw the bars.
model.when(["barsG", "sortedData", "getXScaled", "getYScaled", "xScale", "height", "getColorScaled"],
function (barsG, sortedData, getXScaled, getYScaled, xScale, height, getColorScaled){
var bars = barsG.selectAll("rect").data(sortedData);
bars.enter().append("rect");
bars.attr("x", getXScaled).attr("y", getYScaled)
.attr("width", xScale.rangeBand())
.attr("height", function(d) { return height - getYScaled(d); })
.attr("fill", getColorScaled);
bars.exit().remove();
});
// Set defaults at the end so they override optional properties set to None.
model.set(defaults);
return model;
};
});
// Generates n distinct colors.
// l is the fixed lightness, the L in LAB color space.
// r is the radius of the circle in AB space along which points are taken.
// Documented at http://bl.ocks.org/curran/dd73d3d8925cdf50df86
define([], function (){
var lDefault = 50,
rDefault = 100;
return function generateColors(n, l, r){
var colors = [], a, b, θ;
l = l || lDefault;
r = r || rDefault;
for(var i = 0; i < n; i++){
θ = (i / n) * Math.PI * 2;
a = Math.sin(θ) * r;
b = Math.cos(θ) * r;
colors.push(d3.lab(l, a, b).toString());
}
return colors;
}
});
// This module provides an API layer above the
// Data Canvas - Sense Your City API described at
// http://map.datacanvas.org/#!/data
define(["jquery", "lodash", "async"], function ($, _, async){
// See API documentation at http://map.datacanvas.org/#!/data
var API_URL = "http://sensor-api.localdata.com/api/v1/aggregations.csv",
// List of all cities with available data.
cities = ["San Francisco", "Bangalore", "Boston", "Geneva", "Rio de Janeiro", "Shanghai", "Singapore"],
// The default parameters to pass into the API.
defaultParams = {
// 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",
}
// Fetches the latest data for a given city.
function getLatestDataForCity(city, callback){
// Get data for the last 24 hours.
// 1000 milliseconds/second, 60 seconds/minute, 5 minutes
var params = _.extend({
from: new Date(Date.now() - 1000 * 60 * 60 * 24).toISOString(),
before: new Date().toISOString(),
"over.city": city
}, defaultParams);
// Use jQuery to fetch the data.
// jQuery is used here rather than D3 because of its nice parameter syntax.
$.get(API_URL, params, function(csv) {
// Parse the CSV string.
callback(null, 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;
}));
});
};
// Fetches the current temperature across all cities.
return function getLatestData(callback){
async.map(cities, getLatestDataForCity, function(err, results){
callback(err, _.flatten(results));
});
}
});
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<!-- Use RequireJS for module loading. -->
<script src="//cdnjs.cloudflare.com/ajax/libs/require.js/2.1.14/require.js"></script>
<!-- Configure RequireJS paths for third party libraries. -->
<script>
requirejs.config({
paths: {
// Use ModelJS - github.com/curran/model
model: "//curran.github.io/cdn/model-v0.2.0/dist/model",
d3: "//d3js.org/d3.v3.min",
jquery: "//code.jquery.com/jquery-2.1.1.min",
lodash: "//cdnjs.cloudflare.com/ajax/libs/lodash.js/3.4.0/lodash.min",
async: "//cdnjs.cloudflare.com/ajax/libs/async/0.9.0/async",
crossfilter: "//cdnjs.cloudflare.com/ajax/libs/crossfilter/1.3.11/crossfilter.min"
}
});
</script>
<!-- Include CSS that styles the visualization. -->
<link rel="stylesheet" href="styles.css">
<title>Colors</title>
</head>
<body>
<!-- The visualization will be injected into this div. -->
<div id="container"></div>
<!-- Run the main program. -->
<script src="main.js"></script>
</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", "xColumn"], function (data, xColumn) {
model.getX = function (d) { return d[xColumn]; };
});
// 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", "yColumn"], function (data, yColumn) {
model.getY = function (d) { return d[yColumn]; };
});
// 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));
});
// Add an SVG group to contain the line.
model.when("g", function (g) {
model.lineG = g.append("g");
});
// Compute the Color scale.
model.when(["colorDomain", "colorRange"], function(colorDomain, colorRange){
model.colorScale = d3.scale.ordinal().domain(colorDomain).range(colorRange);
});
// Draw the lines.
model.when(["lineG", "data", "lineColumn", "getXScaled", "getYScaled", "colorScale"],
function (lineG, data, lineColumn, getXScaled, getYScaled, colorScale){
var linesData = d3.nest()
.key(function(d){ return d[lineColumn]; })
.entries(data),
line = d3.svg.line().x(getXScaled).y(getYScaled),
lines = lineG.selectAll(".line").data(linesData);
lines.enter().append("path").attr("class", "line");
lines
.attr("d", function(d){ return line(d.values); })
.style("stroke", function(d){ return colorScale(d.key); });
lines.exit().remove();
});
return model;
};
});
// This is the main program that sets up a bar chart to visualize data from the Data Canvas - Sense Your City API.
// Curran Kelleher March 2015
require(["getLatestData", "barChart", "lineChart", "generateColors", "crossfilter"], function (getLatestData, BarChart, LineChart, generateColors) {
// Initialize the bar chart.
var barChart = BarChart({
// Bar identity.
xColumn: "city",
xAxisLabel: "City",
// Bar height.
yColumn: "temperature",
yAxisLabel: "Temperature (°C)",
// Bar ordering.
sortColumn: "temperature",
sortOrder: "descending",
// Bar color.
colorColumn: "city",
// Use a fixed value of 0 for the temperature axis.
yDomainMin: 0,
// Spacing between bars.
barPadding: 0.1,
// Tell the chart which DOM element to insert itself into.
container: d3.select("#container").node(),
// Specify the margin and text label offsets.
margin: {
top: 10,
right: 10,
bottom: 60,
left: 70
}
});
// Initialize the line chart.
var lineChart = LineChart({
lineColumn: "city",
xColumn: "date",
xAxisLabel: "Time",
yColumn: "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: 0,
right: 20,
bottom: 55,
left: 70
}
});
var commonAxisLabelOffsets = {
yAxisLabelOffset: 1.7, // Unit is CSS "em"s
xAxisLabelOffset: 1.9
};
lineChart.set(commonAxisLabelOffsets);
barChart.set(commonAxisLabelOffsets);
// Pass the latest data into the charts.
function update(){
getLatestData(function(err, data){
var observation = crossfilter(data),
city = observation.dimension( function (d){ return d.city; }),
date = observation.dimension( function (d){ return d.date; });
// Pass the full data (all cities by all time) into the line chart.
lineChart.data = data;
// Pass only the data for the most recent timestamp into the bar chart.
date.filter(date.top(1)[0].date);
var cityRecords = city.top(Infinity);
var cities = cityRecords.map(function (d) { return d.city; });
barChart.data = cityRecords;
// Choose colors to use for cities in both charts.
var colors = generateColors(cityRecords.length);
barChart.colorDomain = lineChart.colorDomain = cities;
barChart.colorRange = lineChart.colorRange = colors;
});
}
// Initialize the data.
update();
// Update the data every 5 minutes.
setInterval(update, 1000 * 60 * 5);
// Sets the `box` model property
// based on the size of the container,
function computeBoxes(){
barChart.box = {
width: container.clientWidth,
height: container.clientHeight / 2
};
lineChart.box = {
width: container.clientWidth,
height: container.clientHeight / 2
};
}
// once to initialize `model.box`, and
computeBoxes();
// whenever the browser window resizes in the future.
window.addEventListener("resize", computeBoxes);
});
/* 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: 18pt 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