From b4cc276fa4618ce58d3a9a2d8b57baeb5f795fe6 Mon Sep 17 00:00:00 2001 From: uberfastman <4575707+uberfastman@users.noreply.github.com> Date: Sat, 12 Oct 2024 14:31:23 -0400 Subject: [PATCH] updated table of contents and fixed toc breaking when it extended beyond one page --- compose.yaml | 2 +- report/pdf/generator.py | 349 ++++++++++++++++++---------------------- utilities/settings.py | 8 +- 3 files changed, 164 insertions(+), 195 deletions(-) diff --git a/compose.yaml b/compose.yaml index 30a9ec2..50b7ed8 100644 --- a/compose.yaml +++ b/compose.yaml @@ -2,7 +2,7 @@ services: app: - image: ghcr.io/uberfastman/fantasy-football-metrics-weekly-report:19.0.3 + image: ghcr.io/uberfastman/fantasy-football-metrics-weekly-report:19.0.4 platform: linux/amd64 ports: - "5001:5000" diff --git a/report/pdf/generator.py b/report/pdf/generator.py index bc12cfa..e14ca4e 100644 --- a/report/pdf/generator.py +++ b/report/pdf/generator.py @@ -9,7 +9,7 @@ from copy import deepcopy from pathlib import Path from random import choice -from typing import List, Dict, Tuple, Callable, Any, Union, Optional +from typing import List, Dict, Tuple, Any, Union, Optional from urllib.error import URLError from PIL import Image @@ -544,7 +544,7 @@ def __init__(self, season: int, league: BaseLeague, playoff_prob_sims: int, ) # table of contents - self.toc = TableOfContents(self.font, self.font_size, self.break_ties) + self.toc = TableOfContents(self.font, self.font_size) # appendix self.appendix = Appendix( @@ -643,11 +643,11 @@ def set_tied_values_style(self, num_ties: int, table_style_list: List[Tuple[Any] return TableStyle(tied_values_table_style_list) - def create_section(self, title_text: str, headers: List[List[str]], data: Any, + def create_section(self, title_text: str, toc_section_key: str, headers: List[List[str]], data: Any, table_style: TableStyle, table_style_ties: Union[TableStyle, None], col_widths: List[float], subtitle_text: Union[str, List[str]] = None, subsubtitle_text: Union[str, List[str]] = None, header_text: str = None, footer_text: str = None, row_heights: List[List[float]] = None, - tied_metric: bool = False, metric_type: str = None, section_title_function: Callable = None, + tied_metric: bool = False, metric_type: str = None, sesqui_max_chars_col_ndxs: Optional[List[int]] = None) -> KeepTogether: logger.debug( f"Creating report section: \"{title_text if title_text else metric_type}\" with " @@ -673,10 +673,17 @@ def create_section(self, title_text: str, headers: List[List[str]], data: Any, subtitle_text=subtitle_text, subsubtitle_text=subsubtitle_text ) - if section_title_function: - section_title_function(title_text) - else: - self.toc.add_metric_section(title_text) + + self.toc.add_toc_entry( + title_text, + toc_section_key, + color=( + "green" if self.break_ties and ( + title == "Team Score Rankings" + or title == "Team Coaching Efficiency Rankings" + ) else None + ) + ) if metric_type == "standings": font_reduction = 0 @@ -1173,9 +1180,9 @@ def create_team_stats_pages(self, doc_elements: List[Flowable], weekly_team_data ) if has_team_tables_page: - self.toc.add_team_section(team_result.name, team_page=1) + self.toc.add_toc_entry(team_result.name, "teams", truncate_title=True, team_page=1) else: - self.toc.add_team_section(team_result.name) + self.toc.add_toc_entry(team_result.name, "teams", truncate_title=True) doc_elements.append(title) @@ -1332,9 +1339,9 @@ def create_team_stats_pages(self, doc_elements: List[Flowable], weekly_team_data ) if has_team_graphics_page: - self.toc.add_team_section(team_result.name, team_page=2) + self.toc.add_toc_entry(team_result.name, "teams", truncate_title=True, team_page=2) else: - self.toc.add_team_section(team_result.name) + self.toc.add_toc_entry(team_result.name, "teams", truncate_title=True) doc_elements.append(title) @@ -1496,6 +1503,7 @@ def generate_pdf(self, filename_with_path: Path, line_chart_data_list: List[List division_table = self.create_section( table_title, + "metrics", self.standings_headers, [team[:-1] for team in division], standings_style, @@ -1516,6 +1524,7 @@ def generate_pdf(self, filename_with_path: Path, line_chart_data_list: List[List else: standings = self.create_section( "League Standings", + "metrics", self.standings_headers, self.report_data.data_for_current_standings, standings_style, @@ -1595,6 +1604,7 @@ def generate_pdf(self, filename_with_path: Path, line_chart_data_list: List[List elements.append(self.create_section( "Playoff Probabilities", + "metrics", self.playoff_probs_headers, data_for_playoff_probs, playoff_probs_style, @@ -1621,6 +1631,7 @@ def generate_pdf(self, filename_with_path: Path, line_chart_data_list: List[List # median standings elements.append(self.create_section( "League Median Matchup Standings", + "metrics", self.median_standings_headers, [team[:-1] for team in self.data_for_median_standings], median_standings_style, @@ -1639,6 +1650,7 @@ def generate_pdf(self, filename_with_path: Path, line_chart_data_list: List[List # power ranking elements.append(self.create_section( "Team Power Rankings", + "metrics", self.power_ranking_headers, self.data_for_power_rankings, self.style, @@ -1655,6 +1667,7 @@ def generate_pdf(self, filename_with_path: Path, line_chart_data_list: List[List if self.data_for_z_scores: elements.append(self.create_section( "Team Z-Score Rankings", + "metrics", self.zscores_headers, self.data_for_z_scores, self.style, @@ -1676,6 +1689,7 @@ def generate_pdf(self, filename_with_path: Path, line_chart_data_list: List[List # scores elements.append(self.create_section( "Team Score Rankings", + "metrics", self.scores_headers, self.data_for_scores, self.style, @@ -1690,6 +1704,7 @@ def generate_pdf(self, filename_with_path: Path, line_chart_data_list: List[List # coaching efficiency elements.append(self.create_section( "Team Coaching Efficiency Rankings", + "metrics", self.efficiency_headers, self.data_for_coaching_efficiency, self.style, @@ -1705,6 +1720,7 @@ def generate_pdf(self, filename_with_path: Path, line_chart_data_list: List[List # luck elements.append(self.create_section( "Team Luck Rankings", + "metrics", self.luck_headers, self.data_for_luck, self.style, @@ -1724,6 +1740,7 @@ def generate_pdf(self, filename_with_path: Path, line_chart_data_list: List[List # optimal scores elements.append(self.create_section( "Team Optimal Score Rankings", + "metrics", self.optimal_scores_headers, self.data_for_optimal_scores, self.style, @@ -1737,6 +1754,7 @@ def generate_pdf(self, filename_with_path: Path, line_chart_data_list: List[List # bad boy rankings elements.append(self.create_section( "Bad Boy Rankings", + "metrics", self.bad_boy_headers, self.data_for_bad_boy_rankings, self.style, @@ -1751,6 +1769,7 @@ def generate_pdf(self, filename_with_path: Path, line_chart_data_list: List[List # beef rankings elements.append(self.create_section( "Beef Rankings", + "metrics", self.beef_headers, self.data_for_beef_rankings, self.style_left_align_right_col, @@ -1770,6 +1789,7 @@ def generate_pdf(self, filename_with_path: Path, line_chart_data_list: List[List # high roller rankings elements.append(self.create_section( "High Roller Rankings", + "metrics", self.high_roller_headers, self.data_for_high_roller_rankings, self.style, @@ -1796,13 +1816,13 @@ def generate_pdf(self, filename_with_path: Path, line_chart_data_list: List[List # weekly top scorers elements.append(self.create_section( "Weekly Top Scorers", + "top_performers", self.weekly_top_scorer_headers, self.data_for_season_weekly_top_scorers, self.style_no_highlight, self.style_no_highlight, self.widths_04_cols_no_1, - metric_type="top_scorers", - section_title_function=self.toc.add_top_performers_section + metric_type="top_scorers" )) elements.append(self.spacer_twentieth_inch) @@ -1816,13 +1836,13 @@ def generate_pdf(self, filename_with_path: Path, line_chart_data_list: List[List # weekly low scorers elements.append(self.create_section( "Weekly Low Scorers", + "top_performers", self.weekly_top_scorer_headers, self.data_for_season_weekly_low_scorers, self.style_no_highlight, self.style_no_highlight, self.widths_04_cols_no_1, - metric_type="top_scorers", - section_title_function=self.toc.add_top_performers_section + metric_type="top_scorers" )) elements.append(self.spacer_twentieth_inch) @@ -1836,13 +1856,13 @@ def generate_pdf(self, filename_with_path: Path, line_chart_data_list: List[List # weekly highest coaching efficiency elements.append(self.create_section( "Weekly Highest Coaching Efficiency", + "top_performers", self.weekly_highest_ce_headers, self.data_for_season_weekly_highest_ce, self.style_no_highlight, self.style_no_highlight, self.widths_04_cols_no_1, - metric_type="highest_ce", - section_title_function=self.toc.add_top_performers_section + metric_type="highest_ce" )) if (settings.report_settings.league_weekly_top_scorers_bool @@ -1850,7 +1870,7 @@ def generate_pdf(self, filename_with_path: Path, line_chart_data_list: List[List or settings.report_settings.league_weekly_highest_ce_bool): elements.append(self.add_page_break()) - if settings.report_settings.report_time_series_charts_bool: + if settings.report_settings.league_time_series_charts_bool: series_names = line_chart_data_list[0] points_data = line_chart_data_list[2] efficiency_data = line_chart_data_list[3] @@ -1874,7 +1894,7 @@ def generate_pdf(self, filename_with_path: Path, line_chart_data_list: List[List charts_time_series_page_title = self.create_title( "" + charts_time_series_page_title_str + "", element_type="chart", anchor="") - self.toc.add_chart_section(charts_time_series_page_title_str) + self.toc.add_toc_entry(charts_time_series_page_title_str, "charts") elements.append(charts_time_series_page_title) elements.append(KeepTogether( self.create_line_chart(points_data, len(points_data[0]), series_names, "Weekly Points", "Weeks", @@ -1891,14 +1911,13 @@ def generate_pdf(self, filename_with_path: Path, line_chart_data_list: List[List elements.append(self.spacer_tenth_inch) elements.append(self.add_page_break()) - if (settings.report_settings.report_team_stats_bool - and ( - settings.report_settings.team_points_by_position_charts_bool - or settings.report_settings.team_boom_or_bust_bool - or settings.report_settings.team_bad_boy_stats_bool - or settings.report_settings.team_beef_stats_bool - or settings.report_settings.team_high_roller_stats_bool - )): + if any([ + settings.report_settings.team_points_by_position_charts_bool, + settings.report_settings.team_boom_or_bust_bool, + settings.report_settings.team_bad_boy_stats_bool, + settings.report_settings.team_beef_stats_bool, + settings.report_settings.team_high_roller_stats_bool + ]): # dynamically build additional pages for individual team stats self.create_team_stats_pages( elements, self.data_for_weekly_points_by_position, self.data_for_season_average_team_points_by_position @@ -1910,7 +1929,11 @@ def generate_pdf(self, filename_with_path: Path, line_chart_data_list: List[List self.toc.add_appendix("Appendix I: Rankings & Metrics") # insert table of contents after report title and spacer - elements.insert(4, self.toc.get_toc()) + toc_elements = self.toc.get_toc() + toc_elements.reverse() # reverse toc tables so that they are inserted in the correct order + + for toc in toc_elements: + elements.insert(4, toc) elements.append(self.report_footer_title) elements.append(self.report_footer) @@ -1925,211 +1948,157 @@ def generate_pdf(self, filename_with_path: Path, line_chart_data_list: List[List class TableOfContents(object): - def __init__(self, font, font_size, break_ties): + def __init__(self, font, font_size): - self.toc_col_widths = [3.25 * inch, 2 * inch, 2.50 * inch] + self.toc_col_widths = [5.00 * inch, 0.75 * inch] - self.toc_line_height = 0.25 * inch + self.toc_line_height = 0.15 * inch self.toc_section_spacer_row_height = 0.05 * inch - self.toc_title_font_size = font_size - self.toc_font_size = font_size - 2 - # map scaled down TOC font size to number of ". " repetitions that should be inserted into center column - self.toc_dot_leaders_ref_dict = { - 4: 61, 5: 49, 6: 41, 7: 35, 8: 30, 9: 27, 10: 24 - } - self.toc_dot_leaders_scalar = self.toc_dot_leaders_ref_dict[self.toc_font_size] + self.toc_font_size = font_size - 4 - # style for page name titles - self.toc_style_title_left = ParagraphStyle( - name="tocl", alignment=TA_LEFT, fontSize=self.toc_title_font_size, fontName=font - ) - # style for page names column - self.toc_style_left = ParagraphStyle( + self.toc_section_header_style_left = ParagraphStyle( name="tocl", alignment=TA_LEFT, fontSize=self.toc_font_size, fontName=font ) - - # style for dot leaders - self.toc_style_center = ParagraphStyle( - name="tocc", alignment=TA_CENTER, fontSize=self.toc_font_size, fontName=font + self.toc_section_header_style_right = ParagraphStyle( + name="tocr", alignment=TA_RIGHT, fontSize=self.toc_font_size, fontName=font ) - # style for page number titles - self.toc_style_title_right = ParagraphStyle( - name="tocr", alignment=TA_RIGHT, fontSize=self.toc_title_font_size, fontName=font + self.toc_style_left = ParagraphStyle( + name="tocl", alignment=TA_LEFT, fontSize=self.toc_font_size, fontName=font ) - # style for page numbers self.toc_style_right = ParagraphStyle( name="tocr", alignment=TA_RIGHT, fontSize=self.toc_font_size, fontName=font ) self.toc_anchor = 0 - # start on page 2 since table of contents is on first two pages - self.toc_page = 2 - - self.toc_metric_section_data = None - self.toc_top_performers_section_data = None - self.toc_chart_section_data = None - self.toc_team_section_data = None - self.toc_appendix_data = None - - self.break_ties = break_ties - - if (settings.report_settings.league_standings_bool - or settings.report_settings.league_playoff_probs_bool - or settings.report_settings.league_median_standings_bool - or settings.report_settings.league_power_rankings_bool - or settings.report_settings.league_z_score_rankings_bool - or settings.report_settings.league_score_rankings_bool - or settings.report_settings.league_coaching_efficiency_rankings_bool - or settings.report_settings.league_luck_rankings_bool - or settings.report_settings.league_optimal_score_rankings_bool - or settings.report_settings.league_bad_boy_rankings_bool - or settings.report_settings.league_beef_rankings_bool): - self.toc_metric_section_data = [ - [Paragraph("Metrics", self.toc_style_title_right), - "", - Paragraph("Page", self.toc_style_title_left)] - ] - self.toc_appendix_data = [ - [Paragraph("Appendices", self.toc_style_title_right), - "", - Paragraph("Page", self.toc_style_title_left)] + # start on page 2 since table of contents is on first page + self.toc_page = 1 + + self.toc_entries_for_sections: Dict[str, List[List[Paragraph]]] = {} + + toc_entries_by_table = { + "metrics": [ + "league_standings_bool", + "league_playoff_probs_bool", + "league_median_standings_bool", + "league_power_rankings_bool", + "league_z_score_rankings_bool", + "league_score_rankings_bool", + "league_coaching_efficiency_rankings_bool", + "league_luck_rankings_bool", + "league_optimal_score_rankings_bool", + "league_bad_boy_rankings_bool", + "league_beef_rankings_bool", + "league_high_roller_rankings_bool" + ], + "top_performers": [ + "league_weekly_top_scorers_bool", + "league_weekly_low_scorers_bool", + "league_weekly_highest_ce_bool" + ], + "charts": [ + "league_time_series_charts_bool" + ], + "teams": [ + "team_points_by_position_charts_bool", + "team_boom_or_bust_bool", + "team_bad_boy_stats_bool", + "team_beef_stats_bool", + "team_high_roller_stats_bool" ] + } - if (settings.report_settings.league_weekly_top_scorers_bool - or settings.report_settings.league_weekly_low_scorers_bool - or settings.report_settings.league_weekly_highest_ce_bool): - self.toc_top_performers_section_data = [ - [Paragraph("Top Performers", self.toc_style_title_right), - "", - Paragraph("Page", self.toc_style_title_left)] - ] + for table_name, table_entries in toc_entries_by_table.items(): - if settings.report_settings.report_time_series_charts_bool: - self.toc_chart_section_data = [ - [Paragraph("Charts", self.toc_style_title_right), - "", - Paragraph("Page", self.toc_style_title_left)] - ] + has_table_feature = False + for table_entry in table_entries: + if getattr(settings.report_settings, table_entry): + has_table_feature = True - if (settings.report_settings.report_team_stats_bool - and ( - settings.report_settings.team_points_by_position_charts_bool - or settings.report_settings.team_bad_boy_stats_bool - or settings.report_settings.team_beef_stats_bool - or settings.report_settings.team_boom_or_bust_bool - )): - self.toc_team_section_data = [ - [Paragraph("Teams", self.toc_style_title_right), - "", - Paragraph("Page", self.toc_style_title_left)] - ] + if has_table_feature: + self.toc_entries_for_sections[table_name] = ( + self._format_toc_section_header(table_name.replace("_", " ").title()) + ) + + self.toc_entries_for_appendix_section = self._format_toc_section_header("Appendices") def add_toc_page(self, pages_to_add: int = 1) -> None: self.toc_page += pages_to_add - def format_toc_section(self, title: str, color: str = "blue") -> List[Paragraph]: + @staticmethod + def _format_toc_entry_section_key(title: str) -> str: + return title.replace(" ", "_").lower() + + @staticmethod + def _format_toc_entries_for_section_attribute(title: str) -> str: + return f"toc_entries_for_{title}_section" + + def _format_toc_section_header(self, header_text: str) -> List[List[Paragraph]]: + return [[ + Paragraph(f"{header_text}", self.toc_section_header_style_left), + # "", + Paragraph("Page", self.toc_section_header_style_right) + ]] + + def _format_toc_entry(self, text: str, color: str = "blue") -> List[Paragraph]: return [ Paragraph( - "" + title + "", - self.toc_style_right), - Paragraph(". " * self.toc_dot_leaders_scalar, self.toc_style_center), - Paragraph(str(self.toc_page), self.toc_style_left) + f"{text}", + self.toc_style_left + ), + Paragraph(str(self.toc_page), self.toc_style_right) ] - def add_metric_section(self, title: str) -> None: - if self.break_ties: - if title == "Team Score Rankings" or title == "Team Coaching Efficiency Rankings": - color = "green" - else: - color = "blue" - else: - color = "blue" - metric_section = self.format_toc_section(title, color) - self.toc_metric_section_data.append(metric_section) - self.toc_anchor += 1 - - def add_top_performers_section(self, title: str) -> None: - top_performers_section = self.format_toc_section(title) - self.toc_top_performers_section_data.append(top_performers_section) - self.toc_anchor += 1 + def add_toc_entry(self, title: str, section_key: str, color: Optional[str] = None, truncate_title: bool = False, + team_page: Optional[int] = None) -> None: - def add_chart_section(self, title: str) -> None: - chart_section = self.format_toc_section(title) - self.toc_chart_section_data.append(chart_section) - self.toc_anchor += 1 - - def add_team_section(self, team_name: str, team_page: Optional[int] = None) -> None: - - if team_page: - team_section_suffix = f" (Part {team_page})" - else: - team_section_suffix = "" + if truncate_title: + title = ( + f"{truncate_cell_for_display(title, sesqui_max_chars=True)}" + f"{f' (Part {team_page})' if team_page else ''}" + ) - # truncate data cell contents to 1.5x specified max characters if team name length exceeds that value - team_section = self.format_toc_section( - f"{truncate_cell_for_display(team_name, sesqui_max_chars=True)}{team_section_suffix}" - ) + toc_section = self._format_toc_entry(title, color) if color else self._format_toc_entry(title) - self.toc_team_section_data.append(team_section) + self.toc_entries_for_sections[section_key].append(toc_section) self.toc_anchor += 1 def add_appendix(self, title: str) -> None: - appendix_section = self.format_toc_section(title) - self.toc_appendix_data.append(appendix_section) + appendix_section = self._format_toc_entry(title) + self.toc_entries_for_appendix_section.append(appendix_section) self.toc_anchor += 1 def get_current_anchor(self) -> int: return self.toc_anchor # noinspection DuplicatedCode - def get_toc(self) -> Table: - """Retrieve Table of Contents element (table comprised of two separate tables that allow the TOC to be divided - into to sections so that when it spans two pages it looks good). + def get_toc(self) -> List[Table]: + """Retrieve Table of Contents comprised of multiple sections. """ - toc_part_one_row_heights: List = [] - if self.toc_metric_section_data: - toc_part_one_row_heights.extend([self.toc_line_height] * len(self.toc_metric_section_data)) - toc_part_one_row_heights.append(self.toc_section_spacer_row_height) - if self.toc_top_performers_section_data: - toc_part_one_row_heights.extend([self.toc_line_height] * len(self.toc_top_performers_section_data)) - toc_part_one_row_heights.append(self.toc_section_spacer_row_height) - if self.toc_chart_section_data: - toc_part_one_row_heights.extend([self.toc_line_height] * len(self.toc_chart_section_data)) - toc_part_one_row_heights.append(self.toc_section_spacer_row_height) - - toc_part_two_row_heights: List = [] - if self.toc_team_section_data: - toc_part_two_row_heights.extend([self.toc_line_height] * len(self.toc_team_section_data)) - toc_part_two_row_heights.append(self.toc_section_spacer_row_height) - if self.toc_appendix_data: - toc_part_two_row_heights.extend([self.toc_line_height] * len(self.toc_appendix_data)) - - toc_part_one_table = Table( - (self.toc_metric_section_data + [["", "", ""]] if self.toc_metric_section_data else []) + - (self.toc_top_performers_section_data + [["", "", ""]] if self.toc_top_performers_section_data else []) + - (self.toc_chart_section_data + [["", "", ""]] if self.toc_chart_section_data else []), - colWidths=self.toc_col_widths, - rowHeights=toc_part_one_row_heights - ) + tables = [] + for toc_section, toc_section_entries in self.toc_entries_for_sections.items(): + row_heights: List = [self.toc_line_height] * len(toc_section_entries) - toc_part_two_table = Table( - (self.toc_team_section_data + [["", "", ""]] if self.toc_team_section_data else []) + - (self.toc_appendix_data if self.toc_appendix_data else []), - colWidths=self.toc_col_widths, - rowHeights=toc_part_two_row_heights - ) + toc_section_entries.append([Paragraph(""), Paragraph("")]) + row_heights.append(self.toc_section_spacer_row_height) - return Table( - [ - [toc_part_one_table], - [toc_part_two_table] - ], - style=TableStyle([("ALIGN", (0, 0), (-1, -1), "CENTER")]) - ) + tables.append( + Table( + toc_section_entries, + colWidths=self.toc_col_widths, + rowHeights=row_heights, + style=TableStyle([ + ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), + ("ROWBACKGROUNDS", (0, 0), (-1, -1), [colors.white, colors.whitesmoke]), # alternate row colors + ("BACKGROUND", (0, -1), (-1, -1), colors.white), # make the ending spacer row white + ]) + ) + ) + + return tables class Appendix(object): diff --git a/utilities/settings.py b/utilities/settings.py index 2ea978f..d719048 100644 --- a/utilities/settings.py +++ b/utilities/settings.py @@ -11,6 +11,7 @@ from colorama import Fore, Style from dotenv import dotenv_values from pydantic import Field, computed_field +# noinspection PyProtectedMember from pydantic.fields import FieldInfo from pydantic_settings import BaseSettings, SettingsConfigDict, EnvSettingsSource, PydanticBaseSettingsSource @@ -236,8 +237,7 @@ class ReportSettings(CustomSettings): league_weekly_top_scorers_bool: bool = Field(True, title=__qualname__) league_weekly_low_scorers_bool: bool = Field(True, title=__qualname__) league_weekly_highest_ce_bool: bool = Field(True, title=__qualname__) - report_time_series_charts_bool: bool = Field(True, title=__qualname__) - report_team_stats_bool: bool = Field(True, title=__qualname__) + league_time_series_charts_bool: bool = Field(True, title=__qualname__) team_points_by_position_charts_bool: bool = Field(True, title=__qualname__) team_bad_boy_stats_bool: bool = Field(True, title=__qualname__) team_beef_stats_bool: bool = Field(True, title=__qualname__) @@ -256,11 +256,11 @@ class ReportSettings(CustomSettings): ) font_size: int = Field( 12, - ge=8, + ge=10, le=14, title=__qualname__, description=( - "set base font size so report element fonts resize dynamically (min: 8, max: 14)" + "set base font size so report element fonts resize dynamically (min: 10, max: 14)" ) ) image_quality: int = Field(