Spaces:
Runtime error
Runtime error
Update LandsD basemap
Browse files- pages/1_Wind.py +210 -0
- pages/2_Temperature.py +201 -0
- pages/3_Humidity.py +205 -0
- pages/4_Smart Lampposts.py +182 -0
- pages/5_Past Records.py +141 -0
- pages/6_Heat island.py +186 -0
- pages/7_CoWIN.py +175 -0
pages/1_Wind.py
ADDED
@@ -0,0 +1,210 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
import requests
|
3 |
+
import folium
|
4 |
+
from streamlit_folium import st_folium
|
5 |
+
import pandas as pd
|
6 |
+
import plotly.graph_objs as go
|
7 |
+
import branca.colormap as cm
|
8 |
+
import pytz
|
9 |
+
from datetime import datetime
|
10 |
+
|
11 |
+
# Set page layout to wide
|
12 |
+
st.set_page_config(layout="wide", page_title="Real-Time Wind Data Dashboard")
|
13 |
+
|
14 |
+
@st.cache_data(ttl=300)
|
15 |
+
def fetch_geojson_data(url):
|
16 |
+
response = requests.get(url)
|
17 |
+
data = response.json()
|
18 |
+
hk_tz = pytz.timezone('Asia/Hong_Kong')
|
19 |
+
fetch_time = datetime.now(hk_tz).strftime('%Y-%m-%dT%H:%M:%S')
|
20 |
+
return data, fetch_time
|
21 |
+
|
22 |
+
# Function to calculate wind statistics
|
23 |
+
def calculate_wind_stats(features):
|
24 |
+
gust_speeds = [feature['properties']['10-Minute Maximum Gust(km/hour)'] for feature in features if
|
25 |
+
feature['properties']['10-Minute Maximum Gust(km/hour)'] is not None]
|
26 |
+
mean_speeds = [feature['properties']['10-Minute Mean Speed(km/hour)'] for feature in features if
|
27 |
+
feature['properties']['10-Minute Mean Speed(km/hour)'] is not None]
|
28 |
+
|
29 |
+
if not gust_speeds:
|
30 |
+
return None, None, None, None
|
31 |
+
avg_gust = sum(gust_speeds) / len(gust_speeds)
|
32 |
+
min_gust = min(gust_speeds)
|
33 |
+
max_gust = max(gust_speeds)
|
34 |
+
avg_mean_speed = sum(mean_speeds) / len(mean_speeds) if mean_speeds else None
|
35 |
+
return avg_gust, min_gust, max_gust, avg_mean_speed
|
36 |
+
|
37 |
+
# Function to convert wind direction to degrees
|
38 |
+
def mean_wind_direction_to_degrees(direction):
|
39 |
+
directions = {
|
40 |
+
'North': 0, 'Northeast': 45, 'East': 90, 'Southeast': 135,
|
41 |
+
'South': 180, 'Southwest': 225, 'West': 270, 'Northwest': 315
|
42 |
+
}
|
43 |
+
return directions.get(direction, 0)
|
44 |
+
|
45 |
+
# Fetch GeoJSON data
|
46 |
+
url = 'https://csdi.vercel.app/weather/wind'
|
47 |
+
geo_data, fetch_time = fetch_geojson_data(url)
|
48 |
+
|
49 |
+
# Calculate wind statistics
|
50 |
+
avg_gust, min_gust, max_gust, avg_mean_speed = calculate_wind_stats(geo_data['features'])
|
51 |
+
|
52 |
+
# Create a map centered on a specific location
|
53 |
+
map_center = [22.35473034278638, 114.14827142452518] # Coordinates of Hong Kong
|
54 |
+
my_map = folium.Map(location=map_center, zoom_start=10.65, tiles='https://landsd.azure-api.net/dev/osm/xyz/basemap/gs/WGS84/tile/{z}/{x}/{y}.png?key=f4d3e21d4fc14954a1d5930d4dde3809',attr="Map infortmation from Lands Department")
|
55 |
+
|
56 |
+
folium.TileLayer(
|
57 |
+
tiles='https://mapapi.geodata.gov.hk/gs/api/v1.0.0/xyz/label/hk/en/wgs84/{z}/{x}/{y}.png',
|
58 |
+
attr="Map infortmation from Lands Department"
|
59 |
+
).add_to(my_map)
|
60 |
+
|
61 |
+
|
62 |
+
# Create a colormap for wind speed with limited width
|
63 |
+
colormap = cm.LinearColormap(colors=['#000000', '#0066eb', '#ff3d77', '#eb0000'],
|
64 |
+
vmin=0, vmax=85)
|
65 |
+
my_map.add_child(colormap)
|
66 |
+
|
67 |
+
# Function to calculate arrow size based on wind speed
|
68 |
+
def get_arrow_size(speed):
|
69 |
+
if speed is None:
|
70 |
+
return 20
|
71 |
+
return max(20, min(50, speed * 2))
|
72 |
+
|
73 |
+
# Add the GeoJSON data to the map with arrow markers
|
74 |
+
for feature in geo_data['features']:
|
75 |
+
coordinates = feature['geometry']['coordinates']
|
76 |
+
mean_wind_direction = feature['properties']['10-Minute Mean Wind Direction(Compass points)']
|
77 |
+
mean_speed = feature['properties']['10-Minute Mean Speed(km/hour)']
|
78 |
+
|
79 |
+
# Skip plotting if wind direction is null
|
80 |
+
if mean_wind_direction is None:
|
81 |
+
continue
|
82 |
+
|
83 |
+
# Calculate rotation angle for wind direction
|
84 |
+
rotation_angle = mean_wind_direction_to_degrees(mean_wind_direction)
|
85 |
+
|
86 |
+
# Calculate arrow size based on wind speed
|
87 |
+
arrow_size = get_arrow_size(mean_speed)
|
88 |
+
|
89 |
+
# Determine color based on wind speed
|
90 |
+
color = colormap(mean_speed) if mean_speed is not None else 'gray'
|
91 |
+
|
92 |
+
# Create an arrow marker for wind direction
|
93 |
+
folium.Marker(
|
94 |
+
location=[coordinates[1], coordinates[0]],
|
95 |
+
icon=folium.DivIcon(html=f"""
|
96 |
+
<div style="
|
97 |
+
width: {arrow_size}px; height: {arrow_size}px;
|
98 |
+
display: flex; align-items: center; justify-content: center;
|
99 |
+
transform: rotate({rotation_angle}deg);
|
100 |
+
">
|
101 |
+
<svg width="{arrow_size}" height="{arrow_size}" viewBox="0 0 24 24" fill="none" stroke="{color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
102 |
+
<line x1="12" y1="5" x2="12" y2="19"></line>
|
103 |
+
<polyline points="5 12 12 5 19 12"></polyline>
|
104 |
+
</svg>
|
105 |
+
</div>
|
106 |
+
"""),
|
107 |
+
popup=folium.Popup(f"""
|
108 |
+
<b>{feature['properties']['Automatic Weather Station']}</b><br>
|
109 |
+
Direction: {mean_wind_direction}<br>
|
110 |
+
Speed: {mean_speed} km/h<br>
|
111 |
+
Max Gust: {feature['properties']['10-Minute Maximum Gust(km/hour)']} km/h
|
112 |
+
""", max_width=300)
|
113 |
+
).add_to(my_map)
|
114 |
+
|
115 |
+
col1, col2, col3 = st.columns([1.65, 2, 1.15])
|
116 |
+
|
117 |
+
with col1:
|
118 |
+
if geo_data['features']:
|
119 |
+
wind_directions = [feature['properties']['10-Minute Mean Wind Direction(Compass points)'] for feature in
|
120 |
+
geo_data['features']]
|
121 |
+
direction_counts = {d: wind_directions.count(d) for d in
|
122 |
+
['North', 'Northeast', 'East', 'Southeast', 'South', 'Southwest', 'West', 'Northwest']}
|
123 |
+
|
124 |
+
# Prepare wind speeds for each direction
|
125 |
+
direction_speeds = {d: [] for d in
|
126 |
+
['North', 'Northeast', 'East', 'Southeast', 'South', 'Southwest', 'West', 'Northwest']}
|
127 |
+
for feature in geo_data['features']:
|
128 |
+
direction = feature['properties']['10-Minute Mean Wind Direction(Compass points)']
|
129 |
+
speed = feature['properties']['10-Minute Mean Speed(km/hour)']
|
130 |
+
if direction in direction_speeds and speed is not None:
|
131 |
+
direction_speeds[direction].append(speed)
|
132 |
+
|
133 |
+
# Calculate average wind speed for each direction
|
134 |
+
average_speeds = {d: sum(speeds) / len(speeds) if speeds else 0 for d, speeds in direction_speeds.items()}
|
135 |
+
|
136 |
+
# Plot wind direction rose with average wind speed
|
137 |
+
fig = go.Figure()
|
138 |
+
|
139 |
+
# Add polar bar for wind direction
|
140 |
+
fig.add_trace(go.Barpolar(
|
141 |
+
r=[direction_counts[d] for d in direction_counts.keys()],
|
142 |
+
theta=list(direction_counts.keys()),
|
143 |
+
name='Wind Direction Count',
|
144 |
+
marker_color='#0008ff',
|
145 |
+
opacity=0.5
|
146 |
+
))
|
147 |
+
|
148 |
+
# Add radial bar for average wind speed
|
149 |
+
fig.add_trace(go.Barpolar(
|
150 |
+
r=list(average_speeds.values()),
|
151 |
+
theta=list(average_speeds.keys()),
|
152 |
+
name='Average Wind Speed',
|
153 |
+
marker_color='#ff0019', # Orange color for wind speed
|
154 |
+
opacity=0.5,
|
155 |
+
thetaunit='radians', # Ensures radial bars are correctly positioned
|
156 |
+
base=0 # Base of the radial bars starts from 0
|
157 |
+
))
|
158 |
+
|
159 |
+
fig.update_layout(
|
160 |
+
polar=dict(
|
161 |
+
radialaxis=dict(
|
162 |
+
visible=False,
|
163 |
+
range=[0, max(direction_counts.values())]
|
164 |
+
),
|
165 |
+
angularaxis=dict(
|
166 |
+
tickvals=list(direction_counts.keys()),
|
167 |
+
ticktext=['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'],
|
168 |
+
rotation=90, # Rotate to make North the top
|
169 |
+
direction='clockwise'
|
170 |
+
)
|
171 |
+
),
|
172 |
+
width=500,
|
173 |
+
height=380,
|
174 |
+
title={'text': 'Wind Direction and Average Speed Rose Plot', 'font': {'size': 18}},
|
175 |
+
legend={'x': 0.8, 'y': 0.95}
|
176 |
+
)
|
177 |
+
|
178 |
+
st.plotly_chart(fig, use_container_width=True)
|
179 |
+
|
180 |
+
st.caption(f"Data fetched at: {fetch_time}")
|
181 |
+
|
182 |
+
if avg_gust is not None:
|
183 |
+
col1a, col1b = st.columns(2)
|
184 |
+
with col1a:
|
185 |
+
st.metric(label="Avg Max Gust (km/h)", value=f"{avg_gust:.2f}")
|
186 |
+
st.metric(label="Min Max Gust (km/h)", value=f"{min_gust}")
|
187 |
+
with col1b:
|
188 |
+
st.metric(label="Max Max Gust (km/h)", value=f"{max_gust}")
|
189 |
+
if avg_mean_speed is not None:
|
190 |
+
st.metric(label="Avg Mean Speed (km/h)", value=f"{avg_mean_speed:.2f}")
|
191 |
+
else:
|
192 |
+
st.write("No valid wind data available to calculate statistics.")
|
193 |
+
|
194 |
+
gust_speeds = [feature['properties']['10-Minute Maximum Gust(km/hour)'] for feature in geo_data['features'] if
|
195 |
+
feature['properties']['10-Minute Maximum Gust(km/hour)'] is not None]
|
196 |
+
|
197 |
+
|
198 |
+
with col3:
|
199 |
+
table_data = [{
|
200 |
+
'Weather Station': feature['properties']['Automatic Weather Station'],
|
201 |
+
'Mean Wind Direction': feature['properties']['10-Minute Mean Wind Direction(Compass points)'],
|
202 |
+
'Mean Speed(km/hour)': feature['properties']['10-Minute Mean Speed(km/hour)'],
|
203 |
+
'Maximum Gust(km/hour)': feature['properties']['10-Minute Maximum Gust(km/hour)']
|
204 |
+
} for feature in geo_data['features']]
|
205 |
+
|
206 |
+
st.dataframe(pd.DataFrame(table_data), height=600)
|
207 |
+
|
208 |
+
with col2:
|
209 |
+
# Display map
|
210 |
+
st_folium(my_map, width=500, height=600)
|
pages/2_Temperature.py
ADDED
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
import requests
|
3 |
+
import json
|
4 |
+
import pandas as pd
|
5 |
+
import folium
|
6 |
+
from streamlit_folium import st_folium
|
7 |
+
import plotly.graph_objects as go
|
8 |
+
import numpy as np
|
9 |
+
from datetime import datetime, timezone
|
10 |
+
import time
|
11 |
+
import pytz
|
12 |
+
|
13 |
+
# Set page layout to wide
|
14 |
+
st.set_page_config(layout="wide", page_title="Real-Time Temperature Data Dashboard")
|
15 |
+
|
16 |
+
# Function to fetch JSON data with caching and expiration
|
17 |
+
@st.cache_data(ttl=300) # Cache data for 5 minutes (300 seconds)
|
18 |
+
def fetch_data():
|
19 |
+
url = 'https://csdi.vercel.app/weather/temp'
|
20 |
+
response = requests.get(url)
|
21 |
+
hk_tz = pytz.timezone('Asia/Hong_Kong')
|
22 |
+
fetch_time = datetime.now(hk_tz).strftime('%Y-%m-%dT%H:%M:%S')
|
23 |
+
return json.loads(response.text), fetch_time
|
24 |
+
|
25 |
+
# Fetch the JSON data
|
26 |
+
data, fetch_time = fetch_data()
|
27 |
+
|
28 |
+
# Create a Pandas DataFrame from the JSON data
|
29 |
+
features = data['features']
|
30 |
+
df = pd.json_normalize(features)
|
31 |
+
|
32 |
+
# Rename columns for easier access
|
33 |
+
df.rename(columns={
|
34 |
+
'properties.Automatic Weather Station': 'Station',
|
35 |
+
'properties.Air Temperature(degree Celsius)': 'Temperature',
|
36 |
+
'geometry.coordinates': 'Coordinates'
|
37 |
+
}, inplace=True)
|
38 |
+
|
39 |
+
# Split Coordinates into separate Longitude and Latitude columns
|
40 |
+
df[['Longitude', 'Latitude']] = pd.DataFrame(df['Coordinates'].tolist(), index=df.index)
|
41 |
+
|
42 |
+
# Extract temperature data
|
43 |
+
temps = df['Temperature'].dropna().tolist()
|
44 |
+
|
45 |
+
# Create three columns
|
46 |
+
col1, col2, col3 = st.columns([1.65, 2, 1.15])
|
47 |
+
|
48 |
+
# Column 1: Histogram and statistics with two-sigma analysis
|
49 |
+
with col1:
|
50 |
+
# Row 1: Histogram
|
51 |
+
with st.container():
|
52 |
+
# Convert list to pandas Series
|
53 |
+
temps_series = pd.Series(temps)
|
54 |
+
|
55 |
+
# Calculate histogram data
|
56 |
+
hist_data = np.histogram(temps_series, bins=10)
|
57 |
+
bin_edges = hist_data[1]
|
58 |
+
counts = hist_data[0]
|
59 |
+
|
60 |
+
|
61 |
+
# Create a color gradient from blue to red
|
62 |
+
def get_color(value, min_value, max_value):
|
63 |
+
ratio = (value - min_value) / (max_value - min_value)
|
64 |
+
r = int(255 * ratio) # Red component
|
65 |
+
b = int(255 * (1 - ratio)) # Blue component
|
66 |
+
return f'rgb({r}, 0, {b})'
|
67 |
+
|
68 |
+
|
69 |
+
# Create histogram with Plotly Graph Objects
|
70 |
+
fig = go.Figure()
|
71 |
+
|
72 |
+
# Add histogram bars with gradient colors
|
73 |
+
for i in range(len(bin_edges) - 1):
|
74 |
+
bin_center = (bin_edges[i] + bin_edges[i + 1]) / 2
|
75 |
+
color = get_color(bin_center, bin_edges.min(), bin_edges.max())
|
76 |
+
fig.add_trace(go.Bar(
|
77 |
+
x=[f'{bin_edges[i]:.1f} - {bin_edges[i + 1]:.1f}'],
|
78 |
+
y=[counts[i]],
|
79 |
+
marker_color=color,
|
80 |
+
name=f'{bin_edges[i]:.1f} - {bin_edges[i + 1]:.1f}'
|
81 |
+
))
|
82 |
+
|
83 |
+
# Customize layout
|
84 |
+
fig.update_layout(
|
85 |
+
xaxis_title='Temperature (°C)',
|
86 |
+
yaxis_title='Count',
|
87 |
+
title='Temperature Distribution',
|
88 |
+
bargap=0.2, # Adjust gap between bars
|
89 |
+
title_font_size=20,
|
90 |
+
xaxis_title_font_size=14,
|
91 |
+
yaxis_title_font_size=14,
|
92 |
+
height=350, # Set plot height
|
93 |
+
xaxis=dict(title_font_size=14),
|
94 |
+
yaxis=dict(title_font_size=14)
|
95 |
+
)
|
96 |
+
|
97 |
+
# Display the plot in Streamlit
|
98 |
+
st.plotly_chart(fig, use_container_width=True)
|
99 |
+
st.caption(f"Data fetched at: {fetch_time}")
|
100 |
+
|
101 |
+
# Row 2: Statistics
|
102 |
+
with st.container():
|
103 |
+
col_1, col_2 = st.columns([1, 1])
|
104 |
+
with col_1:
|
105 |
+
if temps:
|
106 |
+
avg_temp = np.mean(temps)
|
107 |
+
std_temp = np.std(temps)
|
108 |
+
max_temp = np.max(temps)
|
109 |
+
min_temp = np.min(temps)
|
110 |
+
|
111 |
+
two_sigma_range = (avg_temp - 2 * std_temp, avg_temp + 2 * std_temp)
|
112 |
+
|
113 |
+
st.metric(label="Average Temperature (°C)", value=f"{avg_temp:.2f}")
|
114 |
+
st.metric(label="Minimum Temperature (°C)", value=f"{min_temp:.2f}")
|
115 |
+
with col_2:
|
116 |
+
st.metric(label="Maximum Temperature (°C)", value=f"{max_temp:.2f}")
|
117 |
+
st.metric(label="Std. Dev (°C)", value=f"{std_temp:.2f}")
|
118 |
+
|
119 |
+
|
120 |
+
# Column 2: Map
|
121 |
+
def temperature_to_color(temp, min_temp, max_temp):
|
122 |
+
"""Convert temperature to a color based on the gradient from blue (low) to red (high)."""
|
123 |
+
norm_temp = (temp - min_temp) / (max_temp - min_temp)
|
124 |
+
red = int(255 * norm_temp)
|
125 |
+
blue = int(255 * (1 - norm_temp))
|
126 |
+
return f'rgb({red}, 0, {blue})'
|
127 |
+
|
128 |
+
with col2:
|
129 |
+
# Create the base map
|
130 |
+
m = folium.Map(location=[22.3547, 114.1483], zoom_start=11, tiles='https://landsd.azure-api.net/dev/osm/xyz/basemap/gs/WGS84/tile/{z}/{x}/{y}.png?key=f4d3e21d4fc14954a1d5930d4dde3809',attr="Map infortmation from Lands Department")
|
131 |
+
folium.TileLayer(
|
132 |
+
tiles='https://mapapi.geodata.gov.hk/gs/api/v1.0.0/xyz/label/hk/en/wgs84/{z}/{x}/{y}.png',
|
133 |
+
attr="Map infortmation from Lands Department"
|
134 |
+
).add_to(m)
|
135 |
+
# Determine min and max temperatures for color scaling
|
136 |
+
min_temp = df['Temperature'].min()
|
137 |
+
max_temp = df['Temperature'].max()
|
138 |
+
|
139 |
+
# Create a color scale legend
|
140 |
+
colormap = folium.LinearColormap(
|
141 |
+
colors=['blue', 'white', 'red'],
|
142 |
+
index=[min_temp, (min_temp + max_temp) / 2, max_temp],
|
143 |
+
vmin=min_temp,
|
144 |
+
vmax=max_temp,
|
145 |
+
caption='Temperature (°C)'
|
146 |
+
)
|
147 |
+
colormap.add_to(m)
|
148 |
+
|
149 |
+
# Iterate through each row in the DataFrame
|
150 |
+
for _, row in df.iterrows():
|
151 |
+
lat = row['Latitude']
|
152 |
+
lon = row['Longitude']
|
153 |
+
station = row['Station']
|
154 |
+
temp = row['Temperature']
|
155 |
+
|
156 |
+
# Determine the color based on the temperature
|
157 |
+
color = temperature_to_color(temp, min_temp, max_temp) if pd.notna(temp) else 'gray'
|
158 |
+
|
159 |
+
# Create a marker with temperature data
|
160 |
+
folium.Marker(
|
161 |
+
location=[lat, lon],
|
162 |
+
popup=f"<p style='font-size: 12px; background-color: white; padding: 5px; border-radius: 5px;'>{station}: {temp:.1f}°C</p>",
|
163 |
+
icon=folium.DivIcon(
|
164 |
+
html=f'<div style="font-size: 10pt; color: {color}; padding: 2px; border-radius: 5px;">'
|
165 |
+
f'<strong>{temp:.1f}°C</strong></div>'
|
166 |
+
)
|
167 |
+
).add_to(m)
|
168 |
+
|
169 |
+
# Render the map in Streamlit
|
170 |
+
st_folium(m, width=500, height=600)
|
171 |
+
|
172 |
+
# Column 3: Data table
|
173 |
+
with col3:
|
174 |
+
# Set the table height using CSS
|
175 |
+
st.markdown(
|
176 |
+
"""
|
177 |
+
<style>
|
178 |
+
.dataframe-container {
|
179 |
+
height: 600px;
|
180 |
+
overflow-y: auto;
|
181 |
+
}
|
182 |
+
</style>
|
183 |
+
""",
|
184 |
+
unsafe_allow_html=True
|
185 |
+
)
|
186 |
+
|
187 |
+
# Display the DataFrame with the custom CSS class
|
188 |
+
st.dataframe(df[['Station', 'Temperature', 'Latitude', 'Longitude']], height=600)
|
189 |
+
|
190 |
+
# Add a refresh button
|
191 |
+
if st.button("Refresh Data"):
|
192 |
+
st.experimental_rerun()
|
193 |
+
|
194 |
+
# Automatically rerun every 5 minutes
|
195 |
+
if 'last_ran' not in st.session_state:
|
196 |
+
st.session_state.last_ran = datetime.now(timezone.utc)
|
197 |
+
|
198 |
+
current_time = datetime.now(timezone.utc)
|
199 |
+
if (current_time - st.session_state.last_ran).total_seconds() > 300:
|
200 |
+
st.session_state.last_ran = current_time
|
201 |
+
st.experimental_rerun()
|
pages/3_Humidity.py
ADDED
@@ -0,0 +1,205 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
import pandas as pd
|
3 |
+
import plotly.express as px
|
4 |
+
import plotly.graph_objects as go
|
5 |
+
import folium
|
6 |
+
from folium import LinearColormap
|
7 |
+
import requests
|
8 |
+
from datetime import datetime, timedelta
|
9 |
+
from streamlit_folium import st_folium
|
10 |
+
import pytz
|
11 |
+
from datetime import datetime
|
12 |
+
|
13 |
+
# Set page layout to wide
|
14 |
+
st.set_page_config(layout="wide", page_title="Real-Time Relative Humidity Data Dashboard")
|
15 |
+
|
16 |
+
# Function to load data
|
17 |
+
@st.cache_data(ttl=300) # Cache data to avoid reloading every time
|
18 |
+
def load_data():
|
19 |
+
with st.spinner("Loading data..."):
|
20 |
+
response = requests.get("https://csdi.vercel.app/weather/rhum")
|
21 |
+
data = response.json()
|
22 |
+
features = data['features']
|
23 |
+
df = pd.json_normalize(features)
|
24 |
+
df.rename(columns={
|
25 |
+
'properties.Relative Humidity(percent)': 'Relative Humidity (%)',
|
26 |
+
'properties.Automatic Weather Station': 'Station Name',
|
27 |
+
'geometry.coordinates': 'Coordinates'
|
28 |
+
}, inplace=True)
|
29 |
+
df.dropna(subset=['Relative Humidity (%)'], inplace=True)
|
30 |
+
hk_tz = pytz.timezone('Asia/Hong_Kong')
|
31 |
+
fetch_time = datetime.now(hk_tz).strftime('%Y-%m-%dT%H:%M:%S')
|
32 |
+
return df, fetch_time
|
33 |
+
|
34 |
+
# Check if the data has been loaded before
|
35 |
+
if 'last_run' not in st.session_state or (datetime.now() - st.session_state.last_run) > timedelta(minutes=5):
|
36 |
+
st.session_state.df, st.session_state.fetch_time = load_data()
|
37 |
+
st.session_state.last_run = datetime.now()
|
38 |
+
|
39 |
+
# Data
|
40 |
+
df = st.session_state.df
|
41 |
+
fetch_time = st.session_state.fetch_time
|
42 |
+
|
43 |
+
# Compute statistics
|
44 |
+
humidity_data = df['Relative Humidity (%)']
|
45 |
+
avg_humidity = humidity_data.mean()
|
46 |
+
max_humidity = humidity_data.max()
|
47 |
+
min_humidity = humidity_data.min()
|
48 |
+
std_humidity = humidity_data.std()
|
49 |
+
|
50 |
+
# Create three columns
|
51 |
+
col1, col2, col3 = st.columns([1.65, 2, 1.15])
|
52 |
+
|
53 |
+
# Column 1: Histogram and statistics
|
54 |
+
with col1:
|
55 |
+
# Define colors for gradient
|
56 |
+
color_scale = ['#58a0db', '#0033cc']
|
57 |
+
|
58 |
+
# Create histogram
|
59 |
+
fig = px.histogram(df, x='Relative Humidity (%)', nbins=20,
|
60 |
+
labels={'Relative Humidity (%)': 'Relative Humidity (%)'},
|
61 |
+
title='Relative Humidity Histogram',
|
62 |
+
color_discrete_sequence=color_scale)
|
63 |
+
|
64 |
+
# Add average line
|
65 |
+
fig.add_shape(
|
66 |
+
go.layout.Shape(
|
67 |
+
type="line",
|
68 |
+
x0=avg_humidity,
|
69 |
+
y0=0,
|
70 |
+
x1=avg_humidity,
|
71 |
+
y1=df['Relative Humidity (%)'].value_counts().max(),
|
72 |
+
line=dict(color="red", width=2, dash="dash"),
|
73 |
+
)
|
74 |
+
)
|
75 |
+
|
76 |
+
# Update layout
|
77 |
+
fig.update_layout(
|
78 |
+
xaxis_title='Relative Humidity (%)',
|
79 |
+
yaxis_title='Count',
|
80 |
+
title='Relative Humidity Distribution',
|
81 |
+
bargap=0.2,
|
82 |
+
title_font_size=20,
|
83 |
+
xaxis_title_font_size=14,
|
84 |
+
yaxis_title_font_size=14,
|
85 |
+
height=350,
|
86 |
+
shapes=[{
|
87 |
+
'type': 'rect',
|
88 |
+
'x0': min_humidity,
|
89 |
+
'x1': max_humidity,
|
90 |
+
'y0': 0,
|
91 |
+
'y1': df['Relative Humidity (%)'].value_counts().max(),
|
92 |
+
'fillcolor': 'rgba(0, 100, 255, 0.2)',
|
93 |
+
'line': {
|
94 |
+
'color': 'rgba(0, 100, 255, 0.2)',
|
95 |
+
'width': 0
|
96 |
+
},
|
97 |
+
'opacity': 0.1
|
98 |
+
}]
|
99 |
+
)
|
100 |
+
|
101 |
+
# Add annotations
|
102 |
+
fig.add_annotation(
|
103 |
+
x=avg_humidity,
|
104 |
+
y=df['Relative Humidity (%)'].value_counts().max() * 0.9,
|
105 |
+
text=f"Average: {avg_humidity:.2f}%",
|
106 |
+
showarrow=True,
|
107 |
+
arrowhead=1
|
108 |
+
)
|
109 |
+
|
110 |
+
st.plotly_chart(fig, use_container_width=True)
|
111 |
+
st.caption(f"Data fetched at: {fetch_time}")
|
112 |
+
|
113 |
+
# Display statistics
|
114 |
+
col_1, col_2 = st.columns([1, 1])
|
115 |
+
with col_1:
|
116 |
+
st.metric(label="Average R.Humidity (%)", value=f"{avg_humidity:.2f}")
|
117 |
+
st.metric(label="Minimum R.Humidity (%)", value=f"{min_humidity:.2f}")
|
118 |
+
with col_2:
|
119 |
+
st.metric(label="Maximum R.Humidity (%)", value=f"{max_humidity:.2f}")
|
120 |
+
st.metric(label="Std. Dev (%)", value=f"{std_humidity:.2f}")
|
121 |
+
|
122 |
+
|
123 |
+
# Function to convert humidity to color based on gradient
|
124 |
+
def humidity_to_color(humidity, min_humidity, max_humidity):
|
125 |
+
if pd.isna(humidity):
|
126 |
+
return 'rgba(0, 0, 0, 0)' # Return a transparent color if the humidity is NaN
|
127 |
+
|
128 |
+
norm_humidity = (humidity - min_humidity) / (max_humidity - min_humidity)
|
129 |
+
|
130 |
+
# Colors from light blue (#add8e6) to dark blue (#00008b)
|
131 |
+
if norm_humidity < 0.5:
|
132 |
+
r = int(173 + (0 - 173) * (2 * norm_humidity))
|
133 |
+
g = int(216 + (0 - 216) * (2 * norm_humidity))
|
134 |
+
b = int(230 + (139 - 230) * (2 * norm_humidity))
|
135 |
+
else:
|
136 |
+
r = int(0 + (0 - 0) * (2 * (norm_humidity - 0.5)))
|
137 |
+
g = int(0 + (0 - 0) * (2 * (norm_humidity - 0.5)))
|
138 |
+
b = int(139 + (139 - 139) * (2 * (norm_humidity - 0.5)))
|
139 |
+
|
140 |
+
return f'rgb({r}, {g}, {b})'
|
141 |
+
|
142 |
+
# Column 2: Map
|
143 |
+
with col2:
|
144 |
+
with st.spinner("Loading map..."):
|
145 |
+
m = folium.Map(location=[22.3547, 114.1483], zoom_start=11, tiles='https://landsd.azure-api.net/dev/osm/xyz/basemap/gs/WGS84/tile/{z}/{x}/{y}.png?key=f4d3e21d4fc14954a1d5930d4dde3809',attr="Map infortmation from Lands Department")
|
146 |
+
folium.TileLayer(
|
147 |
+
tiles='https://mapapi.geodata.gov.hk/gs/api/v1.0.0/xyz/label/hk/en/wgs84/{z}/{x}/{y}.png',
|
148 |
+
attr="Map infortmation from Lands Department"
|
149 |
+
).add_to(m)
|
150 |
+
|
151 |
+
min_humidity = df['Relative Humidity (%)'].min()
|
152 |
+
max_humidity = df['Relative Humidity (%)'].max()
|
153 |
+
|
154 |
+
colormap = LinearColormap(
|
155 |
+
colors=['#58a0db', 'blue'],
|
156 |
+
index=[min_humidity, max_humidity],
|
157 |
+
vmin=min_humidity,
|
158 |
+
vmax=max_humidity,
|
159 |
+
caption='Relative Humidity (%)'
|
160 |
+
)
|
161 |
+
colormap.add_to(m)
|
162 |
+
|
163 |
+
for _, row in df.iterrows():
|
164 |
+
humidity = row['Relative Humidity (%)']
|
165 |
+
color = humidity_to_color(humidity, min_humidity, max_humidity)
|
166 |
+
|
167 |
+
folium.Marker(
|
168 |
+
location=[row['Coordinates'][1], row['Coordinates'][0]],
|
169 |
+
popup=f"<p style='font-size: 12px; background-color: white; padding: 5px; border-radius: 5px;'>{row['Station Name']}: {humidity:.1f}%</p>",
|
170 |
+
icon=folium.DivIcon(
|
171 |
+
html=f'<div style="font-size: 10pt; color: {color}; padding: 2px; border-radius: 5px;">'
|
172 |
+
f'<strong>{humidity:.1f}%</strong></div>'
|
173 |
+
)
|
174 |
+
).add_to(m)
|
175 |
+
|
176 |
+
st_folium(m, width=500, height=600)
|
177 |
+
|
178 |
+
# Column 3: Data Table
|
179 |
+
with col3:
|
180 |
+
st.markdown(
|
181 |
+
"""
|
182 |
+
<style>
|
183 |
+
.dataframe-container {
|
184 |
+
height: 600px;
|
185 |
+
overflow-y: auto;
|
186 |
+
}
|
187 |
+
.dataframe th, .dataframe td {
|
188 |
+
text-align: left;
|
189 |
+
padding: 8px;
|
190 |
+
}
|
191 |
+
</style>
|
192 |
+
""",
|
193 |
+
unsafe_allow_html=True
|
194 |
+
)
|
195 |
+
|
196 |
+
# Rename column for display
|
197 |
+
df_display = df[['Station Name', 'Relative Humidity (%)']].rename(columns={'Relative Humidity (%)': 'R.Humidity'})
|
198 |
+
st.dataframe(df_display, height=600)
|
199 |
+
|
200 |
+
# Refresh Button
|
201 |
+
if st.button("Refresh Data"):
|
202 |
+
with st.spinner("Refreshing data..."):
|
203 |
+
st.session_state.df, st.session_state.fetch_time = load_data()
|
204 |
+
st.session_state.last_run = datetime.now()
|
205 |
+
st.experimental_rerun()
|
pages/4_Smart Lampposts.py
ADDED
@@ -0,0 +1,182 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
import pandas as pd
|
3 |
+
import requests
|
4 |
+
import plotly.express as px
|
5 |
+
import plotly.graph_objs as go
|
6 |
+
from folium import DivIcon
|
7 |
+
import folium
|
8 |
+
from streamlit_folium import st_folium
|
9 |
+
from sklearn.linear_model import LinearRegression
|
10 |
+
from sklearn.cluster import DBSCAN
|
11 |
+
import matplotlib.cm as cm
|
12 |
+
import matplotlib.colors as mcolors
|
13 |
+
import time
|
14 |
+
import json
|
15 |
+
import pytz
|
16 |
+
from datetime import datetime
|
17 |
+
|
18 |
+
# Set page layout to wide
|
19 |
+
st.set_page_config(layout="wide", page_title="Real-Time Smart Lamppost Data Dashboard")
|
20 |
+
|
21 |
+
# Function to fetch JSON data with caching and expiration
|
22 |
+
@st.cache_data(ttl=600)
|
23 |
+
def fetch_data(url):
|
24 |
+
response = requests.get(url)
|
25 |
+
hk_tz = pytz.timezone('Asia/Hong_Kong')
|
26 |
+
fetch_time = datetime.now(hk_tz).strftime('%Y-%m-%dT%H:%M:%S')
|
27 |
+
return json.loads(response.text), fetch_time
|
28 |
+
|
29 |
+
# Function to calculate "feels like" temperature
|
30 |
+
def feels_like_temperature(temp_celsius, humidity_percent):
|
31 |
+
return temp_celsius - (0.55 - 0.0055 * humidity_percent) * (temp_celsius - 14.5)
|
32 |
+
|
33 |
+
# Function to process the raw data into a DataFrame
|
34 |
+
def process_data(data):
|
35 |
+
features = data['features']
|
36 |
+
records = [
|
37 |
+
{
|
38 |
+
'latitude': feature['geometry']['coordinates'][1],
|
39 |
+
'longitude': feature['geometry']['coordinates'][0],
|
40 |
+
'temperature': feature['properties'].get('Air temperature (°C) / 氣溫 (°C) / 气温 (°C)'),
|
41 |
+
'humidity': feature['properties'].get('Relative humidity (%) / 相對濕度 (%) / 相对湿度 (%)')
|
42 |
+
}
|
43 |
+
for feature in features
|
44 |
+
]
|
45 |
+
df = pd.DataFrame(records)
|
46 |
+
|
47 |
+
# Convert temperature and humidity to numeric, forcing errors to NaN
|
48 |
+
df['temperature'] = pd.to_numeric(df['temperature'], errors='coerce')
|
49 |
+
df['humidity'] = pd.to_numeric(df['humidity'], errors='coerce')
|
50 |
+
|
51 |
+
# Drop rows with NaN values
|
52 |
+
df = df.dropna(subset=['temperature', 'humidity'])
|
53 |
+
|
54 |
+
# Calculate "feels like" temperature
|
55 |
+
df['feels_like'] = df.apply(lambda row: feels_like_temperature(row['temperature'], row['humidity']), axis=1)
|
56 |
+
|
57 |
+
return df
|
58 |
+
|
59 |
+
# Fetch and process data
|
60 |
+
url = "https://csdi.vercel.app/weather/smls"
|
61 |
+
data, fetch_time = fetch_data(url)
|
62 |
+
df = process_data(data)
|
63 |
+
|
64 |
+
# Perform clustering using DBSCAN
|
65 |
+
coords = df[['latitude', 'longitude']].values
|
66 |
+
db = DBSCAN(eps=0.01, min_samples=5).fit(coords)
|
67 |
+
df['cluster'] = db.labels_
|
68 |
+
|
69 |
+
# Initialize the 'predicted_humidity' column with NaN
|
70 |
+
df['predicted_humidity'] = pd.NA
|
71 |
+
|
72 |
+
# Perform linear regression for each cluster
|
73 |
+
for cluster in df['cluster'].unique():
|
74 |
+
cluster_data = df[df['cluster'] == cluster]
|
75 |
+
if len(cluster_data) > 1: # Only perform regression if there are enough points
|
76 |
+
X = cluster_data['temperature'].values.reshape(-1, 1)
|
77 |
+
y = cluster_data['humidity'].values
|
78 |
+
reg = LinearRegression().fit(X, y)
|
79 |
+
df.loc[df['cluster'] == cluster, 'predicted_humidity'] = reg.predict(X)
|
80 |
+
|
81 |
+
# Calculate temperature statistics
|
82 |
+
temp_stats = df['temperature'].describe()
|
83 |
+
avg_temp = temp_stats['mean']
|
84 |
+
min_temp = temp_stats['min']
|
85 |
+
max_temp = temp_stats['max']
|
86 |
+
std_temp = temp_stats['std']
|
87 |
+
|
88 |
+
# Create regression plot using Plotly
|
89 |
+
fig = px.scatter(df, x='temperature', y='humidity', color='cluster',
|
90 |
+
title='Temperature vs. Relative Humidity with Regression by Cluster')
|
91 |
+
|
92 |
+
# Add regression lines to the plot
|
93 |
+
for cluster in df['cluster'].unique():
|
94 |
+
cluster_data = df[df['cluster'] == cluster]
|
95 |
+
if 'predicted_humidity' in cluster_data.columns and not cluster_data['predicted_humidity'].isna().all():
|
96 |
+
fig.add_trace(go.Scatter(x=cluster_data['temperature'], y=cluster_data['predicted_humidity'], mode='lines',
|
97 |
+
name=f'Cluster {cluster}'))
|
98 |
+
|
99 |
+
# Column 1: Regression Plot, Data, and Statistics
|
100 |
+
col1, col2, col3 = st.columns([1.65, 2, 1.15])
|
101 |
+
|
102 |
+
with col1:
|
103 |
+
st.plotly_chart(fig, use_container_width=True, height=300)
|
104 |
+
st.caption(f"Data fetched at: {fetch_time}")
|
105 |
+
|
106 |
+
# Display temperature statistics
|
107 |
+
col_1, col_2 = st.columns([1, 1])
|
108 |
+
with col_1:
|
109 |
+
st.metric(label="Average Temperature (°C)", value=f"{avg_temp:.2f}")
|
110 |
+
st.metric(label="Minimum Temperature (°C)", value=f"{min_temp:.2f}")
|
111 |
+
with col_2:
|
112 |
+
st.metric(label="Maximum Temperature (°C)", value=f"{max_temp:.2f}")
|
113 |
+
st.metric(label="Std. Dev (°C)", value=f"{std_temp:.2f}")
|
114 |
+
|
115 |
+
# Column 2: Map
|
116 |
+
with col2:
|
117 |
+
# Initialize the Folium map
|
118 |
+
m = folium.Map(location=[22.320394086610452, 114.21626912476121], zoom_start=14, tiles='https://landsd.azure-api.net/dev/osm/xyz/basemap/gs/WGS84/tile/{z}/{x}/{y}.png?key=f4d3e21d4fc14954a1d5930d4dde3809',attr="Map infortmation from Lands Department")
|
119 |
+
|
120 |
+
folium.TileLayer(
|
121 |
+
tiles='https://mapapi.geodata.gov.hk/gs/api/v1.0.0/xyz/label/hk/en/wgs84/{z}/{x}/{y}.png',
|
122 |
+
attr="Map infortmation from Lands Department"
|
123 |
+
).add_to(m)
|
124 |
+
|
125 |
+
# Define a color map for clusters
|
126 |
+
unique_clusters = df['cluster'].unique()
|
127 |
+
colors = cm.get_cmap('tab10', len(unique_clusters)) # Using 'tab10' colormap for up to 10 clusters
|
128 |
+
cluster_colors = {cluster: mcolors.to_hex(colors(i)) for i, cluster in enumerate(unique_clusters)}
|
129 |
+
|
130 |
+
# Plot original data points
|
131 |
+
for _, row in df.iterrows():
|
132 |
+
folium.CircleMarker(
|
133 |
+
location=[row['latitude'], row['longitude']],
|
134 |
+
radius=5,
|
135 |
+
color=cluster_colors[row['cluster']],
|
136 |
+
fill=True,
|
137 |
+
fill_color=cluster_colors[row['cluster']],
|
138 |
+
fill_opacity=0.7,
|
139 |
+
popup=f"Temp: {row['temperature']} °C<br>Humidity: {row['humidity']} %<br>Feels Like: {row['feels_like']:.2f} °C<br>Cluster: {row['cluster']}"
|
140 |
+
).add_to(m)
|
141 |
+
|
142 |
+
# Calculate the average temperature for each cluster
|
143 |
+
cluster_centers = df.groupby('cluster').agg({
|
144 |
+
'latitude': 'mean',
|
145 |
+
'longitude': 'mean',
|
146 |
+
'temperature': 'mean'
|
147 |
+
}).reset_index()
|
148 |
+
|
149 |
+
# Plot cluster centers
|
150 |
+
for _, row in cluster_centers.iterrows():
|
151 |
+
folium.Marker(
|
152 |
+
location=[row['latitude'], row['longitude']],
|
153 |
+
icon=DivIcon(
|
154 |
+
icon_size=(150,36),
|
155 |
+
icon_anchor=(85, 20), # Adjusted anchor position to move text away from the point
|
156 |
+
html=f'<strong><div style="font-size: 15px; color: {cluster_colors[row["cluster"]]}">{row["temperature"]:.2f} °C</div></strong>'
|
157 |
+
),
|
158 |
+
popup=f"Cluster: {row['cluster']}<br>Avg Temp: {row['temperature']:.2f} °C"
|
159 |
+
).add_to(m)
|
160 |
+
|
161 |
+
# Display the map in Streamlit
|
162 |
+
st_folium(m, width=500, height=600)
|
163 |
+
|
164 |
+
# Column 3: Data Table
|
165 |
+
with col3:
|
166 |
+
st.markdown(
|
167 |
+
"""
|
168 |
+
<style>
|
169 |
+
.dataframe-container {
|
170 |
+
height: 600px;
|
171 |
+
overflow-y: auto;
|
172 |
+
}
|
173 |
+
.dataframe th, .dataframe td {
|
174 |
+
text-align: left;
|
175 |
+
padding: 8px;
|
176 |
+
}
|
177 |
+
</style>
|
178 |
+
""",
|
179 |
+
unsafe_allow_html=True
|
180 |
+
)
|
181 |
+
# Display the DataFrame
|
182 |
+
st.dataframe(df[['latitude', 'longitude', 'temperature', 'humidity', 'feels_like', 'cluster']], height=600)
|
pages/5_Past Records.py
ADDED
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
import pandas as pd
|
3 |
+
import plotly.express as px
|
4 |
+
import json
|
5 |
+
import os
|
6 |
+
import glob
|
7 |
+
|
8 |
+
# Directory paths
|
9 |
+
data_dir = 'past_temp'
|
10 |
+
geojson_file = os.path.join(data_dir, 'FavgTS.geojson')
|
11 |
+
|
12 |
+
# Load GeoJSON data
|
13 |
+
def load_geojson():
|
14 |
+
with open(geojson_file) as f:
|
15 |
+
return json.load(f)
|
16 |
+
|
17 |
+
# Create a dictionary to map short forms to long forms
|
18 |
+
def create_station_map(geojson):
|
19 |
+
feature_map = {}
|
20 |
+
for feature in geojson['features']:
|
21 |
+
short_name = feature['properties']['WeatherStationShortName']
|
22 |
+
long_name = feature['properties']['WeatherStationName_en']
|
23 |
+
feature_map[short_name] = long_name
|
24 |
+
return feature_map
|
25 |
+
|
26 |
+
# Load CSV files
|
27 |
+
def load_csv_files():
|
28 |
+
return glob.glob(os.path.join(data_dir, '*.csv'))
|
29 |
+
|
30 |
+
# Plot time series
|
31 |
+
def plot_time_series(df, station_name):
|
32 |
+
df['Date'] = pd.to_datetime(df['Date'], format='%Y%m%d', errors='coerce')
|
33 |
+
df['Year'] = df['Date'].dt.year
|
34 |
+
df['Month'] = df['Date'].dt.month
|
35 |
+
|
36 |
+
fig_all_years = px.line(df, x='Date', y='Value', color='Year',
|
37 |
+
title=f'All-Year Temperature Time Series for {station_name}',
|
38 |
+
labels={'Date': 'Date', 'Value': 'Temperature (°C)', 'Year': 'Year'},
|
39 |
+
line_shape='linear')
|
40 |
+
fig_all_years.update_layout(xaxis_title='Date', yaxis_title='Temperature (°C)')
|
41 |
+
|
42 |
+
return fig_all_years
|
43 |
+
|
44 |
+
# Plot monthly averages
|
45 |
+
def plot_monthly_averages(df, station_name):
|
46 |
+
df['Date'] = pd.to_datetime(df['Date'], format='%Y%m%d', errors='coerce')
|
47 |
+
df['Year'] = df['Date'].dt.year
|
48 |
+
df['Month'] = df['Date'].dt.month
|
49 |
+
|
50 |
+
monthly_avg = df.groupby(['Year', 'Month'])['Value'].mean().reset_index()
|
51 |
+
|
52 |
+
fig_monthly_avg = px.line(monthly_avg, x='Month', y='Value', color='Year',
|
53 |
+
title=f'Monthly Average Temperature Time Series for {station_name}',
|
54 |
+
labels={'Month': 'Month', 'Value': 'Average Temperature (°C)', 'Year': 'Year'},
|
55 |
+
line_shape='linear')
|
56 |
+
fig_monthly_avg.update_layout(xaxis_title='Month', yaxis_title='Average Temperature (°C)', xaxis_tickformat='%b')
|
57 |
+
|
58 |
+
return fig_monthly_avg
|
59 |
+
|
60 |
+
def plot_annual_average(df, station_name):
|
61 |
+
annual_avg = df.groupby('Year')['Value'].mean().reset_index()
|
62 |
+
|
63 |
+
fig_annual_avg = px.line(annual_avg, x='Year', y='Value',
|
64 |
+
title=f'Annual Average Temperature Trend for {station_name}',
|
65 |
+
labels={'Year': 'Year', 'Value': 'Average Temperature (°C)'},
|
66 |
+
line_shape='linear')
|
67 |
+
fig_annual_avg.update_layout(xaxis_title='Year', yaxis_title='Average Temperature (°C)')
|
68 |
+
|
69 |
+
return fig_annual_avg
|
70 |
+
|
71 |
+
# Streamlit app layout
|
72 |
+
st.set_page_config(layout="wide", page_title="Temperature Time Series")
|
73 |
+
|
74 |
+
# Load GeoJSON and create mapping
|
75 |
+
geojson = load_geojson()
|
76 |
+
station_map = create_station_map(geojson)
|
77 |
+
|
78 |
+
# Load all CSV files
|
79 |
+
csv_files = load_csv_files()
|
80 |
+
|
81 |
+
# Initialize data storage for all CSV files
|
82 |
+
all_data = []
|
83 |
+
|
84 |
+
# Process each CSV file
|
85 |
+
for file in csv_files:
|
86 |
+
try:
|
87 |
+
file_name = os.path.basename(file)
|
88 |
+
short_form = file_name.split('.')[0] # Get the file name without extension
|
89 |
+
|
90 |
+
df = pd.read_csv(file)
|
91 |
+
|
92 |
+
if df.shape[1] < 2:
|
93 |
+
st.error(f"File {file} does not have the expected number of columns. Skipping.")
|
94 |
+
continue
|
95 |
+
|
96 |
+
if df.columns[0] != 'Date':
|
97 |
+
df.columns = ['Date', 'Value']
|
98 |
+
|
99 |
+
long_form = station_map.get(short_form, "Unknown Station")
|
100 |
+
df['Station'] = long_form
|
101 |
+
all_data.append(df)
|
102 |
+
|
103 |
+
except Exception as e:
|
104 |
+
st.error(f"Error loading or processing file {file}: {e}")
|
105 |
+
|
106 |
+
# Combine all data into a single DataFrame
|
107 |
+
if all_data:
|
108 |
+
combined_df = pd.concat(all_data, ignore_index=True)
|
109 |
+
combined_df['Date'] = pd.to_datetime(combined_df['Date'], format='%Y%m%d', errors='coerce')
|
110 |
+
combined_df = combined_df.dropna(subset=['Date'])
|
111 |
+
combined_df['Year'] = combined_df['Date'].dt.year
|
112 |
+
combined_df['Month'] = combined_df['Date'].dt.month
|
113 |
+
|
114 |
+
stations = combined_df['Station'].unique()
|
115 |
+
default_station = stations[0] if len(stations) > 0 else None
|
116 |
+
|
117 |
+
if not stations.size:
|
118 |
+
st.write("No stations available in the data.")
|
119 |
+
else:
|
120 |
+
st.subheader('Past Daily Average Temperature Time Series')
|
121 |
+
selected_station = st.selectbox("Select a Station", options=stations, index=0)
|
122 |
+
|
123 |
+
station_data = combined_df[combined_df['Station'] == selected_station]
|
124 |
+
|
125 |
+
if not station_data.empty:
|
126 |
+
# Create two columns for plots
|
127 |
+
col1, col2 = st.columns([2,1.5])
|
128 |
+
|
129 |
+
# Top plot: All-year time series
|
130 |
+
with col1:
|
131 |
+
fig_all_years = plot_time_series(station_data, selected_station)
|
132 |
+
st.plotly_chart(fig_all_years, use_container_width=True)
|
133 |
+
|
134 |
+
# Bottom plot: Monthly average temperatures
|
135 |
+
with col2:
|
136 |
+
fig_monthly_avg = plot_monthly_averages(station_data, selected_station)
|
137 |
+
st.plotly_chart(fig_monthly_avg, use_container_width=True)
|
138 |
+
else:
|
139 |
+
st.write(f"No data available for the selected station '{selected_station}'.")
|
140 |
+
else:
|
141 |
+
st.write("No data to display.")
|
pages/6_Heat island.py
ADDED
@@ -0,0 +1,186 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
import folium
|
3 |
+
import json
|
4 |
+
import plotly.express as px
|
5 |
+
import pandas as pd
|
6 |
+
from streamlit_folium import st_folium
|
7 |
+
import plotly.graph_objs as go
|
8 |
+
|
9 |
+
st.set_page_config(layout="wide", page_title="Heat Island Effect Analysis")
|
10 |
+
|
11 |
+
def load_geojson(filepath):
|
12 |
+
with open(filepath, 'r', encoding='utf-8') as f:
|
13 |
+
return json.load(f)
|
14 |
+
|
15 |
+
def plot_geojson(feature_group, geojson_data, property_name, colormap):
|
16 |
+
folium.GeoJson(
|
17 |
+
geojson_data,
|
18 |
+
style_function=lambda feature: {
|
19 |
+
'fillColor': colormap(feature['properties'][property_name]),
|
20 |
+
'color': 'black',
|
21 |
+
'weight': 1,
|
22 |
+
'fillOpacity': 0.7,
|
23 |
+
},
|
24 |
+
popup=folium.GeoJsonPopup(fields=['NAME_EN', property_name], aliases=['District:', 'Value:']),
|
25 |
+
).add_to(feature_group)
|
26 |
+
|
27 |
+
def compute_difference_geojson(geojson_2013, geojson_2023):
|
28 |
+
difference_geojson = {"type": "FeatureCollection", "features": []}
|
29 |
+
|
30 |
+
name_to_hot_nights_2013 = {
|
31 |
+
feature['properties']['NAME_EN']: feature['properties']['Hot_Nights']
|
32 |
+
for feature in geojson_2013['features']
|
33 |
+
}
|
34 |
+
|
35 |
+
for feature in geojson_2023['features']:
|
36 |
+
name_en = feature['properties']['NAME_EN']
|
37 |
+
hot_nights_2013 = name_to_hot_nights_2013.get(name_en, 0)
|
38 |
+
hot_nights_2023 = feature['properties']['Hot_Nights']
|
39 |
+
difference = hot_nights_2023 - hot_nights_2013
|
40 |
+
|
41 |
+
feature['properties']['Difference'] = difference
|
42 |
+
difference_geojson['features'].append(feature)
|
43 |
+
|
44 |
+
return difference_geojson
|
45 |
+
|
46 |
+
def geojson_to_dataframe(geojson_data, year):
|
47 |
+
features = geojson_data['features']
|
48 |
+
data = {
|
49 |
+
'District': [feature['properties']['NAME_EN'] for feature in features],
|
50 |
+
'Hot_Nights': [feature['properties']['Hot_Nights'] for feature in features],
|
51 |
+
'Year': [year] * len(features) # Add year column
|
52 |
+
}
|
53 |
+
return pd.DataFrame(data)
|
54 |
+
|
55 |
+
geojson_2013 = load_geojson('ref/2013_hot.geojson')
|
56 |
+
geojson_2023 = load_geojson('ref/2023_hot.geojson')
|
57 |
+
|
58 |
+
hot_nights_2013 = [feature['properties']['Hot_Nights'] for feature in geojson_2013['features']]
|
59 |
+
hot_nights_2023 = [feature['properties']['Hot_Nights'] for feature in geojson_2023['features']]
|
60 |
+
all_hot_nights = hot_nights_2013 + hot_nights_2023
|
61 |
+
|
62 |
+
colormap = folium.LinearColormap(
|
63 |
+
colors=['white', 'orange', 'red'],
|
64 |
+
vmin=min(all_hot_nights),
|
65 |
+
vmax=max(all_hot_nights),
|
66 |
+
caption='Hot Nights'
|
67 |
+
)
|
68 |
+
|
69 |
+
difference_geojson = compute_difference_geojson(geojson_2013, geojson_2023)
|
70 |
+
|
71 |
+
diff_colormap = folium.LinearColormap(
|
72 |
+
colors=['blue', 'lightblue', 'white', 'pink', 'red'],
|
73 |
+
index=[-50, -10, 0, 10, 50],
|
74 |
+
vmin=-50,
|
75 |
+
vmax=50,
|
76 |
+
caption='Change in Hot Nights'
|
77 |
+
)
|
78 |
+
|
79 |
+
m = folium.Map(location=[22.35994791346238, 114.15924623933743], zoom_start=11, tiles='https://landsd.azure-api.net/dev/osm/xyz/basemap/gs/WGS84/tile/{z}/{x}/{y}.png?key=f4d3e21d4fc14954a1d5930d4dde3809',attr="Map infortmation from Lands Department")
|
80 |
+
folium.TileLayer(
|
81 |
+
tiles='https://mapapi.geodata.gov.hk/gs/api/v1.0.0/xyz/label/hk/en/wgs84/{z}/{x}/{y}.png',
|
82 |
+
attr="Map infortmation from Lands Department").add_to(m)
|
83 |
+
|
84 |
+
feature_group_2013 = folium.FeatureGroup(name='2013 Hot Nights', show=False)
|
85 |
+
feature_group_2023 = folium.FeatureGroup(name='2023 Hot Nights', show=False)
|
86 |
+
feature_group_diff = folium.FeatureGroup(name='Change in Hot Nights', show=True)
|
87 |
+
|
88 |
+
plot_geojson(feature_group_2013, geojson_2013, 'Hot_Nights', colormap)
|
89 |
+
plot_geojson(feature_group_2023, geojson_2023, 'Hot_Nights', colormap)
|
90 |
+
plot_geojson(feature_group_diff, difference_geojson, 'Difference', diff_colormap)
|
91 |
+
|
92 |
+
feature_group_2013.add_to(m)
|
93 |
+
feature_group_2023.add_to(m)
|
94 |
+
feature_group_diff.add_to(m)
|
95 |
+
|
96 |
+
layer_control = folium.LayerControl().add_to(m)
|
97 |
+
|
98 |
+
colormap.add_to(m)
|
99 |
+
diff_colormap.add_to(m)
|
100 |
+
|
101 |
+
df_2013 = geojson_to_dataframe(geojson_2013, '2013')
|
102 |
+
df_2023 = geojson_to_dataframe(geojson_2023, '2023')
|
103 |
+
|
104 |
+
combined_df = pd.concat([df_2013, df_2023])
|
105 |
+
|
106 |
+
def plot_combined_box_plot(df):
|
107 |
+
fig = px.box(
|
108 |
+
df,
|
109 |
+
x='Year',
|
110 |
+
y='Hot_Nights',
|
111 |
+
title='Hot Nights (2013 vs 2023)',
|
112 |
+
labels={'Hot_Nights': 'Number of Hot Nights', 'Year': 'Year'},
|
113 |
+
color='Year'
|
114 |
+
)
|
115 |
+
fig.update_layout(
|
116 |
+
yaxis_title='Number of Hot Nights',
|
117 |
+
boxmode='group'
|
118 |
+
)
|
119 |
+
return fig
|
120 |
+
|
121 |
+
data_table = pd.read_csv('ref/final_summary_with_available_stations.csv')
|
122 |
+
|
123 |
+
stations = data_table['station_name'].unique()
|
124 |
+
|
125 |
+
col1, col2, col3 = st.columns([1.35, 2, 1.1])
|
126 |
+
|
127 |
+
with col1:
|
128 |
+
st.subheader('Heat Island Effect')
|
129 |
+
st.caption(
|
130 |
+
'The "heat island effect" refers to the temperature difference between urban and rural areas, particularly at night.')
|
131 |
+
st.caption(
|
132 |
+
'This phenomenon is a result of the urbanization and development processes. During the day, the urban environment (such as cement pavement) absorbs and stores more heat from solar insolation compared to rural areas (vegetation). This heat is then slowly released in the evening and nighttime, leading to higher temperatures in the urban areas.')
|
133 |
+
|
134 |
+
selected_station = st.selectbox('Select a Station:', options=stations)
|
135 |
+
|
136 |
+
filtered_data_table = data_table[data_table['station_name'] == selected_station]
|
137 |
+
|
138 |
+
fig = go.Figure()
|
139 |
+
|
140 |
+
fig.add_trace(go.Scatter(
|
141 |
+
x=filtered_data_table['month'],
|
142 |
+
y=filtered_data_table['13day_temp'],
|
143 |
+
mode='lines+markers',
|
144 |
+
name='2013 Day Temp',
|
145 |
+
line=dict(color='blue')
|
146 |
+
))
|
147 |
+
fig.add_trace(go.Scatter(
|
148 |
+
x=filtered_data_table['month'],
|
149 |
+
y=filtered_data_table['13night_temp'],
|
150 |
+
mode='lines+markers',
|
151 |
+
name='2013 Night Temp',
|
152 |
+
line=dict(color='blue', dash='dash')
|
153 |
+
))
|
154 |
+
fig.add_trace(go.Scatter(
|
155 |
+
x=filtered_data_table['month'],
|
156 |
+
y=filtered_data_table['23day_temp'],
|
157 |
+
mode='lines+markers',
|
158 |
+
name='2023 Day Temp',
|
159 |
+
line=dict(color='red')
|
160 |
+
))
|
161 |
+
fig.add_trace(go.Scatter(
|
162 |
+
x=filtered_data_table['month'],
|
163 |
+
y=filtered_data_table['23night_temp'],
|
164 |
+
mode='lines+markers',
|
165 |
+
name='2023 Night Temp',
|
166 |
+
line=dict(color='red', dash='dash')
|
167 |
+
))
|
168 |
+
|
169 |
+
fig.update_layout(
|
170 |
+
title=f'Temperature Comparison',
|
171 |
+
xaxis_title='Month',
|
172 |
+
yaxis_title='Temperature (°C)',
|
173 |
+
legend_title='Legend',
|
174 |
+
height =300
|
175 |
+
)
|
176 |
+
|
177 |
+
st.plotly_chart(fig, height=180)
|
178 |
+
|
179 |
+
with col2:
|
180 |
+
st_folium(m, width=550, height=650)
|
181 |
+
|
182 |
+
with col3:
|
183 |
+
st.caption(
|
184 |
+
'From data from the CO-WIN network, there has been a significant increase in the number of hot nights in Hong Kong. "Hot nights" refers to nights where the temperature remains above 28 degrees. Within the period from 2013 to 2023, 9 districts in Hong Kong have experienced an increase in the frequency of hot nights, the most significant are those in the urban.')
|
185 |
+
|
186 |
+
st.plotly_chart(plot_combined_box_plot(combined_df), use_container_width=True ,height=380)
|
pages/7_CoWIN.py
ADDED
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
import requests
|
3 |
+
import json
|
4 |
+
import pandas as pd
|
5 |
+
import folium
|
6 |
+
from streamlit_folium import st_folium
|
7 |
+
import plotly.graph_objects as go
|
8 |
+
import numpy as np
|
9 |
+
from datetime import datetime
|
10 |
+
from branca.colormap import LinearColormap
|
11 |
+
import pytz
|
12 |
+
|
13 |
+
st.set_page_config(layout="wide", page_title="Real-Time CoWIN Weather Data Dashboard")
|
14 |
+
|
15 |
+
@st.cache_data(ttl=300) # Cache data for 5 minutes (300 seconds)
|
16 |
+
def fetch_data():
|
17 |
+
hk_tz = pytz.timezone('Asia/Hong_Kong')
|
18 |
+
current_time = datetime.now(hk_tz).strftime('%Y-%m-%dT%H:%M:%S')
|
19 |
+
url = f'https://cowin.hku.hk/API/data/CoWIN/map?time={current_time}'
|
20 |
+
response = requests.get(url)
|
21 |
+
return json.loads(response.text), current_time
|
22 |
+
|
23 |
+
data, fetched_time = fetch_data()
|
24 |
+
|
25 |
+
features = data
|
26 |
+
df = pd.json_normalize(features)
|
27 |
+
|
28 |
+
df.rename(columns={
|
29 |
+
'station': 'Station',
|
30 |
+
'temp': 'Temperature',
|
31 |
+
'lat': 'Latitude',
|
32 |
+
'lon': 'Longitude',
|
33 |
+
'wd': 'Wind Direction',
|
34 |
+
'ws': 'Wind Speed',
|
35 |
+
'rh': 'Relative Humidity',
|
36 |
+
'uv': 'UV Radiation',
|
37 |
+
'me_name': 'Name'
|
38 |
+
}, inplace=True)
|
39 |
+
|
40 |
+
attribute = st.selectbox(
|
41 |
+
'Select Weather Attributes to Plot and Map (Data from HKO-HKU CoWIN)',
|
42 |
+
['Temperature', 'Wind Speed', 'Relative Humidity', 'UV Radiation']
|
43 |
+
)
|
44 |
+
|
45 |
+
col1, col2, col3 = st.columns([1.65, 2, 1.2])
|
46 |
+
|
47 |
+
with col1:
|
48 |
+
attr_series = pd.Series(df[attribute].dropna())
|
49 |
+
|
50 |
+
hist_data = np.histogram(attr_series, bins=10)
|
51 |
+
bin_edges = hist_data[1]
|
52 |
+
counts = hist_data[0]
|
53 |
+
|
54 |
+
def get_color(value, min_value, max_value):
|
55 |
+
ratio = (value - min_value) / (max_value - min_value)
|
56 |
+
r = int(255 * ratio)
|
57 |
+
b = int(255 * (1 - ratio))
|
58 |
+
return f'rgb({r}, 0, {b})'
|
59 |
+
|
60 |
+
fig = go.Figure()
|
61 |
+
|
62 |
+
for i in range(len(bin_edges) - 1):
|
63 |
+
bin_center = (bin_edges[i] + bin_edges[i + 1]) / 2
|
64 |
+
color = get_color(bin_center, bin_edges.min(), bin_edges.max())
|
65 |
+
fig.add_trace(go.Bar(
|
66 |
+
x=[f'{bin_edges[i]:.1f} - {bin_edges[i + 1]:.1f}'],
|
67 |
+
y=[counts[i]],
|
68 |
+
marker_color=color,
|
69 |
+
name=f'{bin_edges[i]:.1f} - {bin_edges[i + 1]:.1f}'
|
70 |
+
))
|
71 |
+
|
72 |
+
fig.update_layout(
|
73 |
+
xaxis_title=f'{attribute}',
|
74 |
+
yaxis_title='Count',
|
75 |
+
title=f'{attribute} Distribution',
|
76 |
+
bargap=0.2,
|
77 |
+
title_font_size=20,
|
78 |
+
xaxis_title_font_size=14,
|
79 |
+
yaxis_title_font_size=14,
|
80 |
+
height=350,
|
81 |
+
xaxis=dict(title_font_size=14),
|
82 |
+
yaxis=dict(title_font_size=14)
|
83 |
+
)
|
84 |
+
|
85 |
+
st.plotly_chart(fig, use_container_width=True)
|
86 |
+
st.caption(f"Data fetched at: {fetched_time}")
|
87 |
+
|
88 |
+
with st.container():
|
89 |
+
col_1, col_2 = st.columns([1, 1])
|
90 |
+
with col_1:
|
91 |
+
if attr_series.size > 0:
|
92 |
+
avg_attr = np.mean(attr_series)
|
93 |
+
std_attr = np.std(attr_series)
|
94 |
+
max_attr = np.max(attr_series)
|
95 |
+
min_attr = np.min(attr_series)
|
96 |
+
|
97 |
+
st.metric(label=f"Average {attribute}", value=f"{avg_attr:.2f}")
|
98 |
+
st.metric(label=f"Minimum {attribute}", value=f"{min_attr:.2f}")
|
99 |
+
with col_2:
|
100 |
+
st.metric(label=f"Maximum {attribute}", value=f"{max_attr:.2f}")
|
101 |
+
st.metric(label=f"Std. Dev {attribute}", value=f"{std_attr:.2f}")
|
102 |
+
|
103 |
+
def attribute_to_color(value, min_value, max_value):
|
104 |
+
"""Convert a value to a color based on the gradient."""
|
105 |
+
ratio = (value - min_value) / (max_value - min_value)
|
106 |
+
return LinearColormap(['blue', 'purple', 'red']).rgb_hex_str(ratio)
|
107 |
+
|
108 |
+
with col2:
|
109 |
+
m = folium.Map(location=[22.3547, 114.1483], zoom_start=11, tiles='https://landsd.azure-api.net/dev/osm/xyz/basemap/gs/WGS84/tile/{z}/{x}/{y}.png?key=f4d3e21d4fc14954a1d5930d4dde3809',attr="Map infortmation from Lands Department")
|
110 |
+
folium.TileLayer(
|
111 |
+
tiles='https://mapapi.geodata.gov.hk/gs/api/v1.0.0/xyz/label/hk/en/wgs84/{z}/{x}/{y}.png',
|
112 |
+
attr="Map infortmation from Lands Department").add_to(m)
|
113 |
+
|
114 |
+
min_value = df[attribute].min()
|
115 |
+
max_value = df[attribute].max()
|
116 |
+
|
117 |
+
for _, row in df.iterrows():
|
118 |
+
lat = row['Latitude']
|
119 |
+
lon = row['Longitude']
|
120 |
+
station = row['Station']
|
121 |
+
name = row['Name']
|
122 |
+
value = row[attribute]
|
123 |
+
|
124 |
+
color = attribute_to_color(value, min_value, max_value) if pd.notna(value) else 'gray'
|
125 |
+
|
126 |
+
folium.Marker(
|
127 |
+
location=[lat, lon],
|
128 |
+
popup=(
|
129 |
+
f"<p style='font-size: 12px; background-color: white; padding: 5px; border-radius: 5px;'>"
|
130 |
+
f"Station: {station}<br>"
|
131 |
+
f"Name: {name}<br>"
|
132 |
+
f"{attribute}: {value}<br>"
|
133 |
+
f"</p>"
|
134 |
+
),
|
135 |
+
icon=folium.DivIcon(
|
136 |
+
html=f'<div style="font-size: 10pt; color: {color}; padding: 2px; border-radius: 5px;">'
|
137 |
+
f'<strong>{value}</strong></div>'
|
138 |
+
)
|
139 |
+
).add_to(m)
|
140 |
+
|
141 |
+
# Create a color scale legend
|
142 |
+
colormap = folium.LinearColormap(
|
143 |
+
colors=['blue', 'purple', 'red'],
|
144 |
+
index=[min_value, (min_value + max_value) / 2, max_value],
|
145 |
+
vmin=min_value,
|
146 |
+
vmax=max_value,
|
147 |
+
caption=f'{attribute}'
|
148 |
+
)
|
149 |
+
colormap.add_to(m)
|
150 |
+
|
151 |
+
st_folium(m, width=530, height=600)
|
152 |
+
|
153 |
+
with col3:
|
154 |
+
st.markdown(
|
155 |
+
"""
|
156 |
+
<style>
|
157 |
+
.dataframe-container {
|
158 |
+
height: 600px;
|
159 |
+
overflow-y: auto;
|
160 |
+
}
|
161 |
+
</style>
|
162 |
+
""",
|
163 |
+
unsafe_allow_html=True
|
164 |
+
)
|
165 |
+
|
166 |
+
st.dataframe(df[['Station', 'Name', 'Temperature', 'Wind Speed', 'Relative Humidity', 'UV Radiation', 'Latitude', 'Longitude']], height=600)
|
167 |
+
|
168 |
+
if st.button("Refresh Data"):
|
169 |
+
st.experimental_rerun()
|
170 |
+
|
171 |
+
hk_tz = pytz.timezone('Asia/Hong_Kong')
|
172 |
+
current_time = datetime.now(hk_tz)
|
173 |
+
if 'last_ran' not in st.session_state or (current_time - st.session_state.last_ran.replace(tzinfo=hk_tz)).total_seconds() > 300:
|
174 |
+
st.session_state.last_ran = current_time
|
175 |
+
st.experimental_rerun()
|