Skip to content
Snippets Groups Projects
Commit e6d4ee13 authored by Florian Obersteiner's avatar Florian Obersteiner :octopus:
Browse files

Merge branch 'dev-0016' into 'main'

revise callbacks

See merge request !7
parents 9cbeb7af b723d678
No related branches found
No related tags found
1 merge request!7revise callbacks
......@@ -15,11 +15,14 @@ Types of changes
## TODOs
- drop boxes: brighter highlighting of selected element
- add sources for BB tagging and ACN background
- investigate if data caching is necessary (load-from-thredds)?
- scatter-plot: plot as line if sufficient data is available?
## v0.1.0 release
- when trajectory plotting is implemented?
## Backlog
- use a polars dataframe for better efficiency - if that turns out to be a bottleneck. (!) Keep this in mind when using pandas-specific functions.
......@@ -31,6 +34,15 @@ Types of changes
## [Unreleased]
### Added
- prepare checkbox to toggle trajectories plotting
### Changed
- revised callbacks; separate the one big figure-callback into multiple callbacks, in a separate file `./src/callbacks.py
- tweak css for dropboxes
## v0.0.15 - 2025-03-26
### Added
......
......@@ -23,34 +23,6 @@ body {
padding: 15px;
}
.Select-control {
background-color: rgb(25, 25, 25) !important;
color: white;
}
.Select-value-label {
color: white !important;
}
.Select-menu-outer {
background-color: rgb(25, 25, 25);
color: white;
}
#langue_dropdown .Select-value-label {
color: rgb(37, 224, 37) !important;
}
#langue_dropdown .Select-control {
background-color: red !important;
color: white;
}
#langue_dropdown .Select-menu-outer {
background-color: rgb(25, 25, 25);
color: white;
}
.title-bar {
position: fixed;
top: 0;
......@@ -111,6 +83,25 @@ body {
}
}
/* ----- drop boxes ----- */
.Select-control {
background-color: var(--dark-bg);
}
.Select-control:hover {
background-color: var(--grey-bg);
}
.Select-value-label {
color: white !important;
}
.Select-menu-outer {
background-color: var(--dark-bg);
color: white;
}
.footer {
position: fixed;
bottom: 0;
......@@ -127,7 +118,6 @@ body {
font-size: 0.9rem;
}
/* Add some bottom margin to the main content to prevent overlap with footer */
.main-content {
margin-bottom: 20px;
......
# -*- coding: utf-8 -*-
#
# SPDX-FileCopyrightText: 2025 Florian Obersteiner <f.obersteiner@kit.edu>
#
# SPDX-License-Identifier: GPL-3.0-or-later
# -*- coding: utf-8 -*-
import logging as log
from pathlib import Path
import dash_bootstrap_components as dbc
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.callbacks import (
register_info_updaters,
register_map_plot,
register_timeseries_plot,
)
from src.config import DotDict, get_latest_semantic_version_tag
from src.data import load_from_disk, load_from_thredds
from src.layout import create_layout
from src.thredds import get_datasets
appconfig = DotDict.from_toml_file("./config/env.toml")
appconfig: DotDict = DotDict.from_toml_file("./config/env.toml")
appconfig.VERSION = get_latest_semantic_version_tag() or appconfig.VERSION
varconfig = pd.read_csv("./config/parms_units.csv", sep=";")
varconfig: pd.DataFrame = pd.read_csv("./config/parms_units.csv", sep=";")
varconfig = varconfig.set_index("VNAME")
airportsconfig = pd.read_csv("./config/CARIBIC2_Airports.csv", sep=";")
airportsconfig: pd.DataFrame = pd.read_csv("./config/CARIBIC2_Airports.csv", sep=";")
airportsconfig = airportsconfig.set_index("IATA")
# using separate variable declaration here to have a type annotation:
data: pd.DataFrame = pd.DataFrame()
ap_info: dict[int, str] = {}
data, ap_info = (
load_from_thredds(get_datasets(appconfig), appconfig, airportsconfig, True) # type: ignore
if appconfig.USE_THREDDS
else load_from_disk(Path("./testdata").glob("MS_*.nc"), appconfig, airportsconfig, True) # type: ignore
)
log.debug(f"Data loaded. Used THREDDS: {appconfig.USE_THREDDS}")
# fmt: off
app = Dash(
appconfig.TITLE,
......@@ -38,168 +52,13 @@ app.title = appconfig.TITLE
app.css.config.serve_locally = True
app.scripts.config.serve_locally = True
data, ap_info = (
load_from_thredds(get_datasets(appconfig), appconfig, airportsconfig, True) # type: ignore
if appconfig.USE_THREDDS
else load_from_disk(Path("./testdata").glob("MS_*.nc"), appconfig, airportsconfig, True) # type: ignore
)
log.debug(f"Data loaded. Used THREDDS: {appconfig.USE_THREDDS}")
# log.debug(f"Data size: {data.info()}")
app.layout = create_layout(data, appconfig)
server = app.server # for deployment via gunicorn / WSGI server
@app.callback(
[
Output("fig-map", "figure"),
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"),
],
)
def update_plots(selected_flight: int, primary_variable: str, secondary_variable: str):
filtered_df = data[data["flight_number"] == selected_flight]
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(
lon=filtered_df.loc[valid_indices, "lon"],
lat=filtered_df.loc[valid_indices, "lat"],
mode="markers",
marker={
"color": filtered_df.loc[valid_indices, primary_variable],
"colorscale": "Rainbow",
"colorbar": {
"x": 0.995,
"title": {"text": f"{primary_variable} {unit_primary}", "side": "right"},
},
"showscale": True,
},
connectgaps=False,
hoverinfo="text",
hovertext=[f"{v:.1f}" for v in filtered_df[primary_variable]],
name="Flight Path",
)
)
fig_map.add_trace( # marker to indicate departure airport
go.Scattermap(
lat=[filtered_df.lat.iloc[0]],
lon=[filtered_df.lon.iloc[0]],
mode="markers",
marker={"size": 15, "allowoverlap": True, "symbol": "airport", "angle": 55},
marker_symbol="airport",
text=["departure airport"],
hoverinfo="text",
name="departure",
)
)
fig_map.add_trace( # marker to indicate destination airport
go.Scattermap(
lat=[filtered_df.lat.iloc[-1]],
lon=[filtered_df.lon.iloc[-1]],
mode="markers",
marker={"size": 15, "allowoverlap": True, "symbol": "airport", "angle": 235},
text=["destination airport"],
hoverinfo="text",
name="destination",
)
)
fig_map.update_layout(
map={
"style": "outdoors",
"zoom": 1.7,
"center": {"lat": filtered_df.lat.mean(), "lon": filtered_df.lon.mean()},
},
margin={"l": 0, "r": 10, "t": 0, "b": 0},
showlegend=False,
)
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"], primary_variable],
mode="markers",
marker={
"size": 3,
"color": "blue",
"showscale": False,
},
showlegend=False,
)
)
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"], primary_variable],
mode="markers",
marker={
"size": 3,
"color": "red",
"showscale": False,
},
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",
margin={"l": 45, "r": 10, "t": 20, "b": 50},
legend={
"x": 0.01,
"y": 0.99,
"xanchor": "left",
"yanchor": "top",
"bgcolor": "rgba(255, 255, 255, 0.7)",
"bordercolor": "rgba(0, 0, 0, 0.2)",
"borderwidth": 1,
},
)
fig_ts.update_xaxes(title="UTC")
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],
lname_primary,
lname_secondary,
]
register_info_updaters(app, varconfig, ap_info)
register_map_plot(app, data, varconfig)
register_timeseries_plot(app, data, varconfig)
if __name__ == "__main__":
......
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
def register_info_updaters(app: Dash, var_info: pd.DataFrame, ap_info: dict[int, str]):
"""
Register callbacks for updating flight- and variable information.
html fields / ids to update:
- flight-dep-dest
- var-info
- second-var-info
"""
@app.callback(Output("flight-dep-dest", "children"), Input("flight-dropdown", "value"))
def set_flight_dep_dest(selected_flight: int) -> str:
return ap_info.get(selected_flight, "?!")
@app.callback(Output("var-info", "children"), Input("variable-dropdown", "value"))
def set_var_info(selected_variable: str) -> str:
return var_info.loc[selected_variable, :].Long_Name
@app.callback(Output("second-var-info", "children"), Input("second-variable-dropdown", "value"))
def set_second_var_info(secondary_variable: str) -> str:
return (
var_info.loc[secondary_variable, :].Long_Name
if secondary_variable in var_info.index
else ""
)
def register_map_plot(app: Dash, timeseries_data: pd.DataFrame, var_info: pd.DataFrame):
"""
Register callbacks for updating map plot.
html field / id to update:
- fig-plot
"""
@app.callback(
[Output("fig-map", "figure")],
[
Input("flight-dropdown", "value"),
Input("variable-dropdown", "value"),
Input("trajectories-checkbox", "value"),
],
)
def update_map_plot(selected_flight: int, primary_variable: str, show_trajectories: bool):
filtered_df = timeseries_data[timeseries_data["flight_number"] == selected_flight]
valid_indices = filtered_df[primary_variable].notna()
unit_primary = var_info.loc[primary_variable, :].Unit
if show_trajectories:
print("ON")
# try to find the trajectory file for selected flight
# show pop up if no trajectory file found ?
# make plots if trajectory file found
fig_map = go.Figure(
go.Scattermap(
lon=filtered_df.loc[valid_indices, "lon"],
lat=filtered_df.loc[valid_indices, "lat"],
mode="markers",
marker={
"color": filtered_df.loc[valid_indices, primary_variable],
"colorscale": "Rainbow",
"colorbar": {
"x": 0.995,
"title": {"text": f"{primary_variable} {unit_primary}", "side": "right"},
},
"showscale": True,
},
connectgaps=False,
hoverinfo="text",
hovertext=[f"{v:.1f}" for v in filtered_df[primary_variable]],
name="Flight Path",
)
)
fig_map.add_trace( # marker to indicate departure airport
go.Scattermap(
lat=[filtered_df.lat.iloc[0]],
lon=[filtered_df.lon.iloc[0]],
mode="markers",
marker={"size": 15, "allowoverlap": True, "symbol": "airport", "angle": 55},
marker_symbol="airport",
text=["departure airport"],
hoverinfo="text",
name="departure",
)
)
fig_map.add_trace( # marker to indicate destination airport
go.Scattermap(
lat=[filtered_df.lat.iloc[-1]],
lon=[filtered_df.lon.iloc[-1]],
mode="markers",
marker={"size": 15, "allowoverlap": True, "symbol": "airport", "angle": 235},
text=["destination airport"],
hoverinfo="text",
name="destination",
)
)
fig_map.update_layout(
map={
"style": "outdoors",
"zoom": 1.7,
"center": {"lat": filtered_df.lat.mean(), "lon": filtered_df.lon.mean()},
},
margin={"l": 0, "r": 10, "t": 0, "b": 0},
showlegend=False,
)
return [fig_map]
def register_timeseries_plot(app: Dash, timeseries_data: pd.DataFrame, var_info: pd.DataFrame):
"""
Register a callback for timeseries plot.
html field / id to update:
- fig_ts
"""
@app.callback(
[Output("fig-ts", "figure")],
[
Input("flight-dropdown", "value"),
Input("variable-dropdown", "value"),
Input("second-variable-dropdown", "value"),
],
)
def update_ts_plot(selected_flight: int, primary_variable: str, secondary_variable: str):
filtered_df = timeseries_data[timeseries_data["flight_number"] == selected_flight]
valid_indices = filtered_df[primary_variable].notna()
unit_primary = var_info.loc[primary_variable, :].Unit
unit_secondary = (
var_info.loc[secondary_variable, :].Unit
if secondary_variable in var_info.index
else "-"
)
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"], primary_variable],
mode="markers",
marker={
"size": 3,
"color": "blue",
"showscale": False,
},
showlegend=False,
)
)
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"], primary_variable],
mode="markers",
marker={
"size": 3,
"color": "red",
"showscale": False,
},
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",
margin={"l": 45, "r": 10, "t": 20, "b": 50},
legend={
"x": 0.01,
"y": 0.99,
"xanchor": "left",
"yanchor": "top",
"bgcolor": "rgba(255, 255, 255, 0.7)",
"bordercolor": "rgba(0, 0, 0, 0.2)",
"borderwidth": 1,
},
)
fig_ts.update_xaxes(title="UTC")
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_ts]
......@@ -107,7 +107,7 @@ def create_layout(df: pd.DataFrame, config: DotDict) -> list:
html.Label(
"",
id="flight-dep-dest",
style={"margin-left": "29px", "font-size": "12px"},
style={"margin-left": "12px", "font-size": "12px"},
),
],
style={
......@@ -115,30 +115,40 @@ def create_layout(df: pd.DataFrame, config: DotDict) -> list:
"flex-direction": "row",
"justify-content": "left",
"align-items": "center",
"margin-bottom": "8px",
},
),
html.Div(
id="variable-select",
children=[
html.Label(
"Variable:",
style={"font-weight": "bold", "margin-right": "20px"},
),
dcc.Dropdown(
id="variable-dropdown",
options=var_opts, # type: ignore
value=var_opts[var_sel_idx]["value"], # type: ignore
clearable=False,
searchable=True,
html.Div(
[
html.Label(
"Variable:",
style={"font-weight": "bold", "margin-right": "20px"},
),
dcc.Dropdown(
id="variable-dropdown",
options=var_opts, # type: ignore
value=var_opts[var_sel_idx]["value"], # type: ignore
clearable=False,
searchable=True,
style={
"display": "inline-block",
"vertical-align": "middle",
"width": "190px",
},
),
],
style={
"width": "310px",
"display": "inline-block",
"width": "190px",
},
),
html.Label(
"",
id="var-info",
style={"margin-left": "29px", "font-size": "12px"},
style={"font-size": "12px"},
),
],
style={
......@@ -146,37 +156,77 @@ def create_layout(df: pd.DataFrame, config: DotDict) -> list:
"flex-direction": "row",
"justify-content": "left",
"align-items": "center",
"margin-bottom": "8px",
},
),
html.Div(
id="second-variable-select",
children=[
html.Label(
"Secondary:",
style={"font-weight": "bold", "margin-right": "10px"},
html.Div(
[
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",
"vertical-align": "middle",
"width": "190px",
},
),
],
style={
"width": "310px",
"display": "inline-block",
},
),
dcc.Dropdown(
id="second-variable-dropdown",
options=([{"value": "None", "label": "(None)"}] + var_opts), # type: ignore
value="None", # type: ignore
clearable=False,
searchable=True,
html.Div(
[
html.Label(
"",
id="second-var-info",
style={"font-size": "12px"},
),
],
style={
"width": "40%",
"display": "inline-block",
"width": "190px",
"vertical-align": "middle",
},
),
html.Label(
"",
id="second-var-info",
style={"margin-left": "29px", "font-size": "12px"},
html.Div(
[
html.Label(
"Show trajectories",
style={"margin-right": "10px", "vertical-align": "middle"},
),
dcc.Checklist(
id="trajectories-checkbox",
options=[{"label": "", "value": 1}],
inline=True,
style={"display": "inline-block", "vertical-align": "middle"},
),
],
style={
"width": "30%",
"display": "inline-block",
"text-align": "right",
"vertical-align": "middle",
},
),
],
style={
"display": "flex",
"flex-direction": "row",
"justify-content": "left",
# "flex-direction": "row",
# "justify-content": "left",
"align-items": "center",
"width": "100%",
},
),
dcc.Graph(
......
......@@ -80,7 +80,7 @@ wheels = [
[[package]]
name = "caribic-dash"
version = "0.0.13"
version = "0.0.16"
source = { virtual = "." }
dependencies = [
{ name = "dash" },
......@@ -554,16 +554,16 @@ wheels = [
[[package]]
name = "protobuf"
version = "6.30.1"
version = "6.30.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/55/de/8216061897a67b2ffe302fd51aaa76bbf613001f01cd96e2416a4955dd2b/protobuf-6.30.1.tar.gz", hash = "sha256:535fb4e44d0236893d5cf1263a0f706f1160b689a7ab962e9da8a9ce4050b780", size = 429304 }
sdist = { url = "https://files.pythonhosted.org/packages/c8/8c/cf2ac658216eebe49eaedf1e06bc06cbf6a143469236294a1171a51357c3/protobuf-6.30.2.tar.gz", hash = "sha256:35c859ae076d8c56054c25b59e5e59638d86545ed6e2b6efac6be0b6ea3ba048", size = 429315 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/83/f6/28460c49a8a93229e2264cd35fd147153fb524cbd944789db6b6f3cc9b13/protobuf-6.30.1-cp310-abi3-win32.whl", hash = "sha256:ba0706f948d0195f5cac504da156d88174e03218d9364ab40d903788c1903d7e", size = 419150 },
{ url = "https://files.pythonhosted.org/packages/96/82/7045f5b3f3e338a8ab5852d22ce9c31e0a40d8b0f150a3735dc494be769a/protobuf-6.30.1-cp310-abi3-win_amd64.whl", hash = "sha256:ed484f9ddd47f0f1bf0648806cccdb4fe2fb6b19820f9b79a5adf5dcfd1b8c5f", size = 431007 },
{ url = "https://files.pythonhosted.org/packages/b0/b6/732d04d0cdf457d05b7cba83ae73735d91ceced2439735b4500e311c44a5/protobuf-6.30.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:aa4f7dfaed0d840b03d08d14bfdb41348feaee06a828a8c455698234135b4075", size = 417579 },
{ url = "https://files.pythonhosted.org/packages/fc/22/29dd085f6e828ab0424e73f1bae9dbb9e8bb4087cba5a9e6f21dc614694e/protobuf-6.30.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:47cd320b7db63e8c9ac35f5596ea1c1e61491d8a8eb6d8b45edc44760b53a4f6", size = 317319 },
{ url = "https://files.pythonhosted.org/packages/26/10/8863ba4baa4660e3f50ad9ae974c47fb63fa6d4089b15f7db82164b1c879/protobuf-6.30.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e3083660225fa94748ac2e407f09a899e6a28bf9c0e70c75def8d15706bf85fc", size = 316213 },
{ url = "https://files.pythonhosted.org/packages/a1/d6/683a3d470398e45b4ad9b6c95b7cbabc32f9a8daf454754f0e3df1edffa6/protobuf-6.30.1-py3-none-any.whl", hash = "sha256:3c25e51e1359f1f5fa3b298faa6016e650d148f214db2e47671131b9063c53be", size = 167064 },
{ url = "https://files.pythonhosted.org/packages/be/85/cd53abe6a6cbf2e0029243d6ae5fb4335da2996f6c177bb2ce685068e43d/protobuf-6.30.2-cp310-abi3-win32.whl", hash = "sha256:b12ef7df7b9329886e66404bef5e9ce6a26b54069d7f7436a0853ccdeb91c103", size = 419148 },
{ url = "https://files.pythonhosted.org/packages/97/e9/7b9f1b259d509aef2b833c29a1f3c39185e2bf21c9c1be1cd11c22cb2149/protobuf-6.30.2-cp310-abi3-win_amd64.whl", hash = "sha256:7653c99774f73fe6b9301b87da52af0e69783a2e371e8b599b3e9cb4da4b12b9", size = 431003 },
{ url = "https://files.pythonhosted.org/packages/8e/66/7f3b121f59097c93267e7f497f10e52ced7161b38295137a12a266b6c149/protobuf-6.30.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:0eb523c550a66a09a0c20f86dd554afbf4d32b02af34ae53d93268c1f73bc65b", size = 417579 },
{ url = "https://files.pythonhosted.org/packages/d0/89/bbb1bff09600e662ad5b384420ad92de61cab2ed0f12ace1fd081fd4c295/protobuf-6.30.2-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:50f32cc9fd9cb09c783ebc275611b4f19dfdfb68d1ee55d2f0c7fa040df96815", size = 317319 },
{ url = "https://files.pythonhosted.org/packages/28/50/1925de813499546bc8ab3ae857e3ec84efe7d2f19b34529d0c7c3d02d11d/protobuf-6.30.2-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:4f6c687ae8efae6cf6093389a596548214467778146b7245e886f35e1485315d", size = 316212 },
{ url = "https://files.pythonhosted.org/packages/e5/a1/93c2acf4ade3c5b557d02d500b06798f4ed2c176fa03e3c34973ca92df7f/protobuf-6.30.2-py3-none-any.whl", hash = "sha256:ae86b030e69a98e08c77beab574cbcb9fff6d031d57209f574a5aea1445f4b51", size = 167062 },
]
[[package]]
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment