diff --git a/.gitignore b/.gitignore index cf03591..65bfa33 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,7 @@ db/schema.prisma # svg backend/routes/processData/results_svg/*.svg .DS_Store +SEAS5_CHANGES.md +LOCAL_DOCKER_SETUP.md +.gitignore +.gitignore diff --git a/backend/index.py b/backend/index.py index 5e195c2..b296a4c 100644 --- a/backend/index.py +++ b/backend/index.py @@ -382,7 +382,11 @@ def get_uncertainty_svg(): cellID = -1 if request.method == "GET": filename = request.args["filename"] - cellID = int(request.args["cellID"]) + raw_cell_id = request.args.get("cellID") + try: + cellID = int(raw_cell_id) if raw_cell_id and raw_cell_id != "NaN" else -1 + except (ValueError, TypeError): + cellID = -1 filename = str(cellID) + "_" + filename if not filename or not filename.endswith(".svg"): return jsonify({"ERROR": "Invalid or missing filename"}), 400 @@ -399,7 +403,16 @@ def get_uncertainty_svg(): # Derive which plot to generate from the requested filename original_filename = request.args["filename"] if "climate_forecast" in original_filename: - create_ENSO_suitability_visualizations(cell_id=cellID) + # Optional: caller specifies which seas5_forecast_* table to read + # and which forecast month to highlight (e.g. "aug") + dataset = request.args.get("dataset") + month = request.args.get("month") + if dataset: + create_ENSO_suitability_visualizations( + cell_id=cellID, dataset_template=dataset, active_month=month + ) + else: + create_ENSO_suitability_visualizations(cell_id=cellID) else: if "calibration" in original_filename: plot_type = "calibration" diff --git a/backend/routes/processData/ensoSuitability.py b/backend/routes/processData/ensoSuitability.py index fac45ed..da96b8e 100644 --- a/backend/routes/processData/ensoSuitability.py +++ b/backend/routes/processData/ensoSuitability.py @@ -1,10 +1,12 @@ """ Backend route handler for ENSO Suitability static visualization generator. -Generates static SVG/PNG plots containing 4 climatic factor line charts (ERA5 vs SEAS5) -and monthly mean delta bar charts for a selected grid cell. +Generates static SVG/PNG plots containing 5 panels (4 climatic factors + habitat +suitability) with ERA5 2000-2025 reference (mean ±1 std) and SEAS5 forecast for a +selected grid cell from the seas5_forecast_* database tables. """ import numpy as np +import re import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt @@ -18,11 +20,32 @@ _SVG_DIR = Path(__file__).resolve().parent / "results_svg" +# Climate variables and their display labels / units +CLIMATE_VARS = [ + ("t2m", "Temperature (2m) (°C)"), + ("d2m", "Dewpoint Temperature (°C)"), + ("si10", "Wind Speed (m/s)"), + ("tp", "Total Precipitation (m/day)"), +] -def cleanup_climate_forecast_svgs(out_dir: Path | str | None = None, pattern: str = "*_climate_forecast_cell.svg"): - """ - Clean up previously generated SVG plot files from the output directory. - """ +# Suitability display label +SUITABILITY_LABEL = "Habitat Suitability" + +# Month labels for display +MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] + +# Color scheme +COLOR_REF_LINE = '#2b5c8f' +COLOR_REF_BAND = '#2b5c8f' +COLOR_FC_MARK = '#e04d39' +COLOR_DELTA_POS = '#e04d39' +COLOR_DELTA_NEG = '#2b5c8f' + + +def cleanup_climate_forecast_svgs(out_dir: Path | str | None = None, + pattern: str = "*_climate_forecast_cell.svg"): + """Clean up previously generated SVG plot files from the output directory.""" out_path = Path(out_dir).resolve() if out_dir else _SVG_DIR if not out_path.exists(): return @@ -37,10 +60,9 @@ def cleanup_climate_forecast_svgs(out_dir: Path | str | None = None, pattern: st print(f"Cleaned up {removed_count} old climate forecast SVG file(s) in {out_path}") -def _save_and_cleanup(fig, out_path: Path, filename: str, pattern: str = "*_climate_forecast_cell.svg"): - """ - Remove previous SVGs matching pattern, save new figure, and close plot context. - """ +def _save_and_cleanup(fig, out_path: Path, filename: str, + pattern: str = "*_climate_forecast_cell.svg"): + """Remove previous SVGs matching pattern, save new figure, and close plot context.""" for old in out_path.glob(pattern): try: old.unlink(missing_ok=True) @@ -53,83 +75,242 @@ def _save_and_cleanup(fig, out_path: Path, filename: str, pattern: str = "*_clim return target +# ============================================================================== +# Database query helpers +# ============================================================================== + +# Sanitized DB column names of the consolidated forecast tables +# (CSV header -> DB name, see parseCSVdata.sanitize_names): +# "Referenz: Jan 2000-2025" -> referenz_jan_2000_2025 +# "Referenz: Jan 2000-2025 (std)" -> referenz_jan_2000_2025_(std) +# "Forecast: Jan 2027" -> forecast_jan_2027 +_FC_PROB_RE = re.compile(r"^forecast_([a-z]{3})_(\d{4})$") +_REF_PROB_RE = re.compile(r"^referenz_([a-z]{3})_(\d{4}_\d{4})$") + +# lowercase month abbreviation -> 1-based calendar month index +_MONTH_IDX = {mon.lower(): i + 1 for i, mon in enumerate(MONTHS)} + + +def _fetch_forecast_cell_row(cell_id: int, table_name: str) -> dict | None: + """Fetch a single grid-cell row from a seas5_forecast_* table by primary key. + + Uses SELECT * — the consolidated tables carry all reference and forecast + columns, so no explicit column list has to be maintained. + """ + query = psycopg_sql.SQL("SELECT * FROM {tbl} WHERE {id} = {val}").format( + tbl=psycopg_sql.Identifier(table_name), + id=psycopg_sql.Identifier("id"), + val=psycopg_sql.Literal(cell_id), + ) + with psycopg.connect(**get_db_connection_params()) as conn: + with conn.cursor() as cur: + cur.execute(query) + if cur.description is None: + return None + columns = [d[0] for d in cur.description] + row = cur.fetchone() + return dict(zip(columns, row)) if row else None + + +def _forecast_prob_items(row: dict) -> list[tuple[int, int, str]]: + """Find the forecast suitability columns (forecast_{mon}_{year}) in the row. + + Returns chronologically sorted (year, month_idx_1based, column_name) tuples. + """ + items = [] + for key in row.keys(): + m = _FC_PROB_RE.fullmatch(key) + if m and m.group(1) in _MONTH_IDX: + items.append((int(m.group(2)), _MONTH_IDX[m.group(1)], key)) + return sorted(items) + + +def _build_ref_series(row: dict, prefix: str, length: int = 12): + """Extract mean and std arrays for a reference variable across 12 calendar months.""" + mean = np.full(length, np.nan) + std = np.full(length, np.nan) + for m in range(1, length + 1): + mean_col = f"{prefix}_ref_{m:02d}_mean" + std_col = f"{prefix}_ref_{m:02d}_std" + if mean_col in row: + val = row[mean_col] + mean[m - 1] = float(val) if val is not None else np.nan + if std_col in row: + val = row[std_col] + std[m - 1] = float(val) if val is not None else np.nan + return mean, std + + +def _build_fc_series(row: dict, prefix: str, fc_labels: list[str]): + """Extract forecast values for the given variable from {prefix}_fc_YYYY_MM columns.""" + vals = [] + for label in fc_labels: + col = f"{prefix}_fc_{label}" + if col in row: + val = row[col] + vals.append(float(val) if val is not None else np.nan) + else: + vals.append(np.nan) + return np.array(vals) + + +def _build_ref_suitability_series(row: dict) -> tuple[np.ndarray, np.ndarray]: + """Extract reference suitability mean/std per calendar month (Jan..Dec). + + Columns: referenz_{mon}_{period} and referenz_{mon}_{period}_(std), + where the period (e.g. "2000_2025") is discovered from the row. + """ + mean = np.full(12, np.nan) + std = np.full(12, np.nan) + period = None + for key in row.keys(): + m = _REF_PROB_RE.fullmatch(key) + if m: + period = m.group(2) + break + if period is None: + return mean, std + for i, mon in enumerate(MONTHS): + lc = mon.lower() + mean_col = f"referenz_{lc}_{period}" + std_col = f"referenz_{lc}_{period}_(std)" + if mean_col in row: + val = row[mean_col] + mean[i] = float(val) if val is not None else np.nan + if std_col in row: + val = row[std_col] + std[i] = float(val) if val is not None else np.nan + return mean, std + + +# ============================================================================== +# Main visualization generator +# ============================================================================== + def create_ENSO_suitability_visualizations( cell_id: int, out_dir: Path | str | None = None, - dataset_template: str = "t_2024_monthly_mean_{month}_ocsvm_aegypti_predictions_2023_mod_sim", + dataset_template: str = "seas5_forecast_albopictus_habitat_probability", + active_month: str | None = None, ) -> Path: """ - Generate static plot with 4 climate factors (Temperature, Dewpoint Temp, Wind Speed, Total Precipitation) - line charts (ERA5 historical mean ±1 std vs SEAS5 forecast) and delta bar charts. + Generate static plot with 5 panels: 4 climate factors + habitat suitability. + Each panel shows ERA5 2000-2025 reference (blue line ±1 std band) and + SEAS5 forecast points (orange markers) for the selected grid cell. """ out_path = Path(out_dir).resolve() if out_dir else _SVG_DIR out_path.mkdir(parents=True, exist_ok=True) filename = f"{cell_id}_climate_forecast_cell.svg" - # Months: Jan - Dec - months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] - forecast_months = ['Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] - - # ── Synthetic Data Generation (Placeholder until DB data connected) ── - np.random.seed(cell_id % 10000) - - # 1. Temperature (2m) (°C) - temp_ref = 20 - 5 * np.cos(np.linspace(0, 2*np.pi, 12)) - temp_fc = temp_ref.copy() - temp_fc[6:] += np.random.uniform(0.5, 1.8, 6) - - # 2. Dewpoint Temperature (°C) - dew_ref = 15 - 4 * np.cos(np.linspace(0, 2*np.pi, 12)) - dew_fc = dew_ref.copy() - dew_fc[6:] += np.random.uniform(0.2, 0.9, 6) - - # 3. Wind Speed (m/s) - wind_ref = 2.5 + 0.3 * np.sin(np.linspace(0, 2*np.pi, 12)) - wind_fc = wind_ref.copy() - wind_fc[6:] += np.random.uniform(-0.3, 0.1, 6) - - # 4. Total Precipitation per Day (m/day) - precip_ref = 0.005 + 0.002 * np.sin(np.linspace(0, 2*np.pi, 12)) - precip_fc = precip_ref.copy() - precip_fc[6:] += np.random.uniform(-0.001, 0.001, 6) - - factors = [ - ("Temperature (2m) (°C)", temp_ref, temp_fc, "#d95f02", "#1b9e77"), - ("Dewpoint Temperature (°C)", dew_ref, dew_fc, "#e7298a", "#7570b3"), - ("Wind Speed (m/s)", wind_ref, wind_fc, "#e6ab02", "#66a61e"), - ("Total Precipitation (m/day)", precip_ref, precip_fc, "#a6761d", "#666666"), - ] + # ── Fetch real data from DB ── + row = _fetch_forecast_cell_row(cell_id, dataset_template) + if row is None: + # Fallback: generate a "no data" placeholder SVG + fig, ax = plt.subplots(figsize=(20, 5)) + ax.text(0.5, 0.5, f"No data for cell_id={cell_id}\ntable={dataset_template}", + transform=ax.transAxes, ha='center', va='center', fontsize=14) + target = out_path / filename + fig.savefig(target, format='svg', bbox_inches='tight') + plt.close(fig) + return target + + # forecast_{mon}_{year} columns, chronological — fc_labels feed the + # climate series ({var}_fc_YYYY_MM), fc_months the X-axis positions + fc_items = _forecast_prob_items(row) + fc_labels = [f"{y}_{m:02d}" for y, m, _ in fc_items] + fc_months = [m for _, m, _ in fc_items] # 1-based calendar months + + # Build data series for each of the 5 panels + panels: list[tuple[str, np.ndarray, np.ndarray, np.ndarray, list[int]]] = [] + + # 4 climate variables + for var, label in CLIMATE_VARS: + ref_mean, ref_std = _build_ref_series(row, var) + fc_vals = _build_fc_series(row, var, fc_labels) + panels.append((label, ref_mean, ref_std, fc_vals, fc_months)) + + # Suitability (consolidated column names, see _REF_PROB_RE / _FC_PROB_RE) + suit_ref_mean, suit_ref_std = _build_ref_suitability_series(row) + suit_fc_vals = np.array([ + float(row[c]) if row.get(c) is not None else np.nan + for c in (c for _, _, c in fc_items) + ]) + panels.append((SUITABILITY_LABEL, suit_ref_mean, suit_ref_std, + suit_fc_vals, fc_months)) + + # Highlight the forecast month selected in the UI (optional "month" query + # param, e.g. "aug") — only when it lies inside the forecast window. + fc_active_month = 0 + if active_month: + mon_idx = _MONTH_IDX.get(active_month.lower()) + if mon_idx is not None and mon_idx in fc_months: + fc_active_month = mon_idx + + # ── Render SVG ── + n_panels = len(panels) # 5 fontPlusSize = 4 - fig, axes = plt.subplots(2, 4, figsize=(20, 5), gridspec_kw={'height_ratios': [2, 1]}) - fig.suptitle(f"Grid Cell #{cell_id} - ERA5 (mean ±1 std) vs SEAS5 forecast", fontsize=(12+fontPlusSize), fontweight='bold') - - for col, (title, ref_vals, fc_vals, color_fc, color_ref) in enumerate(factors): - ax_line = axes[0, col] - ax_bar = axes[1, col] - - # Line chart - ax_line.plot(months, ref_vals, label='ERA5 Mean', color='#2b5c8f', marker='o', linewidth=1.5) - ax_line.fill_between(months, ref_vals - 0.5, ref_vals + 0.5, color='#2b5c8f', alpha=0.15) - ax_line.plot(months[6:], fc_vals[6:], label='SEAS5 Forecast', color='#e04d39', linestyle='--', marker='s', linewidth=1.5) - ax_line.set_title(title, fontsize=(9+fontPlusSize), fontweight='semibold') - ax_line.tick_params(axis='x', labelrotation=45, labelsize=(8+fontPlusSize)) - ax_line.tick_params(axis='y', labelsize=(8+fontPlusSize)) - ax_line.grid(True, linestyle=':', alpha=0.5) - - # Delta bar chart (Jul - Dec) - deltas = fc_vals[6:] - ref_vals[6:] - bar_colors = ['#e04d39' if d >= 0 else '#2b5c8f' for d in deltas] - ax_bar.bar(forecast_months, deltas, color=bar_colors, alpha=0.85, width=0.6) - ax_bar.set_title("Forecast - Mean Delta", fontsize=(8+fontPlusSize)) - ax_bar.axhline(0, color='black', linewidth=0.8, linestyle='--') - ax_bar.tick_params(axis='x', labelrotation=45, labelsize=(8+fontPlusSize)) - ax_bar.tick_params(axis='y', labelsize=(8+fontPlusSize)) - ax_bar.grid(True, linestyle=':', alpha=0.4) + fig, axes = plt.subplots(1, n_panels, figsize=(4 * n_panels, 6)) # doubled height + fig.suptitle( + f"Grid Cell #{cell_id} — ERA5 2000-2025 (mean ±1 std) vs SEAS5 forecast", + fontsize=(12 + fontPlusSize), fontweight='bold' + ) + + coord = float(row["latitude"]), float(row["longitude"]) + title_sub = f"Lat: {coord[0]:.1f}, Lng: {coord[1]:.1f}" + # fc_active_month set above — used to highlight the active lead month + + for col, (title, ref_mean, ref_std, fc_vals, fc_months) in enumerate(panels): + ax = axes[col] + + # Blue reference line with ±1 std band + ax.fill_between(MONTHS, + ref_mean - ref_std, ref_mean + ref_std, + color=COLOR_REF_BAND, alpha=0.15) + ax.plot(MONTHS, ref_mean, label='ERA5 Mean (2000-2025)', + color=COLOR_REF_LINE, marker='o', linewidth=1.5) + + # Orange forecast points — split into segments so the line doesn't + # wrap backwards across the year boundary (Dec → Jan). + fc_segments: list[tuple[list[str], list[float]]] = [] + seg_x: list[str] = [] + seg_y: list[float] = [] + for idx, (m, v) in enumerate(zip(fc_months, fc_vals)): + if idx > 0 and m < fc_months[idx - 1]: + # year boundary reached — push current segment and start new one + fc_segments.append((seg_x, seg_y)) + seg_x, seg_y = [], [] + seg_x.append(MONTHS[m - 1]) + seg_y.append(float(v)) + fc_segments.append((seg_x, seg_y)) + + for si, (sx, sy) in enumerate(fc_segments): + ax.plot(sx, sy, label=('SEAS5 Forecast' if si == 0 else None), + color=COLOR_FC_MARK, linestyle='--', marker='s', + linewidth=1.5, markersize=6, zorder=5) + + ax.set_title(title, fontsize=(9 + fontPlusSize), fontweight='semibold') + ax.set_title(f"{title}\n{title_sub}", fontsize=(8 + fontPlusSize), + fontweight='semibold') + ax.tick_params(axis='x', labelrotation=45, labelsize=(8 + fontPlusSize)) + ax.tick_params(axis='y', labelsize=(8 + fontPlusSize)) + ax.grid(True, linestyle=':', alpha=0.5) + if col == 0: + ax.legend(fontsize=(8 + fontPlusSize)) plt.tight_layout() - target_file = _save_and_cleanup(fig, out_path, filename, pattern="*_climate_forecast_cell.svg") + # Highlight the active lead month's X-axis label in dark red + for ax in axes: + if fc_active_month and fc_active_month <= len(MONTHS): + for tick in ax.get_xticklabels(): + if tick.get_text() == MONTHS[fc_active_month - 1]: + tick.set_color('#8b0000') # dark red + tick.set_fontweight('bold') + break + target_file = _save_and_cleanup(fig, out_path, filename, + pattern="*_climate_forecast_cell.svg") return target_file + if __name__ == "__main__": - create_ENSO_suitability_visualizations(cell_id=200506) + create_ENSO_suitability_visualizations(cell_id=1) \ No newline at end of file diff --git a/src/app/[locale]/home/showCases/ENSO_Suitability/page.tsx b/src/app/[locale]/home/showCases/ENSO_Suitability/page.tsx index dacad76..afbc4cd 100644 --- a/src/app/[locale]/home/showCases/ENSO_Suitability/page.tsx +++ b/src/app/[locale]/home/showCases/ENSO_Suitability/page.tsx @@ -19,12 +19,53 @@ import { useLoadingTask, LoadingSpinnerAnimation } from '@/components/plots/maps const isSWAPY = true; +// ── Forecast configuration ──────────────────────────────────────────────── +type SpeciesKey = "albopictus" | "aegypti"; +const SPECIES: Record = { + albopictus: "Aedes albopictus", + aegypti: "Aedes aegypti", +}; + +/** One consolidated DB table per species (all 6 forecast months in it). */ +function tableName(species: SpeciesKey): string { + return `seas5_forecast_${species}_habitat_probability`; +} + +// Sanitized forecast suitability columns, e.g. forecast___aug_2026 +// (CSV header "Forecast: Aug 2026" — see parseCSVdata.sanitize_names) +const FC_COL_RE = /^forecast_([a-z]{3})_(\d{4})$/; +const MONTH_ABBR = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + +/** Pretty label for a forecast column: forecast___aug_2026 -> "Forecast: Aug 2026". */ +function prettyFcLabel(col: string): string { + const m = FC_COL_RE.exec(col); + if (!m) return col; + return `Forecast: ${m[1][0].toUpperCase()}${m[1].slice(1)} ${m[2]}`; +} + +/** Reference suitability column (2000-2025 mean) of the same calendar month. */ +function refColumnForFcCol(col: string): string { + const m = FC_COL_RE.exec(col); + if (!m) return ""; + return `referenz_${m[1]}_2000_2025`; +} + +/** (year, month) sort key for a forecast column; last if it doesn't match. */ +function fcColSortKey(col: string): [number, number] { + const m = FC_COL_RE.exec(col); + if (!m) return [Infinity, Infinity]; + const monIdx = MONTH_ABBR.findIndex((x) => x.toLowerCase() === m[1]); + return [parseInt(m[2], 10), monIdx + 1]; +} + /** * Returns the cell-linked ENSO Suitability visualization showcase. * * @remarks - * Displays backend-rendered static climate forecast charts (4 climate factors & mean deltas) - * alongside side-by-side Forecast (Prediction) and Reference maps. + * Displays backend-rendered static climate forecast charts (4 climate factors + + * habitat suitability) alongside side-by-side Forecast (Prediction) and Reference + * maps for SEAS5 forecast data. */ export default function Home() { const t = useTranslations("page_home.ShowCases.page_ENSO_Suitability"); @@ -34,6 +75,44 @@ export default function Home() { const UI_contextT = useUIContext(); const layoutSizes = UI_contextT.layoutDims; + // ── User selection state ── + const [species, setSpecies] = useState("albopictus"); + // forecast suitability columns found in the current table (chronological) + const [fcColumns, setFcColumns] = useState([]); + const [fcIdx, setFcIdx] = useState(0); + const currentTableName = tableName(species); + + // Load the forecast columns of the current table (drives the month selector) + useEffect(() => { + let cancelled = false; + setFcColumns([]); + setFcIdx(0); + (async () => { + try { + const res = await fetch( + apiRoutes.fetchDbColumnNames({ relationName: currentTableName }) + ); + if (!res.ok) return; + const cols: string[] = await res.json(); + const fc = cols + .filter((c) => FC_COL_RE.test(c)) + .sort((a, b) => { + const [ya, ma] = fcColSortKey(a); + const [yb, mb] = fcColSortKey(b); + return ya - yb || ma - mb; + }); + if (!cancelled) setFcColumns(fc); + } catch { + // table missing / backend down — selector stays empty, maps self-select + } + })(); + return () => { cancelled = true; }; + }, [currentTableName]); + + const activeFcCol = fcColumns[fcIdx] ?? ""; + const activeRefCol = refColumnForFcCol(activeFcCol); + const activeMonthAbbr = FC_COL_RE.exec(activeFcCol)?.[1] ?? ""; + // ── 1. Forecast Map Props (Left Map) ── let forecastMapProps = LeafD3MapLayerProps(); forecastMapProps.chartName = 'map_Forecast'; @@ -59,9 +138,9 @@ export default function Home() { forecastMapProps.isApplyContextData = false; forecastMapProps.isApplyTransitions = true; forecastMapProps.isProjection_equirectangular = true; - forecastMapProps.mapUIsettings.filterStringForAvailableDatasetInclude = "_sim"; - forecastMapProps.mapUIsettings.defaultDatasetName = "t_2024_monthly_mean_4_ocsvm_aegypti_predictions_2023_mod_sim"; - forecastMapProps.mapUIsettings.defaultFeatureName = "prob_1"; + forecastMapProps.mapUIsettings.filterStringForAvailableDatasetInclude = "seas5"; + forecastMapProps.mapUIsettings.defaultDatasetName = currentTableName; + forecastMapProps.mapUIsettings.defaultFeatureName = activeFcCol; // "" → map self-selects forecastMapProps.mapUIsettings.defaultFeatureColorMap = availableColorMapsNames.interpolateInferno; forecastMapProps.mapUIsettings.areSettingsOpen = true; forecastMapProps.mapDataSets.isCityNames = false; @@ -94,12 +173,12 @@ export default function Home() { referenceMapProps.mapUIsettings.isSettingsBlendAnimation = true; referenceMapProps.mapUIsettings.defaultDonutSize = 25; referenceMapProps.isStaticAutoFitFullSize = false; - referenceMapProps.isApplyContextData = true; + referenceMapProps.isApplyContextData = false; // don't inherit forecast map's feature referenceMapProps.isApplyTransitions = true; referenceMapProps.isProjection_equirectangular = true; - referenceMapProps.mapUIsettings.filterStringForAvailableDatasetInclude = "_sim"; - referenceMapProps.mapUIsettings.defaultDatasetName = "t_2024_monthly_mean_4_ocsvm_aegypti_predictions_2023_mod_sim"; - referenceMapProps.mapUIsettings.defaultFeatureName = "prob_1"; + referenceMapProps.mapUIsettings.filterStringForAvailableDatasetInclude = "seas5"; + referenceMapProps.mapUIsettings.defaultDatasetName = currentTableName; + referenceMapProps.mapUIsettings.defaultFeatureName = activeRefCol; // same calendar month as the active forecast column referenceMapProps.mapUIsettings.defaultFeatureColorMap = availableColorMapsNames.interpolateInferno; referenceMapProps.mapUIsettings.areSettingsOpen = false; referenceMapProps.mapDataSets.isCityNames = false; @@ -123,8 +202,8 @@ export default function Home() { // ── Card Titles & Props ── let climateFactorsCardProps = CardPropsClass( - getTranslation('climateFactorsPlot', 'Climatic Factors & Forecast Sanity Check'), - getTranslation('climateFactorsPlot', 'Climatic Factors & Forecast Sanity Check'), + getTranslation('climateFactorsPlot', 'Climatic Factors'), + getTranslation('climateFactorsPlot', 'Climatic Factors'), "", "" ); climateFactorsCardProps.infoCard = { content: MDX.DummyContent, footer: undefined }; @@ -137,8 +216,8 @@ export default function Home() { forecastMapCardProps.infoCard = { content: MDX.DummyContent, footer: undefined }; let referenceMapCardProps = CardPropsClass( - getTranslation('referenceMapTitle', 'Historical Baseline / Reference View'), - getTranslation('referenceMapTitle', 'Historical Baseline / Reference View'), + getTranslation('referenceMapTitle', 'Historical Baseline'), + getTranslation('referenceMapTitle', 'Historical Baseline'), "", "" ); referenceMapCardProps.infoCard = { content: MDX.DummyContent, footer: undefined }; @@ -173,6 +252,38 @@ export default function Home() { + {/* ** Species & Forecast Month Selector ** */} +
+ + + + + +
+ {/*** START: grid layout ***/}
- {/*** Top Card: 4 Climatic Factors & Forecast Sanity Check (Static Backend Image) ***/} + {/*** Top Card: 4 Climatic Factors + Suitability (Backend SVG) ***/} - + {/*** Bottom Left Card: Forecast Map (Prediction) with Overview Minimap ***/} - + {/* key forces a remount when species or forecast month changes — + LeafD3Map only applies defaultDatasetName/defaultFeatureName at mount */} + - +
@@ -215,7 +332,15 @@ export default function Home() { /** * Renders backend-generated static SVG/PNG charts for climate factors & forecasts for selected cell. */ -function ClimateForecastStaticChartComponent({ fileName }: { fileName: string }) { +function ClimateForecastStaticChartComponent({ + fileName, + dataset, + month, +}: { + fileName: string; + dataset?: string; + month?: string; +}) { const contexT = useInterfaceContext(); const rowID = contexT.dbRowID_of_selectedGridcellID; @@ -230,7 +355,9 @@ function ClimateForecastStaticChartComponent({ fileName }: { fileName: string }) } }, [isLoading, L_svgLoader]); - const initialSrc = rowID !== -1 ? apiRoutes.getUncertaintySvg({ filename: fileName, cellID: rowID }) : ""; + const initialSrc = rowID !== -1 + ? apiRoutes.getUncertaintySvg({ filename: fileName, cellID: rowID, dataset, month }) + : ""; const [displayedSrc, setDisplayedSrc] = useState(initialSrc); const prevRowID = useRef(rowID); const loadKey = useRef(0); @@ -243,8 +370,20 @@ function ClimateForecastStaticChartComponent({ fileName }: { fileName: string }) } }, [rowID]); + // Also reload when the dataset or the highlighted month changes (selectors) + const prevDataset = useRef(dataset); + const prevMonth = useRef(month); + useEffect(() => { + if ((dataset !== prevDataset.current || month !== prevMonth.current) && rowID !== -1) { + prevDataset.current = dataset; + prevMonth.current = month; + loadKey.current += 1; + setIsLoading(true); + } + }, [dataset, month, rowID]); + const pendingSrc = (isLoading && rowID !== -1) - ? apiRoutes.getUncertaintySvg({ filename: fileName, cellID: rowID }) + ? apiRoutes.getUncertaintySvg({ filename: fileName, cellID: rowID, dataset, month }) : ""; const currentKey = loadKey.current; @@ -292,4 +431,4 @@ function ClimateForecastStaticChartComponent({ fileName }: { fileName: string }) )} ); -} +} \ No newline at end of file diff --git a/src/app/api_routes.ts b/src/app/api_routes.ts index e2f7066..ce9401c 100644 --- a/src/app/api_routes.ts +++ b/src/app/api_routes.ts @@ -238,9 +238,12 @@ class apiRoutes { static getUncertaintySvg(params: { filename: string; cellID: number; + dataset?: string; // optional: seas5_forecast_* table name + month?: string; // optional: highlight this forecast month (e.g. "aug") }): string { return buildUrl(`${apiRoutes.API_URL}/get_uncertainty_svg`, params); } + /** * Builds the administration URL for a database relation.