Skip to content
Snippets Groups Projects
legend.js 8.73 KiB
Newer Older
  • Learn to ignore specific revisions
  • // functions to facilitate legend creation for a d3 plot
    
    import memoize from 'fast-memoize';
    
    import {createSelector, createStructuredSelector} from 'reselect';
    
    import {CIRCLE_RADIUS, getMainLayout} from './layout';
    
    import { defineLineMarker, defineTextOnlyMarker, defineRectangleMarker } from '../../d3-rendering/markers';
    
    import { currentVariableLineSegmentsSelector, HASH_ID, MASK_DESC } from './drawing-data';
    
    import config from '../../config';
    
    import { getCurrentVariableMedianMetadata } from '../../selectors/median-statistics-selector';
    
    import { mediaQuery } from '../../utils';
    
    import {link} from '../../lib/d3-redux';
    
    const TS_LABEL = {
    
        'current': 'Current: ',
        'compare': 'Last year: ',
        'median': 'Median: '
    
    const tsMaskMarkers = function(tsKey, masks) {
    
        return Array.from(masks.values()).map((mask) => {
    
            const maskName = MASK_DESC[mask];
            const tsClass = `${maskName.replace(' ', '-').toLowerCase()}-mask`;
            const fill = `url(#${HASH_ID[tsKey]})`;
    
            return defineRectangleMarker(null, `mask ${tsClass}`, maskName, fill);
    
    const tsLineMarkers = function(tsKey, lineClasses) {
        let result = [];
    
        if (lineClasses.default) {
    
    Bucknell, Mary S.'s avatar
    Bucknell, Mary S. committed
            result.push(defineLineMarker(null, `line-segment ts-${tsKey}`, 'Provisional'));
    
        }
        if (lineClasses.approved) {
    
    Bucknell, Mary S.'s avatar
    Bucknell, Mary S. committed
            result.push(defineLineMarker(null, `line-segment approved ts-${tsKey}`, 'Approved'));
    
        }
        if (lineClasses.estimated) {
    
    Bucknell, Mary S.'s avatar
    Bucknell, Mary S. committed
            result.push(defineLineMarker(null, `line-segment estimated ts-${tsKey}`, 'Estimated'));
    
    /**
     * create elements for the legend in the svg
     *
     * @param {Object} displayItems - Object containing keys for each ts. The current and compare will contain an
     *                 object that has a masks property containing the Set of masks that are currently displayed.
     *                 The median property will contain the metadata for the median statistics
    
     * @return {Object} - Each key represents a ts and contains an array of markers to show.
    
     */
    const createLegendMarkers = function(displayItems) {
        const legendMarkers = [];
    
        if (displayItems.current) {
    
                ...tsLineMarkers('current', displayItems.current),
                ...tsMaskMarkers('current', displayItems.current.dataMasks)
    
            ];
            if (currentMarkers.length) {
                legendMarkers.push([
    
                    defineTextOnlyMarker(TS_LABEL.current, null, 'ts-legend-current-text'),
    
                ...tsLineMarkers('compare', displayItems.compare),
                ...tsMaskMarkers('compare', displayItems.compare.dataMasks)
    
            ];
            if (compareMarkers.length) {
                legendMarkers.push([
    
                    defineTextOnlyMarker(TS_LABEL.compare, null, 'ts-legend-compare-text'),
    
            const medians = Object.values(displayItems.median);
            for (let index = 0; index < medians.length; index++) {
                const stats = medians[index];
    
                // Get the unique non-null years, in chronological order
                const years = [];
                if (stats.beginYear) {
                    years.push(stats.beginYear);
                }
                if (stats.endYear && stats.beginYear !== stats.endYear) {
                    years.push(stats.endYear);
                }
                const dateText = years.join(' - ');
    
    
                const descriptionText = stats.methodDescription ? `${stats.methodDescription} ` : '';
    
    Yan, Andrew N.'s avatar
    Yan, Andrew N. committed
                const classes = `median-data-series median-step median-step-${index % 6}`;
    
                const label = `${descriptionText}${dateText}`;
    
                    defineTextOnlyMarker(TS_LABEL.median),
    
    Yan, Andrew N.'s avatar
    Yan, Andrew N. committed
                    defineLineMarker(null, classes, label)]);
    
    Yan, Andrew N.'s avatar
    Yan, Andrew N. committed
    /**
    
    Yan, Andrew N.'s avatar
    Yan, Andrew N. committed
     *
    
    Bucknell, Mary S.'s avatar
    Bucknell, Mary S. committed
     * @param {Object} div - d3 selector where legend should be created
    
     * @param {Object} legendMarkerRows - Array of rows. Each row should be an array of legend markers.
    
     * @param {Object} layout - width and height of svg.
    
    Yan, Andrew N.'s avatar
    Yan, Andrew N. committed
     */
    
    export const drawSimpleLegend = function(div, {legendMarkerRows, layout}) {
    
        div.selectAll('.legend-svg').remove();
    
        if (!legendMarkerRows.length || !layout) {
    
        const markerGroupXOffset = 15;
    
    Bucknell, Mary S.'s avatar
    Bucknell, Mary S. committed
            .attr('class', 'legend-svg');
    
    Yan, Andrew N.'s avatar
    Yan, Andrew N. committed
        let legend = svg
            .append('g')
    
                .attr('transform', `translate(${mediaQuery(config.USWDS_MEDIUM_SCREEN) ? layout.margin.left : 0}, 0)`);
    
    Yan, Andrew N.'s avatar
    Yan, Andrew N. committed
    
    
        legendMarkerRows.forEach((rowMarkers, rowIndex) => {
    
            let yPosition = verticalRowOffset * (rowIndex + 1);
    
                    text: marker.text,
                    domId: marker.domId,
                    domClass: marker.domClass,
    
                    r: marker.r,
    
    
                //console.log('UV: marker text:'+marker.text+ ' xPosition:'+xPosition+ ' yPosition:'+yPosition );
    
    
                let markerGroup = marker.type(legend, markerArgs);
                let markerGroupBBox;
    
                // Long story short, firefox is unable to get the bounding box if
                // the svg element isn't actually taking up space and visible. Folks on the
                // internet seem to have gotten around this by setting `visibility:hidden`
                // to hide things, but that would still mean the elements will take up space.
                // which we don't want. So, here's some error handling for getBBox failures.
                // This handling ends up not creating the legend, but that's okay because the
                // graph is being shown anyway. A more detailed discussion of this can be found at:
                // https://bugzilla.mozilla.org/show_bug.cgi?id=612118 and
                // https://stackoverflow.com/questions/28282295/getbbox-of-svg-when-hidden.
    
                    markerGroupBBox = markerGroup.node().getBBox();
                    xPosition = markerGroupBBox.x + markerGroupBBox.width + markerGroupXOffset;
    
    
                    //console.log('UV: markerGroupBBox.x:'+markerGroupBBox.x+ ' markerGroupBBox.width:'+markerGroupBBox.width+ ' markerGroupXOffset:'+markerGroupXOffset);
                    //console.log('UV: xPosition:'+xPosition);
    
    
    Yan, Andrew N.'s avatar
    Yan, Andrew N. committed
    
    
        // Set the size of the containing svg node to the size of the legend.
    
        let bBox;
        try {
            bBox = legend.node().getBBox();
        } catch(error) {
            return;
        }
    
        svg.attr('viewBox', `-${CIRCLE_RADIUS} 0 ${layout.width} ${bBox.height + 10}`);
    
    Yan, Andrew N.'s avatar
    Yan, Andrew N. committed
    
    
    const uniqueClassesSelector = memoize(tsKey => createSelector(
    
            let classes = [].concat(...Object.values(tsLineSegments)).map((line) => line.classes);
            return {
    
                default: classes.some((cls) => !cls.approved && !cls.estimated && !cls.dataMask),
                approved: classes.some((cls) => cls.approved),
                estimated: classes.some((cls) => cls.estimated),
    
                dataMasks: set(classes.map((cls) => cls.dataMask).filter((mask) => {
    
                    return mask;
                }))
            };
    
    /**
     * Select attributes from the state useful for legend creation
     */
    
    const legendDisplaySelector = createSelector(
    
        (state) => state.timeSeriesState.showSeries,
    
        uniqueClassesSelector('current'),
        uniqueClassesSelector('compare'),
    
        (showSeries, medianSeries, currentClasses, compareClasses) => {
    
                current: showSeries.current ? currentClasses : undefined,
                compare: showSeries.compare ? compareClasses : undefined,
    
                median: showSeries.median ? medianSeries : undefined
    
    /*
     * Factory function  that returns an array of array of markers to be used for the
    
     * time series graph legend
    
     * @return {Array of Array} of markers
     */
    export const legendMarkerRowsSelector = createSelector(
    
        legendDisplaySelector,
        displayItems => createLegendMarkers(displayItems)
    );
    
    
    export const drawTimeSeriesLegend = function(elem, store) {
    
        elem.append('div')
            .classed('hydrograph-container', true)
    
            .call(link(store, drawSimpleLegend, createStructuredSelector({
    
                legendMarkerRows: legendMarkerRowsSelector,