diff --git a/assets/src/scripts/components/hydrograph/date-controls.js b/assets/src/scripts/components/hydrograph/date-controls.js index c7e89abe5458618446bdf7c272f48cf2d5ba2117..5c8919a603d67e4325a3fefafade7660523d98b9 100644 --- a/assets/src/scripts/components/hydrograph/date-controls.js +++ b/assets/src/scripts/components/hydrograph/date-controls.js @@ -1,33 +1,48 @@ -import { DateTime } from 'luxon'; -import { createStructuredSelector } from 'reselect'; +import {DateTime} from 'luxon'; +import {createStructuredSelector} from 'reselect'; -import { link } from '../../lib/d3-redux'; +import {link} from '../../lib/d3-redux'; -import { drawLoadingIndicator } from '../../d3-rendering/loading-indicator'; -import { isLoadingTS, hasAnyTimeSeries } from '../../selectors/time-series-selector'; -import { Actions } from '../../store'; +import {drawLoadingIndicator} from '../../d3-rendering/loading-indicator'; +import { + isLoadingTS, + hasAnyTimeSeries, + getCurrentDateRange, + getCustomTimeRange, + getIanaTimeZone +} from '../../selectors/time-series-selector'; +import {Actions} from '../../store'; export const drawDateRangeControls = function(elem, store, siteno) { const DATE_RANGE = [{ - label: 'seven-day', name: '7 days', period: 'P7D' }, { - label: 'thirty-days', name: '30 days', period: 'P30D' }, { - label: 'one-year', + label: 'one-', name: '1 year', period: 'P1Y' }, { - label: 'custom-date-range', name: 'Custom', period: 'custom', ariaExpanded: false }]; + const initialDateRange = getCurrentDateRange(store.getState()); + let initialCustomTimeRange; + /* TODO: add back in once we straighten out time zone retrieval */ + //if (initialDateRange === 'custom') { + // const customTimeRangeInMillis = getCustomTimeRange(store.getState()); + // const locationIanaTimeZone = getIanaTimeZone(store.getState()); + // initialCustomTimeRange = { + // start : DateTime.fromMillis(customTimeRangeInMillis.startDT, {zone: locationIanaTimeZone}).toFormat('LL/dd/yyyy'), + // end: DateTime.fromMillis(customTimeRangeInMillis.endDT, {zone: locationIanaTimeZone}).toFormat('LL/dd/yyyy') + // }; + // } + const container = elem.insert('div', ':nth-child(2)') .attr('id', 'ts-daterange-select-container') .attr('role', 'radiogroup') @@ -39,8 +54,9 @@ export const drawDateRangeControls = function(elem, store, siteno) { const customDateContainer = elem.insert('div', ':nth-child(3)') .attr('id', 'ts-customdaterange-select-container') .attr('role', 'customdate') + .attr('class', 'usa-form') .attr('aria-label', 'Custom date specification') - .attr('hidden', true); + .attr('hidden', initialDateRange !== 'custom' ? true : null); customDateContainer.append('label') .attr('for', 'date-input') @@ -77,6 +93,9 @@ export const drawDateRangeControls = function(elem, store, siteno) { .attr('name', 'user-specified-start-date') .attr('aria-labelledby', 'custom-start-date-label') .attr('type', 'date'); + if (initialCustomTimeRange) { + customStartDate.attr('value', initialCustomTimeRange.start); + } endDateContainer.append('label') .attr('class', 'usa-label') @@ -90,8 +109,9 @@ export const drawDateRangeControls = function(elem, store, siteno) { .attr('name', 'user-specified-end-date') .attr('aria-labelledby', 'custom-end-date-label') .attr('type', 'date'); - - customDateContainer.append('br'); + if (initialCustomTimeRange) { + customEndDate.attr('value', initialCustomTimeRange.end); + } const submitContainer = customDateContainer.append('div') .attr('class', 'submit-button'); @@ -137,7 +157,7 @@ export const drawDateRangeControls = function(elem, store, siteno) { li.append('input') .attr('type', 'radio') .attr('name', 'ts-daterange-input') - .attr('id', d => d.label) + .attr('id', d => `${d.period}-input`) .attr('class', 'usa-radio__input') .attr('value', d => d.period) .attr('ga-on', 'click') @@ -163,7 +183,7 @@ export const drawDateRangeControls = function(elem, store, siteno) { }); li.append('label') .attr('class', 'usa-radio__label') - .attr('for', (d) => d.label) + .attr('for', (d) => `${d.period}-input`) .text((d) => d.name); - li.select(`#${DATE_RANGE[0].label}`).attr('checked', true); + li.select(`#${initialDateRange}-input`).attr('checked', true); }; \ No newline at end of file diff --git a/assets/src/scripts/components/hydrograph/index.js b/assets/src/scripts/components/hydrograph/index.js index 38ec2298a9a95a54abd9e4a5254276631b80eb4c..aba149763d5b92920faf1b996409874caf33ba34 100644 --- a/assets/src/scripts/components/hydrograph/index.js +++ b/assets/src/scripts/components/hydrograph/index.js @@ -7,7 +7,8 @@ import {createStructuredSelector} from 'reselect'; import {drawWarningAlert, drawInfoAlert} from '../../d3-rendering/alerts'; import {link} from '../../lib/d3-redux'; -import {isLoadingTS, hasAnyTimeSeries, getTimeSeries} from '../../selectors/time-series-selector'; +import {isLoadingTS, hasAnyTimeSeries, getTimeSeries, getCurrentParmCd, + getVariables} from '../../selectors/time-series-selector'; import {Actions} from '../../store'; import {cursorSlider} from './cursor'; @@ -34,16 +35,16 @@ const controlDisplay = function(elem, showElem) { elem.attr('hidden', showElem ? null : true); }; + export const attachToNode = function (store, node, { siteno, - parameter, + parameterCode, compare, period, startDT, endDT, - cursorOffset, showOnlyGraph = false, showMLName = false } = {}) { @@ -62,29 +63,52 @@ export const attachToNode = function (store, sizeClass: () => 'fa-3x' }))); - // If specified, turn the visibility of the comparison time series on. - if (compare) { - store.dispatch(Actions.toggleTimeSeries('compare', true)); - } - - // If specified, initialize the cursorOffset - if (cursorOffset !== undefined) { - store.dispatch(Actions.setCursorOffset(cursorOffset)); - } - - if (startDT !==undefined && endDT !== undefined) { - store.dispatch(Actions.retrieveDataForDateRange(siteno, startDT, endDT, parameter)); - } - - // Fetch the time series data - if (period) { - store.dispatch(Actions.retrieveCustomTimePeriodTimeSeries(siteno, parameter ? parameter : '00060', period)) - .catch((message) => drawInfoAlert(nodeElem, {body: message ? message : 'No data returned'})); + let fetchPromise; + if (showOnlyGraph) { + // Only fetch what is needed + if (parameterCode && period) { + fetchPromise = store.dispatch(Actions.retrieveCustomTimePeriodTimeSeries(siteno, parameterCode, period)); + } else if (parameterCode && startDT && endDT) { + fetchPromise = store.dispatch(Actions.retrieveDataForDateRange(siteno, startDT, endDT, parameterCode)); + } else { + fetchPromise = store.dispatch(Actions.retrieveTimeSeries(siteno, parameterCode ? [parameterCode] : null)); + } } else { - store.dispatch(Actions.retrieveTimeSeries(siteno, parameter ? [parameter] : null)) - .catch(() => drawInfoAlert(nodeElem, {body: 'No current time series data available for this site'})); + // Retrieve all parameter codes for 7 days and median statistics + fetchPromise = store.dispatch(Actions.retrieveTimeSeries(siteno)) + .then(() => { + // Fetch any extended data needed to set initial state + const currentParamCode = parameterCode ? parameterCode : getCurrentParmCd(store.getState()); + if (period === 'P30D' || period === 'P1Y') { + store.dispatch(Actions.retrieveExtendedTimeSeries(siteno, period, currentParamCode)); + } else if (startDT && endDT) { + store.dispatch(Actions.retrieveDataForDateRange(siteno, startDT, endDT, currentParamCode)); + } + }); + store.dispatch(Actions.retrieveMedianStatistics(siteno)); + } + fetchPromise.then(() => { + if (!hasAnyTimeSeries(store.getState())) { + drawInfoAlert(nodeElem, {body: 'No time series data available for this site'}); + } else { + //Update time series state + if (parameterCode) { + const isThisParamCode = function(variable) { + return variable.variableCode.value === parameterCode; + }; + const thisVariable = Object.values(getVariables(store.getState())).find(isThisParamCode); + store.dispatch(Actions.setCurrentVariable(thisVariable.oid)); + } + if (compare) { + store.dispatch(Actions.toggleTimeSeries('compare', true)); + } + } + }); + // Set currentDateRange prior to rendering so initial state is correct + if (period || (startDT && endDT)) { + store.dispatch(Actions.setCurrentDateRange(period ? period : 'custom')); + store.dispatch(Actions.setCustomDateRange()) } - store.dispatch(Actions.retrieveMedianStatistics(siteno)); // Set up rendering functions for the graph-container let graphContainer = nodeElem.select('.graph-container') diff --git a/assets/src/scripts/index.js b/assets/src/scripts/index.js index 42738734c02ecd34b8bc27c6b1627d2e67045ac4..8826d34eb7face538ed0ef63e59f7a6c223e71f8 100644 --- a/assets/src/scripts/index.js +++ b/assets/src/scripts/index.js @@ -31,7 +31,9 @@ const load = function () { // If options is specified on the node, expect it to be a JSON string. // Otherwise, use the dataset attributes as the component options. const options = node.dataset.options ? JSON.parse(node.dataset.options) : node.dataset; - COMPONENTS[node.dataset.component](store, node, options); + let hash = window.location.hash; + const hashOptions = hash.length ? Object.fromEntries(new window.URLSearchParams(hash.substring(1))) : {}; + COMPONENTS[node.dataset.component](store, node, Object.assign({}, options, hashOptions)); } diff --git a/assets/src/scripts/polyfills.js b/assets/src/scripts/polyfills.js index d5b4182106b5f1feb5461e1b86f16061156236c2..63e8f539f20c715df2c7bb7b6057d6db9398eabf 100644 --- a/assets/src/scripts/polyfills.js +++ b/assets/src/scripts/polyfills.js @@ -2,6 +2,7 @@ import 'element-closest'; import 'matchmedia-polyfill'; import 'core-js/features/array/fill'; +import 'core-js/features/array/find'; import 'core-js/features/array/from'; import 'core-js/features/array/iterator'; import 'core-js/features/array/includes'; @@ -10,10 +11,12 @@ import 'core-js/features/math/sign'; import 'core-js/features/math/trunc'; import 'core-js/features/number/is-integer'; import 'core-js/features/number/is-nan'; +import 'core-js/features/from-entries'; import 'core-js/features/object/values'; import 'core-js/features/promise'; import 'core-js/features/string/repeat'; import 'core-js/features/string/starts-with'; +import 'core-js/features/url-search-params'; diff --git a/assets/src/scripts/selectors/time-series-selector.js b/assets/src/scripts/selectors/time-series-selector.js index c49ee49f98e260a55163197f5651c5ef40ec91be..840adc14ab1bb86dc95fb5a1868b407e9869b220 100644 --- a/assets/src/scripts/selectors/time-series-selector.js +++ b/assets/src/scripts/selectors/time-series-selector.js @@ -34,7 +34,13 @@ export const getCustomTimeRange = state => state.timeSeriesState.customTimeRange export const getTimeSeries = state => state.series.timeSeries ? state.series.timeSeries : {}; -export const hasAnyTimeSeries = state => state.series && state.series.timeSeries && state.series.timeSeries != {}; +export const hasAnyTimeSeries = createSelector( + getTimeSeries, + (timeSeries) => { + return Object.keys(timeSeries).length ? true : false; + } +); + /* * Selectors the return derived data from the state */ diff --git a/assets/src/scripts/selectors/time-series-selector.spec.js b/assets/src/scripts/selectors/time-series-selector.spec.js index ead4b611dfdc4f85411fc28ac51e52f2c4e98d33..ac63789457efbf7f609a49edc1c7c34dfabd9d63 100644 --- a/assets/src/scripts/selectors/time-series-selector.spec.js +++ b/assets/src/scripts/selectors/time-series-selector.spec.js @@ -1,5 +1,5 @@ import { getVariables, getSourceInfo, getSiteCodes, getCurrentVariableID, getCurrentDateRange, getTimeSeries, - getMonitoringLocationName, getAgencyCode, getCurrentVariable, getQueryInfo, getRequests, getCurrentParmCd, + hasAnyTimeSeries, getMonitoringLocationName, getAgencyCode, getCurrentVariable, getQueryInfo, getRequests, getCurrentParmCd, hasTimeSeries, getTsRequestKey, getTsQueryInfo, getRequestTimeRange, isLoadingTS, getTSRequest, getTimeSeriesCollectionIds, getIanaTimeZone, getNwisTimeZone } from './time-series-selector'; @@ -57,6 +57,26 @@ describe('timeSeriesSelector', () => { }); }); + describe('hasAnyTimeSeries', () => { + it('Return false if series is empty', () => { + expect(hasAnyTimeSeries({ + series: {} + })).toBe(false); + }); + + it('Return true if series is not empty', () => { + expect(hasAnyTimeSeries({ + series: { + timeSeries: { + '00010': { + prop1: 'value1' + } + } + } + })).toBe(true); + }); + }); + describe('getSourceInfo', () => { it('Return an empty object if series is empty', () => { expect(getSourceInfo({ diff --git a/assets/src/scripts/store/index.js b/assets/src/scripts/store/index.js index ff542a675e1b9e992108dd7a99307b715911fea1..e3b2062b76a0667a0223f8817af2721893c8c094 100644 --- a/assets/src/scripts/store/index.js +++ b/assets/src/scripts/store/index.js @@ -200,19 +200,19 @@ export const Actions = { ); }; }, - retrieveExtendedTimeSeries(site, period) { + retrieveExtendedTimeSeries(site, period, paramCd=null) { return function(dispatch, getState) { const state = getState(); - const parmCd = getCurrentParmCd(state); - const requestKey = getTsRequestKey ('current', period, parmCd)(state); + const thisParamCd = paramCd ? paramCd : getCurrentParmCd(state); + const requestKey = getTsRequestKey ('current', period, thisParamCd)(state); dispatch(Actions.setCurrentDateRange(period)); - if (!hasTimeSeries('current', period, parmCd)(state)) { + if (!hasTimeSeries('current', period, thisParamCd)(state)) { dispatch(Actions.addTimeSeriesLoading([requestKey])); const endTime = getRequestTimeRange('current', 'P7D')(state).end; const startTime = calcStartTime(period, endTime); return getTimeSeries({ sites: [site], - params: [parmCd], + params: [thisParamCd], startDate: startTime, endDate: endTime }).then( @@ -221,10 +221,9 @@ export const Actions = { dispatch(Actions.retrieveCompareTimeSeries(site, period, startTime, endTime)); dispatch(Actions.addSeriesCollection(requestKey, collection)); dispatch(Actions.removeTimeSeriesLoading([requestKey])); - dispatch(Actions.toggleTimeSeries('median', true)); }, () => { - console.log(`Unable to fetch data for period ${period} and parameter code ${parmCd}`); + console.log(`Unable to fetch data for period ${period} and parameter code ${thisParamCd}`); dispatch(Actions.addSeriesCollection(requestKey, {})); dispatch(Actions.removeTimeSeriesLoading([requestKey])); } diff --git a/graph-server/src/renderer/index.js b/graph-server/src/renderer/index.js index 11349885454fe23093641e91a51b218c90984b69..46a7a8ad481f6271b4c69c3377a9c5a46bb9f8cb 100644 --- a/graph-server/src/renderer/index.js +++ b/graph-server/src/renderer/index.js @@ -9,7 +9,7 @@ const renderToResponse = function (res, {siteID, parameterCode, compare, period, console.log(`Using static root ${STATIC_ROOT}`); const componentOptions = { siteno: siteID, - parameter: parameterCode, + parameterCode: parameterCode, compare: compare, period: period, startDT: startDT,