KarrLab/datanator_frontend

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

Summary

Maintainability
F
3 days
Test Coverage
A
100%
import React, { Component } from "react";
import PropTypes from "prop-types";
import MeasurementsBoxScatterPlot from "../MeasurementsBoxScatterPlot/MeasurementsBoxScatterPlot";
import { mean, median, std, min, max } from "mathjs";
import { formatScientificNotation } from "~/utils/utils";

import "./StatsToolPanel.scss";

/**
 * Class to render the statistics of results
 * This class takes the data from the store, and summarizes the data with mean, median, standard deviation, minimum, maximum, and a chart at top.
 * This class mostly handles the rendering, while the logic is handled elsewhere
 */
class StatsToolPanel extends Component {
  static propTypes = {
    /* AG-Grid API */
    api: PropTypes.shape({
      addEventListener: PropTypes.func.isRequired,
      removeEventListener: PropTypes.func.isRequired,
    }).isRequired,

    /**
     * This prop indicates which column contains the values that need to be summarized.
     * For example, in metabolite concentrations, we want the summarize the value of "concentration",
     * so we need to tell the compomenet to look for the column labeled "concentration"
     */
    col: PropTypes.array.isRequired,
  };

  constructor(props) {
    super(props);

    this.state = {
      all: {
        values: null,
        count: null,
        mean: null,
        median: null,
        stdDev: null,
        min: null,
        max: null,
      },
      filtered: {
        values: null,
        count: null,
        mean: null,
        median: null,
        stdDev: null,
        min: null,
        max: null,
      },
      selected: {
        values: null,
        count: null,
        mean: null,
        median: null,
        stdDev: null,
        min: null,
        max: null,
      },
    };

    this.updateStats = this.updateStats.bind(this);
    this.updateSelectedStats = this.updateSelectedStats.bind(this);
  }

  /**
   * When mounted, setup listeners to update stats
   */
  componentDidMount() {
    this.updateStats(this.props);
    this.props.api.addEventListener("rowDataChanged", this.updateStats);
    this.props.api.addEventListener("filterChanged", this.updateSelectedStats);
    this.props.api.addEventListener(
      "selectionChanged",
      this.updateSelectedStats
    );
  }

  /**
   * When component updates, update listeners to update stats
   */
  componentDidUpdate(prevProps) {
    /* istanbul ignore next */
    /* Never reached because the application never changes the 'api' property */
    if (prevProps.api !== this.props.api) {
      prevProps.api.removeEventListener("rowDataChanged", this.updateStats);
      prevProps.api.removeEventListener(
        "filterChanged",
        this.updateSelectedStats
      );
      prevProps.api.removeEventListener(
        "selectionChanged",
        this.updateSelectedStats
      );

      this.props.api.addEventListener("rowDataChanged", this.updateStats);
      this.props.api.addEventListener(
        "filterChanged",
        this.updateSelectedStats
      );
      this.props.api.addEventListener(
        "selectionChanged",
        this.updateSelectedStats
      );
    }

    /* istanbul ignore next */
    /* Never reached because the application never changes the 'api' or 'col' properties */
    if (prevProps.api !== this.props.api || prevProps.col !== this.props.col) {
      this.updateStats(this.props);
    }
  }

  updateStats(event) {
    const dataPoints = [];
    event.api.forEachNode((node) => {
      dataPoints.push(node);
    });

    this.setState({
      all: this.calcStats(dataPoints),
    });

    this.updateSelectedStats(event);
  }

  updateSelectedStats(event) {
    const filteredDataPoints = [];
    const selectedDataPoints = [];
    event.api.forEachNodeAfterFilter((node) => {
      filteredDataPoints.push(node);
      if (node.selected) {
        selectedDataPoints.push(node);
      }
    });

    this.setState({
      filtered: this.calcStats(filteredDataPoints),
      selected: this.calcStats(selectedDataPoints),
    });
  }

  /**
   * Calculate the summary statistics
   */
  calcStats(nodes) {
    // get values
    const vals = [];
    const nonNullVals = [];
    for (const node of nodes) {
      let val = node.data;
      for (const col of this.props.col) {
        if (val != null && val !== undefined && col in val) {
          val = val[col];
        } else {
          val = null;
          break;
        }
      }

      vals.push(val);
      if (val != null) {
        nonNullVals.push(val);
      }
    }

    // calcalate statistics
    const stats = {
      nodes: nodes,
      values: vals,
      count: null,
      mean: null,
      median: null,
      stdDev: null,
      min: null,
      max: null,
    };

    if (nonNullVals.length > 0) {
      stats.count = nonNullVals.length;
      stats.mean = formatScientificNotation(mean(nonNullVals));
      stats.median = formatScientificNotation(median(nonNullVals));
      stats.stdDev = formatScientificNotation(std(nonNullVals));
      stats.min = formatScientificNotation(min(nonNullVals));
      stats.max = formatScientificNotation(max(nonNullVals));
    }

    if (nonNullVals.length < 2) {
      stats.stdDev = null;
    }

    // return statistics
    return stats;
  }

  render() {
    if (this.state.all.values == null) {
      return <div></div>;
    } else {
      return (
        <div className="biochemical-entity-scene-stats-tool-panel">
          <div className="biochemical-entity-scene-stats-tool-panel-plot">
            <MeasurementsBoxScatterPlot
              all={{
                nodes: this.state.all.nodes,
                values: this.state.all.values,
              }}
              filtered={{
                nodes: this.state.filtered.nodes,
                values: this.state.filtered.values,
              }}
              selected={{
                nodes: this.state.selected.nodes,
                values: this.state.selected.values,
              }}
            />
          </div>

          <table className="summary">
            <tbody>
              <tr>
                <th></th>
                <th scope="col">All</th>
                <th scope="col">Filtered</th>
                <th scope="col">Selected</th>
              </tr>
              <tr>
                <th scope="row">Count</th>
                <td>{this.state.all.count}</td>
                <td>{this.state.filtered.count}</td>
                <td>{this.state.selected.count}</td>
              </tr>
              <tr>
                <th scope="row">Mean</th>
                <td>{this.state.all.mean}</td>
                <td>{this.state.filtered.mean}</td>
                <td>{this.state.selected.mean}</td>
              </tr>
              <tr>
                <th scope="row">Median</th>
                <td>{this.state.all.median}</td>
                <td>{this.state.filtered.median}</td>
                <td>{this.state.selected.median}</td>
              </tr>
              <tr>
                <th scope="row">Std dev</th>
                <td>{this.state.all.stdDev}</td>
                <td>{this.state.filtered.stdDev}</td>
                <td>{this.state.selected.stdDev}</td>
              </tr>
              <tr>
                <th scope="row">Min</th>
                <td>{this.state.all.min}</td>
                <td>{this.state.filtered.min}</td>
                <td>{this.state.selected.min}</td>
              </tr>
              <tr>
                <th scope="row">Max</th>
                <td>{this.state.all.max}</td>
                <td>{this.state.filtered.max}</td>
                <td>{this.state.selected.max}</td>
              </tr>
            </tbody>
          </table>
        </div>
      );
    }
  }
}

export { StatsToolPanel };