diff --git a/src/components/DataControllerMixin/DataControllerMixin.js b/src/components/DataControllerMixin/DataControllerMixin.js
index 9760de32..a5e508c4 100644
--- a/src/components/DataControllerMixin/DataControllerMixin.js
+++ b/src/components/DataControllerMixin/DataControllerMixin.js
@@ -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.
+
/*********************************************************************
* DataControllerMixin.js - shared functionality for data controllers
*
@@ -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);
diff --git a/src/components/DataTable/DataTable.js b/src/components/DataTable/DataTable.js
index 6ea47c2f..cf99d58f 100644
--- a/src/components/DataTable/DataTable.js
+++ b/src/components/DataTable/DataTable.js
@@ -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 (
-
+
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 (
+
+
+
+
+
+ {statsTableLabel}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
diff --git a/src/components/StatisticalSummaryTable/__tests__/smoke.js b/src/components/StatisticalSummaryTable/__tests__/smoke.js
new file mode 100644
index 00000000..51e5d0d3
--- /dev/null
+++ b/src/components/StatisticalSummaryTable/__tests__/smoke.js
@@ -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(
+ ,
+ div
+ );
+});
diff --git a/src/components/StatisticalSummaryTable/package.json b/src/components/StatisticalSummaryTable/package.json
new file mode 100644
index 00000000..210aa4d1
--- /dev/null
+++ b/src/components/StatisticalSummaryTable/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "StatisticalSummaryTable",
+ "version": "0.0.0",
+ "private": true,
+ "main": "./StatisticalSummaryTable.js"
+}
diff --git a/src/components/data-controllers/DualDataController/DualDataController.js b/src/components/data-controllers/DualDataController/DualDataController.js
index f9ac44bf..11533e22 100644
--- a/src/components/data-controllers/DualDataController/DualDataController.js
+++ b/src/components/data-controllers/DualDataController/DualDataController.js
@@ -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';
@@ -59,11 +55,9 @@ 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,
@@ -71,20 +65,9 @@ export default createReactClass({
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.
@@ -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 },
@@ -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
@@ -128,10 +115,10 @@ export default createReactClass({
);
- },
-});
+ }
+}
diff --git a/src/components/data-controllers/MotiDataController/MotiDataController.js b/src/components/data-controllers/MotiDataController/MotiDataController.js
index 94451569..2bfa12e6 100644
--- a/src/components/data-controllers/MotiDataController/MotiDataController.js
+++ b/src/components/data-controllers/MotiDataController/MotiDataController.js
@@ -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
*
diff --git a/src/components/data-controllers/SingleDataController/SingleDataController.js b/src/components/data-controllers/SingleDataController/SingleDataController.js
index 3ed21c60..cb9fd9ce 100644
--- a/src/components/data-controllers/SingleDataController/SingleDataController.js
+++ b/src/components/data-controllers/SingleDataController/SingleDataController.js
@@ -33,147 +33,57 @@
import PropTypes from 'prop-types';
import React from 'react';
-import createReactClass from 'create-react-class';
import { Row, Col, Panel } from 'react-bootstrap';
import _ from 'underscore';
-import { parseBootstrapTableData,
- timeKeyToResolutionIndex,
- resolutionIndexToTimeKey,
- validateStatsData } from '../../../core/util';
-import DataTable from '../../DataTable/DataTable';
-import TimeOfYearSelector from '../../Selector/TimeOfYearSelector';
-import DataControllerMixin from '../../DataControllerMixin';
-import { displayError, multiYearMeanSelected } from '../../graphs/graph-helpers';
import SingleAnnualCycleGraph from '../../graphs/SingleAnnualCycleGraph';
import SingleLongTermAveragesGraph from '../../graphs/SingleLongTermAveragesGraph';
import SingleContextGraph from '../../graphs/SingleContextGraph';
import SingleTimeSeriesGraph from '../../graphs/SingleTimeSeriesGraph';
-import { getStats } from '../../../data-services/ce-backend';
import AnomalyAnnualCycleGraph from '../../graphs/AnomalyAnnualCycleGraph';
import SingleTimeSliceGraph from '../../graphs/SingleTimeSliceGraph';
import {
singleAnnualCycleTabLabel, changeFromBaselineTabLabel,
singleLtaTabLabel, modelContextTabLabel, snapshotTabLabel,
- timeSeriesTabLabel, statsTableLabel,
+ timeSeriesTabLabel,
graphsPanelLabel,
} from '../../guidance-content/info/InformationItems';
import styles from '../DataController.css';
import { MEVSummary } from '../../data-presentation/MEVSummary';
-import ExportButtons from '../../graphs/ExportButtons';
import FlowArrow from '../../data-presentation/FlowArrow';
import GraphTabs from '../GraphTabs';
+import StatisticalSummaryTable from '../../StatisticalSummaryTable';
-// TODO: Remove DataControllerMixin and convert to class extension style when
-// no more dependencies on DataControllerMixin remain
-export default createReactClass({
- displayName: 'SingleDataController',
-
- propTypes: {
+export default class SingleDataController 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,
- ensemble_name: PropTypes.string,
- },
-
- mixins: [DataControllerMixin],
-
- getInitialState: function () {
- return {
- dataTableTimeOfYear: 0,
- dataTableTimeScale: 'monthly',
- statsData: undefined,
- };
- },
-
- /*
- * Called when SingleDataController is first loaded. Selects and fetches
- * arbitrary initial data to display in the graphs and stats table.
- * Monthly time resolution, January, on the first run returned by the API.
- */
- getData: function (props) {
- //if the selected dataset is a multi-year mean, load annual cycle
- //and long term average graphs, otherwise load a timeseries graph
- if (multiYearMeanSelected(props)) {
- this.loadDataTable(props);
- }
- else {
- this.loadDataTable(props, { timeidx: 0, timescale: "yearly" });
- }
- },
+ ensemble_name: PropTypes.string, // TODO: Why is this declared? Remove?
+ };
- //Removes all data from the Stats Table and displays a message
- setStatsTableNoDataMessage: function (message) {
- this.setState({
- statsTableOptions: { noDataText: message },
- statsData: [],
- });
- },
-
- shouldComponentUpdate: function (nextProps, nextState) {
+ // TODO: Is this necessary?
+ 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.
// If immutable data objects are used (or functionally equivalently,
// new data objects each time), then shallow comparison works.
return !(
- _.isEqual(nextState.statsData, this.state.statsData) &&
_.isEqual(nextProps.meta, this.props.meta) &&
- _.isEqual(nextState.statsTableOptions, this.state.statsTableOptions) &&
_.isEqual(nextProps.area, this.props.area)
);
- },
-
- /*
- * Called when the user selects a time of year to display on the stats
- * table. Fetches new data, records the new time index and resolution
- * in state, and updates the table.
- */
- updateDataTableTimeOfYear: function (timeidx) {
- this.loadDataTable(this.props, timeKeyToResolutionIndex(timeidx));
- },
-
- /*
- * This function fetches and loads data for the Stats Table.
- * If passed a time of year(resolution and index), it will load
- * data for that time of year. Otherwise, it defaults to January
- * (resolution: "monthly", index 0).
- */
- loadDataTable: function (props, time) {
-
- var timeidx = time ? time.timeidx : this.state.dataTableTimeOfYear;
- var timeres = time ? time.timescale : this.state.dataTableTimeScale;
-
- //load stats table
- this.setStatsTableNoDataMessage('Loading Data');
- var myStatsPromise = getStats(props, timeidx, timeres).then(validateStatsData);
+ }
- myStatsPromise.then(response => {
- if (_.allKeys(response.data).length > 0) {
- this.setState({
- dataTableTimeOfYear: timeidx,
- dataTableTimeScale: timeres,
- statsData: parseBootstrapTableData(this.injectRunIntoStats(response.data), props.meta),
- });
- }
- else {
- this.setState({
- dataTableTimeOfYear: timeidx,
- dataTableTimeScale: timeres,
- });
- this.setStatsTableNoDataMessage('Statistics unavailable for this time period.');
- }
- }).catch(error => {
- displayError(error, this.setStatsTableNoDataMessage);
- });
- },
-
- 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 SingleVariableGraphs
+ static graphTabsSpecs = {
mym: [
{ title: singleAnnualCycleTabLabel, graph: SingleAnnualCycleGraph },
{ title: singleLtaTabLabel, graph: SingleLongTermAveragesGraph },
@@ -184,19 +94,11 @@ export default createReactClass({
notMym: [
{ title: timeSeriesTabLabel, graph: SingleTimeSeriesGraph },
],
- },
-
- render: function () {
- const statsData = this.state.statsData ? this.state.statsData : this.blankStatsData;
-
- const dataTableSelected = resolutionIndexToTimeKey(
- this.state.dataTableTimeScale,
- this.state.dataTableTimeOfYear
- );
+ };
- // Spec for generating tabs containing various graphs.
- // Property names indicate whether the dataset is a multi-year mean or not.
+ render() {
return (
+ // TODO: https://github.com/pacificclimate/climate-explorer-frontend/issues/246
@@ -216,48 +118,16 @@ export default createReactClass({
filtered datasets
-
-
-
-
-
- {statsTableLabel}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
);
- },
-});
+ }
+}
diff --git a/src/core/util.js b/src/core/util.js
index e23433f9..7c156ec6 100644
--- a/src/core/util.js
+++ b/src/core/util.js
@@ -236,6 +236,10 @@ export function timeResolutionIndexToTimeOfYear(res, idx) {
}
export function timeResolutions(meta) {
+ // Given an array of (standard) metadata,
+ // return an object containing flags indicating whether each of the
+ // 3 standard timescales are present in the datasets described by
+ // the metadata.
const timescales = _.pluck(meta, 'timescale');
return {
monthly: _.contains(timescales, 'monthly'),
@@ -245,6 +249,10 @@ export function timeResolutions(meta) {
}
export function defaultTimeOfYear({ monthly, seasonal, yearly }) {
+ // Given a set of flags indicating the timescales present,
+ // return an object giving the default timescale and time index.
+ // The default timescale is the highest-resolution one present;
+ // the default time index is the first item in the default timescale.
if (monthly) {
return 0; // January
}
diff --git a/src/data-services/ce-backend.js b/src/data-services/ce-backend.js
index ca4976c8..43374e28 100644
--- a/src/data-services/ce-backend.js
+++ b/src/data-services/ce-backend.js
@@ -59,8 +59,9 @@ function getData(
});
}
+// TODO: getTimeseries, getData and getStats are almost identical. Factor.
function getStats (
- { ensemble_name, model_id, variable_id, experiment, area }, timeidx, timeres
+ { ensemble_name, model_id, variable_id, experiment, timescale, timeidx, area }
) {
// Query the "multistats" API endpoint.
// Gets an object from each qualifying dataset file with the following
@@ -78,18 +79,18 @@ function getStats (
// modtime (last time dataset was modified)
// }
// }
- const emissionString = guessExperimentFormatFromVariable(variable_id, experiment);
+ const emission = guessExperimentFormatFromVariable(variable_id, experiment);
return axios({
baseURL: urljoin(CE_BACKEND_URL, 'multistats'),
params: {
ensemble_name: ensemble_name,
model: model_id,
variable: variable_id,
- emission: emissionString,
- area: area || null,
+ emission,
time: timeidx,
- timescale: timeres
- }
+ timescale,
+ area: area || null,
+ },
});
}