Skip to content

Commit

Permalink
Merge pull request #328 from RENCI/issue327
Browse files Browse the repository at this point in the history
Units of measurement for the chart
  • Loading branch information
lstillwe authored Nov 14, 2024
2 parents 8cd9b65 + 50994cb commit 2f17dbc
Show file tree
Hide file tree
Showing 2 changed files with 148 additions and 121 deletions.
239 changes: 118 additions & 121 deletions src/components/dialog/observation-chart.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Typography } from '@mui/material';
import axios from 'axios';
import axiosRetry from 'axios-retry';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, ResponsiveContainer, Tooltip, ReferenceLine } from 'recharts';
import {getNamespacedEnvParam} from "@utils/map-utils";
import {feetToMeters, metersToFeet, getNamespacedEnvParam} from "@utils/map-utils";
import dayjs from 'dayjs';
import { useSettings } from '@context';

Expand All @@ -17,7 +17,7 @@ dayjs.extend(utc);
/**
* renders the observations as a chart
*
* @param dataUrl
* @param chartProps
* @returns React.ReactElement
* @constructor
*/
Expand All @@ -27,7 +27,7 @@ export default function ObservationChart(chartProps) {
}

/**
* this suppresses the re-chart errors on the x/y-axis rendering.
* this captures the re-chart deprecation warnings on the chart rendering.
*
* @type {{(message?: any, ...optionalParams: any[]): void, (...data: any[]): void}}
*/
Expand All @@ -41,9 +41,10 @@ console.error = (...args) => {
* Retrieves and returns the chart data in JSON format
*
* @param url
* @returns { json }
* @param setLineButtonView
* @returns { [json] || '' }
*/
function getObsChartData(url, setLineButtonView, useUTC) {
function getObsChartData(url, setLineButtonView) {
// configure the retry count to be zero
axiosRetry(axios, {
retries: 0
Expand All @@ -63,25 +64,17 @@ function getObsChartData(url, setLineButtonView, useUTC) {
};

// make the call to get the data
const ret_val = await axios
// make the call to get the data
.get(url, requestOptions)
const ret_val = await axios.get(url, requestOptions)
// use the data returned
.then((response) => {
// return the data
return response.data;
})
// otherwise post the issue to the console log
.catch((error) => {
// make sure we do not render anything
return error.response.status;
});
.then((response) => { return response.data; })
// otherwise capture the error
.catch((error) => { return error.response.status; });

// if there was not an error
if (ret_val !== 500) {
if (ret_val !== 500)
// return the csv data in JSON format
return csvToJSON(ret_val, setLineButtonView, useUTC);
} else
return csvToJSON(ret_val, setLineButtonView);
else
// just return nothing and nothing will be rendered
return '';
}, refetchOnWindowFocus: false
Expand All @@ -92,12 +85,13 @@ function getObsChartData(url, setLineButtonView, useUTC) {
* converts CSV data into json format
*
* @param csvData
* @param setLineButtonView
* @returns { json [] }
*/
const csvToJSON = (csvData, setLineButtonView, useUTC) => {
const csvToJSON = (csvData, setLineButtonView) => {
// ensure that there is csv data to convert
if (csvData !== "") {
// split on carriage returns. also removing all the windows \r characters if they exist
// split on carriage returns. also removing all the windows "\r" characters when they exist
const lines = csvData.replaceAll('\r', '').split('\n');

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

// init the converted data
// init storage for the processed data
const jsonObj = {};

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

// add the data to the return
ret_val.push(jsonObj);
}

// remove the timezone from the time value
ret_val.map(function (e) {
// only convert records with a valid time
if (e.time !== "") {
// put the date/time in the chosen format
if (useUTC) {
// reformat the text given into UTC format
e.time = e.time.substring(0, e.time.split(':', 2).join(':').length) + 'Z';
}
else {
// reformat the date/time to the local timezone
e.time = new Date(e.time).toLocaleString();
}

// data that is missing a value will not result in plotting
if (e["Observations"]) {
e["Observations"] = +parseFloat(e["Observations"]).toFixed(3);

// set the line button to be in view
setLineButtonView("Observations");
} else
e["Observations"] = null;

if (e["NOAA Tidal Predictions"]) {
e["NOAA Tidal Predictions"] = +parseFloat(e["NOAA Tidal Predictions"]).toFixed(3);

// set the line button to be in view
setLineButtonView("NOAA Tidal Predictions");
} else
e["NOAA Tidal Predictions"] = null;

if (e["APS Nowcast"]) {
e["APS Nowcast"] = +parseFloat(e["APS Nowcast"]).toFixed(3);
// make sure there is a good record (has a timestamp)
if (jsonObj.time.length) {
// add these so the "units" converter will initially format the data properly
jsonObj['useUTC'] = null;
jsonObj['units'] = null;

// set the line button to be in view
setLineButtonView("APS Nowcast");
} else
e["APS Nowcast"] = null;

if (e["APS Forecast"]) {
e["APS Forecast"] = +parseFloat(e["APS Forecast"]).toFixed(3);

// set the line button to be in view
setLineButtonView("APS Forecast");
} else
e["APS Forecast"] = null;

if (e["SWAN Nowcast"]) {
e["SWAN Nowcast"] = +parseFloat(e["SWAN Nowcast"]).toFixed(3);

// set the line button to be in view
setLineButtonView("SWAN Nowcast");
} else
e["SWAN Nowcast"] = null;
// add the data to the return
ret_val.push(jsonObj);
}
}

if (e["SWAN Forecast"]) {
e["SWAN Forecast"] = +parseFloat(e["SWAN Forecast"]).toFixed(3);
// set the chart line toggle and get undefined data formatted for the chart rendering
ret_val.forEach( function (chartItem) {
// loop through the keys
Object.keys(chartItem).forEach(function (key) {
// if there is a value for the key
if (chartItem[key])
setLineButtonView(key);
// undefined data gets set to null for proper chart rendering
else
chartItem[key] = null;
});
});

// set the line button to be in view
setLineButtonView("SWAN Forecast");
} else
e["SWAN Forecast"] = null;
// return the data
return ret_val;
}
};

if (e["Difference (APS-OBS)"]) {
e["Difference (APS-OBS)"] = +parseFloat(e["Difference (APS-OBS)"]).toFixed(3);
/**
* reformats the data based on user selections for the timezone and units of measurement.
*
* note: this method modifies the data in-place.
*
* @param data
* @param newUnits
* @param useUTC
*/
const getReformattedData = (data, newUnits, useUTC) => {
// if there is data to process
if (data !== undefined && data.length) {
// loop through each chart data item
data.forEach( function (chartItem) {
// loop through all the keys and change the format if needed
Object.keys(chartItem).forEach(function(key){
// check for timezone conversion on the time element
if (key === 'time') {
// convert the date/time to UTC format
if (useUTC) {
// get the date/time in ISO format
const newTime = new Date(chartItem[key]).toISOString();

// reformat the date/time into the new format
chartItem[key] = newTime.replace('T', ' ')
.substring(0, newTime.split(':', 2) .join(':').length) + 'Z';
}
// convert the date/time to local timezone
else {
// reformat the date/time to the local timezone
chartItem[key] = new Date(chartItem[key]).toLocaleString();
}
}
// check for measurement units conversion
else if (newUnits !== chartItem['units']) {
// if the data element is null, it stays null
if (chartItem[key] !== null) {
// convert the value to the new measurement units
chartItem[key] = (newUnits === 'imperial') ? +parseFloat(metersToFeet(chartItem[key])).toFixed(3) :
+parseFloat(feetToMeters(chartItem[key])).toFixed(3);
}
}
});

// set the line button to be in view
setLineButtonView("Difference (APS-OBS)");
} else
e["Difference (APS-OBS)"] = null;
}
// save the new timezone and measurement unit types
chartItem['useUTC'] = useUTC;
chartItem['units'] = newUnits;
});

// return the json data representation
return ret_val;
}
};

/**
* reformats the data label shown on the x-axis
* reformats the data label shown on the y-axis
*
* @param value
* @returns {string}
Expand All @@ -217,6 +209,7 @@ function formatY_axis(value) {
* reformats the data label shown on the x-axis. this uses the chosen timezone.
*
* @param value
* @param useUTC
* @returns {string}
*/
function formatX_axis(value, useUTC) {
Expand All @@ -226,7 +219,7 @@ function formatX_axis(value, useUTC) {
// empty data will be ignored
if (value !== "")
// put this in the proper format
if(useUTC)
if (useUTC)
ret_val = dayjs.utc(value).format('MM/DD-HH').split('+')[0] + 'Z';
// else use reformat using the local time zone
else
Expand Down Expand Up @@ -307,47 +300,51 @@ function get_xtick_interval(data) {
let interval = one_hour_interval * 24 - 1;

// all ticks for <= 0.5 days>
if (days <= 0.5) {
if (days <= 0.5)
interval = 0;
}
// hour labels for <= 1.5 days
else if (days <= 1.5) {
else if (days <= 1.5)
interval = one_hour_interval - 1;
}
// 6-hour labels for <= 4.5 days
else if (days <= 4.5) {
else if (days <= 4.5)
interval = one_hour_interval * 6 - 1;
}
// 12-hour labels for <= 7.5 days
else if (days <= 7.5) {
else if (days <= 7.5)
interval = one_hour_interval * 12 - 1;
}

// return the calculated interval
return interval;
}

/**
* Creates the chart.
*
* @param url
* @param c: the chart props
* @returns React.ReactElement
* @constructor
*/
const CreateObsChart = (c) => {
// get the timezone preference
const { useUTC } = useSettings();
const { useUTC, unitsType } = useSettings();

// set the "units" label
const unitLabel = (unitsType.current === "imperial") ? "ft" : "m";

// call to get the data. expect back some information too
const {status, data} = getObsChartData(c.chartProps.url, c.chartProps.setLineButtonView, useUTC.enabled);
const {status, data} = getObsChartData(c.chartProps.url, c.chartProps.setLineButtonView);

// reformat the data to the desired time zone and units of measurement
getReformattedData(data, unitsType.current, useUTC.enabled);

// render the chart
return (
<Fragment>
{
status === 'pending' ? (<Typography sx={{alignItems: 'center', fontSize: 12}}>Gathering chart data...</Typography>) :
(status === 'error' || data === '') ? (
<Typography sx={{alignItems: 'center', color: 'red', fontSize: 12}}>
There was a problem collecting data for this location.
</Typography>) :
<Typography sx={{alignItems: 'center', color: 'red', fontSize: 12}}>
There was a problem collecting data for this location.
</Typography>) :
<ResponsiveContainer>
<LineChart margin={{top: 5, right: 10, left: -25, bottom: 5}} data={data} isHide={c.chartProps.isHideLine}>
<CartesianGrid strokeDasharray="1 1"/>
Expand All @@ -357,24 +354,24 @@ const CreateObsChart = (c) => {

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

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

<Tooltip/>

<Line unit={'m'} type="monotone" dataKey="Observations" stroke="black" strokeWidth={1} dot={false}
<Line unit={unitLabel} type="monotone" dataKey="Observations" stroke="black" strokeWidth={1} dot={false}
isAnimationActive={false} hide={c.chartProps.isHideLine['Observations']}/>
<Line unit={'m'} type="monotone" dataKey="NOAA Tidal Predictions" stroke="teal" strokeWidth={1} dot={false}
<Line unit={unitLabel} type="monotone" dataKey="NOAA Tidal Predictions" stroke="teal" strokeWidth={1} dot={false}
isAnimationActive={false} hide={c.chartProps.isHideLine["NOAA Tidal Predictions"]}/>
<Line unit={'m'} type="monotone" dataKey="APS Nowcast" stroke="CornflowerBlue" strokeWidth={2} dot={false}
<Line unit={unitLabel} type="monotone" dataKey="APS Nowcast" stroke="CornflowerBlue" strokeWidth={2} dot={false}
isAnimationActive={false} hide={c.chartProps.isHideLine["APS Nowcast"]}/>
<Line unit={'m'} type="monotone" dataKey="APS Forecast" stroke="LimeGreen" strokeWidth={2} dot={false}
<Line unit={unitLabel} type="monotone" dataKey="APS Forecast" stroke="LimeGreen" strokeWidth={2} dot={false}
isAnimationActive={false} hide={c.chartProps.isHideLine["APS Forecast"]}/>
<Line unit={'m'} type="monotone" dataKey="SWAN Nowcast" stroke="CornflowerBlue" strokeWidth={2} dot={false}
<Line unit={unitLabel} type="monotone" dataKey="SWAN Nowcast" stroke="CornflowerBlue" strokeWidth={2} dot={false}
isAnimationActive={false} hide={c.chartProps.isHideLine["SWAN Nowcast"]}/>
<Line unit={'m'} type="monotone" dataKey="SWAN Forecast" stroke="LimeGreen" strokeWidth={2} dot={false}
<Line unit={unitLabel} type="monotone" dataKey="SWAN Forecast" stroke="LimeGreen" strokeWidth={2} dot={false}
isAnimationActive={false} hide={c.chartProps.isHideLine["SWAN Forecast"]}/>
<Line unit={'m'} type="monotone" dataKey="Difference (APS-OBS)" stroke="red" strokeWidth={1} dot={false}
<Line unit={unitLabel} type="monotone" dataKey="Difference (APS-OBS)" stroke="red" strokeWidth={1} dot={false}
isAnimationActive={false} hide={c.chartProps.isHideLine["Difference (APS-OBS)"]}/>
</LineChart>
</ResponsiveContainer>
Expand Down
Loading

0 comments on commit 2f17dbc

Please sign in to comment.