Little progress
This commit is contained in:
@@ -131,7 +131,7 @@
|
|||||||
<img
|
<img
|
||||||
src="data:image/png;base64, {{ rhr_table }}"
|
src="data:image/png;base64, {{ rhr_table }}"
|
||||||
alt="Resting Heart Rate Table"
|
alt="Resting Heart Rate Table"
|
||||||
class="w-full max-w-4xl h-auto object-contain"
|
class="table-image"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
<img
|
<img
|
||||||
src="data:image/png;base64, {{ vo2_max_table }}"
|
src="data:image/png;base64, {{ vo2_max_table }}"
|
||||||
alt="VO2 Max Table"
|
alt="VO2 Max Table"
|
||||||
class="w-full max-w-4xl h-auto"
|
class="table-image"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
<img
|
<img
|
||||||
src="data:image/png;base64, {{ hr_zones_table }}"
|
src="data:image/png;base64, {{ hr_zones_table }}"
|
||||||
alt="Heart Rate Zones Table"
|
alt="Heart Rate Zones Table"
|
||||||
class="w-full max-w-4xl h-auto"
|
class="table-image"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1163,13 +1163,14 @@ class ContextGenerator:
|
|||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
contexts["page_8"]["vo2_max_table"] = graph_generator.generate_table_image(
|
contexts["page_8"]["vo2_max_table"] = (
|
||||||
|
graph_generator.generate_vo2_max_table(
|
||||||
data=vo2_max_data,
|
data=vo2_max_data,
|
||||||
columns=vo2_max_columns,
|
columns=vo2_max_columns,
|
||||||
cell_colors=vo2_max_colors,
|
cell_colors=vo2_max_colors,
|
||||||
header_color="#4dd0e1",
|
|
||||||
save_as_base64=True,
|
save_as_base64=True,
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# Calculate zone metrics for the table
|
# Calculate zone metrics for the table
|
||||||
zone_metrics = self._calculate_zone_metrics(pnoe_metrics)
|
zone_metrics = self._calculate_zone_metrics(pnoe_metrics)
|
||||||
@@ -1211,11 +1212,10 @@ class ContextGenerator:
|
|||||||
]
|
]
|
||||||
|
|
||||||
contexts["page_8"]["hr_zones_table"] = (
|
contexts["page_8"]["hr_zones_table"] = (
|
||||||
graph_generator.generate_table_image(
|
graph_generator.generate_heart_rate_zones_table(
|
||||||
data=hr_zones_data,
|
data=hr_zones_data,
|
||||||
columns=hr_zones_columns,
|
columns=hr_zones_columns,
|
||||||
cell_colors=hr_zones_colors,
|
cell_colors=hr_zones_colors,
|
||||||
header_color="#4dd0e1",
|
|
||||||
save_as_base64=True,
|
save_as_base64=True,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -1288,13 +1288,14 @@ class ContextGenerator:
|
|||||||
]
|
]
|
||||||
rhr_colors = [["#b2ebf2"] + ["#f5f5f5"] * 7]
|
rhr_colors = [["#b2ebf2"] + ["#f5f5f5"] * 7]
|
||||||
|
|
||||||
contexts["page_11"]["rhr_table"] = graph_generator.generate_table_image(
|
contexts["page_11"]["rhr_table"] = (
|
||||||
|
graph_generator.generate_resting_heart_rate_table(
|
||||||
data=rhr_data,
|
data=rhr_data,
|
||||||
columns=rhr_columns,
|
columns=rhr_columns,
|
||||||
cell_colors=rhr_colors,
|
cell_colors=rhr_colors,
|
||||||
header_color="#4dd0e1",
|
|
||||||
save_as_base64=True,
|
save_as_base64=True,
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# Pages 12-17
|
# Pages 12-17
|
||||||
for i in range(6):
|
for i in range(6):
|
||||||
|
|||||||
+229
-32
@@ -1306,41 +1306,33 @@ class GraphGenerator:
|
|||||||
|
|
||||||
return self._image_to_base64(chart_path) if save_as_base64 else str(chart_path)
|
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,
|
self,
|
||||||
data: list[list],
|
data: list[list],
|
||||||
columns: list[str],
|
columns: list[str],
|
||||||
title: str = None,
|
|
||||||
col_widths: list[float] = None,
|
|
||||||
cell_colors: list[list[str]] = None,
|
cell_colors: list[list[str]] = None,
|
||||||
header_color: str = "#4dd0e1",
|
|
||||||
save_as_base64: bool = True,
|
save_as_base64: bool = True,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Generate a table as an image.
|
Generate VO2 Max table as an image with optimized sizing.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
data: List of rows (each row is a list of values)
|
data: List of rows (each row is a list of values)
|
||||||
columns: List of column headers
|
columns: List of column headers
|
||||||
title: Optional title for the table
|
cell_colors: Optional matrix of cell colors
|
||||||
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
|
|
||||||
save_as_base64: If True, return base64 string
|
save_as_base64: If True, return base64 string
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Base64 string or file path
|
Base64 string or file path
|
||||||
"""
|
"""
|
||||||
# Calculate figure size based on rows and columns
|
import io
|
||||||
# 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
|
|
||||||
|
|
||||||
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")
|
ax.axis("off")
|
||||||
|
|
||||||
if title:
|
# Even column widths for VO2 Max table
|
||||||
plt.title(title, pad=20, fontsize=14, fontweight="bold")
|
col_widths = [1.0 / len(columns)] * len(columns)
|
||||||
|
|
||||||
# Create table
|
# Create table
|
||||||
table = ax.table(
|
table = ax.table(
|
||||||
@@ -1348,43 +1340,248 @@ class GraphGenerator:
|
|||||||
colLabels=columns,
|
colLabels=columns,
|
||||||
cellLoc="center",
|
cellLoc="center",
|
||||||
loc="center",
|
loc="center",
|
||||||
colColours=[header_color] * len(columns),
|
colColours=["#4dd0e1"] * len(columns),
|
||||||
|
colWidths=col_widths,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Style the table
|
# Style the table
|
||||||
table.auto_set_font_size(False)
|
table.auto_set_font_size(False)
|
||||||
table.set_fontsize(10)
|
table.set_fontsize(11)
|
||||||
table.scale(1, 1.5) # Increase row height
|
table.scale(1, 3.0)
|
||||||
|
|
||||||
# Apply cell colors if provided
|
# Apply cell colors
|
||||||
if cell_colors:
|
if cell_colors:
|
||||||
for i, row_colors in enumerate(cell_colors):
|
for i, row_colors in enumerate(cell_colors):
|
||||||
for j, color in enumerate(row_colors):
|
for j, color in enumerate(row_colors):
|
||||||
if color:
|
if color and j < len(columns):
|
||||||
# (row_idx, col_idx) - row_idx starts at 1 for data (0 is header)
|
|
||||||
cell = table[(i + 1, j)]
|
cell = table[(i + 1, j)]
|
||||||
cell.set_facecolor(color)
|
cell.set_facecolor(color)
|
||||||
|
|
||||||
# Bold headers
|
# Style all cells
|
||||||
for (row, col), cell in table.get_celld().items():
|
for (row, col), cell in table.get_celld().items():
|
||||||
if row == 0:
|
if row == 0:
|
||||||
cell.set_text_props(weight="bold")
|
cell.set_text_props(weight="bold", fontsize=12)
|
||||||
cell.set_height(0.1)
|
cell.set_edgecolor("#333333")
|
||||||
|
cell.set_linewidth(1.5)
|
||||||
plt.tight_layout()
|
else:
|
||||||
|
cell.set_edgecolor("#666666")
|
||||||
|
cell.set_linewidth(1.0)
|
||||||
|
cell.set_text_props(fontsize=10)
|
||||||
|
|
||||||
if save_as_base64:
|
if save_as_base64:
|
||||||
import io
|
|
||||||
|
|
||||||
buf = io.BytesIO()
|
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)
|
plt.close(fig)
|
||||||
buf.seek(0)
|
buf.seek(0)
|
||||||
return base64.b64encode(buf.read()).decode("utf-8")
|
return base64.b64encode(buf.read()).decode("utf-8")
|
||||||
else:
|
else:
|
||||||
output_path = (
|
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)
|
plt.close(fig)
|
||||||
return str(output_path)
|
return str(output_path)
|
||||||
|
|||||||
@@ -260,6 +260,13 @@ class ReportGeneratorService:
|
|||||||
.chart-large {{
|
.chart-large {{
|
||||||
max-height: 500px !important;
|
max-height: 500px !important;
|
||||||
}}
|
}}
|
||||||
|
.table-image {{
|
||||||
|
max-height: none !important;
|
||||||
|
width: auto !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
height: auto !important;
|
||||||
|
object-fit: contain;
|
||||||
|
}}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="m-0 p-0">
|
<body class="m-0 p-0">
|
||||||
|
|||||||
Reference in New Issue
Block a user