UNPKG

lumenize

Version:

Illuminating the forest AND the trees in your data.

326 lines (295 loc) 15.1 kB
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>The source code</title> <link href="../resources/prettify/prettify.css" type="text/css" rel="stylesheet" /> <script type="text/javascript" src="../resources/prettify/prettify.js"></script> <style type="text/css"> .highlight { display: block; background-color: #ddd; } </style> <script type="text/javascript"> function highlight() { document.getElementById(location.hash.replace(/#/, "")).className = "highlight"; } </script> </head> <body onload="prettyPrint(); highlight();"> <pre class="prettyprint lang-js">/* &lt;CoffeeScript&gt; {utils, Time} = require(&#39;tztime&#39;) functions = require(&#39;./functions&#39;).functions # !TODO: Do we need this here? {arrayOfMaps_To_CSVStyleArray, csvStyleArray_To_ArrayOfMaps} = require(&#39;./dataTransform&#39;) # !TODO: Do we need this here? INFINITY = &#39;9999-01-01T00:00:00.000Z&#39; class Store &lt;/CoffeeScript&gt; */ <span id='Lumenize-Store-method-constructor'><span id='Lumenize-Store-cfg-defaultValues'><span id='Lumenize-Store-cfg-tz'><span id='Lumenize-Store-cfg-idField'><span id='Lumenize-Store-cfg-validToField'><span id='Lumenize-Store-cfg-validFromField'><span id='Lumenize-Store-cfg-uniqueIDField'><span id='Lumenize-Store'> /** </span></span></span></span></span></span></span></span> * @class Lumenize.Store * * __An efficient, in-memory, datastore for snapshot data.__ * * Note, this store takes advantage of JavaScript&#39;s prototype inheritance to store snapshots in memory. Since the next snapshot might * only have one field different from the prior one, this saves a ton of space. There is some concern that this will * slow down certain operations because the JavaScript engine has to search all fields in the current level before bumping up * to the next. However, there is some evidence that modern JavaScript implementations handle this very efficiently. * * However, this choice means that each row in the snapshots array doesn&#39;t have all of the fields. * * Store keeps track of all of the fields it has seen so you can flatten a row(s) if necessary. * * Example: * * {Store} = require(&#39;../&#39;) * * snapshotCSVStyleArray = [ * [&#39;RecordID&#39;, &#39;DefectID&#39;, &#39;Created_Date&#39;, &#39;Severity&#39;, &#39;Modified_Date&#39;, &#39;Status&#39;], * [ 1, 1, &#39;2014-06-16&#39;, 5, &#39;2014-06-16&#39;, &#39;New&#39;], * [ 100, 1, &#39;2014-06-16&#39;, 5, &#39;2014-07-17&#39;, &#39;In Progress&#39;], * [ 1000, 1, &#39;2014-06-16&#39;, 5, &#39;2014-08-18&#39;, &#39;Done&#39;], * ] * * defects = require(&#39;../&#39;).csvStyleArray_To_ArrayOfMaps(snapshotCSVStyleArray) * * config = * uniqueIDField: &#39;DefectID&#39; * validFromField: &#39;Modified_Date&#39; * idField: &#39;RecordID&#39; * defaultValues: * Severity: 4 * * store = new Store(config, defects) * * console.log(require(&#39;../&#39;).table.toString(store.snapshots, store.fields)) * # | Modified_Date | _ValidTo | _PreviousValues | DefectID | RecordID | Created_Date | Severity | Status | * # | ------------------------ | ------------------------ | --------------- | -------- | -------- | ------------ | -------- | ----------- | * # | 2014-06-16T00:00:00.000Z | 2014-07-17T00:00:00.000Z | [object Object] | 1 | 1 | 2014-06-16 | 5 | New | * # | 2014-07-17T00:00:00.000Z | 2014-08-18T00:00:00.000Z | [object Object] | 1 | 100 | 2014-06-16 | 5 | In Progress | * # | 2014-08-18T00:00:00.000Z | 9999-01-01T00:00:00.000Z | [object Object] | 1 | 1000 | 2014-06-16 | 5 | Done | * * That&#39;s pretty boring. We pretty much got out what we put in. There are a few things to notice though. First, * Notice how the _ValidTo field is automatically set. Also, notice that it added the _PreviousValues field. This is * a record of the immediately proceeding values for the fields that changed. In this way, the records not only * represent the current snapshot; they also represent the state transition that occured to get into this snapshot * state. That&#39;s what stateBoundaryCrossedFilter and other methods key off of. * * Also, under the covers, the prototype of each snapshot is the prior snapshot and only the fields that changed * are actually stored in the next snapshot. So: * * console.log(store.snapshots[1] is store.snapshots[2].__proto__) * # true * * The Store also keeps the equivalent of a database index on uniqueIDField and keeps a pointer to the last snapshot * for each particular uniqueIDField. This provides a convenient way to do per entity analysis. * * console.log(store.byUniqueID[&#39;1&#39;].snapshots[0].RecordID) * # 1 * * console.log(store.byUniqueID[&#39;1&#39;].lastSnapshot.RecordID) * # 1000 * * @constructor * * @param {Object} config See Config options for details. * @param {Object[]} [snapshots] Optional parameter allowing the population of the Store at instantiation. * * @cfg {String} [uniqueIDField = &quot;_EntityID&quot;] Specifies the field that identifies unique entities. * @cfg {String} [validFromField = &quot;_ValidFrom&quot;] * @cfg {String} [validToField = &quot;_ValidTo&quot;] * @cfg {String} [idField = &quot;_id&quot;] * @cfg {String} [tz = &quot;GMT&quot;] * @cfg {Object} [defaultValues = {}] In some datastores, null numeric fields may be assumed to be zero and null * boolean fields may be assumed to be false. Lumenize makes no such assumption and will crash if a field value * is missing. the defaultValues becomes the root of prototype inheritance hierarchy. * */ /* &lt;CoffeeScript&gt; &lt;/CoffeeScript&gt; */ <span id='Lumenize-Store-property-snapshots'> /** </span> * @property snapshots * @member Lumenize.Store * An Array of Objects * * The snapshots in compressed (via JavaScript inheritance) format */ /* &lt;CoffeeScript&gt; &lt;/CoffeeScript&gt; */ <span id='Lumenize-Store-property-fields'> /** </span> * @property fields * @member Lumenize.Store * An Array of Strings * * The list of all fields that this Store has ever seen. Use to expand each row. */ /* &lt;CoffeeScript&gt; &lt;/CoffeeScript&gt; */ <span id='Lumenize-Store-property-byUniqueID'> /** </span> * @property byUniqueID * @member Lumenize.Store * This is the database equivalent of an index by uniqueIDField. * * An Object in the form: * * { * &#39;1234&#39;: { * snapshots: [...], * lastSnapshot: &lt;points to last snapshot for this uniqueID&gt; * }, * &#39;7890&#39;: { * ... * }, * ... * } */ /* &lt;CoffeeScript&gt; @config = utils.clone(@userConfig) unless @config.uniqueIDField? @config.uniqueIDField = &#39;_EntityID&#39; unless @config.validFromField? @config.validFromField = &#39;_ValidFrom&#39; unless @config.validToField? @config.validToField = &#39;_ValidTo&#39; unless @config.tz? @config.tz = &#39;GMT&#39; unless @config.defaultValues? @config.defaultValues = {} @config.defaultValues[@config.validFromField] = new Time(1, Time.MILLISECOND).toString() unless @config.idField? @config.idField = &#39;_id&#39; @snapshots = [] @fields = [@config.validFromField, @config.validToField, &#39;_PreviousValues&#39;, @config.uniqueIDField] @lastValidFrom = new Time(1, Time.MILLISECOND).toString() @byUniqueID = {} @addSnapshots(snapshots) addSnapshots: (snapshots) -&gt; &lt;/CoffeeScript&gt; */ <span id='Lumenize-Store-method-addSnapshots'> /** </span> * @method addSnapshots * @member Lumenize.Store * Adds the snapshots to the Store * @param {Object[]} snapshots * @chainable * @return {Store} Returns this * */ /* &lt;CoffeeScript&gt; snapshots = utils._.sortBy(snapshots, @config.validFromField) for s in snapshots uniqueID = s[@config.uniqueIDField] utils.assert(uniqueID?, &quot;Missing #{@config.uniqueIDField} field in submitted snapshot: \n&quot; + JSON.stringify(s, null, 2)) dataForUniqueID = @byUniqueID[uniqueID] unless dataForUniqueID? # First time we&#39;ve seen this uniqueID dataForUniqueID = snapshots: [] lastSnapshot: @config.defaultValues @byUniqueID[uniqueID] = dataForUniqueID if s[@config.validFromField] &lt; dataForUniqueID.lastSnapshot[@config.validFromField] throw new Error(&quot;Got a new snapshot for a time earlier than the prior last snapshot for #{@config.uniqueIDField} #{uniqueID}.&quot;) # Eventually, we may have to handle this case. I should be able to enable _nextSnapshot and stitch a snapshot in between two existing ones else if s[@config.validFromField] is dataForUniqueID.lastSnapshot[@config.validFromField] for key, value of s dataForUniqueID.lastSnapshot[key] = value else validFrom = s[@config.validFromField] validFrom = new Time(validFrom, null, @config.tz).getISOStringInTZ(@config.tz) utils.assert(validFrom &gt;= dataForUniqueID.lastSnapshot[@config.validFromField], &quot;validFromField (#{validFrom}) must be &gt;= lastValidFrom (#{dataForUniqueID.lastSnapshot[@config.validFromField]}) for this entity&quot; ) # !TODO: Deal with out of order snapshots utils.assert(validFrom &gt;= @lastValidFrom, &quot;validFromField (#{validFrom}) must be &gt;= lastValidFrom (#{@lastValidFrom}) for the Store&quot;) validTo = s[@config.validTo] if validTo? validTo = new Time(validTo, null, @config.tz).getISOStringInTZ(@config.tz) else validTo = INFINITY priorSnapshot = dataForUniqueID.lastSnapshot # Build new Snapshot for adding newSnapshot = {} newSnapshot._PreviousValues = {} for key, value of s unless key in [@config.validFromField, @config.validToField, &#39;_PreviousValues&#39;, @config.uniqueIDField] unless key in @fields @fields.push(key) unless value == priorSnapshot[key] newSnapshot[key] = value unless key in [@config.idField] if priorSnapshot[key]? newSnapshot._PreviousValues[key] = priorSnapshot[key] else newSnapshot._PreviousValues[key] = null newSnapshot[@config.uniqueIDField] = uniqueID newSnapshot[@config.validFromField] = validFrom newSnapshot[@config.validToField] = validTo if s._PreviousValues? newSnapshot._PreviousValues = s._PreviousValues newSnapshot.__proto__ = priorSnapshot # Update priorSnapshot if priorSnapshot[@config.validToField] is INFINITY priorSnapshot[@config.validToField] = validFrom # priorSnapshot._NextSnapshot = newSnapshot # Adding link to next snapshot in case we want to do smart insertion later # Update metadata dataForUniqueID.lastSnapshot = newSnapshot @lastValidFrom = validFrom # Add the newSnapshot to the arrays @byUniqueID[uniqueID].snapshots.push(newSnapshot) @snapshots.push(newSnapshot) return this filtered: (filter) -&gt; &lt;/CoffeeScript&gt; */ <span id='Lumenize-Store-method-filtered'> /** </span> * @method filtered * @member Lumenize.Store * Returns the subset of the snapshots that match the filter * @param {Function} filter * @return {Object[]} An array of snapshots. Note, they will not be flattened so they have references to their prototypes */ /* &lt;CoffeeScript&gt; result = [] for s in @snapshots if filter(s) result.push(s) return result stateBoundaryCrossedFiltered: (field, values, valueToTheRightOfBoundary, forward = true, assumeNullIsLowest = true) -&gt; &lt;/CoffeeScript&gt; */ <span id='Lumenize-Store-method-stateBoundaryCrossedFiltered'> /** </span> * @method stateBoundaryCrossedFiltered * @member Lumenize.Store * Returns the subset of the snapshots where the field transitions from the left of valueToTheRightOfBoundary to * the right (inclusive) * @param {String} field * @param {String[]} values * @param {String} valueToTheRightOfBoundary * @param {Boolean} [forward = true] When true (the default), this will return the transitions from left to right * However, if you set this to false, it will return the transitions right to left. * @param {Boolean} [assumeNullIsLowest = true] Set to false if you don&#39;t want to consider transitions out of null * @return {Object[]} An array or snapshots. Note, they will not be flattened so they have references to their prototypes */ /* &lt;CoffeeScript&gt; index = values.indexOf(valueToTheRightOfBoundary) utils.assert(index &gt;= 0, &quot;stateToTheRightOfBoundary must be in stateList&quot;) left = values.slice(0, index) if assumeNullIsLowest left.unshift(null) right = values.slice(index) if forward filter = (s) -&gt; s._PreviousValues.hasOwnProperty(field) and s._PreviousValues[field] in left and s[field] in right else filter = (s) -&gt; s._PreviousValues.hasOwnProperty(field) and s._PreviousValues[field] in right and s[field] in left return @filtered(filter) stateBoundaryCrossedFilteredBothWays: (field, values, valueToTheRightOfBoundary, assumeNullIsLowest = true) -&gt; &lt;/CoffeeScript&gt; */ <span id='Lumenize-Store-method-stateBoundaryCrossedFilteredBothWays'> /** </span> * @method stateBoundaryCrossedFilteredBothWays * @member Lumenize.Store * Shortcut to stateBoundaryCrossedFiltered for when you need both directions * @param {String} field * @param {String[]} values * @param {String} valueToTheRightOfBoundary * @param {Boolean} [assumeNullIsLowest = true] Set to false if you don&#39;t want to consider transitions out of null * @return {Object} An object with two root keys: 1) forward, 2) backward. The values are the arrays that are returned * from stateBoundaryCrossedFiltered */ /* &lt;CoffeeScript&gt; forward = @stateBoundaryCrossedFiltered(field, values, valueToTheRightOfBoundary, true, assumeNullIsLowest) backward = @stateBoundaryCrossedFiltered(field, values, valueToTheRightOfBoundary, false, assumeNullIsLowest) return {forward, backward} exports.Store = Store &lt;/CoffeeScript&gt; */</pre> </body> </html>