diff --git a/assets/src/scripts/components/hydrograph/index.js b/assets/src/scripts/components/hydrograph/index.js index 832b86419a7ea4cdee37bf6c0153fd7d8773095f..241efbafbab6d9ad17607a01e832b1379cabc3e5 100644 --- a/assets/src/scripts/components/hydrograph/index.js +++ b/assets/src/scripts/components/hydrograph/index.js @@ -11,6 +11,7 @@ const { dispatch, link, provide } = require('../../lib/redux'); const { appendAxes, axesSelector } = require('./axes'); const { ASPECT_RATIO_PERCENT, MARGIN, CIRCLE_RADIUS, layoutSelector } = require('./layout'); const { drawSimpleLegend, legendDisplaySelector, createLegendMarkers } = require('./legend'); +const { plotSeriesSelectTable, availableTimeseriesSelector } = require('./parameters'); const { pointsSelector, lineSegmentsSelector, isVisibleSelector, titleSelector, descriptionSelector } = require('./timeseries'); const { xScaleSelector, yScaleSelector } = require('./scales'); const { Actions, configureStore } = require('./store'); @@ -160,9 +161,12 @@ const timeSeriesGraph = function (elem) { yscale: yScaleSelector, medianStatsData: pointsSelector('medianStatistics'), showLabel: (state) => state.showMedianStatsLabel - }))); + elem.call(link(plotSeriesSelectTable, createStructuredSelector({ + availableTimeseries: availableTimeseriesSelector + }))); + elem.append('div') .call(link(addSROnlyTable, createStructuredSelector({ columnNames: createSelector( diff --git a/assets/src/scripts/components/hydrograph/parameters.js b/assets/src/scripts/components/hydrograph/parameters.js index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..04fa54d063e42bc19c606380cb176d130a497984 100644 --- a/assets/src/scripts/components/hydrograph/parameters.js +++ b/assets/src/scripts/components/hydrograph/parameters.js @@ -0,0 +1,85 @@ +const { createSelector } = require('reselect'); + +const { Actions } = require('./store'); +const { dispatch } = require('../../lib/redux'); + + +/** + * Returns metadata for each available timeseries. + * @param {Object} state Redux state + * @return {Array} Sorted array of [code, metadata] pairs. + */ +export const availableTimeseriesSelector = createSelector( + state => state.tsData, + state => state.currentParameterCode, + (tsData, currentCd) => { + const codes = {}; + for (let key of Object.keys(tsData)) { + for (let code of Object.keys(tsData[key])) { + codes[code] = codes[code] || {}; + codes[code] = { + description: tsData[key][code].description || codes[code].description, + type: tsData[key][code].type || codes[code].type, + selected: currentCd === code, + currentYear: key === 'current' || codes[code].currentYear === true, + previousYear: key === 'compare' || codes[code].previousYear === true + }; + } + } + let sorted = []; + for (let key of Object.keys(codes).sort()) { + sorted.push([key, codes[key]]); + } + return sorted; + } +); + + +/** + * Draws a table with clickable rows of timeseries parameter codes. Selecting + * a row changes the active parameter code. + * @param {Object} elem d3 selection + * @param {Object} availableTimeseries Timeseries metadata to display + */ +export const plotSeriesSelectTable = function (elem, {availableTimeseries}) { + elem.select('#select-timeseries').remove(); + + const table = elem + .append('table') + .attr('id', 'select-timeseries') + .classed('usa-table-borderless', true); + + table.append('caption').text('Select a timeseries'); + + table.append('thead') + .append('tr') + .selectAll('th') + .data(['Parameter Code', 'Description', 'Value Type', 'Available Now', 'Available Last Year']) + .enter().append('th') + .attr('scope', 'col') + .text(d => d); + + table.append('tbody') + .selectAll('tr') + .data(availableTimeseries) + .enter().append('tr') + .classed('selected', parm => parm[1].selected) + .on('click', dispatch(function (parm) { + if (!parm[1].selected) { + return Actions.setCurrentParameterCode(parm[0]); + } + })) + .call(tr => { + tr.append('td') + .attr('scope', 'row') + .text(parm => parm[0]); + tr.append('td') + .text(parm => parm[1].description); + tr.append('td') + .text(parm => parm[1].type); + tr.append('td') + .html(parm => parm[1].currentYear ? '<i class="fa fa-check" aria-label="Current year data available"></i>' : ''); + tr.append('td') + .html(parm => parm[1].previousYear ? '<i class="fa fa-check" aria-label="Previous year data available"></i>' : ''); + }); +}; diff --git a/assets/src/scripts/components/hydrograph/parameters.spec.js b/assets/src/scripts/components/hydrograph/parameters.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..7584bd2cc2d1a90864bb438427df6247c8140180 --- /dev/null +++ b/assets/src/scripts/components/hydrograph/parameters.spec.js @@ -0,0 +1,45 @@ +const { availableTimeseriesSelector } = require('./parameters'); + + +describe('Parameters module', () => { + it('availableTimeseriesSelector returns ordered parameters', () => { + const available = availableTimeseriesSelector({ + tsData: { + current: { + '00060': {}, + '00061': {}, + '00062': {} + } + }, + currentParameterCode: '00060' + }); + expect(available.length).toBe(3); + expect(available[0][0]).toEqual('00060'); + expect(available[1][0]).toEqual('00061'); + expect(available[2][0]).toEqual('00062'); + }); + + it('availableTimeseriesSelector sets attributes correctly', () => { + const available = availableTimeseriesSelector({ + tsData: { + current: { + '00060': {description: '00060', type: '00060type'}, + '00061': {description: '00061', type: '00061type'}, + '00062': {description: '00062', type: '00062type'} + }, + compare: { + '00061': {description: '00061', type: '00061type'}, + '00062': {description: '00062', type: '00062type'}, + '00063': {description: '00063', type: '00063type'} + } + }, + currentParameterCode: '00060' + }); + expect(available).toEqual([ + ['00060', {description: '00060', type: '00060type', selected: true, currentYear: true, previousYear: false}], + ['00061', {description: '00061', type: '00061type', selected: false, currentYear: true, previousYear: true}], + ['00062', {description: '00062', type: '00062type', selected: false, currentYear: true, previousYear: true}], + ['00063', {description: '00063', type: '00063type', selected: false, currentYear: false, previousYear: true}] + ]); + }); +}); diff --git a/assets/src/scripts/components/hydrograph/store.js b/assets/src/scripts/components/hydrograph/store.js index 6f4dd6fe0651c531448132a079b26cfd637f1fb0..667b9d3a19bb2cb44bca24285d8458deb1a3ff62 100644 --- a/assets/src/scripts/components/hydrograph/store.js +++ b/assets/src/scripts/components/hydrograph/store.js @@ -11,11 +11,11 @@ export const Actions = { return function (dispatch) { const timeSeries = getTimeseries({sites: [siteno], params, startDate, endDate}).then( series => { - dispatch(Actions.addTimeseries('current', series[0])); + dispatch(Actions.addTimeseries('current', series)); // Trigger a call to get last year's data dispatch(Actions.retrieveCompareTimeseries(siteno, series[0].startTime, series[0].endTime)); - return series[0]; + return series; }, () => { dispatch(Actions.resetTimeseries('current')); @@ -24,9 +24,9 @@ export const Actions = { const medianStatistics = getMedianStatistics({sites: [siteno]}); Promise.all([timeSeries, medianStatistics]).then((data) => { const [series, stats] = data; - const startDate = series.startTime; - const endDate = series.endTime; - let unit = replaceHtmlEntities(series.name.split(' ').pop()); + const startDate = series[0].startTime; + const endDate = series[0].endTime; + let unit = replaceHtmlEntities(series[0].name.split(' ').pop()); let plotableStats = parseMedianData(stats, startDate, endDate, unit); dispatch(Actions.setMedianStatistics(plotableStats)); }); @@ -35,7 +35,7 @@ export const Actions = { retrieveCompareTimeseries(site, startTime, endTime) { return function (dispatch) { return getPreviousYearTimeseries({site, startTime, endTime}).then( - series => dispatch(Actions.addTimeseries('compare', series[0], false)), + series => dispatch(Actions.addTimeseries('compare', series, false)), () => dispatch(Actions.resetTimeseries('compare')) ); }; @@ -51,8 +51,12 @@ export const Actions = { return { type: 'ADD_TIMESERIES', key, - data, - show + show, + // Key the data on its parameter code + data: data.reduce(function (acc, series) { + acc[series.code] = series; + return acc; + }, {}) }; }, resetTimeseries(key) { @@ -78,6 +82,12 @@ export const Actions = { type: 'RESIZE_TIMESERIES_PLOT', width }; + }, + setCurrentParameterCode(parameterCode) { + return { + type: 'PARAMETER_CODE_SET', + parameterCode + }; } }; @@ -91,7 +101,7 @@ export const timeSeriesReducer = function (state={}, action) { ...state.tsData, [action.key]: { ...state.tsData[action.key], - [action.data.code]: action.data + ...action.data } }, showSeries: { @@ -157,6 +167,12 @@ export const timeSeriesReducer = function (state={}, action) { width: action.width }; + case 'PARAMETER_CODE_SET': + return { + ...state, + currentParameterCode: action.parameterCode + }; + default: return state; } diff --git a/assets/src/scripts/components/hydrograph/store.spec.js b/assets/src/scripts/components/hydrograph/store.spec.js index c487748fc621cdd05daabd94318bdf0e2f157d88..144b6afd920d96ee322b85a0cb917bca6d0acd7a 100644 --- a/assets/src/scripts/components/hydrograph/store.spec.js +++ b/assets/src/scripts/components/hydrograph/store.spec.js @@ -16,10 +16,10 @@ describe('Redux store', () => { }); it('should create an action to add a timeseries', () => { - expect(Actions.addTimeseries('current', 'data', false)).toEqual({ + expect(Actions.addTimeseries('current', [{code: 'code1'}, {code: 'code2'}], false)).toEqual({ type: 'ADD_TIMESERIES', key: 'current', - data: 'data', + data: {code1: {code: 'code1'}, code2: {code: 'code2'}}, show: false }); }); @@ -59,7 +59,7 @@ describe('Redux store', () => { expect(timeSeriesReducer({tsData: {}}, { type: 'ADD_TIMESERIES', key: 'current', - data: {code: '00060'}, + data: {'00060': {code: '00060'}}, show: true })).toEqual({ tsData: { diff --git a/assets/src/scripts/models.js b/assets/src/scripts/models.js index 0714688c81ff55a9f50a6785439affe5525c40fa..efe7d434804599ea9106106acdd902e2370aff3e 100644 --- a/assets/src/scripts/models.js +++ b/assets/src/scripts/models.js @@ -51,8 +51,11 @@ export function getTimeseries({sites, params=null, startDate=null, endDate=null} id: series.name, code: series.variable.variableCode[0].value, name: replaceHtmlEntities(series.variable.variableName), - startTime: new Date(series.values[0].value[0].dateTime), - endTime: new Date(series.values[0].value.slice(-1)[0].dateTime), + type: series.variable.valueType, + startTime: series.values[0].value.length ? + new Date(series.values[0].value[0].dateTime) : null, + endTime: series.values[0].value.length ? + new Date(series.values[0].value.slice(-1)[0].dateTime) : null, description: series.variable.variableDescription, values: series.values[0].value.map(datum => { let date = new Date(datum.dateTime); @@ -71,8 +74,10 @@ export function getTimeseries({sites, params=null, startDate=null, endDate=null} }) }; }); - }, (error) => { - return error; + }) + .catch(reason => { + console.error(reason); + return []; }); } @@ -185,7 +190,7 @@ export function getPreviousYearTimeseries({site, startTime, endTime}) { return getTimeseries({sites: [site], startDate: lastYearStartTime, endDate: lastYearEndTime}); } -export function getMedianStatistics({sites, params=['00060']}) { +export function getMedianStatistics({sites, params=null}) { let medianRDB = getSiteStatistics({sites: sites, statType: 'median', params: params}); return medianRDB.then((response) => { return parseRDB(response); diff --git a/assets/src/styles/components/_hydrograph.scss b/assets/src/styles/components/_hydrograph.scss index 3c46aa3db6ac2be4c7c39a194a2c418b0652ef2f..f490d8ccd3b646d95899b18d8b5670eefc150809 100644 --- a/assets/src/styles/components/_hydrograph.scss +++ b/assets/src/styles/components/_hydrograph.scss @@ -1,6 +1,26 @@ $approved: darkgreen; $estimated: black; +table#select-timeseries { + tbody { + tr { + cursor: pointer; + background-color: $color-gray-lightest; + &:hover { + td { + background-color: $color-gray-lightest; + } + } + &.selected { + cursor: default; + td { + background-color: $color-aqua-lightest; + } + } + } + } +} + .hydrograph-container { display: inline-block; position: relative; @@ -97,4 +117,5 @@ $estimated: black; cursor: pointer; } } + }