models/dataset.js

/**
 * A Dataset is responsible for actually managing the data: based on the filters and their factes,
 * implement callbacks that return the processed data in a standardized format.
 *
 * To help analyze data, a few methods to help autoconfigure your session must be implemented.
 *
 * Implementations for Crossfilter (fully in memory client side filtering) and PostgreSQL datasets are available.
 * @class Dataset
 */

var AmpersandModel = require('ampersand-model');
var Filters = require('./filter-collection');
var Facets = require('./facet-collection');

/*
 * Add implementation of (dataset specific) virutal functions to a facet
 */
function extendFacet (dataset, facet) {
  facet.setMinMax = function (transformed) {
    dataset.setMinMax(dataset, facet, transformed);
  };

  facet.sampleDataset = function (N) {
    return dataset.sampleDataset(dataset, N);
  };

  facet.setCategories = function () {
    dataset.setCategories(dataset, facet);
  };

  facet.setPercentiles = function () {
    dataset.setPercentiles(dataset, facet);
  };

  facet.setExceedances = function () {
    dataset.setExceedances(dataset, facet);
  };
}

/*
 * Add implementation of (dataset specific) virtual functions to a filter
 */
function extendFilter (dataset, filter) {
  filter.initDataFilter = function () {
    dataset.releaseDataFilter(dataset, filter);
    dataset.initDataFilter(dataset, filter);
    dataset.updateDataFilter(dataset, filter);
    dataset.getAllData(dataset);
  };
  filter.releaseDataFilter = function () {
    dataset.releaseDataFilter(dataset, filter);
    dataset.getAllData(dataset);
  };
  filter.updateDataFilter = function () {
    dataset.updateDataFilter(dataset, filter);
    dataset.getAllData(dataset);
  };
}

/*
 * Add implementation of (dataset specific) virutal functions to all facets
 */
function extendFacets (dataset) {
  dataset.facets.forEach(function (facet) {
    extendFacet(dataset, facet);
  });
}

/*
 * Add implementation of (dataset specific) virutal functions to all filters
 */
function extendFilters (dataset) {
  dataset.filters.forEach(function (filter) {
    extendFilter(dataset, filter);
  });
}

/*
 * Stubs for virtual functions
 */
function setPercentiles (dataset, facet) {
  console.error('Virtual method setPercentiles');
}

function setExceedances (dataset, facet) {
  console.error('Virtual method setExceedances');
}

function sampleDataset (dataset, N) {
  console.error('Virtual method sampleDataset');
}

function setMinMax (dataset, facet) {
  console.error('Virtual method setMinMax');
}

function setCategories (dataset, facet, transformed) {
  console.error('Virtual method setCategories');
}

function scanData () {
  console.error('Virtual method scanData');
}

function initDataFilter (filter) {
  console.error('Virtual method initDataFilter');
}

function releaseDataFilter (filter) {
  console.error('Virtual method releaseDataFilter');
}

function updateDataFilter (filter) {
  console.error('Virtual method updateDataFilter');
}

function getAllData (dataset) {
  if (dataset.isPaused) {
    return;
  }
  dataset.filters.forEach(function (filter, i) {
    if (filter.getData) {
      filter.getData();
    }
  });
}

module.exports = AmpersandModel.extend({
  props: {
    /**
     * Type of the dataset: crossfilter, server, generic (none)
     * @memberof! Dataset
     * @readonly
     * @type {string}
     */
    datasetType: {
      type: 'string',
      setOnce: true,
      values: ['client', 'server', 'generic'],
      default: 'generic'
    }
  },
  session: {
    /**
     * isPaused when true, calls to getAllData are ignored.
     * This is useful to suppres calls to getData
     * when adding and removing a number of filters at once.
     * @memberof! Dataset
     * @type {boolean}
     */
    isPaused: ['boolean', false, true]
  },
  initialize: function () {
    this.extendFacets(this);
    this.facets.on('add reset', function () {
      extendFacets(this);
    }, this);

    this.extendFilters(this);
    this.filters.on('add reset', function (filter, filters, options) {
      extendFilters(this);
    }, this);
  },
  collections: {
    /**
     * A Filter collection holding all active filters on the dataset
     * @memberof! Dataset
     * @type {Filter[]}
     */
    filters: Filters,
    /**
     * A Facet collection holding pre defined facets
     * @memberof! Dataset
     * @type {Facet[]}
     */
    facets: Facets
  },

  /**
   * Pause the dataset. This means calls to getData are blocked.
   * Useful when updating a lot of filters and you are not interested in the intermediate state.
   * @memberof Dataset
   */
  pause: function () {
    this.isPaused = true;
  },
  /**
   * Unpause the dataset
   * @memberof Dataset
   */
  play: function () {
    this.isPaused = false;
  },

  /**
   * getAllData
   * Refresh data for all filters, by calling getData for each filter in the filter collection.
   * @see pause and play
   * @memberof! Dataset
   * @function
   */
  getAllData: getAllData,

  /**
   * Autoconfigure a dataset:
   * 1. inspect the dataset, and create facets for the properties
   * 2. for continuous facets, guess the missing values, and set the minimum and maximum values
   * 3. for categorial facets, set the categorialTransform
   *
   * @memberof! Dataset
   * @function
   */
  scanData: scanData,

  /**
   * returns an array containing N datum objects
   * @memberof! Dataset
   * @param {number} N Number of objects to return
   * @virtual
   * @function
   * @returns {Object[]} data Array of objects
   */
  sampleDataset: sampleDataset,

  /**
   * setMinMax finds the range of a continuous facet,
   * @memberof! Facet
   * @virtual
   * @function
   */
  setMinMax: setMinMax,

  /**
   * setCategories finds finds all values on an ordinal (categorial) axis.
   * Updates the categorialTransform property of the facet
   *
   * @memberof! Facet
   * @virtual
   * @function
   */
  setCategories: setCategories,

  /**
   * Calculate 100 percentiles (ie. 1,2,3,4 etc.)
   * Use the recommended method from [NIST](http://www.itl.nist.gov/div898/handbook/prc/section2/prc262.htm)
   * See also the discussion on [Wikipedia](https://en.wikipedia.org/wiki/Percentile)
   *
   * @memberof! Facet
   * @virtual
   * @function
   */
  setPercentiles: setPercentiles,

  /**
   * Calculate value where exceedance probability is one in 10,20,30,40,50,
   * and the same for -exceedance -50, -60, -70, -80, -90, -99, -99.9, -99.99, ... percent
   * Approximate from data: 1 in 10 is larger than value at index trunc(0.1 * len(data))
   *
   * @memberof! Facet
   * @virtual
   * @function
   */
  setExceedances: setExceedances,

  /**
   * initDataFilter
   * Initialize the data filter, and construct the getData callback function on the filter.
   * @memberof! Filter
   * @function
   * @virtual
   */
  initDataFilter: initDataFilter,

  /**
   * relaseDataFilter
   * The opposite or initDataFilter, it should remove the filter and deallocate
   * other data related to the filter.
   * @memberof! Filter
   * @function
   * @virtual
   */
  releaseDataFilter: releaseDataFilter,

  /**
   * updateDataFilter
   * Change the filter parameters for an initialized filter
   * @memberof! Filter
   * @function
   * @virtual
   */
  updateDataFilter: updateDataFilter,

  /**
   * Extends a Facet by adding the dataset dependent callback functions:
   * `setMinMax`, `setCategories`, `setExceedances`, and `setPercentiles`
   * Automatically called when adding facets to the dataset
   * @memberof! Dataset
   * @function
   * @param {Dataset} dataset
   * @param {Facet} facet
   */
  extendFacet: extendFacet,
  extendFacets: extendFacets,

  /**
   * Extends a Filter by adding the dataset dependent callback functions:
   * `initDataFilter`, `updateDataFilter`, and `releaseDataFilter`
   * Automatically called when adding filters to the dataset
   * @memberof! Dataset
   * @function
   * @param {Dataset} dataset
   * @param {Filter} filter
   */
  extendFilter: extendFilter,
  extendFilters: extendFilters
});