Newer
Older
// functions to facilitate legend creation for a d3 plot
Naab, Daniel James
committed
import { set } from 'd3-collection';
import memoize from 'fast-memoize';
import {createSelector, createStructuredSelector} from 'reselect';
Naab, Daniel James
committed
Bucknell, Mary S.
committed
import {CIRCLE_RADIUS, layoutSelector} from './layout';
import { defineLineMarker, defineTextOnlyMarker, defineRectangleMarker } from './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';
Bucknell, Mary S.
committed
import {link} from '../../lib/d3-redux';
Bucknell, Mary S.
committed
'current': 'Current: ',
'compare': 'Last year: ',
'median': 'Median: '
const tsMaskMarkers = function(tsKey, masks) {
Naab, Daniel James
committed
return Array.from(masks.values()).map((mask) => {
const maskName = MASK_DESC[mask];
const tsClass = `${maskName.replace(' ', '-').toLowerCase()}-mask`;
const fill = `url(#${HASH_ID[tsKey]})`;
Bucknell, Mary S.
committed
return defineRectangleMarker(null, `mask ${tsClass}`, maskName, fill);
});
};
const tsLineMarkers = function(tsKey, lineClasses) {
let result = [];
if (lineClasses.default) {
result.push(defineLineMarker(null, `line-segment ts-${tsKey}`, 'Provisional'));
result.push(defineLineMarker(null, `line-segment approved ts-${tsKey}`, 'Approved'));
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) {
Bucknell, Mary S.
committed
const currentMarkers = [
...tsLineMarkers('current', displayItems.current),
...tsMaskMarkers('current', displayItems.current.dataMasks)
Bucknell, Mary S.
committed
];
if (currentMarkers.length) {
legendMarkers.push([
defineTextOnlyMarker(TS_LABEL.current, null, 'ts-legend-current-text'),
Bucknell, Mary S.
committed
...currentMarkers
]);
}
}
if (displayItems.compare) {
Bucknell, Mary S.
committed
const compareMarkers = [
...tsLineMarkers('compare', displayItems.compare),
...tsMaskMarkers('compare', displayItems.compare.dataMasks)
Bucknell, Mary S.
committed
];
if (compareMarkers.length) {
legendMarkers.push([
defineTextOnlyMarker(TS_LABEL.compare, null, 'ts-legend-compare-text'),
Bucknell, Mary S.
committed
...compareMarkers
]);
}
}
if (displayItems.median) {
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(' - ');
Bucknell, Mary S.
committed
const descriptionText = stats.methodDescription ? `${stats.methodDescription} ` : '';
const classes = `median-data-series median-step median-step-${index % 6}`;
const label = `${descriptionText}${dateText}`;
Bucknell, Mary S.
committed
legendMarkers.push([
defineTextOnlyMarker(TS_LABEL.median),
}
}
return legendMarkers;
};
* Create a simple legend
* @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.
export const drawSimpleLegend = function(div, {legendMarkerRows, layout}) {
Bucknell, Mary S.
committed
div.selectAll('.legend-svg').remove();
if (!legendMarkerRows.length || !layout) {
return;
}
Bucknell, Mary S.
committed
const verticalRowOffset = 18;
Bucknell, Mary S.
committed
let svg = div.append('svg')
.attr('class', 'legend')
.attr('transform', `translate(${mediaQuery(config.USWDS_MEDIUM_SCREEN) ? layout.margin.left : 0}, 0)`);
Bucknell, Mary S.
committed
legendMarkerRows.forEach((rowMarkers, rowIndex) => {
let xPosition = 0;
Bucknell, Mary S.
committed
let yPosition = verticalRowOffset * (rowIndex + 1);
Bucknell, Mary S.
committed
rowMarkers.forEach((marker) => {
Bucknell, Mary S.
committed
let markerArgs = {
x: xPosition,
y: yPosition,
Bucknell, Mary S.
committed
text: marker.text,
domId: marker.domId,
domClass: marker.domClass,
Bucknell, Mary S.
committed
width: 20,
height: 10,
length: 20,
Bucknell, Mary S.
committed
r: marker.r ,
fill: marker.fill
Bucknell, Mary S.
committed
};
Bucknell, Mary S.
committed
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.
try {
Bucknell, Mary S.
committed
markerGroupBBox = markerGroup.node().getBBox();
xPosition = markerGroupBBox.x + markerGroupBBox.width + markerGroupXOffset;
} catch(error) {
// See above explanation
}
Bucknell, Mary S.
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}`);
const uniqueClassesSelector = memoize(tsKey => createSelector(
Bucknell, Mary S.
committed
currentVariableLineSegmentsSelector(tsKey),
Naab, Daniel James
committed
(tsLineSegments) => {
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),
Naab, Daniel James
committed
dataMasks: set(classes.map((cls) => cls.dataMask).filter((mask) => {
Bucknell, Mary S.
committed
/**
* Select attributes from the state useful for legend creation
*/
const legendDisplaySelector = createSelector(
(state) => state.timeSeriesState.showSeries,
Bucknell, Mary S.
committed
getCurrentVariableMedianMetadata,
uniqueClassesSelector('current'),
uniqueClassesSelector('compare'),
(showSeries, medianSeries, currentClasses, compareClasses) => {
return {
current: showSeries.current ? currentClasses : undefined,
compare: showSeries.compare ? compareClasses : undefined,
Bucknell, Mary S.
committed
median: showSeries.median ? medianSeries : undefined
/*
* Factory function that returns an array of array of markers to be used for the
* @return {Array of Array} of markers
*/
export const legendMarkerRowsSelector = createSelector(
legendDisplaySelector,
displayItems => createLegendMarkers(displayItems)
);
Bucknell, Mary S.
committed
export const drawTimeSeriesLegend = function(elem, store) {
elem.append('div')
.classed('hydrograph-container', true)
Bucknell, Mary S.
committed
.call(link(store, drawSimpleLegend, createStructuredSelector({
Bucknell, Mary S.
committed
layout: layoutSelector