UNPKG

lumenize

Version:

Illuminating the forest AND the trees in your data.

369 lines (327 loc) 15.8 kB
// Generated by CoffeeScript 1.10.0 (function() { var OLAPCube, Time, Timeline, TransitionsCalculator, ref, utils; OLAPCube = require('./OLAPCube').OLAPCube; ref = require('tztime'), utils = ref.utils, Time = ref.Time, Timeline = ref.Timeline; TransitionsCalculator = (function() { /* @class TransitionsCalculator Used to accumlate counts and sums about transitions. Let's say that you want to create a throughput or velocity chart where each column on the chart represents the number of work items that cross over from one state into another state in a given month/week/quarter/etc. You would send a transitions to a temporal data store like Rally's Lookback API specifying both the current values and the previous values. For instance, the work items crossing from "In Progress" to "Completed" could be found with this query clause `"_PreviousValues.ScheduleState": {"$lte": "In Progress"}, "ScheduleState": {"$gt": "In Progress"}` {TransitionsCalculator, Time} = require('../') snapshots = [ { id: 1, from: '2011-01-03T00:00:00.000Z', PlanEstimate: 10 }, { id: 1, from: '2011-01-05T00:00:00.000Z', PlanEstimate: 10 }, { id: 2, from: '2011-01-04T00:00:00.000Z', PlanEstimate: 20 }, { id: 3, from: '2011-01-10T00:00:00.000Z', PlanEstimate: 30 }, { id: 4, from: '2011-01-11T00:00:00.000Z', PlanEstimate: 40 }, { id: 5, from: '2011-01-17T00:00:00.000Z', PlanEstimate: 50 }, { id: 6, from: '2011-02-07T00:00:00.000Z', PlanEstimate: 60 }, { id: 7, from: '2011-02-08T00:00:00.000Z', PlanEstimate: 70 }, ] But that's not the entire story. What if something crosses over into "Completed" and beyond but crosses back. It could do this several times and get counted multiple times. That would be bad. The way we deal with this is to also look for the list of snapshots that pass backwards across the boundary and subract thier impact on the final calculations. One can think of alternative aproaches for avoiding this double counting. You could, for instance, only count the last transition for each unique work item. The problem with this approach is that the backward moving transition might occur in a different time period from the forward moving one. A later snapshot could invalidate an earlier calculation which is bad for incremental calculation and caching. To complicate matters, the field values being summed by the calculator might have changed between subsequent forward/backward transitions. The chosen algorithm is the only way I know to preserve the idempotency and cachable incremental calculation properties. snapshotsToSubtract = [ { id: 1, from: '2011-01-04T00:00:00.000Z', PlanEstimate: 10 }, { id: 7, from: '2011-02-09T00:00:00.000Z', PlanEstimate: 70 }, ] The calculator will keep track of the count of items automatically (think throughput), but if you want to sum up a particular field (think velocity), you can specify that with the 'fieldsToSum' config property. fieldsToSum = ['PlanEstimate'] Now let's build our config object. config = asOf: '2011-02-10' # Leave this off if you want it to continuously update to today granularity: Time.MONTH tz: 'America/Chicago' validFromField: 'from' validToField: 'to' uniqueIDField: 'id' fieldsToSum: fieldsToSum asterixToDateTimePeriod: true # Set to false or leave off if you are going to reformat the timePeriod In most cases, you'll want to leave off the `asOf` configuration property so the data can be continuously updated with new snapshots as they come in. We include it in this example so the output stays stable. If we hadn't, then the rows would continue to grow to encompass today. startOn = '2011-01-02T00:00:00.000Z' endBefore = '2011-02-27T00:00:00.000Z' calculator = new TransitionsCalculator(config) calculator.addSnapshots(snapshots, startOn, endBefore, snapshotsToSubtract) console.log(calculator.getResults()) * [ { timePeriod: '2011-01', count: 5, PlanEstimate: 150 }, * { timePeriod: '2011-02*', count: 1, PlanEstimate: 60 } ] The asterix on the last row in the results is to indicate that it is a to-date value. As more snapshots come in, this last row will change. The caching and incremental calcuation capability of this Calculator are designed to take this into account. Now, let's use the same data but aggregate in granularity of weeks. config.granularity = Time.WEEK calculator = new TransitionsCalculator(config) calculator.addSnapshots(snapshots, startOn, endBefore, snapshotsToSubtract) console.log(calculator.getResults()) * [ { timePeriod: '2010W52', count: 1, PlanEstimate: 10 }, * { timePeriod: '2011W01', count: 2, PlanEstimate: 50 }, * { timePeriod: '2011W02', count: 2, PlanEstimate: 90 }, * { timePeriod: '2011W03', count: 0, PlanEstimate: 0 }, * { timePeriod: '2011W04', count: 0, PlanEstimate: 0 }, * { timePeriod: '2011W05', count: 1, PlanEstimate: 60 }, * { timePeriod: '2011W06*', count: 0, PlanEstimate: 0 } ] Remember, you can easily convert weeks to other granularities for display. weekStartingLabel = 'week starting ' + new Time('2010W52').inGranularity(Time.DAY).toString() console.log(weekStartingLabel) * week starting 2010-12-27 If you want to display spinners while the chart is rendering, you can read this calculator's upToDateISOString property and compare it directly to the getResults() row's timePeriod property using code like this. Yes, this works eventhough upToDateISOString is an ISOString. row = {timePeriod: '2011W07'} if calculator.upToDateISOString < row.timePeriod console.log("#{row.timePeriod} not yet calculated.") * 2011W07 not yet calculated. */ function TransitionsCalculator(config) { /* @constructor @param {Object} config @cfg {String} tz The timezone for analysis in the form like `America/New_York` @cfg {String} [validFromField = "_ValidFrom"] @cfg {String} [validToField = "_ValidTo"] @cfg {String} [uniqueIDField = "_EntityID"] Not used right now but when drill-down is added it will be @cfg {String} granularity 'month', 'week', 'quarter', etc. Use Time.MONTH, Time.WEEK, etc. @cfg {String[]} [fieldsToSum=[]] It will track the count automatically but it can keep a running sum of other fields also @cfg {Boolean} [asterixToDateTimePeriod=false] If set to true, then the still-in-progress last time period will be asterixed */ var cubeConfig, dimensions, f, i, len, metrics, ref1, ref2; this.config = utils.clone(config); if (this.config.validFromField == null) { this.config.validFromField = "_ValidFrom"; } if (this.config.validToField == null) { this.config.validToField = "_ValidTo"; } if (this.config.uniqueIDField == null) { this.config.uniqueIDField = "_EntityID"; } if (this.config.fieldsToSum == null) { this.config.fieldsToSum = []; } if (this.config.asterixToDateTimePeriod == null) { this.config.asterixToDateTimePeriod = false; } utils.assert(this.config.tz != null, "Must provide a timezone to this calculator."); utils.assert(this.config.granularity != null, "Must provide a granularity to this calculator."); if ((ref1 = this.config.granularity) === Time.HOUR || ref1 === Time.MINUTE || ref1 === Time.SECOND || ref1 === Time.MILLISECOND) { throw new Error("Transitions calculator is not designed to work on granularities finer than 'day'"); } dimensions = [ { field: 'timePeriod' } ]; metrics = [ { field: 'count', f: 'sum' } ]; ref2 = this.config.fieldsToSum; for (i = 0, len = ref2.length; i < len; i++) { f = ref2[i]; metrics.push({ field: f, f: 'sum' }); } cubeConfig = { dimensions: dimensions, metrics: metrics }; this.cube = new OLAPCube(cubeConfig); this.upToDateISOString = null; this.lowestTimePeriod = null; if (this.config.asOf != null) { this.maxTimeString = new Time(this.config.asOf, Time.MILLISECOND).getISOStringInTZ(this.config.tz); } else { this.maxTimeString = Time.getISOStringFromJSDate(); } this.virgin = true; } TransitionsCalculator.prototype.addSnapshots = function(snapshots, startOn, endBefore, snapshotsToSubtract) { var filteredSnapshots, filteredSnapshotsToSubstract, startOnString; if (snapshotsToSubtract == null) { snapshotsToSubtract = []; } /* @method addSnapshots Allows you to incrementally add snapshots to this calculator. @chainable @param {Object[]} snapshots An array of temporal data model snapshots. @param {String} startOn A ISOString (e.g. '2012-01-01T12:34:56.789Z') indicating the time start of the period of interest. On the second through nth call, this should equal the previous endBefore. @param {String} endBefore A ISOString (e.g. '2012-01-01T12:34:56.789Z') indicating the moment just past the time period of interest. @return {TransitionsCalculator} */ if (this.upToDateISOString != null) { utils.assert(this.upToDateISOString === startOn, "startOn (" + startOn + ") parameter should equal endBefore of previous call (" + this.upToDateISOString + ") to addSnapshots."); } this.upToDateISOString = endBefore; startOnString = new Time(startOn, this.config.granularity, this.config.tz).toString(); if (this.lowestTimePeriod != null) { if (startOnString < this.lowestTimePeriod) { this.lowestTimePeriod = startOnString; } } else { this.lowestTimePeriod = startOnString; } filteredSnapshots = this._filterSnapshots(snapshots); this.cube.addFacts(filteredSnapshots); filteredSnapshotsToSubstract = this._filterSnapshots(snapshotsToSubtract, -1); this.cube.addFacts(filteredSnapshotsToSubstract); this.virgin = false; return this; }; TransitionsCalculator.prototype._filterSnapshots = function(snapshots, sign) { var f, filteredSnapshots, fs, i, j, len, len1, ref1, s; if (sign == null) { sign = 1; } filteredSnapshots = []; for (i = 0, len = snapshots.length; i < len; i++) { s = snapshots[i]; if (s[this.config.validFromField] <= this.maxTimeString) { if (s.count != null) { throw new Error('Snapshots passed into a TransitionsCalculator cannot have a `count` field.'); } if (s.timePeriod != null) { throw new Error('Snapshots passed into a TransitionsCalculator cannot have a `timePeriod` field.'); } fs = utils.clone(s); fs.count = sign * 1; fs.timePeriod = new Time(s[this.config.validFromField], this.config.granularity, this.config.tz).toString(); ref1 = this.config.fieldsToSum; for (j = 0, len1 = ref1.length; j < len1; j++) { f = ref1[j]; fs[f] = sign * s[f]; } filteredSnapshots.push(fs); } } return filteredSnapshots; }; TransitionsCalculator.prototype.getResults = function() { /* @method getResults Returns the current state of the calculator @return {Object[]} Returns an Array of Maps like `{timePeriod: '2012-12', count: 10, otherField: 34}` */ var cell, config, f, filter, i, j, k, len, len1, len2, out, outRow, ref1, ref2, t, timeLine, timePeriods, tp; if (this.virgin) { return []; } out = []; this.highestTimePeriod = new Time(this.maxTimeString, this.config.granularity, this.config.tz).toString(); config = { startOn: this.lowestTimePeriod, endBefore: this.highestTimePeriod, granularity: this.config.granularity }; timeLine = new Timeline(config); timePeriods = (function() { var i, len, ref1, results; ref1 = timeLine.getAllRaw(); results = []; for (i = 0, len = ref1.length; i < len; i++) { t = ref1[i]; results.push(t.toString()); } return results; })(); timePeriods.push(this.highestTimePeriod); for (i = 0, len = timePeriods.length; i < len; i++) { tp = timePeriods[i]; filter = {}; filter['timePeriod'] = tp; cell = this.cube.getCell(filter); outRow = {}; outRow.timePeriod = tp; if (cell != null) { outRow.count = cell.count_sum; ref1 = this.config.fieldsToSum; for (j = 0, len1 = ref1.length; j < len1; j++) { f = ref1[j]; outRow[f] = cell[f + '_sum']; } } else { outRow.count = 0; ref2 = this.config.fieldsToSum; for (k = 0, len2 = ref2.length; k < len2; k++) { f = ref2[k]; outRow[f] = 0; } } out.push(outRow); } if (this.config.asterixToDateTimePeriod) { out[out.length - 1].timePeriod += '*'; } return out; }; TransitionsCalculator.prototype.getStateForSaving = function(meta) { /* @method getStateForSaving Enables saving the state of this calculator. See TimeInStateCalculator documentation for a detailed example. @param {Object} [meta] An optional parameter that will be added to the serialized output and added to the meta field within the deserialized calculator. @return {Object} Returns an Ojbect representing the state of the calculator. This Object is suitable for saving to to an object store. Use the static method `newFromSavedState()` with this Object as the parameter to reconstitute the calculator. */ var out; out = { config: this.config, cubeSavedState: this.cube.getStateForSaving(), upToDateISOString: this.upToDateISOString, maxTimeString: this.maxTimeString, lowestTimePeriod: this.lowestTimePeriod, virgin: this.virgin }; if (meta != null) { out.meta = meta; } return out; }; TransitionsCalculator.newFromSavedState = function(p) { /* @method newFromSavedState Deserializes a previously saved calculator and returns a new calculator. See TimeInStateCalculator for a detailed example. @static @param {String/Object} p A String or Object from a previously saved state @return {TransitionsCalculator} */ var calculator; if (utils.type(p) === 'string') { p = JSON.parse(p); } calculator = new TransitionsCalculator(p.config); calculator.cube = OLAPCube.newFromSavedState(p.cubeSavedState); calculator.upToDateISOString = p.upToDateISOString; calculator.maxTimeString = p.maxTimeString; calculator.lowestTimePeriod = p.lowestTimePeriod; calculator.virgin = p.virgin; if (p.meta != null) { calculator.meta = p.meta; } return calculator; }; return TransitionsCalculator; })(); exports.TransitionsCalculator = TransitionsCalculator; }).call(this);