-
Briggs, Aaron Shane authoredBriggs, Aaron Shane authored
date-controls.js 19.39 KiB
import {DateTime} from 'luxon';
import {createStructuredSelector} from 'reselect';
import {link} from 'ui/lib/d3-redux';
import {drawLoadingIndicator} from 'd3render/loading-indicator';
// required to make the USWDS component JS available to init after page load
import components from '../../../../../node_modules/uswds/src/js/components';
import {
isLoadingTS,
hasAnyTimeSeries,
getUserInputsForSelectingTimespan,
getCustomTimeRange,
getCurrentParmCd
} from 'ml/selectors/time-series-selector';
import {getIanaTimeZone} from 'ml/selectors/time-zone-selector';
import {Actions as ivTimeSeriesDataActions} from 'ml/store/instantaneous-value-time-series-data';
import {Actions as ivTimeSeriesStateActions} from 'ml/store/instantaneous-value-time-series-state';
import {MAX_DIGITS_FOR_DAYS_FROM_TODAY} from 'ivhydrograph/hydrograph-utils';
export const drawDateRangeControls = function(elem, store, siteno) {
const DATE_RANGE = [{
name: '7 days',
period: 'P7D'
}, {
name: '30 days',
period: 'P30D'
}, {
name: '1 year',
period: 'P1Y'
}, {
name: 'Custom',
period: 'custom',
ariaExpanded: false
}];
const CUSTOM_TIMEFRAME_RADIO_BUTTON_DETAILS = [
{
id: 'custom-input-days-before-today',
value: 'days',
text: 'Days before today',
checked: true,
ariaExpanded: false
},
{
id: 'custom-input-calender-days',
value: 'calender',
text: 'Calender days',
checked: false,
ariaExpanded: false
}
];
const containerRadioGroupMainSelectButtons = elem.insert('div', ':nth-child(2)')
.attr('id', 'ts-daterange-select-container')
.attr('role', 'radiogroup')
.attr('aria-label', 'Time interval select')
.call(link(store,function(container, showControls) {
container.attr('hidden', showControls ? null : true);
}, hasAnyTimeSeries));
// Add a container that holds the custom selection radio buttons and the form fields
const containerRadioGroupAndFormButtons = elem.insert('div', ':nth-child(3)')
.attr('class', 'container-radio-group-and-form-buttons')
.call(link(store, (container, userInputsForSelectingTimespan) => {
container.attr('hidden', userInputsForSelectingTimespan.mainTimeRangeSelectionButton === 'custom' ? null : true);
}, getUserInputsForSelectingTimespan));
const containerRadioGroupCustomSelectButtons = containerRadioGroupAndFormButtons.append('div')
.attr('id', 'ts-custom-date-radio-group')
.attr('role', 'radiogroup')
.attr('aria-label', 'Custom time interval select');
const containerCustomDaysBeforeToday = containerRadioGroupAndFormButtons.append('div')
.attr('id', 'ts-custom-days-before-today-select-container')
.attr('class', 'usa-form')
.attr('aria-label', 'Custom date by days before today specification')
.call(link(store, (container, userInputsForSelectingTimespan) => {
container.attr('hidden', userInputsForSelectingTimespan.customTimeRangeSelectionButton === 'days-input' && userInputsForSelectingTimespan.mainTimeRangeSelectionButton === 'custom' ? null : true);
}, getUserInputsForSelectingTimespan));
const containerCustomCalenderDays = containerRadioGroupAndFormButtons.append('div')
.attr('id', 'ts-customdaterange-select-container')
.attr('role', 'customdate')
.attr('class', 'usa-form')
.attr('aria-label', 'Custom date specification')
.call(link(store, (container, userInputsForSelectingTimespan) => {
container.attr('hidden', userInputsForSelectingTimespan.customTimeRangeSelectionButton === 'calender-input' && userInputsForSelectingTimespan.mainTimeRangeSelectionButton === 'custom' ? null : true);
}, getUserInputsForSelectingTimespan));
const createRadioButtonsForCustomDaterangeSelection = function(containerRadioGroupCustomSelectButtons) {
containerRadioGroupCustomSelectButtons.append('p').text('Enter timespan using');
const listContainerForCustomSelectRadioButtons = containerRadioGroupCustomSelectButtons.append('ul')
.attr('class', 'usa-fieldset usa-list--unstyled');
const listItemForCustomSelectRadioButtons = listContainerForCustomSelectRadioButtons.selectAll('li')
.attr('class', 'usa-fieldset')
.data(CUSTOM_TIMEFRAME_RADIO_BUTTON_DETAILS)
.enter()
.append('li');
listItemForCustomSelectRadioButtons.append('input')
.attr('type', 'radio')
.attr('name', 'ts-custom-daterange-input')
.attr('id', d => `${d.value}-input`)
.attr('class', 'usa-radio__input')
.attr('value', d => d.value)
.property('checked', d => d.checked)
.attr('ga-on', 'click')
.attr('aria-expanded', d => d.ariaExpanded)
.attr('ga-event-category', 'TimeSeriesGraph')
.attr('ga-event-action', d => `changeDateRangeWith${d.value}`)
.on('change', function() {
const selected = listItemForCustomSelectRadioButtons.select('input:checked');
const selectedVal = selected.attr('id');
store.dispatch(ivTimeSeriesStateActions.setUserInputsForSelectingTimespan('customTimeRangeSelectionButton', selectedVal));
});
listItemForCustomSelectRadioButtons.append('label')
.attr('class', 'usa-radio__label')
.attr('for', (d) => `${d.value}-input`)
.text((d) => d.text);
listItemForCustomSelectRadioButtons.call(link(store, (elem, userInputsForSelectingTimespan) => {
store.dispatch(ivTimeSeriesStateActions.setCustomIVTimeRange(null));
elem.select(`#${userInputsForSelectingTimespan.customTimeRangeSelectionButton}`).property('checked', true);
}, getUserInputsForSelectingTimespan));
};
const createControlsForSelectingTimeSpanInDaysFromToday = function() {
const numberOfDaysSelection = containerCustomDaysBeforeToday.append('div')
.attr('class', 'usa-character-count')
.append('div')
.attr('class', 'usa-form-group');
numberOfDaysSelection.append('label')
.attr('class', 'usa-label')
.attr('for', 'with-hint-input-days-from-today' )
.text('Days');
numberOfDaysSelection.append('span')
.attr('id', 'with-hint-input-days-from-today-hint')
.attr('class', 'usa-hint')
.text('Timespan in days before today');
numberOfDaysSelection.append('input')
.attr('class', 'usa-input usa-character-count__field')
.attr('id', 'with-hint-input-days-from-today')
.attr('maxlength', `${MAX_DIGITS_FOR_DAYS_FROM_TODAY}`)
.attr('name', 'with-hint-input-days-from-today')
.attr('aria-describedby', 'with-hint-input-days-from-today-info with-hint-input-days-from-today-hint');
numberOfDaysSelection.append('span')
.text(`${MAX_DIGITS_FOR_DAYS_FROM_TODAY} digits allowed`)
.attr('id', 'with-hint-input-days-from-today-info')
.attr('class', 'usa-hint usa-character-count__message')
.attr('aria-live', 'polite');
// Create a validation alert for user selection of number of days before today
const customDaysBeforeTodayValidationContainer = containerCustomDaysBeforeToday.append('div')
.attr('class', 'usa-alert usa-alert--warning usa-alert--validation')
.attr('id', 'custom-days-before-today-alert-container')
.attr('hidden', true);
const customDaysBeforeTodayAlertBody = customDaysBeforeTodayValidationContainer.append('div')
.attr('class', 'usa-alert__body')
.attr('id', 'custom-days-before-today-alert');
customDaysBeforeTodayAlertBody.append('h3')
.attr('class', 'usa-alert__heading')
.text('Requirements');
numberOfDaysSelection.call(link(store, (container, userInputsForSelectingTimespan) => {
if (userInputsForSelectingTimespan.mainTimeRangeSelectionButton === 'custom' && userInputsForSelectingTimespan.customTimeRangeSelectionButton === 'days-input') {
container.select('#with-hint-input-days-from-today')
.property('value', userInputsForSelectingTimespan.numberOfDaysFieldValue);
}
}, getUserInputsForSelectingTimespan));
// Adds controls for the 'days before today' submit button
const daysBeforeTodaySubmitContainer = containerCustomDaysBeforeToday.append('div')
.attr('class', 'submit-button');
daysBeforeTodaySubmitContainer.append('button')
.attr('class', 'usa-button')
.attr('id', 'custom-date-submit-days')
.text('Display data on graph')
.on('click', function() {
const userSpecifiedNumberOfDays = document.getElementById('with-hint-input-days-from-today').value;
const formattedPeriodQueryParameter = `P${parseInt(userSpecifiedNumberOfDays)}D`;
// Validate user input for things not a number and blank entries
if (isNaN(userSpecifiedNumberOfDays) || userSpecifiedNumberOfDays.length === 0) {
customDaysBeforeTodayAlertBody.selectAll('p').remove();
customDaysBeforeTodayAlertBody.append('p')
.text('Entry must be a number.');
customDaysBeforeTodayValidationContainer.attr('hidden', null);
} else {
customDaysBeforeTodayValidationContainer.attr('hidden', true);
const parameterCode = getCurrentParmCd(store.getState());
store.dispatch(ivTimeSeriesStateActions.setUserInputsForSelectingTimespan('numberOfDaysFieldValue', userSpecifiedNumberOfDays));
store.dispatch(ivTimeSeriesDataActions.retrieveCustomTimePeriodIVTimeSeries(
siteno,
parameterCode,
formattedPeriodQueryParameter
)).then(() => store.dispatch(ivTimeSeriesStateActions.clearIVGraphBrushOffset()));
}
});
};
const createControlsForDateRangePicker = function() {
const dateRangePicker = containerCustomCalenderDays.append('div')
.attr('class', 'usa-date-range-picker');
const customDateValidationContainer = containerCustomCalenderDays.append('div')
.attr('class', 'usa-alert usa-alert--warning usa-alert--validation')
.attr('id', 'custom-date-alert-container')
.attr('hidden', true);
const dateAlertBody = customDateValidationContainer.append('div')
.attr('class', 'usa-alert__body')
.attr('id', 'custom-date-alert');
dateAlertBody.append('h3')
.attr('class', 'usa-alert__heading')
.text('Date requirements');
const startDateFormGroup = dateRangePicker.append('div')
.attr('id', 'start-date-form-group')
.attr('class', 'usa-form-group');
const endDateFormGroup = dateRangePicker.append('div')
.attr('id', 'end-date-form-group')
.attr('class', 'usa-form-group');
startDateFormGroup.append('label')
.attr('class', 'usa-label')
.attr('id', 'custom-start-date-label')
.attr('for', 'custom-start-date')
.text('Start Date');
startDateFormGroup.append('div')
.attr('class', 'usa-hint')
.attr('id', 'custom-start-date-hint')
.text('mm/dd/yyyy')
.append('div')
.attr('class', 'usa-date-picker')
.attr('data-min-date', '1900-01-01')
.attr('data-max-date', '2100-12-31')
.append('input')
.attr('class', 'usa-input')
.attr('id', 'custom-start-date')
.attr('name', 'custom-start-date')
.attr('aria-describedby', 'custom-start-date-label custom-start-date-hint')
.attr('type', 'text');
endDateFormGroup.append('label')
.attr('class', 'usa-label')
.attr('id', 'custom-end-date-label')
.attr('for', 'custom-end-date')
.text('End Date');
endDateFormGroup.append('div')
.attr('class', 'usa-hint')
.attr('id', 'custom-end-date-hint')
.text('mm/dd/yyyy')
.append('div')
.attr('class', 'usa-date-picker')
.attr('data-min-date', '1900-01-01')
.attr('data-max-date', '2100-12-31')
.append('input')
.attr('class', 'usa-input')
.attr('id', 'custom-end-date')
.attr('name', 'custom-end-date')
.attr('type', 'text')
.attr('aria-describedby', 'custom-end-date-label custom-end-date-hint');
// required to init the USWDS date picker after page load
components.datePicker.init(elem.node());
// required to init the USWDS date range picker after page load
components.dateRangePicker.init(elem.node());
// Adds controls for the calender day submit button
const calenderDaysSubmitContainer = containerCustomCalenderDays.append('div')
.attr('class', 'submit-button');
calenderDaysSubmitContainer.append('button')
.attr('class', 'usa-button')
.attr('id', 'custom-date-submit-calender')
.text('Display data on graph')
.on('click', function() {
let userSpecifiedStart = document.getElementById('custom-start-date').value;
let userSpecifiedEnd = document.getElementById('custom-end-date').value;
if (userSpecifiedStart.length === 0 || userSpecifiedEnd.length === 0) {
dateAlertBody.selectAll('p').remove();
dateAlertBody.append('p')
.text('Both start and end dates must be specified.');
customDateValidationContainer.attr('hidden', null);
} else if (DateTime.fromFormat(userSpecifiedEnd, 'LL/dd/yyyy') < DateTime.fromFormat(userSpecifiedStart, 'LL/dd/yyyy')) {
dateAlertBody.selectAll('p').remove();
dateAlertBody.append('p')
.text('The start date must precede the end date.');
customDateValidationContainer.attr('hidden', null);
} else {
customDateValidationContainer.attr('hidden', true);
userSpecifiedStart = DateTime.fromFormat(userSpecifiedStart, 'LL/dd/yyyy').toISODate();
userSpecifiedEnd = DateTime.fromFormat(userSpecifiedEnd, 'LL/dd/yyyy').toISODate();
store.dispatch(ivTimeSeriesDataActions.retrieveUserRequestedIVDataForDateRange(
siteno,
userSpecifiedStart,
userSpecifiedEnd
)).then(() => store.dispatch(ivTimeSeriesStateActions.clearIVGraphBrushOffset()));
}
});
containerCustomCalenderDays.call(link(store, (container, {customTimeRange, ianaTimeZone}) => {
container.select('#custom-start-date')
.property('value', customTimeRange && customTimeRange.start ? DateTime.fromMillis(customTimeRange.start, {zone: ianaTimeZone}).startOf('day').toFormat('LL/dd/yyyy') : '');
container.select('#custom-end-date')
.property('value', customTimeRange && customTimeRange.end ? DateTime.fromMillis(customTimeRange.end, {zone: ianaTimeZone}).toFormat('LL/dd/yyyy') : '');
}, createStructuredSelector({
customTimeRange: getCustomTimeRange,
ianaTimeZone: getIanaTimeZone
})));
};
const createRadioButtonsForPrimaryTimeframes = function() {
const listContainer = containerRadioGroupMainSelectButtons.append('ul')
.attr('class', 'usa-fieldset usa-list--unstyled');
const li = listContainer.selectAll('li')
.attr('class', 'usa-fieldset')
.data(DATE_RANGE)
.enter().append('li');
listContainer.call(link(store, drawLoadingIndicator, createStructuredSelector({
showLoadingIndicator: isLoadingTS('current'),
sizeClass: () => 'fa-lg'
})));
li.append('input')
.attr('type', 'radio')
.attr('name', 'ts-daterange-input')
.attr('id', d => `${d.period}-input`)
.attr('class', 'usa-radio__input')
.attr('value', d => d.period)
.attr('ga-on', 'click')
.attr('aria-expanded', d => d.ariaExpanded)
.attr('ga-event-category', 'TimeSeriesGraph')
.attr('ga-event-action', d => `changeDateRangeTo${d.period}`)
.on('change', function() {
const selected = li.select('input:checked');
const selectedVal = selected.attr('value');
// Remove any values stored in the form, because they may not match what is shown in the graph until the submit button is pushed
store.dispatch(ivTimeSeriesStateActions.setUserInputsForSelectingTimespan('numberOfDaysFieldValue', ''));
store.dispatch(ivTimeSeriesStateActions.setCustomIVTimeRange(null));
if (selectedVal === 'custom') {
selected.attr('aria-expanded', true);
containerRadioGroupCustomSelectButtons.attr('hidden', null);
containerCustomDaysBeforeToday.attr('hidden', null);
containerCustomCalenderDays.attr('hidden', true);
store.dispatch(ivTimeSeriesStateActions.setUserInputsForSelectingTimespan('mainTimeRangeSelectionButton', 'custom'));
} else {
const userInputTimeframeButtonSelected = li.select('input:checked').attr('value');
li.select('input#custom-date-range').attr('aria-expanded', false);
containerRadioGroupCustomSelectButtons.attr('hidden', true);
containerCustomDaysBeforeToday.attr('hidden', true);
containerCustomCalenderDays.attr('hidden', true);
store.dispatch(ivTimeSeriesStateActions.setUserInputsForSelectingTimespan('mainTimeRangeSelectionButton', userInputTimeframeButtonSelected));
store.dispatch(ivTimeSeriesDataActions.retrieveExtendedIVTimeSeries(
siteno,
userInputTimeframeButtonSelected
)).then(() => {
store.dispatch(ivTimeSeriesStateActions.clearIVGraphBrushOffset());
});
}
});
li.append('label')
.attr('class', 'usa-radio__label')
.attr('for', (d) => `${d.period}-input`)
.text((d) => d.name);
li.call(link(store, (elem, userInputsForSelectingTimespan) => {
store.dispatch(ivTimeSeriesStateActions.setCustomIVTimeRange(null));
elem.select(`#${userInputsForSelectingTimespan.mainTimeRangeSelectionButton}-input`).property('checked', true);
}, getUserInputsForSelectingTimespan));
};
createRadioButtonsForCustomDaterangeSelection(containerRadioGroupCustomSelectButtons);
createControlsForSelectingTimeSpanInDaysFromToday();
createControlsForDateRangePicker();
createRadioButtonsForPrimaryTimeframes();
};