pacificclimate/climate-explorer-frontend

View on GitHub
src/components/StatisticalSummaryTable/StatisticalSummaryTable.js

Summary

Maintainability
D
2 days
Test Coverage
// Statistical Summary Table: Panel containing a Data Table viewer component
// showing statistical information for each climatology period or timeseries.

// TODO: Use HOC `withAsyncData` to manage fetching data

import PropTypes from 'prop-types';
import React from 'react';
import { Row, Col, Panel, ControlLabel } from 'react-bootstrap';

import _ from 'lodash';

import DataTable from '../DataTable/DataTable';
import { TimeOfYearSelector } from 'pcic-react-components';
import ExportButtons from '../graphs/ExportButtons';
import { statsTableLabel, timeOfYearSelectorLabel } from
    '../guidance-content/info/InformationItems';
import { MEVSummary } from '../data-presentation/MEVSummary';

import { getStats } from '../../data-services/ce-backend';
import {
  parseBootstrapTableData,
  timeKeyToResolutionIndex,
  timeResolutions,
  validateStatsData,
} from '../../core/util';
import { errorMessage } from '../graphs/graph-helpers';
import { exportDataToWorksheet } from '../../core/export';

import styles from './StatisticalSummaryTable.module.css';


// TODO: Use `withAsyncData` to factor out common data-fetching code here
export default class StatisticalSummaryTable extends React.Component {
  static propTypes = {
    model_id: PropTypes.string,
    variable_id: PropTypes.string,
    experiment: PropTypes.string,
    area: PropTypes.string,
    meta: PropTypes.array,
    contextMeta: PropTypes.array,
  };

  // Lifecycle hooks
  // Follows React 16+ lifecycle API and recommendations.
  // See https://reactjs.org/blog/2018/03/29/react-v-16-3.html
  // See https://reactjs.org/blog/2018/03/27/update-on-async-rendering.html
  // See https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html

  constructor(props) {
    super(props);

    // See src/components/graphs/README for an explanation of the content and
    // usage of state values. This is important for understanding how this
    // component works.

    this.state = {
      prevMeta: null,
      prevArea: null,
      prevTimeOfYear: undefined,
      timeOfYear: undefined,
      fetchingData: false,
      data: null,
      dataError: null,
    };
  }

  static getDerivedStateFromProps(props, state) {
    if (props.meta !== state.prevMeta || props.area !== state.prevArea) {
      return {
        prevMeta: props.meta,
        prevArea: props.area,
        fetchingData: false,  // not quite yet
        data: null,  // Signal that data fetch is required
        dataError: null,
      };
    }

    // State change (timeOfYear). Signal need for data fetch.
    if (state.prevTimeOfYear !== state.timeOfYear) {
      return {
        prevTimeOfYear: state.timeOfYear,
        fetchingData: false,  // not quite yet
        data: null,  // Signal that data fetch is required
        dataError: null,
      };
    }

    // No state update necessary.
    return null;
  }

  componentDidMount() {
    this.fetchData();
  }

  componentDidUpdate(prevProps, prevState) {
    if (!this.state.fetchingData && this.state.data === null) {
      this.fetchData();
    }
  }

  // Data fetching

  getAndValidateData(metadata) {
    return (
      getStats(metadata)
      .then(validateStatsData)
      .then(response => response.data)
    );
  }

  injectRunIntoStats(data) {
    // TODO: Make this into a pure function
    // Injects model run information into object returned by stats call
    _.map(data, function (val, key) {
      const selected = this.props.meta.filter(el => el.unique_id === key);
      _.extend(val, { run: selected[0].ensemble_member });
    }.bind(this));
    return data;
  }

  fetchData() {
    if (!this.state.timeOfYear) {
      // Don't fetch data when ToY hasn't settled yet.
      return;
    }
    this.setState({ fetchingData: true });
    const metadata = {
      ..._.pick(this.props,
        'ensemble_name', 'model_id', 'variable_id', 'experiment', 'area'),
      ...timeKeyToResolutionIndex(this.state.timeOfYear.value),
    };
    this.getAndValidateData(metadata)
    .then(data => {
      this.setState({
        fetchingData: false,
        data: parseBootstrapTableData(
          this.injectRunIntoStats(data), this.props.meta),
        dataError: null,
      });
    }).catch(dataError => {
      this.setState({
        // Set data non-null here to prevent infinite update loop.
        data: undefined,
        fetchingData: false,
        dataError,
      });
    });
  }

  // User event handlers

  handleChangeTimeOfYear = (timeOfYear) => {
    this.setState({ timeOfYear });
  };

  // TODO: https://github.com/pacificclimate/climate-explorer-frontend/issues/261
  exportDataTable(format) {
    exportDataToWorksheet(
      'stats', this.props, this.state.data, format,
      timeKeyToResolutionIndex(
        this.state.timeOfYear && this.state.timeOfYear.value)
    );
  }

  // render helpers

  dataTableOptions() {
    // Return a data table options object appropriate to the current state.

    // An error occurred
    if (this.state.dataError) {
      return { noDataText: errorMessage(this.state.dataError) };
    }

    // Waiting for data
    if (this.state.fetchingData || this.state.data === null) {
      return { noDataText: 'Loading data...' };
    }

    // We can haz data
    return { noDataText: 'We have data and this message should not show' };
  }

  render() {
    return (
      <Panel>
        <Panel.Heading>
          <Panel.Title>
            <Row>
              <Col lg={4}>
                {statsTableLabel}
              </Col>
              <Col lg={8}>
                <MEVSummary className={styles.mevSummary} {...this.props} />
              </Col>
            </Row>
          </Panel.Title>
        </Panel.Heading>
        <Panel.Body className={styles.data_panel}>
          <Row>
            <Col lg={6} md={6} sm={6}>
              <ControlLabel className={styles.selector_label}>
                {timeOfYearSelectorLabel}
              </ControlLabel>
              <TimeOfYearSelector
                value={this.state.timeOfYear}
                onChange={this.handleChangeTimeOfYear}
                {...timeResolutions(this.props.meta)}
                className={styles.selector}
              />
            </Col>
            <Col lg={6} md={6} sm={6}>
              <ExportButtons
                onExportXlsx={this.exportDataTable.bind(this, 'xlsx')}
                onExportCsv={this.exportDataTable.bind(this, 'csv')}
              />
            </Col>
          </Row>
          <DataTable
            data={this.state.data || []}
            options={this.dataTableOptions()}
          />
        </Panel.Body>
      </Panel>
    );
  }
}