2
\$\begingroup\$

Consider the following crossfilter / dc.js app (screenshot):

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Data Exploration Tool MVP</title>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"/>
    <link rel="stylesheet" href="http://unpkg.com/dc@3/dc.css"/>
    <style>
      #data-count {
        margin-top: 0;
        text-align: left;
        float: none;
      }
      table {
        table-layout: fixed;
      }
      td {
        width: 1%;
      }
    </style>
</head>
<body>

<div class="container-fluid" style="margin: 10px;">

  <div class="row">
    <h2>Data Exploration Tool</h2>
    <div class="col-md-3 well well-sm">
      <div class="dc-data-count" id="data-count">
          <span class="filter-count"></span>
           selected out of
          <span class="total-count"></span>
           points |
          <a href="javascript:dc.filterAll(); dc.renderAll();">Reset All</a><br>
      </div>
    </div>
  </div>


  <div class="row">
    <div class="col-md-12">

      <!-- First row of charts -->
      <div class="row">
        <div class="col-md-3">
          <div id="chart-11" style="width:100%;">
            <div id="chart-11-title"></div>
            <div class="reset" style="visibility: hidden;">range: <span class="filter"></span>
              <a href="javascript:chart_11.filterAll();dc.redrawAll();">reset</a>
            </div>
          </div>
        </div>
        <div class="col-md-3">
          <div id="chart-12" style="width:100%;">
            <div id="chart-12-title"></div>
            <div class="reset" style="visibility: hidden;">selected: <span class="filter"></span>
              <a href="javascript:chart_12.filterAll();dc.redrawAll();">reset</a>
            </div>
          </div>
        </div>
        <div class="col-md-3">
          <div id="chart-13" style="width:100%;">
              <div id="chart-13-title"></div>
            <div class="reset" style="visibility: hidden;">selected: <span class="filter"></span>
              <a href="javascript:chart_13.filterAll();dc.redrawAll();">reset</a>
            </div>
          </div>
        </div>
        <div class="col-md-3">
            <div id="chart-14" style="width:100%;">
                <div id="chart-14-title"></div>
              <div class="reset" style="visibility: hidden;">selected: <span class="filter"></span>
                <a href="javascript:chart_14.filterAll();dc.redrawAll();">reset</a>
              </div>
            </div>
          </div>
      </div>

      <!-- Second row of chart -->
      <div class="row">
        <div class="col-md-3">
          <div id="chart-21" style="width:100%;">
            <div id="chart-21-title"></div>
            <div class="reset" style="visibility: hidden;">selected: <span class="filter"></span>
              <a href="javascript:chart_21.filterAll();dc.redrawAll();">reset</a>
            </div>
          </div>
        </div>
        <div class="col-md-3">
          <div id="chart-22" style="width:100%;">
            <div id="chart-22-title"></div>
            <div class="reset" style="visibility: hidden;">range: <span class="filter"></span>
              <a href="javascript:chart_22.filterAll();dc.redrawAll();">reset</a>
            </div>
          </div>
        </div>
        <div class="col-md-3">
          <div id="chart-23"style="width:100%;">
            <div id="chart-23-title"></div>
            <div class="reset" style="visibility: hidden;">selected: <span class="filter"></span>
              <a href="javascript:chart_23.filterAll();dc.redrawAll();">reset</a>
            </div>
          </div>
        </div>
        <div class="col-md-3">
            <div id="chart-24"style="width:100%;">
              <div id="chart-24-title"></div>
              <div class="reset" style="visibility: hidden;">selected: <span class="filter"></span>
                <a href="javascript:chart_24.filterAll();dc.redrawAll();">reset</a>
              </div>
            </div>
          </div>
      </div>
    </div>
  </div>

</div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.9.1/d3.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crossfilter/1.3.12/crossfilter.js"></script>
<script src="http://unpkg.com/dc@3/dc.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.js"></script>

<script type="text/javascript">

'use strict';

dc.config.defaultColors(d3.schemeSet1);

var 
    chart_11   = dc.barChart("#chart-11"),
    chart_12   = dc.barChart("#chart-12"),
    chart_13   = dc.barChart("#chart-13"),
    chart_21   = dc.barChart("#chart-21"),
    chart_22   = dc.barChart("#chart-22"),
    chart_23   = dc.barChart("#chart-23"),
    data_count = dc.dataCount(".dc-data-count");

d3.csv("https://gist.githubusercontent.com/JasonAizkalns/32ece5c815f9ac5d540c41dc0825bbab/raw/362050300ddcb99f195044c00d9f26b0d7d489ca/data.csv").then(function(data) {

var var_names = ["x", "y", "z", "a", "b", "c"];

$("#chart-11-title").append(["<h5>", var_names[0], "<br>Subtitle</h5>"].join(""));
$("#chart-12-title").append(["<h5>", var_names[1], "<br>Subtitle</h5>"].join(""));
$("#chart-13-title").append(["<h5>", var_names[2], "<br>Subtitle</h5>"].join(""));
$("#chart-21-title").append(["<h5>", var_names[3], "<br>Subtitle</h5>"].join(""));
$("#chart-22-title").append(["<h5>", var_names[4], "<br>Subtitle</h5>"].join(""));
$("#chart-23-title").append(["<h5>", var_names[5], "<br>Subtitle</h5>"].join(""));

var c11_bin = 10,
    c12_bin = 10,
    c13_bin = 500,
    c21_bin = 100,
    c22_bin = 20,
    c23_bin = 1000;


var ndx = crossfilter(data),

    chart_11_dim = ndx.dimension(function(d) { return +d[var_names[0]]; }),
    chart_12_dim = ndx.dimension(function(d) { return +d[var_names[1]]; }),
    chart_13_dim = ndx.dimension(function(d) { return +d[var_names[2]]; }),
    chart_21_dim = ndx.dimension(function(d) { return +d[var_names[3]]; }),
    chart_22_dim = ndx.dimension(function(d) { return +d[var_names[4]]; }),
    chart_23_dim = ndx.dimension(function(d) { return +d[var_names[5]]; }),

    chart_11_grp = chart_11_dim.group(function(d) { return Math.floor(d / c11_bin) * c11_bin }).reduceCount(),
    chart_12_grp = chart_12_dim.group(function(d) { return Math.floor(d / c12_bin) * c12_bin }).reduceCount(),
    chart_13_grp = chart_13_dim.group(function(d) { return Math.floor(d / c13_bin) * c13_bin }).reduceCount(),
    chart_21_grp = chart_21_dim.group(function(d) { return Math.floor(d / c21_bin) * c21_bin }).reduceCount(),
    chart_22_grp = chart_22_dim.group(function(d) { return Math.floor(d / c22_bin) * c22_bin }).reduceCount(),
    chart_23_grp = chart_23_dim.group(function(d) { return Math.floor(d / c23_bin) * c23_bin }).reduceCount();

var all = ndx.groupAll();

data_count.dimension(ndx)
  .group(all);

var chart_11_min = +chart_11_dim.bottom(1)[0][var_names[0]],
    chart_11_max = +chart_11_dim.top(1)[0][var_names[0]],
    chart_12_min = +chart_12_dim.bottom(1)[0][var_names[1]],
    chart_12_max = +chart_12_dim.top(1)[0][var_names[1]],
    chart_13_min = +chart_13_dim.bottom(1)[0][var_names[2]],
    chart_13_max = +chart_13_dim.top(1)[0][var_names[2]],
    chart_21_min = +chart_21_dim.bottom(1)[0][var_names[3]],
    chart_21_max = +chart_21_dim.top(1)[0][var_names[3]],
    chart_22_min = +chart_22_dim.bottom(1)[0][var_names[4]],
    chart_22_max = +chart_22_dim.top(1)[0][var_names[4]],
    chart_23_min = +chart_23_dim.bottom(1)[0][var_names[5]],
    chart_23_max = +chart_23_dim.top(1)[0][var_names[5]];

var breathing_room = 0.05;

chart_11
  .dimension(chart_11_dim)
  .group(chart_11_grp)
  .round(dc.round.floor)
  .alwaysUseRounding(true)
  .x(d3.scaleLinear().domain([chart_11_min - ((chart_11_max - chart_11_min) * breathing_room), chart_11_max + ((chart_11_max - chart_11_min) * breathing_room)]))
  .xUnits(function(start, end, xDomain) { return (end - start) / c11_bin; })
  .controlsUseVisibility(true);

chart_12
  .dimension(chart_12_dim)
  .group(chart_12_grp)
  .round(dc.round.floor)
  .alwaysUseRounding(true)
  .x(d3.scaleLinear().domain([chart_12_min - ((chart_12_max - chart_12_min) * breathing_room), chart_12_max + ((chart_12_max - chart_12_min) * breathing_room)]))
  .xUnits(function(start, end, xDomain) { return (end - start) / c12_bin; })
  .controlsUseVisibility(true);

chart_13
  .dimension(chart_13_dim)
  .group(chart_13_grp)
  .round(dc.round.floor)
  .alwaysUseRounding(true)
  .x(d3.scaleLinear().domain([chart_13_min - ((chart_13_max - chart_13_min) * breathing_room), chart_13_max + ((chart_13_max - chart_13_min) * breathing_room)]))
  .xUnits(function(start, end, xDomain) { return (end - start) / c13_bin; })
  .controlsUseVisibility(true);

chart_21
  .dimension(chart_21_dim)
  .group(chart_21_grp)
  .round(dc.round.floor)
  .alwaysUseRounding(true)
  .x(d3.scaleLinear().domain([chart_21_min - ((chart_21_max - chart_21_min) * breathing_room), chart_21_max + ((chart_21_max - chart_21_min) * breathing_room)]))
  .xUnits(function(start, end, xDomain) { return (end - start) / c21_bin; })
  .controlsUseVisibility(true);

chart_22
  .dimension(chart_22_dim)
  .group(chart_22_grp)
  .round(dc.round.floor)
  .alwaysUseRounding(true)
  .x(d3.scaleLinear().domain([chart_22_min - ((chart_22_max - chart_22_min) * breathing_room), chart_22_max + ((chart_22_max - chart_22_min) * breathing_room)]))
  .xUnits(function(start, end, xDomain) { return (end - start) / c22_bin; })
  .controlsUseVisibility(true);

chart_23
  .dimension(chart_23_dim)
  .group(chart_23_grp)
  .round(dc.round.floor)
  .alwaysUseRounding(true)
  .x(d3.scaleLinear().domain([chart_23_min - ((chart_23_max - chart_23_min) * breathing_room), chart_23_max + ((chart_23_max - chart_23_min) * breathing_room)]))
  .xUnits(function(start, end, xDomain) { return (end - start) / c23_bin; })
  .controlsUseVisibility(true);

dc.renderAll();

});

</script>

</div>
</body>
</html>

I am specifically looking for advice on how to cleanup and better scale the javascript / dc.js code:

  1. How to make the chart_11, chart_12, ..., chart_23 variables dynamically?
  2. Is there a way to automatically calculate natural bin sizes (e.g. c11_bin, c12_bin, ..., c23_bin?
  3. Better ways of setting .x() and .xUnits for each dc.js chart object -- the code is not very DRY.

Any other improvements / suggestions for making this code easier to scale and maintain.

\$\endgroup\$
  • \$\begingroup\$ do you have control over the HTML? \$\endgroup\$ – Oh My Goodness Mar 9 at 23:41
  • \$\begingroup\$ @ohmygoodness yes, I have control over the HTML \$\endgroup\$ – JasonAizkalns Mar 10 at 23:02
2
\$\begingroup\$

Here's the Computer Science 101 answer: put the parameters that change between charts into an array, and then loop over that array.

I want to include your code and data as a dc.js example (if you will permit it?) so I started porting it to the standard format used by those examples.

The full example is here: https://github.com/dc-js/dc.js/blob/compare-unfiltered-example/web/examples/compare-unfiltered.html

I started from my fork of your fiddle, which answers the SO question Display original (conditional) brushed unbrushed crossfilter bars with dc.js with different colors

You can look through the history to see all the changes from that point, but the big idea is just to put all the chart specifications into an array:

  var chart_specs = [
      {
          variable: 'x',
          selector: '#chart-11',
          resolution: 10
      },
      {
          variable: 'y',
          selector: '#chart-12',
          resolution: 10
      },
      {
          variable: 'z',
          selector: '#chart-13',
          resolution: 500
      },
      {
          variable: 'a',
          selector: '#chart-21',
          resolution: 100
      },
      {
          variable: 'b',
          selector: '#chart-22',
          resolution: 20
      },
      {
          variable: 'c',
          selector: '#chart-23',
          resolution: 1000
      }
  ];

The three things we need to know about a chart are:

  • what field or variable in the data to look at
  • the CSS selector of the div to put the chart in
  • what resolution to bin the data in this chart

You also had unique selectors for the chart titles, but those were redundant, so I removed them.

Let's walk through the rest of the code.

Cleaning the data

It's more efficient to convert all the strings to numbers before giving the data to crossfilter:

  data.forEach(function(d) {
      chart_specs.forEach(function(spec) {
          d[spec.variable] = +d[spec.variable];
      });
  });

This is the first of many places where you'll see chart_specs.forEach(). That's the essence of the "CS 101" answer!

Creating the charts

Here's it's chart_specs.map() but the same idea:

  var charts = chart_specs.map(function(spec) {
      return dc.compositeChart(spec.selector);
  });

We'll get an array of charts back.

Initializing the charts

It's just one big loop over all the chart specs:

  chart_specs.forEach(function(spec, i) {

Title each chart

      d3.select(spec.selector).select('h5.chart-title').text(spec.variable);

For simplicity I changed the divs to h5s and gave them the class chart-title. Now we can change the titles with a simple generic D3 call. Otherwise it's the same idea as your old jQuery calls.

Create the dimension and group for each chart

We'll read spec.variable and spec.resolution in order to create appropriate dimensions and groups. We'll also make a static copy of the group data

      var dim = cf.dimension(function(d) { return d[spec.variable]; }),
          group = dim.group(function(d) {
              return Math.floor(d / spec.resolution) * spec.resolution;
          }).reduceCount(),
          static_group = static_copy_group(group);

Hiding the red bars if no filters are active

See the original answer for the purpose of this code.

      charts[i].on('pretransition', function(chart) {
          var any_filters = charts.some(chart => chart.filters().length);
          chart.select('.sub._1')
              .attr('visibility', any_filters ? 'visible' : 'hidden')
      });

The only interesting here is using charts[i]. Array.forEach supplies us not just the current item but the index, which we can use to access the chart in the charts array.

Finish the initialization

Same idea with all of the rest of the code. We just look at spec.variable and spec.resolution instead of having special variables for every chart and every value.

      charts[i]
          .compose([
              dc.barChart(charts[i])
                  .dimension(dim)
                  .group(static_group)
                  .controlsUseVisibility(true),
              dc.barChart(charts[i])
                  .dimension(dim)
                  .group(group)
                  .colors('red')
                  .controlsUseVisibility(true)
                  .brushOn(false)
          ]);

      var min = dim.bottom(1)[0][spec.variable],
          max = dim.top(1)[0][spec.variable];

      charts[i]
          .dimension(dim)
          .group(group)
          .round(dc.round.floor)
          .x(d3.scaleLinear().domain([min - ((max - min) * breathing_room), max + ((max - min) * breathing_room)]))
          .xUnits(function(start, end, xDomain) { return (end - start) / spec.resolution; })
          .controlsUseVisibility(true);

Please let me know if I can publish this example and data. It will be useful to a lot of people! (I have no idea if this data is publishable - looks suitably anonymous, hopefully...)

\$\endgroup\$
  • \$\begingroup\$ this is fantastic! Yes, this data is totally random / fake -- just wanted some variety. I agree, this is a great example...get it posted to the dc.js page! \$\endgroup\$ – JasonAizkalns Mar 21 at 16:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.