block by timelyportfolio ffedc8a6ab3a204715d6dfaa7d375c23

Horizon Chart d3v4

Full Screen

testing a d3v4 version of horizon plugin

Work progressing on Github at d3-horizon. Follow along or even better help out!


**forked from mbostock's block: Horizon Chart**

Horizon charts combine position and color to reduce vertical space. Start with a standard area chart, then mirror negative values (in blue) or offset them vertically. Click the + button above to increase the number of bands, turning the area into a horizon.

Implemented with the d3.horizon plugin.

index.html

<!doctype html>

<html lang="en">
<head>
  <meta charset="utf-8">

  <title>Horizon Plugin for d3v4</title>

  <script src = "//unpkg.com/d3"></script>
  <script src = "d3-horizon.js"></script>
  <style>
    body {
    font-family: sans-serif;
    }
    svg {
    position: absolute;
    top: 0;
    }
    #horizon-controls {
    position: absolute;
    width: 940px;
    padding: 10px;
    z-index: 1;
    }
    #horizon-bands {
    float: right;
    }
  </style>
</head>

<body>

<div id="horizon-controls">
  <input name="mode" type="radio" value="mirror" id="horizon-mode-mirror" checked><label for="horizon-mode-mirror"> Mirror</label>
  <input name="mode" type="radio" value="offset" id="horizon-mode-offset"><label for="horizon-mode-offset"> Offset</label>
  <span id="horizon-bands"><span id="horizon-bands-value">1</span> <button class="first">&#x2212;</button><button class="last">+</button></span>
</div>
<div id="horizon-chart"></div>

<script>
var width = 960,
    height = 500;

var chart = d3.horizon()
    .width(width)
    .height(height)
    .bands(1)
    .mode("mirror");

var svg = d3.select("body").append("svg")
    .attr("width", width)
    .attr("height", height);

d3.json("unemployment.json", function(error, data) {
  if (error) throw error;

  // Offset so that positive is above-average and negative is below-average.
  var mean = data.rate.reduce(function(p, v) { return p + v; }, 0) / data.rate.length;

  // Transpose column values to rows.
  data = data.rate.map(function(rate, i) {
    return [Date.UTC(data.year[i], data.month[i] - 1), rate - mean];
  });

  // Render the chart.
  svg.data([data]).call(chart);

  // Enable mode buttons.
  d3.selectAll("#horizon-controls input[name=mode]").on("change", function() {
    svg.call(chart.mode(this.value));
  });

  // Enable bands buttons.
  d3.selectAll("#horizon-bands button").data([-1, 1]).on("click", function(d) {
    var n = Math.max(1, chart.bands() + d);
    d3.select("#horizon-bands-value").text(n);
    svg.call(chart.bands(n).height(height / n));
  });
  
});
</script>
</body>
</html>

d3-horizon.js

(function (global, factory) {
	typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('d3-scale'), require('d3-selection'), require('d3-shape'), require('d3-array'), require('d3-transition')) :
	typeof define === 'function' && define.amd ? define(['exports', 'd3-scale', 'd3-selection', 'd3-shape', 'd3-array', 'd3-transition'], factory) :
	(factory((global.d3 = global.d3 || {}),global.d3,global.d3,global.d3,global.d3,global.d3));
}(this, (function (exports,d3Scale,d3Selection,d3Shape,d3Array,d3Transition) { 'use strict';

var horizon = function() {
    var bands = 1, // between 1 and 5, typically
        mode = "offset", // or mirror
        d3area = d3Shape.area(),
        defined,
        x = d3_horizonX,
        y = d3_horizonY,
        width = 960,
        height = 40;

    var color = d3Scale.scaleLinear()
        .domain([-1, 0, 1])
        .range(["#d62728", "#fff", "#1f77b4"]);

    // For each small multiple…
    function horizon(g) {
      g.each(function(d) {
        var g = d3Selection.select(this),
            xMin = Infinity,
            xMax = -Infinity,
            yMax = -Infinity,
            x0, // old x-scale
            y0, // old y-scale
            t0,
            id; // unique id for paths

        // Compute x- and y-values along with extents.
        var data = d.map(function(d, i) {
          var xv = x.call(this, d, i),
              yv = y.call(this, d, i);
          if (xv < xMin) xMin = xv;
          if (xv > xMax) xMax = xv;
          if (-yv > yMax) yMax = -yv;
          if (yv > yMax) yMax = yv;
          return [xv, yv];
        });

        // Compute the new x- and y-scales, and transform.
        var x1 = d3Scale.scaleLinear().domain([xMin, xMax]).range([0, width]),
            y1 = d3Scale.scaleLinear().domain([0, yMax]).range([0, height * bands]),
            t1 = d3_horizonTransform(bands, height, mode);

        // Retrieve the old scales, if this is an update.
        if (this.__chart__) {
          x0 = this.__chart__.x;
          y0 = this.__chart__.y;
          t0 = this.__chart__.t;
          id = this.__chart__.id;
        } else {
          x0 = x1.copy();
          y0 = y1.copy();
          t0 = t1;
          id = ++d3_horizonId;
        }

        // We'll use a defs to store the area path and the clip path.
        var defs = g.selectAll("defs")
            .data([null]);

        // The clip path is a simple rect.
        var defs_new = defs.enter().append("defs");
        defs_new.append("clipPath")
            .attr("id", "d3_horizon_clip" + id)
          .append("rect")
            .attr("width", width)
            .attr("height", height);

        defs = defs.merge(defs_new);
        defs.select("rect")
            .transition(d3Transition.transition())
            .attr("width", width)
            .attr("height", height);

        // We'll use a container to clip all horizon layers at once.
        g.selectAll("g")
            .data([null])
          .enter().append("g")
            .attr("clip-path", "url(#d3_horizon_clip" + id + ")");

        // Instantiate each copy of the path with different transforms.
        var path = g.select("g").selectAll("path")
            .data(d3Array.range(-1, -bands - 1, -1).concat(d3Array.range(1, bands + 1)), Number);

        if (defined) d3area.defined(function(_, i) { return defined.call(this, d[i], i); });

        var d0 = d3area
            .x(function(d) { return x0(d[0]); })
            .y0(height * bands)
            .y1(function(d) { return height * bands - y0(d[1]); })(data);

        var d1 = d3area
            .x(function(d) { return x1(d[0]); })
            .y1(function(d) { return height * bands - y1(d[1]); })(data);

        path.exit()
            .transition(d3Transition.transition())
            .attr("transform", t1)
            .attr("d", d1)
            .remove();

        path = path.enter().append("path")
            .style("fill", color)
            .attr("transform", t0)
            .attr("d", d0)
            .merge(path);

        path
            .transition(d3Transition.transition())
            .style("fill", color)
            .attr("transform", t1)
            .attr("d", d1);

        // Stash the new scales.
        this.__chart__ = {x: x1, y: y1, t: t1, id: id};
      });
    }

    horizon.bands = function(_) {
      if (!arguments.length) return bands;
      bands = +_;
      color.domain([-bands, 0, bands]);
      return horizon;
    };

    horizon.mode = function(_) {
      if (!arguments.length) return mode;
      mode = _ + "";
      return horizon;
    };

    horizon.colors = function(_) {
      if (!arguments.length) return color.range();
      color.range(_);
      return horizon;
    };

    horizon.x = function(_) {
      if (!arguments.length) return x;
      x = _;
      return horizon;
    };

    horizon.y = function(_) {
      if (!arguments.length) return y;
      y = _;
      return horizon;
    };

    horizon.width = function(_) {
      if (!arguments.length) return width;
      width = +_;
      return horizon;
    };

    horizon.height = function(_) {
      if (!arguments.length) return height;
      height = +_;
      return horizon;
    };

    horizon.defined = function(_) {
      if (!arguments.length) return defined;
      defined = _;
      return horizon;
    };

    horizon.curve = function(_) {
      if (!arguments.length) return d3area.curve;
      d3area.curve(_);
      return horizon;
    };

    var d3_horizonId = 0;

    function d3_horizonX(d) { return d[0]; }
    function d3_horizonY(d) { return d[1]; }

    function d3_horizonTransform(bands, h, mode) {
      return mode == "offset"
          ? function(d) { return "translate(0," + (d + (d < 0) - bands) * h + ")"; }
          : function(d) { return (d < 0 ? "scale(1,-1)" : "") + "translate(0," + (d - bands) * h + ")"; };
    }

    return horizon;
};

exports.horizon = horizon;

Object.defineProperty(exports, '__esModule', { value: true });

})));

unemployment.json

{"year":[2000,2000,2000,2000,2000,2000,2000,2000,2000,2000,2000,2000,2001,2001,2001,2001,2001,2001,2001,2001,2001,2001,2001,2001,2002,2002,2002,2002,2002,2002,2002,2002,2002,2002,2002,2002,2003,2003,2003,2003,2003,2003,2003,2003,2003,2003,2003,2003,2004,2004,2004,2004,2004,2004,2004,2004,2004,2004,2004,2004,2005,2005,2005,2005,2005,2005,2005,2005,2005,2005,2005,2005,2006,2006,2006,2006,2006,2006,2006,2006,2006,2006,2006,2006,2007,2007,2007,2007,2007,2007,2007,2007,2007,2007,2007,2007,2008,2008,2008,2008,2008,2008,2008,2008,2008,2008,2008,2008,2009,2009,2009,2009,2009,2009,2009,2009,2009,2009,2009,2009,2010,2010],"month":[1,2,3,4,5,6,7,8,9,10,11,12,1,2,3,4,5,6,7,8,9,10,11,12,1,2,3,4,5,6,7,8,9,10,11,12,1,2,3,4,5,6,7,8,9,10,11,12,1,2,3,4,5,6,7,8,9,10,11,12,1,2,3,4,5,6,7,8,9,10,11,12,1,2,3,4,5,6,7,8,9,10,11,12,1,2,3,4,5,6,7,8,9,10,11,12,1,2,3,4,5,6,7,8,9,10,11,12,1,2,3,4,5,6,7,8,9,10,11,12,1,2],"rate":[4.5,4.4,4.3,3.7,3.8,4.1,4.2,4.1,3.8,3.6,3.7,3.7,4.7,4.6,4.5,4.2,4.1,4.7,4.7,4.9,4.7,5,5.3,5.4,6.3,6.1,6.1,5.7,5.5,6,5.9,5.7,5.4,5.3,5.6,5.7,6.5,6.4,6.2,5.8,5.8,6.5,6.3,6,5.8,5.6,5.6,5.4,6.3,6,6,5.4,5.3,5.8,5.7,5.4,5.1,5.1,5.2,5.1,5.7,5.8,5.4,4.9,4.9,5.2,5.2,4.9,4.8,4.6,4.8,4.6,5.1,5.1,4.8,4.5,4.4,4.8,5,4.6,4.4,4.1,4.3,4.3,5,4.9,4.5,4.3,4.3,4.7,4.9,4.6,4.5,4.4,4.5,4.8,5.4,5.2,5.2,4.8,5.2,5.7,6,6.1,6,6.1,6.5,7.1,8.5,8.9,9,8.6,9.1,9.7,9.7,9.6,9.5,9.5,9.4,9.7,10.6,10.4]}