Skip to content
Snippets Groups Projects
legend.js 8.06 KiB
Newer Older
// functions to facilitate legend creation for a d3 plot
import memoize from 'fast-memoize';
Bucknell, Mary S.'s avatar
Bucknell, Mary S. committed

import { createSelector } from 'reselect';
import { CIRCLE_RADIUS } from './layout';
import { defineLineMarker, defineTextOnlyMarker, defineRectangleMarker } from './markers';
import { currentVariableLineSegmentsSelector, HASH_ID, MASK_DESC } from './drawingData';
import config from '../../config';
import { getMethods } from '../../selectors/timeSeriesSelector';
import { getCurrentVariableMedianMetadata } from '../../selectors/medianStatisticsSelector';
import { mediaQuery } from '../../utils';
const TS_LABEL = {
    'current': 'Current: ',
    'compare': 'Last year: ',
    'median': 'Median: '
const tsMaskMarkers = function(tsKey, masks) {
    return Array.from(masks.entries()).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 respresnts 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,
            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;

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: new 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, methods, 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)
);