Newer
Older
const { createSelector } = require('reselect');
const { line } = require('d3-shape');
const { select } = require('d3-selection');
const { allTimeSeriesSelector } = require('./timeseries');
Bucknell, Mary S.
committed
const { Actions } = require('../../store');
const { sortedParameters } = require('../../models');
const { SPARK_LINE_DIM, CIRCLE_RADIUS_SINGLE_PT } = require('./layout');
const { dispatch } = require('../../lib/redux');
const { MASK_DESC } = require('./drawingData');
Bucknell, Mary S.
committed
/**
* Returns metadata for each available timeseries.
* @param {Object} state Redux state
* @return {Array} Sorted array of [code, metadata] pairs.
*/
export const availableTimeseriesSelector = createSelector(
Naab, Daniel James
committed
state => state.series.variables,
allTimeSeriesSelector,
Naab, Daniel James
committed
state => state.currentVariableID,
(variables, timeSeries, currentVariableID) => {
if (!variables) {
return [];
}
Naab, Daniel James
committed
const seriesList = Object.values(timeSeries);
const timeSeriesVariables = seriesList.map(x => x.variable);
const sortedVariables = sortedParameters(variables).map(x => x.oid);
for (const variableID of sortedVariables) {
// start the next iteration if a variable is not a
// series returned by the allTimeSeriesSelector
if (!timeSeriesVariables.includes(variableID)) {
continue;
}
Naab, Daniel James
committed
const variable = variables[variableID];
Naab, Daniel James
committed
const currentTimeseriesCount = seriesList.filter(ts => ts.tsKey === 'current' && ts.variable === variableID).length;
if (currentTimeseriesCount > 0) {
let varCodes = {
variableID: variable.oid,
description: variable.variableDescription,
selected: currentVariableID === variableID,
currentTimeseriesCount: currentTimeseriesCount
};
sorted.push([variable.variableCode.value, varCodes]);
}
}
return sorted;
}
);
/**
* Draw a sparkline in a selected SVG element
*
* @param svgSelection
* @param tsData
*/
export const addSparkLine = function(svgSelection, {seriesLineSegments, scales}) {
let spark = line()
.x(function(d) {
return scales.x(d.dateTime);
})
.y(function(d) {
return scales.y(d.value);
});
const seriesDataMasks = seriesLineSegments.map(x => x.classes.dataMask);
if (seriesDataMasks.includes(null)) {
for (const lineSegment of seriesLineSegments) {
if (lineSegment.classes.dataMask === null) {
if (lineSegment.points.length === 1) {
svgSelection.append('circle')
.data(lineSegment.points)
.classed('spark-point', true)
.attr('r', CIRCLE_RADIUS_SINGLE_PT/2)
.attr('cx', d => scales.x(d.dateTime))
.attr('cy', d => scales.y(d.value));
} else {
svgSelection.append('path')
.attr('d', spark(lineSegment.points))
.classed('spark-line', true);
}
const centerElement = function (svgElement) {
const elementWidth = svgElement.node().getBoundingClientRect().width;
const xLocation = (SPARK_LINE_DIM.width - elementWidth) / 2;
svgElement.attr('x', xLocation);
};
let svgText = svgSelection.append('text')
.attr('x', 0)
.attr('y', 0)
.classed('sparkline-text', true);
const maskDescs = seriesDataMasks.map(x => MASK_DESC[x.toLowerCase()]);
const maskDesc = maskDescs.length === 1 ? maskDescs[0] : 'Masked';
const maskDescWords = maskDesc.split(' ');
if (maskDescWords.length > 1) {
Array.from(maskDescWords.entries()).forEach(x => {
const yPosition = 15 + x[0]*12;
const maskText = x[1];
let tspan = svgText.append('tspan')
.attr('x', 0)
.attr('y', yPosition)
centerElement(svgText);
centerElement(tspan);
} else {
svgText.text(maskDesc)
.attr('y', '20');
/**
* Draws a table with clickable rows of timeseries parameter codes. Selecting
* a row changes the active parameter code.
* @param {Object} elem d3 selection
* @param {Object} availableTimeseries Timeseries metadata to display
* @param {Object} lineSegmentsByParmCd line segments for each parameter code
* @param {Object} timeSeriesScalesByParmCd scales for each parameter code
*/
export const plotSeriesSelectTable = function (elem, {availableTimeseries, lineSegmentsByParmCd, timeSeriesScalesByParmCd}) {
Bucknell, Mary S.
committed
// Get the position of the scrolled window before removing it so it can be set to the same value.
const lastTable = elem.select('#select-timeseries table');
const scrollTop = lastTable.size() ? lastTable.property('scrollTop') : null;
elem.select('#select-timeseries').remove();
if (!availableTimeseries.length) {
return;
}
const columnHeaders = ['Parameter', 'Preview', '#'];
Bucknell, Mary S.
committed
const tableContainer = elem.append('div')
.attr('id', 'select-timeseries');
tableContainer.append('label')
.attr('id', 'select-timeseries-label')
Bucknell, Mary S.
committed
.text('Select a timeseries');
const table = tableContainer.append('table')
.classed('usa-table-borderless', true)
.attr('aria-labelledby', 'select-timeseries-label')
Bucknell, Mary S.
committed
.attr('tabindex', 0)
.attr('role', 'listbox');
table.append('thead')
.append('tr')
.selectAll('th')
.enter().append('th')
.attr('scope', 'col')
.text(d => d);
.selectAll('tr')
.enter().append('tr')
Bucknell, Mary S.
committed
.attr('ga-on', 'click')
.attr('ga-event-category', 'selectTimeSeries')
.attr('ga-event-action', (parm) => `timeseries-parmcd-${parm[0]}`)
.attr('role', 'option')
.classed('selected', parm => parm[1].selected)
.attr('aria-selected', parm => parm[1].selected)
.on('click', dispatch(function (parm) {
if (!parm[1].selected) {
Naab, Daniel James
committed
return Actions.setCurrentParameterCode(parm[0], parm[1].variableID);
}
}))
.call(tr => {
let parmCdCol = tr.append('th')
.attr('scope', 'row');
parmCdCol.append('span')
.text(parm => parm[1].description);
let tooltip = parmCdCol.append('div')
.attr('class', 'tooltip-item');
tooltip.append('span')
.attr('class', 'fa fa-info-circle');
tooltip.append('div')
.attr('class', 'tooltip parameter-tooltip')
Bucknell, Mary S.
committed
.append('p')
.text(parm => `Parameter code: ${parm[0]}`);
.attr('width', SPARK_LINE_DIM.width.toString())
.attr('height', SPARK_LINE_DIM.height.toString());
.text(parm => parm[1].currentTimeseriesCount);
});
Bucknell, Mary S.
committed
table.property('scrollTop', scrollTop);
table.selectAll('tbody svg').each(function(d) {
const lineSegments = lineSegmentsByParmCd[parmCd] ? lineSegmentsByParmCd[parmCd] : [];
for (const seriesLineSegments of lineSegments) {
selection.call(addSparkLine, {
seriesLineSegments: seriesLineSegments,
scales: timeSeriesScalesByParmCd[parmCd]
});
}