1

Say I have the following:

var sales = [5, 4, 2];
var months = ["Jan 2011", "Apr 2011", "Feb 2012"];

If I had a given range say:

var range = ["Jan 2011", "Mar 2012"];

I want to "interpolate" between months such that I can get a result of:

var sales = [5, 4, 4, 4, ..., 2, 2];
var months = ["Jan 2011", "Feb 2011", "Mar 2011", ...., "Feb 2012", "Mar 2012"];

Is it necessary to make a "range of all possible dates in sequential order" array with all the "months + year" that is desired to be included? Or is it possible with javascript to not have to do that? How to accomplish this?

NOTE: the assumption is that if a date isnt present in the months array, it would be filled in with data from any previous available month.

Joe
  • 46,419
  • 33
  • 155
  • 245
Rolando
  • 58,640
  • 98
  • 266
  • 407
  • Is the assumption that if a date isn't present in the `months` array that it had the same value for sales as the previous month? – atkretsch Mar 07 '13 at 22:44
  • Yes, the assumption is that if a date isnt present in the months array, it would be filled in with data from any previous available month. – Rolando Mar 08 '13 at 06:25
  • Are you using actual Date objects in your `months` array? If so (or if it's possible to use Dates), then you should be able to iterate through the months array, keep track of the index, and increment the date by 1 month until you reach the value at the next index, and populate the "result" sales array with the "input" sales value at the previous index. See http://stackoverflow.com/questions/5645058/how-to-add-months-to-a-date-in-javascript for adding months to dates in javascript – atkretsch Mar 08 '13 at 16:59
  • They could be actual date objects to come up with the strings stored in those arrays. – Rolando Mar 08 '13 at 18:20
  • If at all possible, I'd recommend using the actual Date objects instead of strings, since the Date object exposes ways to do date arithmetic (which I think is essentially what you need here); doing the same on strings is obviously harder. If the end result has to be string representations, then I'd recommend doing the conversion to string AFTER you've done your interpolation (i.e. at the presentation layer) – atkretsch Mar 08 '13 at 18:53
  • I would not do something like make gaps in your data lists expressive to assumptions ("no month? take last month!"). This is a mistake. You also really don't *need* two variables... as you could do for instance http://jsfiddle.net/userdude/J3kj9/ Also consider sending dates in format `YYYYMMDD` or `YYYY-MM-DD` or 'YYYYMMM', which can then easily be manipulated into a string value but always parse the same and naturally sort. – Jared Farrish Mar 08 '13 at 18:53
  • possible duplicate of [How to add in zero values into a time series in d3.js / JavaScript](http://stackoverflow.com/questions/23227991/how-to-add-in-zero-values-into-a-time-series-in-d3-js-javascript) – dmvianna Aug 01 '15 at 12:10

1 Answers1

2

If you don't strictly need to use the previous known sales figure, you can use a nice kind of spline interpolation for the sales figures:

/* Fritsch-Carlson monotone cubic spline interpolation
   Usage example:
    var f = createInterpolant([0, 1, 2, 3], [0, 1, 4, 9]);
    var message = '';
    for (var x = 0; x <= 3; x += 0.5) {
        var xSquared = f(x);
        message += x + ' squared is about ' + xSquared + '\n';
    }
    alert(message);
*/
var createInterpolant = function(xs, ys) {
    var i, length = xs.length;

    // Deal with length issues
    if (length != ys.length) { throw 'Need an equal count of xs and ys.'; }
    if (length === 0) { return function(x) { return 0; }; }
    if (length === 1) {
        // Impl: Precomputing the result prevents problems if ys is mutated later and allows garbage collection of ys
        // Impl: Unary plus properly converts values to numbers
        var result = +ys[0];
        return function(x) { return result; };
    }

    // Rearrange xs and ys so that xs is sorted
    var indexes = [];
    for (i = 0; i < length; i++) { indexes.push(i); }
    indexes.sort(function(a, b) { return xs[a] < xs[b] ? -1 : 1; });
    var oldXs = xs, oldYs = ys;
    // Impl: Creating new arrays also prevents problems if the input arrays are mutated later
    xs = []; ys = [];
    // Impl: Unary plus properly converts values to numbers
    for (i = 0; i < length; i++) { xs.push(+oldXs[indexes[i]]); ys.push(+oldYs[indexes[i]]); }

    // Get consecutive differences and slopes
    var dys = [], dxs = [], ms = [];
    for (i = 0; i < length - 1; i++) {
        var dx = xs[i + 1] - xs[i], dy = ys[i + 1] - ys[i];
        dxs.push(dx); dys.push(dy); ms.push(dy/dx);
    }

    // Get degree-1 coefficients
    var c1s = [ms[0]];
    for (i = 0; i < dxs.length - 1; i++) {
        var m = ms[i], mNext = ms[i + 1];
        if (m*mNext <= 0) {
            c1s.push(0);
        } else {
            var dx = dxs[i], dxNext = dxs[i + 1], common = dx + dxNext;
            c1s.push(3*common/((common + dxNext)/m + (common + dx)/mNext));
        }
    }
    c1s.push(ms[ms.length - 1]);

    // Get degree-2 and degree-3 coefficients
    var c2s = [], c3s = [];
    for (i = 0; i < c1s.length - 1; i++) {
        var c1 = c1s[i], m = ms[i], invDx = 1/dxs[i], common = c1 + c1s[i + 1] - m - m;
        c2s.push((m - c1 - common)*invDx); c3s.push(common*invDx*invDx);
    }

    // Return interpolant function
    return function(x) {
        // The rightmost point in the dataset should give an exact result
        var i = xs.length - 1;
        if (x == xs[i]) { return ys[i]; }

        // Search for the interval x is in, returning the corresponding y if x is one of the original xs
        var low = 0, mid, high = c3s.length - 1;
        while (low <= high) {
            mid = Math.floor(0.5*(low + high));
            var xHere = xs[mid];
            if (xHere < x) { low = mid + 1; }
            else if (xHere > x) { high = mid - 1; }
            else { return ys[mid]; }
        }
        i = Math.max(0, high);

        // Interpolate
        var diff = x - xs[i], diffSq = diff*diff;
        return ys[i] + c1s[i]*diff + c2s[i]*diffSq + c3s[i]*diff*diffSq;
    };
};

That can be used with the following code to do what you wanted:

var monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
var fromMonthNumber = function(monthNumber) {
    return monthNames[monthNumber % 12] + ' ' + ((monthNumber / 12) | 0);
};

var toMonthNumber = function(monthName) {
    var date = new Date(Date.parse(monthName));
    return 12*date.getFullYear() + date.getMonth();
};

var interpolateSales = function(sales, months, range) {
    var f = createInterpolant(months.map(toMonthNumber), sales);

    var resultSales = [], resultMonths = [];
    var firstMonth = toMonthNumber(range[0]), lastMonth = toMonthNumber(range[1]);
    for (var x = firstMonth; x <= lastMonth; x++) {
        resultSales.push(Math.round(f(x)));
        resultMonths.push(fromMonthNumber(x));
    }

    return { sales: resultSales, months: resultMonths };
};

With that, interpolateSales([5, 4, 2], ["Jan 2011", "Apr 2011", "Feb 2012"], ["Jan 2011", "Mar 2012"]) is:

{
    sales: [5, 5, 4, 4, 4, 4, 3, 3, 3, 3, 3, 2, 2, 2, 2],
    months: ["Jan 2011", "Feb 2011", "Mar 2011", "Apr 2011", "May 2011", "Jun 2011", "Jul 2011", "Aug 2011", "Sep 2011", "Oct 2011", "Nov 2011", "Dec 2011", "Jan 2012", "Feb 2012", "Mar 2012"]
}
Olathe
  • 1,885
  • 1
  • 15
  • 23