Compare commits
18 Commits
894fbbcee3
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 5844cb6cff | |||
| e58d9b0158 | |||
| 85ea73ade8 | |||
| 1d5625b61a | |||
| f5d304aec5 | |||
| d862577ecf | |||
| 2568e991e2 | |||
| bad8f18f19 | |||
| e2f6eaab66 | |||
| 192c598e18 | |||
| 7e55ee6954 | |||
| 6b2c61a48e | |||
| f52729d703 | |||
| 54e0189301 | |||
| a20f21d288 | |||
| d12add210b | |||
| a44a763640 | |||
| 604ef375aa |
@@ -1,3 +1,5 @@
|
|||||||
.venv
|
.venv
|
||||||
|
|
||||||
data/
|
data/
|
||||||
|
|
||||||
|
.env
|
||||||
@@ -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": [
|
||||||
|
|||||||
@@ -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"
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
@@ -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
|
||||||
|
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 91 KiB |
@@ -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}")
|
||||||
|
After Width: | Height: | Size: 265 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 300 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 291 KiB |
|
After Width: | Height: | Size: 175 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 425 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 196 KiB |
|
After Width: | Height: | Size: 183 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 251 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 322 KiB |
@@ -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 ",
|
"contact_email": "info@ishplabs.com ",
|
||||||
"website": "www.ishplabs.com",
|
"website": "www.ishplabs.com",
|
||||||
"social": "@ishplabs",
|
"social": "@ishplabs",
|
||||||
"page_number": i + 1,
|
"page_number": i + 1,
|
||||||
} for i in range(len(context_list))]
|
}
|
||||||
|
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")
|
||||||
|
|
||||||
|
|||||||
@@ -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 -->
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 }}"
|
||||||
|
|||||||
@@ -1,37 +1,55 @@
|
|||||||
<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
|
||||||
|
results to expected/normal values can highlight potential
|
||||||
|
limitations that would require additional lung training to improve
|
||||||
|
overall physical activity.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- Lung Volume Chart -->
|
<!-- Lung Volume Chart -->
|
||||||
<img src="data:image/png;base64,{{ lung_analysis_chart }}" alt="Lung Volume Analysis Chart" class="w-full mb-6">
|
<div class="flex justify-center">
|
||||||
|
<img
|
||||||
|
src="data:image/png;base64,{{ lung_analysis_chart }}"
|
||||||
|
alt="Lung Volume Analysis Chart"
|
||||||
|
class="w-full max-w-4xl h-auto object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Indications Box -->
|
<!-- Indications Box -->
|
||||||
<div class="bg-gray-200 rounded-lg p-4 text-center mb-8">
|
<div class="bg-gray-200 rounded-lg p-4 text-center mb-2">
|
||||||
<h3 class="font-semibold text-lg mb-2">Indications</h3>
|
<h3 class="font-semibold text-lg mb-2">Indications</h3>
|
||||||
<p class="text-gray-700">{{ indication }}</p>
|
<p class="text-gray-700">{{ indication }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Respiratory Section -->
|
<!-- Respiratory Section -->
|
||||||
<div class="mb-8">
|
<div class="mb-4">
|
||||||
<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 -->
|
||||||
<img src="data:image/png;base64,{{ respiratory_analysis_chart }}" alt="Respiratory Analysis Chart" class="w-full mb-4">
|
<div class="flex justify-center mb-4">
|
||||||
|
<img
|
||||||
|
src="data:image/png;base64,{{ respiratory_analysis_chart }}"
|
||||||
|
alt="Respiratory Analysis Chart"
|
||||||
|
class="w-full mb-4 object-contain max-w-4xl h-auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Peak VT Info Box -->
|
<!-- Peak VT Info Box -->
|
||||||
<div class="bg-gray-200 rounded-lg p-4 text-center">
|
<div class="bg-gray-200 rounded-lg p-4 text-center">
|
||||||
<h3 class="font-semibold mb-2">Peak VT</h3>
|
<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">
|
||||||
|
{{ 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>
|
<p class="text-sm">{{ fev1_percentage }}% of FEV1</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
<div class="w-full page bg-white">
|
<div class="w-full page bg-white">
|
||||||
|
|
||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
<div class="flex flex-col items-center justify-center h-full">
|
<div class="flex flex-col items-center justify-center h-full">
|
||||||
<!-- Fuel Utilization Chart -->
|
<!-- Fuel Utilization Chart -->
|
||||||
<div class="w-full max-w-5xl">
|
<div class="w-full max-w-5xl">
|
||||||
<img src="data:image/png;base64,{{ fuel_utilization_chart }}"
|
<img
|
||||||
|
src="data:image/png;base64,{{ fuel_utilization_chart }}"
|
||||||
alt="Fuel Utilization Report - Institute of Science, Health and Performance"
|
alt="Fuel Utilization Report - Institute of Science, Health and Performance"
|
||||||
class="w-full h-auto object-contain">
|
class="w-full h-auto object-contain chart-large"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Chart Information -->
|
<!-- Chart Information -->
|
||||||
@@ -17,5 +18,4 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||