Index: frontend/src/pages/Dashboard/pages/Training/TrainingTracking.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Training/TrainingTracking.jsx	(revision e9a0543bbb622a2b343d1f5fe3b2df7ddb6a0bd3)
+++ frontend/src/pages/Dashboard/pages/Training/TrainingTracking.jsx	(revision b3d5fed65cdab945e6c84835e2e04bc1814d9699)
@@ -2,5 +2,4 @@
 import { useNavigate } from "react-router-dom";
 import api from "../../../../api/axios";
-import graphPlaceholder from "../../../../assets/graph-placeholder.svg";
 
 import TrainingTrackingHeader from "./components/TrainingTrackingHeader.jsx";
@@ -103,5 +102,5 @@
         onAddSession={() => navigate("/dashboard/training/sessions/new")}
       />
-      <TrainingProgressCard graphSrc={graphPlaceholder} />
+      <TrainingProgressCard sessions={sessions} />
       <TrainingSessionsTable
         columns={columns}
Index: frontend/src/pages/Dashboard/pages/Training/components/TrainingProgressCard.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Training/components/TrainingProgressCard.jsx	(revision e9a0543bbb622a2b343d1f5fe3b2df7ddb6a0bd3)
+++ frontend/src/pages/Dashboard/pages/Training/components/TrainingProgressCard.jsx	(revision b3d5fed65cdab945e6c84835e2e04bc1814d9699)
@@ -1,5 +1,25 @@
-import React from "react";
+import React, { useMemo, useState } from "react";
 
-const TrainingProgressCard = ({ graphSrc }) => {
+import TimeRangeToggle from "../../../../../components/graphs/TimeRangeToggle.jsx";
+import PercentChangeAreaChart from "../../../../../components/graphs/PercentChangeAreaChart.jsx";
+import { percentChangeSeries, sumByTimeBucket } from "../../../../../utils/timeSeries.js";
+
+const TrainingProgressCard = ({ sessions }) => {
+  const [range, setRange] = useState("weekly");
+
+  const points = useMemo(() => {
+    const buckets = sumByTimeBucket(
+      sessions,
+      range,
+      (s) => s.date,
+      (s) => s.calories
+    );
+    return percentChangeSeries(buckets);
+  }, [range, sessions]);
+
+  const latest = points?.length ? points[points.length - 1] : null;
+  const latestPct = latest ? Number(latest.value ?? 0) : 0;
+  const latestBase = latest ? Number(latest.base ?? 0) : 0;
+
   return (
     <div className="card bg-base-200 border border-base-300">
@@ -7,12 +27,29 @@
         <div className="flex items-center justify-between gap-4">
           <h2 className="card-title">Progress</h2>
-          <span className="badge badge-ghost">Graph placeholder</span>
+          <div className="flex items-center gap-3">
+            <span className="badge badge-ghost">
+              {latestPct >= 0 ? "+" : ""}
+              {latestPct.toFixed(1)}% (last)
+            </span>
+            <TimeRangeToggle value={range} onChange={setRange} />
+          </div>
         </div>
-        <div className="mt-4 w-full overflow-hidden rounded-xl border border-base-300 bg-base-100">
-          <img
-            src={graphSrc}
-            alt="Training progress graph placeholder"
-            className="block h-65 w-full object-cover"
-          />
+
+        <div className="mt-4 w-full overflow-hidden rounded-xl border border-base-300 bg-base-100 p-2">
+          {points.length < 2 ? (
+            <div className="flex h-65 items-center justify-center text-sm opacity-70">
+              Add at least 2 sessions to see % change.
+            </div>
+          ) : (
+            <PercentChangeAreaChart
+              points={points}
+              granularity={range}
+              height={260}
+            />
+          )}
+        </div>
+
+        <div className="mt-2 text-xs opacity-70">
+          Showing percentage change in calories burned per {range} bucket. Latest bucket total: {Math.round(latestBase)}.
         </div>
       </div>
