From dbee12341a70f0795635dcd47338403740015eac Mon Sep 17 00:00:00 2001 From: bolade Date: Fri, 21 Nov 2025 14:15:29 +0100 Subject: [PATCH] Good good progress --- app/report_gen/page_1.html | 2 +- app/report_gen/page_8.html | 4 - .../context_generator.cpython-312.pyc | Bin 45857 -> 46085 bytes .../graph_generator.cpython-312.pyc | Bin 58444 -> 57999 bytes app/services/context_generator.py | 52 +++++------ app/services/graph_generator.py | 81 ++++++++---------- notebooks/graphs.ipynb | 44 +++++----- 7 files changed, 80 insertions(+), 103 deletions(-) diff --git a/app/report_gen/page_1.html b/app/report_gen/page_1.html index d7f2297..ffd43fb 100644 --- a/app/report_gen/page_1.html +++ b/app/report_gen/page_1.html @@ -26,7 +26,7 @@

- {{ first_name|upper }} + {{ name|upper }}

{{ surname|upper }} diff --git a/app/report_gen/page_8.html b/app/report_gen/page_8.html index 7dfd679..7e0e15d 100644 --- a/app/report_gen/page_8.html +++ b/app/report_gen/page_8.html @@ -29,10 +29,6 @@
-

- Personalized Heart Rate Zones -

-
9ntrcVs6s#}wkoKFy;!saTo7at0X4WJ?oq^&R}^c3x5|yYN|{MIVpraNPDrsx0f1JpS)^UkUN z{{MIX^}hG+`){56O7??4$Re*zo2KF5+9{Y@oS$Bbd^by@22l>byUjF8ZJ@*IRI1Ca znzmh(E#m6CIL@o^sOj{jMp~K>r%(uTJsrvR-hzhTg zj#nno1qr$wC2Dw8qDo*Kj4DK6RO2Xw9g}oU2-_6}1y#v=d>p4~5Z)a#7PHTg6#5 z-E$W?JdQ?>dF|?1X1hy(vB2zUCJuLVXS?90wnja@WqmRsoJ2shkg~-kICh2P?G9H+ zZf|d=CS>=@KZ}hY z(IpjWY&z290xjYwu*_1@g;-85JXz?16-!W!8u} zYalFWo;9vk7~*@YBE~gboPI2cOSFz9`)Tes3O?4q&o&-zx&SGY=aK9|@(hxNNCuId zr+xEJ@UPP1k`4T2N=sI&t|I&%{Zq*+s`ruofF3Ay@%jGmOaG|mFVU|TiTofHsy_ww zikd|JO}e2bM@>$m>K*z*%{%-de{Jo#F#d{vd_|{HHpo%u8ncX;{Ligf$@Bl^|Lxij z74+oBtO$|_OE@|Y4ZoouZ8R;I1F}^%x)hPQ5?_P-Jc(yX7fbvC#OF(VHu5Djche@{ zA_-Z7NVUXQBVWUK@(uE}5E87-R9QIP_~W01rl!G$43NM-|ppih7<1ZOKjmP=R} z+Dl773Q>0j?_(8|qJRh(K88vY2DQkqWH_@U_y~j31k_(4>F1#i0)T&uytJZBl>y+h z3USGHv>f?Wl5H^u@yTjQn28f>Bz`gS6So6)5ss{r@CxMDOZ)^LC?^j~lA$+dR$ zwE&1!MK?C;ld4cgV&zFI4&fvky|>YQjmGFLINOY*1&Ncs*|>qfMW+i#`0Mn%V5Q?i ze6*loTdA@e?7FqEe*y^mY$=Gy(ylNSef^0PIwmaT)wIN6q2{7+>TqPK*>)>vD0O7P zR{C?GTKXm6TIMiME=Ix_ zEo#Il7^jO`F}g|aRC=d7IU(j0?~M^+>Ay5t_`Q^OS|M_-(*ksvbB4?lMX$ERS#%F} zdNFY_)KN}|JFf5=L_O^OK4(&4JWDeHKR}FQvNu^wZp9`1AD7z;cr3kIU{Iz27l)kB z=t-DV6H`QEH+&!`qX`LZGFWo8m?9(|RR~GPWnPm@Ga>0j;{>Yknq-_9D;g*5Hi4Db z?6oj7t;+((V_Qk>7)n%c`CcCGh(N4BD z5WAGl5^D8YE%s)d&Pgcbv@6I)9>3}i!$6#`^x^eZ zIr$8v?kICdSF@%d!@R`R*xurD+~?5aXNQ$K*$G!)g@XC@(7wh)b9We$YvIph|BW91wWjUH2#fuU9A_8$4!LUqdXbh$kd3||-thqt` zyxaQnpuYT;zOpCk(f-93jGx=11??!q?MP!V(s*S1t;mcX`G~)xYYU%zPsOE}`;|wR z9x3hLb|H3P)nMX)W+*+7G`D9dy|Ha`N?>+%Aibt9{7#l_D7L?I=<%W5!?8o1f$BAZ z^tH5xRM_$YX)F4|?qu5fHw{f6nt!=5P_S^=GQ4!S^jF&gk8ccYY6z^{64=raNOOUj zc_zI|G8Od!Qx*MpQk6DhNim z+a@0|X4B(ty&5*&%#yJR{}uPY$}9tLU%oEH%Ux3e-;{GGvj;3`3Cic``#aLo@ljuX zPIW(gt%K|~uk7q3Y{w5NYV0k9RKhG-NHsgtD~Hhh#&x`^(EQSryFTXrwFmRey&vT! zyuMn7`mfW@orcPvO<8?XiRc?sTA7PbesgMmLoLdeQI?`UhVonV+D?P-ZA4jV{xU>= zKBcvvd3|TfOCR(4k5lpnmfIDO;gjlQ={3!&kn(JrgTCIK((Rj%@-WB*(*~Ypf8)9y z)*xabPF_W2c);N~tw#Ah>f2?gd>>JkbRA3Qg9kE8=ND5}6|Bf#PRU#ydijuky364E z6{4)j!cs(kjk0wADp39>klFeH|6m!vVmIR&%wd5nH4X}tvYCx`*U^u3W9FfJ17&G3 zS)7|FbDSP+S^6I#zyg25(htxVcN_R19o(Ilh^2xLRx$H&mzjJ_|Ge8!c?%7tNCg<_ zHz-RLu}pqD<+zV!^1CVhrUj^fn?Bb4ye|^a(JV-&TUlt!FDxdkHQWejx3b8VUsO!$ zP@{xt%(msvE@n+DYmvK^b8Pu@iV163u{G+JXD_n?sj(8)0geE6pD$(mEqMoANc~a+ zz*ad~4}hAiMYxf!dnzy00eDEx8YSdZa0=Vr?jcSPCR!hQ_Nivx<&PAb?AD?_apt&*S@X%pZrVrTvX{<{>#uDTRYa| zjmD$+360%*Q@S>Xw75)!<yhM#q!djRov1=2< zgpEwH9#wc^nSz9FqLD9W@bz@dL5pFt5HkoHI$lDE68{WP3Upyu8L zdi0Q%4!xXEYky!NCJ0+G0({~dkIO(3P=e9%2-?K5a2`{E_vQ)j8I#_XiBMY>Yp9Zd zvm(dqF(SViGc>h4h$pljlZlCEc`*rodiZICHu}L~yBwOim!6C>vv&M+tMHPO%e z5@wr3BXpNhaKpC`DzPpI2@8N)!VX+kj|nlngq@Sc(EbBi(|2L3EkwZLX`s#4!ha1T z2lDydxX8NhhnDxLWANXGAePeKAJ)^qA5cYmCybtEcTKa3{{0o}OGkAwtIwZvG?ur$ z4Sop{cx{Sd2x+_QPDg`ld&gD>2`QOCy!c?VW9?_O<5&@|q^FKORX+x3%V$sr8Aom> zD&Z^8at}H7M*JB$6k8fV4~{e#f+(?{jzkiL1pP1<(hI~4@6qn#8T==-|M-tg*v5^9 zqapjF;wQ-c6v;&-Lv;14W<4Ie{*UQ{ujcyj#SZZ`A$}P_&2E`F`#x{AstG%6;;z9y z%;cxYN#AAUaAzUx==J}qthOpc3f7+4Fe3vNg}YWL(rw@2Xt2BKyc5Q_ZRm_38Ah!u zNZhpTgr3*X=TBq+y>P+;^yY~qpno`#2{fvIPgQ)eEhE?5*<@a3-(_CxfJ3GE3E0z! z4_)m+@(hxHK=Le-=YYWe3I`94u!NxbKY4KBRSXzH@*a}+k$eEesv^mt8j9Z0SE|bt_-y*^$&>^$#0V*D&}XvaZ-{TQhQVUeS*I*3t6+|lAt^^vfdnzD z@2;8QULc5e3Ax5AMd|Bf2P|I&z!9zT^2kKDCWb^Lvrcsf<}1$z9N zR-Cr*Kc-tx=fwRE^+u6I;M5Ext@PySSd|Oet@Q2FSqWyTJ7-VecL;ArgYW6Lr_11v zs<~&((Q%Li`8S-BZXtD^DdIn;$IcYeJ7-q%9dy>&*pvcHRLc(do$cE@T<%a93V8e7 zW@xq3SeyOBvt_C}1+>|v{*TVSso;M@_n!Z2asd~Yep^=+)K!h7YFhZ&O{BTPwh|ZOBvHLF@n=X_B8eu R^|b6W-J8}*UwG~Ce*>DpKB@o! delta 5790 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 diff --git a/app/services/__pycache__/graph_generator.cpython-312.pyc b/app/services/__pycache__/graph_generator.cpython-312.pyc index f78943026faa1b817a325dd487c6303571c632d9..b61675b59930110003f559f1da004c2e085c974d 100644 GIT binary patch delta 5168 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 diff --git a/app/services/context_generator.py b/app/services/context_generator.py index 45b1b21..925a216 100644 --- a/app/services/context_generator.py +++ b/app/services/context_generator.py @@ -804,42 +804,42 @@ class ContextGenerator: ) zone_df = self.pnoe_df[mask] + # HR BPM Range - match notebook exactly + hr_bpm_str = f"{int(start)}-{int(end)} bpm" + if not zone_df.empty: - # Speed calculation + # Speed calculation - match notebook exactly speed_series = zone_df[zone_df["Speed"] > 0.1]["Speed"] if not speed_series.empty: min_speed = speed_series.min() max_speed = speed_series.max() if abs(min_speed - max_speed) < 0.1: - speed_str = f"{min_speed:.1f}mph\n2% Incline" + speed_str = f"{min_speed:.1f} mph\n2% Incline" else: - speed_str = f"{min_speed:.1f}-{max_speed:.1f}mph\n2% Incline" + speed_str = f"{min_speed:.1f}-{max_speed:.1f} mph\n2% Incline" # Pace calculation (max speed -> min pace, min speed -> max pace) min_pace_m, min_pace_s = speed_to_pace(max_speed) max_pace_m, max_pace_s = speed_to_pace(min_speed) if min_pace_m == max_pace_m and min_pace_s == max_pace_s: - pace_str = f"{min_pace_m}:{min_pace_s:02d}min/km Pace" + pace_str = f"{min_pace_m}:{min_pace_s:02d} min/km Pace" else: - pace_str = ( - f"{max_pace_m}:{max_pace_s:02d}-{min_pace_m}:{min_pace_s:02d}\n" - f"min/km Pace" - ) + pace_str = f"{max_pace_m}:{max_pace_s:02d}-{min_pace_m}:{min_pace_s:02d}\nmin/km Pace" else: speed_str = "-\n2% Incline" pace_str = "-" - # Calories (using raw EE) + # Calories (using raw EE) - match notebook exactly avg_cals = zone_df["EE(kcal/min)"].mean() - calories_str = f"Avg:\n{avg_cals:.1f}kcals/minute" + calories_str = f"Avg:\n{avg_cals:.1f} kcals/minute" - # Carb utilization (g/min) + # Carb utilization (g/min) - match notebook exactly avg_carbs_g = zone_df["CHO"].mean() / 4 carb_str = f"Avg: {avg_carbs_g:.1f}g/min\nCarb Utilization" - # Breathing + # Breathing - match notebook exactly avg_breaths = zone_df["BF(bpm)_smoothed"].mean() breath_str = ( f"Avg: {int(avg_breaths)} breaths\n{ideal_breath_ranges[i]}" @@ -854,7 +854,7 @@ class ContextGenerator: zone_data.append( { "zone_name": name, - "hr_bpm": f"{int(start)}-{int(end)}bpm", + "hr_bpm": hr_bpm_str, "speed": speed_str, "pace": pace_str, "calories": calories_str, @@ -1246,18 +1246,18 @@ class ContextGenerator: hr_zones_columns = ["Zone 1", "Zone 2", "Zone 3", "Zone 4", "Zone 5"] hr_zones_data = [ [ - "Improves health and recovery capacity", - "Improves endurance and fat burning", - "Improves Aerobic fitness", - "Improves maximum performance capacity", - "Develops maximum performance and speed", + "Improves health and\nrecovery capacity", + "Improves endurance\nand fat burning", + "Improves Aerobic\nfitness", + "Improves maximum\nperformance capacity", + "Develops maximum\nperformance and speed", ], [ "55-65% of Max Heart Rate", "65-75% of Max Heart Rate", "80-85% of Max Heart Rate", "85-88% of Max Heart Rate", - "90% of Max Heart Rate", + "90%+ of Max Heart Rate", ], [zone_metrics["zones"][i]["hr_bpm"] for i in range(5)], [zone_metrics["zones"][i]["speed"] for i in range(5)], @@ -1266,22 +1266,12 @@ class ContextGenerator: [zone_metrics["zones"][i]["carb"] for i in range(5)], [zone_metrics["zones"][i]["breathing"] for i in range(5)], ] - hr_zones_colors = [ - ["#ffffff"] * 5, - ["#ffffff"] * 5, - ["#ffcdd2", "#ffcdd2", "#fff9c4", "#c8e6c9", "#c8e6c9"], - ["#ffffff"] * 5, - ["#ffffff"] * 5, - ["#ffffff"] * 5, - ["#ffffff"] * 5, - ["#ffcdd2", "#ffcdd2", "#fff9c4", "#c8e6c9", "#c8e6c9"], - ] - + # Colors are now handled directly in the graph generator to match notebook + # No need to pass cell_colors contexts["page_8"]["hr_zones_table"] = ( graph_generator.generate_heart_rate_zones_table( data=hr_zones_data, columns=hr_zones_columns, - cell_colors=hr_zones_colors, save_as_base64=True, ) ) diff --git a/app/services/graph_generator.py b/app/services/graph_generator.py index 4c9f31a..441bc78 100644 --- a/app/services/graph_generator.py +++ b/app/services/graph_generator.py @@ -1465,67 +1465,60 @@ class GraphGenerator: Args: data: List of rows (each row is a list of values) columns: List of column headers (Zone 1-5) - cell_colors: Optional matrix of cell colors + cell_colors: Optional matrix of cell colors (IGNORED - using notebook colors) save_as_base64: If True, return base64 string Returns: Base64 string or file path """ import io - import textwrap - # Optimal sizing for HR Zones table (5 columns, 8 rows) - fig, ax = plt.subplots(figsize=(14, 8)) + # Optimal sizing for HR Zones table (5 columns, 8 rows) - match notebook exactly + fig, ax = plt.subplots(figsize=(12, 8)) ax.axis("off") - # Column widths - slightly wider for first column which has longer text - # col_widths = [0.24, 0.19, 0.19, 0.19, 0.19] + # Data comes pre-formatted with newlines from context_generator - use as-is + # No text wrapping needed - # Wrap text in cells for better readability - wrapped_data = [] - for row in data: - wrapped_row = [] - for i, cell in enumerate(row): - cell_text = str(cell) - # First column needs more wrapping space - wrap_width = 35 if i == 0 else 25 - wrapped_text = "\n".join(textwrap.wrap(cell_text, width=wrap_width)) - wrapped_row.append(wrapped_text) - wrapped_data.append(wrapped_row) - - # Create table + # Create table without rowLabels - match notebook exactly table = ax.table( - cellText=wrapped_data, + cellText=data, colLabels=columns, - cellLoc="center", loc="center", - colColours=["#4dd0e1"] * len(columns), - # colWidths=col_widths, + cellLoc="center", ) - # Style the table + # Style the table - match notebook exactly table.auto_set_font_size(False) - table.set_fontsize(11) - table.scale(1, 2.0) + table.set_fontsize(10) + table.scale(1, 3.5) # Increased vertical scale for multi-line text - # Apply cell colors - if cell_colors: - for i, row_colors in enumerate(cell_colors): - for j, color in enumerate(row_colors): - if color and j < len(columns): - cell = table[(i + 1, j)] - cell.set_facecolor(color) + # Header row styling + for j, label in enumerate(columns): + cell = table[(0, j)] + cell.set_facecolor("#7dd3fc") # cyan-300 + cell.set_text_props(weight="bold") - # Style all cells - for (row, col), cell in table.get_celld().items(): - if row == 0: - cell.set_text_props(weight="bold", fontsize=13) - cell.set_edgecolor("#333333") - cell.set_linewidth(1.5) - else: - cell.set_edgecolor("#666666") - cell.set_linewidth(0.8) - cell.set_text_props(fontsize=10) + # Row specific styling - match notebook colors exactly + colors = ["#fecaca", "#fecaca", "#fef08a", "#bbf7d0", "#bbf7d0"] + + # HR BPM row is at index 2 (0-based in data) -> row 3 in table (0 is header) + for j in range(len(columns)): + cell = table[(3, j)] + cell.set_facecolor(colors[j]) + cell.set_text_props(weight="bold") + + # Breathing row is at index 7 -> row 8 in table + for j in range(len(columns)): + cell = table[(8, j)] + cell.set_facecolor(colors[j]) + cell.set_text_props(weight="bold") + + # Add title matching notebook + plt.title( + "Personalized Heart Rate Zones", fontsize=16, fontweight="bold", pad=5 + ) + plt.tight_layout() if save_as_base64: buf = io.BytesIO() @@ -1535,7 +1528,6 @@ class GraphGenerator: bbox_inches="tight", dpi=300, facecolor="white", - pad_inches=0.1, ) plt.close(fig) buf.seek(0) @@ -1549,7 +1541,6 @@ class GraphGenerator: bbox_inches="tight", dpi=300, facecolor="white", - pad_inches=0.1, ) plt.close(fig) return str(output_path) diff --git a/notebooks/graphs.ipynb b/notebooks/graphs.ipynb index 7d12178..c418f07 100644 --- a/notebooks/graphs.ipynb +++ b/notebooks/graphs.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, + "execution_count": 21, "id": "63f43af5", "metadata": {}, "outputs": [], @@ -16,7 +16,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 22, "id": "97da3d1c", "metadata": {}, "outputs": [], @@ -26,7 +26,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 23, "id": "b0ee2af1", "metadata": {}, "outputs": [ @@ -42,7 +42,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "/tmp/ipykernel_103354/3076306744.py:3: FutureWarning: errors='ignore' is deprecated and will raise in a future version. Use to_numeric without passing `errors` and catch exceptions explicitly instead\n", + "/tmp/ipykernel_150441/3076306744.py:3: FutureWarning: errors='ignore' is deprecated and will raise in a future version. Use to_numeric without passing `errors` and catch exceptions explicitly instead\n", " df = df.apply(pd.to_numeric, errors='ignore')\n" ] } @@ -72,7 +72,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 24, "id": "99116a35", "metadata": {}, "outputs": [], @@ -84,7 +84,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 25, "id": "fbd292c3", "metadata": {}, "outputs": [ @@ -102,7 +102,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 26, "id": "4c439b2c", "metadata": {}, "outputs": [ @@ -167,7 +167,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 27, "id": "a565f1b3", "metadata": {}, "outputs": [ @@ -237,7 +237,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 28, "id": "470e871e", "metadata": {}, "outputs": [ @@ -431,7 +431,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 29, "id": "0ab6f0b0", "metadata": {}, "outputs": [ @@ -577,7 +577,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 30, "id": "ef8bc7ac", "metadata": {}, "outputs": [ @@ -638,7 +638,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 31, "id": "06244aa2", "metadata": {}, "outputs": [ @@ -753,7 +753,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 32, "id": "8a1878a0", "metadata": {}, "outputs": [ @@ -832,7 +832,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 33, "id": "7361fb05", "metadata": {}, "outputs": [ @@ -893,7 +893,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 34, "id": "c89478ff", "metadata": {}, "outputs": [ @@ -953,7 +953,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 35, "id": "1db16040", "metadata": {}, "outputs": [ @@ -1028,7 +1028,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 36, "id": "c8ad6076", "metadata": {}, "outputs": [ @@ -1109,7 +1109,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 37, "id": "25327cc1", "metadata": {}, "outputs": [ @@ -1415,7 +1415,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 38, "id": "c46b53f0", "metadata": {}, "outputs": [ @@ -1731,7 +1731,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 39, "id": "84addc63", "metadata": {}, "outputs": [ @@ -1869,7 +1869,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 40, "id": "f324fe94", "metadata": {}, "outputs": [ @@ -2066,7 +2066,7 @@ ], "metadata": { "kernelspec": { - "display_name": ".venv", + "display_name": "report-generation", "language": "python", "name": "python3" },