Newer
Older
/**
* Hydrograph charting module.
*/
const {extent} = require('d3-array');
const {line: d3Line, curveStepAfter} = require('d3-shape');
const {select} = require('d3-selection');
const {createStructuredSelector} = require('reselect');
const {addSVGAccessibility} = require('../../accessibility');
const {USWDS_SMALL_SCREEN, STATIC_URL} = require('../../config');
const {dispatch, link, provide} = require('../../lib/redux');
const {Actions} = require('../../store');
const {mediaQuery} = require('../../utils');
const {audibleUI} = require('./audible');
const {appendAxes, axesSelector} = require('./axes');
const {cursorSlider} = require('./cursor');
const {
lineSegmentsByParmCdSelector, currentVariableLineSegmentsSelector,
MASK_DESC, HASH_ID
} = require('./drawingData');
const {CIRCLE_RADIUS_SINGLE_PT, SPARK_LINE_DIM, layoutSelector} = require('./layout');
const {drawSimpleLegend, legendMarkerRowsSelector} = require('./legend');
const {plotSeriesSelectTable, availableTimeSeriesSelector} = require('./parameters');
const {xScaleSelector, yScaleSelector, timeSeriesScalesByParmCdSelector} = require('./scales');
const {
allTimeSeriesSelector, isVisibleSelector, titleSelector,
descriptionSelector, currentVariableTimeSeriesSelector, hasTimeSeriesWithPoints
} = require('./timeSeries');
const {createTooltipFocus, createTooltipText} = require('./tooltip');
const {coerceStatisticalSeries} = require('./statistics');
const {getCurrentDateRange, getTimeSeriesCollection, isLoadingTS} = require('../../selectors/timeSeriesSelector');
const drawMessage = function (elem, message) {
// Set up parent element and SVG
elem.innerHTML = '';
const alertBox = elem
.append('div')
.attr('class', 'usa-alert usa-alert-warning')
.append('div')
.attr('class', 'usa-alert-body');
alertBox
.append('h3')
.attr('class', 'usa-alert-heading')
.html('Hydrograph Alert');
alertBox
.append('p')
.html(message);
const plotDataLine = function (elem, {visible, lines, tsKey, xScale, yScale}) {
Naab, Daniel James
committed
if (!visible) {
Naab, Daniel James
committed
for (let line of lines) {
Naab, Daniel James
committed
// If this is a single point line, then represent it as a circle.
// Otherwise, render as a line.
if (line.points.length === 1) {
elem.append('circle')
.data(line.points)
.classed('line-segment', true)
.classed('approved', line.classes.approved)
.classed('estimated', line.classes.estimated)
.attr('r', CIRCLE_RADIUS_SINGLE_PT)
.attr('cx', d => xScale(d.dateTime))
.attr('cy', d => yScale(d.value));
} else {
const tsLine = d3Line()
.x(d => xScale(d.dateTime))
.y(d => yScale(d.value));
elem.append('path')
.datum(line.points)
.classed('line-segment', true)
.classed('approved', line.classes.approved)
.classed('estimated', line.classes.estimated)
.classed(`ts-${tsKey}`, true)
.attr('d', tsLine);
}
const maskCode = line.classes.dataMask.toLowerCase();
const maskDisplayName = MASK_DESC[maskCode].replace(' ', '-').toLowerCase();
Naab, Daniel James
committed
const [xDomainStart, xDomainEnd] = extent(line.points, d => d.dateTime);
const [yRangeStart, yRangeEnd] = yScale.domain();
let maskGroup = elem.append('g')
.attr('class', `${tsKey}-mask-group`);
const xSpan = xScale(xDomainEnd) - xScale(xDomainStart);
Bucknell, Mary S.
committed
const rectWidth = xSpan > 1 ? xSpan : 1;
maskGroup.append('rect')
.attr('x', xScale(xDomainStart))
.attr('y', yScale(yRangeEnd))
.attr('width', rectWidth)
.attr('height', Math.abs(yScale(yRangeEnd) - yScale(yRangeStart)))
.attr('class', `mask ${maskDisplayName}-mask`);
const patternId = HASH_ID[tsKey] ? `url(#${HASH_ID[tsKey]})` : '';
maskGroup.append('rect')
.attr('x', xScale(xDomainStart))
.attr('y', yScale(yRangeEnd))
.attr('width', rectWidth)
.attr('height', Math.abs(yScale(yRangeEnd) - yScale(yRangeStart)))
.attr('fill', patternId);
Naab, Daniel James
committed
}
Naab, Daniel James
committed
const plotDataLines = function (elem, {visible, tsLinesMap, tsKey, xScale, yScale}, container) {
container = container || elem.append('g');
Naab, Daniel James
committed
Naab, Daniel James
committed
const elemId = `ts-${tsKey}-group`;
container.selectAll(`#${elemId}`).remove();
const tsLineGroup = container
Naab, Daniel James
committed
.append('g')
.attr('id', elemId)
.classed('tsKey', true);
Naab, Daniel James
committed
Naab, Daniel James
committed
for (const lines of Object.values(tsLinesMap)) {
plotDataLine(tsLineGroup, {visible, lines, tsKey, xScale, yScale});
Naab, Daniel James
committed
}
Naab, Daniel James
committed
return container;
Naab, Daniel James
committed
};
const plotSvgDefs = function (elem) {
let defs = elem.append('defs');
defs.append('mask')
.attr('id', 'display-mask')
.attr('maskUnits', 'userSpaceOnUse')
.append('rect')
.attr('x', '0')
.attr('y', '0')
.attr('width', '100%')
.attr('height', '100%')
.attr('fill', '#0000ff');
.attr('id', HASH_ID.current)
.attr('width', '8')
.attr('height', '8')
.attr('patternUnits', 'userSpaceOnUse')
.attr('patternTransform', 'rotate(45)')
.append('rect')
.attr('width', '4')
.attr('height', '8')
.attr('transform', 'translate(0, 0)')
.attr('mask', 'url(#display-mask)');
.attr('id', HASH_ID.compare)
.attr('width', '8')
.attr('height', '8')
.attr('patternUnits', 'userSpaceOnUse')
.attr('patternTransform', 'rotate(135)')
.append('rect')
.attr('width', '4')
.attr('height', '8')
.attr('transform', 'translate(0, 0)')
.attr('mask', 'url(#display-mask)');
const timeSeriesLegend = function (elem) {
elem.append('div')
.classed('hydrograph-container', true)
Bucknell, Mary S.
committed
.call(link(drawSimpleLegend, createStructuredSelector({
legendMarkerRows: legendMarkerRowsSelector,
layout: layoutSelector
})));
/**
* Plots the median points for a single median time series.
* @param {Object} elem
* @param {Function} xscale
* @param {Function} yscale
* @param {Number} modulo
* @param {Array} points
const plotMedianPoints = function (elem, {xscale, yscale, modulo, points}) {
.curve(curveStepAfter)
.x(function (d) {
return xscale(d.dateTime);
})
.y(function (d) {
return yscale(d.value);
});
let medianGrp = elem.append('g');
medianGrp.append('path')
.datum(points)
.classed('median-data-series', true)
.classed('median-step', true)
.classed(`median-step-${modulo}`, true)
.attr('d', stepFunction);
/**
* Plots the median points for all median time series for the current variable.
* @param {Object} elem
* @param {Boolean} visible
* @param {Function} xscale
* @param {Function} yscale
* @param {Array} pointsList
const plotAllMedianPoints = function (elem, {visible, xscale, yscale, seriesMap, dateRange}) {
elem.select('#median-points').remove();
if (!visible) {
return;
}
const container = elem
.append('g')
.attr('id', 'median-points');
Naab, Daniel James
committed
for (const [index, seriesID] of Object.keys(seriesMap).entries()) {
const points = coerceStatisticalSeries(seriesMap[seriesID], dateRange);
plotMedianPoints(container, {xscale, yscale, modulo: index % 6, points});
}
};
const createTitle = function (elem) {
Bucknell, Mary S.
committed
elem.append('div')
.classed('time-series-graph-title', true)
Bucknell, Mary S.
committed
.call(link((elem, title) => {
elem.html(title);
}, titleSelector));
const watermark = function (elem) {
elem.append('img')
.classed('watermark', true)
.attr('src', STATIC_URL + '/img/USGS_green_logo.svg')
.call(link(function (elem, layout) {
const transformStringSmallScreen = `matrix(0.5, 0, 0, 0.5, ${(layout.width - layout.margin.left) * .025
+ layout.margin.left - 50}, ${layout.height * .60})`;
const transformStringForAllOtherScreens = `matrix(1, 0, 0, 1, ${(layout.width - layout.margin.left) * .025
+ layout.margin.left}, ${(layout.height * .75 - (-1 * layout.height + 503) * .12)})`;
Briggs, Aaron Shane
committed
if (!mediaQuery(USWDS_SMALL_SCREEN)) {
// calculates the watermark position based on current layout dimensions
// and a conversion factor minus the area for blank space due to scaling
elem.style('transform', transformStringSmallScreen);
Briggs, Aaron Shane
committed
// adapts code for Safari browser
elem.style('-webkit-transform', transformStringSmallScreen);
Briggs, Aaron Shane
committed
// calculates the watermark position based on current layout dimensions and a conversion factor
elem.style('transform', transformStringForAllOtherScreens);
Briggs, Aaron Shane
committed
// adapts code for Safari browser
elem.style('-webkit-transform', transformStringForAllOtherScreens);
}
}, layoutSelector));
Naab, Daniel James
committed
const timeSeriesGraph = function (elem) {
elem.call(watermark)
.append('div')
.attr('class', 'hydrograph-container')
Bucknell, Mary S.
committed
.call(createTitle)
Naab, Daniel James
committed
.call(createTooltipText)
.append('svg')
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
.attr('xmlns', 'http://www.w3.org/2000/svg')
.classed('hydrograph-svg', true)
.call(link((elem, layout) => {
elem.attr('viewBox', `0 0 ${layout.width + layout.margin.left + layout.margin.right} ${layout.height + layout.margin.top + layout.margin.bottom}`);
elem.attr('width', layout.width);
elem.attr('height', layout.height);
}, layoutSelector))
.call(link(addSVGAccessibility, createStructuredSelector({
title: titleSelector,
description: descriptionSelector,
isInteractive: () => true
})))
.call(plotSvgDefs)
.call(svg => {
svg.append('g')
.call(link((elem, layout) => elem.attr('transform', `translate(${layout.margin.left},${layout.margin.top})`), layoutSelector))
.call(link(appendAxes, axesSelector))
.call(link(plotDataLines, createStructuredSelector({
visible: isVisibleSelector('current'),
tsLinesMap: currentVariableLineSegmentsSelector('current'),
xScale: xScaleSelector('current'),
yScale: yScaleSelector,
tsKey: () => 'current'
})))
.call(link(plotDataLines, createStructuredSelector({
visible: isVisibleSelector('compare'),
tsLinesMap: currentVariableLineSegmentsSelector('compare'),
xScale: xScaleSelector('compare'),
yScale: yScaleSelector,
tsKey: () => 'compare'
})))
.call(createTooltipFocus)
.call(link(plotAllMedianPoints, createStructuredSelector({
visible: isVisibleSelector('median'),
xscale: xScaleSelector('current'),
yscale: yScaleSelector,
seriesMap: currentVariableTimeSeriesSelector('median'),
dateRange: getCurrentDateRange
})));
});
* Create the show last year toggle and the audible toggle for the time series graph.
const graphControls = function (elem) {
const graphControlDiv = elem.append('ul')
.classed('usa-fieldset-inputs', true)
.classed('usa-unstyled-list', true)
.classed('graph-controls-container', true);
graphControlDiv.append('li')
.call(audibleUI);
const compareControlDiv = graphControlDiv.append('li');
compareControlDiv.append('input')
.attr('type', 'checkbox')
.attr('id', 'last-year-checkbox')
.attr('aria-labelledby', 'last-year-label')
.attr('ga-on', 'click')
.attr('ga-event-category', 'TimeSeriesGraph')
.attr('ga-event-action', 'toggleCompare')
.on('click', dispatch(function () {
return Actions.toggleTimeSeries('compare', this.checked);
// Disables the checkbox if no compare time series for the current variable
.call(link(function (elem, compareTimeSeries) {
const exists = Object.keys(compareTimeSeries) ?
Object.values(compareTimeSeries).filter(tsValues => tsValues.points.length).length > 0 : false;
Bucknell, Mary S.
committed
elem.property('disabled', !exists);
}, currentVariableTimeSeriesSelector('compare')))
.call(link(function (elem, checked) {
Bucknell, Mary S.
committed
}, isVisibleSelector('compare')));
compareControlDiv.append('label')
.attr('id', 'last-year-label')
.attr('for', 'last-year-checkbox')
Bucknell, Mary S.
committed
/**
Bucknell, Mary S.
committed
* Modify styling to hide or display the elem.
Bucknell, Mary S.
committed
*
* @param elem
Bucknell, Mary S.
committed
* @param {Boolean} showElem
Bucknell, Mary S.
committed
*/
Bucknell, Mary S.
committed
const controlDisplay = function (elem, showElem) {
elem.attr('hidden', showElem ? null : true);
Bucknell, Mary S.
committed
};
const loadingIndicator = function (elem, {showLoadingIndicator, sizeClass}) {
Bucknell, Mary S.
committed
elem.select('.loading-indicator').remove();
if (showLoadingIndicator) {
elem.append('i')
.attr('class', `loading-indicator fas ${sizeClass} fa-spin fa-spinner`);
}
};
const dateRangeControls = function (elem, siteno) {
const DATE_RANGE = [{
label: 'seven-day',
name: '7 days',
period: 'P7D'
}, {
label: 'thirty-days',
name: '30 days',
period: 'P30D'
}, {
label: 'one-year',
name: '1 year',
period: 'P1Y'
}];
const container = elem.insert('div', ':nth-child(2)')
.attr('id', 'ts-daterange-select-container')
.call(link(function(container, showControls) {
container.attr('hidden', showControls ? null : true);
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
}, hasTimeSeriesWithPoints('current', 'P7D')));
const listContainer = container.append('ul')
.attr('class', 'usa-fieldset-inputs usa-unstyled-list');
const li = listContainer.selectAll('li')
.data(DATE_RANGE)
.enter().append('li');
listContainer.call(link(loadingIndicator, createStructuredSelector({
showLoadingIndicator: isLoadingTS('current'),
sizeClass: () => 'fa-lg'
})));
li.append('input')
.attr('type', 'radio')
.attr('name', 'ts-daterange-input')
.attr('id', d => d.label)
.attr('value', d => d.period)
.attr('ga-on', 'click')
.attr('ga-event-category', 'TimeSeriesGraph')
.attr('ga-event-action', d => `changeDateRangeTo${d.period}`)
.on('change', dispatch(function () {
return Actions.retrieveExtendedTimeSeries(
siteno,
li.select('input:checked').attr('value')
);
}));
li.append('label')
.attr('for', (d) => d.label)
.text((d) => d.name);
li.select(`#${DATE_RANGE[0].label}`).attr('checked', true);
Bucknell, Mary S.
committed
};
const noDataAlert = function (elem, tsCollections) {
elem.select('#no-data-message').remove();
if (tsCollections && tsCollections.length === 0) {
elem.append('div')
.attr('id', 'no-data-message')
.attr('class', 'usa-alert usa-alert-info')
.append('div')
.attr('class', 'usa-alert-body')
.append('p')
.attr('class', 'usa-alert-text')
.text('No current time series data available for this site');
const attachToNode = function (store, node, {siteno, parameter, compare, cursorOffset} = {}) {
if (!siteno) {
select(node).call(drawMessage, 'No data is available.');
return;
Bucknell, Mary S.
committed
}
Bucknell, Mary S.
committed
store.dispatch(Actions.resizeUI(window.innerWidth, node.offsetWidth));
select(node)
.call(provide(store));
.call(link(noDataAlert, getTimeSeriesCollection('current', 'P7D')));
select(node).select('.loading-indicator-container')
.call(link(loadingIndicator, createStructuredSelector({
Bucknell, Mary S.
committed
showLoadingIndicator: isLoadingTS('current', 'P7D'),
sizeClass: () => 'fa-3x'
Bucknell, Mary S.
committed
})));
select(node)
.call(dateRangeControls, siteno);
Naab, Daniel James
committed
// If specified, turn the visibility of the comparison time series on.
if (compare) {
store.dispatch(Actions.toggleTimeSeries('compare', true));
}
// If specified, initialize the cursorOffset
if (cursorOffset !== undefined) {
store.dispatch(Actions.setCursorOffset(cursorOffset));
}
Briggs, Aaron Shane
committed
select(node).select('.graph-container')
Bucknell, Mary S.
committed
.call(link(controlDisplay, hasTimeSeriesWithPoints('current', 'P7D')))
Bucknell, Mary S.
committed
.call(timeSeriesGraph, siteno)
Briggs, Aaron Shane
committed
.call(cursorSlider)
.append('div')
.classed('ts-legend-controls-container', true)
.call(timeSeriesLegend)
.call(graphControls);
select(node).select('.select-time-series-container')
Briggs, Aaron Shane
committed
.call(link(plotSeriesSelectTable, createStructuredSelector({
availableTimeSeries: availableTimeSeriesSelector,
lineSegmentsByParmCd: lineSegmentsByParmCdSelector('current', 'P7D'),
Bucknell, Mary S.
committed
timeSeriesScalesByParmCd: timeSeriesScalesByParmCdSelector('current', 'P7D', SPARK_LINE_DIM),
Briggs, Aaron Shane
committed
layout: layoutSelector
})));
select(node).select('.provisional-data-alert')
.call(link(function (elem, allTimeSeries) {
Briggs, Aaron Shane
committed
elem.attr('hidden', Object.keys(allTimeSeries).length ? null : true);
}, allTimeSeriesSelector));
Bucknell, Mary S.
committed
window.onresize = function () {
store.dispatch(Actions.resizeUI(window.innerWidth, node.offsetWidth));
Bucknell, Mary S.
committed
};
store.dispatch(Actions.retrieveTimeSeries(siteno, parameter ? [parameter] : null));
module.exports = {attachToNode, timeSeriesLegend, timeSeriesGraph};