Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Compare line page #1351

Open
wants to merge 25 commits into
base: development
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
8074066
Compare Line--color of original graph for custom is not working properly
nmqng Aug 17, 2024
0bc7873
Fix custom shift date interval causing wrong data line
nmqng Sep 6, 2024
180fe83
fix compare line page custom date range stay up-to-date with shift da…
nmqng Sep 21, 2024
3fbb673
add notification when origin/shifted date range crosses a leap year
nmqng Sep 23, 2024
eeef793
change error noti to warn noti, translation key and wording
nmqng Sep 25, 2024
793df8e
add warning msg when new meter/group/interval selected and rewrite ch…
nmqng Oct 2, 2024
0a10a6b
Merge branch 'OpenEnergyDashboard:development' into compareLinePage
nmqng Oct 3, 2024
750bad1
fix syntax, code style based on run check
nmqng Oct 3, 2024
f6a94a3
add license header for CompareLineControlComponent.tsx
nmqng Oct 3, 2024
c753331
Merge branch 'OpenEnergyDashboard:development' into compareLinePage
nmqng Oct 3, 2024
42afcf3
Merge branch 'development' into compareLinePage
nmqng Oct 9, 2024
865a0e3
fix merge of development issue
nmqng Oct 14, 2024
58af2cf
fix some comments
nmqng Nov 22, 2024
8a382c8
resolved PR comments
nmqng Nov 28, 2024
b3ce0a0
resolved PR comment about alphabetical order in data.ts
nmqng Nov 28, 2024
501ec19
Merge branch 'development' into compareLinePage
nmqng Nov 28, 2024
ebc9d31
raw update on shiftDate(...) and checking reading data based on updat…
nmqng Nov 29, 2024
c3fec24
resolved PR comments
nmqng Nov 29, 2024
78bcbc2
resolved PR comments
nmqng Dec 2, 2024
eb46b1b
refactor code for compare line UI
nmqng Dec 2, 2024
1e2931f
refactor compare line chart and control components
nmqng Dec 4, 2024
5f1eeea
remove extra es key
huss Dec 6, 2024
3ea033c
resolved PR comments and refactor CompareLineControlComponent.tsx
nmqng Dec 7, 2024
3caaedf
Merge branch 'development' into compareLinePage
nmqng Dec 11, 2024
affccd2
remove duplicate properties w same name in data.ts
nmqng Dec 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
216 changes: 216 additions & 0 deletions src/client/app/components/CompareLineChartComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import { debounce } from 'lodash';
import { utc } from 'moment';
import * as React from 'react';
import Plot from 'react-plotly.js';
import { TimeInterval } from '../../../common/TimeInterval';
import { updateSliderRange } from '../redux/actions/extraActions';
import { readingsApi, stableEmptyLineReadings } from '../redux/api/readingsApi';
import { useAppDispatch, useAppSelector } from '../redux/reduxHooks';
import { selectCompareLineQueryArgs } from '../redux/selectors/chartQuerySelectors';
import { selectLineUnitLabel } from '../redux/selectors/plotlyDataSelectors';
import { selectSelectedLanguage } from '../redux/slices/appStateSlice';
import Locales from '../types/locales';
import translate from '../utils/translate';
import SpinnerComponent from './SpinnerComponent';
import { selectGraphState, selectShiftAmount, selectShiftTimeInterval, updateShiftTimeInterval } from '../redux/slices/graphSlice';
import ThreeDPillComponent from './ThreeDPillComponent';
import { selectThreeDComponentInfo } from '../redux/selectors/threeDSelectors';
import { selectPlotlyGroupData, selectPlotlyMeterData } from '../redux/selectors/lineChartSelectors';
import { MeterOrGroup, ShiftAmount } from '../types/redux/graph';
import { PlotRelayoutEvent } from 'plotly.js';
import { shiftDateFunc } from './CompareLineControlsComponent';
/**
* @returns plotlyLine graphic
*/
export default function CompareLineChartComponent() {
const dispatch = useAppDispatch();
const graphState = useAppSelector(selectGraphState);
const meterOrGroupID = useAppSelector(selectThreeDComponentInfo).meterOrGroupID;
const unitLabel = useAppSelector(selectLineUnitLabel);
const locale = useAppSelector(selectSelectedLanguage);
const shiftInterval = useAppSelector(selectShiftTimeInterval);
const shiftAmount = useAppSelector(selectShiftAmount);
const { args, shouldSkipQuery, argsDeps } = useAppSelector(selectCompareLineQueryArgs);

// getting the time interval of current data
const timeInterval = graphState.queryTimeInterval;

// Storing the time interval strings for the original data and the shifted data to use for range in plot
const [timeIntervalStr, setTimeIntervalStr] = React.useState(['', '']);
nmqng marked this conversation as resolved.
Show resolved Hide resolved
const [shiftIntervalStr, setShiftIntervalStr] = React.useState(['', '']);

// Fetch original data, and derive plotly points
const { data, isFetching } = graphState.threeD.meterOrGroup === MeterOrGroup.meters ?
readingsApi.useLineQuery(args,
{
skip: shouldSkipQuery,
selectFromResult: ({ data, ...rest }) => ({
...rest,
data: selectPlotlyMeterData(data ?? stableEmptyLineReadings,
{ ...argsDeps, compatibleEntities: [meterOrGroupID!] })
})
})
:
readingsApi.useLineQuery(args,
{
skip: shouldSkipQuery,
selectFromResult: ({ data, ...rest }) => ({
...rest,
data: selectPlotlyGroupData(data ?? stableEmptyLineReadings,
{ ...argsDeps, compatibleEntities: [meterOrGroupID!] })
})
});

// Callback function to update the shifted interval based on current interval and shift amount
const updateShiftedInterval = React.useCallback((start: moment.Moment, end: moment.Moment, shift: ShiftAmount) => {
nmqng marked this conversation as resolved.
Show resolved Hide resolved
const { shiftedStart, shiftedEnd } = shiftDateFunc(start, end, shift);
const newShiftedInterval = new TimeInterval(shiftedStart, shiftedEnd);
dispatch(updateShiftTimeInterval(newShiftedInterval));
}, []);

// Update shifted interval based on current interval and shift amount
React.useEffect(() => {
const startDate = timeInterval.getStartTimestamp();
const endDate = timeInterval.getEndTimestamp();

if (startDate && endDate) {
setTimeIntervalStr([startDate.toISOString(), endDate.toISOString()]);

if (shiftAmount !== ShiftAmount.none && shiftAmount !== ShiftAmount.custom) {
updateShiftedInterval(startDate, endDate, shiftAmount);
}
}
}, [timeInterval, shiftAmount, updateShiftedInterval]);
nmqng marked this conversation as resolved.
Show resolved Hide resolved

// Update shift interval string based on shift interval or time interval
React.useEffect(() => {
const shiftStart = shiftInterval.getStartTimestamp();
const shiftEnd = shiftInterval.getEndTimestamp();

if (shiftStart && shiftEnd) {
setShiftIntervalStr([shiftStart.toISOString(), shiftEnd.toISOString()]);
} else {
// If shift interval is not set, use the original time interval
const startDate = timeInterval.getStartTimestamp();
const endDate = timeInterval.getEndTimestamp();
if (startDate && endDate) {
setShiftIntervalStr([startDate.toISOString(), endDate.toISOString()]);
}
}
}, [shiftInterval, timeInterval]);

// Getting the shifted data
const { data: dataNew, isFetching: isFetchingNew } = graphState.threeD.meterOrGroup === MeterOrGroup.meters ?
readingsApi.useLineQuery({ ...args, timeInterval: shiftInterval.toString() },
{
skip: shouldSkipQuery,
selectFromResult: ({ data, ...rest }) => ({
...rest,
data: selectPlotlyMeterData(data ?? stableEmptyLineReadings,
{ ...argsDeps, compatibleEntities: [meterOrGroupID!] })
})
})
:
readingsApi.useLineQuery({ ...args, timeInterval: shiftInterval.toString() },
{
skip: shouldSkipQuery,
selectFromResult: ({ data, ...rest }) => ({
...rest,
data: selectPlotlyGroupData(data ?? stableEmptyLineReadings,
{ ...argsDeps, compatibleEntities: [meterOrGroupID!] })
})
});

if (isFetching || isFetchingNew) {
return <SpinnerComponent loading height={50} width={50} />;
}

// Check if there is at least one valid graph for current data and shifted data
const enoughData = data.find(data => data.x!.length > 1);
nmqng marked this conversation as resolved.
Show resolved Hide resolved
const enoughNewData = dataNew.find(dataNew => dataNew.x!.length > 1);

// Customize the layout of the plot
// See https://community.plotly.com/t/replacing-an-empty-graph-with-a-message/31497 for showing text `not plot.
if (!graphState.threeD.meterOrGroup && (data.length === 0 || dataNew.length === 0)) {
nmqng marked this conversation as resolved.
Show resolved Hide resolved
return <><ThreeDPillComponent /><h1>{`${translate('select.meter.group')}`}</h1></>;
nmqng marked this conversation as resolved.
Show resolved Hide resolved
} else if (!enoughData || !enoughNewData) {
return <><ThreeDPillComponent /><h1>{`${translate('no.data.in.range')}`}</h1></>;
} else if (!timeInterval.getIsBounded()) {
nmqng marked this conversation as resolved.
Show resolved Hide resolved
return <><ThreeDPillComponent /><h1>{`${translate('please.set.the.date.range')}`}</h1></>;
} else {
// adding information to the shifted data so that it can be plotted on the same graph with current data
const updateDataNew = dataNew.map(item => ({
...item,
name: 'Shifted ' + item.name,
line: { ...item.line, color: '#1AA5F0' },
xaxis: 'x2',
text: Array.isArray(item.text)
? item.text.map(text => text.replace('<br>', '<br>Shifted '))
: item.text?.replace('<br>', '<br>Shifted ')
}));

return (
<>
<ThreeDPillComponent />
<Plot
// only plot shifted data if the shiftAmount has been chosen
data={shiftAmount === ShiftAmount.none ? [...data] : [...data, ...updateDataNew]}
style={{ width: '100%', height: '100%', minHeight: '750px' }}
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: {
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={{
responsive: true,
displayModeBar: false,
// Current Locale
locale,
// Available Locales
locales: Locales
}}
onRelayout={debounce(
(e: PlotRelayoutEvent) => {
// 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 })
}
/>
</>

);

}
}
Loading
Loading