Skip to content

Commit 589f9b4

Browse files
Merge branch 'development' into pr-update
2 parents 28c2cd9 + 33aa0a5 commit 589f9b4

File tree

112 files changed

+6587
-1614
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

112 files changed

+6587
-1614
lines changed

.gitattributes

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@
1414
*.pdf diff=astextplain
1515
*.PDF diff=astextplain
1616
*.rtf diff=astextplain
17-
*.RTF diff=astextplain
17+
*.RTF diff=astextplain
18+
* text=auto eol=lf

.vscode/launch.json

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,44 @@
66
"type": "node",
77
"request": "attach",
88
"port": 9229,
9-
"address": "localhost",
10-
"localRoot": "${workspaceFolder}",
119
"remoteRoot": "/usr/src/app",
12-
"protocol": "inspector",
13-
"restart": true
10+
"sourceMaps": true
11+
},
12+
{
13+
"type": "chrome",
14+
"request": "launch",
15+
"name": "Launch Chrome against localhost",
16+
"url": "http://localhost:3000",
17+
"webRoot": "${workspaceFolder}"
18+
},
19+
{
20+
"type": "firefox",
21+
"request": "launch",
22+
"name": "Launch Firefox against localhost",
23+
"url": "http://localhost:3000",
24+
"webRoot": "${workspaceFolder}",
25+
"pathMappings": [
26+
{
27+
"url": "webpack://open-energy-dashboard/src",
28+
"path": "${workspaceFolder}/src"
29+
}
30+
]
31+
}
32+
],
33+
"compounds": [
34+
{
35+
"name": "Debug Server and Client (Chrome)",
36+
"configurations": [
37+
"Attach to Node in Docker",
38+
"Launch Chrome against localhost"
39+
]
40+
},
41+
{
42+
"name": "Debug Server and Client (Firefox)",
43+
"configurations": [
44+
"Attach to Node in Docker",
45+
"Launch Firefox against localhost"
46+
]
1447
}
1548
]
1649
}

cypress.config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ export default defineConfig({
88
e2e: {
99
setupNodeEvents(on, config) {
1010
// implement node event listeners here
11+
on("task", {
12+
log(args) {
13+
console.log(...args);
14+
return null;
15+
}
16+
});
1117
},
1218
specPattern: 'src/cypress/e2e/*.cy.ts',
1319
supportFile: 'src/cypress/support/e2e.ts',

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"repository": "https://github.com/OpenEnergyDashboard/OED",
77
"scripts": {
88
"start": "node ./src/bin/www",
9-
"start:dev": "nodemon --legacy-watch --inspect=0.0.0.0 ./src/bin/www",
9+
"start:dev": "nodemon --legacy-watch --inspect=0.0.0.0:9229 ./src/bin/www",
1010
"checkWebsiteStatus": "node src/server/services/checkWebsiteStatus.js",
1111
"webpack:dev": "webpack watch --color --progress --mode development",
1212
"webpack:build": "webpack build --node-env production",
@@ -137,4 +137,4 @@
137137
"webpack": "~5.76.0",
138138
"webpack-cli": "~5.1.4"
139139
}
140-
}
140+
}

src/client/app/components/BarChartComponent.tsx

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { selectPlotlyBarDataFromResult, selectPlotlyBarDeps } from '../redux/sel
1616
import { selectBarChartQueryArgs } from '../redux/selectors/chartQuerySelectors';
1717
import { selectBarUnitLabel, selectIsRaw } from '../redux/selectors/plotlyDataSelectors';
1818
import { selectSelectedLanguage } from '../redux/slices/appStateSlice';
19-
import { selectSliderRangeInterval ,selectBarStacking } from '../redux/slices/graphSlice';
19+
import { selectSliderRangeInterval ,selectBarStacking, setInitialXAxisRange } from '../redux/slices/graphSlice';
2020
import Locales from '../types/locales';
2121
import SpinnerComponent from './SpinnerComponent';
2222
import { useTranslate } from '../redux/componentHooks';
@@ -67,6 +67,33 @@ export default function BarChartComponent() {
6767
// useQueryHooks for data fetching
6868
const datasets: Partial<Plotly.PlotData>[] = meterReadings.concat(groupData);
6969

70+
// Getting the entire x-axis range from all traces
71+
// This is used to set the initial x-axis range when the component mounts.
72+
// It ensures that the graph starts with a range that covers all data points. That would be used for querying the data.
73+
// If there are no data points, minX and maxX will be undefined.
74+
const allX = React.useMemo(
75+
() =>
76+
datasets.flatMap(trace => {
77+
if (!trace.x) return [];
78+
// If trace.x is an array of arrays, flatten it
79+
if (Array.isArray(trace.x[0])) {
80+
return (trace.x as any[][]).flat();
81+
}
82+
// Otherwise, it's a flat array
83+
return trace.x as (string | number | Date)[];
84+
}),
85+
[datasets]
86+
);
87+
88+
const minX = allX.length ? utc(allX.reduce((a, b) => utc(a).isBefore(utc(b)) ? a : b)) : undefined;
89+
const maxX = allX.length ? utc(allX.reduce((a, b) => utc(a).isAfter(utc(b)) ? a : b)) : undefined;
90+
91+
React.useEffect(() => {
92+
if (minX && maxX) {
93+
dispatch(setInitialXAxisRange(new TimeInterval(minX, maxX)));
94+
}
95+
}, [minX, maxX]);
96+
7097
if (meterIsFetching || groupIsFetching) {
7198
return <SpinnerComponent loading height={50} width={50} />;
7299
}

src/client/app/components/ChartDataSelectComponent.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44

55
import * as React from 'react';
6-
import { ChartTypes, MeterOrGroup } from '../types/redux/graph';
6+
import { ChartTypes } from '../types/redux/graph';
77
import MeterAndGroupSelectComponent from './MeterAndGroupSelectComponent';
88
import UnitSelectComponent from './UnitSelectComponent';
99
import { useAppSelector } from '../redux/reduxHooks';
10-
import { selectChartToRender, selectQueryTimeInterval} from '../redux/slices/graphSlice';
10+
import { selectChartToRender, selectQueryTimeInterval } from '../redux/slices/graphSlice';
1111
import DateRangeComponent from './DateRangeComponent';
1212

1313
/**
@@ -17,14 +17,15 @@ import DateRangeComponent from './DateRangeComponent';
1717
export default function ChartDataSelectComponent() {
1818
const chartToRender = useAppSelector(selectChartToRender);
1919
const queryTimeInterval = useAppSelector(selectQueryTimeInterval);
20+
const isHalfBounded = queryTimeInterval.getIsHalfBounded();
2021
const isBounded = queryTimeInterval.getIsBounded();
22+
const visibleDateRange = isHalfBounded || isBounded;
2123

2224
return (
2325
<div>
24-
<MeterAndGroupSelectComponent meterOrGroup={MeterOrGroup.groups} />
25-
<MeterAndGroupSelectComponent meterOrGroup={MeterOrGroup.meters} />
26+
<MeterAndGroupSelectComponent />
2627
<UnitSelectComponent />
27-
{(isBounded && chartToRender !== ChartTypes.threeD && chartToRender !== ChartTypes.compareLine) && <DateRangeComponent />}
28+
{(visibleDateRange && chartToRender !== ChartTypes.threeD && chartToRender !== ChartTypes.compareLine) && <DateRangeComponent />}
2829
</div>
2930
);
3031
}

src/client/app/containers/CompareChartContainer.ts renamed to src/client/app/components/CompareBarComponent.tsx

Lines changed: 49 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,26 @@
1-
/*
2-
* This Source Code Form is subject to the terms of the Mozilla Public
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
32
* License, v. 2.0. If a copy of the MPL was not distributed with this
43
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
54
*/
65

7-
import { connect } from 'react-redux';
8-
import { getComparePeriodLabels, getCompareChangeSummary, calculateCompareShift } from '../utils/calculateCompare';
9-
// import { useTranslate } from 'redux/componentHooks';
10-
// import * as React from 'react'; Convert from containers to components
11-
// import { useState } from 'react';
12-
// When this container gets converted to component,migrate to useTranslate() from componentHooks.ts
13-
import translate from '../utils/translate';
6+
import * as React from 'react';
147
import Plot from 'react-plotly.js';
15-
// import { Icons } from 'plotly.js';
16-
import Locales from '../types/locales';
178
import * as moment from 'moment';
18-
import { UnitRepresentType } from '../types/redux/units';
19-
import { getAreaUnitConversion } from '../utils/getAreaUnitConversion';
20-
import { selectUnitDataById } from '../redux/api/unitsApi';
21-
import { RootState } from '../store';
22-
import { selectGroupDataById } from '../redux/api/groupsApi';
23-
import { selectMeterDataById } from '../redux/api/metersApi';
9+
import { useAppSelector } from '../redux/reduxHooks';
2410
import {
2511
selectAreaUnit, selectComparePeriod,
2612
selectCompareTimeInterval, selectGraphAreaNormalization,
2713
selectSelectedUnit
2814
} from '../redux/slices/graphSlice';
2915
import { selectSelectedLanguage } from '../redux/slices/appStateSlice';
16+
import Locales from '../types/locales';
17+
import { getComparePeriodLabels, getCompareChangeSummary, calculateCompareShift } from '../utils/calculateCompare';
18+
import { getAreaUnitConversion } from '../utils/getAreaUnitConversion';
19+
import { UnitRepresentType } from '../types/redux/units';
20+
import { selectUnitDataById } from '../redux/api/unitsApi';
21+
import { selectMeterDataById } from '../redux/api/metersApi';
22+
import { selectGroupDataById } from '../redux/api/groupsApi';
23+
import { useTranslate } from '../redux/componentHooks';
3024

3125
export interface CompareEntity {
3226
id: number;
@@ -39,32 +33,24 @@ export interface CompareEntity {
3933
prevTotalUsage?: number;
4034
}
4135

42-
interface CompareChartContainerProps {
36+
interface CompareBarComponentProps {
4337
entity: CompareEntity;
4438
}
4539

46-
/**
47-
* Passes the current redux state of the of the chart container and it's props, and turns it into props for the React
48-
* component, which is what will be visible on the page. Makes it possible to access
49-
* your reducer state objects from within your React components.
50-
* @param state The redux state
51-
* @param ownProps Chart container props
52-
* @returns The props object
53-
*/
54-
function mapStateToProps(state: RootState, ownProps: CompareChartContainerProps): any {
55-
const comparePeriod = selectComparePeriod(state);
56-
const compareTimeInterval = selectCompareTimeInterval(state);
57-
const datasets: any[] = [];
40+
const CompareBarComponent: React.FC<CompareBarComponentProps> = ({ entity }) => {
41+
const comparePeriod = useAppSelector(selectComparePeriod);
42+
const compareTimeInterval = useAppSelector(selectCompareTimeInterval);
5843
const periodLabels = getComparePeriodLabels(comparePeriod);
59-
// The unit label depends on the unit which is in selectUnit state.
60-
// Also need to determine if raw.
61-
const graphingUnit = selectSelectedUnit(state);
62-
// This container is not called if there is no data of there are not units so this is safe.
63-
const unitDataById = selectUnitDataById(state);
64-
const meterDataById = selectMeterDataById(state);
65-
const groupDataById = selectGroupDataById(state);
44+
45+
const datasets: any[] = [];
46+
const graphingUnit = useAppSelector(selectSelectedUnit);
47+
const unitDataById = useAppSelector(selectUnitDataById);
48+
const meterDataById = useAppSelector(selectMeterDataById);
49+
const groupDataById = useAppSelector(selectGroupDataById);
6650
const selectUnitState = unitDataById[graphingUnit];
67-
const locale = selectSelectedLanguage(state);
51+
const locale = useAppSelector(selectSelectedLanguage);
52+
53+
const translate = useTranslate();
6854
let unitLabel: string = '';
6955
// If graphingUnit is -99 then none selected and nothing to graph so label is empty.
7056
// This will probably happen when the page is first loaded.
@@ -89,17 +75,16 @@ function mapStateToProps(state: RootState, ownProps: CompareChartContainerProps)
8975
}
9076
}
9177

92-
/* TODO When I click this icon it crashes OED. The error relates to using a Hook (useState, I think)
93-
outside a component. This does not use a component as the other graphics do as it is
94-
a container. It either needs a modified solution or the component needs to be converted.
95-
Only after the component has been converted uncomment the code below and in plotly config
78+
// Unlike line, bar, etc., testing found that the only two buttons that can be shown
79+
// (outside download plot and plotly info) is box select & lasso select. Neither is
80+
// desired so this lists the default ones to remove (those two) but does not use state
81+
// to change it dynamically.
82+
// Note: Since this is a 2D graphic and the regular bar graphic has more buttons it is
83+
// unclear why it only has these two buttons.
84+
// This website was consulted: https://plotly.com/javascript/configuration-options/#remove-modebar-buttons
85+
// on the expected buttons.
9686
// Display Plotly Buttons Feature:
97-
// The number of items in defaultButtons and advancedButtons must differ as discussed below */
98-
const defaultButtons: Plotly.ModeBarDefaultButtons[] = ['zoom2d', 'pan2d', 'select2d', 'lasso2d', 'zoomIn2d', 'zoomOut2d', 'autoScale2d',
99-
'resetScale2d'];
100-
/* const advancedButtons: Plotly.ModeBarDefaultButtons[] = ['select2d', 'lasso2d', 'autoScale2d', 'resetScale2d'];
101-
// Manage button states with useState
102-
const [listOfButtons, setListOfButtons] = useState(defaultButtons); */
87+
const defaultButtons: Plotly.ModeBarDefaultButtons[] = ['select2d', 'lasso2d'];
10388

10489
// Get the time shift for this comparison as a moment duration
10590
const compareShift = calculateCompareShift(comparePeriod);
@@ -142,20 +127,19 @@ function mapStateToProps(state: RootState, ownProps: CompareChartContainerProps)
142127
};
143128

144129
// Compose the text to display to the user.
145-
const entity = ownProps.entity;
146130
const changeSummary = getCompareChangeSummary(entity.change, entity.identifier, periodLabels);
147131

148132
const barColor = 'rgba(218, 165, 32, 1)';
149133

150134
let previousPeriod = entity.prevUsage;
151135
let currentPeriod = entity.currUsage;
152-
const areaNormalization = selectGraphAreaNormalization(state);
136+
const areaNormalization = useAppSelector(selectGraphAreaNormalization);
153137
// Check if there is data to graph.
154138
if (previousPeriod !== null && currentPeriod !== null) {
155139
if (areaNormalization) {
156140
const area = entity.isGroup ? groupDataById[entity.id].area : meterDataById[entity.id].area;
157141
const areaUnit = entity.isGroup ? groupDataById[entity.id].areaUnit : meterDataById[entity.id].areaUnit;
158-
const normalization = area * getAreaUnitConversion(areaUnit, selectAreaUnit(state));
142+
const normalization = area * getAreaUnitConversion(areaUnit, useAppSelector(selectAreaUnit));
159143
previousPeriod /= normalization;
160144
currentPeriod /= normalization;
161145
}
@@ -241,31 +225,18 @@ function mapStateToProps(state: RootState, ownProps: CompareChartContainerProps)
241225
};
242226
}
243227

244-
// Assign all the parameters required to create the Plotly object (data, layout, config) to the variable props, returned by mapStateToProps
245-
// The Plotly toolbar is displayed if displayModeBar is set to true
246-
const props: any = {
247-
data: datasets,
248-
layout,
249-
config: {
250-
displayModeBar: true,
251-
modeBarButtonsToRemove: defaultButtons,
252-
// TODO: Removes line above and uncomment below. Read above for more info
253-
// modeBarButtonsToRemove: listOfButtons,
254-
// modeBarButtonsToAdd: [{
255-
// name: 'toggle-options',
256-
// title: translate('toggle.options'),
257-
// icon: Icons.pencil,
258-
// click: function () {
259-
// // # of items must differ so the length can tell which list of buttons is being set
260-
// setListOfButtons(listOfButtons.length === defaultButtons.length ? advancedButtons : defaultButtons); // Update the state
261-
// }
262-
// }],
263-
locale,
264-
locales: Locales // makes locales available for use
265-
}
266-
};
267-
props.config.locale = state.appState.selectedLanguage;
268-
return props;
269-
}
228+
return (
229+
<Plot
230+
data={datasets}
231+
layout={layout}
232+
config={{
233+
displayModeBar: true,
234+
modeBarButtonsToRemove: defaultButtons,
235+
locale,
236+
locales: Locales
237+
}}
238+
/>
239+
);
240+
};
270241

271-
export default connect(mapStateToProps)(Plot);
242+
export default CompareBarComponent;

src/client/app/components/CompareLineChartComponent.tsx

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import * as moment from 'moment';
66
import * as React from 'react';
77
import Plot from 'react-plotly.js';
8+
import { Icons } from 'plotly.js';
89
import { readingsApi, stableEmptyLineReadings } from '../redux/api/readingsApi';
910
import { useAppSelector } from '../redux/reduxHooks';
1011
import { selectCompareLineQueryArgs } from '../redux/selectors/chartQuerySelectors';
@@ -86,6 +87,14 @@ export default function CompareLineChartComponent() {
8687
// Check if there is at least one valid graph for current data and shifted data
8788
const enoughData = data.find(data => data.x!.length > 1) && dataNew.find(dataNew => dataNew.x!.length > 1);
8889

90+
// Display Plotly Buttons Feature
91+
// The number of items in defaultButtons and advancedButtons must differ as discussed below
92+
const defaultButtons: Plotly.ModeBarDefaultButtons[] = ['zoom2d', 'pan2d', 'select2d', 'lasso2d', 'zoomIn2d',
93+
'zoomOut2d', 'autoScale2d', 'resetScale2d'];
94+
const advancedButtons: Plotly.ModeBarDefaultButtons[] = ['zoom2d', 'select2d', 'lasso2d', 'autoScale2d', 'resetScale2d'];
95+
// Manage button states with useState
96+
const [listOfButtons, setListOfButtons] = React.useState(defaultButtons);
97+
8998
// Customize the layout of the plot
9099
// See https://community.plotly.com/t/replacing-an-empty-graph-with-a-message/31497 for showing text `not plot.
91100
if (!meterOrGroupID) {
@@ -148,7 +157,17 @@ export default function CompareLineChartComponent() {
148157
layout={layout}
149158
config={{
150159
responsive: true,
151-
displayModeBar: false,
160+
displayModeBar: true,
161+
modeBarButtonsToRemove: listOfButtons,
162+
modeBarButtonsToAdd: [{
163+
name: 'toggle-options',
164+
title: translate('toggle.options'),
165+
icon: Icons.pencil,
166+
click: function () {
167+
// # of items must differ so the length can tell which list of buttons is being set
168+
setListOfButtons(listOfButtons.length === defaultButtons.length ? advancedButtons : defaultButtons); // Update the state
169+
}
170+
}],
152171
// Current Locale
153172
locale,
154173
// Available Locales
@@ -235,4 +254,4 @@ function monthDateSame(firstDate: moment.Moment, secondDate: moment.Moment) {
235254
// the first one will mismatch the month or day unless those happen to match in which
236255
// case it is still true that they are generally okay so ignore all this.
237256
return firstDate.month() === secondDate.month() && firstDate.date() === secondDate.date();
238-
}
257+
}

0 commit comments

Comments
 (0)