Exporting multi page Dash app to pdf with entire layout

Hi,

I have been working for six months with Plotly to generate charts that were created with multiple tools with lot of manual effort running programs to generate data into csv files, charts from Excel pasted to a templated word document. Plotly works great and thanks for the great software !!

Now I have a multi page app that has has text, tables, figures with dynamic charts controlled by datepicker. I am researching for ways to generate pdfs on the server side to be downloaded by the user for the selected dates.

Print Pdf button used by https://dash-gallery.plotly.host/dash-vanguard-report/ does not work for me. The approaches to export individual figures is also cumbersome.

I have looked into using weasyprint type of approaches but I need to be able to POST and that’s approach seems clumsy but can work.

Is there way to generate HTML for the entire layout after user selects a date range with an ‘Export pdf’ button? Any pointers are greatly appreciated.

Thanks

It looks like this can’t be done on the server side as the layout is rendered with react (https://github.com/plotly/dash/issues/145). Does anybody have any suggestion to do this on the client side or a way to export text, tables and figures on the server side such that there isn’t lot of duplication of layout HTML and pdf generation code?

Thanks a lot.

You can do that using External JS, something like this logic:

function printData()
{
   var divToPrint=document.getElementById("your_page_id");
   newWin= window.open("");
   newWin.document.write(divToPrint.outerHTML);
   newWin.print();
   newWin.close();
}

$('button').on('click',function(){
printData();
})

You can concatenate the different htmls into one object then try to print it.

reference: https://stackoverflow.com/questions/33732739/print-a-div-content-using-jquery

tried this today with vanilla javascript if jquery doesn’t work:

function printData()

{

    var divToPrint=document.getElementById("main_container");

    newWin= window.open("");

    newWin.document.write(divToPrint.outerHTML);

    newWin.print();

    newWin.close();

}

setTimeout(function mainFunction(){

    try {

        document.getElementById("run").addEventListener("click", function(){

            printData();

        })

      }

      catch(err) {

        console.log(err)

      }

    console.log('Listener Added!');

}, 5000);

Hi arham,

The dash export to pdf is cumbersome for me. Could you explain how to add this javascript into a dash app.

Let’s just assume a simple app as stated below:

import dash
import dash_core_components as dcc
import dash_html_components as html

app = dash.Dash()
app.layout = html.Div(
className=“three columns”,
children=html.Div([
dcc.Graph(
id=‘right-top-graph’,
figure={
‘data’: [{
‘x’: [1, 2, 3],
‘y’: [3, 1, 2],
‘type’: ‘bar’
}],
‘layout’: {
‘height’: 400,
‘margin’: {‘l’: 10, ‘b’: 20, ‘t’: 0, ‘r’: 0}
}
}
),

            ])
        )

app.css.append_css({
‘external_url’: ‘https://codepen.io/chriddyp/pen/bWLwgP.css’
})

if name == ‘main’:
app.run_server(debug=False)

Hi @NeeradjPelargos
as far as I know you can’t export PDF from python code(server side), you need something like external javascript linked with your frontend, in this javascript file you can define function for exporting PDF, like I did above.

Hi arham,

Thank you for you help, really appreciated.
I have been going through your messages and external links, but still came up short. You mention that the print button should work if the different html’s are concatenated into one object. Could you give a simple example how to do that with the simple dash app I supplied and the print code button you provided?

share your HTML(Dash version) and js file here, this is how I use it:

......
html.Div([
    html.Button('click', id='run')
])
......

I also created a file pdf_print.js and added to assets folder with following code.

function printData() {
    var divToPrint=document.getElementById("main_container");

    newWin= window.open("");

    newWin.document.write(divToPrint.outerHTML);

    newWin.print();

    newWin.close();
}

setTimeout(function mainFunction(){
    try {
        document.getElementById("run").addEventListener("click", function(){
            printData();
        })
      }
      catch(err) {
        console.log(err)
      }
    console.log('Listener Added!');
}, 30000);

Hi arham, thanks to your great help I’m almost finished with this (for me) difficult problem. I only need the pdf to adopt the css.

I used this template to test your button

I added an explicit reference to the assets folder on line 23 and your dash snippet in line 82. Thereafter I added your code (adjusting the element id in the printData function from main_container to mainContainer). This worked! I now have a code that has a print button that will allow me to print the dash in pdf…but the styling is entirely off. It does not seem like the print pdf function incorporates the css.

Do you have any recommendations on this?

For more clarity:

  1. here a screenshot so you can see how it looks when i run app.py

  2. a screenshot that shows that the pdf print does not incorporate the css.

  3. see below the adjusted app.py code (with explicit reference to assets folder and the button code you provided):

# Import required libraries
import pickle
import copy
import pathlib
import dash
import math
import datetime as dt
import pandas as pd
from dash.dependencies import Input, Output, State, ClientsideFunction
import dash_core_components as dcc
import dash_html_components as html

# Multi-dropdown options
from controls import COUNTIES, WELL_STATUSES, WELL_TYPES, WELL_COLORS

# get relative data folder
PATH = pathlib.Path(__file__).parent
DATA_PATH = PATH.joinpath("data").resolve()

app = dash.Dash(
    __name__
    , meta_tags=[{"name": "viewport", "content": "width=device-width"}]
    ,assets_folder = str(PATH.joinpath("assets").resolve())
)
server = app.server

# Create controls
county_options = [
    {"label": str(COUNTIES[county]), "value": str(county)} for county in COUNTIES
]

well_status_options = [
    {"label": str(WELL_STATUSES[well_status]), "value": str(well_status)}
    for well_status in WELL_STATUSES
]

well_type_options = [
    {"label": str(WELL_TYPES[well_type]), "value": str(well_type)}
    for well_type in WELL_TYPES
]


# Load data
df = pd.read_csv(DATA_PATH.joinpath("wellspublic.csv"), low_memory=False)
df["Date_Well_Completed"] = pd.to_datetime(df["Date_Well_Completed"])
df = df[df["Date_Well_Completed"] > dt.datetime(1960, 1, 1)]

trim = df[["API_WellNo", "Well_Type", "Well_Name"]]
trim.index = trim["API_WellNo"]
dataset = trim.to_dict(orient="index")

points = pickle.load(open(DATA_PATH.joinpath("points.pkl"), "rb"))


# Create global chart template
mapbox_access_token = "pk.eyJ1IjoicGxvdGx5bWFwYm94IiwiYSI6ImNrOWJqb2F4djBnMjEzbG50amg0dnJieG4ifQ.Zme1-Uzoi75IaFbieBDl3A"

layout = dict(
    autosize=True,
    automargin=True,
    margin=dict(l=30, r=30, b=20, t=40),
    hovermode="closest",
    plot_bgcolor="#F9F9F9",
    paper_bgcolor="#F9F9F9",
    legend=dict(font=dict(size=10), orientation="h"),
    title="Satellite Overview",
    mapbox=dict(
        accesstoken=mapbox_access_token,
        style="light",
        center=dict(lon=-78.05, lat=42.54),
        zoom=7,
    ),
)

# Create app layout
app.layout = html.Div(
    [
        dcc.Store(id="aggregate_data"),
        # empty Div to trigger javascript file for graph resizing
        html.Div(id="output-clientside"),
        
        html.Div([

                html.Button('click', id='run')
                ]),
        
        html.Div(
            [
                html.Div(
                    [
                        html.Img(
                            src=app.get_asset_url("dash-logo.png"),
                            id="plotly-image",
                            style={
                                "height": "60px",
                                "width": "auto",
                                "margin-bottom": "25px",
                            },
                        )
                    ],
                    className="one-third column",
                ),
                html.Div(
                    [
                        html.Div(
                            [
                                html.H3(
                                    "New York Oil and Gas",
                                    style={"margin-bottom": "0px"},
                                ),
                                html.H5(
                                    "Production Overview", style={"margin-top": "0px"}
                                ),
                            ]
                        )
                    ],
                    className="one-half column",
                    id="title",
                ),
                html.Div(
                    [
                        html.A(
                            html.Button("Learn More", id="learn-more-button"),
                            href="https://plot.ly/dash/pricing/",
                        )
                    ],
                    className="one-third column",
                    id="button",
                ),
            ],
            id="header",
            className="row flex-display",
            style={"margin-bottom": "25px"},
        ),
        html.Div(
            [
                html.Div(
                    [
                        html.P(
                            "Filter by construction date (or select range in histogram):",
                            className="control_label",
                        ),
                        dcc.RangeSlider(
                            id="year_slider",
                            min=1960,
                            max=2017,
                            value=[1990, 2010],
                            className="dcc_control",
                        ),
                        html.P("Filter by well status:", className="control_label"),
                        dcc.RadioItems(
                            id="well_status_selector",
                            options=[
                                {"label": "All ", "value": "all"},
                                {"label": "Active only ", "value": "active"},
                                {"label": "Customize ", "value": "custom"},
                            ],
                            value="active",
                            labelStyle={"display": "inline-block"},
                            className="dcc_control",
                        ),
                        dcc.Dropdown(
                            id="well_statuses",
                            options=well_status_options,
                            multi=True,
                            value=list(WELL_STATUSES.keys()),
                            className="dcc_control",
                        ),
                        dcc.Checklist(
                            id="lock_selector",
                            options=[{"label": "Lock camera", "value": "locked"}],
                            className="dcc_control",
                            value=[],
                        ),
                        html.P("Filter by well type:", className="control_label"),
                        dcc.RadioItems(
                            id="well_type_selector",
                            options=[
                                {"label": "All ", "value": "all"},
                                {"label": "Productive only ", "value": "productive"},
                                {"label": "Customize ", "value": "custom"},
                            ],
                            value="productive",
                            labelStyle={"display": "inline-block"},
                            className="dcc_control",
                        ),
                        dcc.Dropdown(
                            id="well_types",
                            options=well_type_options,
                            multi=True,
                            value=list(WELL_TYPES.keys()),
                            className="dcc_control",
                        ),
                    ],
                    className="pretty_container four columns",
                    id="cross-filter-options",
                ),
                html.Div(
                    [
                        html.Div(
                            [
                                html.Div(
                                    [html.H6(id="well_text"), html.P("No. of Wells")],
                                    id="wells",
                                    className="mini_container",
                                ),
                                html.Div(
                                    [html.H6(id="gasText"), html.P("Gas")],
                                    id="gas",
                                    className="mini_container",
                                ),
                                html.Div(
                                    [html.H6(id="oilText"), html.P("Oil")],
                                    id="oil",
                                    className="mini_container",
                                ),
                                html.Div(
                                    [html.H6(id="waterText"), html.P("Water")],
                                    id="water",
                                    className="mini_container",
                                ),
                            ],
                            id="info-container",
                            className="row container-display",
                        ),
                        html.Div(
                            [dcc.Graph(id="count_graph")],
                            id="countGraphContainer",
                            className="pretty_container",
                        ),
                    ],
                    id="right-column",
                    className="eight columns",
                ),
            ],
            className="row flex-display",
        ),
        html.Div(
            [
                html.Div(
                    [dcc.Graph(id="main_graph")],
                    className="pretty_container seven columns",
                ),
                html.Div(
                    [dcc.Graph(id="individual_graph")],
                    className="pretty_container five columns",
                ),
            ],
            className="row flex-display",
        ),
        html.Div(
            [
                html.Div(
                    [dcc.Graph(id="pie_graph")],
                    className="pretty_container seven columns",
                ),
                html.Div(
                    [dcc.Graph(id="aggregate_graph")],
                    className="pretty_container five columns",
                ),
            ],
            className="row flex-display",
        ),
    ],
    id="mainContainer",
    style={"display": "flex", "flex-direction": "column"},
)


# Helper functions
def human_format(num):
    if num == 0:
        return "0"

    magnitude = int(math.log(num, 1000))
    mantissa = str(int(num / (1000 ** magnitude)))
    return mantissa + ["", "K", "M", "G", "T", "P"][magnitude]


def filter_dataframe(df, well_statuses, well_types, year_slider):
    dff = df[
        df["Well_Status"].isin(well_statuses)
        & df["Well_Type"].isin(well_types)
        & (df["Date_Well_Completed"] > dt.datetime(year_slider[0], 1, 1))
        & (df["Date_Well_Completed"] < dt.datetime(year_slider[1], 1, 1))
    ]
    return dff


def produce_individual(api_well_num):
    try:
        points[api_well_num]
    except:
        return None, None, None, None

    index = list(
        range(min(points[api_well_num].keys()), max(points[api_well_num].keys()) + 1)
    )
    gas = []
    oil = []
    water = []

    for year in index:
        try:
            gas.append(points[api_well_num][year]["Gas Produced, MCF"])
        except:
            gas.append(0)
        try:
            oil.append(points[api_well_num][year]["Oil Produced, bbl"])
        except:
            oil.append(0)
        try:
            water.append(points[api_well_num][year]["Water Produced, bbl"])
        except:
            water.append(0)

    return index, gas, oil, water


def produce_aggregate(selected, year_slider):

    index = list(range(max(year_slider[0], 1985), 2016))
    gas = []
    oil = []
    water = []

    for year in index:
        count_gas = 0
        count_oil = 0
        count_water = 0
        for api_well_num in selected:
            try:
                count_gas += points[api_well_num][year]["Gas Produced, MCF"]
            except:
                pass
            try:
                count_oil += points[api_well_num][year]["Oil Produced, bbl"]
            except:
                pass
            try:
                count_water += points[api_well_num][year]["Water Produced, bbl"]
            except:
                pass
        gas.append(count_gas)
        oil.append(count_oil)
        water.append(count_water)

    return index, gas, oil, water


# Create callbacks
app.clientside_callback(
    ClientsideFunction(namespace="clientside", function_name="resize"),
    Output("output-clientside", "children"),
    [Input("count_graph", "figure")],
)


@app.callback(
    Output("aggregate_data", "data"),
    [
        Input("well_statuses", "value"),
        Input("well_types", "value"),
        Input("year_slider", "value"),
    ],
)
def update_production_text(well_statuses, well_types, year_slider):

    dff = filter_dataframe(df, well_statuses, well_types, year_slider)
    selected = dff["API_WellNo"].values
    index, gas, oil, water = produce_aggregate(selected, year_slider)
    return [human_format(sum(gas)), human_format(sum(oil)), human_format(sum(water))]


# Radio -> multi
@app.callback(
    Output("well_statuses", "value"), [Input("well_status_selector", "value")]
)
def display_status(selector):
    if selector == "all":
        return list(WELL_STATUSES.keys())
    elif selector == "active":
        return ["AC"]
    return []


# Radio -> multi
@app.callback(Output("well_types", "value"), [Input("well_type_selector", "value")])
def display_type(selector):
    if selector == "all":
        return list(WELL_TYPES.keys())
    elif selector == "productive":
        return ["GD", "GE", "GW", "IG", "IW", "OD", "OE", "OW"]
    return []


# Slider -> count graph
@app.callback(Output("year_slider", "value"), [Input("count_graph", "selectedData")])
def update_year_slider(count_graph_selected):

    if count_graph_selected is None:
        return [1990, 2010]

    nums = [int(point["pointNumber"]) for point in count_graph_selected["points"]]
    return [min(nums) + 1960, max(nums) + 1961]


# Selectors -> well text
@app.callback(
    Output("well_text", "children"),
    [
        Input("well_statuses", "value"),
        Input("well_types", "value"),
        Input("year_slider", "value"),
    ],
)
def update_well_text(well_statuses, well_types, year_slider):

    dff = filter_dataframe(df, well_statuses, well_types, year_slider)
    return dff.shape[0]


@app.callback(
    [
        Output("gasText", "children"),
        Output("oilText", "children"),
        Output("waterText", "children"),
    ],
    [Input("aggregate_data", "data")],
)
def update_text(data):
    return data[0] + " mcf", data[1] + " bbl", data[2] + " bbl"


# Selectors -> main graph
@app.callback(
    Output("main_graph", "figure"),
    [
        Input("well_statuses", "value"),
        Input("well_types", "value"),
        Input("year_slider", "value"),
    ],
    [State("lock_selector", "value"), State("main_graph", "relayoutData")],
)
def make_main_figure(
    well_statuses, well_types, year_slider, selector, main_graph_layout
):

    dff = filter_dataframe(df, well_statuses, well_types, year_slider)

    traces = []
    for well_type, dfff in dff.groupby("Well_Type"):
        trace = dict(
            type="scattermapbox",
            lon=dfff["Surface_Longitude"],
            lat=dfff["Surface_latitude"],
            text=dfff["Well_Name"],
            customdata=dfff["API_WellNo"],
            name=WELL_TYPES[well_type],
            marker=dict(size=4, opacity=0.6),
        )
        traces.append(trace)

    # relayoutData is None by default, and {'autosize': True} without relayout action
    if main_graph_layout is not None and selector is not None and "locked" in selector:
        if "mapbox.center" in main_graph_layout.keys():
            lon = float(main_graph_layout["mapbox.center"]["lon"])
            lat = float(main_graph_layout["mapbox.center"]["lat"])
            zoom = float(main_graph_layout["mapbox.zoom"])
            layout["mapbox"]["center"]["lon"] = lon
            layout["mapbox"]["center"]["lat"] = lat
            layout["mapbox"]["zoom"] = zoom

    figure = dict(data=traces, layout=layout)
    return figure


# Main graph -> individual graph
@app.callback(Output("individual_graph", "figure"), [Input("main_graph", "hoverData")])
def make_individual_figure(main_graph_hover):

    layout_individual = copy.deepcopy(layout)

    if main_graph_hover is None:
        main_graph_hover = {
            "points": [
                {"curveNumber": 4, "pointNumber": 569, "customdata": 31101173130000}
            ]
        }

    chosen = [point["customdata"] for point in main_graph_hover["points"]]
    index, gas, oil, water = produce_individual(chosen[0])

    if index is None:
        annotation = dict(
            text="No data available",
            x=0.5,
            y=0.5,
            align="center",
            showarrow=False,
            xref="paper",
            yref="paper",
        )
        layout_individual["annotations"] = [annotation]
        data = []
    else:
        data = [
            dict(
                type="scatter",
                mode="lines+markers",
                name="Gas Produced (mcf)",
                x=index,
                y=gas,
                line=dict(shape="spline", smoothing=2, width=1, color="#fac1b7"),
                marker=dict(symbol="diamond-open"),
            ),
            dict(
                type="scatter",
                mode="lines+markers",
                name="Oil Produced (bbl)",
                x=index,
                y=oil,
                line=dict(shape="spline", smoothing=2, width=1, color="#a9bb95"),
                marker=dict(symbol="diamond-open"),
            ),
            dict(
                type="scatter",
                mode="lines+markers",
                name="Water Produced (bbl)",
                x=index,
                y=water,
                line=dict(shape="spline", smoothing=2, width=1, color="#92d8d8"),
                marker=dict(symbol="diamond-open"),
            ),
        ]
        layout_individual["title"] = dataset[chosen[0]]["Well_Name"]

    figure = dict(data=data, layout=layout_individual)
    return figure


# Selectors, main graph -> aggregate graph
@app.callback(
    Output("aggregate_graph", "figure"),
    [
        Input("well_statuses", "value"),
        Input("well_types", "value"),
        Input("year_slider", "value"),
        Input("main_graph", "hoverData"),
    ],
)
def make_aggregate_figure(well_statuses, well_types, year_slider, main_graph_hover):

    layout_aggregate = copy.deepcopy(layout)

    if main_graph_hover is None:
        main_graph_hover = {
            "points": [
                {"curveNumber": 4, "pointNumber": 569, "customdata": 31101173130000}
            ]
        }

    chosen = [point["customdata"] for point in main_graph_hover["points"]]
    well_type = dataset[chosen[0]]["Well_Type"]
    dff = filter_dataframe(df, well_statuses, well_types, year_slider)

    selected = dff[dff["Well_Type"] == well_type]["API_WellNo"].values
    index, gas, oil, water = produce_aggregate(selected, year_slider)

    data = [
        dict(
            type="scatter",
            mode="lines",
            name="Gas Produced (mcf)",
            x=index,
            y=gas,
            line=dict(shape="spline", smoothing="2", color="#F9ADA0"),
        ),
        dict(
            type="scatter",
            mode="lines",
            name="Oil Produced (bbl)",
            x=index,
            y=oil,
            line=dict(shape="spline", smoothing="2", color="#849E68"),
        ),
        dict(
            type="scatter",
            mode="lines",
            name="Water Produced (bbl)",
            x=index,
            y=water,
            line=dict(shape="spline", smoothing="2", color="#59C3C3"),
        ),
    ]
    layout_aggregate["title"] = "Aggregate: " + WELL_TYPES[well_type]

    figure = dict(data=data, layout=layout_aggregate)
    return figure


# Selectors, main graph -> pie graph
@app.callback(
    Output("pie_graph", "figure"),
    [
        Input("well_statuses", "value"),
        Input("well_types", "value"),
        Input("year_slider", "value"),
    ],
)
def make_pie_figure(well_statuses, well_types, year_slider):

    layout_pie = copy.deepcopy(layout)

    dff = filter_dataframe(df, well_statuses, well_types, year_slider)

    selected = dff["API_WellNo"].values
    index, gas, oil, water = produce_aggregate(selected, year_slider)

    aggregate = dff.groupby(["Well_Type"]).count()

    data = [
        dict(
            type="pie",
            labels=["Gas", "Oil", "Water"],
            values=[sum(gas), sum(oil), sum(water)],
            name="Production Breakdown",
            text=[
                "Total Gas Produced (mcf)",
                "Total Oil Produced (bbl)",
                "Total Water Produced (bbl)",
            ],
            hoverinfo="text+value+percent",
            textinfo="label+percent+name",
            hole=0.5,
            marker=dict(colors=["#fac1b7", "#a9bb95", "#92d8d8"]),
            domain={"x": [0, 0.45], "y": [0.2, 0.8]},
        ),
        dict(
            type="pie",
            labels=[WELL_TYPES[i] for i in aggregate.index],
            values=aggregate["API_WellNo"],
            name="Well Type Breakdown",
            hoverinfo="label+text+value+percent",
            textinfo="label+percent+name",
            hole=0.5,
            marker=dict(colors=[WELL_COLORS[i] for i in aggregate.index]),
            domain={"x": [0.55, 1], "y": [0.2, 0.8]},
        ),
    ]
    layout_pie["title"] = "Production Summary: {} to {}".format(
        year_slider[0], year_slider[1]
    )
    layout_pie["font"] = dict(color="#777777")
    layout_pie["legend"] = dict(
        font=dict(color="#CCCCCC", size="10"), orientation="h", bgcolor="rgba(0,0,0,0)"
    )

    figure = dict(data=data, layout=layout_pie)
    return figure


# Selectors -> count graph
@app.callback(
    Output("count_graph", "figure"),
    [
        Input("well_statuses", "value"),
        Input("well_types", "value"),
        Input("year_slider", "value"),
    ],
)
def make_count_figure(well_statuses, well_types, year_slider):

    layout_count = copy.deepcopy(layout)

    dff = filter_dataframe(df, well_statuses, well_types, [1960, 2017])
    g = dff[["API_WellNo", "Date_Well_Completed"]]
    g.index = g["Date_Well_Completed"]
    g = g.resample("A").count()

    colors = []
    for i in range(1960, 2018):
        if i >= int(year_slider[0]) and i < int(year_slider[1]):
            colors.append("rgb(123, 199, 255)")
        else:
            colors.append("rgba(123, 199, 255, 0.2)")

    data = [
        dict(
            type="scatter",
            mode="markers",
            x=g.index,
            y=g["API_WellNo"] / 2,
            name="All Wells",
            opacity=0,
            hoverinfo="skip",
        ),
        dict(
            type="bar",
            x=g.index,
            y=g["API_WellNo"],
            name="All Wells",
            marker=dict(color=colors),
        ),
    ]

    layout_count["title"] = "Completed Wells/Year"
    layout_count["dragmode"] = "select"
    layout_count["showlegend"] = False
    layout_count["autosize"] = True

    figure = dict(data=data, layout=layout_count)
    return figure


# Main
if __name__ == "__main__":
    app.run_server(debug=False)

Hi, I think I found something that could work as a solution. Like Arham stated, I added external js files. 3 to be precise. The first 2 are javascript libraries that I downloaded to make the 3rd (main) js file work faster. These libraries I found here https://cdnjs.cloudflare.com/ajax/libs/html2canvas/0.4.1/html2canvas.js (saved as html2canvas.js) and https://cdnjs.cloudflare.com/ajax/libs/jspdf/1.5.1/jspdf.debug.js (saved as jspdf.js)

Then I created a file called print_pdf.js with code below

function createPDF(){

var jQueryScript2 = document.createElement('script'); 
jQueryScript2.setAttribute('src','YOUR LOCAL ADDRESS HERE/assets/html2canvas.js');
document.head.appendChild(jQueryScript2);
var jQueryScript3 = document.createElement('script'); 
jQueryScript3.setAttribute('src','YOUR LOCAL ADDRESS HERE/assets/jsPDF.js');
document.head.appendChild(jQueryScript3);
	



const printArea = document.getElementById("mainContainer");

html2canvas(printArea, {scale:3}).then(function(canvas){						
	var imgData = canvas.toDataURL('image/png');
	var doc = new jsPDF('p', 'mm', "a4");
	
	const pageHeight = doc.internal.pageSize.getHeight();
	const imgWidth = doc.internal.pageSize.getWidth();
	var imgHeight = canvas.height * imgWidth / canvas.width;
	var heightLeft = imgHeight;
	
	
	var position = 10; // give some top padding to first page

	doc.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
	heightLeft -= pageHeight;

	while (heightLeft >= 0) {
	  position += heightLeft - imgHeight; // top padding for other pages
	  doc.addPage();
	  doc.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
	  heightLeft -= pageHeight;
	}
	doc.save( 'file.pdf');
	
	
})

}

setTimeout(function mainFunction(){
try {
document.getElementById(“run”).addEventListener(“click”, function(){
createPDF();
})
}
catch(err) {
console.log(err)
}
console.log(‘Listener Added!’);
}, 30000);
<

finally, adding the code (as arham indicated) in the python dash code script

html.Button(‘CREATE PDF’, id=‘run’) <

I actually was able to create a pdf - see below.

The only thing is that the code creates the pdf slowly. I think this is because it takes a while before the page understands that there is external code (in the webpage console log I see that it takes a bit before i get the message “Listener Added!”. After this message is printed the button works. Then also, the main code has a while loop to assess whether several pdf pages are required (assuming a4 pdf size).

If anybody has any suggestions speeding up this code, please share - it would be greatly appreciated.

3 Likes

(FYI to any Dash Enterprise customers reading this thread: For production-ready programmatic PDF export of Dash apps, we recommend using the Dash Enterprise Snapshot Engine)

I’ve been able to achieve this using pdfkit and with clientside callbacks (I only want the pdf to be generated with a button click). I need landscape and multiple pages and pdfkit has been the easiest so far. Just need blob-stream-v0.1.2.js and pdfkit.js in assets folder.

app.clientside_callback(
        '''
        function (n_clicks) {
            if (n_clicks > 0) {
                var doc = new PDFDocument({layout:'landscape', margin: 25});
                var stream = doc.pipe(blobStream());
                
                doc.fontSize(28);
                doc.font('Helvetica-Bold');
                doc.text('Example'.toUpperCase(), 15, 40);
                doc.addPage().fontSize(28);
                doc.text('Showing that multiple pages work');
                doc.end();

                var saveData = (function () {
                    var a = document.createElement("a");
                    document.body.appendChild(a);
                    a.style = "display: none";
                    return function (blob, fileName) {
                        var url = window.URL.createObjectURL(blob);
                        a.href = url;
                        a.download = fileName;
                        a.click();
                        window.URL.revokeObjectURL(url);
                    };
                }());
                
                stream.on('finish', function() {
                
                  var blob = stream.toBlob('application/pdf');
                  saveData(blob, 'Report.pdf');
                
                    // iframe.src = stream.toBlobURL('application/pdf');
                });
            }
            return false;
        }
        ''',
        Output('AnythingHere', 'Unused_Property'),
        [
            Input('button', 'n_clicks'),
        ]
    )

My use case is to generate a chart pack. I turn all my figures into png images using figure.to_image() then grab them within the JavaScript via var parent = document.getElementById('div_containing_img') and add them to the report via doc.image(parent[0].src); I also have a html.H1 in the input so that the title of the page isn’t hardcoded.

3 Likes

Hey philphi, this is great, can you give me some information aboout the .js files?
“Just need blob-stream-v0.1.2.js and pdfkit.js in assets folder.” do you have some examples about this scripts?

thanks for all.

I found it in this following jsfiddle example: http://jsfiddle.net/viebel/3V6pZ/12. If you need direct links the following will directly download the files: https://github.com/devongovett/pdfkit/releases/download/v0.6.2/pdfkit.js
https://github.com/devongovett/blob-stream/releases/download/v0.1.2/blob-stream-v0.1.2.js

I just wrote the following that will print a 2-page landscape PDF with a plotly graph on the second page as an image:

import dash
import dash_core_components as dcc
import dash_html_components as html
import plotly.express as px
import pandas as pd
from dash.dependencies import Input, Output, State
import base64
import plotly.graph_objects as go

external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']

app = dash.Dash(__name__, external_stylesheets=external_stylesheets)

# assume you have a "long-form" data frame
# see https://plotly.com/python/px-arguments/ for more options
df = pd.DataFrame({
    "Fruit": ["Apples", "Oranges", "Bananas", "Apples", "Oranges", "Bananas"],
    "Amount": [4, 1, 2, 2, 4, 5],
    "City": ["SF", "SF", "SF", "Montreal", "Montreal", "Montreal"]
})

fig = px.bar(df, x="Fruit", y="Amount", color="City", barmode="group")

app.layout = html.Div(children=[
    html.H1(children='Hello Dash'),

    html.Div(children='''
        Dash: A web application framework for Python.
    '''),

    html.Button('Graph to PDF', id='button'),

    dcc.Graph(
        id='example-graph',
        figure=fig
    ),

    html.Div(id='graph_img')
])


app.clientside_callback(
    '''
    function (chart_children) {
        if (chart_children.type == "Img") {
            console.log(chart_children);
            var doc = new PDFDocument({layout:'landscape', margin: 25});
            var stream = doc.pipe(blobStream());

            doc.fontSize(28);
            doc.font('Helvetica-Bold');
            doc.text('Example'.toUpperCase(), 15, 40);
            doc.addPage().fontSize(28);
            doc.text('Showing that multiple pages work');
            doc.image(chart_children.props.src, {width: 780});
            doc.end();

            var saveData = (function () {
                var a = document.createElement("a");
                document.body.appendChild(a);
                a.style = "display: none";
                return function (blob, fileName) {
                    var url = window.URL.createObjectURL(blob);
                    a.href = url;
                    a.download = fileName;
                    a.click();
                    window.URL.revokeObjectURL(url);
                };
            }());

            stream.on('finish', function() {

              var blob = stream.toBlob('application/pdf');
              saveData(blob, 'Report.pdf');

                // iframe.src = stream.toBlobURL('application/pdf');
            });
        }
        return 0;
    }
    ''',
    Output('graph_img', 'n_clicks'),
    [
        Input('graph_img', 'children'),
    ]
)


@app.callback(
    Output('graph_img', 'children'),
    [
        Input('button', 'n_clicks')
    ],
    [
        State('example-graph', 'figure')
    ]
)
def figure_to_image(n_clicks, figure_dict):
    if n_clicks:
        # Higher scale = better resolution but also takes longer/larger size
        figure = go.Figure(figure_dict)
        img_uri = figure.to_image(format="png", scale=3)
        src = "data:image/png;base64," + base64.b64encode(img_uri).decode('utf8')
        return html.Img(src=src)
    return ''


if __name__ == '__main__':
    app.run_server(debug=True)

There is a chained callback where the button first converts the graph to an image (which is shown) then that triggers the clientside callback to generate a pdf via PDFKit. This example will only work if you have an assets folder in the same directory location as the script above and the two JS files in there.

1 Like

Great! I will give it a try!

Thanks so much!

Hahaha what if you aren’t a dash enterprise customer :frowning:

2 Likes

Hi @philphi… Thank you very much for your contribution to it so far, matte;

Someone has tried the solution and it worked?

I tried to implement philphi’s solution with but I’m getting an error. I would appreciate it a lot if someone can give me some help to make it works (because I’m not experienced in JS)

Error on the dash callback:

PDFDocument is not defined

image


The error on console is:

>  dash_renderer.v1_9_0m1611578476.dev.js:100499 ReferenceError: PDFDocument is not defined
    at Object.ns.n_clicks ((index):35)
    at handleClientside (dash_renderer.v1_9_0m1611578476.dev.js:93282)
    at dash_renderer.v1_9_0m1611578476.dev.js:93526
    at new Promise (<anonymous>)
    at executeCallback (dash_renderer.v1_9_0m1611578476.dev.js:93513)
    at dash_renderer.v1_9_0m1611578476.dev.js:99173
    at _map (dash_renderer.v1_9_0m1611578476.dev.js:75415)
    at map (dash_renderer.v1_9_0m1611578476.dev.js:78219)
    at dash_renderer.v1_9_0m1611578476.dev.js:74584
    at f2 (dash_renderer.v1_9_0m1611578476.dev.js:74400)


It's generating the png image, but do not creating the any pdf

Thank you in advance guys;

Hi,

It can’t find the JS files. Where your python file is it must have an assets folder in the same directory with the two JS files

1 Like

Hey bro, thank you very much, it worked now!!!

As I did create a folder called “experiment”, I thought that it was getting the assets of the main folder, but it wasn’t finding the folder. I tried to validate if the .js file was really working, but I did not find references to be sure about it !! My fault!

Thanks for the answer and the great work!

Hi @philphi , thanks for the awesome example, was wondering if this could be applied to a dash_table.DataTable instead of a figure? Thanks for your time!

1 Like