Compare commits

...

18 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
38 changed files with 3621 additions and 502 deletions
+3 -1
View File
@@ -1,3 +1,5 @@
.venv .venv
data/ data/
.env
Binary file not shown.
+89 -97
View File
@@ -5,99 +5,90 @@
"execution_count": 6, "execution_count": 6,
"id": "b18c1027", "id": "b18c1027",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [],
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'id': 'gen-1758708788-9UUhU8KfktBmyteT4BUC', 'provider': 'Google', 'model': 'google/gemini-2.5-flash-lite', 'object': 'chat.completion', 'created': 1758708788, 'choices': [{'logprobs': None, 'finish_reason': 'stop', 'native_finish_reason': 'STOP', 'index': 0, 'message': {'role': 'assistant', 'content': 'Parameters,Best,LLN,Pred.,%Pred.,ZScore,PRE#1,PRE#2,PRE#3\\nFVC,4.24,3.03,3.79,112.0,0.95,4.24,4.17,4.15\\nFEV1,3.26,2.53,3.16,103.3,0.28,3.26,3.21,3.14\\nFEV1/FVC%,76.89,72.47,83.78,91.8,-1.05,76.9,77.0,75.7\\nPEF,684,222,384,178.7,-,444,438,684\\nFEF2575,2.74,2.15,3.42,80.2,-0.84,2.74,2.68,2.48\\nFEF25,6.08,,,0.0,-,6.08,6.0,5.53\\nFEF50,3.06,,,0.0,-,3.06,3.1,2.77\\nFEF75,1.06,0.71,1.41,75.1,-0.72,1.06,1.12,0.94\\nPEFTime,79,,,49,-,79,40,39\\nEVol,78.0,,,77.0,-,78.0,77.0,197.0\\nFEV6,4.22,3.03,3.79,111.4,-,4.22,4.17,4.13', 'refusal': None, 'reasoning': None}}], 'usage': {'prompt_tokens': 1348, 'completion_tokens': 434, 'total_tokens': 1782, 'prompt_tokens_details': {'cached_tokens': 0}, 'completion_tokens_details': {'reasoning_tokens': 0, 'image_tokens': 0}}}\n",
"Content saved to extracted_table.csv\n"
]
}
],
"source": [ "source": [
"\n", "\n",
"import requests\n", "# import requests\n",
"import json\n", "# import json\n",
"import base64\n", "# import base64\n",
"from pathlib import Path\n", "# from pathlib import Path\n",
"\n", "\n",
"API_KEY_REF = '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'\n", "# API_KEY_REF = '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'\n",
"def encode_pdf_to_base64(pdf_path):\n", "# def encode_pdf_to_base64(pdf_path):\n",
" with open(pdf_path, \"rb\") as pdf_file:\n", "# with open(pdf_path, \"rb\") as pdf_file:\n",
" return base64.b64encode(pdf_file.read()).decode('utf-8')\n", "# return base64.b64encode(pdf_file.read()).decode('utf-8')\n",
"\n", "\n",
"url = \"https://openrouter.ai/api/v1/chat/completions\"\n", "# url = \"https://openrouter.ai/api/v1/chat/completions\"\n",
"headers = {\n", "# headers = {\n",
" \"Authorization\": f\"Bearer {API_KEY_REF}\",\n", "# \"Authorization\": f\"Bearer {API_KEY_REF}\",\n",
" \"Content-Type\": \"application/json\"\n", "# \"Content-Type\": \"application/json\"\n",
"}\n", "# }\n",
"\n", "\n",
"# Read and encode the PDF\n", "# # Read and encode the PDF\n",
"pdf_path = \"data/~Moran~K~19910201~Spirometry Exam~20250729~20250729032843.pdf\"\n", "# pdf_path = \"data/~Moran~K~19910201~Spirometry Exam~20250729~20250729032843.pdf\"\n",
"base64_pdf = encode_pdf_to_base64(pdf_path)\n", "# base64_pdf = encode_pdf_to_base64(pdf_path)\n",
"data_url = f\"data:application/pdf;base64,{base64_pdf}\"\n", "# data_url = f\"data:application/pdf;base64,{base64_pdf}\"\n",
"\n", "\n",
"messages = [\n", "# messages = [\n",
" {\n", "# {\n",
" \"role\": \"user\",\n", "# \"role\": \"user\",\n",
" \"content\": [\n", "# \"content\": [\n",
" {\n", "# {\n",
" \"type\": \"text\",\n", "# \"type\": \"text\",\n",
" \"text\": \"Please extract the table from the pdf and return the values in csv format, \"\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", "# \"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", "# \"The '-' Should be treated as empty values.\"\n",
" \"do not add 'csv' at the start or end of the response\"\n", "# \"do not add 'csv' at the start or end of the response\"\n",
" },\n", "# },\n",
" {\n", "# {\n",
" \"type\": \"file\",\n", "# \"type\": \"file\",\n",
" \"file\": {\n", "# \"file\": {\n",
" \"filename\": \"document.pdf\",\n", "# \"filename\": \"document.pdf\",\n",
" \"file_data\": data_url\n", "# \"file_data\": data_url\n",
" }\n", "# }\n",
" },\n", "# },\n",
" ]\n", "# ]\n",
" }\n", "# }\n",
"]\n", "# ]\n",
"\n", "\n",
"# Optional: Configure PDF processing engine\n", "# # Optional: Configure PDF processing engine\n",
"# PDF parsing will still work even if the plugin is not explicitly set\n", "# # PDF parsing will still work even if the plugin is not explicitly set\n",
"plugins = [\n", "# plugins = [\n",
" {\n", "# {\n",
" \"id\": \"file-parser\",\n", "# \"id\": \"file-parser\",\n",
" \"pdf\": {\n", "# \"pdf\": {\n",
" \"engine\": \"pdf-text\" # defaults to \"mistral-ocr\". See Pricing above\n", "# \"engine\": \"pdf-text\" # defaults to \"mistral-ocr\". See Pricing above\n",
" }\n", "# }\n",
" }\n", "# }\n",
"]\n", "# ]\n",
"\n", "\n",
"payload = {\n", "# payload = {\n",
" \"model\": \"google/gemini-2.5-flash-lite\",\n", "# \"model\": \"google/gemini-2.5-flash-lite\",\n",
" \"messages\": messages,\n", "# \"messages\": messages,\n",
"}\n", "# }\n",
"\n", "\n",
"response = requests.post(url, headers=headers, json=payload)\n", "# response = requests.post(url, headers=headers, json=payload)\n",
"# Get the response content\n", "# # Get the response content\n",
"response_data = response.json()\n", "# response_data = response.json()\n",
"print(response_data)\n", "# print(response_data)\n",
"\n", "\n",
"# Extract the content from the response\n", "# # Extract the content from the response\n",
"if 'choices' in response_data and len(response_data['choices']) > 0:\n", "# if 'choices' in response_data and len(response_data['choices']) > 0:\n",
" content = response_data['choices'][0]['message']['content']\n", "# content = response_data['choices'][0]['message']['content']\n",
" \n", " \n",
" # Save to a CSV file\n", "# # Save to a CSV file\n",
" output_file = \"extracted_table.csv\"\n", "# output_file = \"extracted_table.csv\"\n",
" with open(output_file, 'w', encoding='utf-8') as f:\n", "# with open(output_file, 'w', encoding='utf-8') as f:\n",
" f.write(content)\n", "# f.write(content)\n",
" \n", " \n",
" print(f\"Content saved to {output_file}\")\n", "# print(f\"Content saved to {output_file}\")\n",
"else:\n", "# else:\n",
" print(\"No content found in response\")" "# print(\"No content found in response\")"
] ]
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 12, "execution_count": 7,
"id": "56a9d655", "id": "56a9d655",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
@@ -107,13 +98,13 @@
"text": [ "text": [
"FVC Best: 4.24, FVC Pred: 112.0\n", "FVC Best: 4.24, FVC Pred: 112.0\n",
"FEV1 Best: 3.26, FEV1 Pred: 103.3\n", "FEV1 Best: 3.26, FEV1 Pred: 103.3\n",
"FEV1/FVC% Best: 76.89, FEV1/FVC% Pred: 91.8\n" "FEV1/FVC% Best: 76.9, FEV1/FVC% Pred: 91.8\n"
] ]
} }
], ],
"source": [ "source": [
"import pandas as pd\n", "import pandas as pd\n",
"spirometry_df = pd.read_csv(\"extracted_table.csv\")\n", "spirometry_df = pd.read_csv(\"data/spirometry_data.csv\")\n",
"\n", "\n",
"fvc_best = spirometry_df.loc[spirometry_df['Parameters'] == 'FVC', 'Best'].values[0]\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", "fvc_pred = spirometry_df.loc[spirometry_df['Parameters'] == 'FVC', '%Pred.'].values[0]\n",
@@ -131,7 +122,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 16, "execution_count": 8,
"id": "990f4b4f", "id": "990f4b4f",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
@@ -155,7 +146,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 18, "execution_count": 9,
"id": "041cbc3d", "id": "041cbc3d",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
@@ -171,7 +162,7 @@
"name": "stderr", "name": "stderr",
"output_type": "stream", "output_type": "stream",
"text": [ "text": [
"/tmp/ipykernel_301535/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", "/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" " df = df.apply(pd.to_numeric, errors='ignore')\n"
] ]
} }
@@ -204,7 +195,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 20, "execution_count": 10,
"id": "de7cadd1", "id": "de7cadd1",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
@@ -223,7 +214,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 24, "execution_count": 11,
"id": "cb972ed3", "id": "cb972ed3",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
@@ -320,7 +311,7 @@
"[1 rows x 147 columns]" "[1 rows x 147 columns]"
] ]
}, },
"execution_count": 24, "execution_count": 11,
"metadata": {}, "metadata": {},
"output_type": "execute_result" "output_type": "execute_result"
} }
@@ -334,7 +325,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 26, "execution_count": 12,
"id": "98d9295a", "id": "98d9295a",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
@@ -354,7 +345,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 32, "execution_count": 13,
"id": "cdfeb309", "id": "cdfeb309",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
@@ -418,7 +409,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 33, "execution_count": 14,
"id": "4420cfea", "id": "4420cfea",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
@@ -476,7 +467,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 37, "execution_count": 21,
"id": "62803668", "id": "62803668",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
@@ -561,7 +552,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 40, "execution_count": 16,
"id": "07593b56", "id": "07593b56",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
@@ -572,8 +563,8 @@
"Zone 1 (Active Recovery): 81.7 - 96.7 bpm\n", "Zone 1 (Active Recovery): 81.7 - 96.7 bpm\n",
"Zone 2 (Aerobic Base): 96.7 - 100.5 bpm\n", "Zone 2 (Aerobic Base): 96.7 - 100.5 bpm\n",
"Zone 3 (Aerobic): 100.5 - 179.7 bpm\n", "Zone 3 (Aerobic): 100.5 - 179.7 bpm\n",
"Zone 4 (Lactate Threshold): 179.7 - 199.7 bpm\n", "Zone 4 (Lactate Threshold): 179.7 - 189.7 bpm\n",
"Zone 5 (VO2 Max): 199.7+ bpm\n" "Zone 5 (VO2 Max): 189.7 - 199.7 bpm\n"
] ]
} }
], ],
@@ -582,7 +573,8 @@
"zone_2_start = optimal_row['HR(bpm)_smoothed']\n", "zone_2_start = optimal_row['HR(bpm)_smoothed']\n",
"zone_3_start = vt1\n", "zone_3_start = vt1\n",
"zone_4_start = vt2['HeartRate'] - 10\n", "zone_4_start = vt2['HeartRate'] - 10\n",
"zone_5_start = vt2['HeartRate'] + 10\n", "zone_5_start = vt2['HeartRate']\n",
"zone_5_end = vt2['HeartRate'] + 10\n",
"\n", "\n",
"zone_1_end = zone_2_start\n", "zone_1_end = zone_2_start\n",
"zone_2_end = vt1['HeartRate']\n", "zone_2_end = vt1['HeartRate']\n",
@@ -593,12 +585,12 @@
"print(f\"Zone 2 (Aerobic Base): {zone_2_start:.1f} - {zone_2_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 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 4 (Lactate Threshold): {zone_4_start:.1f} - {zone_4_end:.1f} bpm\")\n",
"print(f\"Zone 5 (VO2 Max): {zone_5_start:.1f}+ bpm\")" "print(f\"Zone 5 (VO2 Max): {zone_5_start:.1f} - {zone_5_end:.1f} bpm\")"
] ]
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 60, "execution_count": 17,
"id": "c90415b2", "id": "c90415b2",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
@@ -661,7 +653,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 66, "execution_count": 18,
"id": "c3b2cc59", "id": "c3b2cc59",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
@@ -750,7 +742,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": 19,
"id": "672d68f3", "id": "672d68f3",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
+25 -14
View File
@@ -12,7 +12,7 @@ def image_to_base64(image_path):
### Defining Page Contexts ### ### Defining Page Contexts ###
page_1_context = { page_1_context = {
"name": "John Doe", "name": "Keirstyn",
"surname": "Moran", "surname": "Moran",
"date": "July 29, 2025", "date": "July 29, 2025",
} }
@@ -27,22 +27,28 @@ page_3_context = {
page_4_context = { page_4_context = {
"body_composition_chart": image_to_base64( "body_composition_chart": image_to_base64(
"/home/oluwasanmi/Documents/Work/MKD/report_generation/graphs/page_1_body_composition.png" "/home/oluwasanmi/Documents/Work/MKD/report_generation/graphs/body_composition_chart.png"
), ),
"body_fat_chart": image_to_base64( "body_fat_chart": image_to_base64(
"/home/oluwasanmi/Documents/Work/MKD/report_generation/graphs/page_1_body_fat.png" "/home/oluwasanmi/Documents/Work/MKD/report_generation/graphs/body_fat_percent_chart.png"
), ),
"fat_percentage": "22.4",
} }
page_5_context = { page_5_context = {
"metabolism_chart": "", "metabolism_chart": image_to_base64(
"fuel_source_chart": "", "/home/oluwasanmi/Documents/Work/MKD/report_generation/graphs/metabolism_chart.png"
"resting_calories": 1540, ),
"neat_calories": 310, "fuel_source_chart": image_to_base64(
"weight_loss_calories": 1725, "/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", "weight_loss_rate": "1lb/week",
"total_calories": 3575, "total_calories": "null",
} }
page_6_context = { page_6_context = {
@@ -78,7 +84,7 @@ page_7_context = {
"peak_vt_bpm": 198, "peak_vt_bpm": 198,
"peak_vt_zone": 3, "peak_vt_zone": 3,
"fev1_percentage": 85, "fev1_percentage": 85,
"lung_analysis_chart": "", "lung_analysis_chart": image_to_base64("/home/oluwasanmi/Documents/Work/MKD/report_generation/graphs/spirometry_chart.png"),
"respiratory_analysis_chart": image_to_base64( "respiratory_analysis_chart": image_to_base64(
"/home/oluwasanmi/Documents/Work/MKD/report_generation/graphs/respiratory.png" "/home/oluwasanmi/Documents/Work/MKD/report_generation/graphs/respiratory.png"
), ),
@@ -102,8 +108,8 @@ page_8_context = {
"zone1_bpm": "81-96bpm", "zone1_bpm": "81-96bpm",
"zone2_bpm": "96-100bpm", "zone2_bpm": "96-100bpm",
"zone3_bpm": "100-178bpm", "zone3_bpm": "100-178bpm",
"zone4_bpm": "178-188bpm", "zone4_bpm": "178-189bpm",
"zone5_bpm": "188-198bpm", "zone5_bpm": "189-199bpm",
"zone1_speed": "3.5mph", "zone1_speed": "3.5mph",
"zone2_speed": "3.5-4.0mph", "zone2_speed": "3.5-4.0mph",
"zone3_speed": "4.0-6.5mph", "zone3_speed": "4.0-6.5mph",
@@ -192,7 +198,12 @@ page_11_context = {
} }
page_12_context = { 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 = { page_13_context = {
@@ -308,7 +319,7 @@ page_17_context = {
page_18_context = { page_18_context = {
"body_fat_percentage_chart": image_to_base64( "body_fat_percentage_chart": image_to_base64(
"/home/oluwasanmi/Documents/Work/MKD/report_generation/graphs/body_fat_percentage_chart.png" "/home/oluwasanmi/Documents/Work/MKD/report_generation/fat_percentage_master_chart.png"
), ),
} }
+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

Before

Width:  |  Height:  |  Size: 91 KiB

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.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

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.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 425 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

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.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 322 KiB

+24 -10
View File
@@ -6,7 +6,12 @@ from context import context_list
env = Environment(loader=FileSystemLoader("report_gen")) env = Environment(loader=FileSystemLoader("report_gen"))
html_pages = [] html_pages = []
a = 1
b = "string"
c = [1,2,3,5]
d = {
"key": "value"
}
header_context = { header_context = {
"patient_name": "Keirstyn Moran", "patient_name": "Keirstyn Moran",
"age": 34, "age": 34,
@@ -15,16 +20,21 @@ header_context = {
"focus": "Endurance", "focus": "Endurance",
} }
footer_context = [{ footer_context = [
"contact_email": "info@ishplabs.com ", {
"website": "www.ishplabs.com", "contact_email": "info@ishplabs.com ",
"social": "@ishplabs", "website": "www.ishplabs.com",
"page_number": i + 1, "social": "@ishplabs",
} for i in range(len(context_list))] "page_number": i + 1,
}
for i in range(len(context_list))
]
header_html = env.get_template("header.html").render(header_context) 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] footer_html_list = [
env.get_template("footer.html").render(context) for context in footer_context
]
for i, context in enumerate(context_list): for i, context in enumerate(context_list):
template = env.get_template(f"page_{i + 1}.html").render(context) template = env.get_template(f"page_{i + 1}.html").render(context)
@@ -81,8 +91,11 @@ html_doc = f"""
}} }}
/* Prevent images from being too large */ /* Prevent images from being too large */
img {{ img {{
max-height: 200px; max-height: 300px;
object-fit: contain; }}
/* Larger images for specific charts */
.chart-large {{
max-height: 500px !important;
}} }}
</style> </style>
</head> </head>
@@ -114,3 +127,4 @@ html_string_to_pdf(html_doc, "multi_page_report.pdf")
# pdfkit.from_string(html_doc, "truth_report.pdf", options=options) # pdfkit.from_string(html_doc, "truth_report.pdf", options=options)
print("✅ PDF generated: multi_page_report.pdf") print("✅ PDF generated: multi_page_report.pdf")
Binary file not shown.
+1094 -90
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -15,7 +15,7 @@
<div class="flex gap-8"> <div class="flex gap-8">
<!-- Chart Image --> <!-- Chart Image -->
<div class="flex-1"> <div class="flex-1">
<img src="right-leg-chart.png" alt="Right Leg SMO2 Chart" class="w-full h-auto"> <img src= "data:image/png;base64,{{ right_leg }}" alt="Right Leg SMO2 Chart" class="w-full h-auto">
</div> </div>
<!-- Right Side Info --> <!-- Right Side Info -->
@@ -48,7 +48,7 @@
<div class="flex gap-8"> <div class="flex gap-8">
<!-- Chart Image --> <!-- Chart Image -->
<div class="flex-1"> <div class="flex-1">
<img src="left-leg-chart.png" alt="Left Leg SMO2 Chart" class="w-full h-auto"> <img src= "data:image/png;base64,{{ left_leg }}" alt="Left Leg SMO2 Chart" class="w-full h-auto">
</div> </div>
<!-- Right Side Info --> <!-- Right Side Info -->
+4 -2
View File
@@ -25,11 +25,13 @@
</div> </div>
<div class="w-full max-w-5xl"> <div class="w-full max-w-5xl">
<h1 class="text-2xl font-bold mb-4 text-center">Body Fat Percent Master Chart</h1> <h1 class="text-2xl font-bold mb-4 text-center">
Body Fat Percent Master Chart
</h1>
<img <img
src="data:image/png;base64,{{ body_fat_percentage_chart }}" src="data:image/png;base64,{{ body_fat_percentage_chart }}"
alt="Body Fat Percentage" alt="Body Fat Percentage"
class="w-full h-auto object-contain" class="w-full h-auto object-contain chart-large"
/> />
</div> </div>
</div> </div>
+196 -196
View File
@@ -18,42 +18,42 @@
<thead> <thead>
<tr class="bg-cyan-200"> <tr class="bg-cyan-200">
<th <th
class="border border-gray-300 p-1 font-bold" class="border border-gray-300 p-1 font-bold text-center"
> >
Age (M) Age (M)
</th> </th>
<th <th
class="border border-gray-300 p-1 font-bold" class="border border-gray-300 p-1 font-bold text-center"
> >
Poor Poor
</th> </th>
<th <th
class="border border-gray-300 p-1 font-bold" class="border border-gray-300 p-1 font-bold text-center"
> >
Below Average Below Average
</th> </th>
<th <th
class="border border-gray-300 p-1 font-bold" class="border border-gray-300 p-1 font-bold text-center"
> >
Average Average
</th> </th>
<th <th
class="border border-gray-300 p-1 font-bold" class="border border-gray-300 p-1 font-bold text-center"
> >
Above Average Above Average
</th> </th>
<th <th
class="border border-gray-300 p-1 font-bold" class="border border-gray-300 p-1 font-bold text-center"
> >
Good Good
</th> </th>
<th <th
class="border border-gray-300 p-1 font-bold" class="border border-gray-300 p-1 font-bold text-center"
> >
Excellent Excellent
</th> </th>
<th <th
class="border border-gray-300 p-1 font-bold" class="border border-gray-300 p-1 font-bold text-center"
> >
Athlete Athlete
</th> </th>
@@ -62,169 +62,169 @@
<tbody> <tbody>
<tr> <tr>
<td <td
class="border border-gray-300 p-1 font-medium" class="border border-gray-300 p-1 font-medium text-center"
> >
18-25 18-25
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
85bpm + 85bpm +
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
76-84bpm 76-84bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
74-78bpm 74-78bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
70-73bpm 70-73bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
66-69bpm 66-69bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
61-65bpm 61-65bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
60-60bpm 60-60bpm
</td> </td>
</tr> </tr>
<tr> <tr>
<td <td
class="border border-gray-300 p-1 font-medium" class="border border-gray-300 p-1 font-medium text-center"
> >
26-35 26-35
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
83bpm + 83bpm +
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
77-82bpm 77-82bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
73-76bpm 73-76bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
69-72bpm 69-72bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
65-68bpm 65-68bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
60-64bpm 60-64bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
55-59bpm 55-59bpm
</td> </td>
</tr> </tr>
<tr> <tr>
<td <td
class="border border-gray-300 p-1 font-medium" class="border border-gray-300 p-1 font-medium text-center"
> >
36-45 36-45
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
85bpm + 85bpm +
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
79-84bpm 79-84bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
74-78bpm 74-78bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
70-73bpm 70-73bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
65-69bpm 65-69bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
60-64bpm 60-64bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
55-59bpm 55-59bpm
</td> </td>
</tr> </tr>
<tr> <tr>
<td <td
class="border border-gray-300 p-1 font-medium" class="border border-gray-300 p-1 font-medium text-center"
> >
46-55 46-55
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
84bpm + 84bpm +
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
76-83bpm 76-83bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
73-77bpm 73-77bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
70-72bpm 70-72bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
66-69bpm 66-69bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
61-65bpm 61-65bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
56-60bpm 56-60bpm
</td> </td>
</tr> </tr>
<tr> <tr>
<td <td
class="border border-gray-300 p-1 font-medium" class="border border-gray-300 p-1 font-medium text-center"
> >
56-65 56-65
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
85bpm + 85bpm +
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
78-84bpm 78-84bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
74-77bpm 74-77bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
70-73bpm 70-73bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
65-69bpm 65-69bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
60-64bpm 60-64bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
50-59bpm 50-59bpm
</td> </td>
</tr> </tr>
<tr> <tr>
<td <td
class="border border-gray-300 p-1 font-medium" class="border border-gray-300 p-1 font-medium text-center"
> >
65+ 65+
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
84bpm + 84bpm +
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
77-83bpm 77-83bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
73-76bpm 73-76bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
70-73bpm 70-73bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
65-69bpm 65-69bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
60-64bpm 60-64bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
55-59bpm 55-59bpm
</td> </td>
</tr> </tr>
@@ -240,42 +240,42 @@
<thead> <thead>
<tr class="bg-cyan-200"> <tr class="bg-cyan-200">
<th <th
class="border border-gray-300 p-1 font-bold" class="border border-gray-300 p-1 font-bold text-center"
> >
Age (F) Age (F)
</th> </th>
<th <th
class="border border-gray-300 p-1 font-bold" class="border border-gray-300 p-1 font-bold text-center"
> >
Poor Poor
</th> </th>
<th <th
class="border border-gray-300 p-1 font-bold" class="border border-gray-300 p-1 font-bold text-center"
> >
Below Average Below Average
</th> </th>
<th <th
class="border border-gray-300 p-1 font-bold" class="border border-gray-300 p-1 font-bold text-center"
> >
Average Average
</th> </th>
<th <th
class="border border-gray-300 p-1 font-bold" class="border border-gray-300 p-1 font-bold text-center"
> >
Above Average Above Average
</th> </th>
<th <th
class="border border-gray-300 p-1 font-bold" class="border border-gray-300 p-1 font-bold text-center"
> >
Good Good
</th> </th>
<th <th
class="border border-gray-300 p-1 font-bold" class="border border-gray-300 p-1 font-bold text-center"
> >
Excellent Excellent
</th> </th>
<th <th
class="border border-gray-300 p-1 font-bold" class="border border-gray-300 p-1 font-bold text-center"
> >
Athlete Athlete
</th> </th>
@@ -284,169 +284,169 @@
<tbody> <tbody>
<tr> <tr>
<td <td
class="border border-gray-300 p-1 font-medium" class="border border-gray-300 p-1 font-medium text-center"
> >
18-25 18-25
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
81bpm + 81bpm +
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
74-81bpm 74-81bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
73-78bpm 73-78bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
66-69bpm 66-69bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
62-65bpm 62-65bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
56-61bpm 56-61bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
50-55bpm 50-55bpm
</td> </td>
</tr> </tr>
<tr> <tr>
<td <td
class="border border-gray-300 p-1 font-medium" class="border border-gray-300 p-1 font-medium text-center"
> >
26-35 26-35
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
82bpm + 82bpm +
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
75-81bpm 75-81bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
71-74bpm 71-74bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
66-70bpm 66-70bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
62-65bpm 62-65bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
55-61bpm 55-61bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
54-54bpm 54-54bpm
</td> </td>
</tr> </tr>
<tr> <tr>
<td <td
class="border border-gray-300 p-1 font-medium" class="border border-gray-300 p-1 font-medium text-center"
> >
36-45 36-45
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
83bpm + 83bpm +
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
76-82bpm 76-82bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
71-75bpm 71-75bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
67-70bpm 67-70bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
63-66bpm 63-66bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
57-62bpm 57-62bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
47-56bpm 47-56bpm
</td> </td>
</tr> </tr>
<tr> <tr>
<td <td
class="border border-gray-300 p-1 font-medium" class="border border-gray-300 p-1 font-medium text-center"
> >
46-55 46-55
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
84bpm + 84bpm +
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
77-83bpm 77-83bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
72-76bpm 72-76bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
68-71bpm 68-71bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
64-67bpm 64-67bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
58-63bpm 58-63bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
49-57bpm 49-57bpm
</td> </td>
</tr> </tr>
<tr> <tr>
<td <td
class="border border-gray-300 p-1 font-medium" class="border border-gray-300 p-1 font-medium text-center"
> >
56-65 56-65
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
82bpm + 82bpm +
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
76-81bpm 76-81bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
72-75bpm 72-75bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
68-71bpm 68-71bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
62-67bpm 62-67bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
57-61bpm 57-61bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
51-56bpm 51-56bpm
</td> </td>
</tr> </tr>
<tr> <tr>
<td <td
class="border border-gray-300 p-1 font-medium" class="border border-gray-300 p-1 font-medium text-center"
> >
65+ 65+
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
80bpm + 80bpm +
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
74-79bpm 74-79bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
70-73bpm 70-73bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
66-69bpm 66-69bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
62-65bpm 62-65bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
56-61bpm 56-61bpm
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
52-55bpm 52-55bpm
</td> </td>
</tr> </tr>
@@ -469,37 +469,37 @@
<thead> <thead>
<tr class="bg-cyan-200"> <tr class="bg-cyan-200">
<th <th
class="border border-gray-300 p-1 font-bold" class="border border-gray-300 p-1 font-bold text-center"
> >
Age (M) Age (M)
</th> </th>
<th <th
class="border border-gray-300 p-1 font-bold" class="border border-gray-300 p-1 font-bold text-center"
> >
Very Poor Very Poor
</th> </th>
<th <th
class="border border-gray-300 p-1 font-bold" class="border border-gray-300 p-1 font-bold text-center"
> >
Poor Poor
</th> </th>
<th <th
class="border border-gray-300 p-1 font-bold" class="border border-gray-300 p-1 font-bold text-center"
> >
Fair Fair
</th> </th>
<th <th
class="border border-gray-300 p-1 font-bold" class="border border-gray-300 p-1 font-bold text-center"
> >
Good Good
</th> </th>
<th <th
class="border border-gray-300 p-1 font-bold" class="border border-gray-300 p-1 font-bold text-center"
> >
Excellent Excellent
</th> </th>
<th <th
class="border border-gray-300 p-1 font-bold" class="border border-gray-300 p-1 font-bold text-center"
> >
Superior Superior
</th> </th>
@@ -508,126 +508,126 @@
<tbody> <tbody>
<tr> <tr>
<td <td
class="border border-gray-300 p-1 font-medium" class="border border-gray-300 p-1 font-medium text-center"
> >
20-29 20-29
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
29.0-38.1 29.0-38.1
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
38.1-44.9 38.1-44.9
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
44.9-50.2 44.9-50.2
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
50.2-61.8 50.2-61.8
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
57.1-66.3 57.1-66.3
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
66.3+ 66.3+
</td> </td>
</tr> </tr>
<tr> <tr>
<td <td
class="border border-gray-300 p-1 font-medium" class="border border-gray-300 p-1 font-medium text-center"
> >
30-39 30-39
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
27.2-34.1 27.2-34.1
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
34.1-39.6 34.1-39.6
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
39.6-45.2 39.6-45.2
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
45.2-51.6 45.2-51.6
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
51.6-59.8 51.6-59.8
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
59.8+ 59.8+
</td> </td>
</tr> </tr>
<tr> <tr>
<td <td
class="border border-gray-300 p-1 font-medium" class="border border-gray-300 p-1 font-medium text-center"
> >
40-49 40-49
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
24.2-30.5 24.2-30.5
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
30.5-35.7 30.5-35.7
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
35.7-40.3 35.7-40.3
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
40.3-46.7 40.3-46.7
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
46.7-55.6 46.7-55.6
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
55.6+ 55.6+
</td> </td>
</tr> </tr>
<tr> <tr>
<td <td
class="border border-gray-300 p-1 font-medium" class="border border-gray-300 p-1 font-medium text-center"
> >
50-59 50-59
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
20.9-26.1 20.9-26.1
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
26.1-30.7 26.1-30.7
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
30.7-35.1 30.7-35.1
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
35.1-41.2 35.1-41.2
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
41.2-50.7 41.2-50.7
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
50.7+ 50.7+
</td> </td>
</tr> </tr>
<tr> <tr>
<td <td
class="border border-gray-300 p-1 font-medium" class="border border-gray-300 p-1 font-medium text-center"
> >
60-69 60-69
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
17.4-22.4 17.4-22.4
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
22.4-26.6 22.4-26.6
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
26.6-30.5 26.6-30.5
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
30.5-36.1 30.5-36.1
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
36.1-43.0 36.1-43.0
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
43.0+ 43.0+
</td> </td>
</tr> </tr>
@@ -643,37 +643,37 @@
<thead> <thead>
<tr class="bg-cyan-200"> <tr class="bg-cyan-200">
<th <th
class="border border-gray-300 p-1 font-bold" class="border border-gray-300 p-1 font-bold text-center"
> >
Age (F) Age (F)
</th> </th>
<th <th
class="border border-gray-300 p-1 font-bold" class="border border-gray-300 p-1 font-bold text-center"
> >
Very Poor Very Poor
</th> </th>
<th <th
class="border border-gray-300 p-1 font-bold" class="border border-gray-300 p-1 font-bold text-center"
> >
Poor Poor
</th> </th>
<th <th
class="border border-gray-300 p-1 font-bold" class="border border-gray-300 p-1 font-bold text-center"
> >
Fair Fair
</th> </th>
<th <th
class="border border-gray-300 p-1 font-bold" class="border border-gray-300 p-1 font-bold text-center"
> >
Good Good
</th> </th>
<th <th
class="border border-gray-300 p-1 font-bold" class="border border-gray-300 p-1 font-bold text-center"
> >
Excellent Excellent
</th> </th>
<th <th
class="border border-gray-300 p-1 font-bold" class="border border-gray-300 p-1 font-bold text-center"
> >
Superior Superior
</th> </th>
@@ -682,126 +682,126 @@
<tbody> <tbody>
<tr> <tr>
<td <td
class="border border-gray-300 p-1 font-medium" class="border border-gray-300 p-1 font-medium text-center"
> >
20-29 20-29
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
21.7-28.6 21.7-28.6
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
28.6-34.6 28.6-34.6
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
34.6-40.6 34.6-40.6
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
40.6-46.5 40.6-46.5
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
46.5-56.0 46.5-56.0
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
56.0+ 56.0+
</td> </td>
</tr> </tr>
<tr> <tr>
<td <td
class="border border-gray-300 p-1 font-medium" class="border border-gray-300 p-1 font-medium text-center"
> >
30-39 30-39
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
19.0-24.1 19.0-24.1
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
24.1-28.2 24.1-28.2
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
28.2-32.2 28.2-32.2
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
32.2-35.7 32.2-35.7
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
35.7-45.8 35.7-45.8
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
45.8+ 45.8+
</td> </td>
</tr> </tr>
<tr> <tr>
<td <td
class="border border-gray-300 p-1 font-medium" class="border border-gray-300 p-1 font-medium text-center"
> >
40-49 40-49
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
17.0-21.3 17.0-21.3
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
21.3-24.9 21.3-24.9
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
24.9-28.7 24.9-28.7
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
28.7-34.0 28.7-34.0
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
34.0-41.7 34.0-41.7
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
41.7+ 41.7+
</td> </td>
</tr> </tr>
<tr> <tr>
<td <td
class="border border-gray-300 p-1 font-medium" class="border border-gray-300 p-1 font-medium text-center"
> >
50-59 50-59
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
16.0-19.1 16.0-19.1
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
19.1-24.4 19.1-24.4
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
21.8-27.6 21.8-27.6
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
25.2-28.6 25.2-28.6
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
28.6-35.9 28.6-35.9
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
35.9+ 35.9+
</td> </td>
</tr> </tr>
<tr> <tr>
<td <td
class="border border-gray-300 p-1 font-medium" class="border border-gray-300 p-1 font-medium text-center"
> >
60-69 60-69
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
13.4-16.5 13.4-16.5
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
16.5-18.9 16.5-18.9
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
18.9-21.2 18.9-21.2
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
21.2-24.6 21.2-24.6
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
24.6-29.4 24.6-29.4
</td> </td>
<td class="border border-gray-300 p-1"> <td class="border border-gray-300 p-1 text-center">
29.4+ 29.4+
</td> </td>
</tr> </tr>
+44 -30
View File
@@ -1,7 +1,7 @@
<div class="bg-white w-full page m-0 px-10"> <div class="bg-white w-full page m-0 px-10">
<div class="px-16 py-10"> <div class="px-16 pt-10">
<!-- Table of Contents Header --> <!-- Table of Contents Header -->
<div class="mb-8"> <div class="mb-2">
<h1 <h1
class="text-5xl font-bold text-black mb-6 tracking-wide border-b-4 border-blue-500 pb-2 text-center" class="text-5xl font-bold text-black mb-6 tracking-wide border-b-4 border-blue-500 pb-2 text-center"
> >
@@ -12,12 +12,46 @@
<!-- Table of Contents Items --> <!-- Table of Contents Items -->
<div class="flex flex-col justify-between space-y-6 py-6"> <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 --> <!-- Lung Analysis -->
<div class="flex items-start bg-gray-200 h-24"> <div class="flex items-start bg-gray-200 h-24">
<div <div
class="bg-black text-white text-2xl font-bold w-16 h-full flex items-center justify-center mr-8 flex-shrink-0" class="bg-black text-white text-2xl font-bold w-16 h-full flex items-center justify-center mr-8 flex-shrink-0"
> >
3 7
</div> </div>
<div class="flex flex-col flex-1 py-1 justify-center h-full"> <div class="flex flex-col flex-1 py-1 justify-center h-full">
<h2 class="text-2xl font-semibold text-black"> <h2 class="text-2xl font-semibold text-black">
@@ -37,10 +71,10 @@
<div <div
class="bg-black text-white text-2xl font-bold w-16 h-full flex items-center justify-center mr-8 flex-shrink-0" class="bg-black text-white text-2xl font-bold w-16 h-full flex items-center justify-center mr-8 flex-shrink-0"
> >
4 8
</div> </div>
<div class="flex flex-col py-1 flex-1 justify-center h-full"> <div class="flex flex-col py-1 flex-1 justify-center h-full">
<h2 class="text-2xl font-semibold text-black mb-3"> <h2 class="text-2xl font-semibold text-black">
Cardio Metrics Cardio Metrics
</h2> </h2>
<p class="text-gray-600 text-base"> <p class="text-gray-600 text-base">
@@ -49,26 +83,12 @@
</div> </div>
</div> </div>
<!-- Fuel Utilization -->
<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"
>
5
</div>
<div class="flex flex-col py-1 flex-1 justify-center flex-1 h-full">
<h2 class="text-2xl font-semibold text-black">
Fuel Utilization
</h2>
</div>
</div>
<!-- Local Muscle Activity --> <!-- Local Muscle Activity -->
<div class="flex items-start bg-gray-200 h-24"> <div class="flex items-start bg-gray-200 h-24">
<div <div
class="bg-black text-white text-2xl font-bold w-16 h-full flex items-center justify-center mr-8 flex-shrink-0" class="bg-black text-white text-2xl font-bold w-16 h-full flex items-center justify-center mr-8 flex-shrink-0"
> >
9 11
</div> </div>
<div class="flex flex-col justify-center h-full flex-1"> <div class="flex flex-col justify-center h-full flex-1">
<h2 class="text-2xl font-semibold text-black"> <h2 class="text-2xl font-semibold text-black">
@@ -82,7 +102,7 @@
<div <div
class="bg-black text-white text-2xl font-bold w-16 h-full flex items-center justify-center mr-8 flex-shrink-0" class="bg-black text-white text-2xl font-bold w-16 h-full flex items-center justify-center mr-8 flex-shrink-0"
> >
10 12
</div> </div>
<div class="flex flex-col h-full justify-center flex-1"> <div class="flex flex-col h-full justify-center flex-1">
<h2 class="text-2xl font-semibold text-black"> <h2 class="text-2xl font-semibold text-black">
@@ -96,15 +116,12 @@
<div <div
class="bg-black text-white text-2xl font-bold w-16 h-full flex items-center justify-center mr-8 flex-shrink-0" class="bg-black text-white text-2xl font-bold w-16 h-full flex items-center justify-center mr-8 flex-shrink-0"
> >
12 14
</div> </div>
<div class="flex flex-col h-full justify-center flex-1"> <div class="flex flex-col h-full justify-center flex-1">
<h2 class="text-2xl font-semibold text-black"> <h2 class="text-2xl font-semibold text-black">
Next Steps Next Steps
</h2> </h2>
<div class="space-y-2">
<!-- No sub-items -->
</div>
</div> </div>
</div> </div>
@@ -113,15 +130,12 @@
<div <div
class="bg-black text-white text-2xl font-bold w-16 h-full flex items-center justify-center mr-8 flex-shrink-0" class="bg-black text-white text-2xl font-bold w-16 h-full flex items-center justify-center mr-8 flex-shrink-0"
> >
13 15
</div> </div>
<div class="flex flex-col h-full justify-center flex-1"> <div class="flex flex-col h-full justify-center flex-1">
<h2 class="text-2xl font-semibold text-black"> <h2 class="text-2xl font-semibold text-black">
Glossary Glossary
</h2> </h2>
<div class="space-y-2">
<!-- No sub-items -->
</div>
</div> </div>
</div> </div>
</div> </div>
+4 -8
View File
@@ -13,19 +13,15 @@
<h3 class="text-2xl font-bold text-center text-black mb-6">Body Composition</h3> <h3 class="text-2xl font-bold text-center text-black mb-6">Body Composition</h3>
<!-- Body Composition Chart --> <!-- Body Composition Chart -->
<div class="flex justify-center mb-8"> <div class="flex justify-center mb-16 w-full">
<div class="relative">
<img src="data:image/png;base64, {{ body_composition_chart}}" <img src="data:image/png;base64, {{ body_composition_chart}}"
alt="Body Composition Chart" alt="Body Composition Chart"
class="w-80 h-80 object-contain"> class=" object-contain ">
<!-- Chart Labels -->
</div>
</div> </div>
<!-- Body Fat Percentage Section --> <!-- Body Fat Percentage Section -->
<div class="mb-8"> <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 --> <!-- Body Fat Chart -->
<div class="flex justify-center"> <div class="flex justify-center">
<img src="data:image/png;base64, {{ body_fat_chart }}" <img src="data:image/png;base64, {{ body_fat_chart }}"
+53 -35
View File
@@ -1,38 +1,56 @@
<div class="w-full page bg-white p-8"> <div class="w-full page bg-white p-4">
<!-- Header --> <!-- Header -->
<h1 class="text-3xl font-bold mb-6">Lung Analysis</h1> <h1 class="text-3xl font-bold mb-2">Lung Analysis</h1>
<!-- Spirometry Assessment Section --> <!-- Spirometry Assessment Section -->
<div class="mb-8"> <div class="mb-2">
<h2 class="text-xl font-semibold mb-4">Spirometry Assessment</h2> <h2 class="text-xl font-semibold mb-4">Spirometry Assessment</h2>
<p class="text-sm text-gray-700 mb-6"> <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 Spirometry is a diagnostic device that assesses how well a person
is crucial for oxygen delivery during physical activity. Comparing results to expected/normal values can highlight potential limitations breathes and how their lungs are functioning. Lung function is
that would require additional lung training to improve overall physical activity. crucial for oxygen delivery during physical activity. Comparing
</p> results to expected/normal values can highlight potential
limitations that would require additional lung training to improve
<!-- Lung Volume Chart --> overall physical activity.
<img src="data:image/png;base64,{{ lung_analysis_chart }}" alt="Lung Volume Analysis Chart" class="w-full mb-6"> </p>
<!-- Indications Box --> <!-- Lung Volume Chart -->
<div class="bg-gray-200 rounded-lg p-4 text-center mb-8"> <div class="flex justify-center">
<h3 class="font-semibold text-lg mb-2">Indications</h3> <img
<p class="text-gray-700">{{ indication }}</p> 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> </div>
</div>
<!-- Respiratory Section -->
<!-- Respiratory Section --> <div class="mb-4">
<div class="mb-8"> <h2 class="text-xl font-semibold mb-4 text-center">Respiratory</h2>
<h2 class="text-xl font-semibold mb-4 text-center">Respiratory</h2>
<!-- Respiratory Chart -->
<!-- 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"> <img
src="data:image/png;base64,{{ respiratory_analysis_chart }}"
<!-- Peak VT Info Box --> alt="Respiratory Analysis Chart"
<div class="bg-gray-200 rounded-lg p-4 text-center"> class="w-full mb-4 object-contain max-w-4xl h-auto"
<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> </div>
<p class="text-sm">{{ fev1_percentage }}% of FEV1</p>
<!-- 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>
</div>
</div> </div>
+17 -17
View File
@@ -1,21 +1,21 @@
<div class="w-full page bg-white"> <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>
<!-- Main Content --> <!-- Chart Information -->
<div class="flex flex-col items-center justify-center h-full"> <div class="mt-8 text-center">
<!-- Fuel Utilization Chart --> <p class="text-gray-700 text-sm">
<div class="w-full max-w-5xl"> Client: {{ client_name | default('Keirstyn Moran') }} |
<img src="data:image/png;base64,{{ fuel_utilization_chart }}" Assessment Date: {{ assessment_date | default('July 29 2025') }}
alt="Fuel Utilization Report - Institute of Science, Health and Performance" </p>
class="w-full h-auto object-contain"> </div>
</div> </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> </div>
Binary file not shown.