Skip to content
Snippets Groups Projects
time-series-graph.js 11.3 KiB
Newer Older
  • Learn to ignore specific revisions
  • import {line as d3Line, curveStepAfter} from 'd3-shape';
    
    import {createSelector, createStructuredSelector} from 'reselect';
    
    import config from 'ui/config';
    
    import {link} from 'ui/lib/d3-redux';
    import {mediaQuery}  from 'ui/utils';
    
    
    import {addSVGAccessibility} from 'd3render/accessibility';
    import {appendAxes} from 'd3render/axes';
    import {renderMaskDefs} from 'd3render/data-masks';
    
    import {appendInfoTooltip} from 'd3render/info-tooltip';
    
    import {showFloodLevels} from 'ml/selectors/flood-data-selector';
    
    import {getPrimaryMedianStatisticsData} from 'ml/selectors/hydrograph-data-selector';
    
    import {getAxes}  from './selectors/axes';
    
    import {getIVParameter} from 'ml/selectors/hydrograph-data-selector';
    
    import {getGroundwaterLevelPoints} from './selectors/discrete-data';
    
    import {getFloodLevelData} from './selectors/flood-level-data';
    
    import {getIVDataSegments, HASH_ID} from './selectors/iv-data';
    
    import {getMainLayout} from './selectors/layout';
    
    import {getMainXScale, getMainYScale, getYScale} from './selectors/scales';
    
    import {
        getTitle,
        getDescription,
        isVisible,
    
    Briggs, Aaron Shane's avatar
    Briggs, Aaron Shane committed
        getPrimaryParameter
    
    } from './selectors/time-series-data';
    
    import {drawGroundwaterLevels} from './discrete-data';
    
    import {drawFloodLevelLines} from './flood-level-lines';
    
    import {drawThresholdLines} from './threshold-lines';
    
    import {drawDataSegments} from './time-series-lines';
    
    import {drawTooltipFocus, drawTooltipText}  from './tooltip';
    
    import {getThresholdsInRange} from './selectors/thresholds-data';
    
    const addDefsPatterns = function(elem) {
        const patterns = [{
    
    Briggs, Aaron Shane's avatar
    Briggs, Aaron Shane committed
            patternId: HASH_ID.secondary,
            patternTransform: 'rotate(90)'
        },
        {
    
            patternId: HASH_ID.compare,
            patternTransform: 'rotate(135)'
        }];
        const defs = elem.append('defs');
        renderMaskDefs(defs, 'iv-graph-pattern-mask', patterns);
    
    /**
     * Plots the median points for a single median time series.
     * @param  {Object} elem
     * @param  {Function} xscale
     * @param  {Function} yscale
     * @param  {Number} modulo
     * @param  {Array} points
    
    const drawMedianPoints = function(elem, {xscale, yscale, modulo, points}) {
    
        const stepFunction = d3Line()
            .curve(curveStepAfter)
    
    Briggs, Aaron Shane's avatar
    Briggs, Aaron Shane committed
            .x(function(d) {
    
                return xscale(d.dateTime);
    
    Briggs, Aaron Shane's avatar
    Briggs, Aaron Shane committed
            .y(function(d) {
    
                return yscale(d.point);
    
        const medianGrp = elem.append('g')
            .attr('class', 'median-stats-group');
    
        medianGrp.append('path')
            .datum(points)
            .classed('median-data-series', true)
            .classed('median-step', true)
            .classed(`median-step-${modulo}`, true)
            .attr('d', stepFunction);
    };
    
    /**
     * Plots the median points for all median time series for the current variable.
     * @param  {Object} elem
     * @param  {Boolean} visible
     * @param  {Function} xscale
     * @param  {Function} yscale
    
     * @param  {Array} seriesPoints
     * @param {Boolean} enableClip
    
    const drawAllMedianPoints = function(elem, {visible, xscale, yscale, seriesPoints, enableClip}) {
    
        elem.select('#median-points').remove();
        const container = elem
            .append('g')
    
    Briggs, Aaron Shane's avatar
    Briggs, Aaron Shane committed
                .attr('id', 'median-points');
    
        if (enableClip) {
            container.attr('clip-path', 'url(#graph-clip');
        }
    
        Object.values(seriesPoints).forEach((series, index) => {
    
            drawMedianPoints(container, {xscale, yscale, modulo: index % 6, points: series.values});
    
    const drawTitle = function(elem, store, siteNo, agencyCode, sitename, showMLName, showTooltip) {
    
        let titleDiv = elem.append('div')
            .classed('time-series-graph-title', true);
    
        if (showMLName) {
            titleDiv.append('div')
    
                .attr('class', 'monitoring-location-name-div')
                .html(`${sitename}, ${agencyCode} ${siteNo}`);
    
            .attr('class', 'primary-title')
    
            .call(link(store, (elem, {title, primaryParameter}) => {
    
                    elem.call(
                        appendInfoTooltip,
    
                        primaryParameter ? primaryParameter.description || 'No description available' : '',
    
            }, createStructuredSelector({
    
    Briggs, Aaron Shane's avatar
    Briggs, Aaron Shane committed
                title: getTitle('primary'),
    
                primaryParameter: getPrimaryParameter
    
        titleDiv.append('div')
            .attr('class', 'secondary-title')
    
            .call(link(store, (elem, {title, secondaryParameter}) => {
    
                elem.html(title);
    
                if (showTooltip && secondaryParameter) {
    
                    elem.call(
                        appendInfoTooltip,
    
                        secondaryParameter ? secondaryParameter.description || 'No description available' : '',
    
                }
            }, createStructuredSelector({
    
    Briggs, Aaron Shane's avatar
    Briggs, Aaron Shane committed
                title: getTitle('secondary'),
    
                secondaryParameter: getIVParameter('secondary')
    
            })));
    
    const drawWatermark = function(elem, store) {
    
        // These constants will need to change if the watermark svg is updated
        const watermarkHalfHeight = 87 / 2;
        const watermarkHalfWidth = 235 / 2;
        elem.append('img')
            .classed('watermark', true)
            .attr('alt', 'USGS - science for a changing world')
            .attr('src', config.STATIC_URL + '/img/USGS_green_logo.svg')
    
                const centerX = layout.margin.left + (layout.width - layout.margin.right - layout.margin.left) / 2;
                const centerY = layout.margin.top + (layout.height - layout.margin.bottom - layout.margin.top) / 2;
                const scale = !mediaQuery(config.USWDS_MEDIUM_SCREEN) ? 0.5 : 1;
                const translateX = centerX - watermarkHalfWidth;
                const translateY = centerY - watermarkHalfHeight;
                const transform = `matrix(${scale}, 0, 0, ${scale}, ${translateX}, ${translateY})`;
    
                elem.style('transform', transform);
                // for Safari browser
                elem.style('-webkit-transform', transform);
    
    
    /**
     * Redux selector helper function that can combine the primary and secondary data titles into a single string.
     * @return {String} the concatenated accessibility title when a secondary data parameter is selected, otherwise returns
     *      the standard primary parameter title.
     */
    const combinedAccessibilityTitle = createSelector(
        getTitle('primary'),
        getTitle('secondary'),
        (primaryTitle, secondaryTitle) => {
            return `${primaryTitle}${secondaryTitle ? ` -- ${secondaryTitle}` : ''}`;
        });
    
    
    Bucknell, Mary S.'s avatar
    Bucknell, Mary S. committed
    /*
    
     * Initialize the time series svg and other elements but don't render any data
    
    Bucknell, Mary S.'s avatar
    Bucknell, Mary S. committed
     * @param {D3 selection} elem
     * @param {Redux store} store
     * @param {String} siteNo
    
     * @param {String} agencyCd
     * @param {String} sitename
     * @param {Boolean} showMLName - Set to true when the sitename should be rendered above the time series graph
     * @param {Boolean} showTooltip
    
    Bucknell, Mary S.'s avatar
    Bucknell, Mary S. committed
     */
    
    export const initializeTimeSeriesGraph = function(elem, store, siteNo, agencyCode, sitename, showMLName, showTooltip) {
    
            .attr('class', 'hydrograph-container')
    
            .attr('ga-on', 'click')
            .attr('ga-event-category', 'hydrograph-interaction')
            .attr('ga-event-action', 'clickOnTimeSeriesGraph')
    
            .call(drawWatermark, store)
            .call(drawTitle, store, siteNo, agencyCode, sitename, showMLName, showTooltip);
    
        if (showTooltip) {
            graphDiv.call(drawTooltipText, store);
        }
    
            .attr('xmlns', 'http://www.w3.org/2000/svg')
            .classed('hydrograph-svg', true)
    
                elem.select('#graph-clip').remove();
    
                elem.attr('viewBox', `0 0 ${layout.width + layout.margin.left + layout.margin.right} ${layout.height + layout.margin.top + layout.margin.bottom}`)
    
    Briggs, Aaron Shane's avatar
    Briggs, Aaron Shane committed
                        .attr('id', 'graph-clip')
                        .append('rect')
                            .attr('x', 0)
                            .attr('y', 0)
                            .attr('width', layout.width - layout.margin.right)
                            .attr('height', layout.height - layout.margin.bottom);
    
            }, getMainLayout))
    
            .call(link(store, addSVGAccessibility, createStructuredSelector({
    
                title: combinedAccessibilityTitle,
    
                isInteractive: () => true,
                idPrefix: () => 'hydrograph'
            })))
    
     * Sets up the event handlers to render the time series graph data and tooltips.
    
     * @param {D3 selection} elem
     * @param {Redux store} store
     * @param {Boolean} showTooltip - If true render the tooltip text and add the tooltip focus element
     */
    export const drawTimeSeriesGraphData = function(elem, store, showTooltip) {
        const graphDiv = elem.select('.hydrograph-container');
    
        const graphSvg = graphDiv.select('.hydrograph-svg');
    
    
        const dataGroup = graphSvg.append('g')
            .attr('class', 'plot-data-lines-group')
            .call(link(store, (group, layout) => {
                group.attr('transform', `translate(${layout.margin.left},${layout.margin.top})`);
            }, getMainLayout))
    
            .call(link(store, appendAxes, getAxes('MAIN')))
            .call(link(store, drawDataSegments, createStructuredSelector({
                visible: () => true,
    
                xScale: getMainXScale('current'),
                yScale: getMainYScale,
    
                enableClip: () => true
    
            .call(link(store, drawDataSegments, createStructuredSelector({
                visible: isVisible('secondary'),
                segments: getIVDataSegments('secondary'),
                dataKind: () => 'secondary',
                xScale: getMainXScale('current'),
    
                yScale: getYScale('MAIN', 'secondary'),
    
                enableClip: () => true
            })))
    
            .call(link(store, drawDataSegments, createStructuredSelector({
    
                dataKind: () => 'compare',
                xScale: getMainXScale('prioryear'),
    
                enableClip: () => true
    
            })))
            .call(link(store, drawGroundwaterLevels, createStructuredSelector({
                levels: getGroundwaterLevelPoints,
                xScale: getMainXScale('current'),
                yScale: getMainYScale,
                enableClip: () => true
    
            .call(link(store, drawAllMedianPoints, createStructuredSelector({
    
                xscale: getMainXScale('current'),
                yscale: getMainYScale,
    
                seriesPoints: getPrimaryMedianStatisticsData,
    
    Briggs, Aaron Shane's avatar
    Briggs, Aaron Shane committed
                enableClip: () => true
    
            .call(link(store, drawFloodLevelLines, createStructuredSelector({
    
                yscale: getMainYScale,
    
            })))
            .call(link(store, drawThresholdLines, createStructuredSelector({
                xscale: getMainXScale('current'),
                yscale: getMainYScale,
    
        if (showTooltip) {
            dataGroup.call(drawTooltipFocus, store);
        }