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, + }, }); }