Snippet Wednesday - How to add a Plotly figure in your report đź“ť

For this week, I’ve decided to dedicate a snippet on a development that I am seeing recurring in many projects. Many developers are creating user-friendly plots and graphs using the graphing libary plotly. But when it comes to the point where they have to include the figure in a report that needs to be downloaded, they get stuck. So, to help all future developers, herewith a step-by-step guide to do this.

The Snippet

This snippet will go through the following steps:

  1. Create a Plotly figure and PlotlyView
  2. Converting this figure to a static image (PNG)
  3. Adding this to a Word report
  4. Displaying as a PDFView

1. Plotly figure and PlotlyView

First of, let us take an example of a plotly figure that is rendered as a view:

👇Click here to see the code 👇
class Controller(ViktorController):
    label = '...'
    parametrization = Parametrization

    def get_plotly_figure(self):
        """Create 3D surface plot.

        Source: https://plotly.com/python/3d-surface-plots/
        """
        # Read data from a csv
        z_data = pd.read_csv('https://raw.githubusercontent.com/plotly/datasets/master/api_docs/mt_bruno_elevation.csv')

        fig = go.Figure(data=[go.Surface(z=z_data.values)])
        camera = dict(
            eye=dict(x=1.5, y=1.5, z=0.1)
        )

        fig.update_layout(title='Mt Bruno Elevation', scene_camera=camera)
        return fig

    @PlotlyView("Plotly view", duration_guess=1)
    def get_plotly_view(self, params, **kwargs):
        fig = self.get_plotly_figure()
        return PlotlyResult(fig.to_json())

2. Convert figure to a static image

To convert your plotly figure to a static PNG image, the following steps should be performed:

2.1. Add kaleido to your requirements.

:exclamation:THE FOLLOWING IS VERY IMPORTANT :exclamation:

There seems to be an issue with kaleido that, when using the wrong version, it hangs during the process of converting the image. What makes it more annoying is that the version differs per operating system. Therefore, take note of the following:

When developing on Windows using venv, the conversion only works with versions kaleido==0.1.*. Therefore, only during development, install kaleido==0.1.0.post1.
For Linux developers and publishing, use kaleido==0.2.1.

Or to make life easier for all (this is a tip I got from my colleague @regbers ), simply add the following the requirements.txt file:

kaleido==0.2.1; platform_system == "Linux"
kaleido==0.1.0.post1; platform_system == "Windows"

2.2. Use the following code to convert your image

fig = self.get_plotly_figure()
# https://plotly.com/python/static-image-export/
img_bytes = BytesIO(fig.to_image(format="png", scale=2))

3. Add image to Word document

Adding an image to your Word template is quite straightforward once you’ve done the tutorial on Automatic Reporting, so I would recommend taking a look at that if you have not yet. Here is the snippet of the method that generates a report with the image:

    def generate_word_document(self, params):
        # Create emtpy components list to be filled later
        components = []

        # Fill components list with data
        fig = self.get_plotly_figure()
        # https://plotly.com/python/static-image-export/
        img_bytes = BytesIO(fig.to_image(format="png", scale=2))
        word_file_image = WordFileImage(img_bytes, 'img_tag', width=400)
        components.append(word_file_image)

        # Get path to template and render word file
        template_path = Path(__file__).parent / "report_template.docx"
        with open(template_path, 'rb') as template:
            word_file = render_word_file(template, components)

        return word_file

4. Display as PDF

To round it off, you could download it as a Word document using a DownloadButton, or view as a PDF. I like to view it within the app, so I chose for the latter:

All code

👇Click here to see the code 👇

requirements.txt

viktor
pandas
kaleido==0.2.1; platform_system == "Linux"
kaleido==0.1.0.post1; platform_system == "Windows"

app.py

from io import BytesIO
from pathlib import Path

import pandas as pd
import plotly.graph_objects as go

from viktor import ViktorController
from viktor.external.word import WordFileImage, render_word_file
from viktor.utils import convert_word_to_pdf
from viktor.views import PlotlyView, PlotlyResult, PDFView, PDFResult
from viktor.parametrization import ViktorParametrization


class Parametrization(ViktorParametrization):
    pass


class Controller(ViktorController):
    label = '...'
    parametrization = Parametrization

    def get_plotly_figure(self):
        """Create 3D surface plot.

        Source: https://plotly.com/python/3d-surface-plots/
        """
        # Read data from a csv
        z_data = pd.read_csv('https://raw.githubusercontent.com/plotly/datasets/master/api_docs/mt_bruno_elevation.csv')

        fig = go.Figure(data=[go.Surface(z=z_data.values)])
        camera = dict(
            eye=dict(x=1.5, y=1.5, z=0.1)
        )

        fig.update_layout(title='Mt Bruno Elevation', scene_camera=camera)
        return fig

    @PlotlyView("Plotly view", duration_guess=1)
    def get_plotly_view(self, params, **kwargs):
        fig = self.get_plotly_figure()
        return PlotlyResult(fig.to_json())

    def generate_word_document(self, params):
        # Create emtpy components list to be filled later
        components = []

        # Fill components list with data
        fig = self.get_plotly_figure()
        # https://plotly.com/python/static-image-export/
        img_bytes = BytesIO(fig.to_image(format="png", scale=2))
        word_file_image = WordFileImage(img_bytes, 'img_tag', width=400)
        components.append(word_file_image)

        # Get path to template and render word file
        template_path = Path(__file__).parent / "report_template.docx"
        with open(template_path, 'rb') as template:
            word_file = render_word_file(template, components)

        return word_file

    @PDFView("PDF viewer", duration_guess=5)
    def pdf_view(self, params, **kwargs):
        word_file = self.generate_word_document(params)

        with word_file.open_binary() as f1:
            pdf_file = convert_word_to_pdf(f1)

        return PDFResult(file=pdf_file)

Wrap up

I hope this helps all developers that are struggling with this. For anyone that finds that they can add to this topic, or has other suggestions/alternatives, it would be great to hear your input.

6 Likes

Wow, nice post Marcel!!!

Thanks Marcel! That is a common problem when working with Plotly.

1 Like

Great post Marcel, may I ask you some advice?

Using the snippet below:

from io import BytesIO

import pandas as pd
from plotly import graph_objects as go


def get_plotly_figure():
    # Read data from a csv
    z_data = pd.read_csv('https://raw.githubusercontent.com/plotly/datasets/master/api_docs/mt_bruno_elevation.csv')

    fig = go.Figure(data=[go.Surface(z=z_data.values)])
    camera = dict(
        eye=dict(x=1.5, y=1.5, z=0.1)
    )

    fig.update_layout(title='Mt Bruno Elevation', scene_camera=camera)
    return fig

_fig = get_plotly_figure()
img = BytesIO(_fig.to_image(format="png", scale=2))

# Save to disk
with open("test.png", "wb") as f:
    f.write(img.getvalue())
print("Finished")

This method works perfectly fine, however once I install Viktor with v install it no longer works.
My requirements are as follows:

viktor==14.6.1
markdown==3.5.1
lxml==4.9.3
numpy==1.26.1
matplotlib==3.8.1
pandas==2.1.2
python-docx==1.0.1
pathlib==1.0.1
shapely==2.0.2
plotly==5.18.0
d-geolib==0.1.8
openpyxl==3.1.2
hypothesis==6.88.1
requests==2.31.0
kaleido==0.2.1; platform_system == "Linux"
kaleido==0.1.0.post1; platform_system == "Windows"

The error I get is as following:

Traceback (most recent call last):
  File "C:\Users\johan.tuls\AppData\Roaming\JetBrains\PyCharmCE2023.2\scratches\scratch_53.py", line 20, in <module>
    img = BytesIO(_fig.to_image(format="png", scale=2))
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "....\venv\Lib\site-packages\plotly\basedatatypes.py", line 3778, in to_image
    return pio.to_image(self, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "...\venv\Lib\site-packages\plotly\io\_kaleido.py", line 143, in to_image
    img_bytes = scope.transform(
                ^^^^^^^^^^^^^^^^
  File "...\venv\Lib\site-packages\kaleido\scopes\plotly.py", line 111, in transform
    raise ValueError(
ValueError: Transform failed with error code 1: Failed to serialize document: Uncaught

@mslootweg any idea why this changes suddenly once I install Viktor?

I have checked using the same repository on a (newer) laptop of a colleague and it works just fine. So I suppose it has something to do with my laptop settings. It happens to be the case that I will get a new laptop today, so I will try it out using a clean environment. I’ll let you know what my findings are, as this is one of the issues which gives problems by certain apps, while others have no problem at all.

Hi Johan,

Thanks for raising this! I am not sure what causes this, but good that you put it here. Hopefully if another community member runs into this, they can provide more context.

On my new laptop it seems to be working whoop whoop!!

The weird thing is:

  • The first three applications didn’t give any problems at all
  • Then we start using WSL for a while
  • All projects / apps afterwards didn’t work
  • Changing my laptop (4 years later) and it works (for now)

Which would be awesome, as only the first 3 apps were able to use Plotly images in word files. As it worked sometimes and sometimes it didn’t.

So my hypothosis is that it is related to Ubuntu WSL, however, this is just a wild guess…

Thanks for the update!

Hi Marcel, great snippet of code!

We’ve found that although working in our development workspace, the figures aren’t being generated in production. Hard for us to debug as we don’t use Linux or docker at the moment.

Any ideas on how we might resolve this?

Using python 3.10 and have separate requirements for the Windows and Linux platform systems.

kaleido==0.2.1; platform_system == “Linux”
kaleido==0.1.0.post1; platform_system == “Windows”

I just tried reproducing the example to see what the behavior is on Linux. With the following set up it adds the figure to the report without problems (both locally and using the deployed app):

viktor.config.toml

app_type = 'editor'
python_version = '3.10'

requirements.txt

viktor
kaleido==0.2.1
plotly
pandas

app.py

from io import BytesIO
from pathlib import Path

import viktor as vkt
import plotly.graph_objects as go
import pandas as pd


class Parametrization(vkt.Parametrization):
    download = vkt.DownloadButton("Download report", "generate_word_document")

class Controller(vkt.Controller):
    parametrization = Parametrization

    def get_plotly_figure(self):
            """Create 3D surface plot.

            Source: https://plotly.com/python/3d-surface-plots/
            """
            # Read data from a csv
            z_data = pd.read_csv('https://raw.githubusercontent.com/plotly/datasets/master/api_docs/mt_bruno_elevation.csv')

            fig = go.Figure(data=[go.Surface(z=z_data.values)])
            camera = dict(
                eye=dict(x=1.5, y=1.5, z=0.1)
            )

            fig.update_layout(title='Mt Bruno Elevation', scene_camera=camera)
            return fig
    
    @vkt.PlotlyView("Plotly view", duration_guess=1)
    def get_plotly_view(self, params, **kwargs):
        fig = self.get_plotly_figure()
        return vkt.PlotlyResult(fig.to_json())
    
    def generate_word_document(self, params, **kwargs):
        # Create emtpy components list to be filled later
        components = []

        # Fill components list with data
        fig = self.get_plotly_figure()
        img_bytes = BytesIO(fig.to_image(format="png", scale=2))
        word_file_image = vkt.word.WordFileImage(img_bytes, 'img_tag', width=400)
        components.append(word_file_image)

        # Get path to template and render word file
        template_path = Path(__file__).parent / "report_template.docx"
        with open(template_path, 'rb') as template:
            word_file = vkt.word.render_word_file(template, components)
        return vkt.DownloadResult(file_content=word_file, file_name="report.docx")

word template

report_template.docx (4.9 KB)

Maybe this helps!