score:1

Accepted answer

I created a horizon chart example for dc.js.

horizon chart example

The main problem with integrating d3-horizon-chart with dc.js is that it doesn't have an X scale - it just displays each value as a 1-pixel column of color, in the array order.

This means that it doesn't use the group keys, and the data must be sorted in the correct order before drawing the horizon chart.

Here is the definition of the chart:

  class HorizonChart {
      constructor(parent, group) {
          this._group = null;
          this._colors = null;
          this._seriesAccessor = null;
          this._root = d3.select(parent);
          dc.registerChart(this, group);
      }

      // initialization functions for user

      group(group) {
          if(!arguments.length)
              return this._group;
          this._group = group;
          return this;
      }

      // takes array of colors (not scale)
      colors(colors) {
          if(!arguments.length)
              return this._colors;
          this._colors = colors;
          return this;
      }

      seriesAccessor(seriesAccessor) {
          if(!arguments.length)
              return this._seriesAccessor;
          this._seriesAccessor = seriesAccessor;
          return this;
      }

      valueAccessor(valueAccessor) {
          if(!arguments.length)
              return this._valueAccessor;
          this._valueAccessor = valueAccessor;
          return this;
      }

      // interface for dc.js chart registry

      render() {
          this.redraw();
      }

      redraw() {
          const nester = d3.nest().key(this._seriesAccessor),
                nesting = nester.entries(this._group.all());
          let horizon = this._root.selectAll('.horizon')
              .data(nesting);
          horizon = horizon.enter()
              .append('div')
              .attr('class', 'horizon')
              .merge(horizon);
          const colors = this._colors,
                valueAccessor = this._valueAccessor;
          horizon
              .each(function(series) {
                  d3.select(this).selectAll('*').remove();
                  d3.horizonChart()
                      .colors(typeof colors === 'function' ? colors(series.key) : colors)
                      .title(series.key)
                      .call(this, series.values.map(valueAccessor));
            });
      }

It implements a small part of the interface of the dc.js series chart. One difference is that d3-horizon-chart takes an array of colors and decides how many positive and negative bands to draw based on the length of that array. So HorizonChart.colors() accepts either an array of colors, or a function that takes the series key and returns an array of colors.

It uses d3.nest() to split the data by the series accessor. Then it adds or removes divs for the horizon charts, removes anything inside each div and draws a horizon chart there.

sorting the data

Since the data needs to be sorted, and what we get from group.all() for a multikey will be sorted wrong, we can use a fake group to order by the two elements of the key:

  function sort_multikey_group(group) {
      return {
          all: () => {
              return group.all().slice().sort(
                  ({key: keyA}, {key: keyB}) => d3.ascending(keyA[0],keyB[0]) || d3.ascending(keyA[1],keyB[1]));
          }
      };
  }

We apply the fake group when initializing the horizon chart:

  var horizonChart = new HorizonChart("#horizon"),
  horizonChart
      .group(sort_multikey_group(exptRunGroup))
      .colors(n => [d3.schemeBlues, d3.schemeOranges, d3.schemeGreens, d3.schemeReds, d3.schemePurples][n-1][6]) // levels * 2
      .seriesAccessor(d => d.key[0])
      .valueAccessor(d => d.value - 500)

The function parameter to .colors() chooses one of the D3 Color Schemes for each of the horizon charts. It chooses the scheme with 6 colors to show 3 positive and 3 negative bands.

It puts data into each of the series charts based on the first element of the key. In this example, data is shifted lower to use the negative color bands.

example data

The data in the example is the Michelson-Morley Experiment data used in the dc.js Series Chart Example, interpolated 20 points for every one:

  const experiments2 = d3.range(experiments.length-1).flatMap(i => {
      if(experiments[i].Expt !== experiments[i+1].Expt)
          return [];
      let {Expt, Run, Speed: Speed0} = experiments[i],
            {Speed: Speed1} = experiments[i+1];
      Expt = +Expt; Run = +Run; Speed0 = +Speed0; Speed1 = +Speed1;
      const terp = d3.scaleLinear().range([Speed0, Speed1]);
      return d3.range(mult).map(j => ({Expt, Run: Run + j/mult, Speed: terp(j/mult)}));
  });

That's another difference between d3-horizon-chart and the built-in dc.js charts: it needs data for every pixel.


Related Query

More Query from same tag