holoviews icon indicating copy to clipboard operation
holoviews copied to clipboard

SVG Support for Bokeh Backend

Open ahuang11 opened this issue 6 years ago • 9 comments

I wrote this a while back for HoloExt; was wondering if there's any interest for me to make a PR for this?

import os
from bokeh import models
from bokeh.io import export_svgs
from holoext.utils import DEFAULT_N, get_cmap, flatten, tidy_fn

def _get_figures_core(objs):
    if isinstance(objs, list):
        objs = [_get_figures_core(plot) for plot in objs]
    elif isinstance(objs, (models.Column, models.Row)):
        objs = [_get_figures_core(child) for child in objs.children
                if not isinstance(child, (models.ToolbarBox,
                                          models.WidgetBox))]
    return objs

def _get_figures(objs):
    try:
        return list(flatten(_get_figures_core(objs)))
    except TypeError:
        return [_get_figures_core(objs)]

def _save_to_svg(hv_obj, save):
    bokeh_obj = hv.renderer('bokeh').get_plot(hv_obj).state
    figures = _get_figures(bokeh_obj)
    for i, figure in enumerate(figures):
        figure.output_backend = 'svg'

        if len(figures) != 1:
            if not os.path.exists(save):
                os.mkdir(save)
            tidied_title = tidy_fn(figure.title.text)
            save_fp = os.path.join(
                save, '{0}_{1}'.format(tidied_title, i))
        else:
            save_fp = save

        if not save_fp.endswith('svg'):
            save_fp = '{0}.{1}'.format(save_fp, 'svg')

        export_svgs(figure, save_fp)
import numpy as np
import holoviews as hv
hv.extension('bokeh')

frequencies = [0.5, 0.75, 1.0, 1.25]

def sine_curve(phase, freq):
    xvals = [0.1* i for i in range(100)]
    return hv.Curve((xvals, [np.sin(phase+freq*x) for x in xvals]))

curve_dict = {f:sine_curve(0,f) for f in frequencies}

NdLayout = hv.NdLayout(curve_dict, kdims='frequency')
_save_to_svg(NdLayout, 'ndlayout_svgs')

image image

ahuang11 avatar Aug 29 '18 16:08 ahuang11

There is definitely interest, I initially didn't implement this because each subplot is exported individually but we can start with that and maybe work out how to combine the individual svgs later.

philippjfr avatar Sep 01 '18 17:09 philippjfr

Okay, I thought it would be a straightforward implementation, but the renderer pipeline is confusing to me.

My attempt https://github.com/ioam/holoviews/compare/export_svg?expand=1

It works on a single element, but with multiple elements, it now complains about RuntimeError: Models must be owned by only a single document, Range1d(id='7308857a-9088-49fb-9be8-993d2d514c40', ...) is already in a doc even though I just copied the code straight from holoext, change a couple variable names and add self_or_cls to the functions themselves, which I don't fully understand either (why it's self_or_cls instead of self).

ahuang11 avatar Sep 03 '18 03:09 ahuang11

Looks like a good start, thanks! Can't immediately see the issue but it may be as simple as adding a simple function that removes the existing document from the models.

philippjfr avatar Sep 05 '18 12:09 philippjfr

Hi guys, I think I'm missing something. I generated from Holoviews some graphs (Scatterplot, Violinplot) and managed to save all of them in svg format, using the following code:

#set the backend and the renderer
hv.extension('bokeh')
br = hv.renderer('bokeh')

#set the data
np.random.seed(37)
groups = [chr(65+g) for g in np.random.randint(0, 3, 200)]

#plot 
violin = hv.Violin((groups, np.random.randint(0, 5, 200), np.random.randn(200)),
          ['Group', 'Category'], 'Value')

#export the plot
plot = br.get_plot(violin )
plot = plot.state
plot.output_backend='svg'
export_svgs(plot, filename="Violin.svg")

It seems to work properly, so I can't understand why Bokeh's guide (https://bokeh.pydata.org/en/latest/docs/user_guide/compat.html) and the code behind Bokeh's renderer in Holoviews state it is not possible doing this (i.e. exporting to svg format using Bokeh backend, while it is possible from matplotlib). Is there any limitation I can't see that is preventing this function to be included in Holoviews?

RColetto avatar Sep 26 '18 09:09 RColetto

The problematic part is when you try to export a layout with multiple plots. Currently bokeh will export each subplot as a separate SVG. There is of course still some value in that and it might be worth adding but it's not as simple as other exporters which can render everything as one single file.

philippjfr avatar Sep 26 '18 11:09 philippjfr

I understand the issue, but couldn't it be useful to add this note somewhere in the docs? I don't think it's necessary to remove the functionality altogether, because it's common to use a SVG export on a single graph. In my use case, I can use the workaround i posted, but it's not necessary. In my opinion this limitation of the SVG export is not enough to prevent the entire use of the function, but your opinion may be different. Anyhow, a simple note in the documentation can help other users with a use case similar to ours. Thank you anyway for the quick response :-)

RColetto avatar Sep 26 '18 12:09 RColetto

As of Bokeh 0.12.6 it is also possible to export to SVG directly from python. I found this solution pretty straight forward:

from bokeh.io import export_svgs
p =  hv.render(heatmap, backend='bokeh')
p.output_backend = "svg"
export_svgs(p, filename="heatmap.svg")

Sieboldianus avatar Mar 22 '19 08:03 Sieboldianus

Interestingly, neither of the solutions posted by @RColetto and @Sieboldianus seem to be working on a simple hv.Scatter example using HoloViews 1.12.7:

Both...

br = hv.renderer('bokeh')
x = hv.Scatter(np.random.rand(100,2))
plot = br.get_plot(x)
plot = plot.state
plot.output_backend='svg'
export_svgs(plot, filename="out.svg")

and...

x = hv.Scatter(np.random.rand(100,2))
p =  hv.render(x, backend='bokeh')
p.output_backend = "svg"
export_svgs(p, filename="out.svg")

yield an svg which contains the axes but no points... image

I'll be excited to see the hv.Layout svg rendering with bokeh backend, since I'm hoping to make plots of the type:

image

...essentially a collection of bivariate plots for QC of automated intensity thresholding.

eburling avatar Jan 28 '20 00:01 eburling

@eburling I tested your example and it works for me: out

Still, the way all of this needs to be setup deserves more attention in the Bokeh/Holoviews docs.

I added a gist that shows how to install Chrome, chromedriver (webdriver) and example code for svg_export as of Bokeh 2.4.3 / Holoviews 1.14.8.

[btw. I think this issue can be closed.]

Sieboldianus avatar Aug 30 '22 07:08 Sieboldianus

Unfortunately, the Bokeh svg export does not respect themes as reported in this bug. Apparently, this has since been fixed for Bokeh >3. So as far as I can see there is currently no way to get a vector graphics export from holoviews using bokeh with non-default or customised themes, right? Is there a roadmap when holoviews might be compatible with Bokeh >3? Thanks

W-L avatar Apr 19 '23 15:04 W-L

Is there a roadmap when holoviews might be compatible with Bokeh >3?

It is planned to be released next month. If absolutely needed, you can play around with the pre-release version using pip install holoviews --pre or conda install holoviews=1.16 -c pyviz/label/dev.

hoxbro avatar Apr 19 '23 15:04 hoxbro