From 9c1cb1966bd39705f100dfbd4223d3159d9b4519 Mon Sep 17 00:00:00 2001 From: bolade Date: Fri, 21 Nov 2025 12:34:53 +0100 Subject: [PATCH] Little progress --- app/report_gen/page_11.html | 2 +- app/report_gen/page_8.html | 4 +- .../context_generator.cpython-312.pyc | Bin 43566 -> 43605 bytes .../graph_generator.cpython-312.pyc | Bin 49398 -> 55507 bytes .../report_generator.cpython-312.pyc | Bin 22040 -> 22278 bytes app/services/context_generator.py | 29 +- app/services/graph_generator.py | 261 +++++++++++++++--- app/services/report_generator.py | 7 + 8 files changed, 254 insertions(+), 49 deletions(-) diff --git a/app/report_gen/page_11.html b/app/report_gen/page_11.html index 7c79a95..0ef0c45 100644 --- a/app/report_gen/page_11.html +++ b/app/report_gen/page_11.html @@ -131,7 +131,7 @@ Resting Heart Rate Table diff --git a/app/report_gen/page_8.html b/app/report_gen/page_8.html index 52cb11f..63d7391 100644 --- a/app/report_gen/page_8.html +++ b/app/report_gen/page_8.html @@ -27,7 +27,7 @@ VO2 Max Table @@ -43,7 +43,7 @@ Heart Rate Zones Table diff --git a/app/services/__pycache__/context_generator.cpython-312.pyc b/app/services/__pycache__/context_generator.cpython-312.pyc index b291d4c95624efa3c7613421626d428c18c51108..fb6bb682314e3f878cca502827e498f14429cca3 100644 GIT binary patch delta 1213 zcmaJ=O>7%Q6y9;x_3x(k`u`8xS%T%5Bqa$UE$Ja~5VRzvRil#DDX!~rT)S>uHl`G_ zQ8^G12x;t$1WA)ZWJF7psx2770Rckdh6Guuh$RxJS0oPGQXzpjFf$1)l@KG%+xOmn z-}l~|*}Zy0^ZgAC_YKDx8N9w63gucphqL-g8$6T+&{@{vQg=Np`D_i`i~rx-L}M+x#O8zOK-`9cbGnZI zm}R7w^rwf1X@}7zwq)C!ie`_XBXi9lG5lW#`V4xiSAy$JEp zrxiDDqA8ZWR|}q4)=19xf#ky5jkgizs1_@Jjd@Wzczd-BV(1oU19|jC)nz&f3-A<- z!vQ#sZdL_w68&C{qSkqT^El}#tIvW-KP=)aIF0x7`0zUolTZf z#|s(pUHoy2GekUi7jYt!5=%)6E*Hl$6Pr+!yzH>#2|R_(Mwr6UzC$i8RjPlrbQ^%b z>K|W>v5$BeYv>0{w`}QFJk5$fri9uQA)*=>>tnw$S}U(MFr1?@BAXvrI47IiRePPU z9R;m%*&bH{ElOyI@?fVDiD80c0APm0i5XVUf?KvUtqYy9rAu|z-+6DBrc>QUOZCZ? zKE)GO{1GM8vQ?^|F#oA_ST+Y&OtSgmJ9cEa^ih+GPqgX3>){if#&r*$=rOPF;uE_Y zWgkDPvB}dsOo4+&-jFuuEjRh zKtm(rZCVi)1FDIJX2#e!ovw6E=Uxxq;Ydm(f1hZ;CTbL)LLaQ|2SezO)rYo^+JXBv ILo9{-4cnn#2LJ#7 delta 1186 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 J&Y%>7{tb2;S0Vra delta 2611 zcmY*bYfu~46~4PG2|ZTQ>ajxN5eo#wGWdyyV8C`Tei2--YmX^Ry3tLFqHhV7#bpW(`PCb%Tjm=2fe7HTF8a8f&r;F9%-1ia(+X3>>j- zYBa~>lISu9@*iVLJtabpXczRNQP7H-cAbz*BD2-VDC5=}5PXCu@e`Wta*a?RdPKX2 zR4o(|<`Ff(Rof?O){}5T5lNEs&3ymn+0z==2-Pv$6a9dqF3leN-W7(rh*!Q}Nxd{{ zf4P;_oud$v&f|fr-=;3(|6XmQ7I5q2bNmI6e?pQ=vj8&ylX!8mlKRc8`A4H!Pu+z8 zKgcf<__zVsd!QWx_IqGw2@GWU4j(#HTYEs72kCcsVR|d~0niH&Hw^GT=Hk1m7m0XS zS|aGzN@PorR1RT#0saO6<4Bi*T>-WXED5lJBk@Wqg>S|y_zywQf&V1H(_mh})HMe+ ziB~2|aP_s*x<7*W=h?Aq*XhA1@XttS%GeePww&z}UF0yRJ+fgaEzq%aaK(7lwvfELj!n(!XWhEDW) zCk=%6nq^3?Hbt}U(}FKzz;_H!H5%hvVFvd;Oq-tmsQcO58#hWRG7FdB*bRCpT;XnP#Wr>@oGhI=HRapG> z86M}n#ZoM$%6NS1mWd|fOdUT+3D#-YvrIOvbWr9q7MZnnt-6EgxeA2T8qE>Qh6u}s z2;XXiHNq)em>MxCtYXfrqGD~Q;jBk&imf|)EkFFlO_I)~aBFEOG*YZVBb6FN%G+tl z!$nPbg=F-dncQjx_K1Vzu?x(Z%?Ocktcnfa3G-HCd_x+d1>8JddDp~@lxy()mQDC} zmC2eVT@o0<9<)WAinF^I&uq`pM0i1z$4hJ56-hu!18cQoY zF4*R@2+ZR$<*oSj+xZONy%Ta6*BssD>gJ{2HZUiVf`9lPuMymG1#Ui-M|W4_#7}dn zz0kq(DiS=eF#{&DCa%ozdc~yUOCVj z6up74H_+?t>Irm+m^zb>KY6qJD{_|{6sx>VeR5Z*HxTrywzoYbdF4(q)6eML&>ml- z4V{8rVc8pM_Y!A+Pp?r8}1DBsly8aIpF`ld>e0HmA4M=YpaFl)ej~z8}AZyFCh~`c6f_yac8b7(6|GMtTwWMNlsTd(7?Vkg*J4{e6=@ff@V*;GY0q@a_RN2&@5E4Y1EB z-1zgt;vW+s#gP4yzED|fe|yRl>X-ZaNf`n1NWulgCtl9RH|BNO&JM9xlmfEYl9{x6 zga&+g^cnZy1|^?7CgBpYIp@%$3gmD_1?(RW#I{b9Cu}7%{2RWx+JtT2kZxtm?ki06 z2xiB_?{kH>d~@Bu+z3G~M8U7z-M{Z}eAo4wH)@tks}rTw3#EIPOOGTSCsV|uo6v4W=o(pjphN1P*VP^l5|o^d|bkT$6%i(EP5MdRUD z{IQm_S(Pj(J11>bDTh4yX*GS?j*R9<4p1dC2NlE5ub-DVd}sL#^#i;$>8^=E)mdRN x&>E6@rPC1g_`d*%mxf?+YIh4UKiAjvgoOT}xKGN*=aY5TXL+jVQv|36{SToh!MXqd diff --git a/app/services/__pycache__/report_generator.cpython-312.pyc b/app/services/__pycache__/report_generator.cpython-312.pyc index 602f5625b16d262c16b2816771214288d492d260..de346c828db1b04cf0d6c85d72788f632ca701ac 100644 GIT binary patch delta 258 zcmbQShOuoOBj0IWUM>b8*zc>5Dd)eDFIArD49n(H`FYGd3JQ89iAg!Bx|z9&lTRth zv*hLHrA~gQB+09wP@b7ml3}HgSXz<~5)o8T$W5%!g^C&)7^s3oCrc}GvA~pwC@AD7 zWu+#U=%!_sSScju=anR8PVQBd-Yl$K!K|Vq!TlkEk%337L+1knvpDC6NJf1To(_%= zNsJ5v5*_?MKRGgQUaywM%qX#0OshbSQGIi}#}sa%T1Hlh8!`$Dgcl?)keKQBfw^|F lZio~=NcaIK&u3N;<3ru%u#nA+{Qew_T%Q;~WRW7!%K(^bP8b8P;*qs)brcOmnzS6k9l*c{52$RHrm!TS@f3f}2^i3*;CTHkW!%;T9@mWRQO7Vk)A8_(~W(6@m6l``0-OR}E!NJJ&i2+0wDFU4W0OKSo ALjV8( diff --git a/app/services/context_generator.py b/app/services/context_generator.py index fd9a1f7..06195ab 100644 --- a/app/services/context_generator.py +++ b/app/services/context_generator.py @@ -1163,12 +1163,13 @@ class ContextGenerator: ] ] - contexts["page_8"]["vo2_max_table"] = graph_generator.generate_table_image( - data=vo2_max_data, - columns=vo2_max_columns, - cell_colors=vo2_max_colors, - header_color="#4dd0e1", - save_as_base64=True, + contexts["page_8"]["vo2_max_table"] = ( + graph_generator.generate_vo2_max_table( + data=vo2_max_data, + columns=vo2_max_columns, + cell_colors=vo2_max_colors, + save_as_base64=True, + ) ) # Calculate zone metrics for the table @@ -1211,11 +1212,10 @@ class ContextGenerator: ] contexts["page_8"]["hr_zones_table"] = ( - graph_generator.generate_table_image( + graph_generator.generate_heart_rate_zones_table( data=hr_zones_data, columns=hr_zones_columns, cell_colors=hr_zones_colors, - header_color="#4dd0e1", save_as_base64=True, ) ) @@ -1288,12 +1288,13 @@ class ContextGenerator: ] rhr_colors = [["#b2ebf2"] + ["#f5f5f5"] * 7] - contexts["page_11"]["rhr_table"] = graph_generator.generate_table_image( - data=rhr_data, - columns=rhr_columns, - cell_colors=rhr_colors, - header_color="#4dd0e1", - save_as_base64=True, + contexts["page_11"]["rhr_table"] = ( + graph_generator.generate_resting_heart_rate_table( + data=rhr_data, + columns=rhr_columns, + cell_colors=rhr_colors, + save_as_base64=True, + ) ) # Pages 12-17 diff --git a/app/services/graph_generator.py b/app/services/graph_generator.py index f8a0632..616b337 100644 --- a/app/services/graph_generator.py +++ b/app/services/graph_generator.py @@ -1306,41 +1306,33 @@ class GraphGenerator: return self._image_to_base64(chart_path) if save_as_base64 else str(chart_path) - def generate_table_image( + def generate_vo2_max_table( self, data: list[list], columns: list[str], - title: str = None, - col_widths: list[float] = None, cell_colors: list[list[str]] = None, - header_color: str = "#4dd0e1", save_as_base64: bool = True, ) -> str: """ - Generate a table as an image. + Generate VO2 Max table as an image with optimized sizing. Args: data: List of rows (each row is a list of values) columns: List of column headers - title: Optional title for the table - col_widths: Optional list of column widths - cell_colors: Optional matrix of cell colors (same shape as data) - header_color: Color for the header row + cell_colors: Optional matrix of cell colors save_as_base64: If True, return base64 string Returns: Base64 string or file path """ - # Calculate figure size based on rows and columns - # Approximate height: header + rows - height = (len(data) + 1) * 0.5 + (0.5 if title else 0) - width = len(columns) * 2.5 if not col_widths else sum(col_widths) * 10 + import io - fig, ax = plt.subplots(figsize=(width, height)) + # Fixed optimal sizing for VO2 Max table (8 columns, 1 data row) + fig, ax = plt.subplots(figsize=(16, 2.5)) ax.axis("off") - if title: - plt.title(title, pad=20, fontsize=14, fontweight="bold") + # Even column widths for VO2 Max table + col_widths = [1.0 / len(columns)] * len(columns) # Create table table = ax.table( @@ -1348,43 +1340,248 @@ class GraphGenerator: colLabels=columns, cellLoc="center", loc="center", - colColours=[header_color] * len(columns), + colColours=["#4dd0e1"] * len(columns), + colWidths=col_widths, ) # Style the table table.auto_set_font_size(False) - table.set_fontsize(10) - table.scale(1, 1.5) # Increase row height + table.set_fontsize(11) + table.scale(1, 3.0) - # Apply cell colors if provided + # Apply cell colors if cell_colors: for i, row_colors in enumerate(cell_colors): for j, color in enumerate(row_colors): - if color: - # (row_idx, col_idx) - row_idx starts at 1 for data (0 is header) + if color and j < len(columns): cell = table[(i + 1, j)] cell.set_facecolor(color) - # Bold headers + # Style all cells for (row, col), cell in table.get_celld().items(): if row == 0: - cell.set_text_props(weight="bold") - cell.set_height(0.1) - - plt.tight_layout() + cell.set_text_props(weight="bold", fontsize=12) + cell.set_edgecolor("#333333") + cell.set_linewidth(1.5) + else: + cell.set_edgecolor("#666666") + cell.set_linewidth(1.0) + cell.set_text_props(fontsize=10) if save_as_base64: - import io - buf = io.BytesIO() - plt.savefig(buf, format="png", bbox_inches="tight", dpi=300) + plt.savefig( + buf, + format="png", + bbox_inches="tight", + dpi=300, + facecolor="white", + pad_inches=0.1, + ) plt.close(fig) buf.seek(0) return base64.b64encode(buf.read()).decode("utf-8") else: output_path = ( - self.charts_dir / f"table_{pd.Timestamp.now().timestamp()}.png" + self.charts_dir / f"vo2_max_table_{pd.Timestamp.now().timestamp()}.png" + ) + plt.savefig( + output_path, + bbox_inches="tight", + dpi=300, + facecolor="white", + pad_inches=0.1, + ) + plt.close(fig) + return str(output_path) + + def generate_heart_rate_zones_table( + self, + data: list[list], + columns: list[str], + cell_colors: list[list[str]] = None, + save_as_base64: bool = True, + ) -> str: + """ + Generate Heart Rate Zones table as an image with optimized sizing. + + 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 + 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=(18, 12)) + 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] + + # 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 + table = ax.table( + cellText=wrapped_data, + colLabels=columns, + cellLoc="center", + loc="center", + colColours=["#4dd0e1"] * len(columns), + colWidths=col_widths, + ) + + # Style the table + table.auto_set_font_size(False) + table.set_fontsize(11) + table.scale(1, 2.8) + + # 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) + + # 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) + + if save_as_base64: + buf = io.BytesIO() + plt.savefig( + buf, + format="png", + bbox_inches="tight", + dpi=300, + facecolor="white", + pad_inches=0.1, + ) + plt.close(fig) + buf.seek(0) + return base64.b64encode(buf.read()).decode("utf-8") + else: + output_path = ( + self.charts_dir / f"hr_zones_table_{pd.Timestamp.now().timestamp()}.png" + ) + plt.savefig( + output_path, + bbox_inches="tight", + dpi=300, + facecolor="white", + pad_inches=0.1, + ) + plt.close(fig) + return str(output_path) + + def generate_resting_heart_rate_table( + self, + data: list[list], + columns: list[str], + cell_colors: list[list[str]] = None, + save_as_base64: bool = True, + ) -> str: + """ + Generate Resting Heart Rate table as an image with optimized sizing. + + Args: + data: List of rows (each row is a list of values) + columns: List of column headers + cell_colors: Optional matrix of cell colors + save_as_base64: If True, return base64 string + + Returns: + Base64 string or file path + """ + import io + + # Optimal sizing for RHR table (8 columns, 1 data row) + fig, ax = plt.subplots(figsize=(18, 2.5)) + ax.axis("off") + + # Even column widths + col_widths = [1.0 / len(columns)] * len(columns) + + # Create table + table = ax.table( + cellText=data, + colLabels=columns, + cellLoc="center", + loc="center", + colColours=["#4dd0e1"] * len(columns), + colWidths=col_widths, + ) + + # Style the table + table.auto_set_font_size(False) + table.set_fontsize(11) + table.scale(1, 3.0) + + # 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) + + # Style all cells + for (row, col), cell in table.get_celld().items(): + if row == 0: + cell.set_text_props(weight="bold", fontsize=12) + cell.set_edgecolor("#333333") + cell.set_linewidth(1.5) + else: + cell.set_edgecolor("#666666") + cell.set_linewidth(1.0) + cell.set_text_props(fontsize=10) + + if save_as_base64: + buf = io.BytesIO() + plt.savefig( + buf, + format="png", + bbox_inches="tight", + dpi=300, + facecolor="white", + pad_inches=0.1, + ) + plt.close(fig) + buf.seek(0) + return base64.b64encode(buf.read()).decode("utf-8") + else: + output_path = ( + self.charts_dir / f"rhr_table_{pd.Timestamp.now().timestamp()}.png" + ) + plt.savefig( + output_path, + bbox_inches="tight", + dpi=300, + facecolor="white", + pad_inches=0.1, ) - plt.savefig(output_path, bbox_inches="tight", dpi=300) plt.close(fig) return str(output_path) diff --git a/app/services/report_generator.py b/app/services/report_generator.py index 2588901..55b5498 100644 --- a/app/services/report_generator.py +++ b/app/services/report_generator.py @@ -260,6 +260,13 @@ class ReportGeneratorService: .chart-large {{ max-height: 500px !important; }} + .table-image {{ + max-height: none !important; + width: auto !important; + max-width: 100% !important; + height: auto !important; + object-fit: contain; + }}