block by curran 3b811f05a0ce39d0d7cd

Data Canvas Part 4 - Colors

Full Screen

This program makes a bar chart and line chart from data in the Data Canvas - Sense Your City API.

The line chart shows the temperature for all cities with available data for the past 24 hours, while the bar chart shows the current temperature of each city. The data is up to date, and updates every 5 minutes. Colors code the lines to the city.

Based on

index.html

<!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>

barChart.js

// 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;
  };
});

generateColors.js

// 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;
  }
});

getLatestData.js

// 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));
    });
  }
});

lineChart.js

// 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;
  };
});

main.js

// 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);
});

styles.css

/* 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;
}