diff --git a/assets/src/scripts/components/hydrograph/index.js b/assets/src/scripts/components/hydrograph/index.js index 19a8f19537cda26531e1510dba725cf873d9b0a6..02d4f99086f2d715c8db093580587319563025db 100644 --- a/assets/src/scripts/components/hydrograph/index.js +++ b/assets/src/scripts/components/hydrograph/index.js @@ -38,10 +38,6 @@ const drawMessage = function (elem, message) { const plotDataLine = function (elem, {visible, lines, tsDataKey, xScale, yScale}) { - const elemId = 'ts-' + tsDataKey; - elem.selectAll(`#${elemId}`).remove(); - elem.selectAll(`.${tsDataKey}-mask-group`).remove(); - if (!visible) { return; } @@ -57,8 +53,7 @@ const plotDataLine = function (elem, {visible, lines, tsDataKey, xScale, yScale} .classed('line', true) .classed('approved', line.classes.approved) .classed('estimated', line.classes.estimated) - .attr('data-title', tsDataKey) - .attr('id', `ts-${tsDataKey}`) + .classed(`ts-${tsDataKey}`, true) .attr('d', tsLine); } else { const maskCode = line.classes.dataMask.toLowerCase(); @@ -90,6 +85,21 @@ const plotDataLine = function (elem, {visible, lines, tsDataKey, xScale, yScale} }; +const plotDataLines = function (elem, {visible, tsLines, tsDataKey, xScale, yScale}) { + const elemId = `ts-${tsDataKey}-group`; + + elem.selectAll(`#${elemId}`).remove(); + const tsLineGroup = elem + .append('g') + .attr('id', elemId) + .classed('tsDataKey', true); + + for (const lines of tsLines) { + plotDataLine(tsLineGroup, {visible, lines, tsDataKey, xScale, yScale}); + } +}; + + const plotSvgDefs = function(elem) { let defs = elem.append('defs'); @@ -201,16 +211,16 @@ const timeSeriesGraph = function (elem) { .append('g') .attr('transform', `translate(${MARGIN.left},${MARGIN.top})`) .call(link(appendAxes, axesSelector)) - .call(link(plotDataLine, createStructuredSelector({ + .call(link(plotDataLines, createStructuredSelector({ visible: isVisibleSelector('current'), - lines: lineSegmentsSelector('current'), + tsLines: lineSegmentsSelector('current'), xScale: xScaleSelector('current'), yScale: yScaleSelector, tsDataKey: () => 'current' }))) - .call(link(plotDataLine, createStructuredSelector({ + .call(link(plotDataLines, createStructuredSelector({ visible: isVisibleSelector('compare'), - lines: lineSegmentsSelector('compare'), + tsLines: lineSegmentsSelector('compare'), xScale: xScaleSelector('compare'), yScale: yScaleSelector, tsDataKey: () => 'compare' diff --git a/assets/src/scripts/components/hydrograph/legend.js b/assets/src/scripts/components/hydrograph/legend.js index 27a818bc2f450e4fff128580f307810f11e85574..28acc7c66577d647569bb4285eb4cc7170b4bb12 100644 --- a/assets/src/scripts/components/hydrograph/legend.js +++ b/assets/src/scripts/components/hydrograph/legend.js @@ -124,9 +124,11 @@ const createLegendMarkers = function(displayItems) { const uniqueMasksSelector = memoize(tsDataKey => createSelector( lineSegmentsSelector(tsDataKey), - (lineSegments) => { - let masks = lineSegments.map((segment) => segment.classes.dataMask ); - return new Set(masks.filter(x => x !== null)); + (tsLineSegments) => { + return new Set(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/parameters.js b/assets/src/scripts/components/hydrograph/parameters.js index f8e9ed385c3adca3fe4472b56081d54c18a04e94..aa62484585237354012b19602e3d6b917fd1f568 100644 --- a/assets/src/scripts/components/hydrograph/parameters.js +++ b/assets/src/scripts/components/hydrograph/parameters.js @@ -26,12 +26,12 @@ export const availableTimeseriesSelector = createSelector( variableID: variable.oid, description: variable.variableDescription, selected: currentVariableID === variableID, - currentYear: Boolean(seriesList.find( - ts => ts.tsKey === 'current' && ts.variable === variableID)), - previousYear: Boolean(seriesList.find( - ts => ts.tsKey === 'compare' && ts.variable === variableID)), - medianData: Boolean(seriesList.find( - ts => ts.tsKey === 'median' && ts.variable === variableID)) + currentYear: seriesList.filter( + ts => ts.tsKey === 'current' && ts.variable === variableID).length, + previousYear: seriesList.filter( + ts => ts.tsKey === 'compare' && ts.variable === variableID).length, + medianData: seriesList.filter( + ts => ts.tsKey === 'median' && ts.variable === variableID).length }; } let sorted = []; @@ -88,10 +88,19 @@ export const plotSeriesSelectTable = function (elem, {availableTimeseries}) { tr.append('td') .text(parm => parm[1].description); tr.append('td') - .html(parm => parm[1].currentYear ? '<i class="fa fa-check" aria-label="Current year data available"></i>' : ''); + .html(parm => { + const subScript = parm[1].currentYear > 1 ? `<sub>${parm[1].currentYear}</sub>` : ''; + return parm[1].currentYear ? `<i class="fa fa-check" aria-label="Current year data available"></i>${subScript}` : ''; + }); tr.append('td') - .html(parm => parm[1].previousYear ? '<i class="fa fa-check" aria-label="Previous year data available"></i>' : ''); + .html(parm => { + const subScript = parm[1].previousYear > 1 ? `<sub>${parm[1].previousYear}</sub>` : ''; + return parm[1].previousYear ? `<i class="fa fa-check" aria-label="Previous year data available"></i>${subScript}` : ''; + }); tr.append('td') - .html(parm => parm[1].medianData ? '<i class="fa fa-check" aria-label="Median data available"></i>' : ''); + .html(parm => { + const subScript = parm[1].medianData > 1 ? `<sub>${parm[1].medianData}</sub>` : ''; + return parm[1].medianData ? `<i class="fa fa-check" aria-label="Median data available"></i>${subScript}` : ''; + }); }); }; diff --git a/assets/src/scripts/components/hydrograph/parameters.spec.js b/assets/src/scripts/components/hydrograph/parameters.spec.js index 4d4af33a6d1d90b5cf4f1ba8b53e703547f96e19..290f84c90565d237e0473aaf514c625c604d9252 100644 --- a/assets/src/scripts/components/hydrograph/parameters.spec.js +++ b/assets/src/scripts/components/hydrograph/parameters.spec.js @@ -48,10 +48,10 @@ describe('Parameters module', () => { }); // Series are ordered by parameter code and have expected values. expect(available).toEqual([ - ['00060', {variableID: 'code0', description: 'code0 desc', selected: true, currentYear: true, previousYear: false, medianData: false}], - ['00061', {variableID: 'code1', description: 'code1 desc', selected: false, currentYear: true, previousYear: true, medianData: false}], - ['00062', {variableID: 'code2', description: 'code2 desc', selected: false, currentYear: true, previousYear: true, medianData: false}], - ['00063', {variableID: 'code3', description: 'code3 desc', selected: false, currentYear: false, previousYear: true, medianData: false}] + ['00060', {variableID: 'code0', description: 'code0 desc', selected: true, currentYear: 1, previousYear: 0, medianData: 0}], + ['00061', {variableID: 'code1', description: 'code1 desc', selected: false, currentYear: 1, previousYear: 1, medianData: 0}], + ['00062', {variableID: 'code2', description: 'code2 desc', selected: false, currentYear: 1, previousYear: 1, medianData: 0}], + ['00063', {variableID: 'code3', description: 'code3 desc', selected: false, currentYear: 0, previousYear: 1, medianData: 0}] ]); }); }); diff --git a/assets/src/scripts/components/hydrograph/timeseries.js b/assets/src/scripts/components/hydrograph/timeseries.js index 17036f9c9cb3f1790342827aff21cee2446beb1a..cae99b8689d5b13c76ad3197744e625ddfbd9f57 100644 --- a/assets/src/scripts/components/hydrograph/timeseries.js +++ b/assets/src/scripts/components/hydrograph/timeseries.js @@ -106,6 +106,7 @@ export const HASH_ID = { }; /** + * Returns a selector that, for a given tsKey: * Returns the points for a given timeseries. * @param {Object} state Redux store * @param {String} tsKey Timeseries key @@ -121,6 +122,21 @@ 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 newPointsSelector = memoize(tsKey => createSelector( + currentTimeSeriesSelector(tsKey), + (timeSeries) => { + return timeSeries.map(series => series.points); + } +)); + + export const classesForPoint = point => { return { approved: point.qualifiers.indexOf('A') > -1, @@ -195,54 +211,55 @@ export const pointsTableDataSelector = memoize(tsDataKey => createSelector( /** * Factory function creates a function that: - * Returns all points in a timeseries grouped into line segments. + * Returns all points in a timeseries grouped into line segments, for each time series. * @param {Object} state Redux store * @param {String} tsDataKey Timeseries key - * @return {Array} Array of points. + * @return {Array} Array of array of points. */ export const lineSegmentsSelector = memoize(tsDataKey => createSelector( - pointsSelector(tsDataKey), - (points) => { - // Accumulate data into line groups, splitting on the estimated and - // approval status. - let lines = []; - - let lastClasses = {}; - const masks = new Set(Object.keys(MASK_DESC)); - - for (let pt of points) { - // Classes to put on the line with this point. - let lineClasses = { - approved: pt.approved, - estimated: pt.estimated, - dataMask: null - }; - if (pt.value === null) { - let qualifiers = new Set(pt.qualifiers.map(q => q.toLowerCase())); - // current business rules specify that a particular data point - // will only have at most one masking qualifier - let maskIntersection = new Set([...masks].filter(x => qualifiers.has(x))); - lineClasses.dataMask = [...maskIntersection][0]; + newPointsSelector(tsDataKey), + (tsPoints) => { + const linePoints = []; + for (const points of tsPoints) { + // Accumulate data into line groups, splitting on the estimated and + // approval status. + const lines = []; + let lastClasses = {}; + const masks = new Set(Object.keys(MASK_DESC)); + + for (let pt of points) { + // Classes to put on the line with this point. + let lineClasses = { + ...classesForPoint(pt), + dataMask: null + }; + if (pt.value === null) { + let qualifiers = new Set(pt.qualifiers.map(q => q.toLowerCase())); + // current business rules specify that a particular data point + // will only have at most one masking qualifier + let maskIntersection = new Set([...masks].filter(x => qualifiers.has(x))); + lineClasses.dataMask = [...maskIntersection][0]; + } + // If this point doesn't have the same classes as the last point, + // create a new line for it. + if (lastClasses.approved !== lineClasses.approved || + lastClasses.estimated !== lineClasses.estimated || + lastClasses.dataMask !== lineClasses.dataMask) { + lines.push({ + classes: lineClasses, + points: [] + }); + } + + // Add this point to the current line. + lines[lines.length - 1].points.push(pt); + + // Cache the classes for the next loop iteration. + lastClasses = lineClasses; } - // If this point doesn't have the same classes as the last point, - // create a new line for it. - if (lastClasses.approved !== lineClasses.approved || - lastClasses.estimated !== lineClasses.estimated || - lastClasses.dataMask !== lineClasses.dataMask) { - lines.push({ - classes: lineClasses, - points: [] - }); - } - - // Add this point to the current line. - lines[lines.length - 1].points.push(pt); - - // Cache the classes for the next loop iteration. - lastClasses = lineClasses; + linePoints.push(lines); } - - return lines; + return linePoints; } )); diff --git a/assets/src/scripts/components/hydrograph/timeseries.spec.js b/assets/src/scripts/components/hydrograph/timeseries.spec.js index 6b0b3cd423168151abdf854afbc6063409a644d9..311c4e19ecee1e895485f7729f74ec643eac6abd 100644 --- a/assets/src/scripts/components/hydrograph/timeseries.spec.js +++ b/assets/src/scripts/components/hydrograph/timeseries.spec.js @@ -45,7 +45,6 @@ const TEST_DATA = { currentVariableID: '45807197' }; - describe('Timeseries module', () => { describe('line segment selector', () => { it('should separate on approved', () => { @@ -58,24 +57,18 @@ describe('Timeseries module', () => { '00060': { points: [{ value: 10, - qualifiers: ['P'], - approved: false, - estimated: false + qualifiers: [] }, { value: 10, - qualifiers: ['P'], - approved: true, - estimated: false + qualifiers: ['A'] }, { value: 10, - qualifiers: ['P'], - approved: true, - estimated: false + qualifiers: ['A'] }] } } } - })).toEqual([{ + })).toEqual([[{ classes: { approved: false, estimated: false, @@ -83,9 +76,7 @@ describe('Timeseries module', () => { }, points: [{ value: 10, - qualifiers: ['P'], - approved: false, - estimated: false + qualifiers: [] }] }, { classes: { @@ -95,16 +86,12 @@ describe('Timeseries module', () => { }, points: [{ value: 10, - qualifiers: ['P'], - approved: true, - estimated: false + qualifiers: ['A'] }, { value: 10, - qualifiers: ['P'], - approved: true, - estimated: false + qualifiers: ['A'] }] - }]); + }]]); }); it('should separate on estimated', () => { @@ -117,24 +104,18 @@ describe('Timeseries module', () => { '00060': { points: [{ value: 10, - qualifiers: ['P'], - approved: false, - estimated: false + qualifiers: ['P'] }, { value: 10, - qualifiers: ['P'], - approved: false, - estimated: true + qualifiers: ['P', 'E'] }, { value: 10, - qualifiers: ['P'], - approved: false, - estimated: true + qualifiers: ['P', 'E'] }] } } } - })).toEqual([{ + })).toEqual([[{ classes: { approved: false, estimated: false, @@ -142,9 +123,7 @@ describe('Timeseries module', () => { }, points: [{ value: 10, - qualifiers: ['P'], - approved: false, - estimated: false + qualifiers: ['P'] }] }, { classes: { @@ -154,16 +133,12 @@ describe('Timeseries module', () => { }, points: [{ value: 10, - qualifiers: ['P'], - approved: false, - estimated: true + qualifiers: ['P', 'E'] }, { value: 10, - qualifiers: ['P'], - approved: false, - estimated: true + qualifiers: ['P', 'E'] }] - }]); + }]]); }); it('should separate out masked values', () => { @@ -176,24 +151,18 @@ describe('Timeseries module', () => { '00060': { points: [{ value: 10, - qualifiers: ['P'], - approved: false, - estimated: false + qualifiers: ['P'] }, { value: null, - qualifiers: ['P', 'ICE'], - approved: false, - estimated: false + qualifiers: ['P', 'ICE'] }, { value: null, - qualifiers: ['P', 'FLD'], - approved: false, - estimated: false + qualifiers: ['P', 'FLD'] }] } } } - })).toEqual([ + })).toEqual([[ { classes: { approved: false, @@ -202,9 +171,7 @@ describe('Timeseries module', () => { }, points: [{ value: 10, - qualifiers: ['P'], - approved: false, - estimated: false + qualifiers: ['P'] }] }, { @@ -215,9 +182,7 @@ describe('Timeseries module', () => { }, points: [{ value: null, - qualifiers: ['P', 'ICE'], - approved: false, - estimated: false + qualifiers: ['P', 'ICE'] }] }, { @@ -228,12 +193,10 @@ describe('Timeseries module', () => { }, points: [{ value: null, - qualifiers: ['P', 'FLD'], - approved: false, - estimated: false + qualifiers: ['P', 'FLD'] }] } - ]); + ]]); }); }); diff --git a/assets/src/styles/components/_hydrograph.scss b/assets/src/styles/components/_hydrograph.scss index c397f89b0517a38a2c97cdc4ef16f5b1495add39..8d6b0c50a85f401ec203cc394b42951bbd191d9b 100644 --- a/assets/src/styles/components/_hydrograph.scss +++ b/assets/src/styles/components/_hydrograph.scss @@ -82,7 +82,7 @@ table#select-timeseries { stroke-dasharray: 1, 3; } } - #ts-compare, #ts-legend-compare { + .ts-compare, #ts-legend-compare { stroke-width: 1px; } .axis {