Accelerated Foundation in Applied Causal InferenceFor Economists · 15 modules · 45 lessons

15.2 The Executive Dashboard: Next.js, Recharts and Vercel

about 2 hoursArchitecture: serving a causal model via FastAPI to a Next.js dashboard
Lesson 15.2 · Module 15
The Executive Dashboard: Next.js, Recharts and Vercel
Video coming soon. The written lesson and the Colab notebook are ready below.
Colab notebook (.ipynb)
By the end of this lesson you can
  • ✓Build a Next.js page that posts an applicant profile to the causal API
  • ✓Visualise expected impact against cost with a Recharts bar chart and a break-even line
  • ✓Configure the API address with an environment variable
  • ✓Deploy the frontend to Vercel and the API to a Python host
Lesson plan (120 minutes)
Written lesson

Who is the dashboard for?

A caseworker or a deputy minister will never read a DoubleML summary table. They need to enter a case, see the expected effect in dollars, compare it with the cost, and get a clear recommendation with the uncertainty stated in words. The dashboard is where the econometrics becomes decision intelligence.

Scaffold the frontend

In your Codespace (or locally, with Node.js 20+), from the repository root:

Terminal
npx create-next-app@latest frontend --typescript --tailwind --eslint --app --src-dir
cd frontend
npm install recharts

Create frontend/.env.local so the frontend knows where the API runs:

Terminal
NEXT_PUBLIC_CAUSAL_API_URL=http://localhost:8000
The why

Never hard-code the API address. Locally it is localhost:8000; in production it is your hosted API. One environment variable per environment keeps the same code working everywhere.

The intervention simulator component

TypeScript (React)
// frontend/src/components/InterventionSimulator.tsx
"use client";
import { useState } from "react";
import ImpactChart from "./ImpactChart";

type Profile = { poverty_index: number; years_education: number; unemployment_duration_months: number };
type Result = {
  expected_wage_lift_cad: number;
  subsidy_cost_cad: number;
  net_benefit_cad: number;
  recommend_intervention: boolean;
  average_effect_cad: number;
  model_version: string;
};

const API = process.env.NEXT_PUBLIC_CAUSAL_API_URL ?? "http://localhost:8000";

export default function InterventionSimulator() {
  const [profile, setProfile] = useState<Profile>({ poverty_index: 1.2, years_education: 10, unemployment_duration_months: 14 });
  const [result, setResult] = useState<Result | null>(null);
  const [error, setError] = useState<string | null>(null);

  async function evaluate() {
    setError(null);
    try {
      const res = await fetch(`${API}/predict_impact`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(profile),
      });
      if (!res.ok) throw new Error(`API returned ${res.status}`);
      setResult(await res.json());
    } catch (e) {
      setError(e instanceof Error ? e.message : "Request failed");
    }
  }

  const field = (key: keyof Profile, label: string, step = 1) => (
    <label className="flex flex-col gap-1 text-sm font-medium text-slate-700">
      {label}
      <input type="number" step={step} value={profile[key]}
        onChange={(e) => setProfile({ ...profile, [key]: Number(e.target.value) })}
        className="rounded border border-slate-300 p-2" />
    </label>
  );

  return (
    <div className="mx-auto max-w-2xl space-y-5 rounded-xl bg-white p-6 shadow">
      <h2 className="text-xl font-bold text-slate-800">Retraining subsidy: impact simulator</h2>
      <div className="grid gap-3 sm:grid-cols-3">
        {field("unemployment_duration_months", "Months unemployed")}
        {field("years_education", "Years of education")}
        {field("poverty_index", "Poverty index", 0.1)}
      </div>
      <button onClick={evaluate} className="w-full rounded bg-blue-600 py-2 text-white hover:bg-blue-700">
        Simulate causal impact
      </button>
      {error && <p className="text-sm text-red-600">{error}</p>}
      {result && (
        <div className={`rounded-lg border p-4 ${result.recommend_intervention ? "border-green-300 bg-green-50" : "border-amber-300 bg-amber-50"}`}>
          <p className="text-lg font-semibold">
            Expected wage gain: ${result.expected_wage_lift_cad.toLocaleString("en-CA")} per year
          </p>
          <p className="text-sm">
            {result.recommend_intervention ? "Recommend the subsidy" : "Do not recommend"}: net benefit
            ${result.net_benefit_cad.toLocaleString("en-CA")} after a cost of ${result.subsidy_cost_cad.toLocaleString("en-CA")}.
          </p>
          <p className="mt-1 text-xs text-slate-500">
            Average effect across all applicants: ${result.average_effect_cad.toLocaleString("en-CA")} · model {result.model_version}
          </p>
          <ImpactChart predictedLift={result.expected_wage_lift_cad} baseline={28000} policyCost={result.subsidy_cost_cad} />
        </div>
      )}
    </div>
  );
}

The impact chart

The chart compares the status quo (the counterfactual) with the intervention and draws a red line at the break-even point: baseline plus cost. A bar above the line pays for itself.

TypeScript (React)
// frontend/src/components/ImpactChart.tsx
"use client";
import { Bar, BarChart, CartesianGrid, Legend, ReferenceLine, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";

type Props = { predictedLift: number; baseline: number; policyCost: number };
const cad = (v: number) => new Intl.NumberFormat("en-CA", { style: "currency", currency: "CAD", maximumFractionDigits: 0 }).format(v);

export default function ImpactChart({ predictedLift, baseline, policyCost }: Props) {
  const data = [
    { scenario: "Status quo", outcome: baseline },
    { scenario: "With subsidy", outcome: baseline + predictedLift },
  ];
  return (
    <div className="mt-4 h-72 w-full">
      <ResponsiveContainer>
        <BarChart data={data} margin={{ top: 20, right: 20, left: 10, bottom: 5 }}>
          <CartesianGrid strokeDasharray="3 3" vertical={false} />
          <XAxis dataKey="scenario" />
          <YAxis tickFormatter={(v) => `$${Math.round(v / 1000)}k`} domain={[baseline * 0.9, "auto"]} />
          <Tooltip formatter={(v) => cad(Number(v))} />
          <Legend />
          <ReferenceLine y={baseline + policyCost} stroke="#ef4444" strokeDasharray="4 4" label="Break-even" />
          <Bar dataKey="outcome" name="Expected annual earnings" fill="#3b82f6" radius={[4, 4, 0, 0]} />
        </BarChart>
      </ResponsiveContainer>
    </div>
  );
}

Use it in the home page:

TypeScript (React)
// frontend/src/app/page.tsx
import InterventionSimulator from "@/components/InterventionSimulator";

export default function Home() {
  return (
    <main className="min-h-screen bg-slate-100 p-8">
      <InterventionSimulator />
    </main>
  );
}

Start both halves (two terminals) and try different profiles:

Terminal
uvicorn backend.app:app --reload --port 8000     # terminal 1, repository root
cd frontend && npm run dev                        # terminal 2, then open http://localhost:3000

Deploying both halves

Key takeaway

Frontend on Vercel

  1. Push the repository to GitHub.
  2. On vercel.com: Add New → Project, import the repository, set Root Directory to frontend.
  3. Add the environment variable NEXT_PUBLIC_CAUSAL_API_URL with your API's public URL. Deploy.
  4. Every push to main redeploys automatically.

API on a Python host (Render, Railway, Fly.io, Google Cloud Run…)

  1. Add backend/requirements.txt (fastapi, uvicorn, scikit-learn, pandas, joblib) with pinned versions.
  2. Start command: uvicorn backend.app:app --host 0.0.0.0 --port $PORT.
  3. In production, restrict CORS allow_origins to your Vercel domain instead of *.
Exercise

Extend the simulator with a coverage planner: a slider for the budget share (10–100%) that calls the /predict_batch endpoint from Lesson 15.1's exercise on a sample of applicants and plots total net benefit against coverage with a Recharts line chart.

Checklist and recap

  • My dashboard posts a profile to the API and shows the effect, cost, net benefit and recommendation.
  • The chart shows the counterfactual, the intervention and the break-even line.
  • The API address is an environment variable, and both halves are deployed.