Newer
Older
/**
* Hydrograph charting module.
*/
const { bisector } = 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');
const { WIDTH, HEIGHT, ASPECT_RATIO_PERCENT, MARGIN } = require('./layout');
const { pointsSelector, validPointsSelector, 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, points, tsDataKey}) {
const elemId = 'ts-' + tsDataKey;
elem.selectAll(`#${elemId}`).remove();
Naab, Daniel James
committed
if (!visible) {
Naab, Daniel James
committed
elem.datum(points)
.append('path')
.classed('line', true)
.attr('id', elemId)
.attr('d', line().x(d => d.x)
.y(d => d.y));
};
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
};
};
Naab, Daniel James
committed
const plotTooltips = function (elem, {xScale, yScale, data}) {
// Create a node to hightlight the currently selected date/time.
let focus = elem.append('g')
.attr('class', 'focus')
.style('display', 'none');
focus.append('circle')
.attr('r', 7.5);
focus.append('text');
elem.append('rect')
.attr('class', 'overlay')
.attr('width', WIDTH)
.attr('height', HEIGHT)
.on('mouseover', () => focus.style('display', null))
.on('mouseout', () => focus.style('display', 'none'))
.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;
}
Naab, Daniel James
committed
// Move the focus node to this date/time.
focus.attr('transform', `translate(${xScale(datum.time)}, ${yScale(datum.value)})`);
// Draw text, anchored to the left or right, depending on
// which side of the graph the point is on.
const isFirstHalf = index < data.length / 2;
focus.select('text')
.text(() => datum.label)
.attr('text-anchor', isFirstHalf ? 'start' : 'end')
.attr('x', isFirstHalf ? 15 : -15)
.attr('dy', isFirstHalf ? '.31em' : '-.31em');
});
};
Naab, Daniel James
committed
Naab, Daniel James
committed
const plotMedianPoints = function (elem, {xscale, yscale, medianStatsData}) {
elem.select('#median-points').remove();
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')
.attr('preserveAspectRatio', 'xMinYMin meet')
.attr('viewBox', `0 0 ${WIDTH} ${HEIGHT}`)
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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
.call(link(appendAxes, axesSelector))
.call(link(plotDataLine, createStructuredSelector({
visible: state => isVisibleSelector(state, 'current'),
points: state => validPointsSelector(state, 'current'),
tsDataKey: () => 'current'
}), 'current'))
.call(link(plotDataLine, createStructuredSelector({
visible: state => isVisibleSelector(state, 'compare'),
points: state => validPointsSelector(state, 'compare'),
tsDataKey: () => 'compare'
}), 'compare'))
.call(link(plotTooltips, createStructuredSelector({
xScale: state => xScaleSelector(state, 'current'),
yScale: state => yScaleSelector(state, 'current'),
data: state => pointsSelector(state, 'current')
})))
.call(link(plotMedianPoints, createStructuredSelector({
xscale: state => xScaleSelector(state),
yscale: state => yScaleSelector(state),
medianStatsData: state => pointsSelector(state, 'medianStatistics')
})));
elem.call(link(addSROnlyTable, createStructuredSelector({
columnNames: createSelector(
(state) => state.title,
(title) => [title, 'Time']
),
data: createSelector(
state => pointsSelector(state, 'current'),
points => points.map((value) => {
return [value.value, value.time];
Bucknell, Mary S.
committed
})
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();
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);
}));
store.dispatch(Actions.retrieveTimeseries(siteno));
};
module.exports = {attachToNode, getNearestTime, timeSeriesGraph};