diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..ab996e15dcc31e21bffc58cb941cf19c4760a814 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,16 @@ +Dockerfile +.dockerignore +.git +.gitignore +__pycache__/ +*.pyc +*.pyo +*.pyd +venv/ +.venv/ +env/ +*.env +*.db +*.sqlite3 +instance/ +experiments/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..1287b3caf432336f776902fa5db21ac879ae33e0 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,55 @@ +stages: + - test + # - build_and_deploy + +variables: + UV_VERSION: 0.6 + PYTHON_VERSION: 3.12 + BASE_LAYER: bookworm-slim + # GitLab CI creates a separate mountpoint for the build directory, + # so we need to copy instead of using hard links. + UV_LINK_MODE: copy + +# Only run the pipeline for tag pushes matching the pattern "v*-pre" +workflow: + rules: + - if: $CI_COMMIT_TAG =~ /^v.*-pre/ + +# Run tests... +test: + stage: test + # image: python:3.12 + # script: + # - pip install pip --upgrade + # - pip install -r requirements.txt + # - python -m pytest . -v + image: ghcr.io/astral-sh/uv:$UV_VERSION-python$PYTHON_VERSION-$BASE_LAYER + script: + - uv sync + - uv run pytest . -v + - uv cache prune --ci + only: + - tags +# +# +# # Build and deploy the Docker container directly on the server +# build_and_deploy: +# stage: build_and_deploy +# script: +# - echo "Starting build and deployment of $APP_NAME version $CI_COMMIT_TAG" +# # Build the Docker image locally +# - docker build -t $APP_NAME:$CI_COMMIT_TAG . +# # Stop and remove existing container if it exists +# - | +# if docker ps -a | grep -q $APP_NAME; then +# docker stop $APP_NAME +# docker rm $APP_NAME +# fi +# # Start the new container +# - docker run -d --name $APP_NAME -p $HOST_PORT:$CONTAINER_PORT --restart unless-stopped $APP_NAME:$CI_COMMIT_TAG +# - echo "Build and deployment complete!" +# only: +# - tags +# # This ensures the job runs on the specific runner on your server +# tags: +# - deployment-server diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f99247d9d21b9b09089c91ebdfca8aa122ac176..03a3804b70734d907a551537d42d021b329861b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ Types of changes ## TODOs -- set log level via config +- disable the "show trajectories" checkbox if no data - 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? @@ -26,6 +26,7 @@ Types of changes ## Backlog +- containerize & auto-deploy - use a polars dataframe or [fireducks](https://fireducks-dev.github.io/) for better performance - if that turns out to be a bottleneck. (!) Keep this in mind when using pandas-specific functions. - add tests - add pre-commit @@ -35,6 +36,18 @@ Types of changes ## [Unreleased] +### Added + +- trajectory data info +- dockerfile and dockerignore + +### Changed + +- select a secondary parameter by default +- same bg color for both plots +- show map attribution on top of map instead of hiding it below timeseries plot +- put 'show trajectories' button below map plot + ## v0.0.18, 2025-03-31 ### Fixed diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..84a6d16e1ab6a1eac99196da40bac1732d8000ae --- /dev/null +++ b/Dockerfile @@ -0,0 +1,42 @@ +# ### +# generated with Gemini 2.5 pro/experimental +# ### +# +# Use an official Python runtime as a parent image +# Choose a specific version for reproducibility, -slim versions are smaller +FROM python:3.12-slim + +# Set environment variables +# Prevents Python from writing pyc files to disc (optional) +ENV PYTHONDONTWRITEBYTECODE=1 +# Ensures Python output is sent straight to terminal (useful for logs) +ENV PYTHONUNBUFFERED=1 + +# Set the working directory in the container +WORKDIR /app + +# Copy the requirements file into the container at /app +COPY requirements.txt . + +# Install any needed packages specified in requirements.txt +# --no-cache-dir reduces image size +# --upgrade pip ensures you have the latest pip +# RUN pip install --no-cache-dir --upgrade pip && \ +# pip install --no-cache-dir -r requirements.txt + +# uv variant: +RUN pip install --no-cache-dir uv +RUN uv pip install --no-cache -r requirements.txt --system + +# Copy the rest of the application code into the container at /app +# Assumes your Dockerfile is in the root of your project directory +COPY . . + +# Make port 19000 available to the world outside this container +EXPOSE 19000 + +# Define the command to run your app using Gunicorn +# Binds Gunicorn to 0.0.0.0 so it's accessible from outside the container +# 'app:server' assumes your Dash app instance is 'app' in 'app.py' +# Gunicorn needs the underlying Flask server instance, typically 'app.server' +CMD ["gunicorn", "-b", ":19000", "main:server", "-w", "2"] diff --git a/assets/custom.css b/assets/custom.css index 5d16dcf101f4c7c8c9c08d3feb8f3f08b165c6e0..bb16e74aab7b03354fb5ec076b260322dbce01ec 100644 --- a/assets/custom.css +++ b/assets/custom.css @@ -1,124 +1,160 @@ :root { - --neon-pink: #ff71ce; - --neon-blue: #01cdfe; - --neon-green: #05ff81; - --neon-purple: #b967ff; - --neon-yellow: #fdfb96; - --grey-bg: #404040; - --grey-bg-bright: #d4d4d4; - --dark-bg: #1a1a1a; - --darker-bg: #121212; - --grid-color: rgba(33, 33, 99, 0.8); + --neon-pink: #ff71ce; + --neon-blue: #01cdfe; + --neon-green: #05ff81; + --neon-purple: #b967ff; + --neon-yellow: #fdfb96; + --grey-bg: #404040; + --grey-bg-bright: #d4d4d4; + --dark-bg: #1a1a1a; + --darker-bg: #121212; + --grid-color: rgba(33, 33, 99, 0.8); } body { - background-color: var(--dark-bg); - background-image: - linear-gradient(0deg, var(--grid-color) 1px, transparent 1px), - linear-gradient(90deg, var(--grid-color) 1px, transparent 1px); - background-size: 20px 20px; - color: white; - font-family: 'VT323', 'Courier New', monospace; - margin-top: 70px; - padding: 15px; + background-color: var(--dark-bg); + background-image: linear-gradient( + 0deg, + var(--grid-color) 1px, + transparent 1px + ), + linear-gradient(90deg, var(--grid-color) 1px, transparent 1px); + background-size: 20px 20px; + color: white; + font-family: "VT323", "Courier New", monospace; + margin-top: 70px; + padding: 15px; } .title-bar { - position: fixed; - top: 0; - left: 0; - width: 100%; - background-color: var(--grey-bg); - padding: 10px 20px; - display: flex; - justify-content: space-between; - align-items: center; - border-bottom: 1px solid var(--grid-color); - z-index: 1000; - box-sizing: border-box; - height: 60px; + position: fixed; + top: 0; + left: 0; + width: 100%; + background-color: var(--grey-bg); + padding: 10px 20px; + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid var(--grid-color); + z-index: 1000; + box-sizing: border-box; + height: 60px; } .title-text { - color: white; - font-size: 1.4rem; - font-weight: bold; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - max-width: 70%; + color: white; + font-size: 1.4rem; + font-weight: bold; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 70%; } .logo-container { - display: flex; - align-items: center; + display: flex; + align-items: center; } .header-logo { - height: 40px; - background-color: var(--grey-bg-bright); - border-radius: 4px; + height: 40px; + background-color: var(--grey-bg-bright); + border-radius: 4px; } /* Logo wrapper to create the background box */ .logo-wrapper { - display: flex; - align-items: center; - justify-content: center; - background-color: var(--grey-bg-bright); - border-radius: 0.4rem; - padding: 0.3rem 0.7rem 0.3rem 0.7rem; - margin-left: 0.5rem; - border: 1px solid var(--grid-color); + display: flex; + align-items: center; + justify-content: center; + background-color: var(--grey-bg-bright); + border-radius: 0.4rem; + padding: 0.3rem 0.7rem 0.3rem 0.7rem; + margin-left: 0.5rem; + border: 1px solid var(--grid-color); } /* Responsive adjustments */ @media (max-width: 600px) { - .logo-wrapper { - padding: 0.3rem; - } + .logo-wrapper { + padding: 0.3rem; + } - .header-logo { - height: 2rem; - } + .header-logo { + height: 2rem; + } } /* ----- drop boxes ----- */ .Select-control { - background-color: var(--dark-bg); + background-color: var(--dark-bg); } .Select-control:hover { - background-color: var(--grey-bg); + background-color: var(--grey-bg); } .Select-value-label { - color: white !important; + color: white !important; } .Select-menu-outer { - background-color: var(--dark-bg); - color: white; + background-color: var(--dark-bg); + color: white; } +/* The following styles ensure the attribution tag floats above the map and should be removed if the issue is fixed in a future plotly release. */ +.maplibregl-ctrl-bottom-right { + bottom: 5px; + left: 10px; +} + +.maplibregl-ctrl-bottom-left, +.maplibregl-ctrl-bottom-right, +.maplibregl-ctrl-top-left, +.maplibregl-ctrl-top-right { + pointer-events: none; + position: absolute; + z-index: 2; +} + +.maplibregl-map { + font: + 12px/20px Helvetica Neue, + Arial, + Helvetica, + sans-serif; + font-family: + Helvetica Neue, + Arial, + Helvetica, + sans-serif; + color: var(--grey-bg); +} + +/*This is necessary to remove spacing below the canvas.*/ +.maplibregl-canvas { + display: block; +} .footer { - position: fixed; - bottom: 0; - left: 0; - width: 100%; - background-color: var(--dark-bg); - padding: 10px 20px; - display: flex; - justify-content: space-between; - align-items: center; - border-top: 1px solid var(--grid-color); - z-index: 1000; - box-sizing: border-box; - font-size: 0.9rem; + position: fixed; + bottom: 0; + left: 0; + width: 100%; + background-color: var(--dark-bg); + padding: 10px 20px; + display: flex; + justify-content: space-between; + align-items: center; + border-top: 1px solid var(--grid-color); + z-index: 1000; + box-sizing: border-box; + font-size: 0.9rem; } /* Add some bottom margin to the main content to prevent overlap with footer */ .main-content { - margin-bottom: 20px; + margin-bottom: 20px; } diff --git a/pyproject.toml b/pyproject.toml index a39da93213033b51d8c9f43304a9df20d9a01d71..3d11354b19e768c27f9e0bc77a39a97098ae51a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,8 @@ [project] name = "caribic-dash" -version = "0.0.18" +version = "0.0.19" description = "IRISCC dashboard with CARIBIC data" +authors = [{ name = "Florian Obersteiner", email = "f.obersteiner@kit.edu" }] readme = "README.md" requires-python = ">=3.12" classifiers = [ diff --git a/requirements.txt b/requirements.txt index 3944ec763444c9efd6edb63791b188d3b174f490..ae8e7bd430f3a2504a22ced29bb43765d698292c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -36,7 +36,7 @@ markupsafe==3.0.2 # via # jinja2 # werkzeug -narwhals==1.32.0 +narwhals==1.33.0 # via plotly nest-asyncio==1.6.0 # via dash @@ -63,7 +63,7 @@ plotly==6.0.1 # via # caribic-dash (pyproject.toml) # dash -protobuf==6.30.1 +protobuf==6.30.2 # via siphon python-dateutil==2.9.0.post0 # via pandas @@ -99,7 +99,7 @@ werkzeug==3.0.6 # via # dash # flask -xarray==2025.3.0 +xarray==2025.3.1 # via caribic-dash (pyproject.toml) zipp==3.21.0 # via importlib-metadata diff --git a/src/callbacks.py b/src/callbacks.py index e0779e99857a5833752e83e74e1fdfe0f9dcdfb3..69024eb7bc7c0f4230921f1191f04876827307e9 100644 --- a/src/callbacks.py +++ b/src/callbacks.py @@ -68,8 +68,6 @@ def register_map_plot( # try to find the trajectory file for selected flight if trajs := trajectory_data.get(selected_flight): # make plots if trajectory file found - print(trajs["latitude"].dtype.byteorder) - print(trajs["longitude"].dtype.byteorder) for i in range(trajs.trajectory.size): try: fig_map.add_trace( @@ -145,6 +143,7 @@ def register_map_plot( }, margin={"l": 0, "r": 10, "t": 0, "b": 0}, showlegend=False, + paper_bgcolor="#fbf8f3", ) return [fig_map, show_trajdata_warn] @@ -220,7 +219,7 @@ def register_timeseries_plot(app: Dash, timeseries_data: pd.DataFrame, var_info: ) fig_ts.update_layout( - paper_bgcolor="#e8e8e8", + paper_bgcolor="#fbf8f3", margin={"l": 45, "r": 10, "t": 20, "b": 50}, legend={ "x": 0.01, diff --git a/src/layout.py b/src/layout.py index b76dca336c37d4bdb89ac25be19a8f9d77796fd2..ffa7f039b77858048e6af22ed1499868ef6c81f8 100644 --- a/src/layout.py +++ b/src/layout.py @@ -9,7 +9,7 @@ from dash import dcc, html from .config import DotDict -def create(df: pd.DataFrame, config: DotDict) -> list: +def create(df: pd.DataFrame, config: DotDict) -> list[html.Header]: """ Create the layout for the dashboard. @@ -40,6 +40,8 @@ def create(df: pd.DataFrame, config: DotDict) -> list: if vo["label"] == "ACN": var_sel_idx = idx + var2_sel_idx = 3 + return [ html.Header( [ @@ -171,7 +173,7 @@ def create(df: pd.DataFrame, config: DotDict) -> list: dcc.Dropdown( id="second-variable-dropdown", options=([{"value": "None", "label": "(None)"}] + var_opts), # type: ignore - value="None", # type: ignore + value=var_opts[var2_sel_idx]["value"], # type: ignore clearable=False, searchable=True, style={ @@ -200,26 +202,6 @@ def create(df: pd.DataFrame, config: DotDict) -> list: "vertical-align": "middle", }, ), - 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": "25%", - "display": "inline-block", - "text-align": "right", - "vertical-align": "middle", - }, - ), ], style={ "display": "flex", @@ -247,6 +229,44 @@ def create(df: pd.DataFrame, config: DotDict) -> list: ) ), ), + html.Div( + children=[ + html.Div( + html.Label( + "Trajectories: KNMI Trajectory Model TRAJKS, driven by ECMWF re-analysis data", + id="trajectory-info", + style={"margin-left": "15px", "font-size": "12px"}, + ) + ), + html.Div( + [ + html.Label( + "Show trajectories", + style={"margin-right": "10px"}, + ), + dcc.Checklist( + id="trajectories-checkbox", + options=[{"label": "", "value": 1}], + inline=True, + style={"display": "inline-block", "vertical-align": "right"}, + ), + ], + style={ + "text-align": "right", + "vertical-align": "right", + }, + ), + ], + style={ + "display": "flex", + "justify-content": "space-between", + "align-items": "center", + "padding-top": "5px", + "padding-bottom": "10px", + "padding-left": "5px", + "padding-right": "15px", + }, + ), dcc.Loading( id="loading-ts", type="circle", # Options: "graph", "cube", "circle", "dot", or "default" @@ -263,10 +283,17 @@ def create(df: pd.DataFrame, config: DotDict) -> list: ), ), ), - html.Label( - "BB flagging: BB influence if ACN > (145 ppt + 3*ACN_prc)", - id="flagging-info", - style={"margin-left": "15px", "font-size": "12px"}, + html.Div( + children=[ + html.Div( + html.Label( + "BB flagging: BB influence if ACN > (145 ppt + 3*ACN_prc)", + id="flagging-info", + style={"margin-left": "15px", "font-size": "12px"}, + ) + ), + ], + style={"padding-top": "5px", "padding-bottom": "10px"}, ), ] ), @@ -304,7 +331,7 @@ def create(df: pd.DataFrame, config: DotDict) -> list: html.A( [ html.I(className="fab fa-gitlab", style={"marginRight": "5px"}), - f"src ({config.VERSION})", + f"src {config.VERSION}", ], href=config.CODEURL, target="_blank", diff --git a/uv.lock b/uv.lock index a1b94f181effeb420306e01f8a305085dae17839..aebf8af8a967201619bd5222105b9e6b835a5c0c 100644 --- a/uv.lock +++ b/uv.lock @@ -80,7 +80,7 @@ wheels = [ [[package]] name = "caribic-dash" -version = "0.0.18" +version = "0.0.19" source = { virtual = "." } dependencies = [ { name = "dash" }, @@ -392,11 +392,11 @@ wheels = [ [[package]] name = "narwhals" -version = "1.32.0" +version = "1.33.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c1/e5/aa97891440bf6bc4239e9b918d89fea58eb87dff1d8d14a69b45ef677e66/narwhals-1.32.0.tar.gz", hash = "sha256:bd0aa41434737adb4b26f8593f3559abc7d938730ece010fe727b58bc363580d", size = 258915 } +sdist = { url = "https://files.pythonhosted.org/packages/85/fd/484aa8bb557f97a1781f38c78b79f795a2fa320e4165c4230f679937d1e8/narwhals-1.33.0.tar.gz", hash = "sha256:6233d2457debf4b5fe4a1da54530c6fe2d84326f4a8e3bca35bbbff580a347cb", size = 262554 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/96/79a6168dc7a5098066e097c01a45d01608c8df6552dfb92a2676ce623186/narwhals-1.32.0-py3-none-any.whl", hash = "sha256:8bdbf3f76155887412eea04b0b06303856ac1aa3d9e8bda5b5e54612855fa560", size = 320073 }, + { url = "https://files.pythonhosted.org/packages/41/c1/e9bc6b67c774e7c1f939c91ea535f18f7644fedc61b20d6baa861ad52b34/narwhals-1.33.0-py3-none-any.whl", hash = "sha256:f653319112fd121a1f1c18a40cf70dada773cdacfd53e62c2aa0afae43c17129", size = 322750 }, ] [[package]]