"""Stance heatmap | register heatmap | per-file justification distribution
— single composite, all three sharing the category y-axis."""
stance_long = []
stance_keys = ["directive_pct", "expository_pct",
"positive_evaluative_pct", "negative_evaluative_pct",
"dialogic_pct", "pronouns_2p_pct", "pronouns_1p_pct"]
register_keys = ["frozen_pct", "formal_pct", "consultative_pct", "casual_pct"]
for cat, b in by_category.items():
s = b["metrics"]["stance"]
for k in stance_keys:
stance_long.append({"category": cat, "metric": k.replace("_pct", ""),
"value": s[k], "kind": "stance"})
r = b["metrics"]["register"]
for k in register_keys:
stance_long.append({"category": cat, "metric": k.replace("_pct", ""),
"value": r[k], "kind": "register"})
sr_long_df = pd.DataFrame(stance_long)
CAT_HEIGHT = 280
y_cat = alt.Y("category:N", title=None, sort=cats)
heat_stance = (
alt.Chart(sr_long_df[sr_long_df["kind"] == "stance"])
.mark_rect()
.encode(
x=alt.X("metric:N",
sort=[k.replace("_pct", "") for k in stance_keys],
title=None,
axis=alt.Axis(labelAngle=-30)),
y=y_cat,
color=alt.Color("value:Q", scale=alt.Scale(scheme="magma", reverse=True),
title="stance %"),
tooltip=[alt.Tooltip("category:N"),
alt.Tooltip("metric:N"),
alt.Tooltip("value:Q", format=".3f")],
)
.properties(width=400, height=CAT_HEIGHT,
title="Stance × category (% of file tokens)")
)
heat_register = (
alt.Chart(sr_long_df[sr_long_df["kind"] == "register"])
.mark_rect()
.encode(
x=alt.X("metric:N",
sort=[k.replace("_pct", "") for k in register_keys],
title=None,
axis=alt.Axis(labelAngle=-30)),
y=alt.Y("category:N", title=None, sort=cats,
axis=alt.Axis(labels=False, ticks=False)),
color=alt.Color("value:Q", scale=alt.Scale(scheme="viridis"),
title="register %"),
tooltip=[alt.Tooltip("category:N"),
alt.Tooltip("metric:N"),
alt.Tooltip("value:Q", format=".3f")],
)
.properties(width=240, height=CAT_HEIGHT,
title="Register × category (% of file tokens)")
)
# Justification distribution rotated so category sits on Y too — same axis
# as the two heatmaps so the row reads horizontally per category.
just_box = (
alt.Chart(alt_df)
.mark_boxplot(extent="min-max", opacity=0.55, color="#4e79a7")
.encode(
y=alt.Y("category:N", title=None, sort=cats,
axis=alt.Axis(labels=False, ticks=False)),
x=alt.X("just_ratio:Q", title="justification ratio per file"),
)
.properties(width=320, height=CAT_HEIGHT,
title="Per-file justification ratio")
)
just_strip = (
alt.Chart(alt_df)
.mark_circle(size=35, opacity=0.45, color="#e15759")
.encode(
y=alt.Y("category:N", title=None, sort=cats,
axis=alt.Axis(labels=False, ticks=False)),
x=alt.X("just_ratio:Q"),
yOffset="jitter:Q",
tooltip=[alt.Tooltip("path:N"),
alt.Tooltip("category:N"),
alt.Tooltip("n_tokens:Q"),
alt.Tooltip("just_ratio:Q", format=".2f")],
)
.transform_calculate(jitter="random()-0.5")
)
just_layer = just_box + just_strip
stance_register_composite = alt.hconcat(heat_stance, heat_register, just_layer).resolve_scale(
color="independent"
).properties(
title=alt.TitleParams(
"Register profile per category — stance | register | justification",
subtitle=["All three share the category y-axis sort. "
"Read a row horizontally for one category's profile."],
anchor="start",
)
)
save_chart(stance_register_composite, "12-stance-register-heatmaps")