Spaces:
Running
Running
Update ui_generators.py
Browse files- ui_generators.py +97 -19
ui_generators.py
CHANGED
@@ -1,11 +1,13 @@
|
|
1 |
# ui_generators.py
|
2 |
"""
|
3 |
-
Generates HTML content and Matplotlib plots for the Gradio UI tabs
|
|
|
4 |
"""
|
5 |
import pandas as pd
|
6 |
import logging
|
7 |
import matplotlib.pyplot as plt
|
8 |
import matplotlib # To ensure backend is switched before any plt import from other modules if app structure changes
|
|
|
9 |
|
10 |
# Switch backend for Matplotlib to Agg for Gradio compatibility
|
11 |
matplotlib.use('Agg')
|
@@ -18,6 +20,10 @@ from config import (
|
|
18 |
FOLLOWER_STATS_PAID_COLUMN, FOLLOWER_STATS_CATEGORY_COLUMN_DT, UI_DATE_FORMAT, UI_MONTH_FORMAT
|
19 |
)
|
20 |
|
|
|
|
|
|
|
|
|
21 |
def display_main_dashboard(token_state):
|
22 |
"""Generates HTML for the main dashboard display using data from token_state."""
|
23 |
if not token_state or not token_state.get("token"):
|
@@ -37,8 +43,11 @@ def display_main_dashboard(token_state):
|
|
37 |
display_df_posts = posts_df.copy()
|
38 |
if BUBBLE_POST_DATE_COLUMN_NAME in display_df_posts.columns:
|
39 |
try:
|
40 |
-
|
|
|
41 |
display_df_posts = display_df_posts.sort_values(by=BUBBLE_POST_DATE_COLUMN_NAME, ascending=False)
|
|
|
|
|
42 |
except Exception as e:
|
43 |
logging.error(f"Error formatting post dates for display: {e}")
|
44 |
html_parts.append("<p>Error formatting post dates.</p>")
|
@@ -58,8 +67,9 @@ def display_main_dashboard(token_state):
|
|
58 |
display_df_mentions = mentions_df.copy()
|
59 |
if BUBBLE_MENTIONS_DATE_COLUMN_NAME in display_df_mentions.columns:
|
60 |
try:
|
61 |
-
display_df_mentions[BUBBLE_MENTIONS_DATE_COLUMN_NAME] = pd.to_datetime(display_df_mentions[BUBBLE_MENTIONS_DATE_COLUMN_NAME], errors='coerce')
|
62 |
display_df_mentions = display_df_mentions.sort_values(by=BUBBLE_MENTIONS_DATE_COLUMN_NAME, ascending=False)
|
|
|
63 |
except Exception as e:
|
64 |
logging.error(f"Error formatting mention dates for display: {e}")
|
65 |
html_parts.append("<p>Error formatting mention dates.</p>")
|
@@ -76,15 +86,13 @@ def display_main_dashboard(token_state):
|
|
76 |
if not monthly_gains.empty and FOLLOWER_STATS_CATEGORY_COLUMN in monthly_gains.columns and \
|
77 |
FOLLOWER_STATS_ORGANIC_COLUMN in monthly_gains.columns and FOLLOWER_STATS_PAID_COLUMN in monthly_gains.columns:
|
78 |
try:
|
79 |
-
# FOLLOWER_STATS_CATEGORY_COLUMN for monthly gains is 'YYYY-MM-DD'
|
80 |
monthly_gains.loc[:, FOLLOWER_STATS_CATEGORY_COLUMN_DT] = pd.to_datetime(monthly_gains[FOLLOWER_STATS_CATEGORY_COLUMN], errors='coerce')
|
81 |
-
# Format original date column for display after sorting by datetime
|
82 |
monthly_gains_display = monthly_gains.sort_values(by=FOLLOWER_STATS_CATEGORY_COLUMN_DT, ascending=False)
|
83 |
-
latest_gain = monthly_gains_display.head(1).copy()
|
84 |
if not latest_gain.empty:
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
else:
|
89 |
html_parts.append("<p>No valid monthly follower gain data to display after processing.</p>")
|
90 |
except Exception as e:
|
@@ -120,8 +128,9 @@ def run_mentions_tab_display(token_state):
|
|
120 |
mentions_df_display = mentions_df.copy()
|
121 |
if BUBBLE_MENTIONS_DATE_COLUMN_NAME in mentions_df_display.columns:
|
122 |
try:
|
123 |
-
mentions_df_display[BUBBLE_MENTIONS_DATE_COLUMN_NAME] = pd.to_datetime(mentions_df_display[BUBBLE_MENTIONS_DATE_COLUMN_NAME], errors='coerce')
|
124 |
mentions_df_display = mentions_df_display.sort_values(by=BUBBLE_MENTIONS_DATE_COLUMN_NAME, ascending=False)
|
|
|
125 |
except Exception as e:
|
126 |
logging.error(f"Error formatting mention dates for tab display: {e}")
|
127 |
html_parts.append("<p>Error formatting mention dates.</p>")
|
@@ -132,7 +141,7 @@ def run_mentions_tab_display(token_state):
|
|
132 |
html_parts.append(mentions_df_display[display_columns].head(20).to_html(escape=False, index=False, classes="table table-sm"))
|
133 |
|
134 |
mentions_html_output = "\n".join(html_parts)
|
135 |
-
fig = None
|
136 |
if not mentions_df.empty and "sentiment_label" in mentions_df.columns:
|
137 |
try:
|
138 |
fig_plot, ax = plt.subplots(figsize=(6,4))
|
@@ -142,13 +151,17 @@ def run_mentions_tab_display(token_state):
|
|
142 |
ax.set_ylabel("Count")
|
143 |
plt.xticks(rotation=45, ha='right')
|
144 |
plt.tight_layout()
|
145 |
-
fig = fig_plot
|
146 |
logging.info("Mentions tab: Sentiment distribution plot generated.")
|
147 |
except Exception as e:
|
148 |
logging.error(f"Error generating mentions plot: {e}", exc_info=True)
|
149 |
-
fig
|
150 |
-
|
151 |
-
|
|
|
|
|
|
|
|
|
152 |
|
153 |
return mentions_html_output, fig
|
154 |
|
@@ -187,17 +200,15 @@ def run_follower_stats_tab_display(token_state):
|
|
187 |
|
188 |
html_parts.append("<h4>Monthly Follower Gains (Last 13 Months):</h4>")
|
189 |
table_display_df = monthly_gains_df_sorted_table.copy()
|
190 |
-
table_display_df.loc[:,FOLLOWER_STATS_CATEGORY_COLUMN] = table_display_df[FOLLOWER_STATS_CATEGORY_COLUMN_DT].dt.strftime(UI_MONTH_FORMAT)
|
191 |
html_parts.append(table_display_df[[FOLLOWER_STATS_CATEGORY_COLUMN, FOLLOWER_STATS_ORGANIC_COLUMN, FOLLOWER_STATS_PAID_COLUMN]].head(13).to_html(escape=True, index=False, classes="table table-sm"))
|
192 |
|
193 |
monthly_gains_df_sorted_plot = monthly_gains_df.sort_values(by=FOLLOWER_STATS_CATEGORY_COLUMN_DT, ascending=True).copy()
|
194 |
-
# For plotting, group by month string to ensure unique x-ticks if multiple entries exist for a month (though unlikely for this data type)
|
195 |
monthly_gains_df_sorted_plot.loc[:, '_plot_month'] = monthly_gains_df_sorted_plot[FOLLOWER_STATS_CATEGORY_COLUMN_DT].dt.strftime(UI_MONTH_FORMAT)
|
196 |
plot_data = monthly_gains_df_sorted_plot.groupby('_plot_month').agg(
|
197 |
organic=(FOLLOWER_STATS_ORGANIC_COLUMN, 'sum'),
|
198 |
paid=(FOLLOWER_STATS_PAID_COLUMN, 'sum')
|
199 |
-
).reset_index().sort_values(by='_plot_month')
|
200 |
-
|
201 |
|
202 |
fig_gains, ax_gains = plt.subplots(figsize=(10,5))
|
203 |
ax_gains.plot(plot_data['_plot_month'], plot_data['organic'], marker='o', linestyle='-', label='Organic Gain')
|
@@ -214,6 +225,11 @@ def run_follower_stats_tab_display(token_state):
|
|
214 |
except Exception as e:
|
215 |
logging.error(f"Error processing or plotting monthly gains: {e}", exc_info=True)
|
216 |
html_parts.append("<p>Error displaying monthly follower gain data.</p>")
|
|
|
|
|
|
|
|
|
|
|
217 |
else:
|
218 |
html_parts.append("<p>No monthly follower gain data available or required columns missing.</p>")
|
219 |
html_parts.append("<hr/>")
|
@@ -243,6 +259,11 @@ def run_follower_stats_tab_display(token_state):
|
|
243 |
except Exception as e:
|
244 |
logging.error(f"Error processing or plotting seniority data: {e}", exc_info=True)
|
245 |
html_parts.append("<p>Error displaying follower seniority data.</p>")
|
|
|
|
|
|
|
|
|
|
|
246 |
else:
|
247 |
html_parts.append("<p>No follower seniority data available or required columns missing.</p>")
|
248 |
html_parts.append("<hr/>")
|
@@ -272,9 +293,66 @@ def run_follower_stats_tab_display(token_state):
|
|
272 |
except Exception as e:
|
273 |
logging.error(f"Error processing or plotting industry data: {e}", exc_info=True)
|
274 |
html_parts.append("<p>Error displaying follower industry data.</p>")
|
|
|
|
|
|
|
|
|
|
|
|
|
275 |
else:
|
276 |
html_parts.append("<p>No follower industry data available or required columns missing.</p>")
|
277 |
|
278 |
html_parts.append("</div>")
|
279 |
follower_html_output = "\n".join(html_parts)
|
280 |
return follower_html_output, plot_monthly_gains, plot_seniority_dist, plot_industry_dist
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
# ui_generators.py
|
2 |
"""
|
3 |
+
Generates HTML content and Matplotlib plots for the Gradio UI tabs,
|
4 |
+
and UI components for the Analytics tab.
|
5 |
"""
|
6 |
import pandas as pd
|
7 |
import logging
|
8 |
import matplotlib.pyplot as plt
|
9 |
import matplotlib # To ensure backend is switched before any plt import from other modules if app structure changes
|
10 |
+
import gradio as gr # Added for UI components
|
11 |
|
12 |
# Switch backend for Matplotlib to Agg for Gradio compatibility
|
13 |
matplotlib.use('Agg')
|
|
|
20 |
FOLLOWER_STATS_PAID_COLUMN, FOLLOWER_STATS_CATEGORY_COLUMN_DT, UI_DATE_FORMAT, UI_MONTH_FORMAT
|
21 |
)
|
22 |
|
23 |
+
# Configure logging for this module if not already configured at app level
|
24 |
+
# logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(module)s - %(message)s')
|
25 |
+
|
26 |
+
|
27 |
def display_main_dashboard(token_state):
|
28 |
"""Generates HTML for the main dashboard display using data from token_state."""
|
29 |
if not token_state or not token_state.get("token"):
|
|
|
43 |
display_df_posts = posts_df.copy()
|
44 |
if BUBBLE_POST_DATE_COLUMN_NAME in display_df_posts.columns:
|
45 |
try:
|
46 |
+
# Ensure the date column is datetime before formatting
|
47 |
+
display_df_posts[BUBBLE_POST_DATE_COLUMN_NAME] = pd.to_datetime(display_df_posts[BUBBLE_POST_DATE_COLUMN_NAME], errors='coerce')
|
48 |
display_df_posts = display_df_posts.sort_values(by=BUBBLE_POST_DATE_COLUMN_NAME, ascending=False)
|
49 |
+
# Format for display after sorting
|
50 |
+
display_df_posts[BUBBLE_POST_DATE_COLUMN_NAME] = display_df_posts[BUBBLE_POST_DATE_COLUMN_NAME].dt.strftime(UI_DATE_FORMAT)
|
51 |
except Exception as e:
|
52 |
logging.error(f"Error formatting post dates for display: {e}")
|
53 |
html_parts.append("<p>Error formatting post dates.</p>")
|
|
|
67 |
display_df_mentions = mentions_df.copy()
|
68 |
if BUBBLE_MENTIONS_DATE_COLUMN_NAME in display_df_mentions.columns:
|
69 |
try:
|
70 |
+
display_df_mentions[BUBBLE_MENTIONS_DATE_COLUMN_NAME] = pd.to_datetime(display_df_mentions[BUBBLE_MENTIONS_DATE_COLUMN_NAME], errors='coerce')
|
71 |
display_df_mentions = display_df_mentions.sort_values(by=BUBBLE_MENTIONS_DATE_COLUMN_NAME, ascending=False)
|
72 |
+
display_df_mentions[BUBBLE_MENTIONS_DATE_COLUMN_NAME] = display_df_mentions[BUBBLE_MENTIONS_DATE_COLUMN_NAME].dt.strftime(UI_DATE_FORMAT)
|
73 |
except Exception as e:
|
74 |
logging.error(f"Error formatting mention dates for display: {e}")
|
75 |
html_parts.append("<p>Error formatting mention dates.</p>")
|
|
|
86 |
if not monthly_gains.empty and FOLLOWER_STATS_CATEGORY_COLUMN in monthly_gains.columns and \
|
87 |
FOLLOWER_STATS_ORGANIC_COLUMN in monthly_gains.columns and FOLLOWER_STATS_PAID_COLUMN in monthly_gains.columns:
|
88 |
try:
|
|
|
89 |
monthly_gains.loc[:, FOLLOWER_STATS_CATEGORY_COLUMN_DT] = pd.to_datetime(monthly_gains[FOLLOWER_STATS_CATEGORY_COLUMN], errors='coerce')
|
|
|
90 |
monthly_gains_display = monthly_gains.sort_values(by=FOLLOWER_STATS_CATEGORY_COLUMN_DT, ascending=False)
|
91 |
+
latest_gain = monthly_gains_display.head(1).copy()
|
92 |
if not latest_gain.empty:
|
93 |
+
latest_gain.loc[:, FOLLOWER_STATS_CATEGORY_COLUMN] = latest_gain[FOLLOWER_STATS_CATEGORY_COLUMN_DT].dt.strftime(UI_DATE_FORMAT)
|
94 |
+
html_parts.append("<h5>Latest Monthly Follower Gain:</h5>")
|
95 |
+
html_parts.append(latest_gain[[FOLLOWER_STATS_CATEGORY_COLUMN, FOLLOWER_STATS_ORGANIC_COLUMN, FOLLOWER_STATS_PAID_COLUMN]].to_html(escape=True, index=False, classes="table table-sm"))
|
96 |
else:
|
97 |
html_parts.append("<p>No valid monthly follower gain data to display after processing.</p>")
|
98 |
except Exception as e:
|
|
|
128 |
mentions_df_display = mentions_df.copy()
|
129 |
if BUBBLE_MENTIONS_DATE_COLUMN_NAME in mentions_df_display.columns:
|
130 |
try:
|
131 |
+
mentions_df_display[BUBBLE_MENTIONS_DATE_COLUMN_NAME] = pd.to_datetime(mentions_df_display[BUBBLE_MENTIONS_DATE_COLUMN_NAME], errors='coerce')
|
132 |
mentions_df_display = mentions_df_display.sort_values(by=BUBBLE_MENTIONS_DATE_COLUMN_NAME, ascending=False)
|
133 |
+
mentions_df_display[BUBBLE_MENTIONS_DATE_COLUMN_NAME] = mentions_df_display[BUBBLE_MENTIONS_DATE_COLUMN_NAME].dt.strftime(UI_DATE_FORMAT)
|
134 |
except Exception as e:
|
135 |
logging.error(f"Error formatting mention dates for tab display: {e}")
|
136 |
html_parts.append("<p>Error formatting mention dates.</p>")
|
|
|
141 |
html_parts.append(mentions_df_display[display_columns].head(20).to_html(escape=False, index=False, classes="table table-sm"))
|
142 |
|
143 |
mentions_html_output = "\n".join(html_parts)
|
144 |
+
fig = None # Initialize fig to None
|
145 |
if not mentions_df.empty and "sentiment_label" in mentions_df.columns:
|
146 |
try:
|
147 |
fig_plot, ax = plt.subplots(figsize=(6,4))
|
|
|
151 |
ax.set_ylabel("Count")
|
152 |
plt.xticks(rotation=45, ha='right')
|
153 |
plt.tight_layout()
|
154 |
+
fig = fig_plot # Assign the figure object
|
155 |
logging.info("Mentions tab: Sentiment distribution plot generated.")
|
156 |
except Exception as e:
|
157 |
logging.error(f"Error generating mentions plot: {e}", exc_info=True)
|
158 |
+
# fig remains None
|
159 |
+
finally:
|
160 |
+
if fig is not None and fig is not plt: # Ensure we don't close the global plt if fig is just an alias
|
161 |
+
plt.close(fig) # Close the specific figure to free memory
|
162 |
+
elif fig is None and plt.get_fignums(): # If fig is None but some global figure might exist from error
|
163 |
+
plt.close('all')
|
164 |
+
|
165 |
|
166 |
return mentions_html_output, fig
|
167 |
|
|
|
200 |
|
201 |
html_parts.append("<h4>Monthly Follower Gains (Last 13 Months):</h4>")
|
202 |
table_display_df = monthly_gains_df_sorted_table.copy()
|
203 |
+
table_display_df.loc[:,FOLLOWER_STATS_CATEGORY_COLUMN] = table_display_df[FOLLOWER_STATS_CATEGORY_COLUMN_DT].dt.strftime(UI_MONTH_FORMAT)
|
204 |
html_parts.append(table_display_df[[FOLLOWER_STATS_CATEGORY_COLUMN, FOLLOWER_STATS_ORGANIC_COLUMN, FOLLOWER_STATS_PAID_COLUMN]].head(13).to_html(escape=True, index=False, classes="table table-sm"))
|
205 |
|
206 |
monthly_gains_df_sorted_plot = monthly_gains_df.sort_values(by=FOLLOWER_STATS_CATEGORY_COLUMN_DT, ascending=True).copy()
|
|
|
207 |
monthly_gains_df_sorted_plot.loc[:, '_plot_month'] = monthly_gains_df_sorted_plot[FOLLOWER_STATS_CATEGORY_COLUMN_DT].dt.strftime(UI_MONTH_FORMAT)
|
208 |
plot_data = monthly_gains_df_sorted_plot.groupby('_plot_month').agg(
|
209 |
organic=(FOLLOWER_STATS_ORGANIC_COLUMN, 'sum'),
|
210 |
paid=(FOLLOWER_STATS_PAID_COLUMN, 'sum')
|
211 |
+
).reset_index().sort_values(by='_plot_month') # Ensure month order for plot
|
|
|
212 |
|
213 |
fig_gains, ax_gains = plt.subplots(figsize=(10,5))
|
214 |
ax_gains.plot(plot_data['_plot_month'], plot_data['organic'], marker='o', linestyle='-', label='Organic Gain')
|
|
|
225 |
except Exception as e:
|
226 |
logging.error(f"Error processing or plotting monthly gains: {e}", exc_info=True)
|
227 |
html_parts.append("<p>Error displaying monthly follower gain data.</p>")
|
228 |
+
finally:
|
229 |
+
if plot_monthly_gains is not None and plot_monthly_gains is not plt:
|
230 |
+
plt.close(plot_monthly_gains)
|
231 |
+
elif plot_monthly_gains is None and plt.get_fignums():
|
232 |
+
plt.close('all')
|
233 |
else:
|
234 |
html_parts.append("<p>No monthly follower gain data available or required columns missing.</p>")
|
235 |
html_parts.append("<hr/>")
|
|
|
259 |
except Exception as e:
|
260 |
logging.error(f"Error processing or plotting seniority data: {e}", exc_info=True)
|
261 |
html_parts.append("<p>Error displaying follower seniority data.</p>")
|
262 |
+
finally:
|
263 |
+
if plot_seniority_dist is not None and plot_seniority_dist is not plt:
|
264 |
+
plt.close(plot_seniority_dist)
|
265 |
+
elif plot_seniority_dist is None and plt.get_fignums():
|
266 |
+
plt.close('all')
|
267 |
else:
|
268 |
html_parts.append("<p>No follower seniority data available or required columns missing.</p>")
|
269 |
html_parts.append("<hr/>")
|
|
|
293 |
except Exception as e:
|
294 |
logging.error(f"Error processing or plotting industry data: {e}", exc_info=True)
|
295 |
html_parts.append("<p>Error displaying follower industry data.</p>")
|
296 |
+
finally:
|
297 |
+
if plot_industry_dist is not None and plot_industry_dist is not plt:
|
298 |
+
plt.close(plot_industry_dist)
|
299 |
+
elif plot_industry_dist is None and plt.get_fignums():
|
300 |
+
plt.close('all')
|
301 |
+
|
302 |
else:
|
303 |
html_parts.append("<p>No follower industry data available or required columns missing.</p>")
|
304 |
|
305 |
html_parts.append("</div>")
|
306 |
follower_html_output = "\n".join(html_parts)
|
307 |
return follower_html_output, plot_monthly_gains, plot_seniority_dist, plot_industry_dist
|
308 |
+
|
309 |
+
|
310 |
+
# --- NEW UI GENERATION LOGIC FOR ANALYTICS TAB ---
|
311 |
+
def create_analytics_plot_ui_row(label, plot_id_str):
|
312 |
+
"""
|
313 |
+
Creates a Gradio Row with a Plot component, a Bomb (insights) button,
|
314 |
+
and a hidden Column for insights.
|
315 |
+
"""
|
316 |
+
# This function will be called within a gr.Blocks() context in app.py
|
317 |
+
with gr.Row(equal_height=False):
|
318 |
+
with gr.Column(scale=7): # Main column for the plot
|
319 |
+
plot_component = gr.Plot(label=label)
|
320 |
+
with gr.Column(scale=1, min_width=60): # Smaller column for the bomb button
|
321 |
+
bomb_button = gr.Button("💣", variant="secondary", size="sm", elem_id=f"bomb_{plot_id_str}")
|
322 |
+
# Insights column, initially hidden, appears to the right
|
323 |
+
with gr.Column(scale=4, visible=False, elem_id=f"insights_col_{plot_id_str}") as insights_col_ui:
|
324 |
+
gr.Markdown(f"##### Insights: {label}") # Title for the insights panel
|
325 |
+
insights_markdown_ui = gr.Markdown(f"Click 💣 for insights on {label}...") # Placeholder text
|
326 |
+
return plot_component, bomb_button, insights_col_ui, insights_markdown_ui
|
327 |
+
|
328 |
+
def build_analytics_tab_ui_components(plot_configs):
|
329 |
+
"""
|
330 |
+
Builds and returns all UI components for the analytics plots based on plot_configs.
|
331 |
+
This function should be called within the "Analytics" TabItem context in app.py.
|
332 |
+
It will create the section headers and the plot rows.
|
333 |
+
"""
|
334 |
+
logging.info(f"Building UI components for {len(plot_configs)} analytics plots.")
|
335 |
+
plot_ui_objects = {} # Stores {"plot_id": {"plot": gr.Plot, "bomb": gr.Button, "insights_col": gr.Column, "insights_md": gr.Markdown, "label": str}}
|
336 |
+
|
337 |
+
current_section_title = ""
|
338 |
+
for config in plot_configs:
|
339 |
+
if config["section"] != current_section_title:
|
340 |
+
gr.Markdown(f"### {config['section']}") # Create section header
|
341 |
+
current_section_title = config["section"]
|
342 |
+
|
343 |
+
# Create the plot row using the helper
|
344 |
+
plot_component, bomb_button, insights_col_ui, insights_markdown_ui = \
|
345 |
+
create_analytics_plot_ui_row(config["label"], config["id"])
|
346 |
+
|
347 |
+
# Store the created components in the dictionary
|
348 |
+
plot_ui_objects[config["id"]] = {
|
349 |
+
"plot": plot_component,
|
350 |
+
"bomb": bomb_button,
|
351 |
+
"insights_col": insights_col_ui,
|
352 |
+
"insights_md": insights_markdown_ui,
|
353 |
+
"label": config["label"] # Store label for easy access, e.g., for insights text
|
354 |
+
}
|
355 |
+
logging.debug(f"Created UI for plot_id: {config['id']} in section: {config['section']}")
|
356 |
+
|
357 |
+
logging.info(f"Finished building UI components for analytics tab. Total objects: {len(plot_ui_objects)}")
|
358 |
+
return plot_ui_objects
|