From ce1cc1c46f63f9e390bddebee48ede43f5f4f9c3 Mon Sep 17 00:00:00 2001 From: danielshid Date: Fri, 12 Jul 2024 22:23:22 -0400 Subject: [PATCH 01/43] added const arrow functions to MapControlsComponent --- .../app/components/MapControlsComponent.tsx | 154 ++++++++++++++---- 1 file changed, 120 insertions(+), 34 deletions(-) diff --git a/src/client/app/components/MapControlsComponent.tsx b/src/client/app/components/MapControlsComponent.tsx index 7157e6788..0fed9d9f7 100644 --- a/src/client/app/components/MapControlsComponent.tsx +++ b/src/client/app/components/MapControlsComponent.tsx @@ -3,51 +3,137 @@ * 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 MapChartSelectComponent from './MapChartSelectComponent'; +// import TooltipMarkerComponent from './TooltipMarkerComponent'; +// /** +// * @returns Map page controls +// */ +// export default function MapControlsComponent() { +// const dispatch = useAppDispatch(); +// const mapDuration = useAppSelector(selectMapBarWidthDays); + +// const handleDurationChange = (value: number) => { +// dispatch(updateMapsBarDuration(moment.duration(value, 'days'))); +// }; + +// const barDurationDays = mapDuration.asDays(); + +// return ( +//
+//
+//

+// {translate('map.interval')}: +//

+// +// +// +// +// +// +//
+// +//
+// ); +// } + + +// const labelStyle: React.CSSProperties = { +// fontWeight: 'bold', +// margin: 0 +// }; + +// const zIndexFix: React.CSSProperties = { +// zIndex: 0 +// }; + import * as moment from 'moment'; import * as React from 'react'; -import { Button, ButtonGroup } from 'reactstrap'; +import { FormattedMessage } from 'react-intl'; +import { FormFeedback, FormGroup, Input, Label } from 'reactstrap'; import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; -import { selectMapBarWidthDays, updateMapsBarDuration } from '../redux/slices/graphSlice'; +import { graphSlice, selectBarWidthDays } from '../redux/slices/graphSlice'; import translate from '../utils/translate'; import MapChartSelectComponent from './MapChartSelectComponent'; import TooltipMarkerComponent from './TooltipMarkerComponent'; + /** * @returns Map page controls */ export default function MapControlsComponent() { const dispatch = useAppDispatch(); - const barDuration = useAppSelector(selectMapBarWidthDays); - const handleDurationChange = (value: number) => { - dispatch(updateMapsBarDuration(moment.duration(value, 'days'))); + // The min/max days allowed for user selection + const MIN_MAP_DAYS = 1; + const MAX_MAP_DAYS = 366; + // Special value if custom input for standard menu. + const CUSTOM_INPUT = '-99'; + + // This is the current map interval for graphic. + const mapDuration = useAppSelector(selectBarWidthDays); + // Holds the value of standard map duration choices used so decoupled from custom. + const [mapDays, setMapDays] = React.useState(mapDuration.asDays().toString()); + // Holds the value during custom map duration input so only update graphic when done entering and + // separate from standard choices. + const [mapDaysCustom, setMapDaysCustom] = React.useState(mapDuration.asDays()); + // True if custom map duration input is active. + const [showCustomMapDuration, setShowCustomMapDuration] = 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(() => { + // 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 == mapDuration.asDays().toString())); + setShowCustomMapDuration(isCustom); + setMapDaysCustom(mapDuration.asDays()); + setMapDays(isCustom ? CUSTOM_INPUT : mapDuration.asDays().toString()); + }, [mapDuration]); + + // Returns true if this is a valid map duration. + const mapDaysValid = (mapDays: number) => { + return Number.isInteger(mapDays) && mapDays >= MIN_MAP_DAYS && mapDays <= MAX_MAP_DAYS; + }; + + // Updates values when the standard map duration menu is used. + const handleMapDaysChange = (value: string) => { + if (value === CUSTOM_INPUT) { + // Set menu value for standard map to special value to show custom + // and show the custom input area. + setMapDays(CUSTOM_INPUT); + setShowCustomMapDuration(true); + } else { + // Set the standard menu value, hide the custom map duration input + // and map duration for graphing. + // Since controlled values know it is a valid integer. + setShowCustomMapDuration(false); + updateMapDurationChange(Number(value)); + } + }; + + // Updates value when the custom map duration input is used. + const handleCustomMapDaysChange = (value: number) => { + setMapDaysCustom(value); + }; + + const handleEnter = (key: string) => { + // This detects the enter key and then uses the previously entered custom + // map duration to set the map duration for the graphic. + if (key == 'Enter') { + updateMapDurationChange(mapDaysCustom); + } + }; + + const updateMapDurationChange = (value: number) => { + // Update if okay value. May not be okay if this came from user entry in custom form. + if (mapDaysValid(value)) { + dispatch(graphSlice.actions.updateBarDuration(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 -}; \ No newline at end of file + \ No newline at end of file From 0f05b009ffbbf17662b7627bf76f833dd037eb3f Mon Sep 17 00:00:00 2001 From: danielshid Date: Fri, 12 Jul 2024 22:26:24 -0400 Subject: [PATCH 02/43] added drop-down menu for map --- .../app/components/MapControlsComponent.tsx | 48 ++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/src/client/app/components/MapControlsComponent.tsx b/src/client/app/components/MapControlsComponent.tsx index 0fed9d9f7..1003b12ec 100644 --- a/src/client/app/components/MapControlsComponent.tsx +++ b/src/client/app/components/MapControlsComponent.tsx @@ -136,4 +136,50 @@ export default function MapControlsComponent() { } }; - \ No newline at end of file + return ( +
+
+

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

+ handleMapDaysChange(e.target.value)} + > + + + + + + {/* This has a little more spacing at bottom than optimal. */} + {showCustomMapDuration && + + + handleCustomMapDaysChange(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_MAP_DAYS} + max={MAX_MAP_DAYS} + value={mapDaysCustom} + invalid={!mapDaysValid(mapDaysCustom)} /> + + + + + } +
+ +
+ ); +} + +const labelStyle: React.CSSProperties = { + fontWeight: 'bold', + margin: 0 +}; From 6963f7f2086a30b5c9936a78f8af7759eee58ba1 Mon Sep 17 00:00:00 2001 From: danielshid Date: Fri, 19 Jul 2024 14:49:49 -0400 Subject: [PATCH 03/43] copied and stripped BarControlsComponent of bar specific components to replicate map --- .../app/components/MapControlsComponent.tsx | 207 +++++++++--------- 1 file changed, 106 insertions(+), 101 deletions(-) diff --git a/src/client/app/components/MapControlsComponent.tsx b/src/client/app/components/MapControlsComponent.tsx index 1003b12ec..edff06d30 100644 --- a/src/client/app/components/MapControlsComponent.tsx +++ b/src/client/app/components/MapControlsComponent.tsx @@ -3,54 +3,54 @@ * 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 MapChartSelectComponent from './MapChartSelectComponent'; -// import TooltipMarkerComponent from './TooltipMarkerComponent'; -// /** -// * @returns Map page controls -// */ -// export default function MapControlsComponent() { -// const dispatch = useAppDispatch(); -// const mapDuration = useAppSelector(selectMapBarWidthDays); - -// const handleDurationChange = (value: number) => { -// dispatch(updateMapsBarDuration(moment.duration(value, 'days'))); -// }; - -// const barDurationDays = mapDuration.asDays(); - -// return ( -//
-//
-//

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

-// -// -// -// -// -// -//
-// -//
-// ); -// } - - -// const labelStyle: React.CSSProperties = { -// fontWeight: 'bold', -// margin: 0 -// }; - -// const zIndexFix: React.CSSProperties = { -// zIndex: 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 MapChartSelectComponent from './MapChartSelectComponent'; +// import TooltipMarkerComponent from './TooltipMarkerComponent'; +// /** +// * @returns Map page controls +// */ +// 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 +// }; import * as moment from 'moment'; import * as React from 'react'; @@ -69,86 +69,86 @@ export default function MapControlsComponent() { const dispatch = useAppDispatch(); // The min/max days allowed for user selection - const MIN_MAP_DAYS = 1; - const MAX_MAP_DAYS = 366; + 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 map interval for graphic. - const mapDuration = useAppSelector(selectBarWidthDays); - // Holds the value of standard map duration choices used so decoupled from custom. - const [mapDays, setMapDays] = React.useState(mapDuration.asDays().toString()); - // Holds the value during custom map duration input so only update graphic when done entering and + // This is the current bar interval for graphic. + const barDuration = useAppSelector(selectBarWidthDays); + // 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 [mapDaysCustom, setMapDaysCustom] = React.useState(mapDuration.asDays()); - // True if custom map duration input is active. - const [showCustomMapDuration, setShowCustomMapDuration] = React.useState(false); + const [barDaysCustom, setBarDaysCustom] = React.useState(barDuration.asDays()); + // True if custom bar duration input is active. + const [showCustomBarDuration, setShowCustomBarDuration] = 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(() => { - // Assume value is valid since it is coming from state. + // 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 == mapDuration.asDays().toString())); - setShowCustomMapDuration(isCustom); - setMapDaysCustom(mapDuration.asDays()); - setMapDays(isCustom ? CUSTOM_INPUT : mapDuration.asDays().toString()); - }, [mapDuration]); - - // Returns true if this is a valid map duration. - const mapDaysValid = (mapDays: number) => { - return Number.isInteger(mapDays) && mapDays >= MIN_MAP_DAYS && mapDays <= MAX_MAP_DAYS; + 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 map duration menu is used. - const handleMapDaysChange = (value: string) => { + // Updates values when the standard bar duration menu is used. + const handleBarDaysChange = (value: string) => { if (value === CUSTOM_INPUT) { - // Set menu value for standard map to special value to show custom + // Set menu value for standard bar to special value to show custom // and show the custom input area. - setMapDays(CUSTOM_INPUT); - setShowCustomMapDuration(true); + setBarDays(CUSTOM_INPUT); + setShowCustomBarDuration(true); } else { - // Set the standard menu value, hide the custom map duration input - // and map duration for graphing. + // 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. - setShowCustomMapDuration(false); - updateMapDurationChange(Number(value)); + setShowCustomBarDuration(false); + updateBarDurationChange(Number(value)); } }; - // Updates value when the custom map duration input is used. - const handleCustomMapDaysChange = (value: number) => { - setMapDaysCustom(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 - // map duration to set the map duration for the graphic. + // bar duration to set the bar duration for the graphic. if (key == 'Enter') { - updateMapDurationChange(mapDaysCustom); + updateBarDurationChange(barDaysCustom); } }; - const updateMapDurationChange = (value: number) => { + const updateBarDurationChange = (value: number) => { // Update if okay value. May not be okay if this came from user entry in custom form. - if (mapDaysValid(value)) { + if (barDaysValid(value)) { dispatch(graphSlice.actions.updateBarDuration(moment.duration(value, 'days'))); } }; return (
-
+

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

handleMapDaysChange(e.target.value)} + value={barDays} + onChange={e => handleBarDaysChange(e.target.value)} > @@ -156,20 +156,20 @@ export default function MapControlsComponent() { {/* This has a little more spacing at bottom than optimal. */} - {showCustomMapDuration && + {showCustomBarDuration && - - handleCustomMapDaysChange(Number(e.target.value))} + + 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_MAP_DAYS} - max={MAX_MAP_DAYS} - value={mapDaysCustom} - invalid={!mapDaysValid(mapDaysCustom)} /> + min={MIN_BAR_DAYS} + max={MAX_BAR_DAYS} + value={barDaysCustom} + invalid={!barDaysValid(barDaysCustom)} /> - + } @@ -178,8 +178,13 @@ export default function MapControlsComponent() {
); } - + +const divTopBottomPadding: React.CSSProperties = { + paddingTop: '15px', + paddingBottom: '15px' +}; + const labelStyle: React.CSSProperties = { fontWeight: 'bold', margin: 0 -}; +}; \ No newline at end of file From bf45a4241f9cd3b5caf31b4b11104ed0c1a128f9 Mon Sep 17 00:00:00 2001 From: danielshid Date: Tue, 23 Jul 2024 18:19:17 -0400 Subject: [PATCH 04/43] functional map interval dropdown --- .../app/components/BarControlsComponent.tsx | 2 +- .../app/components/MapControlsComponent.tsx | 214 +++++++++--------- .../redux/selectors/chartQuerySelectors.ts | 4 +- src/client/app/redux/slices/graphSlice.ts | 10 +- src/client/app/types/redux/graph.ts | 1 - 5 files changed, 112 insertions(+), 119 deletions(-) diff --git a/src/client/app/components/BarControlsComponent.tsx b/src/client/app/components/BarControlsComponent.tsx index 03e858d11..2a95f98b8 100644 --- a/src/client/app/components/BarControlsComponent.tsx +++ b/src/client/app/components/BarControlsComponent.tsx @@ -63,7 +63,7 @@ export default function BarControlsComponent() { setShowCustomBarDuration(true); } else { // Set the standard menu value, hide the custom bar duration input - // and bar duration for graphing. + // and bar duration for graphing. // Since controlled values know it is a valid integer. setShowCustomBarDuration(false); updateBarDurationChange(Number(value)); diff --git a/src/client/app/components/MapControlsComponent.tsx b/src/client/app/components/MapControlsComponent.tsx index edff06d30..cc8b989f7 100644 --- a/src/client/app/components/MapControlsComponent.tsx +++ b/src/client/app/components/MapControlsComponent.tsx @@ -66,125 +66,125 @@ import TooltipMarkerComponent from './TooltipMarkerComponent'; * @returns Map page controls */ export default function MapControlsComponent() { - const dispatch = useAppDispatch(); + 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'; + // 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); - // 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); + // This is the current bar interval for graphic. + const barDuration = useAppSelector(selectBarWidthDays); + // 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); - // 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]); + // 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; - }; + // 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 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); - }; + // 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 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'))); - } - }; + 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('map.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)} /> - - - - - } -
- -
- ); + return ( +
+
+

+ {translate('map.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', - paddingBottom: '15px' + paddingTop: '15px', + paddingBottom: '15px' }; const labelStyle: React.CSSProperties = { - fontWeight: 'bold', - margin: 0 -}; \ No newline at end of file + fontWeight: 'bold', + margin: 0 +}; diff --git a/src/client/app/redux/selectors/chartQuerySelectors.ts b/src/client/app/redux/selectors/chartQuerySelectors.ts index b0253fcc4..35c42e716 100644 --- a/src/client/app/redux/selectors/chartQuerySelectors.ts +++ b/src/client/app/redux/selectors/chartQuerySelectors.ts @@ -9,7 +9,7 @@ import { calculateCompareShift } from '../../utils/calculateCompare'; import { roundTimeIntervalForFetch } from '../../utils/dateRangeCompatibility'; import { selectBarWidthDays, selectComparePeriod, - selectCompareTimeInterval, selectMapBarWidthDays, selectQueryTimeInterval, + selectCompareTimeInterval, selectQueryTimeInterval, selectSelectedGroups, selectSelectedMeters, selectSelectedUnit, selectThreeDState } from '../slices/graphSlice'; @@ -139,7 +139,7 @@ export const selectCompareChartQueryArgs = createSelector( export const selectMapChartQueryArgs = createSelector( selectBarChartQueryArgs, - selectMapBarWidthDays, + selectBarWidthDays, (state: RootState) => state.maps, (barChartArgs, barWidthDays, maps) => { const durationDays = Math.round(barWidthDays.asDays()); diff --git a/src/client/app/redux/slices/graphSlice.ts b/src/client/app/redux/slices/graphSlice.ts index 1114225e4..dcb132973 100644 --- a/src/client/app/redux/slices/graphSlice.ts +++ b/src/client/app/redux/slices/graphSlice.ts @@ -26,7 +26,6 @@ const defaultState: GraphState = { queryTimeInterval: TimeInterval.unbounded(), rangeSliderInterval: TimeInterval.unbounded(), barDuration: moment.duration(4, 'weeks'), - mapsBarDuration: moment.duration(4, 'weeks'), comparePeriod: ComparePeriod.Week, compareTimeInterval: calculateCompareTimeInterval(ComparePeriod.Week, moment()), compareSortingOrder: SortingOrder.Descending, @@ -82,9 +81,6 @@ export const graphSlice = createSlice({ updateBarDuration: (state, action: PayloadAction) => { state.current.barDuration = action.payload; }, - updateMapsBarDuration: (state, action: PayloadAction) => { - state.current.mapsBarDuration = action.payload; - }, updateTimeInterval: (state, action: PayloadAction) => { // always update if action is bounded, else only set unbounded if current isn't already unbounded. // clearing when already unbounded should be a no-op @@ -373,7 +369,6 @@ export const graphSlice = createSlice({ selectShowMinMax: state => state.current.showMinMax, selectBarStacking: state => state.current.barStacking, selectBarWidthDays: state => state.current.barDuration, - selectMapBarWidthDays: state => state.current.mapsBarDuration, selectAreaUnit: state => state.current.selectedAreaUnit, selectSelectedUnit: state => state.current.selectedUnit, selectChartToRender: state => state.current.chartToRender, @@ -410,8 +405,7 @@ export const { selectThreeDMeterOrGroupID, selectThreeDReadingInterval, selectGraphAreaNormalization, selectSliderRangeInterval, selectDefaultGraphState, selectHistoryIsDirty, - selectPlotlySliderMax, selectPlotlySliderMin, - selectMapBarWidthDays + selectPlotlySliderMax, selectPlotlySliderMin } = graphSlice.selectors; // actionCreators exports @@ -428,6 +422,6 @@ export const { toggleAreaNormalization, updateThreeDMeterOrGroup, changeCompareSortingOrder, updateThreeDMeterOrGroupID, updateThreeDReadingInterval, updateThreeDMeterOrGroupInfo, - updateSelectedMetersOrGroups, updateMapsBarDuration + updateSelectedMetersOrGroups } = graphSlice.actions; diff --git a/src/client/app/types/redux/graph.ts b/src/client/app/types/redux/graph.ts index e649dd143..ab91412e0 100644 --- a/src/client/app/types/redux/graph.ts +++ b/src/client/app/types/redux/graph.ts @@ -63,7 +63,6 @@ export interface GraphState { selectedAreaUnit: AreaUnitType; rangeSliderInterval: TimeInterval; barDuration: moment.Duration; - mapsBarDuration: moment.Duration; comparePeriod: ComparePeriod; compareTimeInterval: TimeInterval; compareSortingOrder: SortingOrder; From 6b882df3f5b470b7a48b50812ba0c733637562b2 Mon Sep 17 00:00:00 2001 From: danielshid Date: Tue, 23 Jul 2024 18:22:50 -0400 Subject: [PATCH 05/43] delete original MapControlsComponent code with buttons --- .../app/components/MapControlsComponent.tsx | 50 ------------------- 1 file changed, 50 deletions(-) diff --git a/src/client/app/components/MapControlsComponent.tsx b/src/client/app/components/MapControlsComponent.tsx index cc8b989f7..ee506fa89 100644 --- a/src/client/app/components/MapControlsComponent.tsx +++ b/src/client/app/components/MapControlsComponent.tsx @@ -2,56 +2,6 @@ * 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 MapChartSelectComponent from './MapChartSelectComponent'; -// import TooltipMarkerComponent from './TooltipMarkerComponent'; -// /** -// * @returns Map page controls -// */ -// 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 -// }; - import * as moment from 'moment'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; From 5110a823de45061377df0448e81bd9c14e086694 Mon Sep 17 00:00:00 2001 From: danielshid Date: Mon, 29 Jul 2024 19:05:25 -0400 Subject: [PATCH 06/43] implemented compare interval dropodwn menu --- .../components/CompareControlsComponent.tsx | 275 +++++++++++++----- .../app/components/MapControlsComponent.tsx | 6 +- src/client/app/translations/data.ts | 3 + 3 files changed, 210 insertions(+), 74 deletions(-) diff --git a/src/client/app/components/CompareControlsComponent.tsx b/src/client/app/components/CompareControlsComponent.tsx index d990852cd..6a397d90b 100644 --- a/src/client/app/components/CompareControlsComponent.tsx +++ b/src/client/app/components/CompareControlsComponent.tsx @@ -2,11 +2,97 @@ * 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 { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; +// import { ComparePeriod, SortingOrder } from '../utils/calculateCompare'; +// import translate from '../utils/translate'; +// import TooltipMarkerComponent from './TooltipMarkerComponent'; + +// /** +// * @returns controls for the compare page +// */ +// export default function CompareControlsComponent() { +// const dispatch = useAppDispatch(); +// const comparePeriod = useAppSelector(selectComparePeriod); +// 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) => { +// 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 +// }; + 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 { Dropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap'; import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; +import { graphSlice, selectComparePeriod, selectSortingOrder } from '../redux/slices/graphSlice'; import { ComparePeriod, SortingOrder } from '../utils/calculateCompare'; import translate from '../utils/translate'; import TooltipMarkerComponent from './TooltipMarkerComponent'; @@ -15,75 +101,122 @@ import TooltipMarkerComponent from './TooltipMarkerComponent'; * @returns controls for the compare page */ export default function CompareControlsComponent() { - const dispatch = useAppDispatch(); - const comparePeriod = useAppSelector(selectComparePeriod); - 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) => { - dispatch(graphSlice.actions.changeCompareSortingOrder(sortingOrder)); - }; + const dispatch = useAppDispatch(); + const comparePeriod = useAppSelector(selectComparePeriod); + const compareSortingOrder = useAppSelector(selectSortingOrder); + const [comparePeriodDropdownOpen, setComparePeriodDropdownOpen] = React.useState(false); + const [compareSortingDropdownOpen, setCompareSortingDropdownOpen] = React.useState(false); + const handleCompare = (comparePeriod: ComparePeriod) => { + dispatch(graphSlice.actions.updateComparePeriod({ comparePeriod, currentTime: moment() })); + }; + const handleSorting = (sortingOrder: SortingOrder) => { + dispatch(graphSlice.actions.changeCompareSortingOrder(sortingOrder)); + }; + + const getComparePeriodDisplayText = () => { + switch (comparePeriod) { + case ComparePeriod.Day: + return translate('day'); + case ComparePeriod.Week: + return translate('week'); + case ComparePeriod.FourWeeks: + return translate('4.weeks'); + } + }; + + const getSortDisplayText = () => { + switch (compareSortingOrder) { + case SortingOrder.Alphabetical: + return translate('alphabetically'); + case SortingOrder.Ascending: + return translate('ascending'); + case SortingOrder.Descending: + return translate('descending'); + } + }; - return ( -
- - - - - - - setCompareSortingDropdownOpen(current => !current)}> - - {translate('sort')} - - - - handleSortingButton(SortingOrder.Alphabetical)} - > - {translate('alphabetically')} - - handleSortingButton(SortingOrder.Ascending)} - > - {translate('ascending')} - - handleSortingButton(SortingOrder.Descending)} - > - {translate('descending')} - - - -
- ); + return ( +
+
+

+ {translate('compare.interval')}: + +

+ setComparePeriodDropdownOpen(current => !current)}> + + {getComparePeriodDisplayText()} + + + handleCompare(ComparePeriod.Day)} + > + {translate('day')} + + handleCompare(ComparePeriod.Week)} + > + {translate('week')} + + handleCompare(ComparePeriod.FourWeeks)} + > + {translate('4.weeks')} + + {/* TODO: Add custom option. Compare is currently not ready for this. */} + + +
+
+

+ {translate('sort')}: + +

+ setCompareSortingDropdownOpen(current => !current)}> + + {getSortDisplayText()} + + + handleSorting(SortingOrder.Alphabetical)} + > + {translate('alphabetically')} + + handleSorting(SortingOrder.Ascending)} + > + {translate('ascending')} + + handleSorting(SortingOrder.Descending)} + > + {translate('descending')} + + + +
+
+ ); } -const zIndexFix: React.CSSProperties = { - zIndex: 0 -}; \ No newline at end of file +const divTopBottomPadding: React.CSSProperties = { + paddingTop: '10px', + paddingBottom: '10px' +}; + +const labelStyle: React.CSSProperties = { + fontWeight: 'bold', + margin: 0 +}; + +const dropdownToggleStyle: React.CSSProperties = { + backgroundColor: '#ffffff', + color: '#000000', + border: '1px solid #ced4da', + boxShadow: 'none' +}; diff --git a/src/client/app/components/MapControlsComponent.tsx b/src/client/app/components/MapControlsComponent.tsx index ee506fa89..8e5b6c08c 100644 --- a/src/client/app/components/MapControlsComponent.tsx +++ b/src/client/app/components/MapControlsComponent.tsx @@ -91,7 +91,7 @@ export default function MapControlsComponent() {

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

} -
+
-
+ ); } diff --git a/src/client/app/translations/data.ts b/src/client/app/translations/data.ts index aa9ff683a..bbf0b530c 100644 --- a/src/client/app/translations/data.ts +++ b/src/client/app/translations/data.ts @@ -49,6 +49,7 @@ const LocaleTranslationData = { "clipboard.not.copied": "Failed to Copy To Clipboard", "close": "Close", "compare": "Compare", + "compare.interval": "Compare Interval", "compare.raw": "Cannot create comparison graph on raw units such as temperature", "confirm.action": "Confirm Action", "contact.us": "Contact us", @@ -544,6 +545,7 @@ const LocaleTranslationData = { "clipboard.not.copied": "Failed to Copy To Clipboard\u{26A1}", "close": "Close\u{26A1}", "compare": "Comparer", + "compare.interval": "Compare Interval\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", @@ -1039,6 +1041,7 @@ const LocaleTranslationData = { "clipboard.not.copied": "Error al copiar al portapapeles", "close": "Cerrar", "compare": "Comparar", + "compare.interval": "Compare Interval\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", From 90cac8460dc1855f7434ee4e7cb7d4cb10fb5c69 Mon Sep 17 00:00:00 2001 From: danielshid Date: Mon, 29 Jul 2024 19:07:12 -0400 Subject: [PATCH 07/43] delete original compare button code --- .../components/CompareControlsComponent.tsx | 86 ------------------- 1 file changed, 86 deletions(-) diff --git a/src/client/app/components/CompareControlsComponent.tsx b/src/client/app/components/CompareControlsComponent.tsx index 6a397d90b..d66aaa9ed 100644 --- a/src/client/app/components/CompareControlsComponent.tsx +++ b/src/client/app/components/CompareControlsComponent.tsx @@ -2,92 +2,6 @@ * 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 { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; -// import { ComparePeriod, SortingOrder } from '../utils/calculateCompare'; -// import translate from '../utils/translate'; -// import TooltipMarkerComponent from './TooltipMarkerComponent'; - -// /** -// * @returns controls for the compare page -// */ -// export default function CompareControlsComponent() { -// const dispatch = useAppDispatch(); -// const comparePeriod = useAppSelector(selectComparePeriod); -// 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) => { -// 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 -// }; - import * as moment from 'moment'; import * as React from 'react'; import { Dropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap'; From 97f384b5de0cdb659a4cffb5c29655f3de76a79c Mon Sep 17 00:00:00 2001 From: danielshid Date: Tue, 30 Jul 2024 14:24:58 -0400 Subject: [PATCH 08/43] touchups to CompareControlsComponent and added comments --- .../components/CompareControlsComponent.tsx | 21 ++++++++++++++----- .../app/components/TooltipHelpComponent.tsx | 2 +- src/client/app/translations/data.ts | 18 ++++++++-------- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/src/client/app/components/CompareControlsComponent.tsx b/src/client/app/components/CompareControlsComponent.tsx index d66aaa9ed..1037c5fab 100644 --- a/src/client/app/components/CompareControlsComponent.tsx +++ b/src/client/app/components/CompareControlsComponent.tsx @@ -16,17 +16,27 @@ import TooltipMarkerComponent from './TooltipMarkerComponent'; */ export default function CompareControlsComponent() { const dispatch = useAppDispatch(); + + // This is the current compare period for graphic const comparePeriod = useAppSelector(selectComparePeriod); + // This is the current sorting order for graphic const compareSortingOrder = useAppSelector(selectSortingOrder); + // State to manage the dropdown open status for compare interval const [comparePeriodDropdownOpen, setComparePeriodDropdownOpen] = React.useState(false); + // State to manage the dropdown open status for sorting order const [compareSortingDropdownOpen, setCompareSortingDropdownOpen] = React.useState(false); + + // Updates values when the compare interval menu is used const handleCompare = (comparePeriod: ComparePeriod) => { dispatch(graphSlice.actions.updateComparePeriod({ comparePeriod, currentTime: moment() })); }; + + // Updates sorting order when the sort order menu is used const handleSorting = (sortingOrder: SortingOrder) => { dispatch(graphSlice.actions.changeCompareSortingOrder(sortingOrder)); }; + // Updates the text in the compare interval dropdown menu when switching between intervals const getComparePeriodDisplayText = () => { switch (comparePeriod) { case ComparePeriod.Day: @@ -38,6 +48,7 @@ export default function CompareControlsComponent() { } }; + // Updates the text in the sort dropdown menu when switching between sorting types const getSortDisplayText = () => { switch (compareSortingOrder) { case SortingOrder.Alphabetical: @@ -53,8 +64,8 @@ export default function CompareControlsComponent() {

- {translate('compare.interval')}: - + {translate('compare.period')}: +

setComparePeriodDropdownOpen(current => !current)}> @@ -83,7 +94,7 @@ export default function CompareControlsComponent() {
-
+

{translate('sort')}: @@ -119,8 +130,8 @@ export default function CompareControlsComponent() { } const divTopBottomPadding: React.CSSProperties = { - paddingTop: '10px', - paddingBottom: '10px' + paddingTop: '15px', + paddingBottom: '15px' }; const labelStyle: React.CSSProperties = { diff --git a/src/client/app/components/TooltipHelpComponent.tsx b/src/client/app/components/TooltipHelpComponent.tsx index 8ff8fad01..8f8ed0ac2 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.html#redrawRestore` }, 'help.home.chart.select': { link: `${helpUrl}/graphType.html` }, - 'help.home.compare.interval.tip': { link: `${helpUrl}/compareGraphic.html#usage` }, + 'help.home.compare.period.tip': { link: `${helpUrl}/compareGraphic.html#usage` }, 'help.home.compare.sort.tip': { link: `${helpUrl}/compareGraphic.html#usage` }, 'help.home.error.bar': { link: `${helpUrl}/errorBar.html#usage` }, 'help.home.export.graph.data': { link: `${helpUrl}/export.html` }, diff --git a/src/client/app/translations/data.ts b/src/client/app/translations/data.ts index bbf0b530c..b23fcacca 100644 --- a/src/client/app/translations/data.ts +++ b/src/client/app/translations/data.ts @@ -49,7 +49,7 @@ const LocaleTranslationData = { "clipboard.not.copied": "Failed to Copy To Clipboard", "close": "Close", "compare": "Compare", - "compare.interval": "Compare Interval", + "compare.period": "Compare Period", "compare.raw": "Cannot create comparison graph on raw units such as temperature", "confirm.action": "Confirm Action", "contact.us": "Contact us", @@ -245,7 +245,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.", @@ -414,7 +414,7 @@ const LocaleTranslationData = { "show": "Show", "show.grid": "Show grid", "show.options": "Show options", - "sort": "Sort", + "sort": "Sort Order", "submit": "Submit", "submitting": "submitting", "submit.changes": "Submit changes", @@ -545,7 +545,7 @@ const LocaleTranslationData = { "clipboard.not.copied": "Failed to Copy To Clipboard\u{26A1}", "close": "Close\u{26A1}", "compare": "Comparer", - "compare.interval": "Compare Interval\u{26A1}", + "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", @@ -741,7 +741,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}", @@ -910,7 +910,7 @@ const LocaleTranslationData = { "show": "Montrer", "show.grid": "Show grid\u{26A1}", "show.options": "Options de désancrage", - "sort": "Trier", + "sort": "Sort Order\u{26A1}", "submit": "Soumettre", "submitting": "submitting\u{26A1}", "submit.changes": "Soumettre les changements", @@ -1041,7 +1041,7 @@ const LocaleTranslationData = { "clipboard.not.copied": "Error al copiar al portapapeles", "close": "Cerrar", "compare": "Comparar", - "compare.interval": "Compare Interval\u{26A1}", + "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", @@ -1237,7 +1237,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.", @@ -1406,7 +1406,7 @@ const LocaleTranslationData = { "show": "Mostrar", "show.grid": "Mostrar rejilla", "show.options": "Mostrar opciones", - "sort": "Ordenar", + "sort": "Sort Order\u{26A1}", "submit": "Enviar", "submitting": "Enviando", "submit.changes": "Ingresar los cambios", From 3b8281a1a8b8bb3d73037ea7371ddee20de06852 Mon Sep 17 00:00:00 2001 From: danielshid Date: Thu, 1 Aug 2024 18:36:54 -0400 Subject: [PATCH 09/43] implemented bar, map, and compare controls into one file --- .../app/components/BarControlsComponent.tsx | 2 +- .../components/CompareControlsComponent.tsx | 6 +- .../components/IntervalControlsComponent.tsx | 284 ++++++++++++++++++ .../app/components/UIOptionsComponent.tsx | 16 +- 4 files changed, 298 insertions(+), 10 deletions(-) create mode 100644 src/client/app/components/IntervalControlsComponent.tsx diff --git a/src/client/app/components/BarControlsComponent.tsx b/src/client/app/components/BarControlsComponent.tsx index 2a95f98b8..62433ce14 100644 --- a/src/client/app/components/BarControlsComponent.tsx +++ b/src/client/app/components/BarControlsComponent.tsx @@ -41,7 +41,7 @@ export default function BarControlsComponent() { // 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. + // 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); diff --git a/src/client/app/components/CompareControlsComponent.tsx b/src/client/app/components/CompareControlsComponent.tsx index 1037c5fab..e521396e5 100644 --- a/src/client/app/components/CompareControlsComponent.tsx +++ b/src/client/app/components/CompareControlsComponent.tsx @@ -21,12 +21,12 @@ export default function CompareControlsComponent() { const comparePeriod = useAppSelector(selectComparePeriod); // This is the current sorting order for graphic const compareSortingOrder = useAppSelector(selectSortingOrder); - // State to manage the dropdown open status for compare interval + // State to manage the dropdown open status for compare period const [comparePeriodDropdownOpen, setComparePeriodDropdownOpen] = React.useState(false); // State to manage the dropdown open status for sorting order const [compareSortingDropdownOpen, setCompareSortingDropdownOpen] = React.useState(false); - // Updates values when the compare interval menu is used + // Updates values when the compare period menu is used const handleCompare = (comparePeriod: ComparePeriod) => { dispatch(graphSlice.actions.updateComparePeriod({ comparePeriod, currentTime: moment() })); }; @@ -36,7 +36,7 @@ export default function CompareControlsComponent() { dispatch(graphSlice.actions.changeCompareSortingOrder(sortingOrder)); }; - // Updates the text in the compare interval dropdown menu when switching between intervals + // Updates the text in the compare period dropdown menu when switching between periods const getComparePeriodDisplayText = () => { switch (comparePeriod) { case ComparePeriod.Day: diff --git a/src/client/app/components/IntervalControlsComponent.tsx b/src/client/app/components/IntervalControlsComponent.tsx new file mode 100644 index 000000000..1ee76b8ff --- /dev/null +++ b/src/client/app/components/IntervalControlsComponent.tsx @@ -0,0 +1,284 @@ +/* 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, Dropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap'; +import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; +import { selectChartToRender, graphSlice, selectBarStacking, selectBarWidthDays, selectComparePeriod, selectSortingOrder } from '../redux/slices/graphSlice'; +import { ChartTypes } from '../types/redux/graph'; +import { ComparePeriod, SortingOrder } from '../utils/calculateCompare'; +import translate from '../utils/translate'; +import MapChartSelectComponent from './MapChartSelectComponent'; +import TooltipMarkerComponent from './TooltipMarkerComponent'; + +/** + * @returns 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_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 = chartType === ChartTypes.bar ? useAppSelector(selectBarStacking) : undefined; + // This is the current compare period for graphic + const comparePeriod = chartType === ChartTypes.compare ? useAppSelector(selectComparePeriod) : undefined; + // This is the current sorting order for graphic + const compareSortingOrder = chartType === ChartTypes.compare ? useAppSelector(selectSortingOrder) : undefined; + + // 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); + // State to manage the dropdown open status for compare period + const [comparePeriodDropdownOpen, setComparePeriodDropdownOpen] = React.useState(false); + // State to manage the dropdown open status for sorting order + const [compareSortingDropdownOpen, setCompareSortingDropdownOpen] = React.useState(false); + + const handleChangeBarStacking = () => { + if (chartType === ChartTypes.bar) { + 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'))); + } + }; + + // Updates values when the compare period menu is used + const handleCompare = (comparePeriod: ComparePeriod) => { + if (chartType === ChartTypes.compare) { + dispatch(graphSlice.actions.updateComparePeriod({ comparePeriod, currentTime: moment() })); + } + }; + + // Updates sorting order when the sort order menu is used + const handleSorting = (sortingOrder: SortingOrder) => { + if (chartType === ChartTypes.compare) { + dispatch(graphSlice.actions.changeCompareSortingOrder(sortingOrder)); + } + }; + + // Updates the text in the compare period dropdown menu when switching between periods + const getComparePeriodDisplayText = () => { + switch (comparePeriod) { + case ComparePeriod.Day: + return translate('day'); + case ComparePeriod.Week: + return translate('week'); + case ComparePeriod.FourWeeks: + return translate('4.weeks'); + default: + return ''; + } + }; + + // Updates the text in the sort dropdown menu when switching between sorting types + const getSortDisplayText = () => { + switch (compareSortingOrder) { + case SortingOrder.Alphabetical: + return translate('alphabetically'); + case SortingOrder.Ascending: + return translate('ascending'); + case SortingOrder.Descending: + return translate('descending'); + default: + return ''; + } + }; + + return ( +

+ {chartType === ChartTypes.bar && ( +
+ + + +
+ )} + {(chartType === ChartTypes.bar || chartType === ChartTypes.map) && ( +
+

+ {chartType === ChartTypes.bar ? translate('bar.interval') : translate('map.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)} /> + + + + + } +
+ )} + {chartType === ChartTypes.map && } + {chartType === ChartTypes.compare && ( + <> +
+

+ {translate('compare.period')}: + +

+ setComparePeriodDropdownOpen(current => !current)}> + + {getComparePeriodDisplayText()} + + + handleCompare(ComparePeriod.Day)} + > + {translate('day')} + + handleCompare(ComparePeriod.Week)} + > + {translate('week')} + + handleCompare(ComparePeriod.FourWeeks)} + > + {translate('4.weeks')} + + {/* TODO: Add custom option. Compare is currently not ready for this. */} + + +
+
+

+ {translate('sort')}: + +

+ setCompareSortingDropdownOpen(current => !current)}> + + {getSortDisplayText()} + + + handleSorting(SortingOrder.Alphabetical)} + > + {translate('alphabetically')} + + handleSorting(SortingOrder.Ascending)} + > + {translate('ascending')} + + handleSorting(SortingOrder.Descending)} + > + {translate('descending')} + + + +
+ + )} +
+ ); +} + +const divTopBottomPadding: React.CSSProperties = { + paddingTop: '15px', + paddingBottom: '15px' +}; + +const labelStyle: React.CSSProperties = { + fontWeight: 'bold', + margin: 0 +}; + +const dropdownToggleStyle: React.CSSProperties = { + backgroundColor: '#ffffff', + color: '#000000', + border: '1px solid #ced4da', + boxShadow: 'none' +}; diff --git a/src/client/app/components/UIOptionsComponent.tsx b/src/client/app/components/UIOptionsComponent.tsx index 13c6bb572..9885ce2f0 100644 --- a/src/client/app/components/UIOptionsComponent.tsx +++ b/src/client/app/components/UIOptionsComponent.tsx @@ -9,15 +9,16 @@ import { useAppSelector } from '../redux/reduxHooks'; import { selectChartToRender, selectSelectedGroups, selectSelectedMeters } from '../redux/slices/graphSlice'; import { ChartTypes } from '../types/redux/graph'; import AreaUnitSelectComponent from './AreaUnitSelectComponent'; -import BarControlsComponent from './BarControlsComponent'; +// import BarControlsComponent from './BarControlsComponent'; import ChartDataSelectComponent from './ChartDataSelectComponent'; import ChartLinkComponent from './ChartLinkComponent'; import ChartSelectComponent from './ChartSelectComponent'; -import CompareControlsComponent from './CompareControlsComponent'; +// import CompareControlsComponent from './CompareControlsComponent'; import DateRangeComponent from './DateRangeComponent'; import ErrorBarComponent from './ErrorBarComponent'; import GraphicRateMenuComponent from './GraphicRateMenuComponent'; -import MapControlsComponent from './MapControlsComponent'; +import IntervalControlsComponent from './IntervalControlsComponent'; +// import MapControlsComponent from './MapControlsComponent'; import ReadingsPerDaySelectComponent from './ReadingsPerDaySelectComponent'; /** @@ -72,13 +73,16 @@ export default function UIOptionsComponent() { chartToRender === ChartTypes.line && } { /* Controls specific to the bar chart. */ - chartToRender === ChartTypes.bar && } + // chartToRender === ChartTypes.bar && } + chartToRender === ChartTypes.bar && } { /* Controls specific to the compare chart */ - chartToRender === ChartTypes.compare && } + // chartToRender === ChartTypes.compare && } + chartToRender === ChartTypes.compare && } { /* Controls specific to the compare chart */ - chartToRender === ChartTypes.map && } + // chartToRender === ChartTypes.map && } + chartToRender === ChartTypes.map && } { /* We can't export compare, map, radar or 3D data */ chartToRender !== ChartTypes.compare && From e583f3f222e744482aaa0881a1849d214c88aa68 Mon Sep 17 00:00:00 2001 From: danielshid Date: Fri, 2 Aug 2024 14:41:28 -0400 Subject: [PATCH 10/43] delete original code fragments and generalize variable/function names --- .../app/components/BarControlsComponent.tsx | 148 ------------------ .../components/CompareControlsComponent.tsx | 147 ----------------- .../components/IntervalControlsComponent.tsx | 112 ++++++------- .../app/components/MapChartComponent.tsx | 20 +-- .../app/components/MapControlsComponent.tsx | 140 ----------------- .../app/components/UIOptionsComponent.tsx | 6 - .../app/containers/MapChartContainer.ts | 22 +-- .../app/redux/selectors/barChartSelectors.ts | 4 +- .../redux/selectors/chartQuerySelectors.ts | 6 +- src/client/app/redux/selectors/uiSelectors.ts | 2 +- src/client/app/redux/slices/graphSlice.ts | 16 +- src/client/app/types/redux/graph.ts | 2 +- 12 files changed, 92 insertions(+), 533 deletions(-) delete mode 100644 src/client/app/components/BarControlsComponent.tsx delete mode 100644 src/client/app/components/CompareControlsComponent.tsx delete mode 100644 src/client/app/components/MapControlsComponent.tsx diff --git a/src/client/app/components/BarControlsComponent.tsx b/src/client/app/components/BarControlsComponent.tsx deleted file mode 100644 index 62433ce14..000000000 --- a/src/client/app/components/BarControlsComponent.tsx +++ /dev/null @@ -1,148 +0,0 @@ -/* 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 { graphSlice, selectBarStacking, selectBarWidthDays } from '../redux/slices/graphSlice'; -import translate from '../utils/translate'; -import TooltipMarkerComponent from './TooltipMarkerComponent'; - -/** - * @returns controls for the Options Ui 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', - 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 deleted file mode 100644 index e521396e5..000000000 --- a/src/client/app/components/CompareControlsComponent.tsx +++ /dev/null @@ -1,147 +0,0 @@ -/* 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 { Dropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap'; -import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; -import { graphSlice, selectComparePeriod, selectSortingOrder } from '../redux/slices/graphSlice'; -import { ComparePeriod, SortingOrder } from '../utils/calculateCompare'; -import translate from '../utils/translate'; -import TooltipMarkerComponent from './TooltipMarkerComponent'; - -/** - * @returns controls for the compare page - */ -export default function CompareControlsComponent() { - const dispatch = useAppDispatch(); - - // This is the current compare period for graphic - const comparePeriod = useAppSelector(selectComparePeriod); - // This is the current sorting order for graphic - const compareSortingOrder = useAppSelector(selectSortingOrder); - // State to manage the dropdown open status for compare period - const [comparePeriodDropdownOpen, setComparePeriodDropdownOpen] = React.useState(false); - // State to manage the dropdown open status for sorting order - const [compareSortingDropdownOpen, setCompareSortingDropdownOpen] = React.useState(false); - - // Updates values when the compare period menu is used - const handleCompare = (comparePeriod: ComparePeriod) => { - dispatch(graphSlice.actions.updateComparePeriod({ comparePeriod, currentTime: moment() })); - }; - - // Updates sorting order when the sort order menu is used - const handleSorting = (sortingOrder: SortingOrder) => { - dispatch(graphSlice.actions.changeCompareSortingOrder(sortingOrder)); - }; - - // Updates the text in the compare period dropdown menu when switching between periods - const getComparePeriodDisplayText = () => { - switch (comparePeriod) { - case ComparePeriod.Day: - return translate('day'); - case ComparePeriod.Week: - return translate('week'); - case ComparePeriod.FourWeeks: - return translate('4.weeks'); - } - }; - - // Updates the text in the sort dropdown menu when switching between sorting types - const getSortDisplayText = () => { - switch (compareSortingOrder) { - case SortingOrder.Alphabetical: - return translate('alphabetically'); - case SortingOrder.Ascending: - return translate('ascending'); - case SortingOrder.Descending: - return translate('descending'); - } - }; - - return ( -
-
-

- {translate('compare.period')}: - -

- setComparePeriodDropdownOpen(current => !current)}> - - {getComparePeriodDisplayText()} - - - handleCompare(ComparePeriod.Day)} - > - {translate('day')} - - handleCompare(ComparePeriod.Week)} - > - {translate('week')} - - handleCompare(ComparePeriod.FourWeeks)} - > - {translate('4.weeks')} - - {/* TODO: Add custom option. Compare is currently not ready for this. */} - - -
-
-

- {translate('sort')}: - -

- setCompareSortingDropdownOpen(current => !current)}> - - {getSortDisplayText()} - - - handleSorting(SortingOrder.Alphabetical)} - > - {translate('alphabetically')} - - handleSorting(SortingOrder.Ascending)} - > - {translate('ascending')} - - handleSorting(SortingOrder.Descending)} - > - {translate('descending')} - - - -
-
- ); -} - -const divTopBottomPadding: React.CSSProperties = { - paddingTop: '15px', - paddingBottom: '15px' -}; - -const labelStyle: React.CSSProperties = { - fontWeight: 'bold', - margin: 0 -}; - -const dropdownToggleStyle: React.CSSProperties = { - backgroundColor: '#ffffff', - color: '#000000', - border: '1px solid #ced4da', - boxShadow: 'none' -}; diff --git a/src/client/app/components/IntervalControlsComponent.tsx b/src/client/app/components/IntervalControlsComponent.tsx index 1ee76b8ff..600507786 100644 --- a/src/client/app/components/IntervalControlsComponent.tsx +++ b/src/client/app/components/IntervalControlsComponent.tsx @@ -7,7 +7,7 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { FormFeedback, FormGroup, Input, Label, Dropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap'; import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; -import { selectChartToRender, graphSlice, selectBarStacking, selectBarWidthDays, selectComparePeriod, selectSortingOrder } from '../redux/slices/graphSlice'; +import { selectChartToRender, graphSlice, selectBarStacking, selectWidthDays, selectComparePeriod, selectSortingOrder } from '../redux/slices/graphSlice'; import { ChartTypes } from '../types/redux/graph'; import { ComparePeriod, SortingOrder } from '../utils/calculateCompare'; import translate from '../utils/translate'; @@ -22,29 +22,29 @@ export default function IntervalControlsComponent() { const chartType = useAppSelector(selectChartToRender); // The min/max days allowed for user selection - const MIN_BAR_DAYS = 1; - const MAX_BAR_DAYS = 366; + const MIN_DAYS = 1; + const MAX_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); + // This is the current interval for the bar and map graphics. + const duration = useAppSelector(selectWidthDays); const barStacking = chartType === ChartTypes.bar ? useAppSelector(selectBarStacking) : undefined; // This is the current compare period for graphic const comparePeriod = chartType === ChartTypes.compare ? useAppSelector(selectComparePeriod) : undefined; // This is the current sorting order for graphic const compareSortingOrder = chartType === ChartTypes.compare ? useAppSelector(selectSortingOrder) : undefined; - // 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); - // State to manage the dropdown open status for compare period + // 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); + // State to manage the dropdown open status for compare period. const [comparePeriodDropdownOpen, setComparePeriodDropdownOpen] = React.useState(false); - // State to manage the dropdown open status for sorting order + // State to manage the dropdown open status for sorting order. const [compareSortingDropdownOpen, setCompareSortingDropdownOpen] = React.useState(false); const handleChangeBarStacking = () => { @@ -58,68 +58,68 @@ export default function IntervalControlsComponent() { 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]); + const isCustom = !(['1', '7', '28'].find(days => days == duration.asDays().toString())); + setShowCustomDuration(isCustom); + setDaysCustom(duration.asDays()); + setDays(isCustom ? CUSTOM_INPUT : duration.asDays().toString()); + }, [duration]); - // 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; + // Returns true if this is a valid duration for bar and map. + const daysValid = (days: number) => { + return Number.isInteger(days) && days >= MIN_DAYS && days <= MAX_DAYS; }; - // Updates values when the standard bar duration menu is used. - const handleBarDaysChange = (value: string) => { + // Updates values when the standard duration menu for bar or map is used. + const handleDaysChange = (value: string) => { if (value === CUSTOM_INPUT) { - // Set menu value for standard bar to special value to show custom + // Set menu value from standard value to special value to show custom // and show the custom input area. - setBarDays(CUSTOM_INPUT); - setShowCustomBarDuration(true); + setDays(CUSTOM_INPUT); + setShowCustomDuration(true); } else { - // Set the standard menu value, hide the custom bar duration input - // and bar duration for graphing. + // Set the standard menu value, hide the custom duration input + // and duration for graphing. // Since controlled values know it is a valid integer. - setShowCustomBarDuration(false); - updateBarDurationChange(Number(value)); + setShowCustomDuration(false); + updateDurationChange(Number(value)); } }; - // Updates value when the custom bar duration input is used. - const handleCustomBarDaysChange = (value: number) => { - setBarDaysCustom(value); + // Updates value when the custom duration input is used for bar or map. + const handleCustomDaysChange = (value: number) => { + setDaysCustom(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. + // duration to set the duration for the graphic. if (key === 'Enter') { - updateBarDurationChange(barDaysCustom); + updateDurationChange(daysCustom); } }; - const updateBarDurationChange = (value: number) => { + const updateDurationChange = (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'))); + if (daysValid(value)) { + dispatch(graphSlice.actions.updateDuration(moment.duration(value, 'days'))); } }; - // Updates values when the compare period menu is used + // Updates values when the compare period menu is used. const handleCompare = (comparePeriod: ComparePeriod) => { if (chartType === ChartTypes.compare) { dispatch(graphSlice.actions.updateComparePeriod({ comparePeriod, currentTime: moment() })); } }; - // Updates sorting order when the sort order menu is used + // Updates sorting order when the sort order menu is used. const handleSorting = (sortingOrder: SortingOrder) => { if (chartType === ChartTypes.compare) { dispatch(graphSlice.actions.changeCompareSortingOrder(sortingOrder)); } }; - // Updates the text in the compare period dropdown menu when switching between periods + // Updates the text in the compare period dropdown menu when switching between periods. const getComparePeriodDisplayText = () => { switch (comparePeriod) { case ComparePeriod.Day: @@ -133,7 +133,7 @@ export default function IntervalControlsComponent() { } }; - // Updates the text in the sort dropdown menu when switching between sorting types + // Updates the text in the sort dropdown menu when switching between sorting types. const getSortDisplayText = () => { switch (compareSortingOrder) { case SortingOrder.Alphabetical: @@ -163,11 +163,11 @@ export default function IntervalControlsComponent() {

handleBarDaysChange(e.target.value)} + value={days} + onChange={e => handleDaysChange(e.target.value)} > @@ -175,20 +175,20 @@ export default function IntervalControlsComponent() { {/* This has a little more spacing at bottom than optimal. */} - {showCustomBarDuration && + {showCustomDuration && - - handleCustomBarDaysChange(Number(e.target.value))} + + 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_BAR_DAYS} - max={MAX_BAR_DAYS} - value={barDaysCustom} - invalid={!barDaysValid(barDaysCustom)} /> + min={MIN_DAYS} + max={MAX_DAYS} + value={daysCustom} + invalid={!daysValid(daysCustom)} /> - + } diff --git a/src/client/app/components/MapChartComponent.tsx b/src/client/app/components/MapChartComponent.tsx index 56ca819bd..d9ddf445a 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'; @@ -46,7 +46,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); @@ -93,7 +93,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. @@ -165,10 +165,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) { @@ -196,13 +196,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; } @@ -240,7 +240,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]; @@ -269,13 +269,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 deleted file mode 100644 index 8e5b6c08c..000000000 --- a/src/client/app/components/MapControlsComponent.tsx +++ /dev/null @@ -1,140 +0,0 @@ -/* 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 { graphSlice, selectBarWidthDays } from '../redux/slices/graphSlice'; -import translate from '../utils/translate'; -import MapChartSelectComponent from './MapChartSelectComponent'; -import TooltipMarkerComponent from './TooltipMarkerComponent'; - -/** - * @returns Map page controls - */ -export default function MapControlsComponent() { - 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); - // 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); - - // 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('map.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', - paddingBottom: '15px' -}; - -const labelStyle: React.CSSProperties = { - fontWeight: 'bold', - margin: 0 -}; diff --git a/src/client/app/components/UIOptionsComponent.tsx b/src/client/app/components/UIOptionsComponent.tsx index 9885ce2f0..f909faa3e 100644 --- a/src/client/app/components/UIOptionsComponent.tsx +++ b/src/client/app/components/UIOptionsComponent.tsx @@ -9,16 +9,13 @@ import { useAppSelector } from '../redux/reduxHooks'; import { selectChartToRender, selectSelectedGroups, selectSelectedMeters } from '../redux/slices/graphSlice'; import { ChartTypes } from '../types/redux/graph'; import AreaUnitSelectComponent from './AreaUnitSelectComponent'; -// import BarControlsComponent from './BarControlsComponent'; import ChartDataSelectComponent from './ChartDataSelectComponent'; import ChartLinkComponent from './ChartLinkComponent'; import ChartSelectComponent from './ChartSelectComponent'; -// import CompareControlsComponent from './CompareControlsComponent'; import DateRangeComponent from './DateRangeComponent'; import ErrorBarComponent from './ErrorBarComponent'; import GraphicRateMenuComponent from './GraphicRateMenuComponent'; import IntervalControlsComponent from './IntervalControlsComponent'; -// import MapControlsComponent from './MapControlsComponent'; import ReadingsPerDaySelectComponent from './ReadingsPerDaySelectComponent'; /** @@ -73,15 +70,12 @@ export default function UIOptionsComponent() { chartToRender === ChartTypes.line && } { /* Controls specific to the bar chart. */ - // chartToRender === ChartTypes.bar && } chartToRender === ChartTypes.bar && } { /* Controls specific to the compare chart */ - // chartToRender === ChartTypes.compare && } chartToRender === ChartTypes.compare && } { /* Controls specific to the compare chart */ - // chartToRender === ChartTypes.map && } chartToRender === ChartTypes.map && } { /* We can't export compare, map, radar or 3D data */ diff --git a/src/client/app/containers/MapChartContainer.ts b/src/client/app/containers/MapChartContainer.ts index 016b6176a..02e1b4b61 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 35c42e716..73e68216b 100644 --- a/src/client/app/redux/selectors/chartQuerySelectors.ts +++ b/src/client/app/redux/selectors/chartQuerySelectors.ts @@ -8,7 +8,7 @@ import { MeterOrGroup, ReadingInterval } from '../../types/redux/graph'; import { calculateCompareShift } from '../../utils/calculateCompare'; import { roundTimeIntervalForFetch } from '../../utils/dateRangeCompatibility'; import { - selectBarWidthDays, selectComparePeriod, + selectWidthDays, selectComparePeriod, selectCompareTimeInterval, selectQueryTimeInterval, selectSelectedGroups, selectSelectedMeters, selectSelectedUnit, selectThreeDState @@ -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, - selectBarWidthDays, + 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 dcb132973..91e38edd2 100644 --- a/src/client/app/redux/slices/graphSlice.ts +++ b/src/client/app/redux/slices/graphSlice.ts @@ -25,7 +25,7 @@ const defaultState: GraphState = { selectedAreaUnit: AreaUnitType.none, queryTimeInterval: TimeInterval.unbounded(), rangeSliderInterval: TimeInterval.unbounded(), - barDuration: moment.duration(4, 'weeks'), + duration: moment.duration(4, 'weeks'), comparePeriod: ComparePeriod.Week, compareTimeInterval: calculateCompareTimeInterval(ComparePeriod.Week, moment()), compareSortingOrder: SortingOrder.Descending, @@ -78,8 +78,8 @@ export const graphSlice = createSlice({ updateSelectedAreaUnit: (state, action: PayloadAction) => { state.current.selectedAreaUnit = action.payload; }, - updateBarDuration: (state, action: PayloadAction) => { - state.current.barDuration = 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. @@ -294,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'; @@ -368,7 +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, + selectWidthDays: state => state.current.duration, selectAreaUnit: state => state.current.selectedAreaUnit, selectSelectedUnit: state => state.current.selectedUnit, selectChartToRender: state => state.current.chartToRender, @@ -396,7 +396,7 @@ export const { selectAreaUnit, selectShowMinMax, selectGraphState, selectPrevHistory, selectThreeDState, selectBarStacking, - selectSortingOrder, selectBarWidthDays, + selectSortingOrder, selectWidthDays, selectSelectedUnit, selectLineGraphRate, selectComparePeriod, selectChartToRender, selectForwardHistory, selectSelectedMeters, @@ -413,7 +413,7 @@ export const { setShowMinMax, setGraphState, setBarStacking, toggleShowMinMax, changeBarStacking, resetTimeInterval, - updateBarDuration, changeSliderRange, + updateDuration, changeSliderRange, updateTimeInterval, updateSelectedUnit, changeChartToRender, updateComparePeriod, updateSelectedMeters, updateLineGraphRate, diff --git a/src/client/app/types/redux/graph.ts b/src/client/app/types/redux/graph.ts index ab91412e0..601dd51b9 100644 --- a/src/client/app/types/redux/graph.ts +++ b/src/client/app/types/redux/graph.ts @@ -62,7 +62,7 @@ export interface GraphState { selectedUnit: number; selectedAreaUnit: AreaUnitType; rangeSliderInterval: TimeInterval; - barDuration: moment.Duration; + duration: moment.Duration; comparePeriod: ComparePeriod; compareTimeInterval: TimeInterval; compareSortingOrder: SortingOrder; From c5742bcedb96a51a984b4b803554bc19ddb296e9 Mon Sep 17 00:00:00 2001 From: danielshid Date: Sat, 3 Aug 2024 21:51:31 -0400 Subject: [PATCH 11/43] fixed import to meet length requirement --- src/client/app/components/IntervalControlsComponent.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/client/app/components/IntervalControlsComponent.tsx b/src/client/app/components/IntervalControlsComponent.tsx index 600507786..8c07c0c96 100644 --- a/src/client/app/components/IntervalControlsComponent.tsx +++ b/src/client/app/components/IntervalControlsComponent.tsx @@ -7,7 +7,11 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { FormFeedback, FormGroup, Input, Label, Dropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap'; import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; -import { selectChartToRender, graphSlice, selectBarStacking, selectWidthDays, selectComparePeriod, selectSortingOrder } from '../redux/slices/graphSlice'; +import { + selectChartToRender, graphSlice, + selectBarStacking, selectWidthDays, + selectComparePeriod, selectSortingOrder +} from '../redux/slices/graphSlice'; import { ChartTypes } from '../types/redux/graph'; import { ComparePeriod, SortingOrder } from '../utils/calculateCompare'; import translate from '../utils/translate'; From 3c15d962fc563d494ed5a0abe79322e410a3a0d0 Mon Sep 17 00:00:00 2001 From: danielshid Date: Sat, 3 Aug 2024 21:54:08 -0400 Subject: [PATCH 12/43] delete trailing space --- src/client/app/components/IntervalControlsComponent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/app/components/IntervalControlsComponent.tsx b/src/client/app/components/IntervalControlsComponent.tsx index 8c07c0c96..8505b0841 100644 --- a/src/client/app/components/IntervalControlsComponent.tsx +++ b/src/client/app/components/IntervalControlsComponent.tsx @@ -7,7 +7,7 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { FormFeedback, FormGroup, Input, Label, Dropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap'; import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; -import { +import { selectChartToRender, graphSlice, selectBarStacking, selectWidthDays, selectComparePeriod, selectSortingOrder From 1394d1faa6affa93cb2f8dc4244e2a9794d10c90 Mon Sep 17 00:00:00 2001 From: danielshid Date: Fri, 9 Aug 2024 03:35:01 -0400 Subject: [PATCH 13/43] used input instead of dropdown for compare controls --- .../components/IntervalControlsComponent.tsx | 174 ++++++------------ 1 file changed, 52 insertions(+), 122 deletions(-) diff --git a/src/client/app/components/IntervalControlsComponent.tsx b/src/client/app/components/IntervalControlsComponent.tsx index 8505b0841..59a5b12d1 100644 --- a/src/client/app/components/IntervalControlsComponent.tsx +++ b/src/client/app/components/IntervalControlsComponent.tsx @@ -5,7 +5,7 @@ import * as moment from 'moment'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import { FormFeedback, FormGroup, Input, Label, Dropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap'; +import { FormFeedback, FormGroup, Input, Label } from 'reactstrap'; import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; import { selectChartToRender, graphSlice, @@ -46,10 +46,6 @@ export default function IntervalControlsComponent() { const [daysCustom, setDaysCustom] = React.useState(duration.asDays()); // True if custom duration input for bar or map is active. const [showCustomDuration, setShowCustomDuration] = React.useState(false); - // State to manage the dropdown open status for compare period. - const [comparePeriodDropdownOpen, setComparePeriodDropdownOpen] = React.useState(false); - // State to manage the dropdown open status for sorting order. - const [compareSortingDropdownOpen, setCompareSortingDropdownOpen] = React.useState(false); const handleChangeBarStacking = () => { if (chartType === ChartTypes.bar) { @@ -68,7 +64,7 @@ export default function IntervalControlsComponent() { setDays(isCustom ? CUSTOM_INPUT : duration.asDays().toString()); }, [duration]); - // Returns true if this is a valid duration for bar and map. + // Returns true if this is a valid duration. const daysValid = (days: number) => { return Number.isInteger(days) && days >= MIN_DAYS && days <= MAX_DAYS; }; @@ -110,76 +106,67 @@ export default function IntervalControlsComponent() { }; // Updates values when the compare period menu is used. - const handleCompare = (comparePeriod: ComparePeriod) => { + const handleComparePeriodChange = (value: string) => { if (chartType === ChartTypes.compare) { - dispatch(graphSlice.actions.updateComparePeriod({ comparePeriod, currentTime: moment() })); + const period = value as unknown as ComparePeriod; + dispatch(graphSlice.actions.updateComparePeriod({ comparePeriod: period, currentTime: moment() })); } }; // Updates sorting order when the sort order menu is used. - const handleSorting = (sortingOrder: SortingOrder) => { + const handleSortingChange = (value: string) => { if (chartType === ChartTypes.compare) { + const sortingOrder = value as unknown as SortingOrder; dispatch(graphSlice.actions.changeCompareSortingOrder(sortingOrder)); } }; - // Updates the text in the compare period dropdown menu when switching between periods. - const getComparePeriodDisplayText = () => { - switch (comparePeriod) { - case ComparePeriod.Day: - return translate('day'); - case ComparePeriod.Week: - return translate('week'); - case ComparePeriod.FourWeeks: - return translate('4.weeks'); - default: - return ''; - } - }; - - // Updates the text in the sort dropdown menu when switching between sorting types. - const getSortDisplayText = () => { - switch (compareSortingOrder) { - case SortingOrder.Alphabetical: - return translate('alphabetically'); - case SortingOrder.Ascending: - return translate('ascending'); - case SortingOrder.Descending: - return translate('descending'); - default: - return ''; - } - }; - return (
{chartType === ChartTypes.bar && ( -
+
)} - {(chartType === ChartTypes.bar || chartType === ChartTypes.map) && ( + {(chartType === ChartTypes.bar || chartType === ChartTypes.map || chartType === ChartTypes.compare) && (

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

handleDaysChange(e.target.value)} + value={chartType === ChartTypes.compare ? comparePeriod?.toString() : days} + onChange={e => chartType === ChartTypes.compare ? handleComparePeriodChange(e.target.value) : handleDaysChange(e.target.value)} > - - - - + {chartType !== ChartTypes.compare && ( + <> + + + + + + )} + {chartType === ChartTypes.compare && ( + <> + + + + + )} - {/* This has a little more spacing at bottom than optimal. */} - {showCustomDuration && + {/* TODO: Compare is currently not ready for a custom option. */} + {showCustomDuration && chartType !== ChartTypes.compare && } {chartType === ChartTypes.compare && ( - <> -
-

- {translate('compare.period')}: - -

- setComparePeriodDropdownOpen(current => !current)}> - - {getComparePeriodDisplayText()} - - - handleCompare(ComparePeriod.Day)} - > - {translate('day')} - - handleCompare(ComparePeriod.Week)} - > - {translate('week')} - - handleCompare(ComparePeriod.FourWeeks)} - > - {translate('4.weeks')} - - {/* TODO: Add custom option. Compare is currently not ready for this. */} - - -
-
-

- {translate('sort')}: - -

- setCompareSortingDropdownOpen(current => !current)}> - - {getSortDisplayText()} - - - handleSorting(SortingOrder.Alphabetical)} - > - {translate('alphabetically')} - - handleSorting(SortingOrder.Ascending)} - > - {translate('ascending')} - - handleSorting(SortingOrder.Descending)} - > - {translate('descending')} - - - -
- +
+

+ {translate('sort')}: + +

+ handleSortingChange(e.target.value)} + > + + + + +
)}
); } const divTopBottomPadding: React.CSSProperties = { - paddingTop: '15px', + paddingTop: '0px', paddingBottom: '15px' }; @@ -279,10 +216,3 @@ const labelStyle: React.CSSProperties = { fontWeight: 'bold', margin: 0 }; - -const dropdownToggleStyle: React.CSSProperties = { - backgroundColor: '#ffffff', - color: '#000000', - border: '1px solid #ced4da', - boxShadow: 'none' -}; From 90f74c2a7d72801852de2132bba3ae6ffd84c34e Mon Sep 17 00:00:00 2001 From: danielshid Date: Fri, 9 Aug 2024 15:48:11 -0400 Subject: [PATCH 14/43] Reimplemented three separate components --- .../app/components/BarControlsComponent.tsx | 39 +++++ .../components/CompareControlsComponent.tsx | 64 +++++++ .../components/IntervalControlsComponent.tsx | 161 ++++++------------ .../app/components/MapControlsComponent.tsx | 19 +++ .../app/components/UIOptionsComponent.tsx | 10 +- 5 files changed, 184 insertions(+), 109 deletions(-) create mode 100644 src/client/app/components/BarControlsComponent.tsx create mode 100644 src/client/app/components/CompareControlsComponent.tsx create mode 100644 src/client/app/components/MapControlsComponent.tsx diff --git a/src/client/app/components/BarControlsComponent.tsx b/src/client/app/components/BarControlsComponent.tsx new file mode 100644 index 000000000..bdf2bc4eb --- /dev/null +++ b/src/client/app/components/BarControlsComponent.tsx @@ -0,0 +1,39 @@ +/* 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 React from 'react'; +import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; +import { graphSlice, selectBarStacking } from '../redux/slices/graphSlice'; +import translate from '../utils/translate'; +import TooltipMarkerComponent from './TooltipMarkerComponent' +import IntervalControlsComponent from './IntervalControlsComponent'; + +/** + * @returns controls for bar page. + */ +export default function BarControlsComponent() { + const dispatch = useAppDispatch(); + + const barStacking = useAppSelector(selectBarStacking); + + const handleChangeBarStacking = () => { + dispatch(graphSlice.actions.changeBarStacking()); + }; + + return ( +
+
+ + + +
+ {} +
+ ); +} + +const divTopBottomPadding: React.CSSProperties = { + paddingTop: '0px', + paddingBottom: '15px' +}; diff --git a/src/client/app/components/CompareControlsComponent.tsx b/src/client/app/components/CompareControlsComponent.tsx new file mode 100644 index 000000000..9c78a0e56 --- /dev/null +++ b/src/client/app/components/CompareControlsComponent.tsx @@ -0,0 +1,64 @@ +/* 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 React from 'react'; +import { Input } from 'reactstrap'; +import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; +import { selectChartToRender, graphSlice, selectSortingOrder } from '../redux/slices/graphSlice'; +import { ChartTypes } from '../types/redux/graph'; +import { SortingOrder } from '../utils/calculateCompare'; +import translate from '../utils/translate'; +import TooltipMarkerComponent from './TooltipMarkerComponent' +import IntervalControlsComponent from './IntervalControlsComponent'; + +/** + * @returns controls for bar page. + */ +export default function CompareControlsComponent() { + const dispatch = useAppDispatch(); + + const chartType = useAppSelector(selectChartToRender); + + // This is the current sorting order for graphic + const compareSortingOrder = chartType === ChartTypes.compare ? useAppSelector(selectSortingOrder) : undefined; + + // Updates sorting order when the sort order menu is used. + const handleSortingChange = (value: string) => { + if (chartType === ChartTypes.compare) { + const sortingOrder = value as unknown as SortingOrder; + dispatch(graphSlice.actions.changeCompareSortingOrder(sortingOrder)); + } + }; + + return ( +
+ {} +
+

+ {translate('sort')}: + +

+ handleSortingChange(e.target.value)} + > + + + + +
+
+ ); +} + +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 index 59a5b12d1..027791a94 100644 --- a/src/client/app/components/IntervalControlsComponent.tsx +++ b/src/client/app/components/IntervalControlsComponent.tsx @@ -7,19 +7,14 @@ 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, - selectBarStacking, selectWidthDays, - selectComparePeriod, selectSortingOrder -} from '../redux/slices/graphSlice'; +import { selectChartToRender, graphSlice, selectWidthDays, selectComparePeriod } from '../redux/slices/graphSlice'; import { ChartTypes } from '../types/redux/graph'; -import { ComparePeriod, SortingOrder } from '../utils/calculateCompare'; +import { ComparePeriod } from '../utils/calculateCompare'; import translate from '../utils/translate'; -import MapChartSelectComponent from './MapChartSelectComponent'; import TooltipMarkerComponent from './TooltipMarkerComponent'; /** - * @returns controls for the bar, map, and compare pages + * @returns interval controls for the bar, map, and compare pages */ export default function IntervalControlsComponent() { const dispatch = useAppDispatch(); @@ -33,11 +28,8 @@ export default function IntervalControlsComponent() { // This is the current interval for the bar and map graphics. const duration = useAppSelector(selectWidthDays); - const barStacking = chartType === ChartTypes.bar ? useAppSelector(selectBarStacking) : undefined; // This is the current compare period for graphic const comparePeriod = chartType === ChartTypes.compare ? useAppSelector(selectComparePeriod) : undefined; - // This is the current sorting order for graphic - const compareSortingOrder = chartType === ChartTypes.compare ? useAppSelector(selectSortingOrder) : undefined; // Holds the value of standard duration choices used for bar and map, decoupled from custom. const [days, setDays] = React.useState(duration.asDays().toString()); @@ -47,12 +39,6 @@ export default function IntervalControlsComponent() { // True if custom duration input for bar or map is active. const [showCustomDuration, setShowCustomDuration] = React.useState(false); - const handleChangeBarStacking = () => { - if (chartType === ChartTypes.bar) { - 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(() => { @@ -113,96 +99,61 @@ export default function IntervalControlsComponent() { } }; - // Updates sorting order when the sort order menu is used. - const handleSortingChange = (value: string) => { - if (chartType === ChartTypes.compare) { - const sortingOrder = value as unknown as SortingOrder; - dispatch(graphSlice.actions.changeCompareSortingOrder(sortingOrder)); - } - }; - return (
- {chartType === ChartTypes.bar && ( -
- - - -
- )} - {(chartType === ChartTypes.bar || chartType === ChartTypes.map || chartType === ChartTypes.compare) && ( -
-

- {(chartType === ChartTypes.bar && translate('bar.interval')) || - (chartType === ChartTypes.map && translate('map.interval')) || - (chartType === ChartTypes.compare && translate('compare.period'))}: - -

- chartType === ChartTypes.compare ? handleComparePeriodChange(e.target.value) : handleDaysChange(e.target.value)} - > - {chartType !== ChartTypes.compare && ( - <> - - - - - - )} - {chartType === ChartTypes.compare && ( - <> - - - - - )} - - {/* TODO: Compare is currently not ready for a custom option. */} - {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)} /> - - - - - } -
- )} - {chartType === ChartTypes.map && } - {chartType === ChartTypes.compare && ( -
-

- {translate('sort')}: - -

- handleSortingChange(e.target.value)} - > - - - - -
- )} +
+

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

+ chartType === ChartTypes.compare ? handleComparePeriodChange(e.target.value) : handleDaysChange(e.target.value)} + > + {chartType !== ChartTypes.compare && ( + <> + + + + + + )} + {chartType === ChartTypes.compare && ( + <> + + + + + )} + + {/* TODO: Compare is currently not ready for a custom option. */} + {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)} /> + + + + + } +
); } diff --git a/src/client/app/components/MapControlsComponent.tsx b/src/client/app/components/MapControlsComponent.tsx new file mode 100644 index 000000000..958cfcbfb --- /dev/null +++ b/src/client/app/components/MapControlsComponent.tsx @@ -0,0 +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/. */ + +import * as React from 'react'; +import IntervalControlsComponent from './IntervalControlsComponent'; +import MapChartSelectComponent from './MapChartSelectComponent'; + +/** + * @returns controls for bar page. + */ +export default function MapControlsComponent() { + return ( +
+ {} + {} +
+ ); +} diff --git a/src/client/app/components/UIOptionsComponent.tsx b/src/client/app/components/UIOptionsComponent.tsx index 03b25ba63..40d2f18b8 100644 --- a/src/client/app/components/UIOptionsComponent.tsx +++ b/src/client/app/components/UIOptionsComponent.tsx @@ -7,10 +7,12 @@ import ReactTooltip from 'react-tooltip'; import { useAppSelector } from '../redux/reduxHooks'; import { selectChartToRender, selectSelectedGroups, selectSelectedMeters } from '../redux/slices/graphSlice'; import { ChartTypes } from '../types/redux/graph'; +import BarControlsComponent from './BarControlsComponent'; import ChartDataSelectComponent from './ChartDataSelectComponent'; import ChartSelectComponent from './ChartSelectComponent'; +import CompareControlsComponent from './CompareControlsComponent'; import DateRangeComponent from './DateRangeComponent'; -import IntervalControlsComponent from './IntervalControlsComponent'; +import MapControlsComponent from './MapControlsComponent'; import ReadingsPerDaySelectComponent from './ReadingsPerDaySelectComponent'; import MoreOptionsComponent from './MoreOptionsComponent'; @@ -63,13 +65,13 @@ export default function UIOptionsComponent() { {chartToRender == ChartTypes.line} {/* UI options for bar graphic */} - {chartToRender == ChartTypes.bar && } + {chartToRender == ChartTypes.bar && } {/* UI options for compare graphic */} - {chartToRender == ChartTypes.compare && } + {chartToRender == ChartTypes.compare && } {/* UI options for map graphic */} - {chartToRender == ChartTypes.map && } + {chartToRender == ChartTypes.map && } {/* UI options for 3D graphic */} {chartToRender == ChartTypes.threeD && } From c4204acb14cdddf59310934cce983542bebbc4b1 Mon Sep 17 00:00:00 2001 From: danielshid Date: Fri, 9 Aug 2024 15:53:42 -0400 Subject: [PATCH 15/43] corrected function comments and format docs --- src/client/app/components/CompareControlsComponent.tsx | 2 +- src/client/app/components/MapControlsComponent.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/app/components/CompareControlsComponent.tsx b/src/client/app/components/CompareControlsComponent.tsx index 9c78a0e56..1aff5b491 100644 --- a/src/client/app/components/CompareControlsComponent.tsx +++ b/src/client/app/components/CompareControlsComponent.tsx @@ -13,7 +13,7 @@ import TooltipMarkerComponent from './TooltipMarkerComponent' import IntervalControlsComponent from './IntervalControlsComponent'; /** - * @returns controls for bar page. + * @returns controls for compare page. */ export default function CompareControlsComponent() { const dispatch = useAppDispatch(); diff --git a/src/client/app/components/MapControlsComponent.tsx b/src/client/app/components/MapControlsComponent.tsx index 958cfcbfb..ecc496f04 100644 --- a/src/client/app/components/MapControlsComponent.tsx +++ b/src/client/app/components/MapControlsComponent.tsx @@ -7,7 +7,7 @@ import IntervalControlsComponent from './IntervalControlsComponent'; import MapChartSelectComponent from './MapChartSelectComponent'; /** - * @returns controls for bar page. + * @returns controls for map page. */ export default function MapControlsComponent() { return ( From 2415d7ad06e86d6f643615cdf6601ca70af8db1a Mon Sep 17 00:00:00 2001 From: danielshid Date: Fri, 9 Aug 2024 15:53:57 -0400 Subject: [PATCH 16/43] format doc --- .../components/CompareControlsComponent.tsx | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/src/client/app/components/CompareControlsComponent.tsx b/src/client/app/components/CompareControlsComponent.tsx index 1aff5b491..711a72bd1 100644 --- a/src/client/app/components/CompareControlsComponent.tsx +++ b/src/client/app/components/CompareControlsComponent.tsx @@ -21,44 +21,44 @@ export default function CompareControlsComponent() { const chartType = useAppSelector(selectChartToRender); // This is the current sorting order for graphic - const compareSortingOrder = chartType === ChartTypes.compare ? useAppSelector(selectSortingOrder) : undefined; + const compareSortingOrder = chartType === ChartTypes.compare ? useAppSelector(selectSortingOrder) : undefined; // Updates sorting order when the sort order menu is used. - const handleSortingChange = (value: string) => { - if (chartType === ChartTypes.compare) { - const sortingOrder = value as unknown as SortingOrder; - dispatch(graphSlice.actions.changeCompareSortingOrder(sortingOrder)); - } - }; + const handleSortingChange = (value: string) => { + if (chartType === ChartTypes.compare) { + const sortingOrder = value as unknown as SortingOrder; + dispatch(graphSlice.actions.changeCompareSortingOrder(sortingOrder)); + } + }; return (
{}
-

- {translate('sort')}: - -

- handleSortingChange(e.target.value)} - > - - - - -
+

+ {translate('sort')}: + +

+ handleSortingChange(e.target.value)} + > + + + + +
); } const divTopBottomPadding: React.CSSProperties = { - paddingTop: '0px', - paddingBottom: '15px' + paddingTop: '0px', + paddingBottom: '15px' }; const labelStyle: React.CSSProperties = { - fontWeight: 'bold', - margin: 0 + fontWeight: 'bold', + margin: 0 }; From 161a7b50167555edec6a125344fde3c751818d95 Mon Sep 17 00:00:00 2001 From: danielshid Date: Fri, 9 Aug 2024 16:00:47 -0400 Subject: [PATCH 17/43] removed conditionals for comparecontrols --- .../app/components/CompareControlsComponent.tsx | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/client/app/components/CompareControlsComponent.tsx b/src/client/app/components/CompareControlsComponent.tsx index 711a72bd1..0be402283 100644 --- a/src/client/app/components/CompareControlsComponent.tsx +++ b/src/client/app/components/CompareControlsComponent.tsx @@ -5,8 +5,7 @@ import * as React from 'react'; import { Input } from 'reactstrap'; import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; -import { selectChartToRender, graphSlice, selectSortingOrder } from '../redux/slices/graphSlice'; -import { ChartTypes } from '../types/redux/graph'; +import { graphSlice, selectSortingOrder } from '../redux/slices/graphSlice'; import { SortingOrder } from '../utils/calculateCompare'; import translate from '../utils/translate'; import TooltipMarkerComponent from './TooltipMarkerComponent' @@ -18,17 +17,13 @@ import IntervalControlsComponent from './IntervalControlsComponent'; export default function CompareControlsComponent() { const dispatch = useAppDispatch(); - const chartType = useAppSelector(selectChartToRender); - // This is the current sorting order for graphic - const compareSortingOrder = chartType === ChartTypes.compare ? useAppSelector(selectSortingOrder) : undefined; - + const compareSortingOrder = useAppSelector(selectSortingOrder); + // Updates sorting order when the sort order menu is used. const handleSortingChange = (value: string) => { - if (chartType === ChartTypes.compare) { - const sortingOrder = value as unknown as SortingOrder; - dispatch(graphSlice.actions.changeCompareSortingOrder(sortingOrder)); - } + const sortingOrder = value as unknown as SortingOrder; + dispatch(graphSlice.actions.changeCompareSortingOrder(sortingOrder)); }; return ( From 80a11d6f7016333763c8ac5021f8a58899bea884 Mon Sep 17 00:00:00 2001 From: danielshid Date: Sat, 10 Aug 2024 18:51:34 -0400 Subject: [PATCH 18/43] covert indentation to tabs --- .../components/CompareControlsComponent.tsx | 66 +++++++++---------- .../app/components/MapControlsComponent.tsx | 12 ++-- 2 files changed, 39 insertions(+), 39 deletions(-) diff --git a/src/client/app/components/CompareControlsComponent.tsx b/src/client/app/components/CompareControlsComponent.tsx index 0be402283..bf6e3480a 100644 --- a/src/client/app/components/CompareControlsComponent.tsx +++ b/src/client/app/components/CompareControlsComponent.tsx @@ -15,45 +15,45 @@ import IntervalControlsComponent from './IntervalControlsComponent'; * @returns controls for compare page. */ export default function CompareControlsComponent() { - const dispatch = useAppDispatch(); + const dispatch = useAppDispatch(); - // This is the current sorting order for graphic - const compareSortingOrder = useAppSelector(selectSortingOrder); - - // 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)); - }; + // This is the current sorting order for graphic + const compareSortingOrder = useAppSelector(selectSortingOrder); - return ( -
- {} -
-

- {translate('sort')}: - -

- handleSortingChange(e.target.value)} - > - - - - -
-
- ); + // 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 ( +
+ {} +
+

+ {translate('sort')}: + +

+ handleSortingChange(e.target.value)} + > + + + + +
+
+ ); } const divTopBottomPadding: React.CSSProperties = { - paddingTop: '0px', - paddingBottom: '15px' + paddingTop: '0px', + paddingBottom: '15px' }; const labelStyle: React.CSSProperties = { - fontWeight: 'bold', - margin: 0 + fontWeight: 'bold', + margin: 0 }; diff --git a/src/client/app/components/MapControlsComponent.tsx b/src/client/app/components/MapControlsComponent.tsx index ecc496f04..536cc0744 100644 --- a/src/client/app/components/MapControlsComponent.tsx +++ b/src/client/app/components/MapControlsComponent.tsx @@ -10,10 +10,10 @@ import MapChartSelectComponent from './MapChartSelectComponent'; * @returns controls for map page. */ export default function MapControlsComponent() { - return ( -
- {} - {} -
- ); + return ( +
+ {} + {} +
+ ); } From e427b882ef410c3306b61cc949cf5a3399021e39 Mon Sep 17 00:00:00 2001 From: danielshid Date: Sat, 10 Aug 2024 18:55:08 -0400 Subject: [PATCH 19/43] added missing semicolons --- src/client/app/components/BarControlsComponent.tsx | 2 +- src/client/app/components/CompareControlsComponent.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/app/components/BarControlsComponent.tsx b/src/client/app/components/BarControlsComponent.tsx index bdf2bc4eb..58f1b4fca 100644 --- a/src/client/app/components/BarControlsComponent.tsx +++ b/src/client/app/components/BarControlsComponent.tsx @@ -6,7 +6,7 @@ import * as React from 'react'; import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; import { graphSlice, selectBarStacking } from '../redux/slices/graphSlice'; import translate from '../utils/translate'; -import TooltipMarkerComponent from './TooltipMarkerComponent' +import TooltipMarkerComponent from './TooltipMarkerComponent'; import IntervalControlsComponent from './IntervalControlsComponent'; /** diff --git a/src/client/app/components/CompareControlsComponent.tsx b/src/client/app/components/CompareControlsComponent.tsx index bf6e3480a..7a22d2cf3 100644 --- a/src/client/app/components/CompareControlsComponent.tsx +++ b/src/client/app/components/CompareControlsComponent.tsx @@ -8,7 +8,7 @@ import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; import { graphSlice, selectSortingOrder } from '../redux/slices/graphSlice'; import { SortingOrder } from '../utils/calculateCompare'; import translate from '../utils/translate'; -import TooltipMarkerComponent from './TooltipMarkerComponent' +import TooltipMarkerComponent from './TooltipMarkerComponent'; import IntervalControlsComponent from './IntervalControlsComponent'; /** From 6032a9665fb2bf7546ffc4cd8af2c3ee889300c8 Mon Sep 17 00:00:00 2001 From: danielshid Date: Mon, 12 Aug 2024 17:44:56 -0400 Subject: [PATCH 20/43] "made interval component cleaner" --- .../components/IntervalControlsComponent.tsx | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/src/client/app/components/IntervalControlsComponent.tsx b/src/client/app/components/IntervalControlsComponent.tsx index 027791a94..a7e281708 100644 --- a/src/client/app/components/IntervalControlsComponent.tsx +++ b/src/client/app/components/IntervalControlsComponent.tsx @@ -91,13 +91,20 @@ export default function IntervalControlsComponent() { } }; - // Updates values when the compare period menu is used. - const handleComparePeriodChange = (value: string) => { - if (chartType === ChartTypes.compare) { - const period = value as unknown as ComparePeriod; - dispatch(graphSlice.actions.updateComparePeriod({ comparePeriod: period, currentTime: moment() })); - } - }; + // Define the initial set of options for the dropdown when the chart type is not 'compare'. + const options = [ + { label: 'day', value: '1' }, + { label: 'week', value: '7' }, + { label: '4.weeks', value: '28' } + ]; + + // If the chart type is 'compare', we change the options to reflect comparison periods. + if (chartType === ChartTypes.compare) { + options.length = 0; // Clear the previous options + Object.entries(ComparePeriod).forEach(([key, value]) => { + options.push({ label: key, value: value.toString() }); + }); + } return (
@@ -117,23 +124,16 @@ export default function IntervalControlsComponent() { name='durationDays' type='select' value={chartType === ChartTypes.compare ? comparePeriod?.toString() : days} - onChange={e => chartType === ChartTypes.compare ? handleComparePeriodChange(e.target.value) : handleDaysChange(e.target.value)} + onChange={e => handleDaysChange(e.target.value)} > - {chartType !== ChartTypes.compare && ( - <> - - - - - - )} - {chartType === ChartTypes.compare && ( - <> - - - - - )} + {options.map(option => ( + + ))} + {/* TODO: Compare is currently not ready for a custom option. */} {showCustomDuration && chartType !== ChartTypes.compare && From 396f48c73fb69df2125d4a3309c481fdba477bfc Mon Sep 17 00:00:00 2001 From: danielshid Date: Mon, 16 Sep 2024 21:47:03 -0400 Subject: [PATCH 21/43] consistency changes to IntervalControlsComponent.tsx --- .../components/IntervalControlsComponent.tsx | 78 +++++++++++-------- src/client/app/translations/data.ts | 2 +- 2 files changed, 46 insertions(+), 34 deletions(-) diff --git a/src/client/app/components/IntervalControlsComponent.tsx b/src/client/app/components/IntervalControlsComponent.tsx index a7e281708..5f59d893a 100644 --- a/src/client/app/components/IntervalControlsComponent.tsx +++ b/src/client/app/components/IntervalControlsComponent.tsx @@ -14,7 +14,7 @@ import translate from '../utils/translate'; import TooltipMarkerComponent from './TooltipMarkerComponent'; /** - * @returns interval controls for the bar, map, and compare pages + * @returns Interval controls for the bar, map, and compare pages */ export default function IntervalControlsComponent() { const dispatch = useAppDispatch(); @@ -44,7 +44,7 @@ export default function IntervalControlsComponent() { 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 == duration.asDays().toString())); + const isCustom = !['1', '7', '28'].includes(duration.asDays().toString()); setShowCustomDuration(isCustom); setDaysCustom(duration.asDays()); setDays(isCustom ? CUSTOM_INPUT : duration.asDays().toString()); @@ -60,8 +60,8 @@ export default function IntervalControlsComponent() { if (value === CUSTOM_INPUT) { // Set menu value from standard value to special value to show custom // and show the custom input area. - setDays(CUSTOM_INPUT); setShowCustomDuration(true); + setDays(CUSTOM_INPUT); } else { // Set the standard menu value, hide the custom duration input // and duration for graphing. @@ -91,28 +91,43 @@ export default function IntervalControlsComponent() { } }; - // Define the initial set of options for the dropdown when the chart type is not 'compare'. - const options = [ - { label: 'day', value: '1' }, - { label: 'week', value: '7' }, - { label: '4.weeks', value: '28' } - ]; - - // If the chart type is 'compare', we change the options to reflect comparison periods. - if (chartType === ChartTypes.compare) { - options.length = 0; // Clear the previous options - Object.entries(ComparePeriod).forEach(([key, value]) => { - options.push({ label: key, value: value.toString() }); - }); - } + // 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' + }; + + // Defines the set of options for the dropdown + const options = chartType === ChartTypes.compare + ? Object.entries(ComparePeriod).map(([key, value]) => ( + + )) + : ( + <> + + + + + + ); return (

- {(chartType === ChartTypes.bar && translate('bar.interval')) || - (chartType === ChartTypes.map && translate('map.interval')) || - (chartType === ChartTypes.compare && translate('compare.period'))}: + {translate( + chartType === ChartTypes.bar ? 'bar.interval' : + chartType === ChartTypes.map ? 'map.interval' : + 'compare.period' + )}: handleDaysChange(e.target.value)} + onChange={e => chartType === ChartTypes.compare ? handleComparePeriodChange(e.target.value) : handleDaysChange(e.target.value)} > - {options.map(option => ( - - ))} - + {options} {/* TODO: Compare is currently not ready for a custom option. */} {showCustomDuration && chartType !== ChartTypes.compare && - - {translate('days.enter')}: + handleCustomDaysChange(Number(e.target.value))} // This grabs each key hit and then finishes input when hit enter. - onKeyDown={e => { handleEnter(e.key); }} + onKeyDown={e => handleEnter(e.key)} step='1' min={MIN_DAYS} max={MAX_DAYS} value={daysCustom} - invalid={!daysValid(daysCustom)} /> + invalid={!daysValid(daysCustom)} + /> diff --git a/src/client/app/translations/data.ts b/src/client/app/translations/data.ts index 76a21485f..aaf65d524 100644 --- a/src/client/app/translations/data.ts +++ b/src/client/app/translations/data.ts @@ -29,7 +29,7 @@ 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", + "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", From c83fa57b45d9796c7be558d0fafda51607131f9f Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 24 Sep 2024 17:33:51 -0400 Subject: [PATCH 22/43] added bg15 --- src/server/test/web/readingsBarGroupFlow.js | 36 +++++++++++++------ .../test/web/readingsBarGroupQuantity.js | 17 +++++++++ 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/src/server/test/web/readingsBarGroupFlow.js b/src/server/test/web/readingsBarGroupFlow.js index 4544fc664..27a1ef9e5 100644 --- a/src/server/test/web/readingsBarGroupFlow.js +++ b/src/server/test/web/readingsBarGroupFlow.js @@ -9,16 +9,16 @@ const { chai, mocha, app } = require('../common'); const Unit = require('../../models/Unit'); -// const { prepareTest, -// parseExpectedCsv, -// createTimeString, -// expectReadingToEqualExpected, -// getUnitId, -// ETERNITY, -// METER_ID, -// unitDatakWh, -// conversionDatakWh, -// meterDatakWh } = require('../../util/readingsUtils'); +const { prepareTest, + parseExpectedCsv, + createTimeString, + expectReadingToEqualExpected, + getUnitId, + ETERNITY, + METER_ID, + unitDatakWh, + conversionDatakWh, + meterDatakWh } = require('../../util/readingsUtils'); mocha.describe('readings API', () => { mocha.describe('readings test, test if data returned by API is as expected', () => { @@ -26,6 +26,22 @@ mocha.describe('readings API', () => { mocha.describe('for quantity groups', () => { // Add BG15 here + mocha.it('BG15: should have daily points for 15 + 20 minute reading intervals and flow units with +-inf start/end time & kW as kW', async () =>{ + //load data into database + await prepareTest(unitDatakWh, conversionDatakWh, meterDatakWGroups, groupDatakWh); + //get unit ID since the DB could use any value. + const unitId = await getUnitId('kW'); + // Load the expected response data from the corresponding csv file + const expected = await parseExpectedCsv('src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kW_gu_kW_st_-inf_et_inf.csv'); + // Create a request to the API for unbounded reading times and save the response + const res = await chai.request(app).get(`/api/unitReadings/bar/groups/${GROUP_ID}`) + .query({ + timeInterval: ETERNITY.toString(), + barWidthDays: 'L6:N10', + graphicUnitId: unitId }); + // Check that the API reading is equal to what it is expected to equal + expectReadingToEqualExpected(res, expected, GROUP_ID); + }); // Add BG16 here diff --git a/src/server/test/web/readingsBarGroupQuantity.js b/src/server/test/web/readingsBarGroupQuantity.js index 776443704..d9f1922c6 100644 --- a/src/server/test/web/readingsBarGroupQuantity.js +++ b/src/server/test/web/readingsBarGroupQuantity.js @@ -101,6 +101,23 @@ mocha.describe('readings API', () => { expectReadingToEqualExpected(res, expected, GROUP_ID); }); // Add BG7 here + mocha.it('BG7: 13 day bars for 15 + 20 minute reading intervals and quantity units with reduced, partial days & kWh as kWh', async () =>{ + //load data into database + await prepareTest(unitDatakWh, conversionDatakWh, meterDatakWhGroups, groupDatakWh); + //get unit ID since the DB could use any value. + const unitId = await getUnitId('kWh'); + // Load the expected response data from the corresponding csv file + const expected = await parseExpectedCsv('src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kWh_gu_kWh_st_2022-08-20%07#25#35_et_2022-10-28%13#18#28_bd_13.csv'); + // Create a request to the API for unbounded reading times and save the response + const res = await chai.request(app).get(`/api/unitReadings/bar/groups/${GROUP_ID}`) + .query({ + timeInterval: ETERNITY.toString(), + barWidthDays: '13', + graphicUnitId: unitId }); + // Check that the API reading is equal to what it is expected to equal + expectReadingToEqualExpected(res, expected, GROUP_ID); + }); + // Add BG8 here From ac27dacae09a0c7e58fa01dba10825a80c855e8a Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 24 Sep 2024 17:45:10 -0400 Subject: [PATCH 23/43] restore bg7 --- src/server/test/web/readingsBarGroupQuantity.js | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/server/test/web/readingsBarGroupQuantity.js b/src/server/test/web/readingsBarGroupQuantity.js index d9f1922c6..776443704 100644 --- a/src/server/test/web/readingsBarGroupQuantity.js +++ b/src/server/test/web/readingsBarGroupQuantity.js @@ -101,23 +101,6 @@ mocha.describe('readings API', () => { expectReadingToEqualExpected(res, expected, GROUP_ID); }); // Add BG7 here - mocha.it('BG7: 13 day bars for 15 + 20 minute reading intervals and quantity units with reduced, partial days & kWh as kWh', async () =>{ - //load data into database - await prepareTest(unitDatakWh, conversionDatakWh, meterDatakWhGroups, groupDatakWh); - //get unit ID since the DB could use any value. - const unitId = await getUnitId('kWh'); - // Load the expected response data from the corresponding csv file - const expected = await parseExpectedCsv('src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kWh_gu_kWh_st_2022-08-20%07#25#35_et_2022-10-28%13#18#28_bd_13.csv'); - // Create a request to the API for unbounded reading times and save the response - const res = await chai.request(app).get(`/api/unitReadings/bar/groups/${GROUP_ID}`) - .query({ - timeInterval: ETERNITY.toString(), - barWidthDays: '13', - graphicUnitId: unitId }); - // Check that the API reading is equal to what it is expected to equal - expectReadingToEqualExpected(res, expected, GROUP_ID); - }); - // Add BG8 here From 6443142b6d31cb54dfb83fa225dfcdabec31f01a Mon Sep 17 00:00:00 2001 From: Nischita Nannapaneni Date: Tue, 24 Sep 2024 19:05:30 -0500 Subject: [PATCH 24/43] added code for the case that only meter is selected, the option selected should be shown as selected by default, without the user needing to click it. --- src/client/app/components/ThreeDPillComponent.tsx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/client/app/components/ThreeDPillComponent.tsx b/src/client/app/components/ThreeDPillComponent.tsx index a5c7b8ae2..9c946b310 100644 --- a/src/client/app/components/ThreeDPillComponent.tsx +++ b/src/client/app/components/ThreeDPillComponent.tsx @@ -3,6 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as React from 'react'; +import { useEffect } from 'react'; import { Badge } from 'reactstrap'; import { selectGraphState, selectThreeDState, updateThreeDMeterOrGroupInfo } from '../redux/slices/graphSlice'; import { selectGroupDataById } from '../redux/api/groupsApi'; @@ -40,6 +41,17 @@ export default function ThreeDPillComponent() { return { meterOrGroupID: groupID, isDisabled: isDisabled, meterOrGroup: MeterOrGroup.groups } as MeterOrGroupPill; }); + // when there is only one meter, it must be selected as a default (there is no other option) + useEffect(() => { + if (meterPillData.length === 1) { + const singleMeter = meterPillData[0]; + dispatch(updateThreeDMeterOrGroupInfo({ + meterOrGroupID: singleMeter.meterOrGroupID, + meterOrGroup: singleMeter.meterOrGroup + })); + } + }, [meterPillData, dispatch]); + // When a Pill Badge is clicked update threeD state to indicate new meter or group to render. const handlePillClick = (pillData: MeterOrGroupPill) => dispatch( updateThreeDMeterOrGroupInfo({ @@ -48,6 +60,7 @@ export default function ThreeDPillComponent() { }) ); + // Method Generates Reactstrap Pill Badges for selected meters or groups const populatePills = (meterOrGroupPillData: MeterOrGroupPill[]) => { return meterOrGroupPillData.map(pillData => { From 3910b1056a7a692ea0aa61dc0383e3f47fc6ae8f Mon Sep 17 00:00:00 2001 From: Andrew Date: Fri, 27 Sep 2024 16:33:20 -0400 Subject: [PATCH 25/43] Remove comment --- src/server/test/web/readingsBarGroupFlow.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/server/test/web/readingsBarGroupFlow.js b/src/server/test/web/readingsBarGroupFlow.js index 27a1ef9e5..4862f8367 100644 --- a/src/server/test/web/readingsBarGroupFlow.js +++ b/src/server/test/web/readingsBarGroupFlow.js @@ -24,8 +24,6 @@ mocha.describe('readings API', () => { mocha.describe('readings test, test if data returned by API is as expected', () => { mocha.describe('for bar charts', () => { mocha.describe('for quantity groups', () => { - - // Add BG15 here mocha.it('BG15: should have daily points for 15 + 20 minute reading intervals and flow units with +-inf start/end time & kW as kW', async () =>{ //load data into database await prepareTest(unitDatakWh, conversionDatakWh, meterDatakWGroups, groupDatakWh); From d4387c1f82d7c53f8baee748a5b1080976c31f9b Mon Sep 17 00:00:00 2001 From: Nischita Nannapaneni Date: Sun, 29 Sep 2024 23:27:46 -0500 Subject: [PATCH 26/43] generalized the function of the default selection effect so that it applies to not only meter but also group --- src/client/app/components/ThreeDPillComponent.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/client/app/components/ThreeDPillComponent.tsx b/src/client/app/components/ThreeDPillComponent.tsx index 9c946b310..dd450a3f6 100644 --- a/src/client/app/components/ThreeDPillComponent.tsx +++ b/src/client/app/components/ThreeDPillComponent.tsx @@ -41,16 +41,18 @@ export default function ThreeDPillComponent() { return { meterOrGroupID: groupID, isDisabled: isDisabled, meterOrGroup: MeterOrGroup.groups } as MeterOrGroupPill; }); - // when there is only one meter, it must be selected as a default (there is no other option) + // when there is only one choice, it must be selected as a default (there is no other option) useEffect(() => { - if (meterPillData.length === 1) { - const singleMeter = meterPillData[0]; + const combinedPillData = [...meterPillData, ...groupPillData]; + + if (combinedPillData.length === 1) { + const singlePill = combinedPillData[0]; dispatch(updateThreeDMeterOrGroupInfo({ - meterOrGroupID: singleMeter.meterOrGroupID, - meterOrGroup: singleMeter.meterOrGroup + meterOrGroupID: singlePill.meterOrGroupID, + meterOrGroup: singlePill.meterOrGroup })); } - }, [meterPillData, dispatch]); + }, [meterPillData, groupPillData, dispatch]); // When a Pill Badge is clicked update threeD state to indicate new meter or group to render. const handlePillClick = (pillData: MeterOrGroupPill) => dispatch( From 3117b16d0768d19c8455c83ea3a73e2857f2728f Mon Sep 17 00:00:00 2001 From: Gowrisankar Palanickal <156338043+shankarp05@users.noreply.github.com> Date: Tue, 1 Oct 2024 11:27:51 -0400 Subject: [PATCH 27/43] Adding code for Test BG10 --- .../test/web/readingsBarGroupQuantity.js | 19 +++++ ...5-20_mu_kWh_gu_BTU_st_-inf_et_inf_bd_1.csv | 76 +++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kWh_gu_BTU_st_-inf_et_inf_bd_1.csv diff --git a/src/server/test/web/readingsBarGroupQuantity.js b/src/server/test/web/readingsBarGroupQuantity.js index 8ffb5af7e..3fa411aeb 100644 --- a/src/server/test/web/readingsBarGroupQuantity.js +++ b/src/server/test/web/readingsBarGroupQuantity.js @@ -129,6 +129,7 @@ mocha.describe('readings API', () => { graphicUnitId: unitId }); // Check that the API reading is equal to what it is expected to equal expectReadingToEqualExpected(res, expected, GROUP_ID); + }); // Add BG7 here @@ -138,6 +139,24 @@ mocha.describe('readings API', () => { // Add BG10 here + mocha.it('BG10: 1 day bars for 15 + 20 minute reading intervals and quantity units with +-inf start/end time & kWh as BTU', async () =>{ + //load data into database + await prepareTest(unitDatakWh, conversionDatakWh, meterDatakWhGroups, groupDatakWh); + //get unit ID since the DB could use any value. + const unitId = await getUnitId('BTU'); + // Load the expected response data from the corresponding csv file + const expected = await parseExpectedCsv('src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kWh_gu_BTU_st_-inf_et_inf_bd_1.csv'); + // Create a request to the API for unbounded reading times and save the response + const res = await chai.request(app).get(`/api/unitReadings/bar/groups/${GROUP_ID}`) + .query({ + timeInterval: ETERNITY.toString(), + barWidthDays: '1', + graphicUnitId: unitId }); + // Check that the API reading is equal to what it is expected to equal + expectReadingToEqualExpected(res, expected, GROUP_ID); + + }); + // Add BG11 here // Add BG12 here diff --git a/src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kWh_gu_BTU_st_-inf_et_inf_bd_1.csv b/src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kWh_gu_BTU_st_-inf_et_inf_bd_1.csv new file mode 100644 index 000000000..1dd924a24 --- /dev/null +++ b/src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kWh_gu_BTU_st_-inf_et_inf_bd_1.csv @@ -0,0 +1,76 @@ +reading,start time,end time +29209193.0104336,2022-08-18 00:00:00,2022-08-19 00:00:00 +25795589.743482,2022-08-19 00:00:00,2022-08-20 00:00:00 +29531280.9199285,2022-08-20 00:00:00,2022-08-21 00:00:00 +28972901.8881714,2022-08-21 00:00:00,2022-08-22 00:00:00 +28031764.7371833,2022-08-22 00:00:00,2022-08-23 00:00:00 +28802134.8350591,2022-08-23 00:00:00,2022-08-24 00:00:00 +29089482.1874503,2022-08-24 00:00:00,2022-08-25 00:00:00 +28562754.7754511,2022-08-25 00:00:00,2022-08-26 00:00:00 +26641697.0795456,2022-08-26 00:00:00,2022-08-27 00:00:00 +28730608.5257597,2022-08-27 00:00:00,2022-08-28 00:00:00 +28379171.3854016,2022-08-28 00:00:00,2022-08-29 00:00:00 +26823146.8172871,2022-08-29 00:00:00,2022-08-30 00:00:00 +29302572.2308813,2022-08-30 00:00:00,2022-08-31 00:00:00 +28155331.1018422,2022-08-31 00:00:00,2022-09-01 00:00:00 +27510358.969585,2022-09-01 00:00:00,2022-09-02 00:00:00 +29694622.443881,2022-09-02 00:00:00,2022-09-03 00:00:00 +29773433.0568812,2022-09-03 00:00:00,2022-09-04 00:00:00 +28472315.1152601,2022-09-04 00:00:00,2022-09-05 00:00:00 +26551995.2139845,2022-09-05 00:00:00,2022-09-06 00:00:00 +28947223.2033972,2022-09-06 00:00:00,2022-09-07 00:00:00 +29063083.5525401,2022-09-07 00:00:00,2022-09-08 00:00:00 +27931330.0412951,2022-09-08 00:00:00,2022-09-09 00:00:00 +28781046.7149595,2022-09-09 00:00:00,2022-09-10 00:00:00 +28364055.9788512,2022-09-10 00:00:00,2022-09-11 00:00:00 +30696225.8079846,2022-09-11 00:00:00,2022-09-12 00:00:00 +28473699.6649624,2022-09-12 00:00:00,2022-09-13 00:00:00 +29283145.4581739,2022-09-13 00:00:00,2022-09-14 00:00:00 +27570212.0168767,2022-09-14 00:00:00,2022-09-15 00:00:00 +29023763.7911641,2022-09-15 00:00:00,2022-09-16 00:00:00 +27852817.1090061,2022-09-16 00:00:00,2022-09-17 00:00:00 +29411091.2920606,2022-09-17 00:00:00,2022-09-18 00:00:00 +29604980.7864712,2022-09-18 00:00:00,2022-09-19 00:00:00 +28154458.5434468,2022-09-19 00:00:00,2022-09-20 00:00:00 +31153760.1248859,2022-09-20 00:00:00,2022-09-21 00:00:00 +27666463.2860029,2022-09-21 00:00:00,2022-09-22 00:00:00 +29795563.315951,2022-09-22 00:00:00,2022-09-23 00:00:00 +27360276.2628425,2022-09-23 00:00:00,2022-09-24 00:00:00 +27311035.5953025,2022-09-24 00:00:00,2022-09-25 00:00:00 +29095555.0733551,2022-09-25 00:00:00,2022-09-26 00:00:00 +27726783.808572,2022-09-26 00:00:00,2022-09-27 00:00:00 +27508821.5107749,2022-09-27 00:00:00,2022-09-28 00:00:00 +28655569.414136,2022-09-28 00:00:00,2022-09-29 00:00:00 +29588411.6900755,2022-09-29 00:00:00,2022-09-30 00:00:00 +28738147.3845309,2022-09-30 00:00:00,2022-10-01 00:00:00 +28123963.9046944,2022-10-01 00:00:00,2022-10-02 00:00:00 +28765104.26081,2022-10-02 00:00:00,2022-10-03 00:00:00 +27627814.6610993,2022-10-03 00:00:00,2022-10-04 00:00:00 +28735441.8490875,2022-10-04 00:00:00,2022-10-05 00:00:00 +28285434.407459,2022-10-05 00:00:00,2022-10-06 00:00:00 +28929835.0244939,2022-10-06 00:00:00,2022-10-07 00:00:00 +29599004.1223681,2022-10-07 00:00:00,2022-10-08 00:00:00 +28122739.6885322,2022-10-08 00:00:00,2022-10-09 00:00:00 +26479928.0582859,2022-10-09 00:00:00,2022-10-10 00:00:00 +28355203.9271451,2022-10-10 00:00:00,2022-10-11 00:00:00 +28026344.6838043,2022-10-11 00:00:00,2022-10-12 00:00:00 +29621879.1555374,2022-10-12 00:00:00,2022-10-13 00:00:00 +29014963.8970632,2022-10-13 00:00:00,2022-10-14 00:00:00 +28542153.0589121,2022-10-14 00:00:00,2022-10-15 00:00:00 +27600877.2424139,2022-10-15 00:00:00,2022-10-16 00:00:00 +28194214.6361847,2022-10-16 00:00:00,2022-10-17 00:00:00 +28858052.7695064,2022-10-17 00:00:00,2022-10-18 00:00:00 +28153189.1046015,2022-10-18 00:00:00,2022-10-19 00:00:00 +27184913.6111465,2022-10-19 00:00:00,2022-10-20 00:00:00 +28752720.0055364,2022-10-20 00:00:00,2022-10-21 00:00:00 +29298144.0717527,2022-10-21 00:00:00,2022-10-22 00:00:00 +27754964.2877268,2022-10-22 00:00:00,2022-10-23 00:00:00 +28742598.2237095,2022-10-23 00:00:00,2022-10-24 00:00:00 +30205570.981724,2022-10-24 00:00:00,2022-10-25 00:00:00 +31181745.6220899,2022-10-25 00:00:00,2022-10-26 00:00:00 +27286030.6182213,2022-10-26 00:00:00,2022-10-27 00:00:00 +27517284.75375,2022-10-27 00:00:00,2022-10-28 00:00:00 +28828155.312223,2022-10-28 00:00:00,2022-10-29 00:00:00 +30702649.888834,2022-10-29 00:00:00,2022-10-30 00:00:00 +28494727.6464538,2022-10-30 00:00:00,2022-10-31 00:00:00 +26683896.1942727,2022-10-31 00:00:00,2022-11-01 00:00:00 From 4f425fc62e42227e868120a5fff2be2d80bed3b3 Mon Sep 17 00:00:00 2001 From: Gowrisankar Palanickal <156338043+shankarp05@users.noreply.github.com> Date: Tue, 1 Oct 2024 16:45:19 -0400 Subject: [PATCH 28/43] completed test BG10 --- .../test/web/readingsBarGroupQuantity.js | 50 ++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/src/server/test/web/readingsBarGroupQuantity.js b/src/server/test/web/readingsBarGroupQuantity.js index 3fa411aeb..05c198940 100644 --- a/src/server/test/web/readingsBarGroupQuantity.js +++ b/src/server/test/web/readingsBarGroupQuantity.js @@ -140,8 +140,56 @@ mocha.describe('readings API', () => { // Add BG10 here mocha.it('BG10: 1 day bars for 15 + 20 minute reading intervals and quantity units with +-inf start/end time & kWh as BTU', async () =>{ + const unitData = unitDatakWh.concat([ + { + // u3 + name: 'MJ', + identifier: 'megaJoules', + unitRepresent: Unit.unitRepresentType.QUANTITY, + secInRate: 3600, + typeOfUnit: Unit.unitType.UNIT, + suffix: '', + displayable: Unit.displayableType.ALL, + preferredDisplay: false, + note: 'MJ' + }, + { + // u16 + name: 'BTU', + identifier: '', + unitRepresent: Unit.unitRepresentType.QUANTITY, + secInRate: 3600, + typeOfUnit: Unit.unitType.UNIT, + suffix: '', + displayable: Unit.displayableType.ALL, + preferredDisplay: true, + note: 'OED created standard unit' + } + ]); + + const conversionData = conversionDatakWh.concat([ + { + // c2 + sourceName: 'kWh', + destinationName: 'MJ', + bidirectional: true, + slope: 3.6, + intercept: 0, + note: 'kWh → MJ' + }, + { + // c3 + sourceName: 'MJ', + destinationName: 'BTU', + bidirectional: true, + slope: 947.8, + intercept: 0, + note: 'MJ → BTU' + } + ]); + //load data into database - await prepareTest(unitDatakWh, conversionDatakWh, meterDatakWhGroups, groupDatakWh); + await prepareTest(unitData, conversionData, meterDatakWhGroups, groupDatakWh); //get unit ID since the DB could use any value. const unitId = await getUnitId('BTU'); // Load the expected response data from the corresponding csv file From 5a5103dfea680fa1805864b35341751e4bc2ea85 Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 1 Oct 2024 17:34:55 -0400 Subject: [PATCH 29/43] BG15 passed test --- src/server/app.js | 1 + src/server/test/web/readingsBarGroupFlow.js | 83 ++++++++++++++++++- ...15-20_mu_kW_gu_kW_st_-inf_et_inf_bd_13.csv | 6 ++ 3 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kW_gu_kW_st_-inf_et_inf_bd_13.csv diff --git a/src/server/app.js b/src/server/app.js index 58774f9cc..72763cae4 100644 --- a/src/server/app.js +++ b/src/server/app.js @@ -39,6 +39,7 @@ const ciks = require('./routes/ciks'); // Create a limit of 200 requests/5 seconds const generalLimiter = rateLimit({ windowMs: 5 * 1000, // 5 seconds + //limit: 200, // 200 requests limit: 200, // 200 requests standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers legacyHeaders: false // Disable the `X-RateLimit-*` headers diff --git a/src/server/test/web/readingsBarGroupFlow.js b/src/server/test/web/readingsBarGroupFlow.js index 4862f8367..c4a429f90 100644 --- a/src/server/test/web/readingsBarGroupFlow.js +++ b/src/server/test/web/readingsBarGroupFlow.js @@ -16,6 +16,7 @@ const { prepareTest, getUnitId, ETERNITY, METER_ID, + GROUP_ID, unitDatakWh, conversionDatakWh, meterDatakWh } = require('../../util/readingsUtils'); @@ -24,18 +25,92 @@ mocha.describe('readings API', () => { mocha.describe('readings test, test if data returned by API is as expected', () => { mocha.describe('for bar charts', () => { mocha.describe('for quantity groups', () => { - mocha.it('BG15: should have daily points for 15 + 20 minute reading intervals and flow units with +-inf start/end time & kW as kW', async () =>{ + mocha.it('BG15: expected_bar_group_ri_15-20_mu_kW_gu_kW_st_-inf_et_inf_bd_13.csv', async () =>{ + const unitDatakW = [ + { + // u4 + name: 'kW', + identifier: '', + unitRepresent: Unit.unitRepresentType.FLOW, + secInRate: 3600, + typeOfUnit: Unit.unitType.UNIT, + suffix: '', + displayable: Unit.displayableType.ALL, + preferredDisplay: true, + note: 'kilowatts' + }, + { + // u5 + name: 'Electric', + identifier: '', + unitRepresent: Unit.unitRepresentType.FLOW, + secInRate: 3600, + typeOfUnit: Unit.unitType.METER, + suffix: '', + displayable: Unit.displayableType.NONE, + preferredDisplay: false, + note: 'special unit' + }, + ]; + const conversionDatakW = [ + { + // c4 + sourceName: 'Electric', + destinationName: 'kW', + bidirectional: false, + slope: 1, + intercept: 0, + note: 'Electric → kW' + } + ]; + const meterDatakWGroups = [ + { + name: 'meterDatakWGroups', + unit: 'Electric', + defaultGraphicUnit: 'kW', + displayable: true, + gps: undefined, + note: 'special meter', + file: 'test/web/readingsData/readings_ri_15_days_75.csv', + deleteFile: false, + readingFrequency: '15 minutes', + id: METER_ID + }, + { + name: 'meterDatakWOther', + unit: 'Electric', + defaultGraphicUnit: 'kW', + displayable: true, + gps: undefined, + note: 'special meter', + file: 'test/web/readingsData/readings_ri_20_days_75.csv', + deleteFile: false, + readingFrequency: '20 minutes', + id: (METER_ID + 1) + } + ]; + const groupDatakW = [ + { + id: GROUP_ID, + name: 'meterDatakWGroups + meterDatakWOther', + displayable: true, + note: 'special group', + defaultGraphicUnit: 'kW', + childMeters: ['meterDatakWGroups', 'meterDatakWOther'], + childGroups: [], + } + ] //load data into database - await prepareTest(unitDatakWh, conversionDatakWh, meterDatakWGroups, groupDatakWh); + await prepareTest(unitDatakW, conversionDatakW, meterDatakWGroups, groupDatakW); //get unit ID since the DB could use any value. const unitId = await getUnitId('kW'); // Load the expected response data from the corresponding csv file - const expected = await parseExpectedCsv('src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kW_gu_kW_st_-inf_et_inf.csv'); + const expected = await parseExpectedCsv('src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kW_gu_kW_st_-inf_et_inf_bd_13.csv'); // Create a request to the API for unbounded reading times and save the response const res = await chai.request(app).get(`/api/unitReadings/bar/groups/${GROUP_ID}`) .query({ timeInterval: ETERNITY.toString(), - barWidthDays: 'L6:N10', + barWidthDays: '13', graphicUnitId: unitId }); // Check that the API reading is equal to what it is expected to equal expectReadingToEqualExpected(res, expected, GROUP_ID); diff --git a/src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kW_gu_kW_st_-inf_et_inf_bd_13.csv b/src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kW_gu_kW_st_-inf_et_inf_bd_13.csv new file mode 100644 index 000000000..36b6793fc --- /dev/null +++ b/src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kW_gu_kW_st_-inf_et_inf_bd_13.csv @@ -0,0 +1,6 @@ +reading,start time,end time +30985.6824778472,2022-08-28 00:00:00,2022-09-10 00:00:00 +31584.4206007844,2022-09-10 00:00:00,2022-09-23 00:00:00 +30786.7836050569,2022-09-23 00:00:00,2022-10-06 00:00:00 +30964.873496156,2022-10-06 00:00:00,2022-10-19 00:00:00 +31216.5658075156,2022-10-19 00:00:00,2022-11-01 00:00:00 From ba17e1906f18e199ac4e55b96cf9d0d78e3ecb82 Mon Sep 17 00:00:00 2001 From: Gowrisankar Palanickal <156338043+shankarp05@users.noreply.github.com> Date: Thu, 3 Oct 2024 10:34:06 -0400 Subject: [PATCH 30/43] fixed minor requested changes --- src/server/test/web/readingsBarGroupQuantity.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/server/test/web/readingsBarGroupQuantity.js b/src/server/test/web/readingsBarGroupQuantity.js index 05c198940..bfe6cc59b 100644 --- a/src/server/test/web/readingsBarGroupQuantity.js +++ b/src/server/test/web/readingsBarGroupQuantity.js @@ -129,7 +129,6 @@ mocha.describe('readings API', () => { graphicUnitId: unitId }); // Check that the API reading is equal to what it is expected to equal expectReadingToEqualExpected(res, expected, GROUP_ID); - }); // Add BG7 here @@ -137,8 +136,6 @@ mocha.describe('readings API', () => { // Add BG9 here - // Add BG10 here - mocha.it('BG10: 1 day bars for 15 + 20 minute reading intervals and quantity units with +-inf start/end time & kWh as BTU', async () =>{ const unitData = unitDatakWh.concat([ { @@ -202,7 +199,6 @@ mocha.describe('readings API', () => { graphicUnitId: unitId }); // Check that the API reading is equal to what it is expected to equal expectReadingToEqualExpected(res, expected, GROUP_ID); - }); // Add BG11 here From f63dc14cb9f8fb61893e27265980d35527bdc9d9 Mon Sep 17 00:00:00 2001 From: nicholasyfu1 Date: Thu, 3 Oct 2024 18:27:10 -0400 Subject: [PATCH 31/43] Added BG12 and expected csv file --- .../test/web/readingsBarGroupQuantity.js | 63 +++++++++++++++ ...20_mu_kWh_gu_kgCO2_st_-inf_et_inf_bd_1.csv | 76 +++++++++++++++++++ 2 files changed, 139 insertions(+) create mode 100644 src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kWh_gu_kgCO2_st_-inf_et_inf_bd_1.csv diff --git a/src/server/test/web/readingsBarGroupQuantity.js b/src/server/test/web/readingsBarGroupQuantity.js index bfe6cc59b..944e6786f 100644 --- a/src/server/test/web/readingsBarGroupQuantity.js +++ b/src/server/test/web/readingsBarGroupQuantity.js @@ -204,6 +204,69 @@ mocha.describe('readings API', () => { // Add BG11 here // Add BG12 here + mocha.it('BG12: 1 day bars for 15 + 20 minute reading intervals and quantity units with +-inf start/end time & kWh as kg of CO2', async () =>{ + const unitData = unitDatakWh.concat([ + { + // u10 + name: 'kg', + identifier: '', + unitRepresent: Unit.unitRepresentType.QUANTITY, + secInRate: 3600, + typeOfUnit: Unit.unitType.UNIT, + suffix: '', + displayable: Unit.displayableType.ALL, + preferredDisplay: false, + note: 'OED created standard unit' + }, + { + // u12 + name: 'kg CO₂', + identifier: '', + unitRepresent: Unit.unitRepresentType.QUANTITY, + secInRate: 3600, + typeOfUnit: Unit.unitType.UNIT, + suffix: 'CO₂', + displayable: Unit.displayableType.ALL, + preferredDisplay: false, + note: 'special unit' + } + ]); + const conversionData = conversionDatakWh.concat([ + { + //c11 + sourceName: 'Electric_Utility', + destinationName: 'kg CO₂', + bidirectional: false, + slope: 0.709, + intercept: 0, + note: 'Electric_Utility → kg CO₂' + }, + { + //c12 + sourceName: 'kg CO₂', + destinationName: 'kg', + bidirectional: false, + slope: 1, + intercept: 0, + note: 'CO₂ → kg' + } + ]); + //load data into database + await prepareTest(unitData, conversionData, meterDatakWhGroups, groupDatakWh); + // Get unit ID for 'kg CO₂' + const unitId = await getUnitId('kg of CO₂'); + // Load expected response data from the corresponding CSV file + const expected = await parseExpectedCsv('src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kWh_gu_kgCO2_st_-inf_et_inf_bd_1.csv'); + // Create a request to the API for unbounded reading times and save the response + const res = await chai.request(app).get(`/api/unitReadings/bar/groups/${GROUP_ID}`) + .query({ + timeInterval: ETERNITY.toString(), + barWidthDays: '1', + graphicUnitId: unitId + }); + // Check that the API reading is equal to what it is expected to equal + expectReadingToEqualExpected(res, expected, GROUP_ID); + }); // Add BG13 here diff --git a/src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kWh_gu_kgCO2_st_-inf_et_inf_bd_1.csv b/src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kWh_gu_kgCO2_st_-inf_et_inf_bd_1.csv new file mode 100644 index 000000000..383511541 --- /dev/null +++ b/src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kWh_gu_kgCO2_st_-inf_et_inf_bd_1.csv @@ -0,0 +1,76 @@ +reading,start time,end time +6069.41157428824,2022-08-18 00:00:00,2022-08-19 00:00:00 +5360.0950529087,2022-08-19 00:00:00,2022-08-20 00:00:00 +6136.3385888459,2022-08-20 00:00:00,2022-08-21 00:00:00 +6020.31237213475,2022-08-21 00:00:00,2022-08-22 00:00:00 +5824.75240869586,2022-08-22 00:00:00,2022-08-23 00:00:00 +5984.82849114233,2022-08-23 00:00:00,2022-08-24 00:00:00 +6044.53672566359,2022-08-24 00:00:00,2022-08-25 00:00:00 +5935.08743516999,2022-08-25 00:00:00,2022-08-26 00:00:00 +5535.90866257469,2022-08-26 00:00:00,2022-08-27 00:00:00 +5969.96595764567,2022-08-27 00:00:00,2022-08-28 00:00:00 +5896.94043288837,2022-08-28 00:00:00,2022-08-29 00:00:00 +5573.61231080648,2022-08-29 00:00:00,2022-08-30 00:00:00 +6088.81494914974,2022-08-30 00:00:00,2022-08-31 00:00:00 +5850.42840472852,2022-08-31 00:00:00,2022-09-01 00:00:00 +5716.40890876994,2022-09-01 00:00:00,2022-09-02 00:00:00 +6170.27951065381,2022-09-02 00:00:00,2022-09-03 00:00:00 +6186.65565793557,2022-09-03 00:00:00,2022-09-04 00:00:00 +5916.29487489138,2022-09-04 00:00:00,2022-09-05 00:00:00 +5517.26940948484,2022-09-05 00:00:00,2022-09-06 00:00:00 +6014.97656889892,2022-09-06 00:00:00,2022-09-07 00:00:00 +6039.05132316679,2022-09-07 00:00:00,2022-09-08 00:00:00 +5803.8829685348,2022-09-08 00:00:00,2022-09-09 00:00:00 +5980.44656658293,2022-09-09 00:00:00,2022-09-10 00:00:00 +5893.79958529856,2022-09-10 00:00:00,2022-09-11 00:00:00 +6378.403817572,2022-09-11 00:00:00,2022-09-12 00:00:00 +5916.58257205527,2022-09-12 00:00:00,2022-09-13 00:00:00 +6084.77823786232,2022-09-13 00:00:00,2022-09-14 00:00:00 +5728.84584182247,2022-09-14 00:00:00,2022-09-15 00:00:00 +6030.88102504494,2022-09-15 00:00:00,2022-09-16 00:00:00 +5787.568676668,2022-09-16 00:00:00,2022-09-17 00:00:00 +6111.36424880746,2022-09-17 00:00:00,2022-09-18 00:00:00 +6151.65276828447,2022-09-18 00:00:00,2022-09-19 00:00:00 +5850.24709482304,2022-09-19 00:00:00,2022-09-20 00:00:00 +6473.47539581256,2022-09-20 00:00:00,2022-09-21 00:00:00 +5748.84600295891,2022-09-21 00:00:00,2022-09-22 00:00:00 +6191.25412974175,2022-09-22 00:00:00,2022-09-23 00:00:00 +5685.22305173247,2022-09-23 00:00:00,2022-09-24 00:00:00 +5674.99127718854,2022-09-24 00:00:00,2022-09-25 00:00:00 +6045.79861756136,2022-09-25 00:00:00,2022-09-26 00:00:00 +5761.38007323321,2022-09-26 00:00:00,2022-09-27 00:00:00 +5716.08943844792,2022-09-27 00:00:00,2022-09-28 00:00:00 +5954.37349494221,2022-09-28 00:00:00,2022-09-29 00:00:00 +6148.2098568215,2022-09-29 00:00:00,2022-09-30 00:00:00 +5971.53246571957,2022-09-30 00:00:00,2022-10-01 00:00:00 +5843.91057900997,2022-10-01 00:00:00,2022-10-02 00:00:00 +5977.13386582796,2022-10-02 00:00:00,2022-10-03 00:00:00 +5740.81516105115,2022-10-03 00:00:00,2022-10-04 00:00:00 +5970.9702794199,2022-10-04 00:00:00,2022-10-05 00:00:00 +5877.46271918843,2022-10-05 00:00:00,2022-10-06 00:00:00 +6011.36345934627,2022-10-06 00:00:00,2022-10-07 00:00:00 +6150.41087042478,2022-10-07 00:00:00,2022-10-08 00:00:00 +5843.6561977355,2022-10-08 00:00:00,2022-10-09 00:00:00 +5502.29449289721,2022-10-09 00:00:00,2022-10-10 00:00:00 +5891.96020736497,2022-10-10 00:00:00,2022-10-11 00:00:00 +5823.6261696142,2022-10-11 00:00:00,2022-10-12 00:00:00 +6155.16409969168,2022-10-12 00:00:00,2022-10-13 00:00:00 +6029.05248499971,2022-10-13 00:00:00,2022-10-14 00:00:00 +5930.80658096196,2022-10-14 00:00:00,2022-10-15 00:00:00 +5735.21780405837,2022-10-15 00:00:00,2022-10-16 00:00:00 +5858.50805873689,2022-10-16 00:00:00,2022-10-17 00:00:00 +5996.44774260275,2022-10-17 00:00:00,2022-10-18 00:00:00 +5849.9833166756,2022-10-18 00:00:00,2022-10-19 00:00:00 +5648.7842460619,2022-10-19 00:00:00,2022-10-20 00:00:00 +5974.56052728109,2022-10-20 00:00:00,2022-10-21 00:00:00 +6087.89481690719,2022-10-21 00:00:00,2022-10-22 00:00:00 +5767.23572718058,2022-10-22 00:00:00,2022-10-23 00:00:00 +5972.45731067561,2022-10-23 00:00:00,2022-10-24 00:00:00 +6276.45009086608,2022-10-24 00:00:00,2022-10-25 00:00:00 +6479.29053423768,2022-10-25 00:00:00,2022-10-26 00:00:00 +5669.79546444366,2022-10-26 00:00:00,2022-10-27 00:00:00 +5717.84802537126,2022-10-27 00:00:00,2022-10-28 00:00:00 +5990.23531580915,2022-10-28 00:00:00,2022-10-29 00:00:00 +6379.73868466838,2022-10-29 00:00:00,2022-10-30 00:00:00 +5920.95200034458,2022-10-30 00:00:00,2022-10-31 00:00:00 +5544.67726481774,2022-10-31 00:00:00,2022-11-01 00:00:00 From b0cadd680980218b4b083cec9f1a0a820662774e Mon Sep 17 00:00:00 2001 From: nicholasyfu1 <62448453+nicholasyfu1@users.noreply.github.com> Date: Thu, 3 Oct 2024 20:22:55 -0400 Subject: [PATCH 32/43] Update readingsBarGroupQuantity.js --- src/server/test/web/readingsBarGroupQuantity.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/server/test/web/readingsBarGroupQuantity.js b/src/server/test/web/readingsBarGroupQuantity.js index 944e6786f..4cc4f45e7 100644 --- a/src/server/test/web/readingsBarGroupQuantity.js +++ b/src/server/test/web/readingsBarGroupQuantity.js @@ -203,7 +203,6 @@ mocha.describe('readings API', () => { // Add BG11 here - // Add BG12 here mocha.it('BG12: 1 day bars for 15 + 20 minute reading intervals and quantity units with +-inf start/end time & kWh as kg of CO2', async () =>{ const unitData = unitDatakWh.concat([ { From 265887a1736b1a805ec47a7e51550b858795d470 Mon Sep 17 00:00:00 2001 From: danielshid Date: Fri, 4 Oct 2024 16:36:38 -0400 Subject: [PATCH 33/43] code does not work needs debugging --- .../components/IntervalControlsComponent.tsx | 54 +++++++++---------- src/client/app/translations/data.ts | 6 +-- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/client/app/components/IntervalControlsComponent.tsx b/src/client/app/components/IntervalControlsComponent.tsx index 5f59d893a..8933abad9 100644 --- a/src/client/app/components/IntervalControlsComponent.tsx +++ b/src/client/app/components/IntervalControlsComponent.tsx @@ -38,17 +38,20 @@ export default function IntervalControlsComponent() { 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(() => { - // Assume value is valid since it is coming from state. - // Do not allow bad values in state. - const isCustom = !['1', '7', '28'].includes(duration.asDays().toString()); - setShowCustomDuration(isCustom); - setDaysCustom(duration.asDays()); - setDays(isCustom ? CUSTOM_INPUT : duration.asDays().toString()); - }, [duration]); + // If user is in custom input mode, don't reset to standard options + if (!isCustomInput) { + const isCustom = !['1', '7', '28'].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) => { @@ -57,6 +60,7 @@ export default function IntervalControlsComponent() { // 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. @@ -73,6 +77,7 @@ export default function IntervalControlsComponent() { // Updates value when the custom duration input is used for bar or map. const handleCustomDaysChange = (value: number) => { + setIsCustomInput(true); setDaysCustom(value); }; @@ -103,22 +108,6 @@ export default function IntervalControlsComponent() { FourWeeks: '4.weeks' }; - // Defines the set of options for the dropdown - const options = chartType === ChartTypes.compare - ? Object.entries(ComparePeriod).map(([key, value]) => ( - - )) - : ( - <> - - - - - - ); - return (

@@ -130,7 +119,7 @@ export default function IntervalControlsComponent() { )}:

@@ -141,10 +130,21 @@ export default function IntervalControlsComponent() { value={chartType === ChartTypes.compare ? comparePeriod?.toString() : days} onChange={e => chartType === ChartTypes.compare ? handleComparePeriodChange(e.target.value) : handleDaysChange(e.target.value)} > - {options} + {Object.entries(ComparePeriod).map( + ([key, value]) => ( + + ) + )} + {/* TODO: Compare is currently not ready for the custom option. */} + {chartType !== ChartTypes.compare && + + } - {/* TODO: Compare is currently not ready for a custom option. */} - {showCustomDuration && chartType !== ChartTypes.compare && + {showCustomDuration && Date: Fri, 4 Oct 2024 17:27:11 -0400 Subject: [PATCH 34/43] changed compare enums --- .../app/components/IntervalControlsComponent.tsx | 2 +- src/client/app/utils/calculateCompare.ts | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/client/app/components/IntervalControlsComponent.tsx b/src/client/app/components/IntervalControlsComponent.tsx index 8933abad9..118bba711 100644 --- a/src/client/app/components/IntervalControlsComponent.tsx +++ b/src/client/app/components/IntervalControlsComponent.tsx @@ -144,7 +144,7 @@ export default function IntervalControlsComponent() { } - {showCustomDuration && + {showCustomDuration && chartType !== ChartTypes.compare && Date: Fri, 4 Oct 2024 17:28:56 -0400 Subject: [PATCH 35/43] Removed accidental comment --- src/server/app.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/server/app.js b/src/server/app.js index 72763cae4..58774f9cc 100644 --- a/src/server/app.js +++ b/src/server/app.js @@ -39,7 +39,6 @@ const ciks = require('./routes/ciks'); // Create a limit of 200 requests/5 seconds const generalLimiter = rateLimit({ windowMs: 5 * 1000, // 5 seconds - //limit: 200, // 200 requests limit: 200, // 200 requests standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers legacyHeaders: false // Disable the `X-RateLimit-*` headers From 6845945ea6c72b2ad3514e2441143c37cba476a1 Mon Sep 17 00:00:00 2001 From: mmehta2669 Date: Fri, 4 Oct 2024 20:49:49 -0500 Subject: [PATCH 36/43] Adding negative function to maps --- .../app/components/MapChartComponent.tsx | 363 +++++++++++++----- 1 file changed, 268 insertions(+), 95 deletions(-) diff --git a/src/client/app/components/MapChartComponent.tsx b/src/client/app/components/MapChartComponent.tsx index e1a5dde28..0b2481fe6 100644 --- a/src/client/app/components/MapChartComponent.tsx +++ b/src/client/app/components/MapChartComponent.tsx @@ -2,25 +2,28 @@ * 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 { orderBy } from 'lodash'; -import * as moment from 'moment'; -import * as React from 'react'; -import Plot from 'react-plotly.js'; -import { useSelector } from 'react-redux'; +import { orderBy } from "lodash"; +import * as moment from "moment"; +import * as React from "react"; +import Plot from "react-plotly.js"; +import { useSelector } from "react-redux"; import { - selectAreaUnit, selectBarWidthDays, - selectGraphAreaNormalization, selectSelectedGroups, - selectSelectedMeters, selectSelectedUnit -} from '../redux/slices/graphSlice'; -import { selectGroupDataById } from '../redux/api/groupsApi'; -import { selectMeterDataById } from '../redux/api/metersApi'; -import { readingsApi } from '../redux/api/readingsApi'; -import { selectUnitDataById } from '../redux/api/unitsApi'; -import { useAppSelector } from '../redux/reduxHooks'; -import { selectMapChartQueryArgs } from '../redux/selectors/chartQuerySelectors'; -import { DataType } from '../types/Datasources'; -import { State } from '../types/redux/state'; -import { UnitRepresentType } from '../types/redux/units'; + selectAreaUnit, + selectBarWidthDays, + selectGraphAreaNormalization, + selectSelectedGroups, + selectSelectedMeters, + selectSelectedUnit, +} from "../redux/slices/graphSlice"; +import { selectGroupDataById } from "../redux/api/groupsApi"; +import { selectMeterDataById } from "../redux/api/metersApi"; +import { readingsApi } from "../redux/api/readingsApi"; +import { selectUnitDataById } from "../redux/api/unitsApi"; +import { useAppSelector } from "../redux/reduxHooks"; +import { selectMapChartQueryArgs } from "../redux/selectors/chartQuerySelectors"; +import { DataType } from "../types/Datasources"; +import { State } from "../types/redux/state"; +import { UnitRepresentType } from "../types/redux/units"; import { CartesianPoint, Dimensions, @@ -28,21 +31,27 @@ import { gpsToUserGrid, itemDisplayableOnMap, itemMapInfoOk, - normalizeImageDimensions -} from '../utils/calibration'; -import { AreaUnitType, getAreaUnitConversion } from '../utils/getAreaUnitConversion'; -import getGraphColor from '../utils/getGraphColor'; -import translate from '../utils/translate'; -import SpinnerComponent from './SpinnerComponent'; + normalizeImageDimensions, +} from "../utils/calibration"; +import { + AreaUnitType, + getAreaUnitConversion, +} from "../utils/getAreaUnitConversion"; +import getGraphColor from "../utils/getGraphColor"; +import translate from "../utils/translate"; +import SpinnerComponent from "./SpinnerComponent"; +import { showInfoNotification } from "../utils/notifications"; /** * @returns map component */ export default function MapChartComponent() { - - const { meterArgs, groupArgs, meterShouldSkip, groupShouldSkip } = useAppSelector(selectMapChartQueryArgs); - const { data: meterReadings, isLoading: meterIsFetching } = readingsApi.useBarQuery(meterArgs, { skip: meterShouldSkip }); - const { data: groupData, isLoading: groupIsFetching } = readingsApi.useBarQuery(groupArgs, { skip: groupShouldSkip }); + const { meterArgs, groupArgs, meterShouldSkip, groupShouldSkip } = + useAppSelector(selectMapChartQueryArgs); + const { data: meterReadings, isLoading: meterIsFetching } = + readingsApi.useBarQuery(meterArgs, { skip: meterShouldSkip }); + const { data: groupData, isLoading: groupIsFetching } = + readingsApi.useBarQuery(groupArgs, { skip: groupShouldSkip }); // converting maps to RTK has been proving troublesome, therefore using a combination of old/new stateSelectors const unitID = useAppSelector(selectSelectedUnit); @@ -64,7 +73,6 @@ export default function MapChartComponent() { return ; } - // Map to use. let map; // Holds Plotly mapping info. @@ -82,12 +90,12 @@ export default function MapChartComponent() { // Holds the hover text for each point for Plotly const hoverText: string[] = []; // Holds the size of each circle for Plotly. - const size: number[] = []; + let size: number[] = []; // Holds the color of each circle for Plotly. const colors: string[] = []; // If there is no map then use a new, empty image as the map. I believe this avoids errors // and gives the blank screen. - image = (map) ? map.image : new Image(); + image = map ? map.image : new Image(); // Arrays to hold the Plotly grid location (x, y) for circles to place on map. const x: number[] = []; const y: number[] = []; @@ -99,10 +107,11 @@ export default function MapChartComponent() { // The size of the original map loaded into OED. const imageDimensions: Dimensions = { width: image.width, - height: image.height + height: image.height, }; // Determine the dimensions so within the Plotly coordinates on the user map. - const imageDimensionNormalized = normalizeImageDimensions(imageDimensions); + const imageDimensionNormalized = + normalizeImageDimensions(imageDimensions); // This is the origin & opposite from the calibration. It is the lower, left // and upper, right corners of the user map. const origin = map.origin; @@ -110,10 +119,15 @@ export default function MapChartComponent() { // Get the GPS degrees per unit of Plotly grid for x and y. By knowing the two corners // (or really any two distinct points) you can calculate this by the change in GPS over the // change in x or y which is the map's width & height in this case. - const scaleOfMap = calculateScaleFromEndpoints(origin, opposite, imageDimensionNormalized, map.northAngle); + const scaleOfMap = calculateScaleFromEndpoints( + origin, + opposite, + imageDimensionNormalized, + map.northAngle + ); // Loop over all selected meters. Maps only work for meters at this time. // The y-axis label depends on the unit which is in selectUnit state. - let unitLabel: string = ''; + let unitLabel: string = ""; // If graphingUnit is -99 then none selected and nothing to graph so label is empty. // This will probably happen when the page is first loaded. if (unitID !== -99) { @@ -124,7 +138,7 @@ export default function MapChartComponent() { // Bar graphics are always quantities. if (selectUnitState.unitRepresent === UnitRepresentType.quantity) { // If it is a quantity unit then that is the unit you are graphing but it is normalized to per day. - unitLabel = selectUnitState.identifier + ' / day'; + unitLabel = selectUnitState.identifier + " / day"; } else if (selectUnitState.unitRepresent === UnitRepresentType.flow) { // If it is a flow meter then you need to multiply by time to get the quantity unit then show as per day. // The quantity/time for flow has varying time so label by multiplying by time. @@ -133,10 +147,11 @@ export default function MapChartComponent() { // It might not be usual to take a flow and make it into a quantity so this label is a little different to // catch people's attention. If sites/users don't like OED doing this then we can eliminate flow for these types // of graphics as we are doing for rate. - unitLabel = selectUnitState.identifier + ' * time / day ≡ quantity / day'; + unitLabel = + selectUnitState.identifier + " * time / day ≡ quantity / day"; } if (areaNormalization) { - unitLabel += ' / ' + translate(`AreaUnitType.${selectedAreaUnit}`); + unitLabel += " / " + translate(`AreaUnitType.${selectedAreaUnit}`); } } } @@ -149,19 +164,35 @@ export default function MapChartComponent() { if (gps !== undefined && gps !== null && meterReadings !== undefined) { let meterArea = meterDataById[meterID].area; // we either don't care about area, or we do in which case there needs to be a nonzero area - if (!areaNormalization || (meterArea > 0 && meterDataById[meterID].areaUnit != AreaUnitType.none)) { + if ( + !areaNormalization || + (meterArea > 0 && + meterDataById[meterID].areaUnit != AreaUnitType.none) + ) { if (areaNormalization) { // convert the meter area into the proper unit, if needed - meterArea *= getAreaUnitConversion(meterDataById[meterID].areaUnit, selectedAreaUnit); + meterArea *= getAreaUnitConversion( + meterDataById[meterID].areaUnit, + selectedAreaUnit + ); } // Convert the gps value to the equivalent Plotly grid coordinates on user map. // First, convert from GPS to grid units. Since we are doing a GPS calculation, this happens on the true north map. // It must be on true north map since only there are the GPS axis parallel to the map axis. // To start, calculate the user grid coordinates (Plotly) from the GPS value. This involves calculating // it coordinates on the true north map and then rotating/shifting to the user map. - const meterGPSInUserGrid: CartesianPoint = gpsToUserGrid(imageDimensionNormalized, gps, origin, scaleOfMap, map.northAngle); + const meterGPSInUserGrid: CartesianPoint = gpsToUserGrid( + imageDimensionNormalized, + gps, + origin, + scaleOfMap, + map.northAngle + ); // Only display items within valid info and within map. - if (itemMapInfoOk(meterID, DataType.Meter, map, gps) && itemDisplayableOnMap(imageDimensionNormalized, meterGPSInUserGrid)) { + if ( + itemMapInfoOk(meterID, DataType.Meter, map, gps) && + itemDisplayableOnMap(imageDimensionNormalized, meterGPSInUserGrid) + ) { // The x, y value for Plotly to use that are on the user map. x.push(meterGPSInUserGrid.x); y.push(meterGPSInUserGrid.y); @@ -177,29 +208,40 @@ export default function MapChartComponent() { // The usual color for this meter. colors.push(getGraphColor(meterID, DataType.Meter)); if (!readingsData) { - throw new Error('Unacceptable condition: readingsData.readings is undefined.'); + throw new Error( + "Unacceptable condition: readingsData.readings is undefined." + ); } // Use the most recent time reading for the circle on the map. // This has the limitations of the bar value where the last one can include ranges without // data (GitHub issue on this). // TODO: It might be better to do this similarly to compare. (See GitHub issue) - const readings = orderBy(readingsData, ['startTimestamp'], ['desc']); + const readings = orderBy( + readingsData, + ["startTimestamp"], + ["desc"] + ); const mapReading = readings[0]; let timeReading: string; let averagedReading = 0; if (readings.length === 0) { // No data. The next lines causes an issue so set specially. // There may be a better overall fix for no data. - timeReading = 'no data to display'; + timeReading = "no data to display"; size.push(0); } else { // 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')}`; + timeReading = `${moment + .utc(mapReading.startTimestamp) + .format("ll")}`; if (barDuration.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')}`; + 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(); @@ -210,7 +252,11 @@ export default function MapChartComponent() { size.push(averagedReading); } // The hover text. - hoverText.push(` ${timeReading}
${label}: ${averagedReading.toPrecision(6)} ${unitLabel}`); + hoverText.push( + ` ${timeReading}
${label}: ${averagedReading.toPrecision( + 6 + )} ${unitLabel}` + ); } } } @@ -224,19 +270,35 @@ export default function MapChartComponent() { // Filter groups with actual gps coordinates. if (gps !== undefined && gps !== null && groupData !== undefined) { let groupArea = groupDataById[groupID].area; - if (!areaNormalization || (groupArea > 0 && groupDataById[groupID].areaUnit != AreaUnitType.none)) { + if ( + !areaNormalization || + (groupArea > 0 && + groupDataById[groupID].areaUnit != AreaUnitType.none) + ) { if (areaNormalization) { // convert the meter area into the proper unit, if needed - groupArea *= getAreaUnitConversion(groupDataById[groupID].areaUnit, selectedAreaUnit); + groupArea *= getAreaUnitConversion( + groupDataById[groupID].areaUnit, + selectedAreaUnit + ); } // Convert the gps value to the equivalent Plotly grid coordinates on user map. // First, convert from GPS to grid units. Since we are doing a GPS calculation, this happens on the true north map. // It must be on true north map since only there are the GPS axis parallel to the map axis. // To start, calculate the user grid coordinates (Plotly) from the GPS value. This involves calculating // it coordinates on the true north map and then rotating/shifting to the user map. - const groupGPSInUserGrid: CartesianPoint = gpsToUserGrid(imageDimensionNormalized, gps, origin, scaleOfMap, map.northAngle); + const groupGPSInUserGrid: CartesianPoint = gpsToUserGrid( + imageDimensionNormalized, + gps, + origin, + scaleOfMap, + map.northAngle + ); // Only display items within valid info and within map. - if (itemMapInfoOk(groupID, DataType.Group, map, gps) && itemDisplayableOnMap(imageDimensionNormalized, groupGPSInUserGrid)) { + if ( + itemMapInfoOk(groupID, DataType.Group, map, gps) && + itemDisplayableOnMap(imageDimensionNormalized, groupGPSInUserGrid) + ) { // The x, y value for Plotly to use that are on the user map. x.push(groupGPSInUserGrid.x); y.push(groupGPSInUserGrid.y); @@ -251,28 +313,39 @@ export default function MapChartComponent() { // The usual color for this group. colors.push(getGraphColor(groupID, DataType.Group)); if (!readingsData) { - throw new Error('Unacceptable condition: readingsData.readings is undefined.'); + throw new Error( + "Unacceptable condition: readingsData.readings is undefined." + ); } // Use the most recent time reading for the circle on the map. // This has the limitations of the bar value where the last one can include ranges without // data (GitHub issue on this). // TODO: It might be better to do this similarly to compare. (See GitHub issue) - const readings = orderBy(readingsData, ['startTimestamp'], ['desc']); + const readings = orderBy( + readingsData, + ["startTimestamp"], + ["desc"] + ); const mapReading = readings[0]; let timeReading: string; let averagedReading = 0; if (readings.length === 0) { // No data. The next lines causes an issue so set specially. // There may be a better overall fix for no data. - timeReading = 'no data to display'; + timeReading = "no data to display"; size.push(0); } 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')}`; + timeReading = `${moment + .utc(mapReading.startTimestamp) + .format("ll")}`; if (barDuration.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')}`; + 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(); @@ -283,28 +356,129 @@ export default function MapChartComponent() { size.push(averagedReading); } // The hover text. - hoverText.push(` ${timeReading}
${label}: ${averagedReading.toPrecision(6)} ${unitLabel}`); + hoverText.push( + ` ${timeReading}
${label}: ${averagedReading.toPrecision( + 6 + )} ${unitLabel}` + ); } } } } } + + // TODO DEBUG Using amp 1, 2 & 3 from test data that within map. This arbitrarily changes the value for testing. + // It does not change the hover value. + // size[0] = 300; + // size[1] = 100; + // size[2] = -1; + // size = size.slice(0, x.length); + // TODO Using the following seems to have no impact on the code. It has been noticed that this function is called // many times for each change. Someone should look at why that is happening and why some have no items in the arrays. // if (size.length > 0) { - // TODO The max circle diameter should come from admin/DB. const maxFeatureFraction = map.circleSize; // Find the smaller of width and height. This is used since it means the circle size will be // scaled to that dimension and smaller relative to the other coordinate. - const minDimension = Math.min(imageDimensionNormalized.width, imageDimensionNormalized.height); + const minDimension = Math.min( + imageDimensionNormalized.width, + imageDimensionNormalized.height + ); // The circle size is set to area below. Thus, we need to convert from wanting a max // diameter of minDimension * maxFeatureFraction to an area. - const maxCircleSize = Math.PI * Math.pow(minDimension * maxFeatureFraction / 2, 2); - // Find the largest circle which is usage. - const largestCircleSize = Math.max(...size); - // Scale largest circle to the max size and others will be scaled to be smaller. - // Not that < 1 => a larger circle. - const scaling = largestCircleSize / maxCircleSize; + const maxCircleSize = + Math.PI * Math.pow((minDimension * maxFeatureFraction) / 2, 2); + // What fraction of the max circle size that the min circle size will be. Determine empirically. + let minFeatureFractionOfMax = 0.05; + // If the maxFeatureFraction is too small then it is possible that the min circle will be very + // small and difficult to see. This value is the min circle size to make sure that does not happen. + // The value used was empirically determined so it would not be too small. + const circleSizeThreshold = 75; + // If the circle will be too small then force to min, otherwise use standard value. + minFeatureFractionOfMax = + minFeatureFractionOfMax * maxCircleSize < circleSizeThreshold + ? circleSizeThreshold / maxCircleSize + : minFeatureFractionOfMax; + // Find the min and max of the values to graph. + const min = Math.min(...size); + const max = Math.max(...size); + // Fix the range/difference between the max and min value. + const range = max - min; + // This is the min value that should be graphed if all the values were positive. This treats the + // values as if they started at zero. Thus, the minValue is the fraction of the max on this + // shifted range that will have a circle size at least the fraction of the max that is allowed. + const minValue = minFeatureFractionOfMax * range; + // TODO As also noted above, this component is rerendering multiple times. This is causing the + // information message to appear multiple times. This needs to be figured out. + // Debugging indicates that this happens when selecting a meter/group not recently used so the + // data is not in Redux store. Each time it has readingsData as undefined so it does not process + // that meter. Need to see how can avoid this. + + // Stores the amount to shift values for circle size for graphing where it is normally negative. + // Since subtract it adds to the value. It is negative for when the min is negative. + let shift; + if (range === 0) { + // All the values are the same including only one value and only zero. + // Plotly does not show circles of size 0 even if sizemin is set (hover does happen). + // To fix this, make the shift be min - 0.000123 (arbitrary value) so it will have a circle + // of the max size. The shifted value will be slightly positive (0.000123) so the circle shows. + // Note other cases avoid zero values if not the only one. + shift = min - 0.000123; + } else if (size.length != 0 && max < 0) { + // Need to test the size.length because the value is -infinity if no values. + // TODO Must be internationalized. + showInfoNotification( + "All values are negative so the circle sizes act as if value range was positive which may change the relative sizes." + ); + // All the values are negative. Plotly will only show circles that are positive. It isn't clear + // there is a perfect solution. This will show the size as if it was positive. For example, if + // it is -100, -200 & -300 then it will use 100, 200, 300. Note the ratio of -100 to -200 (the + // two largest values) is 2x whereas the shifted ones are 300 to 200 which is 1.5. This was + // decided the best of the options considered. + if (Math.abs(max) < minValue) { + // This takes care of case where the max is small so the shifted values will have a very small + // circle size. Force max to be at least minValue (note min negative so shift other way). + // TODO Must be internationalized. + showInfoNotification( + "Some values are close to zero so the small circle sizes may be a little larger to be visible" + ); + shift = min - minValue; + } else { + shift = min + max; + } + } else if (min >= minValue) { + // There are no values smaller than desired so all circles will be big enough. + // There is no need to shift the range of values. + shift = 0; + } else { + // Some values are too small. Shifting by min would start the range at zero. + // Subtracting minValue shifts more to a larger value so the smallest one + // is the min value to give the smallest circle desired. + // TODO Must be internationalized. Same as message above. + showInfoNotification( + "Some values are close to zero so the small circle sizes may be a little larger to be visible." + ); + shift = min - minValue; + if (max > 0 && min < 0) { + // Tell user that there are negative and positive values. + // Unlike all positive that sets the range to effectively start at zero to give users circle size + // that scale proportional to value, it is unclear the correct range for mixed sign values. + // Given this, shift as usual where the small value (large negative) will wind up near 0 and + // have a small circle size. This informs the user of the situation. + // TODO Must be internationalized. + showInfoNotification( + "There are negative and positive values and this impacts relative circle size." + ); + } + } + // Change all the sizes by the desired shift. Note the hover is not changed so + // this only impacts the circle size but not the value seen by the user. + size = size.map((size) => size - shift); + // This is how much Plotly will scale all circle sizes. (max - shift) is + // the new max value in the range of sizes + // given the shift just done. Dividing my maxCircleSize means the max value + // will have the a circle as big as the largest one desired. + const scaling = (max - shift) / maxCircleSize; // Per https://plotly.com/javascript/reference/scatter/: // The opacity of 0.5 makes it possible to see the map even when there is a circle but the hover @@ -319,20 +493,22 @@ export default function MapChartComponent() { const traceOne = { x, y, - type: 'scatter', - mode: 'markers', + type: "scatter", + mode: "markers", marker: { color: colors, opacity: 0.5, size, - sizemin: 6, + // The best sizemin of a circle is unclear. Given the shifting of sizes above this probably + // should never happen but left to be safe. + sizemin: 5, sizeref: scaling, - sizemode: 'area' + sizemode: "area", }, text: hoverText, - hoverinfo: 'text', + hoverinfo: "text", opacity: 1, - showlegend: false + showlegend: false, }; data.push(traceOne); } @@ -342,39 +518,36 @@ export default function MapChartComponent() { const layout: any = { // Either the actual map name or text to say it is not available. title: { - text: (map) ? map.name : translate('map.unavailable') + text: map ? map.name : translate("map.unavailable"), }, width: 1000, height: 1000, xaxis: { visible: false, // changes all visibility settings including showgrid, zeroline, showticklabels and hiding ticks - range: [0, 500] // range of displayed graph + range: [0, 500], // range of displayed graph }, yaxis: { visible: false, range: [0, 500], - scaleanchor: 'x' + scaleanchor: "x", }, - images: [{ - layer: 'below', - source: (image) ? image.src : '', - xref: 'x', - yref: 'y', - x: 0, - y: 0, - sizex: 500, - sizey: 500, - xanchor: 'left', - yanchor: 'bottom', - sizing: 'contain', - opacity: 1 - }] + images: [ + { + layer: "below", + source: image ? image.src : "", + xref: "x", + yref: "y", + x: 0, + y: 0, + sizex: 500, + sizey: 500, + xanchor: "left", + yanchor: "bottom", + sizing: "contain", + opacity: 1, + }, + ], }; - return ( - - ); + return ; } From 7a70b289a0ce4ee74fcea38d3e31778d88bc8899 Mon Sep 17 00:00:00 2001 From: mmehta2669 Date: Fri, 4 Oct 2024 20:56:40 -0500 Subject: [PATCH 37/43] Adding negative function to maps --- .../app/components/MapChartComponent.tsx | 997 ++++++++---------- 1 file changed, 455 insertions(+), 542 deletions(-) diff --git a/src/client/app/components/MapChartComponent.tsx b/src/client/app/components/MapChartComponent.tsx index 0b2481fe6..7fe3ca3e3 100644 --- a/src/client/app/components/MapChartComponent.tsx +++ b/src/client/app/components/MapChartComponent.tsx @@ -2,552 +2,465 @@ * 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 { orderBy } from "lodash"; -import * as moment from "moment"; -import * as React from "react"; -import Plot from "react-plotly.js"; -import { useSelector } from "react-redux"; +import { orderBy } from 'lodash'; +import * as moment from 'moment'; +import * as React from 'react'; +import Plot from 'react-plotly.js'; +import { useSelector } from 'react-redux'; import { - selectAreaUnit, - selectBarWidthDays, - selectGraphAreaNormalization, - selectSelectedGroups, - selectSelectedMeters, - selectSelectedUnit, -} from "../redux/slices/graphSlice"; -import { selectGroupDataById } from "../redux/api/groupsApi"; -import { selectMeterDataById } from "../redux/api/metersApi"; -import { readingsApi } from "../redux/api/readingsApi"; -import { selectUnitDataById } from "../redux/api/unitsApi"; -import { useAppSelector } from "../redux/reduxHooks"; -import { selectMapChartQueryArgs } from "../redux/selectors/chartQuerySelectors"; -import { DataType } from "../types/Datasources"; -import { State } from "../types/redux/state"; -import { UnitRepresentType } from "../types/redux/units"; + selectAreaUnit, selectBarWidthDays, + selectGraphAreaNormalization, selectSelectedGroups, + selectSelectedMeters, selectSelectedUnit +} from '../redux/slices/graphSlice'; +import { selectGroupDataById } from '../redux/api/groupsApi'; +import { selectMeterDataById } from '../redux/api/metersApi'; +import { readingsApi } from '../redux/api/readingsApi'; +import { selectUnitDataById } from '../redux/api/unitsApi'; +import { useAppSelector } from '../redux/reduxHooks'; +import { selectMapChartQueryArgs } from '../redux/selectors/chartQuerySelectors'; +import { DataType } from '../types/Datasources'; +import { State } from '../types/redux/state'; +import { UnitRepresentType } from '../types/redux/units'; import { - CartesianPoint, - Dimensions, - calculateScaleFromEndpoints, - gpsToUserGrid, - itemDisplayableOnMap, - itemMapInfoOk, - normalizeImageDimensions, -} from "../utils/calibration"; -import { - AreaUnitType, - getAreaUnitConversion, -} from "../utils/getAreaUnitConversion"; -import getGraphColor from "../utils/getGraphColor"; -import translate from "../utils/translate"; -import SpinnerComponent from "./SpinnerComponent"; -import { showInfoNotification } from "../utils/notifications"; - + CartesianPoint, + Dimensions, + calculateScaleFromEndpoints, + gpsToUserGrid, + itemDisplayableOnMap, + itemMapInfoOk, + normalizeImageDimensions +} from '../utils/calibration'; +import { AreaUnitType, getAreaUnitConversion } from '../utils/getAreaUnitConversion'; +import getGraphColor from '../utils/getGraphColor'; +import translate from '../utils/translate'; +import SpinnerComponent from './SpinnerComponent'; +import { showInfoNotification } from '../utils/notifications'; + /** * @returns map component */ export default function MapChartComponent() { - const { meterArgs, groupArgs, meterShouldSkip, groupShouldSkip } = - useAppSelector(selectMapChartQueryArgs); - const { data: meterReadings, isLoading: meterIsFetching } = - readingsApi.useBarQuery(meterArgs, { skip: meterShouldSkip }); - const { data: groupData, isLoading: groupIsFetching } = - readingsApi.useBarQuery(groupArgs, { skip: groupShouldSkip }); - - // 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 areaNormalization = useAppSelector(selectGraphAreaNormalization); - const selectedAreaUnit = useAppSelector(selectAreaUnit); - const selectedMeters = useAppSelector(selectSelectedMeters); - const selectedGroups = useAppSelector(selectSelectedGroups); - const unitDataById = useAppSelector(selectUnitDataById); - const groupDataById = useAppSelector(selectGroupDataById); - const meterDataById = useAppSelector(selectMeterDataById); - - // RTK Types Disagree with maps ts types so, use old until migration completer for maps. - // This is also an issue when trying to refactor maps reducer into slice. - const selectedMap = useSelector((state: State) => state.maps.selectedMap); - const byMapID = useSelector((state: State) => state.maps.byMapID); - const editedMaps = useSelector((state: State) => state.maps.editedMaps); - if (meterIsFetching || groupIsFetching) { - return ; - } - - // Map to use. - let map; - // Holds Plotly mapping info. - const data = []; - // Holds the image to use. - let image; - if (selectedMap !== 0) { - const mapID = selectedMap; - if (byMapID[mapID]) { - map = byMapID[mapID]; - if (editedMaps[mapID]) { - map = editedMaps[mapID]; - } - } - // Holds the hover text for each point for Plotly - const hoverText: string[] = []; - // Holds the size of each circle for Plotly. - let size: number[] = []; - // Holds the color of each circle for Plotly. - const colors: string[] = []; - // If there is no map then use a new, empty image as the map. I believe this avoids errors - // and gives the blank screen. - image = map ? map.image : new Image(); - // Arrays to hold the Plotly grid location (x, y) for circles to place on map. - const x: number[] = []; - const y: number[] = []; - - // const timeInterval = state.graph.queryTimeInterval; - // const barDuration = state.graph.barDuration - // 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. - const imageDimensions: Dimensions = { - width: image.width, - height: image.height, - }; - // Determine the dimensions so within the Plotly coordinates on the user map. - const imageDimensionNormalized = - normalizeImageDimensions(imageDimensions); - // This is the origin & opposite from the calibration. It is the lower, left - // and upper, right corners of the user map. - const origin = map.origin; - const opposite = map.opposite; - // Get the GPS degrees per unit of Plotly grid for x and y. By knowing the two corners - // (or really any two distinct points) you can calculate this by the change in GPS over the - // change in x or y which is the map's width & height in this case. - const scaleOfMap = calculateScaleFromEndpoints( - origin, - opposite, - imageDimensionNormalized, - map.northAngle - ); - // Loop over all selected meters. Maps only work for meters at this time. - // The y-axis label depends on the unit which is in selectUnit state. - let unitLabel: string = ""; - // If graphingUnit is -99 then none selected and nothing to graph so label is empty. - // This will probably happen when the page is first loaded. - if (unitID !== -99) { - const selectUnitState = unitDataById[unitID]; - if (selectUnitState !== undefined) { - // Quantity and flow units have different unit labels. - // Look up the type of unit if it is for quantity/flow (should not be raw) and decide what to do. - // Bar graphics are always quantities. - if (selectUnitState.unitRepresent === UnitRepresentType.quantity) { - // If it is a quantity unit then that is the unit you are graphing but it is normalized to per day. - unitLabel = selectUnitState.identifier + " / day"; - } else if (selectUnitState.unitRepresent === UnitRepresentType.flow) { - // If it is a flow meter then you need to multiply by time to get the quantity unit then show as per day. - // The quantity/time for flow has varying time so label by multiplying by time. - // To make sure it is clear, also indicate it is a quantity. - // Note this should not be used for raw data. - // It might not be usual to take a flow and make it into a quantity so this label is a little different to - // catch people's attention. If sites/users don't like OED doing this then we can eliminate flow for these types - // of graphics as we are doing for rate. - unitLabel = - selectUnitState.identifier + " * time / day ≡ quantity / day"; - } - if (areaNormalization) { - unitLabel += " / " + translate(`AreaUnitType.${selectedAreaUnit}`); - } - } - } - - for (const meterID of selectedMeters) { - // Get meter id number. - // Get meter GPS value. - const gps = meterDataById[meterID].gps; - // filter meters with actual gps coordinates. - if (gps !== undefined && gps !== null && meterReadings !== undefined) { - let meterArea = meterDataById[meterID].area; - // we either don't care about area, or we do in which case there needs to be a nonzero area - if ( - !areaNormalization || - (meterArea > 0 && - meterDataById[meterID].areaUnit != AreaUnitType.none) - ) { - if (areaNormalization) { - // convert the meter area into the proper unit, if needed - meterArea *= getAreaUnitConversion( - meterDataById[meterID].areaUnit, - selectedAreaUnit - ); - } - // Convert the gps value to the equivalent Plotly grid coordinates on user map. - // First, convert from GPS to grid units. Since we are doing a GPS calculation, this happens on the true north map. - // It must be on true north map since only there are the GPS axis parallel to the map axis. - // To start, calculate the user grid coordinates (Plotly) from the GPS value. This involves calculating - // it coordinates on the true north map and then rotating/shifting to the user map. - const meterGPSInUserGrid: CartesianPoint = gpsToUserGrid( - imageDimensionNormalized, - gps, - origin, - scaleOfMap, - map.northAngle - ); - // Only display items within valid info and within map. - if ( - itemMapInfoOk(meterID, DataType.Meter, map, gps) && - itemDisplayableOnMap(imageDimensionNormalized, meterGPSInUserGrid) - ) { - // 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 - // 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[meterID]; - // This protects against there being no readings or that the data is being updated. - if (readingsData !== undefined && !meterIsFetching) { - // Meter name to include in hover on graph. - const label = meterDataById[meterID].identifier; - // The usual color for this meter. - colors.push(getGraphColor(meterID, DataType.Meter)); - if (!readingsData) { - throw new Error( - "Unacceptable condition: readingsData.readings is undefined." - ); - } - // Use the most recent time reading for the circle on the map. - // This has the limitations of the bar value where the last one can include ranges without - // data (GitHub issue on this). - // TODO: It might be better to do this similarly to compare. (See GitHub issue) - const readings = orderBy( - readingsData, - ["startTimestamp"], - ["desc"] - ); - const mapReading = readings[0]; - let timeReading: string; - let averagedReading = 0; - if (readings.length === 0) { - // No data. The next lines causes an issue so set specially. - // There may be a better overall fix for no data. - timeReading = "no data to display"; - size.push(0); - } else { - // 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) { - // 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(); - if (areaNormalization) { - averagedReading /= meterArea; - } - // The size is the reading value. It will be scaled later. - size.push(averagedReading); - } - // The hover text. - hoverText.push( - ` ${timeReading}
${label}: ${averagedReading.toPrecision( - 6 - )} ${unitLabel}` - ); - } - } - } - } - } - - for (const groupID of selectedGroups) { - // Get group id number. - // Get group GPS value. - const gps = groupDataById[groupID].gps; - // Filter groups with actual gps coordinates. - if (gps !== undefined && gps !== null && groupData !== undefined) { - let groupArea = groupDataById[groupID].area; - if ( - !areaNormalization || - (groupArea > 0 && - groupDataById[groupID].areaUnit != AreaUnitType.none) - ) { - if (areaNormalization) { - // convert the meter area into the proper unit, if needed - groupArea *= getAreaUnitConversion( - groupDataById[groupID].areaUnit, - selectedAreaUnit - ); - } - // Convert the gps value to the equivalent Plotly grid coordinates on user map. - // First, convert from GPS to grid units. Since we are doing a GPS calculation, this happens on the true north map. - // It must be on true north map since only there are the GPS axis parallel to the map axis. - // To start, calculate the user grid coordinates (Plotly) from the GPS value. This involves calculating - // it coordinates on the true north map and then rotating/shifting to the user map. - const groupGPSInUserGrid: CartesianPoint = gpsToUserGrid( - imageDimensionNormalized, - gps, - origin, - scaleOfMap, - map.northAngle - ); - // Only display items within valid info and within map. - if ( - itemMapInfoOk(groupID, DataType.Group, map, gps) && - itemDisplayableOnMap(imageDimensionNormalized, groupGPSInUserGrid) - ) { - // 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 - // 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]; - // This protects against there being no readings or that the data is being updated. - if (readingsData && !groupIsFetching) { - // Group name to include in hover on graph. - const label = groupDataById[groupID].name; - // The usual color for this group. - colors.push(getGraphColor(groupID, DataType.Group)); - if (!readingsData) { - throw new Error( - "Unacceptable condition: readingsData.readings is undefined." - ); - } - // Use the most recent time reading for the circle on the map. - // This has the limitations of the bar value where the last one can include ranges without - // data (GitHub issue on this). - // TODO: It might be better to do this similarly to compare. (See GitHub issue) - const readings = orderBy( - readingsData, - ["startTimestamp"], - ["desc"] - ); - const mapReading = readings[0]; - let timeReading: string; - let averagedReading = 0; - if (readings.length === 0) { - // No data. The next lines causes an issue so set specially. - // There may be a better overall fix for no data. - timeReading = "no data to display"; - size.push(0); - } 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) { - // 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(); - if (areaNormalization) { - averagedReading /= groupArea; - } - // The size is the reading value. It will be scaled later. - size.push(averagedReading); - } - // The hover text. - hoverText.push( - ` ${timeReading}
${label}: ${averagedReading.toPrecision( - 6 - )} ${unitLabel}` - ); - } - } - } - } - } - - // TODO DEBUG Using amp 1, 2 & 3 from test data that within map. This arbitrarily changes the value for testing. - // It does not change the hover value. - // size[0] = 300; - // size[1] = 100; - // size[2] = -1; - // size = size.slice(0, x.length); - - // TODO Using the following seems to have no impact on the code. It has been noticed that this function is called - // many times for each change. Someone should look at why that is happening and why some have no items in the arrays. - // if (size.length > 0) { - const maxFeatureFraction = map.circleSize; - // Find the smaller of width and height. This is used since it means the circle size will be - // scaled to that dimension and smaller relative to the other coordinate. - const minDimension = Math.min( - imageDimensionNormalized.width, - imageDimensionNormalized.height - ); - // The circle size is set to area below. Thus, we need to convert from wanting a max - // diameter of minDimension * maxFeatureFraction to an area. - const maxCircleSize = - Math.PI * Math.pow((minDimension * maxFeatureFraction) / 2, 2); - // What fraction of the max circle size that the min circle size will be. Determine empirically. - let minFeatureFractionOfMax = 0.05; - // If the maxFeatureFraction is too small then it is possible that the min circle will be very - // small and difficult to see. This value is the min circle size to make sure that does not happen. - // The value used was empirically determined so it would not be too small. - const circleSizeThreshold = 75; - // If the circle will be too small then force to min, otherwise use standard value. - minFeatureFractionOfMax = - minFeatureFractionOfMax * maxCircleSize < circleSizeThreshold - ? circleSizeThreshold / maxCircleSize - : minFeatureFractionOfMax; - // Find the min and max of the values to graph. - const min = Math.min(...size); - const max = Math.max(...size); - // Fix the range/difference between the max and min value. - const range = max - min; - // This is the min value that should be graphed if all the values were positive. This treats the - // values as if they started at zero. Thus, the minValue is the fraction of the max on this - // shifted range that will have a circle size at least the fraction of the max that is allowed. - const minValue = minFeatureFractionOfMax * range; - // TODO As also noted above, this component is rerendering multiple times. This is causing the - // information message to appear multiple times. This needs to be figured out. - // Debugging indicates that this happens when selecting a meter/group not recently used so the - // data is not in Redux store. Each time it has readingsData as undefined so it does not process - // that meter. Need to see how can avoid this. - - // Stores the amount to shift values for circle size for graphing where it is normally negative. - // Since subtract it adds to the value. It is negative for when the min is negative. - let shift; - if (range === 0) { - // All the values are the same including only one value and only zero. - // Plotly does not show circles of size 0 even if sizemin is set (hover does happen). - // To fix this, make the shift be min - 0.000123 (arbitrary value) so it will have a circle - // of the max size. The shifted value will be slightly positive (0.000123) so the circle shows. - // Note other cases avoid zero values if not the only one. - shift = min - 0.000123; - } else if (size.length != 0 && max < 0) { - // Need to test the size.length because the value is -infinity if no values. - // TODO Must be internationalized. - showInfoNotification( - "All values are negative so the circle sizes act as if value range was positive which may change the relative sizes." - ); - // All the values are negative. Plotly will only show circles that are positive. It isn't clear - // there is a perfect solution. This will show the size as if it was positive. For example, if - // it is -100, -200 & -300 then it will use 100, 200, 300. Note the ratio of -100 to -200 (the - // two largest values) is 2x whereas the shifted ones are 300 to 200 which is 1.5. This was - // decided the best of the options considered. - if (Math.abs(max) < minValue) { - // This takes care of case where the max is small so the shifted values will have a very small - // circle size. Force max to be at least minValue (note min negative so shift other way). - // TODO Must be internationalized. - showInfoNotification( - "Some values are close to zero so the small circle sizes may be a little larger to be visible" - ); - shift = min - minValue; - } else { - shift = min + max; - } - } else if (min >= minValue) { - // There are no values smaller than desired so all circles will be big enough. - // There is no need to shift the range of values. - shift = 0; - } else { - // Some values are too small. Shifting by min would start the range at zero. - // Subtracting minValue shifts more to a larger value so the smallest one - // is the min value to give the smallest circle desired. - // TODO Must be internationalized. Same as message above. - showInfoNotification( - "Some values are close to zero so the small circle sizes may be a little larger to be visible." - ); - shift = min - minValue; - if (max > 0 && min < 0) { - // Tell user that there are negative and positive values. - // Unlike all positive that sets the range to effectively start at zero to give users circle size - // that scale proportional to value, it is unclear the correct range for mixed sign values. - // Given this, shift as usual where the small value (large negative) will wind up near 0 and - // have a small circle size. This informs the user of the situation. - // TODO Must be internationalized. - showInfoNotification( - "There are negative and positive values and this impacts relative circle size." - ); - } - } - // Change all the sizes by the desired shift. Note the hover is not changed so - // this only impacts the circle size but not the value seen by the user. - size = size.map((size) => size - shift); - // This is how much Plotly will scale all circle sizes. (max - shift) is - // the new max value in the range of sizes - // given the shift just done. Dividing my maxCircleSize means the max value - // will have the a circle as big as the largest one desired. - const scaling = (max - shift) / maxCircleSize; - - // Per https://plotly.com/javascript/reference/scatter/: - // The opacity of 0.5 makes it possible to see the map even when there is a circle but the hover - // opacity is 1 so it is easy to see. - // Set the sizemode to area not diameter. - // Set the sizemin so a circle cannot get so small that it might disappear. Unsure the best size. - // Set the sizeref to scale each point to the desired area. - // Note all sizes are in px so have to estimate the actual size. This could be an issue but maps are currently - // a fixed size so not too much of an issue. - // Also note that the circle can go off the edge of the map. At some point it would be nice to have a border - // around the map to avoid this. - const traceOne = { - x, - y, - type: "scatter", - mode: "markers", - marker: { - color: colors, - opacity: 0.5, - size, - // The best sizemin of a circle is unclear. Given the shifting of sizes above this probably - // should never happen but left to be safe. - sizemin: 5, - sizeref: scaling, - sizemode: "area", - }, - text: hoverText, - hoverinfo: "text", - opacity: 1, - showlegend: false, - }; - data.push(traceOne); - } - } - - // set map background image - const layout: any = { - // Either the actual map name or text to say it is not available. - title: { - text: map ? map.name : translate("map.unavailable"), - }, - width: 1000, - height: 1000, - xaxis: { - visible: false, // changes all visibility settings including showgrid, zeroline, showticklabels and hiding ticks - range: [0, 500], // range of displayed graph - }, - yaxis: { - visible: false, - range: [0, 500], - scaleanchor: "x", - }, - images: [ - { - layer: "below", - source: image ? image.src : "", - xref: "x", - yref: "y", - x: 0, - y: 0, - sizex: 500, - sizey: 500, - xanchor: "left", - yanchor: "bottom", - sizing: "contain", - opacity: 1, - }, - ], - }; - - return ; + + const { meterArgs, groupArgs, meterShouldSkip, groupShouldSkip } = useAppSelector(selectMapChartQueryArgs); + const { data: meterReadings, isLoading: meterIsFetching } = readingsApi.useBarQuery(meterArgs, { skip: meterShouldSkip }); + const { data: groupData, isLoading: groupIsFetching } = readingsApi.useBarQuery(groupArgs, { skip: groupShouldSkip }); + + // 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 areaNormalization = useAppSelector(selectGraphAreaNormalization); + const selectedAreaUnit = useAppSelector(selectAreaUnit); + const selectedMeters = useAppSelector(selectSelectedMeters); + const selectedGroups = useAppSelector(selectSelectedGroups); + const unitDataById = useAppSelector(selectUnitDataById); + const groupDataById = useAppSelector(selectGroupDataById); + const meterDataById = useAppSelector(selectMeterDataById); + + // RTK Types Disagree with maps ts types so, use old until migration completer for maps. + // This is also an issue when trying to refactor maps reducer into slice. + const selectedMap = useSelector((state: State) => state.maps.selectedMap); + const byMapID = useSelector((state: State) => state.maps.byMapID); + const editedMaps = useSelector((state: State) => state.maps.editedMaps); + if (meterIsFetching || groupIsFetching) { + return ; + } + + + // Map to use. + let map; + // Holds Plotly mapping info. + const data = []; + // Holds the image to use. + let image; + if (selectedMap !== 0) { + const mapID = selectedMap; + if (byMapID[mapID]) { + map = byMapID[mapID]; + if (editedMaps[mapID]) { + map = editedMaps[mapID]; + } + } + // Holds the hover text for each point for Plotly + const hoverText: string[] = []; + // Holds the size of each circle for Plotly. + let size: number[] = []; + // Holds the color of each circle for Plotly. + const colors: string[] = []; + // If there is no map then use a new, empty image as the map. I believe this avoids errors + // and gives the blank screen. + image = (map) ? map.image : new Image(); + // Arrays to hold the Plotly grid location (x, y) for circles to place on map. + const x: number[] = []; + const y: number[] = []; + + // const timeInterval = state.graph.queryTimeInterval; + // const barDuration = state.graph.barDuration + // 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. + const imageDimensions: Dimensions = { + width: image.width, + height: image.height + }; + // Determine the dimensions so within the Plotly coordinates on the user map. + const imageDimensionNormalized = normalizeImageDimensions(imageDimensions); + // This is the origin & opposite from the calibration. It is the lower, left + // and upper, right corners of the user map. + const origin = map.origin; + const opposite = map.opposite; + // Get the GPS degrees per unit of Plotly grid for x and y. By knowing the two corners + // (or really any two distinct points) you can calculate this by the change in GPS over the + // change in x or y which is the map's width & height in this case. + const scaleOfMap = calculateScaleFromEndpoints(origin, opposite, imageDimensionNormalized, map.northAngle); + // Loop over all selected meters. Maps only work for meters at this time. + // The y-axis label depends on the unit which is in selectUnit state. + let unitLabel: string = ''; + // If graphingUnit is -99 then none selected and nothing to graph so label is empty. + // This will probably happen when the page is first loaded. + if (unitID !== -99) { + const selectUnitState = unitDataById[unitID]; + if (selectUnitState !== undefined) { + // Quantity and flow units have different unit labels. + // Look up the type of unit if it is for quantity/flow (should not be raw) and decide what to do. + // Bar graphics are always quantities. + if (selectUnitState.unitRepresent === UnitRepresentType.quantity) { + // If it is a quantity unit then that is the unit you are graphing but it is normalized to per day. + unitLabel = selectUnitState.identifier + ' / day'; + } else if (selectUnitState.unitRepresent === UnitRepresentType.flow) { + // If it is a flow meter then you need to multiply by time to get the quantity unit then show as per day. + // The quantity/time for flow has varying time so label by multiplying by time. + // To make sure it is clear, also indicate it is a quantity. + // Note this should not be used for raw data. + // It might not be usual to take a flow and make it into a quantity so this label is a little different to + // catch people's attention. If sites/users don't like OED doing this then we can eliminate flow for these types + // of graphics as we are doing for rate. + unitLabel = selectUnitState.identifier + ' * time / day ≡ quantity / day'; + } + if (areaNormalization) { + unitLabel += ' / ' + translate(`AreaUnitType.${selectedAreaUnit}`); + } + } + } + + for (const meterID of selectedMeters) { + // Get meter id number. + // Get meter GPS value. + const gps = meterDataById[meterID].gps; + // filter meters with actual gps coordinates. + if (gps !== undefined && gps !== null && meterReadings !== undefined) { + let meterArea = meterDataById[meterID].area; + // we either don't care about area, or we do in which case there needs to be a nonzero area + if (!areaNormalization || (meterArea > 0 && meterDataById[meterID].areaUnit != AreaUnitType.none)) { + if (areaNormalization) { + // convert the meter area into the proper unit, if needed + meterArea *= getAreaUnitConversion(meterDataById[meterID].areaUnit, selectedAreaUnit); + } + // Convert the gps value to the equivalent Plotly grid coordinates on user map. + // First, convert from GPS to grid units. Since we are doing a GPS calculation, this happens on the true north map. + // It must be on true north map since only there are the GPS axis parallel to the map axis. + // To start, calculate the user grid coordinates (Plotly) from the GPS value. This involves calculating + // it coordinates on the true north map and then rotating/shifting to the user map. + const meterGPSInUserGrid: CartesianPoint = gpsToUserGrid(imageDimensionNormalized, gps, origin, scaleOfMap, map.northAngle); + // Only display items within valid info and within map. + if (itemMapInfoOk(meterID, DataType.Meter, map, gps) && itemDisplayableOnMap(imageDimensionNormalized, meterGPSInUserGrid)) { + // 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 + // 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[meterID]; + // This protects against there being no readings or that the data is being updated. + if (readingsData !== undefined && !meterIsFetching) { + // Meter name to include in hover on graph. + const label = meterDataById[meterID].identifier; + // The usual color for this meter. + colors.push(getGraphColor(meterID, DataType.Meter)); + if (!readingsData) { + throw new Error('Unacceptable condition: readingsData.readings is undefined.'); + } + // Use the most recent time reading for the circle on the map. + // This has the limitations of the bar value where the last one can include ranges without + // data (GitHub issue on this). + // TODO: It might be better to do this similarly to compare. (See GitHub issue) + const readings = orderBy(readingsData, ['startTimestamp'], ['desc']); + const mapReading = readings[0]; + let timeReading: string; + let averagedReading = 0; + if (readings.length === 0) { + // No data. The next lines causes an issue so set specially. + // There may be a better overall fix for no data. + timeReading = 'no data to display'; + size.push(0); + } else { + // 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) { + // 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(); + if (areaNormalization) { + averagedReading /= meterArea; + } + // The size is the reading value. It will be scaled later. + size.push(averagedReading); + } + // The hover text. + hoverText.push(` ${timeReading}
${label}: ${averagedReading.toPrecision(6)} ${unitLabel}`); + } + } + } + } + } + + for (const groupID of selectedGroups) { + // Get group id number. + // Get group GPS value. + const gps = groupDataById[groupID].gps; + // Filter groups with actual gps coordinates. + if (gps !== undefined && gps !== null && groupData !== undefined) { + let groupArea = groupDataById[groupID].area; + if (!areaNormalization || (groupArea > 0 && groupDataById[groupID].areaUnit != AreaUnitType.none)) { + if (areaNormalization) { + // convert the meter area into the proper unit, if needed + groupArea *= getAreaUnitConversion(groupDataById[groupID].areaUnit, selectedAreaUnit); + } + // Convert the gps value to the equivalent Plotly grid coordinates on user map. + // First, convert from GPS to grid units. Since we are doing a GPS calculation, this happens on the true north map. + // It must be on true north map since only there are the GPS axis parallel to the map axis. + // To start, calculate the user grid coordinates (Plotly) from the GPS value. This involves calculating + // it coordinates on the true north map and then rotating/shifting to the user map. + const groupGPSInUserGrid: CartesianPoint = gpsToUserGrid(imageDimensionNormalized, gps, origin, scaleOfMap, map.northAngle); + // Only display items within valid info and within map. + if (itemMapInfoOk(groupID, DataType.Group, map, gps) && itemDisplayableOnMap(imageDimensionNormalized, groupGPSInUserGrid)) { + // 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 + // 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]; + // This protects against there being no readings or that the data is being updated. + if (readingsData && !groupIsFetching) { + // Group name to include in hover on graph. + const label = groupDataById[groupID].name; + // The usual color for this group. + colors.push(getGraphColor(groupID, DataType.Group)); + if (!readingsData) { + throw new Error('Unacceptable condition: readingsData.readings is undefined.'); + } + // Use the most recent time reading for the circle on the map. + // This has the limitations of the bar value where the last one can include ranges without + // data (GitHub issue on this). + // TODO: It might be better to do this similarly to compare. (See GitHub issue) + const readings = orderBy(readingsData, ['startTimestamp'], ['desc']); + const mapReading = readings[0]; + let timeReading: string; + let averagedReading = 0; + if (readings.length === 0) { + // No data. The next lines causes an issue so set specially. + // There may be a better overall fix for no data. + timeReading = 'no data to display'; + size.push(0); + } 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) { + // 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(); + if (areaNormalization) { + averagedReading /= groupArea; + } + // The size is the reading value. It will be scaled later. + size.push(averagedReading); + } + // The hover text. + hoverText.push(` ${timeReading}
${label}: ${averagedReading.toPrecision(6)} ${unitLabel}`); + } + } + } + } + } + + // TODO DEBUG Using amp 1, 2 & 3 from test data that within map. This arbitrarily changes the value for testing. + // It does not change the hover value. + // size[0] = 300; + // size[1] = 100; + // size[2] = -1; + // size = size.slice(0, x.length); + + // TODO Using the following seems to have no impact on the code. It has been noticed that this function is called + // many times for each change. Someone should look at why that is happening and why some have no items in the arrays. + // if (size.length > 0) { + const maxFeatureFraction = map.circleSize; + // Find the smaller of width and height. This is used since it means the circle size will be + // scaled to that dimension and smaller relative to the other coordinate. + const minDimension = Math.min(imageDimensionNormalized.width, imageDimensionNormalized.height); + // The circle size is set to area below. Thus, we need to convert from wanting a max + // diameter of minDimension * maxFeatureFraction to an area. + const maxCircleSize = Math.PI * Math.pow(minDimension * maxFeatureFraction / 2, 2); + // What fraction of the max circle size that the min circle size will be. Determine empirically. + let minFeatureFractionOfMax = 0.05; + // If the maxFeatureFraction is too small then it is possible that the min circle will be very + // small and difficult to see. This value is the min circle size to make sure that does not happen. + // The value used was empirically determined so it would not be too small. + const circleSizeThreshold = 75; + // If the circle will be too small then force to min, otherwise use standard value. + minFeatureFractionOfMax = minFeatureFractionOfMax * maxCircleSize < circleSizeThreshold ? + circleSizeThreshold / maxCircleSize : minFeatureFractionOfMax; + // Find the min and max of the values to graph. + const min = Math.min(...size); + const max = Math.max(...size); + // Fix the range/difference between the max and min value. + const range = max - min; + // This is the min value that should be graphed if all the values were positive. This treats the + // values as if they started at zero. Thus, the minValue is the fraction of the max on this + // shifted range that will have a circle size at least the fraction of the max that is allowed. + const minValue = minFeatureFractionOfMax * range; + // TODO As also noted above, this component is rerendering multiple times. This is causing the + // information message to appear multiple times. This needs to be figured out. + // Debugging indicates that this happens when selecting a meter/group not recently used so the + // data is not in Redux store. Each time it has readingsData as undefined so it does not process + // that meter. Need to see how can avoid this. + + // Stores the amount to shift values for circle size for graphing where it is normally negative. + // Since subtract it adds to the value. It is negative for when the min is negative. + let shift; + if (range === 0) { + // All the values are the same including only one value and only zero. + // Plotly does not show circles of size 0 even if sizemin is set (hover does happen). + // To fix this, make the shift be min - 0.000123 (arbitrary value) so it will have a circle + // of the max size. The shifted value will be slightly positive (0.000123) so the circle shows. + // Note other cases avoid zero values if not the only one. + shift = min - 0.000123; + } else if (size.length != 0 && max < 0) { + // Need to test the size.length because the value is -infinity if no values. + // TODO Must be internationalized. + showInfoNotification('All values are negative so the circle sizes act as if value range was positive which may change the relative sizes.'); + // All the values are negative. Plotly will only show circles that are positive. It isn't clear + // there is a perfect solution. This will show the size as if it was positive. For example, if + // it is -100, -200 & -300 then it will use 100, 200, 300. Note the ratio of -100 to -200 (the + // two largest values) is 2x whereas the shifted ones are 300 to 200 which is 1.5. This was + // decided the best of the options considered. + if (Math.abs(max) < minValue) { + // This takes care of case where the max is small so the shifted values will have a very small + // circle size. Force max to be at least minValue (note min negative so shift other way). + // TODO Must be internationalized. + showInfoNotification('Some values are close to zero so the small circle sizes may be a little larger to be visible'); + shift = min - minValue; + } else { + shift = min + max; + } + } else if (min >= minValue) { + // There are no values smaller than desired so all circles will be big enough. + // There is no need to shift the range of values. + shift = 0; + } else { + // Some values are too small. Shifting by min would start the range at zero. + // Subtracting minValue shifts more to a larger value so the smallest one + // is the min value to give the smallest circle desired. + // TODO Must be internationalized. Same as message above. + showInfoNotification('Some values are close to zero so the small circle sizes may be a little larger to be visible.'); + shift = min - minValue; + if (max > 0 && min < 0) { + // Tell user that there are negative and positive values. + // Unlike all positive that sets the range to effectively start at zero to give users circle size + // that scale proportional to value, it is unclear the correct range for mixed sign values. + // Given this, shift as usual where the small value (large negative) will wind up near 0 and + // have a small circle size. This informs the user of the situation. + // TODO Must be internationalized. + showInfoNotification('There are negative and positive values and this impacts relative circle size.'); + } + } + // Change all the sizes by the desired shift. Note the hover is not changed so + // this only impacts the circle size but not the value seen by the user. + size = size.map(size => size - shift); + // This is how much Plotly will scale all circle sizes. (max - shift) is + // the new max value in the range of sizes + // given the shift just done. Dividing my maxCircleSize means the max value + // will have the a circle as big as the largest one desired. + const scaling = (max - shift) / maxCircleSize; + + // Per https://plotly.com/javascript/reference/scatter/: + // The opacity of 0.5 makes it possible to see the map even when there is a circle but the hover + // opacity is 1 so it is easy to see. + // Set the sizemode to area not diameter. + // Set the sizemin so a circle cannot get so small that it might disappear. Unsure the best size. + // Set the sizeref to scale each point to the desired area. + // Note all sizes are in px so have to estimate the actual size. This could be an issue but maps are currently + // a fixed size so not too much of an issue. + // Also note that the circle can go off the edge of the map. At some point it would be nice to have a border + // around the map to avoid this. + const traceOne = { + x, + y, + type: 'scatter', + mode: 'markers', + marker: { + color: colors, + opacity: 0.5, + size, + // The best sizemin of a circle is unclear. Given the shifting of sizes above this probably + // should never happen but left to be safe. + sizemin: 5, + sizeref: scaling, + sizemode: 'area' + }, + text: hoverText, + hoverinfo: 'text', + opacity: 1, + showlegend: false + }; + data.push(traceOne); + } + } + + // set map background image + const layout: any = { + // Either the actual map name or text to say it is not available. + title: { + text: (map) ? map.name : translate('map.unavailable') + }, + width: 1000, + height: 1000, + xaxis: { + visible: false, // changes all visibility settings including showgrid, zeroline, showticklabels and hiding ticks + range: [0, 500] // range of displayed graph + }, + yaxis: { + visible: false, + range: [0, 500], + scaleanchor: 'x' + }, + images: [{ + layer: 'below', + source: (image) ? image.src : '', + xref: 'x', + yref: 'y', + x: 0, + y: 0, + sizex: 500, + sizey: 500, + xanchor: 'left', + yanchor: 'bottom', + sizing: 'contain', + opacity: 1 + }] + }; + + return ( + + ); } From eec2bbf3e3998677c0d54efecc67e416ad6a74e5 Mon Sep 17 00:00:00 2001 From: mmehta2669 Date: Fri, 4 Oct 2024 21:25:17 -0500 Subject: [PATCH 38/43] Adding negative function to maps --- .../app/components/MapChartComponent.tsx | 870 +++++++++--------- 1 file changed, 435 insertions(+), 435 deletions(-) diff --git a/src/client/app/components/MapChartComponent.tsx b/src/client/app/components/MapChartComponent.tsx index 7fe3ca3e3..ed7032fd2 100644 --- a/src/client/app/components/MapChartComponent.tsx +++ b/src/client/app/components/MapChartComponent.tsx @@ -8,9 +8,9 @@ import * as React from 'react'; import Plot from 'react-plotly.js'; import { useSelector } from 'react-redux'; import { - selectAreaUnit, selectBarWidthDays, - selectGraphAreaNormalization, selectSelectedGroups, - selectSelectedMeters, selectSelectedUnit + selectAreaUnit, selectBarWidthDays, + selectGraphAreaNormalization, selectSelectedGroups, + selectSelectedMeters, selectSelectedUnit } from '../redux/slices/graphSlice'; import { selectGroupDataById } from '../redux/api/groupsApi'; import { selectMeterDataById } from '../redux/api/metersApi'; @@ -22,445 +22,445 @@ import { DataType } from '../types/Datasources'; import { State } from '../types/redux/state'; import { UnitRepresentType } from '../types/redux/units'; import { - CartesianPoint, - Dimensions, - calculateScaleFromEndpoints, - gpsToUserGrid, - itemDisplayableOnMap, - itemMapInfoOk, - normalizeImageDimensions + CartesianPoint, + Dimensions, + calculateScaleFromEndpoints, + gpsToUserGrid, + itemDisplayableOnMap, + itemMapInfoOk, + normalizeImageDimensions } from '../utils/calibration'; import { AreaUnitType, getAreaUnitConversion } from '../utils/getAreaUnitConversion'; import getGraphColor from '../utils/getGraphColor'; import translate from '../utils/translate'; import SpinnerComponent from './SpinnerComponent'; import { showInfoNotification } from '../utils/notifications'; - + /** * @returns map component */ export default function MapChartComponent() { - - const { meterArgs, groupArgs, meterShouldSkip, groupShouldSkip } = useAppSelector(selectMapChartQueryArgs); - const { data: meterReadings, isLoading: meterIsFetching } = readingsApi.useBarQuery(meterArgs, { skip: meterShouldSkip }); - const { data: groupData, isLoading: groupIsFetching } = readingsApi.useBarQuery(groupArgs, { skip: groupShouldSkip }); - - // 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 areaNormalization = useAppSelector(selectGraphAreaNormalization); - const selectedAreaUnit = useAppSelector(selectAreaUnit); - const selectedMeters = useAppSelector(selectSelectedMeters); - const selectedGroups = useAppSelector(selectSelectedGroups); - const unitDataById = useAppSelector(selectUnitDataById); - const groupDataById = useAppSelector(selectGroupDataById); - const meterDataById = useAppSelector(selectMeterDataById); - - // RTK Types Disagree with maps ts types so, use old until migration completer for maps. - // This is also an issue when trying to refactor maps reducer into slice. - const selectedMap = useSelector((state: State) => state.maps.selectedMap); - const byMapID = useSelector((state: State) => state.maps.byMapID); - const editedMaps = useSelector((state: State) => state.maps.editedMaps); - if (meterIsFetching || groupIsFetching) { - return ; - } - - - // Map to use. - let map; - // Holds Plotly mapping info. - const data = []; - // Holds the image to use. - let image; - if (selectedMap !== 0) { - const mapID = selectedMap; - if (byMapID[mapID]) { - map = byMapID[mapID]; - if (editedMaps[mapID]) { - map = editedMaps[mapID]; - } - } - // Holds the hover text for each point for Plotly - const hoverText: string[] = []; - // Holds the size of each circle for Plotly. - let size: number[] = []; - // Holds the color of each circle for Plotly. - const colors: string[] = []; - // If there is no map then use a new, empty image as the map. I believe this avoids errors - // and gives the blank screen. - image = (map) ? map.image : new Image(); - // Arrays to hold the Plotly grid location (x, y) for circles to place on map. - const x: number[] = []; - const y: number[] = []; - - // const timeInterval = state.graph.queryTimeInterval; - // const barDuration = state.graph.barDuration - // 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. - const imageDimensions: Dimensions = { - width: image.width, - height: image.height - }; - // Determine the dimensions so within the Plotly coordinates on the user map. - const imageDimensionNormalized = normalizeImageDimensions(imageDimensions); - // This is the origin & opposite from the calibration. It is the lower, left - // and upper, right corners of the user map. - const origin = map.origin; - const opposite = map.opposite; - // Get the GPS degrees per unit of Plotly grid for x and y. By knowing the two corners - // (or really any two distinct points) you can calculate this by the change in GPS over the - // change in x or y which is the map's width & height in this case. - const scaleOfMap = calculateScaleFromEndpoints(origin, opposite, imageDimensionNormalized, map.northAngle); - // Loop over all selected meters. Maps only work for meters at this time. - // The y-axis label depends on the unit which is in selectUnit state. - let unitLabel: string = ''; - // If graphingUnit is -99 then none selected and nothing to graph so label is empty. - // This will probably happen when the page is first loaded. - if (unitID !== -99) { - const selectUnitState = unitDataById[unitID]; - if (selectUnitState !== undefined) { - // Quantity and flow units have different unit labels. - // Look up the type of unit if it is for quantity/flow (should not be raw) and decide what to do. - // Bar graphics are always quantities. - if (selectUnitState.unitRepresent === UnitRepresentType.quantity) { - // If it is a quantity unit then that is the unit you are graphing but it is normalized to per day. - unitLabel = selectUnitState.identifier + ' / day'; - } else if (selectUnitState.unitRepresent === UnitRepresentType.flow) { - // If it is a flow meter then you need to multiply by time to get the quantity unit then show as per day. - // The quantity/time for flow has varying time so label by multiplying by time. - // To make sure it is clear, also indicate it is a quantity. - // Note this should not be used for raw data. - // It might not be usual to take a flow and make it into a quantity so this label is a little different to - // catch people's attention. If sites/users don't like OED doing this then we can eliminate flow for these types - // of graphics as we are doing for rate. - unitLabel = selectUnitState.identifier + ' * time / day ≡ quantity / day'; - } - if (areaNormalization) { - unitLabel += ' / ' + translate(`AreaUnitType.${selectedAreaUnit}`); - } - } - } - - for (const meterID of selectedMeters) { - // Get meter id number. - // Get meter GPS value. - const gps = meterDataById[meterID].gps; - // filter meters with actual gps coordinates. - if (gps !== undefined && gps !== null && meterReadings !== undefined) { - let meterArea = meterDataById[meterID].area; - // we either don't care about area, or we do in which case there needs to be a nonzero area - if (!areaNormalization || (meterArea > 0 && meterDataById[meterID].areaUnit != AreaUnitType.none)) { - if (areaNormalization) { - // convert the meter area into the proper unit, if needed - meterArea *= getAreaUnitConversion(meterDataById[meterID].areaUnit, selectedAreaUnit); - } - // Convert the gps value to the equivalent Plotly grid coordinates on user map. - // First, convert from GPS to grid units. Since we are doing a GPS calculation, this happens on the true north map. - // It must be on true north map since only there are the GPS axis parallel to the map axis. - // To start, calculate the user grid coordinates (Plotly) from the GPS value. This involves calculating - // it coordinates on the true north map and then rotating/shifting to the user map. - const meterGPSInUserGrid: CartesianPoint = gpsToUserGrid(imageDimensionNormalized, gps, origin, scaleOfMap, map.northAngle); - // Only display items within valid info and within map. - if (itemMapInfoOk(meterID, DataType.Meter, map, gps) && itemDisplayableOnMap(imageDimensionNormalized, meterGPSInUserGrid)) { - // 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 - // 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[meterID]; - // This protects against there being no readings or that the data is being updated. - if (readingsData !== undefined && !meterIsFetching) { - // Meter name to include in hover on graph. - const label = meterDataById[meterID].identifier; - // The usual color for this meter. - colors.push(getGraphColor(meterID, DataType.Meter)); - if (!readingsData) { - throw new Error('Unacceptable condition: readingsData.readings is undefined.'); - } - // Use the most recent time reading for the circle on the map. - // This has the limitations of the bar value where the last one can include ranges without - // data (GitHub issue on this). - // TODO: It might be better to do this similarly to compare. (See GitHub issue) - const readings = orderBy(readingsData, ['startTimestamp'], ['desc']); - const mapReading = readings[0]; - let timeReading: string; - let averagedReading = 0; - if (readings.length === 0) { - // No data. The next lines causes an issue so set specially. - // There may be a better overall fix for no data. - timeReading = 'no data to display'; - size.push(0); - } else { - // 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) { - // 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(); - if (areaNormalization) { - averagedReading /= meterArea; - } - // The size is the reading value. It will be scaled later. - size.push(averagedReading); - } - // The hover text. - hoverText.push(` ${timeReading}
${label}: ${averagedReading.toPrecision(6)} ${unitLabel}`); - } - } - } - } - } - - for (const groupID of selectedGroups) { - // Get group id number. - // Get group GPS value. - const gps = groupDataById[groupID].gps; - // Filter groups with actual gps coordinates. - if (gps !== undefined && gps !== null && groupData !== undefined) { - let groupArea = groupDataById[groupID].area; - if (!areaNormalization || (groupArea > 0 && groupDataById[groupID].areaUnit != AreaUnitType.none)) { - if (areaNormalization) { - // convert the meter area into the proper unit, if needed - groupArea *= getAreaUnitConversion(groupDataById[groupID].areaUnit, selectedAreaUnit); - } - // Convert the gps value to the equivalent Plotly grid coordinates on user map. - // First, convert from GPS to grid units. Since we are doing a GPS calculation, this happens on the true north map. - // It must be on true north map since only there are the GPS axis parallel to the map axis. - // To start, calculate the user grid coordinates (Plotly) from the GPS value. This involves calculating - // it coordinates on the true north map and then rotating/shifting to the user map. - const groupGPSInUserGrid: CartesianPoint = gpsToUserGrid(imageDimensionNormalized, gps, origin, scaleOfMap, map.northAngle); - // Only display items within valid info and within map. - if (itemMapInfoOk(groupID, DataType.Group, map, gps) && itemDisplayableOnMap(imageDimensionNormalized, groupGPSInUserGrid)) { - // 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 - // 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]; - // This protects against there being no readings or that the data is being updated. - if (readingsData && !groupIsFetching) { - // Group name to include in hover on graph. - const label = groupDataById[groupID].name; - // The usual color for this group. - colors.push(getGraphColor(groupID, DataType.Group)); - if (!readingsData) { - throw new Error('Unacceptable condition: readingsData.readings is undefined.'); - } - // Use the most recent time reading for the circle on the map. - // This has the limitations of the bar value where the last one can include ranges without - // data (GitHub issue on this). - // TODO: It might be better to do this similarly to compare. (See GitHub issue) - const readings = orderBy(readingsData, ['startTimestamp'], ['desc']); - const mapReading = readings[0]; - let timeReading: string; - let averagedReading = 0; - if (readings.length === 0) { - // No data. The next lines causes an issue so set specially. - // There may be a better overall fix for no data. - timeReading = 'no data to display'; - size.push(0); - } 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) { - // 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(); - if (areaNormalization) { - averagedReading /= groupArea; - } - // The size is the reading value. It will be scaled later. - size.push(averagedReading); - } - // The hover text. - hoverText.push(` ${timeReading}
${label}: ${averagedReading.toPrecision(6)} ${unitLabel}`); - } - } - } - } - } - - // TODO DEBUG Using amp 1, 2 & 3 from test data that within map. This arbitrarily changes the value for testing. - // It does not change the hover value. - // size[0] = 300; - // size[1] = 100; - // size[2] = -1; - // size = size.slice(0, x.length); - - // TODO Using the following seems to have no impact on the code. It has been noticed that this function is called - // many times for each change. Someone should look at why that is happening and why some have no items in the arrays. - // if (size.length > 0) { - const maxFeatureFraction = map.circleSize; - // Find the smaller of width and height. This is used since it means the circle size will be - // scaled to that dimension and smaller relative to the other coordinate. - const minDimension = Math.min(imageDimensionNormalized.width, imageDimensionNormalized.height); - // The circle size is set to area below. Thus, we need to convert from wanting a max - // diameter of minDimension * maxFeatureFraction to an area. - const maxCircleSize = Math.PI * Math.pow(minDimension * maxFeatureFraction / 2, 2); - // What fraction of the max circle size that the min circle size will be. Determine empirically. - let minFeatureFractionOfMax = 0.05; - // If the maxFeatureFraction is too small then it is possible that the min circle will be very - // small and difficult to see. This value is the min circle size to make sure that does not happen. - // The value used was empirically determined so it would not be too small. - const circleSizeThreshold = 75; - // If the circle will be too small then force to min, otherwise use standard value. - minFeatureFractionOfMax = minFeatureFractionOfMax * maxCircleSize < circleSizeThreshold ? - circleSizeThreshold / maxCircleSize : minFeatureFractionOfMax; - // Find the min and max of the values to graph. - const min = Math.min(...size); - const max = Math.max(...size); - // Fix the range/difference between the max and min value. - const range = max - min; - // This is the min value that should be graphed if all the values were positive. This treats the - // values as if they started at zero. Thus, the minValue is the fraction of the max on this - // shifted range that will have a circle size at least the fraction of the max that is allowed. - const minValue = minFeatureFractionOfMax * range; - // TODO As also noted above, this component is rerendering multiple times. This is causing the - // information message to appear multiple times. This needs to be figured out. - // Debugging indicates that this happens when selecting a meter/group not recently used so the - // data is not in Redux store. Each time it has readingsData as undefined so it does not process - // that meter. Need to see how can avoid this. - - // Stores the amount to shift values for circle size for graphing where it is normally negative. - // Since subtract it adds to the value. It is negative for when the min is negative. - let shift; - if (range === 0) { - // All the values are the same including only one value and only zero. - // Plotly does not show circles of size 0 even if sizemin is set (hover does happen). - // To fix this, make the shift be min - 0.000123 (arbitrary value) so it will have a circle - // of the max size. The shifted value will be slightly positive (0.000123) so the circle shows. - // Note other cases avoid zero values if not the only one. - shift = min - 0.000123; - } else if (size.length != 0 && max < 0) { - // Need to test the size.length because the value is -infinity if no values. - // TODO Must be internationalized. - showInfoNotification('All values are negative so the circle sizes act as if value range was positive which may change the relative sizes.'); - // All the values are negative. Plotly will only show circles that are positive. It isn't clear - // there is a perfect solution. This will show the size as if it was positive. For example, if - // it is -100, -200 & -300 then it will use 100, 200, 300. Note the ratio of -100 to -200 (the - // two largest values) is 2x whereas the shifted ones are 300 to 200 which is 1.5. This was - // decided the best of the options considered. - if (Math.abs(max) < minValue) { - // This takes care of case where the max is small so the shifted values will have a very small - // circle size. Force max to be at least minValue (note min negative so shift other way). - // TODO Must be internationalized. - showInfoNotification('Some values are close to zero so the small circle sizes may be a little larger to be visible'); - shift = min - minValue; - } else { - shift = min + max; - } - } else if (min >= minValue) { - // There are no values smaller than desired so all circles will be big enough. - // There is no need to shift the range of values. - shift = 0; - } else { - // Some values are too small. Shifting by min would start the range at zero. - // Subtracting minValue shifts more to a larger value so the smallest one - // is the min value to give the smallest circle desired. - // TODO Must be internationalized. Same as message above. - showInfoNotification('Some values are close to zero so the small circle sizes may be a little larger to be visible.'); - shift = min - minValue; - if (max > 0 && min < 0) { - // Tell user that there are negative and positive values. - // Unlike all positive that sets the range to effectively start at zero to give users circle size - // that scale proportional to value, it is unclear the correct range for mixed sign values. - // Given this, shift as usual where the small value (large negative) will wind up near 0 and - // have a small circle size. This informs the user of the situation. - // TODO Must be internationalized. - showInfoNotification('There are negative and positive values and this impacts relative circle size.'); - } - } - // Change all the sizes by the desired shift. Note the hover is not changed so - // this only impacts the circle size but not the value seen by the user. - size = size.map(size => size - shift); - // This is how much Plotly will scale all circle sizes. (max - shift) is - // the new max value in the range of sizes - // given the shift just done. Dividing my maxCircleSize means the max value - // will have the a circle as big as the largest one desired. - const scaling = (max - shift) / maxCircleSize; - - // Per https://plotly.com/javascript/reference/scatter/: - // The opacity of 0.5 makes it possible to see the map even when there is a circle but the hover - // opacity is 1 so it is easy to see. - // Set the sizemode to area not diameter. - // Set the sizemin so a circle cannot get so small that it might disappear. Unsure the best size. - // Set the sizeref to scale each point to the desired area. - // Note all sizes are in px so have to estimate the actual size. This could be an issue but maps are currently - // a fixed size so not too much of an issue. - // Also note that the circle can go off the edge of the map. At some point it would be nice to have a border - // around the map to avoid this. - const traceOne = { - x, - y, - type: 'scatter', - mode: 'markers', - marker: { - color: colors, - opacity: 0.5, - size, - // The best sizemin of a circle is unclear. Given the shifting of sizes above this probably - // should never happen but left to be safe. - sizemin: 5, - sizeref: scaling, - sizemode: 'area' - }, - text: hoverText, - hoverinfo: 'text', - opacity: 1, - showlegend: false - }; - data.push(traceOne); - } - } - - // set map background image - const layout: any = { - // Either the actual map name or text to say it is not available. - title: { - text: (map) ? map.name : translate('map.unavailable') - }, - width: 1000, - height: 1000, - xaxis: { - visible: false, // changes all visibility settings including showgrid, zeroline, showticklabels and hiding ticks - range: [0, 500] // range of displayed graph - }, - yaxis: { - visible: false, - range: [0, 500], - scaleanchor: 'x' - }, - images: [{ - layer: 'below', - source: (image) ? image.src : '', - xref: 'x', - yref: 'y', - x: 0, - y: 0, - sizex: 500, - sizey: 500, - xanchor: 'left', - yanchor: 'bottom', - sizing: 'contain', - opacity: 1 - }] - }; - - return ( - - ); -} + + const { meterArgs, groupArgs, meterShouldSkip, groupShouldSkip } = useAppSelector(selectMapChartQueryArgs); + const { data: meterReadings, isLoading: meterIsFetching } = readingsApi.useBarQuery(meterArgs, { skip: meterShouldSkip }); + const { data: groupData, isLoading: groupIsFetching } = readingsApi.useBarQuery(groupArgs, { skip: groupShouldSkip }); + + // 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 areaNormalization = useAppSelector(selectGraphAreaNormalization); + const selectedAreaUnit = useAppSelector(selectAreaUnit); + const selectedMeters = useAppSelector(selectSelectedMeters); + const selectedGroups = useAppSelector(selectSelectedGroups); + const unitDataById = useAppSelector(selectUnitDataById); + const groupDataById = useAppSelector(selectGroupDataById); + const meterDataById = useAppSelector(selectMeterDataById); + + // RTK Types Disagree with maps ts types so, use old until migration completer for maps. + // This is also an issue when trying to refactor maps reducer into slice. + const selectedMap = useSelector((state: State) => state.maps.selectedMap); + const byMapID = useSelector((state: State) => state.maps.byMapID); + const editedMaps = useSelector((state: State) => state.maps.editedMaps); + if (meterIsFetching || groupIsFetching) { + return ; + } + + + // Map to use. + let map; + // Holds Plotly mapping info. + const data = []; + // Holds the image to use. + let image; + if (selectedMap !== 0) { + const mapID = selectedMap; + if (byMapID[mapID]) { + map = byMapID[mapID]; + if (editedMaps[mapID]) { + map = editedMaps[mapID]; + } + } + // Holds the hover text for each point for Plotly + const hoverText: string[] = []; + // Holds the size of each circle for Plotly. + let size: number[] = []; + // Holds the color of each circle for Plotly. + const colors: string[] = []; + // If there is no map then use a new, empty image as the map. I believe this avoids errors + // and gives the blank screen. + image = (map) ? map.image : new Image(); + // Arrays to hold the Plotly grid location (x, y) for circles to place on map. + const x: number[] = []; + const y: number[] = []; + + // const timeInterval = state.graph.queryTimeInterval; + // const barDuration = state.graph.barDuration + // 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. + const imageDimensions: Dimensions = { + width: image.width, + height: image.height + }; + // Determine the dimensions so within the Plotly coordinates on the user map. + const imageDimensionNormalized = normalizeImageDimensions(imageDimensions); + // This is the origin & opposite from the calibration. It is the lower, left + // and upper, right corners of the user map. + const origin = map.origin; + const opposite = map.opposite; + // Get the GPS degrees per unit of Plotly grid for x and y. By knowing the two corners + // (or really any two distinct points) you can calculate this by the change in GPS over the + // change in x or y which is the map's width & height in this case. + const scaleOfMap = calculateScaleFromEndpoints(origin, opposite, imageDimensionNormalized, map.northAngle); + // Loop over all selected meters. Maps only work for meters at this time. + // The y-axis label depends on the unit which is in selectUnit state. + let unitLabel: string = ''; + // If graphingUnit is -99 then none selected and nothing to graph so label is empty. + // This will probably happen when the page is first loaded. + if (unitID !== -99) { + const selectUnitState = unitDataById[unitID]; + if (selectUnitState !== undefined) { + // Quantity and flow units have different unit labels. + // Look up the type of unit if it is for quantity/flow (should not be raw) and decide what to do. + // Bar graphics are always quantities. + if (selectUnitState.unitRepresent === UnitRepresentType.quantity) { + // If it is a quantity unit then that is the unit you are graphing but it is normalized to per day. + unitLabel = selectUnitState.identifier + ' / day'; + } else if (selectUnitState.unitRepresent === UnitRepresentType.flow) { + // If it is a flow meter then you need to multiply by time to get the quantity unit then show as per day. + // The quantity/time for flow has varying time so label by multiplying by time. + // To make sure it is clear, also indicate it is a quantity. + // Note this should not be used for raw data. + // It might not be usual to take a flow and make it into a quantity so this label is a little different to + // catch people's attention. If sites/users don't like OED doing this then we can eliminate flow for these types + // of graphics as we are doing for rate. + unitLabel = selectUnitState.identifier + ' * time / day ≡ quantity / day'; + } + if (areaNormalization) { + unitLabel += ' / ' + translate(`AreaUnitType.${selectedAreaUnit}`); + } + } + } + + for (const meterID of selectedMeters) { + // Get meter id number. + // Get meter GPS value. + const gps = meterDataById[meterID].gps; + // filter meters with actual gps coordinates. + if (gps !== undefined && gps !== null && meterReadings !== undefined) { + let meterArea = meterDataById[meterID].area; + // we either don't care about area, or we do in which case there needs to be a nonzero area + if (!areaNormalization || (meterArea > 0 && meterDataById[meterID].areaUnit != AreaUnitType.none)) { + if (areaNormalization) { + // convert the meter area into the proper unit, if needed + meterArea *= getAreaUnitConversion(meterDataById[meterID].areaUnit, selectedAreaUnit); + } + // Convert the gps value to the equivalent Plotly grid coordinates on user map. + // First, convert from GPS to grid units. Since we are doing a GPS calculation, this happens on the true north map. + // It must be on true north map since only there are the GPS axis parallel to the map axis. + // To start, calculate the user grid coordinates (Plotly) from the GPS value. This involves calculating + // it coordinates on the true north map and then rotating/shifting to the user map. + const meterGPSInUserGrid: CartesianPoint = gpsToUserGrid(imageDimensionNormalized, gps, origin, scaleOfMap, map.northAngle); + // Only display items within valid info and within map. + if (itemMapInfoOk(meterID, DataType.Meter, map, gps) && itemDisplayableOnMap(imageDimensionNormalized, meterGPSInUserGrid)) { + // 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 + // 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[meterID]; + // This protects against there being no readings or that the data is being updated. + if (readingsData !== undefined && !meterIsFetching) { + // Meter name to include in hover on graph. + const label = meterDataById[meterID].identifier; + // The usual color for this meter. + colors.push(getGraphColor(meterID, DataType.Meter)); + if (!readingsData) { + throw new Error('Unacceptable condition: readingsData.readings is undefined.'); + } + // Use the most recent time reading for the circle on the map. + // This has the limitations of the bar value where the last one can include ranges without + // data (GitHub issue on this). + // TODO: It might be better to do this similarly to compare. (See GitHub issue) + const readings = orderBy(readingsData, ['startTimestamp'], ['desc']); + const mapReading = readings[0]; + let timeReading: string; + let averagedReading = 0; + if (readings.length === 0) { + // No data. The next lines causes an issue so set specially. + // There may be a better overall fix for no data. + timeReading = 'no data to display'; + size.push(0); + } else { + // 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) { + // 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(); + if (areaNormalization) { + averagedReading /= meterArea; + } + // The size is the reading value. It will be scaled later. + size.push(averagedReading); + } + // The hover text. + hoverText.push(` ${timeReading}
${label}: ${averagedReading.toPrecision(6)} ${unitLabel}`); + } + } + } + } + } + + for (const groupID of selectedGroups) { + // Get group id number. + // Get group GPS value. + const gps = groupDataById[groupID].gps; + // Filter groups with actual gps coordinates. + if (gps !== undefined && gps !== null && groupData !== undefined) { + let groupArea = groupDataById[groupID].area; + if (!areaNormalization || (groupArea > 0 && groupDataById[groupID].areaUnit != AreaUnitType.none)) { + if (areaNormalization) { + // convert the meter area into the proper unit, if needed + groupArea *= getAreaUnitConversion(groupDataById[groupID].areaUnit, selectedAreaUnit); + } + // Convert the gps value to the equivalent Plotly grid coordinates on user map. + // First, convert from GPS to grid units. Since we are doing a GPS calculation, this happens on the true north map. + // It must be on true north map since only there are the GPS axis parallel to the map axis. + // To start, calculate the user grid coordinates (Plotly) from the GPS value. This involves calculating + // it coordinates on the true north map and then rotating/shifting to the user map. + const groupGPSInUserGrid: CartesianPoint = gpsToUserGrid(imageDimensionNormalized, gps, origin, scaleOfMap, map.northAngle); + // Only display items within valid info and within map. + if (itemMapInfoOk(groupID, DataType.Group, map, gps) && itemDisplayableOnMap(imageDimensionNormalized, groupGPSInUserGrid)) { + // 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 + // 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]; + // This protects against there being no readings or that the data is being updated. + if (readingsData && !groupIsFetching) { + // Group name to include in hover on graph. + const label = groupDataById[groupID].name; + // The usual color for this group. + colors.push(getGraphColor(groupID, DataType.Group)); + if (!readingsData) { + throw new Error('Unacceptable condition: readingsData.readings is undefined.'); + } + // Use the most recent time reading for the circle on the map. + // This has the limitations of the bar value where the last one can include ranges without + // data (GitHub issue on this). + // TODO: It might be better to do this similarly to compare. (See GitHub issue) + const readings = orderBy(readingsData, ['startTimestamp'], ['desc']); + const mapReading = readings[0]; + let timeReading: string; + let averagedReading = 0; + if (readings.length === 0) { + // No data. The next lines causes an issue so set specially. + // There may be a better overall fix for no data. + timeReading = 'no data to display'; + size.push(0); + } 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) { + // 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(); + if (areaNormalization) { + averagedReading /= groupArea; + } + // The size is the reading value. It will be scaled later. + size.push(averagedReading); + } + // The hover text. + hoverText.push(` ${timeReading}
${label}: ${averagedReading.toPrecision(6)} ${unitLabel}`); + } + } + } + } + } + + // TODO DEBUG Using amp 1, 2 & 3 from test data that within map. This arbitrarily changes the value for testing. + // It does not change the hover value. + // size[0] = 300; + // size[1] = 100; + // size[2] = -1; + // size = size.slice(0, x.length); + + // TODO Using the following seems to have no impact on the code. It has been noticed that this function is called + // many times for each change. Someone should look at why that is happening and why some have no items in the arrays. + // if (size.length > 0) { + const maxFeatureFraction = map.circleSize; + // Find the smaller of width and height. This is used since it means the circle size will be + // scaled to that dimension and smaller relative to the other coordinate. + const minDimension = Math.min(imageDimensionNormalized.width, imageDimensionNormalized.height); + // The circle size is set to area below. Thus, we need to convert from wanting a max + // diameter of minDimension * maxFeatureFraction to an area. + const maxCircleSize = Math.PI * Math.pow(minDimension * maxFeatureFraction / 2, 2); + // What fraction of the max circle size that the min circle size will be. Determine empirically. + let minFeatureFractionOfMax = 0.05; + // If the maxFeatureFraction is too small then it is possible that the min circle will be very + // small and difficult to see. This value is the min circle size to make sure that does not happen. + // The value used was empirically determined so it would not be too small. + const circleSizeThreshold = 75; + // If the circle will be too small then force to min, otherwise use standard value. + minFeatureFractionOfMax = minFeatureFractionOfMax * maxCircleSize < circleSizeThreshold ? + circleSizeThreshold / maxCircleSize : minFeatureFractionOfMax; + // Find the min and max of the values to graph. + const min = Math.min(...size); + const max = Math.max(...size); + // Fix the range/difference between the max and min value. + const range = max - min; + // This is the min value that should be graphed if all the values were positive. This treats the + // values as if they started at zero. Thus, the minValue is the fraction of the max on this + // shifted range that will have a circle size at least the fraction of the max that is allowed. + const minValue = minFeatureFractionOfMax * range; + // TODO As also noted above, this component is rerendering multiple times. This is causing the + // information message to appear multiple times. This needs to be figured out. + // Debugging indicates that this happens when selecting a meter/group not recently used so the + // data is not in Redux store. Each time it has readingsData as undefined so it does not process + // that meter. Need to see how can avoid this. + + // Stores the amount to shift values for circle size for graphing where it is normally negative. + // Since subtract it adds to the value. It is negative for when the min is negative. + let shift; + if (range === 0) { + // All the values are the same including only one value and only zero. + // Plotly does not show circles of size 0 even if sizemin is set (hover does happen). + // To fix this, make the shift be min - 0.000123 (arbitrary value) so it will have a circle + // of the max size. The shifted value will be slightly positive (0.000123) so the circle shows. + // Note other cases avoid zero values if not the only one. + shift = min - 0.000123; + } else if (size.length != 0 && max < 0) { + // Need to test the size.length because the value is -infinity if no values. + // TODO Must be internationalized. + showInfoNotification('All values are negative so the circle sizes act as if value range was positive which may change the relative sizes.'); + // All the values are negative. Plotly will only show circles that are positive. It isn't clear + // there is a perfect solution. This will show the size as if it was positive. For example, if + // it is -100, -200 & -300 then it will use 100, 200, 300. Note the ratio of -100 to -200 (the + // two largest values) is 2x whereas the shifted ones are 300 to 200 which is 1.5. This was + // decided the best of the options considered. + if (Math.abs(max) < minValue) { + // This takes care of case where the max is small so the shifted values will have a very small + // circle size. Force max to be at least minValue (note min negative so shift other way). + // TODO Must be internationalized. + showInfoNotification('Some values are close to zero so the small circle sizes may be a little larger to be visible'); + shift = min - minValue; + } else { + shift = min + max; + } + } else if (min >= minValue) { + // There are no values smaller than desired so all circles will be big enough. + // There is no need to shift the range of values. + shift = 0; + } else { + // Some values are too small. Shifting by min would start the range at zero. + // Subtracting minValue shifts more to a larger value so the smallest one + // is the min value to give the smallest circle desired. + // TODO Must be internationalized. Same as message above. + showInfoNotification('Some values are close to zero so the small circle sizes may be a little larger to be visible.'); + shift = min - minValue; + if (max > 0 && min < 0) { + // Tell user that there are negative and positive values. + // Unlike all positive that sets the range to effectively start at zero to give users circle size + // that scale proportional to value, it is unclear the correct range for mixed sign values. + // Given this, shift as usual where the small value (large negative) will wind up near 0 and + // have a small circle size. This informs the user of the situation. + // TODO Must be internationalized. + showInfoNotification('There are negative and positive values and this impacts relative circle size.'); + } + } + // Change all the sizes by the desired shift. Note the hover is not changed so + // this only impacts the circle size but not the value seen by the user. + size = size.map(size => size - shift); + // This is how much Plotly will scale all circle sizes. (max - shift) is + // the new max value in the range of sizes + // given the shift just done. Dividing my maxCircleSize means the max value + // will have the a circle as big as the largest one desired. + const scaling = (max - shift) / maxCircleSize; + + // Per https://plotly.com/javascript/reference/scatter/: + // The opacity of 0.5 makes it possible to see the map even when there is a circle but the hover + // opacity is 1 so it is easy to see. + // Set the sizemode to area not diameter. + // Set the sizemin so a circle cannot get so small that it might disappear. Unsure the best size. + // Set the sizeref to scale each point to the desired area. + // Note all sizes are in px so have to estimate the actual size. This could be an issue but maps are currently + // a fixed size so not too much of an issue. + // Also note that the circle can go off the edge of the map. At some point it would be nice to have a border + // around the map to avoid this. + const traceOne = { + x, + y, + type: 'scatter', + mode: 'markers', + marker: { + color: colors, + opacity: 0.5, + size, + // The best sizemin of a circle is unclear. Given the shifting of sizes above this probably + // should never happen but left to be safe. + sizemin: 5, + sizeref: scaling, + sizemode: 'area' + }, + text: hoverText, + hoverinfo: 'text', + opacity: 1, + showlegend: false + }; + data.push(traceOne); + } + } + + // set map background image + const layout: any = { + // Either the actual map name or text to say it is not available. + title: { + text: (map) ? map.name : translate('map.unavailable') + }, + width: 1000, + height: 1000, + xaxis: { + visible: false, // changes all visibility settings including showgrid, zeroline, showticklabels and hiding ticks + range: [0, 500] // range of displayed graph + }, + yaxis: { + visible: false, + range: [0, 500], + scaleanchor: 'x' + }, + images: [{ + layer: 'below', + source: (image) ? image.src : '', + xref: 'x', + yref: 'y', + x: 0, + y: 0, + sizex: 500, + sizey: 500, + xanchor: 'left', + yanchor: 'bottom', + sizing: 'contain', + opacity: 1 + }] + }; + + return ( + + ); +} \ No newline at end of file From ac7c5287310669ab7d31c3c95bbf706fe72b036e Mon Sep 17 00:00:00 2001 From: Nischita Nannapaneni Date: Sat, 5 Oct 2024 14:46:27 -0500 Subject: [PATCH 39/43] slight edits made for better formatting --- src/client/app/components/ThreeDPillComponent.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/client/app/components/ThreeDPillComponent.tsx b/src/client/app/components/ThreeDPillComponent.tsx index dd450a3f6..cc6d3ca40 100644 --- a/src/client/app/components/ThreeDPillComponent.tsx +++ b/src/client/app/components/ThreeDPillComponent.tsx @@ -52,7 +52,7 @@ export default function ThreeDPillComponent() { meterOrGroup: singlePill.meterOrGroup })); } - }, [meterPillData, groupPillData, dispatch]); + }, [meterPillData, groupPillData]); // When a Pill Badge is clicked update threeD state to indicate new meter or group to render. const handlePillClick = (pillData: MeterOrGroupPill) => dispatch( @@ -62,7 +62,6 @@ export default function ThreeDPillComponent() { }) ); - // Method Generates Reactstrap Pill Badges for selected meters or groups const populatePills = (meterOrGroupPillData: MeterOrGroupPill[]) => { return meterOrGroupPillData.map(pillData => { From 72daf143b8d33c038848c0cc6f2237dde0bd3c40 Mon Sep 17 00:00:00 2001 From: mmehta2669 <143544538+mmehta2669@users.noreply.github.com> Date: Mon, 7 Oct 2024 16:11:57 -0500 Subject: [PATCH 40/43] Update MapChartComponent.tsx --- src/client/app/components/MapChartComponent.tsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/client/app/components/MapChartComponent.tsx b/src/client/app/components/MapChartComponent.tsx index ed7032fd2..cb0502e85 100644 --- a/src/client/app/components/MapChartComponent.tsx +++ b/src/client/app/components/MapChartComponent.tsx @@ -291,13 +291,6 @@ export default function MapChartComponent() { } } - // TODO DEBUG Using amp 1, 2 & 3 from test data that within map. This arbitrarily changes the value for testing. - // It does not change the hover value. - // size[0] = 300; - // size[1] = 100; - // size[2] = -1; - // size = size.slice(0, x.length); - // TODO Using the following seems to have no impact on the code. It has been noticed that this function is called // many times for each change. Someone should look at why that is happening and why some have no items in the arrays. // if (size.length > 0) { @@ -463,4 +456,4 @@ export default function MapChartComponent() { layout={layout} /> ); -} \ No newline at end of file +} From c7e5d17a10d0b54757a9bb6554780efffd6dad59 Mon Sep 17 00:00:00 2001 From: danielshid Date: Tue, 8 Oct 2024 17:24:48 -0400 Subject: [PATCH 41/43] fully integrate looping over the enum --- src/client/app/components/IntervalControlsComponent.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client/app/components/IntervalControlsComponent.tsx b/src/client/app/components/IntervalControlsComponent.tsx index 118bba711..eea3cd0a6 100644 --- a/src/client/app/components/IntervalControlsComponent.tsx +++ b/src/client/app/components/IntervalControlsComponent.tsx @@ -46,7 +46,8 @@ export default function IntervalControlsComponent() { React.useEffect(() => { // If user is in custom input mode, don't reset to standard options if (!isCustomInput) { - const isCustom = !['1', '7', '28'].includes(duration.asDays().toString()); + 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()); From 00339cf46842707a266544db264413078c0800af Mon Sep 17 00:00:00 2001 From: Andrew Date: Fri, 11 Oct 2024 10:52:19 -0400 Subject: [PATCH 42/43] Fixing requested changes from pull request --- src/server/test/web/readingsBarGroupFlow.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/server/test/web/readingsBarGroupFlow.js b/src/server/test/web/readingsBarGroupFlow.js index c4a429f90..fb598691a 100644 --- a/src/server/test/web/readingsBarGroupFlow.js +++ b/src/server/test/web/readingsBarGroupFlow.js @@ -11,21 +11,17 @@ const { chai, mocha, app } = require('../common'); const Unit = require('../../models/Unit'); const { prepareTest, parseExpectedCsv, - createTimeString, expectReadingToEqualExpected, getUnitId, ETERNITY, METER_ID, - GROUP_ID, - unitDatakWh, - conversionDatakWh, - meterDatakWh } = require('../../util/readingsUtils'); + GROUP_ID, } = require('../../util/readingsUtils'); mocha.describe('readings API', () => { mocha.describe('readings test, test if data returned by API is as expected', () => { mocha.describe('for bar charts', () => { - mocha.describe('for quantity groups', () => { - mocha.it('BG15: expected_bar_group_ri_15-20_mu_kW_gu_kW_st_-inf_et_inf_bd_13.csv', async () =>{ + mocha.describe('for flow groups', () => { + mocha.it('BG15: should have daily points for 15 + 20 minute reading intervals and flow units with +-inf start/end time & kW as kW', async () =>{ const unitDatakW = [ { // u4 From 3d9ccece1cbd3390adb4725039b3f0270a479c5b Mon Sep 17 00:00:00 2001 From: Steven Huss-Lederman Date: Fri, 11 Oct 2024 13:19:20 -0500 Subject: [PATCH 43/43] remove trailing comma --- src/server/test/web/readingsBarGroupFlow.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/test/web/readingsBarGroupFlow.js b/src/server/test/web/readingsBarGroupFlow.js index fb598691a..c87a8df87 100644 --- a/src/server/test/web/readingsBarGroupFlow.js +++ b/src/server/test/web/readingsBarGroupFlow.js @@ -15,7 +15,7 @@ const { prepareTest, getUnitId, ETERNITY, METER_ID, - GROUP_ID, } = require('../../util/readingsUtils'); + GROUP_ID } = require('../../util/readingsUtils'); mocha.describe('readings API', () => { mocha.describe('readings test, test if data returned by API is as expected', () => {