lumenize
Version:
Illuminating the forest AND the trees in your data.
1,120 lines (1,008 loc) • 46.2 kB
JavaScript
// Generated by CoffeeScript 1.10.0
(function() {
var OLAPCube, arrayOfMaps_To_CSVStyleArray, csvStyleArray_To_ArrayOfMaps, functions, ref, utils;
utils = require('tztime').utils;
functions = require('./functions').functions;
ref = require('../'), arrayOfMaps_To_CSVStyleArray = ref.arrayOfMaps_To_CSVStyleArray, csvStyleArray_To_ArrayOfMaps = ref.csvStyleArray_To_ArrayOfMaps;
OLAPCube = (function() {
/*
@class OLAPCube
__An efficient, in-memory, incrementally-updateable, hierarchy-capable OLAP Cube implementation.__
[OLAP Cubes](http://en.wikipedia.org/wiki/OLAP_cube) are a powerful abstraction that makes it easier to do everything
from simple group-by operations to more complex multi-dimensional and hierarchical analysis. This implementation has
the same conceptual ancestry as implementations found in business intelligence and OLAP database solutions. However,
it is meant as a light weight alternative primarily targeting the goal of making it easier for developers to implement
desired analysis. It also supports serialization and incremental updating so it's ideally
suited for visualizations and analysis that are updated on a periodic or even continuous basis.
## Features ##
* In-memory
* Incrementally-updateable
* Serialize (`getStateForSaving()`) and deserialize (`newFromSavedState()`) to preserve aggregations between sessions
* Accepts simple JavaScript Objects as facts
* Storage and output as simple JavaScript Arrays of Objects
* Hierarchy (trees) derived from fact data assuming [materialized path](http://en.wikipedia.org/wiki/Materialized_path)
array model commonly used with NoSQL databases
## 2D Example ##
Let's walk through a simple 2D example from facts to output. Let's say you have this set of facts:
facts = [
{ProjectHierarchy: [1, 2, 3], Priority: 1, Points: 10},
{ProjectHierarchy: [1, 2, 4], Priority: 2, Points: 5 },
{ProjectHierarchy: [5] , Priority: 1, Points: 17},
{ProjectHierarchy: [1, 2] , Priority: 1, Points: 3 },
]
The ProjectHierarchy field models its hierarchy (tree) as an array containing a
[materialized path](http://en.wikipedia.org/wiki/Materialized_path). The first fact is "in" Project 3 whose parent is
Project 2, whose parent is Project 1. The second fact is "in" Project 4 whose parent is Project 2 which still has
Project 1 as its parent. Project 5 is another root Project like Project 1; and the fourth fact is "in" Project 2.
So the first fact will roll-up the tree and be aggregated against [1], and [1, 2] as well as [1, 2, 3]. Root Project 1
will get the data from all but the third fact which will get aggregated against root Project 5.
We specify the ProjectHierarchy field as a dimension of type 'hierarchy' and the Priorty field as a simple value dimension.
dimensions = [
{field: "ProjectHierarchy", type: 'hierarchy'},
{field: "Priority"}
]
This will create a 2D "cube" where each unique value for ProjectHierarchy and Priority defines a different cell.
Note, this happens to be a 2D "cube" (more commonly referred to as a [pivot table](http://en.wikipedia.org/wiki/Pivot_Table)),
but you can also have a 1D cube (a simple group-by), a 3D cube, or even an n-dimensional hypercube where n is greater than 3.
You can specify any number of metrics to be calculated for each cell in the cube.
metrics = [
{field: "Points", f: "sum", as: "Scope"}
]
You can use any of the aggregation functions found in Lumenize.functions except `count`. The count metric is
automatically tracked for each cell. The `as` specification is optional unless you provide a custom function. If missing,
it will build the name of the resulting metric from the field name and the function. So without the `as: "Scope"` the
second metric in the example above would have been named "Points_sum".
You can also use custom functions in the form of `f(values) -> return <some function of values>`.
Next, we build the config parameter from our dimension and metrics specifications.
config = {dimensions, metrics}
Hierarchy dimensions automatically roll up but you can also tell it to keep all totals by setting config.keepTotals to
true. The totals are then kept in the cells where one or more of the dimension values are set to `null`. Note, you
can also set keepTotals for individual dimension and should probably use that if you have more than a few dimensions
but we're going to set it globally here:
config.keepTotals = true
Now, let's create the cube.
{OLAPCube} = require('../')
cube = new OLAPCube(config, facts)
`getCell()` allows you to extract a single cell. The "total" cell for all facts where Priority = 1 can be found as follows:
console.log(cube.getCell({Priority: 1}))
* { ProjectHierarchy: null, Priority: 1, _count: 3, Scope: 30 }
Notice how the ProjectHierarchy field value is `null`. This is because it is a total cell for Priority dimension
for all ProjectHierarchy values. Think of `null` values in this context as wildcards.
Similarly, we can get the total for all descendants of ProjectHierarchy = [1] regarless of Priority as follows:
console.log(cube.getCell({ProjectHierarchy: [1]}))
* { ProjectHierarchy: [ 1 ], Priority: null, _count: 3, Scope: 18 }
`getCell()` uses the cellIndex so it's very efficient. Using `getCell()` and `getDimensionValues()`, you can iterate
over a slice of the OLAPCube. It is usually preferable to access the cells in place like this rather than the
traditional OLAP approach of extracting a slice for processing. However, there is a `slice()` method for extracting
a 2D slice.
rowValues = cube.getDimensionValues('ProjectHierarchy')
columnValues = cube.getDimensionValues('Priority')
s = OLAPCube._padToWidth('', 7) + ' | '
s += ((OLAPCube._padToWidth(JSON.stringify(c), 7) for c in columnValues).join(' | '))
s += ' | '
console.log(s)
for r in rowValues
s = OLAPCube._padToWidth(JSON.stringify(r), 7) + ' | '
for c in columnValues
cell = cube.getCell({ProjectHierarchy: r, Priority: c})
if cell?
cellString = JSON.stringify(cell._count)
else
cellString = ''
s += OLAPCube._padToWidth(cellString, 7) + ' | '
console.log(s)
* | null | 1 | 2 |
* null | 4 | 3 | 1 |
* [1] | 3 | 2 | 1 |
* [1,2] | 3 | 2 | 1 |
* [1,2,3] | 1 | 1 | |
* [1,2,4] | 1 | | 1 |
* [5] | 1 | 1 | |
Or you can just call `toString()` method which extracts a 2D slice for tabular display. Both approachs will work on
cubes of any number of dimensions two or greater. The manual example above extracted the `count` metric. We'll tell
the example below to extract the `Scope` metric.
console.log(cube.toString('ProjectHierarchy', 'Priority', 'Scope'))
* | || Total | 1 2|
* |==============================|
* |Total || 35 | 30 5|
* |------------------------------|
* |[1] || 18 | 13 5|
* |[1,2] || 18 | 13 5|
* |[1,2,3] || 10 | 10 |
* |[1,2,4] || 5 | 5|
* |[5] || 17 | 17 |
## Dimension types ##
The following dimension types are supported:
1. Single value
* Number
* String
* Does not work:
* Boolean - known to fail
* Object - may sorta work but sort-order at least is not obvious
* Date - not tested but may actually work
2. Arrays as materialized path for hierarchical (tree) data
3. Non-hierarchical Arrays ("tags")
There is no need to tell the OLAPCube what type to use with the exception of #2. In that case, add `type: 'hierarchy'`
to the dimensions row like this:
dimensions = [
{field: 'hierarchicalDimensionField', type: 'hierarchy'} #, ...
]
## Hierarchical (tree) data ##
This OLAP Cube implementation assumes your hierarchies (trees) are modeled as a
[materialized path](http://en.wikipedia.org/wiki/Materialized_path) array. This approach is commonly used with NoSQL databases like
[CouchDB](http://probablyprogramming.com/2008/07/04/storing-hierarchical-data-in-couchdb) and
[MongoDB (combining materialized path and array of ancestors)](http://docs.mongodb.org/manual/tutorial/model-tree-structures/)
and even SQL databases supporting array types like [Postgres](http://www.monkeyandcrow.com/blog/hierarchies_with_postgres/).
This approach differs from the traditional OLAP/MDX fixed/named level hierarchy approach. In that approach, you assume
that the number of levels in the hierarchy are fixed. Also, each level in the hierarchy is either represented by a different
column (clothing example --> level 0: SEX column - mens vs womens; level 1: TYPE column - pants vs shorts vs shirts; etc.) or
predetermined ranges of values in a single field (date example --> level 0: year; level 1: quarter; level 2: month; etc.)
However, the approach used by this OLAPCube implementaion is the more general case, because it can easily simulate
fixed/named level hierachies whereas the reverse is not true. In the clothing example above, you would simply key
your dimension off of a derived field that was a combination of the SEX and TYPE columns (e.g. ['mens', 'pants'])
## Date/Time hierarchies ##
Lumenize is designed to work well with the tzTime library. Here is an example of taking a bunch of ISOString data
and doing timezone precise hierarchical roll up based upon the date segments (year, month).
data = [
{date: '2011-12-31T12:34:56.789Z', value: 10},
{date: '2012-01-05T12:34:56.789Z', value: 20},
{date: '2012-01-15T12:34:56.789Z', value: 30},
{date: '2012-02-01T00:00:01.000Z', value: 40},
{date: '2012-02-15T12:34:56.789Z', value: 50},
]
{Time} = require('../')
config =
deriveFieldsOnInput: [{
field: 'dateSegments',
f: (row) ->
return new Time(row.date, Time.MONTH, 'America/New_York').getSegmentsAsArray()
}]
metrics: [{field: 'value', f: 'sum'}]
dimensions: [{field: 'dateSegments', type: 'hierarchy'}]
cube = new OLAPCube(config, data)
console.log(cube.toString(undefined, undefined, 'value_sum'))
* | dateSegments | value_sum |
* |==========================|
* | [2011] | 10 |
* | [2011,12] | 10 |
* | [2012] | 140 |
* | [2012,1] | 90 |
* | [2012,2] | 50 |
Notice how '2012-02-01T00:00:01.000Z' got bucketed in January because the calculation was done in timezone
'America/New_York'.
## Non-hierarchical Array fields ##
If you don't specify type: 'hierarchy' and the OLAPCube sees a field whose value is an Array in a dimension field, the
data in that fact would get aggregated against each element in the Array. So a non-hierarchical Array field like
['x', 'y', 'z'] would get aggregated against 'x', 'y', and 'z' rather than ['x'], ['x', 'y'], and ['x','y','z]. This
functionality is useful for accomplishing analytics on tags, but it can be used in other powerful ways. For instance
let's say you have a list of events:
events = [
{name: 'Renaissance Festival', activeMonths: ['September', 'October']},
{name: 'Concert Series', activeMonths: ['July', 'August', 'September']},
{name: 'Fall Festival', activeMonths: ['September']}
]
You could figure out the number of events active in each month by specifying "activeMonths" as a dimension.
Lumenize.TimeInStateCalculator (and other calculators in Lumenize) use this technique.
*/
function OLAPCube(userConfig, facts) {
var d, j, k, key, l, len1, len2, len3, len4, m, n, ref1, ref2, ref3, ref4, ref5, requiredFieldsObject, value;
this.userConfig = userConfig;
/*
@constructor
@param {Object} config See Config options for details. DO NOT change the config settings after the OLAP class is instantiated.
@param {Object[]} [facts] Optional parameter allowing the population of the OLAPCube with an intitial set of facts
upon instantiation. Use addFacts() to add facts after instantiation.
@cfg {Object[]} dimensions Array which specifies the fields to use as dimension fields. If the field contains a
hierarchy array, say so in the row, (e.g. `{field: 'SomeFieldName', type: 'hierarchy'}`). Any array values that it
finds in the supplied facts will be assumed to be tags rather than a hierarchy specification unless `type: 'hierarchy'`
is specified.
For example, let's say you have a set of facts that look like this:
fact = {
dimensionField: 'a',
hierarchicalDimensionField: ['1','2','3'],
tagDimensionField: ['x', 'y', 'z'],
valueField: 10
}
Then a set of dimensions like this makes sense.
config.dimensions = [
{field: 'dimensionField'},
{field: 'hierarchicalDimensionField', type: 'hierarchy'},
{field: 'tagDimensionField', keepTotals: true}
]
Notice how a keepTotals can be set for an individual dimension. This is preferable to setting it for the entire
cube in cases where you don't want totals in all dimensions.
If no dimension config is provided, then you must use syntactic sugar like groupBy.
@cfg {String} [groupBy] Syntactic sugar for single-dimension/single-metric usage.
@cfg {String} [f] Syntactic sugar for single-dimension/single-metric usage. If provided, you must also provide
a `groupBy` config. If you provided a `groupBy` but no `f` or `field`, then the default `count` metric will be used.
@cfg {String} [field] Syntactic sugar for single-dimension/single-metric usage. If provided, you must also provide
a `groupBy` config. If you provided a `groupBy` but no `f` or `field`, then the default `count` metric will be used.
@cfg {Object[]} [metrics=[]] Array which specifies the metrics to calculate for each cell in the cube.
Example:
config = {}
config.metrics = [
{field: 'field3'}, # defaults to metrics: ['sum']
{field: 'field4', metrics: [
{f: 'sum'}, # will add a metric named field4_sum
{as: 'median4', f: 'p50'}, # renamed p50 to median4 from default of field4_p50
{as: 'myCount', f: (values) -> return values.length} # user-supplied function
]}
]
If you specify a field without any metrics, it will assume you want the sum but it will not automatically
add the sum metric to fields with a metrics specification. User-supplied aggregation functions are also supported as
shown in the 'myCount' metric above.
Note, if the metric has dependencies (e.g. average depends upon count and sum) it will automatically add those to
your metric definition. If you've already added a dependency but put it under a different "as", it's not smart
enough to sense that and it will add it again. Either live with the slight inefficiency and duplication or leave
dependency metrics named their default by not providing an "as" field.
@cfg {Boolean} [keepTotals=false] Setting this will add an additional total row (indicated with field: null) along
all dimensions. This setting can have an impact on the memory usage and performance of the OLAPCube so
if things are tight, only use it if you really need it. If you don't need it for all dimension, you can specify
keepTotals for individual dimensions.
@cfg {Boolean} [keepFacts=false] Setting this will cause the OLAPCube to keep track of the facts that contributed to
the metrics for each cell by adding an automatic 'facts' metric. Note, facts are restored after deserialization
as you would expect, but they are no longer tied to the original facts. This feature, especially after a restore
can eat up memory.
@cfg {Object[]} [deriveFieldsOnInput] An Array of Maps in the form `{field:'myField', f:(fact)->...}`
@cfg {Object[]} [deriveFieldsOnOutput] same format as deriveFieldsOnInput, except the callback is in the form `f(row)`
This is only called for dirty rows that were effected by the latest round of addFacts. It's more efficient to calculate things
like standard deviation and percentile coverage here than in config.metrics. You just have to remember to include the dependencies
in config.metrics. Standard deviation depends upon `sum` and `sumSquares`. Percentile coverage depends upon `values`.
In fact, if you are going to capture values anyway, all of the functions are most efficiently calculated here.
Maybe some day, I'll write the code to analyze your metrics and move them out to here if it improves efficiency.
*/
this.config = utils.clone(this.userConfig);
this.cells = [];
this.cellIndex = {};
this.currentValues = {};
if (this.config.groupBy != null) {
this.config.dimensions = [
{
field: this.config.groupBy
}
];
if ((this.config.f != null) && (this.config.field != null)) {
this.config.metrics = [
{
field: this.config.field,
f: this.config.f
}
];
}
}
utils.assert(this.config.dimensions != null, 'Must provide config.dimensions.');
if (this.config.metrics == null) {
this.config.metrics = [];
}
this._dimensionValues = {};
ref1 = this.config.dimensions;
for (j = 0, len1 = ref1.length; j < len1; j++) {
d = ref1[j];
this._dimensionValues[d.field] = {};
}
if (!this.config.keepTotals) {
this.config.keepTotals = false;
}
if (!this.config.keepFacts) {
this.config.keepFacts = false;
}
ref2 = this.config.dimensions;
for (k = 0, len2 = ref2.length; k < len2; k++) {
d = ref2[k];
if (this.config.keepTotals || d.keepTotals) {
d.keepTotals = true;
} else {
d.keepTotals = false;
}
}
functions.expandMetrics(this.config.metrics, true, true);
requiredFieldsObject = {};
ref3 = this.config.metrics;
for (l = 0, len3 = ref3.length; l < len3; l++) {
m = ref3[l];
if (((ref4 = m.field) != null ? ref4.length : void 0) > 0) {
requiredFieldsObject[m.field] = null;
}
}
this.requiredMetricsFields = (function() {
var results;
results = [];
for (key in requiredFieldsObject) {
value = requiredFieldsObject[key];
results.push(key);
}
return results;
})();
requiredFieldsObject = {};
ref5 = this.config.dimensions;
for (n = 0, len4 = ref5.length; n < len4; n++) {
d = ref5[n];
requiredFieldsObject[d.field] = null;
}
this.requiredDimensionFields = (function() {
var results;
results = [];
for (key in requiredFieldsObject) {
value = requiredFieldsObject[key];
results.push(key);
}
return results;
})();
this.summaryMetrics = {};
this.addFacts(facts);
}
OLAPCube._possibilities = function(key, type, keepTotals) {
var a, len;
switch (utils.type(key)) {
case 'array':
if (keepTotals) {
a = [null];
} else {
a = [];
}
if (type === 'hierarchy') {
len = key.length;
while (len > 0) {
a.push(key.slice(0, len));
len--;
}
} else {
if (keepTotals) {
a = [null].concat(key);
} else {
a = key;
}
}
return a;
case 'string':
case 'number':
if (keepTotals) {
return [null, key];
} else {
return [key];
}
}
};
OLAPCube._decrement = function(a, rollover) {
var i;
i = a.length - 1;
a[i]--;
while (a[i] < 0) {
a[i] = rollover[i];
i--;
if (i < 0) {
return false;
} else {
a[i]--;
}
}
return true;
};
OLAPCube.prototype._expandFact = function(fact) {
var countdownArray, d, index, j, k, l, len1, len2, len3, len4, m, more, n, out, outRow, p, possibilitiesArray, ref1, ref2, ref3, ref4, rolloverArray;
possibilitiesArray = [];
countdownArray = [];
rolloverArray = [];
ref1 = this.config.dimensions;
for (j = 0, len1 = ref1.length; j < len1; j++) {
d = ref1[j];
p = OLAPCube._possibilities(fact[d.field], d.type, d.keepTotals);
if (p === void 0) {
console.log(fact);
}
possibilitiesArray.push(p);
countdownArray.push(p.length - 1);
rolloverArray.push(p.length - 1);
}
ref2 = this.config.metrics;
for (k = 0, len2 = ref2.length; k < len2; k++) {
m = ref2[k];
this.currentValues[m.field] = [fact[m.field]];
}
out = [];
more = true;
while (more) {
outRow = {};
ref3 = this.config.dimensions;
for (index = l = 0, len3 = ref3.length; l < len3; index = ++l) {
d = ref3[index];
outRow[d.field] = possibilitiesArray[index][countdownArray[index]];
}
outRow._count = 1;
if (this.config.keepFacts) {
outRow._facts = [fact];
}
ref4 = this.config.metrics;
for (n = 0, len4 = ref4.length; n < len4; n++) {
m = ref4[n];
outRow[m.as] = m.f([fact[m.field]], void 0, void 0, outRow, m.field + '_');
}
out.push(outRow);
more = OLAPCube._decrement(countdownArray, rolloverArray);
}
return out;
};
OLAPCube._extractFilter = function(row, dimensions) {
var d, j, len1, out;
out = {};
for (j = 0, len1 = dimensions.length; j < len1; j++) {
d = dimensions[j];
out[d.field] = row[d.field];
}
return out;
};
OLAPCube.prototype._mergeExpandedFactArray = function(expandedFactArray) {
var d, er, fieldValue, filterString, j, k, l, len1, len2, len3, m, olapRow, ref1, ref2, results;
results = [];
for (j = 0, len1 = expandedFactArray.length; j < len1; j++) {
er = expandedFactArray[j];
ref1 = this.config.dimensions;
for (k = 0, len2 = ref1.length; k < len2; k++) {
d = ref1[k];
fieldValue = er[d.field];
this._dimensionValues[d.field][JSON.stringify(fieldValue)] = fieldValue;
}
filterString = JSON.stringify(OLAPCube._extractFilter(er, this.config.dimensions));
olapRow = this.cellIndex[filterString];
if (olapRow != null) {
ref2 = this.config.metrics;
for (l = 0, len3 = ref2.length; l < len3; l++) {
m = ref2[l];
olapRow[m.as] = m.f(olapRow[m.field + '_values'], olapRow[m.as], this.currentValues[m.field], olapRow, m.field + '_');
}
if (olapRow._facts != null) {
olapRow._facts = olapRow._facts.concat(er._facts);
}
} else {
olapRow = er;
this.cellIndex[filterString] = olapRow;
this.cells.push(olapRow);
}
results.push(this.dirtyRows[filterString] = olapRow);
}
return results;
};
OLAPCube.prototype.addFacts = function(facts) {
/*
@method addFacts
Adds facts to the OLAPCube.
@chainable
@param {Object[]} facts An Array of facts to be aggregated into OLAPCube. Each fact is a Map where the keys are the field names
and the values are the field values (e.g. `{field1: 'a', field2: 5}`).
*/
var d, dirtyRow, expandedFactArray, fact, fieldName, filterString, j, k, l, len1, len2, len3, len4, n, ref1, ref2, ref3;
this.dirtyRows = {};
if (utils.type(facts) === 'array') {
if (facts.length <= 0) {
return;
}
} else {
if (facts != null) {
facts = [facts];
} else {
return;
}
}
if (this.config.deriveFieldsOnInput) {
for (j = 0, len1 = facts.length; j < len1; j++) {
fact = facts[j];
ref1 = this.config.deriveFieldsOnInput;
for (k = 0, len2 = ref1.length; k < len2; k++) {
d = ref1[k];
if (d.as != null) {
fieldName = d.as;
} else {
fieldName = d.field;
}
fact[fieldName] = d.f(fact);
}
}
}
for (l = 0, len3 = facts.length; l < len3; l++) {
fact = facts[l];
this.addMissingFields(fact);
this.currentValues = {};
expandedFactArray = this._expandFact(fact);
this._mergeExpandedFactArray(expandedFactArray);
}
if (this.config.deriveFieldsOnOutput != null) {
ref2 = this.dirtyRows;
for (filterString in ref2) {
dirtyRow = ref2[filterString];
ref3 = this.config.deriveFieldsOnOutput;
for (n = 0, len4 = ref3.length; n < len4; n++) {
d = ref3[n];
if (d.as != null) {
fieldName = d.as;
} else {
fieldName = d.field;
}
dirtyRow[fieldName] = d.f(dirtyRow);
}
}
}
this.dirtyRows = {};
return this;
};
OLAPCube.prototype.addMissingFields = function(fact) {
var field, j, k, len1, len2, ref1, ref2;
ref1 = this.requiredMetricsFields;
for (j = 0, len1 = ref1.length; j < len1; j++) {
field = ref1[j];
if (fact[field] === void 0) {
fact[field] = null;
}
}
ref2 = this.requiredDimensionFields;
for (k = 0, len2 = ref2.length; k < len2; k++) {
field = ref2[k];
if (fact[field] == null) {
fact[field] = '<missing>';
}
}
return fact;
};
OLAPCube.prototype.getCells = function(filterObject) {
/*
@method getCells
Returns a subset of the cells that match the supplied filter. You can perform slice and dice operations using
this. If you have criteria for all of the dimensions, you are better off using `getCell()`. Most times, it's
better to iterate over the unique values for the dimensions of interest using `getCell()` in place of slice or
dice operations. However, there is a `slice()` method for extracting a 2D slice
@param {Object} [filterObject] Specifies the constraints that the returned cells must match in the form of
`{field1: value1, field2: value2}`. If this parameter is missing, the internal cells array is returned.
@return {Object[]} Returns the cells that match the supplied filter
*/
var c, j, len1, output, ref1;
if (filterObject == null) {
return this.cells;
}
output = [];
ref1 = this.cells;
for (j = 0, len1 = ref1.length; j < len1; j++) {
c = ref1[j];
if (utils.filterMatch(filterObject, c)) {
output.push(c);
}
}
return output;
};
OLAPCube.prototype.getCell = function(filter, defaultValue) {
/*
@method getCell
Returns the single cell matching the supplied filter. Iterating over the unique values for the dimensions of
interest, you can incrementally retrieve a slice or dice using this method. Since `getCell()` always uses an index,
in most cases, this is better than using `getCells()` to prefetch a slice or dice.
@param {Object} [filter={}] Specifies the constraints for the returned cell in the form of `{field1: value1, field2: value2}.
Any fields that are specified in config.dimensions that are missing from the filter are automatically filled in
with null. Calling `getCell()` with no parameter or `{}` will return the total of all dimensions (if @config.keepTotals=true).
@return {Object[]} Returns the cell that match the supplied filter
*/
var cell, d, foundIt, j, k, key, len1, len2, normalizedFilter, ref1, ref2, value;
if (filter == null) {
filter = {};
}
for (key in filter) {
value = filter[key];
foundIt = false;
ref1 = this.config.dimensions;
for (j = 0, len1 = ref1.length; j < len1; j++) {
d = ref1[j];
if (d.field === key) {
foundIt = true;
}
}
if (!foundIt) {
throw new Error(key + " is not a dimension for this cube.");
}
}
normalizedFilter = {};
ref2 = this.config.dimensions;
for (k = 0, len2 = ref2.length; k < len2; k++) {
d = ref2[k];
if (filter.hasOwnProperty(d.field)) {
normalizedFilter[d.field] = filter[d.field];
} else {
if (d.keepTotals) {
normalizedFilter[d.field] = null;
} else {
throw new Error('Must set keepTotals to use getCell with a partial filter.');
}
}
}
cell = this.cellIndex[JSON.stringify(normalizedFilter)];
if (cell != null) {
return cell;
} else {
return defaultValue;
}
};
OLAPCube.prototype.getDimensionValues = function(field, descending) {
var values;
if (descending == null) {
descending = false;
}
/*
@method getDimensionValues
Returns the unique values for the specified dimension in sort order.
@param {String} field The field whose values you want
@param {Boolean} [descending=false] Set to true if you want them in reverse order
*/
values = utils.values(this._dimensionValues[field]);
values.sort(utils.compare);
if (!descending) {
values.reverse();
}
return values;
};
OLAPCube.roundToSignificance = function(value, significance) {
var multiple;
if (significance == null) {
return value;
}
multiple = 1 / significance;
return Math.floor(value * multiple) / multiple;
};
OLAPCube.prototype.toString = function(rows, columns, metric, significance) {
/*
@method toString
Produces a printable table with the first dimension as the rows, the second dimension as the columns, and the count
as the values in the table.
@return {String} A string which will render as a table when written to the console.
@param {String} [rows=<first dimension>]
@param {String} [columns=<second dimension>]
@param {String} [metric='count']
@param {Number} [significance] The multiple to which you want to round the bucket edges. 1 means whole numbers.
0.1 means to round to tenths. 0.01 to hundreds. Etc.
*/
if (metric == null) {
metric = '_count';
}
if (this.config.dimensions.length === 1) {
return this.toStringOneDimension(this.config.dimensions[0].field, metric, significance);
} else {
return this.toStringTwoDimensions(rows, columns, metric, significance);
}
};
OLAPCube.prototype.toStringOneDimension = function(field, metric, significance) {
var cell, cellString, filter, fullWidth, indexRow, j, k, len1, len2, maxColumnWidth, r, rowLabelWidth, rowValueStrings, rowValues, s, valueStrings;
rowValues = this.getDimensionValues(field);
rowValueStrings = (function() {
var j, len1, results;
results = [];
for (j = 0, len1 = rowValues.length; j < len1; j++) {
r = rowValues[j];
results.push(JSON.stringify(r));
}
return results;
})();
rowLabelWidth = Math.max.apply({}, (function() {
var j, len1, results;
results = [];
for (j = 0, len1 = rowValueStrings.length; j < len1; j++) {
s = rowValueStrings[j];
results.push(s.length);
}
return results;
})());
rowLabelWidth = Math.max(rowLabelWidth, 'Total'.length, field.length);
maxColumnWidth = metric.length;
valueStrings = [];
for (indexRow = j = 0, len1 = rowValues.length; j < len1; indexRow = ++j) {
r = rowValues[indexRow];
filter = {};
filter[field] = r;
cell = this.getCell(filter);
if (cell != null) {
cellString = JSON.stringify(OLAPCube.roundToSignificance(cell[metric], significance));
} else {
cellString = '';
}
maxColumnWidth = Math.max(maxColumnWidth, cellString.length);
valueStrings.push(cellString);
}
maxColumnWidth += 1;
fullWidth = rowLabelWidth + maxColumnWidth + 4;
s = '| ' + (OLAPCube._padToWidth(field, rowLabelWidth, ' ', true)) + ' |';
s += OLAPCube._padToWidth(metric, maxColumnWidth) + ' |';
s += '\n|' + OLAPCube._padToWidth('', fullWidth, '=') + '|';
for (indexRow = k = 0, len2 = rowValueStrings.length; k < len2; indexRow = ++k) {
r = rowValueStrings[indexRow];
s += '\n| ';
if (r === 'null') {
s += OLAPCube._padToWidth('Total', rowLabelWidth, ' ', true);
} else {
s += OLAPCube._padToWidth(r, rowLabelWidth, ' ', true);
}
s += ' |' + OLAPCube._padToWidth(valueStrings[indexRow], maxColumnWidth) + ' |';
if (r === 'null') {
s += '\n|' + OLAPCube._padToWidth('', fullWidth, '-') + '|';
}
}
return s;
};
OLAPCube.prototype.toStringTwoDimensions = function(rows, columns, metric, significance) {
var c, cell, cellString, columnValueStrings, columnValues, filter, fullWidth, indexColumn, indexRow, j, k, l, len1, len2, len3, len4, len5, maxColumnWidth, n, o, r, rowLabelWidth, rowValueStrings, rowValues, s, valueStrings, valueStringsRow;
if (rows == null) {
rows = this.config.dimensions[0].field;
}
if (columns == null) {
columns = this.config.dimensions[1].field;
}
rowValues = this.getDimensionValues(rows);
columnValues = this.getDimensionValues(columns);
rowValueStrings = (function() {
var j, len1, results;
results = [];
for (j = 0, len1 = rowValues.length; j < len1; j++) {
r = rowValues[j];
results.push(JSON.stringify(r));
}
return results;
})();
columnValueStrings = (function() {
var j, len1, results;
results = [];
for (j = 0, len1 = columnValues.length; j < len1; j++) {
c = columnValues[j];
results.push(JSON.stringify(c));
}
return results;
})();
rowLabelWidth = Math.max.apply({}, (function() {
var j, len1, results;
results = [];
for (j = 0, len1 = rowValueStrings.length; j < len1; j++) {
s = rowValueStrings[j];
results.push(s.length);
}
return results;
})());
rowLabelWidth = Math.max(rowLabelWidth, 'Total'.length);
valueStrings = [];
maxColumnWidth = Math.max.apply({}, (function() {
var j, len1, results;
results = [];
for (j = 0, len1 = columnValueStrings.length; j < len1; j++) {
s = columnValueStrings[j];
results.push(s.length);
}
return results;
})());
maxColumnWidth = Math.max(maxColumnWidth, 'Total'.length);
for (indexRow = j = 0, len1 = rowValues.length; j < len1; indexRow = ++j) {
r = rowValues[indexRow];
valueStringsRow = [];
for (indexColumn = k = 0, len2 = columnValues.length; k < len2; indexColumn = ++k) {
c = columnValues[indexColumn];
filter = {};
filter[rows] = r;
filter[columns] = c;
cell = this.getCell(filter);
if (cell != null) {
cellString = JSON.stringify(OLAPCube.roundToSignificance(cell[metric], significance));
} else {
cellString = '';
}
maxColumnWidth = Math.max(maxColumnWidth, cellString.length);
valueStringsRow.push(cellString);
}
valueStrings.push(valueStringsRow);
}
maxColumnWidth += 1;
s = '|' + (OLAPCube._padToWidth('', rowLabelWidth)) + ' ||';
for (indexColumn = l = 0, len3 = columnValueStrings.length; l < len3; indexColumn = ++l) {
c = columnValueStrings[indexColumn];
if (c === 'null') {
s += OLAPCube._padToWidth('Total', maxColumnWidth) + ' |';
} else {
s += OLAPCube._padToWidth(c, maxColumnWidth);
}
}
fullWidth = rowLabelWidth + maxColumnWidth * columnValueStrings.length + 3;
if (columnValueStrings[0] === 'null') {
fullWidth += 2;
}
s += '|\n|' + OLAPCube._padToWidth('', fullWidth, '=');
for (indexRow = n = 0, len4 = rowValueStrings.length; n < len4; indexRow = ++n) {
r = rowValueStrings[indexRow];
s += '|\n|';
if (r === 'null') {
s += OLAPCube._padToWidth('Total', rowLabelWidth, ' ', true);
} else {
s += OLAPCube._padToWidth(r, rowLabelWidth, ' ', true);
}
s += ' ||';
for (indexColumn = o = 0, len5 = columnValueStrings.length; o < len5; indexColumn = ++o) {
c = columnValueStrings[indexColumn];
s += OLAPCube._padToWidth(valueStrings[indexRow][indexColumn], maxColumnWidth);
if (c === 'null') {
s += ' |';
}
}
if (r === 'null') {
s += '|\n|' + OLAPCube._padToWidth('', fullWidth, '-');
}
}
s += '|';
return s;
};
OLAPCube.prototype.slice = function(rows, columns, metric, significance) {
/*
@method slice
Extracts a 2D slice of the data. It outputs an array of arrays (JavaScript two-dimensional array) organized as the
C3 charting library would expect if submitting row-oriented data.
Note, the calling parameters and output of this function are very similar to the 2D toString() function output
except the data is organized as a two-dimensional array instead of newline-separated lines and the cells are
filled with actual values instead of padded string representations of those values.
@return {[[]]} An array of arrays with the one row for the header and each row label
@param {String} [rows=<first dimension>]
@param {String} [columns=<second dimension>]
@param {String} [metric='count']
@param {Number} [significance] The multiple to which you want to round the bucket edges. 1 means whole numbers.
0.1 means to round to tenths. 0.01 to hundreds. Etc.
*/
var c, cell, cellValue, columnValues, filter, indexColumn, indexRow, j, k, l, len1, len2, len3, r, rowValues, topRow, values, valuesRow;
if (rows == null) {
rows = this.config.dimensions[0].field;
}
if (columns == null) {
columns = this.config.dimensions[1].field;
}
if (metric == null) {
metric = '_count';
}
rowValues = this.getDimensionValues(rows);
columnValues = this.getDimensionValues(columns);
values = [];
topRow = [];
topRow.push(rows);
for (indexColumn = j = 0, len1 = columnValues.length; j < len1; indexColumn = ++j) {
c = columnValues[indexColumn];
if (c === null) {
topRow.push('Total');
} else {
topRow.push(c);
}
}
values.push(topRow);
for (indexRow = k = 0, len2 = rowValues.length; k < len2; indexRow = ++k) {
r = rowValues[indexRow];
valuesRow = [];
if (r === null) {
valuesRow.push('Total');
} else {
valuesRow.push(r);
}
for (indexColumn = l = 0, len3 = columnValues.length; l < len3; indexColumn = ++l) {
c = columnValues[indexColumn];
filter = {};
filter[rows] = r;
filter[columns] = c;
cell = this.getCell(filter);
if (cell != null) {
cellValue = OLAPCube.roundToSignificance(cell[metric], significance);
} else {
cellValue = null;
}
valuesRow.push(cellValue);
}
values.push(valuesRow);
}
return values;
};
OLAPCube._padToWidth = function(s, width, padCharacter, rightPad) {
var padding;
if (padCharacter == null) {
padCharacter = ' ';
}
if (rightPad == null) {
rightPad = false;
}
if (s.length > width) {
return s.substr(0, width);
}
padding = new Array(width - s.length + 1).join(padCharacter);
if (rightPad) {
return s + padding;
} else {
return padding + s;
}
};
OLAPCube.prototype.getStateForSaving = function(meta) {
/*
@method getStateForSaving
Enables saving the state of an OLAPCube.
@param {Object} [meta] An optional parameter that will be added to the serialized output and added to the meta field
within the deserialized OLAPCube
@return {Object} Returns an Ojbect representing the state of the OLAPCube. 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 OLAPCube.
facts = [
{ProjectHierarchy: [1, 2, 3], Priority: 1},
{ProjectHierarchy: [1, 2, 4], Priority: 2},
{ProjectHierarchy: [5] , Priority: 1},
{ProjectHierarchy: [1, 2] , Priority: 1},
]
dimensions = [
{field: "ProjectHierarchy", type: 'hierarchy'},
{field: "Priority"}
]
config = {dimensions, metrics: []}
config.keepTotals = true
originalCube = new OLAPCube(config, facts)
dateString = '2012-12-27T12:34:56.789Z'
savedState = originalCube.getStateForSaving({upToDate: dateString})
restoredCube = OLAPCube.newFromSavedState(savedState)
newFacts = [
{ProjectHierarchy: [5], Priority: 3},
{ProjectHierarchy: [1, 2, 4], Priority: 1}
]
originalCube.addFacts(newFacts)
restoredCube.addFacts(newFacts)
console.log(restoredCube.toString() == originalCube.toString())
* true
console.log(restoredCube.meta.upToDate)
* 2012-12-27T12:34:56.789Z
*/
var out;
out = {
config: this.userConfig,
cells: this.cells,
summaryMetrics: this.summaryMetrics
};
if (meta != null) {
out.meta = meta;
}
return out;
};
OLAPCube.newFromSavedState = function(p) {
/*
@method newFromSavedState
Deserializes a previously stringified OLAPCube and returns a new OLAPCube.
See `getStateForSaving()` documentation for a detailed example.
Note, if you have specified config.keepFacts = true, the values for the facts will be restored, however, they
will no longer be references to the original facts. For this reason, it's usually better to include a `values` or
`uniqueValues` metric on some ID field if you want fact drill-down support to survive a save and restore.
@static
@param {String/Object} p A String or Object from a previously saved OLAPCube state
@return {OLAPCube}
*/
var c, cube, d, fieldValue, filterString, j, k, l, len1, len2, len3, ref1, ref2, ref3;
if (utils.type(p) === 'string') {
p = JSON.parse(p);
}
cube = new OLAPCube(p.config);
cube.summaryMetrics = p.summaryMetrics;
if (p.meta != null) {
cube.meta = p.meta;
}
if (p.cellsAsCSVStyleArray != null) {
cube.cells = csvStyleArray_To_ArrayOfMaps(p.cellsAsCSVStyleArray);
} else {
cube.cells = p.cells;
}
cube.cellIndex = {};
cube._dimensionValues = {};
ref1 = cube.config.dimensions;
for (j = 0, len1 = ref1.length; j < len1; j++) {
d = ref1[j];
cube._dimensionValues[d.field] = {};
}
ref2 = cube.cells;
for (k = 0, len2 = ref2.length; k < len2; k++) {
c = ref2[k];
filterString = JSON.stringify(OLAPCube._extractFilter(c, cube.config.dimensions));
cube.cellIndex[filterString] = c;
ref3 = cube.config.dimensions;
for (l = 0, len3 = ref3.length; l < len3; l++) {
d = ref3[l];
fieldValue = c[d.field];
cube._dimensionValues[d.field][JSON.stringify(fieldValue)] = fieldValue;
}
}
return cube;
};
return OLAPCube;
})();
exports.OLAPCube = OLAPCube;
}).call(this);