diff --git a/backend/app.py b/backend/app.py
index a41617c..2a2e043 100644
--- a/backend/app.py
+++ b/backend/app.py
@@ -238,11 +238,23 @@ async def get_schools(
latest_year = df.groupby("urn")["year"].max().reset_index()
df_latest = df.merge(latest_year, on=["urn", "year"])
+ # Calculate trend by comparing to previous year
+ # Get second-latest year for each school
+ df_sorted = df.sort_values(["urn", "year"], ascending=[True, False])
+ df_prev = df_sorted.groupby("urn").nth(1).reset_index()
+ if not df_prev.empty and "rwm_expected_pct" in df_prev.columns:
+ prev_rwm = df_prev[["urn", "rwm_expected_pct"]].rename(
+ columns={"rwm_expected_pct": "prev_rwm_expected_pct"}
+ )
+ df_latest = df_latest.merge(prev_rwm, on="urn", how="left")
+
# Include key result metrics for display on cards
location_cols = ["latitude", "longitude"]
result_cols = [
"year",
"rwm_expected_pct",
+ "rwm_high_pct",
+ "prev_rwm_expected_pct",
"reading_expected_pct",
"writing_expected_pct",
"maths_expected_pct",
diff --git a/frontend/app.js b/frontend/app.js
index 01b30b2..2d2b5d6 100644
--- a/frontend/app.js
+++ b/frontend/app.js
@@ -519,9 +519,16 @@ function renderFeaturedSchools(schools) {
${ageRange ? `
${ageRange}
` : ""}
-
${formatMetricValue(school.rwm_expected_pct, "rwm_expected_pct")}
+
+ ${formatMetricValue(school.rwm_expected_pct, "rwm_expected_pct")}
+ ${getTrendIndicator(school.rwm_expected_pct, school.prev_rwm_expected_pct)}
+
RWM Expected
+
+
${formatMetricValue(school.rwm_high_pct, "rwm_high_pct")}
+
RWM Higher
+
${school.total_pupils || "-"}
Pupils
@@ -617,6 +624,28 @@ function formatMetricValue(value, metric) {
return value.toFixed(1);
}
+/**
+ * Calculate trend indicator based on current and previous year values
+ * Returns HTML for trend arrow with class
+ */
+function getTrendIndicator(current, previous) {
+ if (current === null || current === undefined ||
+ previous === null || previous === undefined) {
+ return "";
+ }
+
+ const diff = current - previous;
+ const threshold = 2; // Minimum % change to show trend
+
+ if (diff >= threshold) {
+ return `
▲`;
+ } else if (diff <= -threshold) {
+ return `
▼`;
+ } else {
+ return `
▬`;
+ }
+}
+
// =============================================================================
// MAP FUNCTIONS
// =============================================================================
@@ -806,9 +835,16 @@ function renderSchools(schools) {
${ageRange ? `
${ageRange}
` : ""}
-
${formatMetricValue(school.rwm_expected_pct, "rwm_expected_pct")}
+
+ ${formatMetricValue(school.rwm_expected_pct, "rwm_expected_pct")}
+ ${getTrendIndicator(school.rwm_expected_pct, school.prev_rwm_expected_pct)}
+
RWM Expected
+
+
${formatMetricValue(school.rwm_high_pct, "rwm_high_pct")}
+
RWM Higher
+
${school.total_pupils || "-"}
Pupils
@@ -1212,12 +1248,17 @@ async function openSchoolModal(urn) {
const latest =
sortedData.find((d) => d.rwm_expected_pct !== null) || sortedData[0];
+ // Get previous year for trend calculation
+ const latestIndex = sortedData.indexOf(latest);
+ const previous = sortedData[latestIndex + 1] || null;
+ const prevRwm = previous?.rwm_expected_pct;
+
elements.modalStats.innerHTML = `
KS2 Results (${latest.year})
-
${formatMetricValue(latest.rwm_expected_pct, "rwm_expected_pct")}
+
${formatMetricValue(latest.rwm_expected_pct, "rwm_expected_pct")} ${getTrendIndicator(latest.rwm_expected_pct, prevRwm)}
RWM Expected
diff --git a/frontend/styles.css b/frontend/styles.css
index cf0a913..3e0d700 100644
--- a/frontend/styles.css
+++ b/frontend/styles.css
@@ -536,6 +536,10 @@ body {
font-size: 1.25rem;
font-weight: 700;
color: var(--text-primary);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.25rem;
}
.stat-value.positive {
@@ -546,6 +550,25 @@ body {
color: var(--accent-coral);
}
+/* Trend indicators */
+.trend-indicator {
+ font-size: 0.75rem;
+ cursor: help;
+}
+
+.trend-up {
+ color: var(--accent-teal);
+}
+
+.trend-down {
+ color: var(--accent-coral);
+}
+
+.trend-stable {
+ color: var(--text-muted);
+ font-size: 0.6rem;
+}
+
.stat-label {
font-size: 0.7rem;
color: var(--text-muted);