Add higher standard display and trend indicators to school cards
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 57s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 57s
- Display RWM Higher % alongside RWM Expected % on school cards - Add trend indicators (up/down/stable arrows) showing year-over-year change - Backend calculates previous year's RWM for trend comparison - Trend appears on cards and in school detail modal Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -238,11 +238,23 @@ async def get_schools(
|
|||||||
latest_year = df.groupby("urn")["year"].max().reset_index()
|
latest_year = df.groupby("urn")["year"].max().reset_index()
|
||||||
df_latest = df.merge(latest_year, on=["urn", "year"])
|
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
|
# Include key result metrics for display on cards
|
||||||
location_cols = ["latitude", "longitude"]
|
location_cols = ["latitude", "longitude"]
|
||||||
result_cols = [
|
result_cols = [
|
||||||
"year",
|
"year",
|
||||||
"rwm_expected_pct",
|
"rwm_expected_pct",
|
||||||
|
"rwm_high_pct",
|
||||||
|
"prev_rwm_expected_pct",
|
||||||
"reading_expected_pct",
|
"reading_expected_pct",
|
||||||
"writing_expected_pct",
|
"writing_expected_pct",
|
||||||
"maths_expected_pct",
|
"maths_expected_pct",
|
||||||
|
|||||||
@@ -519,9 +519,16 @@ function renderFeaturedSchools(schools) {
|
|||||||
${ageRange ? `<div class="school-details">${ageRange}</div>` : ""}
|
${ageRange ? `<div class="school-details">${ageRange}</div>` : ""}
|
||||||
<div class="school-stats">
|
<div class="school-stats">
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<div class="stat-value">${formatMetricValue(school.rwm_expected_pct, "rwm_expected_pct")}</div>
|
<div class="stat-value">
|
||||||
|
${formatMetricValue(school.rwm_expected_pct, "rwm_expected_pct")}
|
||||||
|
${getTrendIndicator(school.rwm_expected_pct, school.prev_rwm_expected_pct)}
|
||||||
|
</div>
|
||||||
<div class="stat-label">RWM Expected</div>
|
<div class="stat-label">RWM Expected</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-value">${formatMetricValue(school.rwm_high_pct, "rwm_high_pct")}</div>
|
||||||
|
<div class="stat-label">RWM Higher</div>
|
||||||
|
</div>
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<div class="stat-value">${school.total_pupils || "-"}</div>
|
<div class="stat-value">${school.total_pupils || "-"}</div>
|
||||||
<div class="stat-label">Pupils</div>
|
<div class="stat-label">Pupils</div>
|
||||||
@@ -617,6 +624,28 @@ function formatMetricValue(value, metric) {
|
|||||||
return value.toFixed(1);
|
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 `<span class="trend-indicator trend-up" title="Up ${diff.toFixed(0)}% from last year">▲</span>`;
|
||||||
|
} else if (diff <= -threshold) {
|
||||||
|
return `<span class="trend-indicator trend-down" title="Down ${Math.abs(diff).toFixed(0)}% from last year">▼</span>`;
|
||||||
|
} else {
|
||||||
|
return `<span class="trend-indicator trend-stable" title="Stable (${diff >= 0 ? '+' : ''}${diff.toFixed(0)}%)">▬</span>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// MAP FUNCTIONS
|
// MAP FUNCTIONS
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -806,9 +835,16 @@ function renderSchools(schools) {
|
|||||||
${ageRange ? `<div class="school-details">${ageRange}</div>` : ""}
|
${ageRange ? `<div class="school-details">${ageRange}</div>` : ""}
|
||||||
<div class="school-stats">
|
<div class="school-stats">
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<div class="stat-value">${formatMetricValue(school.rwm_expected_pct, "rwm_expected_pct")}</div>
|
<div class="stat-value">
|
||||||
|
${formatMetricValue(school.rwm_expected_pct, "rwm_expected_pct")}
|
||||||
|
${getTrendIndicator(school.rwm_expected_pct, school.prev_rwm_expected_pct)}
|
||||||
|
</div>
|
||||||
<div class="stat-label">RWM Expected</div>
|
<div class="stat-label">RWM Expected</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-value">${formatMetricValue(school.rwm_high_pct, "rwm_high_pct")}</div>
|
||||||
|
<div class="stat-label">RWM Higher</div>
|
||||||
|
</div>
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<div class="stat-value">${school.total_pupils || "-"}</div>
|
<div class="stat-value">${school.total_pupils || "-"}</div>
|
||||||
<div class="stat-label">Pupils</div>
|
<div class="stat-label">Pupils</div>
|
||||||
@@ -1212,12 +1248,17 @@ async function openSchoolModal(urn) {
|
|||||||
const latest =
|
const latest =
|
||||||
sortedData.find((d) => d.rwm_expected_pct !== null) || sortedData[0];
|
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 = `
|
elements.modalStats.innerHTML = `
|
||||||
<div class="modal-stats-section">
|
<div class="modal-stats-section">
|
||||||
<h4>KS2 Results (${latest.year})</h4>
|
<h4>KS2 Results (${latest.year})</h4>
|
||||||
<div class="modal-stats-grid">
|
<div class="modal-stats-grid">
|
||||||
<div class="modal-stat">
|
<div class="modal-stat">
|
||||||
<div class="modal-stat-value">${formatMetricValue(latest.rwm_expected_pct, "rwm_expected_pct")}</div>
|
<div class="modal-stat-value">${formatMetricValue(latest.rwm_expected_pct, "rwm_expected_pct")} ${getTrendIndicator(latest.rwm_expected_pct, prevRwm)}</div>
|
||||||
<div class="modal-stat-label">RWM Expected</div>
|
<div class="modal-stat-label">RWM Expected</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-stat">
|
<div class="modal-stat">
|
||||||
|
|||||||
@@ -536,6 +536,10 @@ body {
|
|||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-value.positive {
|
.stat-value.positive {
|
||||||
@@ -546,6 +550,25 @@ body {
|
|||||||
color: var(--accent-coral);
|
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 {
|
.stat-label {
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
|
|||||||
Reference in New Issue
Block a user