OttoYu commited on
Commit
90699dd
1 Parent(s): 0131e4f

Update LandsD basemap

Browse files
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()