diff --git a/src/client/app/components/CompareLineChartComponent.tsx b/src/client/app/components/CompareLineChartComponent.tsx new file mode 100644 index 000000000..24603f964 --- /dev/null +++ b/src/client/app/components/CompareLineChartComponent.tsx @@ -0,0 +1,216 @@ +/* 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 { debounce } from 'lodash'; +import { utc } from 'moment'; +import * as React from 'react'; +import Plot from 'react-plotly.js'; +import { TimeInterval } from '../../../common/TimeInterval'; +import { updateSliderRange } from '../redux/actions/extraActions'; +import { readingsApi, stableEmptyLineReadings } from '../redux/api/readingsApi'; +import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; +import { selectCompareLineQueryArgs } from '../redux/selectors/chartQuerySelectors'; +import { selectLineUnitLabel } from '../redux/selectors/plotlyDataSelectors'; +import { selectSelectedLanguage } from '../redux/slices/appStateSlice'; +import Locales from '../types/locales'; +import translate from '../utils/translate'; +import SpinnerComponent from './SpinnerComponent'; +import { selectGraphState, selectShiftAmount, selectShiftTimeInterval, updateShiftTimeInterval } from '../redux/slices/graphSlice'; +import ThreeDPillComponent from './ThreeDPillComponent'; +import { selectThreeDComponentInfo } from '../redux/selectors/threeDSelectors'; +import { selectPlotlyGroupData, selectPlotlyMeterData } from '../redux/selectors/lineChartSelectors'; +import { MeterOrGroup, ShiftAmount } from '../types/redux/graph'; +import { PlotRelayoutEvent } from 'plotly.js'; +import { shiftDateFunc } from './CompareLineControlsComponent'; +/** + * @returns plotlyLine graphic + */ +export default function CompareLineChartComponent() { + const dispatch = useAppDispatch(); + const graphState = useAppSelector(selectGraphState); + const meterOrGroupID = useAppSelector(selectThreeDComponentInfo).meterOrGroupID; + const unitLabel = useAppSelector(selectLineUnitLabel); + const locale = useAppSelector(selectSelectedLanguage); + const shiftInterval = useAppSelector(selectShiftTimeInterval); + const shiftAmount = useAppSelector(selectShiftAmount); + const { args, shouldSkipQuery, argsDeps } = useAppSelector(selectCompareLineQueryArgs); + + // getting the time interval of current data + const timeInterval = graphState.queryTimeInterval; + + // Storing the time interval strings for the original data and the shifted data to use for range in plot + const [timeIntervalStr, setTimeIntervalStr] = React.useState(['', '']); + const [shiftIntervalStr, setShiftIntervalStr] = React.useState(['', '']); + + // Fetch original data, and derive plotly points + const { data, isFetching } = graphState.threeD.meterOrGroup === MeterOrGroup.meters ? + readingsApi.useLineQuery(args, + { + skip: shouldSkipQuery, + selectFromResult: ({ data, ...rest }) => ({ + ...rest, + data: selectPlotlyMeterData(data ?? stableEmptyLineReadings, + { ...argsDeps, compatibleEntities: [meterOrGroupID!] }) + }) + }) + : + readingsApi.useLineQuery(args, + { + skip: shouldSkipQuery, + selectFromResult: ({ data, ...rest }) => ({ + ...rest, + data: selectPlotlyGroupData(data ?? stableEmptyLineReadings, + { ...argsDeps, compatibleEntities: [meterOrGroupID!] }) + }) + }); + + // Callback function to update the shifted interval based on current interval and shift amount + const updateShiftedInterval = React.useCallback((start: moment.Moment, end: moment.Moment, shift: ShiftAmount) => { + const { shiftedStart, shiftedEnd } = shiftDateFunc(start, end, shift); + const newShiftedInterval = new TimeInterval(shiftedStart, shiftedEnd); + dispatch(updateShiftTimeInterval(newShiftedInterval)); + }, []); + + // Update shifted interval based on current interval and shift amount + React.useEffect(() => { + const startDate = timeInterval.getStartTimestamp(); + const endDate = timeInterval.getEndTimestamp(); + + if (startDate && endDate) { + setTimeIntervalStr([startDate.toISOString(), endDate.toISOString()]); + + if (shiftAmount !== ShiftAmount.none && shiftAmount !== ShiftAmount.custom) { + updateShiftedInterval(startDate, endDate, shiftAmount); + } + } + }, [timeInterval, shiftAmount, updateShiftedInterval]); + + // Update shift interval string based on shift interval or time interval + React.useEffect(() => { + const shiftStart = shiftInterval.getStartTimestamp(); + const shiftEnd = shiftInterval.getEndTimestamp(); + + if (shiftStart && shiftEnd) { + setShiftIntervalStr([shiftStart.toISOString(), shiftEnd.toISOString()]); + } else { + // If shift interval is not set, use the original time interval + const startDate = timeInterval.getStartTimestamp(); + const endDate = timeInterval.getEndTimestamp(); + if (startDate && endDate) { + setShiftIntervalStr([startDate.toISOString(), endDate.toISOString()]); + } + } + }, [shiftInterval, timeInterval]); + + // Getting the shifted data + const { data: dataNew, isFetching: isFetchingNew } = graphState.threeD.meterOrGroup === MeterOrGroup.meters ? + readingsApi.useLineQuery({ ...args, timeInterval: shiftInterval.toString() }, + { + skip: shouldSkipQuery, + selectFromResult: ({ data, ...rest }) => ({ + ...rest, + data: selectPlotlyMeterData(data ?? stableEmptyLineReadings, + { ...argsDeps, compatibleEntities: [meterOrGroupID!] }) + }) + }) + : + readingsApi.useLineQuery({ ...args, timeInterval: shiftInterval.toString() }, + { + skip: shouldSkipQuery, + selectFromResult: ({ data, ...rest }) => ({ + ...rest, + data: selectPlotlyGroupData(data ?? stableEmptyLineReadings, + { ...argsDeps, compatibleEntities: [meterOrGroupID!] }) + }) + }); + + if (isFetching || isFetchingNew) { + return ; + } + + // Check if there is at least one valid graph for current data and shifted data + const enoughData = data.find(data => data.x!.length > 1); + const enoughNewData = dataNew.find(dataNew => dataNew.x!.length > 1); + + // Customize the layout of the plot + // See https://community.plotly.com/t/replacing-an-empty-graph-with-a-message/31497 for showing text `not plot. + if (!graphState.threeD.meterOrGroup && (data.length === 0 || dataNew.length === 0)) { + return <>

{`${translate('select.meter.group')}`}

; + } else if (!enoughData || !enoughNewData) { + return <>

{`${translate('no.data.in.range')}`}

; + } else if (!timeInterval.getIsBounded()) { + return <>

{`${translate('please.set.the.date.range')}`}

; + } else { + // adding information to the shifted data so that it can be plotted on the same graph with current data + const updateDataNew = dataNew.map(item => ({ + ...item, + name: 'Shifted ' + item.name, + line: { ...item.line, color: '#1AA5F0' }, + xaxis: 'x2', + text: Array.isArray(item.text) + ? item.text.map(text => text.replace('
', '
Shifted ')) + : item.text?.replace('
', '
Shifted ') + })); + + return ( + <> + + { + // This event emits an object that contains values indicating changes in the user's graph, such as zooming. + if (e['xaxis.range[0]'] && e['xaxis.range[1]']) { + // The event signals changes in the user's interaction with the graph. + // this will automatically trigger a refetch due to updating a query arg. + const startTS = utc(e['xaxis.range[0]']); + const endTS = utc(e['xaxis.range[1]']); + const workingTimeInterval = new TimeInterval(startTS, endTS); + dispatch(updateSliderRange(workingTimeInterval)); + } + else if (e['xaxis.range']) { + // this case is when the slider knobs are dragged. + const range = e['xaxis.range']!; + const startTS = range && range[0]; + const endTS = range && range[1]; + dispatch(updateSliderRange(new TimeInterval(utc(startTS), utc(endTS)))); + } + }, 500, { leading: false, trailing: true }) + } + /> + + + ); + + } +} diff --git a/src/client/app/components/CompareLineControlsComponent.tsx b/src/client/app/components/CompareLineControlsComponent.tsx new file mode 100644 index 000000000..53c797a28 --- /dev/null +++ b/src/client/app/components/CompareLineControlsComponent.tsx @@ -0,0 +1,235 @@ +/* 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'; +// eslint-disable-next-line max-len +import { selectGraphState, selectQueryTimeInterval, selectShiftAmount, selectShiftTimeInterval, updateShiftAmount, updateShiftTimeInterval } from '../redux/slices/graphSlice'; +import translate from '../utils/translate'; +import { FormattedMessage } from 'react-intl'; +import { ShiftAmount } from '../types/redux/graph'; +import DateRangePicker from '@wojtekmaj/react-daterange-picker'; +import { dateRangeToTimeInterval, timeIntervalToDateRange } from '../utils/dateRangeCompatibility'; +import { selectSelectedLanguage } from '../redux/slices/appStateSlice'; +import { Value } from '@wojtekmaj/react-daterange-picker/dist/cjs/shared/types'; +import * as moment from 'moment'; +import { TimeInterval } from '../../../common/TimeInterval'; +import { showWarnNotification } from '../utils/notifications'; + +/** + * @returns compare line control page + */ +export default function CompareLineControlsComponent() { + const dispatch = useAppDispatch(); + const shiftAmount = useAppSelector(selectShiftAmount); + const timeInterval = useAppSelector(selectQueryTimeInterval); + const locale = useAppSelector(selectSelectedLanguage); + const shiftInterval = useAppSelector(selectShiftTimeInterval); + const graphState = useAppSelector(selectGraphState); + + // Hold value of shifting option (week, month, year, or custom) + const [shiftOption, setShiftOption] = React.useState(shiftAmount); + // Hold value to track whether custom data range picker should show up or not + const [showDatePicker, setShowDatePicker] = React.useState(false); + // Hold value to store the custom date range for the shift interval + const [customDateRange, setCustomDateRange] = React.useState(timeIntervalToDateRange(shiftInterval)); + + // Add this useEffect to update the shift interval when the shift option changes + React.useEffect(() => { + if (shiftOption !== ShiftAmount.custom) { + updateShiftInterval(shiftOption); + } + }, [shiftOption, timeInterval]); + + // Update custom date range value when shift interval changes + React.useEffect(() => { + setCustomDateRange(timeIntervalToDateRange(shiftInterval)); + }, [shiftInterval]); + + // Check for leap year shifting when new interval or meter/group is chosen + React.useEffect(() => { + const startDate = timeInterval.getStartTimestamp(); + const endDate = timeInterval.getEndTimestamp(); + if (startDate && endDate) { + // Check whether shifting to (or from) leap year to non leap year or not + checkLeapYearFunc(startDate, endDate, shiftOption); + } + }, [graphState.threeD.meterOrGroupID, timeInterval]); + + // Handle changes in shift option (week, month, year, or custom) + const handleShiftOptionChange = (value: string) => { + if (value === 'custom') { + setShiftOption(ShiftAmount.custom); + dispatch(updateShiftAmount(ShiftAmount.custom)); + setShowDatePicker(true); + } else { + setShowDatePicker(false); + const newShiftOption = value as ShiftAmount; + setShiftOption(newShiftOption); + dispatch(updateShiftAmount(newShiftOption)); + + // notify user when original data or shift data cross leap year + const startDate = timeInterval.getStartTimestamp(); + const endDate = timeInterval.getEndTimestamp(); + + if (startDate && endDate) { + // Check whether shifting to (or from) leap year to non leap year or not + checkLeapYearFunc(startDate, endDate, newShiftOption); + } + // Update shift interval when shift option changes + updateShiftInterval(newShiftOption); + } + }; + + // update shift data date range when shift date interval option is chosen + const updateShiftInterval = (shiftOption: ShiftAmount) => { + const startDate = timeInterval.getStartTimestamp(); + const endDate = timeInterval.getEndTimestamp(); + if (startDate !== null || endDate !== null) { + const { shiftedStart, shiftedEnd } = shiftDateFunc(startDate, endDate, shiftOption); + const newInterval = new TimeInterval(shiftedStart, shiftedEnd); + dispatch(updateShiftTimeInterval(newInterval)); + } + }; + + // Update date when the data range picker is used in custome shifting option + const handleShiftDateChange = (value: Value) => { + setCustomDateRange(value); + dispatch(updateShiftTimeInterval(dateRangeToTimeInterval(value))); + }; + + return ( + <> +
+

+ + {/* // TODO: Add later */} +

+ handleShiftOptionChange(e.target.value)} + > + + + + + + + {/* Show date picker when custom date range is selected */} + {showDatePicker && + } + +
+ + ); + +} + +const labelStyle: React.CSSProperties = { fontWeight: 'bold', margin: 0 }; + +/** + * shifting date function to find the shifted start date and shifted end date + * @param originalStart start date of current graph data + * @param originalEnd end date of current graph data + * @param shiftType shifting amount in week, month, or year + * @returns shifted start and shifted end dates for the new data + */ +export function shiftDateFunc(originalStart: moment.Moment, originalEnd: moment.Moment, shiftType: ShiftAmount) { + let shiftedStart: moment.Moment; + let shiftedEnd: moment.Moment; + + const originalRangeDays = originalEnd.diff(originalStart, 'days'); + + switch (shiftType) { + case 'none': + shiftedStart = originalStart.clone(); + shiftedEnd = originalEnd.clone(); + break; + + case 'week': + shiftedStart = originalStart.clone().subtract(7, 'days'); + shiftedEnd = originalEnd.clone().subtract(7, 'days'); + break; + + case 'month': + shiftedStart = originalStart.clone().subtract(1, 'months'); + shiftedEnd = shiftedStart.clone().add(originalRangeDays, 'days'); + + if (shiftedEnd.isSameOrAfter(originalStart)) { + shiftedEnd = originalStart.clone().subtract(1, 'day'); + } else if (originalStart.date() === 1 && originalEnd.date() === originalEnd.daysInMonth()) { + if (!(shiftedStart.date() === 1 && shiftedEnd.date() === shiftedEnd.daysInMonth())) { + shiftedEnd = shiftedStart.clone().endOf('month'); + } + } + break; + + case 'year': + shiftedStart = originalStart.clone().subtract(1, 'years'); + shiftedEnd = originalEnd.clone().subtract(1, 'years'); + + if (originalStart.isLeapYear() && originalStart.month() === 1 && originalStart.date() === 29) { + shiftedStart = shiftedStart.month(2).date(1); + } + if (originalEnd.isLeapYear() && originalEnd.month() === 1 && originalEnd.date() === 29) { + shiftedEnd = shiftedEnd.month(1).date(28); + } + if (shiftedEnd.isSameOrAfter(originalStart)) { + shiftedEnd = originalStart.clone().subtract(1, 'day'); + } + break; + + default: + shiftedStart = originalStart.clone(); + shiftedEnd = originalEnd.clone(); + } + + return { shiftedStart, shiftedEnd }; +} + +/** + * This function check whether the original date range is leap year or the shifted date range is leap year. + * If it is true, it warns user about shifting to (or from) a leap year to non leap year. + * @param startDate original data start date + * @param endDate original data end date + * @param shiftOption shifting option + */ +function checkLeapYearFunc(startDate: moment.Moment, endDate: moment.Moment, shiftOption: ShiftAmount) { + const { shiftedStart, shiftedEnd } = shiftDateFunc(startDate, endDate, shiftOption); + const originalIsLeapYear = startDate.isLeapYear() || endDate.isLeapYear(); + const shiftedIsLeapYear = shiftedStart.isLeapYear() || shiftedEnd.isLeapYear(); + + // Check if the original date range crosses Feb 29, which causes unaligned graph + const originalCrossFeb29 = ( + startDate.isLeapYear() && + startDate.isBefore(moment(`${startDate.year()}-03-01`)) && + endDate.isAfter(moment(`${startDate.year()}-02-28`)) + ); + + // Check if the shifted date range crosses Feb 29, which causes unaligned graph + const shiftedCrossFeb29 = ( + shiftedStart.isLeapYear() && + shiftedStart.isBefore(moment(`${shiftedStart.year()}-03-01`)) && + shiftedEnd.isAfter(moment(`${shiftedStart.year()}-02-28`)) + ); + + if (originalCrossFeb29 && !shiftedIsLeapYear) { + showWarnNotification(translate('original.data.crosses.leap.year.to.non.leap.year')); + } else if (shiftedCrossFeb29 && !originalIsLeapYear) { + showWarnNotification(translate('shifted.data.crosses.leap.year.to.non.leap.year')); + } +} \ No newline at end of file diff --git a/src/client/app/components/DashboardComponent.tsx b/src/client/app/components/DashboardComponent.tsx index 92fbcf3a2..ce63cdd32 100644 --- a/src/client/app/components/DashboardComponent.tsx +++ b/src/client/app/components/DashboardComponent.tsx @@ -15,6 +15,7 @@ import RadarChartComponent from './RadarChartComponent'; import ThreeDComponent from './ThreeDComponent'; import UIOptionsComponent from './UIOptionsComponent'; import PlotNavComponent from './PlotNavComponent'; +import CompareLineChartComponent from './CompareLineChartComponent'; /** * React component that controls the dashboard @@ -39,6 +40,7 @@ export default function DashboardComponent() { {chartToRender === ChartTypes.map && } {chartToRender === ChartTypes.threeD && } {chartToRender === ChartTypes.radar && } + {chartToRender === ChartTypes.compareLine && } diff --git a/src/client/app/components/MoreOptionsComponent.tsx b/src/client/app/components/MoreOptionsComponent.tsx index 4441d882c..2671c88a4 100644 --- a/src/client/app/components/MoreOptionsComponent.tsx +++ b/src/client/app/components/MoreOptionsComponent.tsx @@ -32,7 +32,7 @@ export default function MoreOptionsComponent() { return ( <> { -
+
@@ -74,6 +74,13 @@ export default function MoreOptionsComponent() { {chartToRender == ChartTypes.radar && } {chartToRender == ChartTypes.radar && } {chartToRender == ChartTypes.radar && } + + {/*More UI options for compare line */} + {chartToRender === ChartTypes.compareLine && } + {chartToRender === ChartTypes.compareLine && } + {chartToRender === ChartTypes.compareLine && } + {chartToRender === ChartTypes.compareLine && } + {chartToRender === ChartTypes.compareLine && } diff --git a/src/client/app/components/UIOptionsComponent.tsx b/src/client/app/components/UIOptionsComponent.tsx index 40d2f18b8..9fa904cbe 100644 --- a/src/client/app/components/UIOptionsComponent.tsx +++ b/src/client/app/components/UIOptionsComponent.tsx @@ -15,6 +15,7 @@ import DateRangeComponent from './DateRangeComponent'; import MapControlsComponent from './MapControlsComponent'; import ReadingsPerDaySelectComponent from './ReadingsPerDaySelectComponent'; import MoreOptionsComponent from './MoreOptionsComponent'; +import CompareLineControlsComponent from './CompareLineControlsComponent'; /** * @returns the UI Control panel @@ -80,6 +81,11 @@ export default function UIOptionsComponent() { {/* UI options for radar graphic */} {chartToRender == ChartTypes.radar} + { /* Controls specific to the compare line chart */} + {chartToRender === ChartTypes.compareLine && } + {chartToRender === ChartTypes.compareLine && } + +
diff --git a/src/client/app/redux/selectors/chartQuerySelectors.ts b/src/client/app/redux/selectors/chartQuerySelectors.ts index 73e68216b..b9e53bcff 100644 --- a/src/client/app/redux/selectors/chartQuerySelectors.ts +++ b/src/client/app/redux/selectors/chartQuerySelectors.ts @@ -14,6 +14,7 @@ import { selectSelectedUnit, selectThreeDState } from '../slices/graphSlice'; import { omit } from 'lodash'; +import { selectLineChartDeps } from './lineChartSelectors'; // query args that 'most' graphs share export interface commonQueryArgs { @@ -25,6 +26,7 @@ export interface commonQueryArgs { // endpoint specific args export interface LineReadingApiArgs extends commonQueryArgs { } +export interface CompareLineReadingApiArgs extends commonQueryArgs { } export interface BarReadingApiArgs extends commonQueryArgs { barWidthDays: number } // ThreeD only queries a single id so extend common, but omit ids array @@ -81,6 +83,32 @@ export const selectLineChartQueryArgs = createSelector( } ); +export const selectCompareLineQueryArgs = createSelector( + selectQueryTimeInterval, + selectSelectedUnit, + selectThreeDState, + selectLineChartDeps, + (queryTimeInterval, selectedUnit, threeD, lineChartDeps) => { + const args: CompareLineReadingApiArgs = + threeD.meterOrGroup === MeterOrGroup.meters + ? { + ids: [threeD.meterOrGroupID!], + timeInterval: queryTimeInterval.toString(), + graphicUnitId: selectedUnit, + meterOrGroup: threeD.meterOrGroup! + } + : { + ids: [threeD.meterOrGroupID!], + timeInterval: queryTimeInterval.toString(), + graphicUnitId: selectedUnit, + meterOrGroup: threeD.meterOrGroup! + }; + const shouldSkipQuery = !threeD.meterOrGroupID || !queryTimeInterval.getIsBounded(); + const argsDeps = threeD.meterOrGroup === MeterOrGroup.meters ? lineChartDeps.meterDeps : lineChartDeps.groupDeps; + return { args, shouldSkipQuery, argsDeps }; + } +); + export const selectRadarChartQueryArgs = createSelector( selectLineChartQueryArgs, lineChartArgs => { @@ -185,11 +213,13 @@ export const selectAllChartQueryArgs = createSelector( selectCompareChartQueryArgs, selectMapChartQueryArgs, selectThreeDQueryArgs, - (line, bar, compare, map, threeD) => ({ + selectCompareLineQueryArgs, + (line, bar, compare, map, threeD, compareLine) => ({ line, bar, compare, map, - threeD + threeD, + compareLine }) ); diff --git a/src/client/app/redux/selectors/lineChartSelectors.ts b/src/client/app/redux/selectors/lineChartSelectors.ts index d4ca92247..b69ec7b28 100644 --- a/src/client/app/redux/selectors/lineChartSelectors.ts +++ b/src/client/app/redux/selectors/lineChartSelectors.ts @@ -45,6 +45,7 @@ export const selectPlotlyMeterData = selectFromLineReadingsResult( const yMinData: number[] = []; const yMaxData: number[] = []; const hoverText: string[] = []; + // The scaling is the factor to change the reading by. It divides by the area while will be 1 if no scaling by area. readings.forEach(reading => { // As usual, we want to interpret the readings in UTC. We lose the timezone as this as the start/endTimestamp diff --git a/src/client/app/redux/slices/graphSlice.ts b/src/client/app/redux/slices/graphSlice.ts index 91e38edd2..0ccbc0c05 100644 --- a/src/client/app/redux/slices/graphSlice.ts +++ b/src/client/app/redux/slices/graphSlice.ts @@ -13,7 +13,7 @@ import { updateHistory, updateSliderRange } from '../../redux/actions/extraActions'; import { SelectOption } from '../../types/items'; -import { ChartTypes, GraphState, LineGraphRate, MeterOrGroup, ReadingInterval } from '../../types/redux/graph'; +import { ChartTypes, GraphState, LineGraphRate, MeterOrGroup, ReadingInterval, ShiftAmount } from '../../types/redux/graph'; import { ComparePeriod, SortingOrder, calculateCompareTimeInterval, validateComparePeriod, validateSortingOrder } from '../../utils/calculateCompare'; import { AreaUnitType } from '../../utils/getAreaUnitConversion'; import { preferencesApi } from '../api/preferencesApi'; @@ -39,7 +39,9 @@ const defaultState: GraphState = { meterOrGroup: undefined, readingInterval: ReadingInterval.Hourly }, - hotlinked: false + hotlinked: false, + shiftAmount: ShiftAmount.none, + shiftTimeInterval: TimeInterval.unbounded() }; interface History { @@ -88,6 +90,14 @@ export const graphSlice = createSlice({ state.current.queryTimeInterval = action.payload; } }, + updateShiftTimeInterval: (state, action: PayloadAction) => { + if (action.payload.getIsBounded() || state.current.shiftTimeInterval.getIsBounded()) { + state.current.shiftTimeInterval = action.payload; + } + }, + updateShiftAmount: (state, action: PayloadAction) => { + state.current.shiftAmount = action.payload; + }, changeSliderRange: (state, action: PayloadAction) => { if (action.payload.getIsBounded() || state.current.rangeSliderInterval.getIsBounded()) { state.current.rangeSliderInterval = action.payload; @@ -387,7 +397,9 @@ export const graphSlice = createSlice({ selectHistoryIsDirty: state => state.prev.length > 0 || state.next.length > 0, selectSliderRangeInterval: state => state.current.rangeSliderInterval, selectPlotlySliderMin: state => state.current.rangeSliderInterval.getStartTimestamp()?.utc().toDate().toISOString(), - selectPlotlySliderMax: state => state.current.rangeSliderInterval.getEndTimestamp()?.utc().toDate().toISOString() + selectPlotlySliderMax: state => state.current.rangeSliderInterval.getEndTimestamp()?.utc().toDate().toISOString(), + selectShiftAmount: state => state.current.shiftAmount, + selectShiftTimeInterval: state => state.current.shiftTimeInterval } }); @@ -405,7 +417,8 @@ export const { selectThreeDMeterOrGroupID, selectThreeDReadingInterval, selectGraphAreaNormalization, selectSliderRangeInterval, selectDefaultGraphState, selectHistoryIsDirty, - selectPlotlySliderMax, selectPlotlySliderMin + selectPlotlySliderMax, selectPlotlySliderMin, + selectShiftAmount, selectShiftTimeInterval } = graphSlice.selectors; // actionCreators exports @@ -422,6 +435,7 @@ export const { toggleAreaNormalization, updateThreeDMeterOrGroup, changeCompareSortingOrder, updateThreeDMeterOrGroupID, updateThreeDReadingInterval, updateThreeDMeterOrGroupInfo, - updateSelectedMetersOrGroups + updateSelectedMetersOrGroups, updateShiftAmount, + updateShiftTimeInterval } = graphSlice.actions; diff --git a/src/client/app/translations/data.ts b/src/client/app/translations/data.ts index 4b0b634a1..d909af5f6 100644 --- a/src/client/app/translations/data.ts +++ b/src/client/app/translations/data.ts @@ -47,7 +47,7 @@ const LocaleTranslationData = { "clipboard.copied": "Copied To Clipboard", "clipboard.not.copied": "Failed to Copy To Clipboard", "close": "Close", - "compare": "Compare", + "compare": "Compare bar", "compare.period": "Compare Period", "compare.raw": "Cannot create comparison graph on raw units such as temperature", "confirm.action": "Confirm Action", @@ -508,7 +508,19 @@ const LocaleTranslationData = { "week": "Week", "yes": "yes", "yesterday": "Yesterday", - "you.cannot.create.a.cyclic.group": "You cannot create a cyclic group" + "you.cannot.create.a.cyclic.group": "You cannot create a cyclic group", + "compare.line": "Compare line", + "shift.date.interval": "Shift Date Interval", + "1.month": "1 month", + "1.year": "1 year", + "2.months": "2 months", + "1.week": "1 week", + "compare.line.days.enter": "Enter in days and then hit enter", + "please.set.the.date.range": "Please choose date range", + "select.shift.amount": "Select shift amount", + "custom.date.range": "Custom date range", + "shifted.data.crosses.leap.year.to.non.leap.year": "Shifted data crosses a leap year so the graph might not align appropriately", + "original.data.crosses.leap.year.to.non.leap.year": "Original data crosses a leap year so the graph might not align appropriately" }, "fr": { "3D": "3D", @@ -1010,7 +1022,20 @@ const LocaleTranslationData = { "week": "Semaine", "yes": " yes\u{26A1}", "yesterday": "Hier", - "you.cannot.create.a.cyclic.group": "Vous ne pouvez pas créer un groupe cyclique" + "you.cannot.create.a.cyclic.group": "Vous ne pouvez pas créer un groupe cyclique", + "compare.line": "Compare line\u{26A1}", + "shift.date.interval": "Shift Date Interval\u{26A1}", + "a few seconds": "a few seconds\u{26A1}", + "1.month": "1 month\u{26A1}", + "1.year": "1 year\u{26A1}", + "2.months": "2 months\u{26A1}", + "1.week": "1 week\u{26A1}", + "compare.line.days.enter": "Enter in days and then hit enter\u{26A1}", + "please.set.the.date.range": "Please choose date range\u{26A1}", + "select.shift.amount": "Select shift amount\u{26A1}", + "custom.date.range": "Custom date range\u{26A1}", + "shifted.data.crosses.leap.year.to.non.leap.year": "Shifted data crosses a leap year so the graph might not align appropriately\u{26A1}", + "original.data.crosses.leap.year.to.non.leap.year": "Original data crosses a leap year so the graph might not align appropriately\u{26A1}" }, "es": { "3D": "3D", @@ -1513,7 +1538,20 @@ const LocaleTranslationData = { "week": "semana", "yes": "sí", "yesterday": "Ayer", - "you.cannot.create.a.cyclic.group": "No se puede crear un grupo cíclico" + "you.cannot.create.a.cyclic.group": "No se puede crear un grupo cíclico", + "compare.line": "Compare line\u{26A1}", + "shift.date.interval": "Shift Date Interval\u{26A1}", + "a few seconds": "a few seconds\u{26A1}", + "1.month": "1 month\u{26A1}", + "1.year": "1 year\u{26A1}", + "2.months": "2 months\u{26A1}", + "1.week": "1 week\u{26A1}", + "compare.line.days.enter": "Enter in days and then hit enter\u{26A1}", + "please.set.the.date.range": "Please choose date range\u{26A1}", + "select.shift.amount": "Select shift amount\u{26A1}", + "custom.date.range": "Custom date range\u{26A1}", + "shifted.data.crosses.leap.year.to.non.leap.year": "Shifted data crosses a leap year so the graph might not align appropriately\u{26A1}", + "original.data.crosses.leap.year.to.non.leap.year": "Original data crosses a leap year so the graph might not align appropriately\u{26A1}" } } diff --git a/src/client/app/types/redux/graph.ts b/src/client/app/types/redux/graph.ts index 601dd51b9..b388dbb4c 100644 --- a/src/client/app/types/redux/graph.ts +++ b/src/client/app/types/redux/graph.ts @@ -13,7 +13,8 @@ export enum ChartTypes { compare = 'compare', map = 'map', radar = 'radar', - threeD = '3D' + threeD = '3D', + compareLine = 'compare.line' } // Rates that can be graphed, only relevant to line graphs. @@ -55,6 +56,14 @@ export interface ThreeDState { readingInterval: ReadingInterval; } +export enum ShiftAmount { + week = 'week', + month = 'month', + year = 'year', + custom = 'custom', + none = 'none' +} + export interface GraphState { areaNormalization: boolean; selectedMeters: number[]; @@ -73,4 +82,6 @@ export interface GraphState { threeD: ThreeDState; queryTimeInterval: TimeInterval; hotlinked: boolean; + shiftAmount: ShiftAmount; + shiftTimeInterval: TimeInterval; }