KarrLab/datanator_frontend

View on GitHub
src/scenes/BiochemicalEntityDetails/NumberFilter.js

Summary

Maintainability
D
2 days
Test Coverage
B
82%
import React, { Component } from "react";
import PropTypes from "prop-types";
import Slider from "@material-ui/core/Slider";
import { formatScientificNotation } from "~/utils/utils";

class NumberFilter extends Component {
  static propTypes = {
    api: PropTypes.shape({
      addEventListener: PropTypes.func.isRequired,
      removeEventListener: PropTypes.func.isRequired,
      forEachNode: PropTypes.func.isRequired,
    }).isRequired,
    valueGetter: PropTypes.func.isRequired,
    loBound: PropTypes.number,
    hiBound: PropTypes.number,
    step: PropTypes.number,
    filterChangedCallback: PropTypes.func.isRequired,
  };

  static defaultProps = {
    loBound: null,
    hiBound: null,
    step: null,
  };

  constructor(props) {
    super(props);

    this.loBound = 0;
    this.hiBound = 1;
    this.min = this.loBound;
    this.max = this.hiBound;
    this.state = {
      loBound: this.loBound,
      hiBound: this.hiBound,
      min: this.min,
      max: this.max,
      marks: [
        {
          value: this.loBound,
          label: formatScientificNotation(this.loBound, 4, 3, 1, 1, 3),
        },
        {
          value: this.hiBound,
          label: formatScientificNotation(this.hiBound, 4, 3, 1, 1, 3),
        },
      ],
      step: 0.01,
      disabled: false,
    };

    this.slider = React.createRef();
    this.updateBounds = this.updateBounds.bind(this);
    this.onChange = this.onChange.bind(this);
  }

  componentDidMount() {
    this.updateBounds();
    this.props.api.addEventListener("firstDataRendered", this.updateBounds);
    this.props.api.addEventListener("rowDataChanged", this.updateBounds);
    this.props.api.addEventListener("rowDataUpdated", this.updateBounds);
  }

  componentDidUpdate(prevProps) {
    if (this.props.api !== prevProps.api) {
      prevProps.api.removeEventListener("firstDataRendered", this.updateBounds);
      prevProps.api.removeEventListener("rowDataChanged", this.updateBounds);
      prevProps.api.removeEventListener("rowDataUpdated", this.updateBounds);

      this.updateBounds();
      this.props.api.addEventListener("firstDataRendered", this.updateBounds);
      this.props.api.addEventListener("rowDataChanged", this.updateBounds);
      this.props.api.addEventListener("rowDataUpdated", this.updateBounds);
    }

    if (
      this.props.loBound !== prevProps.loBound ||
      this.props.hiBound !== prevProps.hiBound
    ) {
      this.updateBounds();
    }
  }

  updateBounds() {
    this.loBound = this.props.loBound;
    this.hiBound = this.props.hiBound;

    if (this.loBound == null || this.hiBound == null) {
      let loBound = Infinity;
      let hiBound = -Infinity;
      this.props.api.forEachNode((node) => {
        const value = this.props.valueGetter(node);
        if (value != null && value !== undefined && !isNaN(value)) {
          loBound = Math.min(loBound, value);
          hiBound = Math.max(hiBound, value);
        }
      });

      if (this.loBound == null) {
        this.loBound = loBound;
      }
      if (this.hiBound == null) {
        this.hiBound = hiBound;
      }
    }

    this.min = this.loBound;
    this.max = this.hiBound;

    let marks = [this.loBound, this.hiBound];

    let step;
    if (this.props.step != null) {
      step = this.props.step;
    } else if (this.hiBound === this.loBound) {
      if (this.hiBound === 0) {
        step = 1e2;
      } else {
        step = Math.pow(10, Math.floor(Math.log10(this.hiBound)) - 2);
      }
    } else {
      step = Math.pow(
        10,
        Math.floor(Math.log10(this.hiBound - this.loBound)) - 1
      );
    }

    let disabled = false;
    if (!isFinite(this.loBound)) {
      marks = [];
      disabled = true;
    } else if (this.loBound === this.hiBound) {
      marks = [this.min];
      this.min -= step;
      this.max += step;
      this.loBound -= step;
      this.hiBound += step;
    }

    this.setState({
      loBound: this.loBound,
      hiBound: this.hiBound,
      min: this.min,
      max: this.max,
      marks: marks.map((mark) => {
        return {
          value: mark,
          label: formatScientificNotation(mark, 4, 3, 1, 1, 3),
        };
      }),
      step: step,
      disabled: disabled,
    });
  }

  isFilterActive() {
    return this.min > this.loBound || this.max < this.hiBound;
  }

  doesFilterPass(params) {
    const val = this.props.valueGetter(params.node);
    return this.min <= val && val <= this.max;
  }

  getModel() {
    return {
      min: this.min,
      max: this.max,
    };
  }

  setModel(range) {
    if (range == null) {
      this.min = this.loBound;
      this.max = this.hiBound;
    } else {
      const rangeMin = range.min != null ? range.min : this.loBound;
      const rangeMax = range.max != null ? range.max : this.hiBound;

      this.min = Math.min(Math.max(this.loBound, rangeMin), this.hiBound);
      this.max = Math.min(Math.max(this.loBound, rangeMax), this.hiBound);

      if (this.min > this.max) {
        const tmp = this.min;
        this.min = this.max;
        this.max = tmp;
      }
    }
    this.setState({
      min: this.min,
      max: this.max,
    });
    this.props.filterChangedCallback(this.getModel());
  }

  onChange(event, range) {
    let min = range[0];
    let max = range[1];

    if (min > max) {
      const temp = min;
      min = max;
      max = temp;
    }

    if (this.min !== min || this.max !== max) {
      this.min = min;
      this.max = max;
      this.setState({
        min: this.min,
        max: this.max,
      });
      this.props.filterChangedCallback(this.getModel());
    }
  }

  render() {
    return (
      <div className="biochemical-entity-scene-filter biochemical-entity-scene-slider-filter biochemical-entity-scene-normal-slider-filter biochemical-entity-scene-horizontal-slider-filter number-slider-filter">
        <Slider
          ref={this.slider}
          min={this.state.loBound}
          max={this.state.hiBound}
          step={this.state.step}
          marks={this.state.marks}
          value={[this.state.min, this.state.max]}
          orientation="horizontal"
          track="normal"
          valueLabelDisplay={"on"}
          onChange={this.onChange}
          getAriaLabel={this.getAriaLabel}
          valueLabelFormat={this.valueText}
          getAriaValueText={this.valueText}
          disabled={this.state.disabled}
        />
      </div>
    );
  }

  getAriaLabel(index) {
    if (index === 0) {
      return "Minimum";
    } else {
      return "Maximum";
    }
  }

  valueText(value) {
    return formatScientificNotation(value, 4, 3, 1, 1, 3);
  }
}

export { NumberFilter };