@@ -4,7 +4,7 @@ import { Typography } from '@mui/material';
4
4
import axios from 'axios' ;
5
5
import axiosRetry from 'axios-retry' ;
6
6
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" ;
8
8
import dayjs from 'dayjs' ;
9
9
import { useSettings } from '@context' ;
10
10
@@ -17,7 +17,7 @@ dayjs.extend(utc);
17
17
/**
18
18
* renders the observations as a chart
19
19
*
20
- * @param dataUrl
20
+ * @param chartProps
21
21
* @returns React.ReactElement
22
22
* @constructor
23
23
*/
@@ -27,7 +27,7 @@ export default function ObservationChart(chartProps) {
27
27
}
28
28
29
29
/**
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.
31
31
*
32
32
* @type {{(message?: any, ...optionalParams: any[]): void, (...data: any[]): void} }
33
33
*/
@@ -41,9 +41,10 @@ console.error = (...args) => {
41
41
* Retrieves and returns the chart data in JSON format
42
42
*
43
43
* @param url
44
- * @returns { json }
44
+ * @param setLineButtonView
45
+ * @returns { [json] || '' }
45
46
*/
46
- function getObsChartData ( url , setLineButtonView , useUTC ) {
47
+ function getObsChartData ( url , setLineButtonView ) {
47
48
// configure the retry count to be zero
48
49
axiosRetry ( axios , {
49
50
retries : 0
@@ -63,25 +64,17 @@ function getObsChartData(url, setLineButtonView, useUTC) {
63
64
} ;
64
65
65
66
// 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 )
69
68
// 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 ; } ) ;
79
72
80
73
// if there was not an error
81
- if ( ret_val !== 500 ) {
74
+ if ( ret_val !== 500 )
82
75
// return the csv data in JSON format
83
- return csvToJSON ( ret_val , setLineButtonView , useUTC ) ;
84
- } else
76
+ return csvToJSON ( ret_val , setLineButtonView ) ;
77
+ else
85
78
// just return nothing and nothing will be rendered
86
79
return '' ;
87
80
} , refetchOnWindowFocus : false
@@ -92,12 +85,13 @@ function getObsChartData(url, setLineButtonView, useUTC) {
92
85
* converts CSV data into json format
93
86
*
94
87
* @param csvData
88
+ * @param setLineButtonView
95
89
* @returns { json [] }
96
90
*/
97
- const csvToJSON = ( csvData , setLineButtonView , useUTC ) => {
91
+ const csvToJSON = ( csvData , setLineButtonView ) => {
98
92
// ensure that there is csv data to convert
99
93
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
101
95
const lines = csvData . replaceAll ( '\r' , '' ) . split ( '\n' ) ;
102
96
103
97
// init the result
@@ -111,99 +105,97 @@ const csvToJSON = (csvData, setLineButtonView, useUTC) => {
111
105
// split the line on commas
112
106
const currentLine = lines [ i ] . split ( "," ) ;
113
107
114
- // init the converted data
108
+ // init storage for the processed data
115
109
const jsonObj = { } ;
116
110
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
118
112
for ( let j = 0 ; j < dataHeader . length ; j ++ ) {
119
113
// save the data
120
114
jsonObj [ dataHeader [ j ] ] = currentLine [ j ] ;
121
115
}
122
116
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 ;
160
122
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
+ }
181
127
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
+ } ) ;
184
140
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
+ } ;
189
145
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
+ } ) ;
192
189
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 ;
198
193
} ) ;
199
-
200
- // return the json data representation
201
- return ret_val ;
202
194
}
203
195
} ;
204
196
205
197
/**
206
- * reformats the data label shown on the x -axis
198
+ * reformats the data label shown on the y -axis
207
199
*
208
200
* @param value
209
201
* @returns {string }
@@ -217,6 +209,7 @@ function formatY_axis(value) {
217
209
* reformats the data label shown on the x-axis. this uses the chosen timezone.
218
210
*
219
211
* @param value
212
+ * @param useUTC
220
213
* @returns {string }
221
214
*/
222
215
function formatX_axis ( value , useUTC ) {
@@ -226,7 +219,7 @@ function formatX_axis(value, useUTC) {
226
219
// empty data will be ignored
227
220
if ( value !== "" )
228
221
// put this in the proper format
229
- if ( useUTC )
222
+ if ( useUTC )
230
223
ret_val = dayjs . utc ( value ) . format ( 'MM/DD-HH' ) . split ( '+' ) [ 0 ] + 'Z' ;
231
224
// else use reformat using the local time zone
232
225
else
@@ -307,47 +300,51 @@ function get_xtick_interval(data) {
307
300
let interval = one_hour_interval * 24 - 1 ;
308
301
309
302
// all ticks for <= 0.5 days>
310
- if ( days <= 0.5 ) {
303
+ if ( days <= 0.5 )
311
304
interval = 0 ;
312
- }
313
305
// hour labels for <= 1.5 days
314
- else if ( days <= 1.5 ) {
306
+ else if ( days <= 1.5 )
315
307
interval = one_hour_interval - 1 ;
316
- }
317
308
// 6-hour labels for <= 4.5 days
318
- else if ( days <= 4.5 ) {
309
+ else if ( days <= 4.5 )
319
310
interval = one_hour_interval * 6 - 1 ;
320
- }
321
311
// 12-hour labels for <= 7.5 days
322
- else if ( days <= 7.5 ) {
312
+ else if ( days <= 7.5 )
323
313
interval = one_hour_interval * 12 - 1 ;
324
- }
314
+
315
+ // return the calculated interval
325
316
return interval ;
326
317
}
327
318
328
319
/**
329
320
* Creates the chart.
330
321
*
331
- * @param url
322
+ * @param c: the chart props
332
323
* @returns React.ReactElement
333
324
* @constructor
334
325
*/
335
326
const CreateObsChart = ( c ) => {
336
327
// 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" ;
338
332
339
333
// 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 ) ;
341
338
342
339
// render the chart
343
340
return (
344
341
< Fragment >
345
342
{
346
343
status === 'pending' ? ( < Typography sx = { { alignItems : 'center' , fontSize : 12 } } > Gathering chart data...</ Typography > ) :
347
344
( 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 > ) :
351
348
< ResponsiveContainer >
352
349
< LineChart margin = { { top : 5 , right : 10 , left : - 25 , bottom : 5 } } data = { data } isHide = { c . chartProps . isHideLine } >
353
350
< CartesianGrid strokeDasharray = "1 1" />
@@ -357,24 +354,24 @@ const CreateObsChart = (c) => {
357
354
358
355
< ReferenceLine y = { 0 } stroke = "Black" strokeDasharray = "3 3" />
359
356
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 } }
361
358
tickFormatter = { ( value ) => formatY_axis ( value ) } />
362
359
363
360
< Tooltip />
364
361
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 }
366
363
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 }
368
365
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 }
370
367
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 }
372
369
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 }
374
371
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 }
376
373
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 }
378
375
isAnimationActive = { false } hide = { c . chartProps . isHideLine [ "Difference (APS-OBS)" ] } />
379
376
</ LineChart >
380
377
</ ResponsiveContainer >
0 commit comments