diff --git a/assets/src/scripts/index.spec.js b/assets/src/scripts/index.spec.js index f4ae9d5d18e046116fa604f790e7ee019ae807f4..ad4b8fc6461ef279ca6ab2ee5e0c8dced2c0f443 100644 --- a/assets/src/scripts/index.spec.js +++ b/assets/src/scripts/index.spec.js @@ -44,20 +44,20 @@ import './monitoring-location/components/daily-value-hydrograph/tooltip.spec'; import './monitoring-location/components/embed.spec'; import './monitoring-location/components/hydrograph/audible.spec'; -import './monitoring-location/components/hydrograph/cursor.spec'; +import './monitoring-location/components/hydrograph/selectors/cursor.spec'; import './monitoring-location/components/hydrograph/date-controls.spec'; -import './monitoring-location/components/hydrograph/domain.spec'; +import './monitoring-location/components/hydrograph/selectors/domain.spec'; import './monitoring-location/components/hydrograph/drawing-data.spec'; import './monitoring-location/components/hydrograph/data-table.spec'; import './monitoring-location/components/hydrograph/graph-brush.spec'; import './monitoring-location/components/hydrograph/graph-controls.spec'; import './monitoring-location/components/hydrograph/index.spec'; -import './monitoring-location/components/hydrograph/layout.spec'; +import './monitoring-location/components/hydrograph/selectors/layout.spec'; import './monitoring-location/components/hydrograph/legend.spec'; import './monitoring-location/components/hydrograph/method-picker.spec'; import './monitoring-location/components/hydrograph/parameters.spec'; -import './monitoring-location/components/hydrograph/scales.spec'; -import './monitoring-location/components/hydrograph/time-series.spec'; +import './monitoring-location/components/hydrograph/selectors/scales.spec'; +import './monitoring-location/components/hydrograph/selectors/time-series-data.spec'; import './monitoring-location/components/hydrograph/time-series-graph.spec'; import './monitoring-location/components/hydrograph/tooltip.spec'; diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/audible.js b/assets/src/scripts/monitoring-location/components/hydrograph/audible.js index c97c016a68bcdc39c5c2668854ba7ed7a9f9453d..42d83036dba295287113bc6fe19fb6a18555677b 100644 --- a/assets/src/scripts/monitoring-location/components/hydrograph/audible.js +++ b/assets/src/scripts/monitoring-location/components/hydrograph/audible.js @@ -1,3 +1,8 @@ +/* + * Note the audible interface is not currently enabled and will likely need a major implementation + * The current patterns of putting the selectors in a separate module from rendering code and + * updating the selectors to use is* or get* pattern. + */ import {scaleLinear} from 'd3-scale'; import memoize from 'fast-memoize'; import {createSelector, createStructuredSelector} from 'reselect'; @@ -7,7 +12,6 @@ import {link} from '../../../lib/d3-redux'; import {getTimeSeries} from '../../selectors/time-series-selector'; import {Actions} from '../../store/instantaneous-value-time-series-state'; -import {tsCursorPointsSelector} from './cursor'; import {getMainXScale, getMainYScale} from './scales'; @@ -72,48 +76,10 @@ export const updateSound = function ({enabled, points}) { } }; -const audibleInterfaceOnSelector = state => state.ivTimeSeriesState.audiblePlayId !== null; - -const audibleScaleSelector = createSelector( - getMainYScale, - (yScale) => { - return scaleLinear() - .domain(yScale.domain()) - .range([80, 1500]); - } -); - -const audiblePointsSelector = createSelector( - getTimeSeries, - tsCursorPointsSelector('current'), - tsCursorPointsSelector('compare'), - audibleScaleSelector, - (allTimeSeries, currentPoints, comparePoints, yScale) => { - // Set null points for all time series, so we can turn audio for those - // points off when toggling to other time series. - let points = Object.keys(allTimeSeries).reduce((points, tsID) => { - points[tsID] = null; - return points; - }, {}); - - // Get the pitches for the current-year points - points = Object.keys(currentPoints).reduce((points, tsID) => { - const pt = currentPoints[tsID]; - points[tsID] = yScale(pt.value); - return points; - }, points); - - // Get the pitches for the compare-year points - return Object.keys(comparePoints).reduce((points, tsID) => { - const pt = comparePoints[tsID]; - points[tsID] = yScale(pt.value); - return points; - }, points); - } -); - +/* + * Renders the audible control if enabled. + */ export const audibleUI = function (elem, store) { - // Only enable the audio interface on dev tiers. if (!config.TIMESERIES_AUDIO_ENABLED) { return; } @@ -142,7 +108,7 @@ export const audibleUI = function (elem, store) { elem.select('i') .classed('fa-play', !audibleOn) .classed('fa-stop', audibleOn); - }, audibleInterfaceOnSelector)) + }, isAudiblePlaying)) .call(link(store, function(elem, xScale) { const domain = xScale.domain(); elem.attr('data-max-offset', domain[1] - domain[0]); @@ -157,15 +123,14 @@ export const audibleUI = function (elem, store) { // Listen for focus changes, and play back the audio representation of // the selected points. - // TODO: Handle more than just the first time series of each tsKey. This can - // piggyback on work to support multiple tooltip selections. + // TODO: This does not correctly handle parameter codes with multiple time series. elem.call(link(store,function (elem, {enabled, points}) { updateSound({ points, enabled }); }, createStructuredSelector({ - enabled: audibleInterfaceOnSelector, - points: audiblePointsSelector + enabled: isAudiblePlaying, + points: getAudiblePoints }))); }; diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/audible.spec.js b/assets/src/scripts/monitoring-location/components/hydrograph/audible.spec.js index 9a19854e8528c8984bdee2fc0f4327dd24dd1bb4..7ffbd7a9c8f8a7caa635035709363d63148c06d0 100644 --- a/assets/src/scripts/monitoring-location/components/hydrograph/audible.spec.js +++ b/assets/src/scripts/monitoring-location/components/hydrograph/audible.spec.js @@ -88,8 +88,4 @@ describe('monitoring-location/components/hydrograph/audible audibleUI', () => { done(); }); }); - - - - }); diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/cursor.js b/assets/src/scripts/monitoring-location/components/hydrograph/cursor.js deleted file mode 100644 index ac880b62573018ed749c5add33e129ff54769003..0000000000000000000000000000000000000000 --- a/assets/src/scripts/monitoring-location/components/hydrograph/cursor.js +++ /dev/null @@ -1,69 +0,0 @@ -import memoize from 'fast-memoize'; -import {createSelector} from 'reselect'; - -import {getCurrentMethodID} from '../../selectors/time-series-selector'; -import {getNearestTime} from '../../../utils'; - -import {currentVariablePointsByTsIdSelector} from './drawing-data'; -import {getMainXScale} from './scales'; -import {isVisibleSelector} from './time-series'; - - -export const cursorOffsetSelector = createSelector( - getMainXScale('current'), - state => state.ivTimeSeriesState.ivGraphCursorOffset, - (xScale, cursorOffset) => { - // If cursorOffset is false, don't show it - if (cursorOffset === false) { - return null; - // If cursorOffset is otherwise unset, default to the last offset - } else if (!cursorOffset) { - const domain = xScale.domain(); - return domain[1] - domain[0]; - } else { - return cursorOffset; - } - } -); - -/** - * Returns a selector that, for a given tsKey: - * Returns the time corresponding to the current cursor offset. - * @param {String} tsKey - * @return {Date} - */ -export const cursorTimeSelector = memoize(tsKey => createSelector( - cursorOffsetSelector, - getMainXScale(tsKey), - (cursorOffset, xScale) => { - return cursorOffset ? new Date(xScale.domain()[0] + cursorOffset) : null; - } -)); - -/* - * Returns a function that the time series data point nearest the tooltip focus time for the current time series - * with the current variable and current method - * @param {Object} state - Redux store - * @param String} tsKey - Time series key - * @return {Object} - */ -export const tsCursorPointsSelector = memoize(tsKey => createSelector( - currentVariablePointsByTsIdSelector(tsKey), - getCurrentMethodID, - cursorTimeSelector(tsKey), - isVisibleSelector(tsKey), - (timeSeries, currentMethodId, cursorTime, isVisible) => { - if (!cursorTime || !isVisible) { - return {}; - } - return Object.keys(timeSeries).reduce((data, tsId) => { - if (timeSeries[tsId].length && parseInt(tsId.split(':')[0]) === currentMethodId) { - const datum = getNearestTime(timeSeries[tsId], cursorTime); - data[tsId] = { - ...datum, - tsKey: tsKey - }; - } - return data; - }, {}); - })); \ No newline at end of file diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/date-controls.js b/assets/src/scripts/monitoring-location/components/hydrograph/date-controls.js index fcccd62b7ef73ae16cdb1041e210b3bf15737fad..cc6047de0b9538029b3ba128144f38fe04255487 100644 --- a/assets/src/scripts/monitoring-location/components/hydrograph/date-controls.js +++ b/assets/src/scripts/monitoring-location/components/hydrograph/date-controls.js @@ -3,6 +3,7 @@ import {createStructuredSelector} from 'reselect'; import {link} from '../../../lib/d3-redux'; import {drawLoadingIndicator} from '../../../d3-rendering/loading-indicator'; + import { isLoadingTS, hasAnyTimeSeries, diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/graph-brush.js b/assets/src/scripts/monitoring-location/components/hydrograph/graph-brush.js index c51939540f86e7ebd1c691dff016fa783bde50a2..7845a28c8ae600098f21b798177c3306be7d4616 100644 --- a/assets/src/scripts/monitoring-location/components/hydrograph/graph-brush.js +++ b/assets/src/scripts/monitoring-location/components/hydrograph/graph-brush.js @@ -6,12 +6,12 @@ import {appendXAxis} from '../../../d3-rendering/axes'; import {link} from '../../../lib/d3-redux'; import {Actions} from '../../store/instantaneous-value-time-series-state'; -import {getBrushXAxis} from './axes'; -import {currentVariableLineSegmentsSelector} from './drawing-data'; -import {getBrushLayout} from './layout'; -import {getBrushXScale, getBrushYScale} from './scales'; -import {isVisibleSelector} from './time-series'; -import {drawDataLines} from './time-series-data'; +import {getBrushXAxis} from './selectors/axes'; +import {getCurrentVariableLineSegments} from './drawing-data'; +import {getBrushLayout} from './selectors/layout'; +import {getBrushXScale, getBrushYScale} from './selectors/scales'; +import {isVisibleSelector} from './selectors/time-series-data'; +import {drawDataLines} from './time-series-lines'; export const drawGraphBrush = function(container, store) { @@ -53,7 +53,7 @@ export const drawGraphBrush = function(container, store) { }))) .call(link(store, drawDataLines, createStructuredSelector({ visible: isVisibleSelector('current'), - tsLinesMap: currentVariableLineSegmentsSelector('current'), + tsLinesMap: getCurrentVariableLineSegments('current'), xScale: getBrushXScale('current'), yScale: getBrushYScale, tsKey: () => 'current', diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/graph-controls.js b/assets/src/scripts/monitoring-location/components/hydrograph/graph-controls.js index b86f3c91e65701f22e55a9a7b6a8f3d5c11209ee..d9ca33d28617417a8f60c9284addbd4ec8c0b26d 100644 --- a/assets/src/scripts/monitoring-location/components/hydrograph/graph-controls.js +++ b/assets/src/scripts/monitoring-location/components/hydrograph/graph-controls.js @@ -1,11 +1,12 @@ import {link} from '../../../lib/d3-redux'; + import {getCurrentVariableMedianStatistics} from '../../selectors/median-statistics-selector'; import {getCurrentVariableTimeSeries} from '../../selectors/time-series-selector'; import {Actions} from '../../store/instantaneous-value-time-series-state'; import {audibleUI} from './audible'; -import {isVisibleSelector} from './time-series'; +import {isVisibleSelector} from './selectors/time-series-data'; /* * Create the show audible toggle, last year toggle, and median toggle for the time series graph. diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/index.js b/assets/src/scripts/monitoring-location/components/hydrograph/index.js index bd7355600a1723361e21b3f1afbabba4c5c0f51a..8c23fe1696ae8a8ef58bd0ef54f6fcba503a9a52 100644 --- a/assets/src/scripts/monitoring-location/components/hydrograph/index.js +++ b/assets/src/scripts/monitoring-location/components/hydrograph/index.js @@ -19,14 +19,15 @@ import {renderTimeSeriesUrlParams} from '../../url-params'; import {drawDateRangeControls} from './date-controls'; import {drawDataTable} from './data-table'; -import {lineSegmentsByParmCdSelector} from './drawing-data'; import {drawGraphBrush} from './graph-brush'; import {drawGraphControls} from './graph-controls'; -import {SPARK_LINE_DIM} from './layout'; +import {SPARK_LINE_DIM} from './selectors/layout'; import {drawTimeSeriesLegend} from './legend'; import {drawMethodPicker} from './method-picker'; -import {plotSeriesSelectTable, availableTimeSeriesSelector} from './parameters'; -import {timeSeriesScalesByParmCdSelector} from './scales'; +import {plotSeriesSelectTable} from './parameters'; +import {getLineSegmentsByParmCd} from './selectors/drawing-data'; +import {getAvailableParameterCodes} from './selectors/parameter-data'; +import {getTimeSeriesScalesByParmCd} from './selectors/scales'; import {drawTimeSeriesGraph} from './time-series-graph'; import {drawTooltipCursorSlider} from './tooltip'; @@ -165,9 +166,9 @@ export const attachToNode = function (store, nodeElem.select('.select-time-series-container') .call(link(store, plotSeriesSelectTable, createStructuredSelector({ siteno: () => siteno, - availableTimeSeries: availableTimeSeriesSelector, - lineSegmentsByParmCd: lineSegmentsByParmCdSelector('current', 'P7D'), - timeSeriesScalesByParmCd: timeSeriesScalesByParmCdSelector('current', 'P7D', SPARK_LINE_DIM) + availableParameterCodes: getAvailableParameterCodes, + lineSegmentsByParmCd: getLineSegmentsByParmCd('current', 'P7D'), + timeSeriesScalesByParmCd: getTimeSeriesScalesByParmCd('current', 'P7D', SPARK_LINE_DIM) }), store)); diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/legend.js b/assets/src/scripts/monitoring-location/components/hydrograph/legend.js index ee35e7fffd9f8c0e81ead8a1aa1b7969d7510841..1718473463ea45815172fdd76c0dacb8458d7210 100644 --- a/assets/src/scripts/monitoring-location/components/hydrograph/legend.js +++ b/assets/src/scripts/monitoring-location/components/hydrograph/legend.js @@ -1,183 +1,17 @@ -// functions to facilitate legend creation for a d3 plot -import {set} from 'd3-collection'; -import memoize from 'fast-memoize'; -import {createSelector, createStructuredSelector} from 'reselect'; +import {createStructuredSelector} from 'reselect'; import {drawSimpleLegend} from '../../../d3-rendering/legend'; -import {defineLineMarker, defineTextOnlyMarker, defineRectangleMarker} from '../../../d3-rendering/markers'; import {link} from '../../../lib/d3-redux'; -import {getWaterwatchFloodLevels, waterwatchVisible} from '../../selectors/flood-data-selector'; -import {getCurrentVariableMedianMetadata} from '../../selectors/median-statistics-selector'; - -import {currentVariableLineSegmentsSelector, HASH_ID, MASK_DESC} from './drawing-data'; -import {getMainLayout} from './layout'; - - -const TS_LABEL = { - 'current': 'Current: ', - 'compare': 'Last year: ', - 'median': 'Median: ' -}; - - -const tsMaskMarkers = function(tsKey, masks) { - return Array.from(masks.values()).map((mask) => { - const maskName = MASK_DESC[mask]; - const tsClass = `${maskName.replace(' ', '-').toLowerCase()}-mask`; - const fill = `url(#${HASH_ID[tsKey]})`; - return defineRectangleMarker(null, `mask ${tsClass}`, maskName, fill); - }); -}; - -const tsLineMarkers = function(tsKey, lineClasses) { - let result = []; - - if (lineClasses.default) { - result.push(defineLineMarker(null, `line-segment ts-${tsKey}`, 'Provisional')); - } - if (lineClasses.approved) { - result.push(defineLineMarker(null, `line-segment approved ts-${tsKey}`, 'Approved')); - } - if (lineClasses.estimated) { - result.push(defineLineMarker(null, `line-segment estimated ts-${tsKey}`, 'Estimated')); - } - return result; -}; - -/** - * create elements for the legend in the svg - * - * @param {Object} displayItems - Object containing keys for each ts. The current and compare will contain an - * object that has a masks property containing the Set of masks that are currently displayed. - * The median property will contain the metadata for the median statistics - * @return {Object} - Each key represents a ts and contains an array of markers to show. - */ -const createLegendMarkers = function(displayItems) { - const legendMarkers = []; - - if (displayItems.current) { - const currentMarkers = [ - ...tsLineMarkers('current', displayItems.current), - ...tsMaskMarkers('current', displayItems.current.dataMasks) - ]; - if (currentMarkers.length) { - legendMarkers.push([ - defineTextOnlyMarker(TS_LABEL.current, null, 'ts-legend-current-text'), - ...currentMarkers - ]); - } - } - if (displayItems.compare) { - const compareMarkers = [ - ...tsLineMarkers('compare', displayItems.compare), - ...tsMaskMarkers('compare', displayItems.compare.dataMasks) - ]; - if (compareMarkers.length) { - legendMarkers.push([ - defineTextOnlyMarker(TS_LABEL.compare, null, 'ts-legend-compare-text'), - ...compareMarkers - ]); - } - } - - if (displayItems.median) { - const medians = Object.values(displayItems.median); - for (let index = 0; index < medians.length; index++) { - const stats = medians[index]; - // Get the unique non-null years, in chronological order - const years = []; - if (stats.beginYear) { - years.push(stats.beginYear); - } - if (stats.endYear && stats.beginYear !== stats.endYear) { - years.push(stats.endYear); - } - const dateText = years.join(' - '); - - const descriptionText = stats.methodDescription ? `${stats.methodDescription} ` : ''; - const classes = `median-data-series median-step median-step-${index % 6}`; - const label = `${descriptionText}${dateText}`; - - legendMarkers.push([ - defineTextOnlyMarker(TS_LABEL.median), - defineLineMarker(null, classes, label)]); - } - } - - if (displayItems.floodLevels) { - const floodLevels = displayItems.floodLevels; - const keys = ['actionStage', 'floodStage', 'moderateFloodStage', 'majorFloodStage']; - const labels = ['Action Stage: ', 'Flood Stage: ', 'Moderate Flood Stage: ', 'Major Flood Stage: ']; - const wwSeriesClass = 'waterwatch-data-series'; - const classes = ['action-stage', 'flood-stage', 'moderate-flood-stage', 'major-flood-stage']; - - for (let index = 0; index < keys.length; index++) { - legendMarkers.push([ - defineTextOnlyMarker(labels[index]), - defineLineMarker(null, `${wwSeriesClass} ${classes[index]}`, - `${floodLevels[keys[index]]} ft`)]); - } - } - - return legendMarkers; -}; - - -const uniqueClassesSelector = memoize(tsKey => createSelector( - currentVariableLineSegmentsSelector(tsKey), - (tsLineSegments) => { - let classes = [].concat(...Object.values(tsLineSegments)).map((line) => line.classes); - return { - default: classes.some((cls) => !cls.approved && !cls.estimated && !cls.dataMask), - approved: classes.some((cls) => cls.approved), - estimated: classes.some((cls) => cls.estimated), - dataMasks: set(classes.map((cls) => cls.dataMask).filter((mask) => { - return mask; - })) - }; - } -)); - - - -/** - * Select attributes from the state useful for legend creation - */ -const legendDisplaySelector = createSelector( - (state) => state.ivTimeSeriesState.showIVTimeSeries, - getCurrentVariableMedianMetadata, - uniqueClassesSelector('current'), - uniqueClassesSelector('compare'), - waterwatchVisible, - getWaterwatchFloodLevels, - (showSeries, medianSeries, currentClasses, compareClasses, visible, floodLevels) => { - return { - current: showSeries.current ? currentClasses : undefined, - compare: showSeries.compare ? compareClasses : undefined, - median: showSeries.median ? medianSeries : undefined, - floodLevels: visible ? floodLevels : undefined - }; - } -); - - -/* - * Factory function that returns an array of array of markers to be used for the - * time series graph legend - * @return {Array of Array} of markers - */ -export const legendMarkerRowsSelector = createSelector( - legendDisplaySelector, - displayItems => createLegendMarkers(displayItems) -); +import {getMainLayout} from './selectors/layout'; +import {getLegendMarkerRows} from './selectors/legend-data'; export const drawTimeSeriesLegend = function(elem, store) { elem.append('div') .classed('hydrograph-container', true) .call(link(store, drawSimpleLegend, createStructuredSelector({ - legendMarkerRows: legendMarkerRowsSelector, + legendMarkerRows: getLegendMarkerRows, layout: getMainLayout }))); }; diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/legend.spec.js b/assets/src/scripts/monitoring-location/components/hydrograph/legend.spec.js index 4e0d18d0fdb7adf24dcc74e5db893161e1ecc9dd..7fa0c6a342a772f4d0a5f3f3fc62de6278c34570 100644 --- a/assets/src/scripts/monitoring-location/components/hydrograph/legend.spec.js +++ b/assets/src/scripts/monitoring-location/components/hydrograph/legend.spec.js @@ -1,11 +1,10 @@ import {select, selectAll} from 'd3-selection'; -import {lineMarker, rectangleMarker, textOnlyMarker} from '../../../d3-rendering/markers'; import {configureStore} from '../../store'; import {Actions} from '../../store/instantaneous-value-time-series-state'; -import {legendMarkerRowsSelector, drawTimeSeriesLegend} from './legend'; +import {drawTimeSeriesLegend} from './legend'; describe('monitoring-location/components/hydrograph/legend module', () => { @@ -107,77 +106,6 @@ describe('monitoring-location/components/hydrograph/legend module', () => { } }; - describe('legendMarkerRowSelector', () => { - - it('Should return no markers if no time series to show', () => { - let newData = { - ...TEST_DATA, - ivTimeSeriesData: { - ...TEST_DATA.ivTimeSeriesData, - timeSeries: {} - }, - statisticsData: {}, - floodState: {} - }; - - expect(legendMarkerRowsSelector(newData)).toEqual([]); - }); - - it('Should return markers for the selected variable', () => { - const result = legendMarkerRowsSelector(TEST_DATA); - - expect(result.length).toBe(2); - expect(result[0].length).toBe(4); - expect(result[0][0].type).toEqual(textOnlyMarker); - expect(result[0][1].type).toEqual(lineMarker); - expect(result[0][2].type).toEqual(rectangleMarker); - expect(result[0][3].type).toEqual(rectangleMarker); - expect(result[1].length).toBe(2); - expect(result[1][0].type).toEqual(textOnlyMarker); - expect(result[1][1].type).toEqual(lineMarker); - }); - - it('Should return markers for a different selected variable', () => { - const newData = { - ...TEST_DATA, - ivTimeSeriesState: { - ...TEST_DATA.ivTimeSeriesState, - currentIVVariableID: '45807202' - } - }; - const result = legendMarkerRowsSelector(newData); - - expect(result.length).toBe(5); - expect(result[0].length).toBe(3); - expect(result[0][0].type).toEqual(textOnlyMarker); - expect(result[0][1].type).toEqual(lineMarker); - expect(result[0][2].type).toEqual(lineMarker); - }); - - it('Should return markers only for time series shown', () => { - const newData = { - ...TEST_DATA, - ivTimeSeriesState: { - ...TEST_DATA.ivTimeSeriesState, - showIVTimeSeries: { - 'current': true, - 'compare': false, - 'median': false - } - } - }; - - const result = legendMarkerRowsSelector(newData); - - expect(result.length).toBe(1); - expect(result[0].length).toBe(4); - expect(result[0][0].type).toEqual(textOnlyMarker); - expect(result[0][1].type).toEqual(lineMarker); - expect(result[0][2].type).toEqual(rectangleMarker); - expect(result[0][3].type).toEqual(rectangleMarker); - }); - }); - describe('legends should render', () => { let graphNode; diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/method-picker.js b/assets/src/scripts/monitoring-location/components/hydrograph/method-picker.js index caccee9178feb98e29dce73671ebd64420f23acd..6634fcd042463c10eff456318628fcf912d8824b 100644 --- a/assets/src/scripts/monitoring-location/components/hydrograph/method-picker.js +++ b/assets/src/scripts/monitoring-location/components/hydrograph/method-picker.js @@ -10,7 +10,7 @@ import{link} from '../../../lib/d3-redux'; import {getCurrentMethodID, getAllMethodsForCurrentVariable} from '../../selectors/time-series-selector'; import {Actions} from '../../store/instantaneous-value-time-series-state'; -import { } from './time-series'; +import { } from './selectors/time-series-data'; export const drawMethodPicker = function(elem, store) { const pickerContainer = elem.insert('div', ':nth-child(2)') diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/parameters.js b/assets/src/scripts/monitoring-location/components/hydrograph/parameters.js index bcf7863566192598923f40efcb7d70e429bd5952..08acbff75ba01bbc381ea5175baec2d5f95f8e41 100644 --- a/assets/src/scripts/monitoring-location/components/hydrograph/parameters.js +++ b/assets/src/scripts/monitoring-location/components/hydrograph/parameters.js @@ -1,56 +1,13 @@ import {line} from 'd3-shape'; import {select} from 'd3-selection'; -import {createSelector} from 'reselect'; import config from '../../../config'; import {appendTooltip} from '../../../tooltips'; -import {sortedParameters} from '../../../utils'; -import {getVariables, getCurrentVariableID, getTimeSeries} from '../../selectors/time-series-selector'; import {Actions} from '../../store/instantaneous-value-time-series-data'; -import {MASK_DESC} from './drawing-data'; -import {SPARK_LINE_DIM, CIRCLE_RADIUS_SINGLE_PT} from './layout'; - -/** - * Returns metadata for each available time series. - * @param {Object} state Redux state - * @return {Array} Sorted array of [code, metadata] pairs. - */ -export const availableTimeSeriesSelector = createSelector( - getVariables, - getTimeSeries, - getCurrentVariableID, - (variables, timeSeries, currentVariableID) => { - if (!variables) { - return []; - } - - let sorted = []; - const seriesList = Object.values(timeSeries); - const timeSeriesVariables = seriesList.map(x => x.variable); - const sortedVariables = sortedParameters(variables).map(x => x.oid); - for (const variableID of sortedVariables) { - // start the next iteration if a variable is not a - // series returned by the getTimeSeries - if (!timeSeriesVariables.includes(variableID)) { - continue; - } - const variable = variables[variableID]; - const currentTimeSeriesCount = seriesList.filter(ts => ts.tsKey === 'current:P7D' && ts.variable === variableID).length; - if (currentTimeSeriesCount > 0) { - let varCodes = { - variableID: variable.oid, - description: variable.variableDescription, - selected: currentVariableID === variableID, - currentTimeSeriesCount: currentTimeSeriesCount - }; - sorted.push([variable.variableCode.value, varCodes]); - } - } - return sorted; - } -); +import {MASK_DESC} from './selectors/drawing-data'; +import {SPARK_LINE_DIM, CIRCLE_RADIUS_SINGLE_PT} from './selectors/layout'; /** * Draw a sparkline in a selected SVG element @@ -126,14 +83,14 @@ export const addSparkLine = function(svgSelection, {seriesLineSegments, scales}) * a row changes the active parameter code. * @param {Object} elem d3 selection * @param {String} siteno - * @param {Object} availableTimeSeries Time series metadata to display + * @param {Object} availableParameterCodes parameter metadata to display * @param {Object} lineSegmentsByParmCd line segments for each parameter code * @param {Object} timeSeriesScalesByParmCd scales for each parameter code */ export const plotSeriesSelectTable = function (elem, { siteno, - availableTimeSeries, + availableParameterCodes, lineSegmentsByParmCd, timeSeriesScalesByParmCd }, store ){ @@ -142,7 +99,7 @@ export const plotSeriesSelectTable = function (elem, const scrollTop = lastTable.size() ? lastTable.property('scrollTop') : null; elem.select('#select-time-series').remove(); - if (!availableTimeSeries.length) { + if (!availableParameterCodes.length) { return; } @@ -171,25 +128,25 @@ export const plotSeriesSelectTable = function (elem, table.append('tbody') .selectAll('tr') - .data(availableTimeSeries) + .data(availableParameterCodes) .enter().append('tr') .attr('ga-on', 'click') .attr('ga-event-category', 'selectTimeSeries') - .attr('ga-event-action', (parm) => `time-series-parmcd-${parm[0]}`) + .attr('ga-event-action', (parm) => `time-series-parmcd-${parm.parameterCode}`) .attr('role', 'option') - .classed('selected', parm => parm[1].selected) - .attr('aria-selected', parm => parm[1].selected) + .classed('selected', parm => parm.selected) + .attr('aria-selected', parm => parm.selected) .on('click', function (parm) { - if (!parm[1].selected) { - store.dispatch(Actions.updateIVCurrentVariableAndRetrieveTimeSeries(siteno, parm[1].variableID)); + if (!parm.selected) { + store.dispatch(Actions.updateIVCurrentVariableAndRetrieveTimeSeries(siteno, parm.variableID)); } }) .call(tr => { let parmCdCol = tr.append('th') .attr('scope', 'row'); parmCdCol.append('span') - .text(parm => parm[1].description) - .call(appendTooltip, parm => `Parameter code: ${parm[0]}`); + .text(parm => parm.description) + .call(appendTooltip, parm => `Parameter code: ${parm.parameterCode}`); tr.append('td') .append('svg') .attr('width', SPARK_LINE_DIM.width.toString()) @@ -198,14 +155,14 @@ export const plotSeriesSelectTable = function (elem, .text(parm => parm[1].currentTimeSeriesCount); tr.append('td') .style('white-space', 'nowrap') - .text(parm =>`${config.uvPeriodOfRecord[parm[0]].begin_date} to ${config.uvPeriodOfRecord[parm[0]].end_date}`); + .text(parm =>`${config.uvPeriodOfRecord[parm.parameterCode].begin_date} to ${config.uvPeriodOfRecord[parm.parameterCode].end_date}`); }); table.property('scrollTop', scrollTop); table.selectAll('tbody svg').each(function(d) { let selection = select(this); - const parmCd = d[0]; + const parmCd = d.parameterCode; const lineSegments = lineSegmentsByParmCd[parmCd] ? lineSegmentsByParmCd[parmCd] : []; for (const seriesLineSegments of lineSegments) { selection.call(addSparkLine, { diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/parameters.spec.js b/assets/src/scripts/monitoring-location/components/hydrograph/parameters.spec.js index f71786aab7ecf82e681cb43fad160c1c87f84279..397a6476f4be8c7378c9d55ac4969ec2c6198019 100644 --- a/assets/src/scripts/monitoring-location/components/hydrograph/parameters.spec.js +++ b/assets/src/scripts/monitoring-location/components/hydrograph/parameters.spec.js @@ -8,169 +8,6 @@ import {addSparkLine, plotSeriesSelectTable, availableTimeSeriesSelector} from ' describe('monitoring-location/components/hydrograph/parameters module', () => { - describe('availableTimeSeriesSelector', () => { - it('sets attributes correctly when all series have data points', () => { - const available = availableTimeSeriesSelector({ - ivTimeSeriesData: { - timeSeries: { - 'current:00060': {description: '00060', tsKey: 'current:P7D', variable: 'code0', points: [{x: 1, y: 2}]}, - 'current:00061': {description: '00061', tsKey: 'current:P7D', variable: 'code1', points: [{x: 2, y: 3}]}, - 'current:00062': {description: '00062', tsKey: 'current:P7D', variable: 'code2', points: [{x: 3, y: 4}]}, - 'compare:00061': {description: '00061', tsKey: 'compare:P7D', variable: 'code1', points: [{x: 1, y: 17}]}, - 'compare:00062': {description: '00062', tsKey: 'compare:P7D', variable: 'code2', points: [{x: 2, y: 18}]}, - 'compare:00063': {description: '00063', tsKey: 'compare:P7D', variable: 'code3', points: [{x: 3, y: 46}]} - }, - variables: { - 'code0': { - oid: 'code0', - variableDescription: 'code0 desc', - variableCode: { - value: '00060' - } - }, - 'code1': { - oid: 'code1', - variableDescription: 'code1 desc', - variableCode: { - value: '00061' - } - }, - 'code2': { - oid: 'code2', - variableDescription: 'code2 desc', - variableCode: { - value: '00062' - } - }, - 'code3': { - oid: 'code3', - variableDescription: 'code3 desc', - variableCode: { - value: '00063' - } - } - } - }, - ivTimeSeriesState: { - currentIVVariableID: 'code0' - } - }); - // Series are ordered by parameter code and have expected values. - expect(available).toEqual([ - ['00060', {variableID: 'code0', description: 'code0 desc', selected: true, currentTimeSeriesCount: 1}], - ['00061', {variableID: 'code1', description: 'code1 desc', selected: false, currentTimeSeriesCount: 1}], - ['00062', {variableID: 'code2', description: 'code2 desc', selected: false, currentTimeSeriesCount: 1}] - ]); - }); - - it('sets attributes correctly when not all series have data points', () => { - const available = availableTimeSeriesSelector({ - ivTimeSeriesData: { - timeSeries: { - 'current:00060': {description: '00060', tsKey: 'current:P7D', variable: 'code0', points: [{x: 1, y: 2}]}, - 'current:00061': {description: '00061', tsKey: 'current:P7D', variable: 'code1', points: [{x: 2, y: 3}]}, - 'current:00062': {description: '00062', tsKey: 'current:P7D', variable: 'code2', points: [{x: 3, y: 4}]}, - 'compare:00061': {description: '00061', tsKey: 'compare:P7D', variable: 'code1', points: []}, - 'compare:00062': {description: '00062', tsKey: 'compare:P7D', variable: 'code2', points: [{x: 2, y: 18}]}, - 'compare:00063': {description: '00063', tsKey: 'compare:P7D', variable: 'code3', points: [{x: 3, y: 46}]} - }, - variables: { - 'code0': { - oid: 'code0', - variableDescription: 'code0 desc', - variableCode: { - value: '00060' - } - }, - 'code1': { - oid: 'code1', - variableDescription: 'code1 desc', - variableCode: { - value: '00061' - } - }, - 'code2': { - oid: 'code2', - variableDescription: 'code2 desc', - variableCode: { - value: '00062' - } - }, - 'code3': { - oid: 'code3', - variableDescription: 'code3 desc', - variableCode: { - value: '00063' - } - } - } - }, - ivTimeSeriesState: { - currentIVVariableID: 'code0' - } - }); - // Series are ordered by parameter code and have expected values. - expect(available).toEqual([ - ['00060', {variableID: 'code0', description: 'code0 desc', selected: true, currentTimeSeriesCount: 1}], - ['00061', {variableID: 'code1', description: 'code1 desc', selected: false, currentTimeSeriesCount: 1}], - ['00062', {variableID: 'code2', description: 'code2 desc', selected: false, currentTimeSeriesCount: 1}] - ]); - }); - - it('time series without data points are considered available', () => { - const available = availableTimeSeriesSelector({ - ivTimeSeriesData: { - timeSeries: { - 'current:00060': {description: '00060', tsKey: 'current:P7D', variable: 'code0', points: [{x: 1, y: 2}]}, - 'current:00061': {description: '00061', tsKey: 'current:P7D', variable: 'code1', points: []}, - 'current:00062': {description: '00062', tsKey: 'current:P7D', variable: 'code2', points: [{x: 3, y: 4}]}, - 'compare:00061': {description: '00061', tsKey: 'compare:P7D', variable: 'code1', points: []}, - 'compare:00062': {description: '00062', tsKey: 'compare:P7D', variable: 'code2', points: [{x: 2, y: 18}]}, - 'compare:00063': {description: '00063', tsKey: 'compare:P7D', variable: 'code3', points: [{x: 3, y: 46}]} - }, - variables: { - 'code0': { - oid: 'code0', - variableDescription: 'code0 desc', - variableCode: { - value: '00060' - } - }, - 'code1': { - oid: 'code1', - variableDescription: 'code1 desc', - variableCode: { - value: '00061' - } - }, - 'code2': { - oid: 'code2', - variableDescription: 'code2 desc', - variableCode: { - value: '00062' - } - }, - 'code3': { - oid: 'code3', - variableDescription: 'code3 desc', - variableCode: { - value: '00063' - } - } - } - }, - ivTimeSeriesState: { - currentIVVariableID: 'code0' - } - }); - // Series are ordered by parameter code and have expected values. - expect(available).toEqual([ - ['00060', {variableID: 'code0', description: 'code0 desc', selected: true, currentTimeSeriesCount: 1}], - ['00061', {variableID: 'code1', description: 'code1 desc', selected: false, currentTimeSeriesCount: 1}], - ['00062', {variableID: 'code2', description: 'code2 desc', selected: false, currentTimeSeriesCount: 1}] - ]); - }); - }); describe('plotSeriesSelectTable', () => { let tableDivSelection; diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/selectors/audible-data.js b/assets/src/scripts/monitoring-location/components/hydrograph/selectors/audible-data.js new file mode 100644 index 0000000000000000000000000000000000000000..607799bb52b951b8043938210d43daec3986ccbe --- /dev/null +++ b/assets/src/scripts/monitoring-location/components/hydrograph/selectors/audible-data.js @@ -0,0 +1,59 @@ +/* + * Note the audible interface is not currently enabled and will likely need a major implementation + * The current patterns of putting the selectors in a separate module from rendering code and + * updating the selectors to use is* or get* pattern. + */ +import {scaleLinear} from 'd3-scale'; +import {createSelector} from 'reselect'; + +import {getTimeSeries} from '../../../selectors/time-series-selector'; + +import {tsCursorPointsSelector} from './cursor'; +import {getMainYScale} from './scales'; + +/* + * Returns a Redux selector function that returns true if the audible interface is playing. + */ +export const isAudiblePlaying = state => state.ivTimeSeriesStat.audiblePlayId !== null; + +const getAudibleYScale = createSelector( + getMainYScale, + (yScale) => { + return scaleLinear() + .domain(yScale.domain()) + .range([80, 1500]); + } +); + +/* + * Returns a Redux selector function which retrieves an array of time series points where the + * value can be used for pitches. + */ +export const getAudiblePoints = createSelector( + getTimeSeries, + tsCursorPointsSelector('current'), + tsCursorPointsSelector('compare'), + getAudibleYScale, + (allTimeSeries, currentPoints, comparePoints, yScale) => { + // Set null points for all time series, so we can turn audio for those + // points off when toggling to other time series. + let points = Object.keys(allTimeSeries).reduce((points, tsID) => { + points[tsID] = null; + return points; + }, {}); + + // Get the pitches for the current-year points + points = Object.keys(currentPoints).reduce((points, tsID) => { + const pt = currentPoints[tsID]; + points[tsID] = yScale(pt.value); + return points; + }, points); + + // Get the pitches for the compare-year points + return Object.keys(comparePoints).reduce((points, tsID) => { + const pt = comparePoints[tsID]; + points[tsID] = yScale(pt.value); + return points; + }, points); + } +); \ No newline at end of file diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/axes.js b/assets/src/scripts/monitoring-location/components/hydrograph/selectors/axes.js similarity index 76% rename from assets/src/scripts/monitoring-location/components/hydrograph/axes.js rename to assets/src/scripts/monitoring-location/components/hydrograph/selectors/axes.js index 199f5bc9ae88ffd9646b972b2fc9799df8d80922..386818c3eb041fa6ab395a6caf4485772e650eb4 100644 --- a/assets/src/scripts/monitoring-location/components/hydrograph/axes.js +++ b/assets/src/scripts/monitoring-location/components/hydrograph/selectors/axes.js @@ -2,14 +2,15 @@ import {axisBottom, axisLeft, axisRight} from 'd3-axis'; import memoize from 'fast-memoize'; import {createSelector} from 'reselect'; -import {generateTimeTicks} from '../../../d3-rendering/tick-marks'; -import {getCurrentDateRangeKind, getCurrentParmCd} from '../../selectors/time-series-selector'; -import {convertCelsiusToFahrenheit, convertFahrenheitToCelsius} from '../../../utils'; +import {generateTimeTicks} from '../../../../d3-rendering/tick-marks'; +import {getCurrentDateRangeKind, getCurrentParmCd} from '../../../selectors/time-series-selector'; +import {convertCelsiusToFahrenheit, convertFahrenheitToCelsius} from '../../../../utils'; import {getYTickDetails} from './domain'; import {getLayout} from './layout'; import {getXScale, getBrushXScale, getYScale, getSecondaryYScale} from './scales'; -import {yLabelSelector, secondaryYLabelSelector, tsTimeZoneSelector, TEMPERATURE_PARAMETERS} from './time-series'; +import {getYLabel, getSecondaryYLabel, getTsTimeZone, TEMPERATURE_PARAMETERS} from './time-series-data'; + const createXAxis = function(xScale, period, ianaTimeZone) { const [startMillis, endMillis] = xScale.domain(); @@ -32,16 +33,15 @@ const createXAxis = function(xScale, period, ianaTimeZone) { * @param {String} ianaTimeZone - Internet Assigned Numbers Authority designation for a time zone * @return {Object} {xAxis, yAxis, secondardYaxis} - D3 Axis */ -export const createAxes = function(xScale, yScale, secondaryYScale, yTickSize, parmCd, period, ianaTimeZone) { +const createAxes = function(xScale, yScale, secondaryYScale, yTickDetails, yTickSize, parmCd, period, ianaTimeZone) { // Create x-axis const xAxis = createXAxis(xScale, period, ianaTimeZone); // Create y-axis - const tickDetails = getYTickDetails(yScale.domain(), parmCd); const yAxis = axisLeft() .scale(yScale) - .tickValues(tickDetails.tickValues) - .tickFormat(tickDetails.tickFormat) + .tickValues(yTickDetails.tickValues) + .tickFormat(yTickDetails.tickFormat) .tickSizeInner(yTickSize) .tickPadding(3) .tickSizeOuter(0); @@ -60,7 +60,7 @@ export const createAxes = function(xScale, yScale, secondaryYScale, yTickSize, p if (secondaryYScale !== null) { let secondaryAxisTicks; - const primaryAxisTicks = tickDetails.tickValues; + const primaryAxisTicks = yTickDetails.tickValues; if (TEMPERATURE_PARAMETERS.celsius.includes(parmCd)) { secondaryAxisTicks = primaryAxisTicks.map(celsius => convertCelsiusToFahrenheit(celsius)); } else if (TEMPERATURE_PARAMETERS.fahrenheit.includes(parmCd)) { @@ -72,35 +72,36 @@ export const createAxes = function(xScale, yScale, secondaryYScale, yTickSize, p }; /** - * Selector that returns the brush x axis + * Returns a Redux selector function that returns the brush x axis */ export const getBrushXAxis = createSelector( getBrushXScale('current'), - tsTimeZoneSelector, + getTsTimeZone, getCurrentDateRangeKind, (xScale, ianaTimeZone, period) => createXAxis(xScale, period, ianaTimeZone) ); /** - * Returns data necessary to render the graph axes. - * @return {Object} + * Returns a Redux Selection that returns an object with xAxis, yAxis, and secondaryYAxis properties */ export const getAxes = memoize(kind => createSelector( getXScale(kind, 'current'), getYScale(kind), getSecondaryYScale(kind), + getYTickDetails, getLayout(kind), - yLabelSelector, - tsTimeZoneSelector, + getYLabel, + getTsTimeZone, getCurrentParmCd, getCurrentDateRangeKind, - secondaryYLabelSelector, - (xScale, yScale, secondaryYScale, layout, plotYLabel, ianaTimeZone, parmCd, currentDateRange, plotSecondaryYLabel) => { + getSecondaryYLabel, + (xScale, yScale, secondaryYScale, yTickDetails, layout, plotYLabel, ianaTimeZone, parmCd, currentDateRange, plotSecondaryYLabel) => { return { ...createAxes( xScale, yScale, secondaryYScale, + yTickDetails, -layout.width + layout.margin.right, parmCd, currentDateRange, diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/selectors/cursor.js b/assets/src/scripts/monitoring-location/components/hydrograph/selectors/cursor.js new file mode 100644 index 0000000000000000000000000000000000000000..a88a2e10e3279b5063bf84e46dfc627f13e1f178 --- /dev/null +++ b/assets/src/scripts/monitoring-location/components/hydrograph/selectors/cursor.js @@ -0,0 +1,97 @@ +import memoize from 'fast-memoize'; +import {createSelector} from 'reselect'; + +import {getNearestTime} from '../../../../utils'; + +import {getCurrentMethodID} from '../../../selectors/time-series-selector'; + +import {getCurrentVariablePointsByTsId} from '../drawing-data'; +import {getMainXScale, getMainYScale} from './scales'; +import {isVisible} from './time-series-data'; + + +export const getCursorOffset = createSelector( + getMainXScale('current'), + state => state.ivTimeSeriesState.ivGraphCursorOffset, + (xScale, cursorOffset) => { + // If cursorOffset is false, don't show it + if (cursorOffset === false) { + return null; + // If cursorOffset is otherwise unset, default to the last offset + } else if (!cursorOffset) { + const domain = xScale.domain(); + return domain[1] - domain[0]; + } else { + return cursorOffset; + } + } +); + +/** + * Returns a selector that, for a given tsKey: + * Returns the time corresponding to the current cursor offset. + * @param {String} tsKey + * @return {Date} + */ +export const getCursorTime = memoize(tsKey => createSelector( + getCursorOffset, + getMainXScale(tsKey), + (cursorOffset, xScale) => { + return cursorOffset ? new Date(xScale.domain()[0] + cursorOffset) : null; + } +)); + +/* + * Returns a Redux selector function that returns the time series data point nearest + * the tooltip focus time for the current time series with the current variable and current method + * @param {Object} state - Redux store + * @param String} tsKey - Time series key + * @return {Object} + */ +export const getTsCursorPoints = memoize(tsKey => createSelector( + getCurrentVariablePointsByTsId(tsKey), + getCurrentMethodID, + getCursorTime(tsKey), + isVisible(tsKey), + (timeSeries, currentMethodId, cursorTime, isVisible) => { + if (!cursorTime || !isVisible) { + return {}; + } + return Object.keys(timeSeries).reduce((data, tsId) => { + if (timeSeries[tsId].length && parseInt(tsId.split(':')[0]) === currentMethodId) { + const datum = getNearestTime(timeSeries[tsId], cursorTime); + data[tsId] = { + ...datum, + tsKey: tsKey + }; + } + return data; + }, {}); + })); + +/* + * Returns a function that returns the time series data point nearest the + * tooltip focus time for the given time series key. Only returns those points + * where the y-value is finite; no use in making a point if y is Infinity. + * + * @param {Object} state - Redux store + * @param String} tsKey - Time series key + * @return {Object} + */ +export const getTooltipPoints = memoize(tsKey => createSelector( + getMainXScale(tsKey), + getMainYScale, + getTsCursorPoints(tsKey), + (xScale, yScale, cursorPoints) => { + return Object.keys(cursorPoints).reduce((tooltipPoints, tsID) => { + const cursorPoint = cursorPoints[tsID]; + if (isFinite(yScale(cursorPoint.value))) { + tooltipPoints.push({ + x: xScale(cursorPoint.dateTime), + y: yScale(cursorPoint.value) + }); + } + return tooltipPoints; + }, []); + } +)); diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/cursor.spec.js b/assets/src/scripts/monitoring-location/components/hydrograph/selectors/cursor.spec.js similarity index 85% rename from assets/src/scripts/monitoring-location/components/hydrograph/cursor.spec.js rename to assets/src/scripts/monitoring-location/components/hydrograph/selectors/cursor.spec.js index 536337f24dc62cd53f047a39e3845c0e8150ce50..0f4702b38ae2e80b90c73603ddf25d922fff80e7 100644 --- a/assets/src/scripts/monitoring-location/components/hydrograph/cursor.spec.js +++ b/assets/src/scripts/monitoring-location/components/hydrograph/selectors/cursor.spec.js @@ -1,7 +1,7 @@ -import {configureStore} from '../../store'; -import {Actions} from '../../store/instantaneous-value-time-series-state'; +import {configureStore} from '../../../store'; +import {Actions} from '../../../store/instantaneous-value-time-series-state'; -import {tsCursorPointsSelector, cursorOffsetSelector} from './cursor'; +import {getTsCursorPoints, getCursorOffset, getTooltipPoints} from './cursor'; let DATA = [12, 13, 14, 15, 16].map(hour => { return { @@ -330,9 +330,9 @@ const TEST_STATE_ONE_VAR = { describe('monitoring-location/components/hydrograph/cursor module', () => { - describe('tsCursorPointsSelector', () => { + describe('getTsCursorPoints', () => { it('Should return last time with non-masked value if the cursor offset is null', function() { - expect(tsCursorPointsSelector('compare')(TEST_STATE_ONE_VAR)).toEqual({ + expect(getTsCursorPoints('compare')(TEST_STATE_ONE_VAR)).toEqual({ '69928:compare:P7D': { dateTime: 1514995200000, qualifiers: ['P'], @@ -340,7 +340,7 @@ describe('monitoring-location/components/hydrograph/cursor module', () => { tsKey: 'compare' } }); - expect(tsCursorPointsSelector('current')(TEST_STATE_ONE_VAR)).toEqual({ + expect(getTsCursorPoints('current')(TEST_STATE_ONE_VAR)).toEqual({ '69928:current:P7D': { dateTime: 1514995200000, qualifiers: ['P'], @@ -359,8 +359,8 @@ describe('monitoring-location/components/hydrograph/cursor module', () => { } }; - expect(tsCursorPointsSelector('current')(state)['69928:current:P7D'].value).toEqual(14); - expect(tsCursorPointsSelector('compare')(state)['69928:compare:P7D'].value).toEqual(14); + expect(getTsCursorPoints('current')(state)['69928:current:P7D'].value).toEqual(14); + expect(getTsCursorPoints('compare')(state)['69928:compare:P7D'].value).toEqual(14); }); it('Selects the nearest point for the current variable streamflow', () => { @@ -373,7 +373,7 @@ describe('monitoring-location/components/hydrograph/cursor module', () => { ivGraphCursorOffset: 16 * 60 * 1000 } }; - expect(tsCursorPointsSelector('current')(newState)).toEqual({ + expect(getTsCursorPoints('current')(newState)).toEqual({ '69929:current:P7D': { value: 2, qualifiers: ['P'], @@ -394,7 +394,7 @@ describe('monitoring-location/components/hydrograph/cursor module', () => { } }; - expect(tsCursorPointsSelector('current')(newState)).toEqual({ + expect(getTsCursorPoints('current')(newState)).toEqual({ '69930:current:P7D': { value: 0.03, qualifiers: ['P'], @@ -405,7 +405,7 @@ describe('monitoring-location/components/hydrograph/cursor module', () => { }); }); - describe('cursorOffsetSelector', () => { + describe('getCursorOffset', () => { let store; beforeEach(() => { store = configureStore(TEST_STATE_ONE_VAR); @@ -413,13 +413,53 @@ describe('monitoring-location/components/hydrograph/cursor module', () => { it('returns null when false', () => { store.dispatch(Actions.setIVGraphCursorOffset(false)); - expect(cursorOffsetSelector(store.getState())).toBe(null); + expect(getCursorOffset(store.getState())).toBe(null); }); it('returns last point when null', () => { store.dispatch(Actions.setIVGraphCursorOffset(null)); const cursorRange = DATA[4].dateTime - DATA[0].dateTime; - expect(cursorOffsetSelector(store.getState())).toBe(cursorRange); + expect(getCursorOffset(store.getState())).toBe(cursorRange); }); }); + + describe('tooltipPointsSelector', () => { + const id = (val) => val; + + it('should return the requested time series focus time', () => { + expect(tooltipPointsSelector('current').resultFunc(id, id, { + '00060:current': { + dateTime: '1date', + value: 1 + }, + '00060:compare': { + dateTime: '2date', + value: 2 + } + })).toEqual([{ + x: '1date', + y: 1 + }, { + x: '2date', + y: 2 + }]); + }); + + it('should exclude values that are infinite', () => { + expect(getTooltipPoints('current').resultFunc(id, id, { + '00060:current': { + dateTime: '1date', + value: Infinity + }, + '00060:compare': { + dateTime: '2date', + value: 2 + } + })).toEqual([{ + x: '2date', + y: 2 + }]); + }); + }); + }); diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/domain.js b/assets/src/scripts/monitoring-location/components/hydrograph/selectors/domain.js similarity index 61% rename from assets/src/scripts/monitoring-location/components/hydrograph/domain.js rename to assets/src/scripts/monitoring-location/components/hydrograph/selectors/domain.js index 6e8bd631127eb6d25d409a46a49335cd889d224a..69d4243065e727a016d9f7259bf285be1577c2ec 100644 --- a/assets/src/scripts/monitoring-location/components/hydrograph/domain.js +++ b/assets/src/scripts/monitoring-location/components/hydrograph/selectors/domain.js @@ -2,11 +2,11 @@ import {extent, ticks} from 'd3-array'; import {format} from 'd3-format'; import {createSelector} from 'reselect'; -import config from '../../../config'; -import {mediaQuery} from '../../../utils'; -import {getCurrentParmCd} from '../../selectors/time-series-selector'; +import config from '../../../../config'; +import {mediaQuery} from '../../../../utils'; +import {getCurrentParmCd} from '../../../selectors/time-series-selector'; -import {visiblePointsSelector} from './drawing-data'; +import {getVisiblePoints} from '../drawing-data'; const PADDING_RATIO = 0.2; @@ -18,8 +18,11 @@ export const SYMLOG_PARMS = [ '72137' ]; +/* + * The helper functions are exported as an aid to testing. Only the selectors are actually imported into other modules + */ /** - * Return domain padded on both ends by paddingRatio. + * Helper function which returns domain padded on both ends by paddingRatio. * For positive domains, a zero-lower bound on the y-axis is enforced. * @param {Array} domain - array of two numbers * @param {Boolean} lowerBoundPOW10 - using log scale @@ -52,50 +55,6 @@ export const extendDomain = function (domain, lowerBoundPOW10) { ]; }; - -export const getYDomain = function (pointArrays, currentVarParmCd) { - let yExtent; - let scaleDomains = []; - - // Calculate max and min for data - for (const points of pointArrays) { - if (points.length === 0) { - continue; - } - const finitePts = points.map(pt => pt.value).filter(val => isFinite(val)); - let ptExtent = extent(finitePts); - if (ptExtent[0] === ptExtent[1]) { - // when both the lower and upper values of - // extent are the same, the domain of the - // extent is from -Infinity to +Infinity; - // this isn't useful for creation of data - // points, so add this broadens the extent - // a bit for single point series - if (ptExtent[0]) { - ptExtent = [ptExtent[0] - ptExtent[0] / 2, ptExtent[0] + ptExtent[0] / 2]; - } else { // ptExtent of 0 so just set to a constant - ptExtent = [0, 1]; - } - } - scaleDomains.push(ptExtent); - } - if (scaleDomains.length > 0) { - const flatDomains = [].concat(...scaleDomains).filter(val => isFinite(val)); - if (flatDomains.length > 0) { - yExtent = [Math.min(...flatDomains), Math.max(...flatDomains)]; - } - } - - // Add padding to the extent and handle empty data sets. - if (yExtent) { - yExtent = extendDomain(yExtent, SYMLOG_PARMS.indexOf(currentVarParmCd) > -1); - } else { - yExtent = [0, 1]; - } - return yExtent; -}; - - /** * Helper function that finds highest negative value (or lowest positive value) in array of tick values, then returns * that number's absolute value @@ -124,7 +83,7 @@ export const getLowestAbsoluteValueOfTickValues = function(tickValues) { * value of negative y-axis values * @returns {array} additionalTickValues, set of new y-axis tick values that will fill tick mark gaps on log scale graphs */ -export const generateAdditionalTickValues = function(lowestTickValueOfLogScale) { +const generateAdditionalTickValues = function(lowestTickValueOfLogScale) { let additionalTickValues = []; while (lowestTickValueOfLogScale > 2) { lowestTickValueOfLogScale = Math.ceil(lowestTickValueOfLogScale / 2); @@ -186,7 +145,7 @@ export const generateNegativeTicks = function(tickValues, additionalTickValues) /** - * Function creates a new set of tick values that will fill in gaps in log scale ticks, then combines this new set with the + * Help function creates a new set of tick values that will fill in gaps in log scale ticks, then combines this new set with the * original set of tick marks. * @param {array} tickValues, the list of y-axis tick values * @param {array} yDomain, an array of two values, the lower and upper extent of the y-axis @@ -211,59 +170,97 @@ export const getFullArrayOfAdditionalTickMarks = function(tickValues, yDomain) { return fullArrayOfTickMarks; }; - -/** - * Helper function which generates y tick values for a scale - * @param {Array} yDomain - Two element array representing the domain on the yscale. - * @param {Array} parmCd - parameter code for time series that is being generated. - * @returns {Array} of tick values +/* + * Returns a Redux selector function that returns the yExtent for the currently + * visible points */ -export const getYTickDetails = function (yDomain, parmCd) { - const isSymlog = SYMLOG_PARMS.indexOf(parmCd) > -1; - - let tickValues = ticks(yDomain[0], yDomain[1], Y_TICK_COUNT); - - // When there are too many log scale ticks they will overlap--reduce the number in proportion to the number of ticks - // For example, if there are 37 tick marks, every 4 ticks will be used... if there are 31 tick marks, every 3 ticks - // will be used. Screens smaller than the USWDS defined medium screen will use fewer tick marks than larger screens. - if (isSymlog) { - // add additional ticks and labels to log scales as needed - tickValues = getFullArrayOfAdditionalTickMarks(tickValues, yDomain); - // remove ticks if there are too many of them - let lengthLimit = 20; - let divisor = 10; - if (!mediaQuery(config.USWDS_MEDIUM_SCREEN)) { - lengthLimit = 10; - divisor = 5; +export const getYDomain = createSelector( + getVisiblePoints, + getCurrentParmCd, + (pointArrays, currentVarParmCd) => { + let yExtent; + let scaleDomains = []; + + // Calculate max and min for data + for (const points of pointArrays) { + if (points.length === 0) { + continue; + } + const finitePts = points.map(pt => pt.value).filter(val => isFinite(val)); + let ptExtent = extent(finitePts); + if (ptExtent[0] === ptExtent[1]) { + // when both the lower and upper values of + // extent are the same, the domain of the + // extent is from -Infinity to +Infinity; + // this isn't useful for creation of data + // points, so add this broadens the extent + // a bit for single point series + if (ptExtent[0]) { + ptExtent = [ptExtent[0] - ptExtent[0] / 2, ptExtent[0] + ptExtent[0] / 2]; + } else { // ptExtent of 0 so just set to a constant + ptExtent = [0, 1]; + } + } + scaleDomains.push(ptExtent); } - if (tickValues.length > lengthLimit) { - tickValues = tickValues - .sort((a, b) => a - b) - .filter((_, index) => { - return !(index % Math.round(tickValues.length/divisor)); - }); + if (scaleDomains.length > 0) { + const flatDomains = [].concat(...scaleDomains).filter(val => isFinite(val)); + if (flatDomains.length > 0) { + yExtent = [Math.min(...flatDomains), Math.max(...flatDomains)]; + } } - } - // If all ticks are integers, don't display right of the decimal place. - // Otherwise, format with two decimal points. - const tickFormat = tickValues.filter(t => !Number.isInteger(t)).length ? '.2f' : 'd'; - return { - tickValues, - tickFormat: format(tickFormat) - }; -}; - - -const yDomainSelector = createSelector( - visiblePointsSelector, - getCurrentParmCd, - getYDomain + // Add padding to the extent and handle empty data sets. + if (yExtent) { + yExtent = extendDomain(yExtent, SYMLOG_PARMS.indexOf(currentVarParmCd) > -1); + } else { + yExtent = [0, 1]; + } + return yExtent; + } ); - -export const tickSelector = createSelector( - yDomainSelector, +/* + * Returns a Redux selector function that returns an Object with two properties: + * @prop tickValues {Array of Number} + * @prop tickFormat {Array of String} - formatted tickValues + */ +export const getYTickDetails = createSelector( + getYDomain, getCurrentParmCd, - getYTickDetails + (yDomain, parmCd) => { + const isSymlog = SYMLOG_PARMS.indexOf(parmCd) > -1; + + let tickValues = ticks(yDomain[0], yDomain[1], Y_TICK_COUNT); + + // When there are too many log scale ticks they will overlap--reduce the number in proportion to the number of ticks + // For example, if there are 37 tick marks, every 4 ticks will be used... if there are 31 tick marks, every 3 ticks + // will be used. Screens smaller than the USWDS defined medium screen will use fewer tick marks than larger screens. + if (isSymlog) { + // add additional ticks and labels to log scales as needed + tickValues = getFullArrayOfAdditionalTickMarks(tickValues, yDomain); + // remove ticks if there are too many of them + let lengthLimit = 20; + let divisor = 10; + if (!mediaQuery(config.USWDS_MEDIUM_SCREEN)) { + lengthLimit = 10; + divisor = 5; + } + if (tickValues.length > lengthLimit) { + tickValues = tickValues + .sort((a, b) => a - b) + .filter((_, index) => { + return !(index % Math.round(tickValues.length / divisor)); + }); + } + } + + // If all ticks are integers, don't display right of the decimal place. + // Otherwise, format with two decimal points. + const tickFormat = tickValues.filter(t => !Number.isInteger(t)).length ? '.2f' : 'd'; + return { + tickValues: tickValues, + tickFormat: format(tickFormat) + }; + } ); diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/domain.spec.js b/assets/src/scripts/monitoring-location/components/hydrograph/selectors/domain.spec.js similarity index 98% rename from assets/src/scripts/monitoring-location/components/hydrograph/domain.spec.js rename to assets/src/scripts/monitoring-location/components/hydrograph/selectors/domain.spec.js index 40c80f49112706b109e00e23826e59499300adea..5eee2781a1897ed9e4c991a4e1cbcdd4ea99b478 100644 --- a/assets/src/scripts/monitoring-location/components/hydrograph/domain.spec.js +++ b/assets/src/scripts/monitoring-location/components/hydrograph/selectors/domain.spec.js @@ -9,7 +9,7 @@ import { } from './domain'; -describe('monitoring-location/componens/hydrograph/domain module', () => { +describe('monitoring-location/componens/hydrograph/selectors/domain module', () => { describe('extendDomain', () => { it('lower bounds are calculated based on order of magnitude with the parameter, upper bound 20%', () => { const lowValDomain = extendDomain([50, 1000], true); diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/drawing-data.js b/assets/src/scripts/monitoring-location/components/hydrograph/selectors/drawing-data.js similarity index 91% rename from assets/src/scripts/monitoring-location/components/hydrograph/drawing-data.js rename to assets/src/scripts/monitoring-location/components/hydrograph/selectors/drawing-data.js index 75e6fb115871f8f45422c9619693f675c5415d7e..aed539361449ad195585164f8ee741389ab2013d 100644 --- a/assets/src/scripts/monitoring-location/components/hydrograph/drawing-data.js +++ b/assets/src/scripts/monitoring-location/components/hydrograph/selectors/drawing-data.js @@ -1,3 +1,9 @@ +/* + * One helper function is exported and used in some of the rendering code. + * It would be good to refactor this but I think ultimately it will likely wait until + * we have a new IV data service that follows similar patterns to what is returned by + * the daily value statistical service. + */ import {format} from 'd3-format'; import memoize from 'fast-memoize'; import find from 'lodash/find'; @@ -60,12 +66,26 @@ const transformToCumulative = function(points) { }); }; +/* + * Helper function that returns an object which identifies which classes to use for the point. + * This is used within a few of the rendering modules. + * @param {Object} point + * @return {Object} + */ +export const classesForPoint = function(point) { + return { + approved: point.qualifiers.indexOf('A') > -1, + estimated: point.qualifiers.indexOf('e') > -1 || point.qualifiers.indexOf('E') > -1 + }; +}; + + /* Factory function that returns a function that returns an object where the properties are ts IDs and the values * are array of point objects that can be used to render a time series graph. * @param {Object} state * @return {Object} where the keys are ts ids and the values are an Array of point Objects. */ -export const allPointsSelector = createSelector( +export const getAllPoints= createSelector( getTimeSeries, getVariables, (timeSeries, variables) => { @@ -89,9 +109,9 @@ export const allPointsSelector = createSelector( * @param {String} tsKey * @return {Object} of keys are tsId, values are Array of point Objects */ -export const pointsByTsKeySelector = memoize((tsKey, period) => createSelector( +export const getPointsByTsKey = memoize((tsKey, period) => createSelector( getTsRequestKey(tsKey, period), - allPointsSelector, + getAllPoints, getTimeSeries, (tsRequestKey, points, timeSeries) => { let result = {}; @@ -109,8 +129,8 @@ export const pointsByTsKeySelector = memoize((tsKey, period) => createSelector( * @param {String} tsKey * @return Object */ -export const currentVariablePointsByTsIdSelector = memoize(tsKey => createSelector( - pointsByTsKeySelector(tsKey), +export const getCurrentVariablePointsByTsId = memoize(tsKey => createSelector( + getPointsByTsKey(tsKey), getCurrentVariableTimeSeries(tsKey), (points, timeSeries) => { let result = {}; @@ -129,8 +149,8 @@ export const currentVariablePointsByTsIdSelector = memoize(tsKey => createSelect * @param {String} tsKey * @return Array of Array of points */ -export const currentVariablePointsSelector = memoize(tsKey => createSelector( - pointsByTsKeySelector(tsKey), +export const getCurrentVariablePoints = memoize(tsKey => createSelector( + getPointsByTsKey(tsKey), getCurrentVariableTimeSeries(tsKey), (points, timeSeries) => { return timeSeries ? Object.keys(timeSeries).map((tsId) => points[tsId]) : []; @@ -145,24 +165,13 @@ export const currentVariablePointsSelector = memoize(tsKey => createSelector( * @param {String} tsKey Time series key * @return {Array} Array of array of points. */ -export const pointsSelector = memoize((tsKey) => createSelector( - pointsByTsKeySelector(tsKey), +export const getPoints = memoize((tsKey) => createSelector( + getPointsByTsKey(tsKey), (points) => { return Object.values(points); } )); -/* - * Returns an object which identifies which classes to use for the point - * @param {Object} point - * @return {Object} - */ -export const classesForPoint = function(point) { - return { - approved: point.qualifiers.indexOf('A') > -1, - estimated: point.qualifiers.indexOf('e') > -1 || point.qualifiers.indexOf('E') > -1 - }; -}; /* * @ return {Array of Arrays of Objects} where the properties are date (universal), and value @@ -226,9 +235,9 @@ export const getCurrentVariableMedianStatPoints = createSelector( * @param {Object} state Redux store * @return {Array} Array of point arrays. */ -export const visiblePointsSelector = createSelector( - currentVariablePointsSelector('current'), - currentVariablePointsSelector('compare'), +export const getVisiblePointsSelector = createSelector( + getCurrentVariablePoints('current'), + getCurrentVariablePoints('compare'), getCurrentVariableMedianStatPoints, (state) => state.ivTimeSeriesState.showIVTimeSeries, (current, compare, median, showSeries) => { @@ -272,7 +281,7 @@ const getLineClasses = function(pt, isCurrentMethod) { * @return {Function} which returns an array of objects. */ export const getCurrentPointData = createSelector( - currentVariablePointsByTsIdSelector('current'), + getCurrentVariablePointsByTsId('current'), getCurrentMethodID, getCurrentVariable, getIanaTimeZone, @@ -320,8 +329,8 @@ export const getCurrentPointData = createSelector( * @param {String} tsKey Time series key * @return {Object} Keys are ts Ids, values are of array of line segments. */ -export const lineSegmentsSelector = memoize((tsKey, period) => createSelector( - pointsByTsKeySelector(tsKey, period), +export const getLineSegments = memoize((tsKey, period) => createSelector( + getPointsByTsKey(tsKey, period), getCurrentMethodID, (tsPoints, currentMethodID) => { let seriesLines = {}; @@ -379,8 +388,8 @@ export const lineSegmentsSelector = memoize((tsKey, period) => createSelector( * Factory function creates a function that, for a given tsKey: * @return {Object} - Mapping of parameter code Array of line segments. */ -export const lineSegmentsByParmCdSelector = memoize((tsKey, period) => createSelector( - lineSegmentsSelector(tsKey, period), +export const getLineSegmentsByParmCd = memoize((tsKey, period) => createSelector( + getLineSegments(tsKey, period), getTimeSeriesForTsKey(tsKey, period), getVariables, (lineSegmentsBySeriesID, timeSeriesMap, variables) => { @@ -402,7 +411,7 @@ export const lineSegmentsByParmCdSelector = memoize((tsKey, period) => createSel */ export const currentVariableLineSegmentsSelector = memoize(tsKey => createSelector( getCurrentVariableTimeSeries(tsKey), - lineSegmentsSelector(tsKey), + getLineSegments(tsKey), (seriesMap, linesMap) => { return Object.keys(seriesMap).reduce((visMap, sID) => { visMap[sID] = linesMap[sID]; diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/drawing-data.spec.js b/assets/src/scripts/monitoring-location/components/hydrograph/selectors/drawing-data.spec.js similarity index 94% rename from assets/src/scripts/monitoring-location/components/hydrograph/drawing-data.spec.js rename to assets/src/scripts/monitoring-location/components/hydrograph/selectors/drawing-data.spec.js index 4dda6492dbde275ef02f784218853d036bfa4d5e..d52cffbbb4a55634e0d2005d4d80efd2ef87b036 100644 --- a/assets/src/scripts/monitoring-location/components/hydrograph/drawing-data.spec.js +++ b/assets/src/scripts/monitoring-location/components/hydrograph/selectors/drawing-data.spec.js @@ -2,16 +2,16 @@ import {DateTime} from 'luxon'; import { - lineSegmentsSelector, - pointsSelector, - allPointsSelector, - pointsByTsKeySelector, + getLineSegments, + getPoints, + getAllPoints, + getPointsByTsKey, classesForPoint, - lineSegmentsByParmCdSelector, - currentVariableLineSegmentsSelector, - currentVariablePointsSelector, - currentVariablePointsByTsIdSelector, - visiblePointsSelector, + getLineSegmentsByParmCdSelector, + getCurrentVariableLineSegments, + getCurrentVariablePoints, + getCurrentVariablePointsByTsId, + getVisiblePoints, getCurrentVariableMedianStatPoints, MAX_LINE_POINT_GAP, getCurrentPointData @@ -204,9 +204,9 @@ const TEST_DATA = { describe('monitoring-location/components/hydrograph/drawingData module', () => { - describe('allPointsSelector', () => { + describe('getAllPoints', () => { - const result = allPointsSelector(TEST_DATA); + const result = getAllPoints(TEST_DATA); it('Return three time series', () => { expect(Object.keys(result).length).toBe(3); expect(result['69928:00060']).toBeDefined(); @@ -223,7 +223,7 @@ describe('monitoring-location/components/hydrograph/drawingData module', () => { }); it('Return the empty object if there are no time series', () => { - expect(allPointsSelector({ivTimeSeriesData: {}})).toEqual({}); + expect(getAllPoints({ivTimeSeriesData: {}})).toEqual({}); }); it('Resets the accumulator for precip if null value is encountered', () => { @@ -265,13 +265,13 @@ describe('monitoring-location/components/hydrograph/drawingData module', () => { } }; - expect(allPointsSelector(newTestData)['69930:00045'].map((point) => point.value)).toEqual([0.01, 0.03, null, 0.04]); + expect(getAllPoints(newTestData)['69930:00045'].map((point) => point.value)).toEqual([0.01, 0.03, null, 0.04]); }); }); - describe('pointsByTsKeySelector', () => { + describe('getPointsByTsKey', () => { it('Return the points array for the ts Key selector', () => { - const result = pointsByTsKeySelector('current')(TEST_DATA); + const result = getPointsByTsKey('current')(TEST_DATA); expect(Object.keys(result).length).toBe(2); expect(result['69928:00060']).toBeDefined(); @@ -279,33 +279,33 @@ describe('monitoring-location/components/hydrograph/drawingData module', () => { }); it('return the empty object if no time series for series', () => { - expect(pointsByTsKeySelector('current:P30D:00010')(TEST_DATA)).toEqual({}); + expect(getPointsByTsKey('current:P30D:00010')(TEST_DATA)).toEqual({}); }); }); - describe('currentVariablePointsByTsIdSelector', () => { + describe('getCurrentVariablePointsByTsId', () => { it('Return the current variable for the tsKey', () => { - const result = currentVariablePointsByTsIdSelector('current')(TEST_DATA); + const result = getCurrentVariablePointsByTsId('current')(TEST_DATA); expect(result['69928:00060']).toBeDefined(); expect(result['69928:00060']).toEqual(TEST_DATA.ivTimeSeriesData.timeSeries['69928:00060'].points); }); it('Return an empty array if the tsKey has no time series with the current variable', () => { - expect(currentVariablePointsByTsIdSelector('compare')(TEST_DATA)).toEqual({}); + expect(getCurrentVariablePointsByTsId('compare')(TEST_DATA)).toEqual({}); }); }); - describe('currentVariablePointsSelector', () => { + describe('getCurrentVariablePoints', () => { it('Return the current variable for the tsKey', () => { - const result = currentVariablePointsSelector('current')(TEST_DATA); + const result = getCurrentVariablePoints('current')(TEST_DATA); expect(result.length).toBe(1); expect(result[0]).toEqual(TEST_DATA.ivTimeSeriesData.timeSeries['69928:00060'].points); }); it('Return an empty array if the tsKey has no time series with the current variable', () => { - expect(currentVariablePointsSelector('compare')(TEST_DATA)).toEqual([]); + expect(getCurrentVariablePoints('compare')(TEST_DATA)).toEqual([]); }); }); @@ -351,7 +351,7 @@ describe('monitoring-location/components/hydrograph/drawingData module', () => { describe('line segment selector', () => { it('should separate on approved', () => { - expect(lineSegmentsSelector('current')({ + expect(getLineSegments('current')({ ...TEST_DATA, ivTimeSeriesData: { ...TEST_DATA.ivTimeSeriesData, @@ -420,7 +420,7 @@ describe('monitoring-location/components/hydrograph/drawingData module', () => { }); it('should separate on estimated', () => { - expect(lineSegmentsSelector('current')({ + expect(getLineSegments('current')({ ...TEST_DATA, ivTimeSeriesData: { ...TEST_DATA.ivTimeSeriesData, @@ -494,7 +494,7 @@ describe('monitoring-location/components/hydrograph/drawingData module', () => { }); it('should separate out masked values', () => { - expect(lineSegmentsSelector('current')({ + expect(getLineSegments('current')({ ...TEST_DATA, ivTimeSeriesData: { ...TEST_DATA.ivTimeSeriesData, @@ -584,7 +584,7 @@ describe('monitoring-location/components/hydrograph/drawingData module', () => { new Date(3 * MAX_LINE_POINT_GAP + 1), new Date(3 * MAX_LINE_POINT_GAP + 2) ]; - expect(lineSegmentsSelector('current')({ + expect(getLineSegments('current')({ ...TEST_DATA, ivTimeSeriesData: { ...TEST_DATA.ivTimeSeriesData, @@ -662,7 +662,7 @@ describe('monitoring-location/components/hydrograph/drawingData module', () => { new Date(3 * MAX_LINE_POINT_GAP + 1), new Date(3 * MAX_LINE_POINT_GAP + 2) ]; - expect(lineSegmentsSelector('current')({ + expect(getLineSegments('current')({ ...TEST_DATA, ivTimeSeriesData: { ...TEST_DATA.ivTimeSeriesData, @@ -723,7 +723,7 @@ describe('monitoring-location/components/hydrograph/drawingData module', () => { }); it('Should not set currentMethod to true if method is selected', () => { - expect(lineSegmentsSelector('current')({ + expect(getLineSegments('current')({ ...TEST_DATA, ivTimeSeriesState : { ...TEST_DATA.ivTimeSeriesState, @@ -803,9 +803,9 @@ describe('monitoring-location/components/hydrograph/drawingData module', () => { }); }); - describe('lineSegmentsByParmCdSelector', () => { + describe('getLineSegmentsByParmCdSelector', () => { it('Should return two mappings for current time series', () => { - const result = lineSegmentsByParmCdSelector('current')(TEST_DATA); + const result = getLineSegmentsByParmCdSelector('current')(TEST_DATA); expect(Object.keys(result).length).toBe(2); expect(result['00060']).toBeDefined(); @@ -813,22 +813,22 @@ describe('monitoring-location/components/hydrograph/drawingData module', () => { }); }); - describe('currentVariableLineSegmentsSelector', () => { + describe('getCurrentVariableLineSegments', () => { it('Should return a single time series for current', () => { - const result = currentVariableLineSegmentsSelector('current')(TEST_DATA); + const result = getCurrentVariableLineSegments('current')(TEST_DATA); expect(Object.keys(result).length).toBe(1); expect(result['69928:00060']).toBeDefined(); }); it('Should return an empty object for the compare time series', () => { - expect(currentVariableLineSegmentsSelector('compare')(TEST_DATA)).toEqual({}); + expect(getCurrentVariableLineSegments('compare')(TEST_DATA)).toEqual({}); }); }); - describe('pointsSelector', () => { + describe('getPoints', () => { it('works with a single collection and two time series', () => { - expect(pointsSelector('current')({ + expect(getPoints('current')({ ivTimeSeriesData: { requests: { current: { @@ -887,7 +887,7 @@ describe('monitoring-location/components/hydrograph/drawingData module', () => { }); }); - describe('visiblePointsSelector', () => { + describe('getVisiblePoints', () => { const testData = { ...TEST_DATA, ivTimeSeriesState: { @@ -901,7 +901,7 @@ describe('monitoring-location/components/hydrograph/drawingData module', () => { }; it('Return two arrays', () => { - expect(visiblePointsSelector(testData).length).toBe(2); + expect(getVisiblePoints(testData).length).toBe(2); }); it('Expects one array if only median is not visible', () => { @@ -917,7 +917,7 @@ describe('monitoring-location/components/hydrograph/drawingData module', () => { } }; - expect(visiblePointsSelector(newTestData).length).toBe(1); + expect(getVisiblePoints(newTestData).length).toBe(1); }); it('Expects an empty array if no visible series has the current variable', () => { @@ -929,7 +929,7 @@ describe('monitoring-location/components/hydrograph/drawingData module', () => { } }; - expect(visiblePointsSelector(newTestData).length).toBe(0); + expect(getVisiblePoints(newTestData).length).toBe(0); }); }); diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/layout.js b/assets/src/scripts/monitoring-location/components/hydrograph/selectors/layout.js similarity index 84% rename from assets/src/scripts/monitoring-location/components/hydrograph/layout.js rename to assets/src/scripts/monitoring-location/components/hydrograph/selectors/layout.js index a3048f0b03bb20f454096dc5693855bb979959da..a59f62f64958692a4a6f07b3eb719eeb78e96e76 100644 --- a/assets/src/scripts/monitoring-location/components/hydrograph/layout.js +++ b/assets/src/scripts/monitoring-location/components/hydrograph/selectors/layout.js @@ -4,13 +4,13 @@ import memoize from 'fast-memoize'; import {createSelector} from 'reselect'; -import config from '../../../config'; -import {mediaQuery} from '../../../utils'; +import config from '../../../../config'; +import {mediaQuery} from '../../../../utils'; -import {getCurrentParmCd} from '../../selectors/time-series-selector'; +import {getCurrentParmCd} from '../../../selectors/time-series-selector'; -import {tickSelector} from './domain'; -import {TEMPERATURE_PARAMETERS} from './time-series'; +import {getYTickDetails} from './domain'; +import {TEMPERATURE_PARAMETERS} from './time-series-data'; export const ASPECT_RATIO = 1 / 2; @@ -53,13 +53,13 @@ export const SPARK_LINE_DIM = { export const getLayout = memoize(kind => createSelector( (state) => state.ui.width, (state) => state.ui.windowWidth, - tickSelector, + getYTickDetails, getCurrentParmCd, - (width, windowWidth, tickDetails,parmCd) => { + (width, windowWidth, yTickDetails,parmCd) => { const isDesktop = mediaQuery(config.USWDS_SITE_MAX_WIDTH); const height = kind === 'BRUSH' ? isDesktop ? BRUSH_HEIGHT : BRUSH_MOBILE_HEIGHT : width * ASPECT_RATIO; const margin = isDesktop ? MARGIN : MARGIN_SMALL_DEVICE; - const tickLengths = tickDetails.tickValues.map(v => tickDetails.tickFormat(v).length); + const tickLengths = yTickDetails.tickValues.map(v => yTickDetails.tickFormat(v).length); const approxLabelLength = Math.max(...tickLengths) * 10; const isTemperatureParameter = TEMPERATURE_PARAMETERS.celsius.concat(TEMPERATURE_PARAMETERS.fahrenheit).includes(parmCd); diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/layout.spec.js b/assets/src/scripts/monitoring-location/components/hydrograph/selectors/layout.spec.js similarity index 100% rename from assets/src/scripts/monitoring-location/components/hydrograph/layout.spec.js rename to assets/src/scripts/monitoring-location/components/hydrograph/selectors/layout.spec.js diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/selectors/legend-data.js b/assets/src/scripts/monitoring-location/components/hydrograph/selectors/legend-data.js new file mode 100644 index 0000000000000000000000000000000000000000..21532fdad19db7b52785dcff943adb94c22cffb4 --- /dev/null +++ b/assets/src/scripts/monitoring-location/components/hydrograph/selectors/legend-data.js @@ -0,0 +1,170 @@ +import {set} from 'd3-collection'; +import memoize from 'fast-memoize'; +import {createSelector} from 'reselect'; + +import {defineLineMarker, defineRectangleMarker, defineTextOnlyMarker} from '../../../../d3-rendering/markers'; + +import {getWaterwatchFloodLevels, isWaterwatchVisible} from '../../../selectors/flood-data-selector'; +import {getCurrentVariableMedianMetadata} from '../../../selectors/median-statistics-selector'; + +import {getCurrentVariableLineSegments, HASH_ID, MASK_DESC} from './drawing-data'; + +const TS_LABEL = { + 'current': 'Current: ', + 'compare': 'Last year: ', + 'median': 'Median: ' +}; + +/* + * Returns a Redux Selector function which returns an Object that represents the unique + * classes that are visible for the tsKey + * @prop {Boolean} default + * @prop {Boolean} approved + * @prop {Boolean} estimated + * @prop {D3 set} dataMask + */ +const getUniqueClasses = memoize(tsKey => createSelector( + getCurrentVariableLineSegments(tsKey), + (tsLineSegments) => { + let classes = [].concat(...Object.values(tsLineSegments)).map((line) => line.classes); + return { + default: classes.some((cls) => !cls.approved && !cls.estimated && !cls.dataMask), + approved: classes.some((cls) => cls.approved), + estimated: classes.some((cls) => cls.estimated), + dataMasks: set(classes.map((cls) => cls.dataMask).filter((mask) => { + return mask; + })) + }; + } +)); + +/** + * Returns a Redux selector function that returns an object of attributes to be used + * to generate the legend markers. The properties will be undefined if not visible + * @prop current {Object} - see getUniqueClasses + * @prop compare {Object} - see getUniqueClasses + * @prop median {Object} - median meta data - each property represents a time series for the current parameter code + * @prop floodLevels {Object} - + */ +const getLegendDisplay = createSelector( + (state) => state.ivTimeSeriesState.showIVTimeSeries, + getCurrentVariableMedianMetadata, + getUniqueClasses('current'), + getUniqueClasses('compare'), + isWaterwatchVisible, + getWaterwatchFloodLevels, + (showSeries, medianSeries, currentClasses, compareClasses, showWaterWatch, floodLevels) => { + return { + current: showSeries.current ? currentClasses : undefined, + compare: showSeries.compare ? compareClasses : undefined, + median: showSeries.median ? medianSeries : undefined, + floodLevels: showWaterWatch ? floodLevels : undefined + }; + } +); + +const getTsMarkers = function(tsKey, uniqueClasses) { + let tsMarkers; + const maskMarkers = uniqueClasses.dataMasks.values().map((mask) => { + const maskName = MASK_DESC[mask]; + const tsClass = `${maskName.replace(' ', '-').toLowerCase()}-mask`; + const fill = `url(#${HASH_ID[tsKey]})`; + return defineRectangleMarker(null, `mask ${tsClass}`, maskName, fill); + }); + + let lineMarkers = []; + if (uniqueClasses.default) { + lineMarkers.push(defineLineMarker(null, `line-segment ts-${tsKey}`, 'Provisional')); + } + if (uniqueClasses.approved) { + lineMarkers.push(defineLineMarker(null, `line-segment approved ts-${tsKey}`, 'Approved')); + } + if (uniqueClasses.estimated) { + lineMarkers.push(defineLineMarker(null, `line-segment estimated ts-${tsKey}`, 'Estimated')); + } + + if (lineMarkers.length || maskMarkers.length) { + tsMarkers = [defineTextOnlyMarker(TS_LABEL[tsKey]), ...lineMarkers, ...maskMarkers]; + } + return tsMarkers; +}; + +/* + * @param {Object} medianMetData + * @return {Array of Array} - each subarray rpresents the markes for a time series median data + */ +const getMedianMarkers = function(medianMetaData) { + return Object.values(medianMetaData).map((stats, index) => { + // Get the unique non-null years, in chronological order + let years = []; + if (stats.beginYear) { + years.push(stats.beginYear); + } + if (stats.endYear && stats.beginYear !== stats.endYear) { + years.push(stats.endYear); + } + const dateText = years.join(' - '); + + const descriptionText = stats.methodDescription ? `${stats.methodDescription} ` : ''; + const classes = `median-data-series median-step median-step-${index % 6}`; + const label = `${descriptionText}${dateText}`; + + return [defineTextOnlyMarker(TS_LABEL.median), defineLineMarker(null, classes, label)]; + }); +}; + +const getFloodLevelMarkers = function(floodLevels) { + const FLOOD_LEVEL_DISPLAY = { + actionStage: { + label: 'Action Stage', + class: 'action-stage' + }, + floodStage: { + label: 'Flood Stage', + class: 'flood-stage' + }, + moderateFloodStage: { + label: 'Moderate Flood Stage', + class: 'moderate-flood-stage' + }, + majorFloodStage: { + label: 'Major Flood Stage', + class: 'major-flood-stage' + } + }; + return Object.keys(floodLevels).map((stage) => { + return [ + defineTextOnlyMarker(FLOOD_LEVEL_DISPLAY[stage].label), + defineLineMarker( + null, + `waterwatch-data-series ${FLOOD_LEVEL_DISPLAY[stage].class}`, + `${floodLevels[stage]} ft`) + ]; + }); +}; + + +/* + * Factory function that returns an array of array of markers to be used for the + * time series graph legend + * @return {Array of Array} of markers - each sub array represents an row of markers + */ +export const getLegendMarkerRows = createSelector( + getLegendDisplay, + (displayItems) => { + const markerRows = []; + const currentTsMarkerRow = displayItems.current ? getTsMarkers('current', displayItems.current) : undefined; + const compareTsMarkerRow = displayItems.compare ? getTsMarkers('compare', displayItems.compare) : undefined; + const medianMarkerRows = displayItems.median ? getMedianMarkers(displayItems.median) : []; + const floodMarkerRows = displayItems.floodLevels ? getFloodLevelMarkers(displayItems.floodLevels) : []; + + if (currentTsMarkerRow) { + markerRows.push(currentTsMarkerRow); + } + if (compareTsMarkerRow) { + markerRows.push(compareTsMarkerRow); + } + markerRows.push(...medianMarkerRows, ...floodMarkerRows); + return markerRows; + } +); diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/selectors/legend-data.spec.js b/assets/src/scripts/monitoring-location/components/hydrograph/selectors/legend-data.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..6a8ddac57204a1d6680345bc044d230dd0c99b9a --- /dev/null +++ b/assets/src/scripts/monitoring-location/components/hydrograph/selectors/legend-data.spec.js @@ -0,0 +1,173 @@ +import {lineMarker, rectangleMarker, textOnlyMarker} from '../../../../d3-rendering/markers'; + +import {getLegendMarkerRows} from './legend-data'; + +describe('monitoring-location/components/hydrograph/selectors/legend-data', () => { + const TEST_DATA = { + ivTimeSeriesData: { + timeSeries: { + '00060:current': { + tsKey: 'current:P7D', + startTime: new Date('2018-03-06T15:45:00.000Z'), + endTime: new Date('2018-03-13T13:45:00.000Z'), + variable: '45807197', + 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 + }] + }, + + '00060:compare': { + tsKey: 'compare:P7D', + startTime: new Date('2018-03-06T15:45:00.000Z'), + endTime: new Date('2018-03-06T15:45:00.000Z'), + variable: '45807202', + points: [{ + value: 1, + qualifiers: ['A'], + approved: false, + estimated: false + }, { + value: 2, + qualifiers: ['A'], + approved: false, + estimated: false + }, { + value: 3, + qualifiers: ['E'], + approved: false, + estimated: false + }] + } + }, + variables: { + '45807197': { + variableCode: {value: '00060'}, + variableName: 'Streamflow', + variableDescription: 'Discharge, cubic feet per second', + oid: '45807197' + }, + '45807202': { + variableCode: {value: '00065'}, + variableName: 'Gage height', + oid: '45807202' + } + } + }, + statisticsData: { + median: { + '00060': { + '1': [{ + month_nu: '2', + day_nu: '25', + p50_va: '43', + begin_yr: '1970', + end_yr: '2017', + loc_web_ds: 'This method' + }] + } + } + }, + ivTimeSeriesState: { + currentIVVariableID: '45807197', + currentIVDateRangeKind: 'P7D', + showIVTimeSeries: { + current: true, + compare: true, + median: true + } + }, + floodData: { + floodLevels: { + site_no: '07144100', + action_stage: '20', + flood_stage: '22', + moderate_flood_stage: '25', + major_flood_stage: '26' + } + } + }; + + describe('getLegendMarkerRows', () => { + + it('Should return no markers if no time series to show', () => { + let newData = { + ...TEST_DATA, + ivTimeSeriesData: { + ...TEST_DATA.ivTimeSeriesData, + timeSeries: {} + }, + statisticsData: {}, + floodState: {} + }; + + expect(getLegendMarkerRows(newData)).toEqual([]); + }); + + it('Should return markers for the selected variable', () => { + const result = getLegendMarkerRows(TEST_DATA); + + expect(result.length).toBe(2); + expect(result[0].length).toBe(4); + expect(result[0][0].type).toEqual(textOnlyMarker); + expect(result[0][1].type).toEqual(lineMarker); + expect(result[0][2].type).toEqual(rectangleMarker); + expect(result[0][3].type).toEqual(rectangleMarker); + expect(result[1].length).toBe(2); + expect(result[1][0].type).toEqual(textOnlyMarker); + expect(result[1][1].type).toEqual(lineMarker); + }); + + it('Should return markers for a different selected variable', () => { + const newData = { + ...TEST_DATA, + ivTimeSeriesState: { + ...TEST_DATA.ivTimeSeriesState, + currentIVVariableID: '45807202' + } + }; + const result = getLegendMarkerRows(newData); + + expect(result.length).toBe(5); + expect(result[0].length).toBe(3); + expect(result[0][0].type).toEqual(textOnlyMarker); + expect(result[0][1].type).toEqual(lineMarker); + expect(result[0][2].type).toEqual(lineMarker); + }); + + it('Should return markers only for time series shown', () => { + const newData = { + ...TEST_DATA, + ivTimeSeriesState: { + ...TEST_DATA.ivTimeSeriesState, + showIVTimeSeries: { + 'current': true, + 'compare': false, + 'median': false + } + } + }; + + const result = getLegendMarkerRows(newData); + + expect(result.length).toBe(1); + expect(result[0].length).toBe(4); + expect(result[0][0].type).toEqual(textOnlyMarker); + expect(result[0][1].type).toEqual(lineMarker); + expect(result[0][2].type).toEqual(rectangleMarker); + expect(result[0][3].type).toEqual(rectangleMarker); + }); + }); +}); \ No newline at end of file diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/selectors/parameter-data.js b/assets/src/scripts/monitoring-location/components/hydrograph/selectors/parameter-data.js new file mode 100644 index 0000000000000000000000000000000000000000..400cba08e6d989410e656c35313118c8bfd31938 --- /dev/null +++ b/assets/src/scripts/monitoring-location/components/hydrograph/selectors/parameter-data.js @@ -0,0 +1,41 @@ +import {createSelector} from 'reselect'; + +import {sortedParameters} from '../../../../utils'; + +import {getCurrentVariableID, getTimeSeries, getVariables} from '../../../selectors/time-series-selector'; + +/** + * Returns a Redux selector function which returns an sorted array of metadata + * for each available parameter code. Each object has the following properties: + * @prop {String} variableID + * @prop {String} parameterCode + * @prop {String} description + * @prop {Boolean} selected - True if this is the currently selected parameter + * @prop {Number} timeSeriesCount - count of unique time series for this parameter + */ +export const getAvailableParameterCodes = createSelector( + getVariables, + getTimeSeries, + getCurrentVariableID, + (variables, timeSeries, currentVariableID) => { + if (!variables) { + return []; + } + + const seriesList = Object.values(timeSeries); + const availableVariableIds = seriesList.map(x => x.variable); + return sortedParameters(variables) + .filter(variable => availableVariableIds.includes(variable.oid)) + .map((variable) => { + return { + variableID: variable.oid, + parameterCode: variable.variableCode.value, + description: variable.variableDescription, + selected: currentVariableID === variable.oid, + timeSeriesCount: seriesList.filter(ts => { + return ts.tsKey === 'current:P7D' && ts.variable === variable.oid; + }).length + }; + }); + } +); diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/selectors/parameter-data.spec.js b/assets/src/scripts/monitoring-location/components/hydrograph/selectors/parameter-data.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..f61db3bc44b8fd5fd26ef3ee47abfb0a3e2d3de8 --- /dev/null +++ b/assets/src/scripts/monitoring-location/components/hydrograph/selectors/parameter-data.spec.js @@ -0,0 +1,169 @@ + +import {getAvailableParameterCodes} from './parameter-data'; + +describe('monitoring-location/components/hydrograph/selectors/parameter-data', () => { + describe('getAvailableParameterCodes', () => { + it('sets attributes correctly when all series have data points', () => { + const available = getAvailableParameterCodes({ + ivTimeSeriesData: { + timeSeries: { + 'current:00060': {description: '00060', tsKey: 'current:P7D', variable: 'code0', points: [{x: 1, y: 2}]}, + 'current:00061': {description: '00061', tsKey: 'current:P7D', variable: 'code1', points: [{x: 2, y: 3}]}, + 'current:00062': {description: '00062', tsKey: 'current:P7D', variable: 'code2', points: [{x: 3, y: 4}]}, + 'compare:00061': {description: '00061', tsKey: 'compare:P7D', variable: 'code1', points: [{x: 1, y: 17}]}, + 'compare:00062': {description: '00062', tsKey: 'compare:P7D', variable: 'code2', points: [{x: 2, y: 18}]}, + 'compare:00063': {description: '00063', tsKey: 'compare:P7D', variable: 'code3', points: [{x: 3, y: 46}]} + }, + variables: { + 'code0': { + oid: 'code0', + variableDescription: 'code0 desc', + variableCode: { + value: '00060' + } + }, + 'code1': { + oid: 'code1', + variableDescription: 'code1 desc', + variableCode: { + value: '00061' + } + }, + 'code2': { + oid: 'code2', + variableDescription: 'code2 desc', + variableCode: { + value: '00062' + } + }, + 'code3': { + oid: 'code3', + variableDescription: 'code3 desc', + variableCode: { + value: '00063' + } + } + } + }, + ivTimeSeriesState: { + currentIVVariableID: 'code0' + } + }); + // Series are ordered by parameter code and have expected values. + expect(available).toEqual([ + ['00060', {variableID: 'code0', description: 'code0 desc', selected: true, currentTimeSeriesCount: 1}], + ['00061', {variableID: 'code1', description: 'code1 desc', selected: false, currentTimeSeriesCount: 1}], + ['00062', {variableID: 'code2', description: 'code2 desc', selected: false, currentTimeSeriesCount: 1}] + ]); + }); + + it('sets attributes correctly when not all series have data points', () => { + const available = getAvailableParameterCodes({ + ivTimeSeriesData: { + timeSeries: { + 'current:00060': {description: '00060', tsKey: 'current:P7D', variable: 'code0', points: [{x: 1, y: 2}]}, + 'current:00061': {description: '00061', tsKey: 'current:P7D', variable: 'code1', points: [{x: 2, y: 3}]}, + 'current:00062': {description: '00062', tsKey: 'current:P7D', variable: 'code2', points: [{x: 3, y: 4}]}, + 'compare:00061': {description: '00061', tsKey: 'compare:P7D', variable: 'code1', points: []}, + 'compare:00062': {description: '00062', tsKey: 'compare:P7D', variable: 'code2', points: [{x: 2, y: 18}]}, + 'compare:00063': {description: '00063', tsKey: 'compare:P7D', variable: 'code3', points: [{x: 3, y: 46}]} + }, + variables: { + 'code0': { + oid: 'code0', + variableDescription: 'code0 desc', + variableCode: { + value: '00060' + } + }, + 'code1': { + oid: 'code1', + variableDescription: 'code1 desc', + variableCode: { + value: '00061' + } + }, + 'code2': { + oid: 'code2', + variableDescription: 'code2 desc', + variableCode: { + value: '00062' + } + }, + 'code3': { + oid: 'code3', + variableDescription: 'code3 desc', + variableCode: { + value: '00063' + } + } + } + }, + ivTimeSeriesState: { + currentIVVariableID: 'code0' + } + }); + // Series are ordered by parameter code and have expected values. + expect(available).toEqual([ + ['00060', {variableID: 'code0', description: 'code0 desc', selected: true, currentTimeSeriesCount: 1}], + ['00061', {variableID: 'code1', description: 'code1 desc', selected: false, currentTimeSeriesCount: 1}], + ['00062', {variableID: 'code2', description: 'code2 desc', selected: false, currentTimeSeriesCount: 1}] + ]); + }); + + it('time series without data points are considered available', () => { + const available = getAvailableParameterCodes({ + ivTimeSeriesData: { + timeSeries: { + 'current:00060': {description: '00060', tsKey: 'current:P7D', variable: 'code0', points: [{x: 1, y: 2}]}, + 'current:00061': {description: '00061', tsKey: 'current:P7D', variable: 'code1', points: []}, + 'current:00062': {description: '00062', tsKey: 'current:P7D', variable: 'code2', points: [{x: 3, y: 4}]}, + 'compare:00061': {description: '00061', tsKey: 'compare:P7D', variable: 'code1', points: []}, + 'compare:00062': {description: '00062', tsKey: 'compare:P7D', variable: 'code2', points: [{x: 2, y: 18}]}, + 'compare:00063': {description: '00063', tsKey: 'compare:P7D', variable: 'code3', points: [{x: 3, y: 46}]} + }, + variables: { + 'code0': { + oid: 'code0', + variableDescription: 'code0 desc', + variableCode: { + value: '00060' + } + }, + 'code1': { + oid: 'code1', + variableDescription: 'code1 desc', + variableCode: { + value: '00061' + } + }, + 'code2': { + oid: 'code2', + variableDescription: 'code2 desc', + variableCode: { + value: '00062' + } + }, + 'code3': { + oid: 'code3', + variableDescription: 'code3 desc', + variableCode: { + value: '00063' + } + } + } + }, + ivTimeSeriesState: { + currentIVVariableID: 'code0' + } + }); + // Series are ordered by parameter code and have expected values. + expect(available).toEqual([ + ['00060', {variableID: 'code0', description: 'code0 desc', selected: true, currentTimeSeriesCount: 1}], + ['00061', {variableID: 'code1', description: 'code1 desc', selected: false, currentTimeSeriesCount: 1}], + ['00062', {variableID: 'code2', description: 'code2 desc', selected: false, currentTimeSeriesCount: 1}] + ]); + }); + }); + +}); \ No newline at end of file diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/scales.js b/assets/src/scripts/monitoring-location/components/hydrograph/selectors/scales.js similarity index 86% rename from assets/src/scripts/monitoring-location/components/hydrograph/scales.js rename to assets/src/scripts/monitoring-location/components/hydrograph/selectors/scales.js index 02bb6df1999e223762bebbf976f1fdeb19ff911e..dc62f286f9927cb5b9648d02aa1232d4c5daa4ec 100644 --- a/assets/src/scripts/monitoring-location/components/hydrograph/scales.js +++ b/assets/src/scripts/monitoring-location/components/hydrograph/selectors/scales.js @@ -2,13 +2,13 @@ import {scaleLinear, scaleSymlog} from 'd3-scale'; import memoize from 'fast-memoize'; import {createSelector} from 'reselect'; -import {getVariables, getCurrentParmCd, getRequestTimeRange, getTimeSeriesForTsKey} from '../../selectors/time-series-selector'; -import {convertCelsiusToFahrenheit, convertFahrenheitToCelsius} from '../../../utils'; +import {getVariables, getCurrentParmCd, getRequestTimeRange, getTimeSeriesForTsKey} from '../../../selectors/time-series-selector'; +import {convertCelsiusToFahrenheit, convertFahrenheitToCelsius} from '../../../../utils'; import {getYDomain, SYMLOG_PARMS} from './domain'; -import {visiblePointsSelector, pointsByTsKeySelector} from './drawing-data'; +import {getPointsByTsKey} from './drawing-data'; import {getLayout} from './layout'; -import {TEMPERATURE_PARAMETERS} from './time-series'; +import {TEMPERATURE_PARAMETERS} from './time-series-data'; const REVERSE_AXIS_PARMS = [ '72019', @@ -20,6 +20,10 @@ const REVERSE_AXIS_PARMS = [ '72148' ]; +/* The two create* functions are helper functions. They are exported primariy + * for ease of testing + */ + /** * Create an x-scale oriented on the left * @param {Array} timeRange - Object containing the start and end times. @@ -101,10 +105,9 @@ export const getBrushXScale = (tsKey) => getXScale('BRUSH', tsKey); */ export const getYScale = memoize(kind => createSelector( getLayout(kind), - visiblePointsSelector, + getYDomain, getCurrentParmCd, - (layout, pointArrays, currentVarParmCd) => { - const yDomain = getYDomain(pointArrays, currentVarParmCd); + (layout, yDomain, currentVarParmCd) => { return createYScale(currentVarParmCd, yDomain, layout.height - (layout.margin.top + layout.margin.bottom)); } )); @@ -114,11 +117,10 @@ export const getBrushYScale = getYScale('BRUSH'); export const getSecondaryYScale = memoize(kind => createSelector( getLayout(kind), - visiblePointsSelector, + getYDomain, getCurrentParmCd, - (layout, pointArrays, currentVarParmCd) => { - const yDomain = getYDomain(pointArrays, currentVarParmCd); - let convertedYDomain = [0, 1]; + (layout, yDomain, currentVarParmCd) => { + let convertedYDomain; if (TEMPERATURE_PARAMETERS.celsius.includes(currentVarParmCd)) { convertedYDomain = yDomain.map(celsius => convertCelsiusToFahrenheit(celsius)); } else if (TEMPERATURE_PARAMETERS.fahrenheit.includes(currentVarParmCd)) { @@ -139,8 +141,8 @@ export const getSecondaryYScale = memoize(kind => createSelector( * @param {String} tsKey Time series key * @return {Object} - keys are parmCd and values are array of array of points */ -const parmCdPointsSelector = memoize((tsKey, period) => createSelector( - pointsByTsKeySelector(tsKey, period), +const getParmCdPoints = memoize((tsKey, period) => createSelector( + getPointsByTsKey(tsKey, period), getTimeSeriesForTsKey(tsKey, period), getVariables, (tsPoints, timeSeries, variables) => { @@ -160,8 +162,8 @@ const parmCdPointsSelector = memoize((tsKey, period) => createSelector( * Returns x and y scales for all "current" time series. * @type {Object} Mapping of parameter code to time series list. */ -export const timeSeriesScalesByParmCdSelector = memoize((tsKey, period, dimensions) => createSelector( - parmCdPointsSelector(tsKey, period), +export const getTimeSeriesScalesByParmCd= memoize((tsKey, period, dimensions) => createSelector( + getParmCdPoints(tsKey, period), getRequestTimeRange(tsKey, period), (pointsByParmCd, requestTimeRange) => { return Object.keys(pointsByParmCd).reduce((tsScales, parmCd) => { diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/scales.spec.js b/assets/src/scripts/monitoring-location/components/hydrograph/selectors/scales.spec.js similarity index 100% rename from assets/src/scripts/monitoring-location/components/hydrograph/scales.spec.js rename to assets/src/scripts/monitoring-location/components/hydrograph/selectors/scales.spec.js diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/time-series.js b/assets/src/scripts/monitoring-location/components/hydrograph/selectors/time-series-data.js similarity index 71% rename from assets/src/scripts/monitoring-location/components/hydrograph/time-series.js rename to assets/src/scripts/monitoring-location/components/hydrograph/selectors/time-series-data.js index 632e20e038f9658096be114cc7b382919b0ccaab..e0f13af46586f988c126ed3111920edf9d3e1b6e 100644 --- a/assets/src/scripts/monitoring-location/components/hydrograph/time-series.js +++ b/assets/src/scripts/monitoring-location/components/hydrograph/selectors/time-series-data.js @@ -5,8 +5,8 @@ import {createSelector} from 'reselect'; import { getRequestTimeRange, getCurrentVariable, getTimeSeriesForTsKey, getCurrentParmCd, getCurrentMethodID, getMethods -} from '../../selectors/time-series-selector'; -import {getIanaTimeZone} from '../../selectors/time-zone-selector'; +} from '../../../selectors/time-series-selector'; +import {getIanaTimeZone} from '../../../selectors/time-zone-selector'; export const TEMPERATURE_PARAMETERS = { @@ -47,7 +47,6 @@ export const hasTimeSeriesWithPoints = memoize((tsKey, period) => createSelector return seriesWithPoints.length > 0; })); - /** * Factory function creates a function that: * Returns the current show state of a time series. @@ -55,22 +54,23 @@ export const hasTimeSeriesWithPoints = memoize((tsKey, period) => createSelector * @param {String} tsKey Time series key * @return {Boolean} Show state of the time series */ -export const isVisibleSelector = memoize(tsKey => (state) => { +export const isVisible = memoize(tsKey => (state) => { return state.ivTimeSeriesState.showIVTimeSeries[tsKey]; }); - /** - * @return {String} The label for the y-axis + * Returns a Redux selector function which returns the label to be used for the Y axis */ -export const yLabelSelector = createSelector( +export const getYLabel = createSelector( getCurrentVariable, variable => variable ? variable.variableDescription : '' ); - -export const secondaryYLabelSelector = createSelector( +/* + * Returns a Redux selector function which returns the label to be used for the secondary y axis + */ +export const getSecondaryYLabel= createSelector( getCurrentParmCd, parmCd => { let secondaryYLabel = null; @@ -85,9 +85,9 @@ export const secondaryYLabelSelector = createSelector( /** - * @return {String} The title to include in the hyrdograph, will include method description if defined. + * Returns a Redux selector function which returns the title to be used for the hydrograph */ -export const titleSelector = createSelector( +export const getTitle = createSelector( getCurrentVariable, getCurrentMethodID, getMethods, @@ -101,11 +101,10 @@ export const titleSelector = createSelector( ); -/** - * @return {String} Description for the currently display set of time - * series +/* + * Returns a Redux selector function which returns the description of the hydrograph */ -export const descriptionSelector = createSelector( +export const getDescription = createSelector( getCurrentVariable, getRequestTimeRange('current', 'P7D'), getIanaTimeZone, @@ -120,14 +119,18 @@ export const descriptionSelector = createSelector( ); /** - * Select the time zone. If the time zone is null, use `local` as the time zone - * - * @ return {String} - IANA time zone - * + * Returns a Redux selector function which returns the iana time zone or local if none is set */ -export const tsTimeZoneSelector = createSelector( +export const getTsTimeZone= createSelector( getIanaTimeZone, ianaTimeZone => { return ianaTimeZone !== null ? ianaTimeZone : 'local'; } ); + +export const getQualifiers = state => state.ivTimeSeriesData.qualifiers; + +export const getCurrentVariableUnitCode = createSelector( + getCurrentVariable, + variable => variable ? variable.unit.unitCode : null +); diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/time-series.spec.js b/assets/src/scripts/monitoring-location/components/hydrograph/selectors/time-series-data.spec.js similarity index 89% rename from assets/src/scripts/monitoring-location/components/hydrograph/time-series.spec.js rename to assets/src/scripts/monitoring-location/components/hydrograph/selectors/time-series-data.spec.js index f71402f1bdc8e0dc778847144047e747a073b6b3..31e5d442b88f8821578c64997ea25d88b385e50a 100644 --- a/assets/src/scripts/monitoring-location/components/hydrograph/time-series.spec.js +++ b/assets/src/scripts/monitoring-location/components/hydrograph/selectors/time-series-data.spec.js @@ -1,6 +1,6 @@ import { - hasTimeSeriesWithPoints, isVisibleSelector, yLabelSelector, titleSelector, - descriptionSelector, tsTimeZoneSelector, secondaryYLabelSelector} from './time-series'; + hasTimeSeriesWithPoints, isVisible, getYLabel, getTitle, + getDescription, getTsTimeZone, getSecondaryYLabel} from './time-series-data'; const TEST_DATA = { @@ -227,7 +227,7 @@ describe('monitoring-location/components/hydrograph/time-series module', () => { }); }); - describe('isVisibleSelector', () => { + describe('isVisible', () => { it('Returns whether the time series is visible', () => { const store = { ivTimeSeriesState: { @@ -239,19 +239,19 @@ describe('monitoring-location/components/hydrograph/time-series module', () => { } }; - expect(isVisibleSelector('current')(store)).toBe(true); - expect(isVisibleSelector('compare')(store)).toBe(false); - expect(isVisibleSelector('median')(store)).toBe(true); + expect(isVisible('current')(store)).toBe(true); + expect(isVisible('compare')(store)).toBe(false); + expect(isVisible('median')(store)).toBe(true); }); }); describe('yLabelSelector', () => { it('Returns string to be used for labeling the y axis', () => { - expect(yLabelSelector(TEST_DATA)).toBe('Discharge, cubic feet per second'); + expect(getYLabel(TEST_DATA)).toBe('Discharge, cubic feet per second'); }); it('Returns empty string if no variable selected', () => { - expect(yLabelSelector({ + expect(getYLabel({ ...TEST_DATA, ivTimeSeriesState: { ...TEST_DATA.ivTimeSeriesState, @@ -261,9 +261,9 @@ describe('monitoring-location/components/hydrograph/time-series module', () => { }); }); - describe('secondaryYLabelSelector', () => { + describe('getSecondaryYLabel', () => { it('returns a secondary label when a celsius parameter is selected', () => { - expect(secondaryYLabelSelector({ + expect(getSecondaryYLabel({ ...TEST_DATA, ivTimeSeriesState: { ...TEST_DATA.ivTimeSeriesState, @@ -273,7 +273,7 @@ describe('monitoring-location/components/hydrograph/time-series module', () => { }); it('returns a secondary label when a fahrenheit parameter is selected', () => { - expect(secondaryYLabelSelector({ + expect(getSecondaryYLabel({ ...TEST_DATA, ivTimeSeriesState: { ...TEST_DATA.ivTimeSeriesState, @@ -283,16 +283,16 @@ describe('monitoring-location/components/hydrograph/time-series module', () => { }); it('does not return a secondary when a parameter when a non-temperature parameter is selected', () => { - expect(secondaryYLabelSelector(TEST_DATA)).toBeNull(); + expect(getSecondaryYLabel(TEST_DATA)).toBeNull(); }); }); - describe('titleSelector', () => { + describe('getTitle', () => { it('Returns the string to used for graph title', () => { - expect(titleSelector(TEST_DATA)).toBe('Streamflow'); + expect(getTitle(TEST_DATA)).toBe('Streamflow'); }); it('Returns the title string with the method description appended', () => { - expect(titleSelector({ + expect(getTitle({ ...TEST_DATA, ivTimeSeriesState: { ...TEST_DATA.ivTimeSeriesState, @@ -301,7 +301,7 @@ describe('monitoring-location/components/hydrograph/time-series module', () => { })).toBe('Streamflow' + ', ' + '4.1 ft from riverbed (middle)'); }); it('Returns empty string if no variable selected', () => { - expect(titleSelector({ + expect(getTitle({ ...TEST_DATA, ivTimeSeriesState: { ...TEST_DATA.ivTimeSeriesState, @@ -311,9 +311,9 @@ describe('monitoring-location/components/hydrograph/time-series module', () => { }); }); - describe('descriptionSelector', () => { + describe('getDescription', () => { it('Returns a description with the date for the current times series', () => { - const result = descriptionSelector(TEST_DATA); + const result = getDescription(TEST_DATA); expect(result).toContain('Discharge, cubic feet per second'); expect(result).toContain('1/2/2017'); @@ -321,24 +321,24 @@ describe('monitoring-location/components/hydrograph/time-series module', () => { }); }); - describe('tsTimeZoneSelector', () => { + describe('getTsTimeZone', () => { it('Returns local if series is empty', () => { - const result = tsTimeZoneSelector({ + const result = getTsTimeZone({ series: {} }); expect(result).toEqual('local'); }); it('Returns local if timezone is null', () => { - const result = tsTimeZoneSelector({ + const result = getTsTimeZone({ ianaTimeZone: null }); expect(result).toEqual('local'); }); it('Returns the IANA timezone NWIS and IANA agree', () => { - const result = tsTimeZoneSelector({ + const result = getTsTimeZone({ ianaTimeZone: 'America/New_York' }); expect(result).toEqual('America/New_York'); diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/time-series-graph.js b/assets/src/scripts/monitoring-location/components/hydrograph/time-series-graph.js index 37939cd82b37dad2fdbea30909c5d8b3be192af1..c0696481b32cdc4e13b4052674ac5fd93dd8bc55 100644 --- a/assets/src/scripts/monitoring-location/components/hydrograph/time-series-graph.js +++ b/assets/src/scripts/monitoring-location/components/hydrograph/time-series-graph.js @@ -9,18 +9,18 @@ import {link} from '../../../lib/d3-redux'; import {mediaQuery} from '../../../utils'; import {getAgencyCode, getMonitoringLocationName} from '../../selectors/time-series-selector'; -import {waterwatchVisible, getWaterwatchFloodLevels} from '../../selectors/flood-data-selector'; +import {isWaterwatchVisible, getWaterwatchFloodLevels} from '../../selectors/flood-data-selector'; -import {getAxes} from './axes'; +import {getAxes} from './selectors/axes'; import { - currentVariableLineSegmentsSelector, + getCurrentVariableLineSegments, getCurrentVariableMedianStatPoints, HASH_ID } from './drawing-data'; -import {getMainLayout} from './layout'; -import {getMainXScale, getMainYScale, getBrushXScale} from './scales'; -import {descriptionSelector, isVisibleSelector, titleSelector} from './time-series'; -import {drawDataLines} from './time-series-data'; +import {getMainLayout} from './selectors/layout'; +import {getMainXScale, getMainYScale, getBrushXScale} from './selectors/scales'; +import {descriptionSelector, isVisibleSelector, titleSelector} from './selectors/time-series-data'; +import {drawDataLines} from './time-series-lines'; import {drawTooltipFocus, drawTooltipText} from './tooltip'; const addDefsPatterns = function(elem) { @@ -236,7 +236,7 @@ export const drawTimeSeriesGraph = function(elem, store, siteNo, showMLName, sho .call(link(store, appendAxes, getAxes())) .call(link(store, drawDataLines, createStructuredSelector({ visible: isVisibleSelector('current'), - tsLinesMap: currentVariableLineSegmentsSelector('current'), + tsLinesMap: getCurrentVariableLineSegments('current'), xScale: getMainXScale('current'), yScale: getMainYScale, tsKey: () => 'current', @@ -244,7 +244,7 @@ export const drawTimeSeriesGraph = function(elem, store, siteNo, showMLName, sho }))) .call(link(store, drawDataLines, createStructuredSelector({ visible: isVisibleSelector('compare'), - tsLinesMap: currentVariableLineSegmentsSelector('compare'), + tsLinesMap: getCurrentVariableLineSegments('compare'), xScale: getMainXScale('compare'), yScale: getMainYScale, tsKey: () => 'compare', @@ -257,7 +257,7 @@ export const drawTimeSeriesGraph = function(elem, store, siteNo, showMLName, sho seriesPoints: getCurrentVariableMedianStatPoints }))) .call(link(store, plotAllFloodLevelPoints, createStructuredSelector({ - visible: waterwatchVisible, + visible: isWaterwatchVisible, xscale: getBrushXScale('current'), yscale: getMainYScale, seriesPoints: getWaterwatchFloodLevels diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/time-series-data.js b/assets/src/scripts/monitoring-location/components/hydrograph/time-series-lines.js similarity index 100% rename from assets/src/scripts/monitoring-location/components/hydrograph/time-series-data.js rename to assets/src/scripts/monitoring-location/components/hydrograph/time-series-lines.js diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/tooltip.js b/assets/src/scripts/monitoring-location/components/hydrograph/tooltip.js index 14aa7d2e340c7c6d4d6fa0cef60357a39e763778..10eb1ad81a1d5b92ed0758f4601788d711573388 100644 --- a/assets/src/scripts/monitoring-location/components/hydrograph/tooltip.js +++ b/assets/src/scripts/monitoring-location/components/hydrograph/tooltip.js @@ -1,7 +1,6 @@ import {set} from 'd3-collection'; import {select} from 'd3-selection'; import {transition} from 'd3-transition'; -import memoize from 'fast-memoize'; import {DateTime} from 'luxon'; import {createSelector, createStructuredSelector} from 'reselect'; @@ -11,43 +10,16 @@ import {drawFocusOverlay, drawFocusCircles, drawFocusLine} from '../../../d3-ren import {link} from '../../../lib/d3-redux'; import {mediaQuery, convertCelsiusToFahrenheit, convertFahrenheitToCelsius} from '../../../utils'; -import {getCurrentVariable, getCurrentParmCd} from '../../selectors/time-series-selector'; +import {getCurrentParmCd} from '../../selectors/time-series-selector'; import {Actions} from '../../store/instantaneous-value-time-series-state'; -import {cursorTimeSelector, tsCursorPointsSelector} from './cursor'; +import {getCursorTime, getTsCursorPoints, getTooltipPoints} from './selectors/cursor'; import {classesForPoint, MASK_DESC} from './drawing-data'; -import {getMainLayout} from './layout'; -import {getMainXScale, getMainYScale} from './scales'; -import {tsTimeZoneSelector, TEMPERATURE_PARAMETERS} from './time-series'; +import {getMainLayout} from './selectors/layout'; +import {getMainXScale, getMainYScale} from './selectors/scales'; +import {getTsTimeZone, getQualifiers, getCurrentVariableUnitCode, TEMPERATURE_PARAMETERS} from './selectors/time-series-data'; -/* - * Returns a function that returns the time series data point nearest the - * tooltip focus time for the given time series key. Only returns those points - * where the y-value is finite; no use in making a point if y is Infinity. - * - * @param {Object} state - Redux store - * @param String} tsKey - Time series key - * @return {Object} - */ -export const tooltipPointsSelector = memoize(tsKey => createSelector( - getMainXScale(tsKey), - getMainYScale, - tsCursorPointsSelector(tsKey), - (xScale, yScale, cursorPoints) => { - return Object.keys(cursorPoints).reduce((tooltipPoints, tsID) => { - const cursorPoint = cursorPoints[tsID]; - if (isFinite(yScale(cursorPoint.value))) { - tooltipPoints.push({ - x: xScale(cursorPoint.dateTime), - y: yScale(cursorPoint.value) - }); - } - return tooltipPoints; - }, []); - } -)); - const getTooltipText = function(datum, qualifiers, unitCode, ianaTimeZone, currentParmCd) { let label = ''; if (datum && qualifiers) { @@ -84,13 +56,6 @@ const getTooltipText = function(datum, qualifiers, unitCode, ianaTimeZone, curre return label; }; -const qualifiersSelector = state => state.ivTimeSeriesData.qualifiers; - -const unitCodeSelector = createSelector( - getCurrentVariable, - variable => variable ? variable.unit.unitCode : null -); - const createTooltipTextGroup = function (elem, {currentPoints, comparePoints, qualifiers, unitCode, ianaTimeZone, layout, currentParmCd}, textGroup) { // Find the width of the between the y-axis and margin and set the tooltip margin based on that number const adjustMarginOfTooltips = function (elem) { @@ -187,12 +152,12 @@ const createTooltipTextGroup = function (elem, {currentPoints, comparePoints, qu */ export const drawTooltipText = function (elem, store) { elem.call(link(store, createTooltipTextGroup, createStructuredSelector({ - currentPoints: tsCursorPointsSelector('current'), - comparePoints: tsCursorPointsSelector('compare'), - qualifiers: qualifiersSelector, - unitCode: unitCodeSelector, + currentPoints: getTsCursorPoints('current'), + comparePoints: getTsCursorPoints('compare'), + qualifiers: getQualifiers, + unitCode: getCurrentVariableUnitCode, layout: getMainLayout, - ianaTimeZone: tsTimeZoneSelector, + ianaTimeZone: getTsTimeZone, currentParmCd: getCurrentParmCd }))); }; @@ -207,12 +172,12 @@ export const drawTooltipFocus = function(elem, store) { elem.call(link(store, drawFocusLine, createStructuredSelector({ xScale: getMainXScale('current'), yScale: getMainYScale, - cursorTime: cursorTimeSelector('current') + cursorTime: getCursorTime('current') }))); elem.call(link(store, drawFocusCircles, createSelector( - tooltipPointsSelector('current'), - tooltipPointsSelector('compare'), + getTooltipPoints('current'), + getTooltipPoints('compare'), (current, compare) => { return current.concat(compare); } @@ -247,4 +212,4 @@ export const drawTooltipCursorSlider = function(elem, store) { xScale: getMainXScale('current'), layout: getMainLayout }), store, Actions.setIVGraphCursorOffset)); -}; \ No newline at end of file +}; diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/tooltip.spec.js b/assets/src/scripts/monitoring-location/components/hydrograph/tooltip.spec.js index 927ea1a2ef6d40882939ee6f11afecf4cc1da8be..7b32995aa28acf21b706e6f2f1d6aa886cfc8888 100644 --- a/assets/src/scripts/monitoring-location/components/hydrograph/tooltip.spec.js +++ b/assets/src/scripts/monitoring-location/components/hydrograph/tooltip.spec.js @@ -171,45 +171,6 @@ describe('monitoring-location/components/hydrograph/tooltip module', () => { } }; - describe('tooltipPointsSelector', () => { - const id = (val) => val; - - it('should return the requested time series focus time', () => { - expect(tooltipPointsSelector('current').resultFunc(id, id, { - '00060:current': { - dateTime: '1date', - value: 1 - }, - '00060:compare': { - dateTime: '2date', - value: 2 - } - })).toEqual([{ - x: '1date', - y: 1 - }, { - x: '2date', - y: 2 - }]); - }); - - it('should exclude values that are infinite', () => { - expect(tooltipPointsSelector('current').resultFunc(id, id, { - '00060:current': { - dateTime: '1date', - value: Infinity - }, - '00060:compare': { - dateTime: '2date', - value: 2 - } - })).toEqual([{ - x: '2date', - y: 2 - }]); - }); - }); - describe('drawTooltipText', () => { let div; beforeEach(() => { diff --git a/assets/src/scripts/monitoring-location/selectors/flood-data-selector.js b/assets/src/scripts/monitoring-location/selectors/flood-data-selector.js index 058add78228e6669eaba7d34aa9a21c70f457cdb..ed4cdc1c00234c8d7b887bb28184bb53d61ad705 100644 --- a/assets/src/scripts/monitoring-location/selectors/flood-data-selector.js +++ b/assets/src/scripts/monitoring-location/selectors/flood-data-selector.js @@ -29,7 +29,7 @@ export const hasWaterwatchData = createSelector( /* * Provides a function which returns True if waterwatch flood levels should be visible. */ -export const waterwatchVisible = createSelector( +export const isWaterwatchVisible = createSelector( hasWaterwatchData, getCurrentParmCd, (hasFloodLevels, paramCd) => diff --git a/assets/src/scripts/monitoring-location/selectors/flood-data-selector.spec.js b/assets/src/scripts/monitoring-location/selectors/flood-data-selector.spec.js index efd7367fdba623c5d66ec968f5f3c0c1adac7d08..c1e663c66b974bac108d9c88a3b7a8a16510656a 100644 --- a/assets/src/scripts/monitoring-location/selectors/flood-data-selector.spec.js +++ b/assets/src/scripts/monitoring-location/selectors/flood-data-selector.spec.js @@ -1,5 +1,5 @@ import {getFloodStageHeight, hasFloodData, getFloodGageHeightStageIndex, - hasWaterwatchData, getWaterwatchFloodLevels, waterwatchVisible} from './flood-data-selector'; + hasWaterwatchData, getWaterwatchFloodLevels, isWaterwatchVisible} from './flood-data-selector'; describe('monitoring-location/selectors/flood-data-selector', () => { @@ -119,9 +119,9 @@ describe('monitoring-location/selectors/flood-data-selector', () => { }); }); - describe('waterwatchVisible', () => { + describe('isWaterwatchVisible', () => { it('Return false if waterwatch flood levels should not be visible due to parameter code', () =>{ - expect(waterwatchVisible({ + expect(isWaterwatchVisible({ floodData: { floodLevels: { site_no: '07144100', @@ -145,7 +145,7 @@ describe('monitoring-location/selectors/flood-data-selector', () => { }); it('Return false if waterwatch flood levels should not be visible due to no flood levels', () =>{ - expect(waterwatchVisible({ + expect(isWaterwatchVisible({ floodData: { floodLevels: null }, @@ -163,7 +163,7 @@ describe('monitoring-location/selectors/flood-data-selector', () => { }); it('Return true if waterwatch flood levels should be visible', () =>{ - expect(waterwatchVisible({ + expect(isWaterwatchVisible({ floodData: { floodLevels: { site_no: '07144100', diff --git a/assets/src/scripts/utils.js b/assets/src/scripts/utils.js index 0d5d3520ee03c6ec54655b0624af7e2a93d250d8..8bf615cac64fd91595868512e91c12f9c745471f 100644 --- a/assets/src/scripts/utils.js +++ b/assets/src/scripts/utils.js @@ -228,8 +228,8 @@ export const convertCelsiusToFahrenheit = function(celsius) { /* * Return the variables sorted with the ones we care about first - * @param {Array of String} - * @return {Array of String} + * @param {Array of variable Object} + * @return {Array of variable Object} */ export const sortedParameters = function (variables) { const PARAM_PERTINENCE = {