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('./util/base');
var CategorialTransform = require('./facet/categorial-transform');
var ContinuousTransform = require('./facet/continuous-transform');
var datetimeTransform = require('./facet/datetime-transform');
var durationTransform = require('./facet/duration-transform');
var textTransform = require('./facet/text-transform');
var moment = require('moment-timezone');

module.exports = BaseModel.extend({
  initialize: function () {
    this.on('change:type', function (facet, newval) {
      // reset transformations on type change
      this.continuousTransform.reset();
      this.categorialTransform.reset();
      this.datetimeTransform.reset();
      this.durationTransform.reset();
    });
  },
  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}
     */
    isActive: ['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 a well defined set of labels and tags)
     *  * `datetime`        The facet is a datetime (using momentjs.tz)
     *  * `duration`        The facet is a duration (using momentjs.duration)
     *  * `text`            Freeform text.
     * Check for facet type using isConstant, isContinuous, isCategorial, isDatetime, isDuration, or isText  properties.
     * @memberof! Facet
     * @type {string}
     */
    type: {
      type: 'string',
      required: true,
      default: 'categorial',
      values: ['constant', 'continuous', 'categorial', 'datetime', 'duration', 'text']
    },

    /**
     * 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',

    /**
     * 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'
  },
  children: {
    /**
     * A categorial transformation to apply to the data
     * @memberof! Facet
     * @type {CategorialTransform}
     */
    categorialTransform: CategorialTransform,
    /**
     * A datetime transformation to apply to the data
     * @memberof! Facet
     * @type {dateimeTransform}
     */
    datetimeTransform: datetimeTransform,
    /**
     * A duration transformation to apply to the data
     * @memberof! Facet
     * @type {dateimeTransform}
     */
    durationTransform: durationTransform,
    /**
     * A continuous transformation to apply to the data
     * @memberof! Facet
     * @type {ContinuousTransform}
     */
    continuousTransform: ContinuousTransform,
    /**
     * A text transform
     * @memberof! Facet
     * @type {TextTransform}
     */
    textTransform: textTransform
  },

  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';
      }
    },
    isDatetime: {
      deps: ['type'],
      fn: function () {
        return this.type === 'datetime';
      }
    },
    isDuration: {
      deps: ['type'],
      fn: function () {
        return this.type === 'duration';
      }
    },
    isText: {
      deps: ['type'],
      fn: function () {
        return this.type === 'text';
      }
    },

    /**
     * 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 {
          if (this.misvalAsText !== null) {
            return JSON.parse('[' + this.misvalAsText + ']');
          } else {
            return [];
          }
        } catch (e) {
          return [];
        }
      },
      cache: false
    },

    /**
     * For continuous or datetime Facets, the minimum value.
     * @memberof! Facet
     * @type {number|datetime}
     * @readonly
     */
    minval: {
      deps: ['minvalAsText', 'type'],
      fn: function () {
        var min;
        if (this.isContinuous) {
          min = parseFloat(this.minvalAsText);
          if (isNaN(min)) {
            min = 0;
          }
        } else if (this.isDatetime) {
          min = moment(this.minvalAsText, moment.ISO_8601);
          if (!min.isValid()) {
            min = moment('2010-01-01 00:00', moment.ISO_8601);
          }
        } else if (this.isDuration) {
          min = moment.duration(this.minvalAsText);
          if (!moment.isDuration(min)) {
            min = moment.duration(1, 'seconds');
          }
        }
        return min;
      },
      cache: false
    },
    /**
     * For continuous or datetime Facets, the maximum value.
     * @memberof! Facet
     * @type {number|datetime}
     * @readonly
     */
    maxval: {
      deps: ['maxvalAsText', 'type'],
      fn: function () {
        var max;
        if (this.isContinuous) {
          max = parseFloat(this.maxvalAsText);
          if (isNaN(max)) {
            max = 100;
          }
        } else if (this.isDatetime) {
          max = moment(this.maxvalAsText, moment.ISO_8601);
          if (!max.isValid()) {
            max = moment('2020-01-01 00:00', moment.ISO_8601);
          }
        } else if (this.isDuration) {
          max = moment.duration(this.maxvalAsText);
          if (!moment.isDuration(max)) {
            max = moment.duration(100, 'seconds');
          }
        }
        return max;
      },
      cache: false
    },
    transform: {
      deps: ['type'],
      fn: function () {
        if (this.isContinuous) {
          return this.continuousTransform;
        } else if (this.isCategorial) {
          return this.categorialTransform;
        } else if (this.isDatetime) {
          return this.datetimeTransform;
        } else if (this.isDuration) {
          return this.durationTransform;
        } else if (this.isText) {
          return this.textTransform;
        }
        console.error('Invalid facet');
      },
      cache: false
    }
  },
  /**
   * setMinMax sets the range of a continuous or time facet
   * For facets in a dataview, the minimum is just the minimum of the facet over all active datasets,
   * and the same for the maximum.
   * For facets in a datset, the actual implementation is in the dataset driver.
   *
   * @memberof! Facet
   */
  setMinMax: function () {
    var Dataset = require('./dataset');
    var Dataview = require('./dataview');

    var ancestor = this.collection.parent;
    var spot;

    if (ancestor instanceof Dataview) {
      // Facet -> Facets -> Dataview -> Spot
      spot = this.collection.parent.parent;
      spot.setFacetMinMax(this);
    } else if (ancestor instanceof Dataset) {
      // Dataset -> Datasets -> Spot
      spot = ancestor.collection.parent;
      spot.driver.setMinMax(ancestor, this);
    }
  },
  /**
   * setCategories finds finds all values on an ordinal (categorial) axis
   * Updates the categorialTransform of the facet
   * For facets in a dataview, this is the union of the categories of facet over all active datasets.
   * For facets in a dataset, the actual implementation is in the dataset driver.
   *
   * @memberof! Facet
   */
  setCategories: function () {
    var Dataset = require('./dataset');
    var Dataview = require('./dataview');

    var ancestor = this.collection.parent;
    var spot;

    if (ancestor instanceof Dataview) {
      // Facet -> Facets -> Dataview -> Spot
      spot = this.collection.parent.parent;
      spot.setFacetCategories(this);
    } else if (ancestor instanceof Dataset) {
      // Facet -> Facets -> Dataset -> Datasets -> Spot
      spot = ancestor.collection.parent;
      spot.driver.setCategories(ancestor, this);
    }
  }
});