diff --git a/.eslintrc b/.eslintrc index 600faec22baee1caac92d98f78687ce4407001b6..6ec43532e447eee88074ac3a73660ccfc95236ab 100644 --- a/.eslintrc +++ b/.eslintrc @@ -5,10 +5,7 @@ "jasmine": true }, "parserOptions": { - "ecmaVersion": 6, - "ecmaFeatures": { - "experimentalObjectRestSpread": true - }, + "ecmaVersion": 2018, "sourceType": "module" }, "plugins": [ diff --git a/CHANGELOG.md b/CHANGELOG.md index c6c31e287bb5085160859995bc1d8d821e68875f..a5ad3220e6091ead4a5d29c4a7610787d80b568e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). ## [Unreleased](https://github.com/usgs/waterdataui/compare/waterdataui-0.22.0...master) -### Changed -- Assets generation and the graph-server now use npm 12.x.x +### Added +- Image server now accepts the period parameter which should be a ISO-8601 duration format. +However please note that NWIS only accepts periods using xxD. ## [0.22.0](https://github.com/usgs/waterdataui/compare/waterdataui-0.21.0...waterdataui-0.22.0) - 2019-12-12 ### Changed diff --git a/assets/package.json b/assets/package.json index 8f5ea8e5f396c3818c3facec552cd20839b9ce36..a8164fc1b59909d8736d4c4da06731dbb2cc2cbf 100644 --- a/assets/package.json +++ b/assets/package.json @@ -28,7 +28,7 @@ "watch:js": "mkdir -p dist/scripts && cp node_modules/date-time-format-timezone/build/browserified/date-time-format-timezone-complete-min.js dist/scripts && rollup -c --watch" }, "engines": { - "node": "12.13.1" + "node": "10.18.0" }, "repository": { "type": "git", diff --git a/assets/src/scripts/components/hydrograph/index.js b/assets/src/scripts/components/hydrograph/index.js index ccf2b3b764f5c2f21c214b45794dc981e64a3ddf..e2344c94e923e184f47604d80ce361d17630b66a 100644 --- a/assets/src/scripts/components/hydrograph/index.js +++ b/assets/src/scripts/components/hydrograph/index.js @@ -8,12 +8,14 @@ import { select } from 'd3-selection'; import { DateTime } from 'luxon'; import { createStructuredSelector } from 'reselect'; +import { dispatch, link, provide } from '../../lib/redux'; + import { addSVGAccessibility } from '../../accessibility'; import config from '../../config'; -import { dispatch, link, provide } from '../../lib/redux'; -import { getTimeSeriesCollectionIds, isLoadingTS } from '../../selectors/time-series-selector'; +import { isLoadingTS, hasAnyTimeSeries } from '../../selectors/time-series-selector'; import { Actions } from '../../store'; import { callIf, mediaQuery } from '../../utils'; + import { appendAxes, axesSelector } from './axes'; import { cursorSlider } from './cursor'; import { lineSegmentsByParmCdSelector, currentVariableLineSegmentsSelector, MASK_DESC, HASH_ID, @@ -24,8 +26,7 @@ import { drawSimpleLegend, legendMarkerRowsSelector } from './legend'; import { drawMethodPicker } from './method-picker'; import { plotSeriesSelectTable, availableTimeSeriesSelector } from './parameters'; import { xScaleSelector, yScaleSelector, timeSeriesScalesByParmCdSelector } from './scales'; -import { allTimeSeriesSelector, isVisibleSelector, titleSelector, descriptionSelector, - hasTimeSeriesWithPoints } from './time-series'; +import { allTimeSeriesSelector, isVisibleSelector, titleSelector, descriptionSelector } from './time-series'; import { createTooltipFocus, createTooltipText } from './tooltip'; @@ -52,6 +53,8 @@ const plotDataLine = function(elem, {visible, lines, tsKey, xScale, yScale}) { return; } + const tsKeyClass = `ts-${tsKey}`; + for (let line of lines) { if (line.classes.dataMask === null) { // If this is a single point line, then represent it as a circle. @@ -63,6 +66,7 @@ const plotDataLine = function(elem, {visible, lines, tsKey, xScale, yScale}) { .classed('approved', line.classes.approved) .classed('estimated', line.classes.estimated) .classed('not-current-method', !line.classes.currentMethod) + .classed(tsKeyClass, true) .attr('r', CIRCLE_RADIUS_SINGLE_PT) .attr('cx', d => xScale(d.dateTime)) .attr('cy', d => yScale(d.value)); @@ -94,7 +98,9 @@ const plotDataLine = function(elem, {visible, lines, tsKey, xScale, yScale}) { .attr('y', yScale(yRangeEnd)) .attr('width', rectWidth) .attr('height', Math.abs(yScale(yRangeEnd) - yScale(yRangeStart))) - .attr('class', `mask ${maskDisplayName}-mask`); + .attr('class', `mask ${maskDisplayName}-mask`) + .classed(`ts-${tsKey}`, true); + const patternId = HASH_ID[tsKey] ? `url(#${HASH_ID[tsKey]})` : ''; @@ -349,7 +355,7 @@ const dateRangeControls = function(elem, siteno) { .attr('aria-label', 'Time interval select') .call(link(function(container, showControls) { container.attr('hidden', showControls ? null : true); - }, hasTimeSeriesWithPoints('current', 'P7D'))); + }, hasAnyTimeSeries)); const customDateContainer = elem.insert('div', ':nth-child(3)') .attr('id', 'ts-customdaterange-select-container') @@ -430,7 +436,7 @@ const dateRangeControls = function(elem, siteno) { customDateValidationContainer.attr('hidden', null); } else { customDateValidationContainer.attr('hidden', true); - return Actions.getUserRequestedDataForDateRange( + return Actions.retrieveUserRequestedDataForDateRange( siteno, userSpecifiedStart, userSpecifiedEnd @@ -481,33 +487,31 @@ const dateRangeControls = function(elem, siteno) { li.select(`#${DATE_RANGE[0].label}`).attr('checked', true); }; - -const noDataAlert = function(elem, tsCollectionIds) { +const dataLoadingAlert = function(elem, message) { elem.select('#no-data-message').remove(); - if (tsCollectionIds && tsCollectionIds.length === 0) { + if (message) { elem.append('div') .attr('id', 'no-data-message') .attr('class', 'usa-alert usa-alert-info') .append('div') - .attr('class', 'usa-alert-body') - .append('p') - .attr('class', 'usa-alert-text') - .text('No current time series data available for this site'); + .attr('class', 'usa-alert-body') + .append('p') + .attr('class', 'usa-alert-text') + .text(message); } }; -export const attachToNode = function (store, node, {siteno, parameter, compare, cursorOffset, interactive = true} = {}) { +export const attachToNode = function (store, node, {siteno, parameter, compare, period, cursorOffset, showOnlyGraph = false} = {}) { + const nodeElem = select(node); if (!siteno) { select(node).call(drawMessage, 'No data is available.'); return; } + // Initialize hydrograph with the store and show the loading indicator store.dispatch(Actions.resizeUI(window.innerWidth, node.offsetWidth)); - select(node) + nodeElem .call(provide(store)) - .call(link(noDataAlert, getTimeSeriesCollectionIds('current', 'P7D'))) - .call(callIf(interactive, drawMethodPicker)) - .call(callIf(interactive, dateRangeControls), siteno) .select('.loading-indicator-container') .call(link(loadingIndicator, createStructuredSelector({ showLoadingIndicator: isLoadingTS('current', 'P7D'), @@ -524,17 +528,35 @@ export const attachToNode = function (store, node, {siteno, parameter, compare, store.dispatch(Actions.setCursorOffset(cursorOffset)); } - select(node).select('.graph-container') - .call(link(controlDisplay, hasTimeSeriesWithPoints('current', 'P7D'))) + // Fetch the time series data + if (period) { + store.dispatch(Actions.retrieveCustomTimePeriodTimeSeries(siteno, '00060', period)) + .catch((message) => dataLoadingAlert(select(node), message ? message : 'No data returned')); + } else { + store.dispatch(Actions.retrieveTimeSeries(siteno, parameter ? [parameter] : null)) + .catch(() => dataLoadingAlert((select(node), 'No current time series data available for this site'))); + } + store.dispatch(Actions.retrieveMedianStatistics(siteno)); + + // Set up rendering functions for the graph-container + nodeElem.select('.graph-container') + .call(link(controlDisplay, hasAnyTimeSeries)) .call(timeSeriesGraph, siteno) - .call(callIf(interactive, cursorSlider)) + .call(callIf(!showOnlyGraph, cursorSlider)) .append('div') .classed('ts-legend-controls-container', true) - .call(timeSeriesLegend) - .call(callIf(interactive, drawGraphControls)); + .call(timeSeriesLegend); - if (interactive) { - select(node).select('.select-time-series-container') + // Add UI interactive elements and the provisional data alert. + if (!showOnlyGraph) { + nodeElem + .call(drawMethodPicker) + .call(dateRangeControls, siteno); + + nodeElem.select('.ts-legend-controls-container') + .call(drawGraphControls); + + nodeElem.select('.select-time-series-container') .call(link(plotSeriesSelectTable, createStructuredSelector({ siteno: () => siteno, availableTimeSeries: availableTimeSeriesSelector, @@ -542,7 +564,7 @@ export const attachToNode = function (store, node, {siteno, parameter, compare, timeSeriesScalesByParmCd: timeSeriesScalesByParmCdSelector('current', 'P7D', SPARK_LINE_DIM), layout: layoutSelector }))); - select(node).select('.provisional-data-alert') + nodeElem.select('.provisional-data-alert') .call(link(function(elem, allTimeSeries) { elem.attr('hidden', Object.keys(allTimeSeries).length ? null : true); }, allTimeSeriesSelector)); @@ -551,6 +573,4 @@ export const attachToNode = function (store, node, {siteno, parameter, compare, window.onresize = function() { store.dispatch(Actions.resizeUI(window.innerWidth, node.offsetWidth)); }; - store.dispatch(Actions.retrieveTimeSeries(siteno, parameter ? [parameter] : null)); - store.dispatch(Actions.retrieveMedianStatistics(siteno)); }; diff --git a/assets/src/scripts/components/hydrograph/index.spec.js b/assets/src/scripts/components/hydrograph/index.spec.js index da5e8a9d913e9325531eca9b5bd7f7278b4a4e70..a191478b1f2cffeaa2d3c9e79e6747add7d65c03 100644 --- a/assets/src/scripts/components/hydrograph/index.spec.js +++ b/assets/src/scripts/components/hydrograph/index.spec.js @@ -163,9 +163,12 @@ describe('Hydrograph charting module', () => { component.append('div').attr('class', 'provisional-data-alert'); graphNode = document.getElementById('hydrograph'); + + jasmine.Ajax.install(); }); afterEach(() => { + jasmine.Ajax.uninstall(); select('#hydrograph').remove(); }); @@ -465,7 +468,7 @@ describe('Hydrograph charting module', () => { }); it('Expects data to be retrieved if both custom start and end dates are provided', () => { - spyOn(Actions, 'getUserRequestedDataForDateRange'); + spyOn(Actions, 'retrieveUserRequestedDataForDateRange'); select(graphNode).select('#custom-start-date').property('value', '2063-04-03'); select(graphNode).select('#custom-end-date').property('value', '2063-04-05'); @@ -475,7 +478,7 @@ describe('Hydrograph charting module', () => { let customDateAlertDiv = select(graphNode).select('#custom-date-alert-container'); expect(customDateAlertDiv.attr('hidden')).toBe('true'); - expect(Actions.getUserRequestedDataForDateRange).toHaveBeenCalledWith( + expect(Actions.retrieveUserRequestedDataForDateRange).toHaveBeenCalledWith( '12345678', '2063-04-03', '2063-04-05' ); }); @@ -493,16 +496,24 @@ describe('Hydrograph charting module', () => { } }; let store = configureStore(newTestState); - spyOn(store, 'dispatch'); + spyOn(store, 'dispatch').and.callThrough(); attachToNode(store, graphNode, {siteno: '12345678'}); expect(select(graphNode).select('.loading-indicator-container').select('.loading-indicator').size()).toBe(1); }); it('Expects the graph loading indicator to not be visible if the current 7 day data is not being loaded', () => { - let store = configureStore(TEST_STATE); - spyOn(store, 'dispatch'); + const newTestState = { + ...TEST_STATE, + timeSeriesState: { + ...TEST_STATE.timeSeriesState, + currentDateRange: 'P7D' + } + }; + let store = configureStore(newTestState); + spyOn(store, 'dispatch').and.callThrough(); attachToNode(store, graphNode, {siteno: '12345678'}); + store.dispatch(Actions.removeTimeSeriesLoading(['current:P7D'])); expect(select(graphNode).select('.loading-indicator-container').select('.loading-indicator').size()).toBe(0); }); @@ -543,23 +554,5 @@ describe('Hydrograph charting module', () => { expect(select(graphNode).select('#no-data-message').size()).toBe(0); }); - - it('Expects the no data alert to be shown if there is no data', () => { - let newTestState = { - ...TEST_STATE, - series: { - ...TEST_STATE.series, - requests: { - 'current:P7D': { - timeSeriesCollections: [] - } - } - } - }; - let store = configureStore(newTestState); - attachToNode(store, graphNode, {siteno: '12345678'}); - - expect(select(graphNode).select('#no-data-message').size()).toBe(1); - }); }); }); diff --git a/assets/src/scripts/components/hydrograph/time-series.js b/assets/src/scripts/components/hydrograph/time-series.js index 014eff34fd21da410c686fd25885a242382e4d6a..3eb3a8ad2922df35119b420a12c0c9b392e6b524 100644 --- a/assets/src/scripts/components/hydrograph/time-series.js +++ b/assets/src/scripts/components/hydrograph/time-series.js @@ -4,8 +4,10 @@ import _includes from 'lodash/includes'; import uniq from 'lodash/uniq'; import { createSelector } from 'reselect'; -import { getRequestTimeRange, getCurrentVariable, getTsRequestKey, getIanaTimeZone, getCurrentParmCd, getCurrentMethodID, - getMethods } from '../../selectors/time-series-selector'; +import { + getRequestTimeRange, getCurrentVariable, getTsRequestKey, getIanaTimeZone, getCurrentParmCd, getCurrentMethodID, + getMethods +} from '../../selectors/time-series-selector'; export const TEMPERATURE_PARAMETERS = { @@ -154,6 +156,7 @@ export const hasTimeSeriesWithPoints = memoize((tsKey, period) => createSelector return seriesWithPoints.length > 0; })); + /** * Factory function creates a function that: * Returns the current show state of a time series. diff --git a/assets/src/scripts/models.js b/assets/src/scripts/models.js index 4de2ef38cf0b312d5edb5567d9532d0267881a6d..fcae28f8186b75dae268d8cca772003d1e86bf9e 100644 --- a/assets/src/scripts/models.js +++ b/assets/src/scripts/models.js @@ -31,13 +31,16 @@ function tsServiceRoot(date) { * @param {Array} params Optional array of parameter codes * @param {Date} startDate * @param {Date} endData + * @param {String} period * @return {Promise} resolves to an array of time series model object, rejects to an error */ -export const getTimeSeries = function ({sites, params=null, startDate=null, endDate=null}) { +export const getTimeSeries = function ({sites, params=null, startDate=null, endDate=null, period=null}) { let timeParams; let serviceRoot; + if (!startDate && !endDate) { - timeParams = 'period=P7D'; + const timePeriod = period || 'P7D'; + timeParams = `period=${timePeriod}`; serviceRoot = SERVICE_ROOT; } else { let startString = startDate ? isoFormatTime(startDate) : ''; diff --git a/assets/src/scripts/models.spec.js b/assets/src/scripts/models.spec.js index 56b249013036836b8adff6d55c8056e190e17b33..6ba03b069b992a4396f00121a642c1a9eddceced 100644 --- a/assets/src/scripts/models.spec.js +++ b/assets/src/scripts/models.spec.js @@ -39,7 +39,7 @@ describe('Models module', () => { expect(request.url).toContain('parameterCd=' + paramCode + ',00080'); }); - it('Get url includes has the default time period if startDate and endDate are null', () => { + it('Get url includes has the default time period if startDate, endDate and period are null', () => { getTimeSeries({sites: [siteID], params: [paramCode]}); const request = jasmine.Ajax.requests.mostRecent(); expect(request.url).toContain('period=P7D'); @@ -47,7 +47,7 @@ describe('Models module', () => { expect(request.url).not.toContain('endDT'); }); - it('Get url includes startDT and endDT when startDate and endDate are non-null', () =>{ + it('Get url includes startDT and endDT when startDate and endDate are non-null', () => { const startDate = new Date('2018-01-02T15:00:00.000-06:00'); const endDate = new Date('2018-01-02T16:45:00.000-06:00'); getTimeSeries({sites: [siteID], params: [paramCode], startDate: startDate, endDate: endDate}); @@ -57,6 +57,18 @@ describe('Models module', () => { expect(request.url).toContain('endDT=2018-01-02T22:45'); }); + it('Get url includes period when available and startDT and endDT are null', () => { + getTimeSeries({ + sites: [siteID], + params: [paramCode], + period: 'P14D' + }); + const request = jasmine.Ajax.requests.mostRecent(); + expect(request.url).toContain('period=P14D'); + expect(request.url).not.toContain('startDT'); + expect(request.url).not.toContain('endDT'); + }); + it('Uses current data service root if data requested is less than 120 days old', () => { getTimeSeries({sites: [siteID], params: [paramCode]}); let request = jasmine.Ajax.requests.mostRecent(); @@ -155,169 +167,6 @@ describe('Models module', () => { }); }); -const MOCK_LAST_YEAR_DATA = ` -{"name" : "ns1:timeSeriesResponseType", -"declaredType" : "org.cuahsi.waterml.TimeSeriesResponseType", -"scope" : "javax.xml.bind.JAXBElement$GlobalScope", -"value" : { - "queryInfo" : { - "queryURL" : "http://waterservices.usgs.gov/nwis/iv/sites=05413500¶meterCd=00060&period=P7D&indent=on&siteStatus=all&format=json", - "criteria" : { - "locationParam" : "[ALL:05413500]", - "variableParam" : "[00060]", - "parameter" : [ ] - }, - "note" : [ { - "value" : "[ALL:05413500]", - "title" : "filter:sites" - }, { - "value" : "[mode=PERIOD, period=P7D, modifiedSince=null]", - "title" : "filter:timeRange" - }, { - "value" : "methodIds=[ALL]", - "title" : "filter:methodId" - }, { - "value" : "2017-01-09T20:46:07.542Z", - "title" : "requestDT" - }, { - "value" : "1df59e50-f57e-11e7-8ba8-6cae8b663fb6", - "title" : "requestId" - }, { - "value" : "Provisional data are subject to revision. Go to http://waterdata.usgs.gov/nwis/help/?provisional for more information.", - "title" : "disclaimer" - }, { - "value" : "vaas01", - "title" : "server" - } ] - }, - "timeSeries" : [ { - "sourceInfo" : { - "siteName" : "GRANT RIVER AT BURTON, WI", - "siteCode" : [ { - "value" : "05413500", - "network" : "NWIS", - "agencyCode" : "USGS" - } ], - "timeZoneInfo" : { - "defaultTimeZone" : { - "zoneOffset" : "-06:00", - "zoneAbbreviation" : "CST" - }, - "daylightSavingsTimeZone" : { - "zoneOffset" : "-05:00", - "zoneAbbreviation" : "CDT" - }, - "siteUsesDaylightSavingsTime" : true - }, - "geoLocation" : { - "geogLocation" : { - "srs" : "EPSG:4326", - "latitude" : 42.72027778, - "longitude" : -90.8191667 - }, - "localSiteXY" : [ ] - }, - "note" : [ ], - "siteType" : [ ], - "siteProperty" : [ { - "value" : "ST", - "name" : "siteTypeCd" - }, { - "value" : "07060003", - "name" : "hucCd" - }, { - "value" : "55", - "name" : "stateCd" - }, { - "value" : "55043", - "name" : "countyCd" - } ] - }, - "variable" : { - "variableCode" : [ { - "value" : "00060", - "network" : "NWIS", - "vocabulary" : "NWIS:UnitValues", - "variableID" : 45807197, - "default" : true - } ], - "variableName" : "Streamflow, ft³/s", - "variableDescription" : "Discharge, cubic feet per second", - "valueType" : "Derived Value", - "unit" : { - "unitCode" : "ft3/s" - }, - "options" : { - "option" : [ { - "name" : "Statistic", - "optionCode" : "00000" - } ] - }, - "note" : [ ], - "noDataValue" : -999999.0, - "variableProperty" : [ ], - "oid" : "45807197" - }, - "values" : [ { - "value" : [ { - "value" : "302", - "qualifiers" : [ "P" ], - "dateTime" : "2017-01-02T15:00:00.000-06:00" - }, { - "value" : "301", - "qualifiers" : [ "P" ], - "dateTime" : "2017-01-02T15:15:00.000-06:00" - }, { - "value" : "302", - "qualifiers" : [ "P" ], - "dateTime" : "2017-01-02T15:30:00.000-06:00" - }, { - "value" : "301", - "qualifiers" : [ "P" ], - "dateTime" : "2017-01-02T15:45:00.000-06:00" - }, { - "value" : "300", - "qualifiers" : [ "P" ], - "dateTime" : "2017-01-02T16:00:00.000-06:00" - }, { - "value" : "302", - "qualifiers" : [ "P" ], - "dateTime" : "2017-01-02T16:15:00.000-06:00" - }, { - "value" : "300", - "qualifiers" : [ "P" ], - "dateTime" : "2017-01-02T16:30:00.000-06:00" - }, { - "value" : "300", - "qualifiers" : [ "P" ], - "dateTime" : "2017-01-02T16:45:00.000-06:00" - }], - "qualifier" : [ { - "qualifierCode" : "P", - "qualifierDescription" : "Provisional data subject to revision.", - "qualifierID" : 0, - "network" : "NWIS", - "vocabulary" : "uv_rmk_cd" - } ], - "qualityControlLevel" : [ ], - "method" : [ { - "methodDescription" : "", - "methodID" : 158049 - } ], - "source" : [ ], - "offset" : [ ], - "sample" : [ ], - "censorCode" : [ ] - } ], - "name" : "USGS:05413500:00060:00000" - } ] -}, -"nil" : false, -"globalScope" : true, -"typeSubstituted" : false -}` -; - const MOCK_DATA = ` {"name" : "ns1:timeSeriesResponseType", "declaredType" : "org.cuahsi.waterml.TimeSeriesResponseType", @@ -3127,16 +2976,4 @@ const MOCK_DATA = ` "globalScope" : true, "typeSubstituted" : false } -`; - -const MOCK_MEDIAN_DATA = [ - {agency_cd: 'USGS', site_no: '05370000', parameter_cd: '00060', ts_id: '153885', loc_web_ds: '', month_nu: '1', day_nu: '1', begin_yr: '1969', end_yr: '2017', count_nu: '49', p50_va: '16'}, - {agency_cd: 'USGS', site_no: '05370000', parameter_cd: '00060', ts_id: '153885', loc_web_ds: '', month_nu: '1', day_nu: '13', begin_yr: '1969', end_yr: '2017', count_nu: '49', p50_va: '15'}, - {agency_cd: 'USGS', site_no: '05370000', parameter_cd: '00060', ts_id: '153885', loc_web_ds: '', month_nu: '8', day_nu: '5', begin_yr: '1969', end_yr: '2017', count_nu: '49', p50_va: '15'}, - {agency_cd: 'USGS', site_no: '05370000', parameter_cd: '00060', ts_id: '153885', loc_web_ds: '', month_nu: '2', day_nu: '29', begin_yr: '1969', end_yr: '2017', count_nu: '49', p50_va: '13'} -]; -const MOCK_MEDIAN_VARIABLES = { - '00060': { - oid: 'varID' - } -}; +`; \ No newline at end of file diff --git a/assets/src/scripts/selectors/time-series-selector.js b/assets/src/scripts/selectors/time-series-selector.js index f53351a698e2fa72be3d3f98983727ec2d405bc0..3d140531de630d56979e27f61a3112994d264cc3 100644 --- a/assets/src/scripts/selectors/time-series-selector.js +++ b/assets/src/scripts/selectors/time-series-selector.js @@ -26,9 +26,9 @@ export const getIanaTimeZone = state => state.series.ianaTimeZone ? state.series export const getNwisTimeZone = state => state.series.timeZones || {}; -export const getRequestedTimeRange = state => state.timeSeriesState.requestedTimeRange; - +export const getCustomTimeRange = state => state.timeSeriesState.customTimeRange; +export const hasAnyTimeSeries = state => state.series && state.series.timeSeries && state.series.timeSeries != {}; /* * Selectors the return derived data from the state */ diff --git a/assets/src/scripts/store/index.js b/assets/src/scripts/store/index.js index 377782011626cf403b399938100013e7dfc233b9..f8143ea781ac3eceb0fc034713976d5bd7073f39 100644 --- a/assets/src/scripts/store/index.js +++ b/assets/src/scripts/store/index.js @@ -1,16 +1,18 @@ +import find from 'lodash/find'; import findKey from 'lodash/findKey'; import last from 'lodash/last'; import { DateTime } from 'luxon'; import { applyMiddleware, createStore, combineReducers, compose } from 'redux'; import { default as thunk } from 'redux-thunk'; + import { getPreviousYearTimeSeries, getTimeSeries, sortedParameters, queryWeatherService } from '../models'; import { calcStartTime } from '../utils'; import { normalize } from '../schema'; import { fetchFloodFeatures, fetchFloodExtent } from '../flood-data'; import { fetchSiteStatistics } from '../statistics-data'; import { getCurrentParmCd, getCurrentDateRange, hasTimeSeries, getTsRequestKey, getRequestTimeRange, - getRequestedTimeRange, getIanaTimeZone, getTimeSeriesCollectionIds } from '../selectors/time-series-selector'; + getCustomTimeRange, getIanaTimeZone } from '../selectors/time-series-selector'; import { floodDataReducer as floodData } from './flood-data-reducer'; import { floodStateReducer as floodState } from './flood-state-reducer'; import { nldiDataReducer as nldiData } from './nldi-data-reducer'; @@ -81,7 +83,6 @@ export const Actions = { const notes = collection.queryInfo[requestKey].notes; const endTime = notes.requestDT; const startTime = calcStartTime('P7D', endTime, 'local'); - dispatch(Actions.setCustomDateRange(startTime, endTime)); if (latitude !== null && longitude !== null) { dispatch(Actions.retrieveLocationTimeZone(latitude, longitude)); } @@ -134,24 +135,46 @@ export const Actions = { ); }; }, - retrieveCustomTimeSeries(site) { + + retrieveCustomTimePeriodTimeSeries(site, parameterCd, period) { return function(dispatch, getState) { const state = getState(); - const parmCd = getCurrentParmCd(state); - const requestedTimeRange = getRequestedTimeRange(state); + const parmCd = parameterCd; const requestKey = getTsRequestKey('current', 'custom', parmCd)(state); - const currentTsIds = getTimeSeriesCollectionIds('current', 'custom', parmCd)(state) || []; - if (currentTsIds.length > 0) { - dispatch(Actions.resetTimeSeries(requestKey)); - } dispatch(Actions.setCurrentDateRange('custom')); dispatch(Actions.addTimeSeriesLoading([requestKey])); + return getTimeSeries({sites: [site], params: [parmCd], period: period}).then( + series => { + const collection = normalize(series, requestKey); + const variables = Object.values(collection.variables); + const variableToDraw = find(variables, v => v.variableCode.value === parameterCd); + dispatch(Actions.setCurrentVariable(variableToDraw.variableCode.variableID)); + dispatch(Actions.addSeriesCollection(requestKey, collection)); + dispatch(Actions.removeTimeSeriesLoading([requestKey])); + }, + () => { + console.log(`Unable to fetch data for period ${period} and parameter code ${parmCd}`); + dispatch(Actions.addSeriesCollection(requestKey, {})); + dispatch(Actions.removeTimeSeriesLoading([requestKey])); + } + ); + }; + }, + + retrieveCustomTimeSeries(site, startTime, endTime) { + return function(dispatch, getState) { + const state = getState(); + const parmCd = getCurrentParmCd(state); + const requestKey = getTsRequestKey('current', 'custom', parmCd)(state); + + dispatch(Actions.setCustomDateRange(startTime, endTime)); + dispatch(Actions.addTimeSeriesLoading([requestKey])); dispatch(Actions.toggleTimeSeries('median', false)); return getTimeSeries({ sites: [site], params: [parmCd], - startDate: requestedTimeRange.startDT, - endDate: requestedTimeRange.endDT + startDate: startTime, + endDate: endTime }).then( series => { const collection = normalize(series, requestKey); @@ -159,7 +182,7 @@ export const Actions = { dispatch(Actions.removeTimeSeriesLoading([requestKey])); }, () => { - console.log(`Unable to fetch data for between ${requestedTimeRange.startDT} and ${requestedTimeRange.endDT} and parameter code ${parmCd}`); + console.log(`Unable to fetch data for between ${startTime} and ${endTime} and parameter code ${parmCd}`); dispatch(Actions.addSeriesCollection(requestKey, {})); dispatch(Actions.removeTimeSeriesLoading([requestKey])); } @@ -187,7 +210,6 @@ export const Actions = { dispatch(Actions.retrieveCompareTimeSeries(site, period, startTime, endTime)); dispatch(Actions.addSeriesCollection(requestKey, collection)); dispatch(Actions.removeTimeSeriesLoading([requestKey])); - dispatch(Actions.setCustomDateRange(startTime, endTime)); dispatch(Actions.toggleTimeSeries('median', true)); }, () => { @@ -230,9 +252,12 @@ export const Actions = { updateCurrentVariable(siteno, variableID) { return function(dispatch, getState) { dispatch(Actions.setCurrentVariable(variableID)); - const currentDateRange = getCurrentDateRange(getState()); + const state = getState(); + const currentDateRange = getCurrentDateRange(state); if (currentDateRange === 'custom') { - dispatch(Actions.retrieveCustomTimeSeries(siteno)); + const timeRange = getCustomTimeRange(state); + dispatch( + Actions.retrieveCustomTimeSeries(siteno, timeRange.startDT, timeRange.endDT)); } else { dispatch(Actions.retrieveExtendedTimeSeries(siteno, currentDateRange)); } @@ -367,14 +392,13 @@ export const Actions = { endTime }; }, - getUserRequestedDataForDateRange(siteno, startTimeStr, endTimeStr) { + retrieveUserRequestedDataForDateRange(siteno, startTimeStr, endTimeStr) { return function(dispatch, getState) { const state = getState(); const locationIanaTimeZone = getIanaTimeZone(state); const startTime = new DateTime.fromISO(startTimeStr,{zone: locationIanaTimeZone}).toMillis(); const endTime = new DateTime.fromISO(endTimeStr, {zone: locationIanaTimeZone}).toMillis(); - dispatch(Actions.setCustomDateRange(startTime, endTime)); - dispatch(Actions.retrieveCustomTimeSeries(siteno)); + dispatch(Actions.retrieveCustomTimeSeries(siteno, startTime, endTime)); }; }, setGageHeightFromStageIndex(index) { @@ -435,7 +459,7 @@ export const configureStore = function (initialState) { median: false }, currentDateRange: 'P7D', - requestedTimeRange: null, + customTimeRange: null, currentVariableID: null, cursorOffset: null, audiblePlayId: null, diff --git a/assets/src/scripts/store/index.spec.js b/assets/src/scripts/store/index.spec.js index 65fe00954a290605cd689ec81528001d494d3d27..96323f7d7757283f1aef7470ba0e9c6eb02d3588 100644 --- a/assets/src/scripts/store/index.spec.js +++ b/assets/src/scripts/store/index.spec.js @@ -55,7 +55,7 @@ describe('Redux store', () => { timeSeriesState: { currentVariableID: '45807042', currentDateRange: 'P7D', - requestedTimeRange: {startDT: 1488348000000, endDT: 1490936400000} + customTimeRange: {startDT: 1488348000000, endDT: 1490936400000} } }; @@ -120,11 +120,12 @@ describe('Redux store', () => { jasmine.Ajax.requests.mostRecent().respondWith({ status: 500 }); - p.then(() => { - expect(Actions.setLocationIanaTimeZone.calls.count()).toBe(1); - expect(Actions.setLocationIanaTimeZone).toHaveBeenCalledWith(null); - done(); - }); + p.then( + () => { + expect(Actions.setLocationIanaTimeZone.calls.count()).toBe(1); + expect(Actions.setLocationIanaTimeZone).toHaveBeenCalledWith(null); + done(); + }); }); }); @@ -167,11 +168,10 @@ describe('Redux store', () => { spyOn(Actions, 'retrieveCompareTimeSeries'); spyOn(Actions, 'toggleTimeSeries'); spyOn(Actions, 'setCurrentVariable'); - spyOn(Actions, 'setCustomDateRange'); let p = Actions.retrieveTimeSeries(SITE_NO)(mockDispatch, mockGetState); p.then(() => { - expect(mockDispatch.calls.count()).toBe(9); + expect(mockDispatch.calls.count()).toBe(8); expect(Actions.addSeriesCollection.calls.count()).toBe(1); expect(Actions.addSeriesCollection.calls.argsFor(0)[0]).toBe('current'); expect(Actions.retrieveLocationTimeZone.calls.count()).toBe(1); @@ -184,7 +184,6 @@ describe('Redux store', () => { expect(Actions.toggleTimeSeries.calls.argsFor(0)).toEqual(['current', true]); expect(Actions.setCurrentVariable.calls.count()).toBe(1); expect(Actions.setCurrentVariable.calls.argsFor(0)).toEqual(['45807197']); - expect(Actions.setCustomDateRange.calls.count()).toBe(1); done(); }); @@ -403,6 +402,72 @@ describe('Redux store', () => { }); }); + describe('retrieveCustomTimePeriodTimeSeries', () => { + let mockDispatch; + let mockGetState; + + beforeEach(() => { + jasmine.Ajax.install(); + + mockDispatch = jasmine.createSpy('mockDispatch'); + mockGetState = jasmine.createSpy('mockGetState').and.returnValue(TEST_STATE); + spyOn(Actions, 'setCurrentDateRange'); + spyOn(Actions, 'addTimeSeriesLoading'); + spyOn(Actions, 'setCurrentVariable'); + spyOn(Actions, 'addSeriesCollection'); + spyOn(Actions, 'removeTimeSeriesLoading'); + }); + + afterEach(() => { + jasmine.Ajax.uninstall(); + }); + + it('Should dispatch an action to set the current date range and set the time series loading key', () => { + Actions.retrieveCustomTimePeriodTimeSeries('12345678', '00060', 'P10D')(mockDispatch, mockGetState); + expect(Actions.addTimeSeriesLoading).toHaveBeenCalledWith(['current:custom:00060']); + expect(Actions.setCurrentDateRange).toHaveBeenCalledWith('custom'); + }); + + it('Should make the service call with the correct parameters', () => { + Actions.retrieveCustomTimePeriodTimeSeries('12345678', '00060', 'P10D')(mockDispatch, mockGetState); + let request = jasmine.Ajax.requests.mostRecent(); + expect(request.url).toContain('sites=1234567'); + expect(request.url).toContain('parameterCd=00060'); + expect(request.url).toContain('period=P10D'); + }); + + it('Should set the current variable and add the time series open successful response', (done) => { + let p = Actions.retrieveCustomTimePeriodTimeSeries('12345678', '00060', 'P10D')(mockDispatch, mockGetState); + let request = jasmine.Ajax.requests.mostRecent(); + request.respondWith({ + responseText: MOCK_DATA, + status: 200 + }); + p.then(() => { + expect(Actions.setCurrentVariable).toHaveBeenCalledWith(45807197); + expect(Actions.addSeriesCollection).toHaveBeenCalled(); + expect(Actions.addSeriesCollection.calls.argsFor(0)[0]).toEqual('current:custom:00060'); + expect(Actions.removeTimeSeriesLoading).toHaveBeenCalledWith(['current:custom:00060']); + done(); + }); + }); + + it('Should clear the data for and remove the time series loader for bad data', (done) => { + let p = Actions.retrieveCustomTimePeriodTimeSeries('12345678', '00060', 'P10D')(mockDispatch, mockGetState); + let request = jasmine.Ajax.requests.mostRecent(); + request.respondWith({ + responseText: 'Bad data', + status: 500 + }); + p.then(() => { + expect(Actions.setCurrentVariable).not.toHaveBeenCalled(); + expect(Actions.addSeriesCollection).toHaveBeenCalledWith('current:custom:00060', {}); + expect(Actions.removeTimeSeriesLoading).toHaveBeenCalledWith(['current:custom:00060']); + done(); + }); + }); + }); + describe('retrieveCustomTimeSeries with good data', () => { let mockDispatch; let mockGetState; @@ -422,16 +487,11 @@ describe('Redux store', () => { }); it('Should dispatch an action to set the current date range', () => { - Actions.retrieveCustomTimeSeries('9876543')(mockDispatch, mockGetState); + Actions.retrieveCustomTimeSeries('9876543', 1488348000000, 1490936400000)(mockDispatch, mockGetState); request = jasmine.Ajax.requests.mostRecent(); request.respondWith({ - responseText: MOCK_DATA, status: 200 }); - expect(mockDispatch).toHaveBeenCalledWith({ - type: 'SET_CURRENT_DATE_RANGE', - period: 'custom' - }); expect(Actions.addTimeSeriesLoading).toHaveBeenCalledWith(['current:custom:00060']); }); @@ -439,10 +499,10 @@ describe('Redux store', () => { mockGetState.and.returnValue(Object.assign({}, TEST_STATE, { timeSeriesState: Object.assign({}, TEST_STATE.timeSeriesState, { currentDateRange: 'custom', - requestedTimeRange: {startDT: 2942805600000, endDT: 2942978400000} + currentTimeRange: {startDT: 2942805600000, endDT: 2942978400000} }) })); - Actions.retrieveCustomTimeSeries('490129388')(mockDispatch, mockGetState); + Actions.retrieveCustomTimeSeries('490129388', 2942805600000, 2942978400000)(mockDispatch, mockGetState); request = jasmine.Ajax.requests.mostRecent(); request.respondWith({ responseText: MOCK_DATA, @@ -455,12 +515,12 @@ describe('Redux store', () => { expect(request.url).toContain('endDT=2063-04-05'); }); - it('Should dispatch add series collection and hide median series', (done) => { + it('Should dispatch add series collection and hide median series after good response', (done) => { mockGetState.and.returnValue(TEST_STATE); spyOn(Actions, 'addSeriesCollection'); spyOn(Actions, 'retrieveCompareTimeSeries'); spyOn(Actions, 'toggleTimeSeries'); - let p = Actions.retrieveCustomTimeSeries('490129388')(mockDispatch, mockGetState); + let p = Actions.retrieveCustomTimeSeries('490129388', 1488348000000, 1490936400000)(mockDispatch, mockGetState); request = jasmine.Ajax.requests.mostRecent(); request.respondWith({ responseText: MOCK_DATA, @@ -476,28 +536,6 @@ describe('Redux store', () => { done(); }); }); - - it('Should reset the time series if it already exists', (done) => { - mockGetState.and.returnValue(Object.assign({}, TEST_STATE, { - series: Object.assign({}, TEST_STATE.series, { - requests: {'current:custom:00060': { - timeSeriesCollections: [7, 8] - }} - }) - })); - spyOn(Actions, 'resetTimeSeries'); - let p = Actions.retrieveCustomTimeSeries('490129388')(mockDispatch, mockGetState); - request = jasmine.Ajax.requests.mostRecent(); - request.respondWith({ - responseText: MOCK_DATA, - status: 200 - }); - p.then(() => { - expect(mockDispatch.calls.count()).toBe(6); - expect(Actions.resetTimeSeries).toHaveBeenCalled(); - done(); - }); - }); }); describe('retrieveCustomTimeSeries with bad data', () => { @@ -513,7 +551,7 @@ describe('Redux store', () => { mockGetState.and.returnValue(Object.assign({}, TEST_STATE, { timeSeriesState: Object.assign({}, TEST_STATE.timeSeriesState, { currentDateRange: 'custom', - requestedTimeRange: {startDT: 2942805600000, endDT: 2942978400000} + currentTimeRange: {startDT: 2942805600000, endDT: 2942978400000} }) })); @@ -526,8 +564,9 @@ describe('Redux store', () => { }); it('Should add a series with an empty collection when it is bad data', (done) => { - let p = Actions.retrieveCustomTimeSeries('9876543')(mockDispatch, mockGetState); - jasmine.Ajax.requests.mostRecent().respondWith({ + let p = Actions.retrieveCustomTimeSeries('9876543', 1488348000000, 1490936400000)(mockDispatch, mockGetState); + let request = jasmine.Ajax.requests.mostRecent(); + request.respondWith({ status: 500 }); expect(Actions.addTimeSeriesLoading).toHaveBeenCalledWith(['current:custom:00060']); @@ -604,13 +643,12 @@ describe('Redux store', () => { status: 200 }); p.then(() => { - expect(mockDispatch.calls.count()).toBe(7); + expect(mockDispatch.calls.count()).toBe(6); expect(Actions.addSeriesCollection).toHaveBeenCalled(); expect(Actions.addSeriesCollection.calls.argsFor(0)[0]).toEqual('current:P30D:00060'); expect(Actions.retrieveCompareTimeSeries).toHaveBeenCalled(); expect(Actions.retrieveCompareTimeSeries.calls.argsFor(0)[1]).toEqual('P30D'); expect(Actions.removeTimeSeriesLoading).toHaveBeenCalledWith(['current:P30D:00060']); - expect(Actions.setCustomDateRange).toHaveBeenCalled(); expect(Actions.toggleTimeSeries).toHaveBeenCalledWith('median', true); done(); }); @@ -900,6 +938,35 @@ describe('Redux store', () => { expect(Actions.timeSeriesPlayStop).toHaveBeenCalled(); }); }); + + describe('retrieveUserRequestedDataForDateRange', () => { + let mockDispatch; + let mockGetState; + + beforeEach(() => { + mockDispatch = jasmine.createSpy('mockDispatch'); + mockGetState = jasmine.createSpy('mockGetState').and.returnValue({ + series: { + ianaTimeZone: 'America/Chicago' + } + }); + + spyOn(Actions, 'retrieveCustomTimeSeries'); + jasmine.Ajax.install(); + }); + + afterEach(() => { + jasmine.Ajax.uninstall(); + }); + + it('Converts time strings to javascript date/time objects correctly', () => { + Actions.retrieveUserRequestedDataForDateRange('12345678', '2010-01-01', '2010-03-01')(mockDispatch, mockGetState); + expect(Actions.retrieveCustomTimeSeries).toHaveBeenCalled(); + expect(Actions.retrieveCustomTimeSeries.calls.argsFor(0)[0]).toEqual('12345678'); + expect(Actions.retrieveCustomTimeSeries.calls.argsFor(0)[1]).toEqual(1262325600000); + expect(Actions.retrieveCustomTimeSeries.calls.argsFor(0)[2]).toEqual(1267423200000); + }); + }); }); describe('synchronous actions', () => { diff --git a/assets/src/scripts/store/time-series-state-reducer.js b/assets/src/scripts/store/time-series-state-reducer.js index 4c383b300fe9d00cbc821765fc942ab81b21134c..f98fda6713ea30d0d5f1836bd0da88679a71d1af 100644 --- a/assets/src/scripts/store/time-series-state-reducer.js +++ b/assets/src/scripts/store/time-series-state-reducer.js @@ -69,10 +69,11 @@ const removeLoadingTimeSeries = function(timeSeriesState, action) { }; }; -const requestedTimeRange = function(timeSeriesState, action) { +const setCustomDateRange = function(timeSeriesState, action) { return { ...timeSeriesState, - requestedTimeRange: {startDT: action.startTime, endDT: action.endTime} + currentDateRange: 'custom', + customTimeRange: {startDT: action.startTime, endDT: action.endTime} }; }; @@ -90,7 +91,7 @@ export const timeSeriesStateReducer = function(timeSeriesState={}, action) { case 'TIME_SERIES_PLAY_STOP': return timeSeriesPlayStop(timeSeriesState, action); case 'TIME_SERIES_LOADING_ADD': return addLoadingTimeSeries(timeSeriesState, action); case 'TIME_SERIES_LOADING_REMOVE': return removeLoadingTimeSeries(timeSeriesState, action); - case 'SET_CUSTOM_DATE_RANGE': return requestedTimeRange(timeSeriesState, action); + case 'SET_CUSTOM_DATE_RANGE': return setCustomDateRange(timeSeriesState, action); default: return timeSeriesState; } }; diff --git a/assets/src/scripts/store/time-series-state-reducer.spec.js b/assets/src/scripts/store/time-series-state-reducer.spec.js index 8417dd1615223b24e7ca567d355d8ac37b018249..9203f92d04edb9a4bb2db451fe060c1de728eb96 100644 --- a/assets/src/scripts/store/time-series-state-reducer.spec.js +++ b/assets/src/scripts/store/time-series-state-reducer.spec.js @@ -88,13 +88,15 @@ describe('time-series-state-reducer', () => { it('should handle SET_CUSTOM_DATE_RANGE', () => { expect(timeSeriesStateReducer({ - requestedTimeRange: null + customTimeRange: null, + currentDateRange: 'P7D' }, { type: 'SET_CUSTOM_DATE_RANGE', startTime: 1551420000000, endTime: 1552197600000 })).toEqual({ - requestedTimeRange: {startDT: 1551420000000, endDT: 1552197600000} + customTimeRange: {startDT: 1551420000000, endDT: 1552197600000}, + currentDateRange: 'custom' }); }); }); diff --git a/graph-server/package.json b/graph-server/package.json index 0d210fdb1756e4d181fa31a4b3a3b25534e294aa..80eabb278591f52bc306e5ca1149a1fa57046c5f 100644 --- a/graph-server/package.json +++ b/graph-server/package.json @@ -6,10 +6,10 @@ "scripts": { "test": "nyc jasmine", "start": "DEBUG=express:* node src/index.js", - "watch": "STATIC_ROOT=http://localhost:9000 nodemon src" + "watch": "DEBUG=express:* STATIC_ROOT=http://localhost:9000 nodemon src" }, "engines": { - "node": "12.13.1" + "node": "10.18.0" }, "repository": { "type": "git", diff --git a/graph-server/src/index.js b/graph-server/src/index.js index cc887a6bc20a369c9e67d1a95a2265d07884b562..64d04b120591126bc13969f052ecfd4cbd18fdac 100644 --- a/graph-server/src/index.js +++ b/graph-server/src/index.js @@ -4,7 +4,7 @@ const cache = require('express-cache-headers'); const { checkSchema, validationResult } = require('express-validator'); const { version } = require('../package.json'); -const renderToRespone = require('./renderer'); +const renderToResponse = require('./renderer'); const PORT = process.env.NODE_PORT || 2929; @@ -57,10 +57,11 @@ app.get(`${PATH_CONTEXT}/monitoring-location/:siteID/`, cache({ttl: CACHE_TIMEOU return; } - renderToRespone(res, { + renderToResponse(res, { siteID: req.params.siteID, parameterCode: req.query.parameterCode, - compare: req.query.compare + compare: req.query.compare, + period: req.query.period }); }); diff --git a/graph-server/src/renderer/index.js b/graph-server/src/renderer/index.js index 799bd645302c6bc539b56cdb24c70ffd696f782d..22ea5b9212ea4c146b727b4dfe53e67b0bca3dd7 100644 --- a/graph-server/src/renderer/index.js +++ b/graph-server/src/renderer/index.js @@ -5,13 +5,15 @@ const PAST_SERVICE_ROOT = process.env.PAST_SERVICE_ROOT || 'https://nwis.waterse const STATIC_ROOT = process.env.STATIC_ROOT || 'https://waterdata.usgs.gov/nwisweb/wsgi/static'; -const renderToRespone = function (res, {siteID, parameterCode, compare}) { +const renderToResponse = function (res, {siteID, parameterCode, compare, period}) { + console.log(`Using static root ${STATIC_ROOT}`); const componentOptions = { siteno: siteID, parameter: parameterCode, compare: compare, + period: period, cursorOffset: false, - interactive: false + showOnlyGraph : true }; renderPNG({ pageURL: 'http://wdfn-graph-server', @@ -54,4 +56,4 @@ const renderToRespone = function (res, {siteID, parameterCode, compare}) { }; -module.exports = renderToRespone; +module.exports = renderToResponse;