Team tab: June 2026 = 0 baseline, calendar grid + split charts to match Overview

Starting Followers is now a fixed, human-confirmed anchor (everyone started this
work June 1, 2026 with 0 followers) instead of borrowing the Page's own unrelated
BASELINE_DATE or the export's meaningless ~365-day window edge. Lives in
team_exports.TEAM_BASELINE_DATE/TEAM_BASELINE_FOLLOWERS so both the dashboard and
the downloadable report use the same anchor.

Rebuilt the chart section to mirror the Overview tab's actual layout: a static
Follower Gains calendar grid (reuses the Page's own _monthly_summary/_month_box_*
helpers, renamed columns to fit) + New Followers per Day bar chart side by side,
then separate Impressions and Daily Engagement Rate line cards instead of one
combined dual-axis chart. Daily CTR and Page Views (Desktop/Mobile) are left out
entirely - personal-profile exports have no click or device-level data at all,
so those two Overview cards have no real equivalent here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
beantownbabeandClaude Sonnet 5 committed 2026-10-30 11:18:43 +01:00
1 parent 0c6baad4f1
commit 0ed6e6c457
2 files changed
+83 -34

No files matched your search

+69 -26
View File
@@ -2735,18 +2735,29 @@ def _team_mark_posts(fig, engagement_df, top_posts_df):
return fig
def _team_engagement_figure(engagement_df, top_posts_df):
fig = go.Figure()
fig.add_trace(go.Scatter(x=engagement_df["Date"], y=engagement_df["Impressions"],
name="Impressions", mode="lines", line=dict(color=BLUE, width=2)))
fig.add_trace(go.Scatter(x=engagement_df["Date"], y=engagement_df["Engagements"],
name="Engagements", mode="lines", line=dict(color=ORANGE, width=2),
yaxis="y2"))
_team_mark_posts(fig, engagement_df, top_posts_df)
fig.update_layout(yaxis=dict(title="Impressions"),
yaxis2=dict(title="Engagements", overlaying="y", side="right"),
template=TMPL)
return fig
def _team_calendar_grid(followers_daily_df, top_posts_df):
"""Static version of the Overview/Followers tabs' "Follower Gains — Calendar" —
same month-box coloring (_monthly_summary/_month_box_style/_month_box_children),
just without the click-to-expand day tiles, which would need their own per-person
callback wiring (those are registered once at app-build time, not per dropdown
selection) to do properly. Reuses the Page's own helpers by renaming this export's
columns to match what they expect (Date/Total followers, Created date)."""
fts_like = followers_daily_df.rename(columns={"New followers": "Total followers"})
posts_like = top_posts_df.rename(columns={"Post Publish Date": "Created date"}) \
if not top_posts_df.empty else top_posts_df
years = _available_years(fts_like, posts_like)
if not years:
return html.Div("No follower data.", className="text-muted")
year = years[0]
monthly_net, monthly_posts, months_with_data, max_gain = _monthly_summary(fts_like, posts_like, year)
boxes = [html.Div(_month_box_children(monthly_net, monthly_posts, months_with_data, m),
style=_month_box_style(monthly_net, months_with_data, max_gain, m, selected=False))
for m in range(1, 13)]
return html.Div([
html.Div(str(year), style={"fontWeight": "800", "fontSize": "0.85rem", "marginBottom": "4px"}),
html.Div(boxes, style={"display": "flex", "gap": "6px"}),
calendar_legend(),
])
def _team_followers_figure(followers_daily_df):
@@ -2755,6 +2766,25 @@ def _team_followers_figure(followers_daily_df):
return fig
def _team_impressions_figure(engagement_df, top_posts_df):
fig = px.line(engagement_df, x="Date", y="Impressions", template=TMPL)
fig.update_traces(line=dict(color=BLUE, width=2))
_team_mark_posts(fig, engagement_df, top_posts_df)
return fig
def _team_engagement_rate_figure(engagement_df):
"""Engagements / Impressions per day, as a % — the personal-profile analog of the
Page's own Daily Engagement Rate card. Days with 0 impressions are left out rather
than shown as a misleading 0% (no reach that day, not zero engagement quality)."""
rated = engagement_df[engagement_df["Impressions"] > 0].copy()
rated["Eng. Rate"] = rated["Engagements"] / rated["Impressions"] * 100
fig = px.line(rated, x="Date", y="Eng. Rate", template=TMPL)
fig.update_traces(line=dict(color=GREEN, width=2))
fig.update_layout(yaxis=dict(title="Eng. Rate (%)"))
return fig
def _team_top_posts_table(top_posts_df):
if top_posts_df.empty:
return html.Div("No posts in this window.", className="text-muted")
@@ -2795,13 +2825,14 @@ def _build_person_dashboard(name, data):
window_label = discovery.get("date_range", "")
window_start, window_end = (window_label.split(" - ") + [""])[:2] if window_label else ("", "")
# Same BASELINE_DATE anchor as the Overview tab's own Starting/New Followers tiles,
# not the export's own ~365-day rolling window edge - keeps the two tabs comparable
# instead of each person showing a different, meaningless "started a year ago" date.
baseline_ts = pd.Timestamp(BASELINE_DATE)
# team_exports.TEAM_BASELINE_DATE/TEAM_BASELINE_FOLLOWERS - everyone started this
# work in June 2026 at 0 followers (confirmed directly), not the Page's own
# BASELINE_DATE (unrelated to any individual) and not the export's own ~365-day
# rolling window edge.
baseline_ts = pd.Timestamp(team_exports.TEAM_BASELINE_DATE)
since_baseline = data["followers_daily"][data["followers_daily"]["Date"] >= baseline_ts]
new_followers_window = int(since_baseline["New followers"].sum())
starting_followers = data["followers_total"] - new_followers_window
starting_followers = team_exports.TEAM_BASELINE_FOLLOWERS
baseline_label = baseline_ts.strftime("%b %d, %Y")
best_eng_pct, avg_eng_pct, rated_post_count = _team_engagement_rate_stats(data["top_posts"])
@@ -2822,11 +2853,18 @@ def _build_person_dashboard(name, data):
f"Source: {', '.join(data['source_files'])} — window {window_label}",
style={"fontSize": "0.72rem", "color": "#6b7a8d", "marginBottom": "0.75rem"})
calendar_row = dbc.Row([
dbc.Col(section("Follower Gains — Calendar",
_team_calendar_grid(data["followers_daily"], data["top_posts"])), md=8),
dbc.Col(section("New Followers per Day",
G(_team_followers_figure(data["followers_daily"]), style={"height": "240px"})), md=4),
], className="gx-3 mb-3")
charts_row = dbc.Row([
dbc.Col(section("Daily Impressions & Engagements",
G(_team_engagement_figure(data["engagement"], data["top_posts"]), style={"height": "260px"})), md=6),
dbc.Col(section("Daily New Followers",
G(_team_followers_figure(data["followers_daily"]), style={"height": "260px"})), md=6),
dbc.Col(section("Impressions",
G(_team_impressions_figure(data["engagement"], data["top_posts"]), style={"height": "220px"})), md=6),
dbc.Col(section("Daily Engagement Rate",
G(_team_engagement_rate_figure(data["engagement"]), style={"height": "220px"})), md=6),
], className="gx-3 mb-3")
posts_row = dbc.Row([
@@ -2841,7 +2879,7 @@ def _build_person_dashboard(name, data):
_team_demographics_table(data["content_demographics"])), md=6),
], className="gx-3")
return html.Div([stat_row, source_note, charts_row, posts_row, demo_row])
return html.Div([stat_row, source_note, calendar_row, charts_row, posts_row, demo_row])
_TEAM_EMPTY_PLACEHOLDER = html.Div("No data yet", className="text-muted text-center",
@@ -2870,9 +2908,14 @@ def _build_empty_person_dashboard(name):
"fill in these boxes automatically on the next refresh - no code change needed.",
color="info", style={"marginBottom": "0.75rem"})
calendar_row = dbc.Row([
dbc.Col(section("Follower Gains — Calendar", _TEAM_EMPTY_PLACEHOLDER), md=8),
dbc.Col(section("New Followers per Day", _TEAM_EMPTY_PLACEHOLDER), md=4),
], className="gx-3 mb-3")
charts_row = dbc.Row([
dbc.Col(section("Daily Impressions & Engagements", _TEAM_EMPTY_PLACEHOLDER), md=6),
dbc.Col(section("Daily New Followers", _TEAM_EMPTY_PLACEHOLDER), md=6),
dbc.Col(section("Impressions", _TEAM_EMPTY_PLACEHOLDER), md=6),
dbc.Col(section("Daily Engagement Rate", _TEAM_EMPTY_PLACEHOLDER), md=6),
], className="gx-3 mb-3")
posts_row = dbc.Row([
@@ -2884,7 +2927,7 @@ def _build_empty_person_dashboard(name):
dbc.Col(section("Content Demographics (who engages)", _TEAM_EMPTY_PLACEHOLDER), md=6),
], className="gx-3")
return html.Div([stat_row, source_note, charts_row, posts_row, demo_row])
return html.Div([stat_row, source_note, calendar_row, charts_row, posts_row, demo_row])
@app.callback(
@@ -2918,7 +2961,7 @@ def _download_team_report(_n_clicks, name):
data = team_exports.load_person_data(name, DATA_DIR, UPLOAD_DIR)
if not data:
raise PreventUpdate
docx_bytes = team_exports.build_person_report_docx(name, data, BASELINE_DATE)
docx_bytes = team_exports.build_person_report_docx(name, data)
filename = f"LinkedIn_Report_{name.replace(' ', '_')}.docx"
return dcc.send_bytes(lambda buf: buf.write(docx_bytes), filename=filename)
+14 -8
View File
@@ -35,6 +35,14 @@ TEAM_MEMBERS = [
"Mckinley Black",
]
# Personal-profile baseline, distinct from the Page's own app.BASELINE_DATE (2026-06-16) -
# that's when Page-level tracking began, unrelated to any individual's own history.
# Mckinley confirmed directly: everyone started this work in June 2026 with 0 followers,
# so this is a fixed, human-confirmed anchor the same way BASELINE_FOLLOWERS is for the
# Page, not something derived from the export's own ~365-day rolling window edge.
TEAM_BASELINE_DATE = "2026-06-01"
TEAM_BASELINE_FOLLOWERS = 0
def _is_personal_export(fname, name):
lower = fname.lower()
@@ -167,22 +175,20 @@ def _engagement_rate_stats(top_posts_df):
return rated["rate"].max(), rated["rate"].mean(), len(rated)
def build_person_report_docx(name, data, baseline_date):
def build_person_report_docx(name, data):
"""A send-to-the-person summary of their own analyzed LinkedIn data - Mckinley's
own review, not a raw dump of the export file. Reuses report._docx_table/_docx_shade
(the same table styling as the Page-level monthly report) rather than duplicating
that OOXML-shading logic here. baseline_date is app.py's BASELINE_DATE, passed in
rather than imported so this stays free of any app.py dependency (same reasoning
as report.py's own "no import-time dependency on app.py" rule) - Starting/New
Followers use that same fixed anchor as the Overview tab's own tiles, not the
export's own ~365-day rolling window edge."""
that OOXML-shading logic here. Starting/New Followers use TEAM_BASELINE_DATE/
TEAM_BASELINE_FOLLOWERS (everyone started this work in June 2026 at 0, confirmed
directly), not the export's own ~365-day rolling window edge."""
discovery = data["discovery"]
window_label = discovery.get("date_range", "")
_, window_end = (window_label.split(" - ") + [""])[:2] if window_label else ("", "")
baseline_ts = pd.Timestamp(baseline_date)
baseline_ts = pd.Timestamp(TEAM_BASELINE_DATE)
since_baseline = data["followers_daily"][data["followers_daily"]["Date"] >= baseline_ts]
new_followers_window = int(since_baseline["New followers"].sum())
starting_followers = data["followers_total"] - new_followers_window
starting_followers = TEAM_BASELINE_FOLLOWERS
baseline_label = baseline_ts.strftime("%b %d, %Y")
best_eng, avg_eng, rated_count = _engagement_rate_stats(data["top_posts"])