Skip to content

Commit 2f17dbc

Browse files
authored
Merge pull request #328 from RENCI/issue327
Units of measurement for the chart
2 parents 8cd9b65 + 50994cb commit 2f17dbc

File tree

2 files changed

+148
-121
lines changed

2 files changed

+148
-121
lines changed

src/components/dialog/observation-chart.js

+118-121
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Typography } from '@mui/material';
44
import axios from 'axios';
55
import axiosRetry from 'axios-retry';
66
import { LineChart, Line, XAxis, YAxis, CartesianGrid, ResponsiveContainer, Tooltip, ReferenceLine } from 'recharts';
7-
import {getNamespacedEnvParam} from "@utils/map-utils";
7+
import {feetToMeters, metersToFeet, getNamespacedEnvParam} from "@utils/map-utils";
88
import dayjs from 'dayjs';
99
import { useSettings } from '@context';
1010

@@ -17,7 +17,7 @@ dayjs.extend(utc);
1717
/**
1818
* renders the observations as a chart
1919
*
20-
* @param dataUrl
20+
* @param chartProps
2121
* @returns React.ReactElement
2222
* @constructor
2323
*/
@@ -27,7 +27,7 @@ export default function ObservationChart(chartProps) {
2727
}
2828

2929
/**
30-
* this suppresses the re-chart errors on the x/y-axis rendering.
30+
* this captures the re-chart deprecation warnings on the chart rendering.
3131
*
3232
* @type {{(message?: any, ...optionalParams: any[]): void, (...data: any[]): void}}
3333
*/
@@ -41,9 +41,10 @@ console.error = (...args) => {
4141
* Retrieves and returns the chart data in JSON format
4242
*
4343
* @param url
44-
* @returns { json }
44+
* @param setLineButtonView
45+
* @returns { [json] || '' }
4546
*/
46-
function getObsChartData(url, setLineButtonView, useUTC) {
47+
function getObsChartData(url, setLineButtonView) {
4748
// configure the retry count to be zero
4849
axiosRetry(axios, {
4950
retries: 0
@@ -63,25 +64,17 @@ function getObsChartData(url, setLineButtonView, useUTC) {
6364
};
6465

6566
// make the call to get the data
66-
const ret_val = await axios
67-
// make the call to get the data
68-
.get(url, requestOptions)
67+
const ret_val = await axios.get(url, requestOptions)
6968
// use the data returned
70-
.then((response) => {
71-
// return the data
72-
return response.data;
73-
})
74-
// otherwise post the issue to the console log
75-
.catch((error) => {
76-
// make sure we do not render anything
77-
return error.response.status;
78-
});
69+
.then((response) => { return response.data; })
70+
// otherwise capture the error
71+
.catch((error) => { return error.response.status; });
7972

8073
// if there was not an error
81-
if (ret_val !== 500) {
74+
if (ret_val !== 500)
8275
// return the csv data in JSON format
83-
return csvToJSON(ret_val, setLineButtonView, useUTC);
84-
} else
76+
return csvToJSON(ret_val, setLineButtonView);
77+
else
8578
// just return nothing and nothing will be rendered
8679
return '';
8780
}, refetchOnWindowFocus: false
@@ -92,12 +85,13 @@ function getObsChartData(url, setLineButtonView, useUTC) {
9285
* converts CSV data into json format
9386
*
9487
* @param csvData
88+
* @param setLineButtonView
9589
* @returns { json [] }
9690
*/
97-
const csvToJSON = (csvData, setLineButtonView, useUTC) => {
91+
const csvToJSON = (csvData, setLineButtonView) => {
9892
// ensure that there is csv data to convert
9993
if (csvData !== "") {
100-
// split on carriage returns. also removing all the windows \r characters if they exist
94+
// split on carriage returns. also removing all the windows "\r" characters when they exist
10195
const lines = csvData.replaceAll('\r', '').split('\n');
10296

10397
// init the result
@@ -111,99 +105,97 @@ const csvToJSON = (csvData, setLineButtonView, useUTC) => {
111105
// split the line on commas
112106
const currentLine = lines[i].split(",");
113107

114-
// init the converted data
108+
// init storage for the processed data
115109
const jsonObj = {};
116110

117-
// loop through the data and get name/vale pairs in JSON format
111+
// loop through the data and get name/value pairs in JSON format
118112
for (let j = 0; j < dataHeader.length; j++) {
119113
// save the data
120114
jsonObj[dataHeader[j]] = currentLine[j];
121115
}
122116

123-
// add the data to the return
124-
ret_val.push(jsonObj);
125-
}
126-
127-
// remove the timezone from the time value
128-
ret_val.map(function (e) {
129-
// only convert records with a valid time
130-
if (e.time !== "") {
131-
// put the date/time in the chosen format
132-
if (useUTC) {
133-
// reformat the text given into UTC format
134-
e.time = e.time.substring(0, e.time.split(':', 2).join(':').length) + 'Z';
135-
}
136-
else {
137-
// reformat the date/time to the local timezone
138-
e.time = new Date(e.time).toLocaleString();
139-
}
140-
141-
// data that is missing a value will not result in plotting
142-
if (e["Observations"]) {
143-
e["Observations"] = +parseFloat(e["Observations"]).toFixed(3);
144-
145-
// set the line button to be in view
146-
setLineButtonView("Observations");
147-
} else
148-
e["Observations"] = null;
149-
150-
if (e["NOAA Tidal Predictions"]) {
151-
e["NOAA Tidal Predictions"] = +parseFloat(e["NOAA Tidal Predictions"]).toFixed(3);
152-
153-
// set the line button to be in view
154-
setLineButtonView("NOAA Tidal Predictions");
155-
} else
156-
e["NOAA Tidal Predictions"] = null;
157-
158-
if (e["APS Nowcast"]) {
159-
e["APS Nowcast"] = +parseFloat(e["APS Nowcast"]).toFixed(3);
117+
// make sure there is a good record (has a timestamp)
118+
if (jsonObj.time.length) {
119+
// add these so the "units" converter will initially format the data properly
120+
jsonObj['useUTC'] = null;
121+
jsonObj['units'] = null;
160122

161-
// set the line button to be in view
162-
setLineButtonView("APS Nowcast");
163-
} else
164-
e["APS Nowcast"] = null;
165-
166-
if (e["APS Forecast"]) {
167-
e["APS Forecast"] = +parseFloat(e["APS Forecast"]).toFixed(3);
168-
169-
// set the line button to be in view
170-
setLineButtonView("APS Forecast");
171-
} else
172-
e["APS Forecast"] = null;
173-
174-
if (e["SWAN Nowcast"]) {
175-
e["SWAN Nowcast"] = +parseFloat(e["SWAN Nowcast"]).toFixed(3);
176-
177-
// set the line button to be in view
178-
setLineButtonView("SWAN Nowcast");
179-
} else
180-
e["SWAN Nowcast"] = null;
123+
// add the data to the return
124+
ret_val.push(jsonObj);
125+
}
126+
}
181127

182-
if (e["SWAN Forecast"]) {
183-
e["SWAN Forecast"] = +parseFloat(e["SWAN Forecast"]).toFixed(3);
128+
// set the chart line toggle and get undefined data formatted for the chart rendering
129+
ret_val.forEach( function (chartItem) {
130+
// loop through the keys
131+
Object.keys(chartItem).forEach(function (key) {
132+
// if there is a value for the key
133+
if (chartItem[key])
134+
setLineButtonView(key);
135+
// undefined data gets set to null for proper chart rendering
136+
else
137+
chartItem[key] = null;
138+
});
139+
});
184140

185-
// set the line button to be in view
186-
setLineButtonView("SWAN Forecast");
187-
} else
188-
e["SWAN Forecast"] = null;
141+
// return the data
142+
return ret_val;
143+
}
144+
};
189145

190-
if (e["Difference (APS-OBS)"]) {
191-
e["Difference (APS-OBS)"] = +parseFloat(e["Difference (APS-OBS)"]).toFixed(3);
146+
/**
147+
* reformats the data based on user selections for the timezone and units of measurement.
148+
*
149+
* note: this method modifies the data in-place.
150+
*
151+
* @param data
152+
* @param newUnits
153+
* @param useUTC
154+
*/
155+
const getReformattedData = (data, newUnits, useUTC) => {
156+
// if there is data to process
157+
if (data !== undefined && data.length) {
158+
// loop through each chart data item
159+
data.forEach( function (chartItem) {
160+
// loop through all the keys and change the format if needed
161+
Object.keys(chartItem).forEach(function(key){
162+
// check for timezone conversion on the time element
163+
if (key === 'time') {
164+
// convert the date/time to UTC format
165+
if (useUTC) {
166+
// get the date/time in ISO format
167+
const newTime = new Date(chartItem[key]).toISOString();
168+
169+
// reformat the date/time into the new format
170+
chartItem[key] = newTime.replace('T', ' ')
171+
.substring(0, newTime.split(':', 2) .join(':').length) + 'Z';
172+
}
173+
// convert the date/time to local timezone
174+
else {
175+
// reformat the date/time to the local timezone
176+
chartItem[key] = new Date(chartItem[key]).toLocaleString();
177+
}
178+
}
179+
// check for measurement units conversion
180+
else if (newUnits !== chartItem['units']) {
181+
// if the data element is null, it stays null
182+
if (chartItem[key] !== null) {
183+
// convert the value to the new measurement units
184+
chartItem[key] = (newUnits === 'imperial') ? +parseFloat(metersToFeet(chartItem[key])).toFixed(3) :
185+
+parseFloat(feetToMeters(chartItem[key])).toFixed(3);
186+
}
187+
}
188+
});
192189

193-
// set the line button to be in view
194-
setLineButtonView("Difference (APS-OBS)");
195-
} else
196-
e["Difference (APS-OBS)"] = null;
197-
}
190+
// save the new timezone and measurement unit types
191+
chartItem['useUTC'] = useUTC;
192+
chartItem['units'] = newUnits;
198193
});
199-
200-
// return the json data representation
201-
return ret_val;
202194
}
203195
};
204196

205197
/**
206-
* reformats the data label shown on the x-axis
198+
* reformats the data label shown on the y-axis
207199
*
208200
* @param value
209201
* @returns {string}
@@ -217,6 +209,7 @@ function formatY_axis(value) {
217209
* reformats the data label shown on the x-axis. this uses the chosen timezone.
218210
*
219211
* @param value
212+
* @param useUTC
220213
* @returns {string}
221214
*/
222215
function formatX_axis(value, useUTC) {
@@ -226,7 +219,7 @@ function formatX_axis(value, useUTC) {
226219
// empty data will be ignored
227220
if (value !== "")
228221
// put this in the proper format
229-
if(useUTC)
222+
if (useUTC)
230223
ret_val = dayjs.utc(value).format('MM/DD-HH').split('+')[0] + 'Z';
231224
// else use reformat using the local time zone
232225
else
@@ -307,47 +300,51 @@ function get_xtick_interval(data) {
307300
let interval = one_hour_interval * 24 - 1;
308301

309302
// all ticks for <= 0.5 days>
310-
if (days <= 0.5) {
303+
if (days <= 0.5)
311304
interval = 0;
312-
}
313305
// hour labels for <= 1.5 days
314-
else if (days <= 1.5) {
306+
else if (days <= 1.5)
315307
interval = one_hour_interval - 1;
316-
}
317308
// 6-hour labels for <= 4.5 days
318-
else if (days <= 4.5) {
309+
else if (days <= 4.5)
319310
interval = one_hour_interval * 6 - 1;
320-
}
321311
// 12-hour labels for <= 7.5 days
322-
else if (days <= 7.5) {
312+
else if (days <= 7.5)
323313
interval = one_hour_interval * 12 - 1;
324-
}
314+
315+
// return the calculated interval
325316
return interval;
326317
}
327318

328319
/**
329320
* Creates the chart.
330321
*
331-
* @param url
322+
* @param c: the chart props
332323
* @returns React.ReactElement
333324
* @constructor
334325
*/
335326
const CreateObsChart = (c) => {
336327
// get the timezone preference
337-
const { useUTC } = useSettings();
328+
const { useUTC, unitsType } = useSettings();
329+
330+
// set the "units" label
331+
const unitLabel = (unitsType.current === "imperial") ? "ft" : "m";
338332

339333
// call to get the data. expect back some information too
340-
const {status, data} = getObsChartData(c.chartProps.url, c.chartProps.setLineButtonView, useUTC.enabled);
334+
const {status, data} = getObsChartData(c.chartProps.url, c.chartProps.setLineButtonView);
335+
336+
// reformat the data to the desired time zone and units of measurement
337+
getReformattedData(data, unitsType.current, useUTC.enabled);
341338

342339
// render the chart
343340
return (
344341
<Fragment>
345342
{
346343
status === 'pending' ? (<Typography sx={{alignItems: 'center', fontSize: 12}}>Gathering chart data...</Typography>) :
347344
(status === 'error' || data === '') ? (
348-
<Typography sx={{alignItems: 'center', color: 'red', fontSize: 12}}>
349-
There was a problem collecting data for this location.
350-
</Typography>) :
345+
<Typography sx={{alignItems: 'center', color: 'red', fontSize: 12}}>
346+
There was a problem collecting data for this location.
347+
</Typography>) :
351348
<ResponsiveContainer>
352349
<LineChart margin={{top: 5, right: 10, left: -25, bottom: 5}} data={data} isHide={c.chartProps.isHideLine}>
353350
<CartesianGrid strokeDasharray="1 1"/>
@@ -357,24 +354,24 @@ const CreateObsChart = (c) => {
357354

358355
<ReferenceLine y={0} stroke="Black" strokeDasharray="3 3"/>
359356

360-
<YAxis unit={'m'} ticks={get_yaxis_ticks(data)} tick={{stroke: 'tan', strokeWidth: .5}}
357+
<YAxis unit={unitLabel} ticks={get_yaxis_ticks(data)} tick={{stroke: 'tan', strokeWidth: .5}}
361358
tickFormatter={(value) => formatY_axis(value)}/>
362359

363360
<Tooltip/>
364361

365-
<Line unit={'m'} type="monotone" dataKey="Observations" stroke="black" strokeWidth={1} dot={false}
362+
<Line unit={unitLabel} type="monotone" dataKey="Observations" stroke="black" strokeWidth={1} dot={false}
366363
isAnimationActive={false} hide={c.chartProps.isHideLine['Observations']}/>
367-
<Line unit={'m'} type="monotone" dataKey="NOAA Tidal Predictions" stroke="teal" strokeWidth={1} dot={false}
364+
<Line unit={unitLabel} type="monotone" dataKey="NOAA Tidal Predictions" stroke="teal" strokeWidth={1} dot={false}
368365
isAnimationActive={false} hide={c.chartProps.isHideLine["NOAA Tidal Predictions"]}/>
369-
<Line unit={'m'} type="monotone" dataKey="APS Nowcast" stroke="CornflowerBlue" strokeWidth={2} dot={false}
366+
<Line unit={unitLabel} type="monotone" dataKey="APS Nowcast" stroke="CornflowerBlue" strokeWidth={2} dot={false}
370367
isAnimationActive={false} hide={c.chartProps.isHideLine["APS Nowcast"]}/>
371-
<Line unit={'m'} type="monotone" dataKey="APS Forecast" stroke="LimeGreen" strokeWidth={2} dot={false}
368+
<Line unit={unitLabel} type="monotone" dataKey="APS Forecast" stroke="LimeGreen" strokeWidth={2} dot={false}
372369
isAnimationActive={false} hide={c.chartProps.isHideLine["APS Forecast"]}/>
373-
<Line unit={'m'} type="monotone" dataKey="SWAN Nowcast" stroke="CornflowerBlue" strokeWidth={2} dot={false}
370+
<Line unit={unitLabel} type="monotone" dataKey="SWAN Nowcast" stroke="CornflowerBlue" strokeWidth={2} dot={false}
374371
isAnimationActive={false} hide={c.chartProps.isHideLine["SWAN Nowcast"]}/>
375-
<Line unit={'m'} type="monotone" dataKey="SWAN Forecast" stroke="LimeGreen" strokeWidth={2} dot={false}
372+
<Line unit={unitLabel} type="monotone" dataKey="SWAN Forecast" stroke="LimeGreen" strokeWidth={2} dot={false}
376373
isAnimationActive={false} hide={c.chartProps.isHideLine["SWAN Forecast"]}/>
377-
<Line unit={'m'} type="monotone" dataKey="Difference (APS-OBS)" stroke="red" strokeWidth={1} dot={false}
374+
<Line unit={unitLabel} type="monotone" dataKey="Difference (APS-OBS)" stroke="red" strokeWidth={1} dot={false}
378375
isAnimationActive={false} hide={c.chartProps.isHideLine["Difference (APS-OBS)"]}/>
379376
</LineChart>
380377
</ResponsiveContainer>

0 commit comments

Comments
 (0)