import pandas as pd
from typing import Optional, List, Any
from tabulate import tabulate
from docx import Document
from docx.shared import Pt, Cm, RGBColor
import seaborn as sns
import matplotlib.pyplot as plt
from docx.oxml import parse_xml
from docx.oxml.ns import nsdecls
from io import BytesIO
from pandas.io.formats.style import Styler
CUSTOM_COLORS = {
"blue": "#1f77b4", # Default Matplotlib blue
"red": "#d62728", # Default Matplotlib red
"dark_gray": "#323232",
"light_gray": "#e3e3e3",
"dark_blue": "#073165",
}
# Default Matplotlib and Seaborn settings
[docs]
class TablePrinter:
"""
A class for printing tables with customizable formatting options.
Attributes:
-----------
title : Optional[str]
Optional title displayed above the printed table.
Methods:
--------
print_table_data(data: List[List[Any]], headers: List[str], tablefmt: str = "fancygrid", numalign: str = "right")
-> None
Prints table data with customizable formatting.
print_table_min_max(data: List[List[Any]], headers: List[str], tablefmt: str = "fancygrid", numalign: str = "right")
-> None
Prints table data with column alignment for minimum and maximum values.
"""
def __init__(self, title: Optional[str] = None) -> None:
"""
Initializes the TablePrinter with an optional title.
Parameters:
-----------
title : Optional[str], default=None
Title to be displayed above the table, if provided.
"""
self.title = title
[docs]
def print_table_data(
self,
data: dict[str, list[Any]],
headers: str,
tablefmt: str = "fancygrid",
numalign: str = "right",
) -> None:
"""
Prints table data with customizable formatting options.
Parameters:
-----------
data : List[List[Any]]
The data to be printed in table format, where each inner list represents a row.
headers : List[str]
Column headers for the table.
tablefmt : str, default="fancygrid"
Table formatting style supported by tabulate (e.g., "plain", "grid", "fancygrid").
numalign : str, default="right"
Number alignment in the table. Common options are "right", "center", or "left".
Returns:
--------
table: Return the formatted table string
"""
# if self.title:
# print(f"======= {self.title} ======= \n")
colalign = ("left", "center", "right", "left")
table = tabulate(
data,
headers=headers,
tablefmt=tablefmt,
numalign=numalign,
colalign=colalign,
)
print(table, "\n")
return None
[docs]
def print_table_min_max(
self,
data: dict[str, list[Any]],
headers: str,
tablefmt: str = "fancygrid",
numalign: str = "right",
) -> None:
"""
Prints table data with column alignment for minimum and maximum values.
Parameters:
-----------
data : List[List[Any]]
The data to be printed in table format, where each inner list represents a row.
headers : List[str]
Column headers for the table.
tablefmt : str, default="fancygrid"
Table formatting style supported by tabulate (e.g., "plain", "grid", "fancygrid").
numalign : str, default="right"
Number alignment in the table. Common options are "right", "center", or "left".
Returns:
--------
None
"""
if self.title:
print(f"======= {self.title} ======= \n")
colalign = ("left", "center", "center", "center", "center", "center", "left")
table = tabulate(
data,
headers=headers,
tablefmt=tablefmt,
numalign=numalign,
colalign=colalign,
)
print(table, "\n")
[docs]
class DocumentBuilder:
"""
A class to build and style a Word document, including adding headings and tables from data frames.
Attributes:
-----------
title : str
Title of the document, displayed at the beginning.
font_name : str
Default font name for the document text.
font_size : int
Default font size for the document text.
doc : Document
The Document object representing the Word document being built.
Methods:
--------
set_document_style() -> None
Configures the document's default style with the specified font name and size.
add_heading(text: str, level: int) -> None
Adds a heading to the document at the specified level.
set_col_widths(table: 'docx.table.Table', column_widths: List[Cm]) -> None
Sets the width for each column in a given table.
add_table(df: pd.DataFrame, column_widths: List[Cm]) -> None
Adds a table to the document, with data from a DataFrame and custom column widths.
save(filename: str) -> None
Saves the document to a specified filename.
"""
def __init__(self, title: str, font_name: str = "Lato", font_size: int = 9) -> None:
"""
Initializes the DocumentBuilder with a title, font name, and font size.
Parameters:
-----------
title : str
Title to be displayed in the document.
font_name : str, default='Lato'
Font name to be used for the document text.
font_size : int, default=9
Font size for the document text.
"""
self.doc = Document()
self.title = title
self.font_name = font_name
self.font_size = font_size
self.set_document_style()
self.set_page_size()
self.set_margins()
[docs]
def set_document_style(self) -> None:
"""
Sets the default style of the document, applying the font name and size.
Returns:
--------
None
"""
# Normal text style
style = self.doc.styles["Normal"]
style.font.name = self.font_name
style.font.size = Pt(self.font_size)
[docs]
def set_page_size(self) -> None:
"""
Sets the page size to A4 by default.
Returns:
--------
None
"""
section = self.doc.sections[0]
section.page_height = Cm(29.7)
section.page_width = Cm(21.0)
[docs]
def set_margins(self, top: float = 3.5, bottom: float = 2.5, left: float = 1.6, right: float = 1.6) -> None:
"""
Set page margins in centimeters.
Parameters
----------
top, bottom, left, right : float
Margins in centimeters. Default is 1 cm for all.
"""
for section in self.doc.sections:
section.top_margin = Cm(top)
section.bottom_margin = Cm(bottom)
section.left_margin = Cm(left)
section.right_margin = Cm(right)
[docs]
def add_heading(self, text: str, level: int, font_size: Optional[float] = 10) -> None:
"""
Adds a heading to the document at the specified level.
Parameters:
-----------
text : str
The text for the heading.
level : int
The heading level (e.g., 0 for title, 1 for main headings, etc.).
font_size : float, optional
Font size in points. Default is 10 pt.
Returns:
--------
None
"""
heading = self.doc.add_heading(text, level=level)
heading.paragraph_format.space_before = Pt(0)
# Set font size for all runs in heading
for run in heading.runs:
run.font.size = Pt(font_size)
run.font.name = self.font_name
[docs]
def add_text(self, text: str) -> None:
"""Adds a paragraph to the document"""
self.doc.add_paragraph(text)
[docs]
def set_col_widths(self, table: Any, column_widths: List[Cm]) -> None:
"""
Sets the width of each column in the table.
Parameters:
-----------
table : docx.table.Table
The table object whose column widths need to be set.
column_widths : List[Cm]
List of widths for each column, in centimeters.
Returns:
--------
None
"""
for row in table.rows:
for idx, width in enumerate(column_widths):
row.cells[idx].width = width
[docs]
def add_table(
self,
df: pd.DataFrame,
column_widths: List[Cm],
font_size: Optional[float] = 9,
) -> None:
"""
Adds a table to the document, populated with data from a DataFrame.
Parameters
----------
df : pd.DataFrame
The DataFrame containing the data for the table.
column_widths : List[Cm]
List of column widths for the table.
font_size : float, optional
Font size (in points) for table text. Default is 10 pt.
Returns
-------
docx.table.Table
The created table object.
"""
from docx.shared import Pt
# --- Create and style table ---
table = self.doc.add_table(rows=df.shape[0] + 1, cols=df.shape[1])
table.style = "Light Shading"
self.set_col_widths(table, column_widths)
# --- Header row ---
for j in range(df.shape[1]):
table.cell(0, j).text = str(df.columns[j])
# --- Data rows ---
for i in range(df.shape[0]):
for j in range(df.shape[1]):
value = df.iat[i, j]
table.cell(i + 1, j).text = str(value)
# --- Format all text ---
if font_size is not None:
for row in table.rows:
for cell in row.cells:
for paragraph in cell.paragraphs:
for run in paragraph.runs:
run.font.size = Pt(font_size)
# --- First column not bold ---
for row in table.rows[1:]:
first_cell = row.cells[0]
for run in first_cell.paragraphs[0].runs:
run.font.bold = False
# --- Add spacer paragraph ---
spacer = self.doc.add_paragraph()
spacer.paragraph_format.space_after = Pt(0)
return table
[docs]
def add_table_data(self, df: pd.DataFrame, column_widths=[Cm(12), Cm(2), Cm(2), Cm(2)]) -> None:
self.add_table(df, column_widths, font_size=self.font_size)
[docs]
def add_table_dcr(self, df: pd.DataFrame) -> None:
"""
Adds a table using `add_table` and modifies the last row's DCR cell color.
- Green if DCR < 1
- Red if DCR >= 1
"""
column_widths = [Cm(12), Cm(2), Cm(2), Cm(2)]
self.add_table(df, column_widths, font_size=self.font_size)
# Get the last table added to the document
table = self.doc.tables[-1] # Retrieve the most recent table
# Apply color to DCR cell (third column of the last row)
last_row_idx = df.shape[0] # Last row index
dcr_column_idx = 2 # Third column index (zero-based)
dcr_value = float(df.iat[-1, dcr_column_idx]) # Extract DCR value
if dcr_value < 1:
shading_color = "C6EFCE" # Green
font_color = "006100" # Green font
else:
shading_color = "FFC7CE" # Red
font_color = "9C0006" # Red font
# Apply shading and font color to the entire last row
for cell in table.rows[last_row_idx].cells:
# Apply background color
cell._element.get_or_add_tcPr().append(parse_xml(f'<w:shd {nsdecls("w")} w:fill="{shading_color}"/>'))
# Apply font color
for paragraph in cell.paragraphs:
for run in paragraph.runs:
run.font.color.rgb = RGBColor.from_string(font_color)
[docs]
def add_table_min_max(self, df: pd.DataFrame) -> None:
column_widths = [Cm(9), Cm(2), Cm(2), Cm(2), Cm(2), Cm(1)]
self.add_table(df, column_widths, font_size=self.font_size)
[docs]
def add_image(self, image_path: str, width: Optional[Cm] = None) -> None:
"""
Insert an external image into the document.
Parameters
----------
image_path : str
Absolute or relative path to the image file.
width : docx.shared.Cm, optional
Target width. Height scales to keep aspect ratio.
"""
# paragraph container so image is not glued to previous element
paragraph = self.doc.add_paragraph()
run = paragraph.add_run()
if width is not None:
run.add_picture(image_path, width=width)
else:
run.add_picture(image_path)
# small space after
spacer = self.doc.add_paragraph()
spacer.paragraph_format.space_after = Pt(0)
[docs]
def save(self, filename: str) -> None:
"""
Saves the document to a specified file.
Parameters:
-----------
filename : str
The filename (including path) where the document will be saved.
Returns:
--------
None
"""
self.doc.save(filename)
# Examples to run in a Jupyter Notebook
# formatter = Formatter()
# display(Markdown(formatter.DCR(0.85)))
# display(Markdown(formatter.DCR_value(0.85)))
# display(Markdown(formatter.is_lower(0.85,1)))
# display(Markdown(formatter.is_greater(0.85,1)))