<commit_before>import React, { Component } from 'react';
import { debounce, isFinite } from 'lodash';
import { Row, Column } from '@ncigdc/uikit/Flex';
import Button from '@ncigdc/uikit/Button';
import RangeTableRow from './RangeTableRow';
import BinningMethodInput from './BinningMethodInput';
import CustomIntervalFields from './CustomIntervalFields';
import styles from './styles';

const defaultRangeRow = [
  {
    active: true,
    fields: {
      from: '',
      name: '',
      to: '',
    },
  },
];

const countDecimals = num => {
  return Math.floor(num) === num ? 0 : (num.toString().split('.')[1].length || 0);
};

class ContinuousCustomBinsModal extends Component {
  state = {
    binningMethod: 'interval', // interval or range
    intervalErrors: {
      amount: '',
      max: '',
      min: '',
    },
    intervalFields: {
      // seed input values, from props
      amount: this.props.defaultContinuousData.quartile,
      max: this.props.defaultContinuousData.max,
      min: this.props.defaultContinuousData.min,
    },
    modalWarning: '',
    rangeNameErrors: [],
    rangeOverlapErrors: [],
    rangeRows: defaultRangeRow,
  };

  componentDidMount = () => {
    const {
      binData, continuousBinType, rangeRows,
    } = this.props;
    this.validateRangeRow(rangeRows);

    this.debounceValidateIntervalFields = debounce(
      this.validateIntervalFields,
      300
    );

    if (continuousBinType === 'range') {
      const nextRangeRows = binData.map(bin => ({
        active: false,
        fields: {
          from: bin.keyArray[0].split('-')[0],
          name: bin.key,
          to: bin.keyArray[0].split('-')[1],
        },
      }));

      this.setState({
        binningMethod: 'range',
        rangeRows: nextRangeRows,
      });
    } else if (continuousBinType === 'interval') {
      const min = binData[0].keyArray.split('-')[0];
      const max = binData[binData.length - 1].keyArray.split('-')[1];
      const amount = binData[0].keyArray.split('-')[1] - min;

      this.setState({
        binningMethod: 'interval',
        intervalFields: {
          amount,
          max,
          min,
        },
      });
    }
  };

  // binning method: interval

  updateIntervalFields = (updateEvent, inputError = null) => {
    const { intervalErrors, intervalFields } = this.state;

    const inputKey = updateEvent.target.id.split('-')[2];
    const inputValue = updateEvent.target.value;

    const nextIntervalFields = {
      ...intervalFields,
      [inputKey]: inputValue,
    };

    const nextIntervalErrors = {
      ...intervalErrors,
      [inputKey]: inputError === null
        ? intervalErrors[inputKey]
        : inputError,
    };

    if (inputError === null) updateEvent.persist();

    this.setState({
      intervalErrors: nextIntervalErrors,
      intervalFields: nextIntervalFields,
    }, () => {
      if (inputError === null) {
        this.debounceValidateIntervalFields(updateEvent);
      }
    });
  };

  validateIntervalFields = event => {
    const { defaultContinuousData } = this.props;
    const { intervalFields } = this.state;

    const inputKey = event.target.id.split('-')[2];
    const inputValue = Number(event.target.value);

    if (!isFinite(inputValue)) {
      const nanError = [`'${event.target.value}' is not a valid number.`];
      this.updateIntervalFields(event, nanError);
      return;
    }

    const currentMin = intervalFields.min;
    const currentMax = intervalFields.max;

    const checkMinInRange = currentMin >= defaultContinuousData.min && currentMin < defaultContinuousData.max;
    const validMin = checkMinInRange ? currentMin : defaultContinuousData.min;

    const checkMaxInRange = currentMax > defaultContinuousData.min && currentMax <= defaultContinuousData.max;
    const validMax = checkMaxInRange ? currentMax : defaultContinuousData.max;

    const validAmount = checkMinInRange && checkMaxInRange ? currentMax - currentMin : defaultContinuousData.max - defaultContinuousData.min;

    let inputError = '';

    const decimalError = 'Use up to 2 decimal places.';

    if (inputValue === '') {
      inputError = 'Required field.';
    } else {
      inputError = countDecimals(inputValue) > 2 ? decimalError : inputError;
    }

    if (inputError !== '') {
      this.updateIntervalFields(event, inputError);
      return;
    }

    if (inputKey === 'amount') {
      const amountTooLargeError = `Must be less than or equal to ${validAmount}.`;
      const amountTooSmallError = 'Must be greater than 0.';
      inputError = inputValue <= 0 ? amountTooSmallError : inputValue > validAmount ? amountTooLargeError : '';
      inputError = countDecimals(inputValue) > 2 ? decimalError : inputError;
    } else if (inputKey === 'max') {
      const maxTooSmallError = `Must be greater than ${validMin}.`;
      const maxTooLargeError = `Must be less than or equal to ${defaultContinuousData.max}.`;
      inputError = inputValue <= validMin ? maxTooSmallError : inputValue > defaultContinuousData.max ? maxTooLargeError : '';
      inputError = countDecimals(inputValue) > 2 ? decimalError : inputError;
    } else if (inputKey === 'min') {
      const minTooLargeError = `Must be less than ${validMax}.`;
      const maxTooSmallError = `Must be greater than or equal to ${defaultContinuousData.min}.`;
      inputError = inputValue >= validMax ? minTooLargeError : inputValue < defaultContinuousData.min ? maxTooSmallError : '';
      inputError = countDecimals(inputValue) > 2 ? decimalError : inputError;
    } else {
      inputError = '';
    }

    this.updateIntervalFields(event, inputError);
  };

  checkSubmitDisabled = () => {
    const {
      binningMethod, intervalErrors, rangeOverlapErrors, rangeRows,
    } = this.state;
    const checkInterval = Object.keys(intervalErrors)
      .filter(int => intervalErrors[int] !== '').length > 0;
    const checkRange = rangeRows
      .filter(row => row.active).length > 0;
    const checkOverlap = rangeOverlapErrors
      .filter(err => err.length > 0).length > 0;
    const result = binningMethod === 'interval'
      ? checkInterval
      : checkRange || checkOverlap;
    return result;
  };

  // binning method: range

  handleRemoveRow = rowIndex => {
    const { rangeRows } = this.state;
    const filteredRangeRows = rangeRows.filter((filterRow, filterRowIndex) => filterRowIndex !== rowIndex);
    const nextRangeRows = filteredRangeRows.length === 0 ? defaultRangeRow : filteredRangeRows;
    this.setState({ rangeRows: nextRangeRows });
    this.validateRangeRow(nextRangeRows);
  };

  handleToggleActiveRow = (inputRowIndex, inputIsActive) => {
    const { rangeRows } = this.state;
    const nextRangeRows = rangeRows.map((rangeRow, rowIndex) => ({
      ...rangeRow,
      active: rowIndex === inputRowIndex ? inputIsActive : rangeRow.active,
    }));
    this.setState({ rangeRows: nextRangeRows });
    this.validateRangeRow(nextRangeRows);
  };

  handleUpdateRow = (inputRowIndex, inputRow) => {
    const { rangeRows } = this.state;
    const nextRangeRows = rangeRows.map((rangeRow, rowIndex) => (rowIndex === inputRowIndex ? inputRow : rangeRow));

    const rowHasErrors = this.validateRangeRow(nextRangeRows);
    if (rowHasErrors) return;

    const checkIsLastRow = inputRowIndex + 1 === rangeRows.length;
    const addNewRow = nextRangeRows.concat(defaultRangeRow);

    this.setState({ rangeRows: checkIsLastRow ? addNewRow : nextRangeRows });
  }

  // submit

  handleSubmit = () => {
    const { onUpdate } = this.props;
    const {
      binningMethod, intervalFields, rangeRows,
    } = this.state;

    const formHasErrors = binningMethod === 'range' &&
      this.validateRangeRow();

    if (!formHasErrors) {
      const makeCustomIntervalBins = () => {
        const intervalAmount = Number(intervalFields.amount);
        const intervalMax = Number(intervalFields.max);
        const intervalMin = Number(intervalFields.min);

        const bucketRange = intervalMax - intervalMin;
        const bucketCount = Math.round(bucketRange / intervalAmount);

        const buckets = Array(bucketCount).fill(1)
          .map((val, key) => {
            const from = Number((key * intervalAmount + intervalMin).toFixed(2));
            const to = Number(((key + 1) === bucketCount
              ? intervalMax
              : intervalMin + (key + 1) * intervalAmount - 1).toFixed(2));

            const objKey = `${from}-${to}`;

            return ({
              [objKey]: {
                groupName: `${from} ${(key + 1) === bucketCount ? 'and up' : `to ${to}`}`,
                key: objKey,
              },
            });
          }).reduce((acc, curr) => ({
            ...acc,
            ...curr,
          }), {});

        return buckets;
      };

      const newBins = binningMethod === 'range'
        ? rangeRows.map(row => row.fields).reduce((acc, curr) => {
          const rowKey = `${curr.from}-${curr.to}`;
          return ({
            ...acc,
            ...(curr.name === '' ? {} : {
              [rowKey]: {
                groupName: curr.name,
                key: rowKey,
              },
            }),
          });
        }, {})
        : makeCustomIntervalBins();

      onUpdate(newBins, binningMethod);
    }
  };

  validateRangeNames = (rows = null) => {
    const { rangeRows } = this.state;
    const rowsToCheck = rows === null ? rangeRows : rows;
    const nameErrors = rowsToCheck.map((rowItem, rowIndex) => {
      const rowName = rowItem.fields.name.toLowerCase().trim();
      const duplicateNames = rowsToCheck.filter((duplicateItem, duplicateIndex) => {
        if (rowIndex === duplicateIndex) return;
        const duplicateName = duplicateItem.fields.name.toLowerCase().trim();
        return duplicateName === rowName;
      });
      return duplicateNames.length > 0 ? 'Bin names must be unique.' : '';
    });
    return nameErrors;
  }

  validateRangeOverlap = (rows = null) => {
    // assume all rows are complete and from < to

    const { rangeRows } = this.state;

    const rowsToCheck = rows === null ? rangeRows : rows;

    const overlapErrors = rowsToCheck.map((rowItem, rowIndex) => {
      const rowFrom = Number(rowItem.fields.from);
      const rowTo = Number(rowItem.fields.to);

      const overlapNames = rowsToCheck.reduce((acc, curr, overlapIndex) => {
        const overlapFromStr = curr.fields.from;
        const overlapToStr = curr.fields.to;

        if (rowIndex === overlapIndex || curr.fields.from === '' || curr.fields.to === '') {
          return acc;
        }

        const overlapFrom = Number(overlapFromStr);
        const overlapTo = Number(overlapToStr);
        const overlapName = curr.fields.name;

        const hasNoOverlap = rowTo < overlapFrom || rowFrom > overlapTo;

        return hasNoOverlap ? acc : [...acc, overlapName];
      }, []);
      return overlapNames.length > 0 ? overlapNames : [];
    });

    return overlapErrors;
  }

  validateRangeRow = (rows = null) => {
    const { rangeRows } = this.state;

    const rowsToCheck = rows === null ? rangeRows : rows;

    const overlapErrors = this.validateRangeOverlap(rowsToCheck);
    const nameErrors = this.validateRangeNames(rowsToCheck);
    const overlapHasError = overlapErrors.filter(overlapErrorItem => overlapErrorItem.length > 0).length > 0;
    const nameHasError = nameErrors.filter(nameErrorItem => nameErrorItem !== '').length > 0;

    this.setState({
      rangeNameErrors: nameErrors,
      rangeOverlapErrors: overlapErrors,
    });

    return nameHasError || overlapHasError;
  }

  render = () => {
    const { defaultContinuousData, fieldName, onClose } = this.props;
    const {
      binningMethod, intervalErrors, intervalFields, modalWarning, rangeNameErrors, rangeOverlapErrors, rangeRows,
    } = this.state;

    const submitDisabled = this.checkSubmitDisabled();

    return (
      <Column style={{ padding: '20px' }}>

            {`Create Custom Bins: ${fieldName}`}

            Available values from
            {` ${defaultContinuousData.min} `}
            to
            {` ${defaultContinuousData.max} `}

            Quartile bin interval:
            {` ${defaultContinuousData.quartile}`}

            Configure your bins then click
             Save Bins
            to update the analysis plots.

          <Row>
            <Column className="binning-interval">

Define bins by:

              <CustomIntervalFields
                countDecimals={countDecimals}
                disabled={binningMethod !== 'interval'}
                handleChange={e => {
                  this.updateIntervalFields(e);
                }}
                handleUpdateBinningMethod={() => {
                  this.setState({ binningMethod: 'interval' });
                }}
                intervalErrors={intervalErrors}
                intervalFields={intervalFields}
                validateIntervalFields={e => {
                  this.validateIntervalFields(e);
                }}
                />
            </Column>
          </Row>

 {
              if (binningMethod !== 'range') {
                this.setState({ binningMethod: 'range' });
              }
            }}
            role="presentation"
            >

              <BinningMethodInput
                binningMethod="range"
                checked={binningMethod === 'range'}
                handleChange={() => {
                  this.setState({ binningMethod: 'range' });
                }}
                label="Manually"
                />

                  Bin Name

                  From

                  To

                  Options

                {rangeRows.map((row, rowIndex) => (
                  <RangeTableRow
                    countDecimals={countDecimals}
                    fields={row.fields}
                    handleRemoveRow={this.handleRemoveRow}
                    handleToggleActiveRow={this.handleToggleActiveRow}
                    handleUpdateBinningMethod={() => {
                      this.setState({ binningMethod: 'range' });
                    }}
                    handleUpdateRow={this.handleUpdateRow}
                    key={`range-row-${rowIndex}`}
                    rangeMethodActive={binningMethod === 'range'}
                    rowActive={row.active}
                    rowIndex={rowIndex}
                    rowNameError={rangeNameErrors[rowIndex] || ''}
                    rowOverlapErrors={rangeOverlapErrors[rowIndex] || []}
                    rowsLength={rangeRows.length}
                    styles={styles}
                    />
                ))}

            {/* <Button
            disabled={binningMethod !== 'range'}
            onClick={() => {
              const nextRangeRows = [...rangeRows, ...defaultRangeRow];
              this.setState({ rangeRows: nextRangeRows });
            }}
            style={{
              display: 'flex',
              marginLeft: 'auto',
              maxWidth: '100px',
              ...(binningMethod === 'range' ? styles.visualizingButton : styles.inputDisabled),
            }}
          >

              Add
            </Button> */}

        <Row
          spacing="1rem"
          style={{
            justifyContent: 'flex-end',
            margin: '20px',
          }}
          >
           0 ? 'visible' : 'hidden',
          }}
                >
            {`Warning: ${modalWarning}`}

          <Button
            onClick={onClose}
            onMouseDown={onClose}
            style={styles.visualizingButton}
            >
            Cancel
          </Button>
          <Button
            disabled={submitDisabled}
            onClick={() => this.handleSubmit()}
            onMouseDown={() => this.handleSubmit()}
            style={submitDisabled
              ? styles.inputDisabled
              : styles.visualizingButton}
            >
            Save Bins
          </Button>
        </Row>
      </Column>
    );
  }
}

export default ContinuousCustomBinsModal;
<commit_msg>fix custom interval bin editing (#2595)<commit_after>import React, { Component } from 'react';
import { debounce, isFinite } from 'lodash';
import { Row, Column } from '@ncigdc/uikit/Flex';
import Button from '@ncigdc/uikit/Button';
import RangeTableRow from './RangeTableRow';
import BinningMethodInput from './BinningMethodInput';
import CustomIntervalFields from './CustomIntervalFields';
import styles from './styles';

const defaultRangeRow = [
  {
    active: true,
    fields: {
      from: '',
      name: '',
      to: '',
    },
  },
];

const countDecimals = num => {
  return Math.floor(num) === num ? 0 : (num.toString().split('.')[1].length || 0);
};

class ContinuousCustomBinsModal extends Component {
  state = {
    binningMethod: 'interval', // interval or range
    intervalErrors: {
      amount: '',
      max: '',
      min: '',
    },
    intervalFields: {
      // seed input values, from props
      amount: this.props.defaultContinuousData.quartile,
      max: this.props.defaultContinuousData.max,
      min: this.props.defaultContinuousData.min,
    },
    modalWarning: '',
    rangeNameErrors: [],
    rangeOverlapErrors: [],
    rangeRows: defaultRangeRow,
  };

  componentDidMount = () => {
    const {
      binData, continuousBinType, rangeRows,
    } = this.props;
    this.validateRangeRow(rangeRows);

    this.debounceValidateIntervalFields = debounce(
      this.validateIntervalFields,
      300
    );

    if (continuousBinType === 'range') {
      const nextRangeRows = binData.map(bin => ({
        active: false,
        fields: {
          from: bin.keyArray[0].split('-')[0],
          name: bin.key,
          to: bin.keyArray[0].split('-')[1],
        },
      }));

      this.setState({
        binningMethod: 'range',
        rangeRows: nextRangeRows,
      });
    } else if (continuousBinType === 'interval') {
      const min = binData[0].keyArray[0].split('-')[0];
      const max = binData[binData.length - 1].keyArray[0].split('-')[1];
      const amount = binData[0].keyArray[0].split('-')[1] - min;

      this.setState({
        binningMethod: 'interval',
        intervalFields: {
          amount,
          max,
          min,
        },
      });
    }
  };

  // binning method: interval

  updateIntervalFields = (updateEvent, inputError = null) => {
    const { intervalErrors, intervalFields } = this.state;

    const inputKey = updateEvent.target.id.split('-')[2];
    const inputValue = updateEvent.target.value;

    const nextIntervalFields = {
      ...intervalFields,
      [inputKey]: inputValue,
    };

    const nextIntervalErrors = {
      ...intervalErrors,
      [inputKey]: inputError === null
        ? intervalErrors[inputKey]
        : inputError,
    };

    if (inputError === null) updateEvent.persist();

    this.setState({
      intervalErrors: nextIntervalErrors,
      intervalFields: nextIntervalFields,
    }, () => {
      if (inputError === null) {
        this.debounceValidateIntervalFields(updateEvent);
      }
    });
  };

  validateIntervalFields = event => {
    const { defaultContinuousData } = this.props;
    const { intervalFields } = this.state;

    const inputKey = event.target.id.split('-')[2];
    const inputValue = Number(event.target.value);

    if (!isFinite(inputValue)) {
      const nanError = [`'${event.target.value}' is not a valid number.`];
      this.updateIntervalFields(event, nanError);
      return;
    }

    const currentMin = intervalFields.min;
    const currentMax = intervalFields.max;

    const checkMinInRange = currentMin >= defaultContinuousData.min && currentMin < defaultContinuousData.max;
    const validMin = checkMinInRange ? currentMin : defaultContinuousData.min;

    const checkMaxInRange = currentMax > defaultContinuousData.min && currentMax <= defaultContinuousData.max;
    const validMax = checkMaxInRange ? currentMax : defaultContinuousData.max;

    const validAmount = checkMinInRange && checkMaxInRange ? currentMax - currentMin : defaultContinuousData.max - defaultContinuousData.min;

    let inputError = '';

    const decimalError = 'Use up to 2 decimal places.';

    if (inputValue === '') {
      inputError = 'Required field.';
    } else {
      inputError = countDecimals(inputValue) > 2 ? decimalError : inputError;
    }

    if (inputError !== '') {
      this.updateIntervalFields(event, inputError);
      return;
    }

    if (inputKey === 'amount') {
      const amountTooLargeError = `Must be less than or equal to ${validAmount}.`;
      const amountTooSmallError = 'Must be greater than 0.';
      inputError = inputValue <= 0 ? amountTooSmallError : inputValue > validAmount ? amountTooLargeError : '';
      inputError = countDecimals(inputValue) > 2 ? decimalError : inputError;
    } else if (inputKey === 'max') {
      const maxTooSmallError = `Must be greater than ${validMin}.`;
      const maxTooLargeError = `Must be less than or equal to ${defaultContinuousData.max}.`;
      inputError = inputValue <= validMin ? maxTooSmallError : inputValue > defaultContinuousData.max ? maxTooLargeError : '';
      inputError = countDecimals(inputValue) > 2 ? decimalError : inputError;
    } else if (inputKey === 'min') {
      const minTooLargeError = `Must be less than ${validMax}.`;
      const maxTooSmallError = `Must be greater than or equal to ${defaultContinuousData.min}.`;
      inputError = inputValue >= validMax ? minTooLargeError : inputValue < defaultContinuousData.min ? maxTooSmallError : '';
      inputError = countDecimals(inputValue) > 2 ? decimalError : inputError;
    } else {
      inputError = '';
    }

    this.updateIntervalFields(event, inputError);
  };

  checkSubmitDisabled = () => {
    const {
      binningMethod, intervalErrors, rangeOverlapErrors, rangeRows,
    } = this.state;
    const checkInterval = Object.keys(intervalErrors)
      .filter(int => intervalErrors[int] !== '').length > 0;
    const checkRange = rangeRows
      .filter(row => row.active).length > 0;
    const checkOverlap = rangeOverlapErrors
      .filter(err => err.length > 0).length > 0;
    const result = binningMethod === 'interval'
      ? checkInterval
      : checkRange || checkOverlap;
    return result;
  };

  // binning method: range

  handleRemoveRow = rowIndex => {
    const { rangeRows } = this.state;
    const filteredRangeRows = rangeRows.filter((filterRow, filterRowIndex) => filterRowIndex !== rowIndex);
    const nextRangeRows = filteredRangeRows.length === 0 ? defaultRangeRow : filteredRangeRows;
    this.setState({ rangeRows: nextRangeRows });
    this.validateRangeRow(nextRangeRows);
  };

  handleToggleActiveRow = (inputRowIndex, inputIsActive) => {
    const { rangeRows } = this.state;
    const nextRangeRows = rangeRows.map((rangeRow, rowIndex) => ({
      ...rangeRow,
      active: rowIndex === inputRowIndex ? inputIsActive : rangeRow.active,
    }));
    this.setState({ rangeRows: nextRangeRows });
    this.validateRangeRow(nextRangeRows);
  };

  handleUpdateRow = (inputRowIndex, inputRow) => {
    const { rangeRows } = this.state;
    const nextRangeRows = rangeRows.map((rangeRow, rowIndex) => (rowIndex === inputRowIndex ? inputRow : rangeRow));

    const rowHasErrors = this.validateRangeRow(nextRangeRows);
    if (rowHasErrors) return;

    const checkIsLastRow = inputRowIndex + 1 === rangeRows.length;
    const addNewRow = nextRangeRows.concat(defaultRangeRow);

    this.setState({ rangeRows: checkIsLastRow ? addNewRow : nextRangeRows });
  }

  // submit

  handleSubmit = () => {
    const { onUpdate } = this.props;
    const {
      binningMethod, intervalFields, rangeRows,
    } = this.state;

    const formHasErrors = binningMethod === 'range' &&
      this.validateRangeRow();

    if (!formHasErrors) {
      const makeCustomIntervalBins = () => {
        const intervalAmount = Number(intervalFields.amount);
        const intervalMax = Number(intervalFields.max);
        const intervalMin = Number(intervalFields.min);

        const bucketRange = intervalMax - intervalMin;
        const bucketCount = Math.round(bucketRange / intervalAmount);

        const buckets = Array(bucketCount).fill(1)
          .map((val, key) => {
            const from = Number((key * intervalAmount + intervalMin).toFixed(2));
            const to = Number(((key + 1) === bucketCount
              ? intervalMax
              : intervalMin + (key + 1) * intervalAmount - 1).toFixed(2));

            const objKey = `${from}-${to}`;

            return ({
              [objKey]: {
                groupName: `${from} ${(key + 1) === bucketCount ? 'and up' : `to ${to}`}`,
                key: objKey,
              },
            });
          }).reduce((acc, curr) => ({
            ...acc,
            ...curr,
          }), {});

        return buckets;
      };

      const newBins = binningMethod === 'range'
        ? rangeRows.map(row => row.fields).reduce((acc, curr) => {
          const rowKey = `${curr.from}-${curr.to}`;
          return ({
            ...acc,
            ...(curr.name === '' ? {} : {
              [rowKey]: {
                groupName: curr.name,
                key: rowKey,
              },
            }),
          });
        }, {})
        : makeCustomIntervalBins();

      onUpdate(newBins, binningMethod);
    }
  };

  validateRangeNames = (rows = null) => {
    const { rangeRows } = this.state;
    const rowsToCheck = rows === null ? rangeRows : rows;
    const nameErrors = rowsToCheck.map((rowItem, rowIndex) => {
      const rowName = rowItem.fields.name.toLowerCase().trim();
      const duplicateNames = rowsToCheck.filter((duplicateItem, duplicateIndex) => {
        if (rowIndex === duplicateIndex) return;
        const duplicateName = duplicateItem.fields.name.toLowerCase().trim();
        return duplicateName === rowName;
      });
      return duplicateNames.length > 0 ? 'Bin names must be unique.' : '';
    });
    return nameErrors;
  }

  validateRangeOverlap = (rows = null) => {
    // assume all rows are complete and from < to

    const { rangeRows } = this.state;

    const rowsToCheck = rows === null ? rangeRows : rows;

    const overlapErrors = rowsToCheck.map((rowItem, rowIndex) => {
      const rowFrom = Number(rowItem.fields.from);
      const rowTo = Number(rowItem.fields.to);

      const overlapNames = rowsToCheck.reduce((acc, curr, overlapIndex) => {
        const overlapFromStr = curr.fields.from;
        const overlapToStr = curr.fields.to;

        if (rowIndex === overlapIndex || curr.fields.from === '' || curr.fields.to === '') {
          return acc;
        }

        const overlapFrom = Number(overlapFromStr);
        const overlapTo = Number(overlapToStr);
        const overlapName = curr.fields.name;

        const hasNoOverlap = rowTo < overlapFrom || rowFrom > overlapTo;

        return hasNoOverlap ? acc : [...acc, overlapName];
      }, []);
      return overlapNames.length > 0 ? overlapNames : [];
    });

    return overlapErrors;
  }

  validateRangeRow = (rows = null) => {
    const { rangeRows } = this.state;

    const rowsToCheck = rows === null ? rangeRows : rows;

    const overlapErrors = this.validateRangeOverlap(rowsToCheck);
    const nameErrors = this.validateRangeNames(rowsToCheck);
    const overlapHasError = overlapErrors.filter(overlapErrorItem => overlapErrorItem.length > 0).length > 0;
    const nameHasError = nameErrors.filter(nameErrorItem => nameErrorItem !== '').length > 0;

    this.setState({
      rangeNameErrors: nameErrors,
      rangeOverlapErrors: overlapErrors,
    });

    return nameHasError || overlapHasError;
  }

  render = () => {
    const { defaultContinuousData, fieldName, onClose } = this.props;
    const {
      binningMethod, intervalErrors, intervalFields, modalWarning, rangeNameErrors, rangeOverlapErrors, rangeRows,
    } = this.state;

    const submitDisabled = this.checkSubmitDisabled();

    return (
      <Column style={{ padding: '20px' }}>

            {`Create Custom Bins: ${fieldName}`}

            Available values from
            {` ${defaultContinuousData.min} `}
            to
            {` ${defaultContinuousData.max} `}

            Quartile bin interval:
            {` ${defaultContinuousData.quartile}`}

            Configure your bins then click
             Save Bins
            to update the analysis plots.

          <Row>
            <Column className="binning-interval">

Define bins by:

              <CustomIntervalFields
                countDecimals={countDecimals}
                disabled={binningMethod !== 'interval'}
                handleChange={e => {
                  this.updateIntervalFields(e);
                }}
                handleUpdateBinningMethod={() => {
                  this.setState({ binningMethod: 'interval' });
                }}
                intervalErrors={intervalErrors}
                intervalFields={intervalFields}
                validateIntervalFields={e => {
                  this.validateIntervalFields(e);
                }}
                />
            </Column>
          </Row>

 {
              if (binningMethod !== 'range') {
                this.setState({ binningMethod: 'range' });
              }
            }}
            role="presentation"
            >

              <BinningMethodInput
                binningMethod="range"
                checked={binningMethod === 'range'}
                handleChange={() => {
                  this.setState({ binningMethod: 'range' });
                }}
                label="Manually"
                />

                  Bin Name

                  From

                  To

                  Options

                {rangeRows.map((row, rowIndex) => (
                  <RangeTableRow
                    countDecimals={countDecimals}
                    fields={row.fields}
                    handleRemoveRow={this.handleRemoveRow}
                    handleToggleActiveRow={this.handleToggleActiveRow}
                    handleUpdateBinningMethod={() => {
                      this.setState({ binningMethod: 'range' });
                    }}
                    handleUpdateRow={this.handleUpdateRow}
                    key={`range-row-${rowIndex}`}
                    rangeMethodActive={binningMethod === 'range'}
                    rowActive={row.active}
                    rowIndex={rowIndex}
                    rowNameError={rangeNameErrors[rowIndex] || ''}
                    rowOverlapErrors={rangeOverlapErrors[rowIndex] || []}
                    rowsLength={rangeRows.length}
                    styles={styles}
                    />
                ))}

            {/* <Button
            disabled={binningMethod !== 'range'}
            onClick={() => {
              const nextRangeRows = [...rangeRows, ...defaultRangeRow];
              this.setState({ rangeRows: nextRangeRows });
            }}
            style={{
              display: 'flex',
              marginLeft: 'auto',
              maxWidth: '100px',
              ...(binningMethod === 'range' ? styles.visualizingButton : styles.inputDisabled),
            }}
          >

              Add
            </Button> */}

        <Row
          spacing="1rem"
          style={{
            justifyContent: 'flex-end',
            margin: '20px',
          }}
          >
           0 ? 'visible' : 'hidden',
          }}
                >
            {`Warning: ${modalWarning}`}

          <Button
            onClick={onClose}
            onMouseDown={onClose}
            style={styles.visualizingButton}
            >
            Cancel
          </Button>
          <Button
            disabled={submitDisabled}
            onClick={() => this.handleSubmit()}
            onMouseDown={() => this.handleSubmit()}
            style={submitDisabled
              ? styles.inputDisabled
              : styles.visualizingButton}
            >
            Save Bins
          </Button>
        </Row>
      </Column>
    );
  }
}

export default ContinuousCustomBinsModal;
<|endoftext|>


<commit_before>/*!
 * ${copyright}
 */

// Provides information about 'explored' samples.
sap.ui.define(['jquery.sap.global', 'sap/ui/documentation/library'],
	function(jQuery, library) {
		"use strict";

		var oPromise;

		var ControlsInfo = {

			loadData: function() {
				if (!oPromise) {

					oPromise = new Promise(function(resolve, reject) {
						library._loadAllLibInfo(
							"", "_getDocuIndex",
							function (aLibs, oDocIndicies) {
								var oData = ControlsInfo._getIndices(aLibs, oDocIndicies, function () {
									// We pass the resolve method to be called when we have all the component data loaded
									resolve(oData);
								});
							});
					});
				}
				return oPromise;
			},

			_getIndices: function (aLibs, oDocIndicies, fnComponentLoadCallback) {

				var aCategoryWhiteList = [
					"Action",
					"Application",
					"Container",
					"Display",
					"Chart",
					"Mini Chart",
					"Layout",
					"List",
					"Popup",
					"Tile",
					"User Input",
					"Testing",
					"Theming",
					"Tutorial",
					"Routing",
					"Data Binding",
					"Data Visualization",
					"Map"
				];
				var afilterProps = ["namespace", "since", "category"]; // content density are set manually
				var oFilterSets = {
					namespace: {},
					since: {},
					category: {},
					formFactors: { // content density are set manually
						"Independent": true,
						"Condensed": true,
						"Compact": true,
						"Cozy": true
					}
				};
				var mFormFactorsMap = {
					"-": "Independent",
					"S": "Condensed",
					"SM": "Condensed, Compact",
					"SL": "Condensed, Cozy",
					"SML": "Condensed, Compact, Cozy",
					"M": "Compact",
					"ML": "Compact, Cozy",
					"L": "Cozy"
				};

				// init data structures
				var data = {};
				data = {};
				data.entityCount = 0;
				data.entities = [];
				data.filter = {};
				data.samples = {};

				// iterate docu indices
				jQuery.each(oDocIndicies, function (i, oDoc) {

					// check data
					if (!oDoc.explored) {
						return;
					} else if (!oDoc.explored.samplesRef) {
						jQuery.sap.log.error("explored: cannot register lib '" + oDoc.library + "'. missing 'explored.samplesRef'");
						return;
					} else if (Array.isArray(oDoc.explored.samplesRef) && oDoc.explored.samplesRef.length !== oDoc.explored.samplesRef.filter(function (oItem) {
							return oItem.namespace && oItem.ref;
						}).length) {
						jQuery.sap.log.error("explored: cannot register lib '" + oDoc.library + "'. missing 'explored.samplesRef.namespace' or 'explored.samplesRef.ref' in one or more of the configured namespaces");
						return;
					} else if (!Array.isArray(oDoc.explored.samplesRef) && !oDoc.explored.samplesRef.namespace) {
						jQuery.sap.log.error("explored: cannot register lib '" + oDoc.library + "'. missing 'explored.samplesRef.namespace'");
						return;
					} else if (!Array.isArray(oDoc.explored.samplesRef) && !oDoc.explored.samplesRef.ref) {
						jQuery.sap.log.error("explored: cannot register lib '" + oDoc.library + "'. missing 'explored.samplesRef.ref'");
						return;
					} else if (!oDoc.explored.entities) {
						jQuery.sap.log.error("explored: cannot register lib '" + oDoc.library + "'. missing 'explored.entities'");
						return;
					} else {
						jQuery.sap.log.info("explored: now reading lib '" + oDoc.library + "'");
					}

					// register sample resources
					if (Array.isArray(oDoc.explored.samplesRef)) {
						// register an array of namespaces
						oDoc.explored.samplesRef.forEach(function (oItem) {
							jQuery.sap.registerModulePath(oItem.namespace, "" + oItem.ref);
						});
					} else {
						// register a single namespace
						jQuery.sap.registerModulePath(oDoc.explored.samplesRef.namespace, "" + oDoc.explored.samplesRef.ref);
					}

					// build sample map
					jQuery.each(oDoc.explored.samples, function (i, oSample) {
						if (!oSample.id) {
							jQuery.sap.log.error("explored: cannot register sample '?'. missing 'id'");
						} else if (!oSample.name) {
							jQuery.sap.log.error("explored: cannot register sample '" + oSample.id + "'. missing 'name'");
						} else {
							data.samples[oSample.id] = oSample;
						}
					});

					// iterate entities
					jQuery.each(oDoc.explored.entities, function (j, oEnt) {

						// check id property
						if (!oEnt.id) {
							jQuery.sap.log.error("explored: cannot register entity '?'. missing 'id'");
							return;
						}

						// apply default properties
						if (oDoc.explored.entitiesDefaults) {
							jQuery.each(oDoc.explored.entitiesDefaults, function (key, value) {
								if (!oEnt.hasOwnProperty(key)) {
									oEnt[key] = value;
								}
							});
						}

						// apply namespace property
						var iIndex = oEnt.id.lastIndexOf(".");
						var sNamespace = (iIndex !== -1) ? oEnt.id.substring(0, iIndex) : oEnt.id;
						oEnt.namespace = sNamespace;

						// check name property
						if (!oEnt.name) {
							jQuery.sap.log.error("explored: cannot register entity '" + oEnt.id + "'. missing 'name'");
							return;
						}

						// check category white list
						if (aCategoryWhiteList.indexOf(oEnt.category) === -1) {
							jQuery.sap.log.error("explored: cannot register entity '" + oEnt.id + "'. category '" + oEnt.category + "' is not allowed");
							return;
						}

						// convert content density
						if (!oEnt.formFactors) {
							jQuery.sap.log.error("explored: cannot register entity '" + oEnt.id + "'. missing 'formFactors'");
							return;
						}
						if (!mFormFactorsMap[oEnt.formFactors]) {
							jQuery.sap.log.error("explored: cannot register entity '" + oEnt.id + "'. formFactors '" + oEnt.formFactors + "' is not allowed");
							return;
						}
						oEnt.formFactors = mFormFactorsMap[oEnt.formFactors];

						// check filter properties
						var bAbortEntity = false;
						jQuery.each(afilterProps, function (i, sProp) {
							if (!oEnt[sProp]) {
								jQuery.sap.log.error("explored: cannot register entity '" + oEnt.id + "'. missing '" + sProp + "'");
								bAbortEntity = true;
								return false;
							}
						});
						if (bAbortEntity) {
							return;
						}

						// add filter properties to sets
						jQuery.each(afilterProps, function (i, sProp) {
							oFilterSets[sProp][oEnt[sProp]] = true;
						});

						oEnt.library = oDoc.library;

						// add entity
						data.entities.push(oEnt);
					});
				});

				// iterate entities one more time and add the sample data
				// (this must be done in a separate loop in order to map samples across libraries/docIndizes)
				jQuery.each(data.entities, function (sNamespace, oEnt) {
					var i = 0,
						oStep,
						fnPrependZero;

					// define search tags
					oEnt.searchTags = oEnt.name + " " + oEnt.name.replace(/\s/g, "") + " " + oEnt.category;

					// check samples property
					if (oEnt.samples && !(oEnt.samples instanceof Array)) {
						oEnt.samples = [];
						jQuery.sap.log.error("explored: cannot register samples for entity '" + oEnt.id + "'. 'samples' is not an array");
						return;
					}
					if (!oEnt.samples) {
						oEnt.samples = [];
					}

					// add samples to entity
					if (oEnt.samplesAsSteps) {
						// step-based entities: generate a sample based on the name of the step and the position in the array
						if (!(oEnt.samplesAsSteps instanceof Array)) {
							jQuery.sap.log.error("explored: cannot register samples for entity '" + oEnt.id + "'. 'samplesAsSteps' is not an array");
							return;
						}

						// helper function to add a leading 0 for all samples (folders will start with 01)
						fnPrependZero = function (iNumber) {
							if (iNumber.toString().length === 1) {
								return "0" + iNumber;
							}
							return iNumber;
						};

						for (; i < oEnt.samplesAsSteps.length; i++) {
							oStep = {
								"id": oEnt.id + "." + fnPrependZero(i + 1),
								"name": oEnt.name + " - Step " + (i + 1) + " - " + oEnt.samplesAsSteps[i]
							};

							// dynamically add a prev / next pointer to be able to cross-navigate between the samples of the same type
							if (i > 0) {
								oStep.previousSampleId = oEnt.id + "." + fnPrependZero(i);
							}
							if (i < oEnt.samplesAsSteps.length - 1) {
								oStep.nextSampleId = oEnt.id + "." + fnPrependZero(i + 2);
							}

							// add generated sample to this entity and to the samples array
							oEnt.samples.push(oStep);
							data.samples[oStep.id] = oStep;
							oEnt.searchTags += " " + oStep.name;
						}
					} else {
						// other entities: lookup samples and build search tags
						var aSamples = [],
							oPreviousSample;

						jQuery.each(oEnt.samples, function (j, sId) {
							var oSample = data.samples[sId];

							if (!oSample) {
								jQuery.sap.log.warning("explored: cannot register sample '" + sId + "' for '" + oEnt.id + "'. not found in the available docu indizes");
							} else {
								// dynamically add a prev / next pointer to be able to cross-navigate between the samples of the same type
								oSample.previousSampleId = (oPreviousSample ? oPreviousSample.id : undefined);
								if (oPreviousSample) {
									oPreviousSample.nextSampleId = oSample.id;
								}
								oPreviousSample = oSample;

								oSample.entityId = oEnt.id;

								// add the sample to the local store
								aSamples.push(oSample);
								oEnt.searchTags += " " + oSample.name;
							}
						});
						oEnt.samples = aSamples;
					}

					// set count
					oEnt.sampleCount = oEnt.samples.length;
				});

				// set count
				data.entityCount = data.entities.length;

				// convert filter sets to arrays
				jQuery.each(oFilterSets, function (setKey, setValue) {
					data.filter[setKey] = [];
					jQuery.each(setValue, function (key, value) {
						data.filter[setKey].push({id: key});
					});
				});
				// Call LibraryInfo API method for collecting all component info from the .library files
				// Note: _getLibraryInfo collects info with a delayed call so we need a callback to know
				// when the last library info is collected

				var oLibInfo = library._getLibraryInfoSingleton();
				var oLibComponents = {};
				var aPromises = [];

				// Create promises for all libraries needed
				for (var i = 0; i < aLibs.length; i++) {
					/* eslint-disable no-loop-func */
					aPromises.push(new Promise(function (fnResolve) {
						var oLibraryComponentInfo = function (oComponent) {
							oLibComponents[oComponent.library] = oComponent.componentInfo;
							fnResolve();
						};
						oLibInfo._getLibraryInfo(aLibs[i], oLibraryComponentInfo);
					}));
					/* eslint-enable no-loop-func */
				}

				// Execute promises
				Promise.all(aPromises).then(function () {
					// Callback so the loading of the library component data can be handled
					fnComponentLoadCallback && fnComponentLoadCallback();
				});

				data.libComponentInfos = oLibComponents;

				data.groups = this.getGroups(data.entities);
				return data;
			},

			findGroup: function (groups, name) {

				var group;

				for (var i = 0; i < groups.length; i++) {

					group = groups[i];

					if (group.name == name) {
						return group;
					}
				}
			},

			getGroups : function (entities) {
				var groups = [],
					entity,
					group,
					samples,
					sample,
					i,
					j;

				for (i = 0; i < entities.length; i++) {
					entity = entities[i];
					entity.key = '#/entity/' + entity.id;

					samples = entity.samples;
					for (j = 0; j < samples.length; j++) {
						sample = samples[j];
						sample.key = '#/sample/' + sample.id + "/preview";
					}

					group = this.findGroup(groups, entity.category);
					if (!group) {
						group = {
							name: entity.category,
							key: '#/group/' + entity.category,
							controls: [entity]
						};

						groups.push(group);
					} else {
						group.controls.push(entity);
					}
				}

				return groups;
			}
		};

		return ControlsInfo;

	}, /* bExport= */ true);
<commit_msg>[FIX] DemoKit: entityId property assigned for Tutorials<commit_after>/*!
 * ${copyright}
 */

// Provides information about 'explored' samples.
sap.ui.define(['jquery.sap.global', 'sap/ui/documentation/library'],
	function(jQuery, library) {
		"use strict";

		var oPromise;

		var ControlsInfo = {

			loadData: function() {
				if (!oPromise) {

					oPromise = new Promise(function(resolve, reject) {
						library._loadAllLibInfo(
							"", "_getDocuIndex",
							function (aLibs, oDocIndicies) {
								var oData = ControlsInfo._getIndices(aLibs, oDocIndicies, function () {
									// We pass the resolve method to be called when we have all the component data loaded
									resolve(oData);
								});
							});
					});
				}
				return oPromise;
			},

			_getIndices: function (aLibs, oDocIndicies, fnComponentLoadCallback) {

				var aCategoryWhiteList = [
					"Action",
					"Application",
					"Container",
					"Display",
					"Chart",
					"Mini Chart",
					"Layout",
					"List",
					"Popup",
					"Tile",
					"User Input",
					"Testing",
					"Theming",
					"Tutorial",
					"Routing",
					"Data Binding",
					"Data Visualization",
					"Map"
				];
				var afilterProps = ["namespace", "since", "category"]; // content density are set manually
				var oFilterSets = {
					namespace: {},
					since: {},
					category: {},
					formFactors: { // content density are set manually
						"Independent": true,
						"Condensed": true,
						"Compact": true,
						"Cozy": true
					}
				};
				var mFormFactorsMap = {
					"-": "Independent",
					"S": "Condensed",
					"SM": "Condensed, Compact",
					"SL": "Condensed, Cozy",
					"SML": "Condensed, Compact, Cozy",
					"M": "Compact",
					"ML": "Compact, Cozy",
					"L": "Cozy"
				};

				// init data structures
				var data = {};
				data = {};
				data.entityCount = 0;
				data.entities = [];
				data.filter = {};
				data.samples = {};

				// iterate docu indices
				jQuery.each(oDocIndicies, function (i, oDoc) {

					// check data
					if (!oDoc.explored) {
						return;
					} else if (!oDoc.explored.samplesRef) {
						jQuery.sap.log.error("explored: cannot register lib '" + oDoc.library + "'. missing 'explored.samplesRef'");
						return;
					} else if (Array.isArray(oDoc.explored.samplesRef) && oDoc.explored.samplesRef.length !== oDoc.explored.samplesRef.filter(function (oItem) {
							return oItem.namespace && oItem.ref;
						}).length) {
						jQuery.sap.log.error("explored: cannot register lib '" + oDoc.library + "'. missing 'explored.samplesRef.namespace' or 'explored.samplesRef.ref' in one or more of the configured namespaces");
						return;
					} else if (!Array.isArray(oDoc.explored.samplesRef) && !oDoc.explored.samplesRef.namespace) {
						jQuery.sap.log.error("explored: cannot register lib '" + oDoc.library + "'. missing 'explored.samplesRef.namespace'");
						return;
					} else if (!Array.isArray(oDoc.explored.samplesRef) && !oDoc.explored.samplesRef.ref) {
						jQuery.sap.log.error("explored: cannot register lib '" + oDoc.library + "'. missing 'explored.samplesRef.ref'");
						return;
					} else if (!oDoc.explored.entities) {
						jQuery.sap.log.error("explored: cannot register lib '" + oDoc.library + "'. missing 'explored.entities'");
						return;
					} else {
						jQuery.sap.log.info("explored: now reading lib '" + oDoc.library + "'");
					}

					// register sample resources
					if (Array.isArray(oDoc.explored.samplesRef)) {
						// register an array of namespaces
						oDoc.explored.samplesRef.forEach(function (oItem) {
							jQuery.sap.registerModulePath(oItem.namespace, "" + oItem.ref);
						});
					} else {
						// register a single namespace
						jQuery.sap.registerModulePath(oDoc.explored.samplesRef.namespace, "" + oDoc.explored.samplesRef.ref);
					}

					// build sample map
					jQuery.each(oDoc.explored.samples, function (i, oSample) {
						if (!oSample.id) {
							jQuery.sap.log.error("explored: cannot register sample '?'. missing 'id'");
						} else if (!oSample.name) {
							jQuery.sap.log.error("explored: cannot register sample '" + oSample.id + "'. missing 'name'");
						} else {
							data.samples[oSample.id] = oSample;
						}
					});

					// iterate entities
					jQuery.each(oDoc.explored.entities, function (j, oEnt) {

						// check id property
						if (!oEnt.id) {
							jQuery.sap.log.error("explored: cannot register entity '?'. missing 'id'");
							return;
						}

						// apply default properties
						if (oDoc.explored.entitiesDefaults) {
							jQuery.each(oDoc.explored.entitiesDefaults, function (key, value) {
								if (!oEnt.hasOwnProperty(key)) {
									oEnt[key] = value;
								}
							});
						}

						// apply namespace property
						var iIndex = oEnt.id.lastIndexOf(".");
						var sNamespace = (iIndex !== -1) ? oEnt.id.substring(0, iIndex) : oEnt.id;
						oEnt.namespace = sNamespace;

						// check name property
						if (!oEnt.name) {
							jQuery.sap.log.error("explored: cannot register entity '" + oEnt.id + "'. missing 'name'");
							return;
						}

						// check category white list
						if (aCategoryWhiteList.indexOf(oEnt.category) === -1) {
							jQuery.sap.log.error("explored: cannot register entity '" + oEnt.id + "'. category '" + oEnt.category + "' is not allowed");
							return;
						}

						// convert content density
						if (!oEnt.formFactors) {
							jQuery.sap.log.error("explored: cannot register entity '" + oEnt.id + "'. missing 'formFactors'");
							return;
						}
						if (!mFormFactorsMap[oEnt.formFactors]) {
							jQuery.sap.log.error("explored: cannot register entity '" + oEnt.id + "'. formFactors '" + oEnt.formFactors + "' is not allowed");
							return;
						}
						oEnt.formFactors = mFormFactorsMap[oEnt.formFactors];

						// check filter properties
						var bAbortEntity = false;
						jQuery.each(afilterProps, function (i, sProp) {
							if (!oEnt[sProp]) {
								jQuery.sap.log.error("explored: cannot register entity '" + oEnt.id + "'. missing '" + sProp + "'");
								bAbortEntity = true;
								return false;
							}
						});
						if (bAbortEntity) {
							return;
						}

						// add filter properties to sets
						jQuery.each(afilterProps, function (i, sProp) {
							oFilterSets[sProp][oEnt[sProp]] = true;
						});

						oEnt.library = oDoc.library;

						// add entity
						data.entities.push(oEnt);
					});
				});

				// iterate entities one more time and add the sample data
				// (this must be done in a separate loop in order to map samples across libraries/docIndizes)
				jQuery.each(data.entities, function (sNamespace, oEnt) {
					var i = 0,
						oStep,
						fnPrependZero;

					// define search tags
					oEnt.searchTags = oEnt.name + " " + oEnt.name.replace(/\s/g, "") + " " + oEnt.category;

					// check samples property
					if (oEnt.samples && !(oEnt.samples instanceof Array)) {
						oEnt.samples = [];
						jQuery.sap.log.error("explored: cannot register samples for entity '" + oEnt.id + "'. 'samples' is not an array");
						return;
					}
					if (!oEnt.samples) {
						oEnt.samples = [];
					}

					// add samples to entity
					if (oEnt.samplesAsSteps) {
						// step-based entities: generate a sample based on the name of the step and the position in the array
						if (!(oEnt.samplesAsSteps instanceof Array)) {
							jQuery.sap.log.error("explored: cannot register samples for entity '" + oEnt.id + "'. 'samplesAsSteps' is not an array");
							return;
						}

						// helper function to add a leading 0 for all samples (folders will start with 01)
						fnPrependZero = function (iNumber) {
							if (iNumber.toString().length === 1) {
								return "0" + iNumber;
							}
							return iNumber;
						};

						for (; i < oEnt.samplesAsSteps.length; i++) {
							oStep = {
								"id": oEnt.id + "." + fnPrependZero(i + 1),
								"name": oEnt.name + " - Step " + (i + 1) + " - " + oEnt.samplesAsSteps[i]
							};

							// dynamically add a prev / next pointer to be able to cross-navigate between the samples of the same type
							if (i > 0) {
								oStep.previousSampleId = oEnt.id + "." + fnPrependZero(i);
							}
							if (i < oEnt.samplesAsSteps.length - 1) {
								oStep.nextSampleId = oEnt.id + "." + fnPrependZero(i + 2);
							}

							oStep.entityId = oEnt.id;

							// add generated sample to this entity and to the samples array
							oEnt.samples.push(oStep);
							data.samples[oStep.id] = oStep;
							oEnt.searchTags += " " + oStep.name;
						}
					} else {
						// other entities: lookup samples and build search tags
						var aSamples = [],
							oPreviousSample;

						jQuery.each(oEnt.samples, function (j, sId) {
							var oSample = data.samples[sId];

							if (!oSample) {
								jQuery.sap.log.warning("explored: cannot register sample '" + sId + "' for '" + oEnt.id + "'. not found in the available docu indizes");
							} else {
								// dynamically add a prev / next pointer to be able to cross-navigate between the samples of the same type
								oSample.previousSampleId = (oPreviousSample ? oPreviousSample.id : undefined);
								if (oPreviousSample) {
									oPreviousSample.nextSampleId = oSample.id;
								}
								oPreviousSample = oSample;

								oSample.entityId = oEnt.id;

								// add the sample to the local store
								aSamples.push(oSample);
								oEnt.searchTags += " " + oSample.name;
							}
						});
						oEnt.samples = aSamples;
					}

					// set count
					oEnt.sampleCount = oEnt.samples.length;
				});

				// set count
				data.entityCount = data.entities.length;

				// convert filter sets to arrays
				jQuery.each(oFilterSets, function (setKey, setValue) {
					data.filter[setKey] = [];
					jQuery.each(setValue, function (key, value) {
						data.filter[setKey].push({id: key});
					});
				});
				// Call LibraryInfo API method for collecting all component info from the .library files
				// Note: _getLibraryInfo collects info with a delayed call so we need a callback to know
				// when the last library info is collected

				var oLibInfo = library._getLibraryInfoSingleton();
				var oLibComponents = {};
				var aPromises = [];

				// Create promises for all libraries needed
				for (var i = 0; i < aLibs.length; i++) {
					/* eslint-disable no-loop-func */
					aPromises.push(new Promise(function (fnResolve) {
						var oLibraryComponentInfo = function (oComponent) {
							oLibComponents[oComponent.library] = oComponent.componentInfo;
							fnResolve();
						};
						oLibInfo._getLibraryInfo(aLibs[i], oLibraryComponentInfo);
					}));
					/* eslint-enable no-loop-func */
				}

				// Execute promises
				Promise.all(aPromises).then(function () {
					// Callback so the loading of the library component data can be handled
					fnComponentLoadCallback && fnComponentLoadCallback();
				});

				data.libComponentInfos = oLibComponents;

				data.groups = this.getGroups(data.entities);
				return data;
			},

			findGroup: function (groups, name) {

				var group;

				for (var i = 0; i < groups.length; i++) {

					group = groups[i];

					if (group.name == name) {
						return group;
					}
				}
			},

			getGroups : function (entities) {
				var groups = [],
					entity,
					group,
					samples,
					sample,
					i,
					j;

				for (i = 0; i < entities.length; i++) {
					entity = entities[i];
					entity.key = '#/entity/' + entity.id;

					samples = entity.samples;
					for (j = 0; j < samples.length; j++) {
						sample = samples[j];
						sample.key = '#/sample/' + sample.id + "/preview";
					}

					group = this.findGroup(groups, entity.category);
					if (!group) {
						group = {
							name: entity.category,
							key: '#/group/' + entity.category,
							controls: [entity]
						};

						groups.push(group);
					} else {
						group.controls.push(entity);
					}
				}

				return groups;
			}
		};

		return ControlsInfo;

	}, /* bExport= */ true);
<|endoftext|>


<commit_before>/* global DATA, $ */

// Base D3 code by @bsmedberg
// With additional code, tweaks and UI
// goodness by @espressive
var Plot = (function() {

    var buildIDParser = d3.time.format.utc("%Y%m%d%H%M%S").parse;
    function buildIDToDate(bid) {
        return buildIDParser(bid);
    }

    function Dimensions(o) {
        if (!(this instanceof Dimensions)) {
            throw Error("Use new Dimensions()");
        }
        if (o !== undefined) {
            for (var k in o) {
            this[k] = o[k];
            }
        }
    }

    Dimensions.prototype.radius = function() {
      return Math.min(this.width, this.height) / 2;
    };

    Dimensions.prototype.totalWidth = function() {
      return this.width + this.marginLeft + this.marginRight;
    };

    Dimensions.prototype.totalHeight = function() {
      return this.height + this.marginTop + this.marginBottom;
    };

    Dimensions.prototype.transformUpperLeft = function(e) {
      e.attr("transform", "translate(" + this.marginLeft + "," + this.marginTop + ")");
    };

    Dimensions.prototype.transformCenter = function(e) {
      e.attr("transform", "translate(" + (this.marginLeft + this.width / 2) + "," +
             (this.marginTop + this.height / 2) + ")");
    };

    Dimensions.prototype.setupSVG = function(e) {
      e.attr({
        width: this.totalWidth(),
        height: this.totalHeight()
      });
    };

    /**
     * Draws the x grid lines
     */
    function make_x_axis(x) {
        return d3.svg.axis()
            .scale(x)
            .orient('bottom')
            .ticks(4);
    }

    /**
     * Draws the y grid lines
     */
    function make_y_axis(y) {
        return d3.svg.axis()
            .scale(y)
            .orient('left')
            .ticks(4);
    }

    /**
     * Draws the ADU graph
     * @param {object} data - The JSON data (data.hits)
     * @param {object} container - The graph container as jQuery object
     */
    function drawGraph(data, container) {

        var finalData = d3.nest()
            .key(function(d) {
                return d.buildid
            })
            .rollup(function(dlist) {
                var r = {
                    adu_count: d3.sum(dlist, function(d) {
                        return d.adu_count;
                    }),
                    crash_count: d3.sum(dlist, function(d) {
                        return d.crash_count;
                    })
                };

                if (r.adu_count) {
                    r.ratio = r.crash_count / r.adu_count;
                }
                return r;
            })
            .sortKeys()
            .entries(data)
            .filter(function(d) {
                return d.values.ratio !== undefined;
            });

        var adus = finalData.map(function(d) {
            return d.values.adu_count;
        });
        adus.sort(d3.ascending);

        var cutoff = d3.quantile(adus, 0.2);

        finalData = finalData.filter(function(d) {
            return d.values.adu_count > cutoff;
        });

        var dims = new Dimensions({
            width: 800,
            height: 250,
            marginTop: 25,
            marginLeft: 95,
            marginRight: 5,
            marginBottom: 50
        });

        // setup the x and y axis scales
        var minx = buildIDToDate(finalData[0].key);
        var maxx = buildIDToDate(finalData[finalData.length -1].key);
        var x = d3.time.scale()
            .range([0, dims.width])
            .domain([minx, maxx]);
        var xaxis = d3.svg.axis()
            .scale(x)
            .orient('bottom');

        var maxy = d3.max(finalData, function(d) {
            return d.values.ratio;
        });
        var y = d3.scale.linear()
            .rangeRound([10, dims.height])
            .domain([maxy, 0]);
        var yaxis = d3.svg.axis()
            .scale(y)
            .ticks(4)
            .orient('left');

        var svg = d3.select('svg', container)
            .call(function(d) {
                dims.setupSVG(d);
            })
            .append('g')
            .call(function(d) {
                dims.transformUpperLeft(d);
            });

        svg.append('text')
            .text('Hover over a point below to see the details for each data point.')
            .attr('x', 0)
            .attr('y', -5);

        svg.append('g')
           .attr('class', 'grid')
           .attr('transform', 'translate(0,' + dims.height + ')')
           .call(make_x_axis(x)
                .tickSize(-dims.height + 10, 0, 0)
                .tickFormat('')
            );

        svg.append('g')
           .attr('class', 'grid')
           .call(make_y_axis(y)
                .tickSize(-dims.width, 0, 0)
                .tickFormat('')
            );

        svg.append('g')
           .attr('class', 'x axis')
           .attr('transform', 'translate(0,' + dims.height + ')')
           .call(xaxis);

        svg.append('text')
           .text('Build Date')
           .attr('x', dims.width / 2)
           .attr('y', dims.height + 32)
           .attr('dominant-baseline', 'hanging');

        svg.append('g')
           .attr('class', 'y axis')
           .call(yaxis);

        svg.append('text')
           .text('Crashes/ADU')
           .attr('transform', 'translate(' + (-dims.marginLeft + 5) + ',' + (dims.height / 2) + ') rotate(-90)')
           .attr('text-anchor', 'middle')
           .attr('dominant-baseline', 'hanging');

        var tooltip = d3.select('.adubysig').append('div')
            .attr('class', 'tooltip')
            .style('top', 0)
            .style('left', 0);

        var points = svg.selectAll('.point')
            .data(finalData);

        var entered = points.enter()
            .append('circle')
            .attr('class', 'point main')
            .attr('fill', '#406a80')
            .attr('cx', function(d) {
                return x(buildIDToDate(d.key));
            })
            .attr('cy', function(d) {
                return y(d.values.ratio);
            })
            .attr('r', function(d) {
                var size = Math.ceil(d.values.crash_count / 100);
                return size < 10 ? size + 2 : size;
            })
            .style('opacity', 0)
            .on('mouseenter', function(d) {
                var pos = d3.mouse(this);

                var tooltipContentContainer = $('<ul />', {
                    class: 'tooltip-content'
                });

                var buildId = $('<li />', {
                    text: 'BuildID: ' + d.key
                });

                var aduCount = $('<li />', {
                    text: 'ADU Count: ' + d.values.adu_count
                });

                var crashCount = $('<li />', {
                    text: 'Crash Count: ' + d.values.crash_count
                });

                tooltipContentContainer.append([buildId, aduCount, crashCount]);

                var calcTop = pos[1] + 60;
                var calcLeft = pos[0] - 60;

                tooltip.style('display', 'block')
                    .style('top', calcTop + 'px')
                    .style('left', calcLeft > 100 ?
                            calcLeft + 'px' :
                            (calcLeft + 200) + 'px')
                    .html(tooltipContentContainer.html())
                    .transition()
                    .delay(300)
                    .duration(500)
                    .style('opacity', 1);
            })
            .on('mouseout', function(d) {
                tooltip.transition()
                    .style('opacity', 0);
            });

        entered.transition()
            .delay(400)
            .duration(600)
            .style('opacity', 1);
    }

    /**
     * Adds a select drop-down to the graph tab, so users
     * can toggle between different channels.
     * param {object} dataSet - Object containing all data attribute values
     */
    function addChannelSelector(dataSet) {
        var channels = dataSet.fallback.split(',');
        var channelsLength = channels.length;

        var label = $('<label />', {
            for: 'channel-select',
            text: 'Release Channel'
        })
        var select = $('<select />', {
            id: 'channel-select'
        });

        $(channels).each(function(i, channel) {
            var option = $('<option />', {
                value: channel,
                text: channel,
            });

            // set nightly release channel as the default selected
            if (channel === 'nightly') {
                option.attr('selected', 'selected');
            }

            select.append(option);
        });

        var heading = $('#adubysig-heading');
        // strick the select and it's label just after the heading
        heading.after(label, select);

        select.on('change', function() {

            var channel = $(this).val();
            var titleCasedChannel = channel.replace(
                channel.charAt(0),
                channel.charAt(0).toUpperCase());

            // Empty the fallback property so that the Ajax
            // requests will not incorrectly follow the fallback path.
            dataSet['fallback'] = '';

            dataSet['channel'] = channel;

            // update the heading text to reflect the new channel
            headingTxt = heading.data('heading').replace('{channel}', titleCasedChannel);
            heading.text(headingTxt);

            aduBySignature(null, dataSet);
        });
    }

    /**
     * Sets up the data for the graph
     * @param {object} panel - The graph panel container.
     * @param {object} [dataSet] - Object containing all data attribute values
     */
    function aduBySignature(panel, dataSet) {

        // if the panel is null, use the provided dataSet
        var dataSet = panel ? panel[0].dataset : dataSet;
        var container = $('.adubysig-graph');
        var noDataContainer = $('.no-data', panel);

        // show the loading graph spinner
        var loader = $('#loading-graph');
        loader.removeClass('hide');

        var params = {
            product_name: dataSet['product'],
            days: dataSet['days'],
            signature: dataSet['signature'],
            channel: dataSet['channel']
        };
        var url = dataSet['jsonurl'] + '?' + $.param(params);

        $.getJSON(url, function(data) {

            loader.addClass('hide');
            $('svg', container).empty().attr({
                width: 0,
                height: 0
            });

            noDataContainer.addClass('hide');

            if (data.total) {

                container.removeClass('hide');

                if (dataSet['fallback']) {
                    // if there was no version passed to report/list
                    // we fallback to nightly and need to provide the
                    // user with a way to switch between release channels.
                    addChannelSelector(dataSet);
                    drawGraph(data.hits, container);
                } else {
                    drawGraph(data.hits, container);
                }
            } else {
                noDataContainer.removeClass('hide');
            }
        })
        .fail(function(jQXhr, textStatus, errorThrown) {
            var error = 'text status: ' + textStatus;

            if(errorThrown) {
                error += ' error: ' + errorThrown;
            }

            noDataContainer.empty()
                .text('Error while loadng data: ' + error)
                .removeClass('hide');
        });
    }

    return {
       draw: function(panel) {
            aduBySignature(panel);
       }
    };

})();

var Graph = (function() {
    var loaded = null;

    return {
       activate: function() {
           if (loaded) return;
           var deferred = $.Deferred();
           var $panel = $('#graph');
           var url = $panel.data('partial-url');
           var qs = location.href.split('?')[1];
           url += '?' + qs;
           var req = $.ajax({
               url: url
           });
           req.done(function(response) {
               $('.inner', $panel).html(response);
               $('.loading-placeholder', $panel).hide();

               Plot.draw($panel);

               deferred.resolve();
           });
           req.fail(function(data, textStatus, errorThrown) {
               $('.loading-failed', $panel).show();
               deferred.reject(data, textStatus, errorThrown);
           });
           loaded = true;
           return deferred.promise();
       }
    };
})();

Panels.register('graph', function() {
    return Graph.activate();
});
<commit_msg>jshint fixes<commit_after>/* global $ */

// Base D3 code by @bsmedberg
// With additional code, tweaks and UI
// goodness by @espressive
var Plot = (function() {

    var buildIDParser = d3.time.format.utc("%Y%m%d%H%M%S").parse;
    function buildIDToDate(bid) {
        return buildIDParser(bid);
    }

    function Dimensions(o) {
        if (!(this instanceof Dimensions)) {
            throw Error("Use new Dimensions()");
        }
        if (o !== undefined) {
            for (var k in o) {
            this[k] = o[k];
            }
        }
    }

    Dimensions.prototype.radius = function() {
      return Math.min(this.width, this.height) / 2;
    };

    Dimensions.prototype.totalWidth = function() {
      return this.width + this.marginLeft + this.marginRight;
    };

    Dimensions.prototype.totalHeight = function() {
      return this.height + this.marginTop + this.marginBottom;
    };

    Dimensions.prototype.transformUpperLeft = function(e) {
      e.attr("transform", "translate(" + this.marginLeft + "," + this.marginTop + ")");
    };

    Dimensions.prototype.transformCenter = function(e) {
      e.attr("transform", "translate(" + (this.marginLeft + this.width / 2) + "," +
             (this.marginTop + this.height / 2) + ")");
    };

    Dimensions.prototype.setupSVG = function(e) {
      e.attr({
        width: this.totalWidth(),
        height: this.totalHeight()
      });
    };

    /**
     * Draws the x grid lines
     */
    function make_x_axis(x) {
        return d3.svg.axis()
            .scale(x)
            .orient('bottom')
            .ticks(4);
    }

    /**
     * Draws the y grid lines
     */
    function make_y_axis(y) {
        return d3.svg.axis()
            .scale(y)
            .orient('left')
            .ticks(4);
    }

    /**
     * Draws the ADU graph
     * @param {object} data - The JSON data (data.hits)
     * @param {object} container - The graph container as jQuery object
     */
    function drawGraph(data, container) {

        var finalData = d3.nest()
            .key(function(d) {
                return d.buildid;
            })
            .rollup(function(dlist) {
                var r = {
                    adu_count: d3.sum(dlist, function(d) {
                        return d.adu_count;
                    }),
                    crash_count: d3.sum(dlist, function(d) {
                        return d.crash_count;
                    })
                };

                if (r.adu_count) {
                    r.ratio = r.crash_count / r.adu_count;
                }
                return r;
            })
            .sortKeys()
            .entries(data)
            .filter(function(d) {
                return d.values.ratio !== undefined;
            });

        var adus = finalData.map(function(d) {
            return d.values.adu_count;
        });
        adus.sort(d3.ascending);

        var cutoff = d3.quantile(adus, 0.2);

        finalData = finalData.filter(function(d) {
            return d.values.adu_count > cutoff;
        });

        var dims = new Dimensions({
            width: 800,
            height: 250,
            marginTop: 25,
            marginLeft: 95,
            marginRight: 5,
            marginBottom: 50
        });

        // setup the x and y axis scales
        var minx = buildIDToDate(finalData[0].key);
        var maxx = buildIDToDate(finalData[finalData.length -1].key);
        var x = d3.time.scale()
            .range([0, dims.width])
            .domain([minx, maxx]);
        var xaxis = d3.svg.axis()
            .scale(x)
            .orient('bottom');

        var maxy = d3.max(finalData, function(d) {
            return d.values.ratio;
        });
        var y = d3.scale.linear()
            .rangeRound([10, dims.height])
            .domain([maxy, 0]);
        var yaxis = d3.svg.axis()
            .scale(y)
            .ticks(4)
            .orient('left');

        var svg = d3.select('svg', container)
            .call(function(d) {
                dims.setupSVG(d);
            })
            .append('g')
            .call(function(d) {
                dims.transformUpperLeft(d);
            });

        svg.append('text')
            .text('Hover over a point below to see the details for each data point.')
            .attr('x', 0)
            .attr('y', -5);

        svg.append('g')
           .attr('class', 'grid')
           .attr('transform', 'translate(0,' + dims.height + ')')
           .call(make_x_axis(x)
                .tickSize(-dims.height + 10, 0, 0)
                .tickFormat('')
            );

        svg.append('g')
           .attr('class', 'grid')
           .call(make_y_axis(y)
                .tickSize(-dims.width, 0, 0)
                .tickFormat('')
            );

        svg.append('g')
           .attr('class', 'x axis')
           .attr('transform', 'translate(0,' + dims.height + ')')
           .call(xaxis);

        svg.append('text')
           .text('Build Date')
           .attr('x', dims.width / 2)
           .attr('y', dims.height + 32)
           .attr('dominant-baseline', 'hanging');

        svg.append('g')
           .attr('class', 'y axis')
           .call(yaxis);

        svg.append('text')
           .text('Crashes/ADU')
           .attr('transform', 'translate(' + (-dims.marginLeft + 5) + ',' + (dims.height / 2) + ') rotate(-90)')
           .attr('text-anchor', 'middle')
           .attr('dominant-baseline', 'hanging');

        var tooltip = d3.select('.adubysig').append('div')
            .attr('class', 'tooltip')
            .style('top', 0)
            .style('left', 0);

        var points = svg.selectAll('.point')
            .data(finalData);

        var entered = points.enter()
            .append('circle')
            .attr('class', 'point main')
            .attr('fill', '#406a80')
            .attr('cx', function(d) {
                return x(buildIDToDate(d.key));
            })
            .attr('cy', function(d) {
                return y(d.values.ratio);
            })
            .attr('r', function(d) {
                var size = Math.ceil(d.values.crash_count / 100);
                return size < 10 ? size + 2 : size;
            })
            .style('opacity', 0)
            .on('mouseenter', function(d) {
                var pos = d3.mouse(this);

                var tooltipContentContainer = $('<ul />', {
                    class: 'tooltip-content'
                });

                var buildId = $('<li />', {
                    text: 'BuildID: ' + d.key
                });

                var aduCount = $('<li />', {
                    text: 'ADU Count: ' + d.values.adu_count
                });

                var crashCount = $('<li />', {
                    text: 'Crash Count: ' + d.values.crash_count
                });

                tooltipContentContainer.append([buildId, aduCount, crashCount]);

                var calcTop = pos[1] + 60;
                var calcLeft = pos[0] - 60;

                tooltip.style('display', 'block')
                    .style('top', calcTop + 'px')
                    .style('left', calcLeft > 100 ?
                            calcLeft + 'px' :
                            (calcLeft + 200) + 'px')
                    .html(tooltipContentContainer.html())
                    .transition()
                    .delay(300)
                    .duration(500)
                    .style('opacity', 1);
            })
            .on('mouseout', function(d) {
                tooltip.transition()
                    .style('opacity', 0);
            });

        entered.transition()
            .delay(400)
            .duration(600)
            .style('opacity', 1);
    }

    /**
     * Adds a select drop-down to the graph tab, so users
     * can toggle between different channels.
     * param {object} dataSet - Object containing all data attribute values
     */
    function addChannelSelector(dataSet) {
        var channels = dataSet.fallback.split(',');
        var channelsLength = channels.length;

        var label = $('<label />', {
            for: 'channel-select',
            text: 'Release Channel'
        });
        var select = $('<select />', {
            id: 'channel-select'
        });

        $(channels).each(function(i, channel) {
            var option = $('<option />', {
                value: channel,
                text: channel,
            });

            // set nightly release channel as the default selected
            if (channel === 'nightly') {
                option.attr('selected', 'selected');
            }

            select.append(option);
        });

        var heading = $('#adubysig-heading');
        // strick the select and it's label just after the heading
        heading.after(label, select);

        select.on('change', function() {

            var channel = $(this).val();
            var titleCasedChannel = channel.replace(
                channel.charAt(0),
                channel.charAt(0).toUpperCase());

            // Empty the fallback property so that the Ajax
            // requests will not incorrectly follow the fallback path.
            dataSet.fallback = '';

            dataSet.channel = channel;

            // update the heading text to reflect the new channel
            headingTxt = heading.data('heading').replace('{channel}', titleCasedChannel);
            heading.text(headingTxt);

            aduBySignature(null, dataSet);
        });
    }

    /**
     * Sets up the data for the graph
     * @param {object} panel - The graph panel container.
     * @param {object} [dataSet] - Object containing all data attribute values
     */
    function aduBySignature(panel, dataSet) {

        // if the panel is null, use the provided dataSet
        dataSet = panel ? panel[0].dataset : dataSet;
        var container = $('.adubysig-graph');
        var noDataContainer = $('.no-data', panel);

        // show the loading graph spinner
        var loader = $('#loading-graph');
        loader.removeClass('hide');

        var params = {
            product_name: dataSet.product,
            days: dataSet.days,
            signature: dataSet.signature,
            channel: dataSet.channel
        };
        var url = dataSet.jsonurl + '?' + $.param(params);

        $.getJSON(url, function(data) {

            loader.addClass('hide');
            $('svg', container).empty().attr({
                width: 0,
                height: 0
            });

            noDataContainer.addClass('hide');

            if (data.total) {

                container.removeClass('hide');

                if (dataSet.fallback) {
                    // if there was no version passed to report/list
                    // we fallback to nightly and need to provide the
                    // user with a way to switch between release channels.
                    addChannelSelector(dataSet);
                    drawGraph(data.hits, container);
                } else {
                    drawGraph(data.hits, container);
                }
            } else {
                noDataContainer.removeClass('hide');
            }
        })
        .fail(function(jQXhr, textStatus, errorThrown) {
            var error = 'text status: ' + textStatus;

            if(errorThrown) {
                error += ' error: ' + errorThrown;
            }

            noDataContainer.empty()
                .text('Error while loadng data: ' + error)
                .removeClass('hide');
        });
    }

    return {
       draw: function(panel) {
            aduBySignature(panel);
       }
    };

})();

var Graph = (function() {
    var loaded = null;

    return {
       activate: function() {
           if (loaded) return;
           var deferred = $.Deferred();
           var $panel = $('#graph');
           var url = $panel.data('partial-url');
           var qs = location.href.split('?')[1];
           url += '?' + qs;
           var req = $.ajax({
               url: url
           });
           req.done(function(response) {
               $('.inner', $panel).html(response);
               $('.loading-placeholder', $panel).hide();

               Plot.draw($panel);

               deferred.resolve();
           });
           req.fail(function(data, textStatus, errorThrown) {
               $('.loading-failed', $panel).show();
               deferred.reject(data, textStatus, errorThrown);
           });
           loaded = true;
           return deferred.promise();
       }
    };
})();

Panels.register('graph', function() {
    return Graph.activate();
});
<|endoftext|>


<commit_before>console.log('using userRoutes.js');

module.exports = function(router) {

  var User = require("../models/userSchema");
  var fblocal = require('../controllers/fblocal');
  var FB = require('fb');
  var fb = new FB.Facebook({version: 'v2.8'});

  //test function. Don't call this unless you need to see derp
  router.get("/derp", function(req, res) {
    fblocal.printDerp();
  });

  router.get("/fb/friends", function(req, res) {
    FB.api('me/friends', { access_token: fblocal.appAccessToken}, function (res) {
    	if(!res || res.error) {
    		console.log(!res ? 'error occurred' : res.error);
    		return;
  		}

    	console.log(res.data);
  	});
    res.sendStatus(200);
  });

  router.post("/fb/userIDToken", function(req, res) {
    fblocal.updateUserID(req.body.userID);
    fblocal.updateToken(req.body.token);

  });

  router.post("/db/userInsert", function(req, res) {

    FB.api('me?fields=id,name,friends,picture{url}', { access_token: fblocal.appAccessToken}, function (res) {
      if(!res || res.error) {
        console.log(!res ? 'error occurred' : res.error);
        return;
      }

      var newUser = new User({
        FBid: res.id,
        FBName: res.name,
        photolink: res.picture.data.url
      });

      newUser.save(function(error, doc) {
        if (error) {
          console.log(error);
        }
        // Or log the doc
        else {
          console.log('saving');
          console.log(doc);
        }
      });

    });
    res.sendStatus(200);

  });

  router.put('db/user/destination', function(req, res) {

    var regionDestination = req.body.zipValue;

    var roomie = new UserModel({regionDestination:regionDestination});

    roomie.save(function(error, result){

      if (error){
        console.log(error);
        return res.status(500).send({error: 'AN_ERROR_OCCURED'});
      }

           res.json({success:true});

    });

  });

router.put("db/user/response", function(req, res) {
    var age = req.body.age;
    var livingStyle = req.body['livingStyle[]'];

    User.findOneAndUpdate({"FBid": fblocal.userID}),
    {
      livingStyle: livingStyle,
      age: age
    },
    function(err, user) {
      if (err) {
        console.log("Something's wrong. Error data:" + err);
        return;
      }
      if (!user) {
        console.log("No User found");
        return;
      }

    }

  });

});
}
<commit_msg>added moments<commit_after>console.log('using userRoutes.js');
var moment = require('moment');

module.exports = function(router) {

  var User = require("../models/userSchema");
  var fblocal = require('../controllers/fblocal');
  var FB = require('fb');
  var fb = new FB.Facebook({version: 'v2.8'});

  //test function. Don't call this unless you need to see derp
  router.get("/derp", function(req, res) {
    fblocal.printDerp();
  });

  router.get("/fb/friends", function(req, res) {
      FB.api("me?fields=friends{id}", { access_token: fblocal.appAccessToken}, function (res) {
        if(!res || res.error) {
          console.log(!res ? 'error occurred' : res.error);
          return;
        }

        console.log(res.friends.data);
      });
      res.sendStatus(200);
  });

  router.post("/fb/userIDToken", function(req, res) {
      fblocal.updateUserID(req.body.userID);
      fblocal.updateToken(req.body.token);
      res.sendStatus(200);

  });

  router.post("/db/userInsert", function(req, res) {

      FB.api("me?fields=id,name,friends,picture{url},email,birthday", { access_token: fblocal.appAccessToken}, function (res) {
          if(!res || res.error) {
            console.log(!res ? 'error occurred' : res.error);
            return;
          }

          var stringDOB = moment(res.birthday);
          var age = moment().diff(stringDOB, 'years');

          User.findOneAndUpdate(
            { FBid: res.id  },
            {
              FBName: res.name,
              photolink: res.picture.data.url,
              FBEmail: res.email,
              friendList: res.friends.data,
              age: age
            },
            {
              upsert: true
            },
            function(err, doc) {
              if(err) {
                console.log(!res ? 'error occurred' : res.error);
                return;
              }
              else {
                console.log('updating:', doc);
              }
          });
      });
      res.sendStatus(200);

  });

  router.put('db/user/destination', function(req, res) {

    var regionDestination = req.body.zipValue;

    var roomie = new UserModel({regionDestination:regionDestination});

    roomie.save(function(error, result){

      if (error){
        console.log(error);
        return res.status(500).send({error: 'AN_ERROR_OCCURED'});
      }

           res.json({success:true});

    });

  });

router.put("db/user/response", function(req, res) {
    var age = req.body.age;
    var livingStyle = req.body['livingStyle[]'];

    User.findOneAndUpdate({"FBid": fblocal.userID}),
    {
      livingStyle: livingStyle,
      age: age
    },
    function(err, user) {
      if (err) {
        console.log("Something's wrong. Error data:" + err);
        return;
      }
      if (!user) {
        console.log("No User found");
        return;
      }

    }

  });

}
<|endoftext|>


<commit_before>// Copyright (c) 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

'use strict';

/**
 * Creates a new scroll bar element.
 * @extends {HTMLDivElement}
 * @constructor
 */
var ScrollBar = cr.ui.define('div');

/**
 * Creates a vertical scrollbar.
 *
 * @param {Element} parent Parent element, must have a relative or absolute
 *     positioning.
 * @param {Element=} opt_scrollableArea Element with scrollable contents.
 *     If not passed, then call attachToView manually when the scrollable
 *     element becomes available.
 */
ScrollBar.createVertical = function(parent, opt_scrollableArea) {
  var scrollbar = new ScrollBar();
  parent.appendChild(scrollbar);
  if (opt_scrollableArea)
    scrollbar.attachToView(opt_scrollableArea);
};

/**
 * Mode of the scrollbar. As for now, only vertical scrollbars are supported.
 * @type {number}
 */
ScrollBar.Mode = {
  VERTICAL: 0,
  HORIZONTAL: 1
};

ScrollBar.prototype = {
  set mode(value) {
    this.mode_ = value;
    if (this.mode_ == ScrollBar.Mode.VERTICAL) {
      this.classList.remove('scrollbar-horizontal');
      this.classList.add('scrollbar-vertical');
    } else {
      this.classList.remove('scrollbar-vertical');
      this.classList.add('scrollbar-horizontal');
    }
    this.redraw_();
  },
  get mode() {
    return this.mode_;
  }
};

/**
 * Inherits after HTMLDivElement.
 */
ScrollBar.prototype.__proto__ = HTMLDivElement.prototype;

/**
 * Initializes the DOM structure of the scrollbar.
 */
ScrollBar.prototype.decorate = function() {
  this.classList.add('scrollbar');
  this.button_ = util.createChild(this, 'scrollbar-button', 'div');
  this.mode = ScrollBar.Mode.VERTICAL;

  this.button_.addEventListener('mousedown',
                                this.onButtonPressed_.bind(this));
  window.addEventListener('mouseup', this.onMouseUp_.bind(this));
  window.addEventListener('mousemove', this.onMouseMove_.bind(this));
};

/**
 * Attaches the scrollbar to a scrollable element and attaches handlers.
 * @param {Element} view Scrollable element.
 */
ScrollBar.prototype.attachToView = function(view) {
  this.view_ = view;
  this.view_.addEventListener('scroll', this.onScroll_.bind(this));
  this.view_.addEventListener('relayout', this.onRelayout_.bind(this));
  this.domObserver_ = new WebKitMutationObserver(this.onDomChanged_.bind(this));
  this.domObserver_.observe(this.view_, {subtree: true, attributes: true});
  this.onRelayout_();
};

/**
 * Scroll handler.
 * @private
 */
ScrollBar.prototype.onScroll_ = function() {
  this.scrollTop_ = this.view_.scrollTop;
  this.redraw_();
};

/**
 * Relayout handler.
 * @private
 */
ScrollBar.prototype.onRelayout_ = function() {
  this.scrollHeight_ = this.view_.scrollHeight;
  this.clientHeight_ = this.view_.clientHeight;
  this.offsetTop_ = this.view_.offsetTop;
  this.scrollTop_ = this.view_.scrollTop;
  this.redraw_();
};

/**
 * Pressing on the scrollbar's button handler.
 *
 * @param {Event} event Pressing event.
 * @private
 */
ScrollBar.prototype.onButtonPressed_ = function(event) {
  this.buttonPressed_ = true;
  this.buttonPressedEvent_ = event;
  this.buttonPressedPosition_ = this.button_.offsetTop - this.view_.offsetTop;
  this.button_.classList.add('pressed');

  event.preventDefault();
};

/**
 * Releasing the button handler. Note, that it may not be called when releasing
 * outside of the window. Therefore this is also called from onMouseMove_.
 *
 * @param {Event} event Mouse event.
 * @private
 */
ScrollBar.prototype.onMouseUp_ = function(event) {
  this.buttonPressed_ = false;
  this.button_.classList.remove('pressed');
};

/**
 * Mouse move handler. Updates the scroll position.
 *
 * @param {Event} event Mouse event.
 * @private
 */
ScrollBar.prototype.onMouseMove_ = function(event) {
  if (!this.buttonPressed_)
    return;
  if (!event.which) {
    this.onMouseUp_(event);
    return;
  }
  var clientSize = this.clientHeight_;
  var totalSize = this.scrollHeight_;
  var buttonSize = Math.max(50, clientSize / totalSize * clientSize);

  var buttonPosition = this.buttonPressedPosition_ +
      (event.screenY - this.buttonPressedEvent_.screenY);
  var scrollPosition = totalSize * (buttonPosition / clientSize);

  this.scrollTop_ = scrollPosition;
  this.view_.scrollTop = scrollPosition;
  this.redraw_();
};

/**
 * Handles changed in Dom by redrawing the scrollbar. Ignores consecutive calls.
 * @private
 */
ScrollBar.prototype.onDomChanged_ = function() {
  if (this.domChangedTimer_) {
    clearTimeout(this.domChangedTimer_);
    this.domChangedTimer_ = null;
  }
  this.domChangedTimer_ = setTimeout(function() {
    this.onRelayout_();
    this.domChangedTimer_ = null;
  }.bind(this), 50);
};

/**
 * Redraws the scrollbar.
 * @private
 */
ScrollBar.prototype.redraw_ = function() {
  if (!this.view_)
    return;

  var clientSize = this.clientHeight_;
  var clientTop = this.offsetTop_;
  var scrollPosition = this.scrollTop_;
  var totalSize = this.scrollHeight_;
  var hidden = totalSize <= clientSize;

  var buttonSize = Math.max(50, clientSize / totalSize * clientSize);
  var buttonPosition = scrollPosition / (totalSize - clientSize) *
      (clientSize - buttonSize);
  var buttonTop = buttonPosition + clientTop;

  var time = Date.now();
  if (this.hidden != hidden ||
      this.lastButtonTop_ != buttonTop ||
      this.lastButtonSize_ != buttonSize) {
    requestAnimationFrame(function() {
      this.hidden = hidden;
      this.button_.style.top = buttonTop + 'px';
      this.button_.style.height = buttonSize + 'px';
    }.bind(this));
  }

  this.lastButtonTop_ = buttonTop;
  this.lastButtonSize_ = buttonSize;
};
<commit_msg>Files.app: Prevent a scroolbar from overshooting the border of the view.<commit_after>// Copyright (c) 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

'use strict';

/**
 * Creates a new scroll bar element.
 * @extends {HTMLDivElement}
 * @constructor
 */
var ScrollBar = cr.ui.define('div');

/**
 * Creates a vertical scrollbar.
 *
 * @param {Element} parent Parent element, must have a relative or absolute
 *     positioning.
 * @param {Element=} opt_scrollableArea Element with scrollable contents.
 *     If not passed, then call attachToView manually when the scrollable
 *     element becomes available.
 */
ScrollBar.createVertical = function(parent, opt_scrollableArea) {
  var scrollbar = new ScrollBar();
  parent.appendChild(scrollbar);
  if (opt_scrollableArea)
    scrollbar.attachToView(opt_scrollableArea);
};

/**
 * Mode of the scrollbar. As for now, only vertical scrollbars are supported.
 * @type {number}
 */
ScrollBar.Mode = {
  VERTICAL: 0,
  HORIZONTAL: 1
};

ScrollBar.prototype = {
  set mode(value) {
    this.mode_ = value;
    if (this.mode_ == ScrollBar.Mode.VERTICAL) {
      this.classList.remove('scrollbar-horizontal');
      this.classList.add('scrollbar-vertical');
    } else {
      this.classList.remove('scrollbar-vertical');
      this.classList.add('scrollbar-horizontal');
    }
    this.redraw_();
  },
  get mode() {
    return this.mode_;
  }
};

/**
 * Inherits after HTMLDivElement.
 */
ScrollBar.prototype.__proto__ = HTMLDivElement.prototype;

/**
 * Initializes the DOM structure of the scrollbar.
 */
ScrollBar.prototype.decorate = function() {
  this.classList.add('scrollbar');
  this.button_ = util.createChild(this, 'scrollbar-button', 'div');
  this.mode = ScrollBar.Mode.VERTICAL;

  this.button_.addEventListener('mousedown',
                                this.onButtonPressed_.bind(this));
  window.addEventListener('mouseup', this.onMouseUp_.bind(this));
  window.addEventListener('mousemove', this.onMouseMove_.bind(this));
};

/**
 * Attaches the scrollbar to a scrollable element and attaches handlers.
 * @param {Element} view Scrollable element.
 */
ScrollBar.prototype.attachToView = function(view) {
  this.view_ = view;
  this.view_.addEventListener('scroll', this.onScroll_.bind(this));
  this.view_.addEventListener('relayout', this.onRelayout_.bind(this));
  this.domObserver_ = new WebKitMutationObserver(this.onDomChanged_.bind(this));
  this.domObserver_.observe(this.view_, {subtree: true, attributes: true});
  this.onRelayout_();
};

/**
 * Scroll handler.
 * @private
 */
ScrollBar.prototype.onScroll_ = function() {
  this.scrollTop_ = this.view_.scrollTop;
  this.redraw_();
};

/**
 * Relayout handler.
 * @private
 */
ScrollBar.prototype.onRelayout_ = function() {
  this.scrollHeight_ = this.view_.scrollHeight;
  this.clientHeight_ = this.view_.clientHeight;
  this.offsetTop_ = this.view_.offsetTop;
  this.scrollTop_ = this.view_.scrollTop;
  this.redraw_();
};

/**
 * Pressing on the scrollbar's button handler.
 *
 * @param {Event} event Pressing event.
 * @private
 */
ScrollBar.prototype.onButtonPressed_ = function(event) {
  this.buttonPressed_ = true;
  this.buttonPressedEvent_ = event;
  this.buttonPressedPosition_ = this.button_.offsetTop - this.view_.offsetTop;
  this.button_.classList.add('pressed');

  event.preventDefault();
};

/**
 * Releasing the button handler. Note, that it may not be called when releasing
 * outside of the window. Therefore this is also called from onMouseMove_.
 *
 * @param {Event} event Mouse event.
 * @private
 */
ScrollBar.prototype.onMouseUp_ = function(event) {
  this.buttonPressed_ = false;
  this.button_.classList.remove('pressed');
};

/**
 * Mouse move handler. Updates the scroll position.
 *
 * @param {Event} event Mouse event.
 * @private
 */
ScrollBar.prototype.onMouseMove_ = function(event) {
  if (!this.buttonPressed_)
    return;
  if (!event.which) {
    this.onMouseUp_(event);
    return;
  }
  var clientSize = this.clientHeight_;
  var totalSize = this.scrollHeight_;
  var buttonSize = Math.max(50, clientSize / totalSize * clientSize);

  var buttonPosition = this.buttonPressedPosition_ +
      (event.screenY - this.buttonPressedEvent_.screenY);
  // Ensures the scrollbar is in the view.
  buttonPosition =
      Math.max(0, Math.min(buttonPosition, clientSize - buttonSize));
  var scrollPosition = totalSize * (buttonPosition / clientSize);

  this.scrollTop_ = scrollPosition;
  this.view_.scrollTop = scrollPosition;
  this.redraw_();
};

/**
 * Handles changed in Dom by redrawing the scrollbar. Ignores consecutive calls.
 * @private
 */
ScrollBar.prototype.onDomChanged_ = function() {
  if (this.domChangedTimer_) {
    clearTimeout(this.domChangedTimer_);
    this.domChangedTimer_ = null;
  }
  this.domChangedTimer_ = setTimeout(function() {
    this.onRelayout_();
    this.domChangedTimer_ = null;
  }.bind(this), 50);
};

/**
 * Redraws the scrollbar.
 * @private
 */
ScrollBar.prototype.redraw_ = function() {
  if (!this.view_)
    return;

  var clientSize = this.clientHeight_;
  var clientTop = this.offsetTop_;
  var scrollPosition = this.scrollTop_;
  var totalSize = this.scrollHeight_;
  var hidden = totalSize <= clientSize;

  var buttonSize = Math.max(50, clientSize / totalSize * clientSize);
  var buttonPosition = scrollPosition / (totalSize - clientSize) *
      (clientSize - buttonSize);
  var buttonTop = buttonPosition + clientTop;

  var time = Date.now();
  if (this.hidden != hidden ||
      this.lastButtonTop_ != buttonTop ||
      this.lastButtonSize_ != buttonSize) {
    requestAnimationFrame(function() {
      this.hidden = hidden;
      this.button_.style.top = buttonTop + 'px';
      this.button_.style.height = buttonSize + 'px';
    }.bind(this));
  }

  this.lastButtonTop_ = buttonTop;
  this.lastButtonSize_ = buttonSize;
};
<|endoftext|>