From 3637cf5af89a248a6659f769154a3ef3688fa965 Mon Sep 17 00:00:00 2001 From: serversdown Date: Thu, 5 Mar 2026 06:56:44 +0000 Subject: [PATCH] Feat: Chart preview function now working. fix: chart formating and styling tweaks to match typical reports. --- backend/routers/projects.py | 235 +++++++++++++++++++++++------------- 1 file changed, 152 insertions(+), 83 deletions(-) diff --git a/backend/routers/projects.py b/backend/routers/projects.py index 5d538be..9a07420 100644 --- a/backend/routers/projects.py +++ b/backend/routers/projects.py @@ -1928,8 +1928,13 @@ async def generate_excel_report( ws.title = "Sound Level Data" # Define styles - title_font = Font(bold=True, size=14) - header_font = Font(bold=True, size=10) + title_font = Font(name='Arial', bold=True, size=12) + subtitle_font = Font(name='Arial', bold=True, size=12) + client_font = Font(name='Arial', italic=True, size=10) + filter_font = Font(name='Arial', italic=True, size=10, color="666666") + header_font = Font(name='Arial', bold=True, size=10) + data_font = Font(name='Arial', size=10) + summary_title_font = Font(name='Arial', bold=True, size=12) thin_border = Border( left=Side(style='thin'), right=Side(style='thin'), @@ -1937,6 +1942,9 @@ async def generate_excel_report( bottom=Side(style='thin') ) header_fill = PatternFill(start_color="DAEEF3", end_color="DAEEF3", fill_type="solid") + center_align = Alignment(horizontal='center', vertical='center') + left_align = Alignment(horizontal='left', vertical='center') + right_align = Alignment(horizontal='right', vertical='center') # Row 1: Report title final_project_name = project_name if project_name else (project.name if project else "") @@ -1945,12 +1953,17 @@ async def generate_excel_report( final_title = f"{report_title} - {final_project_name}" ws['A1'] = final_title ws['A1'].font = title_font + ws['A1'].alignment = center_align ws.merge_cells('A1:G1') + ws.row_dimensions[1].height = 20 # Row 2: Client name (if provided) if client_name: ws['A2'] = f"Client: {client_name}" - ws['A2'].font = Font(italic=True, size=10) + ws['A2'].font = client_font + ws['A2'].alignment = center_align + ws.merge_cells('A2:G2') + ws.row_dimensions[2].height = 16 # Row 3: Location name final_location = location_name @@ -1958,7 +1971,10 @@ async def generate_excel_report( final_location = location.name if final_location: ws['A3'] = final_location - ws['A3'].font = Font(bold=True, size=11) + ws['A3'].font = subtitle_font + ws['A3'].alignment = center_align + ws.merge_cells('A3:G3') + ws.row_dimensions[3].height = 20 # Row 4: Time filter info (if applied) if start_time and end_time: @@ -1967,7 +1983,10 @@ async def generate_excel_report( filter_info += f" | Date Range: {start_date or 'start'} to {end_date or 'end'}" filter_info += f" | {len(rnd_rows)} of {original_count} rows" ws['A4'] = filter_info - ws['A4'].font = Font(italic=True, size=9, color="666666") + ws['A4'].font = filter_font + ws['A4'].alignment = center_align + ws.merge_cells('A4:G4') + ws.row_dimensions[4].height = 14 # Row 7: Headers headers = ['Test Increment #', 'Date', 'Time', 'LAmax (dBA)', 'LA01 (dBA)', 'LA10 (dBA)', 'Comments'] @@ -1976,10 +1995,11 @@ async def generate_excel_report( cell.font = header_font cell.border = thin_border cell.fill = header_fill - cell.alignment = Alignment(horizontal='center') + cell.alignment = center_align + ws.row_dimensions[7].height = 16 - # Set column widths - column_widths = [16, 12, 10, 12, 12, 12, 40] + # Set column widths — A-G sized to fit one printed page width + column_widths = [12, 11, 9, 11, 11, 11, 20] for i, width in enumerate(column_widths, 1): ws.column_dimensions[get_column_letter(i)].width = width @@ -1989,41 +2009,34 @@ async def generate_excel_report( data_row = data_start_row + idx - 1 # Test Increment # - ws.cell(row=data_row, column=1, value=idx).border = thin_border + c = ws.cell(row=data_row, column=1, value=idx) + c.border = thin_border; c.font = data_font; c.alignment = center_align # Parse the Start Time to get Date and Time start_time_str = row.get('Start Time', '') if start_time_str: try: - # Format: "2025/12/26 20:23:38" dt = datetime.strptime(start_time_str, '%Y/%m/%d %H:%M:%S') - ws.cell(row=data_row, column=2, value=dt.date()) - ws.cell(row=data_row, column=3, value=dt.time()) + c2 = ws.cell(row=data_row, column=2, value=dt.strftime('%m/%d/%y')) + c3 = ws.cell(row=data_row, column=3, value=dt.strftime('%H:%M')) except ValueError: - ws.cell(row=data_row, column=2, value=start_time_str) - ws.cell(row=data_row, column=3, value='') + c2 = ws.cell(row=data_row, column=2, value=start_time_str) + c3 = ws.cell(row=data_row, column=3, value='') else: - ws.cell(row=data_row, column=2, value='') - ws.cell(row=data_row, column=3, value='') + c2 = ws.cell(row=data_row, column=2, value='') + c3 = ws.cell(row=data_row, column=3, value='') + for c in (c2, c3): + c.border = thin_border; c.font = data_font; c.alignment = center_align - # LAmax - from Lmax(Main) - lmax = row.get('Lmax(Main)') - ws.cell(row=data_row, column=4, value=lmax if lmax else '').border = thin_border + # LAmax, LA01, LA10 + for col_idx, key in [(4, 'Lmax(Main)'), (5, 'LN1(Main)'), (6, 'LN2(Main)')]: + val = row.get(key) + c = ws.cell(row=data_row, column=col_idx, value=val if val else '') + c.border = thin_border; c.font = data_font; c.alignment = right_align - # LA01 - from LN1(Main) - ln1 = row.get('LN1(Main)') - ws.cell(row=data_row, column=5, value=ln1 if ln1 else '').border = thin_border - - # LA10 - from LN2(Main) - ln2 = row.get('LN2(Main)') - ws.cell(row=data_row, column=6, value=ln2 if ln2 else '').border = thin_border - - # Comments (empty for now, can be populated) - ws.cell(row=data_row, column=7, value='').border = thin_border - - # Apply borders to date/time cells - ws.cell(row=data_row, column=2).border = thin_border - ws.cell(row=data_row, column=3).border = thin_border + # Comments + c = ws.cell(row=data_row, column=7, value='') + c.border = thin_border; c.font = data_font; c.alignment = left_align data_end_row = data_start_row + len(rnd_rows) - 1 @@ -2032,13 +2045,13 @@ async def generate_excel_report( chart.title = f"{final_location or 'Sound Level Data'} - Background Noise Study" chart.style = 10 chart.y_axis.title = "Sound Level (dBA)" - chart.x_axis.title = "Test Increment" - chart.height = 12 - chart.width = 20 + chart.x_axis.title = "Time" + chart.height = 18 + chart.width = 22 # Data references (LAmax, LA01, LA10 are columns D, E, F) data_ref = Reference(ws, min_col=4, min_row=7, max_col=6, max_row=data_end_row) - categories = Reference(ws, min_col=1, min_row=data_start_row, max_row=data_end_row) + categories = Reference(ws, min_col=3, min_row=data_start_row, max_row=data_end_row) chart.add_data(data_ref, titles_from_data=True) chart.set_categories(categories) @@ -2049,12 +2062,20 @@ async def generate_excel_report( chart.series[1].graphicalProperties.line.solidFill = "00B050" # LA01 - Green chart.series[2].graphicalProperties.line.solidFill = "0070C0" # LA10 - Blue - # Position chart to the right of data - ws.add_chart(chart, "I3") + # Position chart starting at column H + ws.add_chart(chart, "H3") + + # Print layout: A-G fits one page width, landscape + from openpyxl.worksheet.properties import PageSetupProperties + ws.sheet_properties.pageSetUpPr = PageSetupProperties(fitToPage=True) + ws.page_setup.orientation = 'landscape' + ws.page_setup.fitToWidth = 1 + ws.page_setup.fitToHeight = 0 # Add summary statistics section below the data summary_row = data_end_row + 3 - ws.cell(row=summary_row, column=1, value="Summary Statistics").font = Font(bold=True, size=12) + c = ws.cell(row=summary_row, column=1, value="Summary Statistics") + c.font = summary_title_font # Calculate time-period statistics time_periods = { @@ -2097,43 +2118,45 @@ async def generate_excel_report( cell.font = header_font cell.fill = header_fill cell.border = thin_border + cell.alignment = center_align # Summary data summary_row += 1 for period_name, samples in time_periods.items(): - ws.cell(row=summary_row, column=1, value=period_name).border = thin_border - ws.cell(row=summary_row, column=2, value=len(samples)).border = thin_border + c = ws.cell(row=summary_row, column=1, value=period_name) + c.border = thin_border; c.font = data_font; c.alignment = left_align + c = ws.cell(row=summary_row, column=2, value=len(samples)) + c.border = thin_border; c.font = data_font; c.alignment = center_align if samples: avg_lmax = sum(s['lmax'] for s in samples) / len(samples) avg_ln1 = sum(s['ln1'] for s in samples) / len(samples) avg_ln2 = sum(s['ln2'] for s in samples) / len(samples) - ws.cell(row=summary_row, column=3, value=round(avg_lmax, 1)).border = thin_border - ws.cell(row=summary_row, column=4, value=round(avg_ln1, 1)).border = thin_border - ws.cell(row=summary_row, column=5, value=round(avg_ln2, 1)).border = thin_border + for col_idx, val in [(3, avg_lmax), (4, avg_ln1), (5, avg_ln2)]: + c = ws.cell(row=summary_row, column=col_idx, value=round(val, 1)) + c.border = thin_border; c.font = data_font; c.alignment = right_align else: - ws.cell(row=summary_row, column=3, value='-').border = thin_border - ws.cell(row=summary_row, column=4, value='-').border = thin_border - ws.cell(row=summary_row, column=5, value='-').border = thin_border + for col_idx in [3, 4, 5]: + c = ws.cell(row=summary_row, column=col_idx, value='-') + c.border = thin_border; c.font = data_font; c.alignment = center_align summary_row += 1 # Overall summary summary_row += 1 - ws.cell(row=summary_row, column=1, value='Overall').font = Font(bold=True) - ws.cell(row=summary_row, column=1).border = thin_border - ws.cell(row=summary_row, column=2, value=len(rnd_rows)).border = thin_border + c = ws.cell(row=summary_row, column=1, value='Overall') + c.font = Font(name='Arial', bold=True, size=10); c.border = thin_border; c.alignment = left_align + c = ws.cell(row=summary_row, column=2, value=len(rnd_rows)) + c.border = thin_border; c.font = data_font; c.alignment = center_align all_lmax = [r.get('Lmax(Main)') for r in rnd_rows if isinstance(r.get('Lmax(Main)'), (int, float))] all_ln1 = [r.get('LN1(Main)') for r in rnd_rows if isinstance(r.get('LN1(Main)'), (int, float))] all_ln2 = [r.get('LN2(Main)') for r in rnd_rows if isinstance(r.get('LN2(Main)'), (int, float))] - if all_lmax: - ws.cell(row=summary_row, column=3, value=round(sum(all_lmax) / len(all_lmax), 1)).border = thin_border - if all_ln1: - ws.cell(row=summary_row, column=4, value=round(sum(all_ln1) / len(all_ln1), 1)).border = thin_border - if all_ln2: - ws.cell(row=summary_row, column=5, value=round(sum(all_ln2) / len(all_ln2), 1)).border = thin_border + for col_idx, vals in [(3, all_lmax), (4, all_ln1), (5, all_ln2)]: + if vals: + c = ws.cell(row=summary_row, column=col_idx, value=round(sum(vals) / len(vals), 1)) + c.border = thin_border; c.font = data_font; c.alignment = right_align # Save to buffer output = io.BytesIO() @@ -2344,7 +2367,7 @@ async def preview_report_data( final_location = location_name if location_name else (location.name if location else "") # Get templates for the dropdown - templates = db.query(ReportTemplate).all() + report_templates = db.query(ReportTemplate).all() return templates.TemplateResponse("report_preview.html", { "request": request, @@ -2364,7 +2387,7 @@ async def preview_report_data( "end_date": end_date, "original_count": original_count, "filtered_count": len(rnd_rows), - "templates": templates, + "templates": report_templates, }) @@ -2419,8 +2442,12 @@ async def generate_report_from_preview( ws.title = "Sound Level Data" # Styles - title_font = Font(bold=True, size=14) - header_font = Font(bold=True, size=10) + title_font = Font(name='Arial', bold=True, size=12) + subtitle_font = Font(name='Arial', bold=True, size=12) + client_font = Font(name='Arial', italic=True, size=10) + filter_font = Font(name='Arial', italic=True, size=10, color="666666") + header_font = Font(name='Arial', bold=True, size=10) + data_font = Font(name='Arial', size=10) thin_border = Border( left=Side(style='thin'), right=Side(style='thin'), @@ -2428,27 +2455,41 @@ async def generate_report_from_preview( bottom=Side(style='thin') ) header_fill = PatternFill(start_color="DAEEF3", end_color="DAEEF3", fill_type="solid") + center_align = Alignment(horizontal='center', vertical='center') + left_align = Alignment(horizontal='left', vertical='center') + right_align = Alignment(horizontal='right', vertical='center') # Row 1: Title final_title = f"{report_title} - {project_name}" if project_name else report_title ws['A1'] = final_title ws['A1'].font = title_font + ws['A1'].alignment = center_align ws.merge_cells('A1:G1') + ws.row_dimensions[1].height = 20 # Row 2: Client if client_name: ws['A2'] = f"Client: {client_name}" - ws['A2'].font = Font(italic=True, size=10) + ws['A2'].font = client_font + ws['A2'].alignment = center_align + ws.merge_cells('A2:G2') + ws.row_dimensions[2].height = 16 # Row 3: Location if location_name: ws['A3'] = location_name - ws['A3'].font = Font(bold=True, size=11) + ws['A3'].font = subtitle_font + ws['A3'].alignment = center_align + ws.merge_cells('A3:G3') + ws.row_dimensions[3].height = 20 # Row 4: Time filter info if time_filter: ws['A4'] = time_filter - ws['A4'].font = Font(italic=True, size=9, color="666666") + ws['A4'].font = filter_font + ws['A4'].alignment = center_align + ws.merge_cells('A4:G4') + ws.row_dimensions[4].height = 14 # Row 7: Headers headers = ['Test Increment #', 'Date', 'Time', 'LAmax (dBA)', 'LA01 (dBA)', 'LA10 (dBA)', 'Comments'] @@ -2457,20 +2498,24 @@ async def generate_report_from_preview( cell.font = header_font cell.border = thin_border cell.fill = header_fill - cell.alignment = Alignment(horizontal='center') + cell.alignment = center_align + ws.row_dimensions[7].height = 16 - # Column widths - column_widths = [16, 12, 10, 12, 12, 12, 40] + # Column widths — A-G sized to fit one printed page width + column_widths = [12, 11, 9, 11, 11, 11, 20] for i, width in enumerate(column_widths, 1): ws.column_dimensions[get_column_letter(i)].width = width # Data rows data_start_row = 8 + col_aligns = [center_align, center_align, center_align, right_align, right_align, right_align, left_align] for idx, row_data in enumerate(spreadsheet_data): data_row = data_start_row + idx for col, value in enumerate(row_data, 1): cell = ws.cell(row=data_row, column=col, value=value if value != '' else None) cell.border = thin_border + cell.font = data_font + cell.alignment = col_aligns[col - 1] if col <= len(col_aligns) else left_align data_end_row = data_start_row + len(spreadsheet_data) - 1 @@ -2480,12 +2525,12 @@ async def generate_report_from_preview( chart.title = f"{location_name or 'Sound Level Data'} - Background Noise Study" chart.style = 10 chart.y_axis.title = "Sound Level (dBA)" - chart.x_axis.title = "Test Increment" - chart.height = 12 - chart.width = 20 + chart.x_axis.title = "Time" + chart.height = 18 + chart.width = 22 data_ref = Reference(ws, min_col=4, min_row=7, max_col=6, max_row=data_end_row) - categories = Reference(ws, min_col=1, min_row=data_start_row, max_row=data_end_row) + categories = Reference(ws, min_col=3, min_row=data_start_row, max_row=data_end_row) chart.add_data(data_ref, titles_from_data=True) chart.set_categories(categories) @@ -2495,7 +2540,14 @@ async def generate_report_from_preview( chart.series[1].graphicalProperties.line.solidFill = "00B050" chart.series[2].graphicalProperties.line.solidFill = "0070C0" - ws.add_chart(chart, "I3") + ws.add_chart(chart, "H3") + + # Print layout: A-G fits one page width, landscape + from openpyxl.worksheet.properties import PageSetupProperties + ws.sheet_properties.pageSetUpPr = PageSetupProperties(fitToPage=True) + ws.page_setup.orientation = 'landscape' + ws.page_setup.fitToWidth = 1 + ws.page_setup.fitToHeight = 0 # Save to buffer output = io.BytesIO() @@ -2584,8 +2636,9 @@ async def generate_combined_excel_report( raise HTTPException(status_code=404, detail="No Leq measurement files found in project. Reports require Leq data (files with '_Leq_' in the name).") # Define styles - title_font = Font(bold=True, size=14) - header_font = Font(bold=True, size=10) + title_font = Font(name='Arial', bold=True, size=12) + header_font = Font(name='Arial', bold=True, size=10) + data_font = Font(name='Arial', size=10) thin_border = Border( left=Side(style='thin'), right=Side(style='thin'), @@ -2593,6 +2646,9 @@ async def generate_combined_excel_report( bottom=Side(style='thin') ) header_fill = PatternFill(start_color="DAEEF3", end_color="DAEEF3", fill_type="solid") + center_align = Alignment(horizontal='center', vertical='center') + left_align = Alignment(horizontal='left', vertical='center') + right_align = Alignment(horizontal='right', vertical='center') # Create Excel workbook wb = openpyxl.Workbook() @@ -2613,11 +2669,16 @@ async def generate_combined_excel_report( final_title = f"{report_title} - {project.name}" ws['A1'] = final_title ws['A1'].font = title_font + ws['A1'].alignment = center_align ws.merge_cells('A1:G1') + ws.row_dimensions[1].height = 20 # Row 3: Location name ws['A3'] = location_name - ws['A3'].font = Font(bold=True, size=11) + ws['A3'].font = Font(name='Arial', bold=True, size=12) + ws['A3'].alignment = center_align + ws.merge_cells('A3:G3') + ws.row_dimensions[3].height = 20 # Row 7: Headers headers = ['Test Increment #', 'Date', 'Time', 'LAmax (dBA)', 'LA01 (dBA)', 'LA10 (dBA)', 'Comments'] @@ -2626,10 +2687,11 @@ async def generate_combined_excel_report( cell.font = header_font cell.border = thin_border cell.fill = header_fill - cell.alignment = Alignment(horizontal='center') + cell.alignment = center_align + ws.row_dimensions[7].height = 16 - # Set column widths - column_widths = [16, 12, 10, 12, 12, 12, 40] + # Set column widths — A-G sized to fit one printed page width + column_widths = [12, 11, 9, 11, 11, 11, 20] for i, width in enumerate(column_widths, 1): ws.column_dimensions[get_column_letter(i)].width = width @@ -2712,12 +2774,12 @@ async def generate_combined_excel_report( chart.title = f"{location_name}" chart.style = 10 chart.y_axis.title = "Sound Level (dBA)" - chart.x_axis.title = "Test Increment" - chart.height = 12 - chart.width = 20 + chart.x_axis.title = "Time" + chart.height = 18 + chart.width = 22 data_ref = Reference(ws, min_col=4, min_row=7, max_col=6, max_row=data_end_row) - categories = Reference(ws, min_col=1, min_row=data_start_row, max_row=data_end_row) + categories = Reference(ws, min_col=3, min_row=data_start_row, max_row=data_end_row) chart.add_data(data_ref, titles_from_data=True) chart.set_categories(categories) @@ -2727,7 +2789,14 @@ async def generate_combined_excel_report( chart.series[1].graphicalProperties.line.solidFill = "00B050" chart.series[2].graphicalProperties.line.solidFill = "0070C0" - ws.add_chart(chart, "I3") + ws.add_chart(chart, "H3") + + # Print layout: A-G fits one page width, landscape + from openpyxl.worksheet.properties import PageSetupProperties + ws.sheet_properties.pageSetUpPr = PageSetupProperties(fitToPage=True) + ws.page_setup.orientation = 'landscape' + ws.page_setup.fitToWidth = 1 + ws.page_setup.fitToHeight = 0 # Calculate summary for this location all_lmax = [r.get('Lmax(Main)') for r in all_rnd_rows if isinstance(r.get('Lmax(Main)'), (int, float))]