Temperature range bars
daily high/low from Open-Meteo
Example from the compendium of canonical charts
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