util-selection.js

/**
 * Selection
 * @module client/util-selection
 */
var misval = require('./misval');
var moment = require('moment-timezone');

/*
 * Set a categorial 1D filter function
 * @param {Partition} partition
 */
function filterFunctionCategorial1D (partition) {
  var haystack = {};

  if (!partition.selected || !partition.selected.length) {
    partition.groups.forEach(function (group) {
      haystack[group.value] = true;
    });
  } else {
    partition.selected.forEach(function (h) {
      haystack[h] = true;
    });
  }

  return function (d) {
    var needle = d;
    if (!(needle instanceof Array)) {
      needle = [d];
    }

    var selected = false;
    needle.forEach(function (s) {
      selected = selected | haystack[s];
    });
    return !!selected;
  };
}

/*
 * Set a continuous 1D filter function
 * @param {Partition} partition
 */
function filterFunctionContinuous1D (partition) {
  var edge = partition.maxval;
  var min;
  var max;

  if (!partition.selected || !partition.selected.length) {
    min = partition.minval;
    max = partition.maxval;
    return function (d) {
      return ((d >= min && d <= max) && (d !== misval));
    };
  } else {
    min = partition.selected[0];
    max = partition.selected[1];
    return function (d) {
      return ((d >= min && d < max) || ((d === edge) && (max === edge))) && (d !== misval);
    };
  }
}

/*
 * Set a continuous 1D filter function on a time dimension
 * @param {Partition} partition
 */
function filterFunctionTime1D (partition) {
  var edge = partition.maxval;
  var min;
  var max;

  if (!partition.selected || !partition.selected.length) {
    min = partition.minval;
    max = partition.maxal;
    return function (d) {
      return ((d !== misval) && (d.isAfter(min) || d.isSame(min)) && (d.isBefore(max) || max.isSame(d)));
    };
  } else {
    min = moment(partition.selected[0]);
    max = moment(partition.selected[1]);
    return function (d) {
      return (d !== misval) && (d.isAfter(min) || d.isSame(min)) && (d.isBefore(max) || (max.isSame(edge) && max.isSame(d)));
    };
  }
}

/**
 * A filter function based for a single partition
 * @function
 * @returns {boolean} selected True if the datapoint is currently selected
 * @param {Partition} partition
 * @param {Object} datapoint
 * @memberof! Selection
 */
function filterFunction (partition) {
  if (partition.type === 'categorial' || partition.type === 'constant') {
    return filterFunctionCategorial1D(partition);
  } else if (partition.type === 'continuous') {
    return filterFunctionContinuous1D(partition);
  } else if (partition.type === 'datetime') {
    return filterFunctionTime1D(partition);
  } else {
    console.error('Cannot make filterfunction for partition', partition.getId());
  }
}

/*
 * @param {Group} group - The group to add or remove from the filter
 */
function updateCategorial1D (partition, group) {
  var selected = partition.selected;

  if (selected.length === 0) {
    // 1. none selected:
    selected.push(group.value);
  } else if (selected.length === 1) {
    if (selected[0] === group.value) {
      // 2. one selected and the group is the same:
      selected.splice(0, selected.length);
      partition.groups.forEach(function (g) {
        if (g.value !== group.value) {
          selected.push(g.value);
        }
      });
    } else {
      // 3. one selected and the group is different:
      selected.push(group.value);
    }
  } else {
    var i;
    i = selected.indexOf(group.value);
    if (i > -1) {
      // 4. more than one selected and the group is in the selection:
      selected.splice(i, 1);
    } else {
      // 5. more than one selected and the group is not in the selection:
      selected.push(group.value);
    }
  }

  // after add: if filters == groups, reset and dont filter
  if (selected.length === partition.groups.length) {
    selected.splice(0, selected.length);
  }
}

/*
 * @param {Group} group - The group to add or remove from the filter
 */
function updateContinuous1D (partition, group) {
  var selected = partition.selected;

  if (selected.length === 0) {
    // nothing selected, start a range
    selected[0] = group.min;
    selected[1] = group.max;
  } else if (group.min >= selected[1]) {
    // clicked outside to the rigth of selection
    selected[1] = group.max;
  } else if (group.max <= selected[0]) {
    // clicked outside to the left of selection
    selected[0] = group.min;
  } else {
    // clicked inside selection
    var d1, d2;
    if (partition.groupLog) {
      d1 = Math.abs(Math.log(selected[0]) - Math.log(group.min));
      d2 = Math.abs(Math.log(selected[1]) - Math.log(group.max));
    } else {
      d1 = Math.abs(selected[0] - group.min);
      d2 = Math.abs(selected[1] - group.max);
    }
    if (d1 < d2) {
      selected[0] = group.min;
    } else {
      selected[1] = group.max;
    }
  }
}

/*
 * @param {Group} group - The group to add or remove from the filter
 */
function updateTime1D (partition, group) {
  var selected = partition.selected;

  if (selected.length === 0) {
    // nothing selected, start a range
    selected[0] = group.min;
    selected[1] = group.max;
    return;
  }

  var selectionStart = moment(selected[0]);
  var selectionEnd = moment(selected[1]);

  var groupStart = moment(group.min);
  var groupEnd = moment(group.max);

  if (groupStart.isAfter(selectionEnd) || groupStart.isSame(selectionEnd)) {
    // clicked outside to the rigth of selection
    selected[1] = group.max;
  } else if (groupEnd.isBefore(selectionStart) || groupEnd.isSame(selectionStart)) {
    // clicked outside to the left of selection
    selected[0] = group.min;
  } else {
    // clicked inside selection
    var d1, d2;
    d1 = Math.abs(selectionStart.diff(groupStart));
    d2 = Math.abs(selectionEnd.diff(groupEnd));

    if (d1 < d2) {
      selected[0] = group.max;
    } else {
      selected[1] = group.min;
    }
  }
}

/**
 * Update the selection with a given group or interval
 * or, if no group is given, clear the selection.
 *
 * For categorial selections the following rules are used:
 * 1. none selected:
 *    add the group to the selection
 * 2. one selected and the group is the same:
 *    invert the selection
 * 3. one selected and the group is different:
 *    add the group to the selection
 * 4. more than one selected and the group is in the selection:
 *    remove the group from the selection
 * 5. more than one selected and the group is not in the selection:
 *    add the group to the selection
 *
 * For continuous selections the following rules are used:
 * 1. no range selected
 *    set the range equal to that of the group
 * 2. a range selected and the group is outside the selection:
 *    extend the selection to include the group
 * 3. a range selected and the group is inside the selection:
 *    set the endpoint closest to the group to that of the group
 *
 * @function
 * @param {Partition} Partition to update
 * @param {(string|number[])} Group or interval
 */
function updateSelection (partition, group) {
  var f;

  if (!group) {
    // Clear the selection (ie. all points are selected)
    partition.selected.splice(0, partition.selected.length);
  } else {
    // Update the selection
    if (partition.type === 'categorial' || partition.type === 'constant') {
      updateCategorial1D(partition, group);
    } else if (partition.type === 'continuous') {
      updateContinuous1D(partition, group);
    } else if (partition.type === 'datetime') {
      updateTime1D(partition, group);
    } else {
      console.error('Cannot update selection', partition.type);
    }
  }

  // update the isSelected value for each group
  f = partition.filterFunction();
  partition.groups.forEach(function (group) {
    group.isSelected = f(group.value);
  });
}

module.exports = {
  filterFunction: filterFunction,
  updateSelection: updateSelection
};