Skip to content
Snippets Groups Projects
index.js 10 KiB
Newer Older
  • Learn to ignore specific revisions
  • /**
     * Hydrograph charting module.
     */
    
    const { bisector, extent, min, max } = require('d3-array');
    
    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');
    
    const { appendAxes, updateYAxis, createAxes } = require('./axes');
    
    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');
    
    
    
        /**
         * @param {Array} data IV data as returned by models/getTimeseries
    
         * @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}) {
    
            this._title = title;
    
            this._element = element;
    
            this._tsData = {};
    
                this._tsData.current = data;
    
                this._drawChart();
            } else {
                this._drawMessage('No data is available for this site.');
            }
        }
    
    
        /**
         * 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) {
    
    Bucknell, Mary S.'s avatar
    Bucknell, Mary S. committed
            //Save data - TODO will be needed in order to implement the tooltips
    
    Bucknell, Mary S.'s avatar
    Bucknell, Mary S. committed
    
            // Update the yScale by determining the new extent
    
            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]])];
            updateYScale(this.scale.yScale, yDataExtent);
    
    Bucknell, Mary S.'s avatar
    Bucknell, Mary S. committed
    
            // Create a x scale for the new data
    
            const xScale = createXScale(data, WIDTH - MARGIN.right);
    
            // Update the yAxis
    
            this.svg.select('.y-axis')
    
    Bucknell, Mary S.'s avatar
    Bucknell, Mary S. committed
            //Update the current ts line
    
                .attr('d', this.currentLine(this._tsData.current));
    
            // Add the new time series
    
            this._plotDataLine(this.plot, {xScale: xScale, yScale: this.scale.yScale}, 'compare');
    
        /**
         * Remove the compare time series from the plot and rescale
         */
        removeCompareTimeSeries() {
    
    Bucknell, Mary S.'s avatar
    Bucknell, Mary S. committed
            // Remove the compare time series
    
            this.svg.select('.x-top-axis').remove();
    
    Bucknell, Mary S.'s avatar
    Bucknell, Mary S. committed
    
            // Update the y scale and  redraw the axis
            const currentYExtent = extent(this._tsData.current, d => d.value);
    
            updateYScale(this.scale.yScale, currentYExtent);
            updateYAxis(this.axis.yAxis, this.scale.yScale);
            this.svg.select('.y-axis')
                .call(this.axis.yAxis);
    
    Bucknell, Mary S.'s avatar
    Bucknell, Mary S. committed
    
            //Redraw the current ts
    
                .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}`);
    
                title: this._title,
                description: this._desc,
                isInteractive: true
            });
    
    
            addSROnlyTable({
                container: this._element,
                columnNames: [this._title, 'Time'],
    
                data: this._tsData.current.map((value) => {
    
            // 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(
    
                this._tsData.current,
    
                WIDTH - MARGIN.right,
                HEIGHT - (MARGIN.top + MARGIN.bottom)
            );
    
            this.axis = createAxes(this.scale.xScale, this.scale.yScale, -WIDTH + MARGIN.right);
    
    
            // Draw the graph components with the given scaling.
    
                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},
    
            this.currentLine = this._plotDataLine(this.plot, this.scale, 'current');
    
            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);
        }
    
    
            let tsLine = line()
    
                .x(d => scale.xScale(d.time))
                .y(d => scale.yScale(d.value));
    
                .datum(this._tsData[tsDataKey])
    
                .classed('line', true)
    
                .attr('d', tsLine);
    
            // 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.
    
                    let time = scale.xScale.invert(mouse(nodes[i])[0]);
                    let {datum, index} = this._getNearestTime(time, tsDataKey);
    
                    // Move the focus node to this date/time.
    
                    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');
                });
        }
    
            let index = bisectDate(this._tsData[tsDataKey], time, 1);
    
            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
    
    function attachToNode(node, {siteno}) {
    
        let hydrograph;
        let getLastYearTS;
        getTimeseries({sites: [siteno]}).then((series) => {
    
                let dataIsValid = series && series[0] &&
                    !series[0].values.some(d => d.value === -999999);
    
                hydrograph = new Hydrograph({
                    element: node,
                    data: dataIsValid ? series[0].values : [],
                    yLabel: dataIsValid ? series[0].variableDescription : 'No data',
                    title: dataIsValid ? series[0].variableName : '',
    
                    desc: dataIsValid ? series[0].variableDescription + ' from ' +
                        formatTime(series[0].seriesStartDate) + ' to ' +
                        formatTime(series[0].seriesEndDate) : ''
    
                    getLastYearTS = getPreviousYearTimeseries({
    
                        site: node.dataset.siteno,
                        startTime: series[0].seriesStartDate,
                        endTime: series[0].seriesEndDate
    
        );
        let lastYearInput = node.getElementsByClassName('hydrograph-last-year-input');
        if (lastYearInput.length > 0) {
            lastYearInput[0].addEventListener('change', (evt) => {
                if (evt.target.checked) {
                    getLastYearTS.then((series) => {
    
                        hydrograph.addCompareTimeSeries(series[0].values);
    
    Bucknell, Mary S.'s avatar
    Bucknell, Mary S. committed
                } else {
    
    }
    
    
    module.exports = {Hydrograph, attachToNode};