Newer
Older
/**
* Hydrograph charting module.
*/
const { bisector, max } = require('d3-array');
Naab, Daniel James
committed
const { mouse, select } = require('d3-selection');
const { line } = require('d3-shape');
Naab, Daniel James
committed
const { createSelector, 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');
Bucknell, Mary S.
committed
const { ASPECT_RATIO_PERCENT, MARGIN, layoutSelector } = require('./layout');
Naab, Daniel James
committed
const { pointsSelector, lineSegmentsSelector, isVisibleSelector } = require('./points');
const { xScaleSelector, yScaleSelector } = require('./scales');
const { Actions, configureStore } = require('./store');
// Function that returns the left bounding point for a given chart point.
const bisectDate = bisector(d => d.time).left;
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')
Naab, Daniel James
committed
const plotDataLine = function (elem, {visible, lines, tsDataKey, xScale, yScale}) {
const elemId = 'ts-' + tsDataKey;
elem.selectAll(`#${elemId}`).remove();
Naab, Daniel James
committed
if (!visible) {
Naab, Daniel James
committed
const tsLine = line()
.x(d => xScale(new Date(d.time)))
.y(d => yScale(d.value));
for (let line of lines) {
elem.append('path')
.datum(line.points)
.classed('line', true)
Naab, Daniel James
committed
.classed('approved', line.classes.approved)
.classed('estimated', line.classes.estimated)
.attr('data-title', tsDataKey)
.attr('id', `ts-${tsDataKey}`)
.attr('d', tsLine);
}
const getNearestTime = function (data, time) {
let index = bisectDate(data, time, 1);
let datum;
let d0 = data[index - 1];
let d1 = data[index];
if (d0 && d1) {
datum = time - d0.time > d1.time - time ? d1 : d0;
} else {
datum = d0 || d1;
// Return the nearest data point and its index.
return {
datum,
index: datum === d0 ? index - 1 : index
};
};
const plotTooltips = function (elem, {xScale, yScale, data, isCompareVisible, compareXScale, compareData}) {
// Create a node to highlight the currently selected date/time.
Bucknell, Mary S.
committed
elem.selectAll('.focus').remove();
elem.select('.overlay').remove();
Bucknell, Mary S.
committed
elem.select('.tooltip-group').remove();
let focus = elem.append('g')
.attr('class', 'focus')
.style('display', 'none');
let tooltipLine = focus.append('line')
.attr('stroke-width', 2)
.attr('class', 'tooltip-focus-line');
//let currentFocus = elem.append('g')
// .attr('class', 'focus')
// .style('display', 'none');
///currentFocus.append('circle')
// .attr('r', 7.5);
//let compareFocus = elem.append('g')
// .attr('class', 'focus')
// .style('display', 'none');
//compareFocus.append('circle')
// .attr('r', 7.5);
Bucknell, Mary S.
committed
let tooltipText = elem.append('g')
.attr('class', 'tooltip-group');
tooltipText.append('text')
.attr('class', 'current-tooltip-text');
tooltipText.append('text')
.attr('class', 'compare-tooltip-text');
elem.append('rect')
.attr('class', 'overlay')
Bucknell, Mary S.
committed
.attr('width', '100%')
.attr('height', '100%')
Bucknell, Mary S.
committed
.on('mouseover', () => {
focus.style('display', null);
//currentFocus.style('display', null);
//if (isCompareVisible) {
// compareFocus.style('display', null);
//}
Bucknell, Mary S.
committed
})
.on('mouseout', () => {
focus.style('display', 'none');
//currentFocus.style('display', 'none');
//compareFocus.style('display', 'none');
Bucknell, Mary S.
committed
})
.on('mousemove', function () {
// Get the nearest data point for the current mouse position.
const time = xScale.invert(mouse(this)[0]);
const {datum, index} = getNearestTime(data, time);
if (!datum) {
return;
}
let compareTime;
let compare;
if (isCompareVisible) {
compareTime = compareXScale.invert(mouse(this)[0]);
compare = getNearestTime(compareData, compareTime);
}
Naab, Daniel James
committed
let yMax = max([max(data.map((datum) => { return datum.value})), max(compareData.map((datum) => { return datum.value; }))])
tooltipLine
.attr('stroke', 'black')
.attr('x1', xScale(datum.time))
.attr('x2', xScale(datum.time))
.attr('y1', yScale.range()[0])
.attr('y2', yScale(yMax));
// Move the focus node to this date/time.
//currentFocus.attr('transform', `translate(${xScale(datum.time)}, ${yScale(datum.value)})`);
//if (isCompareVisible) {
// compareFocus.attr('transform',
// `translate(${compareXScale(compare.datum.time)}, ${yScale(compare.datum.value)})`);
// }
// Draw text, anchored to the left or right, depending on
// which side of the graph the point is on.
// TODO: Should we use the position of the mouse rather than the index of the date?
const isFirstHalf = index < data.length / 2;
tooltipText.select('.current-tooltip-text')
Bucknell, Mary S.
committed
//.attr('text-anchor', isFirstHalf ? 'start' : 'end')
.attr('x', 15)
Bucknell, Mary S.
committed
//.attr('y', '-.31em')
.text(() => datum.label);
tooltipText.select('.compare-tooltip-text')
.text(() => isCompareVisible ? compare.datum.label : '')
Bucknell, Mary S.
committed
// .attr('text-anchor', isFirstHalf ? 'start' : 'end')
.attr('x', 15)
.attr('y', '1em');
Naab, Daniel James
committed
Naab, Daniel James
committed
const plotMedianPoints = function (elem, {visible, xscale, yscale, medianStatsData}) {
elem.select('#median-points').remove();
Naab, Daniel James
committed
if (!visible) {
return;
}
const container = elem
.append('g')
.attr('id', 'median-points');
container.selectAll('medianPoint')
.data(medianStatsData)
.enter()
.append('circle')
.attr('cx', function(d) {
})
.attr('cy', function(d) {
container.selectAll('medianPointText')
.data(medianStatsData)
.enter()
.append('text')
.text(function(d) {
return d.label;
})
Naab, Daniel James
committed
const timeSeriesGraph = function (elem) {
elem.append('div')
.attr('class', 'hydrograph-container')
.style('padding-bottom', ASPECT_RATIO_PERCENT)
.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({
title: state => state.title,
description: state => state.desc,
isInteractive: () => true
})))
.append('g')
.attr('transform', `translate(${MARGIN.left},${MARGIN.top})`)
Naab, Daniel James
committed
.call(link(appendAxes, axesSelector))
.call(link(plotDataLine, createStructuredSelector({
Naab, Daniel James
committed
visible: isVisibleSelector('current'),
Naab, Daniel James
committed
lines: lineSegmentsSelector('current'),
Naab, Daniel James
committed
xScale: xScaleSelector('current'),
yScale: yScaleSelector,
Naab, Daniel James
committed
tsDataKey: () => 'current'
Naab, Daniel James
committed
})))
Naab, Daniel James
committed
.call(link(plotDataLine, createStructuredSelector({
Naab, Daniel James
committed
visible: isVisibleSelector('compare'),
Naab, Daniel James
committed
lines: lineSegmentsSelector('compare'),
Naab, Daniel James
committed
xScale: xScaleSelector('compare'),
yScale: yScaleSelector,
Naab, Daniel James
committed
tsDataKey: () => 'compare'
Naab, Daniel James
committed
})))
Naab, Daniel James
committed
.call(link(plotTooltips, createStructuredSelector({
Naab, Daniel James
committed
xScale: xScaleSelector('current'),
yScale: yScaleSelector,
data: pointsSelector('current'),
isCompareVisible: isVisibleSelector('compare'),
compareXScale: xScaleSelector('compare'),
compareData: pointsSelector('compare')
Naab, Daniel James
committed
})))
.call(link(plotMedianPoints, createStructuredSelector({
Naab, Daniel James
committed
visible: isVisibleSelector('medianStatistics'),
xscale: xScaleSelector('current'),
yscale: yScaleSelector,
medianStatsData: pointsSelector('medianStatistics')
Naab, Daniel James
committed
})));
elem.append('div')
.call(link(addSROnlyTable, createStructuredSelector({
columnNames: createSelector(
(state) => state.title,
(title) => [title, 'Time']
),
data: createSelector(
pointsSelector('current'),
points => points.map((value) => {
return [value.value, value.time];
})
),
describeById: () => {return 'time-series-sr-desc'},
describeByText: () => {return 'current time series data in tabular format'}
})));
elem.append('div')
.call(link(addSROnlyTable, createStructuredSelector({
columnNames: createSelector(
(state) => state.title,
(title) => [`Median ${title}`, 'Time']
),
data: createSelector(
pointsSelector('medianStatistics'),
points => points.map((value) => {
return [value.value, value.time];
})
),
describeById: () => {return 'median-statistics-sr-desc'},
describeByText: () => {return 'median statistical data in tabular format'}
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)
.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, getNearestTime, timeSeriesGraph};