Compare commits

...

28 Commits

Author SHA1 Message Date
bolade 5844cb6cff Update context for report: change patient name to Keirstyn; adjust execution count in analysis notebook 2025-10-22 16:25:33 +01:00
bolade e58d9b0158 Refactor code structure for improved readability and maintainability 2025-10-22 16:01:17 +01:00
bolade 85ea73ade8 Refactor analysis notebook: comment out API calls and update CSV file handling; modify page 2 of report for content and structure adjustments 2025-10-22 15:45:57 +01:00
bolade 1d5625b61a Refactor code structure for improved readability and maintainability 2025-10-22 15:28:14 +01:00
bolade f5d304aec5 Refactor code structure for improved readability and maintainability 2025-10-22 01:11:32 +01:00
bolade d862577ecf Refactor code structure for improved readability and maintainability 2025-10-21 12:50:48 +01:00
bolade 2568e991e2 Implement code changes to enhance functionality and improve performance 2025-10-21 12:42:16 +01:00
bolade bad8f18f19 Refactor fuel mix calculations based on RER; update resting phase filters and add detailed markdown explanations. Adjust execution counts and outputs for clarity. 2025-10-21 12:35:16 +01:00
bolade e2f6eaab66 Refactor code structure for improved readability and maintainability 2025-10-21 12:22:40 +01:00
bolade 192c598e18 Refactor code structure for improved readability and maintainability 2025-10-15 14:57:50 +01:00
bolade 7e55ee6954 Refactor code structure for improved readability and maintainability 2025-10-04 00:06:45 +01:00
bolade 6b2c61a48e Implement code changes to enhance functionality and improve performance 2025-09-29 17:55:04 +01:00
bolade f52729d703 Add graph generation functionality and update charts
- Implemented GraphGenerator class for generating various physiological charts.
- Added methods for generating respiratory, fuel utilization, VO2 pulse, VO2 breath, fat metabolism, recovery, body fat percentage, body composition, and spirometry charts.
- Included functionality to save charts as PNG files or return them as base64 strings.
- Updated existing chart images in the graphs directory.
2025-09-29 11:45:09 +01:00
bolade 54e0189301 Enhance table styling and layout in report pages
- Updated table header and cell classes to center-align text for better readability in page_19.html.
- Adjusted padding and margins in page_7.html for improved layout and visual consistency.
- Reduced spacing in various sections to create a more compact and organized appearance.
2025-09-29 11:17:32 +01:00
bolade a20f21d288 Refactor page_7.html for improved layout and responsiveness
- Enhanced the structure of the Spirometry Assessment and Respiratory sections for better readability.
- Centered images and added max-width constraints to ensure proper scaling on different devices.
- Improved text formatting for clarity and consistency.
2025-09-29 10:42:23 +01:00
bolade d12add210b Refactor code structure for improved readability and maintainability 2025-09-29 09:54:05 +01:00
bolade a44a763640 Refactor code structure for improved readability and maintainability 2025-09-29 09:17:11 +01:00
bolade 604ef375aa Refactor footer context generation and enhance chart image styling for improved layout 2025-09-26 22:32:39 +01:00
bolade 894fbbcee3 Add header and footer templates for report generation
- Created a footer template with contact information and page number.
- Created a header template displaying patient details including name, age, height, weight, and focus.
2025-09-26 21:42:02 +01:00
bolade 1ae1ec2369 Add requirements.txt, tailwind configuration, and initial truth report HTML
- Created requirements.txt with a comprehensive list of dependencies.
- Added tailwindconfig.js for Tailwind CSS configuration.
- Introduced truth_report.html with structured content and Tailwind CSS styling for a visually appealing layout.
2025-09-26 16:37:46 +01:00
bolade 4a61dd7898 Refactor code structure for improved readability and maintainability 2025-09-24 10:36:38 +01:00
bolade 845a7ca099 Add body fat analysis graph for page 1 2025-09-24 09:57:15 +01:00
bolade 4753276778 Add initial HTML structure for report generation
- Created page_1.html with layout including ISHP branding, main content, and dotted pattern.
- Developed page_2.html featuring a Table of Contents with sections for Lung Analysis, Cardio Metrics, Fuel Utilization, Local Muscle Activity, Training Recommendations, Next Steps, and Glossary.
- Added placeholder files for pages 3 to 19 to facilitate future content development.
2025-09-24 08:35:29 +01:00
bolade 804b3fb10e Add report generation with PDF output and respiratory chart 2025-09-23 21:31:15 +01:00
bolade d7437ff975 scaled y axis 2025-09-23 20:36:24 +01:00
bolade 65438bbc6e smoothed the graph 2025-09-23 19:20:36 +01:00
bolade b0846a73d0 added more graphs 2025-09-23 19:08:30 +01:00
bolade 0a08dd13df Added 2 more graphs 2025-09-23 18:47:10 +01:00
51 changed files with 8095 additions and 46 deletions
+3 -1
View File
@@ -1,3 +1,5 @@
.venv .venv
data/ data/
.env
Binary file not shown.
+799
View File
@@ -0,0 +1,799 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 6,
"id": "b18c1027",
"metadata": {},
"outputs": [],
"source": [
"\n",
"# import requests\n",
"# import json\n",
"# import base64\n",
"# from pathlib import Path\n",
"\n",
"# API_KEY_REF = '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'\n",
"# def encode_pdf_to_base64(pdf_path):\n",
"# with open(pdf_path, \"rb\") as pdf_file:\n",
"# return base64.b64encode(pdf_file.read()).decode('utf-8')\n",
"\n",
"# url = \"https://openrouter.ai/api/v1/chat/completions\"\n",
"# headers = {\n",
"# \"Authorization\": f\"Bearer {API_KEY_REF}\",\n",
"# \"Content-Type\": \"application/json\"\n",
"# }\n",
"\n",
"# # Read and encode the PDF\n",
"# pdf_path = \"data/~Moran~K~19910201~Spirometry Exam~20250729~20250729032843.pdf\"\n",
"# base64_pdf = encode_pdf_to_base64(pdf_path)\n",
"# data_url = f\"data:application/pdf;base64,{base64_pdf}\"\n",
"\n",
"# messages = [\n",
"# {\n",
"# \"role\": \"user\",\n",
"# \"content\": [\n",
"# {\n",
"# \"type\": \"text\",\n",
"# \"text\": \"Please extract the Spirometry table from the pdf and return the values in csv format, \"\n",
"# \"note that it is the unit of parameter that is beside it and it should not be a column. \"\n",
"# \"The '-' Should be treated as empty values.\"\n",
"# \"do not add 'csv' at the start or end of the response\"\n",
"# },\n",
"# {\n",
"# \"type\": \"file\",\n",
"# \"file\": {\n",
"# \"filename\": \"document.pdf\",\n",
"# \"file_data\": data_url\n",
"# }\n",
"# },\n",
"# ]\n",
"# }\n",
"# ]\n",
"\n",
"# # Optional: Configure PDF processing engine\n",
"# # PDF parsing will still work even if the plugin is not explicitly set\n",
"# plugins = [\n",
"# {\n",
"# \"id\": \"file-parser\",\n",
"# \"pdf\": {\n",
"# \"engine\": \"pdf-text\" # defaults to \"mistral-ocr\". See Pricing above\n",
"# }\n",
"# }\n",
"# ]\n",
"\n",
"# payload = {\n",
"# \"model\": \"google/gemini-2.5-flash-lite\",\n",
"# \"messages\": messages,\n",
"# }\n",
"\n",
"# response = requests.post(url, headers=headers, json=payload)\n",
"# # Get the response content\n",
"# response_data = response.json()\n",
"# print(response_data)\n",
"\n",
"# # Extract the content from the response\n",
"# if 'choices' in response_data and len(response_data['choices']) > 0:\n",
"# content = response_data['choices'][0]['message']['content']\n",
" \n",
"# # Save to a CSV file\n",
"# output_file = \"extracted_table.csv\"\n",
"# with open(output_file, 'w', encoding='utf-8') as f:\n",
"# f.write(content)\n",
" \n",
"# print(f\"Content saved to {output_file}\")\n",
"# else:\n",
"# print(\"No content found in response\")"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "56a9d655",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"FVC Best: 4.24, FVC Pred: 112.0\n",
"FEV1 Best: 3.26, FEV1 Pred: 103.3\n",
"FEV1/FVC% Best: 76.9, FEV1/FVC% Pred: 91.8\n"
]
}
],
"source": [
"import pandas as pd\n",
"spirometry_df = pd.read_csv(\"data/spirometry_data.csv\")\n",
"\n",
"fvc_best = spirometry_df.loc[spirometry_df['Parameters'] == 'FVC', 'Best'].values[0]\n",
"fvc_pred = spirometry_df.loc[spirometry_df['Parameters'] == 'FVC', '%Pred.'].values[0]\n",
"\n",
"fev1_best = spirometry_df.loc[spirometry_df['Parameters'] == 'FEV1', 'Best'].values[0]\n",
"fev1_pred = spirometry_df.loc[spirometry_df['Parameters'] == 'FEV1', '%Pred.'].values[0]\n",
"\n",
"fev1_fevc_best = spirometry_df.loc[spirometry_df['Parameters'] == 'FEV1/FVC%', 'Best'].values[0]\n",
"fev1_fevc_pred = spirometry_df.loc[spirometry_df['Parameters'] == 'FEV1/FVC%', '%Pred.'].values[0]\n",
"\n",
"print(f\"FVC Best: {fvc_best}, FVC Pred: {fvc_pred}\")\n",
"print(f\"FEV1 Best: {fev1_best}, FEV1 Pred: {fev1_pred}\")\n",
"print(f\"FEV1/FVC% Best: {fev1_fevc_best}, FEV1/FVC% Pred: {fev1_fevc_pred}\")"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "990f4b4f",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Peak VT: 2.75\n",
"HR at Peak VT: 155.0\n"
]
}
],
"source": [
"df = pd.read_csv('data/Pnoe_20250729_1550-Moran_Keirstyn.csv', delimiter=';')\n",
"peak_vt = df['VT(l)'].max()\n",
"max_vt_row = df.loc[df['VT(l)'].idxmax()]\n",
"print(f\"Peak VT: {peak_vt}\")\n",
"hr = max_vt_row['HR(bpm)']\n",
"print(f\"HR at Peak VT: {hr}\")"
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "041cbc3d",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Peak VT: 2.3770000000000002\n",
"HR at Peak VT: 171.525\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"/tmp/ipykernel_69398/4157056299.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('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).mean()\n",
" \n",
"peak_vt = df['VT(l)_smoothed'].max()\n",
"max_vt_row = df.loc[df['VT(l)_smoothed'].idxmax()]\n",
"print(f\"Peak VT: {peak_vt}\")\n",
"hr = max_vt_row['HR(bpm)_smoothed']\n",
"print(f\"HR at Peak VT: {hr}\")"
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "de7cadd1",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Percent FEV: 72.91411042944786\n"
]
}
],
"source": [
"percent_fev = (peak_vt / fev1_best) * 100\n",
"print(f\"Percent FEV: {percent_fev}\")"
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "cb972ed3",
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>MeasurementDate</th>\n",
" <th>Comment</th>\n",
" <th>ExternalDeviceId</th>\n",
" <th>ExternalPatientId</th>\n",
" <th>FirstName</th>\n",
" <th>LastName</th>\n",
" <th>BirthDate</th>\n",
" <th>Age</th>\n",
" <th>Ethnicity</th>\n",
" <th>Gender</th>\n",
" <th>...</th>\n",
" <th>Child_XC</th>\n",
" <th>Child_XC_Unit</th>\n",
" <th>Child_BIVA_ZRh</th>\n",
" <th>Child_BIVA_ZXcH</th>\n",
" <th>Child_PhA</th>\n",
" <th>Child_PhA_Unit</th>\n",
" <th>Child_REE_Kcal</th>\n",
" <th>Child_REE_MJ</th>\n",
" <th>Child_TEE_Kcal</th>\n",
" <th>Child_TEE_MJ</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>13</th>\n",
" <td>2025-07-29T18:58:54.0000000Z</td>\n",
" <td>NaN</td>\n",
" <td>10000001583275_0055003f5631501320313557</td>\n",
" <td>KM6479696509</td>\n",
" <td>Keirstyn</td>\n",
" <td>Moran</td>\n",
" <td>1991-02-01T00:00:00.0000000Z</td>\n",
" <td>34</td>\n",
" <td>Caucasian</td>\n",
" <td>Female</td>\n",
" <td>...</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"<p>1 rows × 147 columns</p>\n",
"</div>"
],
"text/plain": [
" MeasurementDate Comment \\\n",
"13 2025-07-29T18:58:54.0000000Z NaN \n",
"\n",
" ExternalDeviceId ExternalPatientId FirstName \\\n",
"13 10000001583275_0055003f5631501320313557 KM6479696509 Keirstyn \n",
"\n",
" LastName BirthDate Age Ethnicity Gender ... \\\n",
"13 Moran 1991-02-01T00:00:00.0000000Z 34 Caucasian Female ... \n",
"\n",
" Child_XC Child_XC_Unit Child_BIVA_ZRh Child_BIVA_ZXcH Child_PhA \\\n",
"13 NaN NaN NaN NaN NaN \n",
"\n",
" Child_PhA_Unit Child_REE_Kcal Child_REE_MJ Child_TEE_Kcal Child_TEE_MJ \n",
"13 NaN NaN NaN NaN NaN \n",
"\n",
"[1 rows x 147 columns]"
]
},
"execution_count": 11,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"personal_df = pd.read_excel('data/SECA body comp for all patients.xlsx')\n",
"\n",
"keirstyn_data = personal_df[personal_df['LastName'].str.contains('Moran', case=False, na=False)]\n",
"keirstyn_data"
]
},
{
"cell_type": "code",
"execution_count": 12,
"id": "98d9295a",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"VO2 Max: 47.906290322580645\n"
]
}
],
"source": [
"v02_max = df['VO2(ml/min)_smoothed'].max()\n",
"weight = keirstyn_data['Weight'].iloc[0]\n",
"print(f\"VO2 Max: {v02_max/weight}\")"
]
},
{
"cell_type": "code",
"execution_count": 13,
"id": "cdfeb309",
"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"
]
}
],
"source": [
"# 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",
"# # Get the data at maximum fat burning point\n",
"# max_fat_row = df.loc[fat_burn_max_idx]\n",
"# print(f\"Maximum Fat Burning Point:\")\n",
"# print(f\"Time: {max_fat_row['T(sec)']} seconds\")\n",
"# print(f\"Fat burn rate: {max_fat_row['FAT_smoothed']:.3f} kcal/min\")\n",
"# print(f\"Carb burn rate: {max_fat_row['CHO_smoothed']:.3f} kcal/min\")\n",
"# print(f\"Heart Rate: {max_fat_row['HR(bpm)_smoothed']:.1f} bpm\")\n",
"# print(f\"VO2: {max_fat_row['VO2(ml/min)_smoothed']:.1f} ml/min\")\n",
"\n",
"# print(\"\\n\" + \"=\"*50)\n",
"\n",
"# # Get the data at minimum carb burning point\n",
"# min_carb_row = df.loc[carb_burn_min_idx]\n",
"# print(f\"Minimum Carbohydrate Burning Point:\")\n",
"# print(f\"Time: {min_carb_row['T(sec)']} seconds\")\n",
"# print(f\"Fat burn rate: {min_carb_row['FAT_smoothed']:.3f} kcal/min\")\n",
"# print(f\"Carb burn rate: {min_carb_row['CHO_smoothed']:.3f} kcal/min\")\n",
"# print(f\"Heart Rate: {min_carb_row['HR(bpm)_smoothed']:.1f} bpm\")\n",
"# print(f\"VO2: {min_carb_row['VO2(ml/min)_smoothed']:.1f} ml/min\")\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(f\"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\")"
]
},
{
"cell_type": "code",
"execution_count": 14,
"id": "4420cfea",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Found 2 intersections at indices: [18, 47]\n",
"\n",
"Last intersection at index 47:\n",
"Time: 251.0 seconds\n",
"Fat burn rate: 3.040 kcal/min\n",
"Carb burn rate: 3.166 kcal/min\n",
"Heart Rate: 100.5 bpm\n",
"VO2: 1283.0 ml/min\n"
]
}
],
"source": [
"# Find intersections where FAT_smoothed and CHO_smoothed cross each other\n",
"intersections = []\n",
"\n",
"for i in range(1, len(df)):\n",
" # Check if there's a crossover between consecutive points\n",
" prev_fat = df.iloc[i-1]['FAT_smoothed']\n",
" prev_cho = df.iloc[i-1]['CHO_smoothed']\n",
" curr_fat = df.iloc[i]['FAT_smoothed']\n",
" curr_cho = df.iloc[i]['CHO_smoothed']\n",
" \n",
" # Skip if any values are NaN\n",
" if pd.isna(prev_fat) or pd.isna(prev_cho) or pd.isna(curr_fat) or pd.isna(curr_cho):\n",
" continue\n",
" \n",
" # Check if lines cross (fat was above/below cho and now it's below/above)\n",
" if ((prev_fat > prev_cho and curr_fat < curr_cho) or \n",
" (prev_fat < prev_cho and curr_fat > curr_cho)):\n",
" intersections.append(i)\n",
"\n",
"print(f\"Found {len(intersections)} intersections at indices: {intersections}\")\n",
"\n",
"if intersections:\n",
" # Get the last intersection\n",
" last_intersection_idx = intersections[-1]\n",
" last_intersection_row = df.iloc[last_intersection_idx]\n",
" \n",
" print(f\"\\nLast intersection at index {last_intersection_idx}:\")\n",
" print(f\"Time: {last_intersection_row['T(sec)']} seconds\")\n",
" print(f\"Fat burn rate: {last_intersection_row['FAT_smoothed']:.3f} kcal/min\")\n",
" print(f\"Carb burn rate: {last_intersection_row['CHO_smoothed']:.3f} kcal/min\")\n",
" print(f\"Heart Rate: {last_intersection_row['HR(bpm)_smoothed']:.1f} bpm\")\n",
" print(f\"VO2: {last_intersection_row['VO2(ml/min)_smoothed']:.1f} ml/min\")\n",
"else:\n",
" print(\"No intersections found between FAT and CHO curves\")"
]
},
{
"cell_type": "code",
"execution_count": 21,
"id": "62803668",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"VT1: {'HeartRate': 100.5, 'Speed': 4.0, 'Time': 251.0}\n",
"VT2: {'HeartRate': 189.71300000000002, 'Speed': 7.5, 'Time': 1524.0}\n"
]
}
],
"source": [
"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"
]
},
{
"cell_type": "code",
"execution_count": 16,
"id": "07593b56",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"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": [
"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": 17,
"id": "c90415b2",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"VO2 Max detected at index 202:\n",
"Time: 985.0 seconds\n",
"VO2 Breath: 58.2 ml/breath\n",
"VO2: 2167.8 ml/min\n",
"VO2 per kg: 38.8 ml/kg/min\n",
"Heart Rate: 170.5 bpm\n",
"Speed: 6.0 km/h\n",
"VO2 Breath Slope: -0.02\n"
]
}
],
"source": [
"# Calculate the slope of VO2 Breath (first derivative)\n",
"df['vo2_breath_slope'] = df['VO2 Breath_smoothed'].diff()\n",
"\n",
"# Find points where slope is consistently zero or negative\n",
"# We'll use a rolling window to check for consistent negative/zero slope\n",
"window = len(df) // 3 # Number of consecutive points to check\n",
"\n",
"# Calculate rolling mean of slope to smooth out noise\n",
"df['vo2_breath_slope_smoothed'] = df['vo2_breath_slope'].rolling(window=window).mean()\n",
"\n",
"# Find where slope becomes consistently zero or negative\n",
"mask = df['vo2_breath_slope_smoothed'] <= 0\n",
"consistent_negative_indices = mask[mask].index\n",
"\n",
"if len(consistent_negative_indices) > 0:\n",
" # Find the first point where slope becomes consistently negative/zero\n",
" vo2_max_idx = consistent_negative_indices[0]\n",
" vo2_max_row = df.loc[vo2_max_idx]\n",
" \n",
" print(f\"VO2 Max detected at index {vo2_max_idx}:\")\n",
" print(f\"Time: {vo2_max_row['T(sec)']} seconds\")\n",
" print(f\"VO2 Breath: {vo2_max_row['VO2 Breath_smoothed']:.1f} ml/breath\")\n",
" print(f\"VO2: {vo2_max_row['VO2(ml/min)_smoothed']:.1f} ml/min\")\n",
" print(f\"VO2 per kg: {vo2_max_row['VO2(ml/min)_smoothed']/weight:.1f} ml/kg/min\")\n",
" print(f\"Heart Rate: {vo2_max_row['HR(bpm)_smoothed']:.1f} bpm\")\n",
" print(f\"Speed: {vo2_max_row['Speed']} km/h\")\n",
" print(f\"VO2 Breath Slope: {vo2_max_row['vo2_breath_slope_smoothed']:.2f}\")\n",
"else:\n",
" # If no consistent negative slope found, use the maximum VO2 Breath value\n",
" vo2_max_idx = df['VO2 Breath_smoothed'].idxmax()\n",
" vo2_max_row = df.loc[vo2_max_idx]\n",
" \n",
" print(f\"No consistent negative slope found. Using peak VO2 Breath at index {vo2_max_idx}:\")\n",
" print(f\"Time: {vo2_max_row['T(sec)']} seconds\")\n",
" print(f\"VO2 Breath: {vo2_max_row['VO2 Breath_smoothed']:.1f} ml/breath\")\n",
" print(f\"VO2: {vo2_max_row['VO2(ml/min)_smoothed']:.1f} ml/min\")\n",
" print(f\"VO2 per kg: {vo2_max_row['VO2(ml/min)_smoothed']/weight:.1f} ml/kg/min\")\n",
" print(f\"Heart Rate: {vo2_max_row['HR(bpm)_smoothed']:.1f} bpm\")\n",
" print(f\"Speed: {vo2_max_row['Speed']} km/h\")"
]
},
{
"cell_type": "code",
"execution_count": 18,
"id": "c3b2cc59",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"VO2 Pulse and HR slopes diverge consistently starting at index 89:\n",
"Time: 485.0 seconds\n",
"VO2 Pulse (smoothed): 13.91\n",
"Heart Rate (smoothed): 136.2 bpm\n",
"VO2 Pulse Slope: 0.672\n",
"HR Slope: 1.000\n",
"Slope Difference: 1.006\n",
"VO2: 1897.8 ml/min\n",
"Speed: 4.5 km/h\n",
"Threshold used: 0.615\n"
]
}
],
"source": [
"# Calculate slopes for both VO2 Pulse and HR\n",
"df['vo2_pulse_slope'] = df['VO2 Pulse_smoothed'].diff()\n",
"df['hr_slope'] = df['HR(bpm)_smoothed'].diff()\n",
"\n",
"# Calculate the difference between the slopes\n",
"df['slope_difference'] = abs(df['vo2_pulse_slope'] - df['hr_slope'])\n",
"\n",
"# Find where the slope difference becomes consistently large (slopes diverge)\n",
"# Use a rolling window to smooth out noise\n",
"window_size = len(df) // 5 # Adjust window size as needed\n",
"df['slope_difference_smoothed'] = df['slope_difference'].rolling(window=window_size).mean()\n",
"\n",
"# Find the threshold - we'll use the 75th percentile of slope differences as threshold\n",
"threshold = df['slope_difference_smoothed'].quantile(0.75)\n",
"\n",
"# Find points where slope difference exceeds threshold\n",
"divergence_mask = df['slope_difference_smoothed'] > threshold\n",
"divergence_indices = divergence_mask[divergence_mask].index\n",
"\n",
"if len(divergence_indices) > 0:\n",
" # Find the first sustained divergence point\n",
" min_consecutive_points = 5\n",
" consistent_divergence_idx = None\n",
" \n",
" for start_idx in divergence_indices:\n",
" # Check if divergence is sustained for consecutive points\n",
" consecutive_count = 0\n",
" for j in range(start_idx, min(start_idx + min_consecutive_points, len(df))):\n",
" if j in divergence_indices:\n",
" consecutive_count += 1\n",
" else:\n",
" break\n",
" \n",
" if consecutive_count >= min_consecutive_points:\n",
" consistent_divergence_idx = start_idx\n",
" break\n",
" \n",
" if consistent_divergence_idx is not None:\n",
" divergence_row = df.iloc[consistent_divergence_idx]\n",
" \n",
" print(f\"VO2 Pulse and HR slopes diverge consistently starting at index {consistent_divergence_idx}:\")\n",
" print(f\"Time: {divergence_row['T(sec)']} seconds\")\n",
" print(f\"VO2 Pulse (smoothed): {divergence_row['VO2 Pulse_smoothed']:.2f}\")\n",
" print(f\"Heart Rate (smoothed): {divergence_row['HR(bpm)_smoothed']:.1f} bpm\")\n",
" print(f\"VO2 Pulse Slope: {divergence_row['vo2_pulse_slope']:.3f}\")\n",
" print(f\"HR Slope: {divergence_row['hr_slope']:.3f}\")\n",
" print(f\"Slope Difference: {divergence_row['slope_difference_smoothed']:.3f}\")\n",
" print(f\"VO2: {divergence_row['VO2(ml/min)_smoothed']:.1f} ml/min\")\n",
" print(f\"Speed: {divergence_row['Speed']} km/h\")\n",
" print(f\"Threshold used: {threshold:.3f}\")\n",
" else:\n",
" print(f\"No sustained divergence found. Threshold: {threshold:.3f}\")\n",
" # Show the point with maximum slope difference instead\n",
" max_diff_idx = df['slope_difference_smoothed'].idxmax()\n",
" max_diff_row = df.iloc[max_diff_idx]\n",
" \n",
" print(f\"\\nPoint with maximum slope difference at index {max_diff_idx}:\")\n",
" print(f\"Time: {max_diff_row['T(sec)']} seconds\")\n",
" print(f\"VO2 Pulse (smoothed): {max_diff_row['VO2 Pulse_smoothed']:.2f}\")\n",
" print(f\"Heart Rate (smoothed): {max_diff_row['HR(bpm)_smoothed']:.1f} bpm\")\n",
" print(f\"Slope Difference: {max_diff_row['slope_difference_smoothed']:.3f}\")\n",
"else:\n",
" print(\"No significant slope divergence found between VO2 Pulse and HR\")"
]
},
{
"cell_type": "code",
"execution_count": 19,
"id": "672d68f3",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Maximum FAT_smoothed occurs at index 30:\n",
"Heart Rate (smoothed): 96.7 bpm\n",
"FAT (smoothed): 3.894 kcal/min\n"
]
}
],
"source": [
"max_fat_smoothed_idx = df['FAT_smoothed'].idxmax()\n",
"max_fat_smoothed_row = df.loc[max_fat_smoothed_idx]\n",
"max_heart_rate = 220 - keirstyn_data['Age'].iloc[0]\n",
"\n",
"print(f\"Maximum FAT_smoothed occurs at index {max_fat_smoothed_idx}:\")\n",
"print(f\"Heart Rate (smoothed): {max_fat_smoothed_row['HR(bpm)_smoothed']:.1f} bpm\")\n",
"print(f\"FAT (smoothed): {max_fat_smoothed_row['FAT_smoothed']:.3f} kcal/min\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "fe3b7605",
"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.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
+354
View File
@@ -0,0 +1,354 @@
import base64
def image_to_base64(image_path):
try:
with open(image_path, "rb") as image_file:
return base64.b64encode(image_file.read()).decode("utf-8")
except FileNotFoundError:
print(f"Warning: Image not found at {image_path}")
return ""
### Defining Page Contexts ###
page_1_context = {
"name": "Keirstyn",
"surname": "Moran",
"date": "July 29, 2025",
}
page_2_context = {
"content": "This is page 2 content",
}
page_3_context = {
"patient_name": "Keirstyn Moran",
}
page_4_context = {
"body_composition_chart": image_to_base64(
"/home/oluwasanmi/Documents/Work/MKD/report_generation/graphs/body_composition_chart.png"
),
"body_fat_chart": image_to_base64(
"/home/oluwasanmi/Documents/Work/MKD/report_generation/graphs/body_fat_percent_chart.png"
),
"fat_percentage": "22.4",
}
page_5_context = {
"metabolism_chart": image_to_base64(
"/home/oluwasanmi/Documents/Work/MKD/report_generation/graphs/metabolism_chart.png"
),
"fuel_source_chart": image_to_base64(
"/home/oluwasanmi/Documents/Work/MKD/report_generation/graphs/fuel_source_chart.png"
),
"resting_calories": 1385,
"neat_calories": "null",
"weight_loss_calories": "null",
"weight_loss_rate": "1lb/week",
"total_calories": "null",
}
page_6_context = {
"patient_name": "Keirstyn Moran",
"age": "34",
"height": "5'4\"",
"weight": "123lbs",
"focus": "Endurance",
"deficit_calories": "1725KCals",
"deficit_protein": "120g Protein",
"deficit_carbs": "155g Carbs",
"deficit_fat": "69g Fat",
"deficit_fiber": "25g Fibre",
"refeed_weekday_calories": "1615KCals",
"refeed_weekday_protein": "120g Protein",
"refeed_weekday_carbs": "142g Carbs",
"refeed_weekday_fat": "63g Fat",
"refeed_weekday_fiber": "24g Fibre",
"refeed_weekend_calories": "2000KCals",
"refeed_weekend_protein": "120g Protein",
"refeed_weekend_carbs": "190g Carbs",
"refeed_weekend_fat": "84g Fat",
"refeed_weekend_fiber": "30g Fibre",
"protein_percentage": "28%",
"carbs_percentage": "36%",
"fats_percentage": "36%",
"page_number": "6",
}
page_7_context = {
"indication": "No Respiratory Capacity Limitation",
"peak_vt": 3.2,
"peak_vt_bpm": 198,
"peak_vt_zone": 3,
"fev1_percentage": 85,
"lung_analysis_chart": image_to_base64("/home/oluwasanmi/Documents/Work/MKD/report_generation/graphs/spirometry_chart.png"),
"respiratory_analysis_chart": image_to_base64(
"/home/oluwasanmi/Documents/Work/MKD/report_generation/graphs/respiratory.png"
),
}
page_8_context = {
"vo2_max_value": "49.5",
"vo2_max_percentile": "100th percentile",
"age_range": "30-39",
"very_poor_range": "19.0-24.1",
"poor_range": "24.1-28.2",
"fair_range": "28.2-32.2",
"good_range": "32.2-35.7",
"excellent_range": "35.7-45.8",
"superior_range": "45.8+",
"zone1_percentage": "55-65% of Max Heart Rate",
"zone2_percentage": "65-75% of Max Heart Rate",
"zone3_percentage": "80-85% of Max Heart Rate",
"zone4_percentage": "85-88% of Max Heart Rate",
"zone5_percentage": "90% of Max Heart Rate",
"zone1_bpm": "81-96bpm",
"zone2_bpm": "96-100bpm",
"zone3_bpm": "100-178bpm",
"zone4_bpm": "178-189bpm",
"zone5_bpm": "189-199bpm",
"zone1_speed": "3.5mph",
"zone2_speed": "3.5-4.0mph",
"zone3_speed": "4.0-6.5mph",
"zone4_speed": "6.5-7.0mph",
"zone5_speed": "7.0-8.0mph",
"zone1_incline": "2% Incline",
"zone2_incline": "2% Incline",
"zone3_incline": "2% Incline",
"zone4_incline": "2% Incline",
"zone5_incline": "2% Incline",
"zone1_pace": "10:39min/km Pace",
"zone2_pace": "10:39-9:19min/km Pace",
"zone3_pace": "9:19-5:44min/km Pace",
"zone4_pace": "5:44-5:20min/km Pace",
"zone5_pace": "5:20-4:40min/km Pace",
"zone1_calories": "4.4kcals/minute",
"zone2_calories": "5.9kcals/minute",
"zone3_calories": "9.4kcals/minute",
"zone4_calories": "12.5kcals/minute",
"zone5_calories": "12.8kcals/minute",
"zone1_carb": "Avg: 0.4g/min Carb Utilization",
"zone2_carb": "Avg: 0.6g/min Carb Utilization",
"zone3_carb": "Avg: 1.9g/min Carb Utilization",
"zone4_carb": "Avg: 2.9g/min Carb Utilization",
"zone5_carb": "Avg: 3.1g/min Carb Utilization",
"zone1_breaths": "Avg: 27 breaths",
"zone2_breaths": "Avg: 28 breaths",
"zone3_breaths": "Avg: 31 breaths",
"zone4_breaths": "Avg: 42 breaths",
"zone5_breaths": "Avg: 51 breaths",
"zone1_breath_range": "Ideal Range: 15-20 breaths",
"zone2_breath_range": "Ideal Range: 20-25 breaths",
"zone3_breath_range": "Ideal Range: 25-30 breaths",
"zone4_breath_range": "Ideal Range: 30-35 breaths",
"zone5_breath_range": "Ideal Range: 40+ breaths",
}
page_9_context = {
"fuel_utilization_chart": image_to_base64(
"/home/oluwasanmi/Documents/Work/MKD/report_generation/graphs/fuel_utilization_chart.png"
),
}
page_10_context = {
"vo2_pulse_drop_bpm": "180 bpm",
"vo2_pulse_drop_zone": "Zone 4",
"vo2_pulse_chart": image_to_base64(
"/home/oluwasanmi/Documents/Work/MKD/report_generation/graphs/vo2_pulse_chart.png"
),
"vo2_breath_drop_bpm": "173 bpm",
"vo2_breath_drop_zone": "Zone 3",
"vo2_breath_chart": image_to_base64(
"/home/oluwasanmi/Documents/Work/MKD/report_generation/graphs/vo2_breath_chart.png"
),
}
page_11_context = {
"fat_max_optimal": "*Optimal 10-12Kcals/minute",
"fat_max_value": "3.8Kcals/min",
"fat_max_heart_rate": "49% of Max Heart Rate",
"fat_max_bpm": "97 bpm",
"crossover_bpm": "100bpm",
"crossover_heart_rate": "51% of Max Heart Rate",
"fat_metabolism_note": "100bpm at a speed of 4.0mph and incline of 2%",
"fat_metabolism_chart": image_to_base64(
"/home/oluwasanmi/Documents/Work/MKD/report_generation/graphs/fat_metabolism_chart.png"
),
"cardiac_recovery_time": "(1 minute)",
"cardiac_recovery_percentage": "33%",
"metabolic_recovery_time": "(2 minute)",
"metabolic_recovery_percentage": "65%",
"breath_recovery_time": "(2.5 minute)",
"breath_recovery_percentage": "76%",
"recovery_chart": image_to_base64(
"/home/oluwasanmi/Documents/Work/MKD/report_generation/graphs/recovery_chart.png"
),
"resting_heart_rate": "53bpm",
"hr_age_range": "26-35",
"hr_poor": "82bpm +",
"hr_below_avg": "75-81bpm",
"hr_average": "71-74bpm",
"hr_above_avg": "66-70bpm",
"hr_good": "62-65bpm",
"hr_excellent": "55-61bpm",
"hr_athlete": "44-54bpm",
}
page_12_context = {
"right_leg": image_to_base64(
"/home/oluwasanmi/Documents/Work/MKD/report_generation/graphs/right_leg.png"
),
"left_leg": image_to_base64(
"/home/oluwasanmi/Documents/Work/MKD/report_generation/graphs/left_leg.png"
),
}
page_13_context = {
"patient_name": "Keirstyn Moran",
"age": "34",
"height": "5'4\"",
"weight": "123lbs",
"focus": "Endurance",
"zone2_frequency": "3-4x/week",
"zone2_duration": "40+ minutes",
"zone2_hr_range": "96-110bpm",
"zone2_speed": "3.5-4.0mph",
"zone2_incline": "2% Incline",
"zone3_frequency": "1-2x/week",
"zone3_duration": "10-20 minutes",
"zone3_hr_range": "100-178bpm",
"zone3_speed": "4.0-6.5mph",
"zone3_incline": "2% Incline",
"zone3_target_hr": "140bpm",
"zone3_recovery_speed": "3.5mph",
"zone3_recovery_incline": "2% Incline",
"zone1_hr_range": "81-96bpm",
"zone1_duration": "4-8 minutes",
"zone3_repeats": "2-3 times",
"short_sets": "8-10",
"short_duration": "10-30 seconds",
"short_zone": "5",
"short_rpe": "10",
"short_recovery": "20-60 seconds",
"medium_sets": "6-8",
"medium_duration": "30-90 seconds",
"medium_zone": "4",
"medium_rpe": "8-9",
"medium_recovery": "30-90 seconds",
"long_sets": "4-6",
"long_duration": "5-10 minutes",
"long_zone": "3/4",
"long_rpe": "7-8",
"long_recovery": "2.5-5 minutes",
"tempo_sets": "2-3",
"tempo_duration": "10-20 minutes",
"tempo_zone": "3",
"tempo_rpe": "6-7",
"tempo_recovery": "4-8 minutes",
"cardio_sets": "1",
"cardio_duration": ">40 minutes",
"cardio_zone": "2",
"cardio_rpe": "4-5",
"cardio_recovery": "N/A",
"week1_mon_zone": "Zone 2",
"week1_mon_duration": "45 mins",
"week1_tue_zone": "Zone 2",
"week1_tue_duration": "45 mins",
"week1_wed_zone": "Zone 3",
"week1_wed_duration1": "10mins On",
"week1_wed_duration2": "8mins Rest",
"week1_wed_sets": "x2",
"week1_thu_content": "",
"week1_fri_zone": "Zone 2",
"week1_fri_duration": "45 mins",
"week1_sat_content": "",
"week1_sun_content": "",
"week2_mon_zone": "Zone 2",
"week2_mon_duration": "50 mins",
"week2_tue_zone": "Zone 2",
"week2_tue_duration": "50 mins",
"week2_wed_zone": "Zone 3",
"week2_wed_duration1": "10mins On",
"week2_wed_duration2": "6mins Rest",
"week2_wed_sets": "x2",
"week2_thu_content": "",
"week2_fri_zone": "Zone 2",
"week2_fri_duration": "50 mins",
"week2_sat_content": "",
"week2_sun_content": "",
"contact_email": "info@ishplabs.com",
"website": "www.ishplabs.com",
"social": "@ishplabs",
"page_number": "13",
}
page_14_context = {
"patient_name": "Keirstyn Moran",
"contact_email": "info@ishplabs.com",
"website": "www.ishplabs.com",
"social": "@ishplabs",
"page_number": "14",
}
page_15_context = {
"patient_name": "Keirstyn Moran",
"contact_email": "info@ishplabs.com",
"website": "www.ishplabs.com",
"social": "@ishplabs",
"page_number": "15",
}
page_16_context = {
"patient_name": "Keirstyn Moran",
"contact_email": "info@ishplabs.com",
"website": "www.ishplabs.com",
"social": "@ishplabs",
"page_number": "16",
}
page_17_context = {
"patient_name": "Keirstyn Moran",
"contact_email": "info@ishplabs.com",
"website": "www.ishplabs.com",
"social": "@ishplabs",
"page_number": "17",
}
page_18_context = {
"body_fat_percentage_chart": image_to_base64(
"/home/oluwasanmi/Documents/Work/MKD/report_generation/fat_percentage_master_chart.png"
),
}
page_19_context = {
"patient_name": "Keirstyn Moran",
"contact_email": "info@ishplabs.com",
"website": "www.ishplabs.com",
"social": "@ishplabs",
"page_number": "19",
}
context_list = [
page_1_context,
page_2_context,
page_3_context,
page_4_context,
page_5_context,
page_6_context,
page_7_context,
page_8_context,
page_9_context,
page_10_context,
page_11_context,
page_12_context,
page_13_context,
page_14_context,
page_15_context,
page_16_context,
page_17_context,
page_18_context,
page_19_context,
]
+319
View File
@@ -0,0 +1,319 @@
import base64
from pathlib import Path
from typing import Dict, List, Optional, Tuple
import matplotlib.pyplot as plt
import pandas as pd
class ReportGenerator:
def __init__(self):
self.pnoe_df = None
self.patient_df = None
self.spirometry_df = None
self.seca_df = None
self.patient_info = {}
self.charts_dir = Path("graphs")
self.charts_dir.mkdir(exist_ok=True)
def load_data(
self,
pnoe_path: str,
patient_path: str,
spirometry_path: str,
seca_path: str = None,
):
"""Load all required datasets"""
self.pnoe_df = pd.read_csv(pnoe_path, delimiter=";")
self.patient_df = pd.read_csv(patient_path)
self.spirometry_df = pd.read_csv(spirometry_path)
if seca_path:
self.seca_df = pd.read_excel(seca_path)
# Apply preprocessing
self._preprocess_data()
def _preprocess_data(self):
"""Apply preprocessing steps from your notebook"""
# Convert to numeric
self.pnoe_df = self.pnoe_df.apply(pd.to_numeric, errors="ignore")
# Calculate derived columns
self.pnoe_df["VO2 Pulse"] = (
self.pnoe_df["VO2(ml/min)"] / self.pnoe_df["HR(bpm)"]
)
self.pnoe_df["VO2 Breath"] = (
self.pnoe_df["VO2(ml/min)"] / self.pnoe_df["BF(bpm)"]
)
self.pnoe_df["CHO"] = (
self.pnoe_df["EE(kcal/min)"] * self.pnoe_df["CARBS(%)"] / 100
)
self.pnoe_df["FAT"] = (
self.pnoe_df["EE(kcal/min)"] * self.pnoe_df["FAT(%)"] / 100
)
# Apply smoothing
window_size = 10
columns_to_smooth = [
"VO2(ml/min)",
"VCO2(ml/min)",
"HR(bpm)",
"VT(l)",
"BF(bpm)",
"VE(l/min)",
"VO2 Pulse",
"VO2 Breath",
"CHO",
"FAT",
]
for col in columns_to_smooth:
if col in self.pnoe_df.columns:
self.pnoe_df[f"{col}_smoothed"] = (
self.pnoe_df[col].rolling(window=window_size, min_periods=1).mean()
)
def extract_patient_info(self, last_name: str) -> Dict:
"""Extract patient information from datasets"""
if self.seca_df is not None:
patient_data = self.seca_df[
self.seca_df["LastName"].str.contains(last_name, case=False, na=False)
]
if not patient_data.empty:
row = patient_data.iloc[0]
self.patient_info = {
"name": f"{row.get('FirstName', '')} {last_name}",
"age": int(row.get("Age", 0)),
"height": f"{row.get('Height', '')}",
"weight": float(row.get("Weight", 0)),
"gender": row.get("Gender", "").lower(),
"fat_percentage": float(row.get("Adult_FMP", 0)),
}
return self.patient_info
def calculate_spirometry_metrics(self) -> Dict:
"""Calculate spirometry-related metrics"""
metrics = {}
# Extract key spirometry values
for param in ["FVC", "FEV1", "FEV1/FVC%"]:
row = self.spirometry_df.loc[self.spirometry_df["Parameters"] == param]
if not row.empty:
metrics[
f"{param.lower().replace('/', '_').replace('%', '_pct')}_best"
] = row["Best"].values[0]
metrics[
f"{param.lower().replace('/', '_').replace('%', '_pct')}_pred"
] = row["%Pred."].values[0]
return metrics
def calculate_pnoe_metrics(self) -> Dict:
"""Calculate all Pnoe-derived metrics"""
metrics = {}
# Basic metrics
metrics["vo2_max"] = self.pnoe_df["VO2(ml/min)_smoothed"].max()
metrics["vo2_max_per_kg"] = metrics["vo2_max"] / self.patient_info["weight"]
# Peak VT
peak_vt_idx = self.pnoe_df["VT(l)_smoothed"].idxmax()
peak_vt_row = self.pnoe_df.loc[peak_vt_idx]
metrics["peak_vt"] = peak_vt_row["VT(l)_smoothed"]
metrics["peak_vt_hr"] = peak_vt_row["HR(bpm)_smoothed"]
# Fat burning metrics
fat_max_idx = self.pnoe_df["FAT_smoothed"].idxmax()
fat_max_row = self.pnoe_df.loc[fat_max_idx]
metrics["fat_max_value"] = fat_max_row["FAT_smoothed"]
metrics["fat_max_hr"] = fat_max_row["HR(bpm)_smoothed"]
# Calculate zones (simplified from your logic)
metrics.update(self._calculate_hr_zones())
# VT1/VT2 detection
vt1, vt2 = self._detect_thresholds()
metrics["vt1"] = vt1
metrics["vt2"] = vt2
return metrics
def _detect_thresholds(self) -> Tuple[Optional[Dict], Optional[Dict]]:
"""Detect VT1 and VT2 thresholds"""
# VT1: First crossover where carbs > fat
condition = self.pnoe_df["CHO_smoothed"] > self.pnoe_df["FAT_smoothed"]
crossover_indices = condition[condition].index
vt1 = None
if len(crossover_indices) > 0:
vt1_idx = crossover_indices[0]
vt1_row = self.pnoe_df.loc[vt1_idx]
vt1 = {
"HeartRate": vt1_row["HR(bpm)_smoothed"],
"Speed": vt1_row["Speed"],
"Time": vt1_row["T(sec)"],
}
# VT2: Ventilation inflection (simplified)
ve_slope = self.pnoe_df["VE(l/min)_smoothed"].diff()
second_derivative = ve_slope.diff()
vt2_idx = second_derivative.idxmax()
vt2 = None
if pd.notna(vt2_idx):
vt2_row = self.pnoe_df.loc[vt2_idx]
vt2 = {
"HeartRate": vt2_row["HR(bpm)_smoothed"],
"Speed": vt2_row["Speed"],
"Time": vt2_row["T(sec)"],
}
return vt1, vt2
def _calculate_hr_zones(self) -> Dict:
"""Calculate heart rate zones"""
max_hr = 220 - self.patient_info["age"]
# Simplified zone calculation - you can make this more sophisticated
zones = {
"zone1_bpm": f"{int(max_hr * 0.55)}-{int(max_hr * 0.65)}bpm",
"zone2_bpm": f"{int(max_hr * 0.65)}-{int(max_hr * 0.75)}bpm",
"zone3_bpm": f"{int(max_hr * 0.75)}-{int(max_hr * 0.85)}bpm",
"zone4_bpm": f"{int(max_hr * 0.85)}-{int(max_hr * 0.95)}bpm",
"zone5_bpm": f"{int(max_hr * 0.95)}+bpm",
}
return zones
def generate_charts(self) -> Dict[str, str]:
"""Generate all charts and return base64 encoded versions"""
charts = {}
# Generate fuel utilization chart
charts["fuel_utilization_chart"] = self._create_fuel_chart()
# Generate VO2 pulse chart
charts["vo2_pulse_chart"] = self._create_vo2_pulse_chart()
# Generate body composition chart
charts["body_composition_chart"] = self._create_body_comp_chart()
# Add more chart generation methods...
return charts
def _create_fuel_chart(self) -> str:
"""Create and save fuel utilization chart"""
# Use your existing chart code but make it dynamic
speed_groups = self.pnoe_df.groupby("Speed").mean(numeric_only=True).round(1)
speed_groups = speed_groups.iloc[1:-1]
filtered_data = speed_groups[
(speed_groups.index >= 3.5) & (speed_groups.index <= 7.5)
]
plt.figure(figsize=(15, 8))
# ... your chart code here ...
chart_path = self.charts_dir / "fuel_utilization_chart.png"
plt.savefig(chart_path, dpi=300)
plt.close()
return self._image_to_base64(chart_path)
def _create_vo2_pulse_chart(self) -> str:
"""Create VO2 pulse chart"""
# Your VO2 pulse chart code here
chart_path = self.charts_dir / "vo2_pulse_chart.png"
# ... chart generation code ...
return self._image_to_base64(chart_path)
def _create_body_comp_chart(self) -> str:
"""Create body composition chart"""
# Your body composition chart code here
chart_path = self.charts_dir / "body_composition_chart.png"
# ... chart generation code ...
return self._image_to_base64(chart_path)
def _image_to_base64(self, image_path: Path) -> str:
"""Convert image to base64"""
try:
with open(image_path, "rb") as image_file:
return base64.b64encode(image_file.read()).decode("utf-8")
except FileNotFoundError:
return ""
def generate_all_contexts(self, last_name: str = "Moran") -> List[Dict]:
"""Main method to generate all page contexts"""
# Extract patient info
self.extract_patient_info(last_name)
# Calculate metrics
spirometry_metrics = self.calculate_spirometry_metrics()
pnoe_metrics = self.calculate_pnoe_metrics()
# Generate charts
charts = self.generate_charts()
# Build contexts for each page
contexts = []
# Page 1
contexts.append(
{
"name": self.patient_info["name"],
"surname": last_name,
"date": "July 29, 2025",
}
)
# Page 2-6 (add as needed)
for i in range(5):
contexts.append({})
# Page 7 - Spirometry
contexts.append(
{
"peak_vt": pnoe_metrics["peak_vt"],
"peak_vt_bpm": pnoe_metrics["peak_vt_hr"],
"fev1_percentage": (
pnoe_metrics["peak_vt"] / spirometry_metrics["fvc_best"]
)
* 100,
"lung_analysis_chart": charts.get("spirometry_chart", ""),
"respiratory_analysis_chart": charts.get("respiratory_chart", ""),
}
)
# Page 8 - VO2 Max and Zones
contexts.append(
{
"vo2_max_value": f"{pnoe_metrics['vo2_max_per_kg']:.1f}",
"age_range": f"{self.patient_info['age'] // 10 * 10}-{self.patient_info['age'] // 10 * 10 + 9}",
**pnoe_metrics, # Include all zone calculations
}
)
# Continue for all pages...
# Add remaining pages as needed
return contexts
# Usage for backend service
def generate_report(
pnoe_file, patient_file, spirometry_file, seca_file=None, patient_name="Moran"
):
"""Main function for backend service"""
generator = ReportGenerator()
generator.load_data(pnoe_file, patient_file, spirometry_file, seca_file)
return generator.generate_all_contexts(patient_name)
# Example usage
if __name__ == "__main__":
contexts = generate_report(
"data/Pnoe_20250729_1550-Moran_Keirstyn.csv",
"data/patient_data.csv",
"data/spirometry_data.csv",
"data/SECA body comp for all patients.xlsx",
)
print(f"Generated {len(contexts)} page contexts")
+12
View File
@@ -0,0 +1,12 @@
Parameters,Best,LLN,Pred.,%Pred.,ZScore,PRE#1,PRE#2,PRE#3
FVC,L,4.24,3.03,3.79,112.0,0.95,4.24,4.17,4.15
FEV1,L,3.26,2.53,3.16,103.3,0.28,3.26,3.21,3.14
FEV1/FVC%,76.89,72.47,83.78,91.8,-1.05,76.9,77.0,75.7
PEF,L/m,684,222,384,178.7,-,444,438,684
FEF2575,L/s,2.74,2.15,3.42,80.2,-0.84,2.74,2.68,2.48
FEF25,L/s,6.08,-,-,-,6.08,6.0,5.53
FEF50,L/s,3.06,-,-,-,3.06,3.1,2.77
FEF75,L/s,1.06,0.71,1.41,75.1,-0.72,1.06,1.12,0.94
PEFTime,ms,-,-,79,-,79,49,39
Evol,mL,-,-,78.0,-,78.0,77.0,197.0
FEV6,L,4.22,3.03,3.79,111.4,-,4.22,4.17,4.13
1 Parameters,Best,LLN,Pred.,%Pred.,ZScore,PRE#1,PRE#2,PRE#3
2 FVC,L,4.24,3.03,3.79,112.0,0.95,4.24,4.17,4.15
3 FEV1,L,3.26,2.53,3.16,103.3,0.28,3.26,3.21,3.14
4 FEV1/FVC%,76.89,72.47,83.78,91.8,-1.05,76.9,77.0,75.7
5 PEF,L/m,684,222,384,178.7,-,444,438,684
6 FEF2575,L/s,2.74,2.15,3.42,80.2,-0.84,2.74,2.68,2.48
7 FEF25,L/s,6.08,-,-,-,6.08,6.0,5.53
8 FEF50,L/s,3.06,-,-,-,3.06,3.1,2.77
9 FEF75,L/s,1.06,0.71,1.41,75.1,-0.72,1.06,1.12,0.94
10 PEFTime,ms,-,-,79,-,79,49,39
11 Evol,mL,-,-,78.0,-,78.0,77.0,197.0
12 FEV6,L,4.22,3.03,3.79,111.4,-,4.22,4.17,4.13
Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

+942
View File
@@ -0,0 +1,942 @@
import base64
from pathlib import Path
from typing import Dict
import matplotlib.pyplot as plt
import matplotlib.transforms as mtransforms
import numpy as np
import pandas as pd
import seaborn as sns
from matplotlib.patches import FancyBboxPatch
class GraphGenerator:
def __init__(self, charts_dir: str = "graphs"):
"""Initialize the GraphGenerator with output directory for charts"""
self.charts_dir = Path(charts_dir)
self.charts_dir.mkdir(exist_ok=True)
def _image_to_base64(self, image_path: Path) -> str:
"""Convert image to base64 string"""
try:
with open(image_path, "rb") as image_file:
return base64.b64encode(image_file.read()).decode("utf-8")
except FileNotFoundError:
return ""
def generate_respiratory_chart(
self, df: pd.DataFrame, save_as_base64: bool = False
) -> str:
"""Generate respiratory chart showing VT and Speed over time"""
# Get phase times for background regions
first_unique_phase = df.drop_duplicates(subset="PHASE")
phase_times = first_unique_phase["T(sec)"].tolist()
plt.figure(figsize=(18, 5))
ax1 = plt.subplot()
# Plot VT with step-like appearance
sns.lineplot(data=df, x="T(sec)", y="VT(l)_smoothed", label="VT (L)")
ax1.set_xlabel("Time (sec)")
ax1.set_ylabel("VT (L)")
ax1.grid(True, alpha=0.1)
ax1.set_ylim(0, min(8, df["VT(l)_smoothed"].max()))
# Plot speed as step function on secondary y-axis
ax2 = ax1.twinx()
ax1.set_xticks(np.arange(0, df["T(sec)"].max() + 200, 200))
line2 = sns.lineplot(
data=df,
x="T(sec)",
y="Speed",
color="green",
ax=ax2,
drawstyle="steps-post",
linewidth=2,
label="Speed",
)
ax2.set_ylabel("Speed")
ax2.set_ylim(0, min(30, df["Speed"].max()) + 1)
# Remove default legends first
ax1.get_legend().remove()
ax2.get_legend().remove()
# Combine legends from both axes in the top left
lines1, labels1 = ax1.get_legend_handles_labels()
lines2, labels2 = ax2.get_legend_handles_labels()
ax1.legend(lines1 + lines2, labels1 + labels2, loc="upper left")
# Add colored background regions
if len(phase_times) >= 4:
ax1.axvspan(0, phase_times[1], alpha=0.2, color="lightblue")
ax1.axvspan(phase_times[1], phase_times[2], alpha=0.2, color="purple")
ax1.axvspan(phase_times[2], phase_times[3], alpha=0.2, color="lightgreen")
ax1.axvspan(phase_times[3], df["T(sec)"].max(), alpha=0.2, color="blue")
chart_path = self.charts_dir / "respiratory.png"
plt.savefig(chart_path, dpi=300, bbox_inches="tight")
plt.close()
return self._image_to_base64(chart_path) if save_as_base64 else str(chart_path)
def generate_fuel_utilization_chart(
self, df: pd.DataFrame, save_as_base64: bool = False
) -> str:
"""Generate fuel utilization chart with stacked bars showing fat vs carbs"""
# Group by speed and calculate mean for numeric columns only
speed_groups = df.groupby("Speed").mean(numeric_only=True).round(1)
speed_groups = speed_groups.iloc[1:-1]
filtered_data = speed_groups[
(speed_groups.index >= 3.5) & (speed_groups.index <= 7.5)
]
plt.figure(figsize=(15, 8))
plt.style.use("default")
# Create stage labels and positions
stage_labels = [f"Stage {i}" for i in range(1, len(filtered_data) + 1)]
x_positions = np.arange(len(filtered_data))
# Calculate fat and carbs energy expenditure from percentages
fat_ee = filtered_data["EE(kcal/min)"] * filtered_data["FAT(%)"] / 100
carbs_ee = filtered_data["EE(kcal/min)"] * filtered_data["CARBS(%)"] / 100
# Create the main axis for the stacked bars
ax1 = plt.gca()
# Create stacked bar chart with colors
ax1.bar(x_positions, fat_ee, color="#1f77b4", alpha=0.8, width=0.6, label="Fat")
ax1.bar(
x_positions,
carbs_ee,
bottom=fat_ee,
color="#ff7f0e",
alpha=0.8,
width=0.6,
label="Carbs",
)
# Set labels and formatting for primary axis
ax1.set_xlabel("", fontsize=12)
ax1.set_ylabel("Fuel (kcal/min)", fontsize=12)
ax1.set_ylim(0, 20)
# Add individual values on each bar segment
for i, (fat_val, carb_val, total_val) in enumerate(
zip(fat_ee, carbs_ee, filtered_data["EE(kcal/min)"])
):
if fat_val > 0.3: # Fat value
ax1.text(
i,
fat_val / 2,
f"{fat_val:.1f}",
ha="center",
va="center",
fontsize=9,
fontweight="bold",
color="white",
)
if carb_val > 0.3: # Carbs value
ax1.text(
i,
fat_val + carb_val / 2,
f"{carb_val:.1f}",
ha="center",
va="center",
fontsize=9,
fontweight="bold",
color="white",
)
# Total EE
ax1.text(
i,
total_val + 0.5,
f"{total_val:.1f} kcal",
ha="center",
va="bottom",
fontsize=10,
fontweight="bold",
color="black",
)
# Add speed labels below x-axis
for i, speed in enumerate(filtered_data.index):
ax1.text(i, -1.5, f"{speed:.1f} mph", ha="center", va="top", fontsize=9)
ax1.text(
i,
-2.8,
f"{speed * 1.609:.1f} min/km",
ha="center",
va="top",
fontsize=8,
color="gray",
)
# Create secondary y-axis for heart rate
ax2 = ax1.twinx()
# Plot heart rate line
ax2.plot(
x_positions,
filtered_data["HR(bpm)"],
marker="o",
linewidth=3,
markersize=8,
color="red",
label="Heart Rate",
)
# Set heart rate axis formatting
ax2.set_ylabel("Heart Rate (bpm)", fontsize=12, color="red")
ax2.tick_params(axis="y", labelcolor="red")
ax2.set_ylim(0, 220)
# Add HR values above the points
for i, hr in enumerate(filtered_data["HR(bpm)"]):
ax2.text(
i,
hr + 10,
f"{int(hr)}bpm",
ha="center",
va="bottom",
fontsize=10,
fontweight="bold",
color="red",
)
# Set x-axis formatting
ax1.set_xticks(x_positions)
ax1.set_xticklabels(stage_labels, fontsize=11)
# Create legend
lines1, labels1 = ax1.get_legend_handles_labels()
lines2, labels2 = ax2.get_legend_handles_labels()
ax1.legend(
lines1 + lines2,
labels1 + labels2,
loc="upper left",
frameon=True,
fancybox=True,
shadow=True,
)
# Add grid
ax1.grid(True, alpha=0.3, linestyle="-", linewidth=0.5)
ax1.set_axisbelow(True)
# Adjust layout
plt.tight_layout()
plt.subplots_adjust(bottom=0.1, top=0.9)
chart_path = self.charts_dir / "fuel_utilization_chart.png"
plt.savefig(chart_path, dpi=300)
plt.close()
return self._image_to_base64(chart_path) if save_as_base64 else str(chart_path)
def generate_vo2_pulse_chart(
self, df: pd.DataFrame, save_as_base64: bool = False
) -> str:
"""Generate VO2 Pulse chart with heart rate and speed"""
first_unique_phase = df.drop_duplicates(subset="PHASE")
phase_times = first_unique_phase["T(sec)"].tolist()
plt.figure(figsize=(18, 5))
ax1 = plt.subplot()
# Plot VO2 Pulse
sns.lineplot(
data=df,
x="T(sec)",
y="VO2 Pulse_smoothed",
label="VO2 Pulse (mL/beat)",
color="blue",
)
ax1.set_xlabel("Time (sec)")
ax1.set_ylabel("VO2 Pulse (mL/beat)")
ax1.set_ylim(0, df["VO2 Pulse_smoothed"].max())
ax1.grid(True, alpha=0.1)
# Create second y-axis for heart rate
ax2 = ax1.twinx()
sns.lineplot(
data=df,
x="T(sec)",
y="HR(bpm)_smoothed",
color="red",
ax=ax2,
linewidth=2,
label="Heart Rate (bpm)",
)
ax2.set_ylabel("Heart Rate (bpm)", color="red")
ax2.tick_params(axis="y", labelcolor="red")
ax2.set_ylim(0, df["HR(bpm)_smoothed"].max() + 1)
# Create third y-axis for speed
ax3 = ax1.twinx()
ax3.spines["right"].set_position(("outward", 60))
sns.lineplot(
data=df,
x="T(sec)",
y="Speed",
color="green",
ax=ax3,
drawstyle="steps-post",
linewidth=2,
label="Speed",
)
ax3.set_ylabel("Speed", color="green")
ax3.tick_params(axis="y", labelcolor="green")
ax3.set_ylim(0, df["Speed"].max() + 1)
ax1.set_xticks(np.arange(0, df["T(sec)"].max() + 200, 200))
# Remove default legends first
for ax in [ax1, ax2, ax3]:
if ax.get_legend():
ax.get_legend().remove()
# Combine legends from all axes
lines1, labels1 = ax1.get_legend_handles_labels()
lines2, labels2 = ax2.get_legend_handles_labels()
lines3, labels3 = ax3.get_legend_handles_labels()
ax1.legend(
lines1 + lines2 + lines3, labels1 + labels2 + labels3, loc="upper left"
)
# Add colored background regions
if len(phase_times) >= 4:
ax1.axvspan(0, phase_times[1], alpha=0.2, color="lightblue")
ax1.axvspan(phase_times[1], phase_times[2], alpha=0.2, color="purple")
ax1.axvspan(phase_times[2], phase_times[3], alpha=0.2, color="lightgreen")
ax1.axvspan(phase_times[3], df["T(sec)"].max(), alpha=0.2, color="blue")
chart_path = self.charts_dir / "vo2_pulse_chart.png"
plt.savefig(chart_path, bbox_inches="tight", dpi=300)
plt.close()
return self._image_to_base64(chart_path) if save_as_base64 else str(chart_path)
def generate_vo2_breath_chart(
self, df: pd.DataFrame, save_as_base64: bool = False
) -> str:
"""Generate VO2 per Breath chart"""
first_unique_phase = df.drop_duplicates(subset="PHASE")
phase_times = first_unique_phase["T(sec)"].tolist()
plt.figure(figsize=(18, 5))
ax1 = plt.subplot()
# Plot VO2 per Breath
sns.lineplot(
data=df,
x="T(sec)",
y="VO2 Breath_smoothed",
label="VO2 per Breath (mL/breath)",
)
ax1.set_xlabel("Time (sec)")
ax1.set_ylabel("VO2 per Breath (mL/breath)")
ax1.set_ylim(0, df["VO2 Breath_smoothed"].max() + 1)
ax1.grid(True, alpha=0.1)
# Plot speed as step function on secondary y-axis
ax2 = ax1.twinx()
ax1.set_xticks(np.arange(0, df["T(sec)"].max() + 200, 200))
sns.lineplot(
data=df,
x="T(sec)",
y="Speed",
color="green",
ax=ax2,
drawstyle="steps-post",
linewidth=2,
label="Speed",
)
ax2.set_ylim(0, df["Speed"].max() + 1)
ax2.set_ylabel("Speed")
# Remove default legends first
ax1.get_legend().remove()
ax2.get_legend().remove()
# Combine legends from both axes in the top left
lines1, labels1 = ax1.get_legend_handles_labels()
lines2, labels2 = ax2.get_legend_handles_labels()
ax1.legend(lines1 + lines2, labels1 + labels2, loc="upper left")
# Add colored background regions
if len(phase_times) >= 4:
ax1.axvspan(0, phase_times[1], alpha=0.2, color="lightblue")
ax1.axvspan(phase_times[1], phase_times[2], alpha=0.2, color="purple")
ax1.axvspan(phase_times[2], phase_times[3], alpha=0.2, color="lightgreen")
ax1.axvspan(phase_times[3], df["T(sec)"].max(), alpha=0.2, color="blue")
chart_path = self.charts_dir / "vo2_breath_chart.png"
plt.savefig(chart_path, bbox_inches="tight", dpi=300)
plt.close()
return self._image_to_base64(chart_path) if save_as_base64 else str(chart_path)
def generate_fat_metabolism_chart(
self, df: pd.DataFrame, save_as_base64: bool = False
) -> str:
"""Generate CHO and FAT metabolism chart"""
first_unique_phase = df.drop_duplicates(subset="PHASE")
phase_times = first_unique_phase["T(sec)"].tolist()
plt.figure(figsize=(18, 5))
ax1 = plt.subplot()
# Plot CHO
sns.lineplot(data=df, x="T(sec)", y="CHO_smoothed", label="CHO (kcal/min)")
ax1.set_xlabel("Time (sec)")
ax1.set_ylabel("CHO (kcal/min)")
ax1.grid(True, alpha=0.1)
# Plot FAT on secondary y-axis
ax2 = ax1.twinx()
ax1.set_xticks(np.arange(0, df["T(sec)"].max() + 200, 200))
sns.lineplot(
data=df,
x="T(sec)",
y="FAT_smoothed",
color="green",
ax=ax2,
label="FAT (kcal/min)",
)
ax2.set_ylabel("FAT (kcal/min)")
ax2.set_ylim(0, 15)
# Remove default legends first
ax1.get_legend().remove()
ax2.get_legend().remove()
# Combine legends from both axes in the top left
lines1, labels1 = ax1.get_legend_handles_labels()
lines2, labels2 = ax2.get_legend_handles_labels()
ax1.legend(lines1 + lines2, labels1 + labels2, loc="upper left")
# Add colored background regions
if len(phase_times) >= 4:
ax1.axvspan(0, phase_times[1], alpha=0.2, color="lightblue")
ax1.axvspan(phase_times[1], phase_times[2], alpha=0.2, color="purple")
ax1.axvspan(phase_times[2], phase_times[3], alpha=0.2, color="lightgreen")
ax1.axvspan(phase_times[3], df["T(sec)"].max(), alpha=0.2, color="blue")
chart_path = self.charts_dir / "fat_metabolism_chart.png"
plt.savefig(chart_path, bbox_inches="tight", dpi=300)
plt.close()
return self._image_to_base64(chart_path) if save_as_base64 else str(chart_path)
def generate_recovery_chart(
self, df: pd.DataFrame, save_as_base64: bool = False
) -> str:
"""Generate recovery chart with VCO2, HR, and BF"""
first_unique_phase = df.drop_duplicates(subset="PHASE")
phase_times = first_unique_phase["T(sec)"].tolist()
plt.figure(figsize=(18, 5))
ax1 = plt.subplot()
# Plot VCO2
sns.lineplot(
data=df,
x="T(sec)",
y="VCO2(ml/min)_smoothed",
label="VCO2 (ml/min)",
color="blue",
)
ax1.set_xlabel("Time (sec)")
ax1.set_ylabel("VCO2 (ml/min)")
ax1.set_ylim(0, df["VCO2(ml/min)"].max())
ax1.grid(True, alpha=0.1)
# Create second y-axis for heart rate
ax2 = ax1.twinx()
sns.lineplot(
data=df,
x="T(sec)",
y="HR(bpm)_smoothed",
color="red",
ax=ax2,
linewidth=2,
label="Heart Rate (bpm)",
)
ax2.set_ylabel("Heart Rate (bpm)", color="red")
ax2.set_ylim(df["HR(bpm)_smoothed"].min(), df["HR(bpm)_smoothed"].max() + 1)
ax2.tick_params(axis="y", labelcolor="red")
# Create third y-axis for breathing frequency
ax3 = ax1.twinx()
ax3.spines["right"].set_position(("outward", 60))
sns.lineplot(
data=df,
x="T(sec)",
y="BF(bpm)_smoothed",
color="green",
ax=ax3,
linewidth=2,
label="BF (bpm)",
)
ax3.set_ylabel("BF (bpm)", color="green")
ax3.tick_params(axis="y", labelcolor="green")
ax3.set_ylim(0, df["BF(bpm)_smoothed"].max() + 1)
ax1.set_xticks(np.arange(0, df["T(sec)"].max() + 200, 200))
# Remove default legends first
for ax in [ax1, ax2, ax3]:
if ax.get_legend():
ax.get_legend().remove()
# Combine legends from all axes in the top left
lines1, labels1 = ax1.get_legend_handles_labels()
lines2, labels2 = ax2.get_legend_handles_labels()
lines3, labels3 = ax3.get_legend_handles_labels()
ax1.legend(
lines1 + lines2 + lines3, labels1 + labels2 + labels3, loc="upper left"
)
# Add colored background regions
if len(phase_times) >= 4:
ax1.axvspan(0, phase_times[1], alpha=0.2, color="lightblue")
ax1.axvspan(phase_times[1], phase_times[2], alpha=0.2, color="purple")
ax1.axvspan(phase_times[2], phase_times[3], alpha=0.2, color="lightgreen")
ax1.axvspan(phase_times[3], df["T(sec)"].max(), alpha=0.2, color="blue")
chart_path = self.charts_dir / "recovery_chart.png"
plt.savefig(chart_path, bbox_inches="tight", dpi=300)
plt.close()
return self._image_to_base64(chart_path) if save_as_base64 else str(chart_path)
def generate_body_fat_percentage_chart(
self,
gender: str,
age: int,
body_fat_percentage: float,
save_as_base64: bool = False,
) -> str:
"""Generate body fat percentage chart with ranges"""
# Define the segments with muted colors
segments = [
("#F8A8A8", 0, 15), # Muted Red/Salmon: 0% to 15%
("#FFEECC", 15, 5), # Pale Yellow/Cream: 15% to 20%
("#D0F0C0", 20, 15), # Pale Green/Mint: 20% to 35%
("#FFEECC", 35, 5), # Pale Yellow/Cream: 35% to 40%
("#F8A8A8", 40, 10), # Muted Red/Salmon: 40% to 50%
]
# Determine age group
if 20 <= age <= 39:
age_group = "20-39"
elif 40 <= age <= 59:
age_group = "40-59"
elif 60 <= age <= 79:
age_group = "60-79"
else:
age_group = "N/A"
demographic = f"{age_group}\n({gender[0].upper()})"
fig, ax = plt.subplots(figsize=(10, 2))
# Create the Segmented Bar
for color, start, length in segments:
ax.barh(
y=0,
width=length,
left=start,
height=1,
color=color,
edgecolor="black",
linewidth=0.5,
)
# Add the Indicator (Triangle)
ax.plot(
body_fat_percentage,
1.05,
marker="v",
color="black",
markersize=10,
clip_on=False,
transform=ax.get_xaxis_transform(),
)
# Set Axis Properties and Labels
ax.set_xlim(0, 50)
ax.set_xticks(range(0, 51, 5))
ax.set_yticks([])
ax.text(
-0.05,
0,
demographic,
transform=ax.get_yaxis_transform(),
va="center",
ha="right",
fontsize=12,
)
ax.set_xlim(0, 50)
ticks = range(0, 51, 5)
ax.set_xticks(ticks)
labels = [f"{t}%" for t in ticks]
ax.set_xticklabels(labels)
# Clean up spines and add small ticks
ax.spines["right"].set_visible(False)
ax.spines["top"].set_visible(False)
ax.spines["left"].set_visible(False)
ax.spines["bottom"].set_visible(True)
for x in range(0, 51, 5):
ax.plot(
[x, x],
[-0.05, -0.01],
color="black",
transform=ax.get_xaxis_transform(),
clip_on=False,
)
plt.tight_layout()
chart_path = self.charts_dir / "body_fat_percentage_chart.png"
plt.savefig(chart_path, bbox_inches="tight", dpi=300)
plt.close()
return self._image_to_base64(chart_path) if save_as_base64 else str(chart_path)
def generate_body_composition_chart(
self, fat_mass_lbs: float, lean_mass_lbs: float, save_as_base64: bool = False
) -> str:
"""Generate donut chart for body composition"""
# Calculate percentages
total_weight = fat_mass_lbs + lean_mass_lbs
fat_percentage = (fat_mass_lbs / total_weight) * 100
lean_percentage = (lean_mass_lbs / total_weight) * 100
# Data for the chart
sizes = [fat_percentage, lean_percentage]
colors = ["#fde3ac", "#ff9966"] # Light yellow/tan and orange
plt.figure(figsize=(8, 8))
# Create the donut chart without labels first
wedges, texts, autotexts = plt.pie(
sizes,
autopct="", # Remove auto percentages
startangle=90,
wedgeprops=dict(width=0.5, edgecolor="w"),
colors=colors,
labels=["", ""],
) # Remove default labels
# Add custom text annotations positioned manually
plt.text(
-1,
1,
f"Fat Mass ({fat_mass_lbs:.1f}lbs)\n{fat_percentage:.1f}%",
fontsize=14,
fontweight="bold",
ha="center",
va="center",
bbox=dict(boxstyle="round,pad=0.3", facecolor="white", alpha=0.8),
)
plt.text(
1,
-1,
f"Lean Mass ({lean_mass_lbs:.1f}lbs)\n{lean_percentage:.1f}%",
fontsize=14,
fontweight="bold",
ha="center",
va="center",
bbox=dict(boxstyle="round,pad=0.3", facecolor="white", alpha=0.8),
)
# Set the title
plt.axis("equal") # Equal aspect ratio ensures that pie is drawn as a circle
chart_path = self.charts_dir / "body_composition_chart.png"
plt.savefig(chart_path, bbox_inches="tight", dpi=600)
plt.close()
return self._image_to_base64(chart_path) if save_as_base64 else str(chart_path)
def generate_spirometry_chart(
self, spirometry_df: pd.DataFrame, save_as_base64: bool = False
) -> str:
"""Generate spirometry chart with Z-scores and ranges"""
# Coerce numeric columns
for col in ["Best", "LLN", "Pred.", "%Pred.", "ZScore"]:
if col in spirometry_df.columns:
spirometry_df[col] = pd.to_numeric(spirometry_df[col], errors="coerce")
# Select rows of interest and prepare display values
rows_map = {
"Lung Volume": "FVC",
"Lung Power": "FEV1",
"Power/Volume": "FEV1/FVC%",
}
records = []
for label, param in rows_map.items():
row = spirometry_df.loc[spirometry_df["Parameters"].str.strip() == param]
if row.empty:
continue
row = row.iloc[0]
records.append(
{
"label": label,
"param": param,
"best": row["Best"],
"pct": row["%Pred."],
"z": row["ZScore"],
}
)
# Figure setup
fig, axes = plt.subplots(
nrows=3,
ncols=1,
figsize=(11.5, 3.6),
sharex=True,
gridspec_kw={"hspace": 0.65},
)
x_min, x_max = -5, 3
# Segment colors: red -> orange -> yellow -> green
segments = [
(-5, -4, "#f4a7a7"), # red-ish
(-4, -3, "#f7c49a"), # orange-ish
(-3, -1.7, "#f6e3a3"), # yellow-ish
(-1.7, 3, "#c9f0cc"), # green-ish
]
ticks = np.arange(x_min, x_max + 1, 1)
labels = [str(i) for i in ticks]
# Plot each row
for ax, rec in zip(axes, records):
# Background segments
for a, b, color in segments:
ax.barh(
0, width=b - a, left=a, height=0.6, color=color, edgecolor="none"
)
# LLN (-1) and Predicted (0) markers
ax.axvline(0, color="black", lw=1)
# Z-score pointer (downward triangle) at top of each panel
if pd.notna(rec["z"]):
trans = mtransforms.blended_transform_factory(
ax.transData, ax.transAxes
)
ax.plot(
float(rec["z"]),
1.2,
marker="v",
markersize=12,
color="dimgray",
transform=trans,
clip_on=False,
)
# Labels, ticks, and styling
ax.set_title(
rec["label"], loc="left", fontsize=11, fontweight="bold", pad=2
)
ax.set_xlim(x_min, x_max)
ax.set_yticks([])
ax.set_xticks(ticks)
ax.set_xticklabels(labels, fontsize=8)
ax.set_xlabel("")
# Top annotations
axes[0].text(-1.7, 0.45, "LLN", ha="center", va="bottom", fontsize=9)
axes[0].text(0, 0.45, "Predicted", ha="center", va="bottom", fontsize=9)
# Right-side summary boxes
fig.subplots_adjust(right=0.78)
box_ax = fig.add_axes(
[0.805, 0.06, 0.18, 0.90]
) # [left, bottom, width, height]
box_ax.axis("off")
# Helper to draw a pill-shaped text box
def pill(ax, xy, text):
x, y = xy
# Draw rounded rectangle background
bbox = FancyBboxPatch(
(x - 0.48, y - 0.09),
0.96,
0.18,
boxstyle="round,pad=0.02,rounding_size=0.08",
ec="#dddddd",
fc="#f3f3f3",
linewidth=1.0,
)
ax.add_patch(bbox)
ax.text(
x,
y + 0.025,
text,
ha="center",
va="center",
fontsize=11,
fontweight="bold",
)
ax.text(
x,
y - 0.055,
"of predicted",
ha="center",
va="center",
fontsize=9,
color="#555555",
)
box_ax.set_xlim(0, 1)
box_ax.set_ylim(0, 1)
# Prepare display strings and positions (top to bottom)
right_items = []
for rec in records:
name = (
"FVC"
if rec["param"] == "FVC"
else ("FEV1" if rec["param"] == "FEV1" else "FEV1/FVC")
)
unit = "L" if rec["param"] in ("FVC", "FEV1") else "%"
value_fmt = f"{rec['best']:.2f}{unit}"
pct_fmt = f"{rec['pct']:.1f}%"
right_items.append((name, value_fmt, pct_fmt))
# Sort to match image order on the right (FVC, FEV1, FEV1/FVC)
order = ["FVC", "FEV1", "FEV1/FVC"]
right_items_sorted = [
next(item for item in right_items if item[0] == k) for k in order
]
ys = [0.82, 0.48, 0.15]
for (name, value_fmt, pct_fmt), y in zip(right_items_sorted, ys):
main_line = f"{name}\n{value_fmt}{pct_fmt}"
pill(box_ax, (0.5, y), main_line)
chart_path = self.charts_dir / "spirometry_chart.png"
plt.savefig(chart_path, dpi=300, bbox_inches="tight")
plt.close()
return self._image_to_base64(chart_path) if save_as_base64 else str(chart_path)
def generate_all_charts(
self,
pnoe_df: pd.DataFrame,
spirometry_df: pd.DataFrame,
patient_data: Dict,
save_as_base64: bool = False,
) -> Dict[str, str]:
"""Generate all charts at once and return dictionary of paths/base64 strings"""
charts = {}
# Generate physiological charts
charts["respiratory"] = self.generate_respiratory_chart(pnoe_df, save_as_base64)
charts["fuel_utilization_chart"] = self.generate_fuel_utilization_chart(
pnoe_df, save_as_base64
)
charts["vo2_pulse_chart"] = self.generate_vo2_pulse_chart(
pnoe_df, save_as_base64
)
charts["vo2_breath_chart"] = self.generate_vo2_breath_chart(
pnoe_df, save_as_base64
)
charts["fat_metabolism_chart"] = self.generate_fat_metabolism_chart(
pnoe_df, save_as_base64
)
charts["recovery_chart"] = self.generate_recovery_chart(pnoe_df, save_as_base64)
# Generate body composition charts
if (
"gender" in patient_data
and "age" in patient_data
and "fat_percentage" in patient_data
):
charts["body_fat_percentage_chart"] = (
self.generate_body_fat_percentage_chart(
patient_data["gender"],
patient_data["age"],
patient_data["fat_percentage"],
save_as_base64,
)
)
if "fat_mass_lbs" in patient_data and "lean_mass_lbs" in patient_data:
charts["body_composition_chart"] = self.generate_body_composition_chart(
patient_data["fat_mass_lbs"],
patient_data["lean_mass_lbs"],
save_as_base64,
)
# Generate spirometry chart
charts["spirometry_chart"] = self.generate_spirometry_chart(
spirometry_df, save_as_base64
)
return charts
# Example usage
if __name__ == "__main__":
# Initialize graph generator
generator = GraphGenerator()
# Load sample data (you would pass your actual dataframes)
pnoe_df = pd.read_csv("data/Pnoe_20250729_1550-Moran_Keirstyn.csv", delimiter=";")
spirometry_df = pd.read_csv("data/spirometry_data.csv")
# Preprocess pnoe data (same as in your notebook)
pnoe_df = pnoe_df.apply(pd.to_numeric, errors="ignore")
pnoe_df["VO2 Pulse"] = pnoe_df["VO2(ml/min)"] / pnoe_df["HR(bpm)"]
pnoe_df["VO2 Breath"] = pnoe_df["VO2(ml/min)"] / pnoe_df["BF(bpm)"]
pnoe_df["CHO"] = pnoe_df["EE(kcal/min)"] * pnoe_df["CARBS(%)"] / 100
pnoe_df["FAT"] = pnoe_df["EE(kcal/min)"] * pnoe_df["FAT(%)"] / 100
# Apply smoothing
window_size = 10
columns_to_smooth = [
"VO2(ml/min)",
"VCO2(ml/min)",
"HR(bpm)",
"VT(l)",
"BF(bpm)",
"VE(l/min)",
"VO2 Pulse",
"VO2 Breath",
"CHO",
"FAT",
]
for col in columns_to_smooth:
if col in pnoe_df.columns:
pnoe_df[f"{col}_smoothed"] = (
pnoe_df[col].rolling(window=window_size, min_periods=1).mean()
)
# Patient data
patient_data = {
"gender": "female",
"age": 25,
"fat_percentage": 22.4,
"fat_mass_lbs": 27.6,
"lean_mass_lbs": 95.4,
}
# Generate all charts
charts = generator.generate_all_charts(
pnoe_df, spirometry_df, patient_data, save_as_base64=True
)
print(f"Generated {len(charts)} charts:")
for chart_name in charts.keys():
print(f"- {chart_name}")
Binary file not shown.

After

Width:  |  Height:  |  Size: 265 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 425 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 KiB

+130
View File
@@ -0,0 +1,130 @@
from jinja2 import Environment, FileSystemLoader
from playwright.sync_api import sync_playwright
from context import context_list
env = Environment(loader=FileSystemLoader("report_gen"))
html_pages = []
a = 1
b = "string"
c = [1,2,3,5]
d = {
"key": "value"
}
header_context = {
"patient_name": "Keirstyn Moran",
"age": 34,
"height": "5'4\"",
"weight": "123lbs",
"focus": "Endurance",
}
footer_context = [
{
"contact_email": "info@ishplabs.com ",
"website": "www.ishplabs.com",
"social": "@ishplabs",
"page_number": i + 1,
}
for i in range(len(context_list))
]
header_html = env.get_template("header.html").render(header_context)
footer_html_list = [
env.get_template("footer.html").render(context) for context in footer_context
]
for i, context in enumerate(context_list):
template = env.get_template(f"page_{i + 1}.html").render(context)
if (i + 1) > 2:
full_html = f"""
<div class="page flex flex-col justify-between">
<div>
{header_html}
</div>
<main class="flex-grow p-4">
{template}
</main>
<div class="border-t text-center text-sm text-gray-600">
{footer_html_list[i]}
</div>
</div>
"""
html_pages.append(full_html)
else:
html_pages.append(template)
# Combine with page breaks
final_html = "<div class='page-break'></div>".join(html_pages)
# Wrap in full HTML document
html_doc = f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link href="https://cdn.jsdelivr.net/npm/tailwindcss/dist/tailwind.min.css" rel="stylesheet">
<style>
html, body {{
height: 100%;
margin: 0;
padding: 0;
}}
.page-break {{ page-break-after: always; }}
.page {{
height: 100vh;
min-height: 100vh;
display: flex;
flex-direction: column;
}}
.page main {{
flex: 1;
overflow: hidden;
}}
/* Reset margins and padding everywhere */
* {{
margin: 0;
padding: 0;
box-sizing: border-box;
}}
/* Prevent images from being too large */
img {{
max-height: 300px;
}}
/* Larger images for specific charts */
.chart-large {{
max-height: 500px !important;
}}
</style>
</head>
<body class="m-0 p-0">
{final_html}
</body>
</html>
"""
# Generate PDF
def html_string_to_pdf(html_content, pdf_path):
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
# Set the HTML directly
page.set_content(html_content)
# Export to PDF
page.pdf(path=pdf_path, format="A4", print_background=True)
browser.close()
html_string_to_pdf(html_doc, "multi_page_report.pdf")
# pdfkit.from_string(html_doc, "truth_report.pdf", options=options)
print("✅ PDF generated: multi_page_report.pdf")
Binary file not shown.
+1907 -45
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+12
View File
@@ -0,0 +1,12 @@
<div class=" bg-black text-white px-6 py-3">
<div class="flex justify-between items-center text-sm">
<div class="flex space-x-8">
<span>contact: {{ contact_email }}</span>
<span>website: {{ website }}</span>
<span>social: {{ social }}</span>
</div>
<div class="bg-white text-black font-bold px-3 py-1 text-lg">
{{ page_number }}
</div>
</div>
</div>
+14
View File
@@ -0,0 +1,14 @@
<div class="bg-black text-white px-6 py-4 flex items-center justify-between">
<div class="flex items-center">
<div class="text-lg font-bold mr-2">ISHP</div>
<div class="w-6 h-4 bg-cyan-400 mr-8"></div>
</div>
<div class="flex space-x-8 text-sm">
<span>Name: {{ patient_name }}</span>
<span>Age: {{ age }}</span>
<span>Height: {{ height }}</span>
<span>Weight: {{ weight }}</span>
<span>Focus: {{ focus }}</span>
</div>
</div>
+40
View File
@@ -0,0 +1,40 @@
<div
class="w-full page bg-black text-white relative overflow-hidden"
>
<!-- ISHP Logo/Text -->
<div class="absolute top-8 left-8 z-20">
<h1 class="text-2xl font-bold tracking-wider">ISHP</h1>
</div>
<!-- Diagonal Cyan Stripe -->
<div class="absolute top-0 left-0 w-full h-full">
<div
class="absolute top-0 left-0 w-96 h-full bg-gradient-to-r from-cyan-300 to-cyan-400 transform -skew-x-12 origin-top-left"
></div>
</div>
<!-- Main Content Container -->
<div
class="relative z-10 flex flex-col justify-center items-end h-full pr-16"
>
<!-- BIO-PERFORMX Section -->
<div class="text-right mb-8">
<h1 class="text-6xl font-bold tracking-wider mb-2">BIO-PERFORMX</h1>
<p class="text-3xl italic font-light">Endurance</p>
</div>
<!-- Name and Date Section -->
<div class="text-right mt-16">
<h2 class="text-4xl font-bold tracking-wider mb-2">
{{ name|upper }}
</h2>
<h2 class="text-4xl font-bold tracking-wider mb-6">
{{ surname|upper }}
</h2>
<p class="text-xl italic underline">{{ date }}</p>
</div>
</div>
<!-- Dotted Pattern at Bottom -->
</div>
+38
View File
@@ -0,0 +1,38 @@
<div class="w-full page bg-white">
<!-- Main Content -->
<div class="px-8 py-6">
<!-- VO2 Pulse Section -->
<div class="mb-8">
<!-- VO2 Pulse Header -->
<div class="bg-gray-200 p-4 rounded-lg mb-4 text-center">
<h2 class="text-lg font-bold text-black">VO2 Pulse</h2>
<p class="text-black">Begins to drop at {{ vo2_pulse_drop_bpm | default('180 bpm') }} ({{ vo2_pulse_drop_zone | default('Zone 4') }})</p>
</div>
<!-- VO2 Pulse Graph -->
<div class="flex justify-center mb-6">
<img src="data:image/png;base64, {{ vo2_pulse_chart }}"
alt="VO2 Pulse Chart"
class="w-full max-w-4xl h-auto object-contain">
</div>
</div>
<!-- VO2 Breath Section -->
<div class="mb-8">
<!-- VO2 Breath Header -->
<div class="bg-gray-200 p-4 rounded-lg mb-4 text-center">
<h2 class="text-lg font-bold text-black">VO2 Breath</h2>
<p class="text-black">Begins to drop at {{ vo2_breath_drop_bpm | default('173 bpm') }} ({{ vo2_breath_drop_zone | default('Zone 3') }})</p>
</div>
<!-- VO2 Breath Graph -->
<div class="flex justify-center mb-6">
<img src="data:image/png;base64, {{ vo2_breath_chart }}"
alt="VO2 Breath Chart"
class="w-full max-w-4xl h-auto object-contain">
</div>
</div>
</div>
</div>
+231
View File
@@ -0,0 +1,231 @@
<div class="w-full page bg-white">
<!-- Main Content -->
<div class="px-8 py-6">
<!-- Fat Metabolism Section -->
<div class="mb-2">
<h2 class="text-xl font-bold text-black mb-4 text-center">
Fat Metabolism
</h2>
<!-- Fat Metabolism Info Boxes -->
<div class="grid grid-cols-2 gap-4 mb-4">
<!-- Fat Max Box -->
<div class="bg-gray-200 p-3 rounded-lg text-center">
<h3 class="text-base font-bold text-black mb-2">Fat Max</h3>
<p class="text-xs text-gray-600 italic mb-2">
{{ fat_max_optimal | default('*Optimal
10-12Kcals/minute') }}
</p>
<p class="text-lg font-bold text-black">
{{ fat_max_value | default('3.8Kcals/min') }}
</p>
<p class="text-xs text-black">
{{ fat_max_heart_rate | default('49% of Max Heart Rate')
}}
</p>
<p class="text-xs text-black">
{{ fat_max_bpm | default('97 bpm') }}
</p>
</div>
<!-- Carbs and Fat Crossover Box -->
<div class="bg-gray-200 p-3 rounded-lg text-center">
<h3 class="text-base font-bold text-black mb-3">
Carbs and Fat Crossover
</h3>
<p class="text-lg font-bold text-black">
{{ crossover_bpm | default('100bpm') }}
</p>
<p class="text-xs text-black">
{{ crossover_heart_rate | default('51% of Max Heart
Rate') }}
</p>
</div>
</div>
<!-- Fat Metabolism Graph -->
<div class="mb-4">
<div class="bg-gray-100 p-2 rounded-lg mb-2">
<p class="text-black font-semibold text-center text-sm"></p>
{{ fat_metabolism_note | default('100bpm at a speed of
4.0mph and incline of 2%') }}
</p>
</div>
<div class="flex justify-center">
<img
src="data:image/png;base64, {{ fat_metabolism_chart }}"
alt="Fat Metabolism Chart"
class="w-full max-w-4xl h-auto object-contain"
/>
</div>
</div>
</div>
<!-- Recovery Section -->
<div class="mb-2">
<h2 class="text-xl font-bold text-black mb-4 text-center">
Recovery
</h2>
<!-- Recovery Info Boxes -->
<div class="grid grid-cols-3 gap-4 mb-4">
<!-- Cardiac Recovery -->
<div class="bg-gray-200 p-3 rounded-lg text-center">
<h3 class="text-sm font-bold text-black mb-2">
Cardiac Recovery
</h3>
<p class="text-xs text-gray-600 mb-2">
{{ cardiac_recovery_time | default('(1 minute)') }}
</p>
<p class="text-lg font-bold text-black">
{{ cardiac_recovery_percentage | default('33%') }}
</p>
</div>
<!-- Metabolic Recovery -->
<div class="bg-gray-200 p-3 rounded-lg text-center">
<h3 class="text-sm font-bold text-black mb-2">
Metabolic (CO2) Recovery
</h3>
<p class="text-xs text-gray-600 mb-2">
{{ metabolic_recovery_time | default('(2 minute)') }}
</p>
<p class="text-lg font-bold text-black">
{{ metabolic_recovery_percentage | default('65%') }}
</p>
</div>
<!-- Breath Frequency Recovery -->
<div class="bg-gray-200 p-3 rounded-lg text-center">
<h3 class="text-sm font-bold text-black mb-2">
Breath Frequency Recovery
</h3>
<p class="text-xs text-gray-600 mb-2">
{{ breath_recovery_time | default('(2.5 minute)') }}
</p>
<p class="text-lg font-bold text-black">
{{ breath_recovery_percentage | default('76%') }}
</p>
</div>
</div>
<!-- Recovery Graph -->
<div class="flex justify-center mb-6">
<img
src="data:image/png;base64, {{ recovery_chart }}"
alt="Recovery Chart"
class="w-full max-w-4xl h-auto object-contain"
/>
</div>
</div>
<!-- Resting Heart Rate Table -->
<div class="mb-2">
<h3 class="text-base font-bold text-black mb-2 text-center">
Resting Heart Rate - {{ resting_heart_rate | default('53bpm') }}
</h3>
<table class="w-full border-collapse text-xs">
<thead>
<tr>
<th
class="bg-cyan-300 border border-gray-400 p-2 text-black font-bold"
>
Age (F)
</th>
<th
class="bg-cyan-300 border border-gray-400 p-2 text-black font-bold"
>
Poor
</th>
<th
class="bg-cyan-300 border border-gray-400 p-2 text-black font-bold"
>
Below Average
</th>
<th
class="bg-cyan-300 border border-gray-400 p-2 text-black font-bold"
>
Average
</th>
<th
class="bg-cyan-300 border border-gray-400 p-2 text-black font-bold"
>
Above Average
</th>
<th
class="bg-cyan-300 border border-gray-400 p-2 text-black font-bold"
>
Good
</th>
<th
class="bg-cyan-300 border border-gray-400 p-2 text-black font-bold"
>
Excellent
</th>
<th
class="bg-cyan-300 border border-gray-400 p-2 text-black font-bold relative"
>
Athlete
<!-- Arrow indicator -->
<div
class="absolute -bottom-3 left-1/2 transform -translate-x-1/2"
>
<div
class="w-0 h-0 border-l-3 border-r-3 border-t-6 border-transparent border-t-black"
></div>
</div>
</th>
</tr>
</thead>
<tbody>
<tr>
<td
class="bg-cyan-200 border border-gray-400 p-2 text-black font-semibold text-center"
>
{{ hr_age_range | default('26-35') }}
</td>
<td
class="bg-gray-100 border border-gray-400 p-2 text-black text-center"
>
{{ hr_poor | default('82bpm +') }}
</td>
<td
class="bg-gray-100 border border-gray-400 p-2 text-black text-center"
>
{{ hr_below_avg | default('75-81bpm') }}
</td>
<td
class="bg-gray-100 border border-gray-400 p-2 text-black text-center"
>
{{ hr_average | default('71-74bpm') }}
</td>
<td
class="bg-gray-100 border border-gray-400 p-2 text-black text-center"
>
{{ hr_above_avg | default('66-70bpm') }}
</td>
<td
class="bg-gray-100 border border-gray-400 p-2 text-black text-center"
>
{{ hr_good | default('62-65bpm') }}
</td>
<td
class="bg-gray-100 border border-gray-400 p-2 text-black text-center"
>
{{ hr_excellent | default('55-61bpm') }}
</td>
<td
class="bg-green-200 border border-gray-400 p-2 text-black text-center font-bold"
>
{{ hr_athlete | default('44-54bpm') }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
+76
View File
@@ -0,0 +1,76 @@
<div class="page bg-white p-8 max-w-4xl mx-auto">
<!-- Header -->
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900 mb-4">Local Muscle Activity</h1>
<h2 class="text-xl font-semibold text-gray-800 mb-2">Muscle Oxygenation Assessment</h2>
<p class="text-sm text-gray-600 leading-relaxed">
SMO2 testing (Skeletal Muscle Oxygen Saturation) is an analysis of how effectively oxygen is being used at a particular muscle. It helps determine limitations on if the muscle is effectively using oxygen when exercising.
</p>
</div>
<!-- Right Leg Section -->
<div class="mb-12">
<h3 class="text-lg font-semibold text-center text-gray-800 mb-6">Indications - Right Leg</h3>
<div class="flex gap-8">
<!-- Chart Image -->
<div class="flex-1">
<img src= "data:image/png;base64,{{ right_leg }}" alt="Right Leg SMO2 Chart" class="w-full h-auto">
</div>
<!-- Right Side Info -->
<div class="w-48 space-y-4">
<div class="bg-gray-100 p-3 rounded">
<div class="text-xs font-semibold text-gray-700 mb-1">Surplus</div>
<div class="text-xs text-gray-600">Supply > Demand at a heart rate and speed of:</div>
<div class="text-sm font-bold text-gray-800">n/a</div>
</div>
<div class="bg-gray-100 p-3 rounded">
<div class="text-xs font-semibold text-gray-700 mb-1">Supply Threshold</div>
<div class="text-xs text-gray-600">Demand outstrips supply at a heart rate of:</div>
<div class="text-sm font-bold text-gray-800">154bpm @ 5.0mph</div>
</div>
<div class="bg-gray-100 p-3 rounded">
<div class="text-xs font-semibold text-gray-700 mb-1">Recovery</div>
<div class="text-xs text-gray-600">"Optimal >100%"</div>
<div class="text-sm font-bold text-gray-800">n/a</div>
</div>
</div>
</div>
</div>
<!-- Left Leg Section -->
<div>
<h3 class="text-lg font-semibold text-center text-gray-800 mb-6">Indications - Left Leg</h3>
<div class="flex gap-8">
<!-- Chart Image -->
<div class="flex-1">
<img src= "data:image/png;base64,{{ left_leg }}" alt="Left Leg SMO2 Chart" class="w-full h-auto">
</div>
<!-- Right Side Info -->
<div class="w-48 space-y-4">
<div class="bg-gray-100 p-3 rounded">
<div class="text-xs font-semibold text-gray-700 mb-1">Surplus</div>
<div class="text-xs text-gray-600">Supply > Demand at a heart rate and speed of:</div>
<div class="text-sm font-bold text-gray-800">n/a</div>
</div>
<div class="bg-gray-100 p-3 rounded">
<div class="text-xs font-semibold text-gray-700 mb-1">Supply Threshold</div>
<div class="text-xs text-gray-600">Demand outstrips supply at a heart rate of:</div>
<div class="text-sm font-bold text-gray-800">165 bpm @ 5.5mph</div>
</div>
<div class="bg-gray-100 p-3 rounded">
<div class="text-xs font-semibold text-gray-700 mb-1">Recovery</div>
<div class="text-xs text-gray-600">"Optimal >100%"</div>
<div class="text-sm font-bold text-gray-800">n/a</div>
</div>
</div>
</div>
</div>
</div>
+216
View File
@@ -0,0 +1,216 @@
<div class="w-full page bg-white">
<!-- Header Section -->
<!-- Main Content -->
<div class="px-8 py-6">
<!-- Page Title -->
<h1 class="text-3xl font-bold text-black mb-8 text-center">Training Recommendations</h1>
<!-- Training Recommendations Section -->
<div class="grid grid-cols-2 gap-8 mb-8">
<!-- Left Side: Zone Recommendations -->
<div class="bg-gray-200 p-6 rounded-lg">
<!-- Zone 2 Recommendations -->
<div class="mb-6">
<h3 class="text-lg font-bold text-black mb-3">Zone 2 {{ zone2_frequency | default('3-4x/week') }}:</h3>
<ul class="text-sm text-black space-y-1 list-disc list-inside">
<li>{{ zone2_duration | default('40+ minutes') }} of Steady State Cardio (HR {{ zone2_hr_range | default('____') }} bpm)</li>
<li>{{ zone2_speed | default('____ mph') }} at {{ zone2_incline | default('2% Incline') }}</li>
</ul>
</div>
<!-- Zone 3 Recommendations -->
<div class="mb-6">
<h3 class="text-lg font-bold text-black mb-3">Zone 3 {{ zone3_frequency | default('1-2x/week') }}:</h3>
<ul class="text-sm text-black space-y-1 list-disc list-inside">
<li>{{ zone3_duration | default('10-20 minutes') }} in zone 3 (HR {{ zone3_hr_range | default('____ bpm') }})</li>
<li>{{ zone3_speed | default('____mph') }} + at {{ zone3_incline | default('2% Incline') }}</li>
<li>Slow down cadence until HR reaches {{ zone3_target_hr | default('___ bpm') }}</li>
<li>{{ zone3_recovery_speed | default('____mph') }} at {{ zone3_recovery_incline | default('2% Incline') }}</li>
<li>Maintain HR in zone 1 ({{ zone1_hr_range | default('____bpm') }}) for {{ zone1_duration | default('4-8 minutes') }}</li>
<li>Repeat {{ zone3_repeats | default('2-3 times') }}</li>
</ul>
</div>
</div>
<!-- Right Side: Training Table -->
<div>
<table class="w-full border-collapse text-sm">
<thead>
<tr>
<th class="bg-cyan-300 border border-gray-400 p-2 text-black font-bold">Type</th>
<th class="bg-cyan-300 border border-gray-400 p-2 text-black font-bold">Sets</th>
<th class="bg-cyan-300 border border-gray-400 p-2 text-black font-bold">Effort Duration</th>
<th class="bg-cyan-300 border border-gray-400 p-2 text-black font-bold">Zone</th>
<th class="bg-cyan-300 border border-gray-400 p-2 text-black font-bold">RPE</th>
<th class="bg-cyan-300 border border-gray-400 p-2 text-black font-bold">Recovery Duration</th>
</tr>
</thead>
<tbody>
<!-- Short Row -->
<tr>
<td class="border border-gray-400 p-2 text-center text-black font-semibold">Short</td>
<td class="border border-gray-400 p-2 text-center text-black">{{ short_sets | default('8-10') }}</td>
<td class="border border-gray-400 p-2 text-center text-black">{{ short_duration | default('10-30 seconds') }}</td>
<td class="border border-gray-400 p-2 text-center text-black">{{ short_zone | default('5') }}</td>
<td class="border border-gray-400 p-2 text-center text-black">{{ short_rpe | default('10') }}</td>
<td class="border border-gray-400 p-2 text-center text-black">{{ short_recovery | default('20-60 seconds') }}</td>
</tr>
<!-- Medium Row -->
<tr>
<td class="border border-gray-400 p-2 text-center text-black font-semibold">Medium</td>
<td class="border border-gray-400 p-2 text-center text-black">{{ medium_sets | default('6-8') }}</td>
<td class="border border-gray-400 p-2 text-center text-black">{{ medium_duration | default('30-90 seconds') }}</td>
<td class="border border-gray-400 p-2 text-center text-black">{{ medium_zone | default('4') }}</td>
<td class="border border-gray-400 p-2 text-center text-black">{{ medium_rpe | default('8-9') }}</td>
<td class="border border-gray-400 p-2 text-center text-black">{{ medium_recovery | default('30-90 seconds') }}</td>
</tr>
<!-- Long Row -->
<tr>
<td class="border border-gray-400 p-2 text-center text-black font-semibold">Long</td>
<td class="border border-gray-400 p-2 text-center text-black">{{ long_sets | default('4-6') }}</td>
<td class="border border-gray-400 p-2 text-center text-black">{{ long_duration | default('5-10 minutes') }}</td>
<td class="border border-gray-400 p-2 text-center text-black">{{ long_zone | default('3/4') }}</td>
<td class="border border-gray-400 p-2 text-center text-black">{{ long_rpe | default('7-8') }}</td>
<td class="border border-gray-400 p-2 text-center text-black">{{ long_recovery | default('2.5-5 minutes') }}</td>
</tr>
<!-- Tempo Row -->
<tr>
<td class="border border-gray-400 p-2 text-center text-black font-semibold">Tempo</td>
<td class="border border-gray-400 p-2 text-center text-black">{{ tempo_sets | default('2-3') }}</td>
<td class="border border-gray-400 p-2 text-center text-black">{{ tempo_duration | default('10-20 minutes') }}</td>
<td class="border border-gray-400 p-2 text-center text-black">{{ tempo_zone | default('3') }}</td>
<td class="border border-gray-400 p-2 text-center text-black">{{ tempo_rpe | default('6-7') }}</td>
<td class="border border-gray-400 p-2 text-center text-black">{{ tempo_recovery | default('4-8 minutes') }}</td>
</tr>
<!-- Cardio Row -->
<tr>
<td class="border border-gray-400 p-2 text-center text-black font-semibold">Cardio</td>
<td class="border border-gray-400 p-2 text-center text-black">{{ cardio_sets | default('1') }}</td>
<td class="border border-gray-400 p-2 text-center text-black">{{ cardio_duration | default('>40 minutes') }}</td>
<td class="border border-gray-400 p-2 text-center text-black">{{ cardio_zone | default('2') }}</td>
<td class="border border-gray-400 p-2 text-center text-black">{{ cardio_rpe | default('4-5') }}</td>
<td class="border border-gray-400 p-2 text-center text-black">{{ cardio_recovery | default('N/A') }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Training Week Example Section -->
<div class="mb-8">
<h2 class="text-2xl font-bold text-black mb-6 text-center">Training Week Example with Progression</h2>
<!-- Week 1 -->
<div class="mb-6">
<div class="grid grid-cols-7 gap-2">
<!-- Monday -->
<div class="bg-cyan-300 p-3 text-center rounded-lg">
<div class="font-bold text-black mb-1">Monday</div>
<div class="text-sm text-black">{{ week1_mon_zone | default('Zone 2') }}</div>
<div class="text-sm text-black">{{ week1_mon_duration | default('45 mins') }}</div>
</div>
<!-- Tuesday -->
<div class="bg-cyan-300 p-3 text-center rounded-lg">
<div class="font-bold text-black mb-1">Tuesday</div>
<div class="text-sm text-black">{{ week1_tue_zone | default('Zone 2') }}</div>
<div class="text-sm text-black">{{ week1_tue_duration | default('45 mins') }}</div>
</div>
<!-- Wednesday -->
<div class="bg-cyan-300 p-3 text-center rounded-lg">
<div class="font-bold text-black mb-1">Wednesday</div>
<div class="text-sm text-black">{{ week1_wed_zone | default('Zone 3') }}</div>
<div class="text-sm text-black">{{ week1_wed_duration1 | default('10mins On') }}</div>
<div class="text-sm text-black">{{ week1_wed_duration2 | default('8mins Rest') }}</div>
<div class="text-sm text-black">{{ week1_wed_sets | default('x2') }}</div>
</div>
<!-- Thursday -->
<div class="bg-gray-200 p-3 text-center rounded-lg">
<div class="font-bold text-black mb-1">Thursday</div>
<div class="text-sm text-black">{{ week1_thu_content | default('') }}</div>
</div>
<!-- Friday -->
<div class="bg-cyan-300 p-3 text-center rounded-lg">
<div class="font-bold text-black mb-1">Friday</div>
<div class="text-sm text-black">{{ week1_fri_zone | default('Zone 2') }}</div>
<div class="text-sm text-black">{{ week1_fri_duration | default('45 mins') }}</div>
</div>
<!-- Saturday -->
<div class="bg-gray-200 p-3 text-center rounded-lg">
<div class="font-bold text-black mb-1">Saturday</div>
<div class="text-sm text-black">{{ week1_sat_content | default('') }}</div>
</div>
<!-- Sunday -->
<div class="bg-gray-200 p-3 text-center rounded-lg">
<div class="font-bold text-black mb-1">Sunday</div>
<div class="text-sm text-black">{{ week1_sun_content | default('') }}</div>
</div>
</div>
</div>
<!-- Week 2 -->
<div class="mb-6">
<div class="grid grid-cols-7 gap-2">
<!-- Monday -->
<div class="bg-cyan-300 p-3 text-center rounded-lg">
<div class="font-bold text-black mb-1">Monday</div>
<div class="text-sm text-black">{{ week2_mon_zone | default('Zone 2') }}</div>
<div class="text-sm text-black">{{ week2_mon_duration | default('50 mins') }}</div>
</div>
<!-- Tuesday -->
<div class="bg-cyan-300 p-3 text-center rounded-lg">
<div class="font-bold text-black mb-1">Tuesday</div>
<div class="text-sm text-black">{{ week2_tue_zone | default('Zone 2') }}</div>
<div class="text-sm text-black">{{ week2_tue_duration | default('50 mins') }}</div>
</div>
<!-- Wednesday -->
<div class="bg-cyan-300 p-3 text-center rounded-lg">
<div class="font-bold text-black mb-1">Wednesday</div>
<div class="text-sm text-black">{{ week2_wed_zone | default('Zone 3') }}</div>
<div class="text-sm text-black">{{ week2_wed_duration1 | default('10mins On') }}</div>
<div class="text-sm text-black">{{ week2_wed_duration2 | default('6mins Rest') }}</div>
<div class="text-sm text-black">{{ week2_wed_sets | default('x2') }}</div>
</div>
<!-- Thursday -->
<div class="bg-gray-200 p-3 text-center rounded-lg">
<div class="font-bold text-black mb-1">Thursday</div>
<div class="text-sm text-black">{{ week2_thu_content | default('') }}</div>
</div>
<!-- Friday -->
<div class="bg-cyan-300 p-3 text-center rounded-lg">
<div class="font-bold text-black mb-1">Friday</div>
<div class="text-sm text-black">{{ week2_fri_zone | default('Zone 2') }}</div>
<div class="text-sm text-black">{{ week2_fri_duration | default('50 mins') }}</div>
</div>
<!-- Saturday -->
<div class="bg-gray-200 p-3 text-center rounded-lg">
<div class="font-bold text-black mb-1">Saturday</div>
<div class="text-sm text-black">{{ week2_sat_content | default('') }}</div>
</div>
<!-- Sunday -->
<div class="bg-gray-200 p-3 text-center rounded-lg">
<div class="font-bold text-black mb-1">Sunday</div>
<div class="text-sm text-black">{{ week2_sun_content | default('') }}</div>
</div>
</div>
</div>
</div>
</div>
<!-- Footer Section -->
</div>
+148
View File
@@ -0,0 +1,148 @@
<div class="w-full page bg-white">
<!-- Main Content -->
<div class="px-8 py-6">
<!-- Page Title -->
<div class="text-center mb-8">
<h1 class="text-3xl font-bold text-black mb-2">Training Week</h1>
<p class="text-gray-600 italic">(To be filled out by your trainer)</p>
</div>
<!-- First Training Week Grid -->
<div class="mb-8">
<div class="grid grid-cols-7 gap-2 mb-4">
<!-- Week 1 Headers -->
<div class="bg-cyan-300 p-2 text-center rounded-lg">
<div class="font-bold text-black">Monday</div>
</div>
<div class="bg-cyan-300 p-2 text-center rounded-lg">
<div class="font-bold text-black">Tuesday</div>
</div>
<div class="bg-cyan-300 p-2 text-center rounded-lg">
<div class="font-bold text-black">Wednesday</div>
</div>
<div class="bg-cyan-300 p-2 text-center rounded-lg">
<div class="font-bold text-black">Thursday</div>
</div>
<div class="bg-cyan-300 p-2 text-center rounded-lg">
<div class="font-bold text-black">Friday</div>
</div>
<div class="bg-gray-200 p-2 text-center rounded-lg">
<div class="font-bold text-black">Saturday</div>
</div>
<div class="bg-gray-200 p-2 text-center rounded-lg">
<div class="font-bold text-black">Sunday</div>
</div>
</div>
<!-- Week 1 Content Boxes -->
<div class="grid grid-cols-7 gap-2 mb-8">
<div class="bg-gray-100 border-2 border-gray-300 h-32 rounded-lg"></div>
<div class="bg-gray-100 border-2 border-gray-300 h-32 rounded-lg"></div>
<div class="bg-gray-100 border-2 border-gray-300 h-32 rounded-lg"></div>
<div class="bg-gray-100 border-2 border-gray-300 h-32 rounded-lg"></div>
<div class="bg-gray-100 border-2 border-gray-300 h-32 rounded-lg"></div>
<div class="bg-gray-100 border-2 border-gray-300 h-32 rounded-lg"></div>
<div class="bg-gray-100 border-2 border-gray-300 h-32 rounded-lg"></div>
</div>
</div>
<!-- Second Training Week Grid -->
<div class="mb-8">
<div class="grid grid-cols-7 gap-2 mb-4">
<!-- Week 2 Headers -->
<div class="bg-cyan-300 p-2 text-center rounded-lg">
<div class="font-bold text-black">Monday</div>
</div>
<div class="bg-cyan-300 p-2 text-center rounded-lg">
<div class="font-bold text-black">Tuesday</div>
</div>
<div class="bg-cyan-300 p-2 text-center rounded-lg">
<div class="font-bold text-black">Wednesday</div>
</div>
<div class="bg-cyan-300 p-2 text-center rounded-lg">
<div class="font-bold text-black">Thursday</div>
</div>
<div class="bg-cyan-300 p-2 text-center rounded-lg">
<div class="font-bold text-black">Friday</div>
</div>
<div class="bg-gray-200 p-2 text-center rounded-lg">
<div class="font-bold text-black">Saturday</div>
</div>
<div class="bg-gray-200 p-2 text-center rounded-lg">
<div class="font-bold text-black">Sunday</div>
</div>
</div>
<!-- Week 2 Content Boxes -->
<div class="grid grid-cols-7 gap-2 mb-8">
<div class="bg-gray-100 border-2 border-gray-300 h-32 rounded-lg"></div>
<div class="bg-gray-100 border-2 border-gray-300 h-32 rounded-lg"></div>
<div class="bg-gray-100 border-2 border-gray-300 h-32 rounded-lg"></div>
<div class="bg-gray-100 border-2 border-gray-300 h-32 rounded-lg"></div>
<div class="bg-gray-100 border-2 border-gray-300 h-32 rounded-lg"></div>
<div class="bg-gray-100 border-2 border-gray-300 h-32 rounded-lg"></div>
<div class="bg-gray-100 border-2 border-gray-300 h-32 rounded-lg"></div>
</div>
</div>
<!-- Training Week Guidelines Section -->
<div class="mb-6">
<h2 class="text-2xl font-bold text-black mb-6 text-center">Training Week Guidelines</h2>
<!-- Guidelines Grid -->
<div class="grid grid-cols-5 gap-4 mb-6">
<!-- Zone 1 -->
<div>
<h3 class="text-lg font-bold text-black mb-3">Zone 1</h3>
<ul class="text-sm text-black space-y-2 list-disc list-inside">
<li>Zone 1 training is low intensity, for active recovery.</li>
<li>It can be done daily or even consecutively, depending on fitness, volume, and health.</li>
</ul>
</div>
<!-- Zone 2 -->
<div>
<h3 class="text-lg font-bold text-black mb-3">Zone 2</h3>
<ul class="text-sm text-black space-y-2 list-disc list-inside">
<li>Zone 2 training can be done on consecutive or daily basis with moderate sessions.</li>
<li>Can be steady state or interval sessions.</li>
</ul>
</div>
<!-- Zone 3 -->
<div>
<h3 class="text-lg font-bold text-black mb-3">Zone 3</h3>
<ul class="text-sm text-black space-y-2 list-disc list-inside">
<li>Zone 3 training can be done 1-5 times per week.</li>
<li>Wait 24 to 48 hours between sessions for adequate recovery.</li>
</ul>
</div>
<!-- Zone 4 -->
<div>
<h3 class="text-lg font-bold text-black mb-3">Zone 4</h3>
<ul class="text-sm text-black space-y-2 list-disc list-inside">
<li>Zone 4 training: 1-4 times per week.</li>
<li>Wait 24 to 48 hours between intense sessions for recovery.</li>
</ul>
</div>
<!-- Zone 5 -->
<div>
<h3 class="text-lg font-bold text-black mb-3">Zone 5</h3>
<ul class="text-sm text-black space-y-2 list-disc list-inside">
<li>Zone 5 training: 1-2 times per week.</li>
<li>Wait 48 hours between sessions for recovery.</li>
<li>Zone 5 increases VO2 max and endurance at VO2 max.</li>
</ul>
</div>
</div>
<!-- Important Note -->
<div class="text-center">
<p class="text-lg font-bold text-black italic">Zone 3, 4, 5 can be combined with Zone 1 or 2 - the higher zone should be done first!</p>
</div>
</div>
</div>
</div>
+70
View File
@@ -0,0 +1,70 @@
<div class="w-full page bg-white">
<!-- Main Content -->
<div class="px-8 py-6">
<!-- Page Title -->
<h1 class="text-3xl font-bold text-black mb-8">Next Steps:</h1>
<!-- Calorie Tracking Section -->
<div class="mb-8">
<h2 class="text-xl font-bold text-black mb-4">Calorie Tracking</h2>
<ul class="text-black space-y-2 list-disc list-inside ml-4">
<li>Download and create an account with My Fitness Pal (or preferred nutrition tracker)</li>
<li>Fill out the "My Profile" section with your goals (ie: height, weight, target weight)
<ul class="mt-2 ml-6 space-y-1 list-disc list-inside">
<li>Input your Macros</li>
<li>Click the three dots on the bottom right corner</li>
<li>Click "Goals"</li>
<li>Click "Calorie, Carbs, Protein and Fat Goals" under the Nutrition Goals</li>
<li>Set the Calories, Carbs, Protein, and Fat to the recommended macro outlined above.</li>
</ul>
</li>
<li>Once completed fill out your food intake from each meal on the main page</li>
</ul>
<!-- Recommendation Note -->
<div class="mt-6 text-center">
<p class="text-black italic font-semibold">It's highly recommended to purchase a weight and food scale for more accurate results.</p>
</div>
</div>
<!-- Daily Tasks Section -->
<div class="mb-8">
<h2 class="text-xl font-bold text-black mb-4">Daily Tasks</h2>
<ul class="text-black space-y-2 list-disc list-inside ml-4">
<li>Weigh yourself in the morning, after your first bowel movement, and naked</li>
<li>Log your weight into your my fitness pal app</li>
<li>Track calories in grams - FOLLOW YOUR PERSONAL RECOMMENDATIONS.</li>
<li>Log in a diary:
<ul class="mt-2 ml-6 space-y-1 list-disc list-inside">
<li>Log any additional prescribed recommendation (i.e breath work)</li>
<li>Complete the prescribed training recommendations (i.e Zone 2 Training)</li>
<li>Log additional physical activity (i.e Monday - Strength Training 1 hour)</li>
</ul>
</li>
</ul>
</div>
<!-- Two weeks after Appointment Section -->
<div class="mb-8">
<h2 class="text-xl font-bold text-black mb-4">Two weeks after Appointment</h2>
<ul class="text-black space-y-2 list-disc list-inside ml-4">
<li>Should you find the macronutrient breakdown difficult to follow, reach out to us to discuss a change within your caloric parameters</li>
</ul>
</div>
<!-- Contact Information -->
<div class="mb-12 text-center">
<p class="text-lg font-bold text-black">Should you have any questions or concerns please contact us!</p>
</div>
<!-- Recommended Next Testing Date -->
<div class="mb-8 text-center">
<h2 class="text-2xl font-bold text-black">
<span class="underline">Recommended Next Testing Date:</span>
<span class="ml-2">October 2025</span>
</h2>
</div>
</div>
</div>
+57
View File
@@ -0,0 +1,57 @@
<div class="w-full page bg-white">
<!-- Main Content -->
<div class="px-6">
<!-- Page Title -->
<h1 class="text-2xl font-bold text-black mb-4">Glossary</h1>
<!-- Body Fat Percentage -->
<div class="mb-3">
<h2 class="text-base font-bold text-black mb-1">Body Fat Percentage:</h2>
<p class="text-xs text-black leading-snug">The percentage of your overall body weight that is composed of fat cells. Body fat percentage can be reduced by either losing weight from fat mass, while maintaining lean mass, or maintaining fat mass while increasing lean mass.</p>
</div>
<!-- Metabolic Rate -->
<div class="mb-3">
<h2 class="text-base font-bold text-black mb-1">Metabolic Rate:</h2>
<p class="text-xs text-black leading-snug">Metabolic Rate measures the number of calories your body burns for basic functions and movement, based on factors like weight, age, gender, and height. A higher metabolic rate helps prevent weight gain and supports weight loss by ensuring you burn enough calories. Tracking metabolic rate is key for managing weight and preventing conditions linked to metabolic dysfunction. Positive influences include resistance exercise, proper sleep, and adequate protein, while negative factors include extreme dieting, yo-yo dieting, and excessive cardio. Improving it involves resistance training and optimal nutrition.</p>
</div>
<!-- Fuel Source -->
<div class="mb-3">
<h2 class="text-base font-bold text-black mb-1">Fuel Source:</h2>
<p class="text-xs text-black leading-snug mb-1">Fat-burning efficiency measures your cells' ability to use fat as fuel, reflecting mitochondrial and cellular health. It indicates how well your body balances fat and carbohydrate usage to support energy needs, assessed by analyzing oxygen and carbon dioxide in your breath. High fat-burning efficiency suggests strong metabolic and mitochondrial function, linked to better weight management and longevity.</p>
<p class="text-xs text-black leading-snug">To improve fat-burning efficiency, focus on Zone 2 endurance training and potentially intermittent fasting to enhance oxygen absorption and cellular function. Zone 5 interval training will also help improve fat burning mitochondrial density and capillarization. Factors that reduce fat burning ability include diets high in processed foods, alcohol, and large meals before bed. Conditions related to metabolic stress also hinder fat burning abilities.</p>
</div>
<!-- NEAT -->
<div class="mb-3">
<h2 class="text-base font-bold text-black mb-1">NEAT (Non-Exercise Activity Thermogenesis)</h2>
<p class="text-xs text-black leading-snug">refers to the energy expended for all activities that are not deliberate exercise or structured physical activity. This includes daily movements such as walking, fidgeting, standing, cleaning, typing, and even simple tasks like cooking or shopping. NEAT contributes significantly to the total caloric expenditure and plays a key role in maintaining body weight and overall energy balance. It varies widely among individuals, depending on lifestyle, occupation, and habits.</p>
</div>
<!-- Spirometry -->
<div class="mb-3">
<h2 class="text-base font-bold text-black mb-1">Spirometry:</h2>
<p class="text-xs text-black leading-snug mb-1">Spirometry is a diagnostic device used to provide objective measurements of lung volumes and capacities. Lung function is crucial for oxygen delivery during physical activity, and comparing spirometry results to expected values can highlight any potential limitations to performance.</p>
<p class="text-xs text-black leading-snug mb-1">"From a Performance standpoint, it is essential in making informed training recommendations related to respiratory health to optimize endurance performance and metabolic health."</p>
<!-- Spirometry Sub-definitions -->
<ul class="text-xs text-black space-y-1 list-disc list-inside ml-3">
<li><strong>FEV1:</strong> Forced Expiratory Volume - the total amount of air expelled in the first second.</li>
<li><strong>FVC:</strong> Forced Vital Capacity - the maximum amount of air exhaled in one breath after a maximum inhalation</li>
<li><strong>FEV1/FVC:</strong> Calculated ratio used in the diagnosis of obstructive & restrictive lung disease.</li>
</ul>
<p class="text-xs text-black leading-snug mt-1">By comparing these measurements to expected values based on age, gender, height and ethnicity, healthcare professionals can diagnose a range of lung conditions such as asthma, COPD, restrictive lung diseases, and more.</p>
</div>
<!-- VO2 max -->
<div>
<h2 class="text-base font-bold text-black mb-1">VO2 max:</h2>
<p class="text-xs text-black leading-snug mb-1">VO2 Max, or maximal oxygen consumption serves as a valuable indicator of overall fitness, cardiovascular health, and endurance capacity. VO2 max reflects the efficiency of your heart lung system in pumping oxygen-rich blood to working muscles. A higher VO2 max indicates a stronger cardiovascular system, which is associated with a reduced risk of heart disease and other cardiovascular issues.</p>
<p class="text-xs text-black leading-snug">Understanding and training to increase your VO2 max can contribute to enhanced physical performance, longevity and well-being.</p>
</div>
</div>
</div>
+88
View File
@@ -0,0 +1,88 @@
<!-- Header -->
<div class="w-full page">
<!-- Main Content -->
<div class="p-4 text-sm">
<h1 class="text-2xl font-bold mb-4">Glossary</h1>
<div class="space-y-3 leading-tight">
<!-- Peak VT -->
<div>
<p class="font-semibold">Peak VT:</p>
<p class="mb-1">
Peak Volume of air moved throughout the test.
</p>
<p class="mb-1">
Respiratory Capability Limitations that can be found include:
</p>
<ul class="list-disc ml-4 space-y-0">
<li>
<strong>Endurance:</strong> Normal capacity, but cannot maintain their VT over time.
</li>
<li>
<strong>Strength/Power:</strong> Normal capacity, but peak VT is not 75-85% of their FEV1 despite FEV1 being normal
</li>
<li>
<strong>Coordination (Hyper/Hypo-Ventilation):</strong> Normal capacity, but uses low volumes +/- high BFs at lower intensities. A breathing coordination limitation can also be identified by the loss of volume at higher intensities, which are then recovered upon recovery/stop of activity.
</li>
</ul>
</div>
<!-- VO2 Pulse -->
<div>
<p class="font-semibold">VO2 Pulse:</p>
<p>
VO2 Pulse refers to the relationship between oxygen consumption (VO2) and heart rate (HR) during exercise. This measure gives insight into how efficiently the body is using oxygen in relation to the heart's output. A higher VO2 Pulse suggests that an individual is able to deliver oxygen more efficiently to the muscles with each heartbeat.
</p>
</div>
<!-- VO2 Breath -->
<div>
<p class="font-semibold">VO2 Breath:</p>
<p>
VO2 Breath refers to the amount of oxygen consumed per breath during exercise, which indicates how effectively the body delivers oxygen to the bloodstream through the lungs with each breath. A more efficient VO2 Breath means the body requires less effort to obtain the same amount of oxygen, indicating better respiratory efficiency and oxygen utilization.
</p>
</div>
<!-- Carb & Fat Crossover -->
<div>
<p class="font-semibold">Carb & Fat Crossover:</p>
<p class="mb-1">
The point during exercise at which the body shifts its predominant fuel source from fats to carbohydrates. This transition typically occurs as exercise intensity increases, and marks the transition from Zone 2 into Zone 3. As exercise intensity increases, the body starts to rely more on carbohydrates because they provide faster energy.
</p>
<p>
Endurance training (e.g., long, steady-state cardio within Zones 1 & 2) increases the body's ability to burn fat efficiently at higher intensities, shifting the crossover point to a faster speed, or higher heart rate/intensity. Because fat stores are much larger and can provide a steady stream of energy for prolonged periods, a higher CHO/FAT crossover can help delay fatigue, which is especially beneficial in longer-duration events, where carbohydrate depletion can lead to a significant drop in performance.
</p>
</div>
<!-- Cardiovascular Recovery -->
<div>
<p class="font-semibold">Cardiovascular Recovery:</p>
<p>
The percentage your heart rate drops within the first minute of the inactive recovery phase in relation to the lowest heart rate recorded prior to the start of the test.
</p>
</div>
<!-- Metabolic (CO2) Recovery -->
<div>
<p class="font-semibold">Metabolic (CO2) Recovery:</p>
<p class="mb-1">
The percentage that your VCO2 levels (amount of CO2 you are exhaling) drop within the first 1.5 minutes of the inactive recovery phase in relation to the lowest VCO2 recorded prior to the start of the test.
</p>
<p>
refers to the rate at which the body clears carbon dioxide (CO2) after exercise, reflecting the efficiency of the cardiovascular and respiratory systems in returning CO2 levels to baseline. A faster VCO2 recovery indicates effective management of metabolic byproducts, signaling a healthier metabolic system and lower risk of metabolic disorders.
</p>
</div>
<!-- Breath Frequency Recovery -->
<div>
<p class="font-semibold">Breath Frequency Recovery:</p>
<p>
Refers to the speed at which the body returns to a normal breathing rate after physical exertion. Faster breath frequency recovery indicates a well-conditioned cardiovascular and respiratory system, allowing the body to efficiently regulate oxygen and CO2 levels. It supports better endurance, faster recovery between intervals, and the ability to sustain higher performance during repeated efforts or prolonged activity. Additionally, a quick return to baseline signals that the autonomic nervous system is functioning well, reducing stress on the body and promoting more efficient recovery. This also reflects a healthier metabolic system, better management of metabolic byproducts like CO2, and a lower risk of chronic conditions.
</p>
</div>
</div>
</div>
<!-- Footer -->
</div>
+38
View File
@@ -0,0 +1,38 @@
<div class="w-full page">
<!-- Main Content -->
<div class="p-8">
<h1 class="text-4xl font-bold mb-8">Glossary</h1>
<!-- Local Muscle Activity/SMO2 Definition -->
<div class="mb-12 text-sm leading-relaxed">
<p class="font-semibold mb-2">Local Muscle Activity/SMO2:</p>
<p>
SmO2 testing is a valuable tool for understanding how muscles
respond to various physiological stressors and how to fine-tune
training, nutrition and hydration accordingly. Monitoring
changes in tissue oxygen saturation and utilization helps
identify an individual's optimal intensity to work at, as well
as how well they recover between bouts of intensity. This can
help optimize training to improve performance, prevent
overtraining, and tailor strategies for better results.
</p>
<p>
During competitions, athletes can also use SmO2 data to pace
themselves effectively. Adjusting intensity based on muscle
oxygenation can help prevent premature fatigue and optimize race
outcomes
</p>
</div>
<div class="w-full max-w-5xl">
<h1 class="text-2xl font-bold mb-4 text-center">
Body Fat Percent Master Chart
</h1>
<img
src="data:image/png;base64,{{ body_fat_percentage_chart }}"
alt="Body Fat Percentage"
class="w-full h-auto object-contain chart-large"
/>
</div>
</div>
</div>
+814
View File
@@ -0,0 +1,814 @@
<div class="w-full page bg-white">
<!-- Main Content -->
<div class="p-6">
<h1 class="text-3xl font-bold mb-6">Glossary</h1>
<!-- Resting Heart Rate Section -->
<div class="mb-8">
<h2 class="text-xl font-bold mb-4 text-center">
Resting Heart Rate
</h2>
<!-- Male Table -->
<div class="mb-4">
<table
class="w-full border-collapse border border-gray-300 text-xs"
>
<thead>
<tr class="bg-cyan-200">
<th
class="border border-gray-300 p-1 font-bold text-center"
>
Age (M)
</th>
<th
class="border border-gray-300 p-1 font-bold text-center"
>
Poor
</th>
<th
class="border border-gray-300 p-1 font-bold text-center"
>
Below Average
</th>
<th
class="border border-gray-300 p-1 font-bold text-center"
>
Average
</th>
<th
class="border border-gray-300 p-1 font-bold text-center"
>
Above Average
</th>
<th
class="border border-gray-300 p-1 font-bold text-center"
>
Good
</th>
<th
class="border border-gray-300 p-1 font-bold text-center"
>
Excellent
</th>
<th
class="border border-gray-300 p-1 font-bold text-center"
>
Athlete
</th>
</tr>
</thead>
<tbody>
<tr>
<td
class="border border-gray-300 p-1 font-medium text-center"
>
18-25
</td>
<td class="border border-gray-300 p-1 text-center">
85bpm +
</td>
<td class="border border-gray-300 p-1 text-center">
76-84bpm
</td>
<td class="border border-gray-300 p-1 text-center">
74-78bpm
</td>
<td class="border border-gray-300 p-1 text-center">
70-73bpm
</td>
<td class="border border-gray-300 p-1 text-center">
66-69bpm
</td>
<td class="border border-gray-300 p-1 text-center">
61-65bpm
</td>
<td class="border border-gray-300 p-1 text-center">
60-60bpm
</td>
</tr>
<tr>
<td
class="border border-gray-300 p-1 font-medium text-center"
>
26-35
</td>
<td class="border border-gray-300 p-1 text-center">
83bpm +
</td>
<td class="border border-gray-300 p-1 text-center">
77-82bpm
</td>
<td class="border border-gray-300 p-1 text-center">
73-76bpm
</td>
<td class="border border-gray-300 p-1 text-center">
69-72bpm
</td>
<td class="border border-gray-300 p-1 text-center">
65-68bpm
</td>
<td class="border border-gray-300 p-1 text-center">
60-64bpm
</td>
<td class="border border-gray-300 p-1 text-center">
55-59bpm
</td>
</tr>
<tr>
<td
class="border border-gray-300 p-1 font-medium text-center"
>
36-45
</td>
<td class="border border-gray-300 p-1 text-center">
85bpm +
</td>
<td class="border border-gray-300 p-1 text-center">
79-84bpm
</td>
<td class="border border-gray-300 p-1 text-center">
74-78bpm
</td>
<td class="border border-gray-300 p-1 text-center">
70-73bpm
</td>
<td class="border border-gray-300 p-1 text-center">
65-69bpm
</td>
<td class="border border-gray-300 p-1 text-center">
60-64bpm
</td>
<td class="border border-gray-300 p-1 text-center">
55-59bpm
</td>
</tr>
<tr>
<td
class="border border-gray-300 p-1 font-medium text-center"
>
46-55
</td>
<td class="border border-gray-300 p-1 text-center">
84bpm +
</td>
<td class="border border-gray-300 p-1 text-center">
76-83bpm
</td>
<td class="border border-gray-300 p-1 text-center">
73-77bpm
</td>
<td class="border border-gray-300 p-1 text-center">
70-72bpm
</td>
<td class="border border-gray-300 p-1 text-center">
66-69bpm
</td>
<td class="border border-gray-300 p-1 text-center">
61-65bpm
</td>
<td class="border border-gray-300 p-1 text-center">
56-60bpm
</td>
</tr>
<tr>
<td
class="border border-gray-300 p-1 font-medium text-center"
>
56-65
</td>
<td class="border border-gray-300 p-1 text-center">
85bpm +
</td>
<td class="border border-gray-300 p-1 text-center">
78-84bpm
</td>
<td class="border border-gray-300 p-1 text-center">
74-77bpm
</td>
<td class="border border-gray-300 p-1 text-center">
70-73bpm
</td>
<td class="border border-gray-300 p-1 text-center">
65-69bpm
</td>
<td class="border border-gray-300 p-1 text-center">
60-64bpm
</td>
<td class="border border-gray-300 p-1 text-center">
50-59bpm
</td>
</tr>
<tr>
<td
class="border border-gray-300 p-1 font-medium text-center"
>
65+
</td>
<td class="border border-gray-300 p-1 text-center">
84bpm +
</td>
<td class="border border-gray-300 p-1 text-center">
77-83bpm
</td>
<td class="border border-gray-300 p-1 text-center">
73-76bpm
</td>
<td class="border border-gray-300 p-1 text-center">
70-73bpm
</td>
<td class="border border-gray-300 p-1 text-center">
65-69bpm
</td>
<td class="border border-gray-300 p-1 text-center">
60-64bpm
</td>
<td class="border border-gray-300 p-1 text-center">
55-59bpm
</td>
</tr>
</tbody>
</table>
</div>
<!-- Female Table -->
<div class="mb-4">
<table
class="w-full border-collapse border border-gray-300 text-xs"
>
<thead>
<tr class="bg-cyan-200">
<th
class="border border-gray-300 p-1 font-bold text-center"
>
Age (F)
</th>
<th
class="border border-gray-300 p-1 font-bold text-center"
>
Poor
</th>
<th
class="border border-gray-300 p-1 font-bold text-center"
>
Below Average
</th>
<th
class="border border-gray-300 p-1 font-bold text-center"
>
Average
</th>
<th
class="border border-gray-300 p-1 font-bold text-center"
>
Above Average
</th>
<th
class="border border-gray-300 p-1 font-bold text-center"
>
Good
</th>
<th
class="border border-gray-300 p-1 font-bold text-center"
>
Excellent
</th>
<th
class="border border-gray-300 p-1 font-bold text-center"
>
Athlete
</th>
</tr>
</thead>
<tbody>
<tr>
<td
class="border border-gray-300 p-1 font-medium text-center"
>
18-25
</td>
<td class="border border-gray-300 p-1 text-center">
81bpm +
</td>
<td class="border border-gray-300 p-1 text-center">
74-81bpm
</td>
<td class="border border-gray-300 p-1 text-center">
73-78bpm
</td>
<td class="border border-gray-300 p-1 text-center">
66-69bpm
</td>
<td class="border border-gray-300 p-1 text-center">
62-65bpm
</td>
<td class="border border-gray-300 p-1 text-center">
56-61bpm
</td>
<td class="border border-gray-300 p-1 text-center">
50-55bpm
</td>
</tr>
<tr>
<td
class="border border-gray-300 p-1 font-medium text-center"
>
26-35
</td>
<td class="border border-gray-300 p-1 text-center">
82bpm +
</td>
<td class="border border-gray-300 p-1 text-center">
75-81bpm
</td>
<td class="border border-gray-300 p-1 text-center">
71-74bpm
</td>
<td class="border border-gray-300 p-1 text-center">
66-70bpm
</td>
<td class="border border-gray-300 p-1 text-center">
62-65bpm
</td>
<td class="border border-gray-300 p-1 text-center">
55-61bpm
</td>
<td class="border border-gray-300 p-1 text-center">
54-54bpm
</td>
</tr>
<tr>
<td
class="border border-gray-300 p-1 font-medium text-center"
>
36-45
</td>
<td class="border border-gray-300 p-1 text-center">
83bpm +
</td>
<td class="border border-gray-300 p-1 text-center">
76-82bpm
</td>
<td class="border border-gray-300 p-1 text-center">
71-75bpm
</td>
<td class="border border-gray-300 p-1 text-center">
67-70bpm
</td>
<td class="border border-gray-300 p-1 text-center">
63-66bpm
</td>
<td class="border border-gray-300 p-1 text-center">
57-62bpm
</td>
<td class="border border-gray-300 p-1 text-center">
47-56bpm
</td>
</tr>
<tr>
<td
class="border border-gray-300 p-1 font-medium text-center"
>
46-55
</td>
<td class="border border-gray-300 p-1 text-center">
84bpm +
</td>
<td class="border border-gray-300 p-1 text-center">
77-83bpm
</td>
<td class="border border-gray-300 p-1 text-center">
72-76bpm
</td>
<td class="border border-gray-300 p-1 text-center">
68-71bpm
</td>
<td class="border border-gray-300 p-1 text-center">
64-67bpm
</td>
<td class="border border-gray-300 p-1 text-center">
58-63bpm
</td>
<td class="border border-gray-300 p-1 text-center">
49-57bpm
</td>
</tr>
<tr>
<td
class="border border-gray-300 p-1 font-medium text-center"
>
56-65
</td>
<td class="border border-gray-300 p-1 text-center">
82bpm +
</td>
<td class="border border-gray-300 p-1 text-center">
76-81bpm
</td>
<td class="border border-gray-300 p-1 text-center">
72-75bpm
</td>
<td class="border border-gray-300 p-1 text-center">
68-71bpm
</td>
<td class="border border-gray-300 p-1 text-center">
62-67bpm
</td>
<td class="border border-gray-300 p-1 text-center">
57-61bpm
</td>
<td class="border border-gray-300 p-1 text-center">
51-56bpm
</td>
</tr>
<tr>
<td
class="border border-gray-300 p-1 font-medium text-center"
>
65+
</td>
<td class="border border-gray-300 p-1 text-center">
80bpm +
</td>
<td class="border border-gray-300 p-1 text-center">
74-79bpm
</td>
<td class="border border-gray-300 p-1 text-center">
70-73bpm
</td>
<td class="border border-gray-300 p-1 text-center">
66-69bpm
</td>
<td class="border border-gray-300 p-1 text-center">
62-65bpm
</td>
<td class="border border-gray-300 p-1 text-center">
56-61bpm
</td>
<td class="border border-gray-300 p-1 text-center">
52-55bpm
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- VO2 Master Chart Section -->
<div class="mb-4">
<h2 class="text-xl font-bold mb-4 text-center">
VO2 Master Chart
</h2>
<!-- Male VO2 Table -->
<div class="mb-4">
<table
class="w-full border-collapse border border-gray-300 text-xs"
>
<thead>
<tr class="bg-cyan-200">
<th
class="border border-gray-300 p-1 font-bold text-center"
>
Age (M)
</th>
<th
class="border border-gray-300 p-1 font-bold text-center"
>
Very Poor
</th>
<th
class="border border-gray-300 p-1 font-bold text-center"
>
Poor
</th>
<th
class="border border-gray-300 p-1 font-bold text-center"
>
Fair
</th>
<th
class="border border-gray-300 p-1 font-bold text-center"
>
Good
</th>
<th
class="border border-gray-300 p-1 font-bold text-center"
>
Excellent
</th>
<th
class="border border-gray-300 p-1 font-bold text-center"
>
Superior
</th>
</tr>
</thead>
<tbody>
<tr>
<td
class="border border-gray-300 p-1 font-medium text-center"
>
20-29
</td>
<td class="border border-gray-300 p-1 text-center">
29.0-38.1
</td>
<td class="border border-gray-300 p-1 text-center">
38.1-44.9
</td>
<td class="border border-gray-300 p-1 text-center">
44.9-50.2
</td>
<td class="border border-gray-300 p-1 text-center">
50.2-61.8
</td>
<td class="border border-gray-300 p-1 text-center">
57.1-66.3
</td>
<td class="border border-gray-300 p-1 text-center">
66.3+
</td>
</tr>
<tr>
<td
class="border border-gray-300 p-1 font-medium text-center"
>
30-39
</td>
<td class="border border-gray-300 p-1 text-center">
27.2-34.1
</td>
<td class="border border-gray-300 p-1 text-center">
34.1-39.6
</td>
<td class="border border-gray-300 p-1 text-center">
39.6-45.2
</td>
<td class="border border-gray-300 p-1 text-center">
45.2-51.6
</td>
<td class="border border-gray-300 p-1 text-center">
51.6-59.8
</td>
<td class="border border-gray-300 p-1 text-center">
59.8+
</td>
</tr>
<tr>
<td
class="border border-gray-300 p-1 font-medium text-center"
>
40-49
</td>
<td class="border border-gray-300 p-1 text-center">
24.2-30.5
</td>
<td class="border border-gray-300 p-1 text-center">
30.5-35.7
</td>
<td class="border border-gray-300 p-1 text-center">
35.7-40.3
</td>
<td class="border border-gray-300 p-1 text-center">
40.3-46.7
</td>
<td class="border border-gray-300 p-1 text-center">
46.7-55.6
</td>
<td class="border border-gray-300 p-1 text-center">
55.6+
</td>
</tr>
<tr>
<td
class="border border-gray-300 p-1 font-medium text-center"
>
50-59
</td>
<td class="border border-gray-300 p-1 text-center">
20.9-26.1
</td>
<td class="border border-gray-300 p-1 text-center">
26.1-30.7
</td>
<td class="border border-gray-300 p-1 text-center">
30.7-35.1
</td>
<td class="border border-gray-300 p-1 text-center">
35.1-41.2
</td>
<td class="border border-gray-300 p-1 text-center">
41.2-50.7
</td>
<td class="border border-gray-300 p-1 text-center">
50.7+
</td>
</tr>
<tr>
<td
class="border border-gray-300 p-1 font-medium text-center"
>
60-69
</td>
<td class="border border-gray-300 p-1 text-center">
17.4-22.4
</td>
<td class="border border-gray-300 p-1 text-center">
22.4-26.6
</td>
<td class="border border-gray-300 p-1 text-center">
26.6-30.5
</td>
<td class="border border-gray-300 p-1 text-center">
30.5-36.1
</td>
<td class="border border-gray-300 p-1 text-center">
36.1-43.0
</td>
<td class="border border-gray-300 p-1 text-center">
43.0+
</td>
</tr>
</tbody>
</table>
</div>
<!-- Female VO2 Table -->
<div class="mb-4">
<table
class="w-full border-collapse border border-gray-300 text-xs"
>
<thead>
<tr class="bg-cyan-200">
<th
class="border border-gray-300 p-1 font-bold text-center"
>
Age (F)
</th>
<th
class="border border-gray-300 p-1 font-bold text-center"
>
Very Poor
</th>
<th
class="border border-gray-300 p-1 font-bold text-center"
>
Poor
</th>
<th
class="border border-gray-300 p-1 font-bold text-center"
>
Fair
</th>
<th
class="border border-gray-300 p-1 font-bold text-center"
>
Good
</th>
<th
class="border border-gray-300 p-1 font-bold text-center"
>
Excellent
</th>
<th
class="border border-gray-300 p-1 font-bold text-center"
>
Superior
</th>
</tr>
</thead>
<tbody>
<tr>
<td
class="border border-gray-300 p-1 font-medium text-center"
>
20-29
</td>
<td class="border border-gray-300 p-1 text-center">
21.7-28.6
</td>
<td class="border border-gray-300 p-1 text-center">
28.6-34.6
</td>
<td class="border border-gray-300 p-1 text-center">
34.6-40.6
</td>
<td class="border border-gray-300 p-1 text-center">
40.6-46.5
</td>
<td class="border border-gray-300 p-1 text-center">
46.5-56.0
</td>
<td class="border border-gray-300 p-1 text-center">
56.0+
</td>
</tr>
<tr>
<td
class="border border-gray-300 p-1 font-medium text-center"
>
30-39
</td>
<td class="border border-gray-300 p-1 text-center">
19.0-24.1
</td>
<td class="border border-gray-300 p-1 text-center">
24.1-28.2
</td>
<td class="border border-gray-300 p-1 text-center">
28.2-32.2
</td>
<td class="border border-gray-300 p-1 text-center">
32.2-35.7
</td>
<td class="border border-gray-300 p-1 text-center">
35.7-45.8
</td>
<td class="border border-gray-300 p-1 text-center">
45.8+
</td>
</tr>
<tr>
<td
class="border border-gray-300 p-1 font-medium text-center"
>
40-49
</td>
<td class="border border-gray-300 p-1 text-center">
17.0-21.3
</td>
<td class="border border-gray-300 p-1 text-center">
21.3-24.9
</td>
<td class="border border-gray-300 p-1 text-center">
24.9-28.7
</td>
<td class="border border-gray-300 p-1 text-center">
28.7-34.0
</td>
<td class="border border-gray-300 p-1 text-center">
34.0-41.7
</td>
<td class="border border-gray-300 p-1 text-center">
41.7+
</td>
</tr>
<tr>
<td
class="border border-gray-300 p-1 font-medium text-center"
>
50-59
</td>
<td class="border border-gray-300 p-1 text-center">
16.0-19.1
</td>
<td class="border border-gray-300 p-1 text-center">
19.1-24.4
</td>
<td class="border border-gray-300 p-1 text-center">
21.8-27.6
</td>
<td class="border border-gray-300 p-1 text-center">
25.2-28.6
</td>
<td class="border border-gray-300 p-1 text-center">
28.6-35.9
</td>
<td class="border border-gray-300 p-1 text-center">
35.9+
</td>
</tr>
<tr>
<td
class="border border-gray-300 p-1 font-medium text-center"
>
60-69
</td>
<td class="border border-gray-300 p-1 text-center">
13.4-16.5
</td>
<td class="border border-gray-300 p-1 text-center">
16.5-18.9
</td>
<td class="border border-gray-300 p-1 text-center">
18.9-21.2
</td>
<td class="border border-gray-300 p-1 text-center">
21.2-24.6
</td>
<td class="border border-gray-300 p-1 text-center">
24.6-29.4
</td>
<td class="border border-gray-300 p-1 text-center">
29.4+
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
+143
View File
@@ -0,0 +1,143 @@
<div class="bg-white w-full page m-0 px-10">
<div class="px-16 pt-10">
<!-- Table of Contents Header -->
<div class="mb-2">
<h1
class="text-5xl font-bold text-black mb-6 tracking-wide border-b-4 border-blue-500 pb-2 text-center"
>
TABLE OF CONTENTS
</h1>
<div class="w-full h-1 bg-cyan-400"></div>
</div>
<!-- Table of Contents Items -->
<div class="flex flex-col justify-between space-y-6 py-6">
<!-- Nutrition Guidelines -->
<div class="flex items-start bg-gray-200 h-24">
<div
class="bg-black text-white text-2xl font-bold w-16 h-full flex items-center justify-center mr-8 flex-shrink-0"
>
4
</div>
<div class="flex flex-col flex-1 py-1 justify-center h-full">
<h2 class="text-2xl font-semibold text-black">
Nutrition Guidelines
</h2>
<p class="text-gray-600 text-base">
Ultrasound & Body Composition Assessment
</p>
<p class="text-gray-600 text-base">
Resting Metabolic Rate Assessment
</p>
</div>
</div>
<!-- Nutrition Recommendations -->
<div class="flex items-start bg-gray-200 h-24">
<div
class="bg-black text-white text-2xl font-bold w-16 h-full flex items-center justify-center mr-8 flex-shrink-0"
>
6
</div>
<div class="flex flex-col py-1 flex-1 justify-center h-full">
<h2 class="text-2xl font-semibold text-black">
Nutrition Recommendations
</h2>
</div>
</div>
<!-- Lung Analysis -->
<div class="flex items-start bg-gray-200 h-24">
<div
class="bg-black text-white text-2xl font-bold w-16 h-full flex items-center justify-center mr-8 flex-shrink-0"
>
7
</div>
<div class="flex flex-col flex-1 py-1 justify-center h-full">
<h2 class="text-2xl font-semibold text-black">
Lung Analysis
</h2>
<p class="text-gray-600 text-base">
Pulse Oximetry Assessment
</p>
<p class="text-gray-600 text-base">
Spirometry Assessment
</p>
</div>
</div>
<!-- Cardio Metrics -->
<div class="flex items-start bg-gray-200 h-24">
<div
class="bg-black text-white text-2xl font-bold w-16 h-full flex items-center justify-center mr-8 flex-shrink-0"
>
8
</div>
<div class="flex flex-col py-1 flex-1 justify-center h-full">
<h2 class="text-2xl font-semibold text-black">
Cardio Metrics
</h2>
<p class="text-gray-600 text-base">
Active Metabolic Rate Assessment
</p>
</div>
</div>
<!-- Local Muscle Activity -->
<div class="flex items-start bg-gray-200 h-24">
<div
class="bg-black text-white text-2xl font-bold w-16 h-full flex items-center justify-center mr-8 flex-shrink-0"
>
11
</div>
<div class="flex flex-col justify-center h-full flex-1">
<h2 class="text-2xl font-semibold text-black">
Local Muscle Activity
</h2>
</div>
</div>
<!-- Training Recommendations -->
<div class="flex items-start bg-gray-200 h-24">
<div
class="bg-black text-white text-2xl font-bold w-16 h-full flex items-center justify-center mr-8 flex-shrink-0"
>
12
</div>
<div class="flex flex-col h-full justify-center flex-1">
<h2 class="text-2xl font-semibold text-black">
Training Recommendations
</h2>
</div>
</div>
<!-- Next Steps -->
<div class="flex items-start bg-gray-200 h-24">
<div
class="bg-black text-white text-2xl font-bold w-16 h-full flex items-center justify-center mr-8 flex-shrink-0"
>
14
</div>
<div class="flex flex-col h-full justify-center flex-1">
<h2 class="text-2xl font-semibold text-black">
Next Steps
</h2>
</div>
</div>
<!-- Glossary -->
<div class="flex items-start bg-gray-200 h-24">
<div
class="bg-black text-white text-2xl font-bold w-16 h-full flex items-center justify-center mr-8 flex-shrink-0"
>
15
</div>
<div class="flex flex-col h-full justify-center flex-1">
<h2 class="text-2xl font-semibold text-black">
Glossary
</h2>
</div>
</div>
</div>
</div>
</div>
+157
View File
@@ -0,0 +1,157 @@
<div class="w-full page bg-white">
<!-- Main Content -->
<div class="px-6 py-4">
<!-- Page Title -->
<h1 class="text-3xl font-bold text-black mb-6">Overview</h1>
<!-- Top Row: Metabolic and Respiratory -->
<div class="grid grid-cols-2 gap-6 mb-6">
<!-- Metabolic Section -->
<div class="border border-gray-300 rounded-lg p-3">
<div class="flex items-center mb-3">
<div class="w-6 h-6 bg-gray-400 rounded-full mr-2"></div>
<h2 class="text-lg font-bold">Metabolic</h2>
</div>
<table class="w-full text-xs border-collapse">
<tr>
<th class="bg-gray-200 p-2 border text-left font-semibold">Resting Metabolic Rate</th>
<th class="bg-gray-200 p-2 border text-left font-semibold">Active Metabolic Rate</th>
</tr>
<tr>
<td class="bg-red-200 p-2 border">Fat/Carbohydrate Ratio</td>
<td class="bg-red-200 p-2 border">Metabolic Efficiency Low Intensity</td>
</tr>
<tr>
<td class="bg-gray-200 p-2 border">Metabolism</td>
<td class="bg-yellow-200 p-2 border">Metabolic Efficiency High Intensity</td>
</tr>
<tr>
<td class="bg-yellow-200 p-2 border">Breathing Frequency</td>
<td class="bg-white p-2 border"></td>
</tr>
<tr>
<td class="bg-red-200 p-2 border">Breath Volume</td>
<td class="bg-white p-2 border"></td>
</tr>
<tr>
<td class="bg-green-200 p-2 border">Heart Rate</td>
<td class="bg-white p-2 border"></td>
</tr>
</table>
</div>
<!-- Respiratory Section -->
<div class="border border-gray-300 rounded-lg p-3">
<div class="flex items-center mb-3">
<div class="w-6 h-6 bg-gray-400 rounded-full mr-2"></div>
<h2 class="text-lg font-bold">Respiratory</h2>
</div>
<table class="w-full text-xs border-collapse">
<tr>
<th class="bg-gray-200 p-2 border text-left font-semibold">Lung Function</th>
</tr>
<tr>
<td class="bg-green-200 p-2 border">Lung Capacity</td>
</tr>
<tr>
<td class="bg-green-200 p-2 border">Lung Capability</td>
</tr>
<tr>
<td class="bg-red-200 p-2 border">Breathing Frequency Zones</td>
</tr>
</table>
</div>
</div>
<!-- Middle Row: Cardiovascular and Strength -->
<div class="grid grid-cols-2 gap-6 mb-6">
<!-- Cardiovascular Section -->
<div class="border border-gray-300 rounded-lg p-3">
<div class="flex items-center mb-3">
<div class="w-6 h-6 bg-gray-400 rounded-full mr-2"></div>
<h2 class="text-lg font-bold">Cardiovascular</h2>
</div>
<table class="w-full text-xs border-collapse">
<tr>
<th class="bg-gray-200 p-2 border text-left font-semibold">Active Metabolic Rate</th>
</tr>
<tr>
<td class="bg-green-200 p-2 border">Aerobic Health (VO2 Max)</td>
</tr>
<tr>
<td class="bg-red-200 p-2 border">Training Zones</td>
</tr>
<tr>
<td class="bg-red-200 p-2 border">Metabolic Efficiency (VO2 Pulse)</td>
</tr>
</table>
</div>
<!-- Strength Section -->
<div class="border border-gray-300 rounded-lg p-3">
<div class="flex items-center mb-3">
<div class="w-6 h-6 bg-gray-400 rounded-full mr-2"></div>
<h2 class="text-lg font-bold">Strength</h2>
</div>
<table class="w-full text-xs border-collapse">
<tr>
<th class="bg-gray-200 p-2 border text-left font-semibold">Strength - High Intensity</th>
</tr>
<tr>
<td class="bg-green-200 p-2 border">CO2/O2 (RER)</td>
</tr>
<tr>
<td class="bg-green-200 p-2 border">Heart Rate</td>
</tr>
<tr>
<td class="bg-green-200 p-2 border">Breath Frequency</td>
</tr>
<tr>
<td class="bg-yellow-200 p-2 border">Muscle Efficiency</td>
</tr>
</table>
</div>
</div>
<!-- Bottom Row: Recovery -->
<div class="grid grid-cols-2 gap-6">
<!-- Recovery Section -->
<div class="border border-gray-300 rounded-lg p-3">
<div class="flex items-center mb-3">
<div class="w-6 h-6 bg-gray-400 rounded-full mr-2"></div>
<h2 class="text-lg font-bold">Recovery</h2>
</div>
<table class="w-full text-xs border-collapse">
<tr>
<th class="bg-gray-200 p-2 border text-left font-semibold">Active Metabolic Rate</th>
</tr>
<tr>
<td class="bg-green-200 p-2 border">
<div class="flex justify-between items-center">
<span>Heart Rate</span>
<span class="bg-green-600 text-white px-2 py-1 rounded font-bold text-xs">44</span>
</div>
</td>
</tr>
<tr>
<td class="bg-green-200 p-2 border">Metabolic (CO2)</td>
</tr>
<tr>
<td class="bg-gray-200 p-2 border">Muscle Oxygen</td>
</tr>
<tr>
<td class="bg-red-200 p-2 border">Breath Frequency</td>
</tr>
</table>
</div>
<!-- Empty space for balance -->
<div></div>
</div>
</div>
</div>
+36
View File
@@ -0,0 +1,36 @@
<div class="w-full page bg-white">
<!-- Main Content -->
<div class="px-8 py-6">
<!-- Page Title -->
<h1 class="text-3xl font-bold text-black mb-6">Nutrition Guidelines</h1>
<!-- Section Title -->
<h2 class="text-xl font-bold text-black mb-2">Ultrasound & Body Composition Assessment</h2>
<p class="text-gray-700 text-sm mb-8">Designed to track and optimize exercise and diet. Its proven technology can accurately measure tissue structure and body composition.</p>
<!-- Body Composition Section -->
<div class="mb-8">
<h3 class="text-2xl font-bold text-center text-black mb-6">Body Composition</h3>
<!-- Body Composition Chart -->
<div class="flex justify-center mb-16 w-full">
<img src="data:image/png;base64, {{ body_composition_chart}}"
alt="Body Composition Chart"
class=" object-contain ">
</div>
<!-- Body Fat Percentage Section -->
<div class="mb-8">
<h3 class="text-2xl font-bold text-center text-black mb-6">Body Fat Percentage - {{ fat_percentage }}%</h3>
<!-- Body Fat Chart -->
<div class="flex justify-center">
<img src="data:image/png;base64, {{ body_fat_chart }}"
alt="Body Fat Percentage Chart" >
</div>
<!-- Age Range Label -->
</div>
</div>
</div>
<!-- Footer Section -->
</div>
+97
View File
@@ -0,0 +1,97 @@
<div class="w-full bg-white">
<!-- Header Section -->
<!-- Main Content -->
<div class="px-8 py-2">
<!-- Page Title -->
<h1 class="text-3xl font-bold text-black mb-3">Nutrition Guidelines</h1>
<!-- Section Title -->
<h2 class="text-xl font-bold text-black mb-2">
Resting Metabolic Rate Assessment
</h2>
<p class="text-gray-700 text-sm mb-4">
The resting metabolic rate assessment determines the number of
calories that you burn at rest, and metabolic health. It is also an
indicator of overall health and well-being.
</p>
<!-- Slow vs Fast Metabolism Section -->
<div class="mb-6">
<div class="flex justify-center">
<img
src="data:image/png;base64,{{ metabolism_chart }}"
alt="Slow vs Fast Metabolism Chart"
class="max-w-full h-auto max-h-40"
/>
</div>
</div>
<!-- Fuel Source Section -->
<div class="mb-6">
<div class="flex justify-center">
<img
src="data:image/png;base64,{{ fuel_source_chart }}"
alt="Fuel Source Chart"
class="max-w-full h-auto max-h-40"
/>
</div>
</div>
<!-- Caloric Intake Section -->
<div class="px-6">
<h3 class="text-2xl font-bold text-black mb-4 text-center">
Caloric Intake
</h3>
<!-- Calculation Formula -->
<div class="flex items-center justify-center space-x-4 text-center">
<!-- Resting Metabolic -->
<div class="bg-gray-200 p-3 rounded-lg">
<div class="text-lg font-bold text-black">
{{ resting_calories }}kCals
</div>
<div class="text-xs text-gray-600 mt-1">
<div>Resting</div>
<div>Metabolic</div>
</div>
</div>
<!-- Plus sign -->
<div class="text-2xl font-bold text-black">+</div>
<!-- NEAT -->
<div class="bg-gray-200 p-3 rounded-lg">
<div class="text-lg font-bold text-black">
{{ neat_calories }}kCals
</div>
<div class="text-xs text-gray-600 mt-1">NEAT</div>
</div>
<!-- Minus sign -->
<div class="text-2xl font-bold text-black">-</div>
<!-- Weight Loss -->
<div class="bg-gray-200 p-3 rounded-lg">
<div class="text-lg font-bold text-black">
{{ weight_loss_calories }}kCals
</div>
<div class="text-xs text-gray-600 mt-1">
<div>to lose {{ weight_loss_rate }}lbs</div>
<div>per week</div>
</div>
</div>
<!-- Equals sign -->
<div class="text-2xl font-bold text-black">=</div>
<!-- Total -->
<div class="bg-gray-200 p-3 rounded-lg">
<div class="text-lg font-bold text-black">
~{{ total_calories }}kCals
</div>
</div>
</div>
</div>
</div>
</div>
+219
View File
@@ -0,0 +1,219 @@
<div class="w-full page bg-white">
<!-- Header Section -->
<!-- main content -->
<div class="px-8 py-6">
<!-- Page Title -->
<h1 class="text-3xl font-bold text-black mb-8 text-center">Weekly Meal Plan Breakdown</h1>
<!-- Caloric Deficit Example Section -->
<div class="mb-12">
<h2 class="text-2xl font-bold text-black mb-6 text-center">Caloric Deficit Example</h2>
<!-- Weekly Grid -->
<div class="grid grid-cols-7 gap-3 mb-8">
<!-- Monday -->
<div class="bg-cyan-300 p-3 text-left rounded-lg">
<div class="font-bold text-black mb-2">Monday</div>
<div class="text-lg font-bold text-black mb-2">{{ deficit_calories | default('1725KCals') }}</div>
<div class="text-xs text-black space-y-1">
<div>{{ deficit_protein | default('120g Protein') }}</div>
<div>{{ deficit_carbs | default('155g Carbs') }}</div>
<div>{{ deficit_fat | default('69g Fat') }}</div>
<div>{{ deficit_fiber | default('25g Fibre') }}</div>
</div>
</div>
<!-- Tuesday -->
<div class="bg-cyan-300 p-3 text-left rounded-lg">
<div class="font-bold text-black mb-2">Tuesday</div>
<div class="text-lg font-bold text-black mb-2">{{ deficit_calories | default('1725KCals') }}</div>
<div class="text-xs text-black space-y-1">
<div>{{ deficit_protein | default('120g Protein') }}</div>
<div class="text-xs text-black">{{ deficit_carbs | default('155g Carbs') }}</div>
<div>{{ deficit_fat | default('69g Fat') }}</div>
<div>{{ deficit_fiber | default('25g Fibre') }}</div>
</div>
</div>
<!-- Wednesday -->
<div class="bg-cyan-300 p-3 text-left rounded-lg">
<div class="font-bold text-black mb-2">Wednesday</div>
<div class="text-lg font-bold text-black mb-2">{{ deficit_calories | default('1725KCals') }}</div>
<div class="text-xs text-black space-y-1">
<div>{{ deficit_protein | default('120g Protein') }}</div>
<div>{{ deficit_carbs | default('155g Carbs') }}</div>
<div>{{ deficit_fat | default('69g Fat') }}</div>
<div>{{ deficit_fiber | default('25g Fibre') }}</div>
</div>
</div>
<!-- Thursday -->
<div class="bg-cyan-300 p-3 text-left rounded-lg">
<div class="font-bold text-black mb-2">Thursday</div>
<div class="text-lg font-bold text-black mb-2">{{ deficit_calories | default('1725KCals') }}</div>
<div class="text-xs text-black space-y-1">
<div>{{ deficit_protein | default('120g Protein') }}</div>
<div>{{ deficit_carbs | default('155g Carbs') }}</div>
<div>{{ deficit_fat | default('69g Fat') }}</div>
<div>{{ deficit_fiber | default('25g Fibre') }}</div>
</div>
</div>
<!-- Friday -->
<div class="bg-cyan-300 p-3 text-left rounded-lg">
<div class="font-bold text-black mb-2">Friday</div>
<div class="text-lg font-bold text-black mb-2">{{ deficit_calories | default('1725KCals') }}</div>
<div class="text-xs text-black space-y-1">
<div>{{ deficit_protein | default('120g Protein') }}</div>
<div>{{ deficit_carbs | default('155g Carbs') }}</div>
<div>{{ deficit_fat | default('69g Fat') }}</div>
<div>{{ deficit_fiber | default('25g Fibre') }}</div>
</div>
</div>
<!-- Saturday -->
<div class="p-3 text-left rounded-lg">
<div class="font-bold text-black mb-2">Saturday</div>
<div class="text-lg font-bold text-black mb-2">{{ deficit_calories | default('1725KCals') }}</div>
<div class="text-xs text-black space-y-1">
<div>{{ deficit_protein | default('120g Protein') }}</div>
<div>{{ deficit_carbs | default('155g Carbs') }}</div>
<div>{{ deficit_fat | default('69g Fat') }}</div>
<div>{{ deficit_fiber | default('25g Fibre') }}</div>
</div>
</div>
<!-- Sunday -->
<div class="p-3 text-left rounded-lg">
<div class="font-bold text-black mb-2">Sunday</div>
<div class="text-lg font-bold text-black mb-2">{{ deficit_calories | default('1725KCals') }}</div>
<div class="text-xs text-black space-y-1">
<div>{{ deficit_protein | default('120g Protein') }}</div>
<div>{{ deficit_carbs | default('155g Carbs') }}</div>
<div>{{ deficit_fat | default('69g Fat') }}</div>
<div>{{ deficit_fiber | default('25g Fibre') }}</div>
</div>
</div>
</div>
</div>
<!-- Caloric Deficit with Maintenance/Refeed Example Section -->
<div class="mb-12">
<h2 class="text-2xl font-bold text-black mb-6 text-center">Caloric Deficit with Maintenance/Refeed Example</h2>
<!-- Weekly Grid -->
<div class="grid grid-cols-7 gap-2 mb-8">
<!-- Monday -->
<div class="bg-cyan-300 p-3 text-left rounded-lg">
<div class="font-bold text-black mb-2">Monday</div>
<div class="text-lg font-bold text-black mb-2">{{ refeed_weekday_calories | default('1615KCals') }}</div>
<div class="text-xs text-black space-y-1">
<div>{{ refeed_weekday_protein | default('120g Protein') }}</div>
<div>{{ refeed_weekday_carbs | default('142g Carbs') }}</div>
<div>{{ refeed_weekday_fat | default('63g Fat') }}</div>
<div>{{ refeed_weekday_fiber | default('24g Fibre') }}</div>
</div>
</div>
<!-- Tuesday -->
<div class="bg-cyan-300 p-3 text-left rounded-lg">
<div class="font-bold text-black mb-2">Tuesday</div>
<div class="text-lg font-bold text-black mb-2">{{ refeed_weekday_calories | default('1615KCals') }}</div>
<div class="text-xs text-black space-y-1">
<div>{{ refeed_weekday_protein | default('120g Protein') }}</div>
<div>{{ refeed_weekday_carbs | default('142g Carbs') }}</div>
<div>{{ refeed_weekday_fat | default('63g Fat') }}</div>
<div>{{ refeed_weekday_fiber | default('24g Fibre') }}</div>
</div>
</div>
<!-- Wednesday -->
<div class="bg-cyan-300 p-3 text-left rounded-lg">
<div class="font-bold text-black mb-2">Wednesday</div>
<div class="text-lg font-bold text-black mb-2">{{ refeed_weekday_calories | default('1615KCals') }}</div>
<div class="text-xs text-black space-y-1">
<div>{{ refeed_weekday_protein | default('120g Protein') }}</div>
<div>{{ refeed_weekday_carbs | default('142g Carbs') }}</div>
<div>{{ refeed_weekday_fat | default('63g Fat') }}</div>
<div>{{ refeed_weekday_fiber | default('24g Fibre') }}</div>
</div>
</div>
<!-- Thursday -->
<div class="p-3 text-left rounded-lg">
<div class="font-bold text-black mb-2">Thursday</div>
<div class="text-lg font-bold text-black mb-2">{{ refeed_weekday_calories | default('1615KCals') }}</div>
<div class="text-xs text-black space-y-1">
<div>{{ refeed_weekday_protein | default('120g Protein') }}</div>
<div>{{ refeed_weekday_carbs | default('142g Carbs') }}</div>
<div>{{ refeed_weekday_fat | default('63g Fat') }}</div>
<div>{{ refeed_weekday_fiber | default('24g Fibre') }}</div>
</div>
</div>
<!-- Friday -->
<div class="p-3 text-left rounded-lg">
<div class="font-bold text-black mb-2">Friday</div>
<div class="text-lg font-bold text-black mb-2">{{ refeed_weekday_calories | default('1615KCals') }}</div>
<div class="text-xs text-black space-y-1">
<div>{{ refeed_weekday_protein | default('120g Protein') }}</div>
<div>{{ refeed_weekday_carbs | default('142g Carbs') }}</div>
<div>{{ refeed_weekday_fat | default('63g Fat') }}</div>
<div>{{ refeed_weekday_fiber | default('24g Fibre') }}</div>
</div>
</div>
<!-- Saturday -->
<div class="p-3 text-left rounded-lg">
<div class="font-bold text-black mb-2">Saturday</div>
<div class="text-lg font-bold text-black mb-2">{{ refeed_weekend_calories | default('2000KCals') }}</div>
<div class="text-xs text-black space-y-1">
<div>{{ refeed_weekend_protein | default('120g Protein') }}</div>
<div>{{ refeed_weekend_carbs | default('190g Carbs') }}</div>
<div>{{ refeed_weekend_fat | default('84g Fat') }}</div>
<div>{{ refeed_weekend_fiber | default('30g Fibre') }}</div>
</div>
</div>
<!-- Sunday -->
<div class="p-3 text-left rounded-lg">
<div class="font-bold text-black mb-2">Sunday</div>
<div class="text-lg font-bold text-black mb-2">{{ refeed_weekend_calories | default('2000KCals') }}</div>
<div class="text-xs text-black space-y-1">
<div>{{ refeed_weekend_protein | default('120g Protein') }}</div>
<div>{{ refeed_weekend_carbs | default('190g Carbs') }}</div>
<div>{{ refeed_weekend_fat | default('84g Fat') }}</div>
<div>{{ refeed_weekend_fiber | default('30g Fibre') }}</div>
</div>
</div>
</div>
</div>
<!-- Macronutrients Recommendations Section -->
<div class="mb-8">
<h2 class="text-2xl font-bold text-black mb-8 text-center">Macronutrients Recommendations</h2>
<!-- Macronutrient Boxes -->
<div class="flex justify-center space-x-12">
<!-- Protein -->
<div class="bg-gray-200 p-6 rounded-lg text-center">
<div class="text-3xl font-bold text-black mb-2">{{ protein_percentage | default('28%') }}</div>
<div class="text-lg font-semibold text-black">Protein</div>
</div>
<!-- Carbs -->
<div class="bg-gray-200 p-6 rounded-lg text-center">
<div class="text-3xl font-bold text-black mb-2">{{ carbs_percentage | default('36%') }}</div>
<div class="text-lg font-semibold text-black">Carbs</div>
</div>
<!-- Fats -->
<div class="bg-gray-200 p-6 rounded-lg text-center">
<div class="text-3xl font-bold text-black mb-2">{{ fats_percentage | default('36%') }}</div>
<div class="text-lg font-semibold text-black">Fats</div>
</div>
</div>
</div>
</div>
</div>
+56
View File
@@ -0,0 +1,56 @@
<div class="w-full page bg-white p-4">
<!-- Header -->
<h1 class="text-3xl font-bold mb-2">Lung Analysis</h1>
<!-- Spirometry Assessment Section -->
<div class="mb-2">
<h2 class="text-xl font-semibold mb-4">Spirometry Assessment</h2>
<p class="text-sm text-gray-700 mb-4">
Spirometry is a diagnostic device that assesses how well a person
breathes and how their lungs are functioning. Lung function is
crucial for oxygen delivery during physical activity. Comparing
results to expected/normal values can highlight potential
limitations that would require additional lung training to improve
overall physical activity.
</p>
<!-- Lung Volume Chart -->
<div class="flex justify-center">
<img
src="data:image/png;base64,{{ lung_analysis_chart }}"
alt="Lung Volume Analysis Chart"
class="w-full max-w-4xl h-auto object-contain"
/>
</div>
<!-- Indications Box -->
<div class="bg-gray-200 rounded-lg p-4 text-center mb-2">
<h3 class="font-semibold text-lg mb-2">Indications</h3>
<p class="text-gray-700">{{ indication }}</p>
</div>
</div>
<!-- Respiratory Section -->
<div class="mb-4">
<h2 class="text-xl font-semibold mb-4 text-center">Respiratory</h2>
<!-- Respiratory Chart -->
<div class="flex justify-center mb-4">
<img
src="data:image/png;base64,{{ respiratory_analysis_chart }}"
alt="Respiratory Analysis Chart"
class="w-full mb-4 object-contain max-w-4xl h-auto"
/>
</div>
<!-- Peak VT Info Box -->
<div class="bg-gray-200 rounded-lg p-4 text-center">
<h3 class="font-semibold mb-2">Peak VT</h3>
<p class="text-sm">
{{ peak_vt }} L/Breath which occurs at {{ peak_vt_bpm }} bpm
(Zone {{ peak_vt_zone }})
</p>
<p class="text-sm">{{ fev1_percentage }}% of FEV1</p>
</div>
</div>
</div>
+198
View File
@@ -0,0 +1,198 @@
<div class="w-full page bg-white">
<!-- Main Content -->
<div class="px-8 py-6">
<!-- Page Title -->
<h1 class="text-3xl font-bold text-black mb-6">Cardio Metrics</h1>
<!-- Active Metabolic Rate Assessment Section -->
<h2 class="text-xl font-bold text-black mb-4">Active Metabolic Rate Assessment</h2>
<p class="text-gray-700 text-sm mb-8">The active metabolic rate assessment is a key measure of aerobic fitness. It helps determine your specific heart rate zones and how well your body uses carbohydrates and fats as fuel while you exercise. It is also an indicator of overall health and wellbeing.</p>
<!-- VO2 Max Section -->
<div class="mb-8">
<h3 class="text-xl font-bold text-black mb-4 text-center">VO2 Max - {{ vo2_max_value | default('49.5') }} ({{ vo2_max_percentile | default('100th percentile') }})</h3>
<!-- VO2 Max Table -->
<div class="mb-8">
<table class="w-full border-collapse">
<thead>
<tr>
<th class="bg-cyan-300 border border-gray-400 p-3 text-black font-bold">Age (F)</th>
<th class="bg-cyan-300 border border-gray-400 p-3 text-black font-bold">Very Poor</th>
<th class="bg-cyan-300 border border-gray-400 p-3 text-black font-bold">Poor</th>
<th class="bg-cyan-300 border border-gray-400 p-3 text-black font-bold">Fair</th>
<th class="bg-cyan-300 border border-gray-400 p-3 text-black font-bold">Good</th>
<th class="bg-cyan-300 border border-gray-400 p-3 text-black font-bold">Excellent</th>
<th class="bg-cyan-300 border border-gray-400 p-3 text-black font-bold relative">
Superior
<!-- Arrow indicator -->
<div class="absolute -bottom-4 left-1/2 transform -translate-x-1/2">
<div class="w-0 h-0 border-l-4 border-r-4 border-t-8 border-transparent border-t-black"></div>
</div>
</th>
</tr>
</thead>
<tbody>
<tr>
<td class="bg-cyan-200 border border-gray-400 p-3 text-black font-semibold">{{ age_range | default('30-39') }}</td>
<td class="bg-gray-100 border border-gray-400 p-3 text-black text-center">{{ very_poor_range | default('19.0-24.1') }}</td>
<td class="bg-gray-100 border border-gray-400 p-3 text-black text-center">{{ poor_range | default('24.1-28.2') }}</td>
<td class="bg-gray-100 border border-gray-400 p-3 text-black text-center">{{ fair_range | default('28.2-32.2') }}</td>
<td class="bg-gray-100 border border-gray-400 p-3 text-black text-center">{{ good_range | default('32.2-35.7') }}</td>
<td class="bg-gray-100 border border-gray-400 p-3 text-black text-center">{{ excellent_range | default('35.7-45.8') }}</td>
<td class="bg-gray-100 border border-gray-400 p-3 text-black text-center font-bold">{{ superior_range | default('45.8+') }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Personalized Heart Rate Zones Section -->
<div class="mb-8">
<h3 class="text-xl font-bold text-black mb-6 text-center">Personalized Heart Rate Zones</h3>
<!-- Heart Rate Zones Table -->
<table class="w-full border-collapse text-sm">
<thead>
<tr>
<th class="bg-cyan-300 border border-gray-400 p-2 text-black font-bold">Zone 1</th>
<th class="bg-cyan-300 border border-gray-400 p-2 text-black font-bold">Zone 2</th>
<th class="bg-cyan-300 border border-gray-400 p-2 text-black font-bold">Zone 3</th>
<th class="bg-cyan-300 border border-gray-400 p-2 text-black font-bold">Zone 4</th>
<th class="bg-cyan-300 border border-gray-400 p-2 text-black font-bold">Zone 5</th>
</tr>
</thead>
<tbody>
<!-- Zone Descriptions -->
<tr>
<td class="border border-gray-400 p-3 text-center">
<div class="text-black font-semibold mb-1">Improves health and recovery capacity</div>
</td>
<td class="border border-gray-400 p-3 text-center">
<div class="text-black font-semibold mb-1">Improves endurance and fat burning</div>
</td>
<td class="border border-gray-400 p-3 text-center">
<div class="text-black font-semibold mb-1">Improves Aerobic fitness</div>
</td>
<td class="border border-gray-400 p-3 text-center">
<div class="text-black font-semibold mb-1">Improves maximum performance capacity</div>
</td>
<td class="border border-gray-400 p-3 text-center">
<div class="text-black font-semibold mb-1">Develops maximum performance and speed</div>
</td>
</tr>
<!-- Heart Rate Percentages -->
<tr>
<td class="border border-gray-400 p-2 text-center text-black font-semibold">{{ zone1_percentage | default('55-65% of Max Heart Rate') }}</td>
<td class="border border-gray-400 p-2 text-center text-black font-semibold">{{ zone2_percentage | default('65-75% of Max Heart Rate') }}</td>
<td class="border border-gray-400 p-2 text-center text-black font-semibold">{{ zone3_percentage | default('80-85% of Max Heart Rate') }}</td>
<td class="border border-gray-400 p-2 text-center text-black font-semibold">{{ zone4_percentage | default('85-88% of Max Heart Rate') }}</td>
<td class="border border-gray-400 p-2 text-center text-black font-semibold">{{ zone5_percentage | default('90% of Max Heart Rate') }}</td>
</tr>
<!-- Heart Rate BPM -->
<tr>
<td class="bg-red-200 border border-gray-400 p-2 text-center text-black font-bold">{{ zone1_bpm | default('81-96bpm') }}</td>
<td class="bg-red-200 border border-gray-400 p-2 text-center text-black font-bold">{{ zone2_bpm | default('96-100bpm') }}</td>
<td class="bg-yellow-200 border border-gray-400 p-2 text-center text-black font-bold">{{ zone3_bpm | default('100-178bpm') }}</td>
<td class="bg-green-200 border border-gray-400 p-2 text-center text-black font-bold">{{ zone4_bpm | default('178-188bpm') }}</td>
<td class="bg-green-200 border border-gray-400 p-2 text-center text-black font-bold">{{ zone5_bpm | default('188-198bpm') }}</td>
</tr>
<!-- Speed -->
<tr>
<td class="border border-gray-400 p-2 text-center">
<div class="text-black font-semibold">{{ zone1_speed | default('3.5mph') }}</div>
<div class="text-black text-xs">{{ zone1_incline | default('2% Incline') }}</div>
</td>
<td class="border border-gray-400 p-2 text-center">
<div class="text-black font-semibold">{{ zone2_speed | default('3.5-4.0mph') }}</div>
<div class="text-black text-xs">{{ zone2_incline | default('2% Incline') }}</div>
</td>
<td class="border border-gray-400 p-2 text-center">
<div class="text-black font-semibold">{{ zone3_speed | default('4.0-6.5mph') }}</div>
<div class="text-black text-xs">{{ zone3_incline | default('2% Incline') }}</div>
</td>
<td class="border border-gray-400 p-2 text-center">
<div class="text-black font-semibold">{{ zone4_speed | default('6.5-7.0mph') }}</div>
<div class="text-black text-xs">{{ zone4_incline | default('2% Incline') }}</div>
</td>
<td class="border border-gray-400 p-2 text-center">
<div class="text-black font-semibold">{{ zone5_speed | default('7.0-8.0mph') }}</div>
<div class="text-black text-xs">{{ zone5_incline | default('2% Incline') }}</div>
</td>
</tr>
<!-- Pace -->
<tr>
<td class="border border-gray-400 p-2 text-center text-black">{{ zone1_pace | default('10:39min/km Pace') }}</td>
<td class="border border-gray-400 p-2 text-center text-black">{{ zone2_pace | default('10:39-9:19min/km Pace') }}</td>
<td class="border border-gray-400 p-2 text-center text-black">{{ zone3_pace | default('9:19-5:44min/km Pace') }}</td>
<td class="border border-gray-400 p-2 text-center text-black">{{ zone4_pace | default('5:44-5:20min/km Pace') }}</td>
<td class="border border-gray-400 p-2 text-center text-black">{{ zone5_pace | default('5:20-4:40min/km Pace') }}</td>
</tr>
<!-- Average Calories -->
<tr>
<td class="border border-gray-400 p-2 text-center">
<div class="text-black text-xs">Avg:</div>
<div class="text-black font-semibold">{{ zone1_calories | default('4.4kcals/minute') }}</div>
</td>
<td class="border border-gray-400 p-2 text-center">
<div class="text-black text-xs">Avg:</div>
<div class="text-black font-semibold">{{ zone2_calories | default('5.9kcals/minute') }}</div>
</td>
<td class="border border-gray-400 p-2 text-center">
<div class="text-black text-xs">Avg:</div>
<div class="text-black font-semibold">{{ zone3_calories | default('9.4kcals/minute') }}</div>
</td>
<td class="border border-gray-400 p-2 text-center">
<div class="text-black text-xs">Avg:</div>
<div class="text-black font-semibold">{{ zone4_calories | default('12.5kcals/minute') }}</div>
</td>
<td class="border border-gray-400 p-2 text-center">
<div class="text-black text-xs">Avg:</div>
<div class="text-black font-semibold">{{ zone5_calories | default('12.8kcals/minute') }}</div>
</td>
</tr>
<!-- Carb Utilization -->
<tr>
<td class="border border-gray-400 p-2 text-center text-black">{{ zone1_carb | default('Avg: 0.4g/min Carb Utilization') }}</td>
<td class="border border-gray-400 p-2 text-center text-black">{{ zone2_carb | default('Avg: 0.6g/min Carb Utilization') }}</td>
<td class="border border-gray-400 p-2 text-center text-black">{{ zone3_carb | default('Avg: 1.9g/min Carb Utilization') }}</td>
<td class="border border-gray-400 p-2 text-center text-black">{{ zone4_carb | default('Avg: 2.9g/min Carb Utilization') }}</td>
<td class="border border-gray-400 p-2 text-center text-black">{{ zone5_carb | default('Avg: 3.1g/min Carb Utilization') }}</td>
</tr>
<!-- Breathing -->
<tr>
<td class="bg-red-200 border border-gray-400 p-2 text-center">
<div class="text-black font-bold">{{ zone1_breaths | default('Avg: 27 breaths') }}</div>
<div class="text-black text-xs italic">{{ zone1_breath_range | default('Ideal Range: 15-20 breaths') }}</div>
</td>
<td class="bg-red-200 border border-gray-400 p-2 text-center">
<div class="text-black font-bold">{{ zone2_breaths | default('Avg: 28 breaths') }}</div>
<div class="text-black text-xs italic">{{ zone2_breath_range | default('Ideal Range: 20-25 breaths') }}</div>
</td>
<td class="bg-yellow-200 border border-gray-400 p-2 text-center">
<div class="text-black font-bold">{{ zone3_breaths | default('Avg: 31 breaths') }}</div>
<div class="text-black text-xs italic">{{ zone3_breath_range | default('Ideal Range: 25-30 breaths') }}</div>
</td>
<td class="bg-green-200 border border-gray-400 p-2 text-center">
<div class="text-black font-bold">{{ zone4_breaths | default('Avg: 42 breaths') }}</div>
<div class="text-black text-xs italic">{{ zone4_breath_range | default('Ideal Range: 30-35 breaths') }}</div>
</td>
<td class="bg-green-200 border border-gray-400 p-2 text-center">
<div class="text-black font-bold">{{ zone5_breaths | default('Avg: 51 breaths') }}</div>
<div class="text-black text-xs italic">{{ zone5_breath_range | default('Ideal Range: 40+ breaths') }}</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
+21
View File
@@ -0,0 +1,21 @@
<div class="w-full page bg-white">
<!-- Main Content -->
<div class="flex flex-col items-center justify-center h-full">
<!-- Fuel Utilization Chart -->
<div class="w-full max-w-5xl">
<img
src="data:image/png;base64,{{ fuel_utilization_chart }}"
alt="Fuel Utilization Report - Institute of Science, Health and Performance"
class="w-full h-auto object-contain chart-large"
/>
</div>
<!-- Chart Information -->
<div class="mt-8 text-center">
<p class="text-gray-700 text-sm">
Client: {{ client_name | default('Keirstyn Moran') }} |
Assessment Date: {{ assessment_date | default('July 29 2025') }}
</p>
</div>
</div>
</div>
Binary file not shown.
+67
View File
@@ -0,0 +1,67 @@
asttokens==3.0.0
brotli==1.1.0
cffi==2.0.0
chardet==5.2.0
charset-normalizer==3.4.3
click==8.3.0
comm==0.2.3
contourpy==1.3.3
cryptography==46.0.1
cssselect2==0.8.0
cycler==0.12.1
debugpy==1.8.17
decorator==5.2.1
et-xmlfile==2.0.0
executing==2.2.1
fonttools==4.60.0
ipykernel==6.30.1
ipython==9.5.0
ipython-pygments-lexers==1.1.1
jedi==0.19.2
jinja2==3.1.6
jupyter-client==8.6.3
jupyter-core==5.8.1
kiwisolver==1.4.9
markupsafe==3.0.2
matplotlib==3.10.6
matplotlib-inline==0.1.7
nest-asyncio==1.6.0
numpy==2.3.3
opencv-python-headless==4.11.0.86
openpyxl==3.1.5
packaging==25.0
pandas==2.3.2
pango==0.0.1
parso==0.8.5
pdfkit==1.0.0
pdfminer-six==20250506
pexpect==4.9.0
pillow==11.3.0
platformdirs==4.4.0
prompt-toolkit==3.0.52
psutil==7.1.0
ptyprocess==0.7.0
pure-eval==0.2.3
pycparser==2.23
pydyf==0.11.0
pygments==2.19.2
pymupdf==1.26.4
pyparsing==3.2.5
pypdf==5.9.0
pypdfium2==4.30.0
pyphen==0.17.2
python-dateutil==2.9.0.post0
pytz==2025.2
pyzmq==27.1.0
seaborn==0.13.2
six==1.17.0
stack-data==0.6.3
tabulate==0.9.0
tinycss2==1.4.0
tinyhtml5==2.0.0
tornado==6.5.2
traitlets==5.14.3
tzdata==2025.2
wcwidth==0.2.14
webencodings==0.5.1
zopfli==0.2.3.post1