3

I have D3 dataset with multiple numeric columns that I am successfully using to render DC.js charts. The chart is currently fixed to only use one of the numeric columns.

However, I want to be able to dynamically switch between the numeric columns to swap them as the value in the DC.js charts.

I plan to do this by using a set of html links at top of page with an onclick event to create variable to change column selected in the D3 dataset. This is working fine.

However, I am stuck on how to refresh the DC.js charts to use updated dataset.

I have seen other similar questions here and here but they are not helping me.

Here is my code:

bucket.getObject({
    Bucket: 's3-google-analytics', 
    Key: 'merged.csv'
    }, 
    function awsDataFile(error, data) {

        if (error) {
            return console.log(error);
        }

        mergedcsv = d3.csv.parse(data.Body.toString()); 
        var parseDate = d3.time.format("%Y%m%d").parse;
        var counter = 0;

        var varMetric = 'sessions'; 
        // Note, i can successfully manually change varMetric variable  
        //to change numeric column is used in chart. 

        mergedcsv.forEach(function(d) {
            d.date = parseDate(d.date);
            d.dateString = formatDateYYYYMMDD(d.date);
            if (varMetric == 'sessions') {
                d.metric = +d.sessions;
            } else if (varMetric == 'users') {
                d.metric = +d.users;
            } else if (varMetric == 'bounceRate') {
                d.metric = +d.bounceRate;
            } else if (varMetric == 'newUsers') {
                d.metric = +d.newUsers;
            } else if (varMetric == 'sessionDuration') {
                d.metric = +d.sessionDuration;
            } else if (varMetric == 'sessionsPerUser') {
                d.metric = +d.sessionsPerUser;
            } else if (varMetric == 'twitterSessions') {
                d.metric = +d.twitterSessions;
            }
            countLoop = counter++;
        });

        var ndx  = crossfilter(mergedcsv);

        // where to put this?
        $('#metric a').click(function(e) {
            var varMetric = $(e.target).text();
            console.log(varMetric);
            ndx.remove();
        // which to use .. add or crossfilter?
            //ndx.add(mergedcsv);
            //ndx  = crossfilter(mergedcsv);
        // which to use .. redrawAll or renderAll?
            //dc.redrawAll();
            //dc.renderAll();
        });

        var yyyymmDim = ndx.dimension(function(d) { return d["yyyymm"]; });
        var PPCCByYYYYMM = yyyymmDim.group().reduceSum( function(d) {  return +d.metric; });
        BarChartAlltimeByMonth = dc.barChart("#barchart-alltime-by-month");

        BarChartAlltimeByMonth
        .height(100)
        .width(1000)
        .margins({top: 0, right: 10, bottom: 50, left: 35})
        .dimension(yyyymmDim)
        .group(PPCCByYYYYMM)

        dc.renderAll(); 

        //on click put here but doesn't work
        $('#metric a').click(function(e) {
            var varMetric = $(e.target).text();
            console.log('onclick ' + varMetric);
            ndx.remove();
            ndx.add(mergedcsv);
            //dc.redrawAll();
            dc.renderAll();
        });


});

I put the onclick handler at bottom of script.

It seems this is done in the other SO questions above by 1) removing old data, 2) adding new data, and 3) redrawing all charts.

ndx.remove() does remove data but ndx.add(mergedcsv) doesn't add data.

dc.redrawAll() doesn't do anything but while dc.renderAll() does cause charts to be rendered again there is no change in data.

This may be both DC.js and general Javascript issue as I am not fluent in either.

curtisp
  • 2,227
  • 3
  • 30
  • 62
  • 1
    Should work. The essential question is, why is `ndx` not defined? Usually JavaScript makes variables available by their *scope*; that is, if these two fragments are part of the same file and not enclosed by functions, `ndx` should be available. Could you edit your question and say more about where these fragments are located in the larger program/page? – Gordon Oct 06 '19 at 08:20
  • (The line replacing `ndx` won’t work but the remove/add data lines should work.) – Gordon Oct 06 '19 at 13:03
  • Thanks that helps re-frame question as generic javascript issue, . it is not DC.js. The `click` function was before `var ndx = crossfilter(mergedcsv);` so obvs why it wasn't declared. Putting `click` function after `var ndx` does indeed allow `ndx.remove();` to work eg chart data clears out. So that I need to refactor so `click` function can update `mergedcsv` data, then redraw/render chart. Will update code in question to show bigger picture. – curtisp Oct 06 '19 at 18:02
  • 1
    Great. Yep you should define your click handler inside that block that is receiving AWS data. – Gordon Oct 06 '19 at 18:13
  • Or at least that’s the easiest way to solve the problem, other ways including classes and explicit globals like `window.ndx` – Gordon Oct 06 '19 at 20:10
  • What should go inside click handler? Only these? `ndx.remove();` `ndx.add(mergedcsv);` and `dc.redrawAll();` – curtisp Oct 06 '19 at 21:18

1 Answers1

1

Yes, you are right, these are programming and JavaScript questions, not really much to do with dc.js or d3.js.

callbacks that depend on callbacks

First we looked at where to put callbacks, like click handlers, that manipulate the visualization and/or data that is set up by another callback, like a received-data handler.

The problem is that the data and chart may not be available at the top level scope.

The easiest solution is to put the click handler inside the data handler, so that it will have access to the chart and data.

reusing data transformations

The next issue you are dealing with is where to put a data transformation so that it will be called both before the chart is drawn, and after a click.

Currently your click handler doesn't do anything because it replaces the data with... the same data. (sad trombone sound)

There are a lot of ways to solve this, but the easiest way to change your current code is to move your data transformations into a reusable function:

function transform_data(data, varMetric) {
    data.forEach(function(d) {
        d.date = parseDate(d.date);
        d.dateString = formatDateYYYYMMDD(d.date);
        if (varMetric == 'sessions') {
            d.metric = +d.sessions;
        } else if (varMetric == 'users') {
            d.metric = +d.users;
        } else if (varMetric == 'bounceRate') {
            d.metric = +d.bounceRate;
        } else if (varMetric == 'newUsers') {
            d.metric = +d.newUsers;
        } else if (varMetric == 'sessionDuration') {
            d.metric = +d.sessionDuration;
        } else if (varMetric == 'sessionsPerUser') {
            d.metric = +d.sessionsPerUser;
        } else if (varMetric == 'twitterSessions') {
            d.metric = +d.twitterSessions;
        }
        countLoop = counter++;
    });
}

Now this can happen before the chart is drawn:

function awsDataFile(error, data) {

    if (error) {
        return console.log(error);
    }

    mergedcsv = d3.csv.parse(data.Body.toString()); 
    var parseDate = d3.time.format("%Y%m%d").parse;
    var counter = 0;

    var varMetric = 'sessions'; 
    transform_data(mergedcsv, varMetric);
    var ndx  = crossfilter(mergedcsv);
    // ...

And we can repeat the data transformation on a click:

    $('#metric a').click(function(e) {
        var varMetric = $(e.target).text();
        console.log('onclick ' + varMetric);
        transform_data(mergedcsv, varMetric);
        ndx.remove();
        ndx.add(mergedcsv);
        dc.redrawAll();
    });

Note: with dc.js you definitely want to render only the first time, and then redraw whenever the data changes. Render starts from scratch while redraw will do the (usually) nice animated transitions.

Gordon
  • 19,811
  • 4
  • 36
  • 74