"README.Rmd" did not exist on "bb4c1bb1827dac0194864cc83b129b66e08d475c"
Newer
Older
/**
* Hydrograph charting module.
*/
const { bisector, extent, min, max } = require('d3-array');
Naab, Daniel James
committed
const { mouse, select } = require('d3-selection');
const { line } = require('d3-shape');
const { timeFormat } = require('d3-time-format');
const { addSVGAccessibility, addSROnlyTable } = require('../../accessibility');
const { getTimeseries, getPreviousYearTimeseries } = require('../../models');
Bucknell, Mary S.
committed
const { appendAxes, updateYAxis, createAxes } = require('./axes');
Bucknell, Mary S.
committed
const { createScales, createXScale, updateYScale } = require('./scales');
// Define width, height and margin for the SVG.
// Use a fixed size, and scale to device width using CSS.
const WIDTH = 800;
const HEIGHT = WIDTH / 2;
const ASPECT_RATIO_PERCENT = `${100 * HEIGHT / WIDTH}%`;
const MARGIN = {
top: 20,
right: 75,
bottom: 45,
left: 50
};
// Function that returns the left bounding point for a given chart point.
const bisectDate = bisector(d => d.time).left;
// Create a time formatting function from D3's timeFormat
const formatTime = timeFormat('%c %Z');
Naab, Daniel James
committed
class Hydrograph {
/**
* @param {Array} data IV data as returned by models/getTimeseries
Bucknell, Mary S.
committed
* @param {String} yLabel y-axis label
* @param {String} title for svg's title attribute
* @param {String} desc for svg's desc attribute
* @param {Node} element Dom node to insert
*/
constructor({data=[], yLabel='Data', title='', desc='', element}) {
Bucknell, Mary S.
committed
this._yLabel = yLabel;
Bucknell, Mary S.
committed
this._desc = desc;
Bucknell, Mary S.
committed
if (data && data.length) {
this._drawChart();
} else {
this._drawMessage('No data is available for this site.');
}
}
Bucknell, Mary S.
committed
/**
* Add a new time series to the Hydrograph. The time series is assumed to be
* data that is over the same date range in a different year.
* @param {Array} data - IV data as returned by models.getTimeseires
*/
addCompareTimeSeries(data) {
this._tsData.compare = data;
const currentYExtent = extent(this._tsData.current, d => d.value);
const yExtent = extent(data, d => d.value);
const yDataExtent = [min([yExtent[0], currentYExtent[0]]), max([yExtent[1], currentYExtent[1]])];
Bucknell, Mary S.
committed
Bucknell, Mary S.
committed
const xScale = createXScale(data, WIDTH - MARGIN.right);
Bucknell, Mary S.
committed
updateYAxis(this.axis.yAxis, this.scale.yScale);
this.svg.select('.y-axis')
Bucknell, Mary S.
committed
.call(this.axis.yAxis);
Bucknell, Mary S.
committed
//Update the current ts
select('#ts-current')
.attr('d', this.currentLine(this._tsData.current));
// Add the new time series
Bucknell, Mary S.
committed
this._plotDataLine(this.plot, {xScale: xScale, yScale: this.scale.yScale}, 'compare');
Bucknell, Mary S.
committed
}
Bucknell, Mary S.
committed
/**
* Remove the compare time series from the plot and rescale
*/
removeCompareTimeSeries() {
const currentYExtent = extent(this._tsData.current, d => d.value);
Bucknell, Mary S.
committed
this.svg.select('#ts-compare').remove();
delete this._tsData.compare;
Bucknell, Mary S.
committed
updateYScale(this.scale.yScale, currentYExtent);
updateYAxis(this.axis.yAxis, this.scale.yScale);
this.svg.select('.y-axis')
.call(this.axis.yAxis);
//Update the current ts
select('#ts-current')
.attr('d', this.currentLine(this._tsData.current));
_drawChart() {
// Set up parent element and SVG
this.svg = select(this._element)
.append('div')
.attr('class', 'hydrograph-container')
.style('padding-bottom', ASPECT_RATIO_PERCENT)
.append('svg')
.attr('preserveAspectRatio', 'xMinYMin meet')
.attr('viewBox', `0 0 ${WIDTH} ${HEIGHT}`);
Bucknell, Mary S.
committed
addSVGAccessibility({
Bucknell, Mary S.
committed
title: this._title,
description: this._desc,
isInteractive: true
});
Bucknell, Mary S.
committed
addSROnlyTable({
container: this._element,
columnNames: [this._title, 'Time'],
data: this._tsData.current.map((value) => {
Bucknell, Mary S.
committed
return [value.value, value.time];
})
});
// We'll actually be appending to a <g> element
this.plot = this.svg.append('g')
.attr('transform', `translate(${MARGIN.left},${MARGIN.top})`);
// Create x/y scaling for the full (100%) view.
this.scale = createScales(
WIDTH - MARGIN.right,
HEIGHT - (MARGIN.top + MARGIN.bottom)
);
Bucknell, Mary S.
committed
this.axis = createAxes(this.scale.xScale, this.scale.yScale, -WIDTH + MARGIN.right);
// Draw the graph components with the given scaling.
appendAxes({
Bucknell, Mary S.
committed
xAxis: this.axis.xAxis,
yAxis: this.axis.yAxis,
xLoc: {x: 0, y: HEIGHT - (MARGIN.top + MARGIN.bottom)},
yLoc: {x: 0, y: 0},
yLabelLoc: {x: HEIGHT / -2 + MARGIN.top, y: -35},
Bucknell, Mary S.
committed
yTitle: this._yLabel
this.currentLine = this._plotDataLine(this.plot, this.scale, 'current');
Bucknell, Mary S.
committed
this._plotTooltips(this.plot, this.scale, 'current');
}
_drawMessage(message) {
// Set up parent element and SVG
this._element.innerHTML = '';
const alertBox = select(this._element)
.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);
}
Bucknell, Mary S.
committed
_plotDataLine(plot, scale, tsDataKey) {
Bucknell, Mary S.
committed
.x(d => scale.xScale(d.time))
.y(d => scale.yScale(d.value));
.datum(this._tsData[tsDataKey])
Bucknell, Mary S.
committed
.attr('id', 'ts-' + tsDataKey)
Bucknell, Mary S.
committed
return tsLine;
Bucknell, Mary S.
committed
_plotTooltips(plot, scale, tsDataKey) {
// Create a node to hightlight the currently selected date/time.
let focus = plot.append('g')
.attr('class', 'focus')
.style('display', 'none');
focus.append('circle')
.attr('r', 7.5);
focus.append('text');
plot.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', (d, i, nodes) => {
// Get the nearest data point for the current mouse position.
Bucknell, Mary S.
committed
let time = scale.xScale.invert(mouse(nodes[i])[0]);
let {datum, index} = this._getNearestTime(time, tsDataKey);
// Move the focus node to this date/time.
Bucknell, Mary S.
committed
focus.attr('transform', `translate(${scale.xScale(datum.time)}, ${scale.yScale(datum.value)})`);
// Draw text, anchored to the left or right, depending on
// which side of the graph the point is on.
let isFirstHalf = index < this._tsData[tsDataKey].length / 2;
focus.select('text')
.text(() => datum.label)
.attr('text-anchor', isFirstHalf ? 'start' : 'end')
.attr('x', isFirstHalf ? 15 : -15)
.attr('dy', isFirstHalf ? '.31em' : '-.31em');
});
}
Bucknell, Mary S.
committed
_getNearestTime(time, tsDataKey) {
let index = bisectDate(this._tsData[tsDataKey], time, 1);
let datum;
let d0 = this._tsData[tsDataKey][index - 1];
let d1 = this._tsData[tsDataKey][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
function attachToNode(node, {siteno}) {
Bucknell, Mary S.
committed
let hydrograph;
let getLastYearTS;
getTimeseries({sites: [siteno]}).then((series) => {
Bucknell, Mary S.
committed
let dataIsValid = series && series[0] &&
!series[0].values.some(d => d.value === -999999);
Bucknell, Mary S.
committed
hydrograph = new Hydrograph({
element: node,
data: dataIsValid ? series[0].values : [],
yLabel: dataIsValid ? series[0].variableDescription : 'No data',
title: dataIsValid ? series[0].variableName : '',
Bucknell, Mary S.
committed
desc: dataIsValid ? series[0].variableDescription + ' from ' +
formatTime(series[0].seriesStartDate) + ' to ' +
formatTime(series[0].seriesEndDate) : ''
Bucknell, Mary S.
committed
});
if (dataIsValid) {
getLastYearTS = getPreviousYearTimeseries({
Bucknell, Mary S.
committed
site: node.dataset.siteno,
startTime: series[0].seriesStartDate,
endTime: series[0].seriesEndDate
Bucknell, Mary S.
committed
}
}, () =>
hydrograph = new Hydrograph({
element: node,
data: []
})
Bucknell, Mary S.
committed
);
let lastYearInput = node.getElementsByClassName('hydrograph-last-year-input');
if (lastYearInput.length > 0) {
lastYearInput[0].addEventListener('change', (evt) => {
if (evt.target.checked) {
getLastYearTS.then((series) => {
Bucknell, Mary S.
committed
hydrograph.addCompareTimeSeries(series[0].values);
Bucknell, Mary S.
committed
});
Bucknell, Mary S.
committed
}
else {
Bucknell, Mary S.
committed
hydrograph.removeCompareTimeSeries();
Bucknell, Mary S.
committed
}
});
}
}
module.exports = {Hydrograph, attachToNode};