block by milroc 9353922

Contextually aware axis ticks

Full Screen

This is a custom axis for values over time. In order to see different views, click anywhere on the box.

index.html

<!DOCTYPE html>
<meta charset="utf-8">
<html>
  <head>
  <link href='//fonts.googleapis.com/css?family=Inconsolata' rel='stylesheet' type='text/css'>
  <style>

.axis text {
  font: 14px 'Inconsolata';
}

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

.axis path {
  stroke: none;
}

body {
  min-height: 500px;
}

.end {
  fill: steelblue;
}
  </style>
  </head>
  <body>
    <h1 id="title"></h1>
    <div id="axis"></div>
    <script src="//d3js.org/d3.v3.min.js"></script>
    <script src="src.js"></script>
    <script>
var customTimeFormat = d3.time.format;



var data = [
  { key: 'several years',   values: [new Date(2008, 0, 1), new Date(2013, 0, 1)]  },
  { key: 'one year',        values: [new Date(2012, 0, 1), new Date(2013, 0, 1)]  },
  { key: 'several months',  values: [new Date(2012, 0, 1), new Date(2012, 5, 1)]  },
  { key: 'one month',       values: [new Date(2012, 0, 1), new Date(2012, 1, 1)]  },
  { key: 'several weeks',   values: [new Date(2012, 0, 1), new Date(2012, 0, 21)] },
  { key: 'one week',        values: [new Date(2012, 0, 1), new Date(2012, 0, 7)]  },
  { key: 'several days',    values: [new Date(2012, 0, 1), new Date(2012, 0, 4)]  },
  { key: 'one day',         values: [new Date(2012, 0, 1), new Date(2012, 0, 2)]  },
  { key: 'several hours',   values: [new Date(2012, 0, 1), new Date(1325433600000)] },
  { key: 'one hour',        values: [new Date(2012, 0, 1), new Date(1325408400000)] },
  { key: 'several minutes', values: [new Date(2012, 0, 1), new Date(1325406600000)] },
  { key: 'one minute',      values: [new Date(2012, 0, 1), new Date(1325404860000)] },
  { key: 'several seconds', values: [new Date(2012, 0, 1), new Date(1325404830000)] },
  { key: 'one second',      values: [new Date(2012, 0, 1), new Date(1325404801000)] },
  { key: 'several milliseconds', values: [new Date(2012, 0, 1), new Date(1325404800400)] },
  { key: 'one millisecond',      values: [new Date(2012, 0, 1), new Date(1325404800001)] },
];

var i = -1,
    interval = 2000;

var update = function() {
  ++i;
  if (i >= data.length) i = 0;
  var w = Math.random()*600 + 500;
  var margin = {top: 250, right: 40, bottom: 250, left: 40},
    width = w - margin.left - margin.right,
    height = 500 - margin.top - margin.bottom;
  var x = d3.time.scale()
    .domain(data[i].values)
    .range([0, width]);

  var xAxis = d3.svg.haxis()
      .scale(x)
      .tickMultiFormat([
        ["%-Lms", function(d) { return d.getMilliseconds(); }], // milliseconds
        ["%-Ss", function(d) { return d.getSeconds(); }], // seconds
        ["%-I:%M", function(d) { return d.getMinutes(); }], // minute
        ["%-I %p", function(d) { return d.getHours(); }], // hour
        ["%-d", function(d) { return d.getDay() && d.getDate() != 1; }], // day
        ["%b %-d", function(d) { return d.getDate() != 1; }], // monday of the week
        ["%b", function(d) { return d.getMonth(); }], // month
        ["%Y", function() { return true; }] // year
      ])
      .endTickMultiFormat([
        [":%M:%S.%Lms", function(d) { return d.getMilliseconds(); }], // milliseconds
        [":%M:%Ss", function(d) { return d.getSeconds(); }], // seconds
        ["%-I:%M %p", function(d) { return d.getMinutes(); }], // minute
        ["%-I %p", function(d) { return d.getHours(); }], // hour
        ["%b %-d", function(d) { return d.getDay() && d.getDate() != 1; }], // day
        ["%b %-d", function(d) { return d.getDate() != 1; }], // monday of the week
        ["%Y %b", function(d) { return d.getMonth(); }], // month
        ["%Y", function() { return true; }] // year
      ]);

  d3.select("#title").text(data[i].key);

  var svg = d3.select('#axis')
                .selectAll('svg')
                  .data([i]);

  svg.enter()
    .append("svg")
      .attr("height", height + margin.top + margin.bottom)
    .append("g")
      .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
    .append("g")
      .attr("class", "x axis")
      .attr("transform", "translate(0," + height + ")")
  svg.attr("width", width + margin.left + margin.right);
  svg.selectAll('.x.axis')
      .transition()
        .duration(interval/2)
      .call(xAxis);
};

update();
d3.select("body").on('click', update);
setInterval(update, 5000);
    </script>
  </body>
</html>

src.js

// custom axis (hacked axis)
d3.svg.haxis = function() {
  var scale = d3.scale.linear(),
      orient = d3_svg_axisDefaultOrient,
      innerTickSize = 6,
      outerTickSize = 6,
      tickPadding = 3,
      tickArguments_ = [10],
      tickValues = null,
      tickFormat_,
      endTickFormat_,
      tickMultiFormat_,
      endTickMultiFormat_;

  function axis(g) {
    g.each(function() {
      var g = d3.select(this);

      // Stash a snapshot of the new scale, and retrieve the old snapshot.
      var scale0 = this.__chart__ || scale,
          scale1 = this.__chart__ = scale.copy();

      // Ticks, or domain values for ordinal scales.
      if (endTickMultiFormat_ != null) endTickMultiFormat_ = (endTickMultiFormat_ == null && tickMultiFormat_ != null) ? tickMultiFormat_ : null;
      var ticks = tickValues == null ? (scale1.ticks ? scale1.ticks.apply(scale1, tickArguments_) : scale1.domain()) : tickValues,
          tickLength = ticks.length,
          tickFormat = (tickMultiFormat_ === null)?(tickFormat_ == null ? (scale1.tickFormat ? scale1.tickFormat.apply(scale1, tickArguments_) : d3_identity) : tickFormat_):(d3_time_formatMulti(tickMultiFormat_)),
          endTickFormat = (endTickMultiFormat_ === null)?(tickFormat):(d3_time_formatMulti(endTickMultiFormat_)),
          tick = g.selectAll(".tick").data(ticks, scale1),
          tickEnter = tick.enter().insert("g", ".domain").attr("class", "tick").style("opacity", ε),
          tickExit = d3.transition(tick.exit()).style("opacity", ε).remove(),
          tickUpdate = d3.transition(tick).style("opacity", 1).attr("class", function(d, i) { return ((!i || i === tickLength - 1)?"end":"") + " tick"; }),
          tickTransform;

      function d3_time_formatMulti(formats) {
        var n = formats.length, i = -1;
        // convert to formats
        while (++i < n) formats[i][0] = d3.time.format(formats[i][0]);
        return function(date, i) {
          var j = 0,
              k = 0,
              f = formats[j],
              n = formats[k],
              neighbor = ticks[(j > 0)?(j - 1):(j + 1)];
          while (!f[1](date)) f = formats[++j];
          while (!n[1](neighbor)) n = formats[++k];
          if (j - k > 1) {
            f = formats[++k];
          }
          return f[0](date);
        };
      }

      // Domain.
      var range = d3_scaleRange(scale1),
          path = g.selectAll(".domain").data([0]),
          pathUpdate = (path.enter().append("path").attr("class", "domain"), d3.transition(path));

      tickEnter.append("line");
      tickEnter.append("text");
      var lineEnter = tickEnter.select("line"),
          lineUpdate = tickUpdate.select("line"),
          text = tick.select("text").text(function(d, i) {
            var render = (endTickFormat && (!i || i === tickLength - 1))?endTickFormat(d, i):tickFormat(d, i);
            if (!endTickFormat || render.indexOf('s') === -1) return render.toUpperCase();
            return render;
          }),
          textEnter = tickEnter.select("text"),
          textUpdate = tickUpdate.select("text");

      switch (orient) {
        case "bottom": {
          tickTransform = d3_svg_axisX;
          lineEnter.attr("y2", innerTickSize);
          textEnter.attr("y", Math.max(innerTickSize, 0) + tickPadding);
          lineUpdate.attr("x2", 0).attr("y2", innerTickSize);
          textUpdate.attr("x", 0).attr("y", Math.max(innerTickSize, 0) + tickPadding);
          text.attr("dy", ".71em").style("text-anchor", "middle");
          pathUpdate.attr("d", "M" + range[0] + "," + outerTickSize + "V0H" + range[1] + "V" + outerTickSize);
          break;
        }
        case "top": {
          tickTransform = d3_svg_axisX;
          lineEnter.attr("y2", -innerTickSize);
          textEnter.attr("y", -(Math.max(innerTickSize, 0) + tickPadding));
          lineUpdate.attr("x2", 0).attr("y2", -innerTickSize);
          textUpdate.attr("x", 0).attr("y", -(Math.max(innerTickSize, 0) + tickPadding));
          text.attr("dy", "0em").style("text-anchor", "middle");
          pathUpdate.attr("d", "M" + range[0] + "," + -outerTickSize + "V0H" + range[1] + "V" + -outerTickSize);
          break;
        }
        case "left": {
          tickTransform = d3_svg_axisY;
          lineEnter.attr("x2", -innerTickSize);
          textEnter.attr("x", -(Math.max(innerTickSize, 0) + tickPadding));
          lineUpdate.attr("x2", -innerTickSize).attr("y2", 0);
          textUpdate.attr("x", -(Math.max(innerTickSize, 0) + tickPadding)).attr("y", 0);
          text.attr("dy", ".32em").style("text-anchor", "end");
          pathUpdate.attr("d", "M" + -outerTickSize + "," + range[0] + "H0V" + range[1] + "H" + -outerTickSize);
          break;
        }
        case "right": {
          tickTransform = d3_svg_axisY;
          lineEnter.attr("x2", innerTickSize);
          textEnter.attr("x", Math.max(innerTickSize, 0) + tickPadding);
          lineUpdate.attr("x2", innerTickSize).attr("y2", 0);
          textUpdate.attr("x", Math.max(innerTickSize, 0) + tickPadding).attr("y", 0);
          text.attr("dy", ".32em").style("text-anchor", "start");
          pathUpdate.attr("d", "M" + outerTickSize + "," + range[0] + "H0V" + range[1] + "H" + outerTickSize);
          break;
        }
      }

      // If either the new or old scale is ordinal,
      // entering ticks are undefined in the old scale,
      // and so can fade-in in the new scale’s position.
      // Exiting ticks are likewise undefined in the new scale,
      // and so can fade-out in the old scale’s position.
      if (scale1.rangeBand) {
        var x = scale1, dx = x.rangeBand() / 2;
        scale0 = scale1 = function(d) { return x(d) + dx; };
      } else if (scale0.rangeBand) {
        scale0 = scale1;
      } else {
        tickExit.call(tickTransform, scale1);
      }

      tickEnter.call(tickTransform, scale0);
      tickUpdate.call(tickTransform, scale1);
    });
  }

  axis.scale = function(x) {
    if (!arguments.length) return scale;
    scale = x;
    return axis;
  };

  axis.orient = function(x) {
    if (!arguments.length) return orient;
    orient = x in d3_svg_axisOrients ? x + "" : d3_svg_axisDefaultOrient;
    return axis;
  };

  axis.ticks = function() {
    if (!arguments.length) return tickArguments_;
    tickArguments_ = arguments;
    return axis;
  };

  axis.tickValues = function(x) {
    if (!arguments.length) return tickValues;
    tickValues = x;
    return axis;
  };

  axis.tickFormat = function(x) {
    if (!arguments.length) return tickFormat_;
    tickFormat_ = x;
    return axis;
  };

  axis.tickMultiFormat = function(x) {
    if (!arguments.length) return tickMultiFormat_;
    tickMultiFormat_ = x;
    return axis;
  };

  axis.endTickMultiFormat = function(x) {
    if (!arguments.length) return endTickMultiFormat_;
    endTickMultiFormat_ = x;
    return axis;
  };


  axis.tickSize = function(x) {
    var n = arguments.length;
    if (!n) return innerTickSize;
    innerTickSize = +x;
    outerTickSize = +arguments[n - 1];
    return axis;
  };

  axis.innerTickSize = function(x) {
    if (!arguments.length) return innerTickSize;
    innerTickSize = +x;
    return axis;
  };

  axis.outerTickSize = function(x) {
    if (!arguments.length) return outerTickSize;
    outerTickSize = +x;
    return axis;
  };

  axis.tickPadding = function(x) {
    if (!arguments.length) return tickPadding;
    tickPadding = +x;
    return axis;
  };

  axis.tickSubdivide = function() {
    return arguments.length && axis;
  };

  return axis;
};

  // necessary variables from the d3 namespace
  var ε = 1e-6;
  var d3_identity = function(d) { return d; };

  var d3_svg_axisDefaultOrient = "bottom",
      d3_svg_axisOrients = {top: 1, right: 1, bottom: 1, left: 1};

  function d3_svg_axisX(selection, x) {
    selection.attr("transform", function(d) { return "translate(" + x(d) + ",0)"; });
  }

  function d3_svg_axisY(selection, y) {
    selection.attr("transform", function(d) { return "translate(0," + y(d) + ")"; });
  }

  function d3_scaleExtent(domain) {
    var start = domain[0], stop = domain[domain.length - 1];
    return start < stop ? [start, stop] : [stop, start];
  }

  function d3_scaleRange(scale) {
    return scale.rangeExtent ? scale.rangeExtent() : d3_scaleExtent(scale.range());
  }