block by timelyportfolio 5266202

5266202

Full Screen

This is a Binned Line Chart version of Mike Bostock‘s zoomable area chart. You can find his source here. The graph shows number of flights per day in the United States.

A detailed publication as part of Matt Woelk‘s Masters Thesis will be available in Fall, 2013.

index.html

<!DOCTYPE html>
<meta charset="utf-8">
<script src="//d3js.org/d3.v3.min.js"></script>
<script src="readme-binnedChart.js"></script>
<script src="readme-underscore.js"></script>
<script src="readme-msToCentury.js"></script>

<style>
body {
  background-color : #FFF;
  font-family      : sans-serif;
  color            : #000;
  font-size        : 20px;
}

#controls ul {
  float: left;
}

#chart {
  font: 10px sans-serif;
  overflow: none;
}

path, line {
  /*shape-rendering: crispEdges;*/
}

.axis path, .axis line {
  fill: none;
  stroke: #000;
  shape-rendering: crispEdges;
}

.axis path {
  /* Don't show the axis lines. */
  display: none;
}

.y.axis line {
  stroke: #999;
  stroke-dasharray: 3, 3;
}

#chart_container {
  position: relative;
  overflow: none;
}

#chartContainer {
  overflow : none;
  width    : 100%;
  overflow : none;
  float    : left;
}

#zoomSVG {
  position : absolute;
  top      : 0;
  left     : 0;
}

#zoomRect {
  border-style : solid;
  border-width : 1px;
  cursor       : col-resize;
}
</style>
<body>
<script>
var thePlot;
var margin = {top: 20, right: 10, bottom: 25, left: 70};
var height = 200;

var parseDate = d3.time.format("%Y-%m-%d").parse;

d3.csv("readme-flights.csv", function (error, data) {
  // FORMATTING DATA //

  if (error) {
    alert("Try refreshing your browser.");
    return;
  }

  var json = data.map(function (d) {
    return {val: +d.value, ms: parseDate(d.date).getTime()};
  });


  // INITIALIZING THE PLOT //

  var xScale = d3.scale.linear().domain([570693600000, 1201845600000]).range([0, document.getElementById("chartContainer").offsetWidth]);
  var yScale = d3.scale.linear();
  var plotHeight = function () { return thePlot.height(); };
  var pl;

  thePlot = binnedLineChart(json, "TODO-SERVER", "1");
  thePlot.xScale(xScale);

  pl = d3.select("#chartContainer").insert("svg").call(thePlot);

  var sps = 864e5; // 864e5 is the number of milliseconds in a day

  thePlot.containerWidth(document.getElementById("chartContainer").offsetWidth).height(height).showTimeContext(true).milliSecondsPerSample(sps).update();

  d3.select("#chartContainer").attr("height", plotHeight).attr("width", document.getElementById("chartContainer").offsetWidth);


  // ZOOMING //

  var zoomSVG = d3.select("#zoomSVG");
  var zoomRect = d3.select("#zoomRect");
  var zoom = d3.behavior.zoom()
      .scaleExtent([Math.pow(2, -2), Math.pow(2,10)])
      .on("zoom", function () {
        thePlot.xScale(xScale).update();
    });

  zoomSVG.attr("width", document.getElementById("chartContainer").offsetWidth)
         .attr("height", plotHeight);

  zoomRect.attr("width", document.getElementById("chartContainer").offsetWidth - margin.left - margin.right)
          .attr("height", plotHeight)
          .attr("transform", "translate(" + margin.left + ", " + margin.top + ")");

  zoomRect.attr("fill", "rgba(0,0,0,0)")
          .call(zoom);

  // apply zooming
  xScale = thePlot.xScale();
  yScale = thePlot.yScale();
  zoom.x(xScale);
  zoom.y(yScale);


  // UPDATING LINES //

  function changeLines () {
    thePlot.setSelectedLines().update();
  }

  document.getElementById("render-lines").addEventListener("change", changeLines, false);
  document.getElementById("render-depth").addEventListener("change", changeLines, false);
  document.getElementById("render-method").addEventListener("change", changeLines, false);
});


</script>
<div id="chartContainer">
  <svg id="zoomSVG"><rect id="zoomRect" /></svg>
</div>

<div id="controls">
  <form>
    <ul id="render-lines">
      Which functions<br />to render:
      <li><label><input type="checkbox" checked value="average"/>Averages</label></li>
      <li><label><input type="checkbox" checked value="maxes"/>Maximums</label></li>
      <li><label><input type="checkbox" checked value="mins"/>Minimums</label></li>
      <li><label><input type="checkbox"         value="q1"/>1st Quartile</label></li>
      <li><label><input type="checkbox"         value="q3"/>3st Quartile</label></li>
      <li><label><input type="checkbox" checked value="quartiles"/>Quartile Area</label></li>
    </ul>

    <ul id="render-depth">
      Maximum Bin<br />Render Size:
      <br />
      <label><input type="range" min="1" max="200" id="renderdepth" value="40" style="width:200px"/>
    </ul>

    <ul id="render-method">
      Interpolation<br />Method:
      <li><label><input type="radio"         name="render-method" value="linear"/>Linear</li>
      <li><label><input type="radio"         name="render-method" value="step-after"/>Step-After</li>
      <li><label><input type="radio" checked name="render-method" value="monotone"/>Monotone</li>
    </ul>
  </form>
</div>

readme-msToCentury.js

///////////////////////////////////////
// Custom Time Axis                  //
// ----------------                  //
// - Allows for smooth scrolling at  //
//   smaller-than-millisecond zoom   //
//   levels                          //
// - Provides time axis labeling     //
//   see customTimeFormat            //
// - Depends on underscore.js, d3.js //
///////////////////////////////////////
// USAGE
// ----------------
// xAxis = d3.svg.axis()
//   .tickFormat(msToCenturyTickFormat)
//   .tickValues(msToCenturyTickValues(xScale, width))
//   .scale(xScale).orient("bottom");
///////////////////////////////////////

var MIN_DISTANCE_BETWEEN_X_AXIS_LABELS = 75;

function millisecond(val) {
  var newdate = new Date();
  newdate.setTime(roundDownToNearestTime(val, times.ms));
  return newdate;
}

function roundUpToNearestTime(val, tim) {
  return Math.ceil(val/tim) * tim;
}

function roundDownToNearestTime(val, tim) {
  return Math.floor(val/tim) * tim;
}

function getNumberOfDaysInCurrentMonth(dat) {
  var curmo = dat.getMonth();
  var addYear;
  if (( curmo + 1 ) / 12.0 >= 1.0) {
    // we rolled over to the next year
    addYear = dat.getFullYear() + 1;
  } else {
    addYear = dat.getFullYear();
  }
  var newdate = new Date(
      addYear,
      (curmo + 1) % 12,
      1,
      1,
      1,
      1,
      1);
  newdate = dt(newdate.getTime() - 4000000);
  return newdate.getDate();
}

var times = {
  ms: 1, //milliseconds
  s: 1000, //seconds
  m: 6e4, //minutes
  h: 36e5, //hours
  d: 864e5, //days
  // These are approximations:
  mo: 2592e6, //months
  y: 31536e6, //years
};

function getNumberOfDaysInCurrentYear(dat) {
  var newdateStart = new Date(dat.getFullYear()    , 0, 0);
  var newdateEnd   = new Date(dat.getFullYear() + 1, 0, 0);
  var diff = newdateEnd.getTime() - newdateStart.getTime();
  var oneDay = 1000 * 60 * 60 * 24;
  return Math.floor(diff / oneDay);
}

// custom formatting for x axis time
function msToCenturyTickFormat(ti) {
  function timeFormat(formats) {
    return function(date) {
      var newdate = new Date();
      newdate.setTime(date);
      var i = formats.length - 1, f = formats[i];
      while (!f[1](newdate)) f = formats[--i];
      return f[0](newdate);
    };
  }

  var customTimeFormat = timeFormat([
     [ d3.time.format("%Y")    , function() { return true; }                 ],
     [ d3.time.format("%b")    , function(d) { return d.getMonth(); }        ],
     [ d3.time.format("%a %d") , function(d) { return d.getDate() != 1; }    ],
     [ d3.time.format("%H %p") , function(d) { return d.getHours(); }        ],
     [ d3.time.format("%H:%M") , function(d) { return d.getMinutes(); }      ],
     [ d3.time.format("%Ss")   , function(d) { return d.getSeconds(); }      ],
     [ d3.time.format("%Lms")  , function(d) { return d.getMilliseconds(); } ]
    ]);

  return function(d) { return customTimeFormat(ti); }();
}

function onScreenSizeOfLabels(millisecondsPerLabel, screenWidth, distanceBtwnLabels) {
  return millisecondsPerLabel * screenWidth / distanceBtwnLabels;
}

function findLevel(dom, wid) {
  for (i = 0; i < rounding_scales.length; i++) {
    var ro = rounding_scales[i];
    var compr = onScreenSizeOfLabels(ro[0]*ro[1], wid, MIN_DISTANCE_BETWEEN_X_AXIS_LABELS);

    if (dom[1] - dom[0] <= compr ) {
      var result = makeTickRange(dom[0], dom[1], ro[1], ro[3], ro[2], ro[0]*ro[1], wid);

      // filter this for only what is actually on-screen.
      result = _.filter(result, function (num) {
        return num < dom[1] && num > dom[0];
      });

      return i;
    }
  }
  return -1;
}

function msToCenturyTickValues(scal, wid) {
  var dom = scal.domain();

  var lvl = findLevel(dom, wid);

  // This should never occur if the zoom limits are correct
  if (lvl === -1) {
    return [1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, 1e13, 1e14];
  }

  var ro = rounding_scales[lvl];
  var rng = makeTickRange(dom[0], dom[1], ro[1], ro[3], ro[2], ro[0]*ro[1], wid);

  // filter this for only what is actually on-screen.
  var result = _.filter(rng, function (num) {
    return num < dom[1] && num > dom[0];
  });

  return result;
}

function msToCenturyTickSubDivide(scal, wid) {
  var dom = scal.domain();

  var lvl = findLevel(dom, wid);
  if (lvl === -1) { return 0; }

  var baseSize = rounding_scales[lvl][1];
  var tickSpace = rounding_ticks[baseSize];

  return ((baseSize / tickSpace) - 1);
}

function makeTickRange(start, end, increment, incrementOf, baseFunc, smallInc, wid) {
  if ( incrementOf === d3.time.year ) {
    // For Years
    var startyear = d3.time.year.floor(dt(start));
    var endyear   = d3.time.year.ceil( dt(end  ));

    var curange = d3.range(startyear.getFullYear(), endyear.getFullYear());

    // Filter for proper increments
    curange = _.filter(curange, function (d, i) {
      return d % increment == 0;
    });

    curange = _.map(curange, function (d) { return new Date(d, 0); });

    return curange;

  } else if ( incrementOf === d3.time.month ) {
    // For Months
    var startyear = d3.time.year.floor(dt(start));
    var endyear   = d3.time.year.ceil( dt(end  ));

    var curange = d3.range(startyear.getFullYear(), endyear.getFullYear());

    // for each year, get all of the months for it
    curange = _.map(curange, function (d, i) {
      return _.map([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ,11], function (f) {
        // For each month of the year
        return new Date(d, f);
      });
    });
    curange = _.flatten(curange);

    curange = _.filter(curange, function (d, i) {
      // Filter for proper increments
      return i % increment == 0;
    });

    return curange;

  } else if (baseFunc === d3.time.month){
    // For Days
    var startyear = d3.time.year.floor(dt(start));
    var endyear   = d3.time.year.ceil( dt(end  ));

    var curange = d3.range(startyear.getFullYear(), endyear.getFullYear());

    // For each year, get all of the months for it
    curange = _.map(curange, function (year, i) {
      return _.map([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ,11], function (month) {
        // For each month of the year
        var monthDays = getNumberOfDaysInCurrentMonth(new Date(year, month));
        return _.map(d3.range(1, monthDays + 1), function (day) {
          // For each day of the month
          // Filter for proper increments
          //   and remove ones which are too close
          //   together near the ends of the months
          if ((day - 1) % increment == 0 && monthDays + 1 - day >= increment ) {
            return new Date(year, month, day);
          } else {
            return [];
          }
        });
      });
    });

    curange = _.flatten(curange);

    return curange;

  } else {
    // For everything smaller than days
    return d3.range( baseFunc.floor( dt(start) ).getTime(),
                     baseFunc.ceil(  dt( end ) ).getTime(),
                     roundUpToNearestTime(
                       smallInc*MIN_DISTANCE_BETWEEN_X_AXIS_LABELS/wid,
                       smallInc));
  }
}

// for major ticks every 'a' units,
// have a minor tick every 'b' units
// [a, b]
// b should always be a factor of a
var rounding_ticks = {
   1   : 0.5, // when we are showing 1 of something, display a tick every 0.5
   2   : 1  ,
   3   : 1  ,
   5   : 1  ,
   6   : 3  , // when we are showing 6 of something, display a tick every 3
   10  : 5  ,
   12  : 3  ,
   15  : 5  ,
   20  : 5  ,
   25  : 5  ,
   30  : 10 ,
   50  : 10 ,
   100 : 50 ,
   200 : 50 ,
   500 : 100,
}

// Data object to help make custom axis' tick values
// [ estimate size in milliseconds,
//   how many to increment,
//   precise time rounder for anchoring,
//   precise time rounder ]
var rounding_scales = [
  [ times.ms , 1   , d3.time.second , millisecond],
  [ times.ms , 2   , d3.time.second , millisecond],
  [ times.ms , 5   , d3.time.second , millisecond],
  [ times.ms , 10  , d3.time.second , millisecond],
  [ times.ms , 20  , d3.time.second , millisecond],
  [ times.ms , 50  , d3.time.second , millisecond],
  [ times.ms , 100 , d3.time.second , millisecond],
  [ times.ms , 200 , d3.time.second , millisecond],
  [ times.ms , 500 , d3.time.second , millisecond],
  [ times.s  , 1   , d3.time.minute , d3.time.second],
  [ times.s  , 2   , d3.time.minute , d3.time.second],
  [ times.s  , 5   , d3.time.minute , d3.time.second],
  [ times.s  , 15  , d3.time.minute , d3.time.second],
  [ times.s  , 30  , d3.time.minute , d3.time.second],
  [ times.m  , 1   , d3.time.hour   , d3.time.minute],
  [ times.m  , 2   , d3.time.hour   , d3.time.minute],
  [ times.m  , 5   , d3.time.hour   , d3.time.minute],
  [ times.m  , 15  , d3.time.hour   , d3.time.minute],
  [ times.m  , 30  , d3.time.hour   , d3.time.minute],
  [ times.h  , 1   , d3.time.day    , d3.time.hour],
  [ times.h  , 3   , d3.time.day    , d3.time.hour],
  [ times.h  , 6   , d3.time.day    , d3.time.hour],
  [ times.h  , 12  , d3.time.day    , d3.time.hour],
  [ times.d  , 1   , d3.time.month  , d3.time.day],
  [ times.d  , 2   , d3.time.month  , d3.time.day],
  [ times.d  , 5   , d3.time.month  , d3.time.day],
  [ times.d  , 10  , d3.time.month  , d3.time.day],
  [ times.d  , 15  , d3.time.month  , d3.time.day],
  [ times.mo , 1   , d3.time.year   , d3.time.month],
  [ times.mo , 2   , d3.time.year   , d3.time.month],
  [ times.mo , 3   , d3.time.year   , d3.time.month],
  [ times.mo , 6   , d3.time.year   , d3.time.month],
  [ times.mo , 12  , d3.time.year   , d3.time.month],
  [ times.y  , 1  , d3.time.year , d3.time.year],
  [ times.y  , 2  , d3.time.year , d3.time.year],
  [ times.y  , 5  , d3.time.year , d3.time.year],
  [ times.y  , 10 , d3.time.year , d3.time.year],
  [ times.y  , 25 , d3.time.year , d3.time.year],
  [ times.y  , 50 , d3.time.year , d3.time.year],
  [ times.y  , 100, d3.time.year , d3.time.year],
  [ times.y  , 100, d3.time.year , d3.time.year],
];