models/facet.js

/**
 * Facets are the main abstraction over the data.
 *
 * A `Dataset` is a collection of (similar) items, with each item having a certain set of properties, ie. `Facet`s.
 * The `Facet` class defines the property: It can be a continuous value, a set of labels or tags,
 * or it can be result of some transformation or equation.
 *
 * @class Facet
 * @extends Base
 */
var BaseModel = require('./base');
var CategorialTransform = require('./categorial-transform');
var ContinuousTransform = require('./continuous-transform');
var TimeTransform = require('./time-transform');
var moment = require('moment-timezone');

module.exports = BaseModel.extend({
  initialize: function () {
    this.on('change:type', function (facet, newval) {
      // clear transformations on type change
      this.continuousTransform.clear();
      this.categorialTransform.clear();
      this.timeTransform.clear();

      // set default values for transformations
      // NOTE: this could be done in the transformation models using Ampersand default values,
      //       howerver, then they would show up on the model.toJSON(), making the session files
      //       much more cluttered and human-unfriendly
      if (newval === 'timeorduration') {
        facet.timeTransform.type = 'datetime';
      }
    });
  },
  props: {
    /**
     * Show in facet lists (used for interactive searching on Facets page)
     * @memberof! Facet
     * @type {boolean}
     */
    show: ['boolean', false, true],

    /**
     * Show facet bar (on Analyze page)
     * @memberof! Facet
     * @type {boolean}
     */
    active: ['boolean', false, false],

    // general facet properties
    /**
     * Description of this facet, for displaying purposes
     * @memberof! Facet
     * @type {string}
     */
    description: ['string', true, ''],

    /**
     * For continuous facets, its units for displaying purposes
     * @memberof! Facet
     * @type {string}
     */
    units: ['string', true, ''],

    /**
     * Short name for this facet, for displaying purposes
     * @memberof! Facet
     * @type {string}
     */
    name: ['string', true, ''],

    /**
     * Type of this facet:
     *  * `constant`        A constant value of "1" for all data items
     *  * `continuous`      The facet takes on real numbers
     *  * `categorial`      The facet is a string, or an array of strings (for labels and tags)
     *  * `timeorduration`  The facet is a datetime (using momentjs)
     * Check for facet type using isConstant, isContinuous, isCategorial, or isTimeOrDuration properties.
     * @memberof! Facet
     * @type {string}
     */
    type: {
      type: 'string',
      required: true,
      default: 'categorial',
      values: ['constant', 'continuous', 'categorial', 'timeorduration']
    },

    /**
     * The accessor for this facet.
     * For nested properties use dot notation: For a dataset `[ {name: {first: "Santa", last: "Claus"}}, ...]`
     * you can use `name.first` and `name.last` to get Santa and Claus, respectively.
     *
     * @memberof! Facet
     * @type {string}
     */
    accessor: ['string', false, null],

    /**
     * Missing or invalid data indicator; for multiple values, use a comma separated, quoted list
     * Numbers, strings, booleans, and the special value null are allowed.
     * Use single or double quotes for strings "missing".
     * The parsed values are available in the misval property.
     *
     * @memberof! Facet
     * @type {string}
     */
    misvalAsText: ['string', true, 'null'],

    /**
     * For continuous or datetime Facets, the minimum value as text.
     * Parsed value available in the `minval` property
     * @memberof! Facet
     * @type {string}
     */
    minvalAsText: 'string',
    /**
     * For continuous or datetime Facets, the maximum value as text.
     * Parsed value available in the `maxval` property
     * @memberof! Facet
     * @type {string}
     */
    maxvalAsText: 'string',

    transformType: {
      type: 'string',
      required: true,
      default: 'none',
      values: ['none', 'percentiles', 'exceedances']
    }
  },
  collections: {
    /**
     * A categorial transformation to apply to the data
     * @memberof! Facet
     * @type {CategorialTransform}
     */
    categorialTransform: CategorialTransform,

    /**
     * A continuous transformation to apply to the data
     * @memberof! Facet
     * @type {ContinuousTransform}
     */
    continuousTransform: ContinuousTransform
  },
  children: {
    /**
     * A time (or duration) transformation to apply to the data
     * @memberof! Facet
     * @type {TimeTransform}
     */
    timeTransform: TimeTransform
  },

  derived: {

    // properties for: type
    isConstant: {
      deps: ['type'],
      fn: function () {
        return this.type === 'constant';
      }
    },
    isContinuous: {
      deps: ['type'],
      fn: function () {
        return this.type === 'continuous';
      }
    },
    isCategorial: {
      deps: ['type'],
      fn: function () {
        return this.type === 'categorial';
      }
    },
    isTimeOrDuration: {
      deps: ['type'],
      fn: function () {
        return this.type === 'timeorduration';
      }
    },

    /**
     * The actual type of the facet after transformation.
     * Do not check directly, but use `displayConstant`, `displayContinuous`, `displayCategorial`, `displayDatetime`
     * @memberof! Facet
     * @type {string}
     * @readonly
     */
    displayType: {
      deps: ['type', 'timeTransform.transformedType'],
      fn: function () {
        if (this.type === 'timeorduration') {
          return this.timeTransform.transformedType;
        }
        return this.type;
      }
    },
    displayConstant: {
      deps: ['displayType'],
      fn: function () {
        return this.displayType === 'constant';
      }
    },
    displayContinuous: {
      deps: ['displayType'],
      fn: function () {
        return this.displayType === 'continuous';
      }
    },
    displayCategorial: {
      deps: ['displayType'],
      fn: function () {
        return this.displayType === 'categorial';
      }
    },
    displayDatetime: {
      deps: ['displayType'],
      fn: function () {
        return this.displayType === 'datetime';
      }
    },

    /**
     * Array of missing data indicators
     * @memberof! Facet
     * @type {Object[]}
     * @readonly
     */
    misval: {
      deps: ['misvalAsText'],
      fn: function () {
        // Parse the text content as a JSON array:
        //  - strings should be quoted
        //  - numbers unquoated
        //  - special numbers not allowed: NaN, Infinity
        try {
          return JSON.parse('[' + this.misvalAsText + ']');
        } catch (e) {
          return [null];
        }
      },
      cache: false
    },

    /**
     * For continuous or datetime Facets, the minimum value.
     * @memberof! Facet
     * @type {number|datetime}
     * @readonly
     */
    minval: {
      deps: ['minvalAsText', 'displayType'],
      fn: function () {
        if (this.displayType === 'continuous') {
          return parseFloat(this.minvalAsText);
        } else if (this.displayType === 'datetime') {
          return moment(this.minvalAsText);
        } else if (this.displayType === 'categorial') {
          return 0;
        }
      }
    },
    /**
     * For continuous or datetime Facets, the maximum value.
     * @memberof! Facet
     * @type {number|datetime}
     * @readonly
     */
    maxval: {
      deps: ['maxvalAsText', 'displayType'],
      fn: function () {
        if (this.displayType === 'continuous') {
          return parseFloat(this.maxvalAsText);
        } else if (this.displayType === 'datetime') {
          return moment(this.maxvalAsText);
        } else if (this.displayType === 'categorial') {
          return 1;
        }
      }
    },
    transformNone: {
      deps: ['transformType'],
      fn: function () {
        return this.transformType === 'none';
      }
    },
    transformPercentiles: {
      deps: ['transformType'],
      fn: function () {
        return this.transformType === 'percentiles';
      }
    },
    transformExceedances: {
      deps: ['transformType'],
      fn: function () {
        return this.transformType === 'exceedances';
      }
    }
  }
});