Skip to content
Snippets Groups Projects
legend.js 6.86 KiB
Newer Older
// functions to facilitate legend creation for a d3 plot
const { createSelector } = require('reselect');
const { defineLineMarker, defineCircleMarker, defineRectangleMarker, rectangleMarker } = require('./markers');
const { CIRCLE_RADIUS } = require('./layout');
const { MASK_DESC } = require('./timeseries');
Yan, Andrew N.'s avatar
Yan, Andrew N. committed
/**
 * Create a simple horizontal legend
 *
 * @param svg
 * @param legendMarkers
Yan, Andrew N.'s avatar
Yan, Andrew N. committed
 * @param startingXPosition
 * @param markerYPosition
 * @param textYPosition
 * @param markerGroupOffset
 * @param markerTextOffset
 */
function drawSimpleLegend(svg,
                          legendMarkers,
Yan, Andrew N.'s avatar
Yan, Andrew N. committed
                          startingXPosition=0,
                          markerYPosition=-4,
                          textYPosition=0,
                          markerGroupOffset=40,
                          markerTextOffset=10) {
    const verticalRowOffset = 20;
    const svgWidth = layout.width ? layout.width : svgBBox.width;
    let rowCounter = 0;

Yan, Andrew N.'s avatar
Yan, Andrew N. committed
    let legend = svg
        .append('g')
        .attr('class', 'legend');

    let previousMarkerGroup;

    for (let legendMarker of legendMarkers) {
        let xPosition;
        let detachedMarker;
        if (previousMarkerGroup == null) {
            xPosition = startingXPosition;
            let previousMarkerGroupBox = previousMarkerGroup.node().getBBox();
Yan, Andrew N.'s avatar
Yan, Andrew N. committed
            xPosition = previousMarkerGroupBox.x + previousMarkerGroupBox.width + markerGroupOffset;
        }
        let legendGroup = legend.append('g')
            .attr('class', 'legend-marker');
        if (legendMarker.groupId) {
            legendGroup.attr('id', legendMarker.groupId);
        }
        let markerType = legendMarker.type;
        let yPosition;
        if (markerType === rectangleMarker) {
            yPosition = markerYPosition * 2.5 + verticalRowOffset * rowCounter;
        } else {
            yPosition = markerYPosition + verticalRowOffset * rowCounter;
Yan, Andrew N.'s avatar
Yan, Andrew N. committed
        let markerArgs = {
            r: legendMarker.r ? legendMarker.r : null,
            x: xPosition,
            y: yPosition,
            width: 20,
            height: 10,
Yan, Andrew N.'s avatar
Yan, Andrew N. committed
            length: 20,
            domId: legendMarker.domId,
            domClass: legendMarker.domClass,
            fill: legendMarker.fill
Yan, Andrew N.'s avatar
Yan, Andrew N. committed
        };
        // add the marker to the svg
        detachedMarker = markerType(markerArgs);
        legendGroup.node().appendChild(detachedMarker.node());
        // add text for the legend marker
        let detachedMarkerBBox = detachedMarker.node().getBBox();
        legendGroup.append('text')
            .attr('x', detachedMarkerBBox.x + detachedMarkerBBox.width + markerTextOffset)
            .attr('y', textYPosition + verticalRowOffset * rowCounter)
Yan, Andrew N.'s avatar
Yan, Andrew N. committed
            .text(legendMarker.text);
        let legendGroupBBox = legendGroup.node().getBBox();
        let legendGroupRightXCoordinate = legendGroupBBox.x + legendGroupBBox.width;
        if (legendGroupRightXCoordinate/layout.width >= 0.60) {
            rowCounter += 1;
            previousMarkerGroup = null;
            previousMarkerGroup = legendGroup;
        }
Yan, Andrew N.'s avatar
Yan, Andrew N. committed
    }
    // center the legend group in the svg
    let legendBBox = legend.node().getBBox();
    const legendXPosition = (layout.width - legendBBox.width) / 2;
    legend.attr('transform', `translate(${legendXPosition}, ${layout.height - 30})`);
Yan, Andrew N.'s avatar
Yan, Andrew N. committed
}

/**
 * create elements for the legend in the svg
 *
 * @param dataPlotElements
 * @param lineSegments
Yan, Andrew N.'s avatar
Yan, Andrew N. committed
const createLegendMarkers = function(dataPlotElements, lineSegments=[]) {
    let text;
    let marker;
    let legendMarkers = [];
    // create legend markers for data series
    for (let dataItem of dataPlotElements.dataItems) {
        if (dataItem === 'compare' || dataItem === 'current') {
            let svgGroup = `${dataItem}-line-marker`;
            if (dataItem === 'compare') {
Yan, Andrew N.'s avatar
Yan, Andrew N. committed
                hashMarker = defineRectangleMarker(null, 'mask', 'Compare Timeseries Mask', null, 'url(#hash-135)');
                text = 'Last Year';
Yan, Andrew N.'s avatar
Yan, Andrew N. committed
                hashMarker = defineRectangleMarker(null, 'mask', 'Current Timeseries Mask', null, 'url(#hash-45)');
                text = 'Current Year';
            }
            marker = defineLineMarker(domId, 'line', text, svgGroup);
        } else if (dataItem === 'medianStatistics') {
            text = 'Median';
            if (dataPlotElements.metadata.statistics.description) {
                text = `${text} ${dataPlotElements.metadata.statistics.description}`;
            }
            let beginYear = dataPlotElements.metadata.statistics.beginYear;
            let endYear = dataPlotElements.metadata.statistics.endYear;
            if (beginYear && endYear) {
                text = `${text} ${beginYear} - ${endYear}`;
            }
            marker = defineCircleMarker(CIRCLE_RADIUS, null, 'median-data-series', text, 'median-circle-marker');
            marker = null;
        }
        if (marker) {
            legendMarkers.push(marker);
        }
        if (hashMarker) {
            legendMarkers.push(hashMarker);
        }
    // create markers for data masks for different components of data series
    let masks = [];
    lineSegments.map(segment => masks.push(segment.classes.dataMask));
    let uniqueMasks = new Set(masks.filter(x => x !== null));
    for (let uniqueMask of uniqueMasks) {
        let maskDisplayName = MASK_DESC[uniqueMask];
        let maskClass = `mask ${maskDisplayName.replace(' ', '-').toLowerCase()}-mask`;
        marker = defineRectangleMarker(null, maskClass, maskDisplayName, null);
        legendMarkers.push(marker);
    }
    return legendMarkers;
};
Yan, Andrew N.'s avatar
Yan, Andrew N. committed

/**
 * Select attributes from the state useful for legend creation
 */
const legendDisplaySelector = createSelector(
    (state) => state.showSeries,
    (state) => state.tsData,
    (state) => state.currentParameterCode,
    (showSeries, tsData, currentParameterCode) => {
        const medianTS = tsData.medianStatistics[currentParameterCode] || {};
        const statisticalMetaData = medianTS.medianMetadata || {};
        let shownSeries = [];
        let dataPlotElements = {};
        for (let key in showSeries) {
            if (showSeries.hasOwnProperty(key)) {
Yan, Andrew N.'s avatar
Yan, Andrew N. committed
                if (showSeries[key]) {
                    shownSeries.push(key);

        dataPlotElements.dataItems = shownSeries;
        dataPlotElements.metadata = {
            statistics: {
                beginYear: statisticalMetaData.beginYear ? statisticalMetaData.beginYear : undefined,
                endYear: statisticalMetaData.endYear ? statisticalMetaData.endYear : undefined,
                description: medianTS.description || ''
        };
        return dataPlotElements;
module.exports = {drawSimpleLegend, createLegendMarkers, legendDisplaySelector};