Workaround to save as SVG a GridBox object

Hello,

I would like to save as SVG file format a complex plot which is consisted of multiple subplots in a grid-like fashion. So far I managed to save it as PNG which is not acceptable in many scientific journals.

I followed these instructions [Setup and use svg export in bokeh/holoviews inside Docker (webdriver setup, browser installation) · GitHub] and was able to save as SVG simple plots, but not an hv.NdLayout object.

The commands I gave:

 ndlayout = hv.NdLayout(curve_2D_dict, kdims=["Mass", "Velocity"]).cols(5).opts(toolbar=None)
    file_name = "./Plots/a_nice_name"
    ndlayoutRendered = br.get_plot(ndlayout)
    ndlayoutRendered = ndlayoutRendered.state
    ndlayoutRendered.output_backend = "svg" <---- unexpected attribute raised
    export_svgs(ndlayoutRendered, filename=file_name + ".svg", webdriver=WEB_DRIVER)

I’m getting the following message:

“AttributeError: unexpected attribute ‘output_backend’ to GridBox, possible attributes are align, aspect_ratio, background, children, cols, css_classes, disabled, height, height_policy, js_event_callbacks, js_property_callbacks, margin, max_height, max_width, min_height, min_width, name, rows, sizing_mode, spacing, subscribed_events, syncable, tags, visible, width or width_policy”

Is there any workaround to save the object as SVG ? For example, to firstly convent into a different one ? I intend to acknowledge holoviews in a publication.

Thanks in advance.

Looked into this as I was planning to include some svg export function in my app (with lots of plots on a page) in the future as well.

Took me some testing, but I think I figured it out.

Basically you need to use the export_svg() from bokeh, not the export_svgs().

import panel as pn
import holoviews as hv
from bokeh.io import export_svgs, export_svg
import numpy as np
import bokeh as bk

from IPython.display import display_svg, display_png

pn.extension()
hv.extension('bokeh')

userlist = list('ab')

# create the plots
plots = {(plotid): 
    hv.Points({'x': np.random.random(100), 'y': np.random.random(100)}).opts(backend_opts={'plot.output_backend':'svg'})
    for plotid in ['PLOT-A', 'PLOT-B']
}

# create the layout
holomap = hv.HoloMap(plots)
layout = hv.NdLayout(holomap).opts()
layout.opts(width=600)

# display in my jupyter-lab
display(layout)

# get the bokeh render and access the underlying layout bokeh stuff
renderer = hv.renderer('bokeh')
renderer = renderer.instance(mode='default')

r_layout = renderer.get_plot(layout)
print(r_layout)
print(r_layout.state)


# export the plots in the layout all in one SVG
export_svg(r_layout.state, filename='test_layout_all_in_one.svg')

# export the plots in individual svg files
export_svgs(r_layout.state, filename='test_layout_individual.svg')

Hello johann,

Thanks for dedicating your time!

I forgot to mention that I’m not using a Jupyter Notebook but a plain python interpreter in PyCharm 2023.1.2 Community Edition. I don’t know if it plays any role.

The code you provided works, but there is always a but. The export_svg is able to export a single SVG file but it is still of PNG quality. I can tell that by zooming in the picture. This doesn’t happen in non NdLayout objects (where I use the code of the link I provided).

I tried to change the mode=“default” into mode=“server”, with no luck however.

I don’t know, any ideas ?

Hi, That gets interesting. I looked back into the test-files I created along the way. The latest one created by the example code above is really as you suggest a SVG with embedded PNG (it’s easy to see if you open it with an editor/vi or cat it in linux):

<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="600" height="330"><defs/><path fill="rgb(0,0,0)" stroke="none" paint-order="stroke" d="M 0 0 L 600 0 L 600 330 L 0 330 L 0 0 Z" fill-opacity="0"/><g transform="matrix(1, 0, 0, 1, 0, 30)"><path fill="rgb(0,0,0)" stroke="none" paint-order="stroke" d="M 0 30 L 600 30 L 600 330 L 0 330 L 0 30 Z" fill-opacity="0"/><image width="300" height="300" preserveAspectRatio="none" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAAEsCAYAAAB5fY51AAAAAXNSR0IArs4c6QAAIABJREFUeF7tnQl4VNXZx/+Z7NuQPWQH4gqIW4xoKCriEgli1Lq2VtvPqmjBimJpwURjPxTFClX4qrWL1SpdjAiKO7ihxaigyKaRLGQlyZB9ksnMfM97cNJhSDJ3Zu7cuXfue5+nT33Ivee893fO/c9Z3vO+IXa7/VwA9D++mAATYAKqJhBit9vLAZSp2ko2jgkwASYAgAWLuwETYAKaIcCCpZmmYkOZABNgweI+wASYgGYIsGBppqnYUCbABFiwuA8wASagGQIsWJppKjaUCTABFizuA0yACWiGAAuWZpqKDWUCTCAoBauyshJr1qw5qnWXL1+OgoICSa1eVVWFJUuW

But what’s more interesting is that I have an earlier test-version on my disk that contains real SVG.

So there seems to be a way to get it to work, I just need to figure out what I screwed up when I cleaned up the code.

Will take a look this week when I have some spare time. But didn’t play a lot, other than with the “mode” Server/default, sequence of things and reinstalling? selenium, phantomjs and pillow along the way.

I don’t think jupyter-lab makes a difference.
I briefly scanned the bokeh-discourse yesterday, the export_svg came in as a feature in bokeh-2.2.3 once the implemented GridPlot (to support svg) vs. GridBox.

1 Like

Hello johann,

I played alot, especially with the webdriver options, but didn’t succeed in making it work. I’m having these relevant modules in my conda env:

  • geckodriver 0.33.0
  • selenium 4.91
  • pillow 9.4.0
  • firefox 114.0
  • and holoviews 1.17.1.post8+g9d74f7ece

I will try to re-create from scratch the env. Moreover, does holoviews uses pillow underneath ? As far as I can tell, it doesn’t provide support for SVG and they are still looking into it (Add support for SVG · Issue #3509 · python-pillow/Pillow · GitHub).

Thanks again.

ok, weird - the code works again. I put everything into a standalone svg.py script that I run simply with $ python svg.py.

The script is now as simplified as possible

import panel as pn
import holoviews as hv
import bokeh as bk
from bokeh.io import export_svgs, export_svg
import numpy as np

pn.extension()
hv.extension('bokeh')

# create the plots
plots = {(plotid): 
    hv.Points({'x': np.random.random(100), 
               'y': np.random.random(100)}
             ).opts(backend_opts={'plot.output_backend':'svg'})
    for plotid in ['PLOT-A', 'PLOT-B']
}

# create the layout
holomap = hv.HoloMap(plots)
layout = hv.NdLayout(holomap).opts()
layout.opts(width=600)

# get the bokeh render and access the underlying layout bokeh stuff
renderer = hv.renderer('bokeh')
renderer = renderer.instance(mode='default')

r_layout = renderer.get_plot(layout)

# export the plots in the layout all in one SVG
export_svg(r_layout.state, filename='testNEW_layout_all_in_one.svg')

# export the plots in individual svg files
export_svgs(r_layout.state, filename='testNEW_layout_individual.svg')

Running it gives me the following output:

PS D:\tools\hansivex> python svg.py
C:\Python311\Lib\site-packages\holoviews\plotting\bokeh\plot.py:987: UserWarning: found multiple competing values for 'toolbar.active_drag' property; using the latest value
  layout_plot = gridplot(
C:\Python311\Lib\site-packages\holoviews\plotting\bokeh\plot.py:987: UserWarning: found multiple competing values for 'toolbar.active_scroll' property; using the latest value
  layout_plot = gridplot(

DevTools listening on ws://127.0.0.1:53827/devtools/browser/02520ec0-bffe-48be-8793-f2195fac9223
[0921/145634.645:INFO:CONSOLE(210)] "[bokeh] setting log level to: 'info'", source: file:///D:/tools/hansivex/bokehm8yflkp6.html (210)
[0921/145634.722:INFO:CONSOLE(192)] "[bokeh] document idle at 73 ms", source: file:///D:/tools/hansivex/bokehm8yflkp6.html (192)
[0921/145635.549:INFO:CONSOLE(210)] "[bokeh] setting log level to: 'info'", source: file:///D:/tools/hansivex/bokehnsgrj3lm.html (210)
[0921/145635.610:INFO:CONSOLE(192)] "[bokeh] document idle at 57 ms", source: file:///D:/tools/hansivex/bokehnsgrj3lm.html (192)
PS D:\tools\hansivex>

The resulting file looks like follows - a real svg one without embedded png. And it properly scales when opening in Firefox …

<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="600" height="330"><defs><clipPath id="lQkmiJDpXian"><path fill="none" stroke="none" d="M 52.5 29.5 L 290.5 29.5 L 290.5 255.5 L 52.5 255.5 L 52.5 29.5 Z"/></clipPath><clipPath id="mDQwGlzuPPEg"><path fill="none" stroke="none" d="M 52.5 29.5 L 290.5 29.5 L 290.5 255.5 L 52.5 255.5 L 52.5 29.5 Z"/></clipPath><clipPath id="DTHSXvcCeknL"><path fill="none" stroke="none" d="M 52.5 29.5 L 290.5 29.5 L 290.5 255.5 L 52.5 255.5 L 52.5 29.5 Z"/></clipPath><clipPath id="LCIFHHNrLveU"><path fill="none" stroke="none" d="M 52.5 29.5 L 290.5 29.5 L 290.5 255.5 L 52.5 255.5 L 52.5 29.5 Z"/></clipPath><clipPath id="FnPfQxJZNpol"><path fill="none" stroke="none" d="M 52.5 29.5 L 290.5 29.5 L 290.5 255.5 L 52.5 255.5 L 52.5 29.5 Z"/></clipPath><clipPath id="IhcPLnxCgAJD"><path fill="none" stroke="none" d="M 52.5 29.5 L 290.5 29.5 L 290.5 255.5 L 52.5 255.5 L 52.5 29.5 Z"/></clipPath><clipPath id="TaGMizyFhrMi"><path fill="none" stroke="none" d="M 52.5 29.5 L 290.5 29.5 L 290.5 255.5 L 52.5 255.5 L 52.5 29.5 Z"/></clipPath><clipPath id="IMyPutyavaPt"><path fill="none" stroke="none" d="M 52.5 29.5 L 290.5 29.5 L 290.5 255.5 L 52.5 255.5 L 52.5 29.5 Z"/></clipPath></defs><path fill="rgb(0,0,0)" stroke="none" paint-order="stroke" d="M 0 0 L 600 0 L 600 330 L 0 330 L 0 0 Z" fill-opacity="0"/><g transform="matrix(1, 0, 0, 1, 0, 30)"><path fill="rgb(0,0,0)" stroke="none" paint-order="stroke" d="M 0 30 L 600 30 L 600 330 L 0 330 L 0 30 Z" fill-opacity="0"/><path fill="rgb(255,255,255)" stroke="none" paint-order="stroke" d="M 0.5 0.5 L 300.5 0.5 L 300.5 300.5 L 0.5 300.5 L 0.5 0.5 Z" fill-opacity="1"/><rect fill="#FFFFFF" stroke="none" x="52" y="29" width="238" height="226" transform="matrix(1, 0, 0, 1, 0.5, 0.5)"/><path fill="rgb(255,255,255)" stroke="none" paint-order="stroke" d="M 52.5 29.5 L 290.5 29.5 L 290.5 255.5 L 52.5 255.5 L 52.5 29.5 Z" fill-opacity="1"/><path fill="none" stroke="rgb(229,229,229)" paint-order="fill" d="M 52.5 29.5 L 290.5 29.5 L 290.5 255.5 L 52.5 255.5 L 52.5 29.5 Z" stroke-opacity="1" stroke-linejoin="bevel" stroke-miterlimit="10"/><path fill="rgb(48,162,218)" stroke="rgb(48,162,218)" paint-order="fill" d="M 220.38319213701658 210.86875915527344 A 1.224744871391589 1.224744871391589 0 0 1 217.93370239423342 210.86875915527344 A 1.224744871391589 1.224744871391589 0 0 1 220.38319213701658 210.86875915527344" fill-opacity="1" clip-path="url(#DTHSXvcCeknL)" stroke-opacity="1" stroke-linejoin="bevel" stroke-miterlimit="10"/><path fill="rgb(48,162,218)" stroke="rgb(48,162,218)" paint-order="fill" d="M 259.4652233870166 210.9312286376953 A 1.224744871391589 1.224744871391589 0 0 1 257.0157336442334 210.9312286376953 A 1.224744871391589 1.224744871391589 0 0 1 259.4652233870166 210.9312286376953" fill-opacity="1" clip-path="url(#DTHSXvcCeknL)" stroke-opacity="1" stroke-linejoin="bevel" stroke-miterlimit="10"/><path fill="rgb(48,162,218)" stroke="rgb(48,162,218)" paint-order="fill" d="M 147.65798766924314 130.05035400390625 A 1.224744871391589 1.224744871391589 0 0 1 145.20849792645998 130.05035400390625 A 1.224744871391589 1.224744871391589 0 0 1 147.65798766924314 130.05035400390625" fill-opacity="1" clip-path="url(#DTHSXvcCeknL)" stroke-opacity="1" stroke-linejoin="bevel" stroke-miterlimit="10"/><path fill="rgb(48,162,218)" stroke="rgb(48,162,218)" paint-order="fill" d="M 271.8914013655322 168.8401336669922 A 1.224744871391589 1.224744871391589 0 0 1 269.44191162274905 168.8401336669922 A 1.224744871391589 1.224744871391589 0 0 1 271.8914013655322 168.8401336669922" fill-opacity="1" clip-path="url(#DTHSXvcCeknL)" stroke-opacity="1" stroke-linejoin="bevel" stroke-miterlimit="10"/><path fill="rgb(48,162,218)" stroke="rgb(48,162,218)" paint-order="fill" d="M 125.2844372542041 116.4220199584961 A 1.224744871391589 1.224744871391589 0 0 1 122.8349475114209 116.4220199584961 A 1.224744871391589 1.224744871391589 0 0 1 125.2844372542041 116.4220199584961" fill-opacity="1" clip-path="url(#DTHSXvcCeknL)" stroke-opacity="1" stroke-linejoin="bevel" stroke-miterlimit="10"/><path fill="rgb(48,162,218)" stroke="rgb(48,162,218)" paint-order="fill" d="M 201.11024291826658 87.79862976074219 A 1.224744871391589 1.224744871391589 0 0 1 198.66075317548342 87.79862976074219 A 1.224744871391589 1.224744871391589 0 0 1 201.11024291826658 87.79862976074219" fill-opacity="1" clip-path="url(#DTHSXvcCeknL)" stroke-opacity="1" stroke-linejoin="bevel" strok .....

I install my modules via pip (running everything on a Windows10 OS).
phantomjs 1.4.1
selenium-4.12.0
holoviews-1.17.1
bokeh-3.2.2

hmm …

Tried it also on a blank debian-linux (mounted via WSL2 on Windows10).
Worked ok there as well, producing svg output without embedded png.

Installed python3.9 (via apt) and the latest modules via pip (panel1.2.3, holoviews1.7.1 , selenium4.12.0, phantomjs1.4.1) and then latest firefox and geckodriver from mozilla-github).

Ran the same “python3.9 svg.py”

I’m running out of ideas, next step would be likely to debug somewhere between the export_svg and the webdriver/phantomjs handoff.

It works for me as well. I’m using Ubuntu 22.04.

But I have another issue with the xlabel and ylabel rendering blurry, while the rest of the plot renders perfectly and scales. I think it is the LaTeX command I gave is to blame, as below:

curve_2D_dict[(key_mass, key_vel)] = compElement.opts(
                xlabel=r"\[f\;\;\;(\mathrm{kHz})\]",
                ylabel=r"\[\tilde{h}(f)\sqrt{f}\;\;\;(\mathrm{Hz}^{1/2})\]",
                fontsize={'title': 9, 'labels': 8, 'xticks': 11, 'yticks': 11, 'legend': 6}, text_font='Times New Roman',
                title=key_vel + ", " + key_mass + ", " + "FF: " + str(np.round(fittingFactor, 3)),
                backend_opts={'plot.output_backend':'svg'}, toolbar=None)

I have to put exponents and mathematical symbols there. I think it has nothing to do with the rendering/export mechanism of svg.

Well, I think I can live with this situation. Despite being cumbersome, I will manually edit the SVG files to put the labels using InkScape or an equivalent software.

Thanks once again! Considered solved!

Cheers.