Search code examples
dc.jscrossfilter

How to use Javascript eventlistener to call functions which draw dc.js charts/graphs, etc?


I have different html buttons, which call different sets of DC.js graphs/charts/numbers, etc., but at the moment, I don't know how to add an eventlistener to trigger them.

The below code is two of the buttons: all-seasons and s_06. I need to be able to switch between them, by simply clicking the buttons, without having to physically hardcode them in, and by "hardcode" I mean using // to block out the one I don't want and then reload the page!

I have show_all_info running on page load, as you can see below.

show_all_info(ndx);

document.addEventListener("DOMContentLoaded", function() {
    document.getElementById("all-seasons").addEventListener("click", show_all_info);
    document.getElementById("s_06").addEventListener("click", show_06_info);
});

At the moment, show_all_info(ndx) is loading fine and as it should, but then nothing happens when I click the s_06 button.

function show_all_info(ndx) {
  show_all_wins_pie(ndx);
  show_all_poles_pie(ndx);
  show_all_fast_laps_pie(ndx);
  show_all_constructor_points(ndx);
}

function show_06_info(ndx) {
  show_wins_pie(ndx, "06");
  show_poles_pie(ndx, "06");
  show_fast_laps_pie(ndx, "06");
  show_points(ndx, "06");
  show_driver_world_champ_chart(ndx, "06", "Fernando Alonso");
  // etc.
}

Version 1: re-initialize the charts

Here's the actual code (I will paste only 1 pie from each):

function show_all_wins_pie(ndx) {
  var dim = ndx.dimension(dc.pluck("win_car"));
  var group = dim.group().reduce(
    function(p, v) {
      p.count++;
      if(v.win_car != "N/A") {
        p.match++;
      } else {
        return 0;
      }
      return p;
    },
    function(p, v) {
      p.count--;
      if(v.win_car != "N/A") {
        p.match--;
      } else {
        return 0;
      }
      return p;
    },
    function() {
      return { count: 0, match: 0 };
    }
  );

  dc.pieChart("#wins-pie")
    .height(200)
    .width(200)
    .radius(100)
    .innerRadius(40)
    .dimension(dim)
    .valueAccessor(function(d) {
      if(d.value.count > 0) {
        return d.value.match;
      } else {
        return 0;
      }
    })
    .group(group)
    .transitionDuration(1000);
}
function show_wins_pie(ndx, season) {
  var dim = ndx.dimension(dc.pluck("win_car"));
  var group = dim.group().reduce(
    function(p, v) {
      if(v.season == season) {
        p.count++;
        if(v.win_car != "N/A") {
          p.match++;
        } else {
          return 0;
        }
      }
      return p;
    },
    function(p, v) {
      if(v.season == season) {
        p.count--;
        if(v.win_car != "N/A") {
          p.match--;
        } else {
          return 0;
        }
      }
      return p;
    },
    function() {
      return { count: 0, match: 0 };
    }
  );

  dc.pieChart("#wins-pie")
    .height(200)
    .width(200)
    .radius(100)
    .innerRadius(40)
    .dimension(dim)
    .valueAccessor(function(d) {
      if(d.value.count > 0) {
        return d.value.match;
      } else {
        return 0;
      }
    })
    .group(group)
    .transitionDuration(1000);
}

Version 2: only render the first time

After addressing Gordon's answer and comments below, I ended up with the following, but the season buttons still don't do anything:

function show_all_wins_pie(ndx) {
  var dim = ndx.dimension(dc.pluck("win_car"));
  var group = dim.group().reduce(
    function(p, v) {
      p.count++;
      if(v.win_car != "N/A") {
        p.match++;
      } else {
        return 0;
      }
      return p;
    },
    function(p, v) {
      p.count--;
      if(v.win_car != "N/A") {
        p.match--;
      } else {
        return 0;
      }
      return p;
    },
    function() {
      return { count: 0, match: 0 };
    }
  );

  if(allWinsPie) {
    allWinsPie.redraw();
  } else {
    allWinsPie = dc.pieChart("#wins-pie");
  allWinsPie
    .height(200)
    .width(200)
    .radius(100)
    .innerRadius(40)
    .dimension(dim)
    .valueAccessor(function(d) {
      if(d.value.count > 0) {
        return d.value.match;
      } else {
        return 0;
      }
    })
    .group(group)
    .transitionDuration(1000);

  allWinsPie.render();
  }
}
function show_wins_pie(ndx, season) {
  var dim = ndx.dimension(dc.pluck("win_car"));
  var group = dim.group().reduce(
    function(p, v) {
      if(v.season == season) {
        p.count++;
        if(v.win_car != "N/A") {
          p.match++;
        } else {
          return 0;
        }
      }
      return p;
    },
    function(p, v) {
      if(v.season == season) {
        p.count--;
        if(v.win_car != "N/A") {
          p.match--;
        } else {
          return 0;
        }
      }
      return p;
    },
    function() {
      return { count: 0, match: 0 };
    }
  );

  if(winsPie) {
    winsPie.redraw();
  } else {
    winsPie = dc.pieChart("#wins-pie")
  winsPie
    .height(200)
    .width(200)
    .radius(100)
    .innerRadius(40)
    .dimension(dim)
    .valueAccessor(function(d) {
      if(d.value.count > 0) {
        return d.value.match;
      } else {
        return 0;
      }
    })
    .group(group)
    .transitionDuration(1000);

  winsPie.render();
  }
}

Solution

  • It's usually okay to change a bunch of parameters and re-render dc.js charts. If you're just changing the data, you can redraw instead of rendering - that way you get transitions.

    We've had a conversation in the comments, and I have edited it into your question and my answer.

    version 1: don't re-construct charts

    In the first version you attempted to re-construct the charts by calling the chart constructor (dc.pieChart, dc.barChart). The new chart instance may not be able to steal the DOM from the last one.

    I don't have an example I can easily test, but I think that if you make sure the charts are only initialized once, that should get things working:

    // global (top) level
    let stackedChart, pie;
    // ...
    function show_all_constructor_points(ndx) {
    // ...
      if(!stackedChart)
        stackedChart = dc.barChart("#all-constructor-points");
      // as before, but don't re-construct:
      stackedChart
        .width(width)
        .height(400)
      // ...
    
    function show_wins_pie(ndx, season) {
      // ...
      if(!pie)
        pie = dc.pieChart("#wins-pie")
      pie
        .height(200)
        .width(200)
        .radius(100)
    

    In the comments, I mentioned that you should render only the first time, and then redraw whenever the data or filters change.

    version 2: set the data before redrawing

    When you first initialize the chart and set all its parameters, you need to render the chart in order to create the SVG elements.

    Later on, when you change the data, you want to redraw the chart so that it will animate from the old data to the new data. (This works with many but not all chart parameters too.)

    In version 2 of your code, the only problem should be (hopefully) that you need to set the new data before you redraw.

    So instead of

      if(winsPie) {
        winsPie.redraw();
      } else {
        winsPie = dc.pieChart("#wins-pie")
      winsPie
        .height(200)
        .width(200)
        .radius(100)
        .innerRadius(40)
        .dimension(dim)
        .valueAccessor(function(d) {
          if(d.value.count > 0) {
            return d.value.match;
          } else {
            return 0;
          }
        })
        .group(group)
        .transitionDuration(1000);
    
      winsPie.render();
    

    This ought to do the trick:

      if(winsPie) {
        winsPie
          .group(group)
          .dimension(dim)
          .redraw();
      } else {
        winsPie = dc.pieChart("#wins-pie")
        winsPie
          .height(200)
          .width(200)
          .radius(100)
          .innerRadius(40)
          .dimension(dim)
          .valueAccessor(function(d) {
            if(d.value.count > 0) {
              return d.value.match;
            } else {
              return 0;
            }
          })
          .group(group)
          .transitionDuration(1000)
          .render();
      }
    

    version 3: event handlers activated and not stomping eachother

    Looking at your repo, I found a few problems:

    1. As you guessed, the event handlers in the two files were interfering with each other. One way to avoid this is by using d3's .on() with a namespace
    2. Also DOMContentLoaded didn't fire for me, but you don't need it there because you are already waiting for the data, which will take longer.
    3. Also, your event handlers take ndx, so you need to wrap them in some sort of function that provides them that
    4. Finally, you'll need to dispose of the old dimensions when they are no longer needed, since crossfilter only supports 32 (and these objects are heavy weight).
    5. One little glitch: if you let anchor clicks trigger the default handler, the page will scroll to the top when they are clicked. event.preventDefault is the solution here.

    script.js event handlers:

    function wrap_handler(h) { // #3
        return function() {
            d3.event.preventDefault(); // #5
            h(ndx);
        };
    }
    
    document.getElementById("all-seasons").addEventListener("click", show_all_info);
    d3.select("#s-06").on('click.bar' /* #1 */, wrap_handler(show_06_info));
    d3.select("#s-07").on('click.bar', wrap_handler(show_07_info));
    // ...
    

    (I guess you'll need to do the same for any events you want to handle twice. Some functionality seems to be duplicated between the two scripts.)

    custom_jQuery.js handlers look like this:

    d3.select("#s-10").on('click.foo', function() {
    

    Disposing dimension and group on redraw:

      if(allPolesPie) {
        allPolesPie.dimension().dispose(); // will dispose group as well
        allPolesPie
          .group(group)
          .dimension(dim)
          .redraw();
      } else {
    

    I've tested this in my clone of your repo, but I'll let you apply it to your own code since I think you are leaning on me a bit too much... there are many parts of this which I think you should learn how to diagnose yourself.

    proof that i got it to work

    If you continue working with dc.js and d3, you will need to learn how to use the developer tools, especially breakpoints and/or logging, to see what code is getting hit and what the values are.