Skip to content

Commit

Permalink
✨ Add SPC/WPC Outlook Progression Autoplot
Browse files Browse the repository at this point in the history
  • Loading branch information
akrherz committed Nov 26, 2024
1 parent e13fe65 commit 29208e4
Show file tree
Hide file tree
Showing 7 changed files with 421 additions and 15 deletions.
1 change: 1 addition & 0 deletions htdocs/api/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@
<li><a href="/json/ibw_tags.py?help">Impact Based Warning Tags</a></li>
<li><a href="/json/radar.py?help">NEXRAD/TWDR Archive Metadata</a></li>
<li><a href="/json/ridge_current.py?help">NEXRAD/TWDR Current Metadata</a></li>
<li><a href="/json/outlook_progression.py?help">SPC/WPC Outlook Progression by Point by Date</a></li>
<li><a href="/json/spcmcd.py?help">SPC Mesoscale Discussions</a></li>
<li><a href="/json/spcoutlook.py?help">SPC Outlooks</a></li>
<li><a href="/json/spc_bysize.py?help">SPC Outlooks by Size</a></li>
Expand Down
3 changes: 3 additions & 0 deletions htdocs/json/outlook_progression.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""implemented in /pylib/iemweb/json/outlook_progression.py"""

from iemweb.json.outlook_progression import application # noqa

Check notice

Code scanning / CodeQL

Unused import Note

Import of 'application' is not used.
7 changes: 7 additions & 0 deletions pylib/iemweb/autoplot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -868,6 +868,13 @@ def import_script(p: int):
"WPC Excessive Rainfall Outlook Calendar"
),
},
{
"id": 258,
"label": (
"SPC Convective/Fire Wx or "
"WPC Excessive Rainfall Outlook Progression for Lat/Lon"
),
},
{
"id": 230,
"label": (
Expand Down
33 changes: 19 additions & 14 deletions pylib/iemweb/autoplot/scripts200/p200.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,37 +41,42 @@
"yes": "YES: Draw Counties/Parishes",
"no": "NO: Do Not Draw Counties/Parishes",
}
# Note the backwards order here that we care about in p258
ISSUANCE = {
"1.C.A": "Day 1 Convective",
"1.C.1": "Day 1 Convective @1z",
"1.C.6": "Day 1 Convective @6z",
"1.C.13": "Day 1 Convective @13z",
"1.C.16": "Day 1 Convective @16z",
"1.C.20": "Day 1 Convective @20z",
"1.C.16": "Day 1 Convective @16z",
"1.C.13": "Day 1 Convective @13z",
"1.C.6": "Day 1 Convective @6z",
"1.F.A": "Day 1 Fire Weather",
"1.F.7": "Day 1 Fire Weather @7z",
"1.F.17": "Day 1 Fire Weather @17z",
"1.F.7": "Day 1 Fire Weather @7z",
"1.E.A": "Day 1 Excessive Rainfall",
"1.E.8": "Day 1 Excessive Rainfall @8z",
"1.E.16": "Day 1 Excessive Rainfall @16z",
"1.E.20": "Day 1 Excessive Rainfall @20z",
"1.E.1": "Day 1 Excessive Rainfall @1z",
"1.E.20": "Day 1 Excessive Rainfall @20z",
"1.E.16": "Day 1 Excessive Rainfall @16z",
"1.E.8": "Day 1 Excessive Rainfall @8z",
"2.C.A": "Day 2 Convective",
"2.C.7": "Day 2 Convective @7z",
"2.C.17": "Day 2 Convective @17z",
"2.C.7": "Day 2 Convective @7z",
"2.F.A": "Day 2 Fire Weather",
"2.F.8": "Day 2 Fire Weather @8z",
"2.F.18": "Day 2 Fire Weather @18z",
"2.F.8": "Day 2 Fire Weather @8z",
"2.E.A": "Day 2 Excessive Rainfall",
"2.E.9": "Day 2 Excessive Rainfall @9z",
"2.E.21": "Day 2 Excessive Rainfall @21z",
"2.E.20": "Day 2 Excessive Rainfall @20z",
"2.E.8": "Day 2 Excessive Rainfall @8z",
"3.C.20": "Day 3 Convective @20z",
"3.C.8": "Day 3 Convective @8z",
"3.F.21": "Day 3 Fire Weather @21z",
"3.E.9": "Day 3 Excessive Rainfall @9z",
"3.E.20": "Day 3 Excessive Rainfall @20z",
"3.E.8": "Day 3 Excessive Rainfall @8z",
"4.C.10": "Day 4 Convective @10z",
"4.E.9": "Day 4 Excessive Rainfall @9z",
"4.E.20": "Day 4 Excessive Rainfall @20z",
"4.E.8": "Day 4 Excessive Rainfall @8z",
"5.C.10": "Day 5 Convective @10z",
"5.E.9": "Day 5 Excessive Rainfall @9z",
"5.E.20": "Day 5 Excessive Rainfall @20z",
"5.E.8": "Day 5 Excessive Rainfall @8z",
"6.C.10": "Day 6 Convective @10z",
"7.C.10": "Day 7 Convective @10z",
"8.C.10": "Day 8 Convective @10z",
Expand Down
197 changes: 197 additions & 0 deletions pylib/iemweb/autoplot/scripts200/p258.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
"""
This tool generates an info-graphic with the progression of outlooks for a
given point and date. This plot utilizes the
<a href="/json/outlook_progression.py?help">outlook progression</a> web
service.
"""

from datetime import date

import httpx
import pandas as pd
from matplotlib.patches import Rectangle
from pyiem.exceptions import NoDataFound
from pyiem.plot import figure
from pyiem.util import get_autoplot_context, utc

from iemweb.autoplot.scripts200.p200 import ISSUANCE
from iemweb.autoplot.scripts200.p220 import COLORS

PDICT = {
"C": "Convective",
"E": "Excessive Rainfall",
"F": "Fire Weather",
}
LABELS = {
"C": "Storm Prediction Center Convective Outlook",
"E": "Weather Prediction Center Excessive Rainfall Outlook",
"F": "Storm Prediction Center Fire Weather Outlook",
}


def get_description():
"""Return a dict describing how to call this plotter"""
desc = {"description": __doc__, "data": True}
now = utc()
desc["arguments"] = [
{
"type": "date",
"name": "valid",
"default": now.strftime("%Y/%m/%d"),
"label": "Date:",
"min": "1990/01/01",
},
{
"type": "select",
"name": "outlook_type",
"default": "C",
"label": "Outlook Type:",
"options": PDICT,
},
{
"type": "float",
"name": "lat",
"default": 42.0,
"label": "Enter Latitude (deg N):",
},
{
"type": "float",
"name": "lon",
"default": -95.0,
"label": "Enter Longitude (deg E):",
},
]
return desc


def compute_slots(outlook_type: str, valid: date) -> list:
"""Figure out what slots we have"""
slots = []
for key in list(ISSUANCE.keys())[::-1]:
tokens = key.split(".")
if tokens[1] != outlook_type or tokens[2] == "A":
continue
slots.append(key)
if outlook_type == "C" and valid < date(2024, 8, 22):
slots.pop(slots.index("3.C.20"))
return slots


def plotter(fdict):
"""Go"""
ctx = get_autoplot_context(fdict, get_description())
jdata = httpx.get(
"http://iem.local/json/outlook_progression.py",
params={
"lat": ctx["lat"],
"lon": ctx["lon"],
"valid": ctx["valid"].strftime("%Y-%m-%d"),
"outlook_type": ctx["outlook_type"],
"fmt": "json",
},
).json()
outlooks = pd.DataFrame(jdata["outlooks"])
if outlooks.empty:
raise NoDataFound("No outlooks found for this point and date.")

outlooks["product_issue"] = pd.to_datetime(outlooks["product_issue"])
fig = figure(
title=(f"{LABELS[ctx['outlook_type']]} Progression"),
subtitle=(
f"Lon: {ctx['lon']:.02f}E Lat: {ctx['lat']:.02f}N for "
f"{ctx['valid']:%-d %B %Y}"
),
)
ax = fig.add_axes((0.2, 0.1, 0.75, 0.8))
y = 0
xloc = {
"ANY SEVERE": 1,
"CATEGORICAL": 2,
"TORNADO": 3,
"HAIL": 4,
"WIND": 5,
}
ylabels = []
ylocator = {}
slots = compute_slots(fdict["outlook_type"], ctx["valid"])
for (pissue, cat), df2 in outlooks.groupby(["product_issue", "category"]):
row0 = df2[df2["threshold"] != "SIGN"].iloc[0]
if row0["cycle"] != -1:
# Consume up slots as necessary
slotkey = f"{row0['day']}.{fdict['outlook_type']}.{row0['cycle']}"
if slotkey in slots:
removeme = []
for index in range(slots.index(slotkey)):
slot = slots[index]
removeme.append(slot)
ylabels.append(
f"Day {slot.split('.')[0]}@{slot.split('.')[2]}Z"
)
y -= 1
for slot in removeme:
slots.pop(slots.index(slot))
if slots and slotkey == slots[0]:
slots.pop(0)
hatched = "SIGN" in df2["threshold"].values
cycle = row0["cycle"] if row0["cycle"] > -1 else ""
key = f"Day {row0['day']}@{cycle}Z\nIssued:{pissue:%d/%H%M}Z"
if key not in ylocator:
ylocator[key] = y
y -= 1
ylabels.append(key)
if pd.isna(row0["threshold"]):
continue
thisy = ylocator[key]
x = xloc.get(cat, 1)
color = COLORS.get(row0["threshold"], "tan")
rect = Rectangle(
(x - 0.4, thisy - 0.4),
0.8,
0.8,
color=color,
hatch="/" if hatched else None,
zorder=3,
)
ax.add_patch(rect)
if hatched:
ax.add_patch(
Rectangle(
(x - 0.4, thisy - 0.4),
0.8,
0.8,
hatch="//" if hatched else None,
fill=False,
zorder=4,
)
)
pretty = row0["threshold"]
if pretty.startswith("0."):
pretty = f"{int(pretty[2:])}%"
ax.text(
x,
thisy,
f"{pretty} {cat}",
ha="center",
va="center",
zorder=5,
color="w",
bbox=dict(
facecolor="k",
alpha=0.7,
edgecolor="none",
pad=1,
),
)
while slots:
slot = slots.pop(0)
ylabels.append(f"Day {slot.split('.')[0]}@{slot.split('.')[2]}Z")
y -= 1

ax.set_yticks(range(-len(ylabels) + 1, 1))
ax.set_yticklabels(ylabels[::-1])
ax.set_ylim(-len(ylabels), 0.6)
ax.set_xlim(0.5, 5.5)
ax.set_xticks([])
# Only show y-axis grid
ax.grid(axis="y")
return fig, outlooks
Loading

0 comments on commit 29208e4

Please sign in to comment.