<commit_before>import React, { PropTypes } from 'react';
import classNames from 'classnames';
import { scaleLinear } from 'd3';
import bindAll from 'lodash/bindAll';
import difference from 'lodash/difference';
import identity from 'lodash/identity';
import inRange from 'lodash/inRange';
import map from 'lodash/map';
import noop from 'lodash/noop';
import range_ from 'lodash/range';
import reduce from 'lodash/reduce';
import round from 'lodash/round';
import zipObject from 'lodash/zipObject';
import { CommonPropTypes, PureComponent } from '../../../utils';
import { stateFromPropUpdates, updateFunc } from '../../../utils/props';

import Track from './track';
import Fill from './fill';
import Handle from './handle';
import styles from './slider.css';

const VALUE_KEYS = ['low', 'high'];

/**
 * Return object of indexes for 'low' and 'high' values
 * @param values
 * @param rangeList
 * @return {Object}
 */
function getIndexesForValues(values, rangeList) {
  return reduce(values, (acc, value, key) => {
    /* eslint-disable no-nested-ternary */
    // if value is not in the range list, assign either first or last element, depending on key
    const index = rangeList.indexOf(value);
    return {
      ...acc,
      [key]: index !== -1
        ? index : key === 'low'
          ? 0 : rangeList.length - 1,
    };
    /* eslint-enable no-nested-ternary */
  }, {});
}

/**
 * Return object of values for 'low' and 'high' indexes
 * @param indexes
 * @param rangeList
 * @return {Object}
 */
function getValuesForIndexes(indexes, rangeList) {
  return reduce(indexes, (acc, value, key) => ({ ...acc, [key]: rangeList[value] }), {});
}

/**
 * Evaluate value param and return a compatible object with keys 'low' and 'high'.
 * @param value
 * @returns {Object}
 */
function getLowHighValues(value) {
  if (Array.isArray(value)) {
    return zipObject(VALUE_KEYS, value);
  } else if (typeof value === 'object') {
    if (difference(Object.keys(value), VALUE_KEYS).length) {
      throw new TypeError(`Unexpected value keys: [${difference(Object.keys(value), VALUE_KEYS)}]`);
    }
    return value;
  }
  return { low: value };
}

/**
 * `import { Slider } from 'ihme-ui'`
 */
export default class Slider extends PureComponent {
  constructor(props) {
    super(props);

    this.state = {
      ...stateFromPropUpdates(Slider.propUpdates, {}, props, { scale: scaleLinear() }),
      render: false,
      snapTarget: {},
    };

    this.handleCount = Object.keys(this.state.indexes).length;

    bindAll(this, [
      'doSetState',
      'onHandleEnd',
      'onHandleDrag',
      'onHandleKeyDown',
      'onTrackClick',
      'trackRef',
    ]);
  }

  componentDidMount() {
    this.doSetState(this.receiveTrackWidth(this.state));
  }

  componentWillReceiveProps(nextProps) {
    let state = {
      range: this.state.range,
      scale: this.state.scale,
    };

    state = stateFromPropUpdates(Slider.propUpdates, this.props, nextProps, state);

    // If the extents or width changes, the scale and snapTarget must be recalculated.
    if (this.props.range !== nextProps.range) {
      state = this.receiveTrackWidth(state);
    }

    this.setState(state);
  }

  componentDidUpdate(prevProps) {
    if (this.props.width !== prevProps.width) {
      this.doSetState(this.receiveTrackWidth(this.state));
    }
  }

  onHandleDrag(key, offset) {
    return (event) => {
      if (!event.dx) return;

      const { pageX, dx } = event;
      const { scale, indexes } = this.state;

      // Calculate next position, handle extent, and index value
      const nextPos = scale(indexes[key]) + dx;
      const handleExtent = nextPos - (offset * 2);
      const index = round(scale.invert(nextPos));

      // Check that the mouse X pos is within the handle extent,
      // and that the computed value is in the range list
      if (!inRange(pageX - handleExtent, nextPos - handleExtent)
        || !this.state.range[index]) return;

      this.updateValueFromEvent(event, index, key, this.props.onDrag);
    };
  }

  onHandleKeyDown(key) {
    return (event) => {
      let step;
      switch (event.keyCode) {
        case 37: // ArrowLeft
          step = -1;
          break;
        case 39: // ArrowRight
          step = 1;
          break;
        default:
          return;
      }

      event.stopPropagation();
      event.preventDefault();

      const index = this.state.indexes[key] + step;

      this.updateValueFromEvent(event, index, key, this.props.onKey);
    };
  }

  onHandleEnd(callback) {
    return (event) => {
      callback(event, getValuesForIndexes(this.state.indexes, this.state.range), this);
    };
  }

  onTrackClick(event) {
    const { scale, indexes } = this.state;

    const index = round(scale.invert(event.snap.x));

    /* Determine which handle is closer. 'low' == true, 'high' == false */
    const comp = indexes.high === undefined
      || (Math.abs(indexes.low - index) < Math.abs(indexes.high - index) || indexes.low > index);

    const key = comp ? 'low' : 'high';

    this.updateValueFromEvent(event, index, key, this.props.onTrackClick);
  }

  updateValueFromEvent(event, index, key, callback) {
    if (index !== this.state.indexes[key] && this.state.range[index]) {
      const indexes = { ...this.state.indexes, [key]: index };
      const values = getValuesForIndexes(indexes, this.state.range);

      if (indexes.low === undefined
        || indexes.high === undefined
        || indexes.low <= indexes.high) {
        callback(event, values, this);
      }
    }
  }

  receiveTrackWidth(state) {
    return {
      ...state,
      render: true,
      scale: state.scale.range([0, this._track.width]),
      snapTarget: { x: this._track.width / (state.range.length - 1) },
    };
  }

  doSetState(state) {
    this.setState(state);
  }

  trackRef(ref) {
    this._track = ref;
  }<commit_msg>Check explicitly for undefined range value in Slider#updateValueFromEvent.<commit_after>import React, { PropTypes } from 'react';
import classNames from 'classnames';
import { scaleLinear } from 'd3';
import bindAll from 'lodash/bindAll';
import difference from 'lodash/difference';
import identity from 'lodash/identity';
import inRange from 'lodash/inRange';
import map from 'lodash/map';
import noop from 'lodash/noop';
import range_ from 'lodash/range';
import reduce from 'lodash/reduce';
import round from 'lodash/round';
import zipObject from 'lodash/zipObject';
import { CommonPropTypes, PureComponent } from '../../../utils';
import { stateFromPropUpdates, updateFunc } from '../../../utils/props';

import Track from './track';
import Fill from './fill';
import Handle from './handle';
import styles from './slider.css';

const VALUE_KEYS = ['low', 'high'];

/**
 * Return object of indexes for 'low' and 'high' values
 * @param values
 * @param rangeList
 * @return {Object}
 */
function getIndexesForValues(values, rangeList) {
  return reduce(values, (acc, value, key) => {
    /* eslint-disable no-nested-ternary */
    // if value is not in the range list, assign either first or last element, depending on key
    const index = rangeList.indexOf(value);
    return {
      ...acc,
      [key]: index !== -1
        ? index : key === 'low'
          ? 0 : rangeList.length - 1,
    };
    /* eslint-enable no-nested-ternary */
  }, {});
}

/**
 * Return object of values for 'low' and 'high' indexes
 * @param indexes
 * @param rangeList
 * @return {Object}
 */
function getValuesForIndexes(indexes, rangeList) {
  return reduce(indexes, (acc, value, key) => ({ ...acc, [key]: rangeList[value] }), {});
}

/**
 * Evaluate value param and return a compatible object with keys 'low' and 'high'.
 * @param value
 * @returns {Object}
 */
function getLowHighValues(value) {
  if (Array.isArray(value)) {
    return zipObject(VALUE_KEYS, value);
  } else if (typeof value === 'object') {
    if (difference(Object.keys(value), VALUE_KEYS).length) {
      throw new TypeError(`Unexpected value keys: [${difference(Object.keys(value), VALUE_KEYS)}]`);
    }
    return value;
  }
  return { low: value };
}

/**
 * `import { Slider } from 'ihme-ui'`
 */
export default class Slider extends PureComponent {
  constructor(props) {
    super(props);

    this.state = {
      ...stateFromPropUpdates(Slider.propUpdates, {}, props, { scale: scaleLinear() }),
      render: false,
      snapTarget: {},
    };

    this.handleCount = Object.keys(this.state.indexes).length;

    bindAll(this, [
      'doSetState',
      'onHandleEnd',
      'onHandleDrag',
      'onHandleKeyDown',
      'onTrackClick',
      'trackRef',
    ]);
  }

  componentDidMount() {
    this.doSetState(this.receiveTrackWidth(this.state));
  }

  componentWillReceiveProps(nextProps) {
    let state = {
      range: this.state.range,
      scale: this.state.scale,
    };

    state = stateFromPropUpdates(Slider.propUpdates, this.props, nextProps, state);

    // If the extents or width changes, the scale and snapTarget must be recalculated.
    if (this.props.range !== nextProps.range) {
      state = this.receiveTrackWidth(state);
    }

    this.setState(state);
  }

  componentDidUpdate(prevProps) {
    if (this.props.width !== prevProps.width) {
      this.doSetState(this.receiveTrackWidth(this.state));
    }
  }

  onHandleDrag(key, offset) {
    return (event) => {
      if (!event.dx) return;

      const { pageX, dx } = event;
      const { scale, indexes } = this.state;

      // Calculate next position, handle extent, and index value
      const nextPos = scale(indexes[key]) + dx;
      const handleExtent = nextPos - (offset * 2);
      const index = round(scale.invert(nextPos));

      // Check that the mouse X pos is within the handle extent,
      // and that the computed value is in the range list
      if (!inRange(pageX - handleExtent, nextPos - handleExtent)
        || !this.state.range[index]) return;

      this.updateValueFromEvent(event, index, key, this.props.onDrag);
    };
  }

  onHandleKeyDown(key) {
    return (event) => {
      let step;
      switch (event.keyCode) {
        case 37: // ArrowLeft
          step = -1;
          break;
        case 39: // ArrowRight
          step = 1;
          break;
        default:
          return;
      }

      event.stopPropagation();
      event.preventDefault();

      const index = this.state.indexes[key] + step;

      this.updateValueFromEvent(event, index, key, this.props.onKey);
    };
  }

  onHandleEnd(callback) {
    return (event) => {
      callback(event, getValuesForIndexes(this.state.indexes, this.state.range), this);
    };
  }

  onTrackClick(event) {
    const { scale, indexes } = this.state;

    const index = round(scale.invert(event.snap.x));

    /* Determine which handle is closer. 'low' == true, 'high' == false */
    const comp = indexes.high === undefined
      || (Math.abs(indexes.low - index) < Math.abs(indexes.high - index) || indexes.low > index);

    const key = comp ? 'low' : 'high';

    this.updateValueFromEvent(event, index, key, this.props.onTrackClick);
  }

  updateValueFromEvent(event, index, key, callback) {
    if (index !== this.state.indexes[key] && this.state.range[index] !== undefined) {
      const indexes = { ...this.state.indexes, [key]: index };
      const values = getValuesForIndexes(indexes, this.state.range);

      if (indexes.low === undefined
        || indexes.high === undefined
        || indexes.low <= indexes.high) {
        callback(event, values, this);
      }
    }
  }

  receiveTrackWidth(state) {
    return {
      ...state,
      render: true,
      scale: state.scale.range([0, this._track.width]),
      snapTarget: { x: this._track.width / (state.range.length - 1) },
    };
  }

  doSetState(state) {
    this.setState(state);
  }

  trackRef(ref) {
    this._track = ref;
  }
{'fraction_non_alphanumeric': 0.14404127547999662, 'fraction_numerical': 0.0021991034424426966, 'mean_word_length': 2.872911889944317, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 15, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5422363', 'n_tokens_mistral': 3765, 'n_tokens_neox': 3639, 'n_words': 936}