atodorov284 commited on
Commit
15d578c
·
1 Parent(s): 5227116

refactor user view and controller to adhere to MVC

Browse files
streamlit_src/controllers/user_controller.py CHANGED
@@ -1,56 +1,353 @@
 
1
  import streamlit as st
2
  import numpy as np
3
  from models.air_quality_model import AirQualityModel
4
  from views.user_view import UserView
 
 
 
 
 
 
5
 
6
 
7
  class UserController:
8
- def __init__(self):
 
 
 
 
 
 
 
9
  self.model = AirQualityModel()
10
  self.view = UserView()
 
 
 
 
 
 
 
11
 
12
- # Ensure session state for first run and quiz question
13
  if "is_first_run" not in st.session_state:
14
  st.session_state.is_first_run = True
15
-
16
  if "question_choice" not in st.session_state:
17
  st.session_state.question_choice = np.random.randint(0, 5)
18
 
19
- def show_dashboard(self):
20
- # Get today's data and predictions
21
- today_data = self.model.get_today_data()
22
- next_three_days = self.model.next_three_day_predictions()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
- # WHO Guidelines
25
- who_guidelines = {
 
 
 
 
 
 
 
 
26
  "Pollutant": ["NO2 (µg/m³)", "O3 (µg/m³)"],
27
- "WHO Guideline": [self.model.WHO_NO2_LEVEL, self.model.WHO_O3_LEVEL],
 
 
 
 
28
  }
 
 
 
 
 
 
 
 
 
 
29
 
30
- # Display current data and predictions
31
- self.view.show_current_data(today_data, who_guidelines)
 
 
 
32
 
33
- # Use session state to avoid resetting the quiz question after each interaction
34
- if st.session_state.is_first_run:
35
- st.session_state.question_choice = np.random.randint(0, 5)
36
- st.session_state.is_first_run = False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
 
38
- # Raise awareness and display the quiz with the saved question number
39
- self.view.raise_awareness_and_quiz(
40
- today_data, who_guidelines, question_nr=st.session_state.question_choice
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  )
42
 
43
- # Plot selection
44
- plot_type = self.view.view_option_selection()
45
- if plot_type == "Line Plot":
46
- self.view.display_predictions_lineplot(next_three_days, who_guidelines)
47
- elif plot_type == "Gauge Plot":
48
- self.view.display_predictions_gaugeplot(next_three_days, who_guidelines)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
 
50
- # Compare to WHO guidelines
51
- self.view.compare_to_who(
52
- today_data, self.model.WHO_NO2_LEVEL, self.model.WHO_O3_LEVEL
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
 
55
- # Print sources
56
- self.view.print_sources()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Callable, Dict, List, Tuple
2
  import streamlit as st
3
  import numpy as np
4
  from models.air_quality_model import AirQualityModel
5
  from views.user_view import UserView
6
+ import os
7
+ import pandas as pd
8
+ import random
9
+ import json
10
+ from datetime import date, timedelta
11
+ import plotly.graph_objects as go
12
 
13
 
14
  class UserController:
15
+ """
16
+ A class to handle the user interface.
17
+ """
18
+
19
+ def __init__(self) -> None:
20
+ """
21
+ Initializes the UserController class.
22
+ """
23
  self.model = AirQualityModel()
24
  self.view = UserView()
25
+ self.today_data = self.model.get_today_data()
26
+ self.next_three_days = self.model.next_three_day_predictions()
27
+
28
+ self.who_guidelines = {
29
+ "Pollutant": ["NO2 (µg/m³)", "O3 (µg/m³)"],
30
+ "WHO Guideline": [self.model.WHO_NO2_LEVEL, self.model.WHO_O3_LEVEL],
31
+ }
32
 
33
+ # Ensure session state for quiz and quiz answer tracking
34
  if "is_first_run" not in st.session_state:
35
  st.session_state.is_first_run = True
 
36
  if "question_choice" not in st.session_state:
37
  st.session_state.question_choice = np.random.randint(0, 5)
38
 
39
+ # Paths for external data
40
+ self.interactions_path = os.path.join(
41
+ os.path.dirname(os.path.dirname(__file__)), "json_interactions/"
42
+ )
43
+ self.facts_path = os.path.join(self.interactions_path, "facts.json")
44
+ self.questions_path = os.path.join(self.interactions_path, "question.json")
45
+ self.awareness_path = os.path.join(self.interactions_path, "awareness.json")
46
+
47
+ def show_dashboard(self) -> None:
48
+ """
49
+ Shows the main page of the user interface.
50
+ """
51
+ self.show_current_data()
52
+
53
+ self.two_columns_layout(0.7, self.raise_awareness, self.quiz)
54
+
55
+ # Plot selection and rendering
56
+ plot_type = self.view.view_option_selection(["Line Plot", "Gauge Plot"])
57
+ if plot_type == "Line Plot":
58
+ line_fig = self.prepare_line_plot()
59
+ self.view.display_predictions_lineplot(line_fig)
60
+ elif plot_type == "Gauge Plot":
61
+ gauge_plots = self.prepare_gauge_plots()
62
+ self.view.display_predictions_gaugeplot(gauge_plots)
63
+
64
+ # WHO comparison
65
+ who_comparisons = self.compare_to_who()
66
+ self.view.compare_to_who(who_comparisons)
67
+
68
+ # Sources
69
+ sources = [
70
+ (
71
+ "WHO Air Quality Guidelines",
72
+ "https://www.who.int/news-room/fact-sheets/detail/ambient-(outdoor)-air-quality-and-health",
73
+ ),
74
+ (
75
+ "Air Pollution Facts",
76
+ "https://www.un.org/sustainabledevelopment/air-pollution/",
77
+ ),
78
+ ]
79
+ self.view.print_sources(sources)
80
+
81
+ def show_current_data(self) -> None:
82
+ """
83
+ Shows the current data on the main page of the user interface.
84
+ """
85
+ merged_data_df = self.prepare_data_for_view()
86
+ self.view.show_current_data(merged_data_df)
87
 
88
+ def prepare_data_for_view(self) -> pd.DataFrame:
89
+ """
90
+ Prepares the current data for the view.
91
+
92
+ Returns
93
+ -------
94
+ pd.DataFrame
95
+ The current data in a pandas DataFrame.
96
+ """
97
+ merged_data = {
98
  "Pollutant": ["NO2 (µg/m³)", "O3 (µg/m³)"],
99
+ "Current Concentration": [
100
+ self.today_data["NO2 (µg/m³)"],
101
+ self.today_data["O3 (µg/m³)"],
102
+ ],
103
+ "WHO Guideline": self.who_guidelines["WHO Guideline"],
104
  }
105
+ return pd.DataFrame(merged_data)
106
+
107
+ def raise_awareness(self) -> None:
108
+ """
109
+ Shows the awareness content on the main page of the user interface.
110
+ """
111
+ random_fact, awareness_expanders, health_message = (
112
+ self.prepare_awareness_content()
113
+ )
114
+ self.view.raise_awareness(random_fact, awareness_expanders, health_message)
115
 
116
+ def prepare_awareness_content(
117
+ self,
118
+ ) -> Tuple[str, List[Tuple[str, str]], Dict[str, str]]:
119
+ """
120
+ Prepare awareness content including a random fact, expanders, and health message based on air quality data.
121
 
122
+ Returns
123
+ -------
124
+ Tuple[str, List[Tuple[str, str]], Dict[str, str]]
125
+ A tuple containing the random fact, awareness expanders, and health message.
126
+ """
127
+ with open(self.facts_path, "r") as facts_file:
128
+ facts = json.load(facts_file)
129
+ random_fact = random.choice(facts["facts"])
130
+
131
+ with open(self.awareness_path, "r") as awareness_file:
132
+ awareness = json.load(awareness_file)
133
+ awareness_expanders = [
134
+ (title, "\n".join(text)) for title, text in awareness.items()
135
+ ]
136
+
137
+ health_message = {"message": "", "type": ""}
138
+ if (
139
+ self.today_data["NO2 (µg/m³)"] > self.who_guidelines["WHO Guideline"][0]
140
+ or self.today_data["O3 (µg/m³)"] > self.who_guidelines["WHO Guideline"][1]
141
+ ):
142
+ health_message["message"] = (
143
+ "🚨 High pollution levels today. Avoid outdoor activities if possible, especially for vulnerable groups."
144
+ )
145
+ health_message["type"] = "error"
146
+ else:
147
+ health_message["message"] = (
148
+ "✅ Air quality is within safe limits today. Enjoy your outdoor activities!"
149
+ )
150
+ health_message["type"] = "success"
151
+
152
+ return random_fact, awareness_expanders, health_message
153
+
154
+ def quiz(self) -> None:
155
+ """
156
+ Show a quiz question and return the answer and whether the answer was correct.
157
 
158
+ Returns
159
+ -------
160
+ tuple
161
+ A tuple containing the answer and a boolean indicating whether the answer was correct.
162
+ """
163
+ question_number = st.session_state.question_choice
164
+ with open(self.questions_path, "r") as questions_file:
165
+ quiz_data = json.load(questions_file)
166
+ question = quiz_data["quiz"][question_number]["question"]
167
+ options = quiz_data["quiz"][question_number]["options"]
168
+ submitted, answer = self.view.quiz(question, options)
169
+
170
+ if submitted:
171
+ correct_answer = quiz_data["quiz"][question_number]["answer"]
172
+ if answer == correct_answer:
173
+ self.view.success("Correct answer!")
174
+ else:
175
+ self.view.error(
176
+ f"Wrong answer! The correct answer was {correct_answer[0].lower() + correct_answer[1:]}."
177
+ )
178
+
179
+ def two_columns_layout(
180
+ self, ratio: float, left_function: Callable, right_function: Callable
181
+ ) -> None:
182
+ """
183
+ Divide the page into two columns and call the left and right functions within them.
184
+
185
+ Parameters
186
+ ----------
187
+ ratio : float
188
+ The ratio of the left to the right column.
189
+ left_function : Callable
190
+ The function to be called in the left column.
191
+ right_function : Callable
192
+ The function to be called in the right column.
193
+ """
194
+ left, right = st.columns([ratio, 1 - ratio], gap="large")
195
+
196
+ with left:
197
+ left_function()
198
+
199
+ with right:
200
+ right_function()
201
+
202
+ def prepare_line_plot(self) -> go.Figure:
203
+ """
204
+ Prepare a line plot for the next three days' NO2 and O3 levels.
205
+
206
+ Returns:
207
+ go.Figure: A plotly figure object.
208
+ """
209
+ tomorrow, day_after_tomorrow, two_days_after_tomorrow = (
210
+ self.get_next_three_days_dates()
211
+ )
212
+ self.next_three_days["Date"] = [
213
+ tomorrow,
214
+ day_after_tomorrow,
215
+ two_days_after_tomorrow,
216
+ ]
217
+ fig = go.Figure()
218
+ fig.add_trace(
219
+ go.Scatter(
220
+ x=self.next_three_days["Date"],
221
+ y=self.next_three_days["NO2 (µg/m³)"],
222
+ mode="lines+markers+text",
223
+ name="NO2",
224
+ line=dict(color="blue"),
225
+ )
226
+ )
227
+ fig.add_trace(
228
+ go.Scatter(
229
+ x=self.next_three_days["Date"],
230
+ y=self.next_three_days["O3 (µg/m³)"],
231
+ mode="lines+markers+text",
232
+ name="O3",
233
+ line=dict(color="lightblue"),
234
+ )
235
  )
236
 
237
+ # WHO guideline as horizontal dotted lines
238
+ fig.add_hline(
239
+ y=self.who_guidelines["WHO Guideline"][0],
240
+ line_dash="dot",
241
+ line_color="blue",
242
+ annotation_text="WHO NO2 Guideline",
243
+ )
244
+ fig.add_hline(
245
+ y=self.who_guidelines["WHO Guideline"][1],
246
+ line_dash="dot",
247
+ line_color="lightblue",
248
+ annotation_text="WHO O3 Guideline",
249
+ )
250
+
251
+ fig.update_layout(
252
+ title="Predictions for the Next 3 Days",
253
+ xaxis_title="Date",
254
+ yaxis_title="Pollutant Concentration (µg/m³)",
255
+ hovermode="x unified",
256
+ )
257
+ return fig
258
+
259
+ def get_next_three_days_dates(self) -> tuple:
260
+ """
261
+ Get the next three days' dates.
262
+
263
+ Returns:
264
+ tuple: A tuple of three date objects.
265
+ """
266
+ tomorrow = date.today() + timedelta(days=1)
267
+ day_after_tomorrow = date.today() + timedelta(days=2)
268
+ two_days_after_tomorrow = date.today() + timedelta(days=3)
269
+ return tomorrow, day_after_tomorrow, two_days_after_tomorrow
270
+
271
+ def compare_to_who(self) -> list:
272
+ """
273
+ Compare the current pollutant levels to WHO guidelines.
274
 
275
+ Returns:
276
+ list: A list of tuples containing the pollutant name, comparison message, and message type.
277
+ """
278
+ comparisons = []
279
+ for i, pollutant in enumerate(["NO2 (µg/m³)", "O3 (µg/m³)"]):
280
+ if self.today_data[pollutant] > self.who_guidelines["WHO Guideline"][i]:
281
+ comparisons.append(
282
+ (
283
+ pollutant,
284
+ f"🚨 {pollutant} levels exceed WHO guidelines!",
285
+ "error",
286
+ )
287
+ )
288
+ else:
289
+ comparisons.append(
290
+ (
291
+ pollutant,
292
+ f"✅ {pollutant} levels are within safe limits",
293
+ "success",
294
+ )
295
+ )
296
+ return comparisons
297
+
298
+ def prepare_gauge_plots(self) -> list:
299
+ """
300
+ Prepare gauge plots for the next three days' NO2 and O3 levels.
301
+
302
+ Returns:
303
+ list: A list of tuples containing the day index, formatted date, and two plotly figures (for NO2 and O3).
304
+ """
305
+ tomorrow, day_after_tomorrow, two_days_after_tomorrow = (
306
+ self.get_next_three_days_dates()
307
  )
308
+ self.next_three_days["Date"] = [
309
+ tomorrow,
310
+ day_after_tomorrow,
311
+ two_days_after_tomorrow,
312
+ ]
313
+
314
+ gauge_plots = []
315
+ for i, day in enumerate(
316
+ [tomorrow, day_after_tomorrow, two_days_after_tomorrow]
317
+ ):
318
+ no2_value = self.next_three_days["NO2 (µg/m³)"][i]
319
+ o3_value = self.next_three_days["O3 (µg/m³)"][i]
320
+ fig_no2 = self.create_gauge_plot(
321
+ no2_value, self.who_guidelines["WHO Guideline"][0], "NO2 (µg/m³)"
322
+ )
323
+ fig_o3 = self.create_gauge_plot(
324
+ o3_value, self.who_guidelines["WHO Guideline"][1], "O3 (µg/m³)"
325
+ )
326
+ gauge_plots.append((i + 1, day.strftime("%B %d, %Y"), fig_no2, fig_o3))
327
+ return gauge_plots
328
 
329
+ def create_gauge_plot(
330
+ self, value: float, guideline: float, title: str
331
+ ) -> go.Figure:
332
+ """
333
+ Create a gauge plot for a given pollutant value and guideline.
334
+
335
+ Args:
336
+ value (float): The pollutant concentration value.
337
+ guideline (float): The WHO guideline value for the pollutant.
338
+ title (str): The title of the gauge plot.
339
+
340
+ Returns:
341
+ go.Figure: A Plotly figure representing the gauge plot.
342
+ """
343
+ color = "green" if value <= guideline else "red"
344
+ fig = go.Figure(
345
+ go.Indicator(
346
+ mode="gauge+number",
347
+ value=value,
348
+ title={"text": title},
349
+ gauge={"axis": {"range": [0, 2 * guideline]}, "bar": {"color": color}},
350
+ )
351
+ )
352
+ fig.update_layout(height=250, width=250, margin=dict(t=0, b=0, l=0, r=0))
353
+ return fig
streamlit_src/json_interactions/awareness.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "What is Air Pollution?": [
3
+ "Air pollution is a serious concern that affects the environment and public health. High levels of pollutants, such as ozone (O\u2083) and nitrogen dioxide (NO\u2082), can lead to respiratory problems, aggravate pre-existing conditions like asthma, and contribute to cardiovascular diseases."
4
+ ],
5
+ "Why O\u2083 and NO\u2082 Matter": [
6
+ "Ozone (O\u2083): Formed by chemical reactions in the atmosphere, particularly on sunny days. High levels can cause chest pain, coughing, throat irritation, and airway inflammation. \nNitrogen Dioxide (NO\u2082): Mostly emitted from vehicles and industrial activities, this can cause irritation of the respiratory system and decrease lung function, especially during long-term exposure."
7
+ ]
8
+ }
streamlit_src/views/user_view.py CHANGED
@@ -1,335 +1,148 @@
1
- # views/user_view.py
2
  import streamlit as st
3
  import pandas as pd
4
- from datetime import date, datetime, timedelta
5
  import plotly.graph_objects as go
6
- import json
7
- import random
8
- import os
9
-
10
- FACTS_PATH = os.path.join(
11
- os.path.dirname(os.path.dirname(__file__)), "json_interactions/", "facts.json"
12
- )
13
-
14
- QUESTIONS_PATH = os.path.join(
15
- os.path.dirname(os.path.dirname(__file__)), "json_interactions/", "question.json"
16
- )
17
 
18
 
19
  class UserView:
20
- def show_current_data(self, today_data, who_guidelines):
21
- st.sidebar.markdown(f"Today's Date: **{date.today().strftime('%B %d, %Y')}**")
22
-
23
- # Merge today_data and who_guidelines into one DataFrame
24
- merged_data = {
25
- "Pollutant": ["NO2 (µg/m³)", "O3 (µg/m³)"],
26
- "Current Concentration": [
27
- today_data["NO2 (µg/m³)"],
28
- today_data["O3 (µg/m³)"],
29
- ],
30
- "WHO Guideline": [
31
- who_guidelines["WHO Guideline"][0], # NO2 guideline
32
- who_guidelines["WHO Guideline"][1], # O3 guideline
33
- ],
34
- }
35
 
36
- merged_data_df = pd.DataFrame(merged_data)
 
 
37
 
38
- # Use Streamlit column configuration with hide_index=True
 
 
 
 
 
39
  st.sidebar.markdown("### Current Pollutant Concentrations and WHO Guidelines")
40
  st.sidebar.dataframe(merged_data_df, hide_index=True)
41
 
42
- def get_next_three_days_dates(self):
43
  """
44
- Returns the next three days' dates in datetime format as a tuple.
45
 
46
- :return: tuple of three datetime objects, representing tomorrow, day after tomorrow, and two days after tomorrow.
 
47
  """
48
- today = datetime.now() # Get the current date and time
49
- tomorrow = today + timedelta(days=1)
50
- day_after_tomorrow = today + timedelta(days=2)
51
- two_days_after_tomorrow = today + timedelta(days=3)
52
- return tomorrow, day_after_tomorrow, two_days_after_tomorrow
53
 
54
- def display_predictions_lineplot(self, next_three_days, who_guidelines):
55
- tomorrow, day_after_tomorrow, two_days_after_tomorrow = (
56
- self.get_next_three_days_dates()
57
- )
58
-
59
- # Update the dataframe with actual dates in datetime format
60
- next_three_days["Date"] = [
61
- tomorrow,
62
- day_after_tomorrow,
63
- two_days_after_tomorrow,
64
- ]
65
-
66
- # Create the plot using Plotly for more customization
67
- fig = go.Figure()
68
-
69
- # Add NO2 line
70
- fig.add_trace(
71
- go.Scatter(
72
- x=next_three_days["Date"],
73
- y=next_three_days["NO2 (µg/m³)"],
74
- mode="lines+markers+text",
75
- name="NO2 (µg/m³)",
76
- text=[f"{v:.2f} µg/m³" for v in next_three_days["NO2 (µg/m³)"]],
77
- textposition="top right",
78
- line=dict(color="blue"),
79
- )
80
- )
81
-
82
- # Add O3 line
83
- fig.add_trace(
84
- go.Scatter(
85
- x=next_three_days["Date"],
86
- y=next_three_days["O3 (µg/m³)"],
87
- mode="lines+markers+text",
88
- name="O3 (µg/m³)",
89
- text=[f"{v:.2f} µg/m³" for v in next_three_days["O3 (µg/m³)"]],
90
- textposition="top right",
91
- line=dict(color="lightblue"),
92
- )
93
- )
94
 
95
- # Add WHO guideline as horizontal dotted lines
96
- fig.add_hline(
97
- y=who_guidelines["WHO Guideline"][0],
98
- line_dash="dot",
99
- line_color="blue",
100
- annotation_text="WHO NO2 Guideline",
101
- annotation_position="bottom right",
102
- )
103
- fig.add_hline(
104
- y=who_guidelines["WHO Guideline"][1],
105
- line_dash="dot",
106
- line_color="lightblue",
107
- annotation_text="WHO O3 Guideline",
108
- annotation_position="bottom right",
109
- )
110
 
111
- # Update layout for better visuals
112
- fig.update_layout(
113
- title="Predictions for the Next 3 Days with WHO Guidelines",
114
- xaxis_title="Date",
115
- yaxis_title="Pollutant Concentration (µg/m³)",
116
- hovermode="x unified",
117
- )
118
 
119
- # Display the plot in Streamlit
 
 
120
  st.plotly_chart(fig)
121
 
122
- def get_color(self, value, who_limit):
123
- half_who_limit = who_limit / 2
124
-
125
- if value <= half_who_limit:
126
- # Green -> Yellow gradient
127
- return f"rgba({int(255 * value / half_who_limit)}, 255, 0, 1)" # Gradient from green to yellow
128
- elif value <= who_limit:
129
- # Yellow -> Red gradient
130
- excess_value = value - half_who_limit
131
- return f"rgba(255, {int(255 - (255 * excess_value / half_who_limit))}, 0, 1)" # Gradient from yellow to red
132
- else:
133
- # Beyond the WHO limit, fully red
134
- return "rgba(255, 0, 0, 1)" # Fully red
135
-
136
- def display_predictions_gaugeplot(self, next_three_days, who_guidelines):
137
- st.markdown("### Predictions for the Next 3 Days")
138
- # Convert date to datetime and calculate future dates
139
- tomorrow, day_after_tomorrow, two_days_after_tomorrow = (
140
- self.get_next_three_days_dates()
141
- )
142
-
143
- # Update the dataframe with actual dates in datetime format
144
- next_three_days["Date"] = [
145
- tomorrow,
146
- day_after_tomorrow,
147
- two_days_after_tomorrow,
148
- ]
149
- for i in range(3):
150
- formatted_date = next_three_days["Date"][i].strftime("%B %d, %Y")
151
-
152
- st.markdown(f"#### Day {i+1}: {formatted_date}")
153
-
154
- # use multiple columns for centering
155
- _, col1, _, col2, _ = st.columns([0.2, 1, 0.2, 1, 0.2], gap="small")
156
 
157
- # NO2 Gauge
 
 
 
 
 
158
  with col1:
159
- # Get color based on NO2 value
160
- no2_value = next_three_days["NO2 (µg/m³)"][i]
161
- no2_color = self.get_color(
162
- no2_value, who_guidelines["WHO Guideline"][0]
163
- )
164
- fig_no2 = go.Figure(
165
- go.Indicator(
166
- mode="gauge+number",
167
- value=next_three_days["NO2 (µg/m³)"][i],
168
- title={"text": "NO2 (µg/m³)"},
169
- gauge={
170
- "axis": {
171
- "range": [
172
- 0,
173
- 2 * who_guidelines["WHO Guideline"][0],
174
- ]
175
- },
176
- "bar": {"color": no2_color},
177
- },
178
- domain={"x": [0, 1], "y": [0, 1]}, # Controls size
179
- )
180
- )
181
- fig_no2.update_layout(
182
- height=250, width=250, margin=dict(t=0, b=0, l=0, r=0)
183
- )
184
  st.plotly_chart(fig_no2)
185
-
186
- # O3 Gauge
187
  with col2:
188
- o3_value = next_three_days["O3 (µg/m³)"][i]
189
- o3_color = self.get_color(o3_value, who_guidelines["WHO Guideline"][1])
190
- fig_o3 = go.Figure(
191
- go.Indicator(
192
- mode="gauge+number",
193
- value=next_three_days["O3 (µg/m³)"][i],
194
- title={"text": "O3 (µg/m³)"},
195
- gauge={
196
- "axis": {
197
- "range": [
198
- 0,
199
- 1.5 * who_guidelines["WHO Guideline"][1],
200
- ]
201
- },
202
- "bar": {"color": o3_color},
203
- },
204
- domain={"x": [0, 1], "y": [0, 1]}, # Controls size
205
- )
206
- )
207
- fig_o3.update_layout(
208
- height=250, width=250, margin=dict(t=0, b=0, l=0, r=0)
209
- )
210
  st.plotly_chart(fig_o3)
211
 
212
- def view_option_selection(self) -> str:
213
- st.markdown("### Visualizing Air Quality Predictions")
214
- plot_type = st.selectbox("", ("Line Plot", "Gauge Plot"))
215
- return plot_type
216
-
217
- def compare_to_who(self, today_data, no2_level, o3_level):
218
- if today_data["NO2 (µg/m³)"] > no2_level:
219
- st.sidebar.error("⚠️ NO2 levels are above WHO guidelines!")
220
- else:
221
- st.sidebar.success("✅ NO2 levels are within safe limits.")
222
-
223
- if today_data["O3 (µg/m³)"] > o3_level:
224
- st.sidebar.error("⚠️ O3 levels are above WHO guidelines!")
225
- else:
226
- st.sidebar.success("✅ O3 levels are within safe limits.")
227
-
228
- def raise_awareness(self, today_data, who_guidelines):
229
- st.markdown("### Air Quality Awareness")
230
-
231
- # Load facts from the JSON file
232
- with open(FACTS_PATH, "r") as f:
233
- facts = json.load(f)["facts"]
234
 
235
- # Randomly select a fact from the list
236
- random_fact = random.choice(facts)
237
 
238
- # Create expandable sections for key pollutant information
239
- with st.expander("🌍 What is Air Pollution?"):
240
- st.write("""
241
- **Air pollution** is a serious concern that affects the environment and public health.
242
- High levels of pollutants, such as ozone (O₃) and nitrogen dioxide (NO₂), can lead to
243
- respiratory problems, aggravate pre-existing conditions like asthma, and contribute to
244
- cardiovascular diseases.
245
- """)
246
 
247
- with st.expander("⚠️ Why O₃ and NO₂ Matter"):
248
- st.write("""
249
- **Ozone (O₃):** Formed by chemical reactions in the atmosphere, particularly on sunny days.
250
- High levels can cause chest pain, coughing, throat irritation, and airway inflammation.
251
 
252
- **Nitrogen Dioxide (NO₂):** Mostly emitted from vehicles and industrial activities, this can cause
253
- irritation of the respiratory system and decrease lung function, especially during long-term exposure.
254
- """)
 
 
 
 
 
 
 
 
 
 
 
255
 
256
- self.add_spaces(num_lines=3)
 
 
 
 
 
 
 
 
 
257
 
258
- # Display the random fact for user awareness
259
  st.markdown("### Did You Know?")
260
- st.info(random_fact) # Display the random fact in an info box
261
 
262
- self.add_spaces(num_lines=3)
263
-
264
- # Show real-time suggestions for high pollution days
265
  st.markdown("### Health Recommendations Based on Current Levels")
266
- if (
267
- today_data["NO2 (µg/m³)"] > who_guidelines["WHO Guideline"][0]
268
- or today_data["O3 (µg/m³)"] > who_guidelines["WHO Guideline"][1]
269
- ):
270
- st.error(
271
- "🚨 High pollution levels today. Avoid outdoor activities if possible, especially for vulnerable groups."
272
- )
273
  else:
274
- st.success(
275
- "✅ Air quality is within safe limits today. Enjoy your outdoor activities!"
276
- )
277
-
278
- self.add_spaces(num_lines=3)
279
 
280
- def print_sources(self):
281
- # Provide user links to external resources or reports
282
- st.markdown("### Learn More")
283
- st.markdown(
284
- "[WHO Air Quality Guidelines](https://www.who.int/news-room/fact-sheets/detail/ambient-(outdoor)-air-quality-and-health)"
285
- )
286
- st.markdown(
287
- "[Air Pollution Facts](https://www.un.org/sustainabledevelopment/air-pollution/)"
288
- )
289
 
290
- def quiz(self, question_nr=0):
291
- with open(QUESTIONS_PATH, "r") as f:
292
- quiz_data = json.load(f)
293
- # Access the quiz questions
294
- questions = quiz_data["quiz"]
295
- random_question = questions[question_nr]
296
 
297
- # Add a simple quiz to engage the user
 
 
298
  st.markdown("### Quick Quiz: How Much Do You Know About Air Pollution?")
299
  with st.form(key="quiz_form"):
300
- # Display the first question and options
301
- st.write(random_question["question"])
302
- options = random_question["options"]
303
  answer = st.radio("Choose an option:", options)
304
- submitted = st.form_submit_button("Submit Answer")
305
-
306
- if submitted:
307
- if answer == random_question["answer"]:
308
- st.success("Correct!")
309
- else:
310
- st.error(
311
- "Incorrect. The correct answer is: " + random_question["answer"]
312
- )
313
-
314
- def raise_awareness_and_quiz(self, today_data, who_guidelines, question_nr=0):
315
- # Create two columns: main column for awareness and right column for the quiz
316
- col_main, col_right = st.columns(
317
- [0.7, 0.3], gap="large"
318
- ) # 70% for awareness, 30% for quiz
319
 
320
- # Left column: Raise awareness
321
- with col_main:
322
- self.raise_awareness(today_data, who_guidelines)
323
-
324
- # Right column: Quiz
325
- with col_right:
326
- self.quiz(question_nr)
327
-
328
- def add_spaces(self, num_lines=1):
329
- """Add vertical space between sections by adding empty lines.
330
 
331
  Args:
332
- num_lines (int): Number of blank lines to add. Default is 1.
333
  """
334
- for _ in range(num_lines):
335
- st.write("") # This adds a blank line to create space
 
 
 
1
  import streamlit as st
2
  import pandas as pd
 
3
  import plotly.graph_objects as go
4
+ import datetime
 
 
 
 
 
 
 
 
 
 
5
 
6
 
7
  class UserView:
8
+ """
9
+ A class to handle all user interface elements.
10
+ """
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
+ def show_current_data(self, merged_data_df: pd.DataFrame) -> None:
13
+ """
14
+ Show the current pollutant concentrations along with WHO guidelines.
15
 
16
+ Args:
17
+ merged_data_df (pd.DataFrame): A pandas DataFrame containing the current pollutant concentrations and WHO guidelines.
18
+ """
19
+ st.sidebar.markdown(
20
+ f"Today's Date: **{datetime.date.today().strftime('%B %d, %Y')}**"
21
+ )
22
  st.sidebar.markdown("### Current Pollutant Concentrations and WHO Guidelines")
23
  st.sidebar.dataframe(merged_data_df, hide_index=True)
24
 
25
+ def success(self, message: str) -> None:
26
  """
27
+ Show a success message.
28
 
29
+ Args:
30
+ message (str): The message to be displayed.
31
  """
32
+ st.success(message)
 
 
 
 
33
 
34
+ def error(self, message: str) -> None:
35
+ """
36
+ Show an error message.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
 
38
+ Args:
39
+ message (str): The message to be displayed.
40
+ """
41
+ st.error(message)
 
 
 
 
 
 
 
 
 
 
 
42
 
43
+ def display_predictions_lineplot(self, fig: go.Figure) -> None:
44
+ """
45
+ Show a line plot of the predictions.
 
 
 
 
46
 
47
+ Args:
48
+ fig (go.Figure): The plotly figure to be displayed.
49
+ """
50
  st.plotly_chart(fig)
51
 
52
+ def display_predictions_gaugeplot(self, gauge_plots: list) -> None:
53
+ """
54
+ Show a gauge plot of the predictions.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
+ Args:
57
+ gauge_plots (list): A list of tuples containing the day, formatted date, and two plotly figures (for NO2 and O3).
58
+ """
59
+ for day, formatted_date, fig_no2, fig_o3 in gauge_plots:
60
+ st.markdown(f"#### Day {day}: {formatted_date}")
61
+ col1, col2 = st.columns([1, 1])
62
  with col1:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  st.plotly_chart(fig_no2)
 
 
64
  with col2:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  st.plotly_chart(fig_o3)
66
 
67
+ def view_option_selection(self, plot_type: list) -> str:
68
+ """
69
+ Ask the user to select a plot type.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
 
71
+ Args:
72
+ plot_type (list): A list of strings containing the options to be displayed.
73
 
74
+ Returns:
75
+ str: The selected option.
76
+ """
77
+ st.markdown("### Visualizing Air Quality Predictions")
78
+ return st.selectbox("", plot_type)
 
 
 
79
 
80
+ def compare_to_who(self, warnings: list) -> None:
81
+ """
82
+ Compare the current pollutant concentrations with WHO guidelines and display the results.
 
83
 
84
+ Args:
85
+ warnings (list): A list of tuples containing the pollutant, message, and level (error or success) of the warning.
86
+ """
87
+ for pollutant, message, level in warnings:
88
+ if level == "error":
89
+ st.sidebar.error(message)
90
+ elif level == "success":
91
+ st.sidebar.success(message)
92
+
93
+ def raise_awareness(
94
+ self, random_fact: str, awareness_expanders: list, health_message: dict
95
+ ) -> None:
96
+ """
97
+ Raise awareness about air quality issues and provide health recommendations.
98
 
99
+ Args:
100
+ random_fact (str): A random fact about air quality.
101
+ awareness_expanders (list): A list of tuples containing the title and content of the expanders.
102
+ health_message (dict): A dictionary containing the health recommendation message and type (error or success).
103
+ """
104
+ st.markdown("### Air Quality Awareness")
105
+ # Awareness sections
106
+ for expander_title, expander_content in awareness_expanders:
107
+ with st.expander(expander_title):
108
+ st.write(expander_content)
109
 
110
+ # Fact section
111
  st.markdown("### Did You Know?")
112
+ st.info(random_fact)
113
 
114
+ # Health recommendation section
 
 
115
  st.markdown("### Health Recommendations Based on Current Levels")
116
+ if health_message["type"] == "error":
117
+ st.error(health_message["message"])
 
 
 
 
 
118
  else:
119
+ st.success(health_message["message"])
 
 
 
 
120
 
121
+ def quiz(self, question: str, options: list) -> tuple:
122
+ """
123
+ Ask a quiz question and return the answer and whether the answer was correct.
 
 
 
 
 
 
124
 
125
+ Args:
126
+ question (str): The question to be asked.
127
+ options (list): A list of strings containing the options.
 
 
 
128
 
129
+ Returns:
130
+ tuple: A tuple containing the answer and a boolean indicating whether the answer was correct.
131
+ """
132
  st.markdown("### Quick Quiz: How Much Do You Know About Air Pollution?")
133
  with st.form(key="quiz_form"):
134
+ st.write(question)
 
 
135
  answer = st.radio("Choose an option:", options)
136
+ submit_button = st.form_submit_button("Submit Answer")
137
+ return submit_button, answer
 
 
 
 
 
 
 
 
 
 
 
 
 
138
 
139
+ def print_sources(self, sources: list) -> None:
140
+ """
141
+ Print the sources used in the application.
 
 
 
 
 
 
 
142
 
143
  Args:
144
+ sources (list): A list of tuples containing the source text and URL.
145
  """
146
+ st.markdown("### Learn More")
147
+ for source_text, source_url in sources:
148
+ st.markdown(f"[{source_text}]({source_url})")