From 80740669dd72070145442a07f4cf412f0679f1a9 Mon Sep 17 00:00:00 2001 From: jamesngn Date: Sat, 17 Aug 2024 13:08:16 +0700 Subject: [PATCH 01/21] Compare Line--color of original graph for custom is not working properly --- .../components/CompareLineChartComponent.tsx | 255 ++++++++++++++++++ .../CompareLineControlsComponent.tsx | 91 +++++++ .../app/components/DashboardComponent.tsx | 2 + .../app/components/MoreOptionsComponent.tsx | 9 +- .../app/components/UIOptionsComponent.tsx | 6 + .../redux/selectors/chartQuerySelectors.ts | 34 ++- .../app/redux/selectors/lineChartSelectors.ts | 1 + src/client/app/redux/slices/graphSlice.ts | 24 +- src/client/app/translations/data.ts | 38 ++- src/client/app/types/redux/graph.ts | 158 ++++++----- 10 files changed, 534 insertions(+), 84 deletions(-) create mode 100644 src/client/app/components/CompareLineChartComponent.tsx create mode 100644 src/client/app/components/CompareLineControlsComponent.tsx diff --git a/src/client/app/components/CompareLineChartComponent.tsx b/src/client/app/components/CompareLineChartComponent.tsx new file mode 100644 index 000000000..a9000a7d4 --- /dev/null +++ b/src/client/app/components/CompareLineChartComponent.tsx @@ -0,0 +1,255 @@ +/* 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, cloneDeep } from 'lodash'; +import { utc } from 'moment'; +import * as moment 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 } 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'; + + +/** + * @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 { args, shouldSkipQuery, argsDeps } = useAppSelector(selectCompareLineQueryArgs); + // getting the shift amount for the shifted data + const shiftAmount = useAppSelector(selectShiftAmount); + + // getting the time interval of current data + const timeInterval = graphState.queryTimeInterval; + // Hold the value of the time interval, and used to track the time interval of the shifted data + // when shift amount is updated + const [newTimeInterval, setNewTimeInterval] = React.useState(cloneDeep(timeInterval)); + + // Fetch 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!] }) + }) + }); + + // Tracking changes of the shiftAmount and timeInternval in order to update the shifted data + React.useEffect(() => { + const startDate = timeInterval.getStartTimestamp(); + const endDate = timeInterval.getEndTimestamp(); + if (startDate !== null || endDate !== null) { + const { shiftedStart, shiftedEnd } = shiftDateFunc(startDate, endDate, shiftAmount); + const newInterval = new TimeInterval(shiftedStart, shiftedEnd); + setNewTimeInterval(newInterval); + } + }, [shiftAmount, timeInterval]) + + + // Tracking the changes for custom shift date + React.useEffect(() => { + const startDate = timeInterval.getStartTimestamp(); + const endDate = timeInterval.getEndTimestamp(); + const startShiftDate = shiftInterval.getStartTimestamp(); + if (startDate !== null && endDate !== null) { + if (startShiftDate !== null) { + const originalRangeDays = endDate.diff(startDate, 'days'); + const endShiftDate = startShiftDate.clone().add(originalRangeDays, 'days') + const newShitedInterval = new TimeInterval(startShiftDate, endShiftDate); + setNewTimeInterval(newShitedInterval); + } + } + }, [shiftInterval]) + + // Getting the shifted data + const { data: dataNew, isFetching: isFetchingNew } = graphState.threeD.meterOrGroup === MeterOrGroup.meters ? + readingsApi.useLineQuery({ ...args, timeInterval: newTimeInterval.toString() }, + { + skip: shouldSkipQuery, + selectFromResult: ({ data, ...rest }) => ({ + ...rest, + data: selectPlotlyMeterData(data ?? stableEmptyLineReadings, + { ...argsDeps, compatibleEntities: [meterOrGroupID!] }) + }) + }) + : + readingsApi.useLineQuery({ ...args, timeInterval: newTimeInterval.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 (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' + })); + + 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 }) + } + /> + + + ); + + } +} + +/** + * shifting date function + * @param originalStart start date of current graph data + * @param endoriginalEndDate 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 + */ +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, 'days'); + } else if (originalStart.date() === 1 && originalEnd.date() === originalEnd.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 = originalStart.clone().subtract(1, 'years').startOf('month').add(1, 'days'); + } else if (originalEnd.isLeapYear() && originalEnd.month() === 1 && originalEnd.date() === 29) { + shiftedEnd = originalEnd.clone().subtract(1, 'years').endOf('month').subtract(1, 'days'); + } + break; + + default: + shiftedStart = originalStart.clone(); + shiftedEnd = originalEnd.clone(); + } + + return { shiftedStart, shiftedEnd }; +} diff --git a/src/client/app/components/CompareLineControlsComponent.tsx b/src/client/app/components/CompareLineControlsComponent.tsx new file mode 100644 index 000000000..c415842db --- /dev/null +++ b/src/client/app/components/CompareLineControlsComponent.tsx @@ -0,0 +1,91 @@ +import * as React from 'react'; +import { Input } from 'reactstrap'; +import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; +import { 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'; + +/** + * @returns compare line control page + */ +export default function CompareLineControlsComponent() { + const dispatch = useAppDispatch(); + const shiftAmount = useAppSelector(selectShiftAmount); + const locale = useAppSelector(selectSelectedLanguage); + const shiftInterval = useAppSelector(selectShiftTimeInterval); + const [shiftOption, setShiftOption] = React.useState(shiftAmount); + const [showDatePicker, setShowDatePicker] = React.useState(false); + const shiftAmountNotSelected = shiftAmount === ShiftAmount.none; + + const handleShiftOptionChange = (value: string) => { + if (value === 'custom') { + setShiftOption(ShiftAmount.custom); + dispatch(updateShiftAmount(ShiftAmount.custom)); + setShowDatePicker(true); + } else { + setShowDatePicker(false); + if (value === 'none') { + setShiftOption(ShiftAmount.none); + dispatch(updateShiftAmount(ShiftAmount.none)); + } else if (value === 'week') { + setShiftOption(ShiftAmount.week); + dispatch(updateShiftAmount(ShiftAmount.week)); + } else if (value === 'month') { + setShiftOption(ShiftAmount.month); + dispatch(updateShiftAmount(ShiftAmount.month)); + } else if (value === 'year') { + setShiftOption(ShiftAmount.year); + dispatch(updateShiftAmount(ShiftAmount.year)); + } + } + }; + + const handleShiftDateChange = (value: Value) => { + const shiftTimeInterval = dateRangeToTimeInterval(value); + dispatch(updateShiftTimeInterval(shiftTimeInterval)); + } + + return ( + <> +
+

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

+ handleShiftOptionChange(e.target.value)} + > + + + + + + + {showDatePicker && + } + +
+ + ); + +} + +const labelStyle: React.CSSProperties = { fontWeight: 'bold', margin: 0 }; 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 b0253fcc4..92735c502 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..2976e26c6 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 1114225e4..d91a9f916 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'; @@ -40,7 +40,9 @@ const defaultState: GraphState = { meterOrGroup: undefined, readingInterval: ReadingInterval.Hourly }, - hotlinked: false + hotlinked: false, + shiftAmount: ShiftAmount.none, + shiftTimeInterval: TimeInterval.unbounded() }; interface History { @@ -92,6 +94,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; @@ -392,7 +402,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 } }); @@ -411,7 +423,8 @@ export const { selectGraphAreaNormalization, selectSliderRangeInterval, selectDefaultGraphState, selectHistoryIsDirty, selectPlotlySliderMax, selectPlotlySliderMin, - selectMapBarWidthDays + selectMapBarWidthDays, selectShiftAmount, + selectShiftTimeInterval } = graphSlice.selectors; // actionCreators exports @@ -428,6 +441,7 @@ export const { toggleAreaNormalization, updateThreeDMeterOrGroup, changeCompareSortingOrder, updateThreeDMeterOrGroupID, updateThreeDReadingInterval, updateThreeDMeterOrGroupInfo, - updateSelectedMetersOrGroups, updateMapsBarDuration + updateSelectedMetersOrGroups, updateMapsBarDuration, + updateShiftAmount, updateShiftTimeInterval } = graphSlice.actions; diff --git a/src/client/app/translations/data.ts b/src/client/app/translations/data.ts index 740311d8c..4ac393ca8 100644 --- a/src/client/app/translations/data.ts +++ b/src/client/app/translations/data.ts @@ -507,7 +507,17 @@ 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" }, "fr": { "3D": "3D", @@ -1008,7 +1018,18 @@ 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}" }, "es": { "3D": "3D", @@ -1510,7 +1531,18 @@ 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}" } } diff --git a/src/client/app/types/redux/graph.ts b/src/client/app/types/redux/graph.ts index e649dd143..5bc6d34dd 100644 --- a/src/client/app/types/redux/graph.ts +++ b/src/client/app/types/redux/graph.ts @@ -2,76 +2,88 @@ * 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 { TimeInterval } from '../../../../common/TimeInterval'; -import { ComparePeriod, SortingOrder } from '../../utils/calculateCompare'; -import { AreaUnitType } from '../../utils/getAreaUnitConversion'; - -export enum ChartTypes { - line = 'line', - bar = 'bar', - compare = 'compare', - map = 'map', - radar = 'radar', - threeD = '3D' -} - -// Rates that can be graphed, only relevant to line graphs. -export const LineGraphRates = { - 'second': (1 / 3600), - 'minute': (1 / 60), - 'hour': 1, - 'day': 24 -}; - -// Use to determine readings per day on 3D Graphs -// 24 / ReadingInterval.Hourly(1) = 24 readings per day @ 1 hour intervals -// 24 / ReadingInterval.TwoHour(2) = 12 readings per day @ 2 hour intervals -// and so on. -export enum ReadingInterval { - Hourly = 1, - TwoHour = 2, - ThreeHour = 3, - FourHour = 4, - SixHour = 6, - EightHour = 8, - TwelveHour = 12, - Incompatible = -999 -} - - -export interface LineGraphRate { - label: string, - rate: number -} - -export type MeterOrGroupID = number; -export enum MeterOrGroup { meters = 'meters', groups = 'groups' } -export type MeterOrGroupPill = { meterOrGroupID: number, isDisabled: boolean, meterOrGroup: MeterOrGroup } - -export interface ThreeDState { - meterOrGroupID: MeterOrGroupID | undefined; - meterOrGroup: MeterOrGroup | undefined; - readingInterval: ReadingInterval; -} - -export interface GraphState { - areaNormalization: boolean; - selectedMeters: number[]; - selectedGroups: number[]; - selectedUnit: number; - selectedAreaUnit: AreaUnitType; - rangeSliderInterval: TimeInterval; - barDuration: moment.Duration; - mapsBarDuration: moment.Duration; - comparePeriod: ComparePeriod; - compareTimeInterval: TimeInterval; - compareSortingOrder: SortingOrder; - chartToRender: ChartTypes; - barStacking: boolean; - lineGraphRate: LineGraphRate; - showMinMax: boolean; - threeD: ThreeDState; - queryTimeInterval: TimeInterval; - hotlinked: boolean; -} + import * as moment from 'moment'; + import { TimeInterval } from '../../../../common/TimeInterval'; + import { ComparePeriod, SortingOrder } from '../../utils/calculateCompare'; + import { AreaUnitType } from '../../utils/getAreaUnitConversion'; + + export enum ChartTypes { + line = 'line', + bar = 'bar', + compare = 'compare', + map = 'map', + radar = 'radar', + threeD = '3D', + compareLine = 'compare.line' + } + + // Rates that can be graphed, only relevant to line graphs. + export const LineGraphRates = { + 'second': (1 / 3600), + 'minute': (1 / 60), + 'hour': 1, + 'day': 24 + }; + + // Use to determine readings per day on 3D Graphs + // 24 / ReadingInterval.Hourly(1) = 24 readings per day @ 1 hour intervals + // 24 / ReadingInterval.TwoHour(2) = 12 readings per day @ 2 hour intervals + // and so on. + export enum ReadingInterval { + Hourly = 1, + TwoHour = 2, + ThreeHour = 3, + FourHour = 4, + SixHour = 6, + EightHour = 8, + TwelveHour = 12, + Incompatible = -999 + } + + + export interface LineGraphRate { + label: string, + rate: number + } + + export type MeterOrGroupID = number; + export enum MeterOrGroup { meters = 'meters', groups = 'groups' } + export type MeterOrGroupPill = { meterOrGroupID: number, isDisabled: boolean, meterOrGroup: MeterOrGroup } + + export interface ThreeDState { + meterOrGroupID: MeterOrGroupID | undefined; + meterOrGroup: MeterOrGroup | undefined; + readingInterval: ReadingInterval; + } + + export enum ShiftAmount { + week = 'week', + month = 'month', + year = 'year', + custom = 'custom', + none = 'none' + } + + export interface GraphState { + areaNormalization: boolean; + selectedMeters: number[]; + selectedGroups: number[]; + selectedUnit: number; + selectedAreaUnit: AreaUnitType; + rangeSliderInterval: TimeInterval; + barDuration: moment.Duration; + mapsBarDuration: moment.Duration; + comparePeriod: ComparePeriod; + compareTimeInterval: TimeInterval; + compareSortingOrder: SortingOrder; + chartToRender: ChartTypes; + barStacking: boolean; + lineGraphRate: LineGraphRate; + showMinMax: boolean; + threeD: ThreeDState; + queryTimeInterval: TimeInterval; + hotlinked: boolean; + shiftAmount: ShiftAmount; + shiftTimeInterval: TimeInterval; + } + \ No newline at end of file From 0bc7873de4bca007b38029710793b40722988a96 Mon Sep 17 00:00:00 2001 From: jamesngn Date: Thu, 5 Sep 2024 20:38:46 -0400 Subject: [PATCH 02/21] Fix custom shift date interval causing wrong data line --- .../components/CompareLineChartComponent.tsx | 78 ++-------------- .../CompareLineControlsComponent.tsx | 88 ++++++++++++++++++- src/client/app/translations/data.ts | 6 +- 3 files changed, 94 insertions(+), 78 deletions(-) diff --git a/src/client/app/components/CompareLineChartComponent.tsx b/src/client/app/components/CompareLineChartComponent.tsx index a9000a7d4..c8e986942 100644 --- a/src/client/app/components/CompareLineChartComponent.tsx +++ b/src/client/app/components/CompareLineChartComponent.tsx @@ -4,7 +4,7 @@ import { debounce, cloneDeep } from 'lodash'; import { utc } from 'moment'; -import * as moment from 'moment'; +// import * as moment from 'moment'; import * as React from 'react'; import Plot from 'react-plotly.js'; import { TimeInterval } from '../../../common/TimeInterval'; @@ -35,9 +35,8 @@ export default function CompareLineChartComponent() { const unitLabel = useAppSelector(selectLineUnitLabel); const locale = useAppSelector(selectSelectedLanguage); const shiftInterval = useAppSelector(selectShiftTimeInterval); - const { args, shouldSkipQuery, argsDeps } = useAppSelector(selectCompareLineQueryArgs); - // getting the shift amount for the shifted data const shiftAmount = useAppSelector(selectShiftAmount); + const { args, shouldSkipQuery, argsDeps } = useAppSelector(selectCompareLineQueryArgs); // getting the time interval of current data const timeInterval = graphState.queryTimeInterval; @@ -45,7 +44,7 @@ export default function CompareLineChartComponent() { // when shift amount is updated const [newTimeInterval, setNewTimeInterval] = React.useState(cloneDeep(timeInterval)); - // Fetch data, and derive plotly points + // Fetch original data, and derive plotly points const { data, isFetching } = graphState.threeD.meterOrGroup === MeterOrGroup.meters ? readingsApi.useLineQuery(args, { @@ -67,19 +66,7 @@ export default function CompareLineChartComponent() { }) }); - // Tracking changes of the shiftAmount and timeInternval in order to update the shifted data - React.useEffect(() => { - const startDate = timeInterval.getStartTimestamp(); - const endDate = timeInterval.getEndTimestamp(); - if (startDate !== null || endDate !== null) { - const { shiftedStart, shiftedEnd } = shiftDateFunc(startDate, endDate, shiftAmount); - const newInterval = new TimeInterval(shiftedStart, shiftedEnd); - setNewTimeInterval(newInterval); - } - }, [shiftAmount, timeInterval]) - - - // Tracking the changes for custom shift date + // Update shifted time interval for shifted data React.useEffect(() => { const startDate = timeInterval.getStartTimestamp(); const endDate = timeInterval.getEndTimestamp(); @@ -92,7 +79,7 @@ export default function CompareLineChartComponent() { setNewTimeInterval(newShitedInterval); } } - }, [shiftInterval]) + }, [shiftInterval, timeInterval, shiftAmount]) // Getting the shifted data const { data: dataNew, isFetching: isFetchingNew } = graphState.threeD.meterOrGroup === MeterOrGroup.meters ? @@ -162,6 +149,7 @@ export default function CompareLineChartComponent() { tickfont: { color: '#1AA5F0' }, overlaying: 'x', side: 'top', + } }} config={{ @@ -199,57 +187,3 @@ export default function CompareLineChartComponent() { } } - -/** - * shifting date function - * @param originalStart start date of current graph data - * @param endoriginalEndDate 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 - */ -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, 'days'); - } else if (originalStart.date() === 1 && originalEnd.date() === originalEnd.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 = originalStart.clone().subtract(1, 'years').startOf('month').add(1, 'days'); - } else if (originalEnd.isLeapYear() && originalEnd.month() === 1 && originalEnd.date() === 29) { - shiftedEnd = originalEnd.clone().subtract(1, 'years').endOf('month').subtract(1, 'days'); - } - break; - - default: - shiftedStart = originalStart.clone(); - shiftedEnd = originalEnd.clone(); - } - - return { shiftedStart, shiftedEnd }; -} diff --git a/src/client/app/components/CompareLineControlsComponent.tsx b/src/client/app/components/CompareLineControlsComponent.tsx index c415842db..3e8049711 100644 --- a/src/client/app/components/CompareLineControlsComponent.tsx +++ b/src/client/app/components/CompareLineControlsComponent.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { Input } from 'reactstrap'; import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; -import { selectShiftAmount, selectShiftTimeInterval, updateShiftAmount, updateShiftTimeInterval } from '../redux/slices/graphSlice'; +import { 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'; @@ -9,6 +9,8 @@ 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'; /** * @returns compare line control page @@ -16,12 +18,17 @@ import { Value } from '@wojtekmaj/react-daterange-picker/dist/cjs/shared/types'; export default function CompareLineControlsComponent() { const dispatch = useAppDispatch(); const shiftAmount = useAppSelector(selectShiftAmount); + const timeInterval = useAppSelector(selectQueryTimeInterval); const locale = useAppSelector(selectSelectedLanguage); const shiftInterval = useAppSelector(selectShiftTimeInterval); + + // 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); const shiftAmountNotSelected = shiftAmount === ShiftAmount.none; + // Update shifting option when shift data interval is chosen const handleShiftOptionChange = (value: string) => { if (value === 'custom') { setShiftOption(ShiftAmount.custom); @@ -32,22 +39,37 @@ export default function CompareLineControlsComponent() { if (value === 'none') { setShiftOption(ShiftAmount.none); dispatch(updateShiftAmount(ShiftAmount.none)); + updateShiftInterval(ShiftAmount.none); } else if (value === 'week') { setShiftOption(ShiftAmount.week); dispatch(updateShiftAmount(ShiftAmount.week)); + updateShiftInterval(ShiftAmount.week); } else if (value === 'month') { setShiftOption(ShiftAmount.month); dispatch(updateShiftAmount(ShiftAmount.month)); + updateShiftInterval(ShiftAmount.month); } else if (value === 'year') { setShiftOption(ShiftAmount.year); dispatch(updateShiftAmount(ShiftAmount.year)); + updateShiftInterval(ShiftAmount.year); } } }; + // 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) => { - const shiftTimeInterval = dateRangeToTimeInterval(value); - dispatch(updateShiftTimeInterval(shiftTimeInterval)); + dispatch(updateShiftTimeInterval(dateRangeToTimeInterval(value))); } return ( @@ -89,3 +111,63 @@ export default function CompareLineControlsComponent() { } 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 endoriginalEndDate 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 + */ +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 }; +} diff --git a/src/client/app/translations/data.ts b/src/client/app/translations/data.ts index 4ac393ca8..8170d2844 100644 --- a/src/client/app/translations/data.ts +++ b/src/client/app/translations/data.ts @@ -48,7 +48,7 @@ const LocaleTranslationData = { "clipboard.copied": "Copied To Clipboard", "clipboard.not.copied": "Failed to Copy To Clipboard", "close": "Close", - "compare": "Compare", + "compare": "Compare bar", "compare.raw": "Cannot create comparison graph on raw units such as temperature", "confirm.action": "Confirm Action", "contact.us": "Contact us", @@ -561,7 +561,7 @@ const LocaleTranslationData = { "clipboard.copied": "Copied To Clipboard\u{26A1}", "clipboard.not.copied": "Failed to Copy To Clipboard\u{26A1}", "close": "Close\u{26A1}", - "compare": "Comparer", + "compare": "Compare bar\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", @@ -1073,7 +1073,7 @@ const LocaleTranslationData = { "clipboard.copied": "Copiado al portapapeles", "clipboard.not.copied": "Error al copiar al portapapeles", "close": "Cerrar", - "compare": "Comparar", + "compare": "Compare bar\u{26A1}", "compare.raw": "No se puede crear un gráfico de comparación con unidades crudas como temperatura", "confirm.action": "Confirmar acción", "contact.us": "Contáctenos", From 180fe8346dcb6a346ce96cd9c10490102c85f33f Mon Sep 17 00:00:00 2001 From: nmqng Date: Sat, 21 Sep 2024 18:35:25 -0400 Subject: [PATCH 03/21] fix compare line page custom date range stay up-to-date with shift date interval picker --- .../components/CompareLineChartComponent.tsx | 77 +++++++++++++------ .../CompareLineControlsComponent.tsx | 55 ++++++------- 2 files changed, 81 insertions(+), 51 deletions(-) diff --git a/src/client/app/components/CompareLineChartComponent.tsx b/src/client/app/components/CompareLineChartComponent.tsx index c8e986942..ee754b08e 100644 --- a/src/client/app/components/CompareLineChartComponent.tsx +++ b/src/client/app/components/CompareLineChartComponent.tsx @@ -2,9 +2,8 @@ * 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, cloneDeep } from 'lodash'; +import { debounce } from 'lodash'; import { utc } from 'moment'; -// import * as moment from 'moment'; import * as React from 'react'; import Plot from 'react-plotly.js'; import { TimeInterval } from '../../../common/TimeInterval'; @@ -17,14 +16,13 @@ import { selectSelectedLanguage } from '../redux/slices/appStateSlice'; import Locales from '../types/locales'; import translate from '../utils/translate'; import SpinnerComponent from './SpinnerComponent'; -import { selectGraphState, selectShiftAmount, selectShiftTimeInterval } from '../redux/slices/graphSlice'; +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 */ @@ -40,9 +38,10 @@ export default function CompareLineChartComponent() { // getting the time interval of current data const timeInterval = graphState.queryTimeInterval; - // Hold the value of the time interval, and used to track the time interval of the shifted data - // when shift amount is updated - const [newTimeInterval, setNewTimeInterval] = React.useState(cloneDeep(timeInterval)); + + // 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 ? @@ -66,24 +65,47 @@ export default function CompareLineChartComponent() { }) }); - // Update shifted time interval for shifted data + // 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)); + }, [dispatch]); + + // Update shifted interval based on current interval and shift amount React.useEffect(() => { const startDate = timeInterval.getStartTimestamp(); const endDate = timeInterval.getEndTimestamp(); - const startShiftDate = shiftInterval.getStartTimestamp(); - if (startDate !== null && endDate !== null) { - if (startShiftDate !== null) { - const originalRangeDays = endDate.diff(startDate, 'days'); - const endShiftDate = startShiftDate.clone().add(originalRangeDays, 'days') - const newShitedInterval = new TimeInterval(startShiftDate, endShiftDate); - setNewTimeInterval(newShitedInterval); + + if (startDate && endDate) { + setTimeIntervalStr([startDate.toISOString(), endDate.toISOString()]); + + if (shiftAmount !== ShiftAmount.none && shiftAmount !== ShiftAmount.custom) { + updateShiftedInterval(startDate, endDate, shiftAmount); } } - }, [shiftInterval, timeInterval, 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: newTimeInterval.toString() }, + readingsApi.useLineQuery({ ...args, timeInterval: shiftInterval.toString() }, { skip: shouldSkipQuery, selectFromResult: ({ data, ...rest }) => ({ @@ -93,7 +115,7 @@ export default function CompareLineChartComponent() { }) }) : - readingsApi.useLineQuery({ ...args, timeInterval: newTimeInterval.toString() }, + readingsApi.useLineQuery({ ...args, timeInterval: shiftInterval.toString() }, { skip: shouldSkipQuery, selectFromResult: ({ data, ...rest }) => ({ @@ -103,7 +125,6 @@ export default function CompareLineChartComponent() { }) }); - if (isFetching || isFetchingNew) { return ; } @@ -114,8 +135,8 @@ export default function CompareLineChartComponent() { // 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 (data.length === 0 || dataNew.length === 0) { - return <>

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

+ 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()) { @@ -124,9 +145,12 @@ export default function CompareLineChartComponent() { // 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, + name: 'Shifted ' + item.name, line: { ...item.line, color: '#1AA5F0' }, - xaxis: 'x2' + xaxis: 'x2', + text: Array.isArray(item.text) + ? item.text.map(text => text.replace('
', '
Shifted ')) + : item.text?.replace('
', '
Shifted ') })); return ( @@ -143,13 +167,16 @@ export default function CompareLineChartComponent() { yaxis: { title: unitLabel, gridcolor: '#ddd', fixedrange: true }, xaxis: { rangeslider: { visible: true }, + // Set range for x-axis based on timeIntervalStr so that current data and shifted data is aligned + range: timeIntervalStr.length === 2 ? timeIntervalStr : undefined }, xaxis2: { titlefont: { color: '#1AA5F0' }, tickfont: { color: '#1AA5F0' }, overlaying: 'x', side: 'top', - + // Set range for x-axis2 based on shiftIntervalStr so that current data and shifted data is aligned + range: shiftIntervalStr.length === 2 ? shiftIntervalStr : undefined } }} config={{ diff --git a/src/client/app/components/CompareLineControlsComponent.tsx b/src/client/app/components/CompareLineControlsComponent.tsx index 3e8049711..df12c1c2a 100644 --- a/src/client/app/components/CompareLineControlsComponent.tsx +++ b/src/client/app/components/CompareLineControlsComponent.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { Input } from 'reactstrap'; import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; +// eslint-disable-next-line max-len import { selectQueryTimeInterval, selectShiftAmount, selectShiftTimeInterval, updateShiftAmount, updateShiftTimeInterval } from '../redux/slices/graphSlice'; import translate from '../utils/translate'; import { FormattedMessage } from 'react-intl'; @@ -26,9 +27,22 @@ export default function CompareLineControlsComponent() { 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); - const shiftAmountNotSelected = shiftAmount === ShiftAmount.none; + // Hold value to store the custom date range for the shift interval + const [customDateRange, setCustomDateRange] = React.useState(timeIntervalToDateRange(shiftInterval)); - // Update shifting option when shift data interval is chosen + // 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]); + + // Handle changes in shift option (week, month, year, or custom) const handleShiftOptionChange = (value: string) => { if (value === 'custom') { setShiftOption(ShiftAmount.custom); @@ -36,23 +50,10 @@ export default function CompareLineControlsComponent() { setShowDatePicker(true); } else { setShowDatePicker(false); - if (value === 'none') { - setShiftOption(ShiftAmount.none); - dispatch(updateShiftAmount(ShiftAmount.none)); - updateShiftInterval(ShiftAmount.none); - } else if (value === 'week') { - setShiftOption(ShiftAmount.week); - dispatch(updateShiftAmount(ShiftAmount.week)); - updateShiftInterval(ShiftAmount.week); - } else if (value === 'month') { - setShiftOption(ShiftAmount.month); - dispatch(updateShiftAmount(ShiftAmount.month)); - updateShiftInterval(ShiftAmount.month); - } else if (value === 'year') { - setShiftOption(ShiftAmount.year); - dispatch(updateShiftAmount(ShiftAmount.year)); - updateShiftInterval(ShiftAmount.year); - } + const newShiftOption = value as ShiftAmount; + setShiftOption(newShiftOption); + dispatch(updateShiftAmount(newShiftOption)); + updateShiftInterval(newShiftOption); } }; @@ -65,12 +66,13 @@ export default function CompareLineControlsComponent() { 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 ( <> @@ -84,7 +86,7 @@ export default function CompareLineControlsComponent() { name='shiftDateInput' type='select' value={shiftOption} - invalid={shiftAmountNotSelected} + invalid={shiftAmount === ShiftAmount.none} onChange={e => handleShiftOptionChange(e.target.value)} > @@ -93,15 +95,16 @@ export default function CompareLineControlsComponent() { + {/* Show date picker when custom date range is selected */} {showDatePicker && }
@@ -115,11 +118,11 @@ 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 endoriginalEndDate end 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 */ -function shiftDateFunc(originalStart: moment.Moment, originalEnd: moment.Moment, shiftType: ShiftAmount) { +export function shiftDateFunc(originalStart: moment.Moment, originalEnd: moment.Moment, shiftType: ShiftAmount) { let shiftedStart: moment.Moment; let shiftedEnd: moment.Moment; From 3fbb673df42f856b786d2abcab46027181cc79bf Mon Sep 17 00:00:00 2001 From: nmqng Date: Mon, 23 Sep 2024 16:42:08 -0400 Subject: [PATCH 04/21] add notification when origin/shifted date range crosses a leap year --- .../components/CompareLineChartComponent.tsx | 2 +- .../CompareLineControlsComponent.tsx | 35 ++++++++++++++++++- src/client/app/translations/data.ts | 12 +++++-- 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/src/client/app/components/CompareLineChartComponent.tsx b/src/client/app/components/CompareLineChartComponent.tsx index ee754b08e..24603f964 100644 --- a/src/client/app/components/CompareLineChartComponent.tsx +++ b/src/client/app/components/CompareLineChartComponent.tsx @@ -70,7 +70,7 @@ export default function CompareLineChartComponent() { const { shiftedStart, shiftedEnd } = shiftDateFunc(start, end, shift); const newShiftedInterval = new TimeInterval(shiftedStart, shiftedEnd); dispatch(updateShiftTimeInterval(newShiftedInterval)); - }, [dispatch]); + }, []); // Update shifted interval based on current interval and shift amount React.useEffect(() => { diff --git a/src/client/app/components/CompareLineControlsComponent.tsx b/src/client/app/components/CompareLineControlsComponent.tsx index df12c1c2a..af48e85f7 100644 --- a/src/client/app/components/CompareLineControlsComponent.tsx +++ b/src/client/app/components/CompareLineControlsComponent.tsx @@ -12,6 +12,7 @@ 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 { showErrorNotification } from '../utils/notifications'; /** * @returns compare line control page @@ -53,6 +54,38 @@ export default function CompareLineControlsComponent() { 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) { + const { shiftedStart, shiftedEnd } = shiftDateFunc(startDate, endDate, newShiftOption); + // Check if original or shifted date range is a leap year + 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) { + showErrorNotification('shifted.data.crosses.leap.year.to.non.leap.year'); + } else if (shiftedCrossFeb29 && !originalIsLeapYear) { + showErrorNotification('shifted.data.may.not.align.appropriate.due.to.leap.year'); + } + } + // Update shift interval when shift option changes updateShiftInterval(newShiftOption); } }; @@ -173,4 +206,4 @@ export function shiftDateFunc(originalStart: moment.Moment, originalEnd: moment. } return { shiftedStart, shiftedEnd }; -} +} \ No newline at end of file diff --git a/src/client/app/translations/data.ts b/src/client/app/translations/data.ts index 8170d2844..820750036 100644 --- a/src/client/app/translations/data.ts +++ b/src/client/app/translations/data.ts @@ -517,7 +517,9 @@ const LocaleTranslationData = { "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" + "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 appropriate", + "origin.data.crosses.leap.year.to.non.leap.year": "Original data crosses a leap year so the graph might not align appropriate" }, "fr": { "3D": "3D", @@ -1029,7 +1031,9 @@ const LocaleTranslationData = { "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}" + "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 appropriate\u{26A1}", + "origin.data.crosses.leap.year.to.non.leap.year": "Original data crosses a leap year so the graph might not align appropriate\u{26A1}" }, "es": { "3D": "3D", @@ -1542,7 +1546,9 @@ const LocaleTranslationData = { "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}" + "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 appropriate\u{26A1}", + "origin.data.crosses.leap.year.to.non.leap.year": "Original data crosses a leap year so the graph might not align appropriate\u{26A1}" } } From eeef793ba60b9cb98149ac49a86e7494304a6108 Mon Sep 17 00:00:00 2001 From: nmqng Date: Tue, 24 Sep 2024 20:49:10 -0400 Subject: [PATCH 05/21] change error noti to warn noti, translation key and wording --- .../app/components/CompareLineControlsComponent.tsx | 6 +++--- src/client/app/translations/data.ts | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/client/app/components/CompareLineControlsComponent.tsx b/src/client/app/components/CompareLineControlsComponent.tsx index af48e85f7..513dd9cfc 100644 --- a/src/client/app/components/CompareLineControlsComponent.tsx +++ b/src/client/app/components/CompareLineControlsComponent.tsx @@ -12,7 +12,7 @@ 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 { showErrorNotification } from '../utils/notifications'; +import { showWarnNotification } from '../utils/notifications'; /** * @returns compare line control page @@ -80,9 +80,9 @@ export default function CompareLineControlsComponent() { ); if (originalCrossFeb29 && !shiftedIsLeapYear) { - showErrorNotification('shifted.data.crosses.leap.year.to.non.leap.year'); + showWarnNotification(translate('original.data.crosses.leap.year.to.non.leap.year')); } else if (shiftedCrossFeb29 && !originalIsLeapYear) { - showErrorNotification('shifted.data.may.not.align.appropriate.due.to.leap.year'); + showWarnNotification(translate('shifted.data.crosses.leap.year.to.non.leap.year')); } } // Update shift interval when shift option changes diff --git a/src/client/app/translations/data.ts b/src/client/app/translations/data.ts index 820750036..12d1f5ec7 100644 --- a/src/client/app/translations/data.ts +++ b/src/client/app/translations/data.ts @@ -518,8 +518,8 @@ const LocaleTranslationData = { "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 appropriate", - "origin.data.crosses.leap.year.to.non.leap.year": "Original data crosses a leap year so the graph might not align appropriate" + "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", @@ -1032,8 +1032,8 @@ const LocaleTranslationData = { "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 appropriate\u{26A1}", - "origin.data.crosses.leap.year.to.non.leap.year": "Original data crosses a leap year so the graph might not align appropriate\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", @@ -1547,8 +1547,8 @@ const LocaleTranslationData = { "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 appropriate\u{26A1}", - "origin.data.crosses.leap.year.to.non.leap.year": "Original data crosses a leap year so the graph might not align appropriate\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}" } } From 793df8edc8dc0fb8e72826af24ac1c8719152d95 Mon Sep 17 00:00:00 2001 From: nmqng Date: Tue, 1 Oct 2024 22:43:59 -0400 Subject: [PATCH 06/21] add warning msg when new meter/group/interval selected and rewrite checking logic into a function --- .../CompareLineControlsComponent.tsx | 72 ++++++++++++------- 1 file changed, 47 insertions(+), 25 deletions(-) diff --git a/src/client/app/components/CompareLineControlsComponent.tsx b/src/client/app/components/CompareLineControlsComponent.tsx index 513dd9cfc..e3d62b303 100644 --- a/src/client/app/components/CompareLineControlsComponent.tsx +++ b/src/client/app/components/CompareLineControlsComponent.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { Input } from 'reactstrap'; import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; // eslint-disable-next-line max-len -import { selectQueryTimeInterval, selectShiftAmount, selectShiftTimeInterval, updateShiftAmount, updateShiftTimeInterval } from '../redux/slices/graphSlice'; +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'; @@ -23,6 +23,7 @@ export default function CompareLineControlsComponent() { 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); @@ -43,6 +44,16 @@ export default function CompareLineControlsComponent() { 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') { @@ -60,30 +71,8 @@ export default function CompareLineControlsComponent() { const endDate = timeInterval.getEndTimestamp(); if (startDate && endDate) { - const { shiftedStart, shiftedEnd } = shiftDateFunc(startDate, endDate, newShiftOption); - // Check if original or shifted date range is a leap year - 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')); - } + // 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); @@ -206,4 +195,37 @@ export function shiftDateFunc(originalStart: moment.Moment, originalEnd: moment. } 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 From 750bad12871e6008748c2287ba8cf4a9d2c6af32 Mon Sep 17 00:00:00 2001 From: nmqng Date: Thu, 3 Oct 2024 00:36:59 -0400 Subject: [PATCH 07/21] fix syntax, code style based on run check --- .../redux/selectors/chartQuerySelectors.ts | 22 +-- .../app/redux/selectors/lineChartSelectors.ts | 2 +- src/client/app/types/redux/graph.ts | 159 +++++++++--------- 3 files changed, 91 insertions(+), 92 deletions(-) diff --git a/src/client/app/redux/selectors/chartQuerySelectors.ts b/src/client/app/redux/selectors/chartQuerySelectors.ts index 92735c502..c4d863995 100644 --- a/src/client/app/redux/selectors/chartQuerySelectors.ts +++ b/src/client/app/redux/selectors/chartQuerySelectors.ts @@ -92,17 +92,17 @@ export const selectCompareLineQueryArgs = createSelector( 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! + } : { - 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 }; @@ -220,6 +220,6 @@ export const selectAllChartQueryArgs = createSelector( compare, map, threeD, - compareLine, + compareLine }) ); diff --git a/src/client/app/redux/selectors/lineChartSelectors.ts b/src/client/app/redux/selectors/lineChartSelectors.ts index 2976e26c6..b69ec7b28 100644 --- a/src/client/app/redux/selectors/lineChartSelectors.ts +++ b/src/client/app/redux/selectors/lineChartSelectors.ts @@ -45,7 +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/types/redux/graph.ts b/src/client/app/types/redux/graph.ts index 5bc6d34dd..5cfb34610 100644 --- a/src/client/app/types/redux/graph.ts +++ b/src/client/app/types/redux/graph.ts @@ -2,88 +2,87 @@ * 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 { TimeInterval } from '../../../../common/TimeInterval'; - import { ComparePeriod, SortingOrder } from '../../utils/calculateCompare'; - import { AreaUnitType } from '../../utils/getAreaUnitConversion'; - - export enum ChartTypes { - line = 'line', - bar = 'bar', - compare = 'compare', - map = 'map', - radar = 'radar', - threeD = '3D', - compareLine = 'compare.line' - } - - // Rates that can be graphed, only relevant to line graphs. - export const LineGraphRates = { - 'second': (1 / 3600), - 'minute': (1 / 60), - 'hour': 1, - 'day': 24 - }; - - // Use to determine readings per day on 3D Graphs - // 24 / ReadingInterval.Hourly(1) = 24 readings per day @ 1 hour intervals - // 24 / ReadingInterval.TwoHour(2) = 12 readings per day @ 2 hour intervals - // and so on. - export enum ReadingInterval { - Hourly = 1, - TwoHour = 2, - ThreeHour = 3, - FourHour = 4, - SixHour = 6, - EightHour = 8, - TwelveHour = 12, - Incompatible = -999 - } - - - export interface LineGraphRate { - label: string, - rate: number - } - - export type MeterOrGroupID = number; - export enum MeterOrGroup { meters = 'meters', groups = 'groups' } - export type MeterOrGroupPill = { meterOrGroupID: number, isDisabled: boolean, meterOrGroup: MeterOrGroup } - - export interface ThreeDState { - meterOrGroupID: MeterOrGroupID | undefined; - meterOrGroup: MeterOrGroup | undefined; - readingInterval: ReadingInterval; - } - - export enum ShiftAmount { +import * as moment from 'moment'; +import { TimeInterval } from '../../../../common/TimeInterval'; +import { ComparePeriod, SortingOrder } from '../../utils/calculateCompare'; +import { AreaUnitType } from '../../utils/getAreaUnitConversion'; + +export enum ChartTypes { + line = 'line', + bar = 'bar', + compare = 'compare', + map = 'map', + radar = 'radar', + threeD = '3D', + compareLine = 'compare.line' +} + +// Rates that can be graphed, only relevant to line graphs. +export const LineGraphRates = { + 'second': (1 / 3600), + 'minute': (1 / 60), + 'hour': 1, + 'day': 24 +}; + +// Use to determine readings per day on 3D Graphs +// 24 / ReadingInterval.Hourly(1) = 24 readings per day @ 1 hour intervals +// 24 / ReadingInterval.TwoHour(2) = 12 readings per day @ 2 hour intervals +// and so on. +export enum ReadingInterval { + Hourly = 1, + TwoHour = 2, + ThreeHour = 3, + FourHour = 4, + SixHour = 6, + EightHour = 8, + TwelveHour = 12, + Incompatible = -999 +} + + +export interface LineGraphRate { + label: string, + rate: number +} + +export type MeterOrGroupID = number; +export enum MeterOrGroup { meters = 'meters', groups = 'groups' } +export type MeterOrGroupPill = { meterOrGroupID: number, isDisabled: boolean, meterOrGroup: MeterOrGroup } + +export interface ThreeDState { + meterOrGroupID: MeterOrGroupID | undefined; + meterOrGroup: MeterOrGroup | undefined; + readingInterval: ReadingInterval; +} + +export enum ShiftAmount { week = 'week', month = 'month', year = 'year', custom = 'custom', none = 'none' - } - - export interface GraphState { - areaNormalization: boolean; - selectedMeters: number[]; - selectedGroups: number[]; - selectedUnit: number; - selectedAreaUnit: AreaUnitType; - rangeSliderInterval: TimeInterval; - barDuration: moment.Duration; - mapsBarDuration: moment.Duration; - comparePeriod: ComparePeriod; - compareTimeInterval: TimeInterval; - compareSortingOrder: SortingOrder; - chartToRender: ChartTypes; - barStacking: boolean; - lineGraphRate: LineGraphRate; - showMinMax: boolean; - threeD: ThreeDState; - queryTimeInterval: TimeInterval; - hotlinked: boolean; - shiftAmount: ShiftAmount; - shiftTimeInterval: TimeInterval; - } - \ No newline at end of file +} + +export interface GraphState { + areaNormalization: boolean; + selectedMeters: number[]; + selectedGroups: number[]; + selectedUnit: number; + selectedAreaUnit: AreaUnitType; + rangeSliderInterval: TimeInterval; + barDuration: moment.Duration; + mapsBarDuration: moment.Duration; + comparePeriod: ComparePeriod; + compareTimeInterval: TimeInterval; + compareSortingOrder: SortingOrder; + chartToRender: ChartTypes; + barStacking: boolean; + lineGraphRate: LineGraphRate; + showMinMax: boolean; + threeD: ThreeDState; + queryTimeInterval: TimeInterval; + hotlinked: boolean; + shiftAmount: ShiftAmount; + shiftTimeInterval: TimeInterval; +} From f6a94a31f95edaa920acfabe8340ef033869c948 Mon Sep 17 00:00:00 2001 From: nmqng Date: Thu, 3 Oct 2024 13:37:47 -0400 Subject: [PATCH 08/21] add license header for CompareLineControlComponent.tsx --- src/client/app/components/CompareLineControlsComponent.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/client/app/components/CompareLineControlsComponent.tsx b/src/client/app/components/CompareLineControlsComponent.tsx index e3d62b303..53c797a28 100644 --- a/src/client/app/components/CompareLineControlsComponent.tsx +++ b/src/client/app/components/CompareLineControlsComponent.tsx @@ -1,3 +1,7 @@ +/* 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'; From 865a0e37fb49b7657163ea8acd76345311705c0e Mon Sep 17 00:00:00 2001 From: nmqng Date: Mon, 14 Oct 2024 17:58:21 -0400 Subject: [PATCH 09/21] fix merge of development issue --- src/client/app/redux/slices/graphSlice.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/client/app/redux/slices/graphSlice.ts b/src/client/app/redux/slices/graphSlice.ts index 69d62f3d3..0ccbc0c05 100644 --- a/src/client/app/redux/slices/graphSlice.ts +++ b/src/client/app/redux/slices/graphSlice.ts @@ -418,8 +418,7 @@ export const { selectGraphAreaNormalization, selectSliderRangeInterval, selectDefaultGraphState, selectHistoryIsDirty, selectPlotlySliderMax, selectPlotlySliderMin, - selectMapBarWidthDays, selectShiftAmount, - selectShiftTimeInterval + selectShiftAmount, selectShiftTimeInterval } = graphSlice.selectors; // actionCreators exports @@ -436,7 +435,7 @@ export const { toggleAreaNormalization, updateThreeDMeterOrGroup, changeCompareSortingOrder, updateThreeDMeterOrGroupID, updateThreeDReadingInterval, updateThreeDMeterOrGroupInfo, - updateSelectedMetersOrGroups, updateMapsBarDuration, - updateShiftAmount, updateShiftTimeInterval + updateSelectedMetersOrGroups, updateShiftAmount, + updateShiftTimeInterval } = graphSlice.actions; From 58af2cfe167b25d85696e5ea3bcebd1d87002ca6 Mon Sep 17 00:00:00 2001 From: nmqng Date: Fri, 22 Nov 2024 16:33:09 -0500 Subject: [PATCH 10/21] fix some comments --- .../app/components/CompareLineControlsComponent.tsx | 5 +++-- src/client/app/components/UIOptionsComponent.tsx | 1 - src/client/app/translations/data.ts | 8 +++----- src/client/app/types/redux/graph.ts | 2 +- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/client/app/components/CompareLineControlsComponent.tsx b/src/client/app/components/CompareLineControlsComponent.tsx index 53c797a28..210808171 100644 --- a/src/client/app/components/CompareLineControlsComponent.tsx +++ b/src/client/app/components/CompareLineControlsComponent.tsx @@ -5,8 +5,9 @@ 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 { + 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'; diff --git a/src/client/app/components/UIOptionsComponent.tsx b/src/client/app/components/UIOptionsComponent.tsx index 9fa904cbe..5ffdfdec4 100644 --- a/src/client/app/components/UIOptionsComponent.tsx +++ b/src/client/app/components/UIOptionsComponent.tsx @@ -85,7 +85,6 @@ export default function UIOptionsComponent() { {chartToRender === ChartTypes.compareLine && } {chartToRender === ChartTypes.compareLine && } - diff --git a/src/client/app/translations/data.ts b/src/client/app/translations/data.ts index d909af5f6..ace239248 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 bar", + "compare.bar": "Compare bar", "compare.period": "Compare Period", "compare.raw": "Cannot create comparison graph on raw units such as temperature", "confirm.action": "Confirm Action", @@ -563,7 +563,7 @@ const LocaleTranslationData = { "clipboard.copied": "Copied To Clipboard\u{26A1}", "clipboard.not.copied": "Failed to Copy To Clipboard\u{26A1}", "close": "Close\u{26A1}", - "compare": "Comparer", + "compare.bar": "Compare Bar\u{26A1}", "compare.period": "Compare Period\u{26A1}", "compare.raw": "Cannot create comparison graph on raw units such as temperature\u{26A1}", "confirm.action": "Confirm Action\u{26A1}", @@ -1025,7 +1025,6 @@ const LocaleTranslationData = { "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}", @@ -1078,7 +1077,7 @@ const LocaleTranslationData = { "clipboard.copied": "Copiado al portapapeles", "clipboard.not.copied": "Error al copiar al portapapeles", "close": "Cerrar", - "compare": "Comparar", + "compare.bar": "Compare bar\u{26A1}", "compare.period": "Compare Period\u{26A1}", "compare.raw": "No se puede crear un gráfico de comparación con unidades crudas como temperatura", "confirm.action": "Confirmar acción", @@ -1541,7 +1540,6 @@ const LocaleTranslationData = { "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}", diff --git a/src/client/app/types/redux/graph.ts b/src/client/app/types/redux/graph.ts index b388dbb4c..8dd3e5572 100644 --- a/src/client/app/types/redux/graph.ts +++ b/src/client/app/types/redux/graph.ts @@ -10,7 +10,7 @@ import { AreaUnitType } from '../../utils/getAreaUnitConversion'; export enum ChartTypes { line = 'line', bar = 'bar', - compare = 'compare', + compare = 'compare.bar', map = 'map', radar = 'radar', threeD = '3D', From 8a382c8a2aa1e78ab841336029c1b5a5ffce6293 Mon Sep 17 00:00:00 2001 From: nmqng Date: Wed, 27 Nov 2024 20:51:04 -0500 Subject: [PATCH 11/21] resolved PR comments --- .../components/CompareLineChartComponent.tsx | 40 +++++++++---------- .../CompareLineControlsComponent.tsx | 33 +++++++-------- .../components/GraphicRateMenuComponent.tsx | 3 +- .../app/components/MoreOptionsComponent.tsx | 1 - src/client/app/redux/selectors/uiSelectors.ts | 8 +++- src/client/app/redux/slices/graphSlice.ts | 9 +++++ 6 files changed, 53 insertions(+), 41 deletions(-) diff --git a/src/client/app/components/CompareLineChartComponent.tsx b/src/client/app/components/CompareLineChartComponent.tsx index 24603f964..66709fc08 100644 --- a/src/client/app/components/CompareLineChartComponent.tsx +++ b/src/client/app/components/CompareLineChartComponent.tsx @@ -22,7 +22,7 @@ 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'; +import { shiftDate } from './CompareLineControlsComponent'; /** * @returns plotlyLine graphic */ @@ -40,8 +40,8 @@ export default function CompareLineChartComponent() { 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(['', '']); + const [timeIntervalStr, setTimeIntervalStr] = React.useState(TimeInterval.unbounded()); + const [shiftIntervalStr, setShiftIntervalStr] = React.useState(TimeInterval.unbounded()); // Fetch original data, and derive plotly points const { data, isFetching } = graphState.threeD.meterOrGroup === MeterOrGroup.meters ? @@ -65,26 +65,19 @@ export default function CompareLineChartComponent() { }) }); - // 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()]); - + setTimeIntervalStr(timeInterval); if (shiftAmount !== ShiftAmount.none && shiftAmount !== ShiftAmount.custom) { - updateShiftedInterval(startDate, endDate, shiftAmount); + const { shiftedStart, shiftedEnd } = shiftDate(startDate, endDate, shiftAmount); + dispatch(updateShiftTimeInterval(new TimeInterval(shiftedStart, shiftedEnd))); } } - }, [timeInterval, shiftAmount, updateShiftedInterval]); + }, [timeInterval, shiftAmount]); // Update shift interval string based on shift interval or time interval React.useEffect(() => { @@ -92,13 +85,13 @@ export default function CompareLineChartComponent() { const shiftEnd = shiftInterval.getEndTimestamp(); if (shiftStart && shiftEnd) { - setShiftIntervalStr([shiftStart.toISOString(), shiftEnd.toISOString()]); + setShiftIntervalStr(shiftInterval); } 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()]); + setShiftIntervalStr(timeInterval); } } }, [shiftInterval, timeInterval]); @@ -130,17 +123,16 @@ export default function CompareLineChartComponent() { } // 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); + const enoughData = data.find(data => data.x!.length > 1) && 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 if (!enoughData) { + return <>

{`${translate('no.data.in.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 => ({ @@ -168,7 +160,9 @@ export default function CompareLineChartComponent() { xaxis: { rangeslider: { visible: true }, // Set range for x-axis based on timeIntervalStr so that current data and shifted data is aligned - range: timeIntervalStr.length === 2 ? timeIntervalStr : undefined + range: timeIntervalStr.getIsBounded() + ? [timeIntervalStr.getStartTimestamp(), timeIntervalStr.getEndTimestamp()] + : undefined }, xaxis2: { titlefont: { color: '#1AA5F0' }, @@ -176,7 +170,9 @@ export default function CompareLineChartComponent() { overlaying: 'x', side: 'top', // Set range for x-axis2 based on shiftIntervalStr so that current data and shifted data is aligned - range: shiftIntervalStr.length === 2 ? shiftIntervalStr : undefined + range: shiftIntervalStr.getIsBounded() + ? [shiftIntervalStr.getStartTimestamp(), shiftIntervalStr.getEndTimestamp()] + : undefined } }} config={{ diff --git a/src/client/app/components/CompareLineControlsComponent.tsx b/src/client/app/components/CompareLineControlsComponent.tsx index 210808171..59e6fdf08 100644 --- a/src/client/app/components/CompareLineControlsComponent.tsx +++ b/src/client/app/components/CompareLineControlsComponent.tsx @@ -20,7 +20,7 @@ import { TimeInterval } from '../../../common/TimeInterval'; import { showWarnNotification } from '../utils/notifications'; /** - * @returns compare line control page + * @returns compare line control component for compare line graph page */ export default function CompareLineControlsComponent() { const dispatch = useAppDispatch(); @@ -33,9 +33,9 @@ export default function CompareLineControlsComponent() { // 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); + // 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)); + const [customDateRange, setCustomDateRange] = React.useState(shiftInterval); // Add this useEffect to update the shift interval when the shift option changes React.useEffect(() => { @@ -46,7 +46,7 @@ export default function CompareLineControlsComponent() { // Update custom date range value when shift interval changes React.useEffect(() => { - setCustomDateRange(timeIntervalToDateRange(shiftInterval)); + setCustomDateRange(shiftInterval); }, [shiftInterval]); // Check for leap year shifting when new interval or meter/group is chosen @@ -55,7 +55,7 @@ export default function CompareLineControlsComponent() { 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); + checkLeapYear(startDate, endDate, shiftOption); } }, [graphState.threeD.meterOrGroupID, timeInterval]); @@ -64,9 +64,9 @@ export default function CompareLineControlsComponent() { if (value === 'custom') { setShiftOption(ShiftAmount.custom); dispatch(updateShiftAmount(ShiftAmount.custom)); - setShowDatePicker(true); + // setShowDatePicker(true); } else { - setShowDatePicker(false); + // setShowDatePicker(false); const newShiftOption = value as ShiftAmount; setShiftOption(newShiftOption); dispatch(updateShiftAmount(newShiftOption)); @@ -75,9 +75,9 @@ export default function CompareLineControlsComponent() { const startDate = timeInterval.getStartTimestamp(); const endDate = timeInterval.getEndTimestamp(); - if (startDate && endDate) { + if (timeInterval.getIsBounded()) { // Check whether shifting to (or from) leap year to non leap year or not - checkLeapYearFunc(startDate, endDate, newShiftOption); + checkLeapYear(startDate, endDate, newShiftOption); } // Update shift interval when shift option changes updateShiftInterval(newShiftOption); @@ -89,7 +89,7 @@ export default function CompareLineControlsComponent() { const startDate = timeInterval.getStartTimestamp(); const endDate = timeInterval.getEndTimestamp(); if (startDate !== null || endDate !== null) { - const { shiftedStart, shiftedEnd } = shiftDateFunc(startDate, endDate, shiftOption); + const { shiftedStart, shiftedEnd } = shiftDate(startDate, endDate, shiftOption); const newInterval = new TimeInterval(shiftedStart, shiftedEnd); dispatch(updateShiftTimeInterval(newInterval)); } @@ -97,7 +97,7 @@ export default function CompareLineControlsComponent() { // Update date when the data range picker is used in custome shifting option const handleShiftDateChange = (value: Value) => { - setCustomDateRange(value); + setCustomDateRange(dateRangeToTimeInterval(value)); dispatch(updateShiftTimeInterval(dateRangeToTimeInterval(value))); }; @@ -123,9 +123,10 @@ export default function CompareLineControlsComponent() { {/* Show date picker when custom date range is selected */} - {showDatePicker && + {/* {showDatePicker && */} + {shiftOption === ShiftAmount.custom && } {chartToRender === ChartTypes.compareLine && } {chartToRender === ChartTypes.compareLine && } - {chartToRender === ChartTypes.compareLine && } {chartToRender === ChartTypes.compareLine && } diff --git a/src/client/app/redux/selectors/uiSelectors.ts b/src/client/app/redux/selectors/uiSelectors.ts index dc62fa245..48189f0b7 100644 --- a/src/client/app/redux/selectors/uiSelectors.ts +++ b/src/client/app/redux/selectors/uiSelectors.ts @@ -9,7 +9,7 @@ import { selectUnitDataById } from '../../redux/api/unitsApi'; import { selectChartLinkHideOptions } from '../../redux/slices/appStateSlice'; import { DataType } from '../../types/Datasources'; import { GroupedOption, SelectOption } from '../../types/items'; -import { ChartTypes } from '../../types/redux/graph'; +import { ChartTypes, ShiftAmount } from '../../types/redux/graph'; import { GroupDataByID } from '../../types/redux/groups'; import { MeterDataByID } from '../../types/redux/meters'; import { UnitDataById, UnitRepresentType } from '../../types/redux/units'; @@ -482,6 +482,12 @@ export const selectChartLink = createAppSelector( linkText += `&meterOrGroupID=${current.threeD.meterOrGroupID}`; linkText += `&readingInterval=${current.threeD.readingInterval}`; break; + case ChartTypes.compareLine: + linkText += `&meterOrGroup=${current.threeD.meterOrGroup}`; + linkText += `&meterOrGroupID=${current.threeD.meterOrGroupID}`; + linkText += `&shiftAmount=${current.shiftAmount}`; + current.shiftAmount === ShiftAmount.custom && (linkText += `&shiftTimeInterval=${current.shiftTimeInterval}`); + break; } const unitID = current.selectedUnit; linkText += `&unitID=${unitID.toString()}`; diff --git a/src/client/app/redux/slices/graphSlice.ts b/src/client/app/redux/slices/graphSlice.ts index 0ccbc0c05..d0a040523 100644 --- a/src/client/app/redux/slices/graphSlice.ts +++ b/src/client/app/redux/slices/graphSlice.ts @@ -91,6 +91,9 @@ export const graphSlice = createSlice({ } }, updateShiftTimeInterval: (state, action: PayloadAction) => { + // same as updateTimeInterval, always update if action is bounded, + // else only set unbounded if current isn't already unbounded. + // clearing when already unbounded should be a no-op if (action.payload.getIsBounded() || state.current.shiftTimeInterval.getIsBounded()) { state.current.shiftTimeInterval = action.payload; } @@ -357,6 +360,12 @@ export const graphSlice = createSlice({ case 'unitID': current.selectedUnit = parseInt(value); break; + case 'shiftAmount': + current.shiftAmount = value as ShiftAmount; + break; + case 'shiftTimeInterval': + current.shiftTimeInterval = TimeInterval.fromString(value); + break; } }); } From b3ce0a0a5848b1f668a1999806c6c215d5a25076 Mon Sep 17 00:00:00 2001 From: nmqng Date: Wed, 27 Nov 2024 20:57:01 -0500 Subject: [PATCH 12/21] resolved PR comment about alphabetical order in data.ts --- src/client/app/translations/data.ts | 504 ++++++++++++++-------------- 1 file changed, 252 insertions(+), 252 deletions(-) diff --git a/src/client/app/translations/data.ts b/src/client/app/translations/data.ts index ace239248..70c0c7c93 100644 --- a/src/client/app/translations/data.ts +++ b/src/client/app/translations/data.ts @@ -7,10 +7,14 @@ // This file used to be a json file, but had issues with importing, so we declared the json variable in a js file instead. const LocaleTranslationData = { "en": { + "1.month": "1 month", + "1.week": "1 week", + "1.year": "1 year", + "2.months": "2 months", "3D": "3D", + "4.weeks": "4 Weeks", "400": "400 Bad Request", "404": "404 Not Found", - "4.weeks": "4 Weeks", "action": "Action", "add.new.meters": "Add new meters", "admin.only": "Admin Only", @@ -18,16 +22,16 @@ const LocaleTranslationData = { "alphabetically": "Alphabetically", "area": "Area:", "area.but.no.unit": "You have entered a nonzero area but no area unit.", + "area.calculate.auto": "Calculate Group Area", "area.error": "Please enter a number for area", "area.normalize": "Normalize by Area", - "area.calculate.auto": "Calculate Group Area", "area.unit": "Area Unit:", "AreaUnitType.feet": "sq. feet", "AreaUnitType.meters": "sq. meters", "AreaUnitType.none": "no unit", - "ascending": "Ascending", - "as.meter.unit": "as meter unit", "as.meter.defaultgraphicunit": "as meter default graphic unit", + "as.meter.unit": "as meter unit", + "ascending": "Ascending", "bar": "Bar", "bar.interval": "Bar Interval", "bar.raw": "Cannot create bar graph on raw units such as temperature", @@ -48,13 +52,13 @@ const LocaleTranslationData = { "clipboard.not.copied": "Failed to Copy To Clipboard", "close": "Close", "compare.bar": "Compare bar", + "compare.line": "Compare line", + "compare.line.days.enter": "Enter in days and then hit enter", "compare.period": "Compare Period", "compare.raw": "Cannot create comparison graph on raw units such as temperature", "confirm.action": "Confirm Action", "contact.us": "Contact us", "conversion": "Conversion", - "conversions": "Conversions", - "ConversionType.conversion": "conversion", "conversion.bidirectional": "Bidirectional:", "conversion.create.destination.meter": "The destination cannot be a meter", "conversion.create.exists": "This conversion already exists", @@ -80,16 +84,14 @@ const LocaleTranslationData = { "conversion.successfully.create.conversion": "Successfully created a conversion.", "conversion.successfully.delete.conversion": "Successfully deleted conversion.", "conversion.successfully.edited.conversion": "Successfully edited conversion.", + "conversions": "Conversions", + "ConversionType.conversion": "conversion", "create.conversion": "Create a Conversion", "create.group": "Create a Group", "create.map": "Create a Map", - "create.user": "Create a User", "create.unit": "Create a Unit", + "create.user": "Create a User", "csv": "CSV", - "csvMeters": "CSV Meters", - "csvReadings": "CSV Readings", - "csv.file": "CSV File:", - "csv.file.error": "File must be in CSV format or GZIP format (.csv or .gz). ", "csv.clear.button": "Clear Form", "csv.common.param.gzip": "Gzip", "csv.common.param.header.row": "Header Row", @@ -97,6 +99,8 @@ const LocaleTranslationData = { "csv.download.size.limit": "Sorry you don't have permissions to download due to large number of points.", "csv.download.size.warning.size": "Total size of all files will be about (usually within 10% for large exports).", "csv.download.size.warning.verify": "Are you sure you want to download", + "csv.file": "CSV File:", + "csv.file.error": "File must be in CSV format or GZIP format (.csv or .gz). ", "csv.readings.param.create.meter": "Create Meter", "csv.readings.param.honor.dst": "Honor Daylight Savings Time", "csv.readings.param.meter.identifier": "Meter Identifier:", @@ -111,6 +115,9 @@ const LocaleTranslationData = { "csv.tab.readings": "Readings", "csv.upload.meters": "Upload Meters", "csv.upload.readings": "Upload Readings", + "csvMeters": "CSV Meters", + "csvReadings": "CSV Readings", + "custom.date.range": "Custom date range", "custom.value": "Custom value", "date.range": 'Date Range', "day": "Day", @@ -120,52 +127,52 @@ const LocaleTranslationData = { "default.area.normalize": "Normalize readings by area by default", "default.area.unit": "Default Area Unit", "default.bar.stacking": "Stack bars by default", - "default.graph.type": "Default Graph Type", - "default.graph.settings": "Default Graph Settings", - "defaultGraphicUnit": "Default Graphic Unit:", - "default.language": "Default Language", - "default.meter.reading.frequency": "Default meter reading frequency", - "default.warning.file.size": "Default Warning File Size", "default.file.size.limit": "Default File Size Limit", + "default.graph.settings": "Default Graph Settings", + "default.graph.type": "Default Graph Type", "default.help.url": "Documentation URL", - "default.time.zone": "Default Time Zone", - "default.meter.minimum.value": "Default meter minimum reading value check", + "default.language": "Default Language", + "default.meter.disable.checks": "Default meter disable checks", + "default.meter.maximum.date": "Default meter maximum reading date check", + "default.meter.maximum.errors": "Default maximum number of errors in meter reading", "default.meter.maximum.value": "Default meter maximum reading value check", "default.meter.minimum.date": "Default meter minimum reading date check", - "default.meter.maximum.date": "Default meter maximum reading date check", + "default.meter.minimum.value": "Default meter minimum reading value check", + "default.meter.reading.frequency": "Default meter reading frequency", "default.meter.reading.gap": "Default meter reading gap", - "default.meter.maximum.errors": "Default maximum number of errors in meter reading", - "default.meter.disable.checks": "Default meter disable checks", + "default.time.zone": "Default Time Zone", + "default.warning.file.size": "Default Warning File Size", + "defaultGraphicUnit": "Default Graphic Unit:", "delete.group": "Delete Group", "delete.map": "Delete Map", "delete.self": "Cannot delete your own Account.", "delete.user": "Delete User", "descending": "Descending", - "discard.changes": "Discard Changes", "disable": "Disable", + "discard.changes": "Discard Changes", "displayable": "Displayable:", - "DisplayableType.none": "none", - "DisplayableType.all": "all", "DisplayableType.admin": "admin", - "error.bounds": "Must be between {min} and {max}.", - "error.displayable": "Displayable will be set to false because no unit is selected.", - "error.displayable.meter": "Meter units will set displayable to none.", - "error.displayable.suffix.input": "Suffix input will set displayable to none.", - "error.greater": "Must be greater than {min}.", - "error.gps": "Latitude must be between -90 and 90, and Longitude must be between -180 and 180.", - "error.negative": "Cannot be negative.", - "error.required": "Required field.", - "error.unknown": "Oops! An error has occurred.", + "DisplayableType.all": "all", + "DisplayableType.none": "none", "edit": "Edit", - "edited": "edited", "edit.a.group": "Edit a Group", "edit.a.meter": "Edit a Meter", "edit.group": "Edit Group", "edit.meter": "Details/Edit Meter", "edit.unit": "Edit Unit", "edit.user": "Edit User", + "edited": "edited", "enable": "Enable", "error.bar": "Show error bars", + "error.bounds": "Must be between {min} and {max}.", + "error.displayable": "Displayable will be set to false because no unit is selected.", + "error.displayable.meter": "Meter units will set displayable to none.", + "error.displayable.suffix.input": "Suffix input will set displayable to none.", + "error.gps": "Latitude must be between -90 and 90, and Longitude must be between -180 and 180.", + "error.greater": "Must be greater than {min}.", + "error.negative": "Cannot be negative.", + "error.required": "Required field.", + "error.unknown": "Oops! An error has occurred.", "export.graph.data": "Export graph data", "export.raw.graph.data": "Export graph meter data", "failed.to.create.map": "Failed to create map", @@ -182,12 +189,12 @@ const LocaleTranslationData = { "group": "Group", "group.all.meters": "All Meters", "group.area.calculate": "Calculate Group Area", - "group.area.calculate.header": "Group Area will be set to ", + "group.area.calculate.error.group.unit": "No group area unit", "group.area.calculate.error.header": "The following meters were excluded from the sum because:", - "group.area.calculate.error.zero": ": area is unset or zero", - "group.area.calculate.error.unit": ": nonzero area but no area unit", "group.area.calculate.error.no.meters": "No meters in group", - "group.area.calculate.error.group.unit": "No group area unit", + "group.area.calculate.error.unit": ": nonzero area but no area unit", + "group.area.calculate.error.zero": ": area is unset or zero", + "group.area.calculate.header": "Group Area will be set to ", "group.create.nounit": "The default graphic unit was changed to no unit from ", "group.delete.group": "Delete Group", "group.delete.issue": "is contained in the following groups and cannot be deleted", @@ -205,14 +212,14 @@ const LocaleTranslationData = { "group.hidden": "At least one group is not visible to you", "group.input.error": "Input invalid so group not created or edited.", "group.name.error": "Please enter a valid name: (must have at least one character that is not a space)", - "groups": "Groups", "group.successfully.create.group": "Successfully created a group.", "group.successfully.edited.group": "Successfully edited group.", + "groups": "Groups", "groups.select": "Select Groups", "has.no.data": "has no current data", "has.used": "has used", - "header.pages": "Pages", "header.options": "Options", + "header.pages": "Pages", "help": "Help", "help.admin.conversioncreate": "This page allows admins to create conversions. Please visit {link} for further details and information.", "help.admin.conversionedit": "This page allows admins to edit conversions. Please visit {link} for further details and information.", @@ -231,14 +238,13 @@ const LocaleTranslationData = { "help.admin.users": "This page allows admins to view and edit users. Please visit {link} for further details and information.", "help.csv.meters": "This page allows admins to upload meters via a CSV file. Please visit {link} for further details and information.", "help.csv.readings": "This page allows certain users to upload readings via a CSV file. Please visit {link} for further details and information.", + "help.groups.area.calculate": "This will sum together the area of all meters in this group with a nonzero area with an area unit. It will ignore any meters which have no area or area unit. If this group has no area unit, it will do nothing.", "help.groups.groupdetails": "This page shows detailed information on a group. Please visit {link} for further details and information.", "help.groups.groupview": "This page shows information on groups. Please visit {link} for further details and information.", - "help.groups.area.calculate": "This will sum together the area of all meters in this group with a nonzero area with an area unit. It will ignore any meters which have no area or area unit. If this group has no area unit, it will do nothing.", "help.home.area.normalize": "Toggles normalization by area. Meters/Groups without area will be hidden. Please visit {link} for further details and information.", "help.home.bar.days.tip": "Allows user to select the desired number of days for each bar. Please see {link} for further details and information.", "help.home.bar.interval.tip": "Selects the time interval (Day, Week or 4 Weeks) for each bar. Please see {link} for further details and information.", "help.home.bar.stacking.tip": "Bars stack on top of each other. Please see {link} for further details and information.", - "help.home.map.interval.tip": "Selects the time interval (the last Day, Week or 4 Weeks) for map corresponding to bar's time interval. Please see {link} for further details and information.", "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.", @@ -247,6 +253,7 @@ const LocaleTranslationData = { "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.", "help.home.history": "Allows the user to navigate through the recent history of graphs. Please visit {link} for further details and information.", + "help.home.map.interval.tip": "Selects the time interval (the last Day, Week or 4 Weeks) for map corresponding to bar's time interval. Please see {link} for further details and information.", "help.home.navigation": "The \"Graph\" button goes to the graphic page, the \"Pages\" dropdown allows navigation to information pages, the \"Options\" dropdown allows selection of language, hide options and login/out and the \"Help\" button goes to the help pages. See help on the dropdown menus or the linked pages for further information.", "help.home.readings.per.day": "The number of readings shown for each day in a 3D graphic. Please visit {link} for further details and information.", "help.home.select.dateRange": "Select date range used in graphic display. For 3D graphic must be one year or less. Please visit {link} for further details and information.", @@ -291,12 +298,11 @@ const LocaleTranslationData = { "logo": "Logo", "manage": "Manage", "map": "Map", - "maps": "Maps", + "map.bad.digita": "Greater than 360, please change angle to a number between 0 and 360", + "map.bad.digitb": "Less than 0, please change angle to a number between 0 and 360", "map.bad.load": "Map image file needed", "map.bad.name": "Map name needed", "map.bad.number": "Not a number, please change angle to a number between 0 and 360", - "map.bad.digita": "Greater than 360, please change angle to a number between 0 and 360", - "map.bad.digitb": "Less than 0, please change angle to a number between 0 and 360", "map.calibrate": "Calibrate", "map.calibration": "Calibration status", "map.circle.size": "Map Circle Size", @@ -320,37 +326,33 @@ const LocaleTranslationData = { "map.notify.calibration.needed": "Calibration needed before display", "map.unavailable": "There's not an available map", "map.upload.new.file": "Redo", + "maps": "Maps", "max": "max", "menu": "Menu", "meter": "Meter", - "meters": "Meters", "meter.create": "Create a Meter", "meter.cumulative": "Cumulative:", "meter.cumulativeReset": "Cumulative Reset:", "meter.cumulativeResetEnd": "Cumulative Reset End:", "meter.cumulativeResetStart": "Cumulative Reset Start:", + "meter.disableChecks": "Disable Checks", "meter.enabled": "Updates:", "meter.endOnlyTime": "Only End Times:", "meter.endTimeStamp": "End Time Stamp:", - "meter.minVal": "Minimum Reading Value Check", - "meter.maxVal": "Maximum Reading Value Check", - "meter.minDate": "Minimum Reading Date Check", - "meter.maxDate": "Maximum Reading Date Check", - "meter.maxError": "Maximum Number of Errors Check", - "meter.disableChecks": "Disable Checks", "meter.failed.to.create.meter": "Failed to create a meter with message: ", "meter.failed.to.edit.meter": "Failed to edit meter with message: ", "meter.hidden": "At least one meter is not visible to you", "meter.id": "ID", "meter.input.error": "Input invalid so meter not created or edited.", - "meter.unit.change.requires": "needs to be changed before changing this unit's type", - "meter.unitName": "Unit:", - "meter.url": "URL:", "meter.is.displayable": "Display Enabled", "meter.is.enabled": "Updates Enabled", "meter.is.not.displayable": "Display Disabled", "meter.is.not.enabled": "Updates Disabled", - "meter.unit.is.not.editable": "This meter's unit cannot be changed and was put back to the original value because: ", + "meter.maxDate": "Maximum Reading Date Check", + "meter.maxError": "Maximum Number of Errors Check", + "meter.maxVal": "Maximum Reading Value Check", + "meter.minDate": "Minimum Reading Date Check", + "meter.minVal": "Minimum Reading Value Check", "meter.previousEnd": "Previous End Time Stamp:", "meter.reading": "Reading:", "meter.readingDuplication": "Reading Duplication:", @@ -361,23 +363,29 @@ const LocaleTranslationData = { "meter.startTimeStamp": "Start Time Stamp:", "meter.successfully.create.meter": "Successfully created a meter.", "meter.successfully.edited.meter": "Successfully edited meter.", - "meter.timeSort": "Time Sort:", "meter.time.zone": "Time Zone:", + "meter.timeSort": "Time Sort:", "meter.type": "Type:", - "minute": "Minute", + "meter.unit.change.requires": "needs to be changed before changing this unit's type", + "meter.unit.is.not.editable": "This meter's unit cannot be changed and was put back to the original value because: ", + "meter.unitName": "Unit:", + "meter.url": "URL:", + "meters": "Meters", "min": "min", + "minute": "Minute", "more.energy": "more energy", "more.options": "More Options", "name": "Name:", "navigation": "Navigation", "need.more.points": "Need more points", "no": "no", - "note": "Note: ", "no.data.in.range": "No Data In Date Range", + "note": "Note: ", "oed": "Open Energy Dashboard", "oed.description": "Open Energy Dashboard is an independent open source project. ", "oed.version": "OED version ", "options": "Options", + "original.data.crosses.leap.year.to.non.leap.year": "Original data crosses a leap year so the graph might not align appropriately", "page.choice.login": "Page choices & login", "page.choice.logout": "Page choices & logout", "page.restart.button": "Restart OED session", @@ -388,14 +396,15 @@ const LocaleTranslationData = { "per.hour": "Per Hour", "per.minute": "Per Minute", "per.second": "Per Second", + "please.set.the.date.range": "Please choose date range", "projected.to.be.used": "projected to be used", "radar": "Radar", "radar.lines.incompatible": "These meters/groups are not compatible for radar graphs", "radar.no.data": "There are no readings:
likely the data range is outside meter/group reading range", "rate": "Rate", "reading": "Reading:", - "redo.cik.and.refresh.db.views": "Processing changes. This may take a while.", "readings.per.day": "Readings per Day", + "redo.cik.and.refresh.db.views": "Processing changes. This may take a while.", "redraw": "Redraw", "remove": "Remove", "restore": "Restore", @@ -408,10 +417,13 @@ const LocaleTranslationData = { "select.groups": "Select Groups", "select.map": "Select Map", "select.meter": "Select Meter", - "select.meter.type": "Select Meter Type", "select.meter.group": "Select meter or group to graph", + "select.meter.type": "Select Meter Type", "select.meters": "Select Meters", + "select.shift.amount": "Select shift amount", "select.unit": "Select Unit", + "shift.date.interval": "Shift Date Interval", + "shifted.data.crosses.leap.year.to.non.leap.year": "Shifted data crosses a leap year so the graph might not align appropriately", "show": "Show", "show.grid": "Show grid", "show.options": "Show options", @@ -419,12 +431,11 @@ const LocaleTranslationData = { "site.title": "Site Title", "sort": "Sort Order", "submit": "Submit", - "submitting": "submitting", "submit.changes": "Submit changes", "submit.new.user": "Submit new user", + "submitting": "submitting", "the.unit.of.meter": "The unit of meter", "this.four.weeks": "These four weeks", - "timezone.no": "No timezone", "this.week": "This week", "threeD.area.incompatible": "
is incompatible
with area normalization", "threeD.date": "Date", @@ -436,6 +447,7 @@ const LocaleTranslationData = { "threeD.y.axis.label": "Days of Calendar Year", "TimeSortTypes.decreasing": "decreasing", "TimeSortTypes.increasing": "increasing", + "timezone.no": "No timezone", "today": "Today", "toggle.link": "Toggle chart link", "total": "total", @@ -444,19 +456,13 @@ const LocaleTranslationData = { "TrueFalseType.true": "yes", "undefined": "undefined", "unit": "Unit", - "UnitRepresentType.quantity": "quantity", - "UnitRepresentType.flow": "flow", - "UnitRepresentType.raw": "raw", - "UnitType.unit": "unit", - "UnitType.meter": "meter", - "UnitType.suffix": "suffix", "unit.delete.failure": "Failed to deleted unit with error: ", "unit.delete.success": "Successfully deleted unit", "unit.delete.unit": "Delete Unit", "unit.destination.error": "as the destination unit", - "unit.dropdown.displayable.option.none": "None", - "unit.dropdown.displayable.option.all": "All", "unit.dropdown.displayable.option.admin": "admin", + "unit.dropdown.displayable.option.all": "All", + "unit.dropdown.displayable.option.none": "None", "unit.failed.to.create.unit": "Failed to create a unit.", "unit.failed.to.delete.unit": "Delete cannot be done because this unit is used by the following", "unit.failed.to.edit.unit": "Failed to edit unit.", @@ -472,7 +478,13 @@ const LocaleTranslationData = { "unit.suffix": "Suffix:", "unit.type.of.unit": "Type of Unit:", "unit.type.of.unit.suffix": "Added suffix will set type of unit to suffix", + "UnitRepresentType.flow": "flow", + "UnitRepresentType.quantity": "quantity", + "UnitRepresentType.raw": "raw", "units": "Units", + "UnitType.meter": "meter", + "UnitType.suffix": "suffix", + "UnitType.unit": "unit", "unsaved.failure": "Changes failed to save", "unsaved.success": "Changes saved", "unsaved.warning": "You have unsaved change(s). Are you sure you want to leave?", @@ -487,13 +499,13 @@ const LocaleTranslationData = { "upload.readings.csv": "Upload readings CSV file", "used.so.far": "used so far", "used.this.time": "used this time", - "username": "Username:", "user.delete.confirm": "Delete the user: ", "user.password.edit": "Only enter password to update password", "user.password.length": "Password must be a minimum of 8 characters", "user.password.mismatch": "Passwords do not match", "user.role": "Role: ", "user.role.select": "Select Role", + "username": "Username:", "users": "Users", "users.failed.to.create.user": "Failed to create the user: ", "users.failed.to.delete.user": "Failed to delete the user: ", @@ -508,25 +520,17 @@ const LocaleTranslationData = { "week": "Week", "yes": "yes", "yesterday": "Yesterday", - "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" + "you.cannot.create.a.cyclic.group": "You cannot create a cyclic group" }, "fr": { + "1.month": "1 month\u{26A1}", + "1.week": "1 week\u{26A1}", + "1.year": "1 year\u{26A1}", + "2.months": "2 months\u{26A1}", "3D": "3D", + "4.weeks": "4 Semaines", "400": "400 Bad Request\u{26A1}", "404": "404 Introuvable", - "4.weeks": "4 Semaines", "action": "Action\u{26A1}", "add.new.meters": "Ajouter de Nouveaux Mètres", "admin.only": "Uniquement pour Les Administrateurs", @@ -534,16 +538,16 @@ const LocaleTranslationData = { "alphabetically": "Alphabétiquement", "area": "Région:", "area.but.no.unit": "You have entered a nonzero area but no area unit.\u{26A1}", + "area.calculate.auto": "Calculate Group Area\u{26A1}", "area.error": "Please enter a number for area\u{26A1}", "area.normalize": "Normalize by Area\u{26A1}", - "area.calculate.auto": "Calculate Group Area\u{26A1}", "area.unit": "Area Unit:\u{26A1}", "AreaUnitType.feet": "pieds carrés", "AreaUnitType.meters": "mètre carré", "AreaUnitType.none": "no unit\u{26A1}", - "ascending": "Ascendant", - "as.meter.unit": "as meter unit\u{26A1}", "as.meter.defaultgraphicunit": "as meter default graphic unit\u{26A1}", + "as.meter.unit": "as meter unit\u{26A1}", + "ascending": "Ascendant", "bar": "Bande", "bar.interval": "Intervalle du Diagramme à Bandes", "bar.raw": "Cannot create bar graph on raw units such as temperature\u{26A1}", @@ -564,13 +568,13 @@ const LocaleTranslationData = { "clipboard.not.copied": "Failed to Copy To Clipboard\u{26A1}", "close": "Close\u{26A1}", "compare.bar": "Compare Bar\u{26A1}", + "compare.line": "Compare line\u{26A1}", + "compare.line.days.enter": "Enter in days and then hit enter\u{26A1}", "compare.period": "Compare Period\u{26A1}", "compare.raw": "Cannot create comparison graph on raw units such as temperature\u{26A1}", "confirm.action": "Confirm Action\u{26A1}", "contact.us": "Contactez nous", "conversion": "Conversion\u{26A1}", - "conversions": "Conversions\u{26A1}", - "ConversionType.conversion": "conversion\u{26A1}", "conversion.bidirectional": "Bidirectional:\u{26A1}", "conversion.create.destination.meter": "The destination cannot be a meter\u{26A1}", "conversion.create.exists": "This conversion already exists\u{26A1}", @@ -596,14 +600,14 @@ const LocaleTranslationData = { "conversion.successfully.create.conversion": "Successfully created a conversion.\u{26A1}", "conversion.successfully.delete.conversion": "Successfully deleted conversion.\u{26A1}", "conversion.successfully.edited.conversion": "Successfully edited conversion.\u{26A1}", + "conversions": "Conversions\u{26A1}", + "ConversionType.conversion": "conversion\u{26A1}", "create.conversion": "Create a Conversion\u{26A1}", "create.group": "Créer un Groupe", "create.map": "Créer une carte", "create.unit": "Create a Unit\u{26A1}", "create.user": "Créer un utilisateur", "csv": "CSV", - "csv.file": "Fichier CSV:", - "csv.file.error": "Le fichier doit être au format CSV ou GZIP (.csv ou .gz). ", "csv.clear.button": "Forme claire", "csv.common.param.gzip": "Gzip", "csv.common.param.header.row": "Ligne d'en-tête", @@ -611,6 +615,8 @@ const LocaleTranslationData = { "csv.download.size.limit": "Sorry you don't have permissions to download due to large number of points.\u{26A1}", "csv.download.size.warning.size": "Total size of all files will be about (usually within 10% for large exports).\u{26A1}", "csv.download.size.warning.verify": "Are you sure you want to download\u{26A1}", + "csv.file": "Fichier CSV:", + "csv.file.error": "Le fichier doit être au format CSV ou GZIP (.csv ou .gz). ", "csv.readings.param.create.meter": "Créer un compteur", "csv.readings.param.honor.dst": "Honor Daylight Savings Time\u{26A1}", "csv.readings.param.meter.identifier": "Identifiant du compteur:", @@ -625,6 +631,7 @@ const LocaleTranslationData = { "csv.tab.readings": "Lectures", "csv.upload.meters": "Téléverser Mètres", "csv.upload.readings": "Téléverser Lectures", + "custom.date.range": "Custom date range\u{26A1}", "custom.value": "Custom value\u{26A1}", "date.range": 'Plage de dates', "day": "Journée", @@ -634,52 +641,52 @@ const LocaleTranslationData = { "default.area.normalize": "Normalize readings by area by default\u{26A1}", "default.area.unit": "Default Area Unit\u{26A1}", "default.bar.stacking": "Stack bars by default\u{26A1}", - "default.graph.type": "Type du Diagramme par Défaut", - "default.graph.settings": "Default Graph Settings\u{26A1}", - "defaultGraphicUnit": "Default Graphic Unit:\u{26A1}", - "default.language": "Langue par Défaut", - "default.meter.reading.frequency": "Default meter reading frequency\u{26A1}", - "default.warning.file.size": "Taille du fichier d'avertissement par défaut", "default.file.size.limit": "Limite de taille de fichier par défaut", + "default.graph.settings": "Default Graph Settings\u{26A1}", + "default.graph.type": "Type du Diagramme par Défaut", "default.help.url": "Documentation URL\u{26A1}", - "default.time.zone": "Zona Horaria Predeterminada", - "default.meter.minimum.value": "Default meter minimum reading value check\u{26A1}", + "default.language": "Langue par Défaut", + "default.meter.disable.checks": "Default meter disable checks\u{26A1}", + "default.meter.maximum.date": "Default meter maximum reading date check\u{26A1}", + "default.meter.maximum.errors": "Default maximum number of errors in meter reading\u{26A1}", "default.meter.maximum.value": "Default meter maximum reading value check\u{26A1}", "default.meter.minimum.date": "Default meter minimum reading date check\u{26A1}", - "default.meter.maximum.date": "Default meter maximum reading date check\u{26A1}", + "default.meter.minimum.value": "Default meter minimum reading value check\u{26A1}", + "default.meter.reading.frequency": "Default meter reading frequency\u{26A1}", "default.meter.reading.gap": "Default meter reading gap\u{26A1}", - "default.meter.maximum.errors": "Default maximum number of errors in meter reading\u{26A1}", - "default.meter.disable.checks": "Default meter disable checks\u{26A1}", + "default.time.zone": "Zona Horaria Predeterminada", + "default.warning.file.size": "Taille du fichier d'avertissement par défaut", + "defaultGraphicUnit": "Default Graphic Unit:\u{26A1}", "delete.group": "Supprimer le Groupe", "delete.map": "Supprimer la carte", "delete.self": "Impossible de supprimer votre propre compte.", "delete.user": "Supprimer l'utilisateur", "descending": "Descendant", - "discard.changes": "Annuler les Modifications", "disable": "Désactiver", + "discard.changes": "Annuler les Modifications", "displayable": "Affichable:", - "DisplayableType.none": "none\u{26A1}", - "DisplayableType.all": "all\u{26A1}", "DisplayableType.admin": "admin\u{26A1}", - "error.bounds": "Must be between {min} and {max}.\u{26A1}", - "error.displayable": "Displayable will be set to false because no unit is selected.\u{26A1}", - "error.displayable.meter": "Meter units will set displayable to none.\u{26A1}", - "error.displayable.suffix.input": "Suffix input will set displayable to none.\u{26A1}", - "error.greater": "Must be greater than {min}.\u{26A1}", - "error.gps": "Latitude must be between -90 and 90, and Longitude must be between -180 and 180.\u{26A1}", - "error.negative": "Cannot be negative.\u{26A1}", - "error.required": "Required field.\u{26A1}", - "error.unknown": "Oops! An error has occurred.\u{26A1}", + "DisplayableType.all": "all\u{26A1}", + "DisplayableType.none": "none\u{26A1}", "edit": "Modifier", - "edited": "édité", "edit.a.group": "Modifier le Groupe", "edit.a.meter": "Modifier le Métre", "edit.group": "Modifier Groupe", "edit.meter": "Details/Modifier Métre\u{26A1}", "edit.unit": "Edit Unit\u{26A1}", "edit.user": "Modifier l'utilisateur", + "edited": "édité", "enable": "Activer", "error.bar": "Show error bars\u{26A1}", + "error.bounds": "Must be between {min} and {max}.\u{26A1}", + "error.displayable": "Displayable will be set to false because no unit is selected.\u{26A1}", + "error.displayable.meter": "Meter units will set displayable to none.\u{26A1}", + "error.displayable.suffix.input": "Suffix input will set displayable to none.\u{26A1}", + "error.gps": "Latitude must be between -90 and 90, and Longitude must be between -180 and 180.\u{26A1}", + "error.greater": "Must be greater than {min}.\u{26A1}", + "error.negative": "Cannot be negative.\u{26A1}", + "error.required": "Required field.\u{26A1}", + "error.unknown": "Oops! An error has occurred.\u{26A1}", "export.graph.data": "Exporter les données du diagramme", "export.raw.graph.data": "Export graph meter data\u{26A1}", "failed.to.create.map": "Échec de la création d'une carte", @@ -696,12 +703,12 @@ const LocaleTranslationData = { "group": "Groupe", "group.all.meters": "Tous les compteurs", "group.area.calculate": "Calculate Group Area\u{26A1}", - "group.area.calculate.header": "Group Area will be set to \u{26A1}", + "group.area.calculate.error.group.unit": "No group area unit\u{26A1}", "group.area.calculate.error.header": "The following meters were excluded from the sum because:\u{26A1}", - "group.area.calculate.error.zero": ": area is unset or zero\u{26A1}", - "group.area.calculate.error.unit": ": nonzero area but no area unit\u{26A1}", "group.area.calculate.error.no.meters": "No meters in group\u{26A1}", - "group.area.calculate.error.group.unit": "No group area unit\u{26A1}", + "group.area.calculate.error.unit": ": nonzero area but no area unit\u{26A1}", + "group.area.calculate.error.zero": ": area is unset or zero\u{26A1}", + "group.area.calculate.header": "Group Area will be set to \u{26A1}", "group.create.nounit": "The default graphic unit was changed to no unit from \u{26A1}", "group.delete.group": "Delete Group\u{26A1}", "group.delete.issue": "is contained in the following groups and cannot be deleted\u{26A1}", @@ -719,14 +726,14 @@ const LocaleTranslationData = { "group.hidden": "At least one group is not visible to you\u{26A1}", "group.input.error": "Input invalid so group not created or edited.\u{26A1}", "group.name.error": "Please enter a valid name: (must have at least one character that is not a space)\u{26A1}", - "groups": "Groupes", "group.successfully.create.group": "Successfully created a group.\u{26A1}", "group.successfully.edited.group": "Successfully edited group.\u{26A1}", + "groups": "Groupes", "groups.select": "Sélectionnez des Groupes", "has.no.data": "has no current data\u{26A1}", "has.used": "a utilisé", - "header.pages": "Pages\u{26A1}", "header.options": "Options\u{26A1}", + "header.pages": "Pages\u{26A1}", "help": "Help\u{26A1}", "help.admin.conversioncreate": "This page allows admins to create conversions. Please visit {link} for further details and information.\u{26A1}", "help.admin.conversionedit": "This page allows admins to edit conversions. Please visit {link} for further details and information.\u{26A1}", @@ -745,14 +752,13 @@ const LocaleTranslationData = { "help.admin.users": "This page allows admins to view and edit users. Please visit {link} for further details and information.\u{26A1}", "help.csv.meters": "Cette page permet aux administrateurs de téléverser des mètres via un fichier CSV. Veuillez visiter {link} pour plus de détails et d'informations.\u{26A1}", "help.csv.readings": "Cette page permet à certains utilisateurs de télécharger des lectures via un fichier CSV. Veuillez visiter {link} pour plus de détails et d'informations.\u{26A1}", + "help.groups.area.calculate": "This will sum together the area of all meters in this group with a nonzero area with an area unit. It will ignore any meters which have no area or area unit. If this group has no area unit, it will do nothing.\u{26A1}", "help.groups.groupdetails": "This page shows detailed information on a group. Please visit {link} for further details and information.\u{26A1}", "help.groups.groupview": "This page shows information on groups. Please visit {link} for further details and information.\u{26A1}", - "help.groups.area.calculate": "This will sum together the area of all meters in this group with a nonzero area with an area unit. It will ignore any meters which have no area or area unit. If this group has no area unit, it will do nothing.\u{26A1}", "help.home.area.normalize": "Toggles normalization by area. Meters/Groups without area will be hidden. Please visit {link} for further details and information.\u{26A1}", "help.home.bar.days.tip": "Allows user to select the desired number of days for each bar. Please see {link} for further details and information.\u{26A1}", "help.home.bar.interval.tip": "Selects the time interval (Day, Week or 4 Weeks) for each bar. Please see {link} for further details and information.\u{26A1}", "help.home.bar.stacking.tip": "Bars stack on top of each other. Please see {link} for further details and information.\u{26A1}", - "help.home.map.interval.tip": "for map corresponding to bar's time interval. Please see {link} for further details and information.\u{26A1}", "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}", @@ -761,6 +767,7 @@ const LocaleTranslationData = { "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}", "help.home.history": "Permet à l'utilisateur de naviguer dans l'historique récent des graphiques. Veuillez visiter {link} pour plus de détails et d'informations.", + "help.home.map.interval.tip": "for map corresponding to bar's time interval. Please see {link} for further details and information.\u{26A1}", "help.home.navigation": "The \"Graph\" button goes to the graphic page, the \"Pages\" dropdown allows navigation to information pages, the \"Options\" dropdown allows selection of language, hide options and login/out and the \"Help\" button goes to the help pages. See help on the dropdown menus or the linked pages for further information.\u{26A1}", "help.home.readings.per.day": "The number of readings shown for each day in a 3D graphic. Please visit {link} for further details and information.\u{26A1}", "help.home.select.dateRange": "Select date range used in graphic display. For 3D graphic must be one year or less. Please visit {link} for further details and information.\u{26A1}", @@ -805,12 +812,11 @@ const LocaleTranslationData = { "logo": "Logo", "manage": "Manage\u{26A1}", "map": "Carte", - "maps": "Plans", + "map.bad.digita": "Supérieur à 360, veuillez changer l'angle en un nombre compris entre 0 et 360", + "map.bad.digitb": "Moins de 0, veuillez changer l'angle en un nombre compris entre 0 et 360", "map.bad.load": "Fichier image de la carte requis", "map.bad.name": "Nom de la carte requis", "map.bad.number": "Pas un nombre, veuillez changer l'angle en un nombre entre 0 et 360", - "map.bad.digita": "Supérieur à 360, veuillez changer l'angle en un nombre compris entre 0 et 360", - "map.bad.digitb": "Moins de 0, veuillez changer l'angle en un nombre compris entre 0 et 360", "map.calibrate": "Étalonner", "map.calibration": "Statut d'étalonnage", "map.circle.size": "Taille du cercle de la carte", @@ -834,37 +840,33 @@ const LocaleTranslationData = { "map.notify.calibration.needed": "Étalonnage nécessaire pour l'affichage", "map.unavailable": "There's not an available map\u{26A1}", "map.upload.new.file": "Refaire", + "maps": "Plans", "max": "max\u{26A1}", "menu": "Menu", "meter": "Mèter", - "meters": "Mèters", "meter.create": "Create a Meter\u{26A1}", "meter.cumulative": "Cumulative:\u{26A1}", "meter.cumulativeReset": "Cumulative Reset:\u{26A1}", "meter.cumulativeResetEnd": "Cumulative Reset End:\u{26A1}", "meter.cumulativeResetStart": "Cumulative Reset Start:\u{26A1}", + "meter.disableChecks": "Disable Checks\u{26A1}", "meter.enabled": "Mises à Jour du Mèters", "meter.endOnlyTime": "End Only Time:\u{26A1}", "meter.endTimeStamp": "End Time Stamp:\u{26A1}", - "meter.minVal": "Minimum Reading Value Check\u{26A1}", - "meter.maxVal": "Maximum Reading Value Check\u{26A1}", - "meter.minDate": "Minimum Reading Date Check\u{26A1}", - "meter.maxDate": "Maximum Reading Date Check\u{26A1}", - "meter.maxError": "Maximum Number of Errors Check\u{26A1}", - "meter.disableChecks": "Disable Checks\u{26A1}", "meter.failed.to.create.meter": "Failed to create a meter with message: \u{26A1}", "meter.failed.to.edit.meter": "Failed to edit meter with message: \u{26A1}", "meter.hidden": "At least one meter is not visible to you\u{26A1}", "meter.id": "Identifiant du Mèters", "meter.input.error": "Input invalid so meter not created or edited.\u{26A1}", - "meter.unit.change.requires": "needs to be changed before changing this unit's type\u{26A1}", - "meter.unitName": "Unit:\u{26A1}", - "meter.url": "URL", "meter.is.displayable": "Affichage Activées", "meter.is.enabled": "Mises à Jour Activées", "meter.is.not.displayable": "Affichage Désactivé", "meter.is.not.enabled": "Mises à Jour Désactivées", - "meter.unit.is.not.editable": "This meter's unit cannot be changed and was put back to the original value because: \u{26A1}", + "meter.maxDate": "Maximum Reading Date Check\u{26A1}", + "meter.maxError": "Maximum Number of Errors Check\u{26A1}", + "meter.maxVal": "Maximum Reading Value Check\u{26A1}", + "meter.minDate": "Minimum Reading Date Check\u{26A1}", + "meter.minVal": "Minimum Reading Value Check\u{26A1}", "meter.previousEnd": "Previous End Time Stamp:\u{26A1}", "meter.reading": "Reading:\u{26A1}", "meter.readingDuplication": "Reading Duplication:\u{26A1}", @@ -875,23 +877,29 @@ const LocaleTranslationData = { "meter.startTimeStamp": "Start Time Stamp:\u{26A1}", "meter.successfully.create.meter": "Successfully created a meter.\u{26A1}", "meter.successfully.edited.meter": "Successfully edited meter.\u{26A1}", - "meter.timeSort": "Time Sort:\u{26A1}", "meter.time.zone": "fuseau horaire du mètre", + "meter.timeSort": "Time Sort:\u{26A1}", "meter.type": "Type de Mèters", - "minute": "Minute\u{26A1}", + "meter.unit.change.requires": "needs to be changed before changing this unit's type\u{26A1}", + "meter.unit.is.not.editable": "This meter's unit cannot be changed and was put back to the original value because: \u{26A1}", + "meter.unitName": "Unit:\u{26A1}", + "meter.url": "URL", + "meters": "Mèters", "min": "min\u{26A1}", + "minute": "Minute\u{26A1}", "more.energy": "plus d'énergie", "more.options": "More Options\u{26A1}", "name": "Nom:", "navigation": "Navigation", "need.more.points": "Need more points\u{26A1}", "no": "no\u{26A1}", - "note": "Noter: ", "no.data.in.range": "No Data In Date Range\u{26A1}", + "note": "Noter: ", "oed": "Tableau de Bord Ouvert d'énergie", "oed.description": "Le Tableau de Bord Ouvert d'énergie est un projet open source indépendant. ", "oed.version": "OED version \u{26A1}", "options": "Options", + "original.data.crosses.leap.year.to.non.leap.year": "Original data crosses a leap year so the graph might not align appropriately\u{26A1}", "page.choice.login": "Page choices & login\u{26A1}", "page.choice.logout": "Page choices & logout\u{26A1}", "page.restart.button": "Restart OED session\u{26A1}", @@ -902,14 +910,15 @@ const LocaleTranslationData = { "per.hour": "Per Hour\u{26A1}", "per.minute": "Per Minute\u{26A1}", "per.second": "Per Second\u{26A1}", + "please.set.the.date.range": "Please choose date range\u{26A1}", "projected.to.be.used": "projeté pour être utilisé", "radar": "Radar", "radar.lines.incompatible": "These meters/groups are not compatible for radar graphs\u{26A1}", "radar.no.data": "There are no readings:
likely the data range is outside meter/group reading range\u{26A1}", "rate": "Rate\u{26A1}", "reading": "Reading:\u{26A1}", - "redo.cik.and.refresh.db.views": "Processing changes. This may take a while\u{26A1}", "readings.per.day": "Readings per Day\u{26A1}", + "redo.cik.and.refresh.db.views": "Processing changes. This may take a while\u{26A1}", "redraw": "Redessiner", "remove": "Remove\u{26A1}", "restore": "Restaurer", @@ -921,11 +930,14 @@ const LocaleTranslationData = { "second": "Second\u{26A1}", "select.groups": "Sélectionnez des Groupes", "select.map": "Select Map\u{26A1}", - "select.meter.type": "Select Meter Type\u{26A1}", "select.meter": "Sélectionnez de Mètres", "select.meter.group": "Select meter or group to graph\u{26A1}", + "select.meter.type": "Select Meter Type\u{26A1}", "select.meters": "Sélectionnez des Mètres", + "select.shift.amount": "Select shift amount\u{26A1}", "select.unit": "Select Unit\u{26A1}", + "shift.date.interval": "Shift Date Interval\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}", "show": "Montrer", "show.grid": "Show grid\u{26A1}", "show.options": "Options de désancrage", @@ -933,9 +945,9 @@ const LocaleTranslationData = { "site.title": "Site Title\u{26A1}", "sort": "Sort Order\u{26A1}", "submit": "Soumettre", - "submitting": "submitting\u{26A1}", "submit.changes": "Soumettre les changements", "submit.new.user": "Submit new user\u{26A1}", + "submitting": "submitting\u{26A1}", "the.unit.of.meter": "The unit of meter\u{26A1}", "this.four.weeks": "Cette quatre semaines", "this.week": "Cette semaine", @@ -943,13 +955,10 @@ const LocaleTranslationData = { "threeD.date": "Date", "threeD.date.range.too.long": "Date Range Must be a year or less\u{26A1}", "threeD.incompatible": "Not Compatible with 3D\u{26A1}", - 'threeD.rendering': "Rendering\u{26A1}", "threeD.time": "Temps", - 'threeD.x.axis.label': 'Heures de la journée', - 'threeD.y.axis.label': 'Jours de l\'année calendaire', - "timezone.no": "Pas de fuseau horaire", "TimeSortTypes.decreasing": "décroissant", "TimeSortTypes.increasing": "en augmentant", + "timezone.no": "Pas de fuseau horaire", "today": "Aujourd'hui", "toggle.link": "Bascule du lien du diagramme", "total": "total", @@ -958,19 +967,13 @@ const LocaleTranslationData = { "TrueFalseType.true": "yes\u{26A1}", "undefined": "undefined\u{26A1}", "unit": "Unit\u{26A1}", - "UnitRepresentType.quantity": "quantity\u{26A1}", - "UnitRepresentType.flow": "flow\u{26A1}", - "UnitRepresentType.raw": "raw\u{26A1}", - "UnitType.unit": "unit\u{26A1}", - "UnitType.meter": "meter\u{26A1}", - "UnitType.suffix": "suffix\u{26A1}", "unit.delete.failure": "Failed to deleted unit with error: \u{26A1}", "unit.delete.success": "Successfully deleted unit\u{26A1}", "unit.delete.unit": "Delete Unit\u{26A1}", "unit.destination.error": "as the destination unit\u{26A1}", - "unit.dropdown.displayable.option.none": "None\u{26A1}", - "unit.dropdown.displayable.option.all": "All\u{26A1}", "unit.dropdown.displayable.option.admin": "admin\u{26A1}", + "unit.dropdown.displayable.option.all": "All\u{26A1}", + "unit.dropdown.displayable.option.none": "None\u{26A1}", "unit.failed.to.create.unit": "Failed to create a unit.\u{26A1}", "unit.failed.to.delete.unit": "Delete cannot be done because this unit is used by the following\u{26A1}", "unit.failed.to.edit.unit": "Failed to edit unit.\u{26A1}", @@ -986,7 +989,13 @@ const LocaleTranslationData = { "unit.suffix": "Suffix:\u{26A1}", "unit.type.of.unit": "Type of Unit:\u{26A1}", "unit.type.of.unit.suffix": "Added suffix will set type of unit to suffix\u{26A1}", + "UnitRepresentType.flow": "flow\u{26A1}", + "UnitRepresentType.quantity": "quantity\u{26A1}", + "UnitRepresentType.raw": "raw\u{26A1}", "units": "Units\u{26A1}", + "UnitType.meter": "meter\u{26A1}", + "UnitType.suffix": "suffix\u{26A1}", + "UnitType.unit": "unit\u{26A1}", "unsaved.failure": "Changes failed to save\u{26A1}", "unsaved.success": "Changes saved\u{26A1}", "unsaved.warning": "You have unsaved change(s). Are you sure you want to leave?\u{26A1}", @@ -1023,24 +1032,19 @@ const LocaleTranslationData = { "yes": " yes\u{26A1}", "yesterday": "Hier", "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}", + 'threeD.rendering': "Rendering\u{26A1}", + 'threeD.x.axis.label': 'Heures de la journée', + 'threeD.y.axis.label': 'Jours de l\'année calendaire', + }, + "es": { "1.month": "1 month\u{26A1}", + "1.week": "1 week\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", + "4.weeks": "4 Semanas", "400": "400 Solicitud incorrecta", "404": "404 Página no encontrada", - "4.weeks": "4 Semanas", "action": "Acción", "add.new.meters": "Agregar nuevos medidores", "admin.only": "Solo administrador", @@ -1048,16 +1052,16 @@ const LocaleTranslationData = { "alphabetically": "Alfabéticamente", "area": "Área:", "area.but.no.unit": "Ha ingresado un área distinta a cero sin unidad de área.", + "area.calculate.auto": "Calcular el área del grupo", "area.error": "Por favor indique un número para el área", "area.normalize": "Normalizar según el área", - "area.calculate.auto": "Calcular el área del grupo", "area.unit": "Unidad de área:", "AreaUnitType.feet": "pies cuadrados", "AreaUnitType.meters": "metros cuadrados", "AreaUnitType.none": "sin unidad", - "ascending": "Ascendiente", - "as.meter.unit": "como unidad de medidor", "as.meter.defaultgraphicunit": "como unidad gráfica predeterminada del medidor", + "as.meter.unit": "como unidad de medidor", + "ascending": "Ascendiente", "bar": "Barra", "bar.interval": "Intervalo de barra", "bar.raw": "No se puede crear un gráfico de barras con unidades crudas como la temperatura", @@ -1078,13 +1082,13 @@ const LocaleTranslationData = { "clipboard.not.copied": "Error al copiar al portapapeles", "close": "Cerrar", "compare.bar": "Compare bar\u{26A1}", + "compare.line": "Compare line\u{26A1}", + "compare.line.days.enter": "Enter in days and then hit enter\u{26A1}", "compare.period": "Compare Period\u{26A1}", "compare.raw": "No se puede crear un gráfico de comparación con unidades crudas como temperatura", "confirm.action": "Confirmar acción", "contact.us": "Contáctenos", "conversion": "Conversión", - "conversions": "Conversiones", - "ConversionType.conversion": "conversión", "conversion.bidirectional": "Bidireccional:", "conversion.create.destination.meter": "La destinación no puede ser un medidor", "conversion.create.exists": "Esta conversión ya existe", @@ -1110,14 +1114,14 @@ const LocaleTranslationData = { "conversion.successfully.create.conversion": "Se creó una conversión con éxito.", "conversion.successfully.delete.conversion": "Se eliminó una conversión con éxito.", "conversion.successfully.edited.conversion": "Se editó una conversión con éxito", + "conversions": "Conversiones", + "ConversionType.conversion": "conversión", "create.conversion": "Crear una conversión", "create.group": "Crear un grupo", "create.map": "Crear un mapa", "create.unit": "Crear una unidad", "create.user": "Crear un usuario", "csv": "CSV", - "csv.file": "Archivo CSV:", - "csv.file.error": "El archivo debe estar en formato CSV o GZIP (.csv o .gz). ", "csv.clear.button": "Forma clara", "csv.common.param.gzip": "Gzip", "csv.common.param.header.row": "Fila de cabecera", @@ -1125,6 +1129,8 @@ const LocaleTranslationData = { "csv.download.size.limit": "Perdón, no tienes permiso para descargar por el número de puntos grande.", "csv.download.size.warning.size": "El tamaño todos los archivos juntos será de unos (usualmente dentro de 10% para exportaciones largas).", "csv.download.size.warning.verify": "Estás seguro que quieres descargar", + "csv.file": "Archivo CSV:", + "csv.file.error": "El archivo debe estar en formato CSV o GZIP (.csv o .gz). ", "csv.readings.param.create.meter": "Crear medidor", "csv.readings.param.honor.dst": "Seguir el horario de verano", "csv.readings.param.meter.identifier": "Identificador del medidor:", @@ -1139,6 +1145,7 @@ const LocaleTranslationData = { "csv.tab.readings": "Lecturas", "csv.upload.meters": "Subir medidores CSV", "csv.upload.readings": "Subir lecturas CSV", + "custom.date.range": "Custom date range\u{26A1}", "custom.value": "Valor personalizado", "date.range": 'Rango de fechas', "day": "Día", @@ -1148,53 +1155,53 @@ const LocaleTranslationData = { "default.area.normalize": "Normalizar lecturas según el área por defecto", "default.area.unit": "Unidad de área predeterminada", "default.bar.stacking": "Apilar barras por defecto", - "default.graph.type": "Tipo de gráfico por defecto", - "default.graph.settings": "Configuraciones predeterminadas del gráfico", - "defaultGraphicUnit": "Unidad del gráfico predeterminada:", - "default.language": "Idioma predeterminado", - "default.meter.reading.frequency": "Frecuencia de lectura del medidor predeterminada", - "default.site.title": "Título predeterminado de la página ", - "default.warning.file.size": "Advertencia predeterminada de tamaño del archivo", "default.file.size.limit": "Límite predeterminado de tamaño del archivo", + "default.graph.settings": "Configuraciones predeterminadas del gráfico", + "default.graph.type": "Tipo de gráfico por defecto", "default.help.url": "URL Documentación", - "default.time.zone": "Zona de horario predeterminada", - "default.meter.minimum.value": "Revisión del valor de lectura mínima del medidor predeterminado", + "default.language": "Idioma predeterminado", + "default.meter.disable.checks": "Desactivar revisiones de medidor predeterminado", + "default.meter.maximum.date": "Revisión de la fecha de lectura máxima del medidor predeterminado", + "default.meter.maximum.errors": "Número máximo de errores en la lectura del medidor", "default.meter.maximum.value": "Revisión del valor de lectura máxima del medidor predeterminado", "default.meter.minimum.date": "Revisión de la fecha de lectura mínima del medidor predeterminado", - "default.meter.maximum.date": "Revisión de la fecha de lectura máxima del medidor predeterminado", + "default.meter.minimum.value": "Revisión del valor de lectura mínima del medidor predeterminado", + "default.meter.reading.frequency": "Frecuencia de lectura del medidor predeterminada", "default.meter.reading.gap": "Deistancia predeterminada entre lecturas del medidor", - "default.meter.maximum.errors": "Número máximo de errores en la lectura del medidor", - "default.meter.disable.checks": "Desactivar revisiones de medidor predeterminado", + "default.site.title": "Título predeterminado de la página ", + "default.time.zone": "Zona de horario predeterminada", + "default.warning.file.size": "Advertencia predeterminada de tamaño del archivo", + "defaultGraphicUnit": "Unidad del gráfico predeterminada:", "delete.group": "Borrar grupo", "delete.map": "Borrar mapa", "delete.self": "No puedes eliminar tu propia cuenta.", "delete.user": "Borrar usario", "descending": "Descendente", - "discard.changes": "Descartar los cambios", "disable": "Desactivar", + "discard.changes": "Descartar los cambios", "displayable": "Visualizable:", - "DisplayableType.none": "ninguno", - "DisplayableType.all": "todo", "DisplayableType.admin": "administrador", - "error.bounds": "Debe ser entre {min} y {max}.", - "error.displayable": "El elemento visual determinado como falso porque no hay unidad seleccionada.", - "error.displayable.meter": "Las unidades de medición determinarán al elemento visual como ninguno.", - "error.displayable.suffix.input": "Suffix input will set displayable to none.\u{26A1}", - "error.greater": "Debe ser más que {min}.", - "error.gps": "Latitud deber ser entre -90 y 90, y longitud de entre -180 y 180.", - "error.negative": "No puede ser negativo.", - "error.required": "Campo requerido", - "error.unknown": "¡Ups! Ha ocurrido un error.", + "DisplayableType.all": "todo", + "DisplayableType.none": "ninguno", "edit": "Editar", - "edited": "editado", "edit.a.group": "Editar un grupo", "edit.a.meter": "Editar un medidor", "edit.group": "Editar grupo", "edit.meter": "Details/Editar medidor\u{26A1}", "edit.unit": "Editar unidad", "edit.user": "Editar Usuario", + "edited": "editado", "enable": "Activar", "error.bar": "Mostrar las barras de errores", + "error.bounds": "Debe ser entre {min} y {max}.", + "error.displayable": "El elemento visual determinado como falso porque no hay unidad seleccionada.", + "error.displayable.meter": "Las unidades de medición determinarán al elemento visual como ninguno.", + "error.displayable.suffix.input": "Suffix input will set displayable to none.\u{26A1}", + "error.gps": "Latitud deber ser entre -90 y 90, y longitud de entre -180 y 180.", + "error.greater": "Debe ser más que {min}.", + "error.negative": "No puede ser negativo.", + "error.required": "Campo requerido", + "error.unknown": "¡Ups! Ha ocurrido un error.", "export.graph.data": "Exportar los datos del gráfico", "export.raw.graph.data": "Exportar los datos del medidor del gráfico", "failed.to.create.map": "No se pudo crear el mapa", @@ -1211,12 +1218,12 @@ const LocaleTranslationData = { "group": "Grupo", "group.all.meters": "Todos los medidores", "group.area.calculate": "Calcular el área del grupo", - "group.area.calculate.header": "Área del grupo se establecerá en ", + "group.area.calculate.error.group.unit": "No hay unidad de área del grupo", "group.area.calculate.error.header": "Los siguientes medidores fueron excluidos de la suma porque:", - "group.area.calculate.error.zero": ": el área no está determinada o es cero", - "group.area.calculate.error.unit": ": el área es distinta a cero pero no tiene unidad", "group.area.calculate.error.no.meters": "No hay medidores en el grupo", - "group.area.calculate.error.group.unit": "No hay unidad de área del grupo", + "group.area.calculate.error.unit": ": el área es distinta a cero pero no tiene unidad", + "group.area.calculate.error.zero": ": el área no está determinada o es cero", + "group.area.calculate.header": "Área del grupo se establecerá en ", "group.create.nounit": "La unidad predeterminada del gráfico fue cambiado a sin unidad de ", "group.delete.group": "Borrar grupo", "group.delete.issue": "está en los siguientes grupos y no se puede borrar", @@ -1234,14 +1241,14 @@ const LocaleTranslationData = { "group.hidden": "Hay por lo menos un grupo que no es visible para tí", "group.input.error": "Entrada no válida, por tanto no se creó o editó el grupo", "group.name.error": "Por favor indique un nombre válido: (debe tener por lo menos un carácter que no sea un espacio blanco)", - "groups": "Grupos", "group.successfully.create.group": "Grupo creado con éxito.", "group.successfully.edited.group": "Grupo editado con éxito.", + "groups": "Grupos", "groups.select": "Seleccionar grupos", "has.no.data": "No hay datos actuales", "has.used": "ha utilizado", - "header.pages": "Páginas", "header.options": "Opciones", + "header.pages": "Páginas", "help": "Ayuda", "help.admin.conversioncreate": "Esta página permite a los administradores crear conversiones. Por favor visite {link} para más detalles e información.", "help.admin.conversionedit": "Esta página permite a los administradores editar conversiones. Por favor visite {link} para más detalles e información", @@ -1260,14 +1267,13 @@ const LocaleTranslationData = { "help.admin.users": "Esta página permite a los administradores ver y editar usarios. Por favor, visite {link} para más detalles e información.", "help.csv.meters": "Esta página permite a los administradores cargar medidores a través de un archivo CSV. Visite {enlace} para obtener más detalles e información.", "help.csv.readings": "Esta página permite a ciertos usuarios cargar lecturas a través de un archivo CSV. Visite {link} para obtener más detalles e información.", + "help.groups.area.calculate": "Esto sumará el área de todos los medidores en este grupo que tengan un área distinta a cero y una unidad de área. Ignorará cualquier medidor que no tiene área o unidad de área. Si este grupo no tiene unidad de área, no hará nada.", "help.groups.groupdetails": "Esta página muestra información detallada de un grupo. Por favor visite {link} para obtener más detalles e información.", "help.groups.groupview": "Esta página muestra información sobre grupos. Por favor, visite {link} para más detalles e información.", - "help.groups.area.calculate": "Esto sumará el área de todos los medidores en este grupo que tengan un área distinta a cero y una unidad de área. Ignorará cualquier medidor que no tiene área o unidad de área. Si este grupo no tiene unidad de área, no hará nada.", "help.home.area.normalize": "Alterna la normalización por área. Medidores/Grupos sin área quedarán escondidos. Por favor visite {link} para obtener más detalles e información.", "help.home.bar.days.tip": "Permite al usuario seleccionar el número de días deseado para cada barra. Por favor, visite {link} para más detalles e información.", "help.home.bar.interval.tip": "Selecciona el intervalo de tiempo (día, semana o 4 semanas) para cada barra. Por favor, visite {link} para más detalles e información.", "help.home.bar.stacking.tip": "Apila las barras una encima de la otra. Por favor, visite {link} para obtener más detalles e información.", - "help.home.map.interval.tip": "Seleciona el intervalo de tiempo (el último día, semana o 4 semanas) para el mapa correspondiente al intervalo de tiempo de la barra. Por favor visite {link} para mas detalles e información", "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.", @@ -1276,6 +1282,7 @@ const LocaleTranslationData = { "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.", "help.home.history": "Permite al usuario navegar por el historial reciente de gráficos. Por favor visite {link} para obtener más detalles e información.", + "help.home.map.interval.tip": "Seleciona el intervalo de tiempo (el último día, semana o 4 semanas) para el mapa correspondiente al intervalo de tiempo de la barra. Por favor visite {link} para mas detalles e información", "help.home.navigation": "El botón \"Gráfico\" va a la página de gráficos, el desplegable \"Páginas\" permite la navigación a las páginas de información, el desplegable \"Opciones\" permite la selección del idioma, opciones de ocultar y de iniciar/cerrar sesión y el botón \"Ayuda\" va a las páginas de ayuda. Busca ayuda en los menús desplegables o las páginas vínculadas para más información.", "help.home.readings.per.day": "El número de lecturas mostrado para cada día en un gráfico 3D. Por favor visite {link} para obtener más detalles e información.", "help.home.select.dateRange": "Seleccione el rango de datos usado en el gráfico mostrado. Para gráficos 3D debe ser un año o menos. Por favor visite {link} para obtener más detalles e información.", @@ -1320,12 +1327,11 @@ const LocaleTranslationData = { "logo": "Logo", "manage": "Gestionar", "map": "Mapa", - "maps": "Mapas", + "map.bad.digita": "Mayor a 360, por favor cambiar el angúlo a un número entre 0 a 360", + "map.bad.digitb": "Menor a 0, por favor cambiar el angúlo a un número entre 0 a 360", "map.bad.load": "Se necesita el archivo de la imagen del mapa", "map.bad.name": "Se necesita un nombre para el mapa", "map.bad.number": "No es número, por favor cambiar el angúlo a un número entre 0 a 360", - "map.bad.digita": "Mayor a 360, por favor cambiar el angúlo a un número entre 0 a 360", - "map.bad.digitb": "Menor a 0, por favor cambiar el angúlo a un número entre 0 a 360", "map.calibrate": "Calibrar", "map.calibration": "Estado de calibración", "map.circle.size": "Tamaño del círculo en el mapa", @@ -1349,37 +1355,33 @@ const LocaleTranslationData = { "map.notify.calibration.needed": "Necesita calibración antes de visualizar", "map.unavailable": "There's not an available map\u{26A1}", "map.upload.new.file": "Rehacer", + "maps": "Mapas", "max": "máximo", "menu": "Menú", "meter": "Medidor", - "meters": "Medidores", "meter.create": "Crear un medidor", "meter.cumulative": "Cumulativo", "meter.cumulativeReset": "Reinicio cumulativo:", "meter.cumulativeResetEnd": "Final del reinicio cumulativo:", "meter.cumulativeResetStart": "Comienzo del reinicio cumulativo:", + "meter.disableChecks": "Desactivar revisiones", "meter.enabled": "Medidor activado", "meter.endOnlyTime": "Solo tiempos finales.", "meter.endTimeStamp": "Marca de tiempo al final:", - "meter.minVal": "Revisión del valor mínimo de lectura", - "meter.maxVal": "Revisión del valor máximo de lectura", - "meter.minDate": "Revisión de la fecha mínima de lectura", - "meter.maxDate": "Revisión de la fecha máxima de lectura", - "meter.maxError": "Revisión del número máximo de errores", - "meter.disableChecks": "Desactivar revisiones", "meter.failed.to.create.meter": "No se pudo crear un medidor con mensaje: ", "meter.failed.to.edit.meter": "No se pudo editar un medidor con mensaje: ", "meter.hidden": "Al menos un medidor no es visible para tí.", "meter.id": "ID del medidor", "meter.input.error": "Entrada no válida, por tanto no se creó o editó el medidor.", - "meter.unit.change.requires": "necesita cambiarse antes de cambiar el tipo de esta unidad", - "meter.unitName": "Unidad:", - "meter.url": "URL:", "meter.is.displayable": "El medidor es visualizable", "meter.is.enabled": "Actualizaciones activadas", "meter.is.not.displayable": "El medidor no es visualizable", "meter.is.not.enabled": "El medidor no está activado", - "meter.unit.is.not.editable": "La unidad de este medidor no puede cambiarse y se mantuvo el valor original porque: ", + "meter.maxDate": "Revisión de la fecha máxima de lectura", + "meter.maxError": "Revisión del número máximo de errores", + "meter.maxVal": "Revisión del valor máximo de lectura", + "meter.minDate": "Revisión de la fecha mínima de lectura", + "meter.minVal": "Revisión del valor mínimo de lectura", "meter.previousEnd": "Marca de tiempo del final anterior:", "meter.reading": "Lectura:", "meter.readingDuplication": "Duplicación de lectura:", @@ -1390,23 +1392,29 @@ const LocaleTranslationData = { "meter.startTimeStamp": "Marca de tiempo al inicio:", "meter.successfully.create.meter": "Éxito al crear el medidor.", "meter.successfully.edited.meter": "Éxito al editar el medidor.", - "meter.timeSort": "Ordenar por tiempo:", "meter.time.zone": "Zona horaria:", + "meter.timeSort": "Ordenar por tiempo:", "meter.type": "Tipo:", - "minute": "Minuto", + "meter.unit.change.requires": "necesita cambiarse antes de cambiar el tipo de esta unidad", + "meter.unit.is.not.editable": "La unidad de este medidor no puede cambiarse y se mantuvo el valor original porque: ", + "meter.unitName": "Unidad:", + "meter.url": "URL:", + "meters": "Medidores", "min": "mínimo", + "minute": "Minuto", "more.energy": "más energía", "more.options": "More Options\u{26A1}", "name": "Nombre:", "navigation": "Navegación", "need.more.points": "Nececita más puntos", "no": "no", - "note": "Nota: ", "no.data.in.range": "No hay datos para el rango de fechas", + "note": "Nota: ", "oed": "Panel de Energía Abierto", "oed.description": "Open Energy Dashboard es un proyecto independiente. ", "oed.version": "Versión OED", "options": "Opciones", + "original.data.crosses.leap.year.to.non.leap.year": "Original data crosses a leap year so the graph might not align appropriately\u{26A1}", "page.choice.login": "Selección de página y inicio de sesión", "page.choice.logout": "Selección de página y fin de sesión", "page.restart.button": "Restart OED session\u{26A1}", @@ -1417,14 +1425,15 @@ const LocaleTranslationData = { "per.hour": "Por hora", "per.minute": "Por minuto", "per.second": "Por segundo", + "please.set.the.date.range": "Please choose date range\u{26A1}", "projected.to.be.used": "proyectado para ser utilizado", "radar": "Radar", "radar.lines.incompatible": "Estos medidores/grupos no son compatibles para gráficos de radares", "radar.no.data": "No hay lecturas:
es probable que el rango de los datos esté fuera del rango de lecturas de los medidores/grupos", "rate": "Tasa", "reading": "Lectura:", - "redo.cik.and.refresh.db.views": "Procesando los cambios. Esto tardará un momento.", "readings.per.day": "Lecturas por día", + "redo.cik.and.refresh.db.views": "Procesando los cambios. Esto tardará un momento.", "redraw": "Redibujar", "remove": "Eliminar", "restore": "Restaurar", @@ -1436,11 +1445,14 @@ const LocaleTranslationData = { "second": "Segundo", "select.groups": "Seleccionar grupos", "select.map": "Seleccionar mapa", - "select.meter.type": "Seleccionar tipo de medidor", "select.meter": "Seleccionar medidor", "select.meter.group": "Seleccionar medidor o grupo para hacer gráfico", + "select.meter.type": "Seleccionar tipo de medidor", "select.meters": "Seleccionar medidores", + "select.shift.amount": "Select shift amount\u{26A1}", "select.unit": "Seleccionar unidad", + "shift.date.interval": "Shift Date Interval\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}", "show": "Mostrar", "show.grid": "Mostrar rejilla", "show.options": "Mostrar opciones", @@ -1448,12 +1460,11 @@ const LocaleTranslationData = { "site.title": "Site Title\u{26A1}", "sort": "Sort Order\u{26A1}", "submit": "Enviar", - "submitting": "Enviando", "submit.changes": "Ingresar los cambios", "submit.new.user": "Ingresar un nuevo usario", + "submitting": "Enviando", "the.unit.of.meter": "La unidad del medidor", "this.four.weeks": "Estas cuatro semanas", - "timezone.no": "sin zona horaria", "this.week": "Esta semana", "threeD.area.incompatible": "
es incompatible
con normalización del área", "threeD.date": "Fecha", @@ -1465,6 +1476,7 @@ const LocaleTranslationData = { "threeD.y.axis.label": "Días del año calendario", "TimeSortTypes.decreasing": "decreciente", "TimeSortTypes.increasing": "creciente", + "timezone.no": "sin zona horaria", "today": "Hoy", "toggle.link": "Alternar enlace de gráfico", "total": "total", @@ -1473,19 +1485,13 @@ const LocaleTranslationData = { "TrueFalseType.true": "sí", "undefined": "indefinido", "unit": "Unidad", - "UnitRepresentType.quantity": "cantidad", - "UnitRepresentType.flow": "flujo", - "UnitRepresentType.raw": "crudo", - "UnitType.unit": "unidad", - "UnitType.meter": "medidor", - "UnitType.suffix": "sufijo", "unit.delete.failure": "No se pudo borrar la unidad con error: ", "unit.delete.success": "Éxito al borrar la unidad", "unit.delete.unit": "Borrar la unidad", "unit.destination.error": "como la unidad de la destinación", - "unit.dropdown.displayable.option.none": "Ninguna", - "unit.dropdown.displayable.option.all": "Todo", "unit.dropdown.displayable.option.admin": "administrador", + "unit.dropdown.displayable.option.all": "Todo", + "unit.dropdown.displayable.option.none": "Ninguna", "unit.failed.to.create.unit": "No se pudo crear la unidad", "unit.failed.to.delete.unit": "No se puede borrar por que esta unidad está siendo usada por lo siguiente", "unit.failed.to.edit.unit": "No se pudo editar la unidad", @@ -1501,7 +1507,13 @@ const LocaleTranslationData = { "unit.suffix": "Sufijo:", "unit.type.of.unit": "Tipo de unidad:", "unit.type.of.unit.suffix": "El sufijo agregado determina que el tipo de la unidad es sufijo", + "UnitRepresentType.flow": "flujo", + "UnitRepresentType.quantity": "cantidad", + "UnitRepresentType.raw": "crudo", "units": "Unidades", + "UnitType.meter": "medidor", + "UnitType.suffix": "sufijo", + "UnitType.unit": "unidad", "unsaved.failure": "No se pudieron guardar los cambios", "unsaved.success": "Se guardaron los cambios", "unsaved.warning": "Tienes cambios sin guardar. ¿Estás seguro que quieres salir?", @@ -1537,19 +1549,7 @@ const LocaleTranslationData = { "week": "semana", "yes": "sí", "yesterday": "Ayer", - "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}", - "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}" + "you.cannot.create.a.cyclic.group": "No se puede crear un grupo cíclico" } } From ebc9d310035cbc6ada6cadb186310595189e6888 Mon Sep 17 00:00:00 2001 From: nmqng Date: Fri, 29 Nov 2024 01:11:21 -0500 Subject: [PATCH 13/21] raw update on shiftDate(...) and checking reading data based on updated desdoc and resolved some PR comments --- .../components/CompareLineChartComponent.tsx | 89 ++++++- .../CompareLineControlsComponent.tsx | 225 +++++++++--------- src/client/app/components/ThreeDComponent.tsx | 2 +- .../app/components/ThreeDPillComponent.tsx | 2 +- 4 files changed, 191 insertions(+), 127 deletions(-) diff --git a/src/client/app/components/CompareLineChartComponent.tsx b/src/client/app/components/CompareLineChartComponent.tsx index 66709fc08..12ab2632a 100644 --- a/src/client/app/components/CompareLineChartComponent.tsx +++ b/src/client/app/components/CompareLineChartComponent.tsx @@ -4,6 +4,7 @@ import { debounce } from 'lodash'; import { utc } from 'moment'; +import * as moment from 'moment'; import * as React from 'react'; import Plot from 'react-plotly.js'; import { TimeInterval } from '../../../common/TimeInterval'; @@ -21,8 +22,10 @@ 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 { shiftDate } from './CompareLineControlsComponent'; +import { showInfoNotification, showWarnNotification } from '../utils/notifications'; +import { PlotRelayoutEvent } from 'plotly.js'; + /** * @returns plotlyLine graphic */ @@ -81,16 +84,11 @@ export default function CompareLineChartComponent() { // Update shift interval string based on shift interval or time interval React.useEffect(() => { - const shiftStart = shiftInterval.getStartTimestamp(); - const shiftEnd = shiftInterval.getEndTimestamp(); - - if (shiftStart && shiftEnd) { + if (shiftInterval.getIsBounded()) { setShiftIntervalStr(shiftInterval); } else { // If shift interval is not set, use the original time interval - const startDate = timeInterval.getStartTimestamp(); - const endDate = timeInterval.getEndTimestamp(); - if (startDate && endDate) { + if (timeInterval.getIsBounded()) { setShiftIntervalStr(timeInterval); } } @@ -127,14 +125,19 @@ export default function CompareLineChartComponent() { // 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)) { + if (!meterOrGroupID) { return <>

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

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

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

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

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

; } else { - // adding information to the shifted data so that it can be plotted on the same graph with current data + // Checks/warnings on received reading data + if (timeInterval.getIsBounded() && shiftInterval.getIsBounded()) { + checkReceivedData(data[0].x, dataNew[0].x); + } + + // 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, @@ -149,16 +152,15 @@ export default function CompareLineChartComponent() { <> moment(originalReading.at(0).toString())) { + showInfoNotification(`The shifted line overlaps the original line starting at ${originalReading[0]}`); + } + // Now see if day of the week aligns. + // If the number of points is not the same then no horizontal alignment so do not tell user. + if (numberPointsSame && moment(originalReading.at(0)?.toString()).day() === moment(shiftedReading.at(0)?.toString()).day()) { + showInfoNotification('days of week align (unless missing readings)'); + } + // Now see if the month and day align. If the number of points is not the same then no horizontal + // alignment so do not tell user. Check if the first reading matches because only notify if this is true. + if (numberPointsSame && monthDateSame(moment(originalReading.at(0)?.toString()), moment(shiftedReading.at(0)?.toString()))) { + // Loop over all readings but the first. Really okay to do first but just checked that one. + // Note length of original and shifted same so just use original. + let message = 'The month and day of the month align for the original and shifted readings'; + for (let i = 1; i < originalReading.length; i++) { + if (!monthDateSame(moment(originalReading.at(i)?.toString()), moment(shiftedReading.at(i)?.toString()))) { + // Mismatch so inform user. Should be due to leap year crossing and differing leap year. + // Only tell first mistmatch + message += ` until original reading at date ${moment(originalReading.at(i)?.toString()).format('ll')}`; + } + } + showInfoNotification(message); + } +} + +/** + * Check if the two dates have the same date and month + * @param firstDate first date to compare + * @param secondDate second date to compare + * @returns true if the month and date are the same + */ +function monthDateSame(firstDate: moment.Moment, secondDate: moment.Moment) { + // The month (0 up numbering) and date (day of month with 1 up numbering) must match. + // The time could be checked but the granulatity should be the same for original and + // shifted readings and only mismatch if there is missing readings. In the unlikely + // event of having the same number of points but different missing readings then + // the first one will mismatch the month or day unless those happen to match in which + // case it is still true that they are generally okay so ignore all this. + return firstDate.month() === secondDate.month() && firstDate.date() === secondDate.date(); +} \ No newline at end of file diff --git a/src/client/app/components/CompareLineControlsComponent.tsx b/src/client/app/components/CompareLineControlsComponent.tsx index 59e6fdf08..8d86e065d 100644 --- a/src/client/app/components/CompareLineControlsComponent.tsx +++ b/src/client/app/components/CompareLineControlsComponent.tsx @@ -6,7 +6,7 @@ import * as React from 'react'; import { Input } from 'reactstrap'; import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; import { - selectGraphState, selectQueryTimeInterval, selectShiftAmount, selectShiftTimeInterval, updateShiftAmount, updateShiftTimeInterval + selectQueryTimeInterval, selectShiftAmount, selectShiftTimeInterval, updateShiftAmount, updateShiftTimeInterval } from '../redux/slices/graphSlice'; import translate from '../utils/translate'; import { FormattedMessage } from 'react-intl'; @@ -17,7 +17,7 @@ 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'; +// import { showWarnNotification } from '../utils/notifications'; /** * @returns compare line control component for compare line graph page @@ -28,12 +28,9 @@ export default function CompareLineControlsComponent() { 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(shiftInterval); @@ -49,42 +46,20 @@ export default function CompareLineControlsComponent() { setCustomDateRange(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 - checkLeapYear(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 (timeInterval.getIsBounded()) { - // Check whether shifting to (or from) leap year to non leap year or not - checkLeapYear(startDate, endDate, newShiftOption); - } - // Update shift interval when shift option changes updateShiftInterval(newShiftOption); } }; - // update shift data date range when shift date interval option is chosen + // Update shift data date range when shift date interval option is chosen const updateShiftInterval = (shiftOption: ShiftAmount) => { const startDate = timeInterval.getStartTimestamp(); const endDate = timeInterval.getEndTimestamp(); @@ -104,7 +79,7 @@ export default function CompareLineControlsComponent() { return ( <>
-

+

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

@@ -123,7 +98,6 @@ export default function CompareLineControlsComponent() { {/* Show date picker when custom date range is selected */} - {/* {showDatePicker && */} {shiftOption === ShiftAmount.custom && Date: Fri, 29 Nov 2024 14:04:30 -0500 Subject: [PATCH 14/21] resolved PR comments --- .../components/CompareLineChartComponent.tsx | 106 +++++++++--------- src/client/app/translations/data.ts | 4 - 2 files changed, 56 insertions(+), 54 deletions(-) diff --git a/src/client/app/components/CompareLineChartComponent.tsx b/src/client/app/components/CompareLineChartComponent.tsx index 12ab2632a..657087c71 100644 --- a/src/client/app/components/CompareLineChartComponent.tsx +++ b/src/client/app/components/CompareLineChartComponent.tsx @@ -25,6 +25,7 @@ import { MeterOrGroup, ShiftAmount } from '../types/redux/graph'; import { shiftDate } from './CompareLineControlsComponent'; import { showInfoNotification, showWarnNotification } from '../utils/notifications'; import { PlotRelayoutEvent } from 'plotly.js'; +import { setHelpLayout } from './ThreeDComponent'; /** * @returns plotlyLine graphic @@ -45,6 +46,8 @@ export default function CompareLineChartComponent() { // Storing the time interval strings for the original data and the shifted data to use for range in plot const [timeIntervalStr, setTimeIntervalStr] = React.useState(TimeInterval.unbounded()); const [shiftIntervalStr, setShiftIntervalStr] = React.useState(TimeInterval.unbounded()); + // Layout for the plot + let layout = {}; // Fetch original data, and derive plotly points const { data, isFetching } = graphState.threeD.meterOrGroup === MeterOrGroup.meters ? @@ -116,67 +119,67 @@ export default function CompareLineChartComponent() { }) }); - 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) && 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 (!meterOrGroupID) { - return <>

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

; + layout = setHelpLayout(translate('select.meter.group')); } else if (!timeInterval.getIsBounded()) { - return <>

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

; + layout = setHelpLayout(translate('please.set.the.date.range')); } else if (!enoughData) { - return <>

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

; + layout = setHelpLayout(translate('no.data.in.range')); } else { // Checks/warnings on received reading data if (timeInterval.getIsBounded() && shiftInterval.getIsBounded()) { - checkReceivedData(data[0].x, dataNew[0].x); + checkReceivedData(data[0], dataNew[0]); } + layout = { + autosize: true, showlegend: true, + legend: { x: 0, y: 1.1, orientation: 'h' }, + // 'fixedrange' on the yAxis means that dragging is only allowed on the xAxis which we utilize for selecting dateRanges + yaxis: { title: unitLabel, gridcolor: '#ddd', fixedrange: true }, + xaxis: { + // Set range for x-axis based on timeIntervalStr so that current data and shifted data is aligned + range: timeIntervalStr.getIsBounded() + ? [timeIntervalStr.getStartTimestamp(), timeIntervalStr.getEndTimestamp()] + : undefined + }, + xaxis2: { + titlefont: { color: '#1AA5F0' }, + tickfont: { color: '#1AA5F0' }, + overlaying: 'x', + side: 'top', + // Set range for x-axis2 based on shiftIntervalStr so that current data and shifted data is aligned + range: shiftIntervalStr.getIsBounded() + ? [shiftIntervalStr.getStartTimestamp(), shiftIntervalStr.getEndTimestamp()] + : undefined + } + }; + } - // 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 ') - })); + // 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 ( - <> - - + + {isFetching || isFetchingNew + ? + : - + } - ); + + + ); - } } /** @@ -221,10 +225,12 @@ export default function CompareLineChartComponent() { * If not, it is unlikely but can happen if there are missing readings in both lines that do not align but there are the same number missing in both. * This is an ugly edge case that OED is not going to try to catch now. * Use the last index in Redux state as a proxy for the number since need that below. - * @param originalReading original data to compare - * @param shiftedReading shifted data to compare + * @param originalData original data to compare + * @param shiftedData shifted data to compare */ -function checkReceivedData(originalReading: any, shiftedReading: any) { +function checkReceivedData(originalData: any, shiftedData: any) { + const originalReading = originalData.x; + const shiftedReading = shiftedData.x; let numberPointsSame = true; if (originalReading.length !== shiftedReading.length) { // If the number of points vary then then scales will not line up point by point. Warn the user. diff --git a/src/client/app/translations/data.ts b/src/client/app/translations/data.ts index be83b8bb5..830bb4fc6 100644 --- a/src/client/app/translations/data.ts +++ b/src/client/app/translations/data.ts @@ -636,8 +636,6 @@ const LocaleTranslationData = { "csv.download.size.limit": "Sorry you don't have permissions to download due to large number of points.\u{26A1}", "csv.download.size.warning.size": "Total size of all files will be about (usually within 10% for large exports).\u{26A1}", "csv.download.size.warning.verify": "Are you sure you want to download\u{26A1}", - "csv.file": "Fichier CSV:", - "csv.file.error": "Le fichier doit être au format CSV ou GZIP (.csv ou .gz). ", "csv.readings.param.create.meter": "Créer un compteur", "csv.readings.param.honor.dst": "Honor Daylight Savings Time\u{26A1}", "csv.readings.param.meter.identifier": "Identifiant du compteur:", @@ -1171,8 +1169,6 @@ const LocaleTranslationData = { "csv.download.size.limit": "Perdón, no tienes permiso para descargar por el número de puntos grande.", "csv.download.size.warning.size": "El tamaño todos los archivos juntos será de unos (usualmente dentro de 10% para exportaciones largas).", "csv.download.size.warning.verify": "Estás seguro que quieres descargar", - "csv.file": "Archivo CSV:", - "csv.file.error": "El archivo debe estar en formato CSV o GZIP (.csv o .gz). ", "csv.readings.param.create.meter": "Crear medidor", "csv.readings.param.honor.dst": "Seguir el horario de verano", "csv.readings.param.meter.identifier": "Identificador del medidor:", From 78bcbc215faf91ef1823a71e19585bfd426f5a49 Mon Sep 17 00:00:00 2001 From: nmqng Date: Sun, 1 Dec 2024 23:28:45 -0500 Subject: [PATCH 15/21] resolved PR comments --- .../components/CompareLineChartComponent.tsx | 73 +++++++------------ .../CompareLineControlsComponent.tsx | 3 + .../app/components/ThreeDPillComponent.tsx | 2 +- src/client/app/translations/data.ts | 3 + src/client/app/types/redux/graph.ts | 1 + 5 files changed, 36 insertions(+), 46 deletions(-) diff --git a/src/client/app/components/CompareLineChartComponent.tsx b/src/client/app/components/CompareLineChartComponent.tsx index 657087c71..b8b9826f5 100644 --- a/src/client/app/components/CompareLineChartComponent.tsx +++ b/src/client/app/components/CompareLineChartComponent.tsx @@ -2,13 +2,10 @@ * 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 moment 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'; @@ -17,15 +14,15 @@ 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 { selectGraphState, selectShiftAmount, 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 { shiftDate } from './CompareLineControlsComponent'; import { showInfoNotification, showWarnNotification } from '../utils/notifications'; -import { PlotRelayoutEvent } from 'plotly.js'; import { setHelpLayout } from './ThreeDComponent'; +import { toast } from 'react-toastify'; /** * @returns plotlyLine graphic @@ -36,13 +33,11 @@ export default function CompareLineChartComponent() { 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; - + const shiftInterval = graphState.shiftTimeInterval; // Storing the time interval strings for the original data and the shifted data to use for range in plot const [timeIntervalStr, setTimeIntervalStr] = React.useState(TimeInterval.unbounded()); const [shiftIntervalStr, setShiftIntervalStr] = React.useState(TimeInterval.unbounded()); @@ -73,12 +68,13 @@ export default function CompareLineChartComponent() { // Update shifted interval based on current interval and shift amount React.useEffect(() => { - const startDate = timeInterval.getStartTimestamp(); - const endDate = timeInterval.getEndTimestamp(); + console.log('first effect is being called'); - if (startDate && endDate) { + if (timeInterval.getIsBounded()) { setTimeIntervalStr(timeInterval); if (shiftAmount !== ShiftAmount.none && shiftAmount !== ShiftAmount.custom) { + const startDate = timeInterval.getStartTimestamp(); + const endDate = timeInterval.getEndTimestamp(); const { shiftedStart, shiftedEnd } = shiftDate(startDate, endDate, shiftAmount); dispatch(updateShiftTimeInterval(new TimeInterval(shiftedStart, shiftedEnd))); } @@ -87,15 +83,11 @@ export default function CompareLineChartComponent() { // Update shift interval string based on shift interval or time interval React.useEffect(() => { + console.log('second effect is being called, and shift interval is', shiftInterval.getIsBounded()); if (shiftInterval.getIsBounded()) { setShiftIntervalStr(shiftInterval); - } else { - // If shift interval is not set, use the original time interval - if (timeInterval.getIsBounded()) { - setShiftIntervalStr(timeInterval); - } } - }, [shiftInterval, timeInterval]); + }, [shiftInterval]); // Getting the shifted data const { data: dataNew, isFetching: isFetchingNew } = graphState.threeD.meterOrGroup === MeterOrGroup.meters ? @@ -126,13 +118,14 @@ export default function CompareLineChartComponent() { // See https://community.plotly.com/t/replacing-an-empty-graph-with-a-message/31497 for showing text `not plot. if (!meterOrGroupID) { layout = setHelpLayout(translate('select.meter.group')); - } else if (!timeInterval.getIsBounded()) { + } else if (!timeIntervalStr.getIsBounded() || !shiftIntervalStr.getIsBounded()) { layout = setHelpLayout(translate('please.set.the.date.range')); } else if (!enoughData) { layout = setHelpLayout(translate('no.data.in.range')); } else { // Checks/warnings on received reading data - if (timeInterval.getIsBounded() && shiftInterval.getIsBounded()) { + if (shiftAmount !== ShiftAmount.none && !isFetching && !isFetchingNew) { + console.log('shift amount', shiftAmount); checkReceivedData(data[0], dataNew[0]); } layout = { @@ -178,7 +171,7 @@ export default function CompareLineChartComponent() { : { - // 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 }) - } /> } @@ -235,17 +208,26 @@ function checkReceivedData(originalData: any, shiftedData: any) { if (originalReading.length !== shiftedReading.length) { // If the number of points vary then then scales will not line up point by point. Warn the user. numberPointsSame = false; - showWarnNotification(`The original line has ${originalReading.length} readings but the shifted line has ${shiftedReading.length}` - + ' readings which means the points will not align horizontally.'); + showWarnNotification( + `The original line has ${originalReading.length} readings but the shifted line has ${shiftedReading.length}` + + ' readings which means the points will not align horizontally.' + ); } // Now see if the original and shifted lines overlap. if (moment(shiftedReading.at(-1).toString()) > moment(originalReading.at(0).toString())) { - showInfoNotification(`The shifted line overlaps the original line starting at ${originalReading[0]}`); + showInfoNotification( + `The shifted line overlaps the original line starting at ${originalReading[0]}`, + toast.POSITION.TOP_RIGHT, + 15000 + ); } // Now see if day of the week aligns. // If the number of points is not the same then no horizontal alignment so do not tell user. if (numberPointsSame && moment(originalReading.at(0)?.toString()).day() === moment(shiftedReading.at(0)?.toString()).day()) { - showInfoNotification('days of week align (unless missing readings)'); + showInfoNotification('Days of week align (unless missing readings)', + toast.POSITION.TOP_RIGHT, + 15000 + ); } // Now see if the month and day align. If the number of points is not the same then no horizontal // alignment so do not tell user. Check if the first reading matches because only notify if this is true. @@ -258,9 +240,10 @@ function checkReceivedData(originalData: any, shiftedData: any) { // Mismatch so inform user. Should be due to leap year crossing and differing leap year. // Only tell first mistmatch message += ` until original reading at date ${moment(originalReading.at(i)?.toString()).format('ll')}`; + break; } } - showInfoNotification(message); + showInfoNotification(message, toast.POSITION.TOP_RIGHT, 15000); } } diff --git a/src/client/app/components/CompareLineControlsComponent.tsx b/src/client/app/components/CompareLineControlsComponent.tsx index 8d86e065d..e280ce23d 100644 --- a/src/client/app/components/CompareLineControlsComponent.tsx +++ b/src/client/app/components/CompareLineControlsComponent.tsx @@ -92,6 +92,7 @@ export default function CompareLineControlsComponent() { onChange={e => handleShiftOptionChange(e.target.value)} > + @@ -127,6 +128,8 @@ export function shiftDate(originalStart: moment.Moment, originalEnd: moment.Mome if (shiftType === ShiftAmount.none) { shiftedStart = originalStart.clone(); + } else if (shiftType === ShiftAmount.day) { + shiftedStart = originalStart.clone().subtract(1, 'days'); } else if (shiftType === ShiftAmount.week) { shiftedStart = originalStart.clone().subtract(7, 'days'); } else if (shiftType === ShiftAmount.month) { diff --git a/src/client/app/components/ThreeDPillComponent.tsx b/src/client/app/components/ThreeDPillComponent.tsx index 599a92739..2d19df5a6 100644 --- a/src/client/app/components/ThreeDPillComponent.tsx +++ b/src/client/app/components/ThreeDPillComponent.tsx @@ -127,7 +127,7 @@ const pillContainer: React.CSSProperties = { justifyContent: 'space-between', margin: '0px', padding: '0px', - minHeight: '50px', + minHeight: '100px', maxHeight: '200px' }; diff --git a/src/client/app/translations/data.ts b/src/client/app/translations/data.ts index 830bb4fc6..8ac6d2e4f 100644 --- a/src/client/app/translations/data.ts +++ b/src/client/app/translations/data.ts @@ -7,6 +7,7 @@ // This file used to be a json file, but had issues with importing, so we declared the json variable in a js file instead. const LocaleTranslationData = { "en": { + "1.day": "1 day", "1.month": "1 month", "1.week": "1 week", "1.year": "1 year", @@ -540,6 +541,7 @@ const LocaleTranslationData = { "you.cannot.create.a.cyclic.group": "You cannot create a cyclic group" }, "fr": { + "1.day": "1 day\u{26A1}", "1.month": "1 month\u{26A1}", "1.week": "1 week\u{26A1}", "1.year": "1 year\u{26A1}", @@ -1073,6 +1075,7 @@ const LocaleTranslationData = { 'threeD.y.axis.label': 'Jours de l\'année calendaire', }, "es": { + "1.day": "1 day\u{26A1}", "1.month": "1 month\u{26A1}", "1.week": "1 week\u{26A1}", "1.year": "1 year\u{26A1}", diff --git a/src/client/app/types/redux/graph.ts b/src/client/app/types/redux/graph.ts index 8dd3e5572..043a61d65 100644 --- a/src/client/app/types/redux/graph.ts +++ b/src/client/app/types/redux/graph.ts @@ -57,6 +57,7 @@ export interface ThreeDState { } export enum ShiftAmount { + day = 'day', week = 'week', month = 'month', year = 'year', From eb46b1bae6634acfb397bd64ad942c93316e0cbb Mon Sep 17 00:00:00 2001 From: nmqng Date: Mon, 2 Dec 2024 09:43:46 -0500 Subject: [PATCH 16/21] refactor code for compare line UI --- .../components/CompareLineChartComponent.tsx | 38 +++----- .../CompareLineControlsComponent.tsx | 95 +------------------ 2 files changed, 16 insertions(+), 117 deletions(-) diff --git a/src/client/app/components/CompareLineChartComponent.tsx b/src/client/app/components/CompareLineChartComponent.tsx index b8b9826f5..2b1ff9fd5 100644 --- a/src/client/app/components/CompareLineChartComponent.tsx +++ b/src/client/app/components/CompareLineChartComponent.tsx @@ -38,9 +38,6 @@ export default function CompareLineChartComponent() { // getting the time interval of current data const timeInterval = graphState.queryTimeInterval; const shiftInterval = graphState.shiftTimeInterval; - // Storing the time interval strings for the original data and the shifted data to use for range in plot - const [timeIntervalStr, setTimeIntervalStr] = React.useState(TimeInterval.unbounded()); - const [shiftIntervalStr, setShiftIntervalStr] = React.useState(TimeInterval.unbounded()); // Layout for the plot let layout = {}; @@ -68,10 +65,8 @@ export default function CompareLineChartComponent() { // Update shifted interval based on current interval and shift amount React.useEffect(() => { - console.log('first effect is being called'); - if (timeInterval.getIsBounded()) { - setTimeIntervalStr(timeInterval); + // setTimeIntervalStr(timeInterval); if (shiftAmount !== ShiftAmount.none && shiftAmount !== ShiftAmount.custom) { const startDate = timeInterval.getStartTimestamp(); const endDate = timeInterval.getEndTimestamp(); @@ -81,14 +76,6 @@ export default function CompareLineChartComponent() { } }, [timeInterval, shiftAmount]); - // Update shift interval string based on shift interval or time interval - React.useEffect(() => { - console.log('second effect is being called, and shift interval is', shiftInterval.getIsBounded()); - if (shiftInterval.getIsBounded()) { - setShiftIntervalStr(shiftInterval); - } - }, [shiftInterval]); - // Getting the shifted data const { data: dataNew, isFetching: isFetchingNew } = graphState.threeD.meterOrGroup === MeterOrGroup.meters ? readingsApi.useLineQuery({ ...args, timeInterval: shiftInterval.toString() }, @@ -118,14 +105,16 @@ export default function CompareLineChartComponent() { // See https://community.plotly.com/t/replacing-an-empty-graph-with-a-message/31497 for showing text `not plot. if (!meterOrGroupID) { layout = setHelpLayout(translate('select.meter.group')); - } else if (!timeIntervalStr.getIsBounded() || !shiftIntervalStr.getIsBounded()) { + } else if (!timeInterval.getIsBounded() || !shiftInterval.getIsBounded()) { + console.log('shiftInterval', shiftInterval); layout = setHelpLayout(translate('please.set.the.date.range')); + } else if (shiftAmount === ShiftAmount.none) { + layout = setHelpLayout(translate('select.shift.amount')); } else if (!enoughData) { layout = setHelpLayout(translate('no.data.in.range')); } else { // Checks/warnings on received reading data - if (shiftAmount !== ShiftAmount.none && !isFetching && !isFetchingNew) { - console.log('shift amount', shiftAmount); + if (!isFetching && !isFetchingNew) { checkReceivedData(data[0], dataNew[0]); } layout = { @@ -135,8 +124,8 @@ export default function CompareLineChartComponent() { yaxis: { title: unitLabel, gridcolor: '#ddd', fixedrange: true }, xaxis: { // Set range for x-axis based on timeIntervalStr so that current data and shifted data is aligned - range: timeIntervalStr.getIsBounded() - ? [timeIntervalStr.getStartTimestamp(), timeIntervalStr.getEndTimestamp()] + range: timeInterval.getIsBounded() + ? [timeInterval.getStartTimestamp(), timeInterval.getEndTimestamp()] : undefined }, xaxis2: { @@ -145,8 +134,8 @@ export default function CompareLineChartComponent() { overlaying: 'x', side: 'top', // Set range for x-axis2 based on shiftIntervalStr so that current data and shifted data is aligned - range: shiftIntervalStr.getIsBounded() - ? [shiftIntervalStr.getStartTimestamp(), shiftIntervalStr.getEndTimestamp()] + range: shiftInterval.getIsBounded() + ? [shiftInterval.getStartTimestamp(), shiftInterval.getEndTimestamp()] : undefined } }; @@ -221,9 +210,12 @@ function checkReceivedData(originalData: any, shiftedData: any) { 15000 ); } + // Now see if day of the week aligns. // If the number of points is not the same then no horizontal alignment so do not tell user. - if (numberPointsSame && moment(originalReading.at(0)?.toString()).day() === moment(shiftedReading.at(0)?.toString()).day()) { + const firstOriginReadingDay = moment(originalReading.at(0)?.toString()); + const firstShiftedReadingDay = moment(shiftedReading.at(0)?.toString()); + if (numberPointsSame && firstOriginReadingDay.day() === firstShiftedReadingDay.day()) { showInfoNotification('Days of week align (unless missing readings)', toast.POSITION.TOP_RIGHT, 15000 @@ -231,7 +223,7 @@ function checkReceivedData(originalData: any, shiftedData: any) { } // Now see if the month and day align. If the number of points is not the same then no horizontal // alignment so do not tell user. Check if the first reading matches because only notify if this is true. - if (numberPointsSame && monthDateSame(moment(originalReading.at(0)?.toString()), moment(shiftedReading.at(0)?.toString()))) { + if (numberPointsSame && monthDateSame(firstOriginReadingDay, firstShiftedReadingDay)) { // Loop over all readings but the first. Really okay to do first but just checked that one. // Note length of original and shifted same so just use original. let message = 'The month and day of the month align for the original and shifted readings'; diff --git a/src/client/app/components/CompareLineControlsComponent.tsx b/src/client/app/components/CompareLineControlsComponent.tsx index e280ce23d..fd5956b68 100644 --- a/src/client/app/components/CompareLineControlsComponent.tsx +++ b/src/client/app/components/CompareLineControlsComponent.tsx @@ -145,97 +145,4 @@ export function shiftDate(originalStart: moment.Moment, originalEnd: moment.Mome const shiftedEnd = shiftedStart.clone().add(originalDateRange, 'days'); return { shiftedStart, shiftedEnd }; -} - -// /** -// * 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 shiftDate(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 checkLeapYear(startDate: moment.Moment, endDate: moment.Moment, shiftOption: ShiftAmount) { -// const { shiftedStart, shiftedEnd } = shiftDate(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 +} \ No newline at end of file From 1e2931fa260a5768be2edc38c0a01591aea874ca Mon Sep 17 00:00:00 2001 From: nmqng Date: Tue, 3 Dec 2024 23:23:54 -0500 Subject: [PATCH 17/21] refactor compare line chart and control components --- .../components/CompareLineChartComponent.tsx | 25 +++------------- .../CompareLineControlsComponent.tsx | 30 +++++-------------- 2 files changed, 12 insertions(+), 43 deletions(-) diff --git a/src/client/app/components/CompareLineChartComponent.tsx b/src/client/app/components/CompareLineChartComponent.tsx index 2b1ff9fd5..511252f5b 100644 --- a/src/client/app/components/CompareLineChartComponent.tsx +++ b/src/client/app/components/CompareLineChartComponent.tsx @@ -5,21 +5,19 @@ import * as moment from 'moment'; import * as React from 'react'; import Plot from 'react-plotly.js'; -import { TimeInterval } from '../../../common/TimeInterval'; import { readingsApi, stableEmptyLineReadings } from '../redux/api/readingsApi'; -import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; +import { 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, updateShiftTimeInterval } from '../redux/slices/graphSlice'; +import { selectGraphState, selectShiftAmount } 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 { shiftDate } from './CompareLineControlsComponent'; import { showInfoNotification, showWarnNotification } from '../utils/notifications'; import { setHelpLayout } from './ThreeDComponent'; import { toast } from 'react-toastify'; @@ -28,7 +26,6 @@ import { toast } from 'react-toastify'; * @returns plotlyLine graphic */ export default function CompareLineChartComponent() { - const dispatch = useAppDispatch(); const graphState = useAppSelector(selectGraphState); const meterOrGroupID = useAppSelector(selectThreeDComponentInfo).meterOrGroupID; const unitLabel = useAppSelector(selectLineUnitLabel); @@ -63,19 +60,6 @@ export default function CompareLineChartComponent() { }) }); - // Update shifted interval based on current interval and shift amount - React.useEffect(() => { - if (timeInterval.getIsBounded()) { - // setTimeIntervalStr(timeInterval); - if (shiftAmount !== ShiftAmount.none && shiftAmount !== ShiftAmount.custom) { - const startDate = timeInterval.getStartTimestamp(); - const endDate = timeInterval.getEndTimestamp(); - const { shiftedStart, shiftedEnd } = shiftDate(startDate, endDate, shiftAmount); - dispatch(updateShiftTimeInterval(new TimeInterval(shiftedStart, shiftedEnd))); - } - } - }, [timeInterval, shiftAmount]); - // Getting the shifted data const { data: dataNew, isFetching: isFetchingNew } = graphState.threeD.meterOrGroup === MeterOrGroup.meters ? readingsApi.useLineQuery({ ...args, timeInterval: shiftInterval.toString() }, @@ -106,15 +90,14 @@ export default function CompareLineChartComponent() { if (!meterOrGroupID) { layout = setHelpLayout(translate('select.meter.group')); } else if (!timeInterval.getIsBounded() || !shiftInterval.getIsBounded()) { - console.log('shiftInterval', shiftInterval); layout = setHelpLayout(translate('please.set.the.date.range')); } else if (shiftAmount === ShiftAmount.none) { layout = setHelpLayout(translate('select.shift.amount')); } else if (!enoughData) { layout = setHelpLayout(translate('no.data.in.range')); } else { - // Checks/warnings on received reading data if (!isFetching && !isFetchingNew) { + // Checks/warnings on received reading data checkReceivedData(data[0], dataNew[0]); } layout = { @@ -159,7 +142,7 @@ export default function CompareLineChartComponent() { ? : (shiftInterval); - // Add this useEffect to update the shift interval when the shift option changes + // Update the shift interval when the shift option changes React.useEffect(() => { - if (shiftOption !== ShiftAmount.custom) { - updateShiftInterval(shiftOption); + if (shiftOption !== ShiftAmount.custom && timeInterval.getIsBounded()) { + const { shiftedStart, shiftedEnd } = shiftDate(timeInterval.getStartTimestamp(), timeInterval.getEndTimestamp(), shiftOption); + const newInterval = new TimeInterval(shiftedStart, shiftedEnd); + dispatch(updateShiftTimeInterval(newInterval)); } }, [shiftOption, timeInterval]); @@ -55,23 +56,11 @@ export default function CompareLineControlsComponent() { const newShiftOption = value as ShiftAmount; setShiftOption(newShiftOption); dispatch(updateShiftAmount(newShiftOption)); - 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 } = shiftDate(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) => { + const handleCustomShiftDateChange = (value: Value) => { setCustomDateRange(dateRangeToTimeInterval(value)); dispatch(updateShiftTimeInterval(dateRangeToTimeInterval(value))); }; @@ -102,14 +91,13 @@ export default function CompareLineControlsComponent() { {shiftOption === ShiftAmount.custom && } -
); @@ -126,9 +114,7 @@ export default function CompareLineControlsComponent() { export function shiftDate(originalStart: moment.Moment, originalEnd: moment.Moment, shiftType: ShiftAmount) { let shiftedStart = originalStart.clone(); - if (shiftType === ShiftAmount.none) { - shiftedStart = originalStart.clone(); - } else if (shiftType === ShiftAmount.day) { + if (shiftType === ShiftAmount.day) { shiftedStart = originalStart.clone().subtract(1, 'days'); } else if (shiftType === ShiftAmount.week) { shiftedStart = originalStart.clone().subtract(7, 'days'); From 5f1eeeadebc41fe5dc78a23d3e577a4bb2a8a324 Mon Sep 17 00:00:00 2001 From: Steven Huss-Lederman Date: Fri, 6 Dec 2024 15:19:58 -0600 Subject: [PATCH 18/21] remove extra es key --- src/client/app/translations/data.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/client/app/translations/data.ts b/src/client/app/translations/data.ts index 8ac6d2e4f..b9bc6c76e 100644 --- a/src/client/app/translations/data.ts +++ b/src/client/app/translations/data.ts @@ -1209,7 +1209,6 @@ const LocaleTranslationData = { "default.meter.minimum.value": "Revisión del valor de lectura mínima del medidor predeterminado", "default.meter.reading.frequency": "Frecuencia de lectura del medidor predeterminada", "default.meter.reading.gap": "Deistancia predeterminada entre lecturas del medidor", - "default.site.title": "Título predeterminado de la página ", "default.time.zone": "Zona de horario predeterminada", "default.warning.file.size": "Advertencia predeterminada de tamaño del archivo", "defaultGraphicUnit": "Unidad del gráfico predeterminada:", From 3ea033c34311e2d5fbb46aad630ea76915a80d8c Mon Sep 17 00:00:00 2001 From: nmqng Date: Sat, 7 Dec 2024 13:43:36 -0500 Subject: [PATCH 19/21] resolved PR comments and refactor CompareLineControlComponent.tsx --- .../components/CompareLineChartComponent.tsx | 10 ++-- .../CompareLineControlsComponent.tsx | 53 +++++++++++-------- src/client/app/types/redux/graph.ts | 4 +- 3 files changed, 37 insertions(+), 30 deletions(-) diff --git a/src/client/app/components/CompareLineChartComponent.tsx b/src/client/app/components/CompareLineChartComponent.tsx index 511252f5b..5daa924e4 100644 --- a/src/client/app/components/CompareLineChartComponent.tsx +++ b/src/client/app/components/CompareLineChartComponent.tsx @@ -98,7 +98,7 @@ export default function CompareLineChartComponent() { } else { if (!isFetching && !isFetchingNew) { // Checks/warnings on received reading data - checkReceivedData(data[0], dataNew[0]); + checkReceivedData(data[0].x, dataNew[0].x); } layout = { autosize: true, showlegend: true, @@ -170,12 +170,10 @@ export default function CompareLineChartComponent() { * If not, it is unlikely but can happen if there are missing readings in both lines that do not align but there are the same number missing in both. * This is an ugly edge case that OED is not going to try to catch now. * Use the last index in Redux state as a proxy for the number since need that below. - * @param originalData original data to compare - * @param shiftedData shifted data to compare + * @param originalReading original data to compare + * @param shiftedReading shifted data to compare */ -function checkReceivedData(originalData: any, shiftedData: any) { - const originalReading = originalData.x; - const shiftedReading = shiftedData.x; +function checkReceivedData(originalReading: any, shiftedReading: any) { let numberPointsSame = true; if (originalReading.length !== shiftedReading.length) { // If the number of points vary then then scales will not line up point by point. Warn the user. diff --git a/src/client/app/components/CompareLineControlsComponent.tsx b/src/client/app/components/CompareLineControlsComponent.tsx index 0edc461b9..607654e55 100644 --- a/src/client/app/components/CompareLineControlsComponent.tsx +++ b/src/client/app/components/CompareLineControlsComponent.tsx @@ -17,6 +17,7 @@ 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 TooltipMarkerComponent from './TooltipMarkerComponent'; /** * @returns compare line control component for compare line graph page @@ -27,34 +28,36 @@ export default function CompareLineControlsComponent() { const timeInterval = useAppSelector(selectQueryTimeInterval); const locale = useAppSelector(selectSelectedLanguage); const shiftInterval = useAppSelector(selectShiftTimeInterval); - - // Hold value of shifting option (week, month, year, or custom) - const [shiftOption, setShiftOption] = React.useState(shiftAmount); // Hold value to store the custom date range for the shift interval const [customDateRange, setCustomDateRange] = React.useState(shiftInterval); + // Translation for shift amount + const shiftAmountTranslations: Record = { + none: 'select.shift.amount', + day: '1.day', + week: '1.week', + month: '1.month', + year: '1.year', + custom: 'custom.date.range' + }; + // Update the shift interval when the shift option changes React.useEffect(() => { - if (shiftOption !== ShiftAmount.custom && timeInterval.getIsBounded()) { - const { shiftedStart, shiftedEnd } = shiftDate(timeInterval.getStartTimestamp(), timeInterval.getEndTimestamp(), shiftOption); + if (shiftAmount !== ShiftAmount.custom && timeInterval.getIsBounded()) { + const { shiftedStart, shiftedEnd } = shiftDate(timeInterval.getStartTimestamp(), timeInterval.getEndTimestamp(), shiftAmount); const newInterval = new TimeInterval(shiftedStart, shiftedEnd); dispatch(updateShiftTimeInterval(newInterval)); + // set the custom date range to the new interval + setCustomDateRange(newInterval); } - }, [shiftOption, timeInterval]); - - // Update custom date range value when shift interval changes - React.useEffect(() => { - setCustomDateRange(shiftInterval); - }, [shiftInterval]); + }, [shiftAmount, 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)); } else { const newShiftOption = value as ShiftAmount; - setShiftOption(newShiftOption); dispatch(updateShiftAmount(newShiftOption)); } }; @@ -70,25 +73,31 @@ export default function CompareLineControlsComponent() {

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

handleShiftOptionChange(e.target.value)} > - - - - - - + {Object.entries(ShiftAmount).map( + ([key, value]) => ( + + ) + )} {/* Show date picker when custom date range is selected */} - {shiftOption === ShiftAmount.custom && + {shiftAmount === ShiftAmount.custom && Date: Tue, 10 Dec 2024 20:31:29 -0500 Subject: [PATCH 20/21] remove duplicate properties w same name in data.ts --- src/client/app/translations/data.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/client/app/translations/data.ts b/src/client/app/translations/data.ts index 96e354669..6a84c1256 100644 --- a/src/client/app/translations/data.ts +++ b/src/client/app/translations/data.ts @@ -513,9 +513,6 @@ const LocaleTranslationData = { "UnitType.suffix": "suffix", "UnitType.unit": "unit", "units.conversion.page.title": "Units and Conversions Visual Graphics", - "UnitType.meter": "meter", - "UnitType.suffix": "suffix", - "UnitType.unit": "unit", "unsaved.failure": "Changes failed to save", "unsaved.success": "Changes saved", "unsaved.warning": "You have unsaved change(s). Are you sure you want to leave?", @@ -1060,9 +1057,6 @@ const LocaleTranslationData = { "UnitType.suffix": "suffix\u{26A1}", "UnitType.unit": "unit\u{26A1}", "units.conversion.page.title": "Units and Conversions Visual Graphics\u{26A1}", - "UnitType.meter": "meter\u{26A1}", - "UnitType.suffix": "suffix\u{26A1}", - "UnitType.unit": "unit\u{26A1}", "unsaved.failure": "Changes failed to save\u{26A1}", "unsaved.success": "Changes saved\u{26A1}", "unsaved.warning": "You have unsaved change(s). Are you sure you want to leave?\u{26A1}", @@ -1614,9 +1608,6 @@ const LocaleTranslationData = { "UnitType.suffix": "sufijo", "UnitType.unit": "unidad", "units.conversion.page.title": "Gráficos Visuales de Unidades y Conversiones", - "UnitType.meter": "medidor", - "UnitType.suffix": "sufijo", - "UnitType.unit": "unidad", "unsaved.failure": "No se pudieron guardar los cambios", "unsaved.success": "Se guardaron los cambios", "unsaved.warning": "Tienes cambios sin guardar. ¿Estás seguro que quieres salir?", From fc282930144f6152ea109c0d01172aa5effd746d Mon Sep 17 00:00:00 2001 From: Steven Huss-Lederman Date: Mon, 3 Feb 2025 10:24:32 -0600 Subject: [PATCH 21/21] check for shift to skip getting data This helps reduce data draws but does not fix the rerender issue. --- src/client/app/redux/selectors/chartQuerySelectors.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/client/app/redux/selectors/chartQuerySelectors.ts b/src/client/app/redux/selectors/chartQuerySelectors.ts index b9e53bcff..1cb3ceda5 100644 --- a/src/client/app/redux/selectors/chartQuerySelectors.ts +++ b/src/client/app/redux/selectors/chartQuerySelectors.ts @@ -11,7 +11,8 @@ import { selectWidthDays, selectComparePeriod, selectCompareTimeInterval, selectQueryTimeInterval, selectSelectedGroups, selectSelectedMeters, - selectSelectedUnit, selectThreeDState + selectSelectedUnit, selectThreeDState, + selectShiftAmount } from '../slices/graphSlice'; import { omit } from 'lodash'; import { selectLineChartDeps } from './lineChartSelectors'; @@ -88,7 +89,8 @@ export const selectCompareLineQueryArgs = createSelector( selectSelectedUnit, selectThreeDState, selectLineChartDeps, - (queryTimeInterval, selectedUnit, threeD, lineChartDeps) => { + selectShiftAmount, + (queryTimeInterval, selectedUnit, threeD, lineChartDeps, ShiftAmount) => { const args: CompareLineReadingApiArgs = threeD.meterOrGroup === MeterOrGroup.meters ? { @@ -103,7 +105,7 @@ export const selectCompareLineQueryArgs = createSelector( graphicUnitId: selectedUnit, meterOrGroup: threeD.meterOrGroup! }; - const shouldSkipQuery = !threeD.meterOrGroupID || !queryTimeInterval.getIsBounded(); + const shouldSkipQuery = !threeD.meterOrGroupID || !queryTimeInterval.getIsBounded() || ShiftAmount == 'none'; const argsDeps = threeD.meterOrGroup === MeterOrGroup.meters ? lineChartDeps.meterDeps : lineChartDeps.groupDeps; return { args, shouldSkipQuery, argsDeps }; }