GuglielmoTor commited on
Commit
4d2fd83
·
verified ·
1 Parent(s): 0bc9f1f

Update analytics_fetch_and_rendering.py

Browse files
Files changed (1) hide show
  1. analytics_fetch_and_rendering.py +87 -288
analytics_fetch_and_rendering.py CHANGED
@@ -1,349 +1,148 @@
1
  import json
2
  import requests
3
- from sessions import create_session
4
- from datetime import datetime, timezone, timedelta # Added timezone, timedelta
5
- import matplotlib.pyplot as plt # Added for plotting
6
- from error_handling import display_error
7
  import gradio as gr
8
  import traceback
9
  import html
10
 
 
 
11
 
12
  API_V2_BASE = 'https://api.linkedin.com/v2'
13
- API_REST_BASE = "https://api.linkedin.com/rest"
14
 
15
  def extract_follower_gains(data):
16
- """Extracts monthly follower gains from API response."""
17
- results = []
18
- print(f"Raw gains data received for extraction: {json.dumps(data, indent=2)}") # Debug print
19
  elements = data.get("elements", [])
20
  if not elements:
21
- print("Warning: No 'elements' found in follower statistics response.")
22
  return []
23
 
 
24
  for item in elements:
25
- time_range = item.get("timeRange", {})
26
- start_timestamp = time_range.get("start")
27
- if start_timestamp is None:
28
- print("Warning: Skipping item due to missing start timestamp.")
29
  continue
30
 
31
- # Convert timestamp to YYYY-MM format for clearer labeling
32
- # Use UTC timezone explicitly
33
  try:
34
- date_obj = datetime.fromtimestamp(start_timestamp / 1000, tz=timezone.utc)
35
- # Format as Year-Month (e.g., 2024-03)
36
- date_str = date_obj.strftime('%Y-%m')
37
- except Exception as time_e:
38
- print(f"Warning: Could not parse timestamp {start_timestamp}. Error: {time_e}. Skipping item.")
39
  continue
40
 
41
- follower_gains = item.get("followerGains", {})
42
- # Handle potential None values from API by defaulting to 0
43
- organic_gain = follower_gains.get("organicFollowerGain", 0) or 0
44
- paid_gain = follower_gains.get("paidFollowerGain", 0) or 0
45
-
46
  results.append({
47
- "date": date_str, # Store simplified date string
48
- "organic": organic_gain,
49
- "paid": paid_gain
50
  })
51
 
52
- print(f"Extracted follower gains (unsorted): {results}") # Debug print
53
- # Sort results by date string to ensure chronological order for plotting
54
- try:
55
- results.sort(key=lambda x: x['date'])
56
- except Exception as sort_e:
57
- print(f"Warning: Could not sort follower gains by date. Error: {sort_e}")
58
-
59
- print(f"Extracted follower gains (sorted): {results}")
60
- return results
61
 
62
- def fetch_analytics_data(comm_client_id, comm_token):
63
- """Fetches org URN, follower count, and follower gains using the Marketing token."""
64
- print("--- Fetching Analytics Data ---")
65
- if not comm_token:
66
  raise ValueError("comm_token is missing.")
67
 
68
- token_dict = comm_token if isinstance(comm_token, dict) else {'access_token': comm_token, 'token_type': 'Bearer'}
69
- ln_mkt = create_session(comm_client_id, token=token_dict)
70
 
71
  try:
72
- # 1. Fetch Org URN and Name
73
- print("Fetching Org URN for analytics...")
74
- # This function already handles errors and raises ValueError
75
- #org_urn, org_name = fetch_org_urn(token_dict)
76
  org_urn, org_name = "urn:li:organization:19010008", "GRLS"
77
- print(f"Analytics using Org: {org_name} ({org_urn})")
78
 
79
- # 2. Fetch Follower Count (v2 API)
80
- # Endpoint requires r_organization_social permission
81
- print("Fetching follower count...")
82
  count_url = f"{API_V2_BASE}/networkSizes/{org_urn}?edgeType=CompanyFollowedByMember"
83
- print(f"Requesting follower count from: {count_url}")
84
- resp_count = ln_mkt.get(count_url)
85
- print(f"→ COUNT Response Status: {resp_count.status_code}")
86
- print(f"→ COUNT Response Body: {resp_count.text}")
87
- resp_count.raise_for_status() # Check for HTTP errors
88
- count_data = resp_count.json()
89
- # The follower count is in 'firstDegreeSize'
90
- follower_count = count_data.get("firstDegreeSize", 0)
91
- print(f"Follower count: {follower_count}")
92
 
93
-
94
- # 3. Fetch Follower Gains (REST API)
95
- # Endpoint requires r_organization_social permission
96
- print("Fetching follower gains...")
97
- # Calculate start date: 12 months ago, beginning of that month, UTC
98
- now = datetime.now(timezone.utc)
99
- # Go back roughly 365 days
100
- twelve_months_ago = now - timedelta(days=365)
101
- # Set to the first day of that month
102
- start_of_period = twelve_months_ago.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
103
- start_ms = int(start_of_period.timestamp() * 1000)
104
- print(f"Requesting gains starting from: {start_of_period.strftime('%Y-%m-%d %H:%M:%S %Z')} ({start_ms} ms)")
105
 
106
  gains_url = (
107
  f"{API_REST_BASE}/organizationalEntityFollowerStatistics"
108
- f"?q=organizationalEntity"
109
- f"&organizationalEntity={org_urn}"
110
  f"&timeIntervals.timeGranularityType=MONTH"
111
  f"&timeIntervals.timeRange.start={start_ms}"
112
- # No end date needed to get data up to the latest available month
113
  )
114
- print(f"Requesting gains from: {gains_url}")
115
- resp_gains = ln_mkt.get(gains_url)
116
- print(f"→ GAINS Request Headers: {resp_gains.request.headers}")
117
- print(f"→ GAINS Response Status: {resp_gains.status_code}")
118
- print(f"→ GAINS Response Body (first 500 chars): {resp_gains.text[:500]}")
119
- resp_gains.raise_for_status() # Check for HTTP errors
120
- gains_data = resp_gains.json()
121
-
122
- # 4. Process Gains Data using the extraction function
123
- follower_gains_list = extract_follower_gains(gains_data)
124
 
125
- # Return all fetched data
126
- return org_name, follower_count, follower_gains_list
127
 
128
  except requests.exceptions.RequestException as e:
129
- status = e.response.status_code if e.response is not None else "N/A"
130
- details = ""
131
- if e.response is not None:
132
- try:
133
- details = f" Details: {e.response.json()}"
134
- except json.JSONDecodeError:
135
- details = f" Response: {e.response.text[:200]}..."
136
- print(f"ERROR fetching analytics data (Status: {status}).{details}")
137
- # Re-raise a user-friendly error, including the original exception context
138
- raise ValueError(f"Failed to fetch analytics data from LinkedIn API (Status: {status}). Check permissions (r_organization_social) and API status.") from e
139
- except ValueError as ve:
140
- # Catch ValueErrors raised by fetch_org_urn
141
- print(f"ERROR during analytics data fetch (likely Org URN): {ve}")
142
- raise ve # Re-raise the specific error message
143
  except Exception as e:
144
- print(f"UNEXPECTED ERROR processing analytics data: {e}")
145
- tb = traceback.format_exc()
146
- print(tb)
147
- raise ValueError(f"An unexpected error occurred while fetching or processing analytics data.") from e
148
 
 
 
149
 
150
- def plot_follower_gains(follower_data):
151
- """Generates a matplotlib plot for follower gains. Returns the figure object."""
152
- print(f"Plotting follower gains data: {follower_data}")
153
- plt.style.use('seaborn-v0_8-whitegrid') # Use a nice style
154
-
155
- if not follower_data:
156
- print("No follower data to plot.")
157
- # Create an empty plot with a message
158
  fig, ax = plt.subplots(figsize=(10, 5))
159
- ax.text(0.5, 0.5, 'No follower gains data available for the last 12 months.',
160
- horizontalalignment='center', verticalalignment='center',
161
- transform=ax.transAxes, fontsize=12, color='grey')
162
- ax.set_title('Monthly Follower Gains (Last 12 Months)')
163
- ax.set_xlabel('Month')
164
- ax.set_ylabel('Follower Gains')
165
- # Remove ticks if there's no data
166
- ax.set_xticks([])
167
- ax.set_yticks([])
168
- plt.tight_layout()
169
- return fig # Return the figure object
170
-
171
- try:
172
- # Ensure data is sorted by date (should be done in extract_follower_gains, but double-check)
173
- follower_data.sort(key=lambda x: x['date'])
174
-
175
- dates = [entry['date'] for entry in follower_data] # Should be 'YYYY-MM' strings
176
- organic_gains = [entry['organic'] for entry in follower_data]
177
- paid_gains = [entry['paid'] for entry in follower_data]
178
-
179
- # Create the plot
180
- fig, ax = plt.subplots(figsize=(12, 6)) # Use fig, ax pattern
181
-
182
- ax.plot(dates, organic_gains, label='Organic Follower Gain', marker='o', linestyle='-', color='#0073b1') # LinkedIn blue
183
- ax.plot(dates, paid_gains, label='Paid Follower Gain', marker='x', linestyle='--', color='#d9534f') # Reddish color
184
-
185
- # Customize the plot
186
- ax.set_xlabel('Month (YYYY-MM)')
187
- ax.set_ylabel('Number of New Followers')
188
- ax.set_title('Monthly Follower Gains (Last 12 Months)')
189
-
190
- # Improve x-axis label readability
191
- # Show fewer labels if there are many months
192
- tick_frequency = max(1, len(dates) // 10) # Show label roughly every N months
193
- ax.set_xticks(dates[::tick_frequency])
194
- ax.tick_params(axis='x', rotation=45, labelsize=9) # Rotate and adjust size
195
-
196
- ax.legend(title="Gain Type")
197
- ax.grid(True, linestyle='--', alpha=0.6) # Lighter grid
198
-
199
- # Add value labels on top of bars/points (optional, can get crowded)
200
- # for i, (org, paid) in enumerate(zip(organic_gains, paid_gains)):
201
- # if org > 0: ax.text(i, org, f'{org}', ha='center', va='bottom', fontsize=8)
202
- # if paid > 0: ax.text(i, paid, f'{paid}', ha='center', va='bottom', fontsize=8, color='red')
203
-
204
-
205
- plt.tight_layout() # Adjust layout to prevent labels from overlapping
206
-
207
- print("Successfully generated follower gains plot.")
208
- # Return the figure object for Gradio
209
- return fig
210
- except Exception as plot_e:
211
- print(f"ERROR generating follower gains plot: {plot_e}")
212
- tb = traceback.format_exc()
213
- print(tb)
214
- # Return an empty plot with an error message if plotting fails
215
- fig, ax = plt.subplots(figsize=(10, 5))
216
- ax.text(0.5, 0.5, f'Error generating plot: {plot_e}',
217
- horizontalalignment='center', verticalalignment='center',
218
- transform=ax.transAxes, fontsize=12, color='red', wrap=True)
219
- ax.set_title('Follower Gains Plot Error')
220
- plt.tight_layout()
221
  return fig
222
 
223
- # Add this function below `plot_follower_gains` in your code
224
-
225
- def plot_growth_rate(follower_data, total_follower_count):
226
- """
227
- Generates a line chart for Follower Growth Rate (%) on a monthly basis.
228
- """
229
- print(f"Plotting follower growth rate from data: {follower_data} and total followers: {total_follower_count}")
230
- if not follower_data:
 
 
 
 
 
 
 
231
  fig, ax = plt.subplots(figsize=(10, 5))
232
- ax.text(0.5, 0.5, 'No data available to calculate growth rate.',
233
- horizontalalignment='center', verticalalignment='center',
234
- transform=ax.transAxes, fontsize=12, color='grey')
235
- ax.set_title('Monthly Follower Growth Rate (%)')
236
- ax.set_xticks([])
237
- ax.set_yticks([])
238
- plt.tight_layout()
239
  return fig
240
 
241
- try:
242
- follower_data.sort(key=lambda x: x['date'])
243
- dates = [entry['date'] for entry in follower_data]
244
- total_gains = [entry['organic'] + entry['paid'] for entry in follower_data]
245
-
246
- # Reconstruct monthly total followers backwards from the current total
247
- followers_at_end_of_month = []
248
- current_total = total_follower_count
249
- for gain in reversed(total_gains):
250
- followers_at_end_of_month.insert(0, current_total)
251
- current_total -= gain
252
-
253
- # followers_at_end_of_month now contains follower totals at end of each month
254
- growth_rates = []
255
- for i in range(1, len(followers_at_end_of_month)):
256
- start = followers_at_end_of_month[i - 1]
257
- end = followers_at_end_of_month[i]
258
- rate = ((end - start) / start * 100) if start > 0 else 0
259
- growth_rates.append(rate)
260
-
261
- # Trim the first date (because we start calculating growth from second month)
262
- rate_dates = dates[1:]
263
 
264
- fig, ax = plt.subplots(figsize=(12, 6))
265
- ax.plot(rate_dates, growth_rates, label='Growth Rate (%)', marker='o', linestyle='-', color='green')
266
- ax.set_title('Monthly Follower Growth Rate (%)')
267
- ax.set_xlabel('Month (YYYY-MM)')
268
- ax.set_ylabel('Growth Rate (%)')
269
- ax.grid(True, linestyle='--', alpha=0.6)
270
- tick_frequency = max(1, len(rate_dates) // 10)
271
- ax.set_xticks(rate_dates[::tick_frequency])
272
- ax.tick_params(axis='x', rotation=45, labelsize=9)
273
- ax.legend()
274
 
275
- plt.tight_layout()
276
- print("Successfully generated growth rate plot.")
277
- return fig
278
-
279
- except Exception as e:
280
- print(f"ERROR generating growth rate plot: {e}")
281
- tb = traceback.format_exc()
282
- print(tb)
283
- fig, ax = plt.subplots(figsize=(10, 5))
284
- ax.text(0.5, 0.5, f'Error generating growth rate plot: {e}',
285
- horizontalalignment='center', verticalalignment='center',
286
- transform=ax.transAxes, fontsize=12, color='red', wrap=True)
287
- ax.set_title('Follower Growth Rate Plot Error')
288
- plt.tight_layout()
289
- return fig
290
 
 
 
 
 
 
 
 
291
 
 
 
 
292
 
293
- def fetch_and_render_analytics(comm_client_id, comm_token):
294
- """Fetches analytics data and prepares updates for Gradio UI."""
295
- print("--- Rendering Analytics Tab ---")
296
- # Initial state for outputs
297
- count_output = gr.update(value="<p>Loading follower count...</p>", visible=True)
298
- plot_output = gr.update(value=None, visible=False) # Hide plot initially
299
-
300
- if not comm_token:
301
- print("ERROR: Marketing token missing for analytics.")
302
- error_msg = "<p style='color: red; text-align: center; font-weight: bold;'>❌ Error: Missing LinkedIn Marketing token. Please complete the login process first.</p>"
303
- return gr.update(value=error_msg, visible=True), gr.update(value=None, visible=False)
304
 
305
  try:
306
- # Fetch all data together
307
- org_name, follower_count, follower_gains_list = fetch_analytics_data(comm_client_id, comm_token)
308
-
309
- # Format follower count display - Nicer HTML
310
- count_display_html = f"""
311
- <div style='text-align: center; padding: 20px; background-color: #e7f3ff; border: 1px solid #bce8f1; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);'>
312
- <p style='font-size: 1.1em; color: #31708f; margin-bottom: 5px;'>Total Followers for</p>
313
- <p style='font-size: 1.4em; font-weight: bold; color: #005a9e; margin-bottom: 10px;'>{html.escape(org_name)}</p>
314
- <p style='font-size: 2.8em; font-weight: bold; color: #0073b1; margin: 0;'>{follower_count:,}</p>
315
- <p style='font-size: 0.9em; color: #777; margin-top: 5px;'>(As of latest data available)</p>
316
  </div>
317
  """
318
- count_output = gr.update(value=count_display_html, visible=True)
319
-
320
- # Generate plot
321
- print("Generating follower gains plot...")
322
- plot_fig = plot_follower_gains(follower_gains_list)
323
- # If plot generation failed, plot_fig might contain an error message plot
324
- plot_output = gr.update(value=plot_fig, visible=True)
325
-
326
- # Generate follower growth rate plot
327
- growth_rate_fig = plot_growth_rate(follower_gains_list, follower_count)
328
- growth_output = gr.update(value=growth_rate_fig, visible=True)
329
-
330
- return count_output, plot_output, growth_output
331
 
332
- except (ValueError, requests.exceptions.RequestException) as api_ve:
333
- # Catch specific API or configuration errors from fetch_analytics_data
334
- print(f"API or VALUE ERROR during analytics fetch: {api_ve}")
335
- error_update = display_error(f"Failed to load analytics: {api_ve}", api_ve)
336
- # Show error in the count area, hide plot
337
- return gr.update(value=error_update.get('value', "<p style='color: red;'>Error loading follower count.</p>"), visible=True), gr.update(value=None, visible=False)
338
  except Exception as e:
339
- # Catch any other unexpected errors during fetch or plotting
340
- print(f"UNEXPECTED ERROR during analytics rendering: {e}")
341
- tb = traceback.format_exc()
342
- print(tb)
343
- error_update = display_error("An unexpected error occurred while loading analytics.", e)
344
- error_html = error_update.get('value', "<p style='color: red;'>An unexpected error occurred.</p>")
345
- # Ensure the error message is HTML-safe
346
- if isinstance(error_html, str) and not error_html.strip().startswith("<"):
347
- error_html = f"<pre style='color: red; white-space: pre-wrap;'>{html.escape(error_html)}</pre>"
348
- # Show error in the count area, hide plot
349
- return gr.update(value=error_html, visible=True), gr.update(value=None, visible=False)
 
1
  import json
2
  import requests
3
+ from datetime import datetime, timezone, timedelta
4
+ import matplotlib.pyplot as plt
 
 
5
  import gradio as gr
6
  import traceback
7
  import html
8
 
9
+ from sessions import create_session
10
+ from error_handling import display_error
11
 
12
  API_V2_BASE = 'https://api.linkedin.com/v2'
13
+ API_REST_BASE = 'https://api.linkedin.com/rest'
14
 
15
  def extract_follower_gains(data):
 
 
 
16
  elements = data.get("elements", [])
17
  if not elements:
 
18
  return []
19
 
20
+ results = []
21
  for item in elements:
22
+ start_timestamp = item.get("timeRange", {}).get("start")
23
+ if not start_timestamp:
 
 
24
  continue
25
 
 
 
26
  try:
27
+ date_str = datetime.fromtimestamp(start_timestamp / 1000, tz=timezone.utc).strftime('%Y-%m')
28
+ except Exception:
 
 
 
29
  continue
30
 
31
+ gains = item.get("followerGains", {})
 
 
 
 
32
  results.append({
33
+ "date": date_str,
34
+ "organic": gains.get("organicFollowerGain", 0) or 0,
35
+ "paid": gains.get("paidFollowerGain", 0) or 0
36
  })
37
 
38
+ return sorted(results, key=lambda x: x['date'])
 
 
 
 
 
 
 
 
39
 
40
+ def fetch_analytics_data(client_id, token):
41
+ if not token:
 
 
42
  raise ValueError("comm_token is missing.")
43
 
44
+ token_dict = token if isinstance(token, dict) else {'access_token': token, 'token_type': 'Bearer'}
45
+ session = create_session(client_id, token=token_dict)
46
 
47
  try:
 
 
 
 
48
  org_urn, org_name = "urn:li:organization:19010008", "GRLS"
 
49
 
 
 
 
50
  count_url = f"{API_V2_BASE}/networkSizes/{org_urn}?edgeType=CompanyFollowedByMember"
51
+ follower_count = session.get(count_url).json().get("firstDegreeSize", 0)
 
 
 
 
 
 
 
 
52
 
53
+ start = datetime.now(timezone.utc) - timedelta(days=365)
54
+ start = start.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
55
+ start_ms = int(start.timestamp() * 1000)
 
 
 
 
 
 
 
 
 
56
 
57
  gains_url = (
58
  f"{API_REST_BASE}/organizationalEntityFollowerStatistics"
59
+ f"?q=organizationalEntity&organizationalEntity={org_urn}"
 
60
  f"&timeIntervals.timeGranularityType=MONTH"
61
  f"&timeIntervals.timeRange.start={start_ms}"
 
62
  )
63
+ gains_data = session.get(gains_url).json()
64
+ gains = extract_follower_gains(gains_data)
 
 
 
 
 
 
 
 
65
 
66
+ return org_name, follower_count, gains
 
67
 
68
  except requests.exceptions.RequestException as e:
69
+ status = getattr(e.response, 'status_code', 'N/A')
70
+ msg = f"Failed to fetch LinkedIn analytics (Status: {status})."
71
+ raise ValueError(msg) from e
 
 
 
 
 
 
 
 
 
 
 
72
  except Exception as e:
73
+ raise ValueError("Unexpected error during LinkedIn analytics fetch.") from e
 
 
 
74
 
75
+ def plot_follower_gains(data):
76
+ plt.style.use('seaborn-v0_8-whitegrid')
77
 
78
+ if not data:
 
 
 
 
 
 
 
79
  fig, ax = plt.subplots(figsize=(10, 5))
80
+ ax.text(0.5, 0.5, 'No follower gains data.', ha='center', va='center', transform=ax.transAxes)
81
+ ax.set_title('Monthly Follower Gains')
82
+ ax.set_xticks([]); ax.set_yticks([])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  return fig
84
 
85
+ dates = [d['date'] for d in data]
86
+ organic = [d['organic'] for d in data]
87
+ paid = [d['paid'] for d in data]
88
+
89
+ fig, ax = plt.subplots(figsize=(12, 6))
90
+ ax.plot(dates, organic, label='Organic', marker='o', color='#0073b1')
91
+ ax.plot(dates, paid, label='Paid', marker='x', linestyle='--', color='#d9534f')
92
+ ax.set(title='Monthly Follower Gains', xlabel='Month', ylabel='New Followers')
93
+ ax.tick_params(axis='x', rotation=45)
94
+ ax.legend()
95
+ plt.tight_layout()
96
+ return fig
97
+
98
+ def plot_growth_rate(data, total):
99
+ if not data:
100
  fig, ax = plt.subplots(figsize=(10, 5))
101
+ ax.text(0.5, 0.5, 'No data for growth rate.', ha='center', va='center', transform=ax.transAxes)
102
+ ax.set_title('Growth Rate (%)')
103
+ ax.set_xticks([]); ax.set_yticks([])
 
 
 
 
104
  return fig
105
 
106
+ dates = [d['date'] for d in data]
107
+ gains = [d['organic'] + d['paid'] for d in data]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
 
109
+ history = []
110
+ current = total
111
+ for g in reversed(gains):
112
+ history.insert(0, current)
113
+ current -= g
 
 
 
 
 
114
 
115
+ rates = [((history[i] - history[i-1]) / history[i-1] * 100 if history[i-1] else 0) for i in range(1, len(history))]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
 
117
+ fig, ax = plt.subplots(figsize=(12, 6))
118
+ ax.plot(dates[1:], rates, label='Growth Rate (%)', marker='o', color='green')
119
+ ax.set(title='Monthly Growth Rate (%)', xlabel='Month', ylabel='Growth %')
120
+ ax.tick_params(axis='x', rotation=45)
121
+ ax.legend()
122
+ plt.tight_layout()
123
+ return fig
124
 
125
+ def fetch_and_render_analytics(client_id, token):
126
+ loading = gr.update(value="<p>Loading follower count...</p>", visible=True)
127
+ hidden = gr.update(value=None, visible=False)
128
 
129
+ if not token:
130
+ error = "<p style='color:red;'>❌ Missing token. Please log in.</p>"
131
+ return gr.update(value=error, visible=True), hidden, hidden
 
 
 
 
 
 
 
 
132
 
133
  try:
134
+ name, count, gains = fetch_analytics_data(client_id, token)
135
+
136
+ count_html = f"""
137
+ <div style='text-align:center; padding:20px; background:#e7f3ff; border:1px solid #bce8f1; border-radius:8px;'>
138
+ <p style='font-size:1.1em; color:#31708f;'>Total Followers for</p>
139
+ <p style='font-size:1.4em; font-weight:bold; color:#005a9e;'>{html.escape(name)}</p>
140
+ <p style='font-size:2.8em; font-weight:bold; color:#0073b1;'>{count:,}</p>
141
+ <p style='font-size:0.9em; color:#777;'>(As of latest data)</p>
 
 
142
  </div>
143
  """
144
+ return gr.update(value=count_html, visible=True), gr.update(value=plot_follower_gains(gains), visible=True), gr.update(value=plot_growth_rate(gains, count), visible=True)
 
 
 
 
 
 
 
 
 
 
 
 
145
 
 
 
 
 
 
 
146
  except Exception as e:
147
+ error = display_error("Analytics load failed.", e).get('value', "<p style='color:red;'>Error loading data.</p>")
148
+ return gr.update(value=error, visible=True), hidden, hidden