filter.js

/**
 * A filter provides a chart with an interface to the data.
 * The filter contains a number of `Partition`s and `Aggregate`s.
 * It takes care of calling the relevant functions provided by a `Dataset`.
 *
 * @class Filter
 * @extends Base
 */

/**
 * @typedef {Object} DataRecord - Object holding the plot data, partitions are labelled with a single small letter, aggregates with a double small letter
 * @property {string} DataRecord.a Value of first partition
 * @property {string} DataRecord.b Value of second partition
 * @property {string} DataRecord.c Value of third partition, etc.
 * @property {string} DataRecord.aa Value of first aggregate
 * @property {string} DataRecord.bb Value of second aggregate, etc.
 */

/**
 * @typedef {DataRecord[]} Data - Array of DataRecords
 */

var Base = require('./util/base');
var Aggregates = require('./aggregate/collection');
var Partitions = require('./partition/collection');

module.exports = Base.extend({
  props: {
    /**
     * Hint for the client (website) how to visualize this filter
     * @memberof! Filter
     * @type {string}
     */
    chartType: {
      type: 'string',
      required: true,
      default: 'barchart',
      values: ['piechart', 'horizontalbarchart', 'barchart', 'linechart', 'radarchart', 'polarareachart', 'bubbleplot', 'scatterchart', 'networkchart']
    },
    /**
     * Title for displaying purposes
     * @memberof! Filter
     * @type {string}
     */
    title: ['string', true, ''],
    /**
     * Hint for the client (website) how to position the chart for this filter
     * position (col, row) and size (size_x, size_y) of chart
     */
    col: 'number',
    row: 'number',
    size_x: 'number',
    size_y: 'number'
  },
  collections: {
    /**
     * @memberof! Filter
     * @type {Partitions[]}
     */
    partitions: Partitions,
    /**
     * @memberof! Filter
     * @type {Aggregate[]}
     */
    aggregates: Aggregates
  },
  // Session properties are not typically persisted to the server,
  // and are not returned by calls to toJSON() or serialize().
  session: {
    /**
     * Array containing the data to plot
     * @memberof! Filter
     * @type {Data}
     */
    data: {
      type: 'array',
      default: function () {
        return [];
      }
    },
    /*
     * Call this function to request new data.
     * The dataset backing the facet will copy the data to Filter.data.
     * A newData event is fired when the data is ready to be plotted.
     *
     * @function
     * @virtual
     * @private
     * @memberof! Filter
     * @emits newData
     */
    getData: {
      type: 'any'
    },
    /**
     * A history of the current drill-down (ie. partitions.toJSON())
     */
    zoomHistory: {
      type: 'array',
      default: function () {
        return [];
      }
    },
    /**
     * Boolean indicating if the filter is initialized
     */
    isInitialized: {
      type: 'boolean',
      required: true,
      default: false
    }
  },
  initialize: function () {
    // set up callback to free internal state on remove
    this.on('remove', function () {
      this.releaseDataFilter();
    });
  },
  zoomIn: function () {
    this.releaseDataFilter();

    // save current state
    this.zoomHistory.push(JSON.stringify(this.partitions.toJSON()));

    this.partitions.forEach(function (partition) {
      if ((partition.selected.length === 2) && (partition.isDatetime || partition.isContinuous)) {
        if (partition.groupFixedS || partition.groupFixedSC) {
          // scale down binsize
          var newSize = partition.selected[1] - partition.selected[0];
          var oldSize = partition.maxval - partition.minval;
          partition.groupingParam = partition.groupingParam * newSize / oldSize;
        }
        // zoom to selected range, if possible
        partition.set({
          minval: partition.selected[0],
          maxval: partition.selected[1]
        }, { silent: true });
        partition.setGroups();
      } else if (partition.selected.length > 0 && (partition.isCategorial)) {
        // zoom to selected categories, if possible
        partition.groups.reset();
        partition.selected.forEach(function (value) {
          partition.groups.add({
            value: value,
            label: value,
            count: 0,
            isSelected: true
          });
        });
      }
      // select all
      partition.updateSelection();
    });
    this.initDataFilter();
    this.updateDataFilter(); // also triggers a getAllData()
  },
  zoomOut: function () {
    var doReset = true;

    // clear current selection
    this.partitions.forEach(function (partition) {
      if (partition.selected.length > 0) {
        partition.updateSelection();
        doReset = false;
      }
    });

    if (doReset) {
      this.releaseDataFilter();
      if (this.zoomHistory.length > 0) {
        // nothing was selected and we have drilled down: go up
        var state = JSON.parse(this.zoomHistory.pop());
        this.partitions.reset(state);
      } else {
        // nothing was selected and no drill down: reset partitioning
        this.partitions.forEach(function (partition) {
          if (partition.isDatetime || partition.isContinuous) {
            partition.reset({ silent: true });
          }
          partition.setGroups();
        });
      }
      this.initDataFilter();
    }
    this.updateDataFilter(); // also triggers a getAllData()
  },
  // Apply the separate filterFunctions from each partition in a single function
  filterFunction: function () {
    var fs = [];
    this.partitions.forEach(function (partition) {
      fs.push(partition.filterFunction());
    });
    return function (d) {
      if (typeof d === 'string') {
        var groups = d.split('|');
        return fs.every(function (f, i) { return f(groups[i]); });
      } else {
        // shortcut for non-partitioned numeric data
        return fs[0](d);
      }
    };
  },
  /**
   * Initialize the data filter, and construct the getData callback function on the filter.
   *
   * @memberof! Filter
   */
  initDataFilter: function () {
    var dataview = this.collection.parent;
    var spot = dataview.parent;

    spot.driver.releaseDataFilter(dataview, this);
    spot.driver.initDataFilter(dataview, this);
    spot.driver.updateDataFilter(this);

    this.isInitialized = true;
  },
  /**
   * The opposite or initDataFilter, it should remove the filter and deallocate other configuration
   * related to the filter.
   *
   * @memberof! Filter
   */
  releaseDataFilter: function () {
    var dataview = this.collection.parent;
    var spot = dataview.parent;

    spot.driver.releaseDataFilter(dataview, this);

    this.isInitialized = false;
  },
  /**
   * Apply changes to the filter (like selecting groups)
   *
   * @memberof! Filter
   */
  updateDataFilter: function () {
    var dataview = this.collection.parent;
    var spot = dataview.parent;

    spot.driver.updateDataFilter(this);
  }
});