Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix stats summary table time of year bugs #247

Merged
merged 19 commits into from
Dec 8, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion src/components/DataControllerMixin/DataControllerMixin.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
// DEPRECATED: DO NOT MAINTAIN THIS MODULE. REMOVE WHEN SOLE REMAINING
// DEPENDENCY IS REMOVED.
//
// We are moving away from mixins.
// (see [Mixins Considered Harmful](https://reactjs.org/blog/2016/07/13/mixins-considered-harmful.html)
// This mixin is no longer relevant, since it is only used in the deprecated
// component MotiDataController. Dependence on it has been removed from all other
// data controllers.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yay! Finally escaped the clutches of the mixin!

/*********************************************************************
* DataControllerMixin.js - shared functionality for data controllers
*
Expand Down Expand Up @@ -47,7 +56,7 @@ var ModalMixin = {
//In development, could be API or ensemble misconfiguration, database down.
//Display an error message on each viewer in use by this datacontroller.
var text = "No data matching selected parameters available";
var viewerMessageDisplays = [this.setStatsTableNoDataMessage];
var viewerMessageDisplays = [this.displayNoDataMessage];
_.each(viewerMessageDisplays, function(display) {
if(typeof display == 'function') {
display(text);
Expand Down
8 changes: 7 additions & 1 deletion src/components/DataTable/DataTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,18 @@ import { BootstrapTable, TableHeaderColumn } from 'react-bootstrap-table';
class DataTable extends React.Component {
static propTypes = {
data: PropTypes.array,
options: PropTypes.object,
};

render() {
return (
<div id={'table'}>
<BootstrapTable data={this.props.data} options={this.props.options} striped hover >
<BootstrapTable
data={this.props.data}
options={this.props.options}
striped
hover
>
<TableHeaderColumn
dataField='run'
dataAlign='center'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.mevSummary {
text-align: right;
}
204 changes: 204 additions & 0 deletions src/components/StatisticalSummaryTable/StatisticalSummaryTable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
// Statistical Summary Table: Panel containing a Data Table viewer component
// showing statistical information for each climatology period or timeseries.

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

import _ from 'underscore';

import DataTable from '../DataTable/DataTable';
import TimeOfYearSelector from '../Selector/TimeOfYearSelector';
import ExportButtons from '../graphs/ExportButtons';
import { statsTableLabel } from '../guidance-content/info/InformationItems';
import { MEVSummary } from '../data-presentation/MEVSummary';

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


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);

this.state = {
prevMeta: null,
prevArea: null,
timeOfYear: defaultTimeOfYear(timeResolutions(this.props.meta)),
data: null,
dataError: null,
};
}

static getDerivedStateFromProps(props, state) {
if (
props.meta !== state.prevMeta ||
props.area !== state.prevArea
) {
return {
prevMeta: props.meta,
prevArea: props.area,
timeOfYear: defaultTimeOfYear(timeResolutions(props.meta)),
data: null, // Signal that data fetch is required
dataError: null,
};
}

// No state update necessary.
return null;
}

componentDidMount() {
this.fetchData();
}

componentDidUpdate(prevProps, prevState) {
if (
// props changed => data invalid
this.state.data === null ||
// user selected new time of year
this.state.timeOfYear !== prevState.timeOfYear
) {
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() {
const metadata = {
..._.pick(this.props,
'ensemble_name', 'model_id', 'variable_id', 'experiment', 'area'),
...timeKeyToResolutionIndex(this.state.timeOfYear),
};
this.getAndValidateData(metadata)
.then(data => {
this.setState({
data: parseBootstrapTableData(
this.injectRunIntoStats(data), this.props.meta),
dataError: null,
});
}).catch(dataError => {
this.setState({
// Do we have to set data non-null here to prevent infinite update loop?
dataError,
});
});
}

// User event handlers

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

exportDataTable(format) {
exportDataToWorksheet(
'stats', this.props, this.state.data, format,
{ timeidx: this.state.timeOfYear,
timeres: this.state.dataTableTimeScale }
);
}

// 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.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}>
<TimeOfYearSelector
value={this.state.timeOfYear}
onChange={this.handleChangeTimeOfYear}
{...timeResolutions(this.props.meta)}
inlineLabel
/>
</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>
);
}
}
18 changes: 18 additions & 0 deletions src/components/StatisticalSummaryTable/__tests__/smoke.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react';
import ReactDOM from 'react-dom';
import StatisticalSummaryTable from '../StatisticalSummaryTable';
import { noop } from 'underscore';
import { meta } from '../../../test_support/data';

it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(
<StatisticalSummaryTable
model_id={'GFDL-ESM2G'}
variable_id={'tasmax'}
experiment={'historical,rcp26'}
meta={meta}
/>,
div
);
});
6 changes: 6 additions & 0 deletions src/components/StatisticalSummaryTable/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "StatisticalSummaryTable",
"version": "0.0.0",
"private": true,
"main": "./StatisticalSummaryTable.js"
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,9 @@
import PropTypes from 'prop-types';

import React from 'react';
import createReactClass from 'create-react-class';
import { Panel, Row, Col } from 'react-bootstrap';
import _ from 'underscore';


import DataControllerMixin from '../../DataControllerMixin';

import DualAnnualCycleGraph from '../../graphs/DualAnnualCycleGraph';
import DualLongTermAveragesGraph from '../../graphs/DualLongTermAveragesGraph';
import DualTimeSeriesGraph from '../../graphs/DualTimeSeriesGraph';
Expand All @@ -59,32 +55,19 @@ import { MEVSummary } from '../../data-presentation/MEVSummary';
import GraphTabs from '../GraphTabs';


export default createReactClass({
displayName: 'DualDataController',

propTypes: {
ensemble_name: PropTypes.string,
export default class DualDataController extends React.Component {
static propTypes = {
ensemble_name: PropTypes.string, // TODO: Why is this declared? Remove?
model_id: PropTypes.string,
variable_id: PropTypes.string,
comparand_id: PropTypes.string,
experiment: PropTypes.string,
area: PropTypes.string,
meta: PropTypes.array,
comparandMeta: PropTypes.array,
},

mixins: [DataControllerMixin],

getInitialState: function () {
return {
statsData: undefined,
};
},

// TODO: Remove when DataControllerMixin is removed
getData: function (props) {/* Legacy: NOOP*/},
};

shouldComponentUpdate: function (nextProps, nextState) {
shouldComponentUpdate(nextProps, nextState) {
// This guards against re-rendering before calls to the data sever alter the
// state
// TODO: Consider making shallow comparisons. Deep ones are expensive.
Expand All @@ -94,9 +77,12 @@ export default createReactClass({
_.isEqual(nextProps.meta, this.props.meta) &&
_.isEqual(nextProps.comparandMeta, this.props.comparandMeta) &&
_.isEqual(nextProps.area, this.props.area));
},
}

graphTabsSpecs: {
// Spec for generating tabs containing various graphs.
// Property names indicate whether the dataset is a multi-year mean or not.
// TODO: Pull this out into new component CompareVariablesGraphs
static graphTabsSpecs = {
mym: [
{ title: dualAnnualCycleTabLabel, graph: DualAnnualCycleGraph },
{ title: singleLtaTabLabel, graph: DualLongTermAveragesGraph },
Expand All @@ -106,10 +92,11 @@ export default createReactClass({
{ title: timeSeriesTabLabel, graph: DualTimeSeriesGraph },
{ title: variableResponseTabLabel, graph: DualVariableResponseGraph },
],
},
};

render: function () {
render() {
return (
// TODO: https://github.com/pacificclimate/climate-explorer-frontend/issues/246
<Panel>
<Panel.Heading>
<Panel.Title>
Expand All @@ -128,10 +115,10 @@ export default createReactClass({
<Panel.Body className={styles.data_panel}>
<GraphTabs
{...this.props}
specs={this.graphTabsSpecs}
specs={DualDataController.graphTabsSpecs}
/>
</Panel.Body>
</Panel>
);
},
});
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
// DEPRECATED: DO NOT MAINTAIN THIS COMPONENT.
//
// This component is not currently in use and it has not been kept up to date
// with changes made to other data controllers. This component is being kept
// for the sole purpose of keeping a record of what it was intended to do.
//
// If and when a MotiDataController is actually needed, it should be created
// by cloning/interbreeding/genetically modifying one or more of the other data
// controllers, which are much better structured.

/************************************************************************
* MotiDataController.js - controller to display summarized numerical data
*
Expand Down
Loading