Kula Project — Impact & Farm Assessment
2024–2025 Fellowship Cohort · Baseline (0 months) vs. Exit (12 months)
2024–2025 IMPACT & FARM ASSESSMENT DASHBOARD
Comparing Kula fellows at program entry (0 months) and program exit (12 months)
html`<div class="kpi-strip">
${kpiChip(metrics.top_kpis.fellows, "Fellows (program-wide)")}
${kpiChip(metrics.top_kpis.households, "Households")}
${kpiChip(metrics.top_kpis.graduates, "Graduates")}
${kpiChip(metrics.top_kpis.graduating_households, "Graduating Households")}
${kpiChip(metrics.n_matched[region].impact, "Matched pairs — Impact (" + region + ")")}
${kpiChip(metrics.n_matched[region].farm, "Matched pairs — Farm (" + region + ")")}
</div>`function fmt(v, unit) {
if (v === null || v === undefined || Number.isNaN(v)) return "n/a";
if (unit === "%") return (v * 100).toFixed(1) + "%";
if (unit === "RWF") return Math.round(v).toLocaleString() + " RWF";
if (unit === "USD") return "$" + v.toFixed(2);
if (unit === "ha") return v.toFixed(2) + " ha";
if (unit === "kg") return v.toFixed(1) + " kg";
return (Math.round(v * 100) / 100).toLocaleString();
}function miniBarChart(m) {
const data = [
{period: "Baseline", value: m.baseline ?? 0},
{period: "Exit", value: m.exit ?? 0}
];
return Plot.plot({
width: 220,
height: 150,
marginLeft: 40,
marginBottom: 24,
x: {domain: ["Baseline", "Exit"], label: null},
y: {label: null, grid: true},
color: {domain: ["Baseline", "Exit"], range: [COLOR_BASE, COLOR_EXIT]},
marks: [
Plot.barY(data, {x: "period", y: "value", fill: "period", rx: 3}),
Plot.text(data, {x: "period", y: "value", text: d => fmt(d.value, m.unit), dy: -8, fontSize: 10, fontWeight: 600}),
Plot.ruleY([0])
]
});
}function metricCard(m) {
const up = (m.pct_change ?? 0) >= 0;
const sig = m.p_value !== null && m.p_value !== undefined && m.p_value < 0.05;
const chipColor = sig ? COLOR_SIG : COLOR_NS;
const upColor = up ? "#2E7D32" : "#B23A3A";
return html`<div class="metric-card">
<div class="metric-title">${m.label}</div>
${miniBarChart(m)}
<div class="metric-stats">
<div class="pct-change" style=${"color:" + upColor}>
${up ? "▲" : "▼"} ${m.pct_change === null ? "n/a" : (Math.abs(m.pct_change) * 100).toFixed(1) + "%"}
</div>
<div class="pvalue-chip" style=${"background:" + chipColor + "22;color:" + chipColor + ";border-color:" + chipColor}>
p ${m.p_value === null ? "n/a" : m.p_value.toFixed(3)} ${sigStars(m.p_value)}
</div>
</div>
<div class="metric-n">n = ${m.n}</div>
</div>`;
}function incomeBreakdownChart(rows) {
const long = rows.flatMap(r => [
{category: r.category, period: "Baseline", value: r.baseline},
{category: r.category, period: "Exit", value: r.exit}
]);
return html`<div class="section-block">
<div class="section-title">Income Mix by Source (RWF/month, adjusted)</div>
${Plot.plot({
width: 1080,
height: 260,
marginBottom: 60,
x: {label: null, tickRotate: -25},
y: {label: "RWF / month", grid: true},
fx: {label: null},
color: {domain: ["Baseline", "Exit"], range: [COLOR_BASE, COLOR_EXIT], legend: true},
marks: [
Plot.barY(long, {x: "period", y: "value", fill: "period", fx: "category", rx: 2}),
Plot.ruleY([0])
]
})}
</div>`;
}function reasonsBreakdownChart(rows) {
const long = rows.flatMap(r => [
{category: r.category, period: "Baseline", value: r.baseline},
{category: r.category, period: "Exit", value: r.exit}
]);
return html`<div class="section-block">
<div class="section-title">Reported Reasons for Harvest Shortfall (share of fellows)</div>
${Plot.plot({
width: 1080,
height: 260,
marginBottom: 60,
x: {label: null, tickRotate: -15},
y: {label: "Share of fellows", grid: true, percent: true},
fx: {label: null},
color: {domain: ["Baseline", "Exit"], range: [COLOR_BASE, COLOR_EXIT], legend: true},
marks: [
Plot.barY(long, {x: "period", y: "value", fill: "period", fx: "category", rx: 2}),
Plot.ruleY([0])
]
})}
</div>`;
}