diff --git a/src/client/app/components/BarControlsComponent.tsx b/src/client/app/components/BarControlsComponent.tsx index 03e858d11..58f1b4fca 100644 --- a/src/client/app/components/BarControlsComponent.tsx +++ b/src/client/app/components/BarControlsComponent.tsx @@ -2,147 +2,38 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import * as moment from 'moment'; import * as React from 'react'; -import { FormattedMessage } from 'react-intl'; -import { FormFeedback, FormGroup, Input, Label } from 'reactstrap'; import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; -import { graphSlice, selectBarStacking, selectBarWidthDays } from '../redux/slices/graphSlice'; +import { graphSlice, selectBarStacking } from '../redux/slices/graphSlice'; import translate from '../utils/translate'; import TooltipMarkerComponent from './TooltipMarkerComponent'; +import IntervalControlsComponent from './IntervalControlsComponent'; /** - * @returns controls for the Options Ui page. + * @returns controls for bar page. */ export default function BarControlsComponent() { const dispatch = useAppDispatch(); - // The min/max days allowed for user selection - const MIN_BAR_DAYS = 1; - const MAX_BAR_DAYS = 366; - // Special value if custom input for standard menu. - const CUSTOM_INPUT = '-99'; - - // This is the current bar interval for graphic. - const barDuration = useAppSelector(selectBarWidthDays); const barStacking = useAppSelector(selectBarStacking); - // Holds the value of standard bar duration choices used so decoupled from custom. - const [barDays, setBarDays] = React.useState(barDuration.asDays().toString()); - // Holds the value during custom bar duration input so only update graphic when done entering and - // separate from standard choices. - const [barDaysCustom, setBarDaysCustom] = React.useState(barDuration.asDays()); - // True if custom bar duration input is active. - const [showCustomBarDuration, setShowCustomBarDuration] = React.useState(false); const handleChangeBarStacking = () => { dispatch(graphSlice.actions.changeBarStacking()); }; - // Keeps react-level state, and redux state in sync. - // Two different layers in state may differ especially when externally updated (chart link, history buttons.) - React.useEffect(() => { - // Assume value is valid since it is coming from state. - // Do not allow bad values in state. - const isCustom = !(['1', '7', '28'].find(days => days == barDuration.asDays().toString())); - setShowCustomBarDuration(isCustom); - setBarDaysCustom(barDuration.asDays()); - setBarDays(isCustom ? CUSTOM_INPUT : barDuration.asDays().toString()); - }, [barDuration]); - - // Returns true if this is a valid bar duration. - const barDaysValid = (barDays: number) => { - return Number.isInteger(barDays) && barDays >= MIN_BAR_DAYS && barDays <= MAX_BAR_DAYS; - }; - - // Updates values when the standard bar duration menu is used. - const handleBarDaysChange = (value: string) => { - if (value === CUSTOM_INPUT) { - // Set menu value for standard bar to special value to show custom - // and show the custom input area. - setBarDays(CUSTOM_INPUT); - setShowCustomBarDuration(true); - } else { - // Set the standard menu value, hide the custom bar duration input - // and bar duration for graphing. - // Since controlled values know it is a valid integer. - setShowCustomBarDuration(false); - updateBarDurationChange(Number(value)); - } - }; - - // Updates value when the custom bar duration input is used. - const handleCustomBarDaysChange = (value: number) => { - setBarDaysCustom(value); - }; - - const handleEnter = (key: string) => { - // This detects the enter key and then uses the previously entered custom - // bar duration to set the bar duration for the graphic. - if (key == 'Enter') { - updateBarDurationChange(barDaysCustom); - } - }; - - const updateBarDurationChange = (value: number) => { - // Update if okay value. May not be okay if this came from user entry in custom form. - if (barDaysValid(value)) { - dispatch(graphSlice.actions.updateBarDuration(moment.duration(value, 'days'))); - } - }; - return (
-
+
-
-

- {translate('bar.interval')}: - -

- handleBarDaysChange(e.target.value)} - > - - - - - - {/* This has a little more spacing at bottom than optimal. */} - {showCustomBarDuration && - - - handleCustomBarDaysChange(Number(e.target.value))} - // This grabs each key hit and then finishes input when hit enter. - onKeyDown={e => { handleEnter(e.key); }} - step='1' - min={MIN_BAR_DAYS} - max={MAX_BAR_DAYS} - value={barDaysCustom} - invalid={!barDaysValid(barDaysCustom)} /> - - - - - } -
+ {}
); } const divTopBottomPadding: React.CSSProperties = { - paddingTop: '15px', + paddingTop: '0px', paddingBottom: '15px' }; - -const labelStyle: React.CSSProperties = { - fontWeight: 'bold', - margin: 0 -}; diff --git a/src/client/app/components/CompareControlsComponent.tsx b/src/client/app/components/CompareControlsComponent.tsx index d990852cd..7a22d2cf3 100644 --- a/src/client/app/components/CompareControlsComponent.tsx +++ b/src/client/app/components/CompareControlsComponent.tsx @@ -1,89 +1,59 @@ /* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +* License, v. 2.0. If a copy of the MPL was not distributed with this +* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import * as moment from 'moment'; import * as React from 'react'; -import { Button, ButtonGroup, Dropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap'; -import { graphSlice, selectComparePeriod, selectSortingOrder } from '../redux/slices/graphSlice'; +import { Input } from 'reactstrap'; import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; -import { ComparePeriod, SortingOrder } from '../utils/calculateCompare'; +import { graphSlice, selectSortingOrder } from '../redux/slices/graphSlice'; +import { SortingOrder } from '../utils/calculateCompare'; import translate from '../utils/translate'; import TooltipMarkerComponent from './TooltipMarkerComponent'; +import IntervalControlsComponent from './IntervalControlsComponent'; /** - * @returns controls for the compare page + * @returns controls for compare page. */ export default function CompareControlsComponent() { const dispatch = useAppDispatch(); - const comparePeriod = useAppSelector(selectComparePeriod); + + // This is the current sorting order for graphic const compareSortingOrder = useAppSelector(selectSortingOrder); - const [compareSortingDropdownOpen, setCompareSortingDropdownOpen] = React.useState(false); - const handleCompareButton = (comparePeriod: ComparePeriod) => { - dispatch(graphSlice.actions.updateComparePeriod({ comparePeriod, currentTime: moment() })); - }; - const handleSortingButton = (sortingOrder: SortingOrder) => { + + // Updates sorting order when the sort order menu is used. + const handleSortingChange = (value: string) => { + const sortingOrder = value as unknown as SortingOrder; dispatch(graphSlice.actions.changeCompareSortingOrder(sortingOrder)); }; return (
- - - - - - - setCompareSortingDropdownOpen(current => !current)}> - - {translate('sort')} - - - - handleSortingButton(SortingOrder.Alphabetical)} - > - {translate('alphabetically')} - - handleSortingButton(SortingOrder.Ascending)} - > - {translate('ascending')} - - handleSortingButton(SortingOrder.Descending)} - > - {translate('descending')} - - - -
+ + + + +
+ ); } -const zIndexFix: React.CSSProperties = { - zIndex: 0 -}; \ No newline at end of file +const divTopBottomPadding: React.CSSProperties = { + paddingTop: '0px', + paddingBottom: '15px' +}; + +const labelStyle: React.CSSProperties = { + fontWeight: 'bold', + margin: 0 +}; diff --git a/src/client/app/components/IntervalControlsComponent.tsx b/src/client/app/components/IntervalControlsComponent.tsx new file mode 100644 index 000000000..eea3cd0a6 --- /dev/null +++ b/src/client/app/components/IntervalControlsComponent.tsx @@ -0,0 +1,182 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import * as moment from 'moment'; +import * as React from 'react'; +import { FormattedMessage } from 'react-intl'; +import { FormFeedback, FormGroup, Input, Label } from 'reactstrap'; +import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; +import { selectChartToRender, graphSlice, selectWidthDays, selectComparePeriod } from '../redux/slices/graphSlice'; +import { ChartTypes } from '../types/redux/graph'; +import { ComparePeriod } from '../utils/calculateCompare'; +import translate from '../utils/translate'; +import TooltipMarkerComponent from './TooltipMarkerComponent'; + +/** + * @returns Interval controls for the bar, map, and compare pages + */ +export default function IntervalControlsComponent() { + const dispatch = useAppDispatch(); + const chartType = useAppSelector(selectChartToRender); + + // The min/max days allowed for user selection + const MIN_DAYS = 1; + const MAX_DAYS = 366; + // Special value if custom input for standard menu. + const CUSTOM_INPUT = '-99'; + + // This is the current interval for the bar and map graphics. + const duration = useAppSelector(selectWidthDays); + // This is the current compare period for graphic + const comparePeriod = chartType === ChartTypes.compare ? useAppSelector(selectComparePeriod) : undefined; + + // Holds the value of standard duration choices used for bar and map, decoupled from custom. + const [days, setDays] = React.useState(duration.asDays().toString()); + // Holds the value during custom duration input for bar and map, so only update graphic + // when done entering and separate from standard choices. + const [daysCustom, setDaysCustom] = React.useState(duration.asDays()); + // True if custom duration input for bar or map is active. + const [showCustomDuration, setShowCustomDuration] = React.useState(false); + // Define a flag to track if custom input is actively being used + const [isCustomInput, setIsCustomInput] = React.useState(false); + + // Keeps react-level state, and redux state in sync. + // Two different layers in state may differ especially when externally updated (chart link, history buttons.) + React.useEffect(() => { + // If user is in custom input mode, don't reset to standard options + if (!isCustomInput) { + const durationValues = Object.values(ComparePeriod) as string[]; + const isCustom = !(durationValues.includes(duration.asDays().toString())); + setShowCustomDuration(isCustom); + setDaysCustom(duration.asDays()); + setDays(isCustom ? CUSTOM_INPUT : duration.asDays().toString()); + } + }, [duration, isCustomInput]); + + // Returns true if this is a valid duration. + const daysValid = (days: number) => { + return Number.isInteger(days) && days >= MIN_DAYS && days <= MAX_DAYS; + }; + + // Updates values when the standard duration menu for bar or map is used. + const handleDaysChange = (value: string) => { + setIsCustomInput(false); + if (value === CUSTOM_INPUT) { + // Set menu value from standard value to special value to show custom + // and show the custom input area. + setShowCustomDuration(true); + setDays(CUSTOM_INPUT); + } else { + // Set the standard menu value, hide the custom duration input + // and duration for graphing. + // Since controlled values know it is a valid integer. + setShowCustomDuration(false); + updateDurationChange(Number(value)); + } + }; + + // Updates value when the custom duration input is used for bar or map. + const handleCustomDaysChange = (value: number) => { + setIsCustomInput(true); + setDaysCustom(value); + }; + + const handleEnter = (key: string) => { + // This detects the enter key and then uses the previously entered custom + // duration to set the duration for the graphic. + if (key === 'Enter') { + updateDurationChange(daysCustom); + } + }; + + const updateDurationChange = (value: number) => { + // Update if okay value. May not be okay if this came from user entry in custom form. + if (daysValid(value)) { + dispatch(graphSlice.actions.updateDuration(moment.duration(value, 'days'))); + } + }; + + // Handles change for compare period dropdown + const handleComparePeriodChange = (value: string) => { + const period = value as unknown as ComparePeriod; + dispatch(graphSlice.actions.updateComparePeriod({ comparePeriod: period, currentTime: moment() })); + }; + + const comparePeriodTranslations: Record = { + Day: 'day', + Week: 'week', + FourWeeks: '4.weeks' + }; + + return ( +
+
+

+ {translate( + chartType === ChartTypes.bar ? 'bar.interval' : + chartType === ChartTypes.map ? 'map.interval' : + 'compare.period' + )}: + +

+ chartType === ChartTypes.compare ? handleComparePeriodChange(e.target.value) : handleDaysChange(e.target.value)} + > + {Object.entries(ComparePeriod).map( + ([key, value]) => ( + + ) + )} + {/* TODO: Compare is currently not ready for the custom option. */} + {chartType !== ChartTypes.compare && + + } + + {showCustomDuration && chartType !== ChartTypes.compare && + + + handleCustomDaysChange(Number(e.target.value))} + // This grabs each key hit and then finishes input when hit enter. + onKeyDown={e => handleEnter(e.key)} + step='1' + min={MIN_DAYS} + max={MAX_DAYS} + value={daysCustom} + invalid={!daysValid(daysCustom)} + /> + + + + + } +
+
+ ); +} + +const divTopBottomPadding: React.CSSProperties = { + paddingTop: '0px', + paddingBottom: '15px' +}; + +const labelStyle: React.CSSProperties = { + fontWeight: 'bold', + margin: 0 +}; diff --git a/src/client/app/components/MapChartComponent.tsx b/src/client/app/components/MapChartComponent.tsx index cb0502e85..3acd46dd0 100644 --- a/src/client/app/components/MapChartComponent.tsx +++ b/src/client/app/components/MapChartComponent.tsx @@ -8,7 +8,7 @@ import * as React from 'react'; import Plot from 'react-plotly.js'; import { useSelector } from 'react-redux'; import { - selectAreaUnit, selectBarWidthDays, + selectAreaUnit, selectWidthDays, selectGraphAreaNormalization, selectSelectedGroups, selectSelectedMeters, selectSelectedUnit } from '../redux/slices/graphSlice'; @@ -47,7 +47,7 @@ export default function MapChartComponent() { // converting maps to RTK has been proving troublesome, therefore using a combination of old/new stateSelectors const unitID = useAppSelector(selectSelectedUnit); - const barDuration = useAppSelector(selectBarWidthDays); + const mapDuration = useAppSelector(selectWidthDays); const areaNormalization = useAppSelector(selectGraphAreaNormalization); const selectedAreaUnit = useAppSelector(selectAreaUnit); const selectedMeters = useAppSelector(selectSelectedMeters); @@ -94,7 +94,7 @@ export default function MapChartComponent() { const y: number[] = []; // const timeInterval = state.graph.queryTimeInterval; - // const barDuration = state.graph.barDuration + // const mapDuration = state.graph.mapDuration // Make sure there is a map with values so avoid issues. if (map && map.origin && map.opposite) { // The size of the original map loaded into OED. @@ -166,10 +166,10 @@ export default function MapChartComponent() { // The x, y value for Plotly to use that are on the user map. x.push(meterGPSInUserGrid.x); y.push(meterGPSInUserGrid.y); - // Make sure the bar reading data is available. The timeInterval should be fine (but checked) but the barDuration might have changed + // Make sure the bar reading data is available. The timeInterval should be fine (but checked) but the mapDuration might have changed // and be fetching. The unit could change from that menu so also need to check. // Get the bar data to use for the map circle. - // const readingsData = meterReadings[timeInterval.toString()][barDuration.toISOString()][unitID]; + // const readingsData = meterReadings[timeInterval.toString()][mapDuration.toISOString()][unitID]; const readingsData = meterReadings[meterID]; // This protects against there being no readings or that the data is being updated. if (readingsData !== undefined && !meterIsFetching) { @@ -197,13 +197,13 @@ export default function MapChartComponent() { // only display a range of dates for the hover text if there is more than one day in the range // Shift to UTC since want database time not local/browser time which is what moment does. timeReading = `${moment.utc(mapReading.startTimestamp).format('ll')}`; - if (barDuration.asDays() != 1) { + if (mapDuration.asDays() != 1) { // subtracting one extra day caused by day ending at midnight of the next day. // Going from DB unit timestamp that is UTC so force UTC with moment, as usual. timeReading += ` - ${moment.utc(mapReading.endTimestamp).subtract(1, 'days').format('ll')}`; } // The value for the circle is the average daily usage. - averagedReading = mapReading.reading / barDuration.asDays(); + averagedReading = mapReading.reading / mapDuration.asDays(); if (areaNormalization) { averagedReading /= meterArea; } @@ -241,7 +241,7 @@ export default function MapChartComponent() { // The x, y value for Plotly to use that are on the user map. x.push(groupGPSInUserGrid.x); y.push(groupGPSInUserGrid.y); - // Make sure the bar reading data is available. The timeInterval should be fine (but checked) but the barDuration might have changed + // Make sure the bar reading data is available. The timeInterval should be fine (but checked) but the mapDuration might have changed // and be fetching. The unit could change from that menu so also need to check. // Get the bar data to use for the map circle. const readingsData = groupData[groupID]; @@ -270,13 +270,13 @@ export default function MapChartComponent() { } else { // only display a range of dates for the hover text if there is more than one day in the range timeReading = `${moment.utc(mapReading.startTimestamp).format('ll')}`; - if (barDuration.asDays() != 1) { + if (mapDuration.asDays() != 1) { // subtracting one extra day caused by day ending at midnight of the next day. // Going from DB unit timestamp that is UTC so force UTC with moment, as usual. timeReading += ` - ${moment.utc(mapReading.endTimestamp).subtract(1, 'days').format('ll')}`; } // The value for the circle is the average daily usage. - averagedReading = mapReading.reading / barDuration.asDays(); + averagedReading = mapReading.reading / mapDuration.asDays(); if (areaNormalization) { averagedReading /= groupArea; } diff --git a/src/client/app/components/MapControlsComponent.tsx b/src/client/app/components/MapControlsComponent.tsx index ac0934d4e..536cc0744 100644 --- a/src/client/app/components/MapControlsComponent.tsx +++ b/src/client/app/components/MapControlsComponent.tsx @@ -1,53 +1,19 @@ /* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +* License, v. 2.0. If a copy of the MPL was not distributed with this +* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -import * as moment from 'moment'; import * as React from 'react'; -import { Button, ButtonGroup } from 'reactstrap'; -import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; -import { selectMapBarWidthDays, updateMapsBarDuration } from '../redux/slices/graphSlice'; -import translate from '../utils/translate'; +import IntervalControlsComponent from './IntervalControlsComponent'; import MapChartSelectComponent from './MapChartSelectComponent'; -import TooltipMarkerComponent from './TooltipMarkerComponent'; + /** - * @returns Map page controls + * @returns controls for map page. */ export default function MapControlsComponent() { - const dispatch = useAppDispatch(); - const barDuration = useAppSelector(selectMapBarWidthDays); - - const handleDurationChange = (value: number) => { - dispatch(updateMapsBarDuration(moment.duration(value, 'days'))); - }; - - const barDurationDays = barDuration.asDays(); - return (
- -
-

- {translate('map.interval')}: -

- - - - - - -
-
+ {} + {} + ); } - - -const labelStyle: React.CSSProperties = { - fontWeight: 'bold', - margin: 0 -}; - -const zIndexFix: React.CSSProperties = { - zIndex: 0 -}; diff --git a/src/client/app/components/TooltipHelpComponent.tsx b/src/client/app/components/TooltipHelpComponent.tsx index 10a56d79a..fbefa1ea0 100644 --- a/src/client/app/components/TooltipHelpComponent.tsx +++ b/src/client/app/components/TooltipHelpComponent.tsx @@ -54,7 +54,7 @@ export default function TooltipHelpComponent(props: TooltipHelpProps) { 'help.home.chart.plotly.controls': { link: 'https://plotly.com/chart-studio-help/getting-to-know-the-plotly-modebar/' }, 'help.home.chart.redraw.restore': { link: `${helpUrl}/lineGraphic/#redrawRestore` }, 'help.home.chart.select': { link: `${helpUrl}/graphType/` }, - 'help.home.compare.interval.tip': { link: `${helpUrl}/compareGraphic/#usage` }, + 'help.home.compare.period.tip': { link: `${helpUrl}/compareGraphic/#usage` }, 'help.home.compare.sort.tip': { link: `${helpUrl}/compareGraphic/#usage` }, 'help.home.error.bar': { link: `${helpUrl}/errorBar/#usage` }, 'help.home.export.graph.data': { link: `${helpUrl}/export/` }, diff --git a/src/client/app/containers/MapChartContainer.ts b/src/client/app/containers/MapChartContainer.ts index 45a8049c6..ca241ae24 100644 --- a/src/client/app/containers/MapChartContainer.ts +++ b/src/client/app/containers/MapChartContainer.ts @@ -55,7 +55,7 @@ function mapStateToProps(state: State) { // Figure out what time interval the bar is using since user bar data for now. const timeInterval = state.graph.queryTimeInterval; - const barDuration = state.graph.barDuration; + const mapDuration = state.graph.duration; // Make sure there is a map with values so avoid issues. if (map && map.origin && map.opposite) { // The size of the original map loaded into OED. @@ -129,11 +129,11 @@ function mapStateToProps(state: State) { // The x, y value for Plotly to use that are on the user map. x.push(meterGPSInUserGrid.x); y.push(meterGPSInUserGrid.y); - // Make sure the bar reading data is available. The timeInterval should be fine (but checked) but the barDuration might have changed + // Make sure the bar reading data is available. The timeInterval should be fine (but checked) but the mapDuration might have changed // and be fetching. The unit could change from that menu so also need to check. - if (byMeterID[timeInterval.toString()] !== undefined && byMeterID[timeInterval.toString()][barDuration.toISOString()] !== undefined) { + if (byMeterID[timeInterval.toString()] !== undefined && byMeterID[timeInterval.toString()][mapDuration.toISOString()] !== undefined) { // Get the bar data to use for the map circle. - const readingsData = byMeterID[timeInterval.toString()][barDuration.toISOString()][unitID]; + const readingsData = byMeterID[timeInterval.toString()][mapDuration.toISOString()][unitID]; // This protects against there being no readings or that the data is being updated. if (readingsData !== undefined && !readingsData.isFetching) { // Meter name to include in hover on graph. @@ -160,13 +160,13 @@ function mapStateToProps(state: State) { // only display a range of dates for the hover text if there is more than one day in the range // Shift to UTC since want database time not local/browser time which is what moment does. timeReading = `${moment.utc(mapReading.startTimestamp).format('ll')}`; - if (barDuration.asDays() != 1) { + if (mapDuration.asDays() != 1) { // subtracting one extra day caused by day ending at midnight of the next day. // Going from DB unit timestamp that is UTC so force UTC with moment, as usual. timeReading += ` - ${moment.utc(mapReading.endTimestamp).subtract(1, 'days').format('ll')}`; } // The value for the circle is the average daily usage. - averagedReading = mapReading.reading / barDuration.asDays(); + averagedReading = mapReading.reading / mapDuration.asDays(); if (state.graph.areaNormalization) { averagedReading /= meterArea; } @@ -206,11 +206,11 @@ function mapStateToProps(state: State) { // The x, y value for Plotly to use that are on the user map. x.push(groupGPSInUserGrid.x); y.push(groupGPSInUserGrid.y); - // Make sure the bar reading data is available. The timeInterval should be fine (but checked) but the barDuration might have changed + // Make sure the bar reading data is available. The timeInterval should be fine (but checked) but the mapDuration might have changed // and be fetching. The unit could change from that menu so also need to check. - if (byGroupID[timeInterval.toString()] !== undefined && byGroupID[timeInterval.toString()][barDuration.toISOString()] !== undefined) { + if (byGroupID[timeInterval.toString()] !== undefined && byGroupID[timeInterval.toString()][mapDuration.toISOString()] !== undefined) { // Get the bar data to use for the map circle. - const readingsData = byGroupID[timeInterval.toString()][barDuration.toISOString()][unitID]; + const readingsData = byGroupID[timeInterval.toString()][mapDuration.toISOString()][unitID]; // This protects against there being no readings or that the data is being updated. if (readingsData !== undefined && !readingsData.isFetching) { // Group name to include in hover on graph. @@ -236,13 +236,13 @@ function mapStateToProps(state: State) { } else { // only display a range of dates for the hover text if there is more than one day in the range timeReading = `${moment.utc(mapReading.startTimestamp).format('ll')}`; - if (barDuration.asDays() != 1) { + if (mapDuration.asDays() != 1) { // subtracting one extra day caused by day ending at midnight of the next day. // Going from DB unit timestamp that is UTC so force UTC with moment, as usual. timeReading += ` - ${moment.utc(mapReading.endTimestamp).subtract(1, 'days').format('ll')}`; } // The value for the circle is the average daily usage. - averagedReading = mapReading.reading / barDuration.asDays(); + averagedReading = mapReading.reading / mapDuration.asDays(); if (state.graph.areaNormalization) { averagedReading /= groupArea; } diff --git a/src/client/app/redux/selectors/barChartSelectors.ts b/src/client/app/redux/selectors/barChartSelectors.ts index 9e2e067ff..ea60ad0a4 100644 --- a/src/client/app/redux/selectors/barChartSelectors.ts +++ b/src/client/app/redux/selectors/barChartSelectors.ts @@ -5,7 +5,7 @@ import { createSelector } from '@reduxjs/toolkit'; import * as moment from 'moment'; import { BarReadings } from 'types/readings'; -import { selectBarWidthDays } from '../../redux/slices/graphSlice'; +import { selectWidthDays } from '../../redux/slices/graphSlice'; import { DataType } from '../../types/Datasources'; import { MeterOrGroup } from '../../types/redux/graph'; import getGraphColor from '../../utils/getGraphColor'; @@ -18,7 +18,7 @@ export const selectPlotlyBarDeps = createAppSelector( [ selectPlotlyMeterDeps, selectPlotlyGroupDeps, - selectBarWidthDays + selectWidthDays ], (meterDeps, groupDeps, barDuration) => { const barMeterDeps = { ...meterDeps, barDuration }; diff --git a/src/client/app/redux/selectors/chartQuerySelectors.ts b/src/client/app/redux/selectors/chartQuerySelectors.ts index b0253fcc4..73e68216b 100644 --- a/src/client/app/redux/selectors/chartQuerySelectors.ts +++ b/src/client/app/redux/selectors/chartQuerySelectors.ts @@ -8,8 +8,8 @@ import { MeterOrGroup, ReadingInterval } from '../../types/redux/graph'; import { calculateCompareShift } from '../../utils/calculateCompare'; import { roundTimeIntervalForFetch } from '../../utils/dateRangeCompatibility'; import { - selectBarWidthDays, selectComparePeriod, - selectCompareTimeInterval, selectMapBarWidthDays, selectQueryTimeInterval, + selectWidthDays, selectComparePeriod, + selectCompareTimeInterval, selectQueryTimeInterval, selectSelectedGroups, selectSelectedMeters, selectSelectedUnit, selectThreeDState } from '../slices/graphSlice'; @@ -91,7 +91,7 @@ export const selectRadarChartQueryArgs = createSelector( export const selectBarChartQueryArgs = createSelector( selectCommonQueryArgs, - selectBarWidthDays, + selectWidthDays, (common, barWidthDays) => { // QueryArguments to pass into the bar chart component const barWidthAsDays = Math.round(barWidthDays.asDays()); @@ -139,7 +139,7 @@ export const selectCompareChartQueryArgs = createSelector( export const selectMapChartQueryArgs = createSelector( selectBarChartQueryArgs, - selectMapBarWidthDays, + selectWidthDays, (state: RootState) => state.maps, (barChartArgs, barWidthDays, maps) => { const durationDays = Math.round(barWidthDays.asDays()); diff --git a/src/client/app/redux/selectors/uiSelectors.ts b/src/client/app/redux/selectors/uiSelectors.ts index 6baecc5db..dc62fa245 100644 --- a/src/client/app/redux/selectors/uiSelectors.ts +++ b/src/client/app/redux/selectors/uiSelectors.ts @@ -463,7 +463,7 @@ export const selectChartLink = createAppSelector( linkText += `&serverRange=${current.queryTimeInterval.toString()}`; switch (current.chartToRender) { case ChartTypes.bar: - linkText += `&barDuration=${current.barDuration.asDays()}`; + linkText += `&duration=${current.duration.asDays()}`; linkText += `&barStacking=${current.barStacking}`; break; case ChartTypes.line: diff --git a/src/client/app/redux/slices/graphSlice.ts b/src/client/app/redux/slices/graphSlice.ts index 1114225e4..91e38edd2 100644 --- a/src/client/app/redux/slices/graphSlice.ts +++ b/src/client/app/redux/slices/graphSlice.ts @@ -25,8 +25,7 @@ const defaultState: GraphState = { selectedAreaUnit: AreaUnitType.none, queryTimeInterval: TimeInterval.unbounded(), rangeSliderInterval: TimeInterval.unbounded(), - barDuration: moment.duration(4, 'weeks'), - mapsBarDuration: moment.duration(4, 'weeks'), + duration: moment.duration(4, 'weeks'), comparePeriod: ComparePeriod.Week, compareTimeInterval: calculateCompareTimeInterval(ComparePeriod.Week, moment()), compareSortingOrder: SortingOrder.Descending, @@ -79,11 +78,8 @@ export const graphSlice = createSlice({ updateSelectedAreaUnit: (state, action: PayloadAction) => { state.current.selectedAreaUnit = action.payload; }, - updateBarDuration: (state, action: PayloadAction) => { - state.current.barDuration = action.payload; - }, - updateMapsBarDuration: (state, action: PayloadAction) => { - state.current.mapsBarDuration = action.payload; + updateDuration: (state, action: PayloadAction) => { + state.current.duration = action.payload; }, updateTimeInterval: (state, action: PayloadAction) => { // always update if action is bounded, else only set unbounded if current isn't already unbounded. @@ -298,8 +294,8 @@ export const graphSlice = createSlice({ case 'areaUnit': current.selectedAreaUnit = value as AreaUnitType; break; - case 'barDuration': - current.barDuration = moment.duration(parseInt(value), 'days'); + case 'duration': + current.duration = moment.duration(parseInt(value), 'days'); break; case 'barStacking': current.barStacking = value === 'true'; @@ -372,8 +368,7 @@ export const graphSlice = createSlice({ selectThreeDState: state => state.current.threeD, selectShowMinMax: state => state.current.showMinMax, selectBarStacking: state => state.current.barStacking, - selectBarWidthDays: state => state.current.barDuration, - selectMapBarWidthDays: state => state.current.mapsBarDuration, + selectWidthDays: state => state.current.duration, selectAreaUnit: state => state.current.selectedAreaUnit, selectSelectedUnit: state => state.current.selectedUnit, selectChartToRender: state => state.current.chartToRender, @@ -401,7 +396,7 @@ export const { selectAreaUnit, selectShowMinMax, selectGraphState, selectPrevHistory, selectThreeDState, selectBarStacking, - selectSortingOrder, selectBarWidthDays, + selectSortingOrder, selectWidthDays, selectSelectedUnit, selectLineGraphRate, selectComparePeriod, selectChartToRender, selectForwardHistory, selectSelectedMeters, @@ -410,8 +405,7 @@ export const { selectThreeDMeterOrGroupID, selectThreeDReadingInterval, selectGraphAreaNormalization, selectSliderRangeInterval, selectDefaultGraphState, selectHistoryIsDirty, - selectPlotlySliderMax, selectPlotlySliderMin, - selectMapBarWidthDays + selectPlotlySliderMax, selectPlotlySliderMin } = graphSlice.selectors; // actionCreators exports @@ -419,7 +413,7 @@ export const { setShowMinMax, setGraphState, setBarStacking, toggleShowMinMax, changeBarStacking, resetTimeInterval, - updateBarDuration, changeSliderRange, + updateDuration, changeSliderRange, updateTimeInterval, updateSelectedUnit, changeChartToRender, updateComparePeriod, updateSelectedMeters, updateLineGraphRate, @@ -428,6 +422,6 @@ export const { toggleAreaNormalization, updateThreeDMeterOrGroup, changeCompareSortingOrder, updateThreeDMeterOrGroupID, updateThreeDReadingInterval, updateThreeDMeterOrGroupInfo, - updateSelectedMetersOrGroups, updateMapsBarDuration + updateSelectedMetersOrGroups } = graphSlice.actions; diff --git a/src/client/app/translations/data.ts b/src/client/app/translations/data.ts index 740311d8c..4b0b634a1 100644 --- a/src/client/app/translations/data.ts +++ b/src/client/app/translations/data.ts @@ -29,7 +29,6 @@ const LocaleTranslationData = { "as.meter.unit": "as meter unit", "as.meter.defaultgraphicunit": "as meter default graphic unit", "bar": "Bar", - "bar.days.enter": "Enter in days and then hit enter", "bar.interval": "Bar Interval", "bar.raw": "Cannot create bar graph on raw units such as temperature", "bar.stacking": "Bar Stacking", @@ -49,6 +48,7 @@ const LocaleTranslationData = { "clipboard.not.copied": "Failed to Copy To Clipboard", "close": "Close", "compare": "Compare", + "compare.period": "Compare Period", "compare.raw": "Cannot create comparison graph on raw units such as temperature", "confirm.action": "Confirm Action", "contact.us": "Contact us", @@ -115,6 +115,7 @@ const LocaleTranslationData = { "date.range": 'Date Range', "day": "Day", "days": "Days", + "days.enter": "Enter in days and then hit enter", "decreasing": "decreasing", "default.area.normalize": "Normalize readings by area by default", "default.area.unit": "Default Area Unit", @@ -241,7 +242,7 @@ const LocaleTranslationData = { "help.home.chart.plotly.controls": "These controls are provided by Plotly, the graphics package used by OED. You generally do not need them but they are provided in case you want that level of control. Note that some of these options may not interact nicely with OED features. See Plotly documentation at {link}.", "help.home.chart.redraw.restore": "OED automatically averages data when necessary so the graphs have a reasonable number of points. If you use the controls under the graph to scroll and/or zoom, you may find the resolution at this averaged level is not what you desire. Clicking the \"Redraw\" button will have OED recalculate the averaging and bring in higher resolution for the number of points it displays. If you want to restore the graph to the full range of dates, then click the \"Restore\" button. Please visit {link} for further details and information.", "help.home.chart.select": "Any graph type can be used with any combination of groups and meters. Line graphs show the usage (e.g., kW) vs. time. You can zoom and scroll with the controls right below the graph. Bar shows the total usage (e.g., kWh) for the time frame of each bar where you can control the time frame. Compare allows you to see the current usage vs. the usage in the last previous period for a day, week and four weeks. Map graphs show a spatial image of each meter where the circle size is related to four weeks of usage. 3D graphs show usage vs. day vs. hours in the day. Clicking on one of the choices renders that graphic. Please visit {link} for further details and information.", - "help.home.compare.interval.tip": "Selects the time interval (Day, Week or 4 Weeks) to compare for current to previous. Please see {link} for further details and information.", + "help.home.compare.period.tip": "Selects the time interval (Day, Week or 4 Weeks) to compare for current to previous. Please see {link} for further details and information.", "help.home.compare.sort.tip": "Allows user to select the order of multiple comparison graphs to be Alphabetical (by name), Ascending (greatest to least reduction in usage) and Descending (least to greatest reduction in usage). Please see {link} for further details and information.", "help.home.error.bar": "Toggle error bars with min and max value. Please visit {link} for further details and information.", "help.home.export.graph.data": "With the \"Export graph data\" button, one can export the data for the graph when viewing either a line or bar graphic. The zoom and scroll feature on the line graph allows you to control the time frame of the data exported. The \"Export graph data\" button gives the data points for the graph and not the original meter data. The \"Export graph meter data\" gives the underlying meter data (line graphs only). Please visit {link} for further details and information on when meter data export is allowed.", @@ -416,7 +417,7 @@ const LocaleTranslationData = { "show.options": "Show options", "site.settings": "Site Settings", "site.title": "Site Title", - "sort": "Sort", + "sort": "Sort Order", "submit": "Submit", "submitting": "submitting", "submit.changes": "Submit changes", @@ -532,7 +533,6 @@ const LocaleTranslationData = { "as.meter.unit": "as meter unit\u{26A1}", "as.meter.defaultgraphicunit": "as meter default graphic unit\u{26A1}", "bar": "Bande", - "bar.days.enter": "Enter in days and then hit enter\u{26A1}", "bar.interval": "Intervalle du Diagramme à Bandes", "bar.raw": "Cannot create bar graph on raw units such as temperature\u{26A1}", "bar.stacking": "Empilage de Bandes", @@ -552,6 +552,7 @@ const LocaleTranslationData = { "clipboard.not.copied": "Failed to Copy To Clipboard\u{26A1}", "close": "Close\u{26A1}", "compare": "Comparer", + "compare.period": "Compare Period\u{26A1}", "compare.raw": "Cannot create comparison graph on raw units such as temperature\u{26A1}", "confirm.action": "Confirm Action\u{26A1}", "contact.us": "Contactez nous", @@ -616,6 +617,7 @@ const LocaleTranslationData = { "date.range": 'Plage de dates', "day": "Journée", "days": "Journées", + "days.enter": "Enter in days and then hit enter\u{26A1}", "decreasing": "decreasing\u{26A1}", "default.area.normalize": "Normalize readings by area by default\u{26A1}", "default.area.unit": "Default Area Unit\u{26A1}", @@ -742,7 +744,7 @@ const LocaleTranslationData = { "help.home.chart.plotly.controls": "These controls are provided by Plotly, the graphics package used by OED. You generally do not need them but they are provided in case you want that level of control. Note that some of these options may not interact nicely with OED features. See Plotly documentation at {link}.\u{26A1}", "help.home.chart.redraw.restore": "OED automatically averages data when necessary so the graphs have a reasonable number of points. If you use the controls under the graph to scroll and/or zoom, you may find the resolution at this averaged level is not what you desire. Clicking the \"Redraw\" button will have OED recalculate the averaging and bring in higher resolution for the number of points it displays. If you want to restore the graph to the full range of dates, then click the \"Restore\" button. Please visit {link} for further details and information.\u{26A1}", "help.home.chart.select": "for the time frame of each bar where you can control the time frame. Compare allows you to see the current usage vs. the usage in the last previous period for a day, week and four weeks. Map graphs show a spatial image of each meter where the circle size is related to four weeks of usage. 3D graphs show usage vs. day vs. hours in the day. Clicking on one of the choices renders that graphic. Please visit {link} for further details and information.\u{26A1}", - "help.home.compare.interval.tip": "to compare for current to previous. Please see {link} for further details and information.\u{26A1}", + "help.home.compare.period.tip": "to compare for current to previous. Please see {link} for further details and information.\u{26A1}", "help.home.compare.sort.tip": "and Descending (least to greatest reduction in usage). Please see {link} for further details and information.\u{26A1}", "help.home.error.bar": "Toggle error bars with min and max value. Please visit {link} for further details and information.\u{26A1}", "help.home.export.graph.data": "With the \"Export graph data\" button, one can export the data for the graph when viewing either a line or bar graphic. The zoom and scroll feature on the line graph allows you to control the time frame of the data exported. The \"Export graph data\" button gives the data points for the graph and not the original meter data. The \"Export graph meter data\" gives the underlying meter data (line graphs only). Please visit {link} for further details and information on when meter data export is allowed.\u{26A1}", @@ -917,7 +919,7 @@ const LocaleTranslationData = { "show.options": "Options de désancrage", "site.settings": "Site Settings\u{26A1}", "site.title": "Site Title\u{26A1}", - "sort": "Trier", + "sort": "Sort Order\u{26A1}", "submit": "Soumettre", "submitting": "submitting\u{26A1}", "submit.changes": "Soumettre les changements", @@ -1033,7 +1035,6 @@ const LocaleTranslationData = { "as.meter.unit": "como unidad de medidor", "as.meter.defaultgraphicunit": "como unidad gráfica predeterminada del medidor", "bar": "Barra", - "bar.days.enter": "Ingrese los días y presione \"Enter\"", "bar.interval": "Intervalo de barra", "bar.raw": "No se puede crear un gráfico de barras con unidades crudas como la temperatura", "bar.stacking": "Apilamiento de barras", @@ -1053,6 +1054,7 @@ const LocaleTranslationData = { "clipboard.not.copied": "Error al copiar al portapapeles", "close": "Cerrar", "compare": "Comparar", + "compare.period": "Compare Period\u{26A1}", "compare.raw": "No se puede crear un gráfico de comparación con unidades crudas como temperatura", "confirm.action": "Confirmar acción", "contact.us": "Contáctenos", @@ -1117,6 +1119,7 @@ const LocaleTranslationData = { "date.range": 'Rango de fechas', "day": "Día", "days": "Días", + "days.enter": "Ingrese los días y presione \"Enter\"", "decreasing": "decreciente", "default.area.normalize": "Normalizar lecturas según el área por defecto", "default.area.unit": "Unidad de área predeterminada", @@ -1244,7 +1247,7 @@ const LocaleTranslationData = { "help.home.chart.plotly.controls": "Estos controles son proporcionados por Plotly, el paquete de gráficos utilizado por OED. Por lo general no se necesitan pero se proporcionan por si se desea ese nivel de control. Tenga en cuenta que es posible que algunas de estas opciones no interactúen bien con las funciones de OED. Consulte la documentación de Plotly en {link}.", "help.home.chart.redraw.restore": "OED automáticamente toma el promedio de los datos cuando es necesario para que los gráficos tengan un número razonable de puntos. Si usa los controles debajo del gráfico para desplazarse y / o acercarse, puede encontrar que la resolución en este nivel de promedio no es la que desea. Al hacer clic en el botón \"Redraw\" OED volverá a calcular el promedio y obtendrá una resolución más alta para el número de puntos que muestra. Si desea restaurar el gráfico al rango completo de fechas, haga clic en el botón \"Restore\" button. Por favor visite {link} para obtener más detalles e información.", "help.home.chart.select": "Se puede usar cualquier tipo de gráfico con cualquier combinación de grupos y medidores. Los gráficos de líneas muestran el uso (por ejemplo, kW) con el tiempo. Puede hacer zoom y desplazarse con los controles justo debajo del gráfico. La barra muestra el uso total (por ejemplo, kWh) para el período de tiempo de cada barra donde se puede controlar el período de tiempo. Comparar le permite ver el uso actual comparado con el uso del período anterior durante un día, una semana y cuatro semanas. Los gráficos del mapa muestran una imagen espacial de cada medidor donde el tamaño del círculo está relacionado con cuatro semanas de uso. Las gráficas 3D muestran el uso por día y el uso por hora del día. Hacer clic en uno de estas opciones las registra en ese gráfico. Por favor visite {link} para obtener más detalles e información.", - "help.home.compare.interval.tip": "Selecciona el intervalo de tiempo (día, semana o 4 semanas) para comparar el actual con el anterior. Por favor, visite {link} para más detalles e información.", + "help.home.compare.period.tip": "Selecciona el intervalo de tiempo (día, semana o 4 semanas) para comparar el actual con el anterior. Por favor, visite {link} para más detalles e información.", "help.home.compare.sort.tip": "Permite al usuario seleccionar el orden de múltiples gráficos de comparación de forma alfabética (por nombre), ascendente (de mayor a menor reducción de uso) y descendente (de menor a mayor reducción de uso). Por favor, visite {link} para más detalles e información.", "help.home.error.bar": "Alternar barras de error con el valor mínimo y máximo. Por favor, vea {link} para más detalles e información.", "help.home.export.graph.data": "Con el botón \"Exportar datos del gráfico\", uno puede exportar los datos del gráfico al ver una línea o barra el gráfico. La función de zoom y desplazamiento en el gráfico de líneas le permite controlar el período de tiempo de los datos exportados. El botón \"Exportar data de gráfico\" da los puntos de datos para el gráfico y no los datos originales del medidor. \"Exportar el dato gráfhico de medidor\" proporciona los datos subyacentes del medidor (solo gráficos de líneas). Por favor visite {link} para obtener más detalles e información.", @@ -1419,7 +1422,7 @@ const LocaleTranslationData = { "show.options": "Mostrar opciones", "site.settings": "Site Settings\u{26A1}", "site.title": "Site Title\u{26A1}", - "sort": "Ordenar", + "sort": "Sort Order\u{26A1}", "submit": "Enviar", "submitting": "Enviando", "submit.changes": "Ingresar los cambios", diff --git a/src/client/app/types/redux/graph.ts b/src/client/app/types/redux/graph.ts index e649dd143..601dd51b9 100644 --- a/src/client/app/types/redux/graph.ts +++ b/src/client/app/types/redux/graph.ts @@ -62,8 +62,7 @@ export interface GraphState { selectedUnit: number; selectedAreaUnit: AreaUnitType; rangeSliderInterval: TimeInterval; - barDuration: moment.Duration; - mapsBarDuration: moment.Duration; + duration: moment.Duration; comparePeriod: ComparePeriod; compareTimeInterval: TimeInterval; compareSortingOrder: SortingOrder; diff --git a/src/client/app/utils/calculateCompare.ts b/src/client/app/utils/calculateCompare.ts index 499c5bac1..66198cb28 100644 --- a/src/client/app/utils/calculateCompare.ts +++ b/src/client/app/utils/calculateCompare.ts @@ -10,9 +10,9 @@ import translate from '../utils/translate'; * 'Day', 'Week' or 'FourWeeks' */ export enum ComparePeriod { - Day = 'Day', - Week = 'Week', - FourWeeks = 'FourWeeks' + Day = '1', + Week = '7', + FourWeeks = '28' } /** @@ -30,11 +30,11 @@ export enum SortingOrder { */ export function validateComparePeriod(comparePeriod: string): ComparePeriod { switch (comparePeriod) { - case 'Day': + case '1': return ComparePeriod.Day; - case 'Week': + case '7': return ComparePeriod.Week; - case 'FourWeeks': + case '28': return ComparePeriod.FourWeeks; default: throw new Error(`Unknown period value: ${comparePeriod}`);