From 26a497f2b1673697083e4f873ee4860e6d0abf8a Mon Sep 17 00:00:00 2001 From: FObersteiner <florian.obersteiner@kit.edu> Date: Wed, 26 Mar 2025 14:19:28 +0100 Subject: [PATCH 1/4] fail in no data --- src/data.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/data.py b/src/data.py index 487550d..06485da 100644 --- a/src/data.py +++ b/src/data.py @@ -19,8 +19,14 @@ def load_from_thredds( ) -> (pd.DataFrame, dict[int, str]): # type: ignore """ Load multiple netCDF files from a THREDS TDS into a single DataFrame. + + Raises: + ValueError: If 'ACN' not found in dataset. + ValueError: If no datasets are found. """ + log.debug("begin load data from THREDDS...") + dataframes = [] airports = {} @@ -54,7 +60,11 @@ def load_from_thredds( dataframes.append(df) + if len(dataframes) == 0: + raise ValueError("No datasets found!") + log.debug("Data loaded from THREDDS successfully") + return (pd.concat(dataframes), airports) @@ -63,6 +73,10 @@ def load_from_disk( ) -> (pd.DataFrame, dict[int, str]): # type: ignore """ Load multiple netCDF files into a single DataFrame. + + Raises: + ValueError: If 'ACN' not found in dataset. + ValueError: If no datasets are found. """ dataframes = [] airports = {} @@ -99,5 +113,9 @@ def load_from_disk( dataframes.append(df) + if len(dataframes) == 0: + raise ValueError("No datasets found!") + log.info("Data loaded from disk successfully") + return (pd.concat(dataframes), airports) -- GitLab From bb887fbfa0881fe9403c67ea25317b03ccc1c30f Mon Sep 17 00:00:00 2001 From: FObersteiner <florian.obersteiner@kit.edu> Date: Wed, 26 Mar 2025 14:20:28 +0100 Subject: [PATCH 2/4] add second y plot --- main.py | 58 +++++++++++++++++++++++++++++++++++++++------------ src/layout.py | 41 ++++++++++++++++++++++++++++++++++-- 2 files changed, 84 insertions(+), 15 deletions(-) diff --git a/main.py b/main.py index ce8527c..9c5d293 100644 --- a/main.py +++ b/main.py @@ -12,6 +12,7 @@ import pandas as pd import plotly.graph_objects as go from dash import Dash from dash.dependencies import Input, Output +from plotly.subplots import make_subplots from src.config import DotDict, get_latest_semantic_version_tag from src.data import load_from_disk, load_from_thredds @@ -43,7 +44,7 @@ data, ap_info = ( else load_from_disk(Path("./testdata").glob("*.nc"), appconfig, airportsconfig, True) # type: ignore ) log.debug(f"Data loaded. Used THREDDS: {appconfig.USE_THREDDS}") -log.debug(f"Data size: {data.info()}") +# log.debug(f"Data size: {data.info()}") app.layout = create_layout(data, appconfig) server = app.server # for deployment via gunicorn / WSGI server @@ -55,13 +56,27 @@ server = app.server # for deployment via gunicorn / WSGI server Output("fig-ts", "figure"), Output("flight-dep-dest", "children"), Output("var-info", "children"), + Output("second-var-info", "children"), + ], + [ + Input("flight-dropdown", "value"), + Input("variable-dropdown", "value"), + Input("second-variable-dropdown", "value"), ], - [Input("flight-dropdown", "value"), Input("variable-dropdown", "value")], ) -def update_plots(selected_flight: int, selected_variable: str): +def update_plots(selected_flight: int, primary_variable: str, secondary_variable: str): filtered_df = data[data["flight_number"] == selected_flight] - valid_indices = filtered_df[selected_variable].notna() - unit = varconfig.loc[selected_variable, :].Unit + valid_indices = filtered_df[primary_variable].notna() + unit_primary = varconfig.loc[primary_variable, :].Unit + lname_primary = (varconfig.loc[primary_variable, :].Long_Name,) + unit_secondary = ( + varconfig.loc[secondary_variable, :].Unit if secondary_variable in varconfig.index else "-" + ) + lname_secondary = ( + varconfig.loc[secondary_variable, :].Long_Name + if secondary_variable in varconfig.index + else "" + ) fig_map = go.Figure( go.Scattermap( @@ -69,17 +84,17 @@ def update_plots(selected_flight: int, selected_variable: str): lat=filtered_df.loc[valid_indices, "lat"], mode="markers", marker={ - "color": filtered_df.loc[valid_indices, selected_variable], + "color": filtered_df.loc[valid_indices, primary_variable], "colorscale": "Rainbow", "colorbar": { "x": 0.995, - "title": {"text": f"{selected_variable} {unit}", "side": "right"}, + "title": {"text": f"{primary_variable} {unit_primary}", "side": "right"}, }, "showscale": True, }, connectgaps=False, hoverinfo="text", - hovertext=[f"{v:.1f}" for v in filtered_df[selected_variable]], + hovertext=[f"{v:.1f}" for v in filtered_df[primary_variable]], name="Flight Path", ) ) @@ -117,11 +132,11 @@ def update_plots(selected_flight: int, selected_variable: str): showlegend=False, ) - fig_ts = go.Figure() + fig_ts = make_subplots(specs=[[{"secondary_y": True}]]) fig_ts.add_trace( go.Scatter( x=filtered_df.index[valid_indices & ~filtered_df["BB_flag"]], - y=filtered_df.loc[valid_indices & ~filtered_df["BB_flag"], selected_variable], + y=filtered_df.loc[valid_indices & ~filtered_df["BB_flag"], primary_variable], mode="markers", marker={ "size": 3, @@ -134,7 +149,7 @@ def update_plots(selected_flight: int, selected_variable: str): fig_ts.add_trace( go.Scatter( x=filtered_df.index[valid_indices & filtered_df["BB_flag"]], - y=filtered_df.loc[valid_indices & filtered_df["BB_flag"], selected_variable], + y=filtered_df.loc[valid_indices & filtered_df["BB_flag"], primary_variable], mode="markers", marker={ "size": 3, @@ -144,6 +159,21 @@ def update_plots(selected_flight: int, selected_variable: str): name="flagged 'biomass burning'", ) ) + if secondary_variable != "None": + fig_ts.add_trace( + go.Scatter( + x=filtered_df.index, + y=filtered_df[secondary_variable], + mode="markers", + marker={ + "size": 2, + "color": "black", + "showscale": False, + }, + name=secondary_variable, + ), + secondary_y=True, + ) fig_ts.update_layout( paper_bgcolor="#e8e8e8", @@ -160,13 +190,15 @@ def update_plots(selected_flight: int, selected_variable: str): ) fig_ts.update_xaxes(title="UTC") - fig_ts.update_yaxes(title=f"{selected_variable} {unit}") + fig_ts.update_yaxes(title=f"{primary_variable} {unit_primary}") + fig_ts.update_yaxes(title=f"{secondary_variable} {unit_secondary}", secondary_y=True) return [ fig_map, fig_ts, ap_info[selected_flight], - varconfig.loc[selected_variable, :].Long_Name, + lname_primary, + lname_secondary, ] diff --git a/src/layout.py b/src/layout.py index 1e26254..8ab4fbe 100644 --- a/src/layout.py +++ b/src/layout.py @@ -34,6 +34,7 @@ def create_layout(df: pd.DataFrame, config: DotDict) -> list: for c in df.columns if c not in ["flight_number", "date", "BB_flag"] ] + var_sel_idx = 0 for idx, vo in enumerate(var_opts): if vo["label"] == "ACN": @@ -90,7 +91,7 @@ def create_layout(df: pd.DataFrame, config: DotDict) -> list: children=[ html.Label( "Flight:", - style={"font-weight": "bold", "margin-right": "29px"}, + style={"font-weight": "bold", "margin-right": "39px"}, ), dcc.Dropdown( id="flight-dropdown", @@ -121,7 +122,7 @@ def create_layout(df: pd.DataFrame, config: DotDict) -> list: children=[ html.Label( "Variable:", - style={"font-weight": "bold", "margin-right": "10px"}, + style={"font-weight": "bold", "margin-right": "20px"}, ), dcc.Dropdown( id="variable-dropdown", @@ -147,6 +148,37 @@ def create_layout(df: pd.DataFrame, config: DotDict) -> list: "align-items": "center", }, ), + html.Div( + id="second-variable-select", + children=[ + html.Label( + "Secondary:", + style={"font-weight": "bold", "margin-right": "10px"}, + ), + dcc.Dropdown( + id="second-variable-dropdown", + options=([{"value": "None", "label": "(None)"}] + var_opts), # type: ignore + value="None", # type: ignore + clearable=False, + searchable=True, + style={ + "display": "inline-block", + "width": "190px", + }, + ), + html.Label( + "", + id="second-var-info", + style={"margin-left": "29px", "font-size": "12px"}, + ), + ], + style={ + "display": "flex", + "flex-direction": "row", + "justify-content": "left", + "align-items": "center", + }, + ), dcc.Graph( id="fig-map", style={ @@ -165,6 +197,11 @@ def create_layout(df: pd.DataFrame, config: DotDict) -> list: "margin-left": "10px", }, ), + html.Label( + "BB flagging: BB influence if ACN > (145 ppt + 3*ACN_prc)", + id="flagging-info", + style={"margin-left": "15px", "font-size": "12px"}, + ), ] ), # --- ^^^ --- main content -- GitLab From 4a6a4a58e3df219ba74292c0959815c29fe1d420 Mon Sep 17 00:00:00 2001 From: FObersteiner <florian.obersteiner@kit.edu> Date: Wed, 26 Mar 2025 14:20:35 +0100 Subject: [PATCH 3/4] do not fail if no data --- src/thredds.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/thredds.py b/src/thredds.py index c522ef4..be9439d 100644 --- a/src/thredds.py +++ b/src/thredds.py @@ -15,9 +15,6 @@ def get_datasets(cfg: DotDict) -> list[Dataset]: ) datasets = [] - if len(catalog.datasets) == 0: - raise ValueError("No datasets found in the catalog") - for i, ds in enumerate(catalog.datasets): if not (ds.startswith(cfg.FNAME_PREFIX) and ds.endswith(cfg.FNAME_SUFFIX)): continue -- GitLab From df879c688da4c5c0ad959d3698af75bf2559ce77 Mon Sep 17 00:00:00 2001 From: FObersteiner <florian.obersteiner@kit.edu> Date: Wed, 26 Mar 2025 14:20:39 +0100 Subject: [PATCH 4/4] update changelog --- CHANGELOG.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9430e8..66cfeaa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,8 +15,9 @@ Types of changes ## TODOs +- add sources for BB tagging and ACN background - investigate if data caching is necessary (load-from-thredds) -- add a secondary y axis to the timeseries plot; plot as line if sufficient data is available +- scatter-plot: plot as line if sufficient data is available? ## Backlog @@ -29,6 +30,19 @@ Types of changes ## [Unreleased] +## v0.0.15 - 2025-03-26 + +### Added + +- secondary variable to plot (optional) +- info about BB flagging + +### Changed + +- app startup immediately fails if no datasets are found + +## v0.0.14 - 2025-03-26 + ### Changed - skip files that have no ACN data -- GitLab