Skip to content
Snippets Groups Projects
index.js 10.6 KiB
Newer Older
  • Learn to ignore specific revisions
  • /**
     * Hydrograph charting module.
     */
    
    const { select } = require('d3-selection');
    
    const { createSelector, createStructuredSelector } = require('reselect');
    
    
    const { addSVGAccessibility, addSROnlyTable } = require('../../accessibility');
    
    const { dispatch, link, provide } = require('../../lib/redux');
    
    const { appendAxes, axesSelector } = require('./axes');
    
    const { ASPECT_RATIO_PERCENT, MARGIN, CIRCLE_RADIUS, layoutSelector } = require('./layout');
    
    const { drawSimpleLegend, legendDisplaySelector, createLegendMarkers } = require('./legend');
    
    const { pointsSelector, lineSegmentsSelector, isVisibleSelector, MASK_DESC } = require('./points');
    
    const { xScaleSelector, yScaleSelector } = require('./scales');
    const { Actions, configureStore } = require('./store');
    
    const { createTooltip } = require('./tooltip');
    
    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, lines, tsDataKey, xScale, yScale}) {
    
        const elemId = 'ts-' + tsDataKey;
        elem.selectAll(`#${elemId}`).remove();
    
        elem.selectAll(`.${tsDataKey}-mask-group`).remove();
    
        const tsLine = line()
            .x(d => xScale(new Date(d.time)))
            .y(d => yScale(d.value));
    
        for (let line of lines) {
    
    Yan, Andrew N.'s avatar
    Yan, Andrew N. committed
            if (line.classes.dataMask === null) {
    
                elem.append('path')
                    .datum(line.points)
                    .classed('line', true)
                    .classed('approved', line.classes.approved)
                    .classed('estimated', line.classes.estimated)
                    .attr('data-title', tsDataKey)
                    .attr('id', `ts-${tsDataKey}`)
                    .attr('d', tsLine);
            }
            else {
    
                let maskCode = line.classes.dataMask.toLowerCase();
                let maskDisplayName = MASK_DESC[maskCode].replace(' ', '-').toLowerCase();
    
                let xMaskExtent = extent(line.points, d => d.time);
                let [xDomainStart, xDomainEnd] = xMaskExtent;
                let [yRangeStart, yRangeEnd] = yScale.domain();
                let maskGroup = elem
                    .append('g')
    
                        .attr('class', `${tsDataKey}-mask-group`);
    
    
                maskGroup.append('rect')
                    .attr('x', xScale(xDomainStart))
                    .attr('y', yScale(yRangeEnd))
                    .attr('width', xScale(xDomainEnd) - xScale(xDomainStart))
                    .attr('height', Math.abs(yScale(yRangeEnd)- yScale(yRangeStart)))
    
                    .attr('class', `mask ${maskDisplayName}-mask`);
    
                let patternId = tsDataKey === 'compare' ? 'url(#hash-135)' : 'url(#hash-45)';
    
                let xSpan = xScale(xDomainEnd) - xScale(xDomainStart);
                let rectWidth = xSpan > 0 ? xSpan : 1;
    
                maskGroup.append('rect')
                    .attr('x', xScale(xDomainStart))
                    .attr('y', yScale(yRangeEnd))
    
                    .attr('width', rectWidth)
                    .attr('height', Math.abs(yScale(yRangeEnd) - yScale(yRangeStart)))
    
    const plotSvgDefs = function(elem) {
    
    
        let defs = elem.append('defs');
    
        defs.append('mask')
            .attr('id', 'display-mask')
            .attr('maskUnits', 'userSpaceOnUse')
            .append('rect')
    
                .attr('x', '0')
                .attr('y', '0')
                .attr('width', '100%')
                .attr('height', '100%')
                .attr('fill', '#0000ff');
    
    
        defs.append('pattern')
            .attr('id', 'hash-45')
            .attr('width', '8')
            .attr('height', '8')
            .attr('patternUnits', 'userSpaceOnUse')
            .attr('patternTransform', 'rotate(45)')
            .append('rect')
                .attr('width', '4')
                .attr('height', '8')
                .attr('transform', 'translate(0, 0)')
                .attr('mask', 'url(#display-mask)');
    
    
        defs.append('pattern')
            .attr('id', 'hash-135')
            .attr('width', '8')
            .attr('height', '8')
            .attr('patternUnits', 'userSpaceOnUse')
            .attr('patternTransform', 'rotate(135)')
            .append('rect')
                .attr('width', '4')
                .attr('height', '8')
                .attr('transform', 'translate(0, 0)')
                .attr('mask', 'url(#display-mask)');
    
    const plotLegend = function(elem, {displayItems, width, currentSegments, compareSegments}) {
    
        elem.select('.legend').remove();
    
        let plotMarkers = createLegendMarkers(displayItems, currentSegments.concat(compareSegments));
        drawSimpleLegend(elem, plotMarkers, width);
    
    const plotMedianPoints = function (elem, {visible, xscale, yscale, medianStatsData, showLabel}) {
    
        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('class', 'median-data-series')
                .attr('r', CIRCLE_RADIUS)
    
                .attr('cx', function(d) {
    
                    return xscale(d.time);
    
                })
                .attr('cy', function(d) {
    
                    return yscale(d.value);
    
                .on('click', dispatch(function() {
                    return Actions.showMedianStatsLabel(!showLabel);
                }));
    
    Bucknell, Mary S.'s avatar
    Bucknell, Mary S. committed
            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);
                    });
    
    Yan, Andrew N.'s avatar
    Yan, Andrew N. committed
        elem.append('div')
    
            .attr('class', 'hydrograph-container')
            .style('padding-bottom', ASPECT_RATIO_PERCENT)
    
    Yan, Andrew N.'s avatar
    Yan, Andrew N. committed
            .append('svg')
                .call(link((elem, layout) => elem.attr('viewBox', `0 0 ${layout.width} ${layout.height}`), layoutSelector))
                .call(link(addSVGAccessibility, createStructuredSelector({
                    title: state => state.title,
                    description: state => state.desc,
                    isInteractive: () => true
                })))
    
                .call(plotSvgDefs)
    
    Yan, Andrew N.'s avatar
    Yan, Andrew N. committed
                .call(link(plotLegend, createStructuredSelector({
    
                    displayItems: legendDisplaySelector,
    
                    currentSegments: lineSegmentsSelector('current'),
                    compareSegments: lineSegmentsSelector('compare'),
    
                    width: state => state.width
    
    Yan, Andrew N.'s avatar
    Yan, Andrew N. committed
                })))
                .append('g')
                    .attr('transform', `translate(${MARGIN.left},${MARGIN.top})`)
                    .call(link(appendAxes, axesSelector))
                    .call(link(plotDataLine, createStructuredSelector({
                        visible: isVisibleSelector('current'),
                        lines: lineSegmentsSelector('current'),
                        xScale: xScaleSelector('current'),
                        yScale: yScaleSelector,
                        tsDataKey: () => 'current'
                    })))
                    .call(link(plotDataLine, createStructuredSelector({
                        visible: isVisibleSelector('compare'),
                        lines: lineSegmentsSelector('compare'),
                        xScale: xScaleSelector('compare'),
                        yScale: yScaleSelector,
                        tsDataKey: () => 'compare'
                    })))
    
                    .call(link(createTooltip, createStructuredSelector({
    
    Yan, Andrew N.'s avatar
    Yan, Andrew N. committed
                        xScale: xScaleSelector('current'),
                        yScale: yScaleSelector,
    
                        compareXScale: xScaleSelector('compare'),
    
                        currentTsData: pointsSelector('current'),
                        compareTsData: pointsSelector('compare'),
                        isCompareVisible: isVisibleSelector('compare')
    
    Yan, Andrew N.'s avatar
    Yan, Andrew N. committed
                    })))
                    .call(link(plotMedianPoints, createStructuredSelector({
                        visible: isVisibleSelector('medianStatistics'),
                        xscale: xScaleSelector('current'),
                        yscale: yScaleSelector,
    
                        medianStatsData: pointsSelector('medianStatistics'),
                        showLabel: (state) => state.showMedianStatsLabel
    
    Yan, Andrew N.'s avatar
    Yan, Andrew N. committed
                    })));
    
        elem.append('div')
            .call(link(addSROnlyTable, createStructuredSelector({
                columnNames: createSelector(
                    (state) => state.title,
                    (title) => [title, 'Time']
                ),
                data: createSelector(
                    pointsSelector('current'),
                    points => points.map((value) => {
                        return [value.value, value.time];
                    })
                ),
    
    Bucknell, Mary S.'s avatar
    Bucknell, Mary S. committed
                describeById: () => 'time-series-sr-desc',
                describeByText: () => 'current time series data in tabular format'
    
            .call(link(addSROnlyTable, createStructuredSelector({
                columnNames: createSelector(
                    (state) => state.title,
                    (title) => [`Median ${title}`, 'Time']
                ),
                data: createSelector(
                    pointsSelector('medianStatistics'),
                    points => points.map((value) => {
                        return [value.value, value.time];
                    })
                ),
    
    Bucknell, Mary S.'s avatar
    Bucknell, Mary S. committed
                describeById: () => 'median-statistics-sr-desc',
                describeByText: () => 'median statistical data in tabular format'
    
    };
    
    
    const attachToNode = function (node, {siteno} = {}) {
        if (!siteno) {
            select(node).call(drawMessage, 'No data is available.');
            return;
    
        let store = configureStore({
            width: node.offsetWidth
        });
    
            .select('.hydrograph-last-year-input')
                .on('change', dispatch(function () {
    
                    return Actions.toggleTimeseries('compare', this.checked);
                }));
    
    
        window.onresize = function() {
            store.dispatch(Actions.resizeTimeseriesPlot(node.offsetWidth));
        };
    
        store.dispatch(Actions.retrieveTimeseries(siteno));
    };
    
    module.exports = {attachToNode, timeSeriesGraph};