Skip to content
Snippets Groups Projects
index.js 7.48 KiB
Newer Older
/**
 * Hydrograph charting module.
 */
const { bisector } = require('d3-array');
const { mouse, select } = require('d3-selection');
const { line } = require('d3-shape');
const { createSelector, createStructuredSelector } = require('reselect');

const { addSVGAccessibility, addSROnlyTable } = require('../../accessibility');
const { dispatch, link, provide } = require('../../lib/redux');
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');
const plotDataLine = function (elem, {visible, points, tsDataKey}) {
    const elemId = 'ts-' + tsDataKey;
    elem.selectAll(`#${elemId}`).remove();
        .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
    };
};
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;
            }
            // 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');
        });
};
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('id', 'median-point')
            .attr('x', function(d) {
                return xscale(d.time);
            })
            .attr('y', function(d) {
                return yscale(d.value);
            .attr('cx', function(d) {
                return xscale(d.time);
            })
            .attr('cy', function(d) {
                return yscale(d.value);
    container.selectAll('medianPointText')
        .data(medianStatsData)
        .enter()
        .append('text')
            .text(function(d) {
                return d.label;
            })
            .attr('id', 'median-text')
            .attr('x', function(d) {
                return xscale(d.time) + 5;
            })
            .attr('y', function(d) {
                return yscale(d.value);
    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}`)
            .call(link(addSVGAccessibility, createStructuredSelector({
                title: state => state.title,
                description: state => state.desc,
                isInteractive: () => true
            })))
            .append('g')
                .attr('transform', `translate(${MARGIN.left},${MARGIN.top})`)
                .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];
};


const attachToNode = function (node, {siteno} = {}) {
    if (!siteno) {
        select(node).call(drawMessage, 'No data is available.');
        return;

    let store = configureStore();

    select(node)
        .call(provide(store))
        .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};