Created this in a few iterations and 20 mins using latest versions of panel-material-ui and copilot with holoviz-mcp.
Code
# weather_dashboard.py
import numpy as np
import pandas as pd
import panel as pn
import panel_material_ui as pmui
import param
import hvplot.pandas # noqa
pn.extension(throttled=True)
# Constants
CITIES = ["New York", "London", "Tokyo", "Sydney", "Paris"]
WEATHER_CONDITIONS = {
"Clear": "☀️",
"Partly Cloudy": "🌤️",
"Cloudy": "☁️",
"Rainy": "🌧️",
"Stormy": "⛈️",
"Snowy": "❄️",
}
# Extract Data (Mock)
@pn.cache(max_items=10, ttl=600)
def get_weather_data(city):
"""Generate realistic mock weather data for a city"""
np.random.seed(hash(city) % 2**32) # Consistent data per city
# Base temperatures by city
base_temps = {
"New York": 15,
"London": 12,
"Tokyo": 18,
"Sydney": 25,
"Paris": 14,
}
base_temp = base_temps.get(city, 20)
# Current weather
current = {
"city": city,
"temperature": round(base_temp + np.random.uniform(-5, 5), 1),
"feels_like": round(base_temp + np.random.uniform(-7, 3), 1),
"humidity": int(np.random.uniform(40, 85)),
"wind_speed": round(np.random.uniform(5, 25), 1),
"condition": np.random.choice(list(WEATHER_CONDITIONS.keys())),
}
# 5-day forecast
dates = pd.date_range(start=pd.Timestamp.now().normalize(), periods=5, freq='D')
forecast = pd.DataFrame({
"date": dates,
"high": [round(base_temp + np.random.uniform(0, 8), 1) for _ in range(5)],
"low": [round(base_temp + np.random.uniform(-8, 0), 1) for _ in range(5)],
"condition": [np.random.choice(list(WEATHER_CONDITIONS.keys())) for _ in range(5)],
})
return {"current": current, "forecast": forecast}
# Transform Data
def get_current_weather(city):
"""Get current weather conditions"""
data = get_weather_data(city)
return data["current"]
def get_forecast_df(city):
"""Get forecast as DataFrame"""
data = get_weather_data(city)
df = data["forecast"].copy()
df["day"] = df["date"].dt.strftime("%a, %b %d")
df["icon"] = df["condition"].map(WEATHER_CONDITIONS)
return df
# Plot Creation
def create_temperature_chart(df):
"""Create temperature trend chart"""
df_plot = df.copy()
df_plot["day_short"] = df_plot["date"].dt.strftime("%a")
return df_plot.hvplot.line(
x="day_short",
y=["high", "low"],
title="5-Day Temperature Forecast",
ylabel="Temperature (°C)",
xlabel="Day",
height=300,
responsive=True,
color=["#FF6B6B", "#4ECDC4"],
line_width=3,
legend="top_right",
tools=["hover"],
)
# Main Dashboard Component
class WeatherDashboard(pn.viewable.Viewer):
"""Weather forecast dashboard with current conditions and 5-day forecast"""
city = param.Selector(
default="New York",
objects=CITIES,
doc="Select city for weather forecast"
)
dark_theme = param.Boolean(default=False, allow_refs=True, doc="Dark theme enabled")
def __init__(self, **params):
super().__init__(**params)
with pn.config.set(sizing_mode="stretch_width"):
# Create inputs
self._city_selector = pmui.Select.from_param(
self.param.city,
label="Location",
margin=(10, 20)
)
self._inputs = pmui.Column(
pmui.Typography(
"🌍 Weather Dashboard",
variant="h6",
sx={"mb": 2, "fontWeight": 600}
),
self._city_selector,
max_width=300,
)
# Create static output panes with reactive content
self._kpi_pane = self._kpi_indicators
self._current_weather_pane = pmui.Paper(
self._current_weather_card,
height=500,
sx={"p": 3}
)
self._chart_pane = pmui.Paper(
pn.pane.HoloViews(self.param.temperature_chart, sizing_mode="stretch_both"),
height=500,
sx={"p": 2}
)
self._forecast_pane = pmui.Paper(
self._forecast_table,
sx={"p": 2}
)
# Layout outputs in responsive grid
grid = pmui.Grid(
pmui.Grid(self._kpi_pane, size={"xs": 12}),
pmui.Grid(self._chart_pane, size={"xs": 12, "md": 8}),
pmui.Grid(self._current_weather_pane, size={"xs": 12, "md": 4}),
pmui.Grid(self._forecast_pane, size={"xs": 12}),
container=True,
spacing=2,
)
# Wrap in a Grid container with max-width and centering
self._outputs = pmui.Container(
grid,
)
@param.depends("city", watch=True, on_init=True)
def _update_chart(self):
"""Update temperature chart when city changes"""
df = get_forecast_df(self.city)
self.temperature_chart = create_temperature_chart(df)
temperature_chart = param.Parameter(doc="Temperature forecast chart")
@param.depends("city")
def _kpi_indicators(self):
"""Display key performance indicators"""
current = get_current_weather(self.city)
df = get_forecast_df(self.city)
kpi_style = {
"textAlign": "center",
"p": 2,
}
return pmui.FlexBox(
pmui.Paper(
pmui.Column(
pmui.Typography("High", variant="body2", sx={"color": "text.secondary"}),
pmui.Typography(
f"{df['high'].max()}°C",
variant="h5",
sx={"fontWeight": 700, "color": "#FF6B6B"}
),
),
sx=kpi_style, sizing_mode="fixed", width=270,
),
pmui.Paper(
pmui.Column(
pmui.Typography("Low", variant="body2", sx={"color": "text.secondary"}),
pmui.Typography(
f"{df['low'].min()}°C",
variant="h5",
sx={"fontWeight": 700, "color": "#4ECDC4"}
),
),
sx=kpi_style, sizing_mode="fixed", width=270,
),
pmui.Paper(
pmui.Column(
pmui.Typography("Humidity", variant="body2", sx={"color": "text.secondary"}),
pmui.Typography(
f"{current['humidity']}%",
variant="h5",
sx={"fontWeight": 700, "color": "primary.main"}
),
),
sx=kpi_style, sizing_mode="fixed", width=270,
),
pmui.Paper(
pmui.Column(
pmui.Typography("Wind", variant="body2", sx={"color": "text.secondary"}),
pmui.Typography(
f"{current['wind_speed']} m/s",
variant="h5",
sx={"fontWeight": 700, "color": "primary.main"}
),
),
sx=kpi_style, sizing_mode="fixed", width=270,
),
flex_direction="row",
align_content='space-evenly',
gap="5px",
)
@param.depends("city")
def _current_weather_card(self):
"""Display current weather conditions"""
current = get_current_weather(self.city)
icon = WEATHER_CONDITIONS.get(current["condition"], "🌡️")
return pmui.Column(
pmui.Typography(
"Current Weather",
variant="h6",
sx={"mb": 2, "fontWeight": 600}
),
pmui.Row(
pmui.Typography(
icon,
sx={"fontSize": "4rem", "mr": 2}
),
pmui.Column(
pmui.Typography(
f"{current['temperature']}°C",
variant="h3",
sx={"fontWeight": 700, "lineHeight": 1}
),
pmui.Typography(
current["condition"],
variant="h6",
sx={"color": "text.secondary"}
),
),
),
pmui.Typography(
f"Feels like: {current['feels_like']}°C",
variant="body1",
sx={"mt": 2, "color": "text.secondary"}
),
pmui.Typography(
f"Humidity: {current['humidity']}%",
variant="body1",
sx={"color": "text.secondary"}
),
pmui.Typography(
f"Wind: {current['wind_speed']} m/s",
variant="body1",
sx={"color": "text.secondary"}
),
)
@param.depends("city")
def _forecast_table(self):
"""Display 5-day forecast"""
df = get_forecast_df(self.city)
rows = []
with pn.config.set(sizing_mode="stretch_width"):
for _, row in df.iterrows():
rows.append(
pmui.Row(
pmui.Typography(row["day"], sx={"flex": 2, "fontWeight": 600}),
pmui.Typography(row["icon"], sx={"flex": 1, "fontSize": "1.5rem", "textAlign": "center"}),
pmui.Typography(row["condition"], sx={"flex": 2}),
pmui.Typography(f"{row['high']}°C", sx={"flex": 1, "color": "#FF6B6B", "fontWeight": 600}),
pmui.Typography(f"{row['low']}°C", sx={"flex": 1, "color": "#4ECDC4", "fontWeight": 600}),
sx={"borderBottom": "1px solid rgba(0,0,0,0.1)", "py": 1.5}
)
)
return pmui.Column(
pmui.Typography(
"5-Day Forecast",
variant="h6",
sx={"mb": 2, "fontWeight": 600}
),
pmui.Row(
pmui.Typography("Day", sx={"flex": 2, "fontWeight": 700, "color": "text.secondary"}),
pmui.Typography("", sx={"flex": 1}),
pmui.Typography("Conditions", sx={"flex": 2, "fontWeight": 700, "color": "text.secondary"}),
pmui.Typography("High", sx={"flex": 1, "fontWeight": 700, "color": "text.secondary"}),
pmui.Typography("Low", sx={"flex": 1, "fontWeight": 700, "color": "text.secondary"}),
sx={"borderBottom": "2px solid rgba(0,0,0,0.2)", "pb": 1}
),
*rows,
)
def __panel__(self):
return pmui.Row(self._inputs, self._outputs)
@classmethod
def create_app(cls, **params):
"""Create the servable dashboard app"""
dashboard = cls(**params)
page = pmui.Page(
title="Weather Forecast Dashboard",
sidebar=[
pmui.Typography(
"🌍 Weather Dashboard",
variant="h6",
sx={"mb": 2, "fontWeight": 600}
),
pmui.Typography(
"Select a city to view current conditions and 5-day forecast.",
variant="body2",
sx={"mb": 3, "color": "text.secondary"}
),
dashboard._city_selector,
],
main=[dashboard._outputs],
dark_theme=dashboard.dark_theme,
)
# Synchronize Page theme to dashboard theme
dashboard.dark_theme = page.param.dark_theme
return page
# Serve the app
if __name__ == "__main__":
WeatherDashboard.create_app().show(port=5007, autoreload=True, open=True)
elif pn.state.served:
WeatherDashboard.create_app().servable()
