partition.js

/**
 * Partition
 *
 * Describes a partitioning of the data, based on the values a Facet can take.
 *
 * @class Partition
 * @extends Base
 */
var BaseModel = require('./util/base');
var Groups = require('./partition/group-collection');
var moment = require('moment-timezone');
var selection = require('./util/selection');
var util = require('./util/time');

/*
 * @param {Partition} partition
 * @memberof! Partition
 */
function setDatetimeGroups (partition) {
  var timeStart = partition.minval;
  var timeEnd = partition.maxval;
  var timeRes = util.getDatetimeResolution(timeStart, timeEnd);
  var timeZone = partition.zone;

  partition.groups.reset();

  var current = moment(timeStart);
  while ((!current.isAfter(timeEnd)) && partition.groups.length < 500) {
    partition.groups.add({
      min: moment(current).tz(timeZone).startOf(timeRes),
      max: moment(current).tz(timeZone).endOf(timeRes),
      value: moment(current).tz(timeZone).startOf(timeRes).format(),
      label: moment(current).tz(timeZone).startOf(timeRes).format()
    });
    current.add(1, timeRes);
  }
}

/*
 * @param {Partition} partition
 * @memberof! Partition
 */
function setDurationGroups (partition) {
  var dStart = partition.minval;
  var dEnd = partition.maxval;
  var dRes = util.getDurationResolution(dStart, dEnd);

  partition.groups.reset();

  var current = Math.floor(parseFloat(dStart.as(dRes)));
  var last = Math.floor(parseFloat(dEnd.as(dRes)));

  while (current < last) {
    partition.groups.add({
      min: moment.duration(current, dRes),
      max: moment.duration(current + 1, dRes),
      value: moment.duration(current, dRes).toISOString(),
      label: moment.duration(current, dRes).toISOString()
    });

    current = current + 1;
  }
}

/*
 * Setup a grouping based on the `partition.groupingContinuous`, `partition.minval`,
 * `partition.maxval`, and the `partition.groupingParam`.
 * @memberof! Partition
 * @param {Partition} partition
 */
function setContinuousGroups (partition) {
  var param = partition.groupingParam;
  var x0, x1, size, nbins;

  if (partition.groupFixedN) {
    // A fixed number of equally sized bins
    nbins = param;
    x0 = partition.minval;
    x1 = partition.maxval;
    size = (x1 - x0) / nbins;
  } else if (partition.groupFixedS) {
    // A fixed bin size
    size = param;
    x0 = Math.floor(partition.minval / size) * size;
    x1 = Math.ceil(partition.maxval / size) * size;
    nbins = (x1 - x0) / size;
  } else if (partition.groupFixedSC) {
    // A fixed bin size, centered on 0
    size = param;
    x0 = (Math.floor(partition.minval / size) - 0.5) * size;
    x1 = (Math.ceil(partition.maxval / size) + 0.5) * size;
    nbins = (x1 - x0) / size;
  } else if (partition.groupLog) {
    // Fixed number of logarithmically (base 10) sized bins
    nbins = param;
    x0 = Math.log(partition.minval) / Math.log(10.0);
    x1 = Math.log(partition.maxval) / Math.log(10.0);
    size = (x1 - x0) / nbins;
  }

  // and update partition.groups
  partition.groups.reset();
  partition.ordering = 'abc';

  function unlog (x) {
    return Math.exp(x * Math.log(10));
  }

  var i;
  for (i = 0; i < nbins; i++) {
    var start = x0 + i * size;
    var end = x0 + (i + 1) * size;
    var mid = 0.5 * (start + end);

    if (partition.groupLog) {
      partition.groups.add({
        min: unlog(start),
        max: unlog(end),
        value: unlog(start),
        label: unlog(end).toPrecision(5)
      });
    } else {
      partition.groups.add({
        min: start,
        max: end,
        value: mid,
        label: mid.toPrecision(5)
      });
    }
  }
}

/*
 * Setup a grouping based on the `partition.categorialTransform`
 * @memberof! Partition
 * @param {Partition} partition
 */
function setCategorialGroups (partition) {
  // and update partition.groups
  partition.groups.reset();
  partition.ordering = 'abc';

  // partition -> partitions -> filter -> filters -> dataset
  var filter = partition.collection.parent;
  var dataset = filter.collection.parent;
  var facet = dataset.facets.get(partition.facetName, 'name');

  if (facet.isCategorial) {
    // default: a categorial facet, with a categorial parittion
    facet.categorialTransform.rules.forEach(function (rule) {
      partition.groups.add({
        value: rule.group,
        label: rule.group,
        count: rule.count
      });
    });
  } else if (facet.isDatetime) {
    var format = facet.datetimeTransform.transformedFormat;
    var timePart = util.timeParts.get(format, 'description');

    timePart.groups.forEach(function (g) {
      partition.groups.add({
        value: g,
        label: g,
        count: 0
      });
    });
  } else {
    console.warn('Not implemented');
  }
}

/**
 * Setup the partition.groups()
 *
 * @memberof! Partition
 * @param {Partition} partition
 */
function setGroups () {
  var partition = this;

  if (partition.isCategorial) {
    setCategorialGroups(partition);
  } else if (partition.isContinuous) {
    setContinuousGroups(partition);
  } else if (partition.isDatetime) {
    setDatetimeGroups(partition);
  } else if (partition.isDuration) {
    setDurationGroups(partition);
  } else if (partition.isText) {
    partition.groups.reset();
  } else {
    console.error('Cannot set groups for partition', partition.getId());
  }
}

/**
 * Reset type, minimum and maximum values
 * @params {Partition} partition
 * @params {Object} Options - silent do not trigger change events
 * @memberof! Partition
 */
function reset (options) {
  var partition = this;
  // partition -> partitions -> filter -> filters -> dataview
  var filter = partition.collection.parent;
  var dataview = filter.collection.parent;
  var facet = dataview.facets.get(partition.facetName, 'name');
  options = options || {};

  partition.set({
    type: facet.transform.transformedType,
    minval: facet.transform.transformedMin,
    maxval: facet.transform.transformedMax
  }, options);
}

module.exports = BaseModel.extend({
  dataTypes: {
    'numberDatetimeOrDuration': {
      set: function (value) {
        var newValue;

        // check for momentjs objects
        if (moment.isDuration(value)) {
          return {
            val: moment.duration(value),
            type: 'numberDatetimeOrDuration'
          };
        }
        if (moment.isMoment(value)) {
          return {
            val: value.clone(),
            type: 'numberDatetimeOrDuration'
          };
        }

        // try to create momentjs objects
        newValue = moment(value, moment.ISO_8601);
        if (newValue.isValid()) {
          return {
            val: newValue,
            type: 'numberDatetimeOrDuration'
          };
        }
        if (typeof value === 'string' && value[0].toLowerCase() === 'p') {
          newValue = moment.duration(value);
          return {
            val: newValue,
            type: 'numberDatetimeOrDuration'
          };
        }

        // try to set a number
        if (value === +value) {
          return {
            val: +value,
            type: 'numberDatetimeOrDuration'
          };
        }

        // failed..
        return {
          val: value,
          type: typeof value
        };
      },
      compare: function (currentVal, newVal) {
        if (currentVal instanceof moment) {
          return currentVal.isSame(newVal);
        } else {
          return +currentVal === +newVal;
        }
      }
    }
  },
  props: {
    /**
     * Label for displaying on plots
     * @memberof! Partition
     * @type {string}
     */
    label: {
      type: 'string',
      required: true,
      default: ''
    },
    /**
     * Show a legend for this partition
     * @memberof! Partition
     * @type {string}
     */
    showLegend: {
      type: 'boolean',
      required: false,
      default: true
    },
    /**
     * Show an axis label for this partition
     * @memberof! Partition
     * @type {string}
     */
    showLabel: {
      type: 'boolean',
      required: false,
      default: true
    },

    /**
     * Timezone for partitioning
     * @memberof! DatetimeTransform
     * @type {string}
     */
    zone: {
      type: 'string',
      required: 'true',
      default: function () {
        return moment.tz.guess();
      }
    },

    /**
     * Type of this partition
     * @memberof! Partition
     * @type {string}
     */
    type: {
      type: 'string',
      required: true,
      default: 'categorial',
      values: ['constant', 'continuous', 'categorial', 'datetime', 'duration', 'text']
    },

    /**
     * The ID of the facet to partition over
     * @memberof! Partition
     * @type {string}
     */
    facetName: 'string',

    /**
     * When part of a partitioning, this deterimines the ordering
     * @memberof! Partition
     * @type {number}
     */
    rank: {
      type: 'number',
      required: true
    },

    /**
     * For categorial and text Facets, the ordering can be alfabetical or by count
     * @memberof! Partition
     * @type {number|moment}
     */
    ordering: {
      type: 'string',
      values: ['count', 'abc'],
      default: 'abc'
    },

    /**
     * For continuous or datetime Facets, the minimum value. Values lower than this are grouped to 'missing'
     * @memberof! Partition
     * @type {number|moment}
     */
    minval: 'numberDatetimeOrDuration',

    /**
     * For continuous or datetime Facets, the maximum value. Values higher than this are grouped to 'missing'
     * @memberof! Partition
     * @type {number|moment}
     */
    maxval: 'numberDatetimeOrDuration',

    /**
     * Extra parameter used in the grouping strategy: either the number of bins, or the bin size.
     * @memberof! Partition
     * @type {number}
     */
    groupingParam: ['number', true, 15],

    /**
     * Grouping strategy:
     *  * `fixedn`  fixed number of bins in the interval [minval, maxval]
     *  * `fixedsc` a fixed binsize, centered on zero
     *  * `fixeds`  a fixed binsize, starting at zero
     *  * `log`     fixed number of bins but on a logarithmic scale
     * Don't use directly but check grouping via the groupFixedN, groupFixedSC,
     * groupFixedS, and groupLog properties
     * @memberof! Partition
     * @type {number}
     */
    groupingContinuous: {
      type: 'string',
      required: true,
      default: 'fixedn',
      values: ['fixedn', 'fixedsc', 'fixeds', 'log']
    },

    /**
     * Depending on the type of partition, this can be an array of the selected groups,
     * or a numberic interval [start, end]
     * @memberof! Partition
     * @type {array}
     */
    // NOTE: for categorial facets, contains rule.group
    selected: {
      type: 'array',
      required: true,
      default: function () {
        return [];
      }
    }
  },
  collections: {
    /**
     * The (ordered) set of groups this Partition can take, making up this partition.
     * Used for plotting
     * @memberof! Partition
     * @type {Group[]}
     */
    groups: Groups
  },
  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';
      }
    },
    // properties for grouping-continuous
    groupFixedN: {
      deps: ['groupingContinuous'],
      fn: function () {
        return this.groupingContinuous === 'fixedn';
      }
    },
    groupFixedSC: {
      deps: ['groupingContinuous'],
      fn: function () {
        return this.groupingContinuous === 'fixedsc';
      }
    },
    groupFixedS: {
      deps: ['groupingContinuous'],
      fn: function () {
        return this.groupingContinuous === 'fixeds';
      }
    },
    groupLog: {
      deps: ['groupingContinuous'],
      fn: function () {
        return this.groupingContinuous === 'log';
      }
    }
  },
  updateSelection: function (group) {
    selection.updateSelection(this, group);
  },
  filterFunction: function () {
    return selection.filterFunction(this);
  },
  setGroups: setGroups,
  reset: reset
});