Newer
Older
/**
* Hydrograph charting module.
*/
import { extent } from 'd3-array';
import { line as d3Line, curveStepAfter } from 'd3-shape';
import { select } from 'd3-selection';
import { createStructuredSelector } from 'reselect';
import { addSVGAccessibility } from '../../accessibility';
import config from '../../config';
import { dispatch, link, provide } from '../../lib/redux';
import { Actions } from '../../store';
import { callIf, mediaQuery } from '../../utils';
import { audibleUI } from './audible';
import { appendAxes, axesSelector } from './axes';
import { cursorSlider } from './cursor';
import { lineSegmentsByParmCdSelector, currentVariableLineSegmentsSelector, MASK_DESC, HASH_ID, getCurrentVariableMedianStatPoints } from './drawing-data';
import { CIRCLE_RADIUS_SINGLE_PT, SPARK_LINE_DIM, layoutSelector } from './layout';
import { drawSimpleLegend, legendMarkerRowsSelector } from './legend';
import { plotSeriesSelectTable, availableTimeSeriesSelector } from './parameters';
import { xScaleSelector, yScaleSelector, timeSeriesScalesByParmCdSelector } from './scales';
import { allTimeSeriesSelector, isVisibleSelector, titleSelector, descriptionSelector, currentVariableTimeSeriesSelector, hasTimeSeriesWithPoints } from './time-series';
import { createTooltipFocus, createTooltipText } from './tooltip';
import { getTimeSeriesCollectionIds, isLoadingTS } from '../../selectors/time-series-selector';
// 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')
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
}
const plotDataLines = function(elem, {visible, tsLinesMap, tsKey, xScale, yScale}, container) {
Naab, Daniel James
committed
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')
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
};
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)');
export 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) {
Bucknell, Mary S.
committed
return xscale(d.date);
})
.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
Bucknell, Mary S.
committed
const plotAllMedianPoints = function (elem, {visible, xscale, yscale, seriesPoints}) {
elem.select('#median-points').remove();
if (!visible) {
return;
}
const container = elem
.append('g')
.attr('id', 'median-points');
Bucknell, Mary S.
committed
seriesPoints.forEach((points, index) => {
plotMedianPoints(container, {xscale, yscale, modulo: index % 6, points: points});
});
};
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) {
Bucknell, Mary S.
committed
// These constants will need to change if the watermark svg is updated
const watermarkHalfHeight = 87 / 2;
const watermarkHalfWidth = 235 / 2;
elem.append('img')
.classed('watermark', true)
.attr('alt', 'USGS - science for a changing world')
.attr('src', config.STATIC_URL + '/img/USGS_green_logo.svg')
Briggs, Aaron Shane
committed
.call(link(function(elem, layout) {
Bucknell, Mary S.
committed
const centerX = layout.margin.left + (layout.width - layout.margin.right - layout.margin.left) / 2;
const centerY = layout.margin.top + (layout.height - layout.margin.bottom - layout.margin.top) / 2;
const scale = !mediaQuery(config.USWDS_MEDIUM_SCREEN) ? 0.5 : 1;
const translateX = centerX - watermarkHalfWidth;
const translateY = centerY - watermarkHalfHeight;
const transform = `matrix(${scale}, 0, 0, ${scale}, ${translateX}, ${translateY})`;
elem.style('transform', transform);
// for Safari browser
elem.style('-webkit-transform', transform);
}, layoutSelector));
export const timeSeriesGraph = function(elem) {
Naab, Daniel James
committed
elem.append('div')
.attr('class', 'hydrograph-container')
.call(watermark)
Bucknell, Mary S.
committed
.call(createTitle)
Naab, Daniel James
committed
.call(createTooltipText)
.append('svg')
Naab, Daniel James
committed
.attr('xmlns', 'http://www.w3.org/2000/svg')
Briggs, Aaron Shane
committed
.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))
Naab, Daniel James
committed
.call(link(addSVGAccessibility, createStructuredSelector({
Bucknell, Mary S.
committed
title: titleSelector,
description: descriptionSelector,
Yan, Andrew N.
committed
isInteractive: () => true,
idPrefix: () => 'hydrograph'
Naab, Daniel James
committed
})))
.call(svg => {
svg.append('g')
Naab, Daniel James
committed
.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,
Bucknell, Mary S.
committed
seriesPoints: getCurrentVariableMedianStatPoints
* 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', true)
.classed('usa-list--unstyled', true)
.classed('graph-controls-container', true);
graphControlDiv.append('li')
.call(audibleUI);
const compareControlDiv = graphControlDiv.append('li')
.classed('usa-checkbox', true);
compareControlDiv.append('input')
.classed('usa-checkbox__input', true)
.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')))
Bucknell, Mary S.
committed
.call(link(function(elem, checked) {
Bucknell, Mary S.
committed
}, isVisibleSelector('compare')));
compareControlDiv.append('label')
.classed('usa-checkbox__label', true)
.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
*/
const controlDisplay = function(elem, showElem) {
Bucknell, Mary S.
committed
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 CUSTOM_DATE_RANGE = {
start: [{
label: 'start-month',
name: 'month'
}, {
label: 'start-day',
name: 'day'
}, {
label: 'start-year',
name: 'year'
}],
end: [{
label: 'end-month',
name: 'Month'
}, {
label: 'end-day',
name: 'Day'
}, {
label: 'end-year',
name: 'Year'
}]
};
const container = elem.insert('div', ':nth-child(2)')
.attr('id', 'ts-daterange-select-container')
.attr('role', 'radiogroup')
.attr('aria-label', 'Time interval select')
.call(link(function(container, showControls) {
container.attr('hidden', showControls ? null : true);
}, hasTimeSeriesWithPoints('current', 'P7D')));
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
const customDateContainer = elem.insert('div', ':nth-child(3)')
.attr('id', 'ts-customdaterange-select-container')
.attr('role', 'customdate')
.attr('aria-label', 'Custom date specification');
const customStartDateContainer = customDateContainer.append('div')
.attr('class', 'specify-date');
const customEndDateContainer = customDateContainer.append('div')
.attr('class', 'specify-date');
const submitContainer = customDateContainer.append('div')
.attr('class', 'submit-button');
customStartDateContainer.selectAll('input')
.attr('class', 'usa-input usa-input--inline')
.data(CUSTOM_DATE_RANGE.start)
.enter().append('input');
customEndDateContainer.selectAll('input')
.attr('class', 'usa-input usa-input--inline')
.data(CUSTOM_DATE_RANGE.end)
.enter().append('input');
submitContainer.append('button')
.attr('class', 'usa-button')
.text('Submit')
.on('click', dispatch( function() {
return Actions.retrieveCustomTimeSeries(
siteno,
new Date(2012, 3, 14),
new Date(2019, 5, 17)
);
}));
const listContainer = container.append('ul')
const li = listContainer.selectAll('li')
.attr('class', 'usa-fieldset')
.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('class', 'usa-radio__input')
.attr('value', d => d.period)
.attr('ga-on', 'click')
.attr('ga-event-category', 'TimeSeriesGraph')
.attr('ga-event-action', d => `changeDateRangeTo${d.period}`)
return Actions.retrieveExtendedTimeSeries(
siteno,
li.select('input:checked').attr('value')
);
}));
li.append('label')
.attr('class', 'usa-radio__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, tsCollectionIds) {
elem.select('#no-data-message').remove();
if (tsCollectionIds && tsCollectionIds.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');
Bucknell, Mary S.
committed
}
export const attachToNode = function (store, node, {siteno, parameter, compare, cursorOffset, interactive = true} = {}) {
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))
Naab, Daniel James
committed
.call(link(noDataAlert, getTimeSeriesCollectionIds('current', 'P7D')))
.call(callIf(interactive, dateRangeControls), siteno)
.select('.loading-indicator-container')
.call(link(loadingIndicator, createStructuredSelector({
showLoadingIndicator: isLoadingTS('current', 'P7D'),
sizeClass: () => 'fa-3x'
})));
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)
.call(callIf(interactive, cursorSlider))
Briggs, Aaron Shane
committed
.append('div')
.classed('ts-legend-controls-container', true)
.call(timeSeriesLegend)
.call(callIf(interactive, graphControls));
if (interactive) {
select(node).select('.select-time-series-container')
.call(link(plotSeriesSelectTable, createStructuredSelector({
siteno: () => siteno,
availableTimeSeries: availableTimeSeriesSelector,
Naab, Daniel James
committed
lineSegmentsByParmCd: lineSegmentsByParmCdSelector('current', 'P7D'),
timeSeriesScalesByParmCd: timeSeriesScalesByParmCdSelector('current', 'P7D', SPARK_LINE_DIM),
layout: layoutSelector
})));
select(node).select('.provisional-data-alert')
.call(link(function(elem, allTimeSeries) {
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));
Bucknell, Mary S.
committed
store.dispatch(Actions.retrieveMedianStatistics(siteno));