GuglielmoTor commited on
Commit
29818ba
·
verified ·
1 Parent(s): eaf20fd

Update ui_generators.py

Browse files
Files changed (1) hide show
  1. ui_generators.py +52 -58
ui_generators.py CHANGED
@@ -24,10 +24,11 @@ from config import (
24
  # logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(module)s - %(message)s')
25
 
26
  # --- Constants for Button Icons/Text ---
 
27
  BOMB_ICON = "💣"
28
  EXPLORE_ICON = "🧭"
29
  FORMULA_ICON = "ƒ"
30
- ACTIVE_ICON = "❌ Close"
31
 
32
 
33
  def display_main_dashboard(token_state):
@@ -151,23 +152,23 @@ def run_mentions_tab_display(token_state):
151
  fig_plot_local = None
152
  if not mentions_df.empty and "sentiment_label" in mentions_df.columns:
153
  try:
154
- fig_plot_local, ax = plt.subplots(figsize=(6,4))
155
  sentiment_counts = mentions_df["sentiment_label"].value_counts()
156
  sentiment_counts.plot(kind='bar', ax=ax, color=['#4CAF50', '#FFC107', '#F44336', '#9E9E9E', '#2196F3'])
157
- ax.set_title("Mention Sentiment Distribution", y=1.03) # Adjusted y for Matplotlib title
158
  ax.set_ylabel("Count")
159
  plt.xticks(rotation=45, ha='right')
160
 
161
  plt.tight_layout()
162
- fig_plot_local.subplots_adjust(top=0.90) # MODIFIED: Add space at the top of the figure
163
- # This pushes plot content down from Gradio's label
164
  fig = fig_plot_local
165
  logging.info("Mentions tab: Sentiment distribution plot generated.")
166
  except Exception as e:
167
  logging.error(f"Error generating mentions plot: {e}", exc_info=True)
168
  fig = None
169
  finally:
170
- if fig_plot_local and fig_plot_local is not plt:
 
171
  plt.close(fig_plot_local)
172
  return mentions_html_output, fig
173
 
@@ -216,14 +217,14 @@ def run_follower_stats_tab_display(token_state):
216
  organic=(FOLLOWER_STATS_ORGANIC_COLUMN, 'sum'),
217
  paid=(FOLLOWER_STATS_PAID_COLUMN, 'sum')
218
  ).reset_index()
219
- plot_data['_plot_month_dt'] = pd.to_datetime(plot_data['_plot_month'], format=UI_MONTH_FORMAT)
220
  plot_data = plot_data.sort_values(by='_plot_month_dt')
221
 
222
 
223
- fig_gains_local, ax_gains = plt.subplots(figsize=(10,5))
224
  ax_gains.plot(plot_data['_plot_month'], plot_data['organic'], marker='o', linestyle='-', label='Organic Gain')
225
  ax_gains.plot(plot_data['_plot_month'], plot_data['paid'], marker='x', linestyle='--', label='Paid Gain')
226
- ax_gains.set_title("Monthly Follower Gains Over Time", y=1.03) # Adjusted y for Matplotlib title
227
  ax_gains.set_ylabel("Follower Count")
228
  ax_gains.set_xlabel("Month (YYYY-MM)")
229
  plt.xticks(rotation=45, ha='right')
@@ -231,7 +232,7 @@ def run_follower_stats_tab_display(token_state):
231
  plt.grid(True, linestyle='--', alpha=0.7)
232
 
233
  plt.tight_layout()
234
- fig_gains_local.subplots_adjust(top=0.90) # MODIFIED: Add space at the top of the figure
235
  plot_monthly_gains = fig_gains_local
236
  logging.info("Follower stats tab: Monthly gains plot generated.")
237
  else:
@@ -259,16 +260,16 @@ def run_follower_stats_tab_display(token_state):
259
  html_parts.append("<h4>Followers by Seniority (Top 10 Organic):</h4>")
260
  html_parts.append(seniority_df_sorted[[FOLLOWER_STATS_CATEGORY_COLUMN, FOLLOWER_STATS_ORGANIC_COLUMN, FOLLOWER_STATS_PAID_COLUMN]].head(10).to_html(escape=True, index=False, classes="table table-sm"))
261
 
262
- fig_seniority_local, ax_seniority = plt.subplots(figsize=(8,5))
263
  top_n_seniority = seniority_df_sorted.nlargest(10, FOLLOWER_STATS_ORGANIC_COLUMN)
264
  ax_seniority.bar(top_n_seniority[FOLLOWER_STATS_CATEGORY_COLUMN], top_n_seniority[FOLLOWER_STATS_ORGANIC_COLUMN], color='skyblue')
265
- ax_seniority.set_title("Follower Distribution by Seniority (Top 10 Organic)", y=1.03) # Adjusted y
266
  ax_seniority.set_ylabel("Organic Follower Count")
267
  plt.xticks(rotation=45, ha='right')
268
  plt.grid(axis='y', linestyle='--', alpha=0.7)
269
 
270
  plt.tight_layout()
271
- fig_seniority_local.subplots_adjust(top=0.88) # MODIFIED: Add space. Experiment with this value.
272
  plot_seniority_dist = fig_seniority_local
273
  logging.info("Follower stats tab: Seniority distribution plot generated.")
274
  else:
@@ -295,16 +296,16 @@ def run_follower_stats_tab_display(token_state):
295
  html_parts.append("<h4>Followers by Industry (Top 10 Organic):</h4>")
296
  html_parts.append(industry_df_sorted[[FOLLOWER_STATS_CATEGORY_COLUMN, FOLLOWER_STATS_ORGANIC_COLUMN, FOLLOWER_STATS_PAID_COLUMN]].head(10).to_html(escape=True, index=False, classes="table table-sm"))
297
 
298
- fig_industry_local, ax_industry = plt.subplots(figsize=(8,5))
299
  top_n_industry = industry_df_sorted.nlargest(10, FOLLOWER_STATS_ORGANIC_COLUMN)
300
  ax_industry.bar(top_n_industry[FOLLOWER_STATS_CATEGORY_COLUMN], top_n_industry[FOLLOWER_STATS_ORGANIC_COLUMN], color='lightcoral')
301
- ax_industry.set_title("Follower Distribution by Industry (Top 10 Organic)", y=1.03) # Adjusted y
302
  ax_industry.set_ylabel("Organic Follower Count")
303
  plt.xticks(rotation=45, ha='right')
304
  plt.grid(axis='y', linestyle='--', alpha=0.7)
305
 
306
  plt.tight_layout()
307
- fig_industry_local.subplots_adjust(top=0.88) # MODIFIED: Add space. Experiment with this value.
308
  plot_industry_dist = fig_industry_local
309
  logging.info("Follower stats tab: Industry distribution plot generated.")
310
  else:
@@ -322,38 +323,30 @@ def run_follower_stats_tab_display(token_state):
322
  follower_html_output = "\n".join(html_parts)
323
  return follower_html_output, plot_monthly_gains, plot_seniority_dist, plot_industry_dist
324
 
 
325
  def create_analytics_plot_panel(plot_label_str, plot_id_str):
326
  """
327
  Creates an individual plot panel with its plot component and action buttons.
328
  Plot title and action buttons are on the same row.
329
  Returns the panel (Column), plot component, and button components.
330
  """
331
- # Values for BOMB_ICON, EXPLORE_ICON, FORMULA_ICON should be sourced from where they are defined,
332
- # e.g., imported from config or passed as arguments if they vary.
333
- try:
334
- # Assuming these are defined in your config.py and imported in app.py,
335
- # they might not be directly available here unless explicitly passed or re-imported.
336
- # For robustness, using string literals if not found.
337
- from config import BOMB_ICON, EXPLORE_ICON, FORMULA_ICON
338
- except ImportError:
339
- logging.warning("Icons BOMB_ICON, EXPLORE_ICON, FORMULA_ICON not found in config for ui_generators, using defaults.")
340
- BOMB_ICON = "💣"
341
- EXPLORE_ICON = "🧭"
342
- FORMULA_ICON = "ƒ"
343
-
344
  with gr.Column(visible=True) as panel_component: # Main container for this plot
345
- with gr.Row(variant="compact"): # Removed vertical_align="center"
346
- # Removed scale=3 from gr.Markdown as it's not a valid argument
347
- gr.Markdown(f"#### {plot_label_str}") # Plot title
348
- # Removed min_width=120 from gr.Row as it's not a valid argument
349
- with gr.Row(elem_classes="plot-actions", scale=1): # Action buttons container
350
- bomb_button = gr.Button(value=BOMB_ICON, variant="secondary", size="sm", min_width=30, elem_id=f"bomb_btn_{plot_id_str}")
351
- formula_button = gr.Button(value=FORMULA_ICON, variant="secondary", size="sm", min_width=30, elem_id=f"formula_btn_{plot_id_str}")
352
- explore_button = gr.Button(value=EXPLORE_ICON, variant="secondary", size="sm", min_width=30, elem_id=f"explore_btn_{plot_id_str}")
353
 
354
- plot_component = gr.Plot(label=plot_label_str, show_label=False) # Label is shown in Markdown above
 
355
 
356
- logging.debug(f"Created analytics panel (styled) for: {plot_label_str} (ID: {plot_id_str})")
357
  return panel_component, plot_component, bomb_button, explore_button, formula_button
358
 
359
 
@@ -366,7 +359,7 @@ def build_analytics_tab_plot_area(plot_configs):
366
  - plot_ui_objects (dict): Dictionary of plot UI objects.
367
  - section_titles_map (dict): Dictionary mapping section names to their gr.Markdown title components.
368
  """
369
- logging.info(f"Building plot area for {len(plot_configs)} analytics plots with interleaved section titles and styled panels.")
370
  plot_ui_objects = {}
371
  section_titles_map = {}
372
 
@@ -377,37 +370,35 @@ def build_analytics_tab_plot_area(plot_configs):
377
  current_plot_config = plot_configs[idx]
378
  current_section_name = current_plot_config["section"]
379
 
 
380
  if current_section_name != last_rendered_section:
381
  if current_section_name not in section_titles_map:
 
382
  section_md_component = gr.Markdown(f"### {current_section_name}", visible=True)
383
  section_titles_map[current_section_name] = section_md_component
384
  logging.debug(f"Rendered and stored Markdown for section: {current_section_name}")
385
- else:
386
- # This case handles if a section name appears non-contiguously, ensure its component is made visible.
387
- # The component itself is already created.
388
- section_titles_map[current_section_name].visible = True
389
- logging.debug(f"Ensuring visibility for existing Markdown for section: {current_section_name}")
390
  last_rendered_section = current_section_name
391
 
392
- with gr.Row(equal_height=False): # Row for one or two plots
393
  # --- Process the first plot in the row (config1) ---
394
  config1 = plot_configs[idx]
395
- # Safety check, though current_section_name should match config1["section"] here
396
  if config1["section"] != current_section_name:
397
- logging.warning(f"Plot {config1['id']} section mismatch. Expected {current_section_name}, got {config1['section']}. Re-evaluating title.")
398
- # This indicates a potential issue in plot_configs order or logic, but try to recover title
399
- if config1["section"] not in section_titles_map:
400
- sec_md = gr.Markdown(f"### {config1['section']}", visible=True)
401
- section_titles_map[config1["section"]] = sec_md
402
- last_rendered_section = config1["section"]
403
-
404
 
405
  panel_col1, plot_comp1, bomb_btn1, explore_btn1, formula_btn1 = \
406
- create_analytics_plot_panel(config1["label"], config1["id"]) # Use the styled panel creator
407
  plot_ui_objects[config1["id"]] = {
408
  "plot_component": plot_comp1, "bomb_button": bomb_btn1,
409
  "explore_button": explore_btn1, "formula_button": formula_btn1,
410
- "label": config1["label"], "panel_component": panel_col1,
411
  "section": config1["section"]
412
  }
413
  logging.debug(f"Created UI panel for plot_id: {config1['id']} in section {config1['section']}")
@@ -416,9 +407,10 @@ def build_analytics_tab_plot_area(plot_configs):
416
  # --- Process the second plot in the row (config2), if applicable ---
417
  if idx < len(plot_configs):
418
  config2 = plot_configs[idx]
419
- if config2["section"] == current_section_name: # Must be in the same section for the same row
 
420
  panel_col2, plot_comp2, bomb_btn2, explore_btn2, formula_btn2 = \
421
- create_analytics_plot_panel(config2["label"], config2["id"]) # Use the styled panel creator
422
  plot_ui_objects[config2["id"]] = {
423
  "plot_component": plot_comp2, "bomb_button": bomb_btn2,
424
  "explore_button": explore_btn2, "formula_button": formula_btn2,
@@ -427,6 +419,8 @@ def build_analytics_tab_plot_area(plot_configs):
427
  }
428
  logging.debug(f"Created UI panel for plot_id: {config2['id']} in same row, section {config2['section']}")
429
  idx += 1
 
 
430
 
431
  logging.info(f"Finished building plot area. Total plot objects: {len(plot_ui_objects)}. Section titles created: {len(section_titles_map)}")
432
  if len(plot_ui_objects) != len(plot_configs):
 
24
  # logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(module)s - %(message)s')
25
 
26
  # --- Constants for Button Icons/Text ---
27
+ # These are also defined/imported in app.py, ensure consistency
28
  BOMB_ICON = "💣"
29
  EXPLORE_ICON = "🧭"
30
  FORMULA_ICON = "ƒ"
31
+ ACTIVE_ICON = "❌ Close" # Ensure this matches app.py
32
 
33
 
34
  def display_main_dashboard(token_state):
 
152
  fig_plot_local = None
153
  if not mentions_df.empty and "sentiment_label" in mentions_df.columns:
154
  try:
155
+ fig_plot_local, ax = plt.subplots(figsize=(6,4)) # Keep figsize for aspect ratio
156
  sentiment_counts = mentions_df["sentiment_label"].value_counts()
157
  sentiment_counts.plot(kind='bar', ax=ax, color=['#4CAF50', '#FFC107', '#F44336', '#9E9E9E', '#2196F3'])
158
+ ax.set_title("Mention Sentiment Distribution", y=1.03)
159
  ax.set_ylabel("Count")
160
  plt.xticks(rotation=45, ha='right')
161
 
162
  plt.tight_layout()
163
+ fig_plot_local.subplots_adjust(top=0.90)
 
164
  fig = fig_plot_local
165
  logging.info("Mentions tab: Sentiment distribution plot generated.")
166
  except Exception as e:
167
  logging.error(f"Error generating mentions plot: {e}", exc_info=True)
168
  fig = None
169
  finally:
170
+ # Ensure plt.close is called on the figure object, not plt itself if it's not the same
171
+ if fig_plot_local and fig_plot_local is not plt: # Check if fig_plot_local is a Figure object
172
  plt.close(fig_plot_local)
173
  return mentions_html_output, fig
174
 
 
217
  organic=(FOLLOWER_STATS_ORGANIC_COLUMN, 'sum'),
218
  paid=(FOLLOWER_STATS_PAID_COLUMN, 'sum')
219
  ).reset_index()
220
+ plot_data['_plot_month_dt'] = pd.to_datetime(plot_data['_plot_month'], format=UI_MONTH_FORMAT) # Ensure correct month format
221
  plot_data = plot_data.sort_values(by='_plot_month_dt')
222
 
223
 
224
+ fig_gains_local, ax_gains = plt.subplots(figsize=(10,5)) # Keep figsize for aspect ratio
225
  ax_gains.plot(plot_data['_plot_month'], plot_data['organic'], marker='o', linestyle='-', label='Organic Gain')
226
  ax_gains.plot(plot_data['_plot_month'], plot_data['paid'], marker='x', linestyle='--', label='Paid Gain')
227
+ ax_gains.set_title("Monthly Follower Gains Over Time", y=1.03)
228
  ax_gains.set_ylabel("Follower Count")
229
  ax_gains.set_xlabel("Month (YYYY-MM)")
230
  plt.xticks(rotation=45, ha='right')
 
232
  plt.grid(True, linestyle='--', alpha=0.7)
233
 
234
  plt.tight_layout()
235
+ fig_gains_local.subplots_adjust(top=0.90)
236
  plot_monthly_gains = fig_gains_local
237
  logging.info("Follower stats tab: Monthly gains plot generated.")
238
  else:
 
260
  html_parts.append("<h4>Followers by Seniority (Top 10 Organic):</h4>")
261
  html_parts.append(seniority_df_sorted[[FOLLOWER_STATS_CATEGORY_COLUMN, FOLLOWER_STATS_ORGANIC_COLUMN, FOLLOWER_STATS_PAID_COLUMN]].head(10).to_html(escape=True, index=False, classes="table table-sm"))
262
 
263
+ fig_seniority_local, ax_seniority = plt.subplots(figsize=(8,5)) # Keep figsize for aspect ratio
264
  top_n_seniority = seniority_df_sorted.nlargest(10, FOLLOWER_STATS_ORGANIC_COLUMN)
265
  ax_seniority.bar(top_n_seniority[FOLLOWER_STATS_CATEGORY_COLUMN], top_n_seniority[FOLLOWER_STATS_ORGANIC_COLUMN], color='skyblue')
266
+ ax_seniority.set_title("Follower Distribution by Seniority (Top 10 Organic)", y=1.03)
267
  ax_seniority.set_ylabel("Organic Follower Count")
268
  plt.xticks(rotation=45, ha='right')
269
  plt.grid(axis='y', linestyle='--', alpha=0.7)
270
 
271
  plt.tight_layout()
272
+ fig_seniority_local.subplots_adjust(top=0.88)
273
  plot_seniority_dist = fig_seniority_local
274
  logging.info("Follower stats tab: Seniority distribution plot generated.")
275
  else:
 
296
  html_parts.append("<h4>Followers by Industry (Top 10 Organic):</h4>")
297
  html_parts.append(industry_df_sorted[[FOLLOWER_STATS_CATEGORY_COLUMN, FOLLOWER_STATS_ORGANIC_COLUMN, FOLLOWER_STATS_PAID_COLUMN]].head(10).to_html(escape=True, index=False, classes="table table-sm"))
298
 
299
+ fig_industry_local, ax_industry = plt.subplots(figsize=(8,5)) # Keep figsize for aspect ratio
300
  top_n_industry = industry_df_sorted.nlargest(10, FOLLOWER_STATS_ORGANIC_COLUMN)
301
  ax_industry.bar(top_n_industry[FOLLOWER_STATS_CATEGORY_COLUMN], top_n_industry[FOLLOWER_STATS_ORGANIC_COLUMN], color='lightcoral')
302
+ ax_industry.set_title("Follower Distribution by Industry (Top 10 Organic)", y=1.03)
303
  ax_industry.set_ylabel("Organic Follower Count")
304
  plt.xticks(rotation=45, ha='right')
305
  plt.grid(axis='y', linestyle='--', alpha=0.7)
306
 
307
  plt.tight_layout()
308
+ fig_industry_local.subplots_adjust(top=0.88)
309
  plot_industry_dist = fig_industry_local
310
  logging.info("Follower stats tab: Industry distribution plot generated.")
311
  else:
 
323
  follower_html_output = "\n".join(html_parts)
324
  return follower_html_output, plot_monthly_gains, plot_seniority_dist, plot_industry_dist
325
 
326
+
327
  def create_analytics_plot_panel(plot_label_str, plot_id_str):
328
  """
329
  Creates an individual plot panel with its plot component and action buttons.
330
  Plot title and action buttons are on the same row.
331
  Returns the panel (Column), plot component, and button components.
332
  """
333
+ # Icons are defined globally or imported. For this function, ensure they are accessible.
334
+ # If not using from config directly here, you might need to pass them or use fixed strings.
335
+ # Using fixed strings as a fallback if import fails, though they should be available via app.py's import.
336
+ local_bomb_icon, local_explore_icon, local_formula_icon = BOMB_ICON, EXPLORE_ICON, FORMULA_ICON
337
+
 
 
 
 
 
 
 
 
338
  with gr.Column(visible=True) as panel_component: # Main container for this plot
339
+ with gr.Row(variant="compact"):
340
+ gr.Markdown(f"#### {plot_label_str}", scale=3) # Plot title (scale might help balance)
341
+ with gr.Row(elem_classes="plot-actions", scale=1, min_width=150): # Action buttons container, give it some min_width
342
+ bomb_button = gr.Button(value=local_bomb_icon, variant="secondary", size="sm", min_width=30, elem_id=f"bomb_btn_{plot_id_str}")
343
+ formula_button = gr.Button(value=local_formula_icon, variant="secondary", size="sm", min_width=30, elem_id=f"formula_btn_{plot_id_str}")
344
+ explore_button = gr.Button(value=local_explore_icon, variant="secondary", size="sm", min_width=30, elem_id=f"explore_btn_{plot_id_str}")
 
 
345
 
346
+ # MODIFIED: Added height to gr.Plot for consistent sizing
347
+ plot_component = gr.Plot(label=plot_label_str, show_label=False, height=350) # Adjust height as needed
348
 
349
+ logging.debug(f"Created analytics panel for: {plot_label_str} (ID: {plot_id_str}) with fixed plot height.")
350
  return panel_component, plot_component, bomb_button, explore_button, formula_button
351
 
352
 
 
359
  - plot_ui_objects (dict): Dictionary of plot UI objects.
360
  - section_titles_map (dict): Dictionary mapping section names to their gr.Markdown title components.
361
  """
362
+ logging.info(f"Building plot area for {len(plot_configs)} analytics plots with interleaved section titles.")
363
  plot_ui_objects = {}
364
  section_titles_map = {}
365
 
 
370
  current_plot_config = plot_configs[idx]
371
  current_section_name = current_plot_config["section"]
372
 
373
+ # Render section title if it's new for this block of plots
374
  if current_section_name != last_rendered_section:
375
  if current_section_name not in section_titles_map:
376
+ # Create the Markdown component for the section title
377
  section_md_component = gr.Markdown(f"### {current_section_name}", visible=True)
378
  section_titles_map[current_section_name] = section_md_component
379
  logging.debug(f"Rendered and stored Markdown for section: {current_section_name}")
380
+ # No 'else' needed here for visibility, as it's handled by click handlers if sections are hidden/shown.
381
+ # The component is created once and its visibility is controlled elsewhere.
 
 
 
382
  last_rendered_section = current_section_name
383
 
384
+ with gr.Row(equal_height=False): # Row for one or two plots. equal_height=False allows plots to define their height.
385
  # --- Process the first plot in the row (config1) ---
386
  config1 = plot_configs[idx]
387
+ # Safety check for section consistency (should always pass if configs are ordered by section)
388
  if config1["section"] != current_section_name:
389
+ logging.warning(f"Plot {config1['id']} section mismatch. Expected {current_section_name}, got {config1['section']}. This might affect layout if a new section title was expected.")
390
+ # If a new section starts unexpectedly, ensure its title is created if missing
391
+ if config1["section"] not in section_titles_map:
392
+ sec_md = gr.Markdown(f"### {config1['section']}", visible=True) # Create and make visible
393
+ section_titles_map[config1['section']] = sec_md
394
+ last_rendered_section = config1["section"] # Update the current section context
 
395
 
396
  panel_col1, plot_comp1, bomb_btn1, explore_btn1, formula_btn1 = \
397
+ create_analytics_plot_panel(config1["label"], config1["id"])
398
  plot_ui_objects[config1["id"]] = {
399
  "plot_component": plot_comp1, "bomb_button": bomb_btn1,
400
  "explore_button": explore_btn1, "formula_button": formula_btn1,
401
+ "label": config1["label"], "panel_component": panel_col1, # This is the gr.Column containing the plot and its actions
402
  "section": config1["section"]
403
  }
404
  logging.debug(f"Created UI panel for plot_id: {config1['id']} in section {config1['section']}")
 
407
  # --- Process the second plot in the row (config2), if applicable ---
408
  if idx < len(plot_configs):
409
  config2 = plot_configs[idx]
410
+ # Only add to the same row if it's part of the same section
411
+ if config2["section"] == current_section_name:
412
  panel_col2, plot_comp2, bomb_btn2, explore_btn2, formula_btn2 = \
413
+ create_analytics_plot_panel(config2["label"], config2["id"])
414
  plot_ui_objects[config2["id"]] = {
415
  "plot_component": plot_comp2, "bomb_button": bomb_btn2,
416
  "explore_button": explore_btn2, "formula_button": formula_btn2,
 
419
  }
420
  logging.debug(f"Created UI panel for plot_id: {config2['id']} in same row, section {config2['section']}")
421
  idx += 1
422
+ # If the next plot is in a new section, it will be handled in the next iteration of the while loop,
423
+ # starting with a new section title and a new gr.Row.
424
 
425
  logging.info(f"Finished building plot area. Total plot objects: {len(plot_ui_objects)}. Section titles created: {len(section_titles_map)}")
426
  if len(plot_ui_objects) != len(plot_configs):