Skip to content

Commit

Permalink
Initial commit of updated ViewMagic with support for saving output
Browse files Browse the repository at this point in the history
  • Loading branch information
jlstevens committed Feb 25, 2015
1 parent 7a5287b commit eb5f43c
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 65 deletions.
3 changes: 3 additions & 0 deletions holoviews/ipython/display_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ def get_plot_size(size):
def animate(anim, dpi, writer, mime_type, anim_kwargs, extra_args, tag):
if extra_args != []:
anim_kwargs = dict(anim_kwargs, extra_args=extra_args)
ViewMagic.save_anim(anim, mime_type, writer, dpi=dpi, **anim_kwargs)

if not hasattr(anim, '_encoded_video'):
with NamedTemporaryFile(suffix='.%s' % mime_type) as f:
Expand Down Expand Up @@ -106,6 +107,7 @@ def process_cell_magics(obj):
"Hook into %%opts and %%channels magics to process displayed element"
invalid_options = OptsMagic.process_view(obj)
if invalid_options: return invalid_options
ViewMagic.register_object(obj)


def render(plot):
Expand Down Expand Up @@ -152,6 +154,7 @@ def display_figure(fig, message=None, max_width='100%'):
mpld3.plugins.connect(fig, mpld3.plugins.MousePosition(fontsize=14))
html = "<center>" + mpld3.fig_to_html(fig) + "<center/>"
else:
ViewMagic.save_fig(fig, figure_format, dpi=dpi)
figdata = print_figure(fig, figure_format, dpi=dpi)
if figure_format=='svg':
mime_type = 'svg+xml'
Expand Down
90 changes: 85 additions & 5 deletions holoviews/ipython/magics.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import string
import time
import os
import sha
try:
from IPython.core.magic import Magics, magics_class, cell_magic, line_magic, line_cell_magic
from IPython.core.pylabtools import print_figure
except:
from unittest import SkipTest
raise SkipTest("IPython extension requires IPython >= 0.13")
Expand Down Expand Up @@ -61,6 +65,7 @@ class ViewMagic(Magics):
# Codec or system-dependent format options
optional_formats = ['webm','h264', 'gif']

# Lists: strict options, Set: suggested options, Tuple: numeric bounds.
allowed = {'backend' : ['mpl','d3'],
'fig' : ['svg', 'png'],
'holomap' : inbuilt_formats,
Expand All @@ -70,7 +75,8 @@ class ViewMagic(Magics):
'max_branches': (0, float('inf')),
'size' : (0, float('inf')),
'dpi' : (1, float('inf')),
'charwidth' : (0, float('inf'))}
'charwidth' : (0, float('inf')),
'filename' : {None, '{type}-{group}-{label}'}}

defaults = OrderedDict([('backend' , 'mpl'),
('fig' , 'png'),
Expand All @@ -81,7 +87,8 @@ class ViewMagic(Magics):
('max_branches', 2),
('size' , 100),
('dpi' , 72),
('charwidth' , 80)])
('charwidth' , 80),
('filename' , None)])

options = OrderedDict(defaults.items())

Expand All @@ -98,6 +105,9 @@ class ViewMagic(Magics):
'scrubber': ('html', None, {'fps': 5}, None, None)
}

_object_handle = None
_generate_SHA = False # For testing. Whether to compute SHA as output
_SHA = None # For testing purposes: the saved output SHA.

def __init__(self, *args, **kwargs):
super(ViewMagic, self).__init__(*args, **kwargs)
Expand Down Expand Up @@ -133,8 +143,9 @@ def _generate_docstring(cls):
% cls.defaults['dpi'])
chars = ("charwidth : The max character width view magic options display (default %r)"
% cls.defaults['charwidth'])

descriptions = [backend, fig, holomap, widgets, fps, frames, branches, size, dpi, chars]
fname = ("filename : The filename of the saved output, if any (default %r)"
% cls.defaults['filename'])
descriptions = [backend, fig, holomap, widgets, fps, frames, branches, size, dpi, chars, fname]
return '\n'.join(intro + descriptions)


Expand Down Expand Up @@ -168,6 +179,20 @@ def _extract_keywords(self, line, items):
raise SyntaxError("Could not evaluate keyword: %s" % keyword)
return items

@classmethod
def _filename_fields(cls, filename):
"Returns valid filename format fields otherwise raise exception"
if filename is None: return []
valid_fields = ['type', 'group', 'label']
try:
parse = list(string.Formatter().parse(filename))
fields = [f for f in zip(*parse)[1] if f is not None]
except:
raise SyntaxError("Could not parse filename string formatter")
if any(f not in valid_fields for f in fields):
raise ValueError("Valid formatters for the filename are: %s"
% ','.join(valid_fields))
return fields

def _validate(self, options):
"Validation of edge cases and incompatible options"
Expand All @@ -184,6 +209,8 @@ def _validate(self, options):
and options['widgets']!='embed'
and options['fig']=='svg'):
raise ValueError("SVG mode not supported by widgets unless in embed mode")

self._filename_fields(options['filename'])
return options


Expand All @@ -194,7 +221,8 @@ def get_options(self, line, options):
if keyword in items:
value = items[keyword]
allowed = self.allowed[keyword]
if isinstance(allowed, list) and value not in allowed:
if isinstance(allowed, set): pass
elif isinstance(allowed, list) and value not in allowed:
raise ValueError("Value %r for key %r not one of %s"
% (value, keyword, allowed))
elif isinstance(allowed, tuple):
Expand Down Expand Up @@ -268,6 +296,58 @@ def view(self, line, cell=None):
if cell is not None:
self.shell.run_cell(cell, store_history=STORE_HISTORY)
ViewMagic.options = restore_copy
self._object_handle=None

@classmethod
def register_object(cls, obj):
cls._object_handle = obj


@classmethod
def _basename(cls, formatter):
info = {'group':getattr(cls._object_handle, 'group', 'group'),
'label':getattr(cls._object_handle, 'label', 'label'),
'type':cls._object_handle.__class__.__name__}
filtered = {k:v for k,v in info.items()
if k in cls._filename_fields(formatter)}
return formatter.format(**filtered)

@classmethod
def _save_filename(cls, fmt):
if cls.options['filename'] is None: return None
name = cls._basename(cls.options['filename'])
filename = '%s.%s' % (name, fmt)
counter = 1
while os.path.isfile(filename):
basename = '%s-%d' % (name, counter)
filename = '%s.%s' % (basename, fmt)
counter += 1
return filename

@classmethod
def _digest(cls, data):
if cls._generate_SHA:
hashfn = sha.new()
hashfn.update(data)
cls._SHA = hashfn.hexdigest()
return True
else:
return False

@classmethod
def save_fig(cls, fig, figure_format, dpi):
filename = cls._save_filename(figure_format)
if filename is None: return
figure_data = print_figure(fig, figure_format, dpi=dpi)
if cls._digest(figure_data): return
with open(filename, 'w') as f:
f.write(figure_data)

@classmethod
def save_anim(cls, anim, mime_type, writer, dpi, **anim_kwargs):
filename = cls._save_filename(mime_type)
if filename is None: return
anim.save(filename, writer=writer, dpi=dpi, **anim_kwargs)



Expand Down
60 changes: 0 additions & 60 deletions holoviews/plotting/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,66 +13,6 @@
from . import seaborn # pyflakes:ignore (API import)


class PlotSaver(param.ParameterizedFunction):
"""
Parameterized function for saving the plot of a ViewableElement object to
disk either as a static figure or as an animation. Keywords that
are not parameters are passed into the anim method of the
appropriate plot for animations and into matplotlib.figure.savefig
for static figures.
"""

size = param.NumericTuple(default=(5, 5), doc="""
The matplotlib figure size in inches.""")

filename = param.String(default=None, allow_None=True, doc="""
This is the filename of the saved file. The output file type
will be inferred from the file extension.""")

anim_opts = param.Dict(
default={'.webm':({}, ['-vcodec', 'libvpx', '-b', '1000k']),
'.mp4':({'codec':'libx264'}, ['-pix_fmt', 'yuv420p']),
'.gif':({'fps':10}, [])}, doc="""
The arguments to matplotlib.animation.save for different
animation formats. The key is the file extension and the tuple
consists of the kwargs followed by a list of arguments for the
'extra_args' keyword argument.""" )


def __call__(self, view, **params):
anim_exts = ['.webm', '.mp4', 'gif']
image_exts = ['.png', '.jpg', '.svg']
writers = {'.mp4': 'ffmpeg', '.webm':'ffmpeg', '.gif':'imagemagick'}
supported_extensions = image_exts + anim_exts

self.p = param.ParamOverrides(self, params, allow_extra_keywords=True)
if self.p.filename is None:
raise Exception("Please supply a suitable filename.")

_, ext = os.path.splitext(self.p.filename)
if ext not in supported_extensions:
valid_exts = ', '.join(supported_extensions)
raise Exception("File of type %r not in %s" % (ext, valid_exts))
file_format = ext[1:]

plottype = Store.defaults[type(view)]
plotopts = Store.lookup_options(view, 'plot').options
plot = plottype(view, **dict(plotopts, size=self.p.size))

if len(plot) > 1 and ext in anim_exts:
anim_kwargs, extra_args = self.p.anim_opts[ext]
anim = plot.anim(**self.p.extra_keywords())
anim.save(self.p.filename, writer=writers[ext],
**dict(anim_kwargs, extra_args=extra_args))
elif len(plot) > 1:
raise Exception("Invalid extension %r for animation." % ext)
elif ext in anim_exts:
raise Exception("Invalid extension %r for figure." % ext)
else:
plot().savefig(filename=self.p.filename, format=file_format,
**self.p.extra_keywords())


Store.register_plots()

# Charts
Expand Down

0 comments on commit eb5f43c

Please sign in to comment.