Skip to content

crossfilter.ma is a crossfilter group modifier (add-on?) to calculate moving averages and percentage change.

Notifications You must be signed in to change notification settings

r4j4h/crossfilter-ma

Folders and files

NameName
Last commit message
Last commit date

Latest commit

c341274 · Jan 26, 2016

History

66 Commits
Jan 9, 2015
Jan 16, 2015
Jan 20, 2015
Jan 9, 2015
Jan 9, 2015
Jan 9, 2015
Jan 26, 2016
Jan 17, 2015
Jan 14, 2015
Jan 20, 2015
Jan 20, 2015
Jan 20, 2015
Jan 9, 2015
Jan 26, 2016

Repository files navigation

crossfilter.ma

crossfilter.ma is a crossfilter group modifier to calculate moving averages and percent change.

It is intended for time oriented data, but can be used for non-dates just as well as it is powered by JavaScript's native Array.sort to get the keys in order to determine previous nodes for the calculations.

How to Install

  • Ensure requirements are provided on your page
    • crossfilter
  • Copy crossfilter.ma.js to your scripts directory and provide it on your page
  • Use it!

How to Use

Index

Calculating a Rolling/Moving Average

// Get reference
var crossfilterMa = crossfilter$ma;             // Both variables work and are identical.
var crossfilterMa = window['crossfilter-ma'];   // Providing both b/c GitHub naming and convenience impedence mismatch.

// Replace with your data
var data = [
    { date: "2012-01-11", visits: 2  }, // 2 point  | 3 point
    { date: "2012-01-12", visits: 3  }, // 2.5      | null
    { date: "2012-01-13", visits: 10 }, // 6.5      | 5
    { date: "2012-01-14", visits: 3  }, // 6.5      | 5.333
    { date: "2012-01-15", visits: 10 }, // 6.5      | 7.666
    { date: "2012-01-16", visits: 12 }, // 11       | 8.333
    { date: "2012-01-17", visits: 7  }  // 9.5      | 9.666
];
// Build crossfilter on it
var data = crossfilter(data);
// Build crossfilter dimension
var dim = data.dimension(function (d) {
    return d.date
});
// Build crossfilter group
var group = dim.group().reduceSum( function(d) { return d.visits; } );



// Get a 3 day moving average on group
var rollingAverageFakeGroup = crossfilterMa.accumulateGroupForNDayMovingAverage( group, 3 );

// Get results with current crossfilter filtering applied
var resultsWithRollingAverages = rollingAverageFakeGroup.all();

// Use result
/*
expect( resultsWithRollingAverages[0].key ).toBe( '2012-01-11' );
expect( resultsWithRollingAverages[0].value ).toBe( 2 );
expect( resultsWithRollingAverages[0].rollingAverage ).toBe( 0 ); // 2 by itself is not a rolling average

expect( resultsWithRollingAverages[1].key ).toBe( '2012-01-12' );
expect( resultsWithRollingAverages[1].value ).toBe( 3 );
expect( resultsWithRollingAverages[1].rollingAverage ).toBe( 0 ); // 2.5 is a 2 day average, not enough data for 3

expect( resultsWithRollingAverages[2].key ).toBe( '2012-01-13' );
expect( resultsWithRollingAverages[2].value ).toBe( 10 );
expect( resultsWithRollingAverages[2].rollingAverage ).toBe( 5 ); // And here we go

expect( resultsWithRollingAverages[3].key ).toBe( '2012-01-14' );
expect( resultsWithRollingAverages[3].value ).toBe( 3 );
expect( resultsWithRollingAverages[3].rollingAverage ).toBe( 5.333333333333333 );

expect( resultsWithRollingAverages[4].key ).toBe( '2012-01-15' );
expect( resultsWithRollingAverages[4].value ).toBe( 10 );
expect( resultsWithRollingAverages[4].rollingAverage ).toBe( 7.666666666666667 );

expect( resultsWithRollingAverages[5].key ).toBe( '2012-01-15' );
expect( resultsWithRollingAverages[5].value ).toBe( 12 );
expect( resultsWithRollingAverages[5].rollingAverage ).toBe( 8.333333333333334 );

expect( resultsWithRollingAverages[6].key ).toBe( '2012-01-15' );
expect( resultsWithRollingAverages[6].value ).toBe( 7 );
expect( resultsWithRollingAverages[6].rollingAverage ).toBe( 9.666666666666666 );
*/


// Supports changing the number of days
rollingAverageFakeGroup.ndays(2);

// Get results with current crossfilter filtering applied
var resultsWithRollingAverages = rollingAverageFakeGroup.all();

// Use result
/*
expect( resultsWithRollingAverages[0].key ).toBe( '2012-01-11' );
expect( resultsWithRollingAverages[0].value ).toBe( 2 );
expect( resultsWithRollingAverages[0].rollingAverage ).toBe( 0 ); // 2 by itself is not a rolling average

expect( resultsWithRollingAverages[1].key ).toBe( '2012-01-12' );
expect( resultsWithRollingAverages[1].value ).toBe( 3 );
expect( resultsWithRollingAverages[1].rollingAverage ).toBe( 2.5 );
*/


rollingAverageFakeGroup.ndays(3); // (resetting back to 3 for sake of example)



// Supports rolldown
rollingAverageFakeGroup.rolldown(true);

// Get results with current crossfilter filtering applied
var resultsWithRollingAverages = rollingAverageFakeGroup.all();

// Now our result includes averages on data points where a 3 day average is not possible:
/*
expect( resultsWithRollingAverages[0].key ).toBe( '2012-01-11' );
expect( resultsWithRollingAverages[0].value ).toBe( 2 );
expect( resultsWithRollingAverages[0].rollingAverage ).toBe( 2 ); // This averages with itself now

expect( resultsWithRollingAverages[1].key ).toBe( '2012-01-12' );
expect( resultsWithRollingAverages[1].value ).toBe( 3 );
expect( resultsWithRollingAverages[1].rollingAverage ).toBe( 2.5 ); // This is a 2 day average

expect( resultsWithRollingAverages[2].key ).toBe( '2012-01-13' );
expect( resultsWithRollingAverages[2].value ).toBe( 10 );
expect( resultsWithRollingAverages[2].rollingAverage ).toBe( 5 ); // And we proceed with our 3 days...
expect( resultsWithRollingAverages[3].rollingAverage ).toBeCloseTo( 5.333333333333333 );
*/


// Supports a debug mode to see the values used in calculations
rollingAverageFakeGroup._debug(true);

// Regenerate results
var resultsWithRollingAverages = rollingAverageFakeGroup.all();

// Now our results include debugging information..
/*
expect( resultsWithRollingAverages[0]._debug ).toBeDefined();
*/

/*
resultsWithRollingAverages[2] = {
    "key": "2012-01-13",
    "value": 10,
    "rollingAverage": 5,
    "_debug": {
        "cumulate": 15,
        "datumsUsed": [
            {
                "key": "2012-01-11",
                "value": 2
            },
            {
                "key": "2012-01-12",
                "value": 3
            },
            {
                "key": "2012-01-13",
                "value": 10
            }
        ]
    }
}
*/



// We can also sort by moving average
rollingAverageFakeGroup.orderByMovingAverage( 1 ); // Ascending
rollingAverageFakeGroup.orderByMovingAverage( -1 ); // Descending

resultsWithRollingAverages = rollingAverageFakeGroup.all();

/*
expect( resultsWithRollingAverages[ 0 ].rollingAverage ).toBeCloseTo( 9.66666 );
*/

Back to Top

Calculating Percent Change

// Get reference
var crossfilterMa = crossfilter$ma;             // Both variables work and are identical.
var crossfilterMa = window['crossfilter-ma'];   // Providing both b/c GitHub naming and convenience impedence mismatch.

// Replace with your data
var data = [
    { date: "2012-01-11", visits: 2  },
    { date: "2012-01-12", visits: 3  },
    { date: "2012-01-13", visits: 10 },
    { date: "2012-01-14", visits: 3  },
    { date: "2012-01-15", visits: 10 },
    { date: "2012-01-16", visits: 12 },
    { date: "2012-01-17", visits: 7  }
];
// Build crossfilter on it
var data = crossfilter(data);
// Build crossfilter dimension
var dim = data.dimension(function (d) {
    return d.date
});
// Build crossfilter group
var group = dim.group().reduceSum( function(d) { return d.visits; } );



// Get values with Percent Change
var pc = crossfilterMa.accumulateGroupForPercentageChange( group );

// Get results with current crossfilter filtering applied
var results = pc.all();




// Use result
/*
expect( results[0].key ).toBe( '2012-01-11' );
expect( results[0].value ).toBe( 2 );
expect( results[0].percentageChange ).toBe( 0 );
expect( results[0]._debug.thisDayKey ).toBe( '2012-01-11' );
expect( results[0]._debug.prevDayKey ).toBe( 'None' );

expect( results[1].key ).toBe( '2012-01-12' );
expect( results[1].value ).toBe( 3 );
expect( results[1].percentageChange ).toBe( 50 );
expect( results[1]._debug.thisDayKey ).toBe( '2012-01-12' );
expect( results[1]._debug.prevDayKey ).toBe( '2012-01-11' );

expect( results[2].key ).toBe( '2012-01-13' );
expect( results[2].value ).toBe( 10 );
expect( results[2].percentageChange ).toBe( 233.33333333333334 );
expect( results[2]._debug.thisDayKey ).toBe( '2012-01-13' );
expect( results[2]._debug.prevDayKey ).toBe( '2012-01-12' );

expect( results[3].key ).toBe( '2012-01-14' );
expect( results[3].value ).toBe( 10 );
expect( results[3].percentageChange ).toBe( -70 );
expect( results[3]._debug.thisDayKey ).toBe( '2012-01-14' );
expect( results[3]._debug.prevDayKey ).toBe( '2012-01-13' );

expect( results[4].key ).toBe( '2012-01-15' );
expect( results[4].value ).toBe( 10 );
expect( results[4].percentageChange ).toBe( 233.33333333333334 );

expect( results[5].key ).toBe( '2012-01-16' );
expect( results[5].value ).toBe( 12 );
expect( results[5].percentageChange ).toBe( 20 );

expect( results[6].key ).toBe( '2012-01-17' );
expect( results[6].value ).toBe( 7 );
expect( results[6].percentageChange ).toBe( -41.66666666666667 );
*/



// Supports debug mode
// var pc = crossfilterMa.accumulateGroupForPercentageChange( group, true ); // Enabling via constructor
pc._debug( true ); // Enabling via method

// Regenerate results w/ debug mode on
var results = pc.all();

/*
results = [
    {
        "key": "2012-01-11",
        "value": 2,
        "percentageChange": 0,
        "_debug": {
            "thisDayKey": "2012-01-11",
            "thisDayValue": 2,
            "prevDayKey": "None",
            "prevDayValue": "None"
        }
    },
    {
        "key": "2012-01-11",
        "value": 3,
        "percentageChange": 50,
        "_debug": {
            "thisDayKey": "2012-01-12",
            "thisDayValue": 3,
            "prevDayKey": "2012-01-11",
            "prevDayValue": 2
        }
    },
    ...
]
*/



// We can also sort by percentage change
pc.orderByPercentageChange( 1 ); // Ascending
pc.orderByPercentageChange( -1 ); // Descending

resultsAll = pc.all();

/*
expect( resultsAll[ 0 ].percentageChange ).toBe( 233.33333333333334 );
*/

Back to Top

Complex Data / Grouping

Custom Key/Value accessors

// Get reference
var crossfilterMa = crossfilter$ma;             // Both variables work and are identical.
var crossfilterMa = window['crossfilter-ma'];   // Providing both b/c GitHub naming and convenience impedence mismatch.

// Prepare more complex data
setOfNumbers = [
    { date: "2012-01-11", visits: 2,  place: "A", territory: "A" },
    { date: "2012-01-12", visits: 3,  place: "B", territory: "A" },
    { date: "2012-01-13", visits: 10, place: "A", territory: "B" },
    { date: "2012-01-11", visits: 3,  place: "C", territory: "B" },
    { date: "2012-01-15", visits: 10, place: "A", territory: "A" },
    { date: "2012-01-12", visits: 12, place: "B", territory: "A" },
    { date: "2012-01-13", visits: 7,  place: "A", territory: "B" }
];

// Crossfilter it
crossfilterInstance = crossfilter( setOfNumbers );

dimensionDate = crossfilterInstance.dimension(function (d) {
    return d.date
});

// Since we are missing entries for some data points on some days (place C is only present on the 11th)
// We want to fill in everyone with 0, so let's gather the dimensions we want to do this on.
var dimensionPlaces = crossfilterInstance.dimension(function (d) {
    return d.place
});
var dimensionTerritories = crossfilterInstance.dimension(function (d) {
    return d.territory
});
var knownPlaces = dimensionPlaces.group().all().map( function(d) { return d.key; } );
var knownTerritories = dimensionTerritories.group().all().map( function(d) { return d.key; } );
groupVisitsByPlaceAndTerritoryByDate = dimensionDate.group().reduce(
    function ( p, v ) {
        p.totalVisits += v.visits;

        if ( p.places[ v.place ] ) {
            p.places[ v.place ].visits += v.visits;
        } else {
            p.places[ v.place ] = {
                visits: v.visits
            };
        }

        if ( p.territories[ v.territory ] ) {
            p.territories[ v.territory ].visits += v.visits;
        } else {
            p.territories[ v.territory ] = {
                visits: v.visits
            };
        }
        return p;
    },
    function ( p, v ) {
        p.totalVisits -= v.visits;

        if ( p.places[ v.place ] ) {
            p.places[ v.place ].visits -= v.visits;
        } else {
            delete p.places[ v.place ];
        }

        if ( p.territories[ v.territory ] ) {
            p.territories[ v.territory ].visits -= v.visits;
        } else {
            delete p.territories[ v.territory ];
        }
        return p;
    },
    function () {
        var obj = {
            totalVisits: 0,
            places     : {},
            territories: {}
        };

        // Make sure each place is represented, with at least 0
        var t = knownPlaces.length,
            i = -1;
        while ( ++i < t ) {
            obj.places[ knownPlaces[i] ] = {
                visits: 0
            };
        }
        // Make sure each territory is represented, with at least 0
        var t = knownTerritories.length,
            i = -1;
        while ( ++i < t ) {
            obj.territories[ knownTerritories[i] ] = {
                visits: 0
            };
        }

        return obj;
    }
);

// Now let's use that group with crossfilter$ma
percentageChangeGroup = crossfilterMa.accumulateGroupForPercentageChange( groupVisitsByPlaceAndTerritoryByDate );
percentageChangeGroup._debug(true);

var resultsAll = percentageChangeGroup.all();

// Our reduce function's `value` is no longer a primitive, but an object, so this is going to mess up...
/*
expect( resultsAll[ 0 ].percentageChange ).toBe( 0 );
expect( resultsAll[ 0 ].percentageChange ).not.toBe( 1 );
expect( resultsAll[ 1 ].percentageChange ).toBeNaN();
expect( resultsAll[ 1 ].percentageChange ).not.toBe( 200 );
expect( resultsAll[ 2 ].percentageChange ).toBeNaN();
expect( resultsAll[ 2 ].percentageChange ).not.toBe( 13.33 );
expect( resultsAll[ 3 ].percentageChange ).toBeNaN();
expect( resultsAll[ 3 ].percentageChange ).not.toBe( 41.18 );
*/

// Let's inform crossfilter$ma to look deeper into that object for the totalVisits property
percentageChangeGroup.valueAccessor( function(d) { return d.value.totalVisits; } );
groupVisitsByPlaceAndTerritoryByDate.order( function(d) { return d.totalVisits; } );

resultsAll = percentageChangeGroup.all();

// Now we've got our expected data!
/*
expect( resultsAll[ 0 ].key ).toBe( "2012-01-13" );
expect( resultsAll[ 1 ].key ).toBe( "2012-01-12" );
expect( resultsAll[ 2 ].key ).toBe( "2012-01-15" );
expect( resultsAll[ 3 ].key ).toBe( "2012-01-11" );

expect( resultsAll[ 0 ].value.totalVisits ).toBe( 17 );
expect( resultsAll[ 1 ].value.totalVisits ).toBe( 15 );
expect( resultsAll[ 2 ].value.totalVisits ).toBe( 10 );
expect( resultsAll[ 3 ].value.totalVisits ).toBe( 6 );

expect( resultsAll[ 0 ].percentageChange ).toBeCloseTo( 13.33 );
expect( resultsAll[ 1 ].percentageChange ).toBe( 150 );
expect( resultsAll[ 2 ].percentageChange ).toBeCloseTo( -41.18 );
expect( resultsAll[ 3 ].percentageChange ).toBe( 0 );
*/

// We can also still sort by percentage change
percentageChangeGroup.orderByPercentageChange( 1 ); // Ascending
percentageChangeGroup.orderByPercentageChange( -1 ); // Descending

resultsAll = percentageChangeGroup.all();

/*
expect( resultsAll[ 0 ].percentageChange ).toBe( 200 );
*/

Back to Top

How to Test

Tests can be run via CLI using Jasmine or in the browser.

  • Run grunt test to test in the CLI.
  • Run grunt server and visit http://0.0.0.0:8888/spec/ in your browser to run in browser.

How to Test Code Coverage

  • Run grunt coverage
  • Run grunt server and visit http://0.0.0.0:8888/coverage/jasmine/

Back to Top

Inspired By

Thanks for reading! :o)

About

crossfilter.ma is a crossfilter group modifier (add-on?) to calculate moving averages and percentage change.

Resources

Stars

Watchers

Forks

Packages

No packages published