Chris Parmer — home

Temperature range bars

daily high/low from Open-Meteo

Example from the compendium of canonical charts

Temperature range bars — daily high/low from Open-Meteo

Python Code

"""Temperature range bars — daily high/low from Open-Meteo."""


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


# New York City coords — Open-Meteo free, no key required
OPENMETEO_URL = (
    "https://api.open-meteo.com/v1/forecast"
    "?latitude=40.7128&longitude=-74.0060"
    "&daily=temperature_2m_max,temperature_2m_min"
    "&temperature_unit=celsius"
    "&timezone=America%2FNew_York"
    "&past_days=60"
    "&forecast_days=7"
)

ARCHIVE_URL = (
    "https://archive-api.open-meteo.com/v1/archive"
    "?latitude=40.7128&longitude=-74.0060"
    "&start_date=2024-01-01&end_date=2024-12-31"
    "&daily=temperature_2m_max,temperature_2m_min"
    "&temperature_unit=celsius"
    "&timezone=America%2FNew_York"
)


def generate():
    print("fetching Open-Meteo temperature data …")
    warnings.filterwarnings("ignore")
    try:
        requests.packages.urllib3.disable_warnings()
    except Exception:
        pass

    data = None
    is_forecast = False
    # Try archive first (complete past data), then forecast API
    for url in [ARCHIVE_URL, OPENMETEO_URL]:
        try:
            r = requests.get(url, verify=False, timeout=20)
            if r.status_code == 200:
                data = r.json()
                is_forecast = "forecast_days" in url or "past_days" in url
                print(f"  fetched {len(data.get('daily', {}).get('time', []))} days")
                break
        except Exception as e:
            print(f"  {url} failed: {e}")

    if data is None:
        print("  using synthetic temperature data")
        rng = np.random.default_rng(35)
        dates = pd.date_range("2024-01-01", periods=90, freq="D")
        # NYC seasonal curve
        day_of_year = np.array([d.dayofyear for d in dates])
        base = -5 + 25 * (1 - np.cos(2 * np.pi * (day_of_year - 10) / 365)) / 2
        tmax = base + 5 + rng.normal(0, 2, len(dates))
        tmin = base - 5 + rng.normal(0, 2, len(dates))
        data = {"daily": {
            "time": [str(d.date()) for d in dates],
            "temperature_2m_max": list(tmax),
            "temperature_2m_min": list(tmin),
        }}
        is_forecast = False
        source = "Synthetic (NYC seasonal profile)"
    else:
        source = "Open-Meteo — New York City"

    daily = data["daily"]
    dates = pd.to_datetime(daily["time"])
    tmax  = np.array(daily["temperature_2m_max"], dtype=float)
    tmin  = np.array(daily["temperature_2m_min"], dtype=float)

    today = pd.Timestamp.today().normalize()
    is_future = dates >= today

    # Color: past = teal, future = orange
    bar_colors = [PINK if f else TEAL for f in is_future]

    fig = go.Figure()

    # Observed bars
    past_mask   = ~is_future
    future_mask = is_future

    for mask, color, label in [(past_mask, TEAL, "Observed"), (future_mask, PINK, "Forecast")]:
        if mask.sum() == 0:
            continue
        fig.add_trace(go.Bar(
            x=dates[mask],
            y=tmax[mask] - tmin[mask],
            base=tmin[mask],
            marker=dict(color=color, opacity=0.7),
            name=label,
            hovertemplate="%{x|%b %d}<br>High: %{customdata[0]:.1f}°C<br>Low: %{customdata[1]:.1f}°C<extra></extra>",
            customdata=np.stack([tmax[mask], tmin[mask]], axis=1),
        ))

    # Freeze line
    fig.add_hline(
        y=0,
        line=dict(color="#52B3D0", width=1, dash="dot"),
        annotation_text="Freezing",
        annotation_position="right",
        annotation_font=dict(size=9),
    )

    fig.update_layout(
        title=dict(text=f"Daily Temperature Range — {source}", x=0.5),
        xaxis=dict(title=""),
        yaxis=dict(title="Temperature (°C)"),
        barmode="overlay",
        bargap=0.1,
        legend=dict(orientation="h", y=1.08),
        margin=dict(t=60, b=50, l=70, r=80),
        height=440,
    )
    return fig


if __name__ == "__main__":
    generate()

Made with Plotly