diff --git a/assets/src/scripts/components/hydrograph/index.js b/assets/src/scripts/components/hydrograph/index.js index b0a21f36577e8a6cbc476bb1bc93d0694cea8570..915cb4a7d86c6eb64a703ed7eddb9ec0d99c63a3 100644 --- a/assets/src/scripts/components/hydrograph/index.js +++ b/assets/src/scripts/components/hydrograph/index.js @@ -15,7 +15,7 @@ const { drawSimpleLegend, legendMarkerRowsSelector } = require('./legend'); const { plotSeriesSelectTable, availableTimeseriesSelector } = require('./parameters'); const { xScaleSelector, yScaleSelector } = require('./scales'); const { Actions, configureStore } = require('./store'); -const { currentVariableSelector, pointsSelector, lineSegmentsSelector, +const { currentVariableLineSegmentsSelector, currentVariableSelector, currentVariableTimeseries, oldPointsSelector, methodsSelector, pointsTableDataSelector, isVisibleSelector, titleSelector, descriptionSelector, timeSeriesSelector, MASK_DESC, HASH_ID } = require('./timeseries'); const { createTooltipFocus, createTooltipText } = require('./tooltip'); @@ -87,7 +87,7 @@ const plotDataLine = function (elem, {visible, lines, tsKey, xScale, yScale}) { }; -const plotDataLines = function (elem, {visible, tsLines, tsKey, xScale, yScale}) { +const plotDataLines = function (elem, {visible, tsLinesMap, tsKey, xScale, yScale}) { const elemId = `ts-${tsKey}-group`; elem.selectAll(`#${elemId}`).remove(); @@ -96,7 +96,7 @@ const plotDataLines = function (elem, {visible, tsLines, tsKey, xScale, yScale}) .attr('id', elemId) .classed('tsKey', true); - for (const lines of tsLines) { + for (const lines of Object.values(tsLinesMap)) { plotDataLine(tsLineGroup, {visible, lines, tsKey, xScale, yScale}); } }; @@ -208,7 +208,7 @@ const plotMedianPoints = function (elem, {xscale, yscale, modulo, points, showLa * @param {Boolean} options.showLabel * @param {Object} options.variable */ -const plotAllMedianPoints = function (elem, {visible, xscale, yscale, pointsList, showLabel, variable}) { +const plotAllMedianPoints = function (elem, {visible, xscale, yscale, seriesMap, showLabel, variable}) { elem.select('#median-points').remove(); if (!visible) { @@ -218,7 +218,8 @@ const plotAllMedianPoints = function (elem, {visible, xscale, yscale, pointsList .append('g') .attr('id', 'median-points'); - for (const [index, points] of pointsList.entries()) { + for (const [index, seriesID] of Object.keys(seriesMap).entries()) { + const points = seriesMap[seriesID].points; plotMedianPoints(container, {xscale, yscale, modulo: index % 6, points, showLabel, variable}); } }; @@ -269,14 +270,14 @@ const timeSeriesGraph = function (elem) { .call(link(appendAxes, axesSelector)) .call(link(plotDataLines, createStructuredSelector({ visible: isVisibleSelector('current'), - tsLines: lineSegmentsSelector('current'), + tsLinesMap: currentVariableLineSegmentsSelector('current'), xScale: xScaleSelector('current'), yScale: yScaleSelector, tsKey: () => 'current' }))) .call(link(plotDataLines, createStructuredSelector({ visible: isVisibleSelector('compare'), - tsLines: lineSegmentsSelector('compare'), + tsLinesMap: currentVariableLineSegmentsSelector('compare'), xScale: xScaleSelector('compare'), yScale: yScaleSelector, tsKey: () => 'compare' @@ -285,15 +286,15 @@ const timeSeriesGraph = function (elem) { xScale: xScaleSelector('current'), yScale: yScaleSelector, compareXScale: xScaleSelector('compare'), - currentTsData: pointsSelector('current'), - compareTsData: pointsSelector('compare'), + currentTsData: oldPointsSelector('current'), + compareTsData: oldPointsSelector('compare'), isCompareVisible: isVisibleSelector('compare') }))) .call(link(plotAllMedianPoints, createStructuredSelector({ visible: isVisibleSelector('median'), xscale: xScaleSelector('current'), yscale: yScaleSelector, - pointsList: pointsSelector('median'), + seriesMap: currentVariableTimeseries('median'), variable: currentVariableSelector, showLabel: (state) => state.showMedianStatsLabel }))); diff --git a/assets/src/scripts/components/hydrograph/index.spec.js b/assets/src/scripts/components/hydrograph/index.spec.js index 196681ffd8415610bf4a4a0ecda5068dad46f9b0..ec01ee1fa5498a4c4a35a529d666966b231ab1f5 100644 --- a/assets/src/scripts/components/hydrograph/index.spec.js +++ b/assets/src/scripts/components/hydrograph/index.spec.js @@ -17,7 +17,8 @@ const TEST_STATE = { qualifiers: ['P'] }], method: 'method1', - tsKey: 'current' + tsKey: 'current', + variable: 45807197 }, '00060:compare': { startTime: new Date('2018-01-02T15:00:00.000-06:00'), @@ -28,7 +29,8 @@ const TEST_STATE = { qualifiers: ['P'] }], method: 'method1', - tsKey: 'compare' + tsKey: 'compare', + variable: 45807197 }, '00060:median': { startTime: new Date('2018-01-02T15:00:00.000-06:00'), @@ -42,7 +44,8 @@ const TEST_STATE = { endYear: '2015' }, method: 'method1', - tsKey: 'median' + tsKey: 'median', + variable: 45807197 } }, timeSeriesCollections: { diff --git a/assets/src/scripts/components/hydrograph/legend.js b/assets/src/scripts/components/hydrograph/legend.js index 2fb6eca7bbc11c24c6e1294d1961d08083fa9685..c98c44cf7b3e7900e2e6b0e4002799a8d6ff57d6 100644 --- a/assets/src/scripts/components/hydrograph/legend.js +++ b/assets/src/scripts/components/hydrograph/legend.js @@ -142,7 +142,7 @@ function drawSimpleLegend(svg, {legendMarkerRows, layout}) { const uniqueMasksSelector = memoize(tsKey => createSelector( lineSegmentsSelector(tsKey), (tsLineSegments) => { - return new Set(tsLineSegments.reduce((masks, lineSegments) => { + return new Set(Object.values(tsLineSegments).reduce((masks, lineSegments) => { Array.prototype.push.apply(masks, lineSegments.map((segment) => segment.classes.dataMask)); return masks; }, []).filter(x => x !== null)); diff --git a/assets/src/scripts/components/hydrograph/timeseries.js b/assets/src/scripts/components/hydrograph/timeseries.js index 6d24832a0836cbc92dec4b831f5ca1fe59e7c568..8361a1d4a353ea9c668e42f21c68fd02fad4caed 100644 --- a/assets/src/scripts/components/hydrograph/timeseries.js +++ b/assets/src/scripts/components/hydrograph/timeseries.js @@ -73,6 +73,24 @@ export const timeSeriesSelector = memoize(tsKey => createSelector( } )); +/** + * Returns a selector that, for a given tsKey: + * Selects all time series data. + * @param {String} tsKey Time-series key + * @param {Object} state Redux state + * @return {Object} Time-series data + */ +export const timeSeriesSelectorNew = memoize(tsKey => createSelector( + allTimeSeriesSelector, + collectionsSelector(tsKey), + (timeSeries, collections) => { + return collections.reduce((series, collection) => { + collection.timeSeries.forEach(sID => series[sID] = timeSeries[sID]); + return series; + }, {}); + } +)); + export const HASH_ID = { current: 'hash-45', compare: 'hash-135' @@ -110,6 +128,40 @@ export const pointsSelector = memoize((tsKey) => createSelector( )); +/** + * Returns a selector that, for a given tsKey: + * Returns an array of time points for all visible time series. + * @param {Object} state Redux store + * @param {String} tsKey Timeseries key + * @return {Array} Array of array of points. + */ +export const pointsSelectorNew = memoize((tsKey) => createSelector( + timeSeriesSelectorNew(tsKey), + (timeSeries) => { + return Object.values(timeSeries).map(series => series.points); + } +)); + + +/** + * Returns a selector that, for a given tsKey: + * Returns time series for the current variable. + * @param {Object} state Redux store + * @param {String} tsKey Timeseries key + * @return {Array} Array of array of points. + */ +export const currentVariableTimeseries = memoize((tsKey) => createSelector( + timeSeriesSelectorNew(tsKey), + currentVariableSelector, + (timeSeries, variable) => { + return Object.keys(timeSeries).filter(sID => timeSeries[sID].variable === variable.oid).reduce((series, sID) => { + series[sID] = timeSeries[sID]; + return series; + }, {}); + } +)); + + /** * Factory function that returns a selector for a given tsKey, that: * Returns a single array of all points. @@ -159,6 +211,10 @@ export const visiblePointsSelector = createSelector( ); +//export const visibleTimeseriesSelector = createSelector( +//) + + /** * Factory function creates a function that: * Returns the current show state of a timeseries. @@ -209,10 +265,12 @@ export const pointsTableDataSelector = memoize(tsKey => createSelector( * @return {Array} Array of array of points. */ export const lineSegmentsSelector = memoize(tsKey => createSelector( - pointsSelector(tsKey), - (tsPoints) => { - const linePoints = []; - for (const points of tsPoints) { + timeSeriesSelector(tsKey), + (seriesMap) => { + const seriesLines = {}; + for (const sID of Object.keys(seriesMap)) { + const series = seriesMap[sID]; + const points = series.points; // Accumulate data into line groups, splitting on the estimated and // approval status. const lines = []; @@ -249,9 +307,34 @@ export const lineSegmentsSelector = memoize(tsKey => createSelector( // Cache the classes for the next loop iteration. lastClasses = lineClasses; } - linePoints.push(lines); + seriesLines[sID] = lines; } - return linePoints; + return seriesLines; + } +)); + + +const currentVariableTimeseriesSelector = memoize(tsKey => createSelector( + timeSeriesSelectorNew(tsKey), + currentVariableSelector, + (seriesMap, variable) => { + return Object.keys(seriesMap).filter( + sID => seriesMap[sID].variable === variable.oid).reduce((curMap, sID) => { + curMap[sID] = seriesMap[sID]; + return curMap; + }, {}); + } +)); + + +export const currentVariableLineSegmentsSelector = memoize(tsKey => createSelector( + currentVariableTimeseriesSelector(tsKey), + lineSegmentsSelector(tsKey), + (seriesMap, linesMap) => { + return Object.keys(seriesMap).reduce((visMap, sID) => { + visMap[sID] = linesMap[sID]; + return visMap; + }, {}); } )); @@ -270,12 +353,11 @@ export const titleSelector = createSelector( export const descriptionSelector = createSelector( currentVariableSelector, - timeSeriesSelector('current'), + timeSeriesSelectorNew('current'), (variable, timeSeries) => { - const timeSeriesList = Object.values(timeSeries); const desc = variable ? variable.variableDescription : ''; - const startTime = Math.min.apply(timeSeriesList.map(ts => ts.startTime)); - const endTime = Math.max.apply(timeSeriesList.map(ts => ts.endTime)); + const startTime = new Date(Math.min.apply(null, Object.values(timeSeries).map(ts => ts.startTime))); + const endTime = new Date(Math.max.apply(null, Object.values(timeSeries).map(ts => ts.endTime))); return `${desc} from ${formatTime(startTime)} to ${formatTime(endTime)}`; } ); diff --git a/assets/src/scripts/components/hydrograph/timeseries.spec.js b/assets/src/scripts/components/hydrograph/timeseries.spec.js index 7d7dd3d913480a4da1e0fb1a4303923fe912ae53..4787fd56b01808debd0b7f074f6a2fff6af4c9c3 100644 --- a/assets/src/scripts/components/hydrograph/timeseries.spec.js +++ b/assets/src/scripts/components/hydrograph/timeseries.spec.js @@ -68,30 +68,44 @@ describe('Timeseries module', () => { } } } - })).toEqual([[{ - classes: { - approved: false, - estimated: false, - dataMask: null - }, - points: [{ - value: 10, - qualifiers: [] - }] - }, { - classes: { - approved: true, - estimated: false, - dataMask: null - }, - points: [{ - value: 10, - qualifiers: ['A'] - }, { - value: 10, - qualifiers: ['A'] - }] - }]]); + })).toEqual({ + '00060': [ + { + 'classes': { + 'approved': false, + 'estimated': false, + 'dataMask': null + }, + 'points': [ + { + 'value': 10, + 'qualifiers': [] + } + ] + }, + { + 'classes': { + 'approved': true, + 'estimated': false, + 'dataMask': null + }, + 'points': [ + { + 'value': 10, + 'qualifiers': [ + 'A' + ] + }, + { + 'value': 10, + 'qualifiers': [ + 'A' + ] + } + ] + } + ] + }); }); it('should separate on estimated', () => { @@ -115,30 +129,48 @@ describe('Timeseries module', () => { } } } - })).toEqual([[{ - classes: { - approved: false, - estimated: false, - dataMask: null - }, - points: [{ - value: 10, - qualifiers: ['P'] - }] - }, { - classes: { - approved: false, - estimated: true, - dataMask: null - }, - points: [{ - value: 10, - qualifiers: ['P', 'E'] - }, { - value: 10, - qualifiers: ['P', 'E'] - }] - }]]); + })).toEqual({ + '00060': [ + { + 'classes': { + 'approved': false, + 'estimated': false, + 'dataMask': null + }, + 'points': [ + { + 'value': 10, + 'qualifiers': [ + 'P' + ] + } + ] + }, + { + 'classes': { + 'approved': false, + 'estimated': true, + 'dataMask': null + }, + 'points': [ + { + 'value': 10, + 'qualifiers': [ + 'P', + 'E' + ] + }, + { + 'value': 10, + 'qualifiers': [ + 'P', + 'E' + ] + } + ] + } + ] + }); }); it('should separate out masked values', () => { @@ -162,41 +194,56 @@ describe('Timeseries module', () => { } } } - })).toEqual([[ - { - classes: { - approved: false, - estimated: false, - dataMask: null - }, - points: [{ - value: 10, - qualifiers: ['P'] - }] - }, - { - classes: { - approved: false, - estimated: false, - dataMask: 'ice' + })).toEqual({ + '00060': [ + { + 'classes': { + 'approved': false, + 'estimated': false, + 'dataMask': null + }, + 'points': [ + { + 'value': 10, + 'qualifiers': [ + 'P' + ] + } + ] }, - points: [{ - value: null, - qualifiers: ['P', 'ICE'] - }] - }, - { - classes: { - approved: false, - estimated: false, - dataMask: 'fld' + { + 'classes': { + 'approved': false, + 'estimated': false, + 'dataMask': 'ice' + }, + 'points': [ + { + 'value': null, + 'qualifiers': [ + 'P', + 'ICE' + ] + } + ] }, - points: [{ - value: null, - qualifiers: ['P', 'FLD'] - }] - } - ]]); + { + 'classes': { + 'approved': false, + 'estimated': false, + 'dataMask': 'fld' + }, + 'points': [ + { + 'value': null, + 'qualifiers': [ + 'P', + 'FLD' + ] + } + ] + } + ]}); }); }); diff --git a/assets/src/scripts/components/hydrograph/tooltip.js b/assets/src/scripts/components/hydrograph/tooltip.js index d6981f1cb8129abed4addc458cf7e6211e195413..62191a634d9153174a207214621830bf05568891 100644 --- a/assets/src/scripts/components/hydrograph/tooltip.js +++ b/assets/src/scripts/components/hydrograph/tooltip.js @@ -189,10 +189,6 @@ const createTooltipFocus = function(elem, {xScale, yScale, compareXScale, curren elem.select('.tooltip-text-group').remove(); elem.select('.overlay').remove(); - // FIXME: Rather than handling a single arbitrary series, handle them all. - currentTsData = currentTsData[0]; - compareTsData = compareTsData[0]; - if (!currentTsData) { return; }