Chris Parmer — home

SF daily high/low temperature: 7 past days + 5 forecast days

Weather

Example from the compendium of canonical charts

Weather — SF daily high/low temperature: 7 past days + 5 forecast days

Python Code

"""Weather — SF daily high/low temperature: 7 past days + 5 forecast days."""


import numpy as np
import pandas as pd
import plotly.graph_objects as go
import requests, warnings


# San Francisco coords — Open-Meteo free API, no key required
OPENMETEO_URL = (
    "https://api.open-meteo.com/v1/forecast"
    "?latitude=37.7749&longitude=-122.4194"
    "&daily=temperature_2m_max,temperature_2m_min,precipitation_sum"
    "&temperature_unit=fahrenheit"
    "&timezone=America%2FLos_Angeles"
    "&past_days=7"
    "&forecast_days=5"
)


def generate():
    print("fetching Open-Meteo SF 7-day past + 5-day forecast …")
    warnings.filterwarnings("ignore")
    try:
        requests.packages.urllib3.disable_warnings()
    except Exception:
        pass

    df = None
    try:
        r = requests.get(OPENMETEO_URL, verify=False, timeout=20)
        if r.status_code == 200:
            data = r.json()
            daily = data.get("daily", {})
            df = pd.DataFrame({
                "date":   pd.to_datetime(daily["time"]),
                "hi":     daily["temperature_2m_max"],
                "lo":     daily["temperature_2m_min"],
                "precip": daily.get("precipitation_sum", [0] * len(daily["time"])),
            }).dropna(subset=["hi", "lo"])
            print(f"  {len(df)} days, {df['date'].min().date()} – {df['date'].max().date()}")
    except Exception as e:
        print(f"  fetch failed ({e}), using synthetic SF data")

    if df is None:
        # Synthetic SF June weather: cool mornings, mild afternoons
        today = pd.Timestamp.today().normalize()
        dates = pd.date_range(today - pd.Timedelta(days=7), periods=12, freq="D")
        rng = np.random.default_rng(137)
        hi = 62 + 6 * np.sin(np.arange(12) * 0.7) + rng.normal(0, 2, 12)
        lo = 52 + 4 * np.sin(np.arange(12) * 0.5) + rng.normal(0, 1.5, 12)
        df = pd.DataFrame({"date": dates, "hi": hi, "lo": lo, "precip": [0.0] * 12})

    today = pd.Timestamp.today().normalize()
    df["is_forecast"] = df["date"] > today

    past = df[~df["is_forecast"]]
    fcast = df[df["is_forecast"]]

    fig = go.Figure()

    # Past high/low bars (solid VIOLET)
    if len(past):
        fig.add_trace(go.Bar(
            x=past["date"],
            y=past["hi"] - past["lo"],
            base=past["lo"],
            name="Observed",
            marker=dict(color=VIOLET, opacity=0.85, line=dict(width=0)),
            hovertemplate="%{x|%a %b %d}<br>Hi: %{customdata[0]:.0f}°F  Lo: %{customdata[1]:.0f}°F<extra></extra>",
            customdata=list(zip(past["hi"], past["lo"])),
            width=0.7 * 86400000,
        ))

    # Forecast high/low bars (lighter, dashed look via opacity)
    if len(fcast):
        fig.add_trace(go.Bar(
            x=fcast["date"],
            y=fcast["hi"] - fcast["lo"],
            base=fcast["lo"],
            name="Forecast",
            marker=dict(color=TEAL, opacity=0.65, line=dict(width=0)),
            hovertemplate="%{x|%a %b %d}<br>Hi: %{customdata[0]:.0f}°F  Lo: %{customdata[1]:.0f}°F<extra></extra>",
            customdata=list(zip(fcast["hi"], fcast["lo"])),
            width=0.7 * 86400000,
        ))

    # Hi/Lo temperature labels
    all_dates = df["date"]
    hi_colors = [VIOLET if not f else TEAL for f in df["is_forecast"]]
    lo_colors = hi_colors

    fig.add_trace(go.Scatter(
        x=all_dates, y=df["hi"],
        mode="text",
        text=[f"{v:.0f}°" for v in df["hi"]],
        textposition="top center",
        textfont=dict(size=10, color=hi_colors),
        name="High",
        hoverinfo="skip",
        showlegend=False,
    ))
    fig.add_trace(go.Scatter(
        x=all_dates, y=df["lo"],
        mode="text",
        text=[f"{v:.0f}°" for v in df["lo"]],
        textposition="bottom center",
        textfont=dict(size=10, color=lo_colors),
        name="Low",
        hoverinfo="skip",
        showlegend=False,
    ))

    # Divider between past/forecast
    if len(past) and len(fcast):
        divider = today + pd.Timedelta(hours=12)
        fig.add_vline(
            x=divider.value // 10**6,
            line=dict(color=MUTED, width=1.5, dash="dash"),
        )
        fig.add_annotation(
            x=divider.value // 10**6,
            y=1, yref="paper",
            text="Today",
            showarrow=False,
            font=dict(size=10, color=MUTED),
            xanchor="left", yanchor="top",
        )

    fig.update_layout(
        xaxis=dict(
            type="date",
            tickformat="%a\n%b %d",
            showgrid=False,
            dtick=86400000,
        ),
        yaxis=dict(
            title="Temperature (°F)",
            showgrid=False,
            zeroline=False,
        ),
        legend=dict(orientation="h", y=1.08),
        bargap=0.2,
        margin=dict(t=50, b=50, l=60, r=40),
        height=420,
    )
    return fig


if __name__ == "__main__":
    generate()

Made with Plotly