diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 015ddfbe78cafc73d7ac07ddd4ffe5cc96aa3aee..ea39355327ada560864a747e3bb9ae933dd03216 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -72,6 +72,7 @@ javascript_test: - assets/package.json - assets/package-lock.json - "assets/src/scripts/**/*.js" + - "assets/src/scripts/**/*.vue" - if: '$CI_COMMIT_BRANCH == "main"' when: on_success diff --git a/CHANGELOG.md b/CHANGELOG.md index c0c0a8b4fb77d77abce58e04db7b2631f77f8913..972c7e6fea6b93af5f89ee4b2301fb36c59dba36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,18 +5,29 @@ 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-1.2.0...master) +### Added +- Daily statistical data will now appear under the parameter selection list. + +### Changed +- Data download component was converted to Vue. +- Parameter selection table component was converted to Vue. + + ## [1.2.0](https://github.com/usgs/waterdataui/compare/waterdataui-1.1.0...waterdataui-1.2.0) - 2022-06-10 ### Added - Feature toggle added was added for Affiliated Networks accordion. +- Daily statistical data will now appear under the parameter selection list. ### Changed - Links related to NLDI and Flood mapper now use more descriptive wording. - When fetching active sites, precision was dropped to 3 from 4 on the bounding box to all the cached response to be returned with small movements on the map. +- Data download component was converted to Vue. +- Graph controls compone was converted to Vue. ## [1.1.0](https://github.com/usgs/waterdataui/compare/waterdataui-1.0.0...waterdataui-1.1.0) - 2022-06-06 ### Added - Added a download option in the hydrograph data table section. -- The map on monitoring location pages now includes a scale +- The map on monitoring location pages now includes a scale. ### Fixed - Updated a few links in the footer and adding title attributes to the social media icons to meet accessibility requirements. diff --git a/assets/src/scripts/mock-service-data.js b/assets/src/scripts/mock-service-data.js index e165697907f37b6ca447c59486852b0af2df7699..185871eb8f9b5c088eff17ed631f9548d658dcd8 100644 --- a/assets/src/scripts/mock-service-data.js +++ b/assets/src/scripts/mock-service-data.js @@ -254,6 +254,7 @@ USGS 05370000 00010 153885 1 12 1969 2017 49 15 USGS 05370000 00010 153885 1 13 1969 2017 49 15 `; + export const MOCK_IV_DATA = ` {"name" : "ns1:timeSeriesResponseType", "declaredType" : "org.cuahsi.waterml.TimeSeriesResponseType", diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/DataTablesApp.vue b/assets/src/scripts/monitoring-location/components/hydrograph/DataTablesApp.vue index 10c84cd289ef5a1068ea34ef8089d7101374bad2..01371fcbc9178b123635f9187208a20ea5f20a23 100644 --- a/assets/src/scripts/monitoring-location/components/hydrograph/DataTablesApp.vue +++ b/assets/src/scripts/monitoring-location/components/hydrograph/DataTablesApp.vue @@ -1,7 +1,5 @@ <template> - <div class="data-table-container"> - <DataTable /> - </div> + <DataTable /> </template> <script> diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/DownloadDataApp.vue b/assets/src/scripts/monitoring-location/components/hydrograph/DownloadDataApp.vue new file mode 100644 index 0000000000000000000000000000000000000000..2c2b004e6a55ef0f0e7bfba78baac76a78d7aeef --- /dev/null +++ b/assets/src/scripts/monitoring-location/components/hydrograph/DownloadDataApp.vue @@ -0,0 +1,14 @@ +<template> + <DownloadData /> +</template> + +<script> +import DownloadData from './vue-components/download-data.vue'; + +export default { + name: 'DownloadDataApp', + components: { + DownloadData + } +}; +</script> diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/ParameterSelectionApp.vue b/assets/src/scripts/monitoring-location/components/hydrograph/ParameterSelectionApp.vue new file mode 100644 index 0000000000000000000000000000000000000000..174af5dcd96fd99b9a197f57707a1a424058a02e --- /dev/null +++ b/assets/src/scripts/monitoring-location/components/hydrograph/ParameterSelectionApp.vue @@ -0,0 +1,26 @@ +<template> + <ParameterSelection v-if="parameters.length" /> +</template> + +<script> +import {useState} from 'redux-connect-vue'; + +import {getAvailableParameters} from './selectors/parameter-data'; + +import ParameterSelection from './vue-components/parameter-selection.vue'; +export default { + name: 'ParameterSelectionApp', + components: { + ParameterSelection + }, + setup() { + const state = useState({ + parameters: getAvailableParameters + }); + + return { + ...state + }; + } +}; +</script> diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/data-table.js b/assets/src/scripts/monitoring-location/components/hydrograph/data-table.js deleted file mode 100644 index 9a1473f0afcaeefd0f3ecf5d42eab3e93b1bece5..0000000000000000000000000000000000000000 --- a/assets/src/scripts/monitoring-location/components/hydrograph/data-table.js +++ /dev/null @@ -1,58 +0,0 @@ - -import {bindActionCreators} from 'redux'; -import ReduxConnectVue from 'redux-connect-vue'; -import {createStructuredSelector} from 'reselect'; -import {createApp} from 'vue'; - -import config from 'ui/config.js'; - -import DataTablesApp from './DataTablesApp.vue'; -import {drawDownloadForm} from './download-data'; - -/* - * Create the hydrograph data tables section which will update when the data - * displayed on the hydrograph changes. The section includes a button where the user - * can request that the data be downloaded - * @param {D3 selection} elem - * @param {Redux store} store - * @param {String} siteno - * @param {String} agencyCd - */ -export const drawDataTables = function(elem, store, siteno, agencyCd) { - const dataTablesApp = createApp(DataTablesApp, {}); - - elem.append('div') - .attr('id', 'iv-hydrograph-data-table-container'); - dataTablesApp.use(ReduxConnectVue, { - store, - mapDispatchToPropsFactory: (actionCreators) => (dispatch) => bindActionCreators(actionCreators, dispatch), - mapStateToPropsFactory: createStructuredSelector - }); - dataTablesApp.provide('store', store); - dataTablesApp.mount('#iv-hydrograph-data-table-container'); - - var button = elem.append('button') - .attr('id', 'download-graph-data-container-data-table-toggle') - .attr('class', 'usa-button') - .attr('aria-controls', 'download-graph-data-container-data-table') - .attr('ga-on', 'click') - .attr('ga-event-category', 'select-action') - .attr('ga-event-action', 'download-graph-data-container-data-table-toggle') - .on('click', function() { - const downloadContainer = elem.select('#download-graph-data-container-data-table'); - downloadContainer.attr('hidden', downloadContainer.attr('hidden') ? null : true); - }); - button.append('svg') - .attr('class', 'usa-icon') - .attr('aria-hidden', 'true') - .attr('role', 'img') - .html(`<use xlink:href="${config.STATIC_URL}img/sprite.svg#file_download"></use>`); - button.append('span').html('Retrieve data'); - - elem.append('div') - .attr('id', 'download-graph-data-container-data-table') - .attr('class', 'download-graph-data-container') - .attr('hidden', 'true') - .call(drawDownloadForm, store, siteno, agencyCd, 'data-table'); - -}; \ No newline at end of file diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/data-table.test.js b/assets/src/scripts/monitoring-location/components/hydrograph/data-table.test.js deleted file mode 100644 index 1fec9a09a3d6d48c06db340ee9d10154454f2140..0000000000000000000000000000000000000000 --- a/assets/src/scripts/monitoring-location/components/hydrograph/data-table.test.js +++ /dev/null @@ -1,148 +0,0 @@ -import {select} from 'd3-selection'; - -import config from 'ui/config'; - -import {configureStore} from 'ml/store'; - -import {drawDataTables} from './data-table'; -import {TEST_PRIMARY_IV_DATA, TEST_GW_LEVELS} from './mock-hydrograph-state'; - -describe('monitoring-location/components/hydrograph/data-table', () => { - let testDiv; - let store; - - config.locationTimeZone = 'America/Chicago'; - - beforeEach(() => { - testDiv = select('body').append('div'); - }); - - afterEach(() => { - testDiv.remove(); - }); - - it('Shows table with expected data and download section', () => { - store = configureStore({ - hydrographData: { - currentTimeRange: { - start: 1582560000000, - end: 1600620000000 - }, - primaryIVData: TEST_PRIMARY_IV_DATA - }, - groundwaterLevelData: { - all: [TEST_GW_LEVELS] - }, - hydrographState: { - selectedParameterCode: '72019', - selectedIVMethodID: '90649' - } - }); - drawDataTables(testDiv, store, '11112222', 'USGS'); - - const ivTable = testDiv.select('#iv-table-container').select('table'); - expect(ivTable.select('caption').text()).toBe('Instantaneous value data'); - expect(ivTable.selectAll('tr').size()).toBe(10); - const gwTable = testDiv.select('#gw-table-container').select('table'); - expect(gwTable.select('caption').text()).toBe('Field visit data'); - expect(gwTable.selectAll('tr').size()).toBe(5); - const downloadSection = testDiv.select('#download-graph-data-container-data-table'); - expect(downloadSection.selectAll('input[type="radio"]').size()).toBe(3); - }); - - it('Shows single IV table if no GW levels', () => { - store = configureStore({ - hydrographData: { - currentTimeRange: { - start: 1582560900000, - end: 1600619400000 - }, - primaryIVData: TEST_PRIMARY_IV_DATA - }, - groundwaterLevelData: { - all: [] - }, - hydrographState: { - selectedParameterCode: '72019', - selectedIVMethodID: '90649' - } - }); - drawDataTables(testDiv, store); - - expect(testDiv.select('#iv-table-container').style('display')).not.toBe('none'); - expect(testDiv.select('#gw-table-container').style('display')).toBe('none'); - }); - - it('Shows single GW table if no IV data', () => { - store = configureStore({ - hydrographData: { - currentTimeRange: { - start: 1582560900000, - end: 1600619400000 - } - }, - groundwaterLevelData: { - all: [TEST_GW_LEVELS] - }, - hydrographState: { - selectedParameterCode: '72019' - } - }); - drawDataTables(testDiv, store); - - expect(testDiv.select('#iv-table-container').style('display')).toBe('none'); - expect(testDiv.select('#gw-table-container').style('display')).not.toBe('none'); - }); - - it('Clicking the download button shows the download container', () => { - store = configureStore({ - hydrographData: { - currentTimeRange: { - start: 1582560000000, - end: 1600620000000 - }, - primaryIVData: TEST_PRIMARY_IV_DATA - }, - groundwaterLevelData: { - all: [TEST_GW_LEVELS] - }, - hydrographState: { - selectedParameterCode: '72019', - selectedIVMethodID: '90649' - } - }); - drawDataTables(testDiv, store, '11112222', 'USGS'); - - const downloadButton = testDiv.select('#download-graph-data-container-data-table-toggle'); - expect(downloadButton.size()).toBe(1); - - downloadButton.dispatch('click'); - expect(testDiv.select('#download-graph-data-container-data-table').attr('hidden')).toBeNull(); - }); - - it('Clicking the download button twice hides the download container', () => { - store = configureStore({ - hydrographData: { - currentTimeRange: { - start: 1582560000000, - end: 1600620000000 - }, - primaryIVData: TEST_PRIMARY_IV_DATA - }, - groundwaterLevelData: { - all: [TEST_GW_LEVELS] - }, - hydrographState: { - selectedParameterCode: '72019', - selectedIVMethodID: '90649' - } - }); - drawDataTables(testDiv, store, '11112222', 'USGS'); - - const downloadButton = testDiv.select('#download-graph-data-container-data-table-toggle'); - downloadButton.dispatch('click'); - downloadButton.dispatch('click'); - - expect(testDiv.select('#download-graph-data-container-data-table').attr('hidden')).not.toBeNull(); - }); -}); diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/download-data.js b/assets/src/scripts/monitoring-location/components/hydrograph/download-data.js deleted file mode 100644 index 3c25953bad81b749ff480c98d1766e951ac5ae5f..0000000000000000000000000000000000000000 --- a/assets/src/scripts/monitoring-location/components/hydrograph/download-data.js +++ /dev/null @@ -1,219 +0,0 @@ -/** - * Module with functions for processing and structuring download link URLs - */ - -import {DateTime} from 'luxon'; -import {createStructuredSelector} from 'reselect'; - -import config from 'ui/config.js'; -import {link} from 'ui/lib/d3-redux'; - -import {getIVServiceURL, getSiteMetaDataServiceURL} from 'ui/web-services/instantaneous-values'; -import {getStatisticsServiceURL} from 'ui/web-services/statistics-data'; -import {getGroundwaterServiceURL} from 'ui/web-services/groundwater-levels'; - -import {drawErrorAlert} from 'd3render/alerts'; - -import {isCalculatedTemperature} from 'ml/parameter-code-utils'; -import {getTimeRange} from 'ml/selectors/hydrograph-data-selector'; - -import {hasVisibleIVData, hasVisibleMedianStatisticsData, hasVisibleGroundwaterLevels, getPrimaryParameter} from './selectors/time-series-data'; - -const INFO_TEXT = ` -<div> -<div> - A separate tab will open with the requested data. -</div> -<div> - All data is in - <a href="https://waterdata.usgs.gov/nwis/?tab_delimited_format_info" target="_blank">RDB</a> format. -</div> -<div> - Data is retrieved from <a href="https://waterservices.usgs.gov" target="_blank">USGS Water Data Services.</a> -</div> -<div> - If you are an R user, use the - <a href="https://usgs-r.github.io/dataRetrieval/" target="_blank">USGS dataRetrieval package</a> to - download, analyze and plot your data -</div> -</div> -`; - -/* - * Helper function to return a ISO formated string in the location's time zone for the epoch time, inMillis - */ -const toISO = function(inMillis) { - return DateTime.fromMillis(inMillis, {zone: config.locationTimeZone}).toISO(); -}; - -/* - * Helper functions to return the appropriate service URL using information in the Redux store - */ -const getIVDataURL = function(store, siteno, timeRangeKind) { - const currentState = store.getState(); - const timeRange = getTimeRange(timeRangeKind)(currentState); - - return getIVServiceURL({ - siteno, - parameterCode: getPrimaryParameter(currentState).parameterCode, - startTime: toISO(timeRange.start), - endTime: toISO(timeRange.end), - format: 'rdb' - }); -}; -const getMedianDataURL = function(store, siteno) { - return getStatisticsServiceURL({ - siteno, - parameterCode: getPrimaryParameter(store.getState()).parameterCode, - statType: 'median', - format: 'rdb' - }); -}; -const getGroundwaterLevelURL = function(siteno, agencyCd) { - return getGroundwaterServiceURL({ - siteno, - agencyCd, - format: 'json' - }); -}; -const getSiteMetaDataURL = function(siteno) { - return getSiteMetaDataServiceURL({ - siteno, - isExpanded: true - }); -}; - -/* - * Helper function to render a single checkbox in container. - */ -const drawRadioButton = function(container, inputID, label, value, nameSuffix) { - const radioContainer = container.append('div') - .attr('class', 'usa-radio'); - radioContainer.append('input') - .attr('class', 'usa-radio__input') - .attr('id', inputID) - .attr('type', 'radio') - .attr('name', `retrieve-value-${nameSuffix}`) - .attr('value', value); - radioContainer.append('label') - .attr('class', 'usa-radio__label') - .attr('for', inputID) - .text(label); -}; - -/* - * Helper function to remove existing checkboxes and then render the checkboxes for the visible data. - */ -const drawRadioButtons = function(container, { - hasVisiblePrimaryIVData, - hasVisibleCompareIVData, - hasVisibleMedianData, - hasVisibleGroundwaterLevels, - primaryParameter, - uniqueId -}) { - container.select('.download-radio-container').remove(); - const radioContainer = container.append('div') - .attr('class', 'download-radio-container'); - if (primaryParameter && !isCalculatedTemperature(primaryParameter.parameterCode)) { - if (hasVisiblePrimaryIVData) { - radioContainer.call(drawRadioButton, `download-primary-iv-data-${uniqueId}`, 'Current time-series data', 'primary', uniqueId); - } - if (hasVisibleCompareIVData) { - radioContainer.call(drawRadioButton, `download-compare-iv-data-${uniqueId}`, 'Prior year time-series data', 'compare', uniqueId); - } - if (hasVisibleMedianData) { - radioContainer.call(drawRadioButton, `download-median-data-${uniqueId}`, 'Median', 'median', uniqueId); - } - if (hasVisibleGroundwaterLevels) { - radioContainer.call(drawRadioButton, `download-field-visits-${uniqueId}`, 'Field visits', 'groundwater-levels', uniqueId); - } - } - radioContainer.call(drawRadioButton, `download-site-meta-data-${uniqueId}`, 'About this location', 'site', uniqueId); -}; - -/* - * Render the download form and set up all appropriate even handlers. The checkboxes drawn are tied to what data is currently - * visible on the hydrograph. - * @param {D3 selection} container - * @param {Redux store} store - * @param {String} siteno - * @param {String} agencyCd - * @param {String} uniqueId - */ -export const drawDownloadForm = function(container, store, siteno, agencyCd, uniqueId) { - const downloadContainer = container.append('div'); - const formContainer = downloadContainer.append('div') - .attr('class', 'usa-form') - .append('fieldset') - .attr('class', 'usa-fieldset'); - formContainer.append('legend') - .attr('class', 'usa-legend') - .text('Select data to be downloaded'); - formContainer.append('div') - .attr('class', 'select-data-input-container') - .call(link(store, drawRadioButtons, createStructuredSelector({ - hasVisiblePrimaryIVData: hasVisibleIVData('primary'), - hasVisibleCompareIVData: hasVisibleIVData('compare'), - hasVisibleMedianData: hasVisibleMedianStatisticsData, - hasVisibleGroundwaterLevels: hasVisibleGroundwaterLevels, - primaryParameter: getPrimaryParameter, - uniqueId: () => { - return uniqueId; - } - }))); - - const downloadButton = formContainer.append('button') - .attr('class', 'usa-button download-selected-data') - .attr('ga-on', 'click') - .attr('ga-event-category', 'download-selected-data') - .attr('ga-event-action', 'download') - .on('click', function() { - formContainer.selectAll('.alert-error-container').remove(); - const radioInput = formContainer.select('input[type="radio"]:checked'); - if (radioInput.size()) { - let downloadUrl; - let dataType = radioInput.attr('value'); - switch (dataType) { - case 'primary': - downloadUrl = getIVDataURL(store, siteno, 'current'); - break; - case 'compare': - downloadUrl = getIVDataURL(store, siteno, 'prioryear'); - break; - case 'median': - downloadUrl = getMedianDataURL(store, siteno); - break; - case 'groundwater-levels': - downloadUrl = getGroundwaterLevelURL(siteno, agencyCd); - break; - case 'site': - downloadUrl = getSiteMetaDataURL(siteno); - break; - default: - console.log(`Unhandled value for downloading data: ${dataType}`); - } - if (downloadUrl) { - window.open(downloadUrl, '_blank'); - } - - } else { - formContainer.insert('div', 'button') - .attr('class', 'alert-error-container') - .call(drawErrorAlert, { - body:'Please select one of the radio buttons', - useSlim: true - }); - } - }); - downloadButton.append('svg') - .attr('class', 'usa-icon') - .attr('aria-hidden', true) - .attr('role', 'img') - .html(`<use xlink:href="${config.STATIC_URL}img/sprite.svg#file_download"></use>`); - downloadButton.append('span').text('Retrieve'); - - downloadContainer.append('div') - .attr('class', 'download-info') - .html(INFO_TEXT); -}; diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/download-data.test.js b/assets/src/scripts/monitoring-location/components/hydrograph/download-data.test.js deleted file mode 100644 index fe02b2f2630e8e116ea7f572e1bc1e87a9e35164..0000000000000000000000000000000000000000 --- a/assets/src/scripts/monitoring-location/components/hydrograph/download-data.test.js +++ /dev/null @@ -1,180 +0,0 @@ -import {select} from 'd3-selection'; - -import config from 'ui/config'; - -import {configureStore} from 'ml/store'; -import {setCompareDataVisibility, setMedianDataVisibility} from 'ml/store/hydrograph-state'; - -import {drawDownloadForm} from './download-data'; -import {TEST_CURRENT_TIME_RANGE, TEST_PRIMARY_IV_DATA, TEST_MEDIAN_DATA, TEST_GW_LEVELS} from './mock-hydrograph-state'; - - -describe('monitoring-location/components/hydrograph/download-data', () => { - config.SITE_DATA_ENDPOINT = 'https://fakeserviceroot.com/nwis/site'; - config.IV_DATA_ENDPOINT = 'https://fakeserviceroot.com/nwis/iv'; - config.HISTORICAL_IV_DATA_ENDPOINT = 'https://fakeserviceroot-more-than-120-days.com/nwis/iv'; - config.STATISTICS_ENDPOINT = 'https://fakeserviceroot.com/nwis/stat'; - config.GROUNDWATER_LEVELS_ENDPOINT = 'https://fakegroundwater.org/gwlevels/'; - config.locationTimeZone = 'America/Chicago'; - - const TEST_STATE = { - hydrographData: { - currentTimeRange: TEST_CURRENT_TIME_RANGE, - prioryearTimeRange: TEST_CURRENT_TIME_RANGE, - primaryIVData: TEST_PRIMARY_IV_DATA, - compareIVData: TEST_PRIMARY_IV_DATA, - medianStatisticsData: TEST_MEDIAN_DATA - }, - groundwaterLevelData: { - all: [TEST_GW_LEVELS] - }, - hydrographState: { - showCompareIVData: false, - showMedianData: false, - selectedIVMethodID: '90649', - selectedParameterCode: '72019' - } - }; - - describe('drawDownloadForm', () => { - let div; - let store; - let windowSpy; - - beforeEach(() => { - div = select('body').append('div'); - store = configureStore(TEST_STATE); - windowSpy = jest.spyOn(window, 'open').mockImplementation(() => null); - drawDownloadForm(div, store, '11112222', 'USGS'); - }); - - afterEach(() => { - div.remove(); - }); - - it('Renders form with the appropriate radio buttons and download button', () => { - expect(div.selectAll('input[type="radio"]').size()).toBe(3); - expect(div.selectAll('input[value="primary"]').size()).toBe(1); - expect(div.selectAll('input[value="groundwater-levels"]').size()).toBe(1); - expect(div.selectAll('input[value="site"]').size()).toBe(1); - expect(div.selectAll('button.download-selected-data').size()).toBe(1); - }); - - it('Rerenders the radio buttons if data visibility changes', () => { - store.dispatch(setCompareDataVisibility(true)); - store.dispatch(setMedianDataVisibility(true)); - return new Promise(resolve => { - window.requestAnimationFrame(() => { - expect(div.selectAll('input[type="radio"]').size()).toBe(5); - expect(div.selectAll('input[value="compare"]').size()).toBe(1); - expect(div.selectAll('input[value="median"]').size()).toBe(1); - resolve(); - }); - }); - }); - - it('Shows an error message if the download button is clicked with no radio buttons checked', () => { - const downloadButton = div.select('button.download-selected-data'); - downloadButton.dispatch('click'); - expect(div.select('.usa-alert--error').size()).toBe(1); - }); - - it('Opens a window with the URL for the selected data', () => { - const downloadButton = div.select('button.download-selected-data'); - div.select('input[value="site"]').property('checked', true); - downloadButton.dispatch('click'); - - expect(div.select('.usa-alert--error').size()).toBe(0); - expect(windowSpy.mock.calls).toHaveLength(1); - expect(windowSpy.mock.calls[0][0]).toContain('/site/'); - expect(windowSpy.mock.calls[0][0]).toContain('sites=11112222'); - }); - - it('Opens a window for each selected data', () => { - const downloadButton = div.select('button.download-selected-data'); - store.dispatch(setMedianDataVisibility(true)); - return new Promise(resolve => { - window.requestAnimationFrame(() => { - div.select('input[value="primary"]').property('checked', true); - downloadButton.dispatch('click'); - expect(windowSpy.mock.calls[0][0]).toContain('/iv/'); - expect(windowSpy.mock.calls[0][0]).toContain('sites=11112222'); - expect(windowSpy.mock.calls[0][0]).toContain('parameterCd=72019'); - expect(windowSpy.mock.calls[0][0]).toContain('startDT=2020-02-24T10:15:00.000-06:00'); - expect(windowSpy.mock.calls[0][0]).toContain('endDT=2020-09-20T11:45:00.000-05:00'); - - div.select('input[value="median"]').property('checked', true); - downloadButton.dispatch('click'); - expect(windowSpy.mock.calls[1][0]).toContain('/stat/'); - expect(windowSpy.mock.calls[1][0]).toContain('statTypeCd=median'); - expect(windowSpy.mock.calls[1][0]).toContain('parameterCd=72019'); - - div.select('input[value="groundwater-levels"]').property('checked', true); - downloadButton.dispatch('click'); - expect(windowSpy.mock.calls[2][0]).toContain('/gwlevels/'); - expect(windowSpy.mock.calls[2][0]).toContain('featureId=USGS-11112222'); - - resolve(); - }); - }); - }); - - it('Expects the error alert to disappear once a user checks a box and clicks download', () => { - const downloadButton = div.select('button.download-selected-data'); - downloadButton.dispatch('click'); - div.select('input[value="site"]').property('checked', true); - downloadButton.dispatch('click'); - - expect(div.select('.usa-alert--error').size()).toBe(0); - }); - }); - - describe('Tests for calculated primary parameter', () => { - const TEST_STATE_TWO = { - hydrographData: { - currentTimeRange: TEST_CURRENT_TIME_RANGE, - primaryIVData: { - parameter: { - parameterCode: '00010F' - }, - values: { - '11111': { - points: [{value: '26.0', qualifiers: ['A'], dateTime: 1582560900000}], - method: { - methodID: '11111' - } - } - } - } - }, - groundwaterLevelData: { - all: [] - }, - hydrographState: { - showCompareIVData: false, - showMedianData: false, - selectedMethodID: '1111', - selectedParameterCode: '72019' - } - }; - - let div; - let store; - beforeEach(() => { - div = select('body').append('div'); - store = configureStore(TEST_STATE_TWO); - drawDownloadForm(div, store, '11112222'); - }); - - afterEach(() => { - div.remove(); - }); - - it('Expect to render only the site radio button', () => { - expect(div.selectAll('input[type="radio"]').size()).toBe(1); - expect(div.selectAll('input[value="primary"]').size()).toBe(0); - expect(div.selectAll('input[value="groundwater-levels"]').size()).toBe(0); - expect(div.selectAll('input[value="site"]').size()).toBe(1); - }); - }); -}); diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/index.js b/assets/src/scripts/monitoring-location/components/hydrograph/index.js index 0e9c3f46fe2f94a9ffe0642b67de66f7fbdc01e4..f071b9ce21fa501a76d13e219b6820e92a730928 100644 --- a/assets/src/scripts/monitoring-location/components/hydrograph/index.js +++ b/assets/src/scripts/monitoring-location/components/hydrograph/index.js @@ -9,6 +9,8 @@ import {createApp} from 'vue'; import config from 'ui/config.js'; +import {link} from 'ui/lib/d3-redux'; + import {drawInfoAlert} from 'd3render/alerts'; import {renderTimeSeriesUrlParams} from 'ml/url-params'; @@ -24,19 +26,25 @@ import {setSelectedParameterCode, setCompareDataVisibility, setSelectedTimeSpan, import {Actions as floodDataActions} from 'ml/store/flood-data'; -import {getPreferredIVMethodID} from './selectors/time-series-data'; +import {getPreferredIVMethodID, getTitle} from './selectors/time-series-data'; +import {latestSelectedParameterValue} from 'ml/selectors/hydrograph-parameters-selector'; +import {getDailyStatistics} from 'ml/components/hydrograph/selectors/statistics'; import {showDataIndicators} from './data-indicator'; -import {drawDataTables} from './data-table'; import {initializeGraphBrush, drawGraphBrush} from './graph-brush'; import {drawTimeSeriesLegend} from './legend'; -import {drawSelectionList} from './parameters'; import {drawSelectActions} from './select-actions'; +import {drawStatsTable} from './statistics-table'; + import {initializeTimeSeriesGraph, drawTimeSeriesGraphData} from './time-series-graph'; import {initializeTooltipCursorSlider, drawTooltipCursorSlider} from './tooltip'; +import DataTablesApp from './DataTablesApp.vue'; import GraphControlsApp from './GraphControlsApp.vue'; import TimeSpanShortcutsApp from './TimeSpanShortcutsApp.vue'; +import ParameterSelectionApp from './ParameterSelectionApp.vue'; + +/* eslint-disable vue/one-component-per-file */ /* * Renders the hydrograph on the node element using the Redux store for state information. The siteno, latitude, and @@ -91,7 +99,7 @@ export const attachToNode = function(store, // Fetch all data needed to render the hydrograph const fetchHydrographDataPromise = store.dispatch(retrieveHydrographData(siteno, agencyCd, - getInputsForRetrieval(store.getState()))); + getInputsForRetrieval(store.getState()), true)); let fetchDataPromises = [fetchHydrographDataPromise]; // if showing only graph make a call to retrieve all of the groundwater level data. Otherwise @@ -139,6 +147,7 @@ export const attachToNode = function(store, const legendControlsContainer = graphContainer.append('div') .classed('ts-legend-controls-container', true); if (!showOnlyGraph) { + // eslint-disable-next-line vue/one-component-per-file const graphControlsApp = createApp(GraphControlsApp, {}); graphControlsApp.use(ReduxConnectVue, { store, @@ -149,8 +158,16 @@ export const attachToNode = function(store, graphControlsApp.provide('siteno', siteno); graphControlsApp.mount('.ts-legend-controls-container'); - nodeElem.select('.select-time-series-container') - .call(drawSelectionList, store, siteno, agencyCd); + const parameterSelectionApp = createApp(ParameterSelectionApp, {}); + parameterSelectionApp.use(ReduxConnectVue, { + store, + mapDispatchToPropsFactory: (actionCreators) => (dispatch) => bindActionCreators(actionCreators, dispatch), + mapStateToPropsFactory: createStructuredSelector + }); + parameterSelectionApp.provide('store', store); + parameterSelectionApp.provide('siteno', siteno); + parameterSelectionApp.provide('agencyCode', agencyCd); + parameterSelectionApp.mount('.select-time-series-container'); } // Once hydrograph data has been fetched, render the time series data. @@ -171,10 +188,29 @@ export const attachToNode = function(store, legendControlsContainer.call(drawTimeSeriesLegend, store); if (!thisShowOnlyGraph) { - nodeElem.select('#iv-data-table-container') - .call(drawDataTables, store, siteno, agencyCd); + // eslint-disable-next-line vue/one-component-per-file + const dataTablesApp = createApp(DataTablesApp, {}); + dataTablesApp.use(ReduxConnectVue, { + store, + mapDispatchToPropsFactory: (actionCreators) => (dispatch) => bindActionCreators(actionCreators, dispatch), + mapStateToPropsFactory: createStructuredSelector + }); + dataTablesApp.provide('store', store); + // data for DownLoadData component + dataTablesApp.provide('siteno', siteno); + dataTablesApp.provide('agencyCd', agencyCd); + dataTablesApp.provide('buttonSetName', 'data-tables-set'); + + dataTablesApp.mount('#iv-data-table-container'); + renderTimeSeriesUrlParams(store); + nodeElem.select('.daily-statistical-data') + .call(link(store, drawStatsTable, createStructuredSelector({ + statsData: getDailyStatistics(new Date), + latestValue: latestSelectedParameterValue, + parameterName: getTitle + }))); } }) .catch(reason => { diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/index.test.js b/assets/src/scripts/monitoring-location/components/hydrograph/index.test.js index 0a320d94bb32144c2fe2afe96cde8a475abe45e5..a53d2ab7985827987a692f60ee62d2c102c301ec 100644 --- a/assets/src/scripts/monitoring-location/components/hydrograph/index.test.js +++ b/assets/src/scripts/monitoring-location/components/hydrograph/index.test.js @@ -5,18 +5,20 @@ import mockConsole from 'jest-mock-console'; import * as utils from 'ui/utils'; import config from 'ui/config'; +import * as d3Redux from 'ui/lib/d3-redux'; import {configureStore} from 'ml/store'; import {Actions as floodDataActions} from 'ml/store/flood-data'; import * as groundwaterLevelData from 'ml/store/groundwater-level-field-visits'; import * as hydrographData from 'ml/store/hydrograph-data'; import * as hydrographParameters from 'ml/store/hydrograph-parameters'; +import * as hydrographParameterSelectors from 'ml/selectors/hydrograph-parameters-selector'; import {attachToNode} from './index'; import { TEST_CURRENT_TIME_RANGE, TEST_GW_LEVELS, - TEST_HYDROGRAPH_PARAMETERS, TEST_MEDIAN_DATA, + TEST_HYDROGRAPH_PARAMETERS, TEST_STATS_DATA, TEST_PRIMARY_IV_DATA } from './mock-hydrograph-state'; @@ -66,6 +68,8 @@ describe('monitoring-location/components/hydrograph module', () => { let restoreConsole; + let linkSpy; + beforeAll(() => { enableFetchMocks(); restoreConsole = mockConsole(); @@ -92,6 +96,7 @@ describe('monitoring-location/components/hydrograph module', () => { component.append('div').attr('class', 'select-actions-container'); component.append('div').attr('class', 'select-time-series-container'); component.append('div').attr('id', 'iv-data-table-container'); + component.append('div').attr('class', 'daily-statistical-data'); graphNode = document.getElementById('hydrograph'); nodeElem = select(graphNode); @@ -140,9 +145,10 @@ describe('monitoring-location/components/hydrograph module', () => { period: 'P7D', startTime: null, endTime: null, - loadCompare: false, - loadMedian: false - }); + loadCompare: false + }, + true + ); expect(store.getState().hydrographState).toEqual({ selectedParameterCode: '72019', selectedTimeSpan: 'P7D', @@ -161,9 +167,10 @@ describe('monitoring-location/components/hydrograph module', () => { period: 'P45D', startTime: null, endTime: null, - loadCompare: false, - loadMedian: false - }); + loadCompare: false + }, + true + ); expect(store.getState().hydrographState).toEqual({ selectedParameterCode: '72019', selectedTimeSpan: 'P45D', @@ -183,9 +190,10 @@ describe('monitoring-location/components/hydrograph module', () => { period: null, startTime: '2020-02-01T00:00:00.000-06:00', endTime: '2020-02-15T23:59:59.999-06:00', - loadCompare: false, - loadMedian: false - }); + loadCompare: false + }, + true + ); expect(store.getState().hydrographState).toEqual({ selectedParameterCode: '72019', selectedTimeSpan: { @@ -207,9 +215,10 @@ describe('monitoring-location/components/hydrograph module', () => { period: 'P7D', startTime: null, endTime: null, - loadCompare: true, - loadMedian: false - }); + loadCompare: true + }, + true + ); expect(store.getState().hydrographState).toEqual({ selectedParameterCode: '72019', selectedTimeSpan: 'P7D', @@ -240,9 +249,10 @@ describe('monitoring-location/components/hydrograph module', () => { period: 'P7D', startTime: null, endTime: null, - loadCompare: false, - loadMedian: false - }); + loadCompare: false + }, + true + ); expect(store.getState().hydrographState).toEqual({ selectedParameterCode: '72019', selectedTimeSpan: 'P7D', @@ -260,7 +270,7 @@ describe('monitoring-location/components/hydrograph module', () => { hydrographData: { primaryIVData: TEST_PRIMARY_IV_DATA, currentTimeRange: TEST_CURRENT_TIME_RANGE, - medianStatisticsData: TEST_MEDIAN_DATA + statisticsData: TEST_STATS_DATA }, groundwaterLevelData: { all: [TEST_GW_LEVELS] @@ -275,6 +285,7 @@ describe('monitoring-location/components/hydrograph module', () => { ui: { width: 1000 } + }); hydrographData.retrieveHydrographData = jest.fn(() => { @@ -287,10 +298,22 @@ describe('monitoring-location/components/hydrograph module', () => { return Promise.resolve(); }; }); + hydrographParameterSelectors.latestSelectedParameterValue = jest.fn().mockReturnValue(() => { + return function() { + return 123; + }; + }); + linkSpy = jest.spyOn(d3Redux, 'link'); attachToNode(store, graphNode, { ...INITIAL_PARAMETERS, showOnlyGraph: false }); + jest.useFakeTimers('modern'); + jest.setSystemTime(new Date(2020, 0, 1)); + }); + + afterEach(() => { + jest.useRealTimers(); }); it('loading indicator should be hidden', () => { @@ -340,23 +363,27 @@ describe('monitoring-location/components/hydrograph module', () => { expect(selectAll('#change-time-span-container').size()).toBe(1); }); - it('should have two download data forms', () => { + it('should have one download container added with d3 (the vue component will not show in test)', () => { expect(selectAll('#download-graph-data-container-select-actions').size()).toBe(1); - expect(selectAll('#download-graph-data-container-data-table').size()).toBe(1); }); it('should have method select element', () => { - expect(selectAll('#ts-method-select-container').size()).toBe(1); + expect(selectAll('.method-picker-container').size()).toBe(1); }); it('should have the select time series element', () => { - expect(selectAll('#select-time-series').size()).toBe(1); + expect(selectAll('.main-parameter-selection-container').size()).toBe(1); }); it('should have data tables for hydrograph data', () => { expect(select('#iv-table-container').size()).toBe(1); expect(select('#gw-table-container').size()).toBe(1); }); + + it('expects to create a d3redux link for the daily statistics section', () => { + expect(linkSpy).toHaveBeenCalled(); + expect(linkSpy.mock.lastCall.toString()).toContain('drawStatsTable'); + }); }); describe('Tests for rendering once fetching is complete when showOnlyGraph is true', () => { @@ -366,7 +393,7 @@ describe('monitoring-location/components/hydrograph module', () => { hydrographData: { primaryIVData: TEST_PRIMARY_IV_DATA, currentTimeRange: TEST_CURRENT_TIME_RANGE, - medianStatisticsData: TEST_MEDIAN_DATA + statisticsData: TEST_STATS_DATA }, groundwaterLevelData: { all: [TEST_GW_LEVELS] @@ -399,6 +426,7 @@ describe('monitoring-location/components/hydrograph module', () => { return Promise.resolve(); }; }); + linkSpy = jest.spyOn(d3Redux, 'link'); attachToNode(store, graphNode, { ...INITIAL_PARAMETERS, showOnlyGraph: true @@ -463,8 +491,12 @@ describe('monitoring-location/components/hydrograph module', () => { }); it('should not have data tables for hydrograph data', () => { - expect(select('#iv-hydrograph-data-table-container').size()).toBe(0); - expect(select('#gw-hydrograph-data-table-container').size()).toBe(0); + expect(select('#iv-table-container').size()).toBe(0); + expect(select('#gw-table-container').size()).toBe(0); + }); + + it('expects to not create a d3redux link for the daily statistics section', () => { + expect(linkSpy.mock.lastCall.toString()).not.toContain('drawStatsTable'); }); }); }); diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/mock-hydrograph-state.js b/assets/src/scripts/monitoring-location/components/hydrograph/mock-hydrograph-state.js index 7e5934441ee80cff3946e09d203de98ec226fa80..56dca08b4810cf16ddbb89eeaaeb049346635e33 100644 --- a/assets/src/scripts/monitoring-location/components/hydrograph/mock-hydrograph-state.js +++ b/assets/src/scripts/monitoring-location/components/hydrograph/mock-hydrograph-state.js @@ -2,6 +2,12 @@ export const TEST_CURRENT_TIME_RANGE = { start: 1582560900000, end: 1600620300000 }; + +export const TEST_COMPARE_TIME_RANGE = { + start: 1339621064000, + end: 1371157064000 +}; + export const TEST_PRIMARY_IV_DATA = { parameter: { parameterCode: '72019', @@ -37,14 +43,41 @@ export const TEST_PRIMARY_IV_DATA = { } }; -export const TEST_MEDIAN_DATA = { +export const TEST_STATS_DATA = { '153885': [ - {month_nu: 2, day_nu: 24, p50_va: 16, ts_id: '153885', loc_web_ds: 'Method1', begin_yr: '2011', end_yr: '2020'}, - {month_nu: 2, day_nu: 25, p50_va: 16.2, ts_id: '153885', loc_web_ds: 'Method1', begin_yr: '2011', end_yr: '2020'}, - {month_nu: 2, day_nu: 26, p50_va: 15.9, ts_id: '153885', loc_web_ds: 'Method1', begin_yr: '2011', end_yr: '2020'}, - {month_nu: 2, day_nu: 27, p50_va: 16.3, ts_id: '153885', loc_web_ds: 'Method1', begin_yr: '2011', end_yr: '2020'}, - {month_nu: 2, day_nu: 28, p50_va: 16.4, ts_id: '153885', loc_web_ds: 'Method1', begin_yr: '2011', end_yr: '2020'} - ] + {month_nu: 2, day_nu: 24, p50_va: 16, ts_id: '153885', loc_web_ds: 'Method1', begin_yr: '2011', end_yr: '2020', max_va_yr: '2020', + max_va: '273', min_va_yr: '2006', min_va: '55.5', mean_va: '153', p05_va: '', p10_va: '61', p20_va: '88', p25_va: '100', + p75_va: '224', p80_va: '264', p90_va: '271', p95_va: ''}, + {month_nu: 2, day_nu: 25, p50_va: 16.2, ts_id: '153885', loc_web_ds: 'Method1', begin_yr: '2011', end_yr: '2020', max_va_yr: '2020', + max_va: '273', min_va_yr: '2006', min_va: '55.5', mean_va: '153', p05_va: '', p10_va: '61', p20_va: '88', p25_va: '100', + p75_va: '224', p80_va: '264', p90_va: '271', p95_va: ''}, + {month_nu: 2, day_nu: 26, p50_va: 15.9, ts_id: '153885', loc_web_ds: 'Method1', begin_yr: '2011', end_yr: '2020', max_va_yr: '2020', + max_va: '273', min_va_yr: '2006', min_va: '55.5', mean_va: '153', p05_va: '', p10_va: '61', p20_va: '88', p25_va: '100', + p75_va: '224', p80_va: '264', p90_va: '271', p95_va: ''}, + {month_nu: 2, day_nu: 27, p50_va: 16.3, ts_id: '153885', loc_web_ds: 'Method1', begin_yr: '2011', end_yr: '2020', max_va_yr: '2020', + max_va: '273', min_va_yr: '2006', min_va: '55.5', mean_va: '153', p05_va: '', p10_va: '61', p20_va: '88', p25_va: '100', + p75_va: '224', p80_va: '264', p90_va: '271', p95_va: ''}, + {month_nu: 2, day_nu: 28, p50_va: 16.4, ts_id: '153885', loc_web_ds: 'Method1', begin_yr: '2011', end_yr: '2020', max_va_yr: '2020', + max_va: '273', min_va_yr: '2006', min_va: '55.5', mean_va: '153', p05_va: '', p10_va: '61', p20_va: '88', p25_va: '100', + p75_va: '224', p80_va: '264', p90_va: '271', p95_va: ''} + ], + '90649': [ + {month_nu: 2, day_nu: 24, p50_va: 16, ts_id: '153885', loc_web_ds: 'Method1', begin_yr: '2011', end_yr: '2020', max_va_yr: '2020', + max_va: '273', min_va_yr: '2006', min_va: '55.5', mean_va: '153', p05_va: '', p10_va: '61', p20_va: '88', p25_va: '100', + p75_va: '224', p80_va: '264', p90_va: '271', p95_va: ''}, + {month_nu: 2, day_nu: 25, p50_va: 16.2, ts_id: '153885', loc_web_ds: 'Method1', begin_yr: '2011', end_yr: '2020', max_va_yr: '2020', + max_va: '273', min_va_yr: '2006', min_va: '55.5', mean_va: '153', p05_va: '', p10_va: '61', p20_va: '88', p25_va: '100', + p75_va: '224', p80_va: '264', p90_va: '271', p95_va: ''}, + {month_nu: 2, day_nu: 26, p50_va: 15.9, ts_id: '153885', loc_web_ds: 'Method1', begin_yr: '2011', end_yr: '2020', max_va_yr: '2020', + max_va: '273', min_va_yr: '2006', min_va: '55.5', mean_va: '153', p05_va: '', p10_va: '61', p20_va: '88', p25_va: '100', + p75_va: '224', p80_va: '264', p90_va: '271', p95_va: ''}, + {month_nu: 2, day_nu: 27, p50_va: 16.3, ts_id: '153885', loc_web_ds: 'Method1', begin_yr: '2011', end_yr: '2020', max_va_yr: '2020', + max_va: '273', min_va_yr: '2006', min_va: '55.5', mean_va: '153', p05_va: '', p10_va: '61', p20_va: '88', p25_va: '100', + p75_va: '224', p80_va: '264', p90_va: '271', p95_va: ''}, + {month_nu: 2, day_nu: 28, p50_va: 16.4, ts_id: '153885', loc_web_ds: 'Method1', begin_yr: '2011', end_yr: '2020', max_va_yr: '2020', + max_va: '273', min_va_yr: '2006', min_va: '55.5', mean_va: '153', p05_va: '', p10_va: '61', p20_va: '88', p25_va: '100', + p75_va: '224', p80_va: '264', p90_va: '271', p95_va: ''} + ] }; export const TEST_GW_LEVELS = { @@ -92,21 +125,24 @@ export const TEST_HYDROGRAPH_PARAMETERS = { name: 'Streamflow, ft3/s', description: 'Discharge, cubic feet per second', unit: 'ft3/s', - hasIVData: true + hasIVData: true, + latestValue: '123' }, '00010': { parameterCode: '00010', name: 'Temperature, water, C', description: 'Temperature, water, degrees Celsius', unit: 'deg C', - hasIVData: true + hasIVData: true, + latestValue: '123' }, '00010F': { parameterCode: '00010F', name: 'Temperature, water, F', description: 'Temperature, water, degrees Fahrenheit', unit: 'deg F', - hasIVData: true + hasIVData: true, + latestValue: '123' }, '72019': { parameterCode: '72019', @@ -114,13 +150,15 @@ export const TEST_HYDROGRAPH_PARAMETERS = { description: 'Depth to water level, feet below land surface', unit: 'ft', hasIVData: true, - hasGWLevelsData: true + hasGWLevelsData: true, + latestValue: '123' }, '62610': { parameterCode: '62610', name: 'Groundwater level above NGVD 1929, feet', description: 'Groundwater level above NGVD 1929, feet', unit: 'ft', - hasGWLevelsData: true + hasGWLevelsData: true, + latestValue: '123' } }; diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/parameters.js b/assets/src/scripts/monitoring-location/components/hydrograph/parameters.js deleted file mode 100644 index 393f2f710f2223a63b2e166d4e79fa50695415ff..0000000000000000000000000000000000000000 --- a/assets/src/scripts/monitoring-location/components/hydrograph/parameters.js +++ /dev/null @@ -1,314 +0,0 @@ -import {select} from 'd3-selection'; - -import config from 'ui/config'; -import {link} from 'ui/lib/d3-redux'; - -import {getInputsForRetrieval, getSelectedParameterCode} from 'ml/selectors/hydrograph-state-selector'; - -import {setSelectedParameterCode, setSelectedIVMethodID} from 'ml/store/hydrograph-state'; -import {retrieveHydrographData} from 'ml/store/hydrograph-data'; - -import {getAvailableParameters} from './selectors/parameter-data'; -import {getSortedIVMethods} from './selectors/time-series-data'; - -import {showDataIndicators} from './data-indicator'; -import {drawMethodPicker} from './method-picker'; - -const ROW_TOGGLE = { - more: { - class: 'expansion-toggle expansion-toggle-more', - sprite: `${config.STATIC_URL}/img/sprite.svg#expand_more` - }, - less: { - class: 'expansion-toggle expansion-toggle-less', - sprite: `${config.STATIC_URL}/img/sprite.svg#expand_less` - } -}; - -const ROW_TYPES = ['desktop', 'mobile']; - -/* - * Sets the visibility of the expansion rows in selection to isExpanded. - * @param {D3 selection} selection - Can represent multiple expansion rows - * @param {Boolean} isExpanded - */ -const setExpansionRowVisibility = function(selection, isExpanded) { - selection.attr('hidden', isExpanded ? null : 'true'); -}; - -/* - * Sets the state of the expansion toggle - * @param {D3 selection} selection - Can represent multiple expansion toggles - * @param {Boolean} isExpanded - */ -const setExpansionToggleState = function(selection, isExpanded) { - const toggleToUse = isExpanded ? ROW_TOGGLE.less : ROW_TOGGLE.more; - selection - .attr('class', toggleToUse.class) - .attr('aria-expanded', isExpanded ? 'true' : 'false') - .each(function() { - select(this).select('svg') - .attr('aria-hidden', isExpanded ? 'false' : 'true') - .select('use') - .attr('xlink:href', toggleToUse.sprite); - }); -}; - -/* -* Helper function that adds the on click open and close functionality. Stopping event propagation is needed to prevent -* clicks on the containing element from changing this elements behavior. -* @param {D3 selection} container - the element to add the on click action -* @param {Object} parameter - Contains details about the current parameter code -* @param {String} type - Either 'desktop' or 'mobile'--indicates at what screen size the controls will show. -*/ -const drawRowExpansionControl = function(container, parameter, type) { - // Don't show the open/close controls, if there is nothing in the expansion row, such as WaterAlert links. - if (parameter.waterAlert.hasWaterAlert) { - const expansionToggle = container - .append('span') - .attr('id', `expansion-toggle-${type}-${parameter.parameterCode}`); - expansionToggle.append('svg') - .attr('class', 'usa-icon') - .append('use').attr('xlink:href', ROW_TOGGLE.more.sprite); - setExpansionToggleState(expansionToggle, false); - expansionToggle.on('click', function(event) { - // Stop clicks on the toggle from triggering a change in selected parameter. - event.stopPropagation(); - // Hide the expansion container on all rows on all rows except the clicked parameter row. - select('#select-time-series').selectAll('.expansion-container-row') - .filter(function() { - return this.id !== `expansion-container-row-${parameter.parameterCode}`; - }) - .call(setExpansionRowVisibility, false); - select('#select-time-series').selectAll('.expansion-toggle ') - .filter(function() { - return !this.id.includes(parameter.parameterCode); - }) - - .call(setExpansionToggleState, false); - - // Allow the user to change the expansion row visibility on the clicked parameter row. Both - // expansion toggles need to be updated - const selectedRow = select(`#container-row-${parameter.parameterCode}`); - const thisExpansionRow = selectedRow.select('.expansion-container-row'); - const isExpanded = thisExpansionRow.attr('hidden') === 'true'; - setExpansionRowVisibility(thisExpansionRow, isExpanded); - ROW_TYPES.forEach(typeOfRow => { - select(`#expansion-toggle-${typeOfRow}-${parameter.parameterCode}`) - .call(setExpansionToggleState, isExpanded); - - }); - }); - } -}; - -/** - * Helper function that draws the main containing rows. Note the 'parameter selection' is a nested USWD grid. - * The grid has one 'container row' for each parameter (this function creates the 'container rows' for each parameter). - * As a side note - each container row will eventually contain three internal rows. - * 1) The 'Top Period Of Record Row' (shows only on mobile) - * 2) The 'Radio Button Row' (radio button and description show on mobile and desktop, toggle and period of record don't show mobile) - * 3) The 'Expansion Container Row' only shows when row clicked or toggled on. May act as a container for additional rows. - * @param {Object} container - The target element on which to append the row - * @param {Object} store - The application Redux state - * @param {String} siteno - A unique identifier for the monitoring location - * @param {String} agencyCode - Uniquely identifies the data provider, usually something like 'USGS' - * @param {String} parameterCode - five digit code that uniquely identifies the observed property (parameter) - * @param {String} parameterCode - USGS five digit parameter code -*/ -const drawContainingRow = function(container, store, siteno, agencyCode, parameterCode) { - return container.append('div') - .attr('id', `container-row-${parameterCode}`) - .attr('class', 'grid-row-container-row') - .attr('ga-on', 'click') - .attr('ga-event-category', 'selectTimeSeries') - .attr('ga-event-action', `time-series-parmcd-${parameterCode}`) - .call(link(store, (container, selectedParameterCode) => { - container.classed('selected', parameterCode === selectedParameterCode) - .attr('aria-selected', parameterCode === selectedParameterCode); - }, getSelectedParameterCode)) - .on('click', function(event) { - // Don't let clicks on the sampling method selections trigger a parameter reload. - event.stopPropagation(); - - // Get all of the other rows into the close orientation. - select('#select-time-series').selectAll('.expansion-container-row') - .call(setExpansionRowVisibility, false); - const expansionToggles = select('#select-time-series').selectAll('.expansion-toggle'); - expansionToggles - .call(setExpansionToggleState, false); - - // Show the clicked expansion row. - select(`#expansion-container-row-${parameterCode}`) - .call(setExpansionRowVisibility, true); - ROW_TYPES.forEach(typeOfIcon => { - select(`#expansion-toggle-${typeOfIcon}-${parameterCode}`) - .call(setExpansionToggleState, true); - }); - - // Change to the newly selected parameter both in the selection list ond on the graph. - const thisClass = select(this) - .attr('class'); - if (!thisClass || !thisClass.includes('selected')) { - store.dispatch(setSelectedParameterCode(parameterCode)); - showDataIndicators(true, store); - store.dispatch(retrieveHydrographData(siteno, agencyCode, getInputsForRetrieval(store.getState()))) - .then(() => { - const sortedMethods = getSortedIVMethods(store.getState()); - if (sortedMethods && sortedMethods.methods.length) { - store.dispatch(setSelectedIVMethodID(sortedMethods.methods[0].methodID)); - } - showDataIndicators(false, store); - }); - } - }); -}; - -/** - * Helper function that creates the top row of each parameter selection. This row is hidden except on narrow screens - * and contains the period of record that appears above the parameter description. - * @param {D3 selection} container - The target element to append the row - * @param {Object} parameter - Contains details about the current parameter code -*/ -const drawTopPeriodOfRecordRow = function(container, parameter) { - const gridRowInnerTopPeriodOfRecord = container.append('div') - .attr('class', 'grid-row-inner grid-row-period-of-record'); - gridRowInnerTopPeriodOfRecord.append('div') - .attr('class', 'grid-row-period-of-record-text') - .text(`${parameter.periodOfRecord.begin_date} to ${parameter.periodOfRecord.end_date}`); - const topPeriodOfRecordRowExpansionControlDiv = gridRowInnerTopPeriodOfRecord.append('div') - .attr('class', 'toggle-for-top-period-of-record'); - - drawRowExpansionControl(topPeriodOfRecordRowExpansionControlDiv, parameter, 'mobile'); -}; - -/** - * Helper function that draws the row containing the radio button and parameter description. - * @param {D3 selection} container - The target element to append the row - * @param {Object} parameter - Contains details about the current parameter code - * @param {Object} store - The application Redux state - */ -const drawRadioButtonRow = function(container, parameter, store) { - const gridRowInnerWithRadioButton = container.append('div') - .attr('class', 'grid-row grid-row-inner'); - const radioButtonDiv = gridRowInnerWithRadioButton.append('div') - .attr('class', 'radio-button__param-select') - .append('div') - .attr('class', 'usa-radio'); - radioButtonDiv.append('input') - .attr('class', 'usa-radio__input') - .attr('id', `radio-${parameter.parameterCode}`) - .attr('aria-labelledby', `radio-${parameter.parameterCode}-label`) - .attr('type', 'radio') - .attr('name', 'parameter-selection') - .attr('value', `${parameter.parameterCode}`) - .call(link(store, (inputElem, selectedParameterCode) => { - inputElem.property('checked', parameter.parameterCode === selectedParameterCode ? true : null); - }, getSelectedParameterCode)); - radioButtonDiv.append('label') - .attr('class', 'usa-radio__label'); - gridRowInnerWithRadioButton.append('div') - .attr('class', 'description__param-select') - .attr('id', `radio-${parameter.parameterCode}-label`) - .text(parameter.description); - const periodOfRecordToggleContainer = gridRowInnerWithRadioButton.append('div') - .attr('id', 'period-of-record-and-toggle-container') - .attr('class', 'period-of-record__param-select'); - periodOfRecordToggleContainer.append('div') - .attr('id', 'period-of-record-text') - .attr('class', 'period-of-record__param-select') - .text(`${parameter.periodOfRecord.begin_date} to ${parameter.periodOfRecord.end_date}`); - const radioRowExpansionControlDiv = periodOfRecordToggleContainer.append('div') - .attr('class', 'toggle-radio_button_row'); - drawRowExpansionControl(radioRowExpansionControlDiv, parameter, 'desktop'); -}; - -/** - * Helper function that draws a row containing the controls for the WaterAlert subscription. - * @param {Object} container- The target element to append the row - * @param {String} siteno - A unique identifier for the monitoring location - * @param {D3 selection} parameter - Contains details about the current parameter code - */ -const drawWaterAlertRow = function(container, siteno, parameter) { - // WaterAlert doesn't accept the calculated parameter for Fahrenheit (such as 00010F), so we need to adjust the - // parameter code back to the Celsius version (such as 00010). - const waterAlertURL = function() { - return parameter.parameterCode.includes(config.CALCULATED_TEMPERATURE_VARIABLE_CODE) ? - `${config.WATERALERT_SUBSCRIPTION}/?site_no=${siteno}&parm=${parameter.parameterCode.replace(config.CALCULATED_TEMPERATURE_VARIABLE_CODE, '')}` : - `${config.WATERALERT_SUBSCRIPTION}/?site_no=${siteno}&parm=${parameter.parameterCode}`; - }; - - const gridRowInnerWaterAlert = container.append('div') - .attr('class', 'grid-row grid-row-inner'); - - gridRowInnerWaterAlert.append('div') - .attr('id', `wateralert-row-${parameter.parameterCode}`) - .attr('class', 'wateralert-row') - .append('a') - .attr('href', waterAlertURL()) - .attr('target', '_blank') - .attr('class', 'water-alert-cell usa-tooltip') - .attr('data-position', 'left') - .attr('data-classes', 'width-full tablet:width-auto') - .attr('title', parameter.waterAlert.tooltipText) - .text(parameter.waterAlert.displayText); -}; - -/** - * A main function that creates the parameter selection list - * @param {Object} container - The target element to append the selection list - * @param {Object} store - The application Redux state - * @param {String} siteno - A unique identifier for the monitoring location - * @param {String} agencyCode - Usually an abbreviation that indicates the source of the data, most commonly 'USGS' -*/ -export const drawSelectionList = function(container, store, siteno, agencyCode) { - const parameters = getAvailableParameters(store.getState()); - const selectedParameter = getSelectedParameterCode(store.getState()); - - if (!Object.keys(parameters).length) { - return; - } - // Add the primary parameter selection container. - container.append('p') - .attr('id', 'parameter-selection-title') - .attr('class', 'usa-prose') - .text('Select Data to Graph'); - const selectionList = container.append('div') - .attr('id', 'select-time-series') - .attr('class', 'main-parameter-selection-container') - .call(link(store, (selectionList, parameters) => { - selectionList.selectAll('.grid-row-container-row').remove(); - - parameters.forEach(parameter => { - // Add the main grid rows - const containerRow = drawContainingRow(selectionList, store, siteno, agencyCode, parameter.parameterCode); - // Add the nested grid rows - drawTopPeriodOfRecordRow(containerRow, parameter); - drawRadioButtonRow(containerRow, parameter, store); - // Add the expansion container in nested grid - const expansionContainerRow = containerRow.append('div') - .attr('id', `expansion-container-row-${parameter.parameterCode}`) - .attr('class', 'expansion-container-row') - .attr('hidden', 'true'); - - // Add the rows nested in the expansion container - if (parameter.waterAlert.hasWaterAlert) { - drawWaterAlertRow(expansionContainerRow, siteno, parameter); - } - // Expand the selected raw so users can see the Water Alert link and other things like sampling methods - if (parameter.parameterCode === selectedParameter) { - select(`#expansion-container-row-${selectedParameter}`) - .call(setExpansionRowVisibility, true); - ROW_TYPES.forEach(typeOfIcon => { - select(`#expansion-toggle-${typeOfIcon}-${selectedParameter}`) - .call(setExpansionToggleState, true); - }); - } - }); - - }, getAvailableParameters)); - // Draw method picker. This can only appear in the selected parameter row because - // we only know the methods for the currently selected data. Let the code figure out - // whether to draw it and where to put it whenever the sorted IV Methods change. - selectionList.call(link(store, drawMethodPicker, getSortedIVMethods, store)); -}; diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/parameters.test.js b/assets/src/scripts/monitoring-location/components/hydrograph/parameters.test.js deleted file mode 100644 index 944d46564c7336d5fbd79026f578fafb06337505..0000000000000000000000000000000000000000 --- a/assets/src/scripts/monitoring-location/components/hydrograph/parameters.test.js +++ /dev/null @@ -1,220 +0,0 @@ -import {select} from 'd3-selection'; -import {enableFetchMocks, disableFetchMocks} from 'jest-fetch-mock'; -import mockConsole from 'jest-mock-console'; - -import config from 'ui/config'; -import * as utils from 'ui/utils'; - -import {configureStore} from 'ml/store'; -import * as hydrographData from 'ml/store/hydrograph-data'; - -import * as dataIndicator from './data-indicator'; -import {TEST_HYDROGRAPH_PARAMETERS} from './mock-hydrograph-state'; -import {drawSelectionList} from './parameters'; - -describe('monitoring-location/components/hydrograph/parameters module', () => { - utils.mediaQuery = jest.fn().mockReturnValue(true); - - const TEST_STATE = { - hydrographParameters: TEST_HYDROGRAPH_PARAMETERS, - hydrographState: { - selectedTimeSpan: 'P7D', - selectedParameterCode: '72019' - } - }; - - config.ivPeriodOfRecord = { - '00060': { - begin_date: '1980-01-01', - end_date: '2020-01-01' - }, - '72019': { - begin_date: '1980-04-01', - end_date: '2020-04-01' - }, - '00010': { - begin_date: '1981-04-01', - end_date: '2019-04-01' - } - - }; - config.gwPeriodOfRecord = { - '72019': { - begin_date: '1980-03-31', - end_date: '2020-03-31' - }, - '62610': { - begin_date: '1980-05-01', - end_date: '2020-05-01' - } - }; - - let div; - let store; - let retrieveHydrographDataSpy; - let showDataIndicatorSpy; - - let restoreConsole; - beforeAll(() => { - enableFetchMocks(); - restoreConsole = mockConsole(); - }); - - afterAll(() => { - disableFetchMocks(); - restoreConsole(); - }); - - beforeEach(() => { - div = select('body').append('div'); - retrieveHydrographDataSpy = jest.spyOn(hydrographData, 'retrieveHydrographData'); - showDataIndicatorSpy = jest.spyOn(dataIndicator, 'showDataIndicators'); - }); - - afterEach(() => { - div.remove(); - }); - - it('If no parameters defined the element is not rendered', () => { - store = configureStore({ - hydrographParameters: {} - }); - drawSelectionList(div, store, '11112222'); - expect(div.select('#select-time-series').size()).toBe(0); - }); - - it('Expects the selection list to be rendered with the appropriate rows and selection', () => { - store = configureStore(TEST_STATE); - drawSelectionList(div, store, '11112222'); - - const container = div.select('#select-time-series'); - expect(container.size()).toBe(1); - expect(container.selectAll('.grid-row-container-row').size()).toBe(5); - expect(container.selectAll('.expansion-toggle').size()).toBe(8); // Note - there are two for each parameter with expansion rows - expect(container.select('input:checked').attr('value')).toEqual('72019'); - }); - - it('Expects changing the selection retrieves hydrograph data', () => { - store = configureStore(TEST_STATE); - drawSelectionList(div, store, '11112222', 'USGS'); - - const rowOne = div.select('#container-row-00060'); - rowOne.dispatch('click'); - - expect(store.getState().hydrographState.selectedParameterCode).toEqual('00060'); - expect(showDataIndicatorSpy.mock.calls).toHaveLength(1); - expect(showDataIndicatorSpy.mock.calls[0][0]).toBe(true); - expect(retrieveHydrographDataSpy).toHaveBeenCalledWith('11112222', 'USGS', { - parameterCode: '00060', - period: 'P7D', - startTime: null, - endTime: null, - loadCompare: false, - loadMedian: false - }); - }); - - it('Expects that the row for the selected parameter will be open when the page loads', function() { - store = configureStore({ - ...TEST_STATE, - hydrographState: { - ...TEST_STATE.hydrographState, - selectedParameterCode: '00010' - } - }); - drawSelectionList(div, store, '11112222'); - - expect(select('#expansion-container-row-00010').attr('hidden')).toBe(null); - expect(select('#expansion-container-row-72019').attr('hidden')).toBe('true'); - }); - - it('Expects clicking on a row will expand and contract the correct rows', function() { - store = configureStore(TEST_STATE); - drawSelectionList(div, store, '11112222'); - - const firstTargetRow = div.select('#container-row-00010'); - const secondTargetRow = div.select('#container-row-72019'); - // When the page loads the selected parameter row will be open (in this case 72019) - expect(select('#expansion-container-row-00010').attr('hidden')).toBe('true'); - expect(select('#expansion-container-row-72019').attr('hidden')).toBe(null); - - firstTargetRow.dispatch('click'); - expect(select('#expansion-container-row-00010').attr('hidden')).toBe(null); - expect(select('#expansion-container-row-72019').attr('hidden')).toBe('true'); - - secondTargetRow.dispatch('click'); - expect(select('#expansion-container-row-00010').attr('hidden')).toBe('true'); - expect(select('#expansion-container-row-72019').attr('hidden')).toBe(null); - }); - - it('Expects clicking the row toggle will expand the correct row and set the toggle', function() { - store = configureStore(TEST_STATE); - drawSelectionList(div, store, '11112222'); - - const firstToggleTarget = div.select('#expansion-toggle-desktop-00010'); - const secondToggleTarget = div.select('#expansion-toggle-desktop-72019'); - // When the page loads the selected parameter row will be open (not hidden), and rest closed - expect(firstToggleTarget.attr('aria-expanded')).toBe('false'); - expect(select('#expansion-container-row-00010').attr('hidden')).toBe('true'); - expect(select('#expansion-container-row-72019').attr('hidden')).toBe(null); - - firstToggleTarget.dispatch('click'); - // Clicking the row for 00010 will expand that row and close others - expect(firstToggleTarget.attr('aria-expanded')).toBe('true'); - expect(select('#expansion-container-row-00010').attr('hidden')).toBe(null); - expect(select('#expansion-container-row-72019').attr('hidden')).toBe('true'); - - secondToggleTarget.dispatch('click'); - expect(secondToggleTarget.attr('aria-expanded')).toBe('true'); - expect(select('#expansion-container-row-00010').attr('hidden')).toBe('true'); - expect(select('#expansion-container-row-72019').attr('hidden')).toBe(null); - - secondToggleTarget.dispatch('click'); // click same target a second time - expect(secondToggleTarget.attr('aria-expanded')).toBe('false'); - expect(select('#expansion-container-row-00010').attr('hidden')).toBe('true'); - expect(select('#expansion-container-row-72019').attr('hidden')).toBe('true'); - }); - - it('Expects parameters listed in config as having a WaterAlert will have a link in parameter list', function() { - store = configureStore(TEST_STATE); - drawSelectionList(div, store, '11112222'); - - const container = div.select('#select-time-series'); - expect(container.select('#wateralert-row-00010').size()).toBe(1); - expect(container.select('#wateralert-row-00010').select('a').attr('href')).toContain('00010'); - expect(container.select('#wateralert-row-00010').select('a').attr('href')).not.toContain('00010F'); - }); - - it('Expects Celsius temperature parameters will have correct WaterAlert link in parameter list for the calculated version', function() { - // Note - WaterAlert only accepts the standard five digit USGS parameter code (such as 00010) not the calculated version like ('00010F'). - store = configureStore(TEST_STATE); - drawSelectionList(div, store, '11112222'); - - const container = div.select('#select-time-series'); - expect(container.select('#wateralert-row-00010F').size()).toBe(1); - expect(container.select('#wateralert-row-00010F').select('a').attr('href')).toContain('00010'); - expect(container.select('#wateralert-row-00010F').select('a').attr('href')).not.toContain('00010F'); - }); - - it('Expects that clicking a row toggle icon will set the icon to open and all others closed', function() { - store = configureStore(TEST_STATE); - drawSelectionList(div, store, '11112222'); - const container = div.select('#select-time-series'); - const clickedIconToggleOne = div.select('#expansion-toggle-desktop-72019'); - const clickedIconToggleTwo = div.select('#expansion-toggle-desktop-00010F'); - - // test that the selected parameter's row is expanded - expect(container.selectAll('.expansion-toggle-less').size()).toBe(2); - expect(container.selectAll('.expansion-toggle-more').size()).toBe(6); - - // test that a second click closes all previously selected icons - clickedIconToggleOne.dispatch('click'); - expect(container.selectAll('.expansion-toggle-less').size()).toBe(0); - expect(container.selectAll('.expansion-toggle-more').size()).toBe(8); - - // test that a click will open the row - clickedIconToggleTwo.dispatch('click'); - expect(container.selectAll('.expansion-toggle-less').size()).toBe(2); - expect(container.selectAll('.expansion-toggle-more').size()).toBe(6); - }); -}); diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/select-actions.js b/assets/src/scripts/monitoring-location/components/hydrograph/select-actions.js index effb745b40be2b17c0c7017d442cce7fb7b62a91..9d32fa493abc7b7387824200da7c21ba71b7c47d 100644 --- a/assets/src/scripts/monitoring-location/components/hydrograph/select-actions.js +++ b/assets/src/scripts/monitoring-location/components/hydrograph/select-actions.js @@ -1,9 +1,17 @@ + +import {bindActionCreators} from 'redux'; +import ReduxConnectVue from 'redux-connect-vue'; import {select} from 'd3-selection'; +import {createStructuredSelector} from 'reselect'; + +import {createApp} from 'vue'; import config from 'ui/config.js'; import {drawTimeSpanControls} from './time-span-controls'; -import {drawDownloadForm} from './download-data'; + +import DownloadDataApp from './DownloadDataApp.vue'; + /* * Helper function to render a select action button on listContainer @@ -56,6 +64,20 @@ const appendButton = function(listContainer, {uswdsIcon, buttonLabel, idOfDivToC * @param {String} siteno */ export const drawSelectActions = function(container, store, siteno, agencyCode) { + const addDownloadContainer = function() { + const downloadDataApp = createApp(DownloadDataApp, {}); + downloadDataApp.use(ReduxConnectVue, { + store, + mapDispatchToPropsFactory: (actionCreators) => (dispatch) => bindActionCreators(actionCreators, dispatch), + mapStateToPropsFactory: createStructuredSelector + }); + downloadDataApp.provide('store', store); + downloadDataApp.provide('siteno', siteno); + downloadDataApp.provide('agencyCd', agencyCode); + downloadDataApp.provide('buttonSetName', 'select-actions-set'); + downloadDataApp.mount('#download-graph-data-container-select-actions'); + }; + const listContainer = container.append('ul') .attr('class', 'select-actions-button-group usa-button-group'); if (config.ivPeriodOfRecord || config.gwPeriodOfRecord) { @@ -79,5 +101,5 @@ export const drawSelectActions = function(container, store, siteno, agencyCode) .attr('id', 'download-graph-data-container-select-actions') .attr('class', 'download-graph-data-container') .attr('hidden', true) - .call(drawDownloadForm, store, siteno, agencyCode, 'hydrograph'); + .call(addDownloadContainer); }; diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/select-actions.test.js b/assets/src/scripts/monitoring-location/components/hydrograph/select-actions.test.js index 32c90a4d38a12b836b8cd1d8b3d9401ac7de2480..b5640ce3b7fcc2de31166c411361a16929fcc1e4 100644 --- a/assets/src/scripts/monitoring-location/components/hydrograph/select-actions.test.js +++ b/assets/src/scripts/monitoring-location/components/hydrograph/select-actions.test.js @@ -4,7 +4,7 @@ import config from 'ui/config.js'; import {configureStore} from 'ml/store'; -import {TEST_PRIMARY_IV_DATA, TEST_MEDIAN_DATA, TEST_GW_LEVELS, TEST_CURRENT_TIME_RANGE} from './mock-hydrograph-state'; +import {TEST_PRIMARY_IV_DATA, TEST_STATS_DATA, TEST_GW_LEVELS, TEST_CURRENT_TIME_RANGE} from './mock-hydrograph-state'; import {drawSelectActions} from './select-actions'; describe('monitoring-location/components/hydrograph/select-actions', () => { @@ -18,7 +18,7 @@ describe('monitoring-location/components/hydrograph/select-actions', () => { hydrographData: { currentTimeRange: TEST_CURRENT_TIME_RANGE, primaryIVData: TEST_PRIMARY_IV_DATA, - medianStatistics: TEST_MEDIAN_DATA + medianStatistics: TEST_STATS_DATA }, groundwaterLevelData: { all: [TEST_GW_LEVELS] diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/selectors/domain.test.js b/assets/src/scripts/monitoring-location/components/hydrograph/selectors/domain.test.js index b0a5179c9d6a23fc10843c2cd576cdd3697d6d14..fbd2030b48f4842b13dd37becf3582b0b9eb5ad2 100644 --- a/assets/src/scripts/monitoring-location/components/hydrograph/selectors/domain.test.js +++ b/assets/src/scripts/monitoring-location/components/hydrograph/selectors/domain.test.js @@ -1,7 +1,7 @@ import { TEST_PRIMARY_IV_DATA, TEST_GW_LEVELS, - TEST_MEDIAN_DATA, + TEST_STATS_DATA, TEST_CURRENT_TIME_RANGE } from '../mock-hydrograph-state'; import { @@ -154,7 +154,7 @@ describe('monitoring-location/components/hydrograph/selectors/domain module', () hydrographData: { currentTimeRange: TEST_CURRENT_TIME_RANGE, primaryIVData: TEST_PRIMARY_IV_DATA, - medianStatisticsData: TEST_MEDIAN_DATA + statisticsData: TEST_STATS_DATA }, groundwaterLevelData: { all: [TEST_GW_LEVELS] diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/selectors/statistics.js b/assets/src/scripts/monitoring-location/components/hydrograph/selectors/statistics.js new file mode 100644 index 0000000000000000000000000000000000000000..15aa0b40717f7bdfc9b274cef329f69183883265 --- /dev/null +++ b/assets/src/scripts/monitoring-location/components/hydrograph/selectors/statistics.js @@ -0,0 +1,32 @@ +import {createSelector} from 'reselect'; +import {DateTime} from 'luxon'; +import memoize from 'fast-memoize'; + +import {getStatisticsData} from 'ml/selectors/hydrograph-data-selector'; + +/* + * Selector function that gets the statistics for a given day + * @param {Date} a Date object for the day to be fetched + * @returns {Object} + * + */ +export const getDailyStatistics = memoize( (date) => createSelector( + getStatisticsData, + (stats) => { + + if (!stats || !Object.keys(stats).length) { + return {}; + } + + const statsData = stats[Object.keys(stats)[0]]; + const date_obj = DateTime.fromJSDate(date); + + const isLeapYear = date_obj.isInLeapYear; + + const dayInYear = date_obj.ordinal; + // There is a decicated index in stats array for Feb 29 + if (!isLeapYear && date.getMonth() > 1) { + return statsData[dayInYear]; + } + return statsData[dayInYear - 1]; + })); \ No newline at end of file diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/selectors/statistics.test.js b/assets/src/scripts/monitoring-location/components/hydrograph/selectors/statistics.test.js new file mode 100644 index 0000000000000000000000000000000000000000..f5692cbba245235c8c9a9ebd6868f7d041bbe747 --- /dev/null +++ b/assets/src/scripts/monitoring-location/components/hydrograph/selectors/statistics.test.js @@ -0,0 +1,79 @@ +import config from 'ui/config'; + +import {getDailyStatistics} from './statistics'; + +import {TEST_STATS_DATA} from '../mock-hydrograph-state'; + +describe('monitoring-location/components/hydrograph/selectors/statistics-table', () => { + config.locationTimeZone = 'America/Chicago'; + + const TEST_STATE = { + hydrographData: { + statisticsData: TEST_STATS_DATA + } + }; + + describe('getDailyStatistics', () => { + it('Returns empty object if no stats data exists', () => { + expect(getDailyStatistics(new Date(2020, 0, 1))({ + ...TEST_STATE, + hydrographData: { + statisticsData: {} + } + + })).toStrictEqual({}); + }); + + it('Returns data object of correct day', () => { + expect(getDailyStatistics(new Date(2020, 0, 2))({ + ...TEST_STATE, + hydrographData: { + statisticsData: TEST_STATS_DATA + } + })).toStrictEqual({ + month_nu: 2, + day_nu: 25, + p50_va: 16.2, + ts_id: '153885', + loc_web_ds: 'Method1', + begin_yr: '2011', + end_yr: '2020', + max_va_yr: '2020', + max_va: '273', + min_va_yr: '2006', + min_va: '55.5', + mean_va: '153', + p05_va: '', + p10_va: '61', + p20_va: '88', + p25_va: '100', + p75_va: '224', + p80_va: '264', + p90_va: '271', + p95_va: ''}); + }); + + it('Returns array index of correct day on leap years', () => { + expect(getDailyStatistics(new Date(2020, 2, 1))({ + ...TEST_STATE, + hydrographData: { + statisticsData: { + '153885': Array.from(Array(65).keys()) + } + } + })).toStrictEqual(60); + }); + + it('Returns array index of correct day on non-leap years', () => { + expect(getDailyStatistics(new Date(2021, 2, 1))({ + ...TEST_STATE, + hydrographData: { + statisticsData: { + '153885': Array.from(Array(65).keys()) + } + } + })).toStrictEqual(60); + }); + }); + +}); \ No newline at end of file diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/selectors/time-series-data.js b/assets/src/scripts/monitoring-location/components/hydrograph/selectors/time-series-data.js index 32f314b1cca62008bf040d87c52aafc574c296a8..b28857c30f7029357fa4505c6f90ec4584d099a2 100644 --- a/assets/src/scripts/monitoring-location/components/hydrograph/selectors/time-series-data.js +++ b/assets/src/scripts/monitoring-location/components/hydrograph/selectors/time-series-data.js @@ -5,7 +5,7 @@ import {createSelector} from 'reselect'; import config from 'ui/config'; import {getSelectedGroundwaterLevels} from 'ml/selectors/groundwater-level-field-visits-selector'; -import {getIVData, getMedianStatisticsData, getPrimaryMethods, getIVPrimaryParameter, +import {getIVData, getStatisticsData, getPrimaryMethods, getIVPrimaryParameter, getTimeRange } from 'ml/selectors/hydrograph-data-selector'; import {getSelectedIVMethodID, isCompareIVDataVisible, isMedianDataVisible} from 'ml/selectors/hydrograph-state-selector'; @@ -66,7 +66,7 @@ export const hasVisibleIVData = memoize(dataKind => createSelector( export const hasVisibleMedianStatisticsData = createSelector( isVisible('median'), - getMedianStatisticsData, + getStatisticsData, (isVisible, medianStats) => isVisible && medianStats ? Object.keys(medianStats).length > 0 : false ); diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/selectors/time-series-data.test.js b/assets/src/scripts/monitoring-location/components/hydrograph/selectors/time-series-data.test.js index 96d2fb1fdd2038599949c6b0f9a0e75daef81a68..29bf496357224b13d2b46d13a68884d17394268a 100644 --- a/assets/src/scripts/monitoring-location/components/hydrograph/selectors/time-series-data.test.js +++ b/assets/src/scripts/monitoring-location/components/hydrograph/selectors/time-series-data.test.js @@ -1,6 +1,6 @@ import config from 'ui/config'; -import {TEST_PRIMARY_IV_DATA, TEST_MEDIAN_DATA, TEST_GW_LEVELS} from '../mock-hydrograph-state'; +import {TEST_PRIMARY_IV_DATA, TEST_STATS_DATA, TEST_GW_LEVELS} from '../mock-hydrograph-state'; import { isVisible, hasVisibleIVData, hasVisibleGroundwaterLevels, hasVisibleMedianStatisticsData, hasAnyVisibleData, getTitle, getDescription, getPrimaryParameterUnitCode, getPreferredIVMethodID, getSortedIVMethods, @@ -131,7 +131,7 @@ describe('monitoring-location/components/hydrograph/selectors/time-series-data m it('Return false if median data is available for not selected for display', () => { expect(hasVisibleMedianStatisticsData({ hydrographData: { - medianStatisticsData: TEST_MEDIAN_DATA + statisticsData: TEST_STATS_DATA }, hydrographState: { showMedianData: false @@ -142,7 +142,7 @@ describe('monitoring-location/components/hydrograph/selectors/time-series-data m it('Return false if no median data is available and but is selected for display', () => { expect(hasVisibleMedianStatisticsData({ hydrographData: { - medianStatisticsData: {} + statisticsData: {} }, hydrographState: { showMedianData: true @@ -153,7 +153,7 @@ describe('monitoring-location/components/hydrograph/selectors/time-series-data m it('return true if median data is available and it is selected for display', () => { expect(hasVisibleMedianStatisticsData({ hydrographData: { - medianStatisticsData: TEST_MEDIAN_DATA + statisticsData: TEST_STATS_DATA }, hydrographState: { showMedianData: true @@ -235,7 +235,7 @@ describe('monitoring-location/components/hydrograph/selectors/time-series-data m })).toBe(true); expect(hasAnyVisibleData({ hydrographData: { - medianStatisticsData: TEST_MEDIAN_DATA + statisticsData: TEST_STATS_DATA }, groundwaterLevelData: { all: [] @@ -319,7 +319,7 @@ describe('monitoring-location/components/hydrograph/selectors/time-series-data m })).toBe(false); expect(hasAnyVisibleData({ hydrographData: { - medianStatisticsData: TEST_MEDIAN_DATA + statisticsData: TEST_STATS_DATA }, groundwaterLevelData: { all: [] diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/statistics-table.js b/assets/src/scripts/monitoring-location/components/hydrograph/statistics-table.js new file mode 100644 index 0000000000000000000000000000000000000000..51a44a517a6e7a6cf0a66d1d729786b959364273 --- /dev/null +++ b/assets/src/scripts/monitoring-location/components/hydrograph/statistics-table.js @@ -0,0 +1,88 @@ +import {select} from 'd3-selection'; + +const COLUMN_HEADINGS = ['Latest Value', 'Lowest Value', '25th Percentile', 'Median', '75th Percentile', 'Mean', 'Highest Value']; +const DATA_HEADINGS = ['min_va', 'p25_va', 'p50_va', 'p75_va', 'mean_va', 'max_va']; + + +const drawTableBody = function(table, data) { + const body = table.append('tbody'); + const row = body.append('tr'); + COLUMN_HEADINGS.forEach((heading, i) => { + if (i === 0) { + row.append('th') + .attr('data-label', heading[i]) + .attr('scope', 'row') + .text(data[0]); + } else { + row.append('th') + .attr('data-label', heading[i]) + .attr('scope', 'row') + .text(data[1][DATA_HEADINGS[i - 1]]); + } + }); +}; + +const drawStats = function(elem, currentData, name) { + let myHeadings = COLUMN_HEADINGS.slice(0); + myHeadings[1] = `${COLUMN_HEADINGS[1]} (${currentData[1]['min_va_yr']})`; + myHeadings[myHeadings.length - 1] = `${COLUMN_HEADINGS[COLUMN_HEADINGS.length - 1]} (${currentData[1]['max_va_yr']})`; + const tableContainer = elem.append('div') + .attr('id', 'stats-table'); + + const table = tableContainer.append('table') + .classed('usa-table', true) + .classed('usa-table--stacked', true); + table.append('caption') + .text(`Daily ${name} for ${new Date().toDateString()} based on ${currentData[1]['count_nu']} + ${currentData[1]['count_nu'] === '1' ? 'year' : 'years'} of data.`); + table.append('thead') + .append('tr') + .selectAll('th') + .data(myHeadings) + .enter() + .append('th') + .attr('scope', 'col') + .text(col => col); + table.call(drawTableBody, currentData); +}; + +/* + * Create the hydrograph data daily statistics section + * @param {D3 selection} elem + * @param {Redux store subset} statisticsData object from the Redux store + * @param {String} latest reported value of the parameter + * @param {String} name of the parameter + */ +export const drawStatsTable = function(elem, {statsData, latestValue, parameterName}) { + if (!latestValue || !Object.keys(statsData).length) { + select('.stats-accordion') + .attr('hidden', ''); + return; + } + + elem.select('.stats-accordion') + .remove(); + + let accordion = elem.append('div') + .classed('wdfn-accordion', true) + .classed('usa-accordion', true) + .classed('stats-accordion', true); + let heading = accordion.append('h2') + .classed('usa-accordion__heading', true); + + heading.append('button') + .classed('usa-accordion__button', true) + .attr('aria-expanded', 'false') + .attr('aria-controls', 'daily-stats-table') + .attr('ga-on', 'click') + .attr('ga-event-category', 'accordion') + .attr('ga-event-action', 'interactionWithDailyStatisticsAccordion') + .text('Today\'s Statistical Data'); + + accordion.attr('hidden', null); + + accordion.append('div') + .attr('id', 'daily-stats-table') + .attr('hidden', '') + .call(drawStats, [latestValue, statsData], parameterName); +}; diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/statistics-table.test.js b/assets/src/scripts/monitoring-location/components/hydrograph/statistics-table.test.js new file mode 100644 index 0000000000000000000000000000000000000000..34eaf17aa54bc7528efbec910840da4f2c666ec4 --- /dev/null +++ b/assets/src/scripts/monitoring-location/components/hydrograph/statistics-table.test.js @@ -0,0 +1,125 @@ +import {select} from 'd3-selection'; + +import {configureStore} from 'ml/store'; + +import * as stats from './statistics-table'; +import {TEST_STATS_DATA} from './mock-hydrograph-state'; + +describe('monitoring-location/components/hydrograph/statistics', () => { + let testDiv; + let store; + + + beforeEach(() => { + testDiv = select('body').append('div').attr('id', 'daily-statistical-data'); + jest.useFakeTimers('modern'); + jest.setSystemTime(new Date(2020, 0, 1)); + }); + + afterEach(() => { + testDiv.remove(); + jest.useRealTimers(); + }); + + it('Creates the stats table elements', async() => { + store = configureStore({ + hydrographData: { + statisticsData: TEST_STATS_DATA + } + }); + await testDiv.call(stats.drawStatsTable, + {statsData: store.getState().hydrographData.statisticsData, latestValue: '11', parameterName: 'Test Name'}); + + expect(testDiv.select('caption').text()).toContain('Test Name'); + expect(testDiv.select('#daily-stats-table').size()).toBe(1); + expect(testDiv.select('.stats-accordion').size()).toBe(1); + }); + + it('Does not create the table if there is no stats data', async() => { + store = configureStore({ + hydrographData: { + statisticsData: TEST_STATS_DATA + } + }); + await testDiv.call(stats.drawStatsTable, + {statsData: {}, latestValue: '11', parameterName: 'Test Name'}); + + expect(testDiv.select('#daily-stats-table').size()).toBe(0); + expect(testDiv.select('.stats-accordion').size()).toBe(0); + }); + + it('Does not create the table if there is no latestValue available', async() => { + store = configureStore({ + hydrographData: { + statisticsData: TEST_STATS_DATA + } + }); + await testDiv.call(stats.drawStatsTable, + {statsData: store.getState().hydrographData.statisticsData, latestValue: '', parameterName: 'Test Name'}); + + expect(testDiv.select('#daily-stats-table').size()).toBe(0); + expect(testDiv.select('.stats-accordion').size()).toBe(0); + }); + + it('Does not create the accordion if there is no stats data', async() => { + store = configureStore({ + hydrographData: { + statisticsData: TEST_STATS_DATA + } + }); + await testDiv.call(stats.drawStatsTable, + {statsData: {}, latestValue: '11', parameterName: 'Test Name'}); + + expect(testDiv.selectAll('.stats-accordion').size()).toBe(0); + }); + + it('Does not create the accordion if there is no latestValue', async() => { + store = configureStore({ + hydrographData: { + statisticsData: TEST_STATS_DATA + } + }); + await testDiv.call(stats.drawStatsTable, + {statsData: store.getState().hydrographData.statisticsData, latestValue: '', parameterName: 'Test Name'}); + + expect(testDiv.selectAll('.stats-accordion').size()).toBe(0); + }); + + it('Expects the table to have headers', async() => { + store = configureStore({ + hydrographData: { + statisticsData: TEST_STATS_DATA + } + }); + await testDiv.call(stats.drawStatsTable, + {statsData: store.getState().hydrographData.statisticsData['153885'][0], latestValue: '11', parameterName: 'Test Name'}); + let tableHeaders = testDiv.select('thead').select('tr').selectAll('tr > th'); + expect(tableHeaders.size()).toBe(7); + expect(tableHeaders.nodes()[0].textContent).toBe('Latest Value'); + expect(tableHeaders.nodes()[1].textContent).toBe('Lowest Value (2006)'); + expect(tableHeaders.nodes()[2].textContent).toBe('25th Percentile'); + expect(tableHeaders.nodes()[3].textContent).toBe('Median'); + expect(tableHeaders.nodes()[4].textContent).toBe('75th Percentile'); + expect(tableHeaders.nodes()[5].textContent).toBe('Mean'); + expect(tableHeaders.nodes()[6].textContent).toBe('Highest Value (2020)'); + }); + + it('Expects the table to have data in it', async() => { + store = configureStore({ + hydrographData: { + statisticsData: TEST_STATS_DATA + } + }); + await testDiv.call(stats.drawStatsTable, + {statsData: store.getState().hydrographData.statisticsData['153885'][0], latestValue: '25.9', parameterName: 'Test Name'}); + let tableHeaders = testDiv.select('tbody').select('tr').selectAll('tr > th'); + expect(tableHeaders.size()).toBe(7); + expect(tableHeaders.nodes()[0].textContent).toBe('25.9'); + expect(tableHeaders.nodes()[1].textContent).toBe('55.5'); + expect(tableHeaders.nodes()[2].textContent).toBe('100'); + expect(tableHeaders.nodes()[3].textContent).toBe('16'); + expect(tableHeaders.nodes()[4].textContent).toBe('224'); + expect(tableHeaders.nodes()[5].textContent).toBe('153'); + expect(tableHeaders.nodes()[6].textContent).toBe('273'); + }); +}); diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/time-series-graph.test.js b/assets/src/scripts/monitoring-location/components/hydrograph/time-series-graph.test.js index 44a349f643674abb177c2a9e68d4d36b1d4f65fe..6242c6177a1da64df09e82937a43ba8abeaea236 100644 --- a/assets/src/scripts/monitoring-location/components/hydrograph/time-series-graph.test.js +++ b/assets/src/scripts/monitoring-location/components/hydrograph/time-series-graph.test.js @@ -5,7 +5,7 @@ import * as utils from 'ui/utils'; import {configureStore} from 'ml/store'; import {setMedianDataVisibility} from 'ml/store/hydrograph-state'; -import {TEST_PRIMARY_IV_DATA, TEST_GW_LEVELS, TEST_MEDIAN_DATA, +import {TEST_PRIMARY_IV_DATA, TEST_GW_LEVELS, TEST_STATS_DATA, TEST_CURRENT_TIME_RANGE } from './mock-hydrograph-state'; import {drawTimeSeriesGraphData, initializeTimeSeriesGraph} from './time-series-graph'; @@ -14,7 +14,7 @@ import {drawTimeSeriesGraphData, initializeTimeSeriesGraph} from './time-series- const TEST_STATE = { hydrographData: { primaryIVData: TEST_PRIMARY_IV_DATA, - medianStatisticsData: TEST_MEDIAN_DATA, + statisticsData: TEST_STATS_DATA, currentTimeRange: TEST_CURRENT_TIME_RANGE }, groundwaterLevelData: { diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/time-span-controls.test.js b/assets/src/scripts/monitoring-location/components/hydrograph/time-span-controls.test.js index ab7225e470a6d9e4d27aae89bc333a4632f99b1a..767933727169e4e3707de019d246502e852396ec 100644 --- a/assets/src/scripts/monitoring-location/components/hydrograph/time-span-controls.test.js +++ b/assets/src/scripts/monitoring-location/components/hydrograph/time-span-controls.test.js @@ -161,8 +161,7 @@ describe('monitoring-location/components/hydrograph/time-span-controls', () => { period: null, startTime: '2020-02-05T00:00:00.000-06:00', endTime: '2020-02-28T23:59:59.999-06:00', - loadCompare: false, - loadMedian: false + loadCompare: false }); expect(showDataIndicatorSpy).toHaveBeenCalled(); expect(getSelectedTimeSpan(state)).toEqual({ @@ -250,8 +249,7 @@ describe('monitoring-location/components/hydrograph/time-span-controls', () => { period: null, startTime: '2020-01-01T00:00:00.000-06:00', endTime: '2020-01-05T23:59:59.999-06:00', - loadCompare: false, - loadMedian: false + loadCompare: false }); expect(showDataIndicatorSpy).toHaveBeenCalled(); expect(getSelectedTimeSpan(state)).toEqual({ @@ -306,8 +304,7 @@ describe('monitoring-location/components/hydrograph/time-span-controls', () => { period: 'P45D', startTime: null, endTime: null, - loadCompare: false, - loadMedian: false + loadCompare: false }); expect(showDataIndicatorSpy).toHaveBeenCalled(); expect(getSelectedTimeSpan(state)).toEqual('P45D'); diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/vue-components/data-table.test.js b/assets/src/scripts/monitoring-location/components/hydrograph/vue-components/data-table.test.js index d9b5e9c6a5506f6a468b71a0fb42801f57e09f45..9a18e0ad2aff9411631b9e10af47a6460b7428a9 100644 --- a/assets/src/scripts/monitoring-location/components/hydrograph/vue-components/data-table.test.js +++ b/assets/src/scripts/monitoring-location/components/hydrograph/vue-components/data-table.test.js @@ -10,6 +10,7 @@ import {configureStore} from 'ml/store'; import {TEST_PRIMARY_IV_DATA, TEST_GW_LEVELS} from '../mock-hydrograph-state'; import DataTable from './data-table.vue'; +import DownloadData from './download-data.vue'; describe('monitoring-location/components/hydrograph/components/data-table.vue', () => { let store; @@ -126,4 +127,49 @@ describe('monitoring-location/components/hydrograph/components/data-table.vue', expect(wrapper.find('#iv-table-container').isVisible()).toBe(false); expect(wrapper.find('#gw-table-container').isVisible()).toBe(true); }); + + it('Shows only data retrieve button and download container', async() => { + store = configureStore({ + hydrographData: { + currentTimeRange: { + start: 1582560000000, + end: 1600620000000 + }, + primaryIVData: TEST_PRIMARY_IV_DATA + }, + groundwaterLevelData: { + all: [] + }, + hydrographState: { + selectedParameterCode: '72019', + selectedIVMethodID: '90649' + } + }); + + wrapper = mount(DataTable, { + global: { + plugins: [ + [ReduxConnectVue, { + store, + mapDispatchToPropsFactory: (actionCreators) => (dispatch) => bindActionCreators(actionCreators, dispatch), + mapStateToPropsFactory: createStructuredSelector + }] + ], + provide: { + store: store, + siteno: '11112222', + agencyCd: 'USGS', + buttonSetName: 'test-buttons' + } + } + }); + + expect(wrapper.findAll('button')).toHaveLength(1); + expect(wrapper.find('button').text()).toBe('Retrieve data'); + expect(wrapper.findAllComponents(DownloadData)).toHaveLength(0); + await wrapper.find('button').trigger('click'); + expect(wrapper.findAllComponents(DownloadData)).toHaveLength(1); + await wrapper.find('button').trigger('click'); + expect(wrapper.findAllComponents(DownloadData)).toHaveLength(0); + }); }); diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/vue-components/data-table.vue b/assets/src/scripts/monitoring-location/components/hydrograph/vue-components/data-table.vue index 240f3b9d8221a8107802f76352596889c4fad7d8..4281e6f5fa6ee4dffcdb1e4edcf63767a1e3b502 100644 --- a/assets/src/scripts/monitoring-location/components/hydrograph/vue-components/data-table.vue +++ b/assets/src/scripts/monitoring-location/components/hydrograph/vue-components/data-table.vue @@ -1,5 +1,5 @@ <template> - <div> + <div id="iv-hydrograph-data-table-container"> <div v-show="currentIVData.length" id="iv-table-container" @@ -42,6 +42,34 @@ </table> <ul class="pagination" /> </div> + + <button + id="download-graph-data-container-data-table-toggle" + class="usa-button" + aria-controls="download-graph-data-container-data-table" + ga-on="click" + ga-event-category="data-table" + ga-event-action="download-graph-data-container-data-table-toggle" + @click="toggleDownloadContainer" + > + <svg + class="usa-icon" + aria-hidden="true" + role="img" + > + <use + :xlink:href="downloadIcon" + /> + </svg><span>Retrieve data</span> + </button> + + <div + v-if="showDownloadContainer" + id="download-graph-data-container-data-table" + class="download-graph-data-container" + > + <DownloadData /> + </div> </div> </template> @@ -49,16 +77,25 @@ import Pagination from 'list.js/src/pagination.js'; /* eslint no-unused-vars: off */ import List from 'list.js'; import {useState} from 'redux-connect-vue'; -import {inject, onMounted} from 'vue'; +import {ref, inject, onMounted} from 'vue'; import {listen} from 'ui/lib/d3-redux'; +import config from 'ui/config.js'; + import {getIVTableData} from '../selectors/iv-data'; import {getGroundwaterLevelsTableData} from '../selectors/discrete-data'; +import DownloadData from './download-data.vue'; + export default { name: 'DataTable', + components: { + DownloadData + }, setup() { + const downloadIcon = `${config.STATIC_URL}img/sprite.svg#file_download`; + const showDownloadContainer = ref(false); const CONTAINER_ID = { iv: 'iv-table-container', gw: 'gw-table-container' @@ -121,12 +158,18 @@ export default { listen(reduxStore, getGroundwaterLevelsTableData, updateGWDataTable); }); + const toggleDownloadContainer = function() { + showDownloadContainer.value = !showDownloadContainer.value; + }; + return { ...state, + downloadIcon, + showDownloadContainer, + toggleDownloadContainer, ivColumnHeadings: COLUMN_HEADINGS.iv, gwColumnHeadings: COLUMN_HEADINGS.gw }; } - }; </script> diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/vue-components/download-data.test.js b/assets/src/scripts/monitoring-location/components/hydrograph/vue-components/download-data.test.js new file mode 100644 index 0000000000000000000000000000000000000000..322cd5d8ea39e700110c6d5704464d4ba7083438 --- /dev/null +++ b/assets/src/scripts/monitoring-location/components/hydrograph/vue-components/download-data.test.js @@ -0,0 +1,209 @@ + +import config from 'ui/config'; + +import {configureStore} from 'ml/store'; +import {setCompareDataVisibility, setMedianDataVisibility} from 'ml/store/hydrograph-state'; + +import { + TEST_CURRENT_TIME_RANGE, + TEST_COMPARE_TIME_RANGE, + TEST_PRIMARY_IV_DATA, + TEST_STATS_DATA, + TEST_GW_LEVELS +} from '../mock-hydrograph-state'; +import {mount} from '@vue/test-utils'; + +import ReduxConnectVue from 'redux-connect-vue'; +import {createStructuredSelector} from 'reselect'; +import DownloadData from './download-data.vue'; +import USWDSAlert from 'ui/uswds-components/alert.vue'; + +describe('monitoring-location/components/hydrograph/vue-components/download-data', () => { + config.SITE_DATA_ENDPOINT = 'https://fakeserviceroot.com/nwis/site'; + config.IV_DATA_ENDPOINT = 'https://fakeserviceroot.com/nwis/iv'; + config.HISTORICAL_IV_DATA_ENDPOINT = 'https://fakeserviceroot-more-than-120-days.com/nwis/iv'; + config.STATISTICS_ENDPOINT = 'https://fakeserviceroot.com/nwis/stat'; + config.GROUNDWATER_LEVELS_ENDPOINT = 'https://fakegroundwater.org/gwlevels/'; + config.locationTimeZone = 'America/Chicago'; + + const TEST_STATE = { + hydrographData: { + currentTimeRange: TEST_CURRENT_TIME_RANGE, + prioryearTimeRange: TEST_COMPARE_TIME_RANGE, + primaryIVData: TEST_PRIMARY_IV_DATA, + compareIVData: TEST_PRIMARY_IV_DATA, + statisticsData: TEST_STATS_DATA + }, + groundwaterLevelData: { + all: [TEST_GW_LEVELS] + }, + hydrographState: { + showCompareIVData: false, + showMedianData: false, + selectedIVMethodID: '90649', + selectedParameterCode: '72019' + } + }; + + describe('tests for download data component', () => { + let store; + let windowSpy; + let wrapper; + + beforeEach(() => { + windowSpy = jest.spyOn(window, 'open').mockImplementation(() => null); + store = configureStore(TEST_STATE); + wrapper = mount(DownloadData, { + global: { + plugins: [ + [ReduxConnectVue, { + store, + mapStateToPropsFactory: createStructuredSelector + }] + ], + provide: { + store: store, + siteno: '11112222', + agencyCd: 'USGS', + buttonSetName: 'test-buttons' + } + } + }); + }); + + it('Renders form with the appropriate radio buttons and download button', () => { + expect(wrapper.findAll('input[type="radio"]')).toHaveLength(3); + expect(wrapper.findAll('#test-buttons-primary-data-download-button')).toHaveLength(1); + expect(wrapper.findAll('#test-buttons-groundwater-levels-data-download-button')).toHaveLength(1); + expect(wrapper.findAll('#test-buttons-site-data-download-button')).toHaveLength(1); + }); + + it('Rerenders the radio buttons if data visibility changes', async() => { + expect(wrapper.findAll('input[type="radio"]')).toHaveLength(3); + await store.dispatch(setCompareDataVisibility(true)); + expect(wrapper.findAll('input[type="radio"]')).toHaveLength(4); + await store.dispatch(setMedianDataVisibility(true)); + expect(wrapper.findAll('input[type="radio"]')).toHaveLength(5); + }); + + it('Shows an error message if the download button is clicked with no radio buttons checked', async() => { + const downloadButton = wrapper.find('button.download-selected-data'); + await downloadButton.trigger('click'); + expect(wrapper.findAllComponents(USWDSAlert)).toHaveLength(1); + }); + + it('Opens a window with the URL for the selected data', async() => { + const downloadButton = wrapper.find('button.download-selected-data'); + const siteDataButton = wrapper.find('#test-buttons-site-data-download-button'); + await siteDataButton.trigger('click'); + await downloadButton.trigger('click'); + + expect(wrapper.findAllComponents(USWDSAlert)).toHaveLength(0); + expect(windowSpy.mock.calls).toHaveLength(1); + expect(windowSpy.mock.calls[0][0]).toContain('/site/'); + expect(windowSpy.mock.calls[0][0]).toContain('sites=11112222'); + }); + + it('Opens window with correct data for data type selected and downloaded', async() => { + const downloadButton = wrapper.find('button.download-selected-data'); + await store.dispatch(setMedianDataVisibility(true)); + + const primaryDataButton = wrapper.find('#test-buttons-primary-data-download-button'); + await primaryDataButton.trigger('click'); + await downloadButton.trigger('click'); + expect(windowSpy.mock.calls[0][0]).toContain('/iv/'); + expect(windowSpy.mock.calls[0][0]).toContain('sites=11112222'); + expect(windowSpy.mock.calls[0][0]).toContain('parameterCd=72019'); + expect(windowSpy.mock.calls[0][0]).toContain('startDT=2020-02-24T10:15:00.000-06:00'); + expect(windowSpy.mock.calls[0][0]).toContain('endDT=2020-09-20T11:45:00.000-05:00'); + + + const medianDataButton = wrapper.find('#test-buttons-median-data-download-button'); + await medianDataButton.trigger('click'); + await downloadButton.trigger('click'); + expect(windowSpy.mock.calls[1][0]).toContain('/stat/'); + expect(windowSpy.mock.calls[1][0]).toContain('statTypeCd=median'); + expect(windowSpy.mock.calls[1][0]).toContain('parameterCd=72019'); + + + const groundWaterButton = wrapper.find('#test-buttons-groundwater-levels-data-download-button'); + await groundWaterButton.trigger('click'); + await downloadButton.trigger('click'); + expect(windowSpy.mock.calls[2][0]).toContain('/gwlevels/'); + expect(windowSpy.mock.calls[2][0]).toContain('featureId=USGS-11112222'); + }); + + it('Opens window for compare data when selected and downloaded', async() => { + const downloadButton = wrapper.find('button.download-selected-data'); + await store.dispatch(setCompareDataVisibility(true)); + + const compareDataButton = wrapper.find('#test-buttons-compare-data-download-button'); + await compareDataButton.trigger('click'); + await downloadButton.trigger('click'); + expect(windowSpy.mock.calls[0][0]).toContain('/iv/'); + expect(windowSpy.mock.calls[0][0]).toContain('sites=11112222'); + expect(windowSpy.mock.calls[0][0]).toContain('parameterCd=72019'); + expect(windowSpy.mock.calls[0][0]).toContain('startDT=2012-06-13T15:57:44.000-05:00'); + expect(windowSpy.mock.calls[0][0]).toContain('endDT=2013-06-13T15:57:44.000-05:00'); + }); + + it('Expects the error alert to disappear once a user selects a radio', async() => { + const downloadButton = wrapper.find('button.download-selected-data'); + await downloadButton.trigger('click'); + expect(wrapper.findAllComponents(USWDSAlert)).toHaveLength(1); + + const siteDataButton = wrapper.find('#test-buttons-site-data-download-button'); + await siteDataButton.trigger('click'); + expect(wrapper.findAllComponents(USWDSAlert)).toHaveLength(0); + }); + }); + + it('expects only the site button will show for calculated parameter 00010F', () => { + const TEST_STATE_CALCULATED_F = { + hydrographData: { + currentTimeRange: TEST_CURRENT_TIME_RANGE, + prioryearTimeRange: TEST_COMPARE_TIME_RANGE, + primaryIVData: { + ...TEST_PRIMARY_IV_DATA, + parameter: { + parameterCode: '00010F', + name: 'Calculated Temp', + description: 'F (Calculated)', + unit: 'F' + } + }, + compareIVData: TEST_PRIMARY_IV_DATA, + statisticsData: TEST_STATS_DATA + }, + groundwaterLevelData: { + all: [TEST_GW_LEVELS] + }, + hydrographState: { + showCompareIVData: true, + showMedianData: true, + selectedIVMethodID: '', + selectedParameterCode: '00010F' + } + }; + const store = configureStore(TEST_STATE_CALCULATED_F); + const wrapper = mount(DownloadData, { + global: { + plugins: [ + [ReduxConnectVue, { + store, + mapStateToPropsFactory: createStructuredSelector + }] + ], + provide: { + store: store, + siteno: '11112222', + agencyCd: 'USGS', + buttonSetName: 'test-buttons' + } + } + }); + + expect(wrapper.findAll('input[type="radio"]')).toHaveLength(1); + expect(wrapper.findAll('#test-buttons-site-data-download-button')).toHaveLength(1); + }); +}); diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/vue-components/download-data.vue b/assets/src/scripts/monitoring-location/components/hydrograph/vue-components/download-data.vue new file mode 100644 index 0000000000000000000000000000000000000000..075fec0f4645648d0a3a907d4cb550ae66400382 --- /dev/null +++ b/assets/src/scripts/monitoring-location/components/hydrograph/vue-components/download-data.vue @@ -0,0 +1,288 @@ +<template> + <div class="download-data-component"> + <fieldset class="usa-fieldset"> + <legend class="usa-legend"> + Select data to retrieve + </legend> + + <div v-if="hasVisiblePrimaryIVData && !isCalculatedParameter"> + <input + :id="`${buttonSetName}-primary-data-download-button`" + class="usa-radio__input" + type="radio" + :name="buttonSetName" + @click="createDownloadUrl('primary')" + > + <label + class="usa-radio__label" + :for="`${buttonSetName}-primary-data-download-button`" + > + Current time series + </label> + </div> + + <div v-if="hasVisibleCompareIVData && !isCalculatedParameter"> + <input + :id="`${buttonSetName}-compare-data-download-button`" + class="usa-radio__input" + type="radio" + :name="buttonSetName" + @click="createDownloadUrl('compare')" + > + <label + class="usa-radio__label" + :for="`${buttonSetName}-compare-data-download-button`" + > + Prior year time series + </label> + </div> + + <div v-if="hasVisibleMedianData && !isCalculatedParameter"> + <input + :id="`${buttonSetName}-median-data-download-button`" + class="usa-radio__input" + type="radio" + :name="buttonSetName" + @click="createDownloadUrl('median')" + > + <label + class="usa-radio__label" + :for="`${buttonSetName}-median-data-download-button`" + > + Median + </label> + </div> + + <div v-if="hasVisibleGroundwaterLevels"> + <input + :id="`${buttonSetName}-groundwater-levels-data-download-button`" + class="usa-radio__input" + type="radio" + name="buttonSetName" + @click="createDownloadUrl('groundwater-levels')" + > + <label + class="usa-radio__label" + :for="`${buttonSetName}-groundwater-levels-data-download-button`" + > + Field visits + </label> + </div> + + <div> + <input + :id="`${buttonSetName}-site-data-download-button`" + class="usa-radio__input" + type="radio" + :name="buttonSetName" + @click="createDownloadUrl('site')" + > + <label + class="usa-radio__label" + :for="`${buttonSetName}-site-data-download-button`" + > + About this location + </label> + </div> + + <div> + <USWDSAlert + v-if="showErrorMessage" + alert-type="error" + slim-alert + :static-root="staticRoot" + > + <template #default> + <p class="usa-alert__text"> + You must select one of the choices above. + </p> + </template> + </USWDSAlert> + + <button + class="usa-button download-selected-data" + ga-on="click" + ga-event-category="download-selected-data" + ga-event-action="download" + @click="retrieveData" + > + <svg + class="usa-icon" + aria-hidden="true" + role="img" + > + <use + :xlink:href="downloadIcon" + /> + </svg> + Retrieve + </button> + </div> + </fieldset> + + <div class="download-info"> + <div> + <div> + A separate tab will open with the requested data. + </div> + <div> + All data is in + <a + href="https://waterdata.usgs.gov/nwis/?tab_delimited_format_info" + target="_blank" + >RDB</a> format. + </div> + <div> + Data is retrieved from <a + href="https://waterservices.usgs.gov" + target="_blank" + >USGS Water Data + Services.</a> + </div> + <div> + If you are an R user, use the + <a + href="https://usgs-r.github.io/dataRetrieval/" + target="_blank" + >USGS dataRetrieval package</a> to + download, analyze and plot your data + </div> + </div> + </div> + </div> +</template> + +<script> +import {useState} from 'redux-connect-vue'; +import {computed, ref, inject} from 'vue'; + +import {DateTime} from 'luxon'; +import config from 'ui/config.js'; +import USWDSAlert from 'ui/uswds-components/alert.vue'; + +import {getTimeRange} from 'ml/selectors/hydrograph-data-selector'; +import {getIVServiceURL, getSiteMetaDataServiceURL} from 'ui/web-services/instantaneous-values'; +import {getStatisticsServiceURL} from 'ui/web-services/statistics-data'; +import {getGroundwaterServiceURL} from 'ui/web-services/groundwater-levels'; +import {getPrimaryParameter} from 'ml/components/hydrograph/selectors/time-series-data'; + +import {isCalculatedTemperature} from 'ml/parameter-code-utils'; +import { + hasVisibleGroundwaterLevels, + hasVisibleIVData, + hasVisibleMedianStatisticsData +} from '../selectors/time-series-data'; + +export default { + name: 'DownloadData', + components: { + USWDSAlert + }, + setup() { + const downloadUrl = ref(''); + const showErrorMessage = ref(false); + const state = useState({ + hasVisiblePrimaryIVData: hasVisibleIVData('primary'), + hasVisibleCompareIVData: hasVisibleIVData('compare'), + hasVisibleMedianData: hasVisibleMedianStatisticsData, + hasVisibleGroundwaterLevels: hasVisibleGroundwaterLevels, + primaryParameter: getPrimaryParameter + }); + + const reduxStore = inject('store'); + const siteno = inject('siteno'); + const agencyCd = inject('agencyCd'); + const buttonSetName = inject('buttonSetName'); + + const isCalculatedParameter = computed(() => { + return state.primaryParameter.value ? isCalculatedTemperature(state.primaryParameter.value['parameterCode']) : false; + }); + + const staticRoot = config.STATIC_URL; + const downloadIcon = `${config.STATIC_URL}img/sprite.svg#file_download`; + + const toISO = function(inMillis) { + return DateTime.fromMillis(inMillis, {zone: config.locationTimeZone}).toISO(); + }; + + const getIVDataURL = function(reduxStore, siteno, timeRangeKind) { + const currentState = reduxStore.getState(); + const timeRange = getTimeRange(timeRangeKind)(currentState); + + return getIVServiceURL({ + siteno, + parameterCode: getPrimaryParameter(currentState).parameterCode, + startTime: toISO(timeRange.start), + endTime: toISO(timeRange.end), + format: 'rdb' + }); + }; + + const getMedianDataURL = function(store, siteno) { + return getStatisticsServiceURL({ + siteno, + parameterCode: getPrimaryParameter(store.getState()).parameterCode, + statType: 'median', + format: 'rdb' + }); + }; + + const getGroundwaterLevelURL = function(siteno, agencyCd) { + return getGroundwaterServiceURL({ + siteno, + agencyCd, + format: 'json' + }); + }; + + const getSiteMetaDataURL = function(siteno) { + return getSiteMetaDataServiceURL({ + siteno, + isExpanded: true + }); + }; + + const createDownloadUrl = (buttonSelected) => { + showErrorMessage.value = false; + switch (buttonSelected) { + case 'primary': + downloadUrl.value = getIVDataURL(reduxStore, siteno, 'current'); + break; + case 'compare': + downloadUrl.value = getIVDataURL(reduxStore, siteno, 'prioryear'); + break; + case 'median': + downloadUrl.value = getMedianDataURL(reduxStore, siteno); + break; + case 'groundwater-levels': + downloadUrl.value = getGroundwaterLevelURL(siteno, agencyCd); + break; + case 'site': + downloadUrl.value = getSiteMetaDataURL(siteno); + break; + } + }; + + const retrieveData = () => { + if (downloadUrl.value) { + showErrorMessage.value = false; + window.open(downloadUrl.value, '_blank'); + } else { + showErrorMessage.value = true; + } + }; + + return { + ...state, + buttonSetName, + createDownloadUrl, + downloadIcon, + isCalculatedParameter, + retrieveData, + showErrorMessage, + staticRoot + }; + } +}; +</script> + diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/vue-components/graph-controls.test.js b/assets/src/scripts/monitoring-location/components/hydrograph/vue-components/graph-controls.test.js index 3fe06d59233b3a95651797bd5f8fad313cad529c..8074124233725accc6c25085124e2e47a569c9ba 100644 --- a/assets/src/scripts/monitoring-location/components/hydrograph/vue-components/graph-controls.test.js +++ b/assets/src/scripts/monitoring-location/components/hydrograph/vue-components/graph-controls.test.js @@ -25,7 +25,7 @@ describe('monitoring-location/components/hydrograph/components/graph-controls', let restoreConsole; let store; - let retrievePriorYearSpy, retrieveMedianStatisticsSpy; + let retrievePriorYearSpy, retrieveStatisticsSpy; let showDataIndicatorSpy; let wrapper; @@ -41,7 +41,7 @@ describe('monitoring-location/components/hydrograph/components/graph-controls', beforeEach(() => { retrievePriorYearSpy = jest.spyOn(hydrographData, 'retrievePriorYearIVData'); - retrieveMedianStatisticsSpy = jest.spyOn(hydrographData, 'retrieveMedianStatistics'); + retrieveStatisticsSpy = jest.spyOn(hydrographData, 'retrieveStatistics'); showDataIndicatorSpy = jest.spyOn(dataIndicator, 'showDataIndicators'); store = configureStore({ @@ -153,9 +153,9 @@ describe('monitoring-location/components/hydrograph/components/graph-controls', expect(showDataIndicatorSpy).toHaveBeenCalledTimes(1); expect(showDataIndicatorSpy.mock.calls[0][0]).toBe(true); - expect(retrieveMedianStatisticsSpy).toHaveBeenCalledTimes(1); - expect(retrieveMedianStatisticsSpy.mock.calls[0][0]).toBe('12345678'); - expect(retrieveMedianStatisticsSpy.mock.calls[0][1]).toEqual('72019'); + expect(retrieveStatisticsSpy).toHaveBeenCalledTimes(1); + expect(retrieveStatisticsSpy.mock.calls[0][0]).toBe('12345678'); + expect(retrieveStatisticsSpy.mock.calls[0][1]).toEqual('72019'); }); it('expect that unchecking the median does not fetch data and sets data indicators off', async() => { @@ -165,6 +165,6 @@ describe('monitoring-location/components/hydrograph/components/graph-controls', expect(showDataIndicatorSpy).toHaveBeenCalledTimes(1); expect(showDataIndicatorSpy.mock.calls[0][0]).toBe(false); - expect(retrieveMedianStatisticsSpy).not.toHaveBeenCalled(); + expect(retrieveStatisticsSpy).not.toHaveBeenCalled(); }); }); \ No newline at end of file diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/vue-components/graph-controls.vue b/assets/src/scripts/monitoring-location/components/hydrograph/vue-components/graph-controls.vue index 76cc102ed733591ea8883693ca0deb966a34741c..1ed7376dc9d3d5079ef299fc7b36dd6e6ca461e0 100644 --- a/assets/src/scripts/monitoring-location/components/hydrograph/vue-components/graph-controls.vue +++ b/assets/src/scripts/monitoring-location/components/hydrograph/vue-components/graph-controls.vue @@ -1,19 +1,19 @@ <template> <div> <USWDSCheckbox + id="iv-compare-timeseries-checkbox" label="Compare to last year" name="graphControls" value="compare" - id="iv-compare-timeseries-checkbox" :is-checked="compareChecked" :is-checkbox-disabled="!compareEnabled" @toggleCheckbox="selectCompare" /> <USWDSCheckbox + id="iv-median-timeseries-checkbox" label="Display median" name="graphControls" value="median" - id="iv-median-timeseries-checkbox" :is-checked="medianChecked" @toggleCheckbox="selectMedian" /> @@ -30,7 +30,7 @@ import USWDSCheckbox from 'ui/uswds-components/checkbox.vue'; import {getSelectedParameterCode, getSelectedTimeSpan} from 'ml/selectors/hydrograph-state-selector'; import {getTimeRange} from 'ml/selectors/hydrograph-data-selector'; -import {retrieveMedianStatistics, retrievePriorYearIVData} from 'ml/store/hydrograph-data'; +import {retrieveStatistics, retrievePriorYearIVData} from 'ml/store/hydrograph-data'; import {setCompareDataVisibility, setMedianDataVisibility} from 'ml/store/hydrograph-state'; import {isVisible} from '../selectors/time-series-data'; @@ -67,7 +67,7 @@ export default { setCompareDataVisibility, setMedianDataVisibility, retrievePriorYearIVData, - retrieveMedianStatistics + retrieveStatistics }); const reduxStore = inject('store'); @@ -96,7 +96,7 @@ export default { actions.setMedianDataVisibility(checked); if (checked) { showDataIndicators(true, reduxStore); - actions.retrieveMedianStatistics(siteno, getSelectedParameterCode(reduxState)) + actions.retrieveStatistics(siteno, getSelectedParameterCode(reduxState)) .then(() => { showDataIndicators(false, reduxStore); }); diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/vue-components/method-picker.test.js b/assets/src/scripts/monitoring-location/components/hydrograph/vue-components/method-picker.test.js new file mode 100644 index 0000000000000000000000000000000000000000..cc7b2f562fc99f2ee35d892f1736963cffea99f7 --- /dev/null +++ b/assets/src/scripts/monitoring-location/components/hydrograph/vue-components/method-picker.test.js @@ -0,0 +1,80 @@ +import {mount} from '@vue/test-utils'; + +import config from 'ui/config'; + +import MethodPicker from './method-picker.vue'; + +describe('monitoring-location/components/hydrograph/vue-components/method-picker', () => { + config.STATIC_URL = 'https://fakeserver.com/static/'; + + const TEST_SORTED_IV_METHODS = { + 'parameterCode': '00300', + 'methods': [{ + 'pointCount': 672, + 'lastPoint': {'value': '5.5', 'qualifiers': ['P'], 'dateTime': 1655135100000}, + 'methodID': '57501', + 'methodDescription': '[YSI EXO]' + }, { + 'pointCount': 5, + 'lastPoint': null, + 'methodID': '57481', + 'methodDescription': 'Discontinued Mar. 11, 2015' + }, { + 'pointCount': 0, + 'lastPoint': null, + 'methodID': '57487', + 'methodDescription': 'FROM NR BANK MONITOR, [Discontinued June 30, 2015]' + }] + }; + + it('Expects that the expected html is rendered', () => { + const wrapper = mount(MethodPicker, { + props: { + sortedIvMethods: TEST_SORTED_IV_METHODS + } + }); + + expect(wrapper.find('.usa-label').attributes('for')).toBe('method-picker-00300'); + expect(wrapper.find('.usa-icon').html()).toContain(config.STATIC_URL); + expect(wrapper.find('.no-data-points-note').exists()).toBe(true); + expect(wrapper.find('select').attributes('id')).toBe('method-picker-00300'); + const options = wrapper.findAll('option'); + expect(options).toHaveLength(3); + expect(options[0].attributes('value')).toBe('57501'); + expect(options[0].attributes('disabled')).not.toBeDefined(); + expect(options[0].text()).toBe('[YSI EXO]'); + expect(options[1].attributes('value')).toBe('57481'); + expect(options[1].attributes('disabled')).not.toBeDefined(); + expect(options[1].text()).toBe('Discontinued Mar. 11, 2015'); + expect(options[2].attributes('value')).toBe('57487'); + expect(options[2].attributes('disabled')).toBeDefined(); + expect(options[2].text()).toBe('FROM NR BANK MONITOR, [Discontinued June 30, 2015]'); + }); + + it('Should not show the no data note if all methods have points', () => { + const wrapper = mount(MethodPicker, { + props: { + sortedIvMethods: { + parameterCode: TEST_SORTED_IV_METHODS.parameterCode, + methods: [TEST_SORTED_IV_METHODS.methods[0], TEST_SORTED_IV_METHODS.methods[1]] + } + } + }); + + expect(wrapper.find('.no-data-points-note').exists()).toBe(false); + }); + + it('Expects the selectMethod event to be emitted if an option is selected', async() => { + const wrapper = mount(MethodPicker, { + props: { + sortedIvMethods: TEST_SORTED_IV_METHODS + } + }); + + const select = wrapper.find('select'); + select.element.value = '57481'; + await select.trigger('change'); + + expect(wrapper.emitted().selectMethod[0]).toEqual(['57481']); + }); +}); \ No newline at end of file diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/vue-components/method-picker.vue b/assets/src/scripts/monitoring-location/components/hydrograph/vue-components/method-picker.vue new file mode 100644 index 0000000000000000000000000000000000000000..3d34356865d83116b053a02a01621caf068305b0 --- /dev/null +++ b/assets/src/scripts/monitoring-location/components/hydrograph/vue-components/method-picker.vue @@ -0,0 +1,91 @@ +<template> + <div class="method-picker-container"> + <label + class="usa-label" + :for="`method-picker-${sortedIvMethods.parameterCode}`" + > + Sampling Methods/Sub-locations: + <span + ref="tooltip" + class="usa-tooltip" + data-position="right" + title="The names used in dropdown menu are often specific to a particular monitoring location and describe sampling details used to distinguish time-series of the same type--examples include variations in physical location and sensor type." + > + <svg class="usa-icon"> + <use :xlink:href="infoIcon" /> + </svg> + </span> + </label> + <div + v-if="hasMethodsWithNoPoints" + class="no-data-points-note" + > + note - some methods/sub-locations are disabled because there are no data points for + these in your selected time span + </div> + <select + :id="`method-picker-${sortedIvMethods.parameterCode}`" + class="usa-select" + name="method-to-graph" + @change="selectThisMethod" + > + <option + v-for="method in sortedIvMethods.methods" + :key="method.methodID" + :value="method.methodID" + :disabled="!method.pointCount" + > + {{ method.methodDescription }} + </option> + </select> + </div> +</template> + +<script> +import {computed, ref, onMounted} from 'vue'; +import uswds_tooltip from 'uswds-components/usa-tooltip/src/index.js'; + +import config from 'ui/config'; + +/* + * @vue-prop {Object} sortedIvMethods - has two properties + * @prop {String} parameterCode + * @prop {Array of Object} - information about the available methods including + * methodId, methodDescription and pointCount + * @vue-event selectMethod - passes the methodId that is selected + */ +export default { + name: 'MethodPicker', + props: { + sortedIvMethods: { + type: Object, + required: true + } + }, + emits: ['selectMethod'], + setup(props, {emit}) { + const tooltip = ref(null); + + const infoIcon = computed(() => `${config.STATIC_URL}img/sprite.svg#info`); + const hasMethodsWithNoPoints = computed(() => { + return props.sortedIvMethods.methods.findIndex(method => method.pointCount === 0) > -1; + }); + + onMounted(() => { + uswds_tooltip.on(tooltip); + }); + + function selectThisMethod(event) { + emit('selectMethod', event.target.value); + } + + uswds_tooltip.on(); + + return { + infoIcon, + hasMethodsWithNoPoints, + selectThisMethod + }; + } +}; +</script> diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/vue-components/parameter-selection-expansion-control.test.js b/assets/src/scripts/monitoring-location/components/hydrograph/vue-components/parameter-selection-expansion-control.test.js new file mode 100644 index 0000000000000000000000000000000000000000..0865c7ec2f28454016917b1368ba0f58071dd498 --- /dev/null +++ b/assets/src/scripts/monitoring-location/components/hydrograph/vue-components/parameter-selection-expansion-control.test.js @@ -0,0 +1,75 @@ +import {mount} from '@vue/test-utils'; + +import config from 'ui/config'; + +import ParameterSelectionExpansionControl from './parameter-selection-expansion-control.vue'; + +describe('monitoring-location/components/hydrograph/vue-components/', () => { + config.STATIC_URL = 'https://fakeserver.com/static/'; + + it('Renders the expected components when isExpanded is default (false)', () => { + const wrapper = mount(ParameterSelectionExpansionControl, { + props: { + parameterCode: '00060', + idForExpansionRow: 'my-test-row' + } + }); + + const toggleContainer = wrapper.find('.expansion-toggle'); + expect(toggleContainer.attributes('aria-expanded')).toBe('false'); + expect(toggleContainer.attributes('aria-controls')).toBe('my-test-row'); + const svg = toggleContainer.find('svg'); + expect(svg.attributes('aria-label')).toBe('click to expand details'); + expect(svg.find('use').attributes('href')).toBe( + 'https://fakeserver.com/static/img/sprite.svg#expand_more'); + }); + + it('Renders the expected components when isExpanded is true', () => { + const wrapper = mount(ParameterSelectionExpansionControl, { + props: { + parameterCode: '00060', + isExpanded: true, + idForExpansionRow: 'my-test-row' + } + }); + + const toggleContainer = wrapper.find('.expansion-toggle'); + expect(toggleContainer.attributes('aria-expanded')).toBe('true'); + expect(toggleContainer.attributes('aria-controls')).toBe('my-test-row'); + const svg = toggleContainer.find('svg'); + expect(svg.attributes('aria-label')).toBe('click to hide details'); + expect(svg.find('use').attributes('href')).toBe( + 'https://fakeserver.com/static/img/sprite.svg#expand_less'); + }); + + it('emits toggleRowVisibility event when the expansion toggle is clicked when not expanded', async() => { + const wrapper = mount(ParameterSelectionExpansionControl, { + props: { + parameterCode: '00060', + idForExpansionRow: 'my-test-row' + } + }); + const toggleContainer = wrapper.find('.expansion-toggle'); + await toggleContainer.trigger('click'); + + let emittedEvents = wrapper.emitted().toggleExpansionRow; + expect(emittedEvents).toHaveLength(1); + expect(emittedEvents[0]).toEqual(['00060', true]); + }); + + it('emits toggleRowVisibility event when the expansion toggle is clicked when expanded', async() => { + const wrapper = mount(ParameterSelectionExpansionControl, { + props: { + parameterCode: '00060', + idForExpansionRow: 'my-test-row', + isExpanded: true + } + }); + const toggleContainer = wrapper.find('.expansion-toggle'); + await toggleContainer.trigger('click'); + + let emittedEvents = wrapper.emitted().toggleExpansionRow; + expect(emittedEvents).toHaveLength(1); + expect(emittedEvents[0]).toEqual(['00060', false]); + }); +}); \ No newline at end of file diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/vue-components/parameter-selection-expansion-control.vue b/assets/src/scripts/monitoring-location/components/hydrograph/vue-components/parameter-selection-expansion-control.vue new file mode 100644 index 0000000000000000000000000000000000000000..287d1d3edf5b2c53e5d07c3aa8e351b833bd48d5 --- /dev/null +++ b/assets/src/scripts/monitoring-location/components/hydrograph/vue-components/parameter-selection-expansion-control.vue @@ -0,0 +1,67 @@ +<template> + <div class="expansion-toggle-container"> + <span + class="expansion-toggle" + :aria-expanded="isExpanded" + :aria-controls="idForExpansionRow" + @click.stop="toggleRowVisibility" + > + <svg + class="usa-icon" + :aria-label="expansionIconLabel" + role="img" + > + <use :xlink:href="expansionIcon" /> + </svg> + </span> + </div> +</template> +ß +<script> +import {computed} from 'vue'; + +import config from 'ui/config'; + +/* + * @vue-prop {String} parameterCode + * @vue-prop {String} idForExpansionRow - should be unique for the page + * @vue-prop {Boolean} isExpanded - defaults to false + * @vue-event toggleExpansionRow - has two parameters, the parameterCode and a boolean indicating the new expansion state. + * Please note that the user of the component will need to update isExpanded to reflect the new state. + * */ +export default { + name: 'ParameterSelectionExpansionControl', + props: { + parameterCode: { + type: String, + required: true + }, + idForExpansionRow: { + type: String, + required: true + }, + isExpanded: { + type: Boolean, + required: false, + default: false + } + }, + emits: ['toggleExpansionRow'], + setup(props, {emit}) { + + const expansionIcon = + computed(() => `${config.STATIC_URL}img/sprite.svg#${props.isExpanded ? 'expand_less' : 'expand_more'}`); + const expansionIconLabel = `click to ${props.isExpanded ? 'hide' : 'expand'} details`; + + function toggleRowVisibility() { + emit('toggleExpansionRow', props.parameterCode, !props.isExpanded); + } + + return { + expansionIcon, + expansionIconLabel, + toggleRowVisibility + }; + } +}; +</script> diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/vue-components/parameter-selection.test.js b/assets/src/scripts/monitoring-location/components/hydrograph/vue-components/parameter-selection.test.js new file mode 100644 index 0000000000000000000000000000000000000000..4d74ce6dc592431b1963f576ef4d857652a9fa10 --- /dev/null +++ b/assets/src/scripts/monitoring-location/components/hydrograph/vue-components/parameter-selection.test.js @@ -0,0 +1,232 @@ +import {enableFetchMocks, disableFetchMocks} from 'jest-fetch-mock'; +import mockConsole from 'jest-mock-console'; +import {bindActionCreators} from 'redux'; +import ReduxConnectVue from 'redux-connect-vue'; +import {createStructuredSelector} from 'reselect'; +import {shallowMount} from '@vue/test-utils'; + +import config from 'ui/config'; +import * as utils from 'ui/utils'; + +import {configureStore} from 'ml/store'; +import * as hydrographData from 'ml/store/hydrograph-data'; + +import * as dataIndicator from '../data-indicator'; +import {TEST_PRIMARY_IV_DATA, TEST_HYDROGRAPH_PARAMETERS} from '../mock-hydrograph-state'; + +import MethodPicker from './method-picker.vue'; +import ParameterSelection from './parameter-selection.vue'; +import ParameterSelectionExpansionControl from './parameter-selection-expansion-control.vue'; + +describe('monitoring-location/components/hydrograph/vue-components/parameter-selection', () => { + utils.mediaQuery = jest.fn().mockReturnValue(true); + + config.ivPeriodOfRecord = { + '00060': { + begin_date: '1980-01-01', + end_date: '2020-01-01' + }, + '72019': { + begin_date: '1980-04-01', + end_date: '2020-04-01' + }, + '00010': { + begin_date: '1981-04-01', + end_date: '2019-04-01' + } + + }; + config.gwPeriodOfRecord = { + '72019': { + begin_date: '1980-03-31', + end_date: '2020-03-31' + }, + '62610': { + begin_date: '1980-05-01', + end_date: '2020-05-01' + } + }; + + const TEST_STATE = { + hydrographData: { + primaryIVData: TEST_PRIMARY_IV_DATA + }, + hydrographParameters: TEST_HYDROGRAPH_PARAMETERS, + hydrographState: { + selectedTimeSpan: 'P7D', + selectedParameterCode: '72019', + selectedIVMethodID: '90649' + } + }; + + let restoreConsole; + beforeAll(() => { + enableFetchMocks(); + restoreConsole = mockConsole(); + }); + + afterAll(() => { + disableFetchMocks(); + restoreConsole(); + }); + + let retrieveHydrographDataSpy; + let showDataIndicatorSpy; + let store; + let wrapper; + + beforeEach(() => { + retrieveHydrographDataSpy = jest.spyOn(hydrographData, 'retrieveHydrographData'); + showDataIndicatorSpy = jest.spyOn(dataIndicator, 'showDataIndicators'); + store = configureStore(TEST_STATE); + + wrapper = shallowMount(ParameterSelection, { + global: { + plugins: [ + [ReduxConnectVue, { + store, + mapDispatchToPropsFactory: (actionCreators) => (dispatch) => bindActionCreators(actionCreators, dispatch), + mapStateToPropsFactory: createStructuredSelector + }] + ], + provide: { + store: store, + siteno: '12345678', + agencyCode: 'USGS' + } + } + }); + }); + + it('Expects the component to be rendered with the expect rows and selection', () => { + const rowContainers = wrapper.findAll('.parameter-row-container'); + + expect(rowContainers).toHaveLength(5); + expect(rowContainers[0].classes()).not.toContain('selected'); + expect(rowContainers[1].classes()).toContain('selected'); + expect(rowContainers[2].classes()).not.toContain('selected'); + expect(rowContainers[3].classes()).not.toContain('selected'); + expect(rowContainers[4].classes()).not.toContain('selected'); + + expect(rowContainers[0].attributes('aria-selected')).toBe('false'); + expect(rowContainers[1].attributes('aria-selected')).toBe('true'); + expect(rowContainers[2].attributes('aria-selected')).toBe('false'); + expect(rowContainers[3].attributes('aria-selected')).toBe('false'); + expect(rowContainers[4].attributes('aria-selected')).toBe('false'); + }); + + it('Expects a radio button and description for each parameter code with the selected parameter code checked', () =>{ + const radioButtons = wrapper.findAll('.usa-radio'); + + expect(radioButtons).toHaveLength(5); + + const buttonOne = radioButtons[0].find('input'); + const labelOne = radioButtons[0].find('label'); + expect(buttonOne.element.checked).toBe(false); + expect(buttonOne.attributes('value')).toBe('00060'); + expect(buttonOne.attributes('id')).toBe('radio-00060'); + expect(labelOne.text()).toBe('Discharge, cubic feet per second'); + expect(labelOne.attributes('for')).toBe('radio-00060'); + + const buttonTwo = radioButtons[1].find('input'); + const labelTwo = radioButtons[1].find('label'); + expect(buttonTwo.element.checked).toBe(true); + expect(buttonTwo.attributes('value')).toBe('72019'); + expect(buttonTwo.attributes('id')).toBe('radio-72019'); + expect(labelTwo.text()).toBe('Depth to water level, feet below land surface'); + expect(labelTwo.attributes('for')).toBe('radio-72019'); + }); + + it('Expects the period of record and expansion toggle to be properly rendered', () => { + const periodOfRecordContainers = wrapper.findAll('.period-of-record-container'); + + expect(periodOfRecordContainers).toHaveLength(5); + + expect(periodOfRecordContainers[0].find('.period-of-record-text').text()) + .toBe('1980-01-01 to 2020-01-01'); + expect(periodOfRecordContainers[1].find('.period-of-record-text').text()) + .toBe('1980-03-31 to 2020-04-01'); + + const expansionControls = wrapper.findAllComponents(ParameterSelectionExpansionControl); + expect(expansionControls).toHaveLength(4); + expect(expansionControls.find(control => control.props('parameterCode') === '62016')).toBeUndefined(); + + const expansionControlOne = periodOfRecordContainers[0].findComponent(ParameterSelectionExpansionControl); + const expansionControlTwo = periodOfRecordContainers[1].findComponent(ParameterSelectionExpansionControl); + expect(expansionControlOne.props('parameterCode')).toBe('00060'); + expect(expansionControlTwo.props('parameterCode')).toBe('72019'); + expect(expansionControlOne.props('isExpanded')).toBe(false); + expect(expansionControlTwo.props('isExpanded')).toBe(true); + expect(expansionControlOne.props('idForExpansionRow')).toContain('00060'); + expect(expansionControlTwo.props('idForExpansionRow')).toContain('72019'); + }); + + it('Expects that clicking on a row selects that parameter and closes any open row and opens that parameter\'s row', async() => { + const parameterRowContainers = wrapper.findAll('.parameter-row-container'); + await parameterRowContainers[0].find('.parameter-row-info-container').trigger('click'); + + expect(parameterRowContainers[0].classes('selected')).toBe(true); + expect(parameterRowContainers[1].classes('selected')).toBe(false); + expect(parameterRowContainers[0].findComponent(ParameterSelectionExpansionControl).props('isExpanded')).toBe(true); + expect(parameterRowContainers[1].findComponent(ParameterSelectionExpansionControl).props('isExpanded')).toBe(false); + }); + + it('Expects that clicking on a row updates the selected parameter code in the state and fetches new hydrograph data', async() => { + const parameterRowContainers = wrapper.findAll('.parameter-row-container'); + await parameterRowContainers[0].find('.parameter-row-info-container').trigger('click'); + + expect(store.getState().hydrographState.selectedParameterCode).toBe('00060'); + expect(showDataIndicatorSpy).toHaveBeenCalled(); + expect(retrieveHydrographDataSpy).toHaveBeenCalled(); + }); + + it('Expects the expansion container to be only shown for the expanded row on', () => { + const expansionContainers = wrapper.findAll('.expansion-container-row'); + expect(expansionContainers).toHaveLength(4); + expect(expansionContainers[0].isVisible()).toBe(false); + expect(expansionContainers[1].isVisible()).toBe(true); + expect(expansionContainers[2].isVisible()).toBe(false); + expect(expansionContainers[3].isVisible()).toBe(false); + }); + + it('Expects a parameter code without methods shows only the water alert section', () => { + const dischargeExpansionContainer = wrapper.find('#expansion-row-00060'); + const waterAlertLink = dischargeExpansionContainer.find('a'); + expect(waterAlertLink.text()).toBe( 'Subscribe to Alerts'); + expect(waterAlertLink.attributes('title')).toBeDefined(); + expect(dischargeExpansionContainer.findComponent(MethodPicker).exists()).toBe(false); + }); + + it('Expects a parameter code with methods shows the water alert section and the method picker', () => { + const gwlevelsExpansionContainer = wrapper.find('#expansion-row-72019'); + const gwlevelsMethodPicker = gwlevelsExpansionContainer.findComponent(MethodPicker); + expect(gwlevelsExpansionContainer.find('a').exists()).toBe(true); + expect(gwlevelsMethodPicker.exists()).toBe(true); + expect(gwlevelsMethodPicker.attributes('sortedivmethods')).toBeDefined(); + }); + + it('Expects that clicking the expansion toggle in the selected row hides the expansion container', async() => { + const selectedExpansionToggle = wrapper.find('.selected').findComponent(ParameterSelectionExpansionControl); + await selectedExpansionToggle.vm.$emit('toggleExpansionRow', '72019', false); + + expect(wrapper.find('#expansion-row-72019').isVisible()).toBe(false); + }); + + it('Expects that clicking on a hidden expansion toggle shows the expansion container but does not change the selection', async() => { + const expansionToggles = wrapper.findAllComponents(ParameterSelectionExpansionControl); + await expansionToggles[0].vm.$emit('toggleExpansionRow', '00060', true); + + expect(wrapper.find('#expansion-row-00060').isVisible()).toBe(true); + expect(wrapper.find('#expansion-row-72019').isVisible()).toBe(false); + const parameterRows = wrapper.findAll('.parameter-row-container'); + expect(parameterRows[0].classes()).not.toContain('selected'); + expect(parameterRows[1].classes()).toContain('selected'); + }); + + it('Expects that updated the selected method updates the store', async() => { + const gwLevelMethodPicker = wrapper.find('#expansion-row-72019').findComponent(MethodPicker); + await gwLevelMethodPicker.vm.$emit('selectMethod', '252055'); + + expect(store.getState().hydrographState.selectedIVMethodID).toBe('252055'); + }); +}); diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/vue-components/parameter-selection.vue b/assets/src/scripts/monitoring-location/components/hydrograph/vue-components/parameter-selection.vue new file mode 100644 index 0000000000000000000000000000000000000000..85ea87cc3c9b0f64381e84e07db37b4587bc9bfb --- /dev/null +++ b/assets/src/scripts/monitoring-location/components/hydrograph/vue-components/parameter-selection.vue @@ -0,0 +1,169 @@ +<template> + <div> + <p class="usa-prose parameter-selection-title"> + Select data to graph + </p> + <div class="main-parameter-selection-container"> + <div + v-for="parameter in parameters" + :key="parameter.parameterCode" + :class="`parameter-row-container ${parameter.parameterCode === selectedParameterCode ? 'selected' : ''}`" + :aria-selected="parameter.parameterCode === selectedParameterCode" + > + <div + class="parameter-row-info-container" + ga-on="click" + ga-event-category="selectTimeSeries" + :ga-event-action="`time-series-parmcd-${parameter.parameterCode}`" + @click.stop="selectParameter(parameter.parameterCode)" + > + <div class="period-of-record-container"> + <div class="period-of-record-text"> + {{ parameter.periodOfRecord.begin_date }} to {{ parameter.periodOfRecord.end_date }} + </div> + <ParameterSelectionExpansionControl + v-if="parameter.waterAlert.hasWaterAlert" + :parameter-code="parameter.parameterCode" + :is-expanded="parameter.parameterCode === expandedParameterCode" + :id-for-expansion-row="`expansion-row-${parameter.parameterCode}`" + @toggleExpansionRow="toggleExpansionRow" + /> + </div> + <div class="usa-radio"> + <input + :id="`radio-${parameter.parameterCode}`" + class="usa-radio__input" + type="radio" + name="parameter-selection" + :value="parameter.parameterCode" + :checked="parameter.parameterCode === selectedParameterCode" + > + <label + class="usa-radio__label description__param-select" + :for="`radio-${parameter.parameterCode}`" + > + {{ parameter.description }} + </label> + </div> + </div> + <div + v-if="parameter.waterAlert.hasWaterAlert || (sortedIvMethods && parameter.parameterCode === sortedIvMethods.parameterCode && sortedIvMethods.length > 1)" + v-show="parameter.parameterCode === expandedParameterCode" + :id="`expansion-row-${parameter.parameterCode}`" + class="expansion-container-row" + > + <div + v-if="parameter.waterAlert.hasWaterAlert" + > + <a + :href="parameter.waterAlertUrl" + target="_blank" + class="usa-link" + :title="parameter.waterAlert.tooltipText" + > + {{ parameter.waterAlert.displayText }} + </a> + </div> + <MethodPicker + v-if="sortedIvMethods && parameter.parameterCode === sortedIvMethods.parameterCode && sortedIvMethods.methods.length > 1" + :sorted-iv-methods="sortedIvMethods" + @selectMethod="updateSelectedMethod" + /> + </div> + </div> + </div> + </div> +</template> + +<script> +import {useActions, useState} from 'redux-connect-vue'; +import {createSelector} from 'reselect'; +import {inject, ref} from 'vue'; + +import config from 'ui/config'; + +import {getInputsForRetrieval, getSelectedParameterCode} from 'ml/selectors/hydrograph-state-selector'; + +import {setSelectedParameterCode, setSelectedIVMethodID} from 'ml/store/hydrograph-state'; +import {retrieveHydrographData} from 'ml/store/hydrograph-data'; + +import {getAvailableParameters} from '../selectors/parameter-data'; +import {getSortedIVMethods} from '../selectors/time-series-data'; + +import {showDataIndicators} from '../data-indicator'; + +import MethodPicker from './method-picker.vue'; +import ParameterSelectionExpansionControl from './parameter-selection-expansion-control.vue'; + +export default { + name: 'ParameterSelection', + components: { + MethodPicker, + ParameterSelectionExpansionControl + }, + setup() { + const reduxStore = inject('store'); + const siteno = inject('siteno'); + const agencyCode = inject('agencyCode'); + + const expandedParameterCode = ref(getSelectedParameterCode(reduxStore.getState())); + + const getAvailableParameterData = createSelector( + getAvailableParameters, + (parameters) => { + return parameters.map((parameter) => { + const parameterForWaterAlertUrl = parameter.parameterCode.includes(config.CALCULATED_TEMPERATURE_VARIABLE_CODE) ? + parameter.parameterCode.replace(config.CALCULATED_TEMPERATURE_VARIABLE_CODE) : parameter.parameterCode; + return { + ...parameter, + waterAlertUrl: parameter.waterAlert.hasWaterAlert ? + `${config.WATERALERT_SUBSCRIPTION}/?site_no=${siteno}&parm=${parameterForWaterAlertUrl}` : '' + }; + }); + } + ); + const state = useState({ + parameters: getAvailableParameterData, + sortedIvMethods: getSortedIVMethods, + selectedParameterCode: getSelectedParameterCode + }); + + const actions = useActions({ + setSelectedParameterCode, + setSelectedIVMethodID, + retrieveHydrographData + }); + + function toggleExpansionRow(parameterCode, expandRow) { + expandedParameterCode.value = expandRow ? parameterCode : ''; + } + + function selectParameter(parameterCode) { + actions.setSelectedParameterCode(parameterCode); + expandedParameterCode.value = parameterCode; + + showDataIndicators(true, reduxStore); + actions.retrieveHydrographData(siteno, agencyCode, getInputsForRetrieval(reduxStore.getState()), true) + .then(() => { + const sortedMethods = getSortedIVMethods(reduxStore.getState()); + if (sortedMethods && sortedMethods.methods.length) { + actions.setSelectedIVMethodID(sortedMethods.methods[0].methodID); + } + showDataIndicators(false, reduxStore); + }); + } + + function updateSelectedMethod(methodId) { + actions.setSelectedIVMethodID(methodId); + } + + return { + ...state, + expandedParameterCode, + selectParameter, + toggleExpansionRow, + updateSelectedMethod + }; + } +}; +</script> diff --git a/assets/src/scripts/monitoring-location/selectors/hydrograph-data-selector.js b/assets/src/scripts/monitoring-location/selectors/hydrograph-data-selector.js index 4ded7fc1ac3fdd4bad9787ca8f150d40ecc95552..f0e0ec006772c186e4951618fed2ac9f85e8f657 100644 --- a/assets/src/scripts/monitoring-location/selectors/hydrograph-data-selector.js +++ b/assets/src/scripts/monitoring-location/selectors/hydrograph-data-selector.js @@ -12,7 +12,6 @@ import config from 'ui/config'; */ export const getThresholds = state => state.hydrographData.thresholds || null; - /* * Returns a selector function which returns the time range for the timeRangeKind. * @param {String} timeRangeKind - 'current' or 'prioryear' @@ -28,10 +27,10 @@ export const getTimeRange = memoize((timeRangeKind) => state => state.hydrograph export const getIVData = memoize((dataKind) => state => state.hydrographData[`${dataKind}IVData`] || null); /* - * Returns a selector function which returns the median statistics data + * Returns a selector function which returns the daily statistics data * @return {Function} */ -export const getMedianStatisticsData = state => state.hydrographData.medianStatisticsData || null; +export const getStatisticsData = state => state.hydrographData.statisticsData || null; /* @@ -75,7 +74,7 @@ export const getPrimaryMethods = createSelector( */ export const getPrimaryMedianStatisticsData = createSelector( getTimeRange('current'), - getMedianStatisticsData, + getStatisticsData, (timeRange, stats) => { if (!stats || !timeRange) { return null; diff --git a/assets/src/scripts/monitoring-location/selectors/hydrograph-data-selector.test.js b/assets/src/scripts/monitoring-location/selectors/hydrograph-data-selector.test.js index 41622c5a0ab3b9aa43e49d3063ab77fab9dfd92a..ea75eb453296832ab0ed60fafda054528c634595 100644 --- a/assets/src/scripts/monitoring-location/selectors/hydrograph-data-selector.test.js +++ b/assets/src/scripts/monitoring-location/selectors/hydrograph-data-selector.test.js @@ -1,8 +1,7 @@ import config from 'ui/config'; -import {getThresholds, getTimeRange, getIVData, getMedianStatisticsData, getIVPrimaryParameter, - getPrimaryMethods, getPrimaryMedianStatisticsData, - getPrimaryMedianStatisticsValueRange +import {getThresholds, getTimeRange, getIVData, getStatisticsData, getIVPrimaryParameter, + getPrimaryMethods, getPrimaryMedianStatisticsData, getPrimaryMedianStatisticsValueRange } from './hydrograph-data-selector'; describe('monitoring-location/selectors/hydrograph-data-selectors', () => { @@ -44,14 +43,26 @@ describe('monitoring-location/selectors/hydrograph-data-selectors', () => { } }; - const TEST_MEDIAN_DATA = { + const TEST_STATS_DATA = { '153885': [ - {month_nu: 2, day_nu: 1, p50_va: 16, ts_id: '153885', loc_web_ds: 'Method1', begin_yr: '2011', end_yr: '2020'}, - {month_nu: 2, day_nu: 2, p50_va: 16.2, ts_id: '153885', loc_web_ds: 'Method1', begin_yr: '2011', end_yr: '2020'}, - {month_nu: 2, day_nu: 3, p50_va: 15.9, ts_id: '153885', loc_web_ds: 'Method1', begin_yr: '2011', end_yr: '2020'}, - {month_nu: 2, day_nu: 4, p50_va: 16.3, ts_id: '153885', loc_web_ds: 'Method1', begin_yr: '2011', end_yr: '2020'}, - {month_nu: 2, day_nu: 4, p50_va: 16.4, ts_id: '153885', loc_web_ds: 'Method1', begin_yr: '2011', end_yr: '2020'}, - {month_nu: 2, day_nu: 4, p50_va: 16.5, ts_id: '153885', loc_web_ds: 'Method1', begin_yr: '2011', end_yr: '2020'} + {month_nu: 2, day_nu: 1, p50_va: 16, ts_id: '153885', loc_web_ds: 'Method1', begin_yr: '2011', end_yr: '2020', max_va_yr: '2020', + max_va: '273', min_va_yr: '2006', min_va: '55.5', mean_va: '153', p05_va: '', p10_va: '61', p20_va: '88', p25_va: '100', + p75_va: '224', p80_va: '264', p90_va: '271', p95_va: ''}, + {month_nu: 2, day_nu: 2, p50_va: 16.2, ts_id: '153885', loc_web_ds: 'Method1', begin_yr: '2011', end_yr: '2020', max_va_yr: '2020', + max_va: '273', min_va_yr: '2006', min_va: '55.5', mean_va: '153', p05_va: '', p10_va: '61', p20_va: '88', p25_va: '100', + p75_va: '224', p80_va: '264', p90_va: '271', p95_va: ''}, + {month_nu: 2, day_nu: 3, p50_va: 15.9, ts_id: '153885', loc_web_ds: 'Method1', begin_yr: '2011', end_yr: '2020', max_va_yr: '2020', + max_va: '273', min_va_yr: '2006', min_va: '55.5', mean_va: '153', p05_va: '', p10_va: '61', p20_va: '88', p25_va: '100', + p75_va: '224', p80_va: '264', p90_va: '271', p95_va: ''}, + {month_nu: 2, day_nu: 4, p50_va: 16.3, ts_id: '153885', loc_web_ds: 'Method1', begin_yr: '2011', end_yr: '2020', max_va_yr: '2020', + max_va: '273', min_va_yr: '2006', min_va: '55.5', mean_va: '153', p05_va: '', p10_va: '61', p20_va: '88', p25_va: '100', + p75_va: '224', p80_va: '264', p90_va: '271', p95_va: ''}, + {month_nu: 2, day_nu: 4, p50_va: 16.4, ts_id: '153885', loc_web_ds: 'Method1', begin_yr: '2011', end_yr: '2020', max_va_yr: '2020', + max_va: '273', min_va_yr: '2006', min_va: '55.5', mean_va: '153', p05_va: '', p10_va: '61', p20_va: '88', p25_va: '100', + p75_va: '224', p80_va: '264', p90_va: '271', p95_va: ''}, + {month_nu: 2, day_nu: 4, p50_va: 16.5, ts_id: '153885', loc_web_ds: 'Method1', begin_yr: '2011', end_yr: '2020', max_va_yr: '2020', + max_va: '273', min_va_yr: '2006', min_va: '55.5', mean_va: '153', p05_va: '', p10_va: '61', p20_va: '88', p25_va: '100', + p75_va: '224', p80_va: '264', p90_va: '271', p95_va: ''} ] }; @@ -136,19 +147,19 @@ describe('monitoring-location/selectors/hydrograph-data-selectors', () => { }); }); - describe('getMedianStatistics', () => { - it('Returns null if no median data', () => { - expect(getMedianStatisticsData({ + describe('getStatisticsData', () => { + it('Returns null if no stats data', () => { + expect(getStatisticsData({ hydrographData: {} })).toBeNull(); }); - it('Returns the median data', () => { - expect(getMedianStatisticsData({ + it('Returns the stats data', () => { + expect(getStatisticsData({ hydrographData: { - medianStatisticsData: TEST_MEDIAN_DATA + statisticsData: TEST_STATS_DATA } - })).toEqual(TEST_MEDIAN_DATA); + })).toEqual(TEST_STATS_DATA); }); }); @@ -198,7 +209,7 @@ describe('monitoring-location/selectors/hydrograph-data-selectors', () => { start: 1612280374000, end: 1612539574000 }, - medianStatisticsData: TEST_MEDIAN_DATA + statisticsData: TEST_STATS_DATA } })).toEqual({ '153885': { @@ -230,9 +241,10 @@ describe('monitoring-location/selectors/hydrograph-data-selectors', () => { start: 1612280374000, end: 1612539574000 }, - medianStatisticsData: TEST_MEDIAN_DATA + statisticsData: TEST_STATS_DATA } })).toEqual([15.9, 16.3]); }); }); + }); \ No newline at end of file diff --git a/assets/src/scripts/monitoring-location/selectors/hydrograph-parameters-selector.js b/assets/src/scripts/monitoring-location/selectors/hydrograph-parameters-selector.js new file mode 100644 index 0000000000000000000000000000000000000000..2f03093ad6de7f7f8c95a25f0da2fa06e9668590 --- /dev/null +++ b/assets/src/scripts/monitoring-location/selectors/hydrograph-parameters-selector.js @@ -0,0 +1,23 @@ +import {createSelector} from 'reselect'; + +import {getSelectedParameterCode} from 'ml/selectors/hydrograph-state-selector'; + +/* + * The following selector functions return a function which returns the selected data. + */ +export const getHydrographParameters = state => state.hydrographParameters || {}; + +/* + * Returns a selector function which returns the most recently reported value for the primary parameter + * @return {function} + */ +export const latestSelectedParameterValue = createSelector( + getHydrographParameters, + getSelectedParameterCode, + (parameterObject, parameterCode) => { + if (!parameterObject || !Object.keys(parameterObject).length || !parameterCode) { + return null; + } + return parameterObject[parameterCode].latestValue || null; + } +); \ No newline at end of file diff --git a/assets/src/scripts/monitoring-location/selectors/hydrograph-parameters-selector.test.js b/assets/src/scripts/monitoring-location/selectors/hydrograph-parameters-selector.test.js new file mode 100644 index 0000000000000000000000000000000000000000..8e235d9156727c612bd90468eff835de2026e998 --- /dev/null +++ b/assets/src/scripts/monitoring-location/selectors/hydrograph-parameters-selector.test.js @@ -0,0 +1,100 @@ +import { + getHydrographParameters, + latestSelectedParameterValue +} from './hydrograph-parameters-selector'; + +describe('monitoring-location/selectors/hydrograph-parameters-selector', () => { + + describe('getHydrographParameters', () => { + it('if state doesn\'t have hydrograph parameters, an empty object is returned', () => { + expect(getHydrographParameters({})).toEqual({}); + }); + + it('if state has hydrograph parameters, the data is returned', () => { + expect(getHydrographParameters({ + hydrographParameters: { + '72255': { + parameterCode: '72255', + name: 'Mean water velocity for discharge computation, feet per second', + description: 'Mean water velocity for discharge computation, feet per second', + unit: 'ft/sec', + hasIVData: true, + latestValue: '0.45' + } + } + })).toEqual({ + '72255': { + parameterCode: '72255', + name: 'Mean water velocity for discharge computation, feet per second', + description: 'Mean water velocity for discharge computation, feet per second', + unit: 'ft/sec', + hasIVData: true, + latestValue: '0.45' + } + }); + }); + }); + + describe('latestSelectedParameterValue', () => { + it('if state doesn\'t have hydrograph parameters, null is returned', () => { + expect(latestSelectedParameterValue({ + hydrographState: { + selectedParameterCode: '00060' + } + })).toEqual(null); + }); + + it('if state doesn\t have a selected parameter code, null is returned', () => { + expect(latestSelectedParameterValue({ + hydrographParameters: { + '72255': { + parameterCode: '72255', + name: 'Mean water velocity for discharge computation, feet per second', + description: 'Mean water velocity for discharge computation, feet per second', + unit: 'ft/sec', + hasIVData: true, + latestValue: '0.45' + } + }, + hydrographState: { + selectedParameterCode: '' + } + })).toEqual(null); + }); + + it('returns the latest selected parameter value', () => { + expect(latestSelectedParameterValue({ + hydrographParameters: { + '72255': { + parameterCode: '72255', + name: 'Mean water velocity for discharge computation, feet per second', + description: 'Mean water velocity for discharge computation, feet per second', + unit: 'ft/sec', + hasIVData: true, + latestValue: '0.45' + } + }, + hydrographState: { + selectedParameterCode: '72255' + } + })).toEqual('0.45'); + }); + + it('returns null if there is no latest value', () => { + expect(latestSelectedParameterValue({ + hydrographParameters: { + '72255': { + parameterCode: '72255', + name: 'Mean water velocity for discharge computation, feet per second', + description: 'Mean water velocity for discharge computation, feet per second', + unit: 'ft/sec', + hasIVData: true + } + }, + hydrographState: { + selectedParameterCode: '72255' + } + })).toEqual(null); + }); + }); +}); \ No newline at end of file diff --git a/assets/src/scripts/monitoring-location/selectors/hydrograph-state-selector.js b/assets/src/scripts/monitoring-location/selectors/hydrograph-state-selector.js index 0db27ca56879648ca9a5deacbcf14dc87664f6da..ee41bb03ae4648e49b110919ad338e62f0fad10b 100644 --- a/assets/src/scripts/monitoring-location/selectors/hydrograph-state-selector.js +++ b/assets/src/scripts/monitoring-location/selectors/hydrograph-state-selector.js @@ -28,8 +28,7 @@ export const getInputsForRetrieval = createSelector( getSelectedParameterCode, getSelectedTimeSpan, isCompareIVDataVisible, - isMedianDataVisible, - (parameterCode, selectedTimeSpan, loadCompare, loadMedian) => { + (parameterCode, selectedTimeSpan, loadCompare) => { const timeSpanIsDuration = typeof selectedTimeSpan === 'string'; const period = timeSpanIsDuration && selectedTimeSpan.endsWith('D') ? selectedTimeSpan : null; let startTime = null; @@ -60,8 +59,7 @@ export const getInputsForRetrieval = createSelector( period, startTime, endTime, - loadCompare, - loadMedian + loadCompare }; } ); diff --git a/assets/src/scripts/monitoring-location/selectors/hydrograph-state-selector.test.js b/assets/src/scripts/monitoring-location/selectors/hydrograph-state-selector.test.js index 544833a48782a8fa49d8ea82ae2e03fac2811cf1..84302231c384724838027472c9f93ad409aa075c 100644 --- a/assets/src/scripts/monitoring-location/selectors/hydrograph-state-selector.test.js +++ b/assets/src/scripts/monitoring-location/selectors/hydrograph-state-selector.test.js @@ -124,7 +124,6 @@ describe('monitoring-location/selectors/hydrograph-state-selector', () => { }); }); - describe('getInputsForRetrieval', () => { config.ivPeriodOfRecord = { '00010': { @@ -176,8 +175,7 @@ describe('monitoring-location/selectors/hydrograph-state-selector', () => { period: 'P30D', startTime: null, endTime: null, - loadCompare: false, - loadMedian: true + loadCompare: false }); }); it('Return expects inputs when selectedTimeSpan is a date range', () => { @@ -196,8 +194,7 @@ describe('monitoring-location/selectors/hydrograph-state-selector', () => { period: null, startTime: '2021-02-01T00:00:00.000-06:00', endTime: '2021-02-06T23:59:59.999-06:00', - loadCompare: true, - loadMedian: false + loadCompare: true }); }); @@ -214,8 +211,7 @@ describe('monitoring-location/selectors/hydrograph-state-selector', () => { period: null, startTime: '2011-10-01T00:00:00.000-05:00', endTime: '2021-10-01T05:00:00.000-05:00', - loadCompare: true, - loadMedian: false + loadCompare: true }); }); @@ -232,8 +228,7 @@ describe('monitoring-location/selectors/hydrograph-state-selector', () => { period: null, startTime: '2019-01-01T00:00:00.000-06:00', endTime: '2021-10-01T05:00:00.000-05:00', - loadCompare: true, - loadMedian: false + loadCompare: true }); }); }); diff --git a/assets/src/scripts/monitoring-location/store/hydrograph-data.js b/assets/src/scripts/monitoring-location/store/hydrograph-data.js index f3b59516624f445a6cfce5c73ea755730fbe0fff..4990f577500c080b6d9a4495887ca1f36b811817 100644 --- a/assets/src/scripts/monitoring-location/store/hydrograph-data.js +++ b/assets/src/scripts/monitoring-location/store/hydrograph-data.js @@ -42,6 +42,16 @@ const clearHydrographData = function() { }; }; +/* + * Synchronous Redux action which clears all data except for stats data + * @return {Object} Redux action + */ +const clearNonStatsHydrographData = function() { + return { + type: 'CLEAR_NON_STATS_HYDROGRAPH_DATA' + }; +}; + /* * Synchronous Redux action which sets the IV data for the dataKind * @param {Object} ivData @@ -58,13 +68,13 @@ const addIVHydrographData = function(dataKind, ivData) { }; /* - * Synchronous Redux action which sets the median data + * Synchronous Redux action which sets the statistics data * @param {Object} statsData * @return {Object} Redux action */ -const addMedianStatisticsData = function(statsData) { +const addStatisticsData = function(statsData) { return { - type: 'ADD_MEDIAN_STATISTICS_DATA', + type: 'ADD_STATISTICS_DATA', statsData }; }; @@ -202,30 +212,40 @@ export const retrievePriorYearIVData = function(siteno, {parameterCode, startTim * @param {String} parameterCode * @return {Function} that returns a Promise */ -export const retrieveMedianStatistics = function(siteno, parameterCode) { +export const retrieveStatistics = function(siteno, parameterCode) { return function(dispatch, getState) { - if ('medianStatisticsData' in getState().hydrographData) { + if ('statisticsData' in getState().hydrographData) { return Promise.resolve(); } else { const isCalculatedParameterCode = isCalculatedTemperature(parameterCode); const parameterToFetch = getParameterToFetch(parameterCode); - return fetchSiteStatistics({siteno: siteno, statType: 'median', parameterCode: parameterToFetch}) + return fetchSiteStatistics({siteno: siteno, statType: 'All', parameterCode: parameterToFetch}) .then(stats => { let resultStats = {}; if (parameterToFetch in stats) { Object.keys(stats[parameterToFetch]).forEach(methodID => { resultStats[methodID] = stats[parameterToFetch][methodID].map(stat => { const p50Va = isCalculatedParameterCode ? convertCelsiusToFahrenheit(stat.p50_va) : parseFloat(stat.p50_va); + const p25Va = isCalculatedParameterCode ? convertCelsiusToFahrenheit(stat.p25_va) : parseFloat(stat.p25_va); + const p75Va = isCalculatedParameterCode ? convertCelsiusToFahrenheit(stat.p75_va) : parseFloat(stat.p75_va); + const meanVa = isCalculatedParameterCode ? convertCelsiusToFahrenheit(stat.mean_va) : parseFloat(stat.mean_va); + const minVa = isCalculatedParameterCode ? convertCelsiusToFahrenheit(stat.min_va) : parseFloat(stat.min_va); + const maxVa = isCalculatedParameterCode ? convertCelsiusToFahrenheit(stat.max_va) : parseFloat(stat.max_va); return { ...stat, month_nu: parseInt(stat.month_nu), day_nu: parseInt(stat.day_nu), - p50_va: p50Va + p50_va: p50Va, + p25_va: p25Va, + p75_va: p75Va, + mean_va: meanVa, + min_va: minVa, + max_va: maxVa }; }); }); } - dispatch(addMedianStatisticsData(resultStats)); + dispatch(addStatisticsData(resultStats)); }); } }; @@ -294,7 +314,7 @@ export const retrieveHydrographThresholds = function(agencySiteNumberCode, param /* * Asynchronous Redux action which clears all data and then retrieves all of the data needed to display the hydrograph. - * The IV and groundwater levels are automatically loaded if available. Compare and median data + * The IV and groundwater levels are automatically loaded if available. Compare and statistics data * are loaded if requested * @param {String} siteno * @param {String} parameterCode @@ -302,14 +322,18 @@ export const retrieveHydrographThresholds = function(agencySiteNumberCode, param * @param {String} startTime - ISO 8601 time string * @param {String} endTime - ISO 8601 time string * @param {Boolean} loadCompare - * @param {Boolean} loadMedian + * @param {Boolean} loadStats * @return {Function} that returns a promise */ -export const retrieveHydrographData = function(siteno, agencyCode, {parameterCode, period, startTime, endTime, loadCompare, loadMedian}) { +export const retrieveHydrographData = function(siteno, agencyCode, {parameterCode, period, startTime, endTime, loadCompare}, loadStats = false) { return function(dispatch) { const parameterToFetch = getParameterToFetch(parameterCode); const hasIVData = config.ivPeriodOfRecord && parameterToFetch in config.ivPeriodOfRecord; - dispatch(clearHydrographData()); + if (loadStats) { + dispatch(clearHydrographData()); + } else { + dispatch(clearNonStatsHydrographData()); + } let timeRange; if (period && period !== 'custom') { @@ -341,9 +365,10 @@ export const retrieveHydrographData = function(siteno, agencyCode, {parameterCod endTime: timeRange.end }))); } - if (hasIVData && loadMedian) { + + if (hasIVData && loadStats) { fetchPromises.push(dispatch( - retrieveMedianStatistics(siteno, parameterCode))); + retrieveStatistics(siteno, parameterCode))); } return Promise.all(fetchPromises); @@ -365,10 +390,10 @@ export const hydrographDataReducer = function(hydrographData = {}, action) { newData[`${action.dataKind}IVData`] = action.ivData; return Object.assign({}, hydrographData, newData); } - case 'ADD_MEDIAN_STATISTICS_DATA': { + case 'ADD_STATISTICS_DATA': { return { ...hydrographData, - medianStatisticsData: action.statsData + statisticsData: action.statsData }; } case 'ADD_THRESHOLDS': { @@ -378,6 +403,13 @@ export const hydrographDataReducer = function(hydrographData = {}, action) { thresholds: action.thresholds }; } + case 'CLEAR_NON_STATS_HYDROGRAPH_DATA' : { + return { + statisticsData: { + ...hydrographData.statisticsData + } + }; + } default: return hydrographData; } diff --git a/assets/src/scripts/monitoring-location/store/hydrograph-data.test.js b/assets/src/scripts/monitoring-location/store/hydrograph-data.test.js index 34cb28294f428ff912595571a81223790aba6bb7..8a84a68950e10a5c0628d72ab7d42c599ffacc90 100644 --- a/assets/src/scripts/monitoring-location/store/hydrograph-data.test.js +++ b/assets/src/scripts/monitoring-location/store/hydrograph-data.test.js @@ -19,7 +19,7 @@ import config from 'ui/config'; import { hydrographDataReducer, retrieveHydrographData, - retrieveMedianStatistics, + retrieveStatistics, retrievePriorYearIVData } from './hydrograph-data'; @@ -69,9 +69,10 @@ describe('monitoring-location/store/hydrograph-data', () => { period: 'P7D', startTime: null, endTime: null, - loadCompare: false, - loadMedian: false - })); + loadCompare: false + }, + true + )); const mockIVCalls = ivDataService.fetchTimeSeries.mock.calls; const mockStatsCalls = statisticsDataService.fetchSiteStatistics.mock.calls; @@ -83,7 +84,7 @@ describe('monitoring-location/store/hydrograph-data', () => { startTime: null, endTime: null }]); - expect(mockStatsCalls).toHaveLength(0); + expect(mockStatsCalls).toHaveLength(1); }); it('Expects to retrieve all data when all are available or requested', () => { @@ -96,9 +97,10 @@ describe('monitoring-location/store/hydrograph-data', () => { period: 'P7D', startTime: null, endTime: null, - loadCompare: true, - loadMedian: true - })); + loadCompare: true + }, + true + )); const mockIVCalls = ivDataService.fetchTimeSeries.mock.calls; const mockStatsCalls = statisticsDataService.fetchSiteStatistics.mock.calls; @@ -120,7 +122,7 @@ describe('monitoring-location/store/hydrograph-data', () => { expect(mockStatsCalls).toHaveLength(1); expect(mockStatsCalls[0][0]).toEqual({ siteno: '11112222', - statType: 'median', + statType: 'All', parameterCode: '00060' }); }); @@ -135,9 +137,10 @@ describe('monitoring-location/store/hydrograph-data', () => { period: 'P7D', startTime: null, endTime: null, - loadCompare: false, - loadMedian: true - })); + loadCompare: false + }, + true + )); const mockIVCalls = ivDataService.fetchTimeSeries.mock.calls; const mockStatsCalls = statisticsDataService.fetchSiteStatistics.mock.calls; @@ -152,11 +155,29 @@ describe('monitoring-location/store/hydrograph-data', () => { expect(mockStatsCalls).toHaveLength(1); expect(mockStatsCalls[0][0]).toEqual({ siteno: '11112222', - statType: 'median', + statType: 'All', parameterCode: '00060' }); }); + it('Does not call the stats service if loadStats is false', () => { + config.ivPeriodOfRecord = { + '00060': {begin_date: '2010-01-01', end_date: '2020-01-01'} + }; + const mockStatsCalls = statisticsDataService.fetchSiteStatistics.mock.calls; + store.dispatch(retrieveHydrographData('11112222', 'USGS', { + parameterCode: '00060', + period: 'P7D', + startTime: null, + endTime: null, + loadCompare: true + }, + false + )); + + expect(mockStatsCalls).toHaveLength(0); + }); + it('Loads compare data if requested and period is not a custom period', () => { config.ivPeriodOfRecord = { '00060': {begin_date: '2010-01-01', end_date: '2020-01-01'} @@ -166,9 +187,10 @@ describe('monitoring-location/store/hydrograph-data', () => { period: 'P7D', startTime: null, endTime: null, - loadCompare: true, - loadMedian: true - })); + loadCompare: true + }, + false + )); expect(ivDataService.fetchTimeSeries.mock.calls).toHaveLength(2); }); @@ -182,9 +204,10 @@ describe('monitoring-location/store/hydrograph-data', () => { period: 'P10D', startTime: null, endTime: null, - loadCompare: true, - loadMedian: true - })); + loadCompare: true + }, + false + )); expect(ivDataService.fetchTimeSeries.mock.calls).toHaveLength(1); }); @@ -198,9 +221,10 @@ describe('monitoring-location/store/hydrograph-data', () => { period: null, startTime: '2020-01-01', endTime: '2020-01-31', - loadCompare: true, - loadMedian: true - })); + loadCompare: true + }, + false + )); expect(ivDataService.fetchTimeSeries.mock.calls).toHaveLength(1); }); @@ -226,9 +250,10 @@ describe('monitoring-location/store/hydrograph-data', () => { period: 'P7D', startTime: null, endTime: null, - loadCompare: false, - loadMedian: true - })).then(() => { + loadCompare: false + }, + true + )).then(() => { const hydrographData = store.getState().hydrographData; expect(hydrographData.currentTimeRange).toEqual({ @@ -260,9 +285,10 @@ describe('monitoring-location/store/hydrograph-data', () => { period: 'P7D', startTime: null, endTime: null, - loadCompare: false, - loadMedian: true - })).then(() => { + loadCompare: false + }, + false + )).then(() => { const hydrographData = store.getState().hydrographData; expect(hydrographData.primaryIVData.values['158049'].points[669]).toEqual({ @@ -289,9 +315,10 @@ describe('monitoring-location/store/hydrograph-data', () => { period: 'P7D', startTime: null, endTime: null, - loadCompare: false, - loadMedian: true - })).then(() => { + loadCompare: false + }, + false + )).then(() => { const hydrographData = store.getState().hydrographData; expect(hydrographData.primaryIVData.values['158049'].points[667]).toEqual({ @@ -320,9 +347,10 @@ describe('monitoring-location/store/hydrograph-data', () => { period: 'P7D', startTime: null, endTime: null, - loadCompare: false, - loadMedian: true - })); + loadCompare: false + }, + false + )); const mockIVCalls = ivDataService.fetchTimeSeries.mock.calls; expect(mockIVCalls).toHaveLength(1); @@ -341,9 +369,10 @@ describe('monitoring-location/store/hydrograph-data', () => { period: 'P7D', startTime: null, endTime: null, - loadCompare: false, - loadMedian: true - })).then(() => { + loadCompare: false + }, + false + )).then(() => { const hydrographData = store.getState().hydrographData; expect(hydrographData.primaryIVData.parameter).toEqual({ @@ -375,9 +404,10 @@ describe('monitoring-location/store/hydrograph-data', () => { period: 'P7D', startTime: null, endTime: null, - loadCompare: false, - loadMedian: false - })); + loadCompare: false + }, + false + )); }); it('Expects a call to retrievePriorYearIVData sets the prioryearTimeRange and fetches the data', () => { @@ -426,7 +456,7 @@ describe('monitoring-location/store/hydrograph-data', () => { }); }); - describe('retrieveMedianStatistics', () => { + describe('retrieveStatistics', () => { beforeEach(() => { ivDataService.fetchTimeSeries = jest.fn().mockReturnValue(Promise.resolve(JSON.parse(MOCK_IV_DATA))); @@ -443,28 +473,29 @@ describe('monitoring-location/store/hydrograph-data', () => { period: 'P7D', startTime: null, endTime: null, - loadCompare: false, - loadMedian: false - })); + loadCompare: false + }, + true + )); }); - it('Expects median data to fetched and stored', () => { - return store.dispatch(retrieveMedianStatistics('11112222', '00060')).then(() => { + it('Expects statistics data to fetched and stored', () => { + return store.dispatch(retrieveStatistics('11112222', '00060')).then(() => { const mockStatsCalls = statisticsDataService.fetchSiteStatistics.mock.calls; expect(mockStatsCalls).toHaveLength(1); expect(mockStatsCalls[0][0]).toEqual({ siteno: '11112222', - statType: 'median', + statType: 'All', parameterCode: '00060' }); - expect(store.getState().hydrographData.medianStatisticsData).toBeDefined(); + expect(store.getState().hydrographData.statisticsData).toBeDefined(); }); }); it('Expects a second call to median data does not fetch the data again', () => { - const firstRetrieve = store.dispatch(retrieveMedianStatistics('11112222', '00060')); + const firstRetrieve = store.dispatch(retrieveStatistics('11112222', '00060')); return firstRetrieve.then(() => { - store.dispatch(retrieveMedianStatistics('11112222', '00060')) + store.dispatch(retrieveStatistics('11112222', '00060')) .then(() => { const mockStatsCalls = statisticsDataService.fetchSiteStatistics.mock.calls; expect(mockStatsCalls).toHaveLength(1); diff --git a/assets/src/scripts/monitoring-location/store/hydrograph-parameters.js b/assets/src/scripts/monitoring-location/store/hydrograph-parameters.js index 1a5d610d0ff2ff3c518d2e79016a7ba7fc959975..8a05b74c106e9a948f098defae451141e3a7a56a 100644 --- a/assets/src/scripts/monitoring-location/store/hydrograph-parameters.js +++ b/assets/src/scripts/monitoring-location/store/hydrograph-parameters.js @@ -40,7 +40,8 @@ export const retrieveHydrographParameters = function(siteno, agencyCd) { name: ts.variable.variableName, description: ts.variable.variableDescription, unit: ts.variable.unit.unitCode, - hasIVData: true + hasIVData: true, + latestValue: ts.values[0].value[0].value }; // If the parameter is for gage height set the initial flood gage height if (parameterCode === config.GAGE_HEIGHT_PARAMETER_CODE) { diff --git a/assets/src/scripts/monitoring-location/store/hydrograph-parameters.test.js b/assets/src/scripts/monitoring-location/store/hydrograph-parameters.test.js index e2065fedbdfc8e4e5b60ceca6642c7d525b454d2..213e64379f09a74a0c7cce29f020531d03c038b4 100644 --- a/assets/src/scripts/monitoring-location/store/hydrograph-parameters.test.js +++ b/assets/src/scripts/monitoring-location/store/hydrograph-parameters.test.js @@ -96,8 +96,11 @@ describe('monitoring-location/store/hydrograph-parameters', () => { const hydrographParameters = store.getState().hydrographParameters; expect(hydrographParameters['00010']).toBeDefined(); + expect(hydrographParameters['00010'].latestValue).toBeDefined(); expect(hydrographParameters['00010F']).toBeDefined(); + expect(hydrographParameters['00010F'].latestValue).toBeDefined(); expect(hydrographParameters['72019']).toBeDefined(); + expect(hydrographParameters['72019'].latestValue).toBeDefined(); expect(hydrographParameters['62610']).toBeDefined(); expect(hydrographParameters['62611']).toBeDefined(); expect(hydrographParameters['00010'].hasIVData).toBeTruthy(); diff --git a/assets/src/scripts/monitoring-location/store/hydrograph-state.js b/assets/src/scripts/monitoring-location/store/hydrograph-state.js index a0ad27b57785960721f62a2dbb02d0b5cde25091..bc5b09a5dcd4a1fc98d219b8b2c9241f7a454cd0 100644 --- a/assets/src/scripts/monitoring-location/store/hydrograph-state.js +++ b/assets/src/scripts/monitoring-location/store/hydrograph-state.js @@ -112,6 +112,7 @@ export const clearGraphBrushOffset = function() { }; }; + export const hydrographStateReducer = function(hydrographState = INITIAL_STATE, action) { let hasIVData; let hasGWData; @@ -148,7 +149,6 @@ export const hydrographStateReducer = function(hydrographState = INITIAL_STATE, } else { return { ...hydrographState, - selectedParameterCode: action.parameterCode }; } diff --git a/assets/src/scripts/monitoring-location/store/hydrograph-state.test.js b/assets/src/scripts/monitoring-location/store/hydrograph-state.test.js index 8c3a66ee72aa202937d139f0bc6967327b6b61af..11d144fed230d95ef788458e978cb944f8f1cd8d 100644 --- a/assets/src/scripts/monitoring-location/store/hydrograph-state.test.js +++ b/assets/src/scripts/monitoring-location/store/hydrograph-state.test.js @@ -109,6 +109,7 @@ describe('monitoring-location/store/hydrograph-state', () => { expect(state.selectedParameterCode).toBe('62611'); expect(state.selectedTimeSpan).toBe('periodOfRecord'); }); + }); describe('setSelectedIVMethodID', () => { diff --git a/assets/src/scripts/uswds-components/alert.test.js b/assets/src/scripts/uswds-components/alert.test.js new file mode 100644 index 0000000000000000000000000000000000000000..802310d4dff5b9e0d508d2395ae35c474bfebc77 --- /dev/null +++ b/assets/src/scripts/uswds-components/alert.test.js @@ -0,0 +1,119 @@ +import {mount} from '@vue/test-utils'; + +import USWDSAlert from './alert.vue'; + + +describe('components/uswds/alert', () => { + it('expects a success message', () => { + const wrapper = mount(USWDSAlert, { + slots: { + default: '<p class="usa-alert__text">content</p>' + }, + propsData: { + alertType: 'success', + alertTitle: 'This Alert' + } + }); + const wrapperClasses = wrapper.classes(); + expect(wrapper.find('.usa-alert__body').exists()).toBe(true); + expect(wrapper.find('.usa-alert__heading').text()).toContain('This Alert'); + expect(wrapperClasses).toContain('usa-alert--success'); + expect(wrapperClasses).not.toContain('usa-alert--slim'); + expect(wrapperClasses).not.toContain('usa-alert--no-icon'); + expect(wrapper.find('.usa-alert__text').text()).toBe('content'); + }); + + it('expects a error message with no alert icon', () => { + const wrapper = mount(USWDSAlert, { + slots: { + default: '<p>content</p>' + }, + props: { + alertType: 'error', + showAlertIcon: false + } + }); + + + expect(wrapper.find('.usa-alert__body').exists()).toBe(true); + expect(wrapper.find('.usa-alert__heading').exists()).toBe(false); + const wrapperClasses = wrapper.classes(); + expect(wrapperClasses).toContain('usa-alert--error'); + expect(wrapperClasses).toContain('usa-alert--no-icon'); + }); + + it('expects a info message', () => { + const wrapper = mount(USWDSAlert, { + slots: { + default: '<p>content</p>' + }, + props: { + alertType: 'info', + alertTitle: 'Notice' + } + }); + + expect(wrapper.find('.usa-alert__body').exists()).toBe(true); + expect(wrapper.find('.usa-alert__body').text()).toContain('Notice'); + expect(wrapper.classes()).toContain('usa-alert--info'); + }); + + it('expects that the alert component will be slim', () => { + const wrapper = mount(USWDSAlert, { + slots: { + default: '<p>content</p>' + }, + props: { + alertType: 'info', + slimAlert: true + } + }); + + const wrapperClasses = wrapper.classes(); + expect(wrapperClasses).toContain('usa-alert--info'); + expect(wrapperClasses).toContain('usa-alert--slim'); + }); + + it('expects no close button', () => { + const wrapper = mount(USWDSAlert, { + slots: { + default: '<p>content</p>' + }, + props: { + alertType: 'info' + } + }); + + expect(wrapper.findAll('button')).toHaveLength(0); + }); + + it('expects a close icon', () => { + const wrapper = mount(USWDSAlert, { + slots: { + default: '<p>content</p>' + }, + props: { + alertType: 'info', + showClose: true, + staticRoot: '/fake/' + } + }); + expect(wrapper.findAll('.message-close')).toHaveLength(1); + expect(wrapper.find('.message-close').html()).toContain('/fake/'); + }); + + it('expects a click on the close button to emit event', async() => { + const wrapper = mount(USWDSAlert, { + slots: { + default: '<p>content</p>' + }, + props: { + alertType: 'info', + showClose: true + } + }); + expect(wrapper.findAll('.message-close')).toHaveLength(1); + await wrapper.find('.message-close').trigger('click'); + expect(wrapper.emitted().closeAlert).toHaveLength(1); + }); +}); diff --git a/assets/src/scripts/uswds-components/alert.vue b/assets/src/scripts/uswds-components/alert.vue new file mode 100644 index 0000000000000000000000000000000000000000..0a27e72f9c4ac283804af0ce185154412951220b --- /dev/null +++ b/assets/src/scripts/uswds-components/alert.vue @@ -0,0 +1,77 @@ +<template> + <div + class="usa-alert" + :class="`usa-alert--${alertType}${slimAlert ? ' usa-alert--slim' : ''} ${showAlertIcon ? '' : 'usa-alert--no-icon'}`" + > + <div class="usa-alert__body"> + <h4 + v-if="alertTitle" + class="usa-alert__heading" + > + {{ alertTitle }} + </h4> + <!-- @slot markup should start with tag with the class set to usa-alert__text --> + <slot /> + <svg + v-if="showClose" + class="usa-icon message-close" + aria-label="close-icon" + role="img" + @click="$emit('closeAlert', $event)" + > + <use :xlink:href="closeIcon" /> + </svg> + </div> + </div> +</template> + +<script> +/** + * @vue-prop {String} - alertType, A USWDS Alert message type (info, warning, error, or success) + * @vue-prop {Boolean} - slimAlert - set to true if the alert should be shown without the icon, defaults to false + * @vue-prop {Boolean} - showAlertIcon - Set to false if the alert icon should not be shown, defaults to true and will + * be ignored if slimAlert is set to true. + * @vue-prop {Boolean} - showClose, show or hide the close button + * @vue-prop {String} - alertTitle, main title for the alert + * @vue-prop {String} - staticRoot - path to the root of the static assets, defaults to the empty string + * @vue-computed {String} - closeIcon, URL for the close 'X' icon + * @vue-event closeAlert - event emitted when alert close is clicked + */ +export default { + name: 'USWDSAlert', + props: { + alertType: { + type: String, + default: 'error', + validator: function(value) { + return ['info', 'success', 'warning', 'error'].includes(value); + } + }, + slimAlert: { + type: Boolean, + default: false + }, + showAlertIcon: { + type: Boolean, + default: true + }, + showClose: { + type: Boolean, + default: false + }, + alertTitle: { + type: String, + default: '' + }, + staticRoot: { + type: String, + default: '' + } + }, + computed: { + closeIcon() { + return `${this.staticRoot}img/sprite.svg#close`; + } + } +}; +</script> diff --git a/assets/src/scripts/uswds-components/checkbox.test.js b/assets/src/scripts/uswds-components/checkbox.test.js index d04cddad2b0053863299bb501cb5259ef9ec746c..63576ee98a38fc4794ab4f25eec93cce05517ab0 100644 --- a/assets/src/scripts/uswds-components/checkbox.test.js +++ b/assets/src/scripts/uswds-components/checkbox.test.js @@ -2,7 +2,7 @@ import {mount} from '@vue/test-utils'; import USWDSCheckbox from './checkbox.vue'; -describe('components/uswds/checkbox', () => { +describe('vue-components/checkbox', () => { it('Expects a properly rendered unchecked USWDS checkbox', () => { const wrapper = mount(USWDSCheckbox, { props: { diff --git a/assets/src/styles/components/hydrograph/_app.scss b/assets/src/styles/components/hydrograph/_app.scss index c8505aa8747ff606bd02ac7e31eea36cf8beedaf..fd1565c91f08707ee19cd0d840a0ad9a755efc05 100644 --- a/assets/src/styles/components/hydrograph/_app.scss +++ b/assets/src/styles/components/hydrograph/_app.scss @@ -94,11 +94,12 @@ } } - .alert-error-container { + .usa-alert { @include uswds.u-margin-top(2); } .download-selected-data { + @include uswds.u-margin-top(2); @include uswds.u-display('flex'); @include uswds.u-flex('row'); @include uswds.u-flex('align-center'); diff --git a/assets/src/styles/partials/_base.scss b/assets/src/styles/partials/_base.scss index 065f031837ca5c8c801531c7baa3bb0649a7fde3..a2c0bfc90038ff8dda4c3f4bf1cb426b1b541bb8 100644 --- a/assets/src/styles/partials/_base.scss +++ b/assets/src/styles/partials/_base.scss @@ -5,7 +5,7 @@ ); @use 'wdfnviz'; - +@forward 'usa-alert'; @forward 'usa-prose'; @use 'banner_notifications'; @@ -36,3 +36,19 @@ @include uswds.u-padding-right(2); } } + +.usa-alert { + @include uswds.u-position('relative'); + @include uswds.u-margin-bottom(1); + + .message-close { + @include uswds.u-position('absolute'); + @include uswds.u-top(2px); + @include uswds.u-right(1); + @include uswds.u-font('body', 'md'); + } + + .message-close:hover { + cursor: pointer; + } +} diff --git a/assets/src/styles/partials/_parameter-list.scss b/assets/src/styles/partials/_parameter-list.scss index df265cdda807572e7c2ae094aedc06e8e8521db0..9731e34e5ecee5e00bd319356cbe61c9b7f31ae0 100644 --- a/assets/src/styles/partials/_parameter-list.scss +++ b/assets/src/styles/partials/_parameter-list.scss @@ -8,7 +8,7 @@ $row-border-color: 'black'; .select-time-series-container { - #parameter-selection-title { + .parameter-selection-title { @include uswds.u-font-weight('bold'); @include uswds.u-font-size('body', 'lg'); @include uswds.u-margin-bottom(2px); @@ -29,80 +29,84 @@ $row-border-color: 'black'; } } - .grid-row-period-of-record { - @include uswds.grid-row; - .grid-row-period-of-record-text { - @include uswds.grid-col(10); - @include uswds.grid-offset(1); - } - .toggle-for-top-period-of-record { - @include uswds.grid-col(1); - } - @include uswds.at-media('tablet') { - @include uswds.u-display('none'); - } - } - - .grid-row-container-row { - @include uswds.grid-container; + .parameter-row-container { + cursor: pointer; @include uswds.u-border-bottom(1px, $row-border-color); - @include uswds.u-padding(1); - } - .grid-row-container-row:first-child { - @include uswds.u-border-top(1px, $row-border-color); - } - - .grid-row-inner { - @include uswds.u-padding-right(0); - @include uswds.u-padding-left(0); + > div { + @include uswds.u-padding-x(1); + @include uswds.at-media('tablet') { + @include uswds.u-padding-x(3); + } + } - .method-selection-row { - .usa-label { - margin-top: 0; + .parameter-row-info-container { + @include uswds.u-padding-top(0); + @include uswds.u-padding-bottom(1); + @include uswds.u-display('flex'); + @include uswds.u-flex('justify-center'); + @include uswds.u-flex('column'); + @include uswds.at-media('tablet') { + @include uswds.grid-row; + @include uswds.u-flex('row'); } - #ts-method-select-container { + + .period-of-record-container { + @include uswds.grid-row; @include uswds.u-padding-top(1); - @include uswds.grid-col(7); - @include uswds.grid-offset(1); + @include uswds.u-padding-left(4); + @include uswds.u-margin-bottom('neg-105'); + z-index: 2; + + .period-of-record-text { + @include uswds.u-flex(11); + } + + @include uswds.at-media('tablet') { + @include uswds.grid-col(5); + @include uswds.u-order(2); + } } - #no-data-points-note { - font-weight: lighter; + + .usa-radio { + z-index: 1; + @include uswds.at-media('tablet') { + @include uswds.grid-col(7); + @include uswds.u-order(1); + } + + .usa-radio__label { + @include uswds.u-font-weight('bold'); + @include uswds.at-media('tablet') { + @include uswds.u-font-weight('normal'); + } + } } } - .wateralert-row { - @include uswds.grid-col; - @include uswds.grid-offset(1); - } - .usa-radio__label { - @include uswds.u-margin-top(0); - @include uswds.u-margin-bottom(0); - } - .description__param-select { - @include uswds.grid-col(7); - @include uswds.u-font-weight('bold'); - } - @include uswds.at-media('tablet') { - .description__param-select { - @include uswds.u-font-weight('normal'); + .expansion-container-row { + @include uswds.u-margin-bottom(1); + @include uswds.u-padding-left(4); + @include uswds.at-media('tablet') { + @include uswds.u-padding-left(9); } } } - .period-of-record__param-select { - @include uswds.grid-col; - @include uswds.u-display('none'); - @include uswds.at-media('tablet') { - @include uswds.u-display('inline'); - } + .parameter-row-container:first-child { + @include uswds.u-border-top(1px, $row-border-color); } - #period-of-record-and-toggle-container { - @include uswds.u-display('none'); - @include uswds.at-media('tablet') { - @include uswds.u-display('flex'); - justify-content: space-between; + // Leave in for now to have for method-picker + .method-picker-container { + @include uswds.u-padding-top(1); + @include uswds.grid-col(7); + .usa-label { + @include uswds.u-margin-top('05'); + } + + .no-data-points-note { + @include uswds.u-text('light'); } } } diff --git a/assets/vite.config.js b/assets/vite.config.js index 171659ce5e54369879f4ed26908115e9cee2dff1..53195f9b40785c1dfbe3d73e0ca7c49513b7f282 100644 --- a/assets/vite.config.js +++ b/assets/vite.config.js @@ -24,7 +24,7 @@ const entries = [ export default defineConfig(({command, mode}) => { return { - base: '/static/', + base: '/dist/', plugins: [ vue({ isProduction: true, diff --git a/wdfn-server/waterdata/templates/macros/components.html b/wdfn-server/waterdata/templates/macros/components.html index cfcb3c3735e8304132ac545819819acae218f206..0ee520ec8427f6d17e3d0bea965541df916d45d9 100644 --- a/wdfn-server/waterdata/templates/macros/components.html +++ b/wdfn-server/waterdata/templates/macros/components.html @@ -28,6 +28,8 @@ </div> {% endif %} <div class="select-time-series-container"></div> + <div class="daily-statistical-data"></div> + {% if iv_period_of_record or gw_period_of_record %} <div class="wdfn-accordion graph-data usa-accordion"> <h2 class="usa-accordion__heading">