Generate SVG images from Bokeh Plots and include them in a PDF Report with using "svglib" and "reportlab"

Hello!!

I have created a script that generates a pdf with SVG images taken from a bokeh plot. It was done with the python libraries reportlab and svglib. I have followed these basic steps:

  1. Use the export_svgs builtin function in order to get the SVG file
  2. Create a Flowable object to add it to the pdf in reportlab
  3. Creating pdf reports with reportlab by the platypus methd (building the pdf with a bunch of elements and template).

As export_svgs can only export one plot at the same time, this may also useful if somebody want to export a grid of plots or a more complex layout.

from reportlab.lib.pagesizes import A4
from reportlab.platypus import Table, TableStyle, SimpleDocTemplate
from reportlab.lib import colors
from reportlab.lib.units import mm

from svglib.svglib import svg2rlg

import numpy as np
from bokeh.plotting import figure
from bokeh.io import curdoc, export_svgs, export_png
from bokeh.models import Button

def get_plot():
    N = 3000
    x = np.random.random(size=N) * 100
    y = np.random.random(size=N) * 100
    radii = np.random.random(size=N) * 1.5

    p = figure(
        x_axis_label='X axis',
        y_axis_label='Y axis',
        title='Random plot',
        tools="crosshair,pan,wheel_zoom,box_zoom,reset,tap,save,lasso_select",
        output_backend = "webgl"
    )

    p.scatter(
        x=x,
        y=y,
        radius=radii,
        fill_color='red',
        fill_alpha=0.6,
        line_color=None
    )

    print('Exporting plot.svg...')
    p.output_backend = "svg"
    export_svgs(p, filename="plot.svg")

    # create a new SVG or a PDF with the new layout
    print('Reading drawing from plot.svg...')
    return svg2rlg("plot.svg")

drawing = get_plot()

print('Scaling SVG image...')
sx = sy = 0.3  # scaling factor
drawing.width = drawing.minWidth() * sx
drawing.height = drawing.height * sy
drawing.scale(sx, sy)

data = [
    ['TABLE HEADER', None],  # this works as a leading row
    [drawing, drawing],
    [drawing, drawing],
    [drawing, drawing],
    [drawing, drawing],
    [drawing, drawing],
    [drawing, None]
]

table = Table(     # Flowable object
    data,
    colWidths=200,
    rowHeights=[1 * mm] + [drawing.height] * (len(data) - 1),
    # hAlign='LEFT',
    repeatRows=1
)

table.setStyle(TableStyle([
    # LEADING ROW
    ('LINEBELOW', (0, 0), (1, 0), 0.5, colors.black),
    ('SPAN', (0, 0), (1, 0)),                           # colspan
    ('FONTSIZE', (0, 0), (1, 0), 12),
    ('LEFTPADDING', (0, 0), (1, 0), 0),
    ('BOTTOMPADDING', (0, 0), (1, 0), 3),

    # REST OF ROWS
    ('LEFTPADDING', (1, 1), (-1, -1), 0),
    ('RIGHTPADDING', (1, 1), (-1, -1), 0),
    ('BOTTOMPADDING', (1, 1), (-1, -1), 0),
    ('TOPPADDING', (1, 1), (-1, -1), 0),
]))

story = []
story.append(table)

margin = 25 * mm
doc = SimpleDocTemplate(
    'minimal_reportlab_table_.pdf', pagesize=A4,
    rightMargin=margin, leftMargin=margin,
    topMargin=margin, bottomMargin=margin
)

doc.build(story)

If you run this code you may read a warning ā€œUnable to find a suitable font for ā€˜font-family:helveticaā€™ā€. But I did not paid attention to it yet.

References:

3 Likes

This is exciting! I have opened an issue [FEATURE] SVG export for gridplot Ā· Issue #9169 Ā· bokeh/bokeh Ā· GitHub as I have been wanting this feature when working on journal papers.

I did something similar and was thinking that it would be nice if rather than having to save the SVG file as a temporary file it would be great if Bokeh would allow a ByteIO stream with the SVG. Basically writing the svg to a file in memory then SVGlib can read the ByteIO stream.

1 Like

@Mark_Murphy I believe you could use get_svgs for that. It is the lower-level function that export_svgs uses internally. It could use a docstring if you want to contribute a small PR. (I donā€™t think it was originally conceived as public API but itā€™s in __all__ now, so itā€™s not going anywhere.)

For your interest, I have tried with the wkhtmltopdf library. This library is used to convert the whole html page into a pdf file. It worked years ago, but now, at least with the latest version it is not working. I may open another issue to ask what is going on

Anyway I am afraid canvas were renderer as bitmaps (jpg or png), with low resolution, at least non-svg images. With other js graphic libraries seems to work well

Thanks for the suggestion. I was developing this for another group and they had multiple graphs on the same page. This caused a lot of problems because the HTML conversion then to PDF would cause the graphs to be cut off. Also SVG is nice because it can handle scaling much better then any images.

Thanks if I find this does not work Iā€™ll try it out tho.

@Mark_Murphy To return string buffers instead of writing into file, I think with the following code would be enough (I have added a new boolean parameter). I tested it with a couple of examples and it worked:

def get_svgs(obj, driver=None, timeout=5, buffer=False, **kwargs):
    with _tmp_html() as tmp:
        html = get_layout_html(obj, **kwargs)
        with io.open(tmp.path, mode="w", encoding="utf-8") as file:
            file.write(decode_utf8(html))
        web_driver = driver if driver is not None else webdriver_control.get()
        web_driver.get("file:///" + tmp.path)
        wait_until_render_complete(web_driver, timeout)
        svgs = web_driver.execute_script(_SVG_SCRIPT)

    if buffer:
        buffer_list = [StringIO(x) for x in svgs]
        return buffer_list
    else:
        return svgs

And I had to change the code in my example a little bit to make it work with buffers:

svgs = get_svgs(p, buffer=True)
return svg2rlg(svgs[0])

I can do the PR if it is not already done (also adding the docstring)

1 Like