From 1141c99c08f0aeb02566330362b275ac2fd4715d Mon Sep 17 00:00:00 2001 From: gpetrochenkov-usgs <gpetrochenkov@usgs.gov> Date: Mon, 25 May 2020 20:25:42 -0400 Subject: [PATCH] Added water levels to gage height graph --- .../components/hydrograph/drawing-data.js | 43 ++++++- .../hydrograph/drawing-data.spec.js | 113 +++++++++++++++++- .../scripts/components/hydrograph/index.js | 1 + .../scripts/components/hydrograph/legend.js | 20 ++-- .../components/hydrograph/legend.spec.js | 9 +- .../hydrograph/time-series-graph.js | 69 ++++++++++- .../hydrograph/time-series-graph.spec.js | 28 ++++- .../scripts/selectors/flood-data-selector.js | 12 +- .../selectors/flood-data-selector.spec.js | 48 +++++++- assets/src/scripts/store/flood-inundation.js | 8 +- .../scripts/web-services/flood-data.spec.js | 1 - .../styles/components/hydrograph/_graph.scss | 22 ++++ 12 files changed, 348 insertions(+), 26 deletions(-) diff --git a/assets/src/scripts/components/hydrograph/drawing-data.js b/assets/src/scripts/components/hydrograph/drawing-data.js index 40cd82603..22f305c6d 100644 --- a/assets/src/scripts/components/hydrograph/drawing-data.js +++ b/assets/src/scripts/components/hydrograph/drawing-data.js @@ -6,6 +6,7 @@ import {createSelector} from 'reselect'; import {format} from 'd3-format'; import {getCurrentVariableMedianStatistics} from '../../selectors/median-statistics-selector'; +import {getWaterwatchFloodLevels} from '../../selectors/flood-data-selector'; import {getVariables, getCurrentMethodID, getTimeSeries, getCurrentVariableTimeSeries, getTimeSeriesForTsKey, getTsRequestKey, getRequestTimeRange} from '../../selectors/time-series-selector'; import {getIanaTimeZone} from '../../selectors/time-zone-selector'; @@ -165,7 +166,7 @@ export const classesForPoint = point => { }; /* - * @ return {Array of Arrays of Objects} where the properties are date (universal), class, and value + * @ return {Array of Arrays of Objects} where the properties are date (universal), and value */ export const getCurrentVariableMedianStatPoints = createSelector( getCurrentVariableMedianStatistics, @@ -220,6 +221,46 @@ export const getCurrentVariableMedianStatPoints = createSelector( }); }); +/* + * @ return {Array of Arrays of Objects} where the properties are date (universal), and value +*/ +export const getWaterwatchFloodLevelDataPoints = createSelector( + getWaterwatchFloodLevels, + getRequestTimeRange('current'), + getIanaTimeZone, + (floodLevels, timeRange, ianaTimeZone) => { + if (!floodLevels || !timeRange) { + return []; + } + + let datesOfInterest = []; + let nextDateTime = DateTime.fromMillis(timeRange.start, {zone: ianaTimeZone}); + datesOfInterest.push({ + year: nextDateTime.year, + month: nextDateTime.month.toString(), + day: nextDateTime.day.toString(), + utcDate: timeRange.start + }); + nextDateTime = DateTime.fromMillis(timeRange.end, {zone: ianaTimeZone}); + datesOfInterest.push({ + year: nextDateTime.year, + month: nextDateTime.month.toString(), + day: nextDateTime.day.toString(), + utcDate: timeRange.end + }); + + return floodLevels.map((floodLevel) => { + return datesOfInterest + .map((date) => { + return { + value: floodLevel, + date: date.utcDate + }; + }) + }); + } +); + /** * Factory function create a function that diff --git a/assets/src/scripts/components/hydrograph/drawing-data.spec.js b/assets/src/scripts/components/hydrograph/drawing-data.spec.js index c2e95ce6a..82ae49f7a 100644 --- a/assets/src/scripts/components/hydrograph/drawing-data.spec.js +++ b/assets/src/scripts/components/hydrograph/drawing-data.spec.js @@ -1,6 +1,6 @@ import {DateTime} from 'luxon'; -import {lineSegmentsSelector, pointsSelector, allPointsSelector, pointsByTsKeySelector, classesForPoint, lineSegmentsByParmCdSelector, currentVariableLineSegmentsSelector, currentVariablePointsSelector, currentVariablePointsByTsIdSelector, visiblePointsSelector, getCurrentVariableMedianStatPoints, MAX_LINE_POINT_GAP} from './drawing-data'; +import {lineSegmentsSelector, pointsSelector, allPointsSelector, pointsByTsKeySelector, classesForPoint, lineSegmentsByParmCdSelector, currentVariableLineSegmentsSelector, currentVariablePointsSelector, currentVariablePointsByTsIdSelector, visiblePointsSelector, getCurrentVariableMedianStatPoints, MAX_LINE_POINT_GAP, getWaterwatchFloodLevelDataPoints} from './drawing-data'; const TEST_DATA = { ivTimeSeriesData: { @@ -195,6 +195,12 @@ const TEST_DATA = { currentIVVariableID: '45807197', currentIVDateRangeKind: 'P7D', currentIVMethodID: 69928 + }, + floodState: { + actionStage: 1, + floodStage: 2, + moderateFloodStage: 3, + majorFloodStage: 4 } }; @@ -1029,5 +1035,110 @@ describe('drawingData module', () => { }; expect(getCurrentVariableMedianStatPoints(newTestState)).toEqual([]); }); + + describe('getWaterwatchtFloodLevelPoints', () => { + const TEST_VARS = { + '45807042': { + variableCode: { + 'value': '00060' + } + }, + '45807142': { + variableCode: { + 'value': '00010' + } + } + }; + + const TEST_STATE = { + ivTimeSeriesData: { + queryInfo: { + 'current:P7D': { + notes: { + requestDT: 1488388500000, + 'filter:timeRange': { + mode: 'PERIOD', + periodDays: '7', + modifiedSince: null + } + } + } + }, + variables: TEST_VARS, + timeSeries: { + '69928:00060': { + tsKey: 'current:P7D', + startTime: new Date('2018-03-06T15:45:00.000Z'), + endTime: new Date('2018-03-13t13:45:00.000Z'), + variable: '45807197', + method: 69928, + points: [{ + value: 10, + qualifiers: ['P'], + approved: false, + estimated: false + }, { + value: null, + qualifiers: ['P', 'ICE'], + approved: false, + estimated: false + }, { + value: null, + qualifiers: ['P', 'FLD'], + approved: false, + estimated: false + }] + } + } + }, + ianaTimeZone: 'America/Chicago', + ivTimeSeriesState: { + currentIVVariableID: '45807142', + currentIVDateRangeKind: 'P7D' + }, + floodState: { + actionStage: 1, + floodStage: 2, + moderateFloodStage: 3, + majorFloodStage: 4 + }, + }; + + it('Return the expected data points', () => { + let result = getWaterwatchFloodLevelDataPoints(TEST_STATE); + expect(result.length).toBe(1); + expect(result[0].length).toBe(2); + expect(result[0][0]).toEqual({ + value: 1, + date: DateTime.fromObject({ + year: 2018, + month: 3, + day: 6, + hour: 15, + minute: 45, + second: 0, + zone: 'America/Chicago' + }).valueOf() + }); + }); + + it('Return the expected data points', () => { + let result = getWaterwatchFloodLevelDataPoints(TEST_STATE); + expect(result.length).toBe(1); + expect(result[0].length).toBe(2); + expect(result[0][1]).toEqual({ + value: 1, + date: DateTime.fromObject({ + year: 2018, + month: 3, + day: 13, + hour: 13, + minute: 45, + second: 0, + zone: 'America/Chicago' + }).valueOf() + }); + }); + }); }); }); diff --git a/assets/src/scripts/components/hydrograph/index.js b/assets/src/scripts/components/hydrograph/index.js index e09961905..427ba40e5 100644 --- a/assets/src/scripts/components/hydrograph/index.js +++ b/assets/src/scripts/components/hydrograph/index.js @@ -149,6 +149,7 @@ export const attachToNode = function (store, .call(drawDateRangeControls, store, siteno); nodeElem.select('.ts-legend-controls-container') + .call(drawGraphControls, store); nodeElem.select('.select-time-series-container') diff --git a/assets/src/scripts/components/hydrograph/legend.js b/assets/src/scripts/components/hydrograph/legend.js index 665c989b9..173444017 100644 --- a/assets/src/scripts/components/hydrograph/legend.js +++ b/assets/src/scripts/components/hydrograph/legend.js @@ -7,8 +7,9 @@ import {drawSimpleLegend} from '../../d3-rendering/legend'; import {defineLineMarker, defineTextOnlyMarker, defineRectangleMarker} from '../../d3-rendering/markers'; import {link} from '../../lib/d3-redux'; import {getCurrentVariableMedianMetadata} from '../../selectors/median-statistics-selector'; -import {hasWaterwatchData, getWaterwatchFloodLevels} from '../../selectors/flood-data-selector'; -import {getCurrentVariable} from '../../selectors/time-series-selector'; +import {hasWaterwatchData, getWaterwatchFloodLevels, + waterwatchVisible} from '../../selectors/flood-data-selector'; +import {getCurrentParmCd} from '../../selectors/time-series-selector'; import {currentVariableLineSegmentsSelector, HASH_ID, MASK_DESC} from './drawing-data'; import {getMainLayout} from './layout'; @@ -107,15 +108,15 @@ const createLegendMarkers = function(displayItems) { if (displayItems.floodLevels) { const floodLevels = displayItems.floodLevels; - console.log(floodLevels); const labels = ['Action Stage: ', 'Flood Stage: ', 'Moderate Flood Stage: ', 'Major Flood Stage: '] - const classes = ['action-stage-data-series', 'flood-stage-data-series', - 'moderate-flood-stage-data-series', 'major-flood-stage-data-series'] + const wwSeriesClass = 'waterwatch-data-series'; + const classes = ['action-stage', 'flood-stage', 'moderate-flood-stage', 'major-flood-stage'] for (let index = 0; index < floodLevels.length; index++) { legendMarkers.push([ defineTextOnlyMarker(labels[index]), - defineLineMarker(null, classes[index], `${floodLevels[index]} ft`)]); + defineLineMarker(null, `${wwSeriesClass} ${classes[index]}`, + `${floodLevels[index]} ft`)]); } } @@ -148,15 +149,14 @@ const legendDisplaySelector = createSelector( getCurrentVariableMedianMetadata, uniqueClassesSelector('current'), uniqueClassesSelector('compare'), - hasWaterwatchData, + waterwatchVisible, getWaterwatchFloodLevels, - getCurrentVariable, - (showSeries, medianSeries, currentClasses, compareClasses, hasWW, getWW, getVar) => { + (showSeries, medianSeries, currentClasses, compareClasses, visible, getWW) => { return { current: showSeries.current ? currentClasses : undefined, compare: showSeries.compare ? compareClasses : undefined, median: showSeries.median ? medianSeries : undefined, - floodLevels: hasWW && getVar.variableCode.value == "00065" ? getWW : undefined, + floodLevels: visible ? getWW : undefined, }; } ); diff --git a/assets/src/scripts/components/hydrograph/legend.spec.js b/assets/src/scripts/components/hydrograph/legend.spec.js index 46e622f1d..d271dce10 100644 --- a/assets/src/scripts/components/hydrograph/legend.spec.js +++ b/assets/src/scripts/components/hydrograph/legend.spec.js @@ -94,6 +94,12 @@ describe('UV: Legend module', () => { compare: true, median: true } + }, + floodState: { + actionStage: 1, + floodStage: 2, + moderateFloodStage: 3, + majorFloodStage: 4 } }; @@ -106,7 +112,8 @@ describe('UV: Legend module', () => { ...TEST_DATA.ivTimeSeriesData, timeSeries: {} }, - statisticsData: {} + statisticsData: {}, + floodState: {} }; expect(legendMarkerRowsSelector(newData)).toEqual([]); diff --git a/assets/src/scripts/components/hydrograph/time-series-graph.js b/assets/src/scripts/components/hydrograph/time-series-graph.js index 554f01fc5..3c7ea7129 100644 --- a/assets/src/scripts/components/hydrograph/time-series-graph.js +++ b/assets/src/scripts/components/hydrograph/time-series-graph.js @@ -7,17 +7,19 @@ import {addSVGAccessibility} from '../../d3-rendering/accessibility'; import {appendAxes} from '../../d3-rendering/axes'; import {renderMaskDefs} from '../../d3-rendering/data-masks'; import {link} from '../../lib/d3-redux'; -import {getAgencyCode, getMonitoringLocationName} from '../../selectors/time-series-selector'; +import {getAgencyCode, getCurrentParmCd, getMonitoringLocationName} from '../../selectors/time-series-selector'; +import {waterwatchVisible} from '../../selectors/flood-data-selector'; import {mediaQuery} from '../../utils'; import {getAxes} from './axes'; import { currentVariableLineSegmentsSelector, getCurrentVariableMedianStatPoints, + getWaterwatchFloodLevelDataPoints, HASH_ID } from './drawing-data'; import {getMainLayout} from './layout'; -import {getMainXScale, getMainYScale} from './scales'; +import {getMainXScale, getMainYScale, getBrushXScale} from './scales'; import {descriptionSelector, isVisibleSelector, titleSelector} from './time-series'; import {drawDataLines} from './time-series-data'; import {drawTooltipFocus, drawTooltipText} from './tooltip'; @@ -86,6 +88,63 @@ const plotAllMedianPoints = function (elem, {visible, xscale, yscale, seriesPoin }); }; +/** + * Plots the Waterwatch flood level points for a multiple time series. + * @param {Object} elem + * @param {Function} xscale + * @param {Function} yscale + * @param {Number} modulo + * @param {Array} points + */ +const plotFloodLevelPoints = function(elem, {xscale, yscale, points, classes}) { + const stepFunction = d3Line() + .curve(curveStepAfter) + .x(function (d) { + return xscale(d.date); + }) + .y(function (d) { + return yscale(d.value); + }); + const floodLevelGrp = elem.append('g'); + floodLevelGrp.append('path') + .datum(points) + .classed(classes[0], true) + .classed(classes[1], true) + .attr('d', stepFunction); +}; + +/** + * Plots the Waterwatch points for all flood levels for the current variable. + * @param {Object} elem + * @param {Boolean} visible + * @param {Function} xscale + * @param {Function} yscale + * @param {Array} seriesPoints + * @param {Boolean} enableClip + */ +const plotAllFloodLevelPoints = function (elem, {visible, xscale, yscale, seriesPoints, enableClip}) { + elem.select('#flood-level-points').remove(); + if (!visible) { + return; + } + const container = elem + .append('g') + .lower() + .attr('id', 'flood-level-points'); + if (enableClip) { + container.attr('clip-path', 'url(#graph-clip'); + } + const classes = [['waterwatch-data-series','action-stage'], + ['waterwatch-data-series','flood-stage'], + ['waterwatch-data-series','moderate-flood-stage'], + ['waterwatch-data-series','major-flood-stage']]; + + seriesPoints.forEach((points, index) => { + plotFloodLevelPoints(container, {xscale, yscale, points: points, classes: classes[index]}); + }); +}; + + const createTitle = function(elem, store, siteNo, showMLName) { let titleDiv = elem.append('div') .classed('time-series-graph-title', true); @@ -196,6 +255,12 @@ export const drawTimeSeriesGraph = function(elem, store, siteNo, showMLName, sho xscale: getMainXScale('current'), yscale: getMainYScale, seriesPoints: getCurrentVariableMedianStatPoints + }))) + .call(link(store, plotAllFloodLevelPoints, createStructuredSelector({ + visible: waterwatchVisible, + xscale: getBrushXScale('current'), + yscale: getMainYScale, + seriesPoints: getWaterwatchFloodLevelDataPoints }))); if (showTooltip) { dataGroup.call(drawTooltipFocus, store); diff --git a/assets/src/scripts/components/hydrograph/time-series-graph.spec.js b/assets/src/scripts/components/hydrograph/time-series-graph.spec.js index a059bcbf5..39d57b6fb 100644 --- a/assets/src/scripts/components/hydrograph/time-series-graph.spec.js +++ b/assets/src/scripts/components/hydrograph/time-series-graph.spec.js @@ -97,11 +97,11 @@ const TEST_STATE = { variables: { '45807197': { variableCode: { - value: '00060' + value: '00065' }, oid: '45807197', - variableName: 'Test title for 00060', - variableDescription: 'Test description for 00060', + variableName: 'Test title for 00065', + variableDescription: 'Test description for 00065', unit: { unitCode: 'unitCode' } @@ -276,6 +276,28 @@ describe('time series graph', () => { }); }); + describe('flood level lines', () => { + + beforeEach(() => { + div.call(drawTimeSeriesGraph, store, '12345678', false, false); + }); + + it('Should render four lines', () => { + expect(selectAll('#flood-level-points .action-stage').size()).toBe(1); + expect(selectAll('#flood-level-points .flood-stage').size()).toBe(1); + expect(selectAll('#flood-level-points .moderate-flood-stage').size()).toBe(1); + expect(selectAll('#flood-level-points .major-flood-stage').size()).toBe(1); + }); + + it('Should remove the lines when removing the median statistics data', (done) => { + store.dispatch(Actions.setCurrentIVVariable(45807190)); + window.requestAnimationFrame(() => { + expect(selectAll('#flood-level-points').size()).toBe(0); + done(); + }); + }); + }); + describe('monitoring location name', () => { it('Should not render the monitoring location name if showMLName is false', () => { div.call(drawTimeSeriesGraph, store, '12345678', false, false); diff --git a/assets/src/scripts/selectors/flood-data-selector.js b/assets/src/scripts/selectors/flood-data-selector.js index d5fe4b2b9..8a68c7acd 100644 --- a/assets/src/scripts/selectors/flood-data-selector.js +++ b/assets/src/scripts/selectors/flood-data-selector.js @@ -1,5 +1,5 @@ import { createSelector } from 'reselect'; - +import { getCurrentParmCd } from './time-series-selector'; export const getFloodStages = state => state.floodData.stages || []; @@ -35,6 +35,16 @@ export const hasWaterwatchData = createSelector( majorFloodStage != null ); +/* + * Provides a function which returns True if waterwatch flood levels should be visible. + */ +export const waterwatchVisible = createSelector( + hasWaterwatchData, + getCurrentParmCd, + (hasWW, paramCd) => + hasWW && paramCd == "00065" +); + /* * Provides a function which returns the stage closest to the gageHeight */ diff --git a/assets/src/scripts/selectors/flood-data-selector.spec.js b/assets/src/scripts/selectors/flood-data-selector.spec.js index db3d7b288..e9f4173cf 100644 --- a/assets/src/scripts/selectors/flood-data-selector.spec.js +++ b/assets/src/scripts/selectors/flood-data-selector.spec.js @@ -1,5 +1,5 @@ import { getFloodStageHeight, hasFloodData, getFloodGageHeightStageIndex, - hasWaterwatchData, getWaterwatchFloodLevels} from './flood-data-selector'; + hasWaterwatchData, getWaterwatchFloodLevels, waterwatchVisible} from './flood-data-selector'; describe('flood-data-selector', () => { @@ -105,7 +105,7 @@ describe('flood-data-selector', () => { describe('getWaterwatchData', () => { it('Return true if waterwatch flood levels are returned', () =>{ - expect(hasWaterwatchData({ + expect(getWaterwatchFloodLevels({ floodState: { actionStage: 1, floodStage: 2, @@ -116,6 +116,50 @@ describe('flood-data-selector', () => { }); }); + describe('waterwatchVisible', () => { + it('Return false if waterwatch flood levels should not be visible', () =>{ + expect(waterwatchVisible({ + floodState: { + actionStage: 1, + floodStage: 2, + moderateFloodStage: 3, + majorFloodStage: 4 + }, + ivTimeSeriesState: { + currentIVVariableID: '45807197', + }, + ivTimeSeriesData: { + variables: { + '45807197': { + variableCode: {value: '00060'}, + } + } + } + })).toBeFalsy(); + }); + + it('Return true if waterwatch flood levels should be visible', () =>{ + expect(waterwatchVisible({ + floodState: { + actionStage: 1, + floodStage: 2, + moderateFloodStage: 3, + majorFloodStage: 4 + }, + ivTimeSeriesState: { + currentIVVariableID: '45807197', + }, + ivTimeSeriesData: { + variables: { + '45807197': { + variableCode: {value: '00065'}, + } + } + } + })).toBeTruthy(); + }); + }); + describe('getFloodGageHeightStageIndex', () => { it('If stages is empty,null is returned', () => { expect(getFloodGageHeightStageIndex({ diff --git a/assets/src/scripts/store/flood-inundation.js b/assets/src/scripts/store/flood-inundation.js index 8e104532f..27711cab3 100644 --- a/assets/src/scripts/store/flood-inundation.js +++ b/assets/src/scripts/store/flood-inundation.js @@ -112,10 +112,10 @@ export const floodStateReducer = function(floodState={}, action) { case 'SET_WATERWACH_FLOOD_LEVELS': return { ...floodState, - actionStage: parseInt(action.floodLevels[0].action_stage), - floodStage: parseInt(action.floodLevels[0].flood_stage), - moderateFloodStage: parseInt(action.floodLevels[0].moderate_flood_stage), - majorFloodStage: parseInt(action.floodLevels[0].major_flood_stage) + actionStage: action.floodLevels[0] ? parseInt(action.floodLevels[0].action_stage) : null, + floodStage: action.floodLevels[0] ? parseInt(action.floodLevels[0].flood_stage) : null, + moderateFloodStage: action.floodLevels[0] ? parseInt(action.floodLevels[0].moderate_flood_stage) : null, + majorFloodStage: action.floodLevels[0] ? parseInt(action.floodLevels[0].major_flood_stage) : null }; default: return floodState; } diff --git a/assets/src/scripts/web-services/flood-data.spec.js b/assets/src/scripts/web-services/flood-data.spec.js index 22c644bec..7a041752e 100644 --- a/assets/src/scripts/web-services/flood-data.spec.js +++ b/assets/src/scripts/web-services/flood-data.spec.js @@ -1,7 +1,6 @@ import {fetchFloodExtent, fetchFloodFeatures, fetchWaterwatchFloodLevels} from './flood-data'; import MOCK_WATERWATCH_FLOOD_LEVELS from '../mock-service-data'; -import {} from './waterwatch-data'; describe('flood_data module', () => { diff --git a/assets/src/styles/components/hydrograph/_graph.scss b/assets/src/styles/components/hydrograph/_graph.scss index ab247782b..779a99e8d 100644 --- a/assets/src/styles/components/hydrograph/_graph.scss +++ b/assets/src/styles/components/hydrograph/_graph.scss @@ -180,6 +180,28 @@ svg { } } + .waterwatch-data-series { + fill: none; + stroke-width: 1px; + stroke: #001aff; + + &.action-stage { + stroke-dasharray: 3, 3; + } + + &.flood-stage { + stroke-dasharray: 7, 3; + } + + &.moderate-flood-stage { + stroke-dasharray: 13, 5; + } + + &.major-flood-stage { + stroke-dasharray: 20, 7; + } + } + .mask { opacity: 0.2; } -- GitLab