Creating pixel perfect PDF reports using using Plotly, HTML, CSS, WeasyPrint and Jinja2 — Reporting system part 3

This article follows on from my previous post

So once the star schema is loaded into our database(I’m using google big query — in the following post I will go over how I loaded that in the airflow section)

We will generate the assets /charts for the reports — I’ll post a link to the following github where the full code can be seen.

Whilst generating the charts I learnt about the library shutil and also that I could store plotly plots in a dictionary which helped me generate the plots in a loop and write and name them to a folder at the same time.

After generating the charts we will need to generate the HTML reports using the WeasyPrint Flask driver in order to produce the reports.

For those not familiar with flask and Jinja2 , the basic concept is that you can pass variables to a template to generate dynamic HTML, an example of how I used it is below, im passing my dataframe as a variable , along with a list of categories that I used to filter the page and the assets directory to put the images in.

{% macro report(top_5_df,field, category_list, assets_dir) %}
{% for category in category_list %}
<article id="summary" style="page-break-before: always"> <h2> {{ category }} -
Top 5 - Ordered by {{ field }} - Summary </h2>
{% for _,row in
top_5_df[top_5_df['Category']==category].iterrows() %}

You can then embed this macro into a main template , in my example I had one macro that would generate two reports per category and then sandwhich that in between a front cover and ending page

{% from "macros-flask.html" import report %}
....... HTML Code

{{ report(top_5_df,field, category_list, assets_dir) }}

CSS posed a challenge here as I was new to bootstrap and flexbox, I also had an issue generating CSS for print , however this could of been due to the flask weasy print driver. However the following code helped.

@page {
size: A4 landscape;
}@media screen and (max-width: 575px) {
main {
flex-direction: column;
.sidebar-left {
order: 2;

To generate the actual reports we had to install WeasyPrint (Which can be found in the documentation) and then create the driver script that will be used to generate the reports.

The following youtube channel really helped me get started with WeasyPrint

The code for WeasyPrint itself is fairly simple , the main code is here , I had two reports to generate so I created a dictionary to pass the variables through.

# TEMPLATE = 'layout_driver_sales.html'
CSS = 'print-landscape.css'
def start():for key, value in REPORT_TEMPLATE_DICT.items():print('Generating Reports',REPORT_TEMPLATE_DICT[key]['report_name'])OUTPUT_FILENAME = date_str+REPORT_TEMPLATE_DICT[key]['report_name']+'.pdf'env = Environment(loader=FileSystemLoader(TEMPLAT_SRC))template = env.get_template(REPORT_TEMPLATE_DICT[key]['html_template'])css = os.path.join(CSS_SRC, CSS)
df_dir = '../Data/csv_reports'
df_date = date_str
df_name = REPORT_TEMPLATE_DICT[key]['df']
df_path = os.path.join(df_dir,df_date,df_name)top_5_df = pd.read_csv(df_path)# variables
template_vars = { 'assets_dir': 'file://' + ASSETS_DIR , 'top_5_df':top_5_df , 'date_str':date_str, 'field': str(key), 'category_list':category_list}
# rendering to html string
rendered_string = template.render(template_vars)
html = weasyprint.HTML(string=rendered_string)
report = os.path.join(DEST_DIR, OUTPUT_FILENAME)
html.write_pdf(report, stylesheets=[css])
print('file is generated successfully and under {}', DEST_DIR)
if __name__ == '__main__':

I will update this article soon with an example report using fake data