Newer
Older
/**
* Hydrograph charting module.
*/
const { select } = require('d3-selection');
Yan, Andrew N.
committed
const { extent } = require('d3-array');
Naab, Daniel James
committed
const { line } = require('d3-shape');
const { createStructuredSelector } = require('reselect');
const { addSVGAccessibility, addSROnlyTable } = require('../../accessibility');
Naab, Daniel James
committed
const { dispatch, link, provide } = require('../../lib/redux');
Naab, Daniel James
committed
const { appendAxes, axesSelector } = require('./axes');
const { MARGIN, CIRCLE_RADIUS, layoutSelector } = require('./layout');
const { drawSimpleLegend, legendMarkerRowsSelector } = require('./legend');
const { plotSeriesSelectTable, availableTimeseriesSelector } = require('./parameters');
const { xScaleSelector, yScaleSelector } = require('./scales');
const { Actions, configureStore } = require('./store');
Naab, Daniel James
committed
const { currentVariableSelector, pointsSelector, lineSegmentsSelector,
methodsSelector, pointsTableDataSelector, isVisibleSelector, titleSelector,
descriptionSelector, timeSeriesSelector, MASK_DESC, HASH_ID } = require('./timeseries');
Bucknell, Mary S.
committed
const { createTooltipFocus, createTooltipText } = require('./tooltip');
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')
const plotDataLine = function (elem, {visible, lines, tsKey, xScale, yScale}) {
Naab, Daniel James
committed
if (!visible) {
Naab, Daniel James
committed
const tsLine = line()
Naab, Daniel James
committed
.x(d => xScale(d.dateTime))
Naab, Daniel James
committed
.y(d => yScale(d.value));
for (let line of lines) {
elem.append('path')
.datum(line.points)
.classed('line', true)
.classed('approved', line.classes.approved)
.classed('estimated', line.classes.estimated)
.classed(`ts-${tsKey}`, true)
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);
const rectWidth = xSpan > 0 ? 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, tsLines, tsKey, xScale, yScale}) {
const elemId = `ts-${tsKey}-group`;
Naab, Daniel James
committed
elem.selectAll(`#${elemId}`).remove();
const tsLineGroup = elem
.append('g')
.attr('id', elemId)
.classed('tsKey', true);
Naab, Daniel James
committed
for (const lines of tsLines) {
plotDataLine(tsLineGroup, {visible, lines, tsKey, xScale, yScale});
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)');
const timeSeriesLegend = function(elem) {
elem.append('div')
.attr('class', 'hydrograph-container')
.append('svg')
.call(link(drawSimpleLegend, createStructuredSelector({
legendMarkerRows: legendMarkerRowsSelector,
layout: layoutSelector
})));
/**
* Plots the median points for a single median time series.
* @param {Object} elem
* @param {Function} options.xscale
* @param {Function} options.yscale
* @param {Number} options.modulo
* @param {Array} options.points
* @param {Boolean} options.showLabel
* @param {Object} options.variable
*/
const plotMedianPoints = function (elem, {xscale, yscale, modulo, points, showLabel, variable}) {
elem.selectAll('medianPoint')
.data(points)
.enter()
.append('circle')
.classed('median-data-series', true)
.classed(`median-modulo-${modulo}`, true)
.attr('cx', function(d) {
Naab, Daniel James
committed
return xscale(d.dateTime);
})
.attr('cy', function(d) {
Bucknell, Mary S.
committed
.on('click', dispatch(function() {
return Actions.showMedianStatsLabel(!showLabel);
}));
Bucknell, Mary S.
committed
if (showLabel) {
elem.selectAll('medianPointText')
.data(points)
.enter()
.append('text')
.text(function(d) {
return `${d.value} ${variable.unit.unitCode}`;
Naab, Daniel James
committed
return xscale(d.dateTime) + 5;
})
.attr('y', function(d) {
return yscale(d.value);
});
Bucknell, Mary S.
committed
}
/**
* Plots the median points for all median time series for the current variable.
* @param {Object} elem
* @param {Boolean} options.visible
* @param {Function} options.xscale
* @param {Function} options.yscale
* @param {Array} options.pointsList
* @param {Boolean} options.showLabel
* @param {Object} options.variable
*/
const plotAllMedianPoints = function (elem, {visible, xscale, yscale, pointsList, showLabel, variable}) {
elem.select('#median-points').remove();
if (!visible) {
return;
}
const container = elem
.append('g')
.attr('id', 'median-points');
for (const [index, points] of pointsList.entries()) {
plotMedianPoints(container, {xscale, yscale, modulo: index % 6, points, showLabel, variable});
}
};
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
const plotSROnlyTable = function (elem, {tsKey, variable, methods, visible, dataByTsID, timeSeries}) {
elem.selectAll(`sr-only-${tsKey}`).remove();
if (!visible) {
return;
}
const container = elem.append('div')
.attr('id', `sr-only-${tsKey}`);
for (const seriesID of Object.keys(timeSeries)) {
const series = timeSeries[seriesID];
const method = methods[series.method].methodDescription;
let title = variable.variableName;
if (method) {
title += ` (${method})`;
}
if (tsKey === 'median') {
title = `Median ${title}`;
}
addSROnlyTable(container, {
columnNames: [title, 'Time', 'Qualifiers'],
data: dataByTsID[seriesID],
describeById: `${seriesID}-time-series-sr-desc`,
describeByText: `${seriesID} time series data in tabular format`
});
}
};
Naab, Daniel James
committed
const timeSeriesGraph = function (elem) {
elem.append('div')
.attr('class', 'hydrograph-container')
.append('svg')
Bucknell, Mary S.
committed
.call(link((elem, layout) => elem.attr('viewBox', `0 0 ${layout.width} ${layout.height}`), layoutSelector))
Naab, Daniel James
committed
.call(link(addSVGAccessibility, createStructuredSelector({
Naab, Daniel James
committed
titleSelector,
descriptionSelector,
Naab, Daniel James
committed
isInteractive: () => true
})))
.call(createTooltipText)
.append('g')
.attr('transform', `translate(${MARGIN.left},${MARGIN.top})`)
Naab, Daniel James
committed
.call(link(appendAxes, axesSelector))
Naab, Daniel James
committed
.call(link(plotDataLines, createStructuredSelector({
Naab, Daniel James
committed
visible: isVisibleSelector('current'),
Naab, Daniel James
committed
tsLines: lineSegmentsSelector('current'),
Naab, Daniel James
committed
xScale: xScaleSelector('current'),
yScale: yScaleSelector,
tsKey: () => 'current'
Naab, Daniel James
committed
})))
Naab, Daniel James
committed
.call(link(plotDataLines, createStructuredSelector({
Naab, Daniel James
committed
visible: isVisibleSelector('compare'),
Naab, Daniel James
committed
tsLines: lineSegmentsSelector('compare'),
Naab, Daniel James
committed
xScale: xScaleSelector('compare'),
yScale: yScaleSelector,
tsKey: () => 'compare'
Naab, Daniel James
committed
})))
Bucknell, Mary S.
committed
.call(link(createTooltipFocus, createStructuredSelector({
Naab, Daniel James
committed
xScale: xScaleSelector('current'),
yScale: yScaleSelector,
compareXScale: xScaleSelector('compare'),
currentTsData: pointsSelector('current'),
compareTsData: pointsSelector('compare'),
isCompareVisible: isVisibleSelector('compare')
Bucknell, Mary S.
committed
})))
.call(link(plotAllMedianPoints, createStructuredSelector({
visible: isVisibleSelector('median'),
Bucknell, Mary S.
committed
xscale: xScaleSelector('current'),
yscale: yScaleSelector,
Naab, Daniel James
committed
pointsList: pointsSelector('median'),
variable: currentVariableSelector,
Bucknell, Mary S.
committed
showLabel: (state) => state.showMedianStatsLabel
Naab, Daniel James
committed
})));
elem.call(link(plotSeriesSelectTable, createStructuredSelector({
availableTimeseries: availableTimeseriesSelector
})));
.call(link(plotSROnlyTable, createStructuredSelector({
tsKey: () => 'compare',
variable: currentVariableSelector,
methods: methodsSelector,
visible: isVisibleSelector('compare'),
dataByTsID: pointsTableDataSelector('compare'),
timeSeries: timeSeriesSelector('compare')
})));
elem.append('div')
.call(link(plotSROnlyTable, createStructuredSelector({
tsKey: () => 'compare',
variable: currentVariableSelector,
methods: methodsSelector,
visible: isVisibleSelector('compare'),
dataByTsID: pointsTableDataSelector('compare'),
timeSeries: timeSeriesSelector('compare')
Bucknell, Mary S.
committed
})));
.call(link(plotSROnlyTable, createStructuredSelector({
tsKey: () => 'median',
variable: currentVariableSelector,
methods: methodsSelector,
visible: isVisibleSelector('median'),
dataByTsID: pointsTableDataSelector('median'),
timeSeries: timeSeriesSelector('median')
Naab, Daniel James
committed
})));
};
const attachToNode = function (node, {siteno} = {}) {
if (!siteno) {
select(node).call(drawMessage, 'No data is available.');
return;
Bucknell, Mary S.
committed
}
let store = configureStore({
width: node.offsetWidth
});
select(node)
.call(provide(store))
Naab, Daniel James
committed
.call(timeSeriesGraph)
.call(timeSeriesLegend)
.select('.hydrograph-last-year-input')
.on('change', dispatch(function () {
return Actions.toggleTimeseries('compare', this.checked);
}));
Bucknell, Mary S.
committed
window.onresize = function() {
store.dispatch(Actions.resizeTimeseriesPlot(node.offsetWidth));
};
store.dispatch(Actions.retrieveTimeseries(siteno));
};
module.exports = {attachToNode, timeSeriesLegend, timeSeriesGraph};