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

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

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

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

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

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

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