{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "63f43af5",
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "import seaborn as sns\n",
    "import matplotlib.pyplot as plt\n",
    "import numpy as np\n",
    "import os"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "97da3d1c",
   "metadata": {},
   "outputs": [],
   "source": [
    "base_dir = os.path.dirname(os.path.abspath('.'))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "b0ee2af1",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Smoothed columns created:\n",
      "['VO2(ml/min)_smoothed', 'VCO2(ml/min)_smoothed', 'HR(bpm)_smoothed', 'VT(l)_smoothed', 'BF(bpm)_smoothed', 'VE(l/min)_smoothed', 'VO2 Pulse_smoothed', 'VO2 Breath_smoothed', 'CHO_smoothed', 'FAT_smoothed']\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/tmp/ipykernel_103354/3076306744.py:3: FutureWarning: errors='ignore' is deprecated and will raise in a future version. Use to_numeric without passing `errors` and catch exceptions explicitly instead\n",
      "  df = df.apply(pd.to_numeric, errors='ignore')\n"
     ]
    }
   ],
   "source": [
    "df = pd.read_csv(f'{base_dir}/data/Pnoe_20250729_1550-Moran_Keirstyn.csv', delimiter=';')\n",
    "# Convert all columns to numeric where possible, coercing errors to NaN\n",
    "df = df.apply(pd.to_numeric, errors='ignore')\n",
    "df['VO2 Pulse'] = df['VO2(ml/min)'] / df['HR(bpm)']  # VO2 Pulse in mL/beat\n",
    "df['VO2 Breath'] = df['VO2(ml/min)'] / df['BF(bpm)']  # VO2 per Breath in mL/breath\n",
    "df['CHO'] = df['EE(kcal/min)'] * df['CARBS(%)']/100\n",
    "df['FAT'] = df['EE(kcal/min)'] * df['FAT(%)']/100\n",
    "# Smooth key columns using rolling window\n",
    "window_size = 10\n",
    "\n",
    "# List of columns to smooth\n",
    "columns_to_smooth = ['VO2(ml/min)', 'VCO2(ml/min)', 'HR(bpm)', 'VT(l)', 'BF(bpm)', 'VE(l/min)', 'VO2 Pulse', 'VO2 Breath', 'CHO', 'FAT']\n",
    "\n",
    "# Apply smoothing to each column\n",
    "for col in columns_to_smooth:\n",
    "    if col in df.columns:\n",
    "        df[f'{col}_smoothed'] = df[col].rolling(window=window_size, min_periods=1).mean()\n",
    "\n",
    "print(\"Smoothed columns created:\")\n",
    "print([col for col in df.columns if '_smoothed' in col])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "99116a35",
   "metadata": {},
   "outputs": [],
   "source": [
    "df_2 = pd.read_excel(f'{base_dir}/data/SECA body comp for all patients.xlsx')\n",
    "spirometry_data = pd.read_csv(f'{base_dir}/data/spirometry_data.csv')\n",
    "oxygenation = pd.read_csv(f'{base_dir}/data/Keirstyn Train Red NIRS Muscle Oxygen.csv')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "fbd292c3",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "22.369999999999997\n"
     ]
    }
   ],
   "source": [
    "print(df['VO2 Pulse'].max())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "4c439b2c",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 47704 characters]",
      "text/plain": [
       "<Figure size 800x800 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Filter df_2 for Keirstyn Moran\n",
    "keirstyn_data = df_2[df_2['LastName'].str.contains('Moran', case=False, na=False)]\n",
    "# Get the fat mass percentage for Keirstyn\n",
    "fat_percentage = keirstyn_data['Adult_FMP'].iloc[0]\n",
    "weight_kg = keirstyn_data['Weight'].iloc[0]\n",
    "age = keirstyn_data['Age'].iloc[0]\n",
    "gender = keirstyn_data['Gender'].iloc[0]\n",
    "lean_percentage = 100 - fat_percentage\n",
    "\n",
    "# Create donut chart\n",
    "fat_mass_lbs = 27.6\n",
    "lean_mass_lbs = 95.4\n",
    "\n",
    "# Calculate percentages from the provided weights\n",
    "total_weight = fat_mass_lbs + lean_mass_lbs\n",
    "fat_percentage = (fat_mass_lbs / total_weight) * 100\n",
    "lean_percentage = (lean_mass_lbs / total_weight) * 100\n",
    "\n",
    "# Data for the chart\n",
    "sizes = [fat_percentage, lean_percentage]\n",
    "colors = ['#fde3ac', '#ff9966'] # Light yellow/tan and orange from the image\n",
    "\n",
    "plt.figure(figsize=(8, 8))\n",
    "\n",
    "# Create the donut chart without labels first\n",
    "wedges, texts, autotexts = plt.pie(sizes,\n",
    "                                   autopct='',  # Remove auto percentages\n",
    "                                   startangle=90,\n",
    "                                   wedgeprops=dict(width=0.5, edgecolor='w'),\n",
    "                                   colors=colors,\n",
    "                                   labels=['', ''])  # Remove default labels\n",
    "\n",
    "# Add custom text annotations positioned manually\n",
    "plt.text(-1, 1, 'Fat Mass (27.6lbs)\\n22.4%', \n",
    "         fontsize=14, fontweight='bold', ha='center', va='center',\n",
    "         bbox=dict(boxstyle=\"round,pad=0.3\", facecolor='white', alpha=0.8))\n",
    "\n",
    "plt.text(1, -1, 'Lean Mass (95.4lbs)\\n77.6%', \n",
    "         fontsize=14, fontweight='bold', ha='center', va='center',\n",
    "         bbox=dict(boxstyle=\"round,pad=0.3\", facecolor='white', alpha=0.8))\n",
    "\n",
    "# Set the title\n",
    "plt.axis('equal') # Equal aspect ratio ensures that pie is drawn as a circle\n",
    "plt.savefig(f'{base_dir}/graphs/body_composition_chart.png', bbox_inches='tight', dpi=600)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "a565f1b3",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 11228 characters]",
      "text/plain": [
       "<Figure size 1000x200 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "import seaborn as sns\n",
    "\n",
    "# Set a common style\n",
    "sns.set_theme(style=\"whitegrid\")\n",
    "\n",
    "# Define the segments with muted colors\n",
    "segments = [\n",
    "    ('#F8A8A8', 0, 15),     # Muted Red/Salmon: 0% to 15%\n",
    "    ('#FFEECC', 15, 5),     # Pale Yellow/Cream: 15% to 20%\n",
    "    ('#D0F0C0', 20, 15),    # Pale Green/Mint: 20% to 35%\n",
    "    ('#FFEECC', 35, 5),     # Pale Yellow/Cream: 35% to 40%\n",
    "    ('#F8A8A8', 40, 10)     # Muted Red/Salmon: 40% to 50%\n",
    "]\n",
    "\n",
    "target_value = 22.4\n",
    "demographic = \"20-39\\n(F)\"\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(10, 2))\n",
    "\n",
    "# Create the Segmented Bar\n",
    "for color, start, length in segments:\n",
    "    ax.barh(y=0, width=length, left=start, height=1, color=color, edgecolor='black', linewidth=0.5)\n",
    "\n",
    "# Add the Indicator (Triangle)\n",
    "ax.plot(target_value, 1.05, marker='v', color='black', markersize=10, clip_on=False, transform=ax.get_xaxis_transform())\n",
    "\n",
    "# Set Axis Properties and Labels\n",
    "ax.set_xlim(0, 50)\n",
    "ax.set_xticks(range(0, 51, 5))\n",
    "ax.set_yticks([])\n",
    "ax.text(-0.05, 0, demographic, transform=ax.get_yaxis_transform(), va='center', ha='right', fontsize=12)\n",
    "\n",
    "ax.set_xlim(0, 50)\n",
    "ticks = range(0, 51, 5)\n",
    "ax.set_xticks(ticks)\n",
    "labels = [f\"{t}%\" for t in ticks]\n",
    "ax.set_xticklabels(labels)\n",
    "# Clean up spines and add small ticks\n",
    "ax.spines['right'].set_visible(False)\n",
    "ax.spines['top'].set_visible(False)\n",
    "ax.spines['left'].set_visible(False)\n",
    "ax.spines['bottom'].set_visible(True)\n",
    "\n",
    "for x in range(0, 51, 5):\n",
    "    ax.plot([x, x], [-0.05, -0.01], color='black', transform=ax.get_xaxis_transform(), clip_on=False)\n",
    "\n",
    "plt.tight_layout()\n",
    "plt.savefig(f'{base_dir}/graphs/body_fat_percent_chart.png', bbox_inches='tight', dpi=300)\n",
    "plt.show() # This is where the file is saved and displayed above."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "470e871e",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Displaying Metabolism Chart...\n",
      "Estimated RMR from data: 1385 kcal/day\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 23816 characters]",
      "text/plain": [
       "<Figure size 1000x250 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "Displaying Fuel Source Chart...\n",
      "Resting phase fuel mix: Fats 32.9%, Carbs 67.1%\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 25884 characters]",
      "text/plain": [
       "<Figure size 1000x250 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "import matplotlib.patches as patches\n",
    "\n",
    "def plot_metabolism_chart():\n",
    "    \"\"\"\n",
    "    Generates and displays the 'Slow vs Fast Metabolism' chart.\n",
    "    \"\"\"\n",
    "    fig, ax = plt.subplots(figsize=(10, 2.5))\n",
    "\n",
    "    # --- Chart data and positions ---\n",
    "    categories = ['Very Slow', 'Slow', 'Average', 'Fast', 'Very Fast']\n",
    "    positions = [1500, 3000, 4500, 6000, 7500]\n",
    "    # Step 1: Filter resting phase (usually lowest VO2 or MET values)\n",
    "    rest_phase = df[df['MET'] <= 1.1]  # assuming <1.1 MET means rest\n",
    "\n",
    "    # Step 2: Compute resting metabolic rate\n",
    "    rmr = rest_phase['EE(kcal/day)'].mean()\n",
    "\n",
    "    print(f\"Estimated RMR from data: {rmr:.0f} kcal/day\")\n",
    "    kcal_value = rmr\n",
    "    # Position the indicator and highlight based on the kcal value\n",
    "    # For this example, we'll place it in the 'Very Slow' section.\n",
    "    indicator_pos = kcal_value\n",
    "    highlight_end = kcal_value\n",
    "\n",
    "    # --- Main Bar (Background) ---\n",
    "    # Create a rounded rectangle for the main bar\n",
    "    main_bar = patches.FancyBboxPatch(\n",
    "        (0, 0.4), 9000, 0.2,\n",
    "        boxstyle=\"round,pad=0,rounding_size=0.1\",\n",
    "        ec=\"none\", fc=\"#E0E0E0\",\n",
    "    )\n",
    "    ax.add_patch(main_bar)\n",
    "\n",
    "    # --- Highlighted Bar ---\n",
    "    # Create a rounded rectangle for the highlighted section\n",
    "    highlight_bar = patches.FancyBboxPatch(\n",
    "        (0, 0.4), highlight_end, 0.2,\n",
    "        boxstyle=\"round,pad=0,rounding_size=0.1\",\n",
    "        ec=\"none\", fc=\"#B2FFC8\",\n",
    "    )\n",
    "    ax.add_patch(highlight_bar)\n",
    "\n",
    "    # --- Text and Labels ---\n",
    "    # Add the kcal text inside the highlighted bar\n",
    "    ax.text(highlight_end / 2, 0.5, f'{kcal_value}kCals', \n",
    "            ha='center', va='center', color='#006400', fontsize=14, weight='bold')\n",
    "\n",
    "    # --- Indicator Triangle ---\n",
    "    ax.plot(indicator_pos, 0.65, 'v', markersize=15, color='#606060', clip_on=False)\n",
    "\n",
    "    # --- Ticks and Labels ---\n",
    "    # Add category labels and tick marks below the bar\n",
    "    for pos, label in zip(positions, categories):\n",
    "        ax.text(pos, 0.15, label, ha='center', va='center', fontsize=12, color='#333333')\n",
    "        ax.plot([pos, pos], [0.35, 0.39], color='grey', lw=5)\n",
    "\n",
    "\n",
    "    # --- Chart Styling ---\n",
    "    ax.set_title('Slow vs Fast Metabolism', fontsize=18, weight='bold', loc='left')\n",
    "    ax.set_xlim(0, 9000)  # Add padding before and after\n",
    "    ax.set_ylim(0, 1)\n",
    "    \n",
    "    # Hide axes and spines for a cleaner look\n",
    "    ax.axis('off')\n",
    "    \n",
    "    plt.tight_layout()\n",
    "    plt.savefig(f'{base_dir}/graphs/metabolism_chart.png', bbox_inches='tight', dpi=300)\n",
    "    plt.show()\n",
    "\n",
    "\n",
    "def plot_fuel_source_chart():\n",
    "    \"\"\"\n",
    "    Generates and displays the 'Fuel Source' chart.\n",
    "    \"\"\"\n",
    "    fig, ax = plt.subplots(figsize=(10, 2.5))\n",
    "\n",
    "    # --- Chart data and positions ---\n",
    "    rest_phase = df[df['RER'] == 0.9]  # filter rest data\n",
    "    fat_rest = rest_phase['FAT(%)'].mean()\n",
    "    carb_rest = rest_phase['CARBS(%)'].mean()\n",
    "\n",
    "    print(f\"Resting phase fuel mix: Fats {fat_rest:.1f}%, Carbs {carb_rest:.1f}%\")\n",
    "\n",
    "    fat_percentage = fat_rest\n",
    "    carb_percentage = 100 - fat_percentage\n",
    "    optimal_point = 75\n",
    "\n",
    "    # --- Main Bars (Fats and Carbs) ---\n",
    "    # Fats bar (yellow)\n",
    "    fats_bar = patches.FancyBboxPatch(\n",
    "        (0, 0.4), fat_percentage, 0.2,\n",
    "        boxstyle=\"round,pad=0,rounding_size=0.1\",\n",
    "        ec=\"none\", fc=\"#FEEAAB\",\n",
    "    )\n",
    "    ax.add_patch(fats_bar)\n",
    "\n",
    "    # Carbs bar (blue) - starts where the fats bar ends\n",
    "    carbs_bar = patches.FancyBboxPatch(\n",
    "        (fat_percentage, 0.4), carb_percentage, 0.2,\n",
    "        boxstyle=\"round,pad=0,rounding_size=0.1\",\n",
    "        ec=\"none\", fc=\"#A7F5FF\",\n",
    "    )\n",
    "    ax.add_patch(carbs_bar)\n",
    "\n",
    "    # --- Text and Labels ---\n",
    "    # Add percentage labels inside the bars\n",
    "    ax.text(fat_percentage / 2, 0.5, f'Fats\\n{fat_percentage}%', \n",
    "            ha='center', va='center', color='#333333', fontsize=12, weight='bold')\n",
    "    ax.text(fat_percentage + carb_percentage / 2, 0.5, f'Carbs\\n{100-fat_percentage}%', \n",
    "            ha='center', va='center', color='#333333', fontsize=12, weight='bold')\n",
    "            \n",
    "    # Add 'Optimal' label\n",
    "    ax.text(optimal_point, 0.75, 'Optimal', ha='center', va='center', fontsize=12)\n",
    "\n",
    "    # --- Indicator Triangle ---\n",
    "    ax.plot(fat_percentage, 0.65, 'v', markersize=15, color='#606060', clip_on=False)\n",
    "    \n",
    "    # --- Ticks and Labels ---\n",
    "    positions = [0, 25, 50, 75, 100]\n",
    "    for pos in positions:\n",
    "        ax.text(pos, 0.15, str(pos), ha='center', va='center', fontsize=12, color='#333333')\n",
    "        ax.plot([pos, pos], [0.35, 0.39], color='grey', lw=5)\n",
    "        \n",
    "    # Add a special tick for the 'Optimal' point\n",
    "    ax.plot([optimal_point, optimal_point], [0.6, 0.7], color='black', lw=2)\n",
    "\n",
    "    # --- Chart Styling ---\n",
    "    ax.set_title('Fuel Source', fontsize=18, weight='bold', loc='left')\n",
    "    # ax.set_xlim(-5, 105) # Add padding\n",
    "    ax.set_ylim(0, 1)\n",
    "\n",
    "    # Hide axes and spines\n",
    "    ax.axis('off')\n",
    "\n",
    "    plt.tight_layout()\n",
    "    plt.savefig(f'{base_dir}/graphs/fuel_source_chart.png', bbox_inches='tight', dpi=300)\n",
    "    plt.show()\n",
    "\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    # Call the functions to display each plot\n",
    "    print(\"Displaying Metabolism Chart...\")\n",
    "    plot_metabolism_chart()\n",
    "    \n",
    "    print(\"\\nDisplaying Fuel Source Chart...\")\n",
    "    plot_fuel_source_chart()\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "0ab6f0b0",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 41352 characters]",
      "text/plain": [
       "<Figure size 1150x360 with 4 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "import os\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "import matplotlib.pyplot as plt\n",
    "import matplotlib.transforms as mtransforms\n",
    "from matplotlib.patches import FancyBboxPatch\n",
    "# Ensure data is loaded\n",
    "try:\n",
    "    spirometry_df = spirometry_data.copy()\n",
    "except NameError:\n",
    "    spirometry_df = pd.read_csv('data/spirometry_data.csv')\n",
    "\n",
    "# Coerce numeric columns\n",
    "for col in ['Best', 'LLN', 'Pred.', '%Pred.', 'ZScore']:\n",
    "    if col in spirometry_df.columns:\n",
    "        spirometry_df[col] = pd.to_numeric(spirometry_df[col], errors='coerce')\n",
    "\n",
    "# Select rows of interest and prepare display values\n",
    "rows_map = {\n",
    "    'Lung Volume': 'FVC',\n",
    "    'Lung Power': 'FEV1',\n",
    "    'Power/Volume': 'FEV1/FVC%'\n",
    "}\n",
    "\n",
    "records = []\n",
    "for label, param in rows_map.items():\n",
    "    row = spirometry_df.loc[spirometry_df['Parameters'].str.strip() == param]\n",
    "    if row.empty:\n",
    "        continue\n",
    "    row = row.iloc[0]\n",
    "    records.append({\n",
    "        'label': label,\n",
    "        'param': param,\n",
    "        'best': row['Best'],\n",
    "        'pct': row['%Pred.'],\n",
    "        'z': row['ZScore']\n",
    "    })\n",
    "\n",
    "# Figure setup\n",
    "os.makedirs('graphs', exist_ok=True)\n",
    "fig, axes = plt.subplots(nrows=3, ncols=1, figsize=(11.5, 3.6), sharex=True,\n",
    "                        gridspec_kw={'hspace': 0.65})\n",
    "\n",
    "x_min, x_max = -5, 3\n",
    "# Segment colors: red -> orange -> yellow -> green\n",
    "segments = [\n",
    "    (-5, -4, '#f4a7a7'),   # red-ish\n",
    "    (-4, -3, '#f7c49a'),   # orange-ish\n",
    "    (-3, -1.7, '#f6e3a3'),   # yellow-ish\n",
    "    (-1.7,  3, '#c9f0cc'),   # green-ish\n",
    "]\n",
    "\n",
    "ticks = np.arange(x_min, x_max + 1, 1)\n",
    "labels = [str(i) for i in ticks]\n",
    "\n",
    "# Plot each row\n",
    "for ax, rec in zip(axes, records):\n",
    "    # Background segments\n",
    "    for a, b, color in segments:\n",
    "        ax.barh(0, width=b-a, left=a, height=0.6, color=color, edgecolor='none')\n",
    "\n",
    "    # LLN (-1) and Predicted (0) markers\n",
    "    # ax.axvline(-1, color='black', lw=1)\n",
    "    ax.axvline(0, color='black', lw=1)\n",
    "\n",
    "    # Z-score pointer (downward triangle) at top of each panel\n",
    "    if pd.notna(rec['z']):\n",
    "        trans = mtransforms.blended_transform_factory(ax.transData, ax.transAxes)\n",
    "        ax.plot(float(rec['z']), 1.2, marker='v', markersize=12, color='dimgray',\n",
    "                transform=trans, clip_on=False)\n",
    "\n",
    "    # Labels, ticks, and styling\n",
    "    ax.set_title(rec['label'], loc='left', fontsize=11, fontweight='bold', pad=2)\n",
    "    ax.set_xlim(x_min, x_max)\n",
    "    ax.set_yticks([])\n",
    "    ax.set_xticks(ticks)\n",
    "    ax.set_xticklabels(labels, fontsize=8)\n",
    "    ax.set_xlabel('')\n",
    "\n",
    "# Add x-axis label to the bottom axis\n",
    "# axes[-1].set_xlabel('Z-score', fontsize=10)\n",
    "\n",
    "# Top annotations\n",
    "axes[0].text(-1.7, 0.45, 'LLN', ha='center', va='bottom', fontsize=9)\n",
    "axes[0].text(0, 0.45, 'Predicted', ha='center', va='bottom', fontsize=9)\n",
    "\n",
    "# Right-side summary boxes\n",
    "fig.subplots_adjust(right=0.78)\n",
    "box_ax = fig.add_axes([0.805, 0.06, 0.18, 0.90])  # [left, bottom, width, height]\n",
    "box_ax.axis('off')\n",
    "\n",
    "# Helper to draw a pill-shaped text box\n",
    "\n",
    "def pill(ax, xy, text):\n",
    "    x, y = xy\n",
    "    # Draw rounded rectangle background\n",
    "    bbox = FancyBboxPatch((x-0.48, y-0.09), 0.96, 0.18,\n",
    "                          boxstyle='round,pad=0.02,rounding_size=0.08',\n",
    "                          ec='#dddddd', fc='#f3f3f3', linewidth=1.0)\n",
    "    ax.add_patch(bbox)\n",
    "    ax.text(x, y+0.025, text, ha='center', va='center', fontsize=11, fontweight='bold')\n",
    "    ax.text(x, y-0.055, 'of predicted', ha='center', va='center', fontsize=9, color='#555555')\n",
    "\n",
    "box_ax.set_xlim(0, 1)\n",
    "box_ax.set_ylim(0, 1)\n",
    "\n",
    "# Prepare display strings and positions (top to bottom)\n",
    "right_items = []\n",
    "for rec in records:\n",
    "    name = 'FVC' if rec['param'] == 'FVC' else ('FEV1' if rec['param'] == 'FEV1' else 'FEV1/FVC')\n",
    "    unit = 'L' if rec['param'] in ('FVC', 'FEV1') else '%'\n",
    "    value_fmt = f\"{rec['best']:.2f}{unit}\"\n",
    "    pct_fmt = f\"{rec['pct']:.1f}%\"\n",
    "    right_items.append((name, value_fmt, pct_fmt))\n",
    "\n",
    "# Sort to match image order on the right (FVC, FEV1, FEV1/FVC)\n",
    "order = ['FVC', 'FEV1', 'FEV1/FVC']\n",
    "right_items_sorted = [next(item for item in right_items if item[0] == k) for k in order]\n",
    "\n",
    "ys = [0.82, 0.48, 0.15]\n",
    "for (name, value_fmt, pct_fmt), y in zip(right_items_sorted, ys):\n",
    "    main_line = f\"{name}\\n{value_fmt} → {pct_fmt}\"\n",
    "    pill(box_ax, (0.5, y), main_line)\n",
    "\n",
    "plt.savefig(f'{base_dir}/graphs/spirometry_chart.png', dpi=300, bbox_inches='tight')\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "ef8bc7ac",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 77980 characters]",
      "text/plain": [
       "<Figure size 1800x500 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "first_unique_phase = df.drop_duplicates(subset='PHASE')\n",
    "phase_times = first_unique_phase['T(sec)'].tolist()\n",
    "\n",
    "plt.figure(figsize=(18, 5))\n",
    "ax1 = plt.subplot()\n",
    "\n",
    "# Plot VT with step-like appearance\n",
    "line1 = sns.lineplot(data=df, x='T(sec)', y='VT(l)_smoothed', label='VT (L)')\n",
    "ax1.set_xlabel('Time (sec)')\n",
    "ax1.set_ylabel('VT (L)')\n",
    "ax1.grid(True, alpha=0.1)\n",
    "ax1.set_ylim(0, min(8, df['VT(l)_smoothed'].max()))\n",
    "\n",
    "# Set x-axis limits to remove padding\n",
    "ax1.set_xlim(0, df['T(sec)'].max())\n",
    "\n",
    "# Plot speed as step function on secondary y-axis\n",
    "ax2 = ax1.twinx()\n",
    "ax1.set_xticks(np.arange(0, df['T(sec)'].max(), 200))\n",
    "line2 = sns.lineplot(data=df, x='T(sec)', y='Speed', color='green', ax=ax2, \n",
    "             drawstyle='steps-post', linewidth=2, label='Speed')\n",
    "ax2.set_ylabel('Speed')\n",
    "ax2.set_ylim(0, min(30, df['Speed'].max()) + 1)\n",
    "\n",
    "# Remove default legends first\n",
    "ax1.get_legend().remove()\n",
    "ax2.get_legend().remove()\n",
    "\n",
    "# Combine legends from both axes in the top left\n",
    "lines1, labels1 = ax1.get_legend_handles_labels()\n",
    "lines2, labels2 = ax2.get_legend_handles_labels()\n",
    "ax1.legend(lines1 + lines2, labels1 + labels2, loc='upper left')\n",
    "\n",
    "# Add colored background regions if you have phase information\n",
    "ax1.axvspan(0, phase_times[1], alpha=0.2, color='lightblue')\n",
    "ax1.axvspan(phase_times[1], phase_times[2], alpha=0.2, color='purple')\n",
    "ax1.axvspan(phase_times[2], phase_times[3], alpha=0.2, color='lightgreen')\n",
    "ax1.axvspan(phase_times[3], df['T(sec)'].max(), alpha=0.2, color='blue')\n",
    "\n",
    "plt.savefig(f'{base_dir}/graphs/respiratory.png', dpi=300, bbox_inches='tight')\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "06244aa2",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 111948 characters]",
      "text/plain": [
       "<Figure size 1500x800 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "\n",
    "# Group by speed and calculate mean for numeric columns only\n",
    "speed_groups = df.groupby('Speed').mean(numeric_only=True).round(1)\n",
    "\n",
    "# Drop the first and last row from speed_groups\n",
    "speed_groups = speed_groups.iloc[1:-1]\n",
    "\n",
    "# Filter data to only include speeds in the desired range\n",
    "filtered_data = speed_groups[(speed_groups.index >= 3.5) & (speed_groups.index <= 7.5)]\n",
    "\n",
    "# Create figure with specific size\n",
    "plt.figure(figsize=(15, 8))\n",
    "plt.style.use('default')\n",
    "\n",
    "# Create stage labels and positions\n",
    "stage_labels = [f'Stage {i}' for i in range(1, len(filtered_data) + 1)]\n",
    "x_positions = np.arange(len(filtered_data))\n",
    "\n",
    "# Calculate fat and carbs energy expenditure from percentages\n",
    "fat_ee = filtered_data['EE(kcal/min)'] * filtered_data['FAT(%)'] / 100\n",
    "carbs_ee = filtered_data['EE(kcal/min)'] * filtered_data['CARBS(%)'] / 100\n",
    "\n",
    "# Create the main axis for the stacked bars\n",
    "ax1 = plt.gca()\n",
    "\n",
    "# Create stacked bar chart with colors\n",
    "bars_fat = ax1.bar(x_positions, fat_ee, color='#1f77b4', alpha=0.8, width=0.6, label='Fat')\n",
    "bars_carbs = ax1.bar(x_positions, carbs_ee, bottom=fat_ee, color='#ff7f0e', alpha=0.8, width=0.6, label='Carbs')\n",
    "\n",
    "# Set labels and formatting for primary axis\n",
    "ax1.set_xlabel('', fontsize=12)\n",
    "ax1.set_ylabel('Fuel (kcal/min)', fontsize=12)\n",
    "ax1.set_ylim(0, 20)\n",
    "\n",
    "# Add individual values on each bar segment\n",
    "for i, (fat_val, carb_val, total_val) in enumerate(zip(fat_ee, carbs_ee, filtered_data['EE(kcal/min)'])):\n",
    "    if fat_val > 0.3:  # Fat value\n",
    "        ax1.text(i, fat_val/2, f'{fat_val:.1f}', ha='center', va='center',\n",
    "                 fontsize=9, fontweight='bold', color='white')\n",
    "    if carb_val > 0.3:  # Carbs value\n",
    "        ax1.text(i, fat_val + carb_val/2, f'{carb_val:.1f}', ha='center', va='center',\n",
    "                 fontsize=9, fontweight='bold', color='white')\n",
    "    # Total EE\n",
    "    ax1.text(i, total_val + 0.5, f'{total_val:.1f} kcal', ha='center', va='bottom',\n",
    "             fontsize=10, fontweight='bold', color='black')\n",
    "\n",
    "# Add speed labels below x-axis\n",
    "for i, speed in enumerate(filtered_data.index):\n",
    "    ax1.text(i, -1.5, f'{speed:.1f} mph', ha='center', va='top', fontsize=9)\n",
    "    ax1.text(i, -2.8, f'{speed*1.609:.1f} min/km', ha='center', va='top', fontsize=8, color='gray')\n",
    "\n",
    "# Create secondary y-axis for heart rate\n",
    "ax2 = ax1.twinx()\n",
    "\n",
    "# Plot heart rate line (no manual offset)\n",
    "hr_line = ax2.plot(x_positions, filtered_data['HR(bpm)'],\n",
    "                   marker='o', linewidth=3, markersize=8, color='red', label='Heart Rate')\n",
    "\n",
    "# Set heart rate axis formatting\n",
    "ax2.set_ylabel('Heart Rate (bpm)', fontsize=12, color='red')\n",
    "ax2.tick_params(axis='y', labelcolor='red')\n",
    "\n",
    "# Dynamically adjust HR axis to float above bars\n",
    "max_bar_height = max(filtered_data['EE(kcal/min)'])\n",
    "ax2.set_ylim(0, 220)  # ensures HR line is above bars\n",
    "\n",
    "\n",
    "# Add HR values above the points\n",
    "for i, hr in enumerate(filtered_data['HR(bpm)']):\n",
    "    ax2.text(i, hr + 10, f'{int(hr)}bpm', ha='center', va='bottom',\n",
    "             fontsize=10, fontweight='bold', color='red')\n",
    "\n",
    "# Set x-axis formatting\n",
    "ax1.set_xticks(x_positions)\n",
    "ax1.set_xticklabels(stage_labels, fontsize=11)\n",
    "\n",
    "# Add title\n",
    "plt.suptitle('Fuel Utilization Report - Institute of Science, Health and Performance',\n",
    "             fontsize=14, fontweight='bold', y=0.95)\n",
    "\n",
    "# Create legend\n",
    "lines1, labels1 = ax1.get_legend_handles_labels()\n",
    "lines2, labels2 = ax2.get_legend_handles_labels()\n",
    "ax1.legend(lines1 + lines2, labels1 + labels2, loc='upper left',\n",
    "           frameon=True, fancybox=True, shadow=True)\n",
    "\n",
    "# Add grid\n",
    "ax1.grid(True, alpha=0.3, linestyle='-', linewidth=0.5)\n",
    "ax1.set_axisbelow(True)\n",
    "\n",
    "# Adjust layout\n",
    "plt.tight_layout()\n",
    "plt.subplots_adjust(bottom=0.1, top=0.9)\n",
    "plt.savefig(f'{base_dir}/graphs/fuel_utilization_chart.png', dpi=300)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "8a1878a0",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 119668 characters]",
      "text/plain": [
       "<Figure size 1800x500 with 3 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "first_unique_phase = df.drop_duplicates(subset='PHASE')\n",
    "phase_times = first_unique_phase['T(sec)'].tolist()\n",
    "\n",
    "plt.figure(figsize=(18, 5))\n",
    "ax1 = plt.subplot()\n",
    "\n",
    "# Plot VO2 Pulse\n",
    "#\n",
    "line1 = sns.lineplot(data=df, x='T(sec)', y='VO2 Pulse_smoothed', label='VO2 Pulse (mL/beat)', color='blue')\n",
    "ax1.set_xlabel('Time (sec)')\n",
    "ax1.set_ylabel('VO2 Pulse (mL/beat)')\n",
    "# ax1.set_title('VO2 Pulse, Heart Rate, and Speed Over Time')\n",
    "ax1.set_ylim(0, df['VO2 Pulse_smoothed'].max())\n",
    "ax1.grid(True, alpha=0.1)\n",
    "# Set x-axis limits to remove padding\n",
    "ax1.set_xlim(0, df['T(sec)'].max())\n",
    "# Create second y-axis for heart rate\n",
    "#\n",
    "ax2 = ax1.twinx()\n",
    "line2 = sns.lineplot(data=df, x='T(sec)', y='HR(bpm)_smoothed', color='red', ax=ax2, \n",
    "                     linewidth=2, label='Heart Rate (bpm)')\n",
    "ax2.set_ylabel('Heart Rate (bpm)', color='red')\n",
    "ax2.tick_params(axis='y', labelcolor='red')\n",
    "ax2.set_ylim(60, df['HR(bpm)_smoothed'].max() + 1)\n",
    "\n",
    "# Create third y-axis for speed\n",
    "ax3 = ax1.twinx()\n",
    "ax3.spines['right'].set_position(('outward', 60))\n",
    "\n",
    "line3 = sns.lineplot(data=df, x='T(sec)', y='Speed', color='green', ax=ax3, \n",
    "                     drawstyle='steps-post', linewidth=2, label='Speed')\n",
    "ax3.set_ylabel('Speed', color='green')\n",
    "ax3.tick_params(axis='y', labelcolor='green')\n",
    "ax3.set_ylim(0, df['Speed'].max() + 1)\n",
    "\n",
    "ax1.set_xticks(np.arange(0, df['T(sec)'].max(), 200))\n",
    "\n",
    "# Remove default legends first\n",
    "if ax1.get_legend():\n",
    "    ax1.get_legend().remove()\n",
    "if ax2.get_legend():\n",
    "    ax2.get_legend().remove()\n",
    "if ax3.get_legend():\n",
    "    ax3.get_legend().remove()\n",
    "\n",
    "# Combine legends from all axes in the top left\n",
    "lines1, labels1 = ax1.get_legend_handles_labels()\n",
    "lines2, labels2 = ax2.get_legend_handles_labels()\n",
    "lines3, labels3 = ax3.get_legend_handles_labels()\n",
    "ax1.legend(lines1 + lines2 + lines3, labels1 + labels2 + labels3, loc='upper left')\n",
    "\n",
    "# Add colored background regions if you have phase information\n",
    "ax1.axvspan(0, phase_times[1], alpha=0.2, color='lightblue')\n",
    "ax1.axvspan(phase_times[1], phase_times[2], alpha=0.2, color='purple')\n",
    "ax1.axvspan(phase_times[2], phase_times[3], alpha=0.2, color='lightgreen')\n",
    "ax1.axvspan(phase_times[3], df['T(sec)'].max(), alpha=0.2, color='blue')\n",
    "\n",
    "plt.savefig(f'{base_dir}/graphs/vo2_pulse_chart.png', bbox_inches='tight', dpi=300)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "7361fb05",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 91940 characters]",
      "text/plain": [
       "<Figure size 1800x500 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "first_unique_phase = df.drop_duplicates(subset='PHASE')\n",
    "phase_times = first_unique_phase['T(sec)'].tolist()\n",
    "\n",
    "plt.figure(figsize=(18, 5))\n",
    "ax1 = plt.subplot()\n",
    "\n",
    "# Plot VT with step-like appearance\n",
    "line1 = sns.lineplot(data=df, x='T(sec)', y='VO2 Breath_smoothed', label='VO2 per Breath (mL/breath)')\n",
    "ax1.set_xlabel('Time (sec)')\n",
    "ax1.set_ylabel('VO2 per Breath (mL/breath)')\n",
    "# ax1.set_title('VO2 per Breath and Speed Over Time')\n",
    "ax1.set_ylim(0, df['VO2 Breath_smoothed'].max() + 1)\n",
    "ax1.grid(True, alpha=0.1)\n",
    "# Set x-axis limits to remove padding\n",
    "ax1.set_xlim(0, df['T(sec)'].max())\n",
    "\n",
    "# Plot speed as step function on secondary y-axis\n",
    "ax2 = ax1.twinx()\n",
    "ax1.set_xticks(np.arange(0, df['T(sec)'].max(), 200))\n",
    "line2 = sns.lineplot(data=df, x='T(sec)', y='Speed', color='green', ax=ax2, \n",
    "             drawstyle='steps-post', linewidth=2, label='Speed')\n",
    "ax2.set_ylim(0, df['Speed'].max() + 1)\n",
    "ax2.set_ylabel('Speed')\n",
    "\n",
    "# Remove default legends first\n",
    "ax1.get_legend().remove()\n",
    "ax2.get_legend().remove()\n",
    "\n",
    "# Combine legends from both axes in the top left\n",
    "lines1, labels1 = ax1.get_legend_handles_labels()\n",
    "lines2, labels2 = ax2.get_legend_handles_labels()\n",
    "ax1.legend(lines1 + lines2, labels1 + labels2, loc='upper left')\n",
    "\n",
    "# Add colored background regions if you have phase information\n",
    "ax1.axvspan(0, phase_times[1], alpha=0.2, color='lightblue')\n",
    "ax1.axvspan(phase_times[1], phase_times[2], alpha=0.2, color='purple')\n",
    "ax1.axvspan(phase_times[2], phase_times[3], alpha=0.2, color='lightgreen')\n",
    "ax1.axvspan(phase_times[3], df['T(sec)'].max(), alpha=0.2, color='blue')\n",
    "\n",
    "plt.savefig(f'{base_dir}/graphs/vo2_breath_chart.png', bbox_inches='tight', dpi=300)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "c89478ff",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 112436 characters]",
      "text/plain": [
       "<Figure size 1800x500 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "first_unique_phase = df.drop_duplicates(subset='PHASE')\n",
    "phase_times = first_unique_phase['T(sec)'].tolist()\n",
    "\n",
    "plt.figure(figsize=(18, 5))\n",
    "ax1 = plt.subplot()\n",
    "\n",
    "df['CHO']\n",
    "# Plot VT with step-like appearance\n",
    "line1 = sns.lineplot(data=df, x='T(sec)', y='CHO_smoothed', label='CHO (kcal/min)')\n",
    "ax1.set_xlabel('Time (sec)')\n",
    "ax1.set_ylabel('CHO (g/min)')\n",
    "# ax1.set_title('CHO and Speed Over Time')\n",
    "ax1.grid(True, alpha=0.1)\n",
    "# Set x-axis limits to remove padding\n",
    "ax1.set_xlim(0, df['T(sec)'].max())\n",
    "# Plot speed as step function on secondary y-axis\n",
    "ax2 = ax1.twinx()\n",
    "ax1.set_xticks(np.arange(0, df['T(sec)'].max(), 200))\n",
    "line2 = sns.lineplot(data=df, x='T(sec)', y='FAT_smoothed', color='green', ax=ax2, label='FAT (kcal/min)')\n",
    "ax2.set_ylabel('FAT (kcal/min)')\n",
    "\n",
    "ax2.set_ylim(0, 15)  # ensures HR line is above bars\n",
    "\n",
    "# Remove default legends first\n",
    "ax1.get_legend().remove()\n",
    "ax2.get_legend().remove()\n",
    "\n",
    "# Combine legends from both axes in the top left\n",
    "lines1, labels1 = ax1.get_legend_handles_labels()\n",
    "lines2, labels2 = ax2.get_legend_handles_labels()\n",
    "ax1.legend(lines1 + lines2, labels1 + labels2, loc='upper left')\n",
    "\n",
    "# Add colored background regions if you have phase information\n",
    "ax1.axvspan(0, phase_times[1], alpha=0.2, color='lightblue')\n",
    "ax1.axvspan(phase_times[1], phase_times[2], alpha=0.2, color='purple')\n",
    "ax1.axvspan(phase_times[2], phase_times[3], alpha=0.2, color='lightgreen')\n",
    "ax1.axvspan(phase_times[3], df['T(sec)'].max(), alpha=0.2, color='blue')\n",
    "\n",
    "plt.savefig(f'{base_dir}/graphs/fat_metabolism_chart.png', bbox_inches='tight', dpi=300)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "1db16040",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 158512 characters]",
      "text/plain": [
       "<Figure size 1800x500 with 3 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "first_unique_phase = df.drop_duplicates(subset='PHASE')\n",
    "phase_times = first_unique_phase['T(sec)'].tolist()\n",
    "\n",
    "plt.figure(figsize=(18, 5))\n",
    "ax1 = plt.subplot()\n",
    "\n",
    "# Plot VO2 Pulse\n",
    "line1 = sns.lineplot(data=df, x='T(sec)', y='VCO2(ml/min)_smoothed', label='VCO2 (ml/min)', color='blue')\n",
    "ax1.set_xlabel('Time (sec)')\n",
    "ax1.set_ylabel('VO2 Pulse (mL/beat)')\n",
    "# ax1.set_title('VO2 Pulse, Heart Rate, and Speed Over Time')\n",
    "ax1.set_ylim(0, df['VCO2(ml/min)'].max())\n",
    "ax1.grid(True, alpha=0.1)\n",
    "# Set x-axis limits to remove padding\n",
    "ax1.set_xlim(0, df['T(sec)'].max())\n",
    "\n",
    "# Create second y-axis for heart rate\n",
    "ax2 = ax1.twinx()\n",
    "line2 = sns.lineplot(data=df, x='T(sec)', y='HR(bpm)_smoothed', color='red', ax=ax2, \n",
    "                     linewidth=2, label='Heart Rate (bpm)')\n",
    "ax2.set_ylabel('Heart Rate (bpm)', color='red')\n",
    "ax2.set_ylim(df['HR(bpm)_smoothed'].min() - 1, df['HR(bpm)_smoothed'].max() + 1)\n",
    "ax2.tick_params(axis='y', labelcolor='red')\n",
    "\n",
    "# Create third y-axis for speed\n",
    "ax3 = ax1.twinx()\n",
    "ax3.spines['right'].set_position(('outward', 60))\n",
    "line3 = sns.lineplot(data=df, x='T(sec)', y='BF(bpm)_smoothed', color='green', ax=ax3, linewidth=2, label='BF (bpm)')\n",
    "ax3.set_ylabel('BF (bpm)', color='green')\n",
    "ax3.tick_params(axis='y', labelcolor='green')\n",
    "ax3.set_ylim(0, df['BF(bpm)_smoothed'].max() + 1)\n",
    "ax1.set_xticks(np.arange(0, df['T(sec)'].max(), 200))\n",
    "\n",
    "# Remove default legends first\n",
    "if ax1.get_legend():\n",
    "    ax1.get_legend().remove()\n",
    "if ax2.get_legend():\n",
    "    ax2.get_legend().remove()\n",
    "if ax3.get_legend():\n",
    "    ax3.get_legend().remove()\n",
    "\n",
    "# Combine legends from all axes in the top left\n",
    "lines1, labels1 = ax1.get_legend_handles_labels()\n",
    "lines2, labels2 = ax2.get_legend_handles_labels()\n",
    "lines3, labels3 = ax3.get_legend_handles_labels()\n",
    "ax1.legend(lines1 + lines2 + lines3, labels1 + labels2 + labels3, loc='upper left')\n",
    "\n",
    "# Add colored background regions if you have phase information\n",
    "ax1.axvspan(0, phase_times[1], alpha=0.2, color='lightblue')\n",
    "ax1.axvspan(phase_times[1], phase_times[2], alpha=0.2, color='purple')\n",
    "ax1.axvspan(phase_times[2], phase_times[3], alpha=0.2, color='lightgreen')\n",
    "ax1.axvspan(phase_times[3], df['T(sec)'].max(), alpha=0.2, color='blue')\n",
    "\n",
    "plt.savefig(f'{base_dir}/graphs/recovery_chart.png', bbox_inches='tight', dpi=300)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "c8ad6076",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 81868 characters]",
      "text/plain": [
       "<Figure size 1200x550 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Plot TSI (Left) and TSI2 (Right) with Black Slope (Trend) Lines per Stage\n",
    "from numpy.polynomial.polynomial import Polynomial\n",
    "\n",
    "plt.figure(figsize=(12, 5.5))\n",
    "\n",
    "# Plot TSI (Left Leg)\n",
    "plt.plot(\n",
    "    oxygenation['Time'], oxygenation['TSI'], \n",
    "    label='TSI (Left Leg)', color='steelblue', linewidth=2\n",
    ")\n",
    "\n",
    "# Plot TSI2 (Right Leg)\n",
    "plt.plot(\n",
    "    oxygenation['Time'], oxygenation['TSI-second'],\n",
    "    label='TSI2 (Right Leg)', color='orange', linewidth=2\n",
    ")\n",
    "\n",
    "# Define time intervals for stages (adjust these based on your test protocol)\n",
    "# Looking at the data range, we'll create intervals\n",
    "max_time = oxygenation['Time'].max()\n",
    "intervals = [\n",
    "    (0, 250),\n",
    "    (250, 500),\n",
    "    (500, 750),\n",
    "    (750, 1000),\n",
    "    (1000, 1250),\n",
    "    (1250, 1500),\n",
    "    (1500, max_time)\n",
    "]\n",
    "\n",
    "# Calculate and plot trend lines for each interval\n",
    "for start_time, end_time in intervals:\n",
    "    # Filter data for this interval\n",
    "    mask_interval = (oxygenation['Time'] >= start_time) & (oxygenation['Time'] <= end_time)\n",
    "    \n",
    "    # TSI (Left Leg) trend for this interval\n",
    "    mask_left = mask_interval & ~oxygenation['TSI'].isna()\n",
    "    if mask_left.sum() > 1:  # Need at least 2 points for a line\n",
    "        x_left = oxygenation.loc[mask_left, 'Time']\n",
    "        y_left = oxygenation.loc[mask_left, 'TSI']\n",
    "        coefs_left = Polynomial.fit(x_left, y_left, 1).convert().coef\n",
    "        trend_left = coefs_left[0] + coefs_left[1] * x_left\n",
    "        plt.plot(x_left, trend_left, color='black', linestyle='--', linewidth=2, alpha=0.8)\n",
    "    \n",
    "    # TSI-second (Right Leg) trend for this interval\n",
    "    mask_right = mask_interval & ~oxygenation['TSI-second'].isna()\n",
    "    if mask_right.sum() > 1:  # Need at least 2 points for a line\n",
    "        x_right = oxygenation.loc[mask_right, 'Time']\n",
    "        y_right = oxygenation.loc[mask_right, 'TSI-second']\n",
    "        coefs_right = Polynomial.fit(x_right, y_right, 1).convert().coef\n",
    "        trend_right = coefs_right[0] + coefs_right[1] * x_right\n",
    "        plt.plot(x_right, trend_right, color='black', linestyle='--', linewidth=2, alpha=0.8)\n",
    "\n",
    "plt.xlabel('Time (s)')\n",
    "plt.ylabel('TSI (%)')\n",
    "plt.title('TSI (Left) and TSI2 (Right) with Black Slope Lines per Stage')\n",
    "plt.legend(fontsize=10, loc='upper right')\n",
    "plt.tight_layout()\n",
    "plt.grid(alpha=0.25)\n",
    "# plt.savefig('graphs/tsi_comparison_with_trends.png', bbox_inches='tight', dpi=160)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "25327cc1",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Age: 45, Gender: male, VO2 Max: 35.5\n",
      "Age Range: 40-49, Category: Poor\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 34312 characters]",
      "text/plain": [
       "<Figure size 1400x300 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "import matplotlib.patches as patches\n",
    "from matplotlib.table import Table\n",
    "import numpy as np\n",
    "\n",
    "# VO2 Max Master Chart Data\n",
    "VO2_MASTER_CHART = {\n",
    "    'male': {\n",
    "        '20-29': {\n",
    "            'Very Poor': (29.0, 38.1),\n",
    "            'Poor': (38.1, 44.9),\n",
    "            'Fair': (44.9, 50.2),\n",
    "            'Good': (50.2, 61.8),\n",
    "            'Excellent': (57.1, 66.3),\n",
    "            'Superior': (66.3, None)  # None means open-ended\n",
    "        },\n",
    "        '30-39': {\n",
    "            'Very Poor': (27.2, 34.1),\n",
    "            'Poor': (34.1, 39.6),\n",
    "            'Fair': (39.6, 45.2),\n",
    "            'Good': (45.2, 51.6),\n",
    "            'Excellent': (51.6, 59.8),\n",
    "            'Superior': (59.8, None)\n",
    "        },\n",
    "        '40-49': {\n",
    "            'Very Poor': (24.2, 30.5),\n",
    "            'Poor': (30.5, 35.7),\n",
    "            'Fair': (35.7, 40.3),\n",
    "            'Good': (40.3, 46.7),\n",
    "            'Excellent': (46.7, 55.6),\n",
    "            'Superior': (55.6, None)\n",
    "        },\n",
    "        '50-59': {\n",
    "            'Very Poor': (20.9, 26.1),\n",
    "            'Poor': (26.1, 30.7),\n",
    "            'Fair': (30.7, 35.1),\n",
    "            'Good': (35.1, 41.2),\n",
    "            'Excellent': (41.2, 50.7),\n",
    "            'Superior': (50.7, None)\n",
    "        },\n",
    "        '60-69': {\n",
    "            'Very Poor': (17.4, 22.4),\n",
    "            'Poor': (22.4, 26.6),\n",
    "            'Fair': (26.6, 30.5),\n",
    "            'Good': (30.5, 36.1),\n",
    "            'Excellent': (36.1, 43.0),\n",
    "            'Superior': (43.0, None)\n",
    "        }\n",
    "    },\n",
    "    'female': {\n",
    "        '20-29': {\n",
    "            'Very Poor': (21.7, 28.6),\n",
    "            'Poor': (28.6, 34.6),\n",
    "            'Fair': (34.6, 40.6),\n",
    "            'Good': (40.6, 46.5),\n",
    "            'Excellent': (46.5, 56.0),\n",
    "            'Superior': (56.0, None)\n",
    "        },\n",
    "        '30-39': {\n",
    "            'Very Poor': (19.0, 24.1),\n",
    "            'Poor': (24.1, 28.2),\n",
    "            'Fair': (28.2, 32.2),\n",
    "            'Good': (32.2, 35.7),\n",
    "            'Excellent': (35.7, 45.8),\n",
    "            'Superior': (45.8, None)\n",
    "        },\n",
    "        '40-49': {\n",
    "            'Very Poor': (17.0, 21.3),\n",
    "            'Poor': (21.3, 24.9),\n",
    "            'Fair': (24.9, 28.7),\n",
    "            'Good': (28.7, 34.0),\n",
    "            'Excellent': (34.0, 41.7),\n",
    "            'Superior': (41.7, None)\n",
    "        },\n",
    "        '50-59': {\n",
    "            'Very Poor': (16.0, 19.1),\n",
    "            'Poor': (19.1, 24.4),\n",
    "            'Fair': (21.8, 27.6),\n",
    "            'Good': (25.2, 28.6),\n",
    "            'Excellent': (28.6, 35.9),\n",
    "            'Superior': (35.9, None)\n",
    "        },\n",
    "        '60-69': {\n",
    "            'Very Poor': (13.4, 16.5),\n",
    "            'Poor': (16.5, 18.9),\n",
    "            'Fair': (18.9, 21.2),\n",
    "            'Good': (21.2, 24.6),\n",
    "            'Excellent': (24.6, 29.4),\n",
    "            'Superior': (29.4, None)\n",
    "        }\n",
    "    }\n",
    "}\n",
    "\n",
    "def get_age_range(age):\n",
    "    \"\"\"Determine age range from age in years.\"\"\"\n",
    "    if 20 <= age <= 29:\n",
    "        return '20-29'\n",
    "    elif 30 <= age <= 39:\n",
    "        return '30-39'\n",
    "    elif 40 <= age <= 49:\n",
    "        return '40-49'\n",
    "    elif 50 <= age <= 59:\n",
    "        return '50-59'\n",
    "    elif 60 <= age <= 69:\n",
    "        return '60-69'\n",
    "    else:\n",
    "        # Default to closest range\n",
    "        if age < 20:\n",
    "            return '20-29'\n",
    "        elif age >= 70:\n",
    "            return '60-69'\n",
    "        else:\n",
    "            return '30-39'  # fallback\n",
    "\n",
    "def determine_category(vo2_max, age_range, gender):\n",
    "    \"\"\"Determine VO2 max category based on value, age, and gender.\"\"\"\n",
    "    gender_key = 'male' if gender.lower().startswith('m') else 'female'\n",
    "    ranges = VO2_MASTER_CHART[gender_key][age_range]\n",
    "    \n",
    "    categories = ['Very Poor', 'Poor', 'Fair', 'Good', 'Excellent', 'Superior']\n",
    "    \n",
    "    # Check Superior category first (open-ended)\n",
    "    min_val, max_val = ranges['Superior']\n",
    "    if max_val is None and vo2_max >= min_val:\n",
    "        print(f\"It is always superior\")\n",
    "        return 'Superior'\n",
    "    \n",
    "    # Check other categories from Excellent down to Very Poor\n",
    "    # Ranges are typically [min, max) - inclusive of min, exclusive of max\n",
    "    # But when value equals max, it belongs to the next category\n",
    "    for category in reversed(categories[:-1]):  # Exclude Superior as we already checked it\n",
    "        min_val, max_val = ranges[category]\n",
    "        # Check if value falls in this range (inclusive of min, exclusive of max)\n",
    "        if min_val <= vo2_max < max_val:\n",
    "            return category\n",
    "    \n",
    "    # If value is below all ranges, return Very Poor\n",
    "    # This handles the case where vo2_max < min of Very Poor\n",
    "    return 'Very Poor'\n",
    "\n",
    "def format_range(min_val, max_val):\n",
    "    \"\"\"Format range as string.\"\"\"\n",
    "    if max_val is None:\n",
    "        return f\"{min_val}+\"\n",
    "    else:\n",
    "        return f\"{min_val}-{max_val}\"\n",
    "\n",
    "def generate_vo2_max_table(age, gender, vo2_max_value, save_path=None):\n",
    "    \"\"\"\n",
    "    Generate VO2 Max table with indicator arrow.\n",
    "    \n",
    "    Args:\n",
    "        age: Patient age in years\n",
    "        gender: 'male' or 'female' (or 'm'/'f')\n",
    "        vo2_max_value: Patient's VO2 max value\n",
    "        save_path: Optional path to save the figure\n",
    "    \n",
    "    Returns:\n",
    "        fig, ax: matplotlib figure and axes\n",
    "    \"\"\"\n",
    "    # Determine age range and category\n",
    "    age_range = get_age_range(age)\n",
    "    category = determine_category(vo2_max_value, age_range, gender)\n",
    "    \n",
    "    # Debug: print the determined category\n",
    "    print(f\"Age: {age}, Gender: {gender}, VO2 Max: {vo2_max_value}\")\n",
    "    print(f\"Age Range: {age_range}, Category: {category}\")\n",
    "    \n",
    "    # Get the appropriate data\n",
    "    gender_key = 'male' if gender.lower().startswith('m') else 'female'\n",
    "    ranges = VO2_MASTER_CHART[gender_key][age_range]\n",
    "    \n",
    "    # Prepare table data\n",
    "    headers = ['Age', 'Very Poor', 'Poor', 'Fair', 'Good', 'Excellent', 'Superior']\n",
    "    age_label = f\"{age_range} ({gender[0].upper()})\"\n",
    "    \n",
    "    row_data = [age_label]\n",
    "    for cat in ['Very Poor', 'Poor', 'Fair', 'Good', 'Excellent', 'Superior']:\n",
    "        min_val, max_val = ranges[cat]\n",
    "        row_data.append(format_range(min_val, max_val))\n",
    "    \n",
    "    # Create figure\n",
    "    fig, ax = plt.subplots(figsize=(14, 3))\n",
    "    ax.axis('off')\n",
    "    \n",
    "    # Create table\n",
    "    table_data = [headers, row_data]\n",
    "    table = ax.table(cellText=table_data,\n",
    "                     cellLoc='center',\n",
    "                     loc='center',\n",
    "                     bbox=[0, 0, 1, 1])\n",
    "    \n",
    "    # Style the table\n",
    "    table.auto_set_font_size(False)\n",
    "    table.set_fontsize(11)\n",
    "    table.scale(1, 2.5)\n",
    "    \n",
    "    # Header row styling (cyan background)\n",
    "    for i in range(len(headers)):\n",
    "        cell = table[(0, i)]\n",
    "        cell.set_facecolor('#7dd3fc')  # cyan-300 equivalent\n",
    "        cell.set_text_props(weight='bold', color='black')\n",
    "        cell.set_edgecolor('#9ca3af')  # gray-400\n",
    "        cell.set_linewidth(1)\n",
    "    \n",
    "    # Find the column index for the category (before styling)\n",
    "    category_index = headers.index(category)\n",
    "    \n",
    "    # Data row styling\n",
    "    for i in range(len(row_data)):\n",
    "        cell = table[(1, i)]\n",
    "        if i == 0:  # Age column\n",
    "            cell.set_facecolor('#a5f3fc')  # cyan-200\n",
    "            cell.set_text_props(weight='semibold', color='black')\n",
    "        else:\n",
    "            cell.set_facecolor('#f3f4f6')  # gray-100\n",
    "            cell.set_text_props(color='black')\n",
    "            # Bold the cell that corresponds to the patient's category\n",
    "            if i == category_index:\n",
    "                cell.set_text_props(weight='bold', color='black')\n",
    "        cell.set_edgecolor('#9ca3af')  # gray-400\n",
    "        cell.set_linewidth(1)\n",
    "    \n",
    "    # Add arrow indicator below the category column\n",
    "    # Calculate position\n",
    "    cell_width = 1.0 / len(headers)\n",
    "    arrow_x = (category_index + 0.5) * cell_width\n",
    "    \n",
    "    # Draw arrow pointing up\n",
    "    arrow = patches.FancyArrowPatch(\n",
    "        (arrow_x, -0.15), (arrow_x, -0.05),\n",
    "        arrowstyle='->', mutation_scale=20, \n",
    "        linewidth=2, color='black',\n",
    "        transform=ax.transAxes\n",
    "    )\n",
    "    ax.add_patch(arrow)\n",
    "    \n",
    "    # Add triangle at the top\n",
    "    triangle = patches.RegularPolygon(\n",
    "        (arrow_x, -0.05), 3, radius=0.02,\n",
    "        orientation=np.pi/2, color='black',\n",
    "        transform=ax.transAxes\n",
    "    )\n",
    "    ax.add_patch(triangle)\n",
    "    \n",
    "    # Set title - calculate approximate percentile\n",
    "    # For Superior category, use 100th percentile as shown in template\n",
    "    if category == 'Superior':\n",
    "        percentile = '100th percentile'\n",
    "    else:\n",
    "        percentile_map = {\n",
    "            'Very Poor': '1st-10th percentile',\n",
    "            'Poor': '10th-20th percentile',\n",
    "            'Fair': '20th-40th percentile',\n",
    "            'Good': '40th-60th percentile',\n",
    "            'Excellent': '60th-80th percentile'\n",
    "        }\n",
    "        percentile = percentile_map.get(category, 'N/A')\n",
    "    \n",
    "    title = f\"VO2 Max - {vo2_max_value:.1f} ({percentile})\"\n",
    "    ax.set_title(title, fontsize=14, fontweight='bold', pad=20)\n",
    "    \n",
    "    plt.tight_layout()\n",
    "    \n",
    "    if save_path:\n",
    "        plt.savefig(save_path, dpi=300, bbox_inches='tight')\n",
    "    \n",
    "    return fig, ax\n",
    "\n",
    "# Test the function\n",
    "# Example: 30-year-old female with VO2 max of 49.5\n",
    "fig, ax = generate_vo2_max_table(\n",
    "    age=45,\n",
    "    gender='male',\n",
    "    vo2_max_value=35.5,\n",
    "    save_path=f'{base_dir}/graphs/vo2_max_table.png'\n",
    ")\n",
    "plt.show()\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "id": "c46b53f0",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Age: 50, Gender: male, Resting Heart Rate: 76\n",
      "Age Range: 46-55, Category: Average\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 37856 characters]",
      "text/plain": [
       "<Figure size 1600x300 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Resting Heart Rate Master Chart Data\n",
    "RHR_MASTER_CHART = {\n",
    "    'male': {\n",
    "        '18-25': {\n",
    "            'Poor': (85, None),  # 85bpm+\n",
    "            'Below Average': (79, 85),\n",
    "            'Average': (74, 79),\n",
    "            'Above Average': (70, 74),\n",
    "            'Good': (66, 70),\n",
    "            'Excellent': (61, 66),\n",
    "            'Athlete': (40, 61)\n",
    "        },\n",
    "        '26-35': {\n",
    "            'Poor': (83, None),  # 83bpm+\n",
    "            'Below Average': (77, 83),\n",
    "            'Average': (73, 77),\n",
    "            'Above Average': (69, 73),\n",
    "            'Good': (65, 69),\n",
    "            'Excellent': (60, 65),\n",
    "            'Athlete': (42, 60)\n",
    "        },\n",
    "        '36-45': {\n",
    "            'Poor': (85, None),  # 85bpm+\n",
    "            'Below Average': (79, 85),\n",
    "            'Average': (74, 79),\n",
    "            'Above Average': (70, 74),\n",
    "            'Good': (65, 70),\n",
    "            'Excellent': (60, 65),\n",
    "            'Athlete': (45, 60)\n",
    "        },\n",
    "        '46-55': {\n",
    "            'Poor': (84, None),  # 84bpm+\n",
    "            'Below Average': (78, 84),\n",
    "            'Average': (74, 78),\n",
    "            'Above Average': (70, 74),\n",
    "            'Good': (66, 70),\n",
    "            'Excellent': (61, 66),\n",
    "            'Athlete': (48, 61)\n",
    "        },\n",
    "        '56-65': {\n",
    "            'Poor': (84, None),  # 84bpm+\n",
    "            'Below Average': (78, 84),\n",
    "            'Average': (74, 78),\n",
    "            'Above Average': (70, 74),\n",
    "            'Good': (65, 70),\n",
    "            'Excellent': (60, 65),\n",
    "            'Athlete': (50, 60)\n",
    "        },\n",
    "        '65+': {\n",
    "            'Poor': (84, None),  # 84bpm+\n",
    "            'Below Average': (77, 84),\n",
    "            'Average': (73, 77),\n",
    "            'Above Average': (70, 73),\n",
    "            'Good': (65, 70),\n",
    "            'Excellent': (60, 65),\n",
    "            'Athlete': (52, 60)\n",
    "        }\n",
    "    },\n",
    "    'female': {\n",
    "        '18-25': {\n",
    "            'Poor': (82, None),  # 82bpm+\n",
    "            'Below Average': (74, 82),\n",
    "            'Average': (70, 74),\n",
    "            'Above Average': (66, 70),\n",
    "            'Good': (62, 66),\n",
    "            'Excellent': (56, 62),\n",
    "            'Athlete': (40, 56)\n",
    "        },\n",
    "        '26-35': {\n",
    "            'Poor': (82, None),  # 82bpm+\n",
    "            'Below Average': (75, 82),\n",
    "            'Average': (71, 75),\n",
    "            'Above Average': (66, 71),\n",
    "            'Good': (62, 66),\n",
    "            'Excellent': (55, 62),\n",
    "            'Athlete': (44, 55)\n",
    "        },\n",
    "        '36-45': {\n",
    "            'Poor': (83, None),  # 83bpm+\n",
    "            'Below Average': (76, 83),\n",
    "            'Average': (71, 76),\n",
    "            'Above Average': (67, 71),\n",
    "            'Good': (63, 67),\n",
    "            'Excellent': (57, 63),\n",
    "            'Athlete': (47, 57)\n",
    "        },\n",
    "        '46-55': {\n",
    "            'Poor': (84, None),  # 84bpm+\n",
    "            'Below Average': (77, 84),\n",
    "            'Average': (72, 77),\n",
    "            'Above Average': (68, 72),\n",
    "            'Good': (64, 68),\n",
    "            'Excellent': (58, 64),\n",
    "            'Athlete': (49, 58)\n",
    "        },\n",
    "        '56-65': {\n",
    "            'Poor': (82, None),  # 82bpm+\n",
    "            'Below Average': (76, 82),\n",
    "            'Average': (72, 76),\n",
    "            'Above Average': (68, 72),\n",
    "            'Good': (62, 68),\n",
    "            'Excellent': (57, 62),\n",
    "            'Athlete': (51, 57)\n",
    "        },\n",
    "        '65+': {\n",
    "            'Poor': (80, None),  # 80bpm+\n",
    "            'Below Average': (74, 80),\n",
    "            'Average': (70, 74),\n",
    "            'Above Average': (66, 70),\n",
    "            'Good': (62, 66),\n",
    "            'Excellent': (56, 62),\n",
    "            'Athlete': (52, 56)\n",
    "        }\n",
    "    }\n",
    "}\n",
    "\n",
    "def get_rhr_age_range(age):\n",
    "    \"\"\"Determine age range from age in years for resting heart rate.\"\"\"\n",
    "    if 18 <= age <= 25:\n",
    "        return '18-25'\n",
    "    elif 26 <= age <= 35:\n",
    "        return '26-35'\n",
    "    elif 36 <= age <= 45:\n",
    "        return '36-45'\n",
    "    elif 46 <= age <= 55:\n",
    "        return '46-55'\n",
    "    elif 56 <= age <= 65:\n",
    "        return '56-65'\n",
    "    else:\n",
    "        # Default to closest range\n",
    "        if age < 18:\n",
    "            return '18-25'\n",
    "        elif age > 65:\n",
    "            return '65+'\n",
    "        else:\n",
    "            return '26-35'  # fallback\n",
    "\n",
    "def determine_rhr_category(rhr, age_range, gender):\n",
    "    \"\"\"Determine resting heart rate category based on value, age, and gender.\"\"\"\n",
    "    gender_key = 'male' if gender.lower().startswith('m') else 'female'\n",
    "    ranges = RHR_MASTER_CHART[gender_key][age_range]\n",
    "    \n",
    "    categories = ['Poor', 'Below Average', 'Average', 'Above Average', 'Good', 'Excellent', 'Athlete']\n",
    "    \n",
    "    # Check Poor category first (open-ended at top)\n",
    "    min_val, max_val = ranges['Poor']\n",
    "    if max_val is None and rhr >= min_val:\n",
    "        return 'Poor'\n",
    "    \n",
    "    # Check other categories from Below Average down to Athlete\n",
    "    # For RHR, lower is better, so we check from highest to lowest\n",
    "    for category in ['Below Average', 'Average', 'Above Average', 'Good', 'Excellent', 'Athlete']:\n",
    "        min_val, max_val = ranges[category]\n",
    "        # Check if value falls in this range (inclusive of min, exclusive of max)\n",
    "        if min_val <= rhr < max_val:\n",
    "            return category\n",
    "    \n",
    "    # If value is below all ranges (below Athlete minimum), return Athlete\n",
    "    # This handles the case where rhr < min of Athlete\n",
    "    return 'Athlete'\n",
    "\n",
    "def format_rhr_range(min_val, max_val):\n",
    "    \"\"\"Format RHR range as string.\"\"\"\n",
    "    if max_val is None:\n",
    "        return f\"{min_val}bpm +\"\n",
    "    else:\n",
    "        return f\"{min_val}-{max_val}bpm\"\n",
    "\n",
    "def generate_resting_heart_rate_table(age, gender, rhr_value, save_path=None):\n",
    "    \"\"\"\n",
    "    Generate Resting Heart Rate table with indicator arrow.\n",
    "    \n",
    "    Args:\n",
    "        age: Patient age in years\n",
    "        gender: 'male' or 'female' (or 'm'/'f')\n",
    "        rhr_value: Patient's resting heart rate value in bpm\n",
    "        save_path: Optional path to save the figure\n",
    "    \n",
    "    Returns:\n",
    "        fig, ax: matplotlib figure and axes\n",
    "    \"\"\"\n",
    "    # Determine age range and category\n",
    "    age_range = get_rhr_age_range(age)\n",
    "    category = determine_rhr_category(rhr_value, age_range, gender)\n",
    "    \n",
    "    # Debug: print the determined category\n",
    "    print(f\"Age: {age}, Gender: {gender}, Resting Heart Rate: {rhr_value}\")\n",
    "    print(f\"Age Range: {age_range}, Category: {category}\")\n",
    "    \n",
    "    # Get the appropriate data\n",
    "    gender_key = 'male' if gender.lower().startswith('m') else 'female'\n",
    "    ranges = RHR_MASTER_CHART[gender_key][age_range]\n",
    "    \n",
    "    # Prepare table data\n",
    "    headers = ['Age', 'Poor', 'Below Average', 'Average', 'Above Average', 'Good', 'Excellent', 'Athlete']\n",
    "    age_label = f\"{age_range} ({gender[0].upper()})\"\n",
    "    \n",
    "    row_data = [age_label]\n",
    "    for cat in ['Poor', 'Below Average', 'Average', 'Above Average', 'Good', 'Excellent', 'Athlete']:\n",
    "        min_val, max_val = ranges[cat]\n",
    "        row_data.append(format_rhr_range(min_val, max_val))\n",
    "    \n",
    "    # Create figure\n",
    "    fig, ax = plt.subplots(figsize=(16, 3))\n",
    "    ax.axis('off')\n",
    "    \n",
    "    # Create table\n",
    "    table_data = [headers, row_data]\n",
    "    table = ax.table(cellText=table_data,\n",
    "                     cellLoc='center',\n",
    "                     loc='center',\n",
    "                     bbox=[0, 0, 1, 1])\n",
    "    \n",
    "    # Style the table\n",
    "    table.auto_set_font_size(False)\n",
    "    table.set_fontsize(11)\n",
    "    table.scale(1, 2.5)\n",
    "    \n",
    "    # Header row styling (cyan background)\n",
    "    for i in range(len(headers)):\n",
    "        cell = table[(0, i)]\n",
    "        cell.set_facecolor('#7dd3fc')  # cyan-300 equivalent\n",
    "        cell.set_text_props(weight='bold', color='black')\n",
    "        cell.set_edgecolor('#9ca3af')  # gray-400\n",
    "        cell.set_linewidth(1)\n",
    "    \n",
    "    # Find the column index for the category (before styling)\n",
    "    category_index = headers.index(category)\n",
    "    \n",
    "    # Data row styling\n",
    "    for i in range(len(row_data)):\n",
    "        cell = table[(1, i)]\n",
    "        if i == 0:  # Age column\n",
    "            cell.set_facecolor('#a5f3fc')  # cyan-200\n",
    "            cell.set_text_props(weight='semibold', color='black')\n",
    "        else:\n",
    "            # Highlight the category cell with light green background\n",
    "            if i == category_index:\n",
    "                cell.set_facecolor('#d1fae5')  # green-200 equivalent\n",
    "                cell.set_text_props(weight='bold', color='black')\n",
    "            else:\n",
    "                cell.set_facecolor('#f3f4f6')  # gray-100\n",
    "                cell.set_text_props(color='black')\n",
    "        cell.set_edgecolor('#9ca3af')  # gray-400\n",
    "        cell.set_linewidth(1)\n",
    "    \n",
    "    # Add arrow indicator below the category column\n",
    "    # Calculate position\n",
    "    cell_width = 1.0 / len(headers)\n",
    "    arrow_x = (category_index + 0.5) * cell_width\n",
    "    \n",
    "    # Draw arrow pointing up\n",
    "    arrow = patches.FancyArrowPatch(\n",
    "        (arrow_x, -0.15), (arrow_x, -0.05),\n",
    "        arrowstyle='->', mutation_scale=20, \n",
    "        linewidth=2, color='black',\n",
    "        transform=ax.transAxes\n",
    "    )\n",
    "    ax.add_patch(arrow)\n",
    "    \n",
    "    # Add triangle at the top\n",
    "    triangle = patches.RegularPolygon(\n",
    "        (arrow_x, -0.05), 3, radius=0.02,\n",
    "        orientation=np.pi/2, color='black',\n",
    "        transform=ax.transAxes\n",
    "    )\n",
    "    ax.add_patch(triangle)\n",
    "    \n",
    "    # Set title\n",
    "    title = f\"Resting Heart Rate - {rhr_value:.0f}bpm\"\n",
    "    ax.set_title(title, fontsize=14, fontweight='bold', pad=20)\n",
    "    \n",
    "    plt.tight_layout()\n",
    "    \n",
    "    if save_path:\n",
    "        plt.savefig(save_path, dpi=300, bbox_inches='tight')\n",
    "    \n",
    "    return fig, ax\n",
    "\n",
    "# Test the function\n",
    "# Example: 30-year-old female with resting heart rate of 53bpm\n",
    "fig, ax = generate_resting_heart_rate_table(\n",
    "    age=50,\n",
    "    gender='male',\n",
    "    rhr_value=76,\n",
    "    save_path=f'{base_dir}/graphs/resting_heart_rate_table.png'\n",
    ")\n",
    "plt.show()\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "id": "84addc63",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "==================================================\n",
      "Optimal Fat Burning Zone (highest fat:carb ratio):\n",
      "Time: 164.0 seconds\n",
      "Fat burn rate: 3.894 kcal/min\n",
      "Carb burn rate: 1.575 kcal/min\n",
      "Fat:Carb ratio: 2.47\n",
      "Heart Rate: 96.7 bpm\n",
      "VO2: 1147.9 ml/min\n",
      "VT1: {'HeartRate': np.float64(100.5), 'Speed': np.float64(4.0), 'Time': np.float64(251.0)}\n",
      "VT2: {'HeartRate': np.float64(189.71300000000002), 'Speed': np.float64(7.5), 'Time': np.float64(1524.0)}\n",
      "Zone 1 (Active Recovery): 81.7 - 96.7 bpm\n",
      "Zone 2 (Aerobic Base): 96.7 - 100.5 bpm\n",
      "Zone 3 (Aerobic): 100.5 - 179.7 bpm\n",
      "Zone 4 (Lactate Threshold): 179.7 - 189.7 bpm\n",
      "Zone 5 (VO2 Max): 189.7 - 199.7 bpm\n"
     ]
    }
   ],
   "source": [
    "\n",
    "# Find the point where fat burning is highest and carb burning is lowest\n",
    "# Using the smoothed data for more stable results\n",
    "fat_burn_max_idx = df['FAT_smoothed'].idxmax()\n",
    "carb_burn_min_idx = df['CHO_smoothed'].idxmin()\n",
    "\n",
    "\n",
    "print(\"\\n\" + \"=\"*50)\n",
    "\n",
    "# Find the optimal fat burning zone (highest fat:carb ratio)\n",
    "df['fat_carb_ratio'] = df['FAT_smoothed'] / (df['CHO_smoothed'] + 0.00000001)  # Add small value to avoid division by zero\n",
    "optimal_fat_idx = df['fat_carb_ratio'].idxmax()\n",
    "optimal_row = df.loc[optimal_fat_idx]\n",
    "\n",
    "print(\"Optimal Fat Burning Zone (highest fat:carb ratio):\")\n",
    "print(f\"Time: {optimal_row['T(sec)']} seconds\")\n",
    "print(f\"Fat burn rate: {optimal_row['FAT_smoothed']:.3f} kcal/min\")\n",
    "print(f\"Carb burn rate: {optimal_row['CHO_smoothed']:.3f} kcal/min\")\n",
    "print(f\"Fat:Carb ratio: {optimal_row['fat_carb_ratio']:.2f}\")\n",
    "print(f\"Heart Rate: {optimal_row['HR(bpm)_smoothed']:.1f} bpm\")\n",
    "print(f\"VO2: {optimal_row['VO2(ml/min)_smoothed']:.1f} ml/min\")\n",
    "\n",
    "def detect_vt1(df, fat_col=\"FAT_smoothed\", carb_col=\"CHO_smoothed\"):\n",
    "    \"\"\"\n",
    "    Detect VT1 as the first index where carb burn > fat burn and remains higher.\n",
    "    \"\"\"\n",
    "    condition = df[carb_col] > df[fat_col]\n",
    "    crossover_indices = condition[condition].index\n",
    "\n",
    "    if len(crossover_indices) == 0:\n",
    "        return None  # No crossover found\n",
    "    \n",
    "    # Find first crossover where carbs remain higher for the rest\n",
    "    for idx in crossover_indices:\n",
    "        if all(df.loc[idx:][carb_col] > df.loc[idx:][fat_col]):\n",
    "            return idx\n",
    "    return None\n",
    "\n",
    "\n",
    "def detect_vt2(df, vent_col=\"VE(l/min)_smoothed\", bf_col=\"BF(bpm)_smoothed\", smooth_window=5):\n",
    "    \"\"\"\n",
    "    Detect VT2 using slope/inflection method.\n",
    "    Works with either Ventilation (VE) or Breathing Frequency (Bf).\n",
    "    \"\"\"\n",
    "    col = vent_col if vent_col in df.columns else bf_col\n",
    "    \n",
    "    # Use already smoothed data\n",
    "    smoothed_col = col\n",
    "    \n",
    "    # Compute slope (first derivative)\n",
    "    df[\"slope\"] = df[smoothed_col].diff()\n",
    "    \n",
    "    # Detect inflection: largest change in slope (second derivative peak)\n",
    "    df[\"second_derivative\"] = df[\"slope\"].diff()\n",
    "    inflection_idx = df[\"second_derivative\"].idxmax()\n",
    "    \n",
    "    return inflection_idx\n",
    "\n",
    "\n",
    "def analyze_thresholds(df_input):\n",
    "    # Use the existing dataframe\n",
    "    df_copy = df_input.copy()\n",
    "    \n",
    "    # --- Detect VT1 ---\n",
    "    vt1_idx = detect_vt1(df_copy)\n",
    "    vt1 = None\n",
    "    if vt1_idx is not None:\n",
    "        vt1 = {\n",
    "            \"HeartRate\": df_copy.loc[vt1_idx, \"HR(bpm)_smoothed\"],\n",
    "            \"Speed\": df_copy.loc[vt1_idx, \"Speed\"],\n",
    "            \"Time\": df_copy.loc[vt1_idx, \"T(sec)\"]\n",
    "        }\n",
    "    \n",
    "    # --- Detect VT2 ---\n",
    "    vt2_idx = detect_vt2(df_copy)\n",
    "    vt2 = None\n",
    "    if vt2_idx is not None:\n",
    "        vt2 = {\n",
    "            \"HeartRate\": df_copy.loc[vt2_idx, \"HR(bpm)_smoothed\"],\n",
    "            \"Speed\": df_copy.loc[vt2_idx, \"Speed\"],\n",
    "            \"Time\": df_copy.loc[vt2_idx, \"T(sec)\"]\n",
    "        }\n",
    "    \n",
    "    return vt1, vt2\n",
    "\n",
    "\n",
    "vt1, vt2 = analyze_thresholds(df)\n",
    "print(\"VT1:\", vt1)\n",
    "print(\"VT2:\", vt2)\n",
    "zone_1_start = optimal_row['HR(bpm)_smoothed'] - 15\n",
    "zone_2_start = optimal_row['HR(bpm)_smoothed']\n",
    "zone_3_start = vt1\n",
    "zone_4_start = vt2['HeartRate'] - 10\n",
    "zone_5_start = vt2['HeartRate']\n",
    "zone_5_end = vt2['HeartRate'] + 10\n",
    "\n",
    "zone_1_end = zone_2_start\n",
    "zone_2_end = vt1['HeartRate']\n",
    "zone_3_end = zone_4_start\n",
    "zone_4_end = zone_5_start\n",
    "\n",
    "print(f\"Zone 1 (Active Recovery): {zone_1_start:.1f} - {zone_1_end:.1f} bpm\")\n",
    "print(f\"Zone 2 (Aerobic Base): {zone_2_start:.1f} - {zone_2_end:.1f} bpm\")\n",
    "print(f\"Zone 3 (Aerobic): {zone_3_start['HeartRate']:.1f} - {zone_3_end:.1f} bpm\")\n",
    "print(f\"Zone 4 (Lactate Threshold): {zone_4_start:.1f} - {zone_4_end:.1f} bpm\")\n",
    "print(f\"Zone 5 (VO2 Max): {zone_5_start:.1f} - {zone_5_end:.1f} bpm\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "id": "f324fe94",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 128088 characters]",
      "text/plain": [
       "<Figure size 1200x800 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "import math\n",
    "\n",
    "# Define the fixed rows\n",
    "descriptions = [\n",
    "    \"Improves health and\\nrecovery capacity\",\n",
    "    \"Improves endurance\\nand fat burning\",\n",
    "    \"Improves Aerobic\\nfitness\",\n",
    "    \"Improves maximum\\nperformance capacity\",\n",
    "    \"Develops maximum\\nperformance and speed\"\n",
    "]\n",
    "\n",
    "hr_percentages = [\n",
    "    \"55-65% of Max Heart Rate\",\n",
    "    \"65-75% of Max Heart Rate\",\n",
    "    \"80-85% of Max Heart Rate\",\n",
    "    \"85-88% of Max Heart Rate\",\n",
    "    \"90%+ of Max Heart Rate\"\n",
    "]\n",
    "\n",
    "ideal_breath_ranges = [\n",
    "    \"Ideal Range: 15-20 breaths\",\n",
    "    \"Ideal Range: 20-25 breaths\",\n",
    "    \"Ideal Range: 25-30 breaths\",\n",
    "    \"Ideal Range: 30-35 breaths\",\n",
    "    \"Ideal Range: 40+ breaths\"\n",
    "]\n",
    "\n",
    "# Define the zones for iteration\n",
    "# Note: zone_3_start is a dictionary (vt1), others are floats\n",
    "# Use math.floor to ensure values match the exact integer values requested\n",
    "zones_list = [\n",
    "    (\"Zone 1\", math.floor(zone_1_start), math.floor(zone_1_end)),\n",
    "    (\"Zone 2\", math.floor(zone_2_start), math.floor(zone_2_end)),\n",
    "    (\"Zone 3\", math.floor(zone_3_start['HeartRate']), math.floor(zone_3_end)),\n",
    "    (\"Zone 4\", math.floor(zone_4_start), math.floor(zone_4_end)),\n",
    "    (\"Zone 5\", math.floor(zone_5_start), math.floor(zone_5_end))\n",
    "]\n",
    "\n",
    "# Calculate metrics for each zone\n",
    "zone_metrics = {\n",
    "    \"HR BPM\": [],\n",
    "    \"Speed\": [],\n",
    "    \"Pace\": [],\n",
    "    \"Calories\": [],\n",
    "    \"Carb Utilization\": [],\n",
    "    \"Breathing\": []\n",
    "}\n",
    "\n",
    "for i, (name, start, end) in enumerate(zones_list):\n",
    "    # Filter dataframe for the current zone\n",
    "    mask = (df['HR(bpm)_smoothed'] >= start) & (df['HR(bpm)_smoothed'] <= end)\n",
    "    zone_df = df[mask]\n",
    "    \n",
    "    # HR BPM Range\n",
    "    zone_metrics[\"HR BPM\"].append(f\"{int(start)}-{int(end)} bpm\")\n",
    "    \n",
    "    if not zone_df.empty:\n",
    "        # Speed (Range)\n",
    "        # df['Speed'] is in mph\n",
    "        # Filter out 0 speeds for range calculation\n",
    "        speed_series = zone_df[zone_df['Speed'] > 0.1]['Speed']\n",
    "        \n",
    "        if not speed_series.empty:\n",
    "            min_speed = speed_series.min()\n",
    "            max_speed = speed_series.max()\n",
    "            \n",
    "            if abs(min_speed - max_speed) < 0.1:\n",
    "                zone_metrics[\"Speed\"].append(f\"{min_speed:.1f} mph\\n2% Incline\")\n",
    "            else:\n",
    "                zone_metrics[\"Speed\"].append(f\"{min_speed:.1f}-{max_speed:.1f} mph\\n2% Incline\")\n",
    "            \n",
    "            # Pace (Range)\n",
    "            # Higher speed = Lower pace value (faster)\n",
    "            # So max_speed -> min_pace_val, min_speed -> max_pace_val\n",
    "            \n",
    "            def speed_to_pace(s_mph):\n",
    "                if s_mph <= 0: return 0, 0\n",
    "                s_kmh = s_mph * 1.60934\n",
    "                p_min = 60 / s_kmh\n",
    "                p_m = int(p_min)\n",
    "                p_s = int((p_min % 1) * 60)\n",
    "                return p_m, p_s\n",
    "\n",
    "            min_pace_m, min_pace_s = speed_to_pace(max_speed) # Fastest speed -> Lowest pace number\n",
    "            max_pace_m, max_pace_s = speed_to_pace(min_speed) # Slowest speed -> Highest pace number\n",
    "            \n",
    "            if min_pace_m == max_pace_m and min_pace_s == max_pace_s:\n",
    "                pace_str = f\"{min_pace_m}:{min_pace_s:02d} min/km Pace\"\n",
    "            else:\n",
    "                pace_str = f\"{max_pace_m}:{max_pace_s:02d}-{min_pace_m}:{min_pace_s:02d}\\nmin/km Pace\"\n",
    "            zone_metrics[\"Pace\"].append(pace_str)\n",
    "        else:\n",
    "            zone_metrics[\"Speed\"].append(\"-\\n2% Incline\")\n",
    "            zone_metrics[\"Pace\"].append(\"-\")\n",
    "            \n",
    "        # Calories (EE)\n",
    "        # Use raw EE(kcal/min) instead of smoothed sum of FAT+CHO\n",
    "        avg_cals = zone_df['EE(kcal/min)'].mean()\n",
    "        zone_metrics[\"Calories\"].append(f\"Avg:\\n{avg_cals:.1f} kcals/minute\")\n",
    "        \n",
    "        # Carb Utilization (g/min)\n",
    "        # Use raw CHO\n",
    "        avg_carbs_g = zone_df['CHO'].mean() / 4\n",
    "        zone_metrics[\"Carb Utilization\"].append(f\"Avg: {avg_carbs_g:.1f}g/min\\nCarb Utilization\")\n",
    "        \n",
    "        # Breathing (BF)\n",
    "        avg_breaths = zone_df['BF(bpm)_smoothed'].mean()\n",
    "        ideal_range = ideal_breath_ranges[i]\n",
    "        zone_metrics[\"Breathing\"].append(f\"Avg: {int(avg_breaths)} breaths\\n{ideal_range}\")\n",
    "        \n",
    "    else:\n",
    "        zone_metrics[\"Speed\"].append(\"-\\n2% Incline\")\n",
    "        zone_metrics[\"Pace\"].append(\"-\")\n",
    "        zone_metrics[\"Calories\"].append(\"-\")\n",
    "        zone_metrics[\"Carb Utilization\"].append(\"-\")\n",
    "        zone_metrics[\"Breathing\"].append(f\"-\\n{ideal_breath_ranges[i]}\")\n",
    "\n",
    "# Prepare data for the table\n",
    "table_data = []\n",
    "table_data.append(descriptions)\n",
    "table_data.append(hr_percentages)\n",
    "table_data.append(zone_metrics[\"HR BPM\"])\n",
    "table_data.append(zone_metrics[\"Speed\"])\n",
    "table_data.append(zone_metrics[\"Pace\"])\n",
    "table_data.append(zone_metrics[\"Calories\"])\n",
    "table_data.append(zone_metrics[\"Carb Utilization\"])\n",
    "table_data.append(zone_metrics[\"Breathing\"])\n",
    "\n",
    "col_labels = [\"Zone 1\", \"Zone 2\", \"Zone 3\", \"Zone 4\", \"Zone 5\"]\n",
    "\n",
    "# Create the table plot\n",
    "fig, ax = plt.subplots(figsize=(12, 8)) # Reduced width from 16 to 12\n",
    "ax.axis('off')\n",
    "\n",
    "# Create table without rowLabels\n",
    "table = ax.table(cellText=table_data,\n",
    "                 colLabels=col_labels,\n",
    "                 loc='center',\n",
    "                 cellLoc='center')\n",
    "\n",
    "table.auto_set_font_size(False)\n",
    "table.set_fontsize(10)\n",
    "table.scale(1, 3.5) # Increased vertical scale for multi-line text\n",
    "\n",
    "# Styling\n",
    "# Header row\n",
    "for j, label in enumerate(col_labels):\n",
    "    cell = table[(0, j)]\n",
    "    cell.set_facecolor('#7dd3fc') # cyan-300\n",
    "    cell.set_text_props(weight='bold')\n",
    "\n",
    "# Row specific styling\n",
    "colors = ['#fecaca', '#fecaca', '#fef08a', '#bbf7d0', '#bbf7d0']\n",
    "\n",
    "for j in range(len(col_labels)):\n",
    "    # HR BPM row is at index 2 (0-based in data) -> row 3 in table (0 is header, 1 is desc, 2 is HR%, 3 is HR BPM)\n",
    "    cell = table[(3, j)] \n",
    "    cell.set_facecolor(colors[j])\n",
    "    cell.set_text_props(weight='bold')\n",
    "    \n",
    "    # Breathing row is at index 7 -> row 8 in table\n",
    "    cell = table[(8, j)]\n",
    "    cell.set_facecolor(colors[j])\n",
    "    cell.set_text_props(weight='bold')\n",
    "\n",
    "plt.title(\"Personalized Heart Rate Zones\", fontsize=16, fontweight='bold', pad=5) # Reduced pad from 20 to 5\n",
    "plt.tight_layout()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9c00c366",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "report-generation",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
