Skip to content

Commit 47fe960

Browse files
authored
🎨 Update UI and Folium Map (#96)
* 🎨 Update UI and Folium Map Refactored code for folium by adding custom icons and heatmap for visualizing allocation distribution. Also, replaced the dividers with inbuilt dividers of subheader and minor changes in displaying dataframe * 🧹 Remove cluttered code * 🎨 Added bubble chart within folium Replaced heatmap with circle mapper to display the proportion of the allocated students by using the size of the bubble
1 parent 1dcb368 commit 47fe960

File tree

2 files changed

+125
-97
lines changed

2 files changed

+125
-97
lines changed

app.py

Lines changed: 91 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
import pandas as pd
66
import streamlit as st
77
from streamlit_folium import st_folium
8-
from jinja2 import Template
8+
from utils.pretty import pretty_dataframe, custom_map_zoom, custom_map_tooltip
99

10-
# #Page Setup
10+
11+
#Page Setup
1112
st.set_page_config(
1213
page_title="MOEST Exam Center Calculator",
1314
page_icon=":school:",
@@ -24,25 +25,7 @@
2425
}
2526
</style>
2627
"""
27-
legend_template = """
28-
{% macro html(this, kwargs) %}
29-
<div id='maplegend' class='maplegend'
30-
style='position: absolute; z-index: 9999; background-color: rgba(255, 255, 255, 0.65);
31-
border-radius: 6px; padding: 10px; font-size: 10.5px; right: 15px; top: 15px; border: 2px solid black;'>
32-
<div class='legend-scale'>
33-
<ul class='legend-labels'>
34-
<li style='font-size:18px;margin-bottom:5px;'><span style='background: #0096FF; opacity: 0.75;'></span>School</li>
35-
<li style='font-size:18px;'><span style='background: #C41E3A; opacity: 1.75;'></span>Center</li>
36-
</ul>
37-
</div>
38-
</div>
39-
<style type='text/css'>
40-
.maplegend .legend-scale ul {margin: 0; padding: 0; color: #0f0f0f;}
41-
.maplegend .legend-scale ul li {list-style: none; line-height: 18px; margin-bottom: 1.5px;}
42-
.maplegend ul.legend-labels li span {float: left; height: 16px; width: 16px; margin-right: 4.5px;}
43-
</style>
44-
{% endmacro %}
45-
"""
28+
4629
# Render custom CSS
4730
st.markdown(custom_css, unsafe_allow_html=True)
4831

@@ -58,61 +41,51 @@
5841
if 'filter_value' not in st.session_state:
5942
st.session_state.filter_value = None
6043

61-
#Maps setup
62-
m = folium.Map(location=[27.7007, 85.3001], zoom_start=12, )
63-
64-
# Add Legend in map
65-
macro = folium.MacroElement()
66-
macro._template = Template(legend_template)
67-
m.get_root().add_child(macro)
68-
69-
fg = folium.FeatureGroup(name="Allocated Centers")
70-
7144
#Sidebar
7245
with st.sidebar:
73-
7446
add_side_header = st.sidebar.title("Random Center Calculator")
75-
47+
7648
schools_file = st.sidebar.file_uploader("Upload School/College file", type="tsv")
7749
centers_file = st.sidebar.file_uploader("Upload Centers file", type="tsv")
7850
prefs_file = st.sidebar.file_uploader("Upload Preferences file", type="tsv")
7951

8052
calculate = st.sidebar.button("Calculate Centers", type="primary", use_container_width=True)
8153

8254
school_df = None
55+
divider_color = "red"
8356
# Tabs
8457
tab1, tab2, tab3, tab4, tab5 = st.tabs([
85-
"School Center",
86-
"School Center Distance",
87-
"View School Data",
88-
"View Centers Data",
89-
"View Pref Data"
58+
"📍 School Center",
59+
"🚌 School Center Distance",
60+
"🏫 View School Data",
61+
"📍 View Centers Data",
62+
"🧮 View Pref Data"
9063
])
9164

92-
tab1.subheader("School Center")
93-
tab2.subheader("School Center Distance")
94-
tab3.subheader("School Data")
95-
tab4.subheader("Center Data")
96-
tab5.subheader("Pref Data")
65+
tab1.subheader("School Center", divider=divider_color)
66+
tab2.subheader("School Center Distance", divider=divider_color)
67+
tab3.subheader("School Data", divider=divider_color)
68+
tab4.subheader("Center Data", divider=divider_color)
69+
tab5.subheader("Pref Data", divider=divider_color)
9770

9871
# Show data in Tabs as soon as the files are uploaded
9972
if schools_file:
10073
df = pd.read_csv(schools_file, sep="\t")
10174
school_df = df
102-
tab3.dataframe(df)
75+
tab3.dataframe(pretty_dataframe(df), use_container_width=True)
10376

10477
else:
10578
tab3.info("Upload data to view it.", icon="ℹ️")
10679

10780
if centers_file:
10881
df = pd.read_csv(centers_file, sep="\t")
109-
tab4.dataframe(df)
82+
tab4.dataframe(pretty_dataframe(df), use_container_width=True)
11083
else:
11184
tab4.info("Upload data to view it.", icon="ℹ️")
11285

11386
if prefs_file:
11487
df = pd.read_csv(prefs_file, sep="\t")
115-
tab5.dataframe(df)
88+
tab5.dataframe(pretty_dataframe(df), use_container_width=True)
11689
else:
11790
tab5.info("Upload data to view it.", icon="ℹ️")
11891

@@ -181,7 +154,7 @@ def save_file_to_temp(file_obj):
181154
if 'school_center' in st.session_state.calculated_data:
182155
df_school_center = pd.read_csv(st.session_state.calculated_data['school_center'], sep="\t")
183156
allowed_filter_types = ['school', 'center']
184-
st.session_state.filter_type = tab1.radio("Choose a filter type:", allowed_filter_types)
157+
st.session_state.filter_type = tab1.radio("Choose a filter type:", allowed_filter_types, horizontal=True)
185158

186159
# Display an input field based on the selected filter type
187160
if st.session_state.filter_type:
@@ -194,73 +167,94 @@ def save_file_to_temp(file_obj):
194167
filter_options = [f"{code} | {name}" for name, code in zip(df_school_center['center'].unique(), df_school_center['cscode'].unique())]
195168

196169
# Display a selectbox for selection
197-
st.session_state.filter_value = tab1.selectbox(f"Select a value for {st.session_state.filter_type}:", filter_options)
170+
st.session_state.filter_value = tab1.selectbox(f"Select a value for {st.session_state.filter_type.capitalize()}:", filter_options)
198171

199172
# Split the selected value to extract name and code
200173
code, name = st.session_state.filter_value.split(' | ')
201174

202175
# Filter the DataFrame based on the selected type and value
203176
filtered_df = filter_data(df_school_center, st.session_state.filter_type, name)
204177

205-
if st.session_state.filter_value:
178+
with tab1:
179+
if st.session_state.filter_value:
206180
# Remove thousand separator comma in scode and cscode
207-
styled_df = filtered_df.style.format({
181+
styled_df = pretty_dataframe(filtered_df).style.format({
208182
"cscode": lambda x: '{:.0f}'.format(x),
209-
"scode": lambda x: '{:.0f}'.format(x)
183+
"scode": lambda x: '{:.0f}'.format(x)
210184
})
211-
tab1.dataframe(styled_df , hide_index=True)
212-
tab1.subheader('Map')
213-
tab1.divider()
214-
for index, center in filtered_df.iterrows():
215-
fg.add_child(
216-
folium.Marker(
217-
location=[center.center_lat, center.center_long],
218-
popup=f"{(center.center).title()}\nAllocation: {center.allocation}",
219-
tooltip=f"{center.center}",
220-
icon=folium.Icon(color="red")
221-
)
222-
)
223-
224-
# Initialize an empty dictionary to store school coordinates
225-
filtered_schools = {}
226-
185+
st.dataframe(styled_df , hide_index=True, use_container_width=True)
186+
st.markdown("<br/><br/>", unsafe_allow_html=True)
187+
st.subheader('Map', divider=divider_color)
188+
189+
# Initialize data for map
190+
map_data = filtered_df[['center_lat', 'center_long', 'center', 'allocation']].copy()
191+
map_data.columns = ['lat', 'long', 'name', 'allocation']
192+
map_data['type'] = 'Center'
193+
227194
if school_df is not None:
228-
229-
for index, row in school_df.iterrows():
230-
scode = row['scode']
231-
school_lat = row['lat']
232-
school_long = row['long']
233-
234-
if scode in filtered_df['scode'].values:
235-
filtered_schools.setdefault(scode, []).append((school_lat, school_long))
236-
237-
for index, school in filtered_df.iterrows():
238-
lat_long_list = filtered_schools.get(school['scode'], [])
239-
240-
for school_lat, school_long in lat_long_list:
241-
if school_lat is not None and school_long is not None:
242-
fg.add_child(
243-
folium.Marker(
244-
location=[school_lat, school_long],
245-
popup=f"{school['school'].title()}\nAllocation: {school['allocation']}",
246-
tooltip=f"{school['school']}",
247-
icon=folium.Icon(color="blue")
248-
)
249-
)
250-
195+
filter_school = school_df[school_df['scode'].isin(filtered_df['scode']) & school_df['lat'].notnull() & school_df['long'].notnull()]
196+
school_map_data = filter_school[['lat', 'long', 'name-address']].copy().rename(columns={'name-address': 'name'})
197+
school_map_data['type'] = 'School'
198+
map_data = pd.concat([map_data, school_map_data], ignore_index=True)
199+
200+
map_data.drop_duplicates(inplace=True)
201+
202+
try:
203+
if st.session_state.map_type:
204+
st.session_state.map_type = st.radio("Choose a map type:", ["cartodbpositron", "openstreetmap"], horizontal=True)
205+
except:
206+
st.session_state.map_type = "cartodbpositron"
207+
208+
show_heatmap = st.checkbox("View allocation distribution", value=False)
209+
210+
# Maps setup
211+
m = folium.Map(
212+
location=[map_data['lat'].mean(), map_data['long'].mean()], # Center map on the mean of the lat and long
213+
zoom_start=custom_map_zoom(map_data['lat'].values, map_data['long'].values),
214+
tiles=st.session_state.map_type
215+
)
216+
217+
fg = folium.FeatureGroup(name="Allocated Centers")
218+
for _, row in map_data.iterrows():
219+
fg.add_child(folium.Marker(
220+
location=[row['lat'], row['long']],
221+
tooltip=custom_map_tooltip(row),
222+
popup=custom_map_tooltip(row),
223+
icon= folium.CustomIcon(
224+
"https://cdn-icons-png.flaticon.com/256/4996/4996117.png" if row['type'] == "School" else "https://cdn-icons-png.flaticon.com/256/15092/15092199.png",
225+
icon_size=(38, 40),
226+
icon_anchor=(21, 38),
227+
shadow_image="https://static.vecteezy.com/system/resources/thumbnails/013/169/090/small_2x/oval-shadow-for-object-or-product-png.png",
228+
shadow_size=(28, 30) if row['type'] == "School" else (22, 24),
229+
shadow_anchor=(8, 19) if row['type'] == "School" else (8, 13),
230+
)
231+
))
251232
m.add_child(fg)
252-
with tab1:
253-
st_folium( m, width=1200, height=400)
254-
255-
tab1.divider()
256-
tab1.subheader('All Data')
257-
tab1.dataframe(df_school_center)
233+
234+
if show_heatmap:
235+
max_allocation = map_data['allocation'].max()
236+
for _, row in map_data[map_data.allocation > 0].iterrows():
237+
folium.CircleMarker(
238+
location=[row['lat'], row['long']],
239+
radius=row['allocation'] / max_allocation * 25,
240+
color="#3c844a",
241+
opacity=0.35,
242+
fill=True,
243+
fill_color="green",
244+
fill_opacity=0.3
245+
).add_to(m)
246+
247+
st_folium( m, width=1200, height=400)
248+
249+
st.markdown("<br/><br/>", unsafe_allow_html=True)
250+
st.subheader('All Data', divider=divider_color)
251+
st.dataframe(pretty_dataframe(df_school_center), use_container_width=True)
258252
else:
259253
tab1.info("No calculated data available.", icon="ℹ️")
260254

261255
if 'school_center_distance' in st.session_state.calculated_data:
262256
df = pd.read_csv(st.session_state.calculated_data['school_center_distance'], sep="\t")
263-
tab2.dataframe(df)
257+
tab2.dataframe(pretty_dataframe(df), use_container_width=True)
264258

265259
else:
266260
tab2.error("School Center Distance file not found.")

utils/pretty.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
def pretty_dataframe(df):
2+
df = df.copy()
3+
df.columns = df.columns.str.replace("_", " ").str.title()
4+
5+
return df
6+
7+
def custom_map_zoom(lat, long):
8+
min_lat, max_lat = lat.min(), lat.max()
9+
min_long, max_long = long.min(), long.max()
10+
11+
lat_diff = max_lat - min_lat
12+
long_diff = max_long - min_long
13+
14+
# Base zoom levels for approximately 0.01 degree difference
15+
base_zoom_lat = 14
16+
base_zoom_long = 14
17+
18+
# Adjust zoom level based on the span
19+
zoom_lat = base_zoom_lat - int((lat_diff // 0.05))
20+
zoom_long = base_zoom_long - int((long_diff // 0.05))
21+
22+
# Return the smaller of the two zooms (more zoomed out)
23+
return min(zoom_lat, zoom_long)
24+
25+
def custom_map_tooltip(row):
26+
tooltip = f"""
27+
<h6>{row['name'].title()}</h6>
28+
<strong>""" + ("📍 Center" if row['type'] == "Center" else "🏫 School") + f"""</strong><br/>
29+
<b>Latitude:</b> {row['lat']}<br/>
30+
<b>Longitude:</b> {row['long']}<br/> """ + (
31+
f"""<b>Allocation:</b> {int(row['allocation'])}<br/>""" if row["allocation"] > 0 else ""
32+
)
33+
34+
return tooltip

0 commit comments

Comments
 (0)