Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for rendering bokeh and plotly plots as gifs #4017

Merged
merged 2 commits into from
Oct 4, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 27 additions & 8 deletions holoviews/plotting/bokeh/renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class BokehRenderer(Renderer):

holomap = param.ObjectSelector(default='auto',
objects=['widgets', 'scrubber',
None, 'auto'], doc="""
None, 'gif', 'auto'], doc="""
Output render multi-frame (typically animated) format. If
None, no multi-frame rendering will occur.""")

Expand All @@ -51,7 +51,7 @@ class BokehRenderer(Renderer):

# Defines the valid output formats for each mode.
mode_formats = {'fig': ['html', 'auto', 'png'],
'holomap': ['widgets', 'scrubber', 'auto', None]}
'holomap': ['widgets', 'scrubber', 'gif', 'auto', None]}

_loaded = False
_render_with_panel = True
Expand Down Expand Up @@ -99,20 +99,39 @@ def _figure_data(self, plot, fmt, doc=None, as_script=False, **kwargs):
logger = logging.getLogger(bokeh.core.validation.check.__file__)
logger.disabled = True

if fmt == 'png':
if fmt == 'gif':
from bokeh.io.export import get_screenshot_as_png, create_webdriver
webdriver = create_webdriver()

nframes = len(plot)
frames = []
for i in range(nframes):
plot.update(i)
img = get_screenshot_as_png(plot.state, webdriver)
frames.append(img)
webdriver.close()

bio = BytesIO()
duration = (1./self.fps)*1000
frames[0].save(bio, format='GIF', append_images=frames[1:],
save_all=True, duration=duration, loop=0)
bio.seek(0)
data = bio.read()
elif fmt == 'png':
from bokeh.io.export import get_screenshot_as_png
img = get_screenshot_as_png(plot.state, None)
imgByteArr = BytesIO()
img.save(imgByteArr, format='PNG')
data = imgByteArr.getvalue()
if as_script:
b64 = base64.b64encode(data).decode("utf-8")
(mime_type, tag) = MIME_TYPES[fmt], HTML_TAGS[fmt]
src = HTML_TAGS['base64'].format(mime_type=mime_type, b64=b64)
div = tag.format(src=src, mime_type=mime_type, css='')
else:
div = render_mimebundle(plot.state, doc, plot.comm)[0]['text/html']

if as_script and fmt in ['png', 'gif']:
b64 = base64.b64encode(data).decode("utf-8")
(mime_type, tag) = MIME_TYPES[fmt], HTML_TAGS[fmt]
src = HTML_TAGS['base64'].format(mime_type=mime_type, b64=b64)
div = tag.format(src=src, mime_type=mime_type, css='')

plot.document = doc
if as_script:
return div
Expand Down
65 changes: 51 additions & 14 deletions holoviews/plotting/plotly/renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import base64

from io import BytesIO

import param
import panel as pn

Expand Down Expand Up @@ -45,8 +47,15 @@ class PlotlyRenderer(Renderer):
Output render format for static figures. If None, no figure
rendering will occur. """)

holomap = param.ObjectSelector(default='auto',
objects=['scrubber','widgets', 'gif',
None, 'auto'], doc="""
Output render multi-frame (typically animated) format. If
None, no multi-frame rendering will occur.""")


mode_formats = {'fig': ['html', 'png', 'svg'],
'holomap': ['widgets', 'scrubber', 'auto']}
'holomap': ['widgets', 'scrubber', 'gif', 'auto']}

widgets = ['scrubber', 'widgets']

Expand Down Expand Up @@ -75,28 +84,56 @@ def get_plot_state(self_or_cls, obj, doc=None, renderer=None, **kwargs):


def _figure_data(self, plot, fmt, as_script=False, **kwargs):
# Wrapping plot.state in go.Figure here performs validation
# and applies any default theme.
figure = go.Figure(plot.state)
if fmt == 'gif':
import plotly.io as pio

if fmt in ('png', 'svg'):
from PIL import Image
from plotly.io.orca import ensure_server, shutdown_server, status

running = status.state == 'running'
if not running:
ensure_server()

nframes = len(plot)
frames = []
for i in range(nframes):
plot.update(i)
img_bytes = BytesIO()
figure = go.Figure(self.get_plot_state(plot))
img = pio.to_image(figure, 'png', validate=False)
img_bytes.write(img)
frames.append(Image.open(img_bytes))

if not running:
shutdown_server()

bio = BytesIO()
duration = (1./self.fps)*1000
frames[0].save(bio, format='GIF', append_images=frames[1:],
save_all=True, duration=duration, loop=0)
bio.seek(0)
data = bio.read()
elif fmt in ('png', 'svg'):
import plotly.io as pio

# Wrapping plot.state in go.Figure here performs validation
# and applies any default theme.
figure = go.Figure(self.get_plot_state(plot))
data = pio.to_image(figure, fmt)

if fmt == 'svg':
data = data.decode('utf-8')

if as_script:
b64 = base64.b64encode(data).decode("utf-8")
(mime_type, tag) = MIME_TYPES[fmt], HTML_TAGS[fmt]
src = HTML_TAGS['base64'].format(mime_type=mime_type, b64=b64)
div = tag.format(src=src, mime_type=mime_type, css='')
return div
else:
return data
else:
raise ValueError("Unsupported format: {fmt}".format(fmt=fmt))

if as_script:
b64 = base64.b64encode(data).decode("utf-8")
(mime_type, tag) = MIME_TYPES[fmt], HTML_TAGS[fmt]
src = HTML_TAGS['base64'].format(mime_type=mime_type, b64=b64)
div = tag.format(src=src, mime_type=mime_type, css='')
return div
return data


@classmethod
def plot_options(cls, obj, percent_size):
Expand Down