Bertone
Bertone

Reputation: 756

dc.js - reduce function selected from radio button

In the last few weeks I have been playing a lot with dc.js while trying to create a simple personal dashboard.

I managed to implement a pop-up menu to select the time granularity I want to use for grouping the time dimension of my lineChart and thank to the help from the community I managed to boost performances drastically.

Now I am trying to dynamically change the type of aggregation I perform on my grouped data (sum, average, mode, min and max).

I found this example incredibly helpful but, nevertheless, I didn't quite managed to adapt it on my case and I don't manage to make it work. From my understanding, in this case, I just need to change the value accessor function and then re-draw. In fact, the valueAccessor determines the y-axis pixels positions, so that's the only part it should change. Instead, when I handle the change in group aggregation, I had to re-set the whole chart with the new groupings...

Now here is my code that results in nothing being printed at all with any radio button position (only sum and svg implemented).

If I remove the dynamic valueAccessor part, the default "sum" selection works properly.

Here is the code:

// Disable it or dash_reduceAvgAdd will give an error with ++p!
//'use strict';


// TODO temp dirty workaround
var selectedAggr = 'sum';


// ### Create Chart Objects
// Create chart objects associated with the container elements identified by the css selector.
// Note: It is often a good idea to have these objects accessible at the global scope so that they can be modified or
// filtered by other page controls.
var stackChart = dc.lineChart("#stack-chart");
var volumeChart = dc.barChart('#volume-chart');


// Asynchronously load the data and only when finished build the charts
queue()
    .defer(d3.json, "/data")
    .await(makeGraphs);


// Function to elaborate the data and build the charts
function makeGraphs(error, recordsJson) {

    // Clean data
    var records = recordsJson;

    // Works on d3-v4 only: var dateFormat = d3.timeFormat("%Y-%m-%d %H:%M:%S");
    //var dateFormat = d3.time.format("%Y-%m-%d %H:%M");
    console.log(Object.prototype.toString.call(records[0].date));

    // Coerce values to number and create javascript Date objects
    records.forEach(function(d) {
        d.date = new Date(+d.date);
        d.prodPow = +d.prodPow;
        d.consPow = +d.consPow;
    });


    // Crossfilter instance
    var ndx = crossfilter(records);


    // Aggregation functions
    // SUM mode
    //function reduceAdd(attr) { return reduceSum(function (d) { return d[attr]; }); }
    function dash_reduceSumAdd(attr) { return function (p, v) { return p + +v[attr]; }; }
    function dash_reduceSumSub(attr) { return function (p, v) { return p - v[attr]; }; }
    function dash_reduceInit() { return 0; }

    // AVG mode
    function dash_reduceAvgAdd(attr) {
        return function (p, v) {
            ++p.count;
            p.sum += v[attr];
            p.avg = p.sum/p.count;
            return p;
        };
    }
    function dash_reduceAvgSub(attr) {
        return function (p, v) {
            --p.count;
            p.sum -= v[attr];
            p.avg = p.count ? p.sum / p.count : 0;
            return p;
        }
    }
    function dash_reduceAvgInit() {
        return function () {
            return {count:0, sum:0, avg:0};
        }
    }
    function valAccSum(d) {
        return d.value;
    }
    function valAccAvg(d) {
        return d.value.avg;
    }


    // Map selector to correct map-reduce functions
    var aggregators = {
        sum: [dash_reduceSumAdd, dash_reduceSumSub, dash_reduceInit, valAccSum],
        avg: [dash_reduceAvgAdd, dash_reduceAvgSub, dash_reduceAvgInit, valAccAvg]//,
        //mode: reduceMode,
        //min: reduceMin,
        //max: reduceMax
    };


    // Granularities selectable values
    var granularities = {
        Hours: [d3.time.hour, d3.time.hours],
        Days: [d3.time.day, d3.time.days],
        Weeks: [d3.time.week, d3.time.weeks],
        Months: [d3.time.month, d3.time.months],
        Years: [d3.time.year, d3.time.years]
    };

    // Assign default granularity
    d3.select('#granularity').selectAll('option')
        .data(Object.keys(granularities))
        .enter().append('option')
        .text(function(d) { return d; })
        .attr('selected', function(d) { return d === 'Days' ? '' : null; });

    var dateDim, consPowByHour, prodPowByHour;

    // Function to build the charts from the selected granularity
    function setup(aggr) {
        if (dateDim) {
            dateDim.dispose();
            consPowByHour.dispose();
            prodPowByHour.dispose();
        }
        var gran = granularities[d3.select('#granularity')[0][0].value];
        dateDim = ndx.dimension(function (d) { return gran[0](d.date); });
        consPowByHour =
            dateDim
                .group(function (d) { return gran[0](d); })
                .reduce(aggregators[aggr][0]('consPow'), aggregators[aggr][1]('consPow'), aggregators[aggr][2]);
        //consPowByHour = dateDim.group(function (d) { return granularity[0](d); }).reduceSum();
        prodPowByHour =
            dateDim
                .group(function (d) { return gran[0](d); })
                .reduce(aggregators[aggr][0]('prodPow'), aggregators[aggr][1]('prodPow'), aggregators[aggr][2]);

        // Min and max dates to be used in the charts
        var minDate = gran[0](dateDim.bottom(1)[0]["date"]);
        var maxDate = gran[0](dateDim.top(1)[0]["date"]);

        // Charts customization
        stackChart
            .renderArea(true)
            /* Make the chart as big as the bootstrap grid by not setting ".width(960)" */
            .height(350)
            .transitionDuration(1500)
            .margins({top: 30, right: 50, bottom: 25, left: 40})
            .dimension(dateDim)
            /* Grouped data to represent and label to use in the legend */
            .group(consPowByHour, "Consumed Power [kW]")
            /* Function to access grouped-data values in the chart */
            .valueAccessor(aggregators[aggr][2])
            /* x-axis range */
            .x(d3.time.scale().domain([minDate, maxDate]))
            .xUnits(gran[1])
            /* Auto-adjust axis */
            .elasticY(true)
            .renderHorizontalGridLines(true)
            .legend(dc.legend().x(80).y(0).itemHeight(13).gap(5))
            /* When on, you can't visualize values, when off you can filter data */
            .brushOn(false)
            /* Add another line to the chart; pass (i) group, (ii) legend label and (iii) value accessor */
            .stack(prodPowByHour, "Produced Power [kW]", aggregators[aggr][2])
            /* Range chart to link the brush extent of the range with the zoom focus of the current chart. */
            .rangeChart(volumeChart)
            /* dc.js bug, this should be true by default to turn on visibility for reset class */
            .controlsUseVisibility(true)
        ;

        volumeChart//.width(990)
            .height(60)
            .margins({top: 0, right: 50, bottom: 20, left: 40})
            .dimension(dateDim)
            .group(consPowByHour)
            .centerBar(true)
            .gap(1)
            .x(d3.time.scale().domain([minDate, maxDate]))
            .xUnits(gran[1])
            .elasticY(true)
            .alwaysUseRounding(true)
            /* dc.js bug, this avoids the reset and filter to remain after resetting using the brush/focus */
            .on('renderlet', function (chart) {
                var rangeFilter = chart.filter();
                var focusFilter = chart.focusChart().filter();
                if (focusFilter && !rangeFilter) {
                    dc.events.trigger(function () {
                        chart.focusChart().replaceFilter(rangeFilter);
                    });
                }
            })
        ;
    }

    // First time build charts
    setup(selectedAggr);

    // Render all graphs
    dc.renderAll();

    // Listen for changes on granularities selection
    d3.select('#granularity').on('change', function() {
        setup(selectedAggr);
        dc.redrawAll();
    });

    // Listen for changes on aggregation mode selection
    d3.selectAll('#select-operation input')
        .on('click', function() {
            stackChart.valueAccessor(aggregators[this.value][3]);
            selectedAggr = this.value;
            //setup(this.value);
            dc.redrawAll();
        });

And here are a couple of screenshots of what it looks like when working and when not. Working (sum mode) With the dynamic valueAccessor function (not working)

Thanks in advance, I have really no idea how to move forward since I don't even get any error from the console.

Edit: For completion, here is my html code:

<!DOCTYPE html>
<html>

<head>
    <title>Dashboard</title>
    <link rel="stylesheet" href="./static/css/bootstrap.min.css">
    <link rel="stylesheet" href="./static/css/dc.css">
    <link rel="stylesheet" href="./static/css/custom.css">
</head>


<body class="application">

<!-- Header bar on top -->
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
    <div class="container-fluid">
        <div class="navbar-header">
            <a class="navbar-brand" href="./">Dashboard</a>
        </div>
    </div>
</div>

<!-- Chart container page  -->
<div class="container-fluid">

    <!-- First row of charts (compensate on the left part the strange padding on right "trbl") -->
    <div class="row" style="width:100%; padding: 0px 0px 0px 25px;">

        <!-- Control Panel -->
        <div class="col-sm-12">
            <div class="chart-wrapper control-panel">
                <div class="chart-title control-panel">
                    Control Panel
                </div>
                <div class="row">
                    <div class="col-sm-4">
                        <div class="chart-stage control-panel" style="height: 100px; border-right: 1px solid #e2e2e2;">
                            <div class="text-center" style="padding: 10px;">
                            <!--<div class="inner">-->
                                <strong>Granularity:</strong>
                                <select id="granularity" style="margin-left: 10px"></select>
                                <div id="select-operation" style="margin-top: 15px;">
                                    <strong>Aggregation:</strong>
                                    <label><input type=radio name="operation" value="sum" checked="checked" style="margin-left: 10px">&nbsp;sum</label>
                                    <label><input type=radio name="operation" value="avg">&nbsp;average</label>
                                    <label><input type=radio name="operation" value="mode">&nbsp;mode</label>
                                    <label><input type=radio name="operation" value="min">&nbsp;min</label>
                                    <label><input type=radio name="operation" value="max">&nbsp;max</label>
                                </div>
                            </div>
                        </div>
                    </div>
                    <div class="col-sm-4">
                        <div class="chart-stage control-panel" style="height: 100px; border-right: 1px solid #e2e2e2;">
                            Test
                        </div>
                    </div>
                    <div class="col-sm-4">
                        <div class="chart-stage control-panel" style="height: 100px;">
                            Test
                        </div>
                    </div>
                </div>
            </div>
        </div>

        <!-- Stack Chart and its Range Chart as a single bootstrap grid -->
        <div class="col-sm-12">
            <div class="chart-wrapper">
                <div class="chart-title">
                    Stack Chart
                </div>
                <div class="chart-stage">
                    <!-- Stack Chart -->
                    <div class="row">
                          <div id="stack-chart" style="width:100%;">
                              <a class="reset"
                                 href="javascript:stackChart.filterAll();volumeChart.filterAll();dc.redrawAll();"
                                 style="visibility: hidden; float: right; margin-right: 15px;">
                                  reset chart
                              </a>
                              <span class='reset'
                                    style='visibility: hidden; float: right; margin-right: 15px; font-style: italic;'>
                                  Current filter: <span class='filter'></span>
                              </span>
                              <div class="clearfix"></div> <!-- Use it when using the reset class for IE -->
                          </div>
                    </div>
                    <!-- Range Chart -->
                    <div class="row">
                        <div id="volume-chart" style="width:100%;"></div>
                        <p class="muted pull-right" style="margin-right: 15px;"><i>select a time range to zoom in</i></p>
                    </div>
                </div>
            </div>
        </div> <!-- End of "col-sm-12" grid -->

    </div> <!-- End of first row -->

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

Upvotes: 2

Views: 934

Answers (1)

Bertone
Bertone

Reputation: 756

I managed to fix the main issue that was on the reduce functions.

The solution is to just use these reduce functions:

// Custom reduce functions
function dash_reduceAdd(p, v) {
    ++p.count;
    p.conSum += v.consPow;
    p.prodSum += v.prodPow;
    p.consAvg = p.conSum/p.count;
    p.prodAvg = p.prodSum/p.count;
    return p;
}
function dash_reduceSub(p, v) {
    --p.count;
    p.conSum -= v.consPow;
    p.prodSum -= v.prodPow;
    p.consAvg = p.count ? p.conSum / p.count : 0;
    p.prodAvg = p.count ? p.prodSum / p.count : 0;
    return p;
}
function dash_reduceInit() {
    return { count:0, conSum:0, prodSum:0, consAvg:0, prodAvg:0 };
}

Use a unique grouped dimension for "stackChart" and "volumeChart". like this:

powByTime =
        dateDim
            .group(function (d) { return gran[0](d); })
            .reduce(dash_reduceAdd, dash_reduceSub, dash_reduceInit);

Inside the "building" of the stackChart the value accessors for consumed and produced like this:

stackChart.valueAccessor(function(d) { return d.value.conSum; });

and this:

stackChart.stack(powByTime, "Produced Power [kW]", function(d) { return d.value.prodSum; })

And finally just select in the valueAccessor like this:

// Map the selected mode to the correct valueAccessor value
var accessors = {
    sum: {consPow: 'conSum', prodPow: 'prodSum'},
    avg: {consPow: 'consAvg', prodPow: 'prodAvg'}
};

// Listen for changes on the aggregation mode and update the valueAccessor
d3.selectAll('#select-operation input')
    .on('click', function() {
        var aggrMode = this.value;
        stackChart.valueAccessor(function(d) { var sel = accessors[aggrMode]['consPow']; return d.value[sel]; });
        dc.redrawAll();
    });

Now this works for the problem I asked, but if you are reusing this (that's why I posted the solution), please note that this introduces other issues:

  • I can't access/modify the value accessor of the ".stack" layer...I only managed to add new layer till now...
  • When hovering with the mouse over a point in the chart, the value for "consumed" (base layer) is correct but the one for the production (stacked layer) is wrong (it displays the value of the "consumed power").

I didn't figure out yet how to solve them, I will open another thread in case or post the full solution in the future if I manage. Hope this can help someone else.

Upvotes: 1

Related Questions