diff --git a/.travis.yml b/.travis.yml index 669fe3a8d4de..fefdf90e7aeb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,6 +23,8 @@ matrix: install: - pip install -q --use-mirrors nose python-dateutil numpy pep8 pyparsing pillow - sudo apt-get update && sudo apt-get -qq install inkscape libav-tools + # We use --no-install-recommends to avoid pulling in additional large latex docs that we don't need + - if [[ $BUILD_DOCS == true ]]; then sudo apt-get install -qq --no-install-recommends dvipng texlive-latex-base texlive-latex-extra texlive-fonts-recommended graphviz; fi - if [[ $BUILD_DOCS == true ]]; then pip install sphinx numpydoc linkchecker; fi - python setup.py install diff --git a/INSTALL b/INSTALL index f96f230a79cc..02780072a981 100644 --- a/INSTALL +++ b/INSTALL @@ -192,7 +192,7 @@ Required Dependencies using pip, easy_install or installing from source, the installer will attempt to download and install `pyparsing` from PyPI. -six 1.3 or later +six 1.4 or later Python 2/3 compatibility library. This is also a dependency of :term:`dateutil`. @@ -201,6 +201,11 @@ libpng 1.2 (or later) `__). libpng requires zlib. +`pytz` + Used to manipulate time-zone aware datetimes. + + + Optional GUI framework ^^^^^^^^^^^^^^^^^^^^^^ @@ -240,18 +245,8 @@ Optional dependencies selection of image file formats. -:term:`freetype` 2.4 or later - library for reading true type font files. Matplotlib in known - to work with freetype 2.3, and the required version will be reduced - in 1.4.1. If you need to build from source on a system which only has - freetype 2.3 available, please edit L945 of `setupext.py` to reduce - `min_version` to 2.3. - -`pytz` - Required if you want to manipulate datetime objects which are time-zone - aware. An exception will be raised if you try to make time-zone aware - plots with out `pytz` installed. It will become a required dependency - in 1.4.1. +:term:`freetype` 2.3 or later + library for reading true type font files. Required libraries that ship with matplotlib diff --git a/doc/_templates/index.html b/doc/_templates/index.html index 664e92fcf14e..0d6f5754134b 100644 --- a/doc/_templates/index.html +++ b/doc/_templates/index.html @@ -63,9 +63,9 @@

Introduction

For a sampling, see the screenshots, thumbnail gallery, and examples directory

-

For simple plotting the

pyplot
interface provides a +

For simple plotting the pyplot interface provides a MATLAB-like interface, particularly when combined - with

IPython
. For the power user, you have full control + with IPython. For the power user, you have full control of line styles, font properties, axes properties, etc, via an object oriented interface or via a set of functions familiar to MATLAB users.

diff --git a/examples/event_handling/timers.py b/examples/event_handling/timers.py index 0e293a84975c..4cd9351613fc 100644 --- a/examples/event_handling/timers.py +++ b/examples/event_handling/timers.py @@ -13,13 +13,13 @@ def update_title(axes): x = np.linspace(-3, 3) ax.plot(x, x*x) -# Create a new timer object. Set the interval 500 milliseconds (1000 is default) -# and tell the timer what function should be called. +# Create a new timer object. Set the interval to 100 milliseconds +# (1000 is default) and tell the timer what function should be called. timer = fig.canvas.new_timer(interval=100) timer.add_callback(update_title, ax) timer.start() -#Or could start the timer on first figure draw +# Or could start the timer on first figure draw #def start_timer(evt): # timer.start() # fig.canvas.mpl_disconnect(drawid) diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index 25d7e04f3103..8a20272636f0 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -745,7 +745,11 @@ def matplotlib_fname(): - Lastly, it looks in `$MATPLOTLIBDATA/matplotlibrc` for a system-defined copy. """ - fname = os.path.join(os.getcwd(), 'matplotlibrc') + if six.PY2: + cwd = os.getcwdu() + else: + cwd = os.getcwd() + fname = os.path.join(cwd, 'matplotlibrc') if os.path.exists(fname): return fname diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 7301c2c93157..12f79352a539 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -124,9 +124,10 @@ def set_title(self, label, fontdict=None, loc="center", **kwargs): Other parameters ---------------- - Other keyword arguments are text properties, see - :class:`~matplotlib.text.Text` for a list of valid text - properties. + kwargs : text properties + Other keyword arguments are text properties, see + :class:`~matplotlib.text.Text` for a list of valid text + properties. """ try: title = {'left': self._left_title, @@ -2883,7 +2884,7 @@ def xywhere(xs, ys, mask): return errorbar_container # (l0, caplines, barcols) - def boxplot(self, x, notch=False, sym='b+', vert=True, whis=1.5, + def boxplot(self, x, notch=False, sym=None, vert=True, whis=1.5, positions=None, widths=None, patch_artist=False, bootstrap=None, usermedians=None, conf_intervals=None, meanline=False, showmeans=False, showcaps=True, @@ -2919,11 +2920,13 @@ def boxplot(self, x, notch=False, sym='b+', vert=True, whis=1.5, If False, produces a rectangular box plot. If True, will produce a notched box plot - sym : str, default = 'b+' + sym : str or None, default = None The default symbol for flier points. Enter an empty string ('') if you don't want to show fliers. + If `None`, then the fliers default to 'b+' If you want more + control use the fliersprop kwarg. - vert : bool, default = False + vert : bool, default = True If True (default), makes the boxes vertical. If False, makes horizontal boxes. @@ -3021,10 +3024,11 @@ def boxplot(self, x, notch=False, sym='b+', vert=True, whis=1.5, Returns ------- - A dictionary mapping each component of the boxplot - to a list of the :class:`matplotlib.lines.Line2D` - instances created. That dictionary has the following keys - (assuming vertical boxplots): + result : dict + A dictionary mapping each component of the boxplot + to a list of the :class:`matplotlib.lines.Line2D` + instances created. That dictionary has the following keys + (assuming vertical boxplots): - boxes: the main body of the boxplot showing the quartiles and the median's confidence intervals if enabled. @@ -3043,10 +3047,39 @@ def boxplot(self, x, notch=False, sym='b+', vert=True, whis=1.5, """ bxpstats = cbook.boxplot_stats(x, whis=whis, bootstrap=bootstrap, labels=labels) + # make sure we have a dictionary if flierprops is None: - flierprops = dict(sym=sym) - else: - flierprops['sym'] = sym + flierprops = dict() + # if non-default sym value, put it into the flier dictionary + # the logic for providing the default symbol ('b+') now lives + # in bxp in the initial value of final_flierprops + # handle all of the `sym` related logic here so we only have to pass + # on the flierprops dict. + if sym is not None: + # no-flier case, which should really be done with + # 'showfliers=False' but none-the-less deal with it to keep back + # compatibility + if sym == '': + # blow away existing dict and make one for invisible markers + flierprops = dict(linestyle='none', marker='', + markeredgecolor='none', + markerfacecolor='none') + # now process the symbol string + else: + # process the symbol string + # discarded linestyle + _, marker, color = _process_plot_format(sym) + # if we have a marker, use it + if marker is not None: + flierprops['marker'] = marker + # if we have a color, use it + if color is not None: + flierprops['color'] = color + # assume that if color is passed in the user want + # filled symbol, if the users want more control use + # flierprops + flierprops['markeredgecolor'] = color + flierprops['markerfacecolor'] = color # replace medians if necessary: if usermedians is not None: @@ -3288,24 +3321,9 @@ def bxp(self, bxpstats, positions=None, widths=None, vert=True, final_flierprops = dict(linestyle='none', marker='+', markeredgecolor='b', markerfacecolor='none') + # flier (outlier) properties if flierprops is not None: - sym = flierprops.pop('sym', None) - - # watch inverted logic, checks for non-default - # value of `sym` - if not (sym == '' or (sym is None)): - # process the symbol string - # discarded linestyle - _, marker, color = _process_plot_format(sym) - if marker is not None: - flierprops['marker'] = marker - if color is not None: - flierprops['color'] = color - # assume that if color is passed in the user want - # filled symbol - flierprops['markeredgecolor'] = color - flierprops['markerfacecolor'] = color final_flierprops.update(flierprops) # median line properties @@ -5032,6 +5050,7 @@ def pcolormesh(self, *args, **kwargs): *edgecolors*: [*None* | ``'None'`` | ``'face'`` | color | color sequence] + If *None*, the rc setting is used by default. If ``'None'``, edges will not be visible. diff --git a/lib/matplotlib/backends/backend_nbagg.py b/lib/matplotlib/backends/backend_nbagg.py index c69c558ea071..bea523622454 100644 --- a/lib/matplotlib/backends/backend_nbagg.py +++ b/lib/matplotlib/backends/backend_nbagg.py @@ -1,26 +1,37 @@ """Interactive figures in the IPython notebook""" +# Note: There is a notebook in +# lib/matplotlib/backends/web_backend/nbagg_uat.ipynb to help verify +# that changes made maintain expected behaviour. + from base64 import b64encode +from contextlib import contextmanager import json import io import os +import six from uuid import uuid4 as uuid +import tornado.ioloop + from IPython.display import display, Javascript, HTML from IPython.kernel.comm import Comm +from matplotlib import rcParams from matplotlib.figure import Figure +from matplotlib.backends import backend_agg from matplotlib.backends.backend_webagg_core import (FigureManagerWebAgg, FigureCanvasWebAggCore, NavigationToolbar2WebAgg) -from matplotlib.backend_bases import ShowBase, NavigationToolbar2 +from matplotlib.backend_bases import (ShowBase, NavigationToolbar2, + TimerBase, FigureCanvasBase) class Show(ShowBase): def __call__(self, block=None): - import matplotlib._pylab_helpers as pylab_helpers + from matplotlib._pylab_helpers import Gcf from matplotlib import is_interactive - managers = pylab_helpers.Gcf.get_all_fig_managers() + managers = Gcf.get_all_fig_managers() if not managers: return @@ -28,9 +39,16 @@ def __call__(self, block=None): for manager in managers: manager.show() - if not interactive and manager in pylab_helpers.Gcf._activeQue: - pylab_helpers.Gcf._activeQue.remove(manager) + # plt.figure adds an event which puts the figure in focus + # in the activeQue. Disable this behaviour, as it results in + # figures being put as the active figure after they have been + # shown, even in non-interactive mode. + if hasattr(manager, '_cidgcf'): + manager.canvas.mpl_disconnect(manager._cidgcf) + + if not interactive and manager in Gcf._activeQue: + Gcf._activeQue.remove(manager) show = Show() @@ -48,40 +66,41 @@ def draw_if_interactive(): def connection_info(): """ Return a string showing the figure and connection status for - the backend. + the backend. This is intended as a diagnostic tool, and not for general + use. """ - # TODO: Make this useful! - import matplotlib._pylab_helpers as pylab_helpers + from matplotlib._pylab_helpers import Gcf result = [] - for manager in pylab_helpers.Gcf.get_all_fig_managers(): + for manager in Gcf.get_all_fig_managers(): fig = manager.canvas.figure result.append('{} - {}'.format((fig.get_label() or "Figure {0}".format(manager.num)), manager.web_sockets)) - result.append('Figures pending show: ' + - str(len(pylab_helpers.Gcf._activeQue))) + result.append('Figures pending show: {}'.format(len(Gcf._activeQue))) return '\n'.join(result) +# Note: Version 3.2 icons, not the later 4.0 ones. +# http://fontawesome.io/3.2.1/icons/ +_FONT_AWESOME_CLASSES = { + 'home': 'icon-home', + 'back': 'icon-arrow-left', + 'forward': 'icon-arrow-right', + 'zoom_to_rect': 'icon-check-empty', + 'move': 'icon-move', + None: None +} + + class NavigationIPy(NavigationToolbar2WebAgg): - # Note: Version 3.2 icons, not the later 4.0 ones. - # http://fontawesome.io/3.2.1/icons/ - _font_awesome_classes = { - 'home': 'icon-home', - 'back': 'icon-arrow-left', - 'forward': 'icon-arrow-right', - 'zoom_to_rect': 'icon-check-empty', - 'move': 'icon-move', - None: None - } # Use the standard toolbar items + download button toolitems = [(text, tooltip_text, - _font_awesome_classes[image_file], name_of_method) + _FONT_AWESOME_CLASSES[image_file], name_of_method) for text, tooltip_text, image_file, name_of_method in NavigationToolbar2.toolitems - if image_file in _font_awesome_classes] + if image_file in _FONT_AWESOME_CLASSES] class FigureManagerNbAgg(FigureManagerWebAgg): @@ -93,7 +112,8 @@ def __init__(self, canvas, num): def display_js(self): # XXX How to do this just once? It has to deal with multiple - # browser instances using the same kernel. + # browser instances using the same kernel (require.js - but the + # file isn't static?). display(Javascript(FigureManagerNbAgg.get_javascript())) def show(self): @@ -105,6 +125,10 @@ def show(self): self._shown = True def reshow(self): + """ + A special method to re-show the figure in the notebook. + + """ self._shown = False self.show() @@ -137,6 +161,49 @@ def destroy(self): for comm in self.web_sockets.copy(): comm.on_close() + def clearup_closed(self): + """Clear up any closed Comms.""" + self.web_sockets = set([socket for socket in self.web_sockets + if not socket.is_open()]) + + +class TimerTornado(TimerBase): + def _timer_start(self): + import datetime + self._timer_stop() + if self._single: + ioloop = tornado.ioloop.IOLoop.instance() + self._timer = ioloop.add_timeout( + datetime.timedelta(milliseconds=self.interval), + self._on_timer) + else: + self._timer = tornado.ioloop.PeriodicCallback( + self._on_timer, + self.interval) + self._timer.start() + + def _timer_stop(self): + if self._timer is not None: + self._timer.stop() + self._timer = None + + def _timer_set_interval(self): + # Only stop and restart it if the timer has already been started + if self._timer is not None: + self._timer_stop() + self._timer_start() + + +class FigureCanvasNbAgg(FigureCanvasWebAggCore): + def new_timer(self, *args, **kwargs): + return TimerTornado(*args, **kwargs) + + def start_event_loop(self, timeout): + FigureCanvasBase.start_event_loop_default(self, timeout) + + def stop_event_loop(self): + FigureCanvasBase.stop_event_loop_default(self) + def new_figure_manager(num, *args, **kwargs): """ @@ -151,7 +218,9 @@ def new_figure_manager_given_figure(num, figure): """ Create a new figure manager instance for the given figure. """ - canvas = FigureCanvasWebAggCore(figure) + canvas = FigureCanvasNbAgg(figure) + if rcParams['nbagg.transparent']: + figure.patch.set_alpha(0) manager = FigureManagerNbAgg(canvas, num) return manager @@ -170,6 +239,8 @@ def __init__(self, manager): self.supports_binary = None self.manager = manager self.uuid = str(uuid()) + # Publish an output area with a unique ID. The javascript can then + # hook into this area. display(HTML("
" % self.uuid)) try: self.comm = Comm('matplotlib', data={'id': self.uuid}) @@ -178,12 +249,17 @@ def __init__(self, manager): 'instance. Are you in the IPython notebook?') self.comm.on_msg(self.on_message) + manager = self.manager + self.comm.on_close(lambda close_message: manager.clearup_closed()) + + def is_open(self): + return not self.comm._closed + def on_close(self): # When the socket is closed, deregister the websocket with # the FigureManager. - if self.comm in self.manager.web_sockets: - self.manager.remove_web_socket(self) self.comm.close() + self.manager.clearup_closed() def send_json(self, content): self.comm.send({'data': json.dumps(content)}) @@ -191,7 +267,10 @@ def send_json(self, content): def send_binary(self, blob): # The comm is ascii, so we always send the image in base64 # encoded data URL form. - data_uri = "data:image/png;base64,{0}".format(b64encode(blob)) + data = b64encode(blob) + if six.PY3: + data = data.decode('ascii') + data_uri = "data:image/png;base64,{0}".format(data) self.comm.send({'data': data_uri}) def on_message(self, message): diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index d2e8bc4c7e1e..0acd8896f42f 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -568,7 +568,7 @@ def _get_clip_path(self, clippath, clippath_transform): ps_cmd.extend(['clip', 'newpath', '} bind def\n']) self._pswriter.write('\n'.join(ps_cmd)) self._clip_paths[key] = pid - return id + return pid def draw_path(self, gc, path, transform, rgbFace=None): """ diff --git a/lib/matplotlib/backends/backend_webagg.py b/lib/matplotlib/backends/backend_webagg.py index 05e0e1cb23c6..9c1a0e9a7f60 100644 --- a/lib/matplotlib/backends/backend_webagg.py +++ b/lib/matplotlib/backends/backend_webagg.py @@ -39,6 +39,7 @@ from matplotlib.figure import Figure from matplotlib._pylab_helpers import Gcf from . import backend_webagg_core as core +from .backend_nbagg import TimerTornado def new_figure_manager(num, *args, **kwargs): @@ -96,32 +97,6 @@ def run(self): webagg_server_thread = ServerThread() -class TimerTornado(backend_bases.TimerBase): - def _timer_start(self): - self._timer_stop() - if self._single: - ioloop = tornado.ioloop.IOLoop.instance() - self._timer = ioloop.add_timeout( - datetime.timedelta(milliseconds=self.interval), - self._on_timer) - else: - self._timer = tornado.ioloop.PeriodicCallback( - self._on_timer, - self.interval) - self._timer.start() - - def _timer_stop(self): - if self._timer is not None: - self._timer.stop() - self._timer = None - - def _timer_set_interval(self): - # Only stop and restart it if the timer has already been started - if self._timer is not None: - self._timer_stop() - self._timer_start() - - class FigureCanvasWebAgg(core.FigureCanvasWebAggCore): def show(self): # show the figure window diff --git a/lib/matplotlib/backends/backend_webagg_core.py b/lib/matplotlib/backends/backend_webagg_core.py index eee727dbc7c7..dfd77ec450c8 100644 --- a/lib/matplotlib/backends/backend_webagg_core.py +++ b/lib/matplotlib/backends/backend_webagg_core.py @@ -65,13 +65,19 @@ def __init__(self, *args, **kwargs): # sent to the clients will be a full frame. self._force_full = True + # Store the current image mode so that at any point, clients can + # request the information. This should be changed by calling + # self.set_image_mode(mode) so that the notification can be given + # to the connected clients. + self._current_image_mode = 'full' + def show(self): # show the figure window from matplotlib.pyplot import show show() def draw(self): - renderer = self.get_renderer() + renderer = self.get_renderer(cleared=True) self._png_is_old = True @@ -86,26 +92,47 @@ def draw(self): def draw_idle(self): self.send_event("draw") + def set_image_mode(self, mode): + """ + Set the image mode for any subsequent images which will be sent + to the clients. The modes may currently be either 'full' or 'diff'. + + Note: diff images may not contain transparency, therefore upon + draw this mode may be changed if the resulting image has any + transparent component. + + """ + if mode not in ['full', 'diff']: + raise ValueError('image mode must be either full or diff.') + if self._current_image_mode != mode: + self._current_image_mode = mode + self.handle_send_image_mode(None) + def get_diff_image(self): if self._png_is_old: + renderer = self.get_renderer() + # The buffer is created as type uint32 so that entire # pixels can be compared in one numpy call, rather than # needing to compare each plane separately. - buff = np.frombuffer( - self.get_renderer().buffer_rgba(), dtype=np.uint32) - buff.shape = ( - self._renderer.height, self._renderer.width) + buff = np.frombuffer(renderer.buffer_rgba(), dtype=np.uint32) + buff.shape = (renderer.height, renderer.width) + + # If any pixels have transparency, we need to force a full + # draw as we cannot overlay new on top of old. + pixels = buff.view(dtype=np.uint8).reshape(buff.shape + (4,)) - if not self._force_full: - last_buffer = np.frombuffer( - self._last_renderer.buffer_rgba(), dtype=np.uint32) - last_buffer.shape = ( - self._renderer.height, self._renderer.width) + if self._force_full or np.any(pixels[:, :, 3] != 255): + self.set_image_mode('full') + output = buff + else: + self.set_image_mode('diff') + last_buffer = np.frombuffer(self._last_renderer.buffer_rgba(), + dtype=np.uint32) + last_buffer.shape = (renderer.height, renderer.width) diff = buff != last_buffer output = np.where(diff, buff, 0) - else: - output = buff # Clear out the PNG data buffer rather than recreating it # each time. This reduces the number of memory @@ -122,7 +149,7 @@ def get_diff_image(self): # Swap the renderer frames self._renderer, self._last_renderer = ( - self._last_renderer, self._renderer) + self._last_renderer, renderer) self._force_full = False self._png_is_old = False return self._png_buffer.getvalue() @@ -147,6 +174,9 @@ def get_renderer(self, cleared=None): w, h, self.figure.dpi) self._lastKey = key + elif cleared: + self._renderer.clear() + return self._renderer def handle_event(self, event): @@ -223,6 +253,10 @@ def handle_resize(self, event): self._png_is_old = True self.manager.resize(w, h) + def handle_send_image_mode(self, event): + # The client requests notification of what the current image mode is. + self.send_event('image_mode', mode=self._current_image_mode) + def send_event(self, event_type, **kwargs): self.manager._send_event(event_type, **kwargs) @@ -238,24 +272,26 @@ def stop_event_loop(self): backend_bases.FigureCanvasBase.stop_event_loop_default.__doc__ +_JQUERY_ICON_CLASSES = { + 'home': 'ui-icon ui-icon-home', + 'back': 'ui-icon ui-icon-circle-arrow-w', + 'forward': 'ui-icon ui-icon-circle-arrow-e', + 'zoom_to_rect': 'ui-icon ui-icon-search', + 'move': 'ui-icon ui-icon-arrow-4', + 'download': 'ui-icon ui-icon-disk', + None: None, +} + + class NavigationToolbar2WebAgg(backend_bases.NavigationToolbar2): - _jquery_icon_classes = { - 'home': 'ui-icon ui-icon-home', - 'back': 'ui-icon ui-icon-circle-arrow-w', - 'forward': 'ui-icon ui-icon-circle-arrow-e', - 'zoom_to_rect': 'ui-icon ui-icon-search', - 'move': 'ui-icon ui-icon-arrow-4', - 'download': 'ui-icon ui-icon-disk', - None: None, - } # Use the standard toolbar items + download button - toolitems = [(text, tooltip_text, _jquery_icon_classes[image_file], + toolitems = [(text, tooltip_text, _JQUERY_ICON_CLASSES[image_file], name_of_method) for text, tooltip_text, image_file, name_of_method in (backend_bases.NavigationToolbar2.toolitems + (('Download', 'Download plot', 'download', 'download'),)) - if image_file in _jquery_icon_classes] + if image_file in _JQUERY_ICON_CLASSES] def _init_toolbar(self): self.message = '' diff --git a/lib/matplotlib/backends/web_backend/mpl.js b/lib/matplotlib/backends/web_backend/mpl.js index 6f1cf79364cb..6d6a1f3a4e59 100644 --- a/lib/matplotlib/backends/web_backend/mpl.js +++ b/lib/matplotlib/backends/web_backend/mpl.js @@ -41,6 +41,7 @@ mpl.figure = function(figure_id, websocket, ondownload, parent_element) { this.format_dropdown = undefined; this.focus_on_mousover = false; + this.image_mode = 'full'; this.root = $('
'); this.root.attr('style', 'display: inline-block'); @@ -56,10 +57,17 @@ mpl.figure = function(figure_id, websocket, ondownload, parent_element) { this.ws.onopen = function () { fig.send_message("supports_binary", {value: fig.supports_binary}); + fig.send_message("send_image_mode", {}); fig.send_message("refresh", {}); } this.imageObj.onload = function() { + if (fig.image_mode == 'full') { + // Full images could contain transparency (where diff images + // almost always do), so we need to clear the canvas so that + // there is no ghosting. + fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height); + } fig.context.drawImage(fig.imageObj, 0, 0); fig.waiting = false; }; @@ -301,6 +309,10 @@ mpl.figure.prototype.handle_draw = function(fig, msg) { fig.send_draw_message(); } +mpl.figure.prototype.handle_image_mode = function(fig, msg) { + fig.image_mode = msg['mode']; +} + mpl.figure.prototype.updated_canvas_event = function() { // Called whenever the canvas gets updated. this.send_message("ack", {}); @@ -322,6 +334,7 @@ mpl.figure.prototype._make_on_message_function = function(fig) { (window.URL || window.webkitURL).revokeObjectURL( fig.imageObj.src); } + fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL( evt.data); fig.updated_canvas_event(); diff --git a/lib/matplotlib/backends/web_backend/nbagg_mpl.js b/lib/matplotlib/backends/web_backend/nbagg_mpl.js index caac0c17656d..82c020aa8264 100644 --- a/lib/matplotlib/backends/web_backend/nbagg_mpl.js +++ b/lib/matplotlib/backends/web_backend/nbagg_mpl.js @@ -25,6 +25,8 @@ mpl.mpl_figure_comm = function(comm, msg) { // starts-up an IPython Comm through the "matplotlib" channel. var id = msg.content.data.id; + // Get hold of the div created by the display call when the Comm + // socket was opened in Python. var element = $("#" + id); var ws_proxy = comm_websocket_adapter(comm) @@ -44,7 +46,7 @@ mpl.mpl_figure_comm = function(comm, msg) { // Disable right mouse context menu. $(fig.rubberband_canvas).bind("contextmenu",function(e){ - return false; + return false; }); }; @@ -53,12 +55,16 @@ mpl.figure.prototype.handle_close = function(fig, msg) { // Update the output cell to use the data from the current canvas. fig.push_to_output(); var dataURL = fig.canvas.toDataURL(); + // Re-enable the keyboard manager in IPython - without this line, in FF, + // the notebook keyboard shortcuts fail. + IPython.keyboard_manager.enable() $(fig.parent_element).html(''); fig.send_message('closing', {}); fig.ws.close() } mpl.figure.prototype.push_to_output = function(remove_interactive) { + // Turn the data on the canvas into data in the output cell. var dataURL = this.canvas.toDataURL(); this.cell_info[1]['text/html'] = ''; } diff --git a/lib/matplotlib/backends/web_backend/nbagg_uat.ipynb b/lib/matplotlib/backends/web_backend/nbagg_uat.ipynb new file mode 100644 index 000000000000..3601c1490643 --- /dev/null +++ b/lib/matplotlib/backends/web_backend/nbagg_uat.ipynb @@ -0,0 +1,392 @@ +{ + "metadata": { + "name": "", + "signature": "sha256:1d491e506b54b126f6971897d3249a9e6f96f9d50bf4e4ba8d179c6b7b1aefa8" + }, + "nbformat": 3, + "nbformat_minor": 0, + "worksheets": [ + { + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## UAT for NbAgg backend.\n", + "\n", + "The first line simply reloads matplotlib, uses the nbagg backend and then reloads the backend, just to ensure we have the latest modification to the backend code. Note: The underlying JavaScript will not be updated by this process, so a refresh of the browser after clearing the output and saving is necessary to clear everything fully." + ] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "import matplotlib\n", + "reload(matplotlib)\n", + "\n", + "matplotlib.use('nbagg')\n", + "\n", + "import matplotlib.backends.backend_nbagg\n", + "reload(matplotlib.backends.backend_nbagg)" + ], + "language": "python", + "metadata": {}, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### UAT 1 - Simple figure creation using pyplot\n", + "\n", + "Should produce a figure window which is interactive with the pan and zoom buttons. (Do not press the close button, but any others may be used)." + ] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "import matplotlib.backends.backend_webagg_core\n", + "reload(matplotlib.backends.backend_webagg_core)\n", + "\n", + "import matplotlib.pyplot as plt\n", + "plt.interactive(False)\n", + "\n", + "fig1 = plt.figure()\n", + "plt.plot(range(10))\n", + "\n", + "plt.show()" + ], + "language": "python", + "metadata": {}, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### UAT 2 - Creation of another figure, without the need to do plt.figure.\n", + "\n", + "As above, a new figure should be created." + ] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "plt.plot([3, 2, 1])\n", + "plt.show()" + ], + "language": "python", + "metadata": {}, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### UAT 3 - Connection info\n", + "\n", + "The printout should show that there are two figures which have active CommSockets, and no figures pending show." + ] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "print(matplotlib.backends.backend_nbagg.connection_info())" + ], + "language": "python", + "metadata": {}, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### UAT 4 - Closing figures\n", + "\n", + "Closing a specific figure instance should turn the figure into a plain image - the UI should have been removed. In this case, scroll back to the first figure and assert this is the case." + ] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "plt.close(fig1)" + ], + "language": "python", + "metadata": {}, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### UAT 5 - No show without plt.show in non-interactive mode\n", + "\n", + "Simply doing a plt.plot should not show a new figure, nor indeed update an existing one (easily verified in UAT 6).\n", + "The output should simply be a list of Line2D instances." + ] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "plt.plot(range(10))" + ], + "language": "python", + "metadata": {}, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### UAT 6 - Connection information\n", + "\n", + "We just created a new figure, but didn't show it. Connection info should no longer have \"Figure 1\" (as we closed it in UAT 4) and should have figure 2 and 3, with Figure 3 without any connections. There should be 1 figure pending." + ] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "print matplotlib.backends.backend_nbagg.connection_info()" + ], + "language": "python", + "metadata": {}, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### UAT 7 - Show of previously created figure\n", + "\n", + "We should be able to show a figure we've previously created. The following should produce two figure windows." + ] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "plt.show()\n", + "plt.figure()\n", + "plt.plot(range(5))\n", + "plt.show()" + ], + "language": "python", + "metadata": {}, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### UAT 8 - Interactive mode\n", + "\n", + "In interactive mode, creating a line should result in a figure being shown." + ] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "plt.interactive(True)\n", + "plt.figure()\n", + "plt.plot([3, 2, 1])" + ], + "language": "python", + "metadata": {}, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Subsequent lines should be added to the existing figure, rather than creating a new one." + ] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "plt.plot(range(3))" + ], + "language": "python", + "metadata": {}, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Disable interactive mode again." + ] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "plt.interactive(False)" + ], + "language": "python", + "metadata": {}, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### UAT 9 - Multiple shows\n", + "\n", + "Unlike most of the other matplotlib backends, we may want to see a figure multiple times (with or without synchronisation between the views, though the former is not yet implemented). Assert that plt.gcf().canvas.manager.reshow() results in another figure window which is synchronised upon pan & zoom." + ] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "plt.gcf().canvas.manager.reshow()" + ], + "language": "python", + "metadata": {}, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### UAT 10 - Saving notebook\n", + "\n", + "Saving the notebook (with CTRL+S or File->Save) should result in the saved notebook having static versions of the figues embedded within. The image should be the last update from user interaction and interactive plotting. (check by converting with ``ipython nbconvert ``)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### UAT 11 - Creation of a new figure on second show\n", + "\n", + "Create a figure, show it, then create a new axes and show it. The result should be a new figure.\n", + "\n", + "**BUG: Sometimes this doesn't work - not sure why (@pelson).**" + ] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "fig = plt.figure()\n", + "plt.axes()\n", + "plt.show()\n", + "\n", + "plt.plot([1, 2, 3])\n", + "plt.show()" + ], + "language": "python", + "metadata": {}, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### UAT 12 - OO interface\n", + "\n", + "Should produce a new figure and plot it." + ] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "from matplotlib.backends.backend_nbagg import new_figure_manager,show\n", + "\n", + "manager = new_figure_manager(1000)\n", + "fig = manager.canvas.figure\n", + "ax = fig.add_subplot(1,1,1)\n", + "ax.plot([1,2,3])\n", + "fig.show()" + ], + "language": "python", + "metadata": {}, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## UAT 13 - Animation\n", + "\n", + "The following should generate an animated line:" + ] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "import matplotlib.animation as animation\n", + "import numpy as np\n", + "\n", + "fig, ax = plt.subplots()\n", + "\n", + "x = np.arange(0, 2*np.pi, 0.01) # x-array\n", + "line, = ax.plot(x, np.sin(x))\n", + "\n", + "def animate(i):\n", + " line.set_ydata(np.sin(x+i/10.0)) # update the data\n", + " return line,\n", + "\n", + "#Init only required for blitting to give a clean slate.\n", + "def init():\n", + " line.set_ydata(np.ma.array(x, mask=True))\n", + " return line,\n", + "\n", + "ani = animation.FuncAnimation(fig, animate, np.arange(1, 200), init_func=init,\n", + " interval=32., blit=True)\n", + "plt.show()" + ], + "language": "python", + "metadata": {}, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### UAT 14 - Keyboard shortcuts in IPython after close of figure\n", + "\n", + "After closing the previous figure (with the close button above the figure) the IPython keyboard shortcuts should still function.\n", + "\n", + "### UAT 15 - Figure face colours\n", + "\n", + "The nbagg honours all colours appart from that of the figure.patch. The two plots below should produce a figure with a transparent background and a red background respectively (check the transparency by closing the figure, and dragging the resulting image over other content). There should be no yellow figure." + ] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "import matplotlib\n", + "matplotlib.rcParams.update({'figure.facecolor': 'red',\n", + " 'savefig.facecolor': 'yellow'})\n", + "plt.figure()\n", + "plt.plot([3, 2, 1])\n", + "\n", + "with matplotlib.rc_context({'nbagg.transparent': False}):\n", + " plt.figure()\n", + "\n", + "plt.plot([3, 2, 1])\n", + "plt.show()" + ], + "language": "python", + "metadata": {}, + "outputs": [] + } + ], + "metadata": {} + } + ] +} \ No newline at end of file diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index df217258ec14..5160f3180113 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -10,7 +10,8 @@ unicode_literals) import six -from six.moves import xrange +from six.moves import xrange, zip +from itertools import repeat import datetime import errno @@ -122,9 +123,12 @@ def new_function(): Examples -------- - # To warn of the deprecation of "matplotlib.name_of_module" - warn_deprecated('1.4.0', name='matplotlib.name_of_module', - obj_type='module') + + Basic example:: + + # To warn of the deprecation of "matplotlib.name_of_module" + warn_deprecated('1.4.0', name='matplotlib.name_of_module', + obj_type='module') """ message = _generate_deprecation_message( @@ -174,9 +178,12 @@ def new_function(): Examples -------- - @deprecated('1.4.0') - def the_function_to_deprecate(): - pass + + Basic example:: + + @deprecated('1.4.0') + def the_function_to_deprecate(): + pass """ def deprecate(func, message=message, name=name, alternative=alternative, @@ -1890,7 +1897,7 @@ def boxplot_stats(X, whis=1.5, bootstrap=None, labels=None): ======== =================================== label tick label for the boxplot mean arithemetic mean value - median 50th percentile + med 50th percentile q1 first quartile (25th percentile) q3 third quartile (75th percentile) cilo lower notch around the median @@ -1911,7 +1918,7 @@ def boxplot_stats(X, whis=1.5, bootstrap=None, labels=None): General approach from: McGill, R., Tukey, J.W., and Larsen, W.A. (1978) "Variations of - Boxplots", The American Statistician, 32:12-16. + Boxplots", The American Statistician, 32:12-16. ''' @@ -1950,14 +1957,40 @@ def _compute_conf_interval(data, med, iqr, bootstrap): ncols = len(X) if labels is None: - labels = [str(i) for i in range(1, ncols+1)] + labels = repeat(None) elif len(labels) != ncols: raise ValueError("Dimensions of labels and X must be compatible") + input_whis = whis for ii, (x, label) in enumerate(zip(X, labels), start=0): + # empty dict stats = {} - stats['label'] = label + if label is not None: + stats['label'] = label + + # restore whis to the input values in case it got changed in the loop + whis = input_whis + + # note tricksyness, append up here and then mutate below + bxpstats.append(stats) + + # if empty, bail + if len(x) == 0: + stats['fliers'] = np.array([]) + stats['mean'] = np.nan + stats['med'] = np.nan + stats['q1'] = np.nan + stats['q3'] = np.nan + stats['cilo'] = np.nan + stats['ciho'] = np.nan + stats['whislo'] = np.nan + stats['whishi'] = np.nan + stats['med'] = np.nan + continue + + # up-convert to an array, just to be safe + x = np.asarray(x) # arithmetic mean stats['mean'] = np.mean(x) @@ -2011,9 +2044,9 @@ def _compute_conf_interval(data, med, iqr, bootstrap): np.compress(x > stats['whishi'], x) ]) - # add in teh remaining stats and append to final output + # add in the remaining stats stats['q1'], stats['med'], stats['q3'] = q1, med, q3 - bxpstats.append(stats) + return bxpstats diff --git a/lib/matplotlib/colorbar.py b/lib/matplotlib/colorbar.py index b16f01c50c34..6862105bc36e 100644 --- a/lib/matplotlib/colorbar.py +++ b/lib/matplotlib/colorbar.py @@ -1014,12 +1014,12 @@ def make_axes(parents, location=None, orientation=None, fraction=0.15, Keyword arguments may include the following (with defaults): - location : [`None`|'left'|'right'|'top'|'bottom'] + location : [None|'left'|'right'|'top'|'bottom'] The position, relative to **parents**, where the colorbar axes should be created. If None, the value will either come from the given ``orientation``, else it will default to 'right'. - orientation : [`None`|'vertical'|'horizontal'] + orientation : [None|'vertical'|'horizontal'] The orientation of the colorbar. Typically, this keyword shouldn't be used, as it can be derived from the ``location`` keyword. diff --git a/lib/matplotlib/dates.py b/lib/matplotlib/dates.py index 227b39945b31..385fcbbd88fb 100755 --- a/lib/matplotlib/dates.py +++ b/lib/matplotlib/dates.py @@ -520,22 +520,23 @@ class AutoDateFormatter(ticker.Formatter): >>> formatter = AutoDateFormatter() >>> formatter.scaled[1/(24.*60.)] = '%M:%S' # only show min and sec - Custom `FunctionFormatter`s can also be used. The following example shows - how to use a custom format function to strip trailing zeros from decimal - seconds and adds the date to the first ticklabel:: - - >>> def my_format_function(x, pos=None): - ... x = matplotlib.dates.num2date(x) - ... if pos == 0: - ... fmt = '%D %H:%M:%S.%f' - ... else: - ... fmt = '%H:%M:%S.%f' - ... label = x.strftime(fmt) - ... label = label.rstrip("0") - ... label = label.rstrip(".") - ... return label - >>> from matplotlib.ticker import FuncFormatter - >>> formatter.scaled[1/(24.*60.)] = FuncFormatter(my_format_function) + A custom :class:`~matplotlib.ticker.FuncFormatter` can also be used. + The following example shows how to use a custom format function to strip + trailing zeros from decimal seconds and adds the date to the first + ticklabel:: + + >>> def my_format_function(x, pos=None): + ... x = matplotlib.dates.num2date(x) + ... if pos == 0: + ... fmt = '%D %H:%M:%S.%f' + ... else: + ... fmt = '%H:%M:%S.%f' + ... label = x.strftime(fmt) + ... label = label.rstrip("0") + ... label = label.rstrip(".") + ... return label + >>> from matplotlib.ticker import FuncFormatter + >>> formatter.scaled[1/(24.*60.)] = FuncFormatter(my_format_function) """ # This can be improved by providing some user-level direction on @@ -898,6 +899,10 @@ def get_locator(self, dmin, dmax): 'Pick the best locator based on a distance.' delta = relativedelta(dmax, dmin) + # take absolute difference + if dmin > dmax: + delta = -delta + numYears = (delta.years * 1.0) numMonths = (numYears * 12.0) + delta.months numDays = (numMonths * 31.0) + delta.days diff --git a/lib/matplotlib/legend.py b/lib/matplotlib/legend.py index 928f52c3b8a6..23949ce0e562 100644 --- a/lib/matplotlib/legend.py +++ b/lib/matplotlib/legend.py @@ -367,7 +367,9 @@ def __init__(self, parent, handles, labels, # init with null renderer self._init_legend_box(handles, labels) - if framealpha is not None: + if framealpha is None: + self.get_frame().set_alpha(rcParams["legend.framealpha"]) + else: self.get_frame().set_alpha(framealpha) self._loc = loc diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index 6343684a207d..cb7cd3ecb7d0 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -937,6 +937,7 @@ def set_linestyle(self, linestyle): ACCEPTS: [``'-'`` | ``'--'`` | ``'-.'`` | ``':'`` | ``'None'`` | ``' '`` | ``''``] + and any drawstyle in combination with a linestyle, e.g., ``'steps--'``. """ diff --git a/lib/matplotlib/mlab.py b/lib/matplotlib/mlab.py index dea049076a5b..a2917b9e1dd1 100644 --- a/lib/matplotlib/mlab.py +++ b/lib/matplotlib/mlab.py @@ -394,6 +394,7 @@ def demean(x, axis=0): .. seealso:: :func:`delinear` + :func:`denone` :func:`delinear` and :func:`denone` are other detrend algorithms. @@ -427,6 +428,7 @@ def detrend_mean(x, axis=None): for the default *axis*. :func:`detrend_linear` + :func:`detrend_none` :func:`detrend_linear` and :func:`detrend_none` are other detrend algorithms. @@ -474,6 +476,7 @@ def detrend_none(x, axis=None): for the default *axis*, which has no effect. :func:`detrend_mean` + :func:`detrend_linear` :func:`detrend_mean` and :func:`detrend_linear` are other detrend algorithms. @@ -506,6 +509,7 @@ def detrend_linear(y): for the default *axis*. :func:`detrend_mean` + :func:`detrend_none` :func:`detrend_mean` and :func:`detrend_none` are other detrend algorithms. @@ -537,9 +541,11 @@ def stride_windows(x, n, noverlap=None, axis=0): Get all windows of x with length n as a single array, using strides to avoid data duplication. - .. warning:: It is not safe to write to the output array. Multiple - elements may point to the same piece of memory, so modifying one value may - change others. + .. warning:: + + It is not safe to write to the output array. Multiple + elements may point to the same piece of memory, + so modifying one value may change others. Call signature:: @@ -599,9 +605,11 @@ def stride_repeat(x, n, axis=0): Repeat the values in an array in a memory-efficient manner. Array x is stacked vertically n times. - .. warning:: It is not safe to write to the output array. Multiple - elements may point to the same piece of memory, so modifying one value may - change others. + .. warning:: + + It is not safe to write to the output array. Multiple + elements may point to the same piece of memory, so + modifying one value may change others. Call signature:: @@ -878,6 +886,7 @@ def _single_spectrum_helper(x, mode, Fs=None, window=None, pad_to=None, *detrend*: [ 'default' | 'constant' | 'mean' | 'linear' | 'none'] or callable + The function applied to each segment before fft-ing, designed to remove the mean or linear trend. Unlike in MATLAB, where the *detrend* parameter is a vector, in @@ -1244,6 +1253,7 @@ def specgram(x, NFFT=None, Fs=None, detrend=None, window=None, *mode*: [ 'default' | 'psd' | 'complex' | 'magnitude' 'angle' | 'phase' ] + What sort of spectrum to use. Default is 'psd'. which takes the power spectral density. 'complex' returns the complex-valued frequency spectrum. 'magnitude' returns the magnitude spectrum. @@ -1599,7 +1609,9 @@ def longest_ones(x): def prepca(P, frac=0): """ - WARNING: this function is deprecated -- please see class PCA instead + .. warning:: + + This function is deprecated -- please see class PCA instead Compute the principal components of *P*. *P* is a (*numVars*, *numObs*) array. *frac* is the minimum fraction of variance that a diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 5bd6488824e9..e1e9c55bfff6 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -91,8 +91,9 @@ def _backend_selection(): if not PyQt5.QtWidgets.qApp.startingUp(): # The mainloop is running. rcParams['backend'] = 'qt5Agg' - elif 'gtk' in sys.modules and not backend in ('GTK', 'GTKAgg', - 'GTKCairo'): + elif ('gtk' in sys.modules + and backend not in ('GTK', 'GTKAgg', 'GTKCairo') + and 'gi.repository.GObject' not in sys.modules): import gobject if gobject.MainLoop().is_running(): rcParams['backend'] = 'gtk' + 'Agg' * is_agg_backend @@ -1311,17 +1312,24 @@ def title(s, *args, **kwargs): positioned above the axes in the center, flush with the left edge, and flush with the right edge. + .. seealso:: + See :func:`~matplotlib.pyplot.text` for adding text + to the current axes + Parameters ---------- label : str Text to use for the title + fontdict : dict A dictionary controlling the appearance of the title text, the default `fontdict` is: - {'fontsize': rcParams['axes.titlesize'], - 'fontweight' : rcParams['axes.titleweight'], - 'verticalalignment': 'baseline', - 'horizontalalignment': loc} + + {'fontsize': rcParams['axes.titlesize'], + 'fontweight' : rcParams['axes.titleweight'], + 'verticalalignment': 'baseline', + 'horizontalalignment': loc} + loc : {'center', 'left', 'right'}, str, optional Which title to set, defaults to 'center' @@ -1332,13 +1340,10 @@ def title(s, *args, **kwargs): Other parameters ---------------- - Other keyword arguments are text properties, see - :class:`~matplotlib.text.Text` for a list of valid text - properties. - - See also - -------- - See :func:`~matplotlib.pyplot.text` for adding text to the current axes + kwargs : text properties + Other keyword arguments are text properties, see + :class:`~matplotlib.text.Text` for a list of valid text + properties. """ l = gca().set_title(s, *args, **kwargs) @@ -2611,7 +2616,7 @@ def broken_barh(xranges, yrange, hold=None, **kwargs): # This function was autogenerated by boilerplate.py. Do not edit as # changes will be lost @_autogen_docstring(Axes.boxplot) -def boxplot(x, notch=False, sym='b+', vert=True, whis=1.5, positions=None, +def boxplot(x, notch=False, sym=None, vert=True, whis=1.5, positions=None, widths=None, patch_artist=False, bootstrap=None, usermedians=None, conf_intervals=None, meanline=False, showmeans=False, showcaps=True, showbox=True, showfliers=True, boxprops=None, labels=None, diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index 7d4d97df1572..e0bfb8b96395 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -485,6 +485,7 @@ def __call__(self, s): 'webagg.port': [8988, validate_int], 'webagg.open_in_browser': [True, validate_bool], 'webagg.port_retries': [50, validate_int], + 'nbagg.transparent': [True, validate_bool], 'toolbar': ['toolbar2', validate_toolbar], 'datapath': [None, validate_path_exists], # handled by # _get_data_path_cached @@ -632,7 +633,8 @@ def __call__(self, s): 'legend.shadow': [False, validate_bool], # whether or not to draw a frame around legend 'legend.frameon': [True, validate_bool], - + # alpha value of the legend frame + 'legend.framealpha': [1.0, validate_float], ## the following dimensions are in fraction of the font size 'legend.borderpad': [0.4, validate_float], # units are fontsize diff --git a/lib/matplotlib/sphinxext/plot_directive.py b/lib/matplotlib/sphinxext/plot_directive.py index 08093fe2a7ec..74d61006ca01 100644 --- a/lib/matplotlib/sphinxext/plot_directive.py +++ b/lib/matplotlib/sphinxext/plot_directive.py @@ -383,7 +383,9 @@ def remove_coding(text): {{ only_latex }} {% for img in images %} + {% if 'pdf' in img.formats -%} .. image:: {{ build_dir }}/{{ img.basename }}.pdf + {% endif -%} {% endfor %} {{ only_texinfo }} @@ -447,8 +449,10 @@ def run_code(code, code_path, ns=None, function_name=None): # Change the working directory to the directory of the example, so # it can get at its data files, if any. Add its path to sys.path # so it can import any helper modules sitting beside it. - - pwd = os.getcwd() + if six.PY2: + pwd = os.getcwdu() + else: + pwd = os.getcwd() old_sys_path = list(sys.path) if setup.config.plot_working_directory is not None: try: diff --git a/lib/matplotlib/tests/baseline_images/test_dates/date_inverted_limit.png b/lib/matplotlib/tests/baseline_images/test_dates/date_inverted_limit.png new file mode 100644 index 000000000000..73e635eb5189 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_dates/date_inverted_limit.png differ diff --git a/lib/matplotlib/tests/test_cbook.py b/lib/matplotlib/tests/test_cbook.py index d41044c004a4..61d69fd76efd 100644 --- a/lib/matplotlib/tests/test_cbook.py +++ b/lib/matplotlib/tests/test_cbook.py @@ -121,8 +121,7 @@ def setup(self): 'q1': 1.3597529879465153, 'q3': 14.85246294739361, 'whishi': 27.899688243699629, - 'whislo': 0.042143774965502923, - 'label': 1 + 'whislo': 0.042143774965502923 } self.known_bootstrapped_ci = { @@ -136,10 +135,6 @@ def setup(self): 'fliers': np.array([92.55467075, 87.03819018]), } - self.known_res_with_labels = { - 'label': 'Test1' - } - self.known_res_percentiles = { 'whislo': 0.1933685896907924, 'whishi': 42.232049135969874 @@ -229,11 +224,15 @@ def test_results_whiskers_percentiles(self): ) def test_results_withlabels(self): - labels = ['Test1', 2, 3, 4] + labels = ['Test1', 2, 'ardvark', 4] results = cbook.boxplot_stats(self.data, labels=labels) res = results[0] - for key in list(self.known_res_with_labels.keys()): - assert_equal(res[key], self.known_res_with_labels[key]) + for lab, res in zip(labels, results): + assert_equal(res['label'], lab) + + results = cbook.boxplot_stats(self.data) + for res in results: + assert('label' not in res) @raises(ValueError) def test_label_error(self): diff --git a/lib/matplotlib/tests/test_coding_standards.py b/lib/matplotlib/tests/test_coding_standards.py index 3c63cd0c4a24..24bda42fba23 100644 --- a/lib/matplotlib/tests/test_coding_standards.py +++ b/lib/matplotlib/tests/test_coding_standards.py @@ -96,7 +96,6 @@ '*/matplotlib/tri/triinterpolate.py', '*/matplotlib/tests/test_axes.py', '*/matplotlib/tests/test_bbox_tight.py', - '*/matplotlib/tests/test_dates.py', '*/matplotlib/tests/test_delaunay.py', '*/matplotlib/tests/test_dviread.py', '*/matplotlib/tests/test_image.py', diff --git a/lib/matplotlib/tests/test_dates.py b/lib/matplotlib/tests/test_dates.py index e0d6415e2890..7b847bb0df6d 100644 --- a/lib/matplotlib/tests/test_dates.py +++ b/lib/matplotlib/tests/test_dates.py @@ -162,9 +162,9 @@ def test_DateFormatter(): def test_date_formatter_callable(): scale = -11 locator = mock.Mock(_get_unit=mock.Mock(return_value=scale)) - callable_formatting_function = lambda dates, _: \ - [dt.strftime('%d-%m//%Y') for dt in dates] - + callable_formatting_function = (lambda dates, _: + [dt.strftime('%d-%m//%Y') for dt in dates]) + formatter = mdates.AutoDateFormatter(locator) formatter.scaled[-10] = callable_formatting_function assert_equal(formatter([datetime.datetime(2014, 12, 25)]), @@ -223,7 +223,8 @@ def test_auto_date_locator(): def _create_auto_date_locator(date1, date2): locator = mdates.AutoDateLocator() locator.create_dummy_axis() - locator.set_view_interval(mdates.date2num(date1), mdates.date2num(date2)) + locator.set_view_interval(mdates.date2num(date1), + mdates.date2num(date2)) return locator d1 = datetime.datetime(1990, 1, 1) @@ -275,8 +276,10 @@ def _create_auto_date_locator(date1, date2): '1990-01-01 00:00:40+00:00'] ], [datetime.timedelta(microseconds=1500), - ['1989-12-31 23:59:59.999507+00:00', '1990-01-01 00:00:00+00:00', - '1990-01-01 00:00:00.000502+00:00', '1990-01-01 00:00:00.001005+00:00', + ['1989-12-31 23:59:59.999507+00:00', + '1990-01-01 00:00:00+00:00', + '1990-01-01 00:00:00.000502+00:00', + '1990-01-01 00:00:00.001005+00:00', '1990-01-01 00:00:00.001508+00:00'] ], ) @@ -288,6 +291,21 @@ def _create_auto_date_locator(date1, date2): expected) +@image_comparison(baseline_images=['date_inverted_limit'], + extensions=['png']) +def test_date_inverted_limit(): + # test ax hline with date inputs + t0 = datetime.datetime(2009, 1, 20) + tf = datetime.datetime(2009, 1, 31) + fig = plt.figure() + ax = fig.add_subplot(1, 1, 1) + ax.axhline(t0, color="blue", lw=3) + ax.set_ylim(t0 - datetime.timedelta(days=5), + tf + datetime.timedelta(days=5)) + ax.invert_yaxis() + fig.subplots_adjust(left=0.25) + + if __name__ == '__main__': import nose nose.runmodule(argv=['-s', '--with-doctest'], exit=False) diff --git a/lib/matplotlib/tri/trirefine.py b/lib/matplotlib/tri/trirefine.py index 11e3fff3e468..62a9f804ac1c 100644 --- a/lib/matplotlib/tri/trirefine.py +++ b/lib/matplotlib/tri/trirefine.py @@ -37,6 +37,7 @@ class TriRefiner(object): :class:`~matplotlib.tri.TriInterpolator` (optional) - the other optional keyword arguments *kwargs* are defined in each TriRefiner concrete implementation + and which returns (as a tuple) a refined triangular mesh and the interpolated values of the field at the refined triangulation nodes. diff --git a/lib/mpl_toolkits/mplot3d/art3d.py b/lib/mpl_toolkits/mplot3d/art3d.py index ee985cc6de02..6381a685239f 100755 --- a/lib/mpl_toolkits/mplot3d/art3d.py +++ b/lib/mpl_toolkits/mplot3d/art3d.py @@ -436,9 +436,12 @@ def patch_collection_2d_to_3d(col, zs=0, zdir='z', depthshade=True): :class:`Path3DCollection` object). Keywords: + *za* The location or locations to place the patches in the collection along the *zdir* axis. Defaults to 0. + *zdir* The axis in which to place the patches. Default is "z". + *depthshade* Whether to shade the patches to give a sense of depth. Defaults to *True*. diff --git a/matplotlibrc.template b/matplotlibrc.template index 6edf352f7ae5..af7940a4ae67 100644 --- a/matplotlibrc.template +++ b/matplotlibrc.template @@ -57,6 +57,10 @@ backend : %(backend)s # When True, open the webbrowser to the plot that is shown # webagg.open_in_browser : True +# When True, the figures rendered in the nbagg backend are created with +# a transparent background. +# nbagg.transparent : True + # if you are running pyplot inside a GUI and your backend choice # conflicts, we will automatically try to find a compatible one for # you if backend_fallback is True @@ -316,6 +320,7 @@ backend : %(backend)s #legend.columnspacing : 2. # the border between the axes and legend edge in fraction of fontsize #legend.shadow : False #legend.frameon : True # whether or not to draw a frame around legend +#legend.framealpha : 1.0 # opacity of of legend frame #legend.scatterpoints : 3 # number of scatter points ### FIGURE diff --git a/setup.py b/setup.py index 64e3d7e38c40..aeeb5816f38b 100644 --- a/setup.py +++ b/setup.py @@ -66,6 +66,7 @@ setupext.Numpy(), setupext.Six(), setupext.Dateutil(), + setupext.Pytz(), setupext.Tornado(), setupext.Pyparsing(), setupext.CXX(), diff --git a/setupext.py b/setupext.py index 63e87d1352b0..1f36ddd14038 100755 --- a/setupext.py +++ b/setupext.py @@ -120,6 +120,8 @@ def has_include_file(include_dirs, filename): Returns `True` if `filename` can be found in one of the directories in `include_dirs`. """ + if sys.platform == 'win32': + include_dirs += os.environ.get('INCLUDE', '.').split(';') for dir in include_dirs: if os.path.exists(os.path.join(dir, filename)): return True @@ -130,8 +132,6 @@ def check_include_file(include_dirs, filename, package): """ Raises an exception if the given include file can not be found. """ - if sys.platform == 'win32': - include_dirs.extend(os.getenv('INCLUDE', '.').split(';')) if not has_include_file(include_dirs, filename): raise CheckFailed( "The C/C++ header for %s (%s) could not be found. You " @@ -156,6 +156,13 @@ def get_base_dirs(): return basedir_map.get(sys.platform, ['/usr/local', '/usr']) +def get_include_dirs(): + """ + Returns a list of standard include directories on this platform. + """ + return [os.path.join(d, 'include') for d in get_base_dirs()] + + def is_min_version(found, minversion): """ Returns `True` if `found` is at least as high a version as @@ -830,10 +837,13 @@ class CXX(SetupPackage): def check(self): if PY3: # There is no version of PyCXX in the wild that will work - # with Python 3.x + # with Python 3.x and matplotlib, since they lack support + # for the buffer object. self.__class__.found_external = False - return ("Official versions of PyCXX are not compatible with " - "Python 3.x. Using local copy") + return ("Official versions of PyCXX are not compatible " + "with matplotlib on Python 3.x, since they lack " + "support for the buffer object. Using local " + "copy") self.__class__.found_external = True old_stdout = sys.stdout @@ -927,7 +937,8 @@ class FreeType(SetupPackage): def check(self): if sys.platform == 'win32': - return "Unknown version" + check_include_file(get_include_dirs(), 'ft2build.h', 'freetype') + return 'Using unknown version found on system.' status, output = getstatusoutput("freetype-config --ftversion") if status == 0: @@ -984,6 +995,7 @@ def get_extension(self): self.add_flags(ext) return ext + class FT2Font(SetupPackage): name = 'ft2font' @@ -1004,7 +1016,8 @@ class Png(SetupPackage): def check(self): if sys.platform == 'win32': - return "Unknown version" + check_include_file(get_include_dirs(), 'png.h', 'png') + return 'Using unknown version found on system.' status, output = getstatusoutput("libpng-config --version") if status == 0: @@ -1017,9 +1030,7 @@ def check(self): 'libpng', 'png.h', min_version='1.2', version=version) except CheckFailed as e: - include_dirs = [ - os.path.join(dir, 'include') for dir in get_base_dirs()] - if has_include_file(include_dirs, 'png.h'): + if has_include_file(get_include_dirs(), 'png.h'): return str(e) + ' Using unknown version found on system.' raise @@ -1050,7 +1061,7 @@ def check(self): # present on this system, so check if the header files can be # found. include_dirs = [ - os.path.join(x, 'include', 'qhull') for x in get_base_dirs()] + os.path.join(x, 'qhull') for x in get_include_dirs()] if has_include_file(include_dirs, 'qhull_a.h'): return 'Using system Qhull (version unknown, no pkg-config info)' else: @@ -1169,7 +1180,7 @@ def get_extension(self): class Six(SetupPackage): name = "six" - min_version = "1.3" + min_version = "1.4" def check(self): try: @@ -1189,6 +1200,24 @@ def get_install_requires(self): return ['six>={0}'.format(self.min_version)] +class Pytz(SetupPackage): + name = "pytz" + + def check(self): + try: + import pytz + except ImportError: + return ( + "pytz was not found. " + "pip will attempt to install it " + "after matplotlib.") + + return "using pytz version %s" % pytz.__version__ + + def get_install_requires(self): + return ['pytz'] + + class Dateutil(SetupPackage): name = "dateutil" diff --git a/src/_backend_agg.cpp b/src/_backend_agg.cpp index b088c97c9aa3..5aab419f5125 100644 --- a/src/_backend_agg.cpp +++ b/src/_backend_agg.cpp @@ -769,10 +769,10 @@ RendererAgg::draw_markers(const Py::Tuple& args) agg::serialized_scanlines_adaptor_aa8::embedded_scanline sl; agg::rect_d clipping_rect( - -(scanlines.min_x() + 1.0), - (scanlines.max_y() + 1.0), - width + scanlines.max_x() + 1.0, - height - scanlines.min_y() + 1.0); + -1.0 - scanlines.max_x(), + -1.0 - scanlines.max_y(), + 1.0 + width - scanlines.min_x(), + 1.0 + height - scanlines.min_y()); if (has_clippath) { diff --git a/src/_macosx.m b/src/_macosx.m index aa326ef02483..ac4ff6a9f424 100644 --- a/src/_macosx.m +++ b/src/_macosx.m @@ -2531,14 +2531,23 @@ static CGFloat _get_device_scale(CGContextRef cr) "CourierNewPS-Bold-ItalicMT"}, }; - if(!PyList_Check(family)) return 0; + if(!PyList_Check(family)) + { + PyErr_SetString(PyExc_ValueError, "family should be a list"); + return 0; + } n = PyList_GET_SIZE(family); for (i = 0; i < n; i++) { PyObject* item = PyList_GET_ITEM(family, i); ascii = PyUnicode_AsASCIIString(item); - if(!ascii) return 0; + if(!ascii) + { + PyErr_SetString(PyExc_ValueError, + "failed to convert font family name to ASCII"); + return 0; + } temp = PyBytes_AS_STRING(ascii); for (j = 0; j < NMAP; j++) { if (!strcmp(map[j].name, temp)) diff --git a/src/_path.cpp b/src/_path.cpp index 27bb4b563fc2..783aee0654f9 100644 --- a/src/_path.cpp +++ b/src/_path.cpp @@ -770,7 +770,14 @@ _path_module::point_in_path_collection(const Py::Tuple& args) throw Py::ValueError("Offsets array must be Nx2"); } + Py::List result; + size_t Npaths = paths.length(); + + if (Npaths == 0) { + return result; + } + size_t Noffsets = offsets->dimensions[0]; size_t N = std::max(Npaths, Noffsets); size_t Ntransforms = std::min(transforms_obj.length(), N); @@ -788,7 +795,6 @@ _path_module::point_in_path_collection(const Py::Tuple& args) transforms.push_back(trans); } - Py::List result; agg::trans_affine trans; for (i = 0; i < N; ++i)