Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Support inline PNGs of matplotlib plots #381

Closed
wants to merge 2 commits into from

3 participants

@mdboom

Add '--pylab inline_png' option to render PNGs inline using the matplotlib's Agg renderer.

Some (including myself) find the lack of pixel snapping in SVG to be rather "fuzzy". This allows the user to display matplotlib's Agg-rendered PNGs rather than Qt-rendered SVGs.

Comments on the interface to this are welcome -- I just added an "inline_png" option to the --pylab commandline argument. This could be done more dynamically if desired -- it's possible to switch between the SVG and Agg backends in matplotlib at runtime.

@ellisonbg
Owner

Any backend can generate a png in memory right? If so, then I think we don't need a dedicated backend for this. We can use the existing backend and add a new data format. Here is the rough idea:

In the show method in the inline backend:

https://github.com/ipython/ipython/blob/master/IPython/zmq/pylab/backend_inline.py

We can rename send_svg_figure to something like send_figure_data and create both png and svg data. This will probably involve writing the png analogy to figure_to_svg here:

https://github.com/ipython/ipython/blob/master/IPython/lib/pylabtools.py#L78

And then calling that. The important point is that the dict here:

https://github.com/ipython/ipython/blob/master/IPython/zmq/pylab/backend_inline.py#L76

Can accomodate multiple data types. The benefit of sending both is that frontends can then decide which representation to use. In principle, a frontend could view any or all reprsentations that are sent back. We should also add a formatter for png here as well:

https://github.com/ipython/ipython/blob/master/IPython/lib/pylabtools.py#L249

We will also have to modify the frontend to enable selection of the image type. This part of the code is pretty new, so you may run into issues.

@fperez
Owner

Should make it configurable and not compute both all the time by default? The generation of both forms could get expensive... Michael, do you have a sense for which backend takes more time to produce output, the svg or the png one?

@ellisonbg
Owner
@mdboom

The performance of the Agg backend really starts to win as the data gets large. It is able to "simplify" the path by removing points that would otherwise be invisible, which really speeds up the stroking operation. Markers are also much faster because it can render the marker itself only once and then just copy it. Lesser-used features like quadmeshes and gouraud shading are also much faster with the Agg backend because SVG doesn't really have the high-level primitives for those and they have to be rendered as many tiny polygons. But my main motivation for using the Agg PNGs was pixel-snapping. Since we don't know the scale of SVG a priori, matplotlib can't do pixel snapping for SVG. The SVG standard provides a very blunt tool for this in the "shape-rendering" property:

http://www.w3.org/TR/SVG/painting.html#ShapeRenderingProperty

Unfortunately, it's up to the rendering agent how specifically to handle this. Using "crispEdges" in Firefox and Chrome just turns off antialiasing, and doesn't address pixel-snapping, so rather than merely fuzzy lines, you get crisp lines but one side of the rectangle may be 2 pixels wide, while the other sides are 1. The Qt renderer IPython is using seems to ignore the shape-rendering attribute entirely (which isn't surprising, given that shape-rendering is not a part of SVG Tiny).

I would also suggest not generating both every time if we can avoid it.

Ok -- so this patch can at least show the differences, but it sounds like it's not the right approach. Let me summarize and see if I'm following you:

  1. We want a single backend for "inline" mode.

  2. There should be some way to set the format of the image on-the-fly (any suggestions as to what is consistent the rest of IPython? I'm not as experienced with IPython as I probably should be).

  3. There should also be a way to persistently set a default.

Also "would be cool" (but I digress) to bring up an interactive plot window, allow the user to pan/zoom the axes, move the legend etc., then close the window and have the plot placed inline. Would be "best of both worlds" -- the interactivity of the regular matplotlib plot window, and the nice history/serialization of the new inline image stuff.

@ellisonbg
Owner

Thanks, this is quite helpful. It sounds like having png as the default then probably makes more sense.

  1. Yes, I think we want a single backend for inline mode.
  2. We probably want to have a configuration/runtime attribute that controls these things. Let's get the single backend/png code working first and then we can figure out where to put this attribute.
  3. Yep.

We already have this. In a regular GUI backend you can do:

display(*getfigs(1))

And you will get the plot inline for figure 1. No arguments gives you all figs and display_svg and display_png also work.

@fperez
Owner
@ellisonbg
Owner
@mdboom

You can do:

display(figure(1))

though your argument still stands that to display all of them, you have to remember the asterisk:

display(*getfigs())

@fperez
Owner

OK, here's a plan:

  • let's leave the display*() api discussion for later, when the dust has settled a bit more on the capabilities of the display machinery. If we add too many top-level tools and the low-level foundation changes, it will be that much more stuff to change and update.

  • let's make the default inline be png, and for now add --pylab inline_svg as an option instead.

Michael, do you think you could make that update to your pull request?

And one thing to keep in mind: when the mode is png, the qt console should not offer the option to save html with embedded svg, since there will be no svg around to show...

@ellisonbg
Owner

Only one change. I think we should only have 1 inline backend:

--pylab inline

The activation of svg rendering should be config=True attribute of something and handled in the normal config manner.

@minrk minrk referenced this pull request from a commit in minrk/ipython
@minrk minrk allow toggle of svg/png figure format in inline backend
png is now the default, and select_figure_format(fmt) is added to user_ns for switching between the two.

This should address some of the discussion in gh-381
501b44b
@ellisonbg
Owner

We are closing this as this work is continuing here:

#451

@ellisonbg
Owner

Closing

@ellisonbg ellisonbg closed this
@fperez
Owner

@mdboom, eventually the implementation ended up a little different from your approach, but many thanks for getting the ball going on this! Much appreciated.

@mdboom

No worries. Glad to at least proof-of-concept it.

@damianavila damianavila referenced this pull request from a commit
Commit has since been removed from the repository and is no longer available.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Apr 14, 2011
  1. @mdboom
  2. @mdboom
This page is out of date. Refresh to see the latest.
View
12 IPython/core/usage.py
@@ -471,11 +471,13 @@
Inline matplotlib graphics
==========================
-The IPython console is capable of displaying matplotlib figures inline, in SVG
-format. If started with the ``--pylab inline`` flag, then all figures are
-rendered inline automatically. If started with ``--pylab`` or ``--pylab <your
-backend>``, then a GUI backend will be used, but IPython's ``display()`` and
-``getfigs()`` functions can be used to view plots inline::
+The IPython console is capable of displaying matplotlib figures
+inline, in SVG or PNG format. If started with the ``--pylab
+inline_svg`` or ``--pylab inline_png`` flags, then all figures are
+rendered inline automatically. If started with ``--pylab`` or
+``--pylab <your backend>``, then a GUI backend will be used, but
+IPython's ``display()`` and ``getfigs()`` functions can be used to
+view plots inline::
In [9]: display(*getfigs()) # display all figures inline
View
4 IPython/frontend/qt/console/ipythonqt.py
@@ -156,11 +156,11 @@ def main():
egroup = kgroup.add_mutually_exclusive_group()
egroup.add_argument('--pure', action='store_true', help = \
'use a pure Python kernel instead of an IPython kernel')
- egroup.add_argument('--pylab', type=str, metavar='GUI', nargs='?',
+ egroup.add_argument('--pylab', type=str, metavar='GUI', nargs='?',
const='auto', help = \
"Pre-load matplotlib and numpy for interactive use. If GUI is not \
given, the GUI backend is matplotlib's, otherwise use one of: \
- ['tk', 'gtk', 'qt', 'wx', 'inline'].")
+ ['tk', 'gtk', 'qt', 'wx', 'inline_svg', 'inline_png'].")
wgroup = parser.add_argument_group('widget options')
wgroup.add_argument('--paging', type=str, default='inside',
View
68 IPython/lib/pylabtools.py
@@ -31,7 +31,9 @@
'qt': 'Qt4Agg', # qt3 not supported
'qt4': 'Qt4Agg',
'osx': 'MacOSX',
- 'inline' : 'module://IPython.zmq.pylab.backend_inline'}
+ 'inline' : 'module://IPython.zmq.pylab.backend_inline_svg',
+ 'inline_svg' : 'module://IPython.zmq.pylab.backend_inline_svg',
+ 'inline_png' : 'module://IPython.zmq.pylab.backend_inline_png' }
#-----------------------------------------------------------------------------
# Matplotlib utilities
@@ -75,8 +77,8 @@ def figsize(sizex, sizey):
matplotlib.rcParams['figure.figsize'] = [sizex, sizey]
-def figure_to_svg(fig):
- """Convert a figure to svg for inline display."""
+def print_figure(fig, format):
+ """Convert a figure to a serialized format for inline display."""
# When there's an empty figure, we shouldn't return anything, otherwise we
# get big blank areas in the qt console.
if not fig.axes:
@@ -88,12 +90,14 @@ def figure_to_svg(fig):
fig.set_edgecolor('white')
try:
string_io = StringIO()
- fig.canvas.print_figure(string_io, format='svg')
- svg = string_io.getvalue()
+ fig.canvas.print_figure(string_io, format=format, dpi=72)
+ data = string_io.getvalue()
finally:
fig.set_facecolor(fc)
fig.set_edgecolor(ec)
- return svg
+ if format == 'png':
+ data = data.encode('base64')
+ return data
# We need a little factory function here to create the closure where
@@ -149,7 +153,7 @@ def find_gui_and_backend(gui=None):
Parameters
----------
gui : str
- Can be one of ('tk','gtk','wx','qt','qt4','inline').
+ Can be one of ('tk','gtk','wx','qt','qt4','inline','inline_svg','inline_png').
Returns
-------
@@ -215,19 +219,22 @@ def import_pylab(user_ns, backend, import_all=True, shell=None):
if shell is not None:
exec s in shell.user_ns_hidden
- # If using our svg payload backend, register the post-execution
+ # If using our inline payload backend, register the post-execution
# function that will pick up the results for display. This can only be
# done with access to the real shell object.
- if backend == backends['inline']:
- from IPython.zmq.pylab.backend_inline import flush_svg
+ if backend in (backends['inline_svg'], backends['inline_png']):
from matplotlib import pyplot
- shell.register_post_execute(flush_svg)
+ def flush_image():
+ if pyplot.show._draw_called:
+ pyplot.show()
+ pyplot.show._draw_called = False
+ shell.register_post_execute(flush_image)
# The typical default figure size is too large for inline use,
# so we shrink the figure size to 6x4, and tweak fonts to
# make that fit. This is configurable via Global.pylab_inline_rc,
# or rather it will be once the zmq kernel is hooked up to
# the config system.
-
+
default_rc = {
'figure.figsize': (6.0,4.0),
# 12pt labels get cutoff on 6x4 logplots, so use 10pt.
@@ -238,23 +245,34 @@ def import_pylab(user_ns, backend, import_all=True, shell=None):
rc = getattr(shell.config.Global, 'pylab_inline_rc', default_rc)
pyplot.rcParams.update(rc)
shell.config.Global.pylab_inline_rc = rc
-
+
# Add 'figsize' to pyplot and to the user's namespace
user_ns['figsize'] = pyplot.figsize = figsize
shell.user_ns_hidden['figsize'] = figsize
-
- # The old pastefig function has been replaced by display
- # Always add this svg formatter so display works.
- from IPython.core.display import display, display_svg
- svg_formatter = shell.display_formatter.formatters['image/svg+xml']
- svg_formatter.for_type_by_name(
- 'matplotlib.figure','Figure',figure_to_svg
- )
- # Add display and display_png to the user's namespace
+
+ if backend == backends['inline_png']:
+ # The old pastefig function has been replaced by display
+ # Always add this png formatter so display works.
+ from IPython.core.display import display, display_png
+ png_formatter = shell.display_formatter.formatters['image/png']
+ png_formatter.for_type_by_name(
+ 'matplotlib.figure','Figure',lambda fig: print_figure(fig, 'png')
+ )
+ user_ns['display_png'] = display_png
+ shell.user_ns_hidden['display_png'] = display_png
+ else:
+ # The old pastefig function has been replaced by display
+ # Always add this svg formatter so display works.
+ from IPython.core.display import display, display_svg
+ svg_formatter = shell.display_formatter.formatters['image/svg+xml']
+ svg_formatter.for_type_by_name(
+ 'matplotlib.figure','Figure',lambda fig: print_figure(fig, 'svg')
+ )
+ user_ns['display_svg'] = display_svg
+ shell.user_ns_hidden['display_svg'] = display_svg
+ # Add display to the user's namespace
user_ns['display'] = display
shell.user_ns_hidden['display'] = display
- user_ns['display_svg'] = display_svg
- shell.user_ns_hidden['display_svg'] = display_svg
user_ns['getfigs'] = getfigs
shell.user_ns_hidden['getfigs'] = getfigs
@@ -294,6 +312,6 @@ def pylab_activate(user_ns, gui=None, import_all=True):
print """
Welcome to pylab, a matplotlib-based Python environment [backend: %s].
For more information, type 'help(pylab)'.""" % backend
-
+
return gui
View
6 IPython/zmq/ipkernel.py
@@ -612,11 +612,11 @@ def main():
""" The IPython kernel main entry point.
"""
parser = make_argument_parser()
- parser.add_argument('--pylab', type=str, metavar='GUI', nargs='?',
+ parser.add_argument('--pylab', type=str, metavar='GUI', nargs='?',
const='auto', help = \
"Pre-load matplotlib and numpy for interactive use. If GUI is not \
given, the GUI backend is matplotlib's, otherwise use one of: \
-['tk', 'gtk', 'qt', 'wx', 'inline'].")
+['tk', 'gtk', 'qt', 'wx', 'inline_svg', 'inline_png'].")
parser.add_argument('--colors',
type=str, dest='colors',
help="Set the color scheme (NoColor, Linux, and LightBG).",
@@ -629,6 +629,8 @@ def main():
'qt' : QtKernel,
'qt4': QtKernel,
'inline': Kernel,
+ 'inline_svg': Kernel,
+ 'inline_png': Kernel,
'wx' : WxKernel,
'tk' : TkKernel,
'gtk': GTKKernel,
View
70 IPython/zmq/pylab/backend_inline_png.py
@@ -0,0 +1,70 @@
+"""Produce Agg-rendered PNG versions of active plots for display by the rich Qt frontend.
+"""
+#-----------------------------------------------------------------------------
+# Imports
+#-----------------------------------------------------------------------------
+from __future__ import print_function
+
+# Standard library imports
+import sys
+
+# Third-party imports
+import matplotlib
+from matplotlib.backends.backend_agg import new_figure_manager
+from matplotlib._pylab_helpers import Gcf
+
+# Local imports.
+from IPython.core.displaypub import publish_display_data
+from IPython.lib.pylabtools import print_figure
+
+#-----------------------------------------------------------------------------
+# Functions
+#-----------------------------------------------------------------------------
+
+def show(close=True):
+ """Show all figures as PNG payloads sent to the IPython clients.
+
+ Parameters
+ ----------
+ close : bool, optional
+ If true, a ``plt.close('all')`` call is automatically issued after
+ sending all the PNG figures. If this is set, the figures will entirely
+ removed from the internal list of figures.
+ """
+ for figure_manager in Gcf.get_all_fig_managers():
+ send_png_figure(figure_manager.canvas.figure)
+ if close:
+ matplotlib.pyplot.close('all')
+
+
+# This flag will be reset by draw_if_interactive when called
+show._draw_called = False
+
+
+def draw_if_interactive():
+ """
+ Is called after every pylab drawing command
+ """
+ # We simply flag we were called and otherwise do nothing. At the end of
+ # the code execution, a separate call to show_close() will act upon this.
+ show._draw_called = True
+
+
+def send_png_figure(fig):
+ """Draw the current figure and send it as a PNG payload.
+ """
+ # For an empty figure, don't even bother calling print_figure, to avoid
+ # big blank spaces in the qt console
+ if not fig.axes:
+ return
+
+ png = print_figure(fig, 'png')
+ # flush text streams before sending figures, helps a little with output
+ # synchronization in the console (though it's a bandaid, not a real sln)
+ sys.stdout.flush(); sys.stderr.flush()
+ publish_display_data(
+ 'IPython.zmq.pylab.backend_inline.send_png_figure',
+ 'Matplotlib Plot',
+ {'image/png' : png}
+ )
+
View
15 IPython/zmq/pylab/backend_inline.py → IPython/zmq/pylab/backend_inline_svg.py
@@ -15,7 +15,7 @@
# Local imports.
from IPython.core.displaypub import publish_display_data
-from IPython.lib.pylabtools import figure_to_svg
+from IPython.lib.pylabtools import print_figure
#-----------------------------------------------------------------------------
# Functions
@@ -50,17 +50,6 @@ def draw_if_interactive():
show._draw_called = True
-def flush_svg():
- """Call show, close all open figures, sending all SVG images.
-
- This is meant to be called automatically and will call show() if, during
- prior code execution, there had been any calls to draw_if_interactive.
- """
- if show._draw_called:
- show()
- show._draw_called = False
-
-
def send_svg_figure(fig):
"""Draw the current figure and send it as an SVG payload.
"""
@@ -69,7 +58,7 @@ def send_svg_figure(fig):
if not fig.axes:
return
- svg = figure_to_svg(fig)
+ svg = print_figure(fig, 'svg')
# flush text streams before sending figures, helps a little with output
# synchronization in the console (though it's a bandaid, not a real sln)
sys.stdout.flush(); sys.stderr.flush()
Something went wrong with that request. Please try again.