Team tab: real calendar (day tiles + post stars), one-line chart row, top-5 demographics
Calendar now matches the Overview tab for real: reuses _month_day_tiles to show the default month's day-by-day breakdown underneath the month-box grid (not just the month boxes alone), and the New Followers bar chart now overlays star markers on publish days via the Page's own mark_posts(), using TEAM_BASELINE_DATE-filtered data the same way the Page's own fts is filtered to >= BASELINE_DATE (so the calendar/chart show the real June-October window, not the export's full noisy ~365-day span). Month-click-to-expand isn't interactive yet - would need its own per-person callback wiring - so it always shows the most recent month. Impressions, Daily Engagement Rate, Audience Demographics, and Content Demographics now sit on one row (4 cards), and both demographics tables are capped to their top 5 rows by Percentage instead of showing everything. Logged two more real posts in post_details_tracker.py from screenshots (the July 9 and Oct 22 posts) - only one post left unlogged. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ea7aa06484
commit
60f75ba8b5
2 files changed
+78
-35
No files matched your search
@@ -2769,35 +2769,65 @@ def _team_mark_posts(fig, engagement_df, top_posts_df):
|
||||
return fig
|
||||
|
||||
|
||||
def _team_fts_like(followers_daily_df):
|
||||
"""Renamed/shaped to match the Page's own fts_df (Date/Total followers/Cumulative
|
||||
Followers), filtered to >= TEAM_BASELINE_DATE the same way app.load() filters the
|
||||
Page's own fts to >= BASELINE_DATE - so the shared calendar/mark_posts helpers can
|
||||
be reused directly, and the calendar/chart only show the post-baseline window the
|
||||
same way the Overview tab's own do, not the export's full ~365-day span."""
|
||||
baseline_ts = pd.Timestamp(team_exports.TEAM_BASELINE_DATE)
|
||||
df = followers_daily_df.rename(columns={"New followers": "Total followers"}).copy()
|
||||
df = df[df["Date"] >= baseline_ts].reset_index(drop=True)
|
||||
df["Cumulative Followers"] = team_exports.TEAM_BASELINE_FOLLOWERS + df["Total followers"].cumsum()
|
||||
return df
|
||||
|
||||
|
||||
def _team_posts_like(top_posts_df):
|
||||
"""Renamed to match what _month_day_tiles/mark_posts expect (Created date,
|
||||
Short title/Content Type) - Original/Repost stands in for Content Type since
|
||||
that's the one thing distinguishing posts in this export."""
|
||||
if top_posts_df.empty:
|
||||
return top_posts_df
|
||||
df = top_posts_df.rename(columns={"Post Publish Date": "Created date"}).copy()
|
||||
df["Content Type"] = df["Is Repost"].map({True: "Repost", False: "Original"}).fillna("Post")
|
||||
df["Short title"] = df["Content Type"]
|
||||
return df
|
||||
|
||||
|
||||
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
|
||||
"""Same month-box grid + day-tile row as the Overview/Followers tabs' "Follower
|
||||
Gains — Calendar", always showing the default (most recent) month's day tiles
|
||||
rather than needing a click - clicking a different month box would need its own
|
||||
per-person callback wiring (Dash callbacks are registered once at app-build time,
|
||||
not per dropdown selection), so month switching isn't interactive here yet."""
|
||||
fts_like = _team_fts_like(followers_daily_df)
|
||||
posts_like = _team_posts_like(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)
|
||||
default_month = _default_active_month(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))
|
||||
style=_month_box_style(monthly_net, months_with_data, max_gain, m,
|
||||
selected=(m == default_month)))
|
||||
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"}),
|
||||
html.Div(_month_day_tiles(fts_like, posts_like, year, default_month),
|
||||
style={"marginTop": "8px"}),
|
||||
calendar_legend(),
|
||||
])
|
||||
|
||||
|
||||
def _team_followers_figure(followers_daily_df):
|
||||
fig = px.bar(followers_daily_df, x="Date", y="New followers", template=TMPL)
|
||||
def _team_followers_figure(followers_daily_df, top_posts_df):
|
||||
fts_like = _team_fts_like(followers_daily_df)
|
||||
posts_like = _team_posts_like(top_posts_df)
|
||||
fig = px.bar(fts_like, x="Date", y="Total followers", template=TMPL,
|
||||
labels={"Total followers": "New followers"})
|
||||
fig.update_traces(marker_color=GREEN)
|
||||
return fig
|
||||
return _std(mark_posts(fig, fts_like, posts_like))
|
||||
|
||||
|
||||
def _team_impressions_figure(engagement_df, top_posts_df):
|
||||
@@ -2865,15 +2895,16 @@ def _team_top_posts_table(top_posts_df):
|
||||
], bordered=True, size="sm", style={"fontSize": "0.85rem"})
|
||||
|
||||
|
||||
def _team_demographics_table(demo_df):
|
||||
def _team_demographics_table(demo_df, top_n=5):
|
||||
if demo_df.empty:
|
||||
return html.Div("No data.", className="text-muted")
|
||||
top = demo_df.sort_values("Percentage", ascending=False).head(top_n)
|
||||
rows = [html.Tr([html.Td(r["Top Demographics"]), html.Td(r["Value"]), html.Td(f"{r['Percentage']:.0f}%")])
|
||||
for _, r in demo_df.iterrows()]
|
||||
for _, r in top.iterrows()]
|
||||
return dbc.Table([
|
||||
html.Thead(html.Tr([html.Th(c) for c in ["Category", "Value", "%"]])),
|
||||
html.Tbody(rows),
|
||||
], bordered=True, size="sm", style={"fontSize": "0.8rem"})
|
||||
], bordered=True, size="sm", style={"fontSize": "0.75rem"})
|
||||
|
||||
|
||||
def _build_person_dashboard(name, data):
|
||||
@@ -2913,29 +2944,26 @@ def _build_person_dashboard(name, data):
|
||||
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),
|
||||
G(_team_followers_figure(data["followers_daily"], data["top_posts"]), style={"height": "240px"})), md=4),
|
||||
], className="gx-3 mb-3")
|
||||
|
||||
charts_row = dbc.Row([
|
||||
dbc.Col(section("Impressions",
|
||||
G(_team_impressions_figure(data["engagement"], data["top_posts"]), style={"height": "220px"})), md=6),
|
||||
G(_team_impressions_figure(data["engagement"], data["top_posts"]), style={"height": "200px"})), md=3),
|
||||
dbc.Col(section("Daily Engagement Rate",
|
||||
G(_team_engagement_rate_figure(data["engagement"]), style={"height": "220px"})), md=6),
|
||||
G(_team_engagement_rate_figure(data["engagement"]), style={"height": "200px"})), md=3),
|
||||
dbc.Col(section("Audience Demographics — Top 5 (who sees it)",
|
||||
_team_demographics_table(data["audience_demographics"])), md=3),
|
||||
dbc.Col(section("Content Demographics — Top 5 (who engages)",
|
||||
_team_demographics_table(data["content_demographics"])), md=3),
|
||||
], className="gx-3 mb-3")
|
||||
|
||||
posts_row = dbc.Row([
|
||||
dbc.Col(section("Posts This Window — Originals vs Reposts",
|
||||
_team_top_posts_table(data["top_posts"])), md=12),
|
||||
], className="gx-3 mb-3")
|
||||
|
||||
demo_row = dbc.Row([
|
||||
dbc.Col(section("Audience Demographics (who sees it)",
|
||||
_team_demographics_table(data["audience_demographics"])), md=6),
|
||||
dbc.Col(section("Content Demographics (who engages)",
|
||||
_team_demographics_table(data["content_demographics"])), md=6),
|
||||
], className="gx-3")
|
||||
|
||||
return html.Div([stat_row, source_note, calendar_row, charts_row, posts_row, demo_row])
|
||||
return html.Div([stat_row, source_note, calendar_row, charts_row, posts_row])
|
||||
|
||||
|
||||
_TEAM_EMPTY_PLACEHOLDER = html.Div("No data yet", className="text-muted text-center",
|
||||
@@ -2970,20 +2998,17 @@ def _build_empty_person_dashboard(name):
|
||||
], className="gx-3 mb-3")
|
||||
|
||||
charts_row = dbc.Row([
|
||||
dbc.Col(section("Impressions", _TEAM_EMPTY_PLACEHOLDER), md=6),
|
||||
dbc.Col(section("Daily Engagement Rate", _TEAM_EMPTY_PLACEHOLDER), md=6),
|
||||
dbc.Col(section("Impressions", _TEAM_EMPTY_PLACEHOLDER), md=3),
|
||||
dbc.Col(section("Daily Engagement Rate", _TEAM_EMPTY_PLACEHOLDER), md=3),
|
||||
dbc.Col(section("Audience Demographics — Top 5 (who sees it)", _TEAM_EMPTY_PLACEHOLDER), md=3),
|
||||
dbc.Col(section("Content Demographics — Top 5 (who engages)", _TEAM_EMPTY_PLACEHOLDER), md=3),
|
||||
], className="gx-3 mb-3")
|
||||
|
||||
posts_row = dbc.Row([
|
||||
dbc.Col(section("Posts This Window — Originals vs Reposts", _TEAM_EMPTY_PLACEHOLDER), md=12),
|
||||
], className="gx-3 mb-3")
|
||||
|
||||
demo_row = dbc.Row([
|
||||
dbc.Col(section("Audience Demographics (who sees it)", _TEAM_EMPTY_PLACEHOLDER), md=6),
|
||||
dbc.Col(section("Content Demographics (who engages)", _TEAM_EMPTY_PLACEHOLDER), md=6),
|
||||
], className="gx-3")
|
||||
|
||||
return html.Div([stat_row, source_note, calendar_row, charts_row, posts_row, demo_row])
|
||||
return html.Div([stat_row, source_note, calendar_row, charts_row, posts_row])
|
||||
|
||||
|
||||
@app.callback(
|
||||
|
||||
@@ -33,6 +33,24 @@ POST_DETAILS = {
|
||||
"saves": 0,
|
||||
"sends": 0,
|
||||
},
|
||||
"https://www.linkedin.com/posts/mckinley-black-557816412_so-proud-to-be-on-the-team-ugcPost-7480961758367555584-CMnh": {
|
||||
"profile_viewers": 0,
|
||||
"followers_gained": 0,
|
||||
"reactions": 1,
|
||||
"comments": 0,
|
||||
"reposts": 0,
|
||||
"saves": 0,
|
||||
"sends": 0,
|
||||
},
|
||||
"https://www.linkedin.com/posts/mckinley-black-557816412_i-love-learning-and-its-incredible-for-share-7518995227668529152-vHD1": {
|
||||
"profile_viewers": 1,
|
||||
"followers_gained": 0,
|
||||
"reactions": 1,
|
||||
"comments": 0,
|
||||
"reposts": 0,
|
||||
"saves": 0,
|
||||
"sends": 0,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user