diff --git a/doc/devel/MEP/MEP27.rst b/doc/devel/MEP/MEP27.rst index 57b0540a4c91..8e71c5ef0a9f 100644 --- a/doc/devel/MEP/MEP27.rst +++ b/doc/devel/MEP/MEP27.rst @@ -8,14 +8,16 @@ Status ====== -**Discussion** +**Progress** Branches and Pull requests ========================== Main PR (including GTK3): + + https://github.com/matplotlib/matplotlib/pull/4143 Backend specific branch diffs: + + https://github.com/OceanWolf/matplotlib/compare/backend-refactor...OceanWolf:backend-refactor-tkagg + https://github.com/OceanWolf/matplotlib/compare/backend-refactor...OceanWolf:backend-refactor-qt + https://github.com/OceanWolf/matplotlib/compare/backend-refactor...backend-refactor-wx @@ -79,7 +81,7 @@ The description of this MEP gives us most of the solution: 1. To remove the windowing aspect out of ``FigureManagerBase`` letting it simply wrap this new class along with the other backend classes. Create a new ``WindowBase`` class that can handle this - functionality, with pass-through methods (:arrow_right:) to + functionality, with pass-through methods (->) to ``WindowBase``. Classes that subclass ``WindowBase`` should also subclass the GUI specific window class to ensure backward compatibility (``manager.window == manager.window``). @@ -103,30 +105,30 @@ The description of this MEP gives us most of the solution: |FigureManagerBase(canvas, num) |FigureManager(figure, num) |``WindowBase(title)``|Notes | | | | | | +======================================+==============================+=====================+================================+ -|show | |show | | +|show |-> |show | | +--------------------------------------+------------------------------+---------------------+--------------------------------+ |destroy |calls destroy on all |destroy | | | |components | | | +--------------------------------------+------------------------------+---------------------+--------------------------------+ |full_screen_toggle |handles logic |set_fullscreen | | +--------------------------------------+------------------------------+---------------------+--------------------------------+ -|resize | |resize | | +|resize |-> |resize | | +--------------------------------------+------------------------------+---------------------+--------------------------------+ -|key_press |key_press | | | +|key_press |key_press |X | | +--------------------------------------+------------------------------+---------------------+--------------------------------+ -|show_popup |show_poup | |Not used anywhere in mpl, and | +|show_popup |show_poup |X |Not used anywhere in mpl, and | | | | |does nothing. | +--------------------------------------+------------------------------+---------------------+--------------------------------+ -|get_window_title | |get_window_title | | +|get_window_title |-> |get_window_title | | +--------------------------------------+------------------------------+---------------------+--------------------------------+ -|set_window_title | |set_window_title | | +|set_window_title |-> |set_window_title | | +--------------------------------------+------------------------------+---------------------+--------------------------------+ -| |_get_toolbar | |A common method to all | +|X |_get_toolbar |X |A common method to all | | | | |subclasses of FigureManagerBase | +--------------------------------------+------------------------------+---------------------+--------------------------------+ -| | |set_default_size | | +|X |X |set_default_size | | +--------------------------------------+------------------------------+---------------------+--------------------------------+ -| | |add_element_to_window| | +|X |X |add_element | | +--------------------------------------+------------------------------+---------------------+--------------------------------+ @@ -135,14 +137,14 @@ The description of this MEP gives us most of the solution: +==========+============+=============+ |mainloop |begin | | +----------+------------+-------------+ -| |end |Gets called | +|X |end |Gets called | | | |automagically| | | |when no more | | | |instances of | | | |the subclass | | | |exist | +----------+------------+-------------+ -|__call__ | |Method moved | +|__call__ |X |Method moved | | | |to | | | |Gcf.show_all | +----------+------------+-------------+ @@ -191,6 +193,8 @@ in the same manner as everything else. | | |window, so this also | | | |breaks BC. | +-------------------------+-------------------------+-------------------------+ +|WebAgg |canvas | | ++-------------------------+-------------------------+-------------------------+ Alternatives diff --git a/doc/users/whats_new/plotting.rst b/doc/users/whats_new/plotting.rst index 1139b5e78e78..1ca2f6a69434 100644 --- a/doc/users/whats_new/plotting.rst +++ b/doc/users/whats_new/plotting.rst @@ -1,3 +1,11 @@ +Plot bar and barh with labels +````````````````````````````` + +Added kwarg "tick_label" to bar and barh to support plotting bar graphs with a +text label for each bar. +Example: + bar([1, 2], [1, 1], tick_label=['bar1', 'bar2']) + Added center and frame kwargs to pie ```````````````````````````````````` diff --git a/doc/users/whats_new/updated_figimage.rst b/doc/users/whats_new/updated_figimage.rst new file mode 100644 index 000000000000..d9198e225503 --- /dev/null +++ b/doc/users/whats_new/updated_figimage.rst @@ -0,0 +1,8 @@ +updated figimage to take optional resize parameter +---------------------------------------------------- +Added the ability to plot simple 2D-Array using plt.figimage(X, resize=True). +This is useful for plotting simple 2D-Array without the Axes or whitespacing +around the image. +Example: + data = np.random.random( [500, 500] ) + plt.figimage(data, resize=True) diff --git a/doc/users/whats_new/updated_figure.rst b/doc/users/whats_new/updated_figure.rst new file mode 100644 index 000000000000..881fb1192dc4 --- /dev/null +++ b/doc/users/whats_new/updated_figure.rst @@ -0,0 +1,9 @@ +Updated Figure.savefig() +------------------------ + +Added support to save the figure with the same dpi as the figure on the screen using dpi='figure' + +Example: + f = plt.figure(dpi=25) # dpi set to 25 + S = plt.scatter([1,2,3],[4,5,6]) + f.savefig('output.png', dpi='figure') # output savefig dpi set to 25 (same as figure) diff --git a/doc/users/whats_new/updated_widgets.rst b/doc/users/whats_new/updated_widgets.rst new file mode 100644 index 000000000000..702be6f0cefb --- /dev/null +++ b/doc/users/whats_new/updated_widgets.rst @@ -0,0 +1,10 @@ +Moved ``ignore``, ``set_active``, and ``get_active`` methods to base class ``Widget`` +````````````````````````````````````````````````````````````````````````````````` +Pushes up duplicate methods in child class to parent class to avoid duplication of code. + + +Adds enable/disable feature to MultiCursor +`````````````````````````````````````````` +A MultiCursor object can be disabled (and enabled) after it has been created without destroying the object. +Example: + multi_cursor.active = False diff --git a/examples/user_interfaces/embedding_in_qt4.py b/examples/user_interfaces/embedding_in_qt4.py index 5590466f7462..0714b5c4dce3 100755 --- a/examples/user_interfaces/embedding_in_qt4.py +++ b/examples/user_interfaces/embedding_in_qt4.py @@ -13,8 +13,8 @@ import sys import os import random -from matplotlib.backends import qt4_compat -use_pyside = qt4_compat.QT_API == qt4_compat.QT_API_PYSIDE +from matplotlib.backends import qt_compat +use_pyside = qt_compat.QT_API == qt_compat.QT_API_PYSIDE if use_pyside: from PySide import QtGui, QtCore else: diff --git a/examples/user_interfaces/embedding_in_qt5.py b/examples/user_interfaces/embedding_in_qt5.py new file mode 100755 index 000000000000..6f35d7745d3b --- /dev/null +++ b/examples/user_interfaces/embedding_in_qt5.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python + +# embedding_in_qt5.py --- Simple Qt5 application embedding matplotlib canvases +# +# Copyright (C) 2005 Florent Rougon +# 2006 Darren Dale +# 2015 Jens H Nielsen +# +# This file is an example program for matplotlib. It may be used and +# modified with no restriction; raw copies as well as modified versions +# may be distributed without limitation. + +from __future__ import unicode_literals +import sys +import os +import random +import matplotlib +# Make sure that we are using QT5 +matplotlib.use('Qt5Agg') +from PyQt5 import QtGui, QtCore, QtWidgets + +from numpy import arange, sin, pi +from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas +from matplotlib.figure import Figure + +progname = os.path.basename(sys.argv[0]) +progversion = "0.1" + + +class MyMplCanvas(FigureCanvas): + """Ultimately, this is a QWidget (as well as a FigureCanvasAgg, etc.).""" + + def __init__(self, parent=None, width=5, height=4, dpi=100): + fig = Figure(figsize=(width, height), dpi=dpi) + self.axes = fig.add_subplot(111) + # We want the axes cleared every time plot() is called + self.axes.hold(False) + + self.compute_initial_figure() + + # + FigureCanvas.__init__(self, fig) + self.setParent(parent) + + FigureCanvas.setSizePolicy(self, + QtWidgets.QSizePolicy.Expanding, + QtWidgets.QSizePolicy.Expanding) + FigureCanvas.updateGeometry(self) + + def compute_initial_figure(self): + pass + + +class MyStaticMplCanvas(MyMplCanvas): + """Simple canvas with a sine plot.""" + + def compute_initial_figure(self): + t = arange(0.0, 3.0, 0.01) + s = sin(2*pi*t) + self.axes.plot(t, s) + + +class MyDynamicMplCanvas(MyMplCanvas): + """A canvas that updates itself every second with a new plot.""" + + def __init__(self, *args, **kwargs): + MyMplCanvas.__init__(self, *args, **kwargs) + timer = QtCore.QTimer(self) + timer.timeout.connect(self.update_figure) + timer.start(1000) + + def compute_initial_figure(self): + self.axes.plot([0, 1, 2, 3], [1, 2, 0, 4], 'r') + + def update_figure(self): + # Build a list of 4 random integers between 0 and 10 (both inclusive) + l = [random.randint(0, 10) for i in range(4)] + + self.axes.plot([0, 1, 2, 3], l, 'r') + self.draw() + + +class ApplicationWindow(QtWidgets.QMainWindow): + def __init__(self): + QtWidgets.QMainWindow.__init__(self) + self.setAttribute(QtCore.Qt.WA_DeleteOnClose) + self.setWindowTitle("application main window") + + self.file_menu = QtWidgets.QMenu('&File', self) + self.file_menu.addAction('&Quit', self.fileQuit, + QtCore.Qt.CTRL + QtCore.Qt.Key_Q) + self.menuBar().addMenu(self.file_menu) + + self.help_menu = QtWidgets.QMenu('&Help', self) + self.menuBar().addSeparator() + self.menuBar().addMenu(self.help_menu) + + self.help_menu.addAction('&About', self.about) + + self.main_widget = QtWidgets.QWidget(self) + + l = QtWidgets.QVBoxLayout(self.main_widget) + sc = MyStaticMplCanvas(self.main_widget, width=5, height=4, dpi=100) + dc = MyDynamicMplCanvas(self.main_widget, width=5, height=4, dpi=100) + l.addWidget(sc) + l.addWidget(dc) + + self.main_widget.setFocus() + self.setCentralWidget(self.main_widget) + + self.statusBar().showMessage("All hail matplotlib!", 2000) + + def fileQuit(self): + self.close() + + def closeEvent(self, ce): + self.fileQuit() + + def about(self): + QtGui.QMessageBox.about(self, "About", + """embedding_in_qt5.py example +Copyright 2005 Florent Rougon, 2006 Darren Dale, 2015 Jens H Nielsen + +This program is a simple example of a Qt5 application embedding matplotlib +canvases. + +It may be used and modified with no restriction; raw copies as well as +modified versions may be distributed without limitation. + +This is modified from the embedding in qt4 example to show the difference +between qt4 and qt5""" + ) + + +qApp = QtWidgets.QApplication(sys.argv) + +aw = ApplicationWindow() +aw.setWindowTitle("%s" % progname) +aw.show() +sys.exit(qApp.exec_()) +#qApp.exec_() diff --git a/examples/user_interfaces/multifigure.py b/examples/user_interfaces/multifigure.py new file mode 100644 index 000000000000..15d81c9b1490 --- /dev/null +++ b/examples/user_interfaces/multifigure.py @@ -0,0 +1,60 @@ +# from __future__ import print_function +import matplotlib +matplotlib.use('GTK3AGG') +# matplotlib.rcParams['backend.multifigure'] = False +matplotlib.rcParams['toolbar'] = 'toolmanager' +from matplotlib.backend_tools import ToolToggleBase + + +from matplotlib.figure import Figure +from matplotlib.backend_managers import FigureManager + + +class t1(ToolToggleBase): + radio_group = 'multifigure' + description = "change canvas" + def __init__(self, *args, **kwargs): + self.mfigure = kwargs.pop('figure') + ToolToggleBase.__init__(self, *args, **kwargs) + + def enable(self, *args, **kwargs): + self.toolmanager.manager.figure = self.mfigure + + +manager = FigureManager(None, 1) +backend = manager._backend + +fig1 = Figure() +canvas1 = backend.FigureCanvas(fig1) +ax1 = fig1.add_subplot(111) +ax1.plot([1, 2, 3]) + +manager.figure = fig1 + +fig2 = Figure() +canvas2 = backend.FigureCanvas(fig2) +ax2 = fig2.add_subplot(111) +ax2.plot([3, 2, 1]) + +fig3 = Figure() +canvas3 = backend.FigureCanvas(fig3) +ax3 = fig3.add_subplot(111) +ax3.plot([1, 1, 1]) + +sidebar = manager._get_toolbar() +sidebar.set_flow('vertical') + + +manager.toolmanager.add_tool('f1', t1, figure=fig1) +manager.toolmanager.add_tool('f2', t1, figure=fig2) +manager.toolmanager.add_tool('f3', t1, figure=fig3) +sidebar.add_tool('f1', 'foo') +sidebar.add_tool('f2', 'foo') +sidebar.add_tool('f3', 'foo') +manager.window.add_element(sidebar, False, 'west') + + +manager.show() +manager.mpl_connect('window_destroy_event', manager.destroy) +manager._mainloop() + diff --git a/examples/user_interfaces/toolmanager.py b/examples/user_interfaces/toolmanager.py index 5240bab239c2..598b69281bf0 100644 --- a/examples/user_interfaces/toolmanager.py +++ b/examples/user_interfaces/toolmanager.py @@ -12,8 +12,10 @@ matplotlib.use('GTK3Cairo') matplotlib.rcParams['toolbar'] = 'toolmanager' import matplotlib.pyplot as plt -from matplotlib.backend_tools import ToolBase, ToolToggleBase +from matplotlib.backend_tools import (ToolBase, ToolToggleBase, + add_tools_to_container) from gi.repository import Gtk, Gdk +from random import uniform class ListTools(ToolBase): @@ -60,7 +62,6 @@ def disable(self, *args): self.set_lines_visibility(True) def set_lines_visibility(self, state): - gr_lines = [] for ax in self.figure.get_axes(): for line in ax.get_lines(): if line.get_gid() == self.gid: @@ -68,14 +69,44 @@ def set_lines_visibility(self, state): self.figure.canvas.draw() +class LineTool(ToolBase): + description = 'Draw a random line' + + def __init__(self, *args, **kwargs): + self.color = kwargs.pop('color') + ToolBase.__init__(self, *args, **kwargs) + + def trigger(self, *args, **kwargs): + x0, y0, x1, y1 = (uniform(0, 2), uniform(1, 4), uniform(0, 2), + uniform(1, 4)) + fig = self.figure + fig.gca().plot([x0, x1], [y0, y1], color=self.color, gid=self.color) + fig.canvas.draw_idle() + + +class DotTool(ToolBase): + description = 'Draw a random dot' + + def __init__(self, *args, **kwargs): + self.color = kwargs.pop('color') + ToolBase.__init__(self, *args, **kwargs) + + def trigger(self, *args, **kwargs): + x0, y0 = uniform(0, 2), uniform(1, 4) + fig = self.figure + fig.gca().plot([x0], [y0], 'o', color=self.color, gid=self.color) + fig.canvas.draw_idle() + + fig = plt.figure() plt.plot([1, 2, 3], gid='mygroup') plt.plot([2, 3, 4], gid='unknown') plt.plot([3, 2, 1], gid='mygroup') # Add the custom tools that we created -fig.canvas.manager.toolmanager.add_tool('List', ListTools) -fig.canvas.manager.toolmanager.add_tool('Hide', GroupHideTool, gid='mygroup') +tool_mgr = fig.canvas.manager.toolmanager +tool_mgr.add_tool('List', ListTools) +tool_mgr.add_tool('Hide', GroupHideTool, gid='mygroup') # Add an existing tool to new group `foo`. @@ -83,10 +114,20 @@ def set_lines_visibility(self, state): fig.canvas.manager.toolbar.add_tool('zoom', 'foo') # Remove the forward button -fig.canvas.manager.toolmanager.remove_tool('forward') +tool_mgr.remove_tool('forward') # To add a custom tool to the toolbar at specific location inside # the navigation group fig.canvas.manager.toolbar.add_tool('Hide', 'navigation', 1) +for i, c in enumerate(['yellowgreen', 'forestgreen']): + sidebar = fig.canvas.manager._get_toolbar() + sidebar.set_flow('vertical') + tools = [['shapes', [tool_mgr.add_tool('L%s' % i, LineTool, color=c), + tool_mgr.add_tool('D%s' % i, DotTool, color=c)]], + ['hide', [tool_mgr.add_tool('H%s' % i, GroupHideTool, gid=c)]]] + + fig.canvas.manager.window.add_element(sidebar, False, 'west') + add_tools_to_container(sidebar, tools) + plt.show() diff --git a/lib/matplotlib/_mathtext_data.py b/lib/matplotlib/_mathtext_data.py index f846890f2706..7be81e07a0da 100644 --- a/lib/matplotlib/_mathtext_data.py +++ b/lib/matplotlib/_mathtext_data.py @@ -2281,7 +2281,7 @@ 'obar' : 9021, 'supseteq' : 8839, 'nu' : 957, - 'AA' : 8491, + 'AA' : 197, 'AE' : 198, 'models' : 8871, 'ominus' : 8854, diff --git a/lib/matplotlib/_pylab_helpers.py b/lib/matplotlib/_pylab_helpers.py index 840f6bd8e652..a112b67f0806 100644 --- a/lib/matplotlib/_pylab_helpers.py +++ b/lib/matplotlib/_pylab_helpers.py @@ -9,6 +9,8 @@ import gc import atexit +from matplotlib import is_interactive + def error_msg(msg): print(msg, file=sys.stderr) @@ -35,6 +37,16 @@ class Gcf(object): _activeQue = [] figs = {} + @classmethod + def add_figure_manager(cls, manager): + cls.figs[manager.num] = manager + try: # TODO remove once all backends converted to use the new manager. + manager.mpl_connect('window_destroy_event', cls.destroy_cbk) + except: + pass + + cls.set_active(manager) + @classmethod def get_fig_manager(cls, num): """ @@ -46,6 +58,50 @@ def get_fig_manager(cls, num): cls.set_active(manager) return manager + @classmethod + def show_all(cls, block=None): + """ + Show all figures. If *block* is not None, then + it is a boolean that overrides all other factors + determining whether show blocks by calling mainloop(). + The other factors are: + it does not block if run inside ipython's "%pylab" mode + it does not block in interactive mode. + """ + + managers = cls.get_all_fig_managers() + if not managers: + return + + for manager in managers: + manager.show() + + if block is not None: + if block: + manager._mainloop() + return + + from matplotlib import pyplot + try: + ipython_pylab = not pyplot.show._needmain + # IPython versions >= 0.10 tack the _needmain + # attribute onto pyplot.show, and always set + # it to False, when in %pylab mode. + ipython_pylab = ipython_pylab and get_backend() != 'WebAgg' + # TODO: The above is a hack to get the WebAgg backend + # working with ipython's `%pylab` mode until proper + # integration is implemented. + except AttributeError: + ipython_pylab = False + + # Leave the following as a separate step in case we + # want to control this behavior with an rcParam. + if ipython_pylab: + block = False + + if not is_interactive() or get_backend() == 'WebAgg': + manager._mainloop() + @classmethod def destroy(cls, num): """ @@ -137,7 +193,9 @@ def set_active(cls, manager): if m != manager: cls._activeQue.append(m) cls._activeQue.append(manager) - cls.figs[manager.num] = manager + @classmethod + def destroy_cbk(cls, event): + cls.destroy(event.figure_manager.num) atexit.register(Gcf.destroy_all) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index c219bde1042a..9389e7ea4220 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -1838,6 +1838,10 @@ def bar(self, left, height, width=0.8, bottom=None, **kwargs): linewidth; If 0, don't draw edges. default: None + tick_label : string or array-like, optional + the tick labels of the bars + default: None + xerr : scalar or array-like, optional if not None, will be used to generate errorbar(s) on the bar chart default: None @@ -1908,6 +1912,9 @@ def bar(self, left, height, width=0.8, bottom=None, **kwargs): edgecolor = kwargs.pop('edgecolor', None) linewidth = kwargs.pop('linewidth', None) + tick_label = kwargs.pop('tick_label', None) + label_ticks_flag = tick_label is not None + # Because xerr and yerr will be passed to errorbar, # most dimension checking and processing will be left # to the errorbar method. @@ -1938,6 +1945,7 @@ def make_iterable(x): _bottom = bottom bottom = make_iterable(bottom) linewidth = make_iterable(linewidth) + tick_label = make_iterable(tick_label) adjust_ylim = False adjust_xlim = False @@ -1956,6 +1964,9 @@ def make_iterable(x): width *= nbars if len(bottom) == 1: bottom *= nbars + + tick_label_axis = self.xaxis + tick_label_position = left elif orientation == 'horizontal': self._process_unit_info(xdata=width, ydata=bottom, kwargs=kwargs) if log: @@ -1971,11 +1982,16 @@ def make_iterable(x): left *= nbars if len(height) == 1: height *= nbars + + tick_label_axis = self.yaxis + tick_label_position = bottom else: raise ValueError('invalid orientation: %s' % orientation) if len(linewidth) < nbars: linewidth *= nbars + if len(tick_label) < nbars: + tick_label *= nbars if color is None: color = [None] * nbars @@ -2008,6 +2024,9 @@ def make_iterable(x): if len(bottom) != nbars: raise ValueError("incompatible sizes: argument 'bottom' " "must be length %d or scalar" % nbars) + if len(tick_label) != nbars: + raise ValueError("incompatible sizes: argument 'tick_label' " + "must be length %d or string" % nbars) patches = [] @@ -2103,6 +2122,10 @@ def make_iterable(x): bar_container = BarContainer(patches, errorbar, label=label) self.add_container(bar_container) + if label_ticks_flag: + tick_label_axis.set_ticks(tick_label_position) + tick_label_axis.set_ticklabels(tick_label) + return bar_container @docstring.dedent_interpd @@ -2148,6 +2171,9 @@ def barh(self, bottom, width, height=0.8, left=None, **kwargs): width of bar edge(s). If None, use default linewidth; If 0, don't draw edges. + tick_label : string or array-like, optional, default: None + the tick labels of the bars + xerr : scalar or array-like, optional, default: None if not None, will be used to generate errorbar(s) on the bar chart @@ -4683,6 +4709,12 @@ def imshow(self, X, cmap=None, norm=None, aspect=None, -------- matshow : Plot a matrix or an array as an image. + Notes + ----- + Unless *extent* is used, pixel centers will be located at integer + coordinates. In other words: the origin will coincide with the center + of pixel (0, 0). + Examples -------- diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 2513419ef8c4..b78d6e877e19 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -20,6 +20,12 @@ pressed, x and y locations in pixel and :class:`~matplotlib.axes.Axes` coordinates. +:class:`WindowBase` + The base class to display a window. + +:class:`MainLoopBase` + The base class to start the GUI's main loop. + :class:`ShowBase` The base class for the Show class of each interactive backend; the 'show' callable is then set to Show.__call__, inherited from @@ -43,6 +49,7 @@ import warnings import time import io +import weakref import numpy as np import matplotlib.cbook as cbook @@ -138,6 +145,33 @@ def get_registered_canvas_class(format): return backend_class +class MainLoopBase(object): + """This gets used as a key maintaining the event loop. + Backends should only need to override begin and end. + It should not matter if this gets used as a singleton or not due to + clever magic. + """ + _instance_count = {} + def __init__(self): + MainLoopBase._instance_count.setdefault(self.__class__, 0) + MainLoopBase._instance_count[self.__class__] += 1 + + def begin(self): + pass + + def end(self): + pass + + def __call__(self): + self.begin() + + def __del__(self): + MainLoopBase._instance_count[self.__class__] -= 1 + if (MainLoopBase._instance_count[self.__class__] <= 0 and + not is_interactive()): + self.end() + + class ShowBase(object): """ Simple base class to generate a show() callable in backends. @@ -1041,7 +1075,7 @@ def set_linewidth(self, w): """ Set the linewidth in points """ - self._linewidth = w + self._linewidth = float(w) def set_linestyle(self, style): """ @@ -1676,7 +1710,7 @@ class FigureCanvasBase(object): register_backend('tiff', 'matplotlib.backends.backend_agg', 'Tagged Image File Format') - def __init__(self, figure): + def __init__(self, figure, manager=None): figure.set_canvas(self) self.figure = figure # a dictionary from event name to a dictionary that maps cid->func @@ -1690,6 +1724,7 @@ def __init__(self, figure): self.mouse_grabber = None # the axes currently grabbing mouse self.toolbar = None # NavigationToolbar2 will set me self._is_saving = False + self.manager = manager def is_saving(self): """ @@ -2446,6 +2481,19 @@ def stop_event_loop_default(self): """ self._looping = False + def destroy(self): + pass + + @property + def manager(self): + if self._manager is not None: + return self._manager() + + @manager.setter + def manager(self, manager): + if manager is not None: + self._manager = weakref.ref(manager) + def key_press_handler(event, canvas, toolbar=None): """ @@ -2485,7 +2533,10 @@ def key_press_handler(event, canvas, toolbar=None): # quit the figure (defaut key 'ctrl+w') if event.key in quit_keys: - Gcf.destroy_fig(canvas.figure) + if isinstance(canvas.manager, FigureManagerBase): # Using old figman. + Gcf.destroy_fig(canvas.figure) + else: + canvas.manager._destroy('window_destroy_event') if toolbar is not None: # home or reset mnemonic (default key 'h', 'home' and 'r') @@ -2561,6 +2612,145 @@ class NonGuiException(Exception): pass +class WindowEvent(object): + def __init__(self, name, window): + self.name = name + self.window = window + + +class WindowBase(cbook.EventEmitter): + """The base class to show a window on screen. + + Parameters + ---------- + title : str + The title of the window. + """ + + def __init__(self, title): + cbook.EventEmitter.__init__(self) + + def show(self): + """ + For GUI backends, show the figure window and redraw. + For non-GUI backends, raise an exception to be caught + by :meth:`~matplotlib.figure.Figure.show`, for an + optional warning. + """ + raise NonGuiException() + + def destroy(self): + """Destroys the window""" + pass + + def set_fullscreen(self, fullscreen): + """Whether to show the window fullscreen or not, GUI only. + + Parameters + ---------- + fullscreen : bool + True for yes, False for no. + """ + pass + + def set_default_size(self, width, height): + """Sets the default size of the window, defaults to a simple resize. + + Parameters + ---------- + width : int + The default width (in pixels) of the window. + height : int + The default height (in pixels) of the window. + """ + self.resize(width, height) + + def resize(self, width, height): + """"For gui backends, resizes the window. + + Parameters + ---------- + width : int + The new width (in pixels) for the window. + height : int + The new height (in pixels) for the window. + """ + pass + + def get_window_title(self): + """ + Get the title text of the window containing the figure. + Return None for non-GUI backends (e.g., a PS backend). + + Returns + ------- + str : The window's title. + """ + return 'image' + + def set_window_title(self, title): + """ + Set the title text of the window containing the figure. Note that + this has no effect for non-GUI backends (e.g., a PS backend). + + Parameters + ---------- + title : str + The title of the window. + """ + pass + + def add_element(self, element, expand, place): + """ Adds a gui widget to the window. + This has no effect for non-GUI backends and properties only apply + to those backends that support them, or have a suitable workaround. + + Parameters + ---------- + element : A gui element. + The element to add to the window + expand : bool + Whether the element should auto expand to fill as much space within + the window as possible. + place : string + The location to place the element, either compass points north, + east, south, west, or center. + """ + pass + + def replace_element(self, oldelement, newelement): + """Replaces a gui widget for another + + Parameters + ---------- + oldelement: A gui element. + The element to be replaced + newelement: A gui element. + The replacement element + """ + pass + + def remove_element(self, element): + """Removes a widget + + Parameters + ---------- + element: A gui element + Element to be removed from the gui + """ + pass + + def destroy_event(self, *args): + """Fires this event when the window wants to destroy itself. + + Note this method should hook up to the backend's internal window's + close event. + """ + s = 'window_destroy_event' + event = WindowEvent(s, self) + self._callbacks.process(s, event) + + class FigureManagerBase(object): """ Helper class for pyplot mode, wraps everything up into a neat bundle @@ -2578,12 +2768,8 @@ def __init__(self, canvas, num): canvas.manager = self # store a pointer to parent self.num = num - if rcParams['toolbar'] != 'toolmanager': - self.key_press_handler_id = self.canvas.mpl_connect( - 'key_press_event', - self.key_press) - else: - self.key_press_handler_id = None + self.key_press_handler_id = self.canvas.mpl_connect('key_press_event', + self.key_press) """ The returned id from connecting the default key handler via :meth:`FigureCanvasBase.mpl_connnect`. diff --git a/lib/matplotlib/backend_managers.py b/lib/matplotlib/backend_managers.py index 41d80c96a9d1..af882b5d84cd 100644 --- a/lib/matplotlib/backend_managers.py +++ b/lib/matplotlib/backend_managers.py @@ -1,4 +1,8 @@ """ +`FigureManager` + Class that pulls all of the standard GUI elements together, and manages + the interaction between them. + `ToolManager` Class that makes the bridge between user interaction (key press, toolbar clicks, ..) and the actions in response to the user inputs. @@ -14,6 +18,212 @@ from matplotlib.rcsetup import validate_stringlist import matplotlib.backend_tools as tools +from matplotlib import is_interactive +from matplotlib import rcParams +from matplotlib.figure import Figure +from matplotlib.backend_bases import key_press_handler +from matplotlib.backends import get_backend + + +class FigureManagerEvent(object): + """Event for when something happens to this figure manager. + i.e. the figure it controls gets closed + + Attributes + ---------- + signal : str + The name of the signal. + + figure_manager : FigureManager + The figure manager that fired the event. + """ + def __init__(self, signal, figure_manager): + self.name = signal + self.figure_manager = figure_manager + + +class FigureManager(cbook.EventEmitter): + """ + The FigureManager creates and wraps the necessary components to display a + figure, namely the Window, FigureCanvas and Toolbar. It gets used whenever + you want the figure in a standalone window. + + Parameters + ---------- + figure : `matplotlib.figure.Figure` + The figure to manage. + + num : int + The figure number. + + Attributes + ---------- + + canvas : `matplotlib.backend_bases.FigureCanvasBase` + The GUI element on which we draw. + + toolbar : `matplotlib.backend_bases.NavigationToolbar2` + The toolbar used for interacting with the figure. + + window : `matplotlib.backend_bases.WindowBase` + The window that holds the canvas and toolbar. + + num : int + The figure number. + """ + def __init__(self, figure, num): + cbook.EventEmitter.__init__(self) + self.num = num + + self._h = 0 + self._w = 0 + self._figure = None + self.key_press_handler_id = None + self._backend = get_backend() + self._mainloop = self._backend.MainLoop() + self.toolmanager = self._get_toolmanager() + + self.window = self._backend.Window('Figure %d' % num) + self.window.mpl_connect('window_destroy_event', self._destroy) + + self.figure = figure + self.toolbar = self._get_toolbar() + + if self.toolmanager: + tools.add_tools_to_manager(self.toolmanager) + if self.toolbar: + tools.add_tools_to_container(self.toolbar) + self.statusbar = self._backend.Statusbar(self.toolmanager) + self._h += self.window.add_element(self.statusbar, False, + 'south') + + if self.toolbar is not None: + self._h += self.window.add_element(self.toolbar, False, 'south') + + self.window.set_default_size(self._w, self._h) + self._full_screen_flag = False + + if is_interactive(): + self.window.show() + + @property + def figure(self): + return self._figure + + @figure.setter + def figure(self, figure): + if not figure: + return + + if self.key_press_handler_id: + self.figure.canvas.mpl_disconnect(self.key_press_handler_id) + + if not figure.canvas: + self._backend.FigureCanvas(figure, manager=self) + + if rcParams['toolbar'] != 'toolmanager': + self.key_press_handler_id = figure.canvas.mpl_connect( + 'key_press_event', self.key_press) + + if not self._figure: + self._add_figure_to_window(figure) + else: + self.window.replace_element(self._figure.canvas, figure.canvas) + + self._figure = figure + self.toolmanager.figure = figure + + def notify_axes_change(fig): + 'this will be called whenever the current axes is changed' + if self.toolmanager is None and self.toolbar is not None: + self.toolbar.update() + figure.add_axobserver(notify_axes_change) + + def _add_figure_to_window(self, figure): + self._w += int(figure.bbox.width) + self._h += int(figure.bbox.height) + self.window.add_element(figure.canvas, True, 'center') + self.window.set_default_size(self._w, self._h) + figure.canvas.show() + + def key_press(self, event): + """ + Implement the default mpl key bindings defined at + :ref:`key-event-handling` + """ + key_press_handler(event, self.figure.canvas, self.toolbar) + + def _destroy(self, event=None): + # Callback from the when the window wants to destroy itself + s = 'window_destroy_event' + event = FigureManagerEvent(s, self) + self._callbacks.process(s, event) + + def destroy(self, *args): + """Called to destroy this FigureManager, gets called by Gcf through + event magic. + """ + if self.figure and self.figure.canvas: + self.figure.canvas.destroy() + if self.toolbar: + self.toolbar.destroy() + self.window.destroy() + + self._mainloop.__del__() + + def show(self): + """Shows the figure""" + self.window.show() + + def full_screen_toggle(self): + """Toggles whether we show fullscreen, alternatively call + `window.fullscreen()`""" + self._full_screen_flag = not self._full_screen_flag + self.window.set_fullscreen(self._full_screen_flag) + + def resize(self, w, h): + """"For gui backends, resize the window (in pixels).""" + self.window.resize(w, h) + + def get_window_title(self): + """ + Get the title text of the window containing the figure. + Return None for non-GUI backends (e.g., a PS backend). + """ + return self.window.get_window_title() + + def set_window_title(self, title): + """ + Set the title text of the window containing the figure. Note that + this has no effect for non-GUI backends (e.g., a PS backend). + """ + self.window.set_window_title(title) + + def _get_toolbar(self): + # must be inited after the window, drawingArea and figure + # attrs are set + if rcParams['toolbar'] == 'toolmanager': + toolbar = self._backend.Toolbar(self.toolmanager) + elif rcParams['toolbar'] == 'toolbar2': + toolbar = self._backend.Toolbar2(self.figure.canvas, self.window) + else: + toolbar = None + return toolbar + + def _get_toolmanager(self): + # must be initialised after toolbar has been setted + if rcParams['toolbar'] != 'toolbar2': + toolmanager = ToolManager(self) + else: + toolmanager = None + return toolmanager + + def show_popup(self, msg): + """ + Display message in a popup -- GUI only + """ + pass + class ToolEvent(object): """Event for tool manipulation (add/remove)""" @@ -56,21 +266,41 @@ class ToolManager(object): `LockDraw` object to know if the message is available to write """ - def __init__(self, canvas): - self.canvas = canvas - - self._key_press_handler_id = self.canvas.mpl_connect( - 'key_press_event', self._key_press) - + def __init__(self, manager): + self._figure = None self._tools = {} self._keys = {} self._toggled = {} self._callbacks = cbook.CallbackRegistry() + self._key_press_handler_id = None # to process keypress event self.keypresslock = widgets.LockDraw() self.messagelock = widgets.LockDraw() + self.manager = manager + self.figure = manager.figure + + @property + def figure(self): + return self._figure + + @figure.setter + def figure(self, figure): + if not figure: + return + + if self._key_press_handler_id: + self.figure.canvas.mpl_disconnect(self._key_press_handler_id) + + self._key_press_handler_id = figure.canvas.mpl_connect( + 'key_press_event', self._key_press) + + for tool in self._tools.values(): + tool.figure = figure + + self._figure = figure + def toolmanager_connect(self, s, func): """ Connect event with string *s* to *func*. diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index 1d952f2417da..82e95447e9ec 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -13,7 +13,6 @@ from matplotlib import rcParams -from matplotlib._pylab_helpers import Gcf import matplotlib.cbook as cbook from weakref import WeakKeyDictionary import numpy as np @@ -75,12 +74,36 @@ def __init__(self, toolmanager, name): self._name = name self._figure = None self.toolmanager = toolmanager - self.figure = toolmanager.canvas.figure + self.figure = toolmanager.figure @property def figure(self): return self._figure + @figure.setter + def figure(self, newfig): + """ + Set the figure + + Set the figure to be affected by this tool + + Parameters + ---------- + figure: `Figure` + """ + if not newfig: + return + oldfig = self._figure + self.pre_set_figure(oldfig, newfig) + self._figure = newfig + self.post_set_figure(oldfig, newfig) + + def pre_set_figure(self, oldfig, newfig): + pass + + def post_set_figure(self, oldfig, newfig): + pass + def trigger(self, sender, event, data=None): """ Called when this tool gets used @@ -100,20 +123,6 @@ def trigger(self, sender, event, data=None): pass - @figure.setter - def figure(self, figure): - """ - Set the figure - - Set the figure to be affected by this tool - - Parameters - ---------- - figure: `Figure` - """ - - self._figure = figure - @property def name(self): """Tool Id""" @@ -147,8 +156,16 @@ class ToolToggleBase(ToolBase): """Cursor to use when the tool is active""" def __init__(self, *args, **kwargs): - ToolBase.__init__(self, *args, **kwargs) self._toggled = False + ToolBase.__init__(self, *args, **kwargs) + + def pre_set_figure(self, oldfig, newfig): + if self._toggled: + self.disable(None) + + def post_set_figure(self, oldfig, newfig): + if self._toggled: + self.enable(None) def trigger(self, sender, event, data=None): """Calls `enable` or `disable` based on `toggled` value""" @@ -198,9 +215,8 @@ class SetCursorBase(ToolBase): set_cursor when a tool gets triggered """ def __init__(self, *args, **kwargs): + self._idDrag = None ToolBase.__init__(self, *args, **kwargs) - self._idDrag = self.figure.canvas.mpl_connect( - 'motion_notify_event', self._set_cursor_cbk) self._cursor = None self._default_cursor = cursors.POINTER self._last_cursor = self._default_cursor @@ -211,6 +227,14 @@ def __init__(self, *args, **kwargs): for tool in self.toolmanager.tools.values(): self._add_tool(tool) + def pre_set_figure(self, oldfig, newfig): + if self._idDrag: + oldfig.canvas.mpl_disconnect('motion_notify_event') + + def post_set_figure(self, oldfig, newfig): + self._idDrag = newfig.canvas.mpl_connect('motion_notify_event', + self._set_cursor_cbk) + def _tool_trigger_cbk(self, event): if event.tool.toggled: self._cursor = event.tool.cursor @@ -262,9 +286,16 @@ class ToolCursorPosition(ToolBase): This tool runs in the background reporting the position of the cursor """ def __init__(self, *args, **kwargs): + self._idDrag = None ToolBase.__init__(self, *args, **kwargs) - self._idDrag = self.figure.canvas.mpl_connect( - 'motion_notify_event', self.send_message) + + def pre_set_figure(self, oldfig, newfig): + if self._idDrag: + oldfig.canvas.mpl_disconnect('motion_notify_event') + + def post_set_figure(self, oldfig, newfig): + self._idDrag = newfig.canvas.mpl_connect('motion_notify_event', + self.send_message) def send_message(self, event): """Call `matplotlib.backend_managers.ToolManager.message_event`""" @@ -318,7 +349,12 @@ class ToolQuit(ToolBase): default_keymap = rcParams['keymap.quit'] def trigger(self, sender, event, data=None): - Gcf.destroy_fig(self.figure) + try: + manager = self.figure.canvas.manager + except: + pass + else: + manager._destroy('window_destroy_event') class ToolEnableAllNavigation(ToolBase): @@ -484,7 +520,6 @@ def update_view(self): def push_current(self): """push the current view limits and position onto the stack""" - lims = [] pos = [] for a in self.figure.get_axes(): @@ -602,6 +637,7 @@ def __init__(self, *args): def enable(self, event): """Connect press/release events and lock the canvas""" + self.toolmanager.get_tool(_views_positions).add_figure() self.figure.canvas.widgetlock(self) self._idPress = self.figure.canvas.mpl_connect( 'button_press_event', self._press) @@ -618,10 +654,6 @@ def disable(self, event): self.figure.canvas.mpl_disconnect(self._idRelease) self.figure.canvas.mpl_disconnect(self._idScroll) - def trigger(self, sender, event, data=None): - self.toolmanager.get_tool(_views_positions).add_figure() - ToolToggleBase.trigger(self, sender, event, data) - def scroll_zoom(self, event): # https://gist.github.com/tacaswell/3144287 if event.inaxes is None: @@ -926,7 +958,7 @@ def _mouse_move(self, event): # safer to use the recorded button at the _press than current # button: # multiple button can get pressed during motion... a.drag_pan(self._button_pressed, event.key, event.x, event.y) - self.toolmanager.canvas.draw_idle() + self.figure.canvas.draw_idle() default_tools = {'home': ToolHome, 'back': ToolBack, 'forward': ToolForward, diff --git a/lib/matplotlib/backends/__init__.py b/lib/matplotlib/backends/__init__.py index cf80dc0f9ff5..b1b6d3ee0f51 100644 --- a/lib/matplotlib/backends/__init__.py +++ b/lib/matplotlib/backends/__init__.py @@ -14,18 +14,18 @@ 'new_figure_manager', 'backend_version'] backend = matplotlib.get_backend() # validates, to match all_backends +if backend.startswith('module://'): + backend_name = backend[9:] +else: + backend_name = 'matplotlib.backends.backend_' + backend.lower() + +def get_backend(): + return __import__(backend_name, globals(), locals(), + [backend_name], 0) def pylab_setup(): 'return new_figure_manager, draw_if_interactive and show for pylab' # Import the requested backend into a generic module object - - if backend.startswith('module://'): - backend_name = backend[9:] - else: - backend_name = 'backend_'+backend - backend_name = backend_name.lower() # until we banish mixed case - backend_name = 'matplotlib.backends.%s'%backend_name.lower() - # the last argument is specifies whether to use absolute or relative # imports. 0 means only perform absolute imports. backend_mod = __import__(backend_name, @@ -37,15 +37,7 @@ def pylab_setup(): # image backends like pdf, agg or svg do not need to do anything # for "show" or "draw_if_interactive", so if they are not defined # by the backend, just do nothing - def do_nothing_show(*args, **kwargs): - frame = inspect.currentframe() - fname = frame.f_back.f_code.co_filename - if fname in ('', ''): - warnings.warn(""" -Your currently selected backend, '%s' does not support show(). -Please select a GUI backend in your matplotlibrc file ('%s') -or with matplotlib.use()""" % - (backend, matplotlib.matplotlib_fname())) + def do_nothing(*args, **kwargs): pass backend_version = getattr(backend_mod,'backend_version', 'unknown') show = getattr(backend_mod, 'show', do_nothing_show) @@ -60,3 +52,13 @@ def do_nothing(*args, **kwargs): pass matplotlib.verbose.report('backend %s version %s' % (backend,backend_version)) return backend_mod, new_figure_manager, draw_if_interactive, show + +def do_nothing_show(*args, **kwargs): + frame = inspect.currentframe() + fname = frame.f_back.f_code.co_filename + if fname in ('', ''): + warnings.warn(""" +Your currently selected backend, '%s' does not support show(). +Please select a GUI backend in your matplotlibrc file ('%s') +or with matplotlib.use()""" % + (backend, matplotlib.matplotlib_fname())) diff --git a/lib/matplotlib/backends/backend_cairo.py b/lib/matplotlib/backends/backend_cairo.py index ec6a93d849d1..59569821f23a 100644 --- a/lib/matplotlib/backends/backend_cairo.py +++ b/lib/matplotlib/backends/backend_cairo.py @@ -407,7 +407,7 @@ def set_joinstyle(self, js): def set_linewidth(self, w): - self._linewidth = w + self._linewidth = float(w) self.ctx.set_line_width (self.renderer.points_to_pixels(w)) diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 32d15fb4fbcc..c05dcde62db0 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -28,8 +28,9 @@ def fn_name(): return sys._getframe(1).f_code.co_name import matplotlib from matplotlib._pylab_helpers import Gcf -from matplotlib.backend_bases import RendererBase, GraphicsContextBase, \ - FigureManagerBase, FigureCanvasBase, NavigationToolbar2, cursors, TimerBase +from matplotlib.backend_bases import (RendererBase, GraphicsContextBase, + FigureManagerBase, FigureCanvasBase, NavigationToolbar2, cursors, + TimerBase, WindowBase, MainLoopBase) from matplotlib.backend_bases import (ShowBase, ToolContainerBase, StatusbarBase) from matplotlib.backend_managers import ToolManager @@ -70,6 +71,17 @@ def draw_if_interactive(): if figManager is not None: figManager.canvas.draw_idle() + +class MainLoopGTK3(MainLoopBase): + def begin(self): + if Gtk.main_level() == 0: + Gtk.main() + + def end(self): + if Gtk.main_level() >= 1: + Gtk.main_quit() + + class Show(ShowBase): def mainloop(self): if Gtk.main_level() == 0: @@ -184,9 +196,9 @@ class FigureCanvasGTK3 (Gtk.DrawingArea, FigureCanvasBase): Gdk.EventMask.POINTER_MOTION_HINT_MASK| Gdk.EventMask.SCROLL_MASK) - def __init__(self, figure): + def __init__(self, figure, manager=None): if _debug: print('FigureCanvasGTK3.%s' % fn_name()) - FigureCanvasBase.__init__(self, figure) + FigureCanvasBase.__init__(self, figure, manager) GObject.GObject.__init__(self) self._idle_draw_id = 0 @@ -381,6 +393,102 @@ def stop_event_loop(self): stop_event_loop.__doc__=FigureCanvasBase.stop_event_loop_default.__doc__ +_flow = [Gtk.Orientation.HORIZONTAL, Gtk.Orientation.VERTICAL] +flow_types = ['horizontal', 'vertical'] + + +class WindowGTK3(WindowBase, Gtk.Window): + def __init__(self, title): + WindowBase.__init__(self, title) + Gtk.Window.__init__(self) + self.set_window_title(title) + + try: + self.set_icon_from_file(window_icon) + except (SystemExit, KeyboardInterrupt): + # re-raise exit type Exceptions + raise + except: + # some versions of gtk throw a glib.GError but not + # all, so I am not sure how to catch it. I am unhappy + # doing a blanket catch here, but am not sure what a + # better way is - JDH + verbose.report('Could not load matplotlib icon: %s' % sys.exc_info()[1]) + + self._boxes = {} + self._layout = {} + self._setup_box('_outer', Gtk.Orientation.VERTICAL, False, None) + self._setup_box('north', Gtk.Orientation.VERTICAL, False, '_outer') + self._setup_box('_middle', Gtk.Orientation.HORIZONTAL, True, '_outer') + self._setup_box('south', Gtk.Orientation.VERTICAL, False, '_outer') + + self._setup_box('west', Gtk.Orientation.HORIZONTAL, False, '_middle') + self._setup_box('center', Gtk.Orientation.VERTICAL, True, '_middle') + self._setup_box('east', Gtk.Orientation.HORIZONTAL, False, '_middle') + + self.add(self._layout['_outer']) + + self.connect('destroy', self.destroy_event) + self.connect('delete_event', self.destroy_event) + + def _setup_box(self, name, orientation, grow, parent): + self._layout[name] = Gtk.Box(orientation=orientation) + if parent: + self._layout[parent].pack_start(self._layout[name], grow, grow, 0) + self._layout[name].show() + + def add_element(self, element, expand, place): + box = Gtk.Box() + box.pack_start(element, expand=True, fill=True, padding=0) + + flow = _flow[not _flow.index(self._layout[place].get_orientation())] + separator = Gtk.Separator(orientation=flow) + separator.show() + if place in ['north', 'west', 'center']: + self._layout[place].pack_start(box, expand, expand, 0) + self._layout[place].pack_start(separator, False, False, 0) + elif place in ['south', 'east']: + self._layout[place].pack_end(box, expand, expand, 0) + self._layout[place].pack_end(separator, False, False, 0) + else: + raise KeyError('Unknown value for place, %s' % place) + size_request = box.size_request() + element.show() + box.show() + self._boxes[element] = box + return size_request.height + separator.size_request().height + + def replace_element(self, original, new): + box = self._boxes.pop(original) + box.remove(original) + box.pack_start(new, expand=True, fill=True, padding=0) + new.show() + box.show() + self._boxes[new] = box + + def set_default_size(self, width, height): + Gtk.Window.set_default_size(self, width, height) + + def show(self): + # show the figure window + Gtk.Window.show(self) + + def destroy(self): + Gtk.Window.destroy(self) + + def set_fullscreen(self, fullscreen): + if fullscreen: + self.fullscreen() + else: + self.unfullscreen() + + def get_window_title(self): + return self.get_title() + + def set_window_title(self, title): + self.set_title(title) + + class FigureManagerGTK3(FigureManagerBase): """ Public attributes @@ -421,9 +529,7 @@ def __init__(self, canvas, num): w = int (self.canvas.figure.bbox.width) h = int (self.canvas.figure.bbox.height) - self.toolmanager = self._get_toolmanager() self.toolbar = self._get_toolbar() - self.statusbar = None def add_widget(child, expand, fill, padding): child.show() @@ -431,14 +537,6 @@ def add_widget(child, expand, fill, padding): size_request = child.size_request() return size_request.height - if self.toolmanager: - backend_tools.add_tools_to_manager(self.toolmanager) - if self.toolbar: - backend_tools.add_tools_to_container(self.toolbar) - self.statusbar = StatusbarGTK3(self.toolmanager) - h += add_widget(self.statusbar, False, False, 0) - h += add_widget(Gtk.HSeparator(), False, False, 0) - if self.toolbar is not None: self.toolbar.show() h += add_widget(self.toolbar, False, False, 0) @@ -454,9 +552,7 @@ def destroy(*args): def notify_axes_change(fig): 'this will be called whenever the current axes is changed' - if self.toolmanager is not None: - pass - elif self.toolbar is not None: + if self.toolbar is not None: self.toolbar.update() self.canvas.figure.add_axobserver(notify_axes_change) @@ -493,20 +589,10 @@ def _get_toolbar(self): # attrs are set if rcParams['toolbar'] == 'toolbar2': toolbar = NavigationToolbar2GTK3 (self.canvas, self.window) - elif rcParams['toolbar'] == 'toolmanager': - toolbar = ToolbarGTK3(self.toolmanager) else: toolbar = None return toolbar - def _get_toolmanager(self): - # must be initialised after toolbar has been setted - if rcParams['toolbar'] != 'toolbar2': - toolmanager = ToolManager(self.canvas) - else: - toolmanager = None - return toolmanager - def get_window_title(self): return self.window.get_title() @@ -761,13 +847,12 @@ def draw_rubberband(self, x0, y0, x1, y1): class ToolbarGTK3(ToolContainerBase, Gtk.Box): - def __init__(self, toolmanager): + def __init__(self, toolmanager, flow='horizontal'): ToolContainerBase.__init__(self, toolmanager) Gtk.Box.__init__(self) - self.set_property("orientation", Gtk.Orientation.VERTICAL) - self._toolarea = Gtk.Box() - self._toolarea.set_property('orientation', Gtk.Orientation.HORIZONTAL) + self.set_flow(flow) + self.pack_start(self._toolarea, False, False, 0) self._toolarea.show_all() self._groups = {} @@ -800,7 +885,7 @@ def _add_button(self, button, group, position): if group not in self._groups: if self._groups: self._add_separator() - toolbar = Gtk.Toolbar() + toolbar = Gtk.Toolbar(orientation=_flow[self._flow]) toolbar.set_style(Gtk.ToolbarStyle.ICONS) self._toolarea.pack_start(toolbar, False, False, 0) toolbar.show_all() @@ -829,9 +914,22 @@ def remove_toolitem(self, name): self._groups[group].remove(toolitem) del self._toolitems[name] + def set_flow(self, flow): + try: + self._flow = flow_types.index(flow) + except ValueError: + raise ValueError('Flow (%s), not in list %s' % (flow, flow_types)) + self.set_property("orientation", _flow[not self._flow]) + self._toolarea.set_property('orientation', _flow[self._flow]) + for item in self._toolarea: + if isinstance(item, Gtk.Separator): + item.set_property("orientation", _flow[not self._flow]) + else: + item.set_property("orientation", _flow[self._flow]) + def _add_separator(self): sep = Gtk.Separator() - sep.set_property("orientation", Gtk.Orientation.VERTICAL) + sep.set_property("orientation", _flow[not self._flow]) self._toolarea.pack_start(sep, False, True, 0) sep.show_all() diff --git a/lib/matplotlib/backends/backend_gtk3agg.py b/lib/matplotlib/backends/backend_gtk3agg.py index c3eb1da68be3..4627436c33d0 100644 --- a/lib/matplotlib/backends/backend_gtk3agg.py +++ b/lib/matplotlib/backends/backend_gtk3agg.py @@ -21,8 +21,8 @@ class FigureCanvasGTK3Agg(backend_gtk3.FigureCanvasGTK3, backend_agg.FigureCanvasAgg): - def __init__(self, figure): - backend_gtk3.FigureCanvasGTK3.__init__(self, figure) + def __init__(self, figure, manager=None): + backend_gtk3.FigureCanvasGTK3.__init__(self, figure, manager) self._bbox_queue = [] def _renderer_init(self): @@ -121,4 +121,9 @@ def new_figure_manager_given_figure(num, figure): FigureCanvas = FigureCanvasGTK3Agg FigureManager = FigureManagerGTK3Agg +Window = backend_gtk3.WindowGTK3 +Toolbar = backend_gtk3.ToolbarGTK3 +Statusbar = backend_gtk3.StatusbarGTK3 +Toolbar2 = backend_gtk3.NavigationToolbar2GTK3 +MainLoop = backend_gtk3.MainLoopGTK3 show = backend_gtk3.show diff --git a/lib/matplotlib/backends/backend_gtk3cairo.py b/lib/matplotlib/backends/backend_gtk3cairo.py index da8f099be7f6..65934824e554 100644 --- a/lib/matplotlib/backends/backend_gtk3cairo.py +++ b/lib/matplotlib/backends/backend_gtk3cairo.py @@ -22,8 +22,8 @@ def set_context(self, ctx): class FigureCanvasGTK3Cairo(backend_gtk3.FigureCanvasGTK3, backend_cairo.FigureCanvasCairo): - def __init__(self, figure): - backend_gtk3.FigureCanvasGTK3.__init__(self, figure) + def __init__(self, figure, manager=None): + backend_gtk3.FigureCanvasGTK3.__init__(self, figure, manager) def _renderer_init(self): """use cairo renderer""" @@ -72,4 +72,9 @@ def new_figure_manager_given_figure(num, figure): FigureCanvas = FigureCanvasGTK3Cairo FigureManager = FigureManagerGTK3Cairo +Window = backend_gtk3.WindowGTK3 +Toolbar = backend_gtk3.ToolbarGTK3 +Statusbar = backend_gtk3.StatusbarGTK3 +Toolbar2 = backend_gtk3.NavigationToolbar2GTK3 +MainLoop = backend_gtk3.MainLoopGTK3 show = backend_gtk3.show diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index c45c1fd61b55..cea707d37b25 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -259,6 +259,7 @@ def set_color(self, r, g, b, store=1): if store: self.color = (r,g,b) def set_linewidth(self, linewidth, store=1): + linewidth = float(linewidth) if linewidth != self.linewidth: self._pswriter.write("%1.3f setlinewidth\n"%linewidth) if store: self.linewidth = linewidth @@ -451,10 +452,10 @@ def option_scale_image(self): ps backend support arbitrary scaling of image. """ return True - + def option_image_nocomposite(self): """ - return whether to generate a composite image from multiple images on + return whether to generate a composite image from multiple images on a set of axes """ return not rcParams['image.composite_image'] diff --git a/lib/matplotlib/backends/backend_qt5.py b/lib/matplotlib/backends/backend_qt5.py index 8e72a087c36d..e3e0624552fc 100644 --- a/lib/matplotlib/backends/backend_qt5.py +++ b/lib/matplotlib/backends/backend_qt5.py @@ -234,6 +234,8 @@ def __init__(self, figure): # NB: Using super for this call to avoid a TypeError: # __init__() takes exactly 2 arguments (1 given) on QWidget # PyQt5 + # The need for this change is documented here + # http://pyqt.sourceforge.net/Docs/PyQt5/pyqt4_differences.html#cooperative-multi-inheritance super(FigureCanvasQT, self).__init__(figure=figure) self.figure = figure self.setMouseTracking(True) diff --git a/lib/matplotlib/backends/backend_wx.py b/lib/matplotlib/backends/backend_wx.py index 652b47bcae16..81f41d7c3f33 100644 --- a/lib/matplotlib/backends/backend_wx.py +++ b/lib/matplotlib/backends/backend_wx.py @@ -558,12 +558,15 @@ def set_linewidth(self, w): """ Set the line width. """ + w = float(w) DEBUG_MSG("set_linewidth()", 1, self) self.select() - if w>0 and w<1: w = 1 + if w > 0 and w < 1: + w = 1 GraphicsContextBase.set_linewidth(self, w) lw = int(self.renderer.points_to_pixels(self._linewidth)) - if lw==0: lw = 1 + if lw == 0: + lw = 1 self._pen.SetWidth(lw) self.gfx_ctx.SetPen(self._pen) self.unselect() @@ -789,12 +792,12 @@ def draw_idle(self): """ DEBUG_MSG("draw_idle()", 1, self) self._isDrawn = False # Force redraw - + # Triggering a paint event is all that is needed to defer drawing # until later. The platform will send the event when it thinks it is # a good time (usually as soon as there are no other events pending). self.Refresh(eraseBackground=False) - + def draw(self, drawDC=None): """ Render the figure using RendererWx instance renderer, or using a @@ -1739,7 +1742,7 @@ def draw_rubberband(self, event, x0, y0, x1, y1): color.Set(r,g,b, 0x60) dc.SetBrush(wx.Brush(color)) dc.DrawRectangleRect(rect) - + def set_status_bar(self, statbar): self.statbar = statbar diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index 814df14d9ab4..00b1b4c241ab 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -566,6 +566,17 @@ def process(self, s, *args, **kwargs): self._remove_proxy(proxy) +class EventEmitter(object): + def __init__(self): + self._callbacks = CallbackRegistry() + + def mpl_connect(self, s, func): + return self._callbacks.connect(s, func) + + def mpl_disconnect(self, cid): + return self._callbacks.disconnect(cid) + + class Scheduler(threading.Thread): """ Base class for timeout and idle scheduling diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index f14c8c8c64a6..a876fc339b74 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -576,6 +576,7 @@ def figimage(self, X, vmin=None, vmax=None, origin=None, + resize=False, **kwargs): """ Adds a non-resampled image to the figure. @@ -603,6 +604,8 @@ def figimage(self, X, ========= ========================================================= Keyword Description ========= ========================================================= + resize a boolean, True or False. If "True", then re-size the + Figure to match the given image size. xo or yo An integer, the *x* and *y* image offset in pixels cmap a :class:`matplotlib.colors.Colormap` instance, e.g., cm.jet. If *None*, default to the rc ``image.cmap`` @@ -637,6 +640,11 @@ def figimage(self, X, if not self._hold: self.clf() + if resize: + dpi = self.get_dpi() + figsize = [x / float(dpi) for x in (X.shape[1], X.shape[0])] + self.set_size_inches(figsize, forward=True) + im = FigureImage(self, cmap, norm, xo, yo, origin, **kwargs) im.set_array(X) im.set_alpha(alpha) @@ -1366,11 +1374,17 @@ def __setstate__(self, state): if restore_to_pylab: # lazy import to avoid circularity + # TODO clean on removal of Gcf from backends import matplotlib.pyplot as plt import matplotlib._pylab_helpers as pylab_helpers + import matplotlib.backend_managers as managers allnums = plt.get_fignums() num = max(allnums) + 1 if allnums else 1 - mgr = plt._backend_mod.new_figure_manager_given_figure(num, self) + if hasattr(plt._backend_mod, 'Window'): # Can we use MEP 27 code? + mgr = managers.FigureManager(self, num) + else: + mgr = plt._backend_mod.new_figure_manager_given_figure(num, + self) # XXX The following is a copy and paste from pyplot. Consider # factoring to pylab_helpers @@ -1385,7 +1399,7 @@ def make_active(event): mgr._cidgcf = mgr.canvas.mpl_connect('button_press_event', make_active) - pylab_helpers.Gcf.set_active(mgr) + pylab_helpers.Gcf.add_figure_manager(mgr) self.number = num plt.draw_if_interactive() @@ -1424,9 +1438,10 @@ def savefig(self, *args, **kwargs): Keyword arguments: - *dpi*: [ *None* | ``scalar > 0`` ] + *dpi*: [ *None* | ``scalar > 0`` | 'figure'] The resolution in dots per inch. If *None* it will default to - the value ``savefig.dpi`` in the matplotlibrc file. + the value ``savefig.dpi`` in the matplotlibrc file. If 'figure' + it will set the dpi to be the value of the figure. *facecolor*, *edgecolor*: the colors of the figure rectangle @@ -1473,6 +1488,8 @@ def savefig(self, *args, **kwargs): """ kwargs.setdefault('dpi', rcParams['savefig.dpi']) + if kwargs['dpi'] == 'figure': + kwargs['dpi'] = self.get_dpi() frameon = kwargs.pop('frameon', rcParams['savefig.frameon']) transparent = kwargs.pop('transparent', rcParams['savefig.transparent']) diff --git a/lib/matplotlib/image.py b/lib/matplotlib/image.py index a0f12fb946a3..ac968223d06d 100644 --- a/lib/matplotlib/image.py +++ b/lib/matplotlib/image.py @@ -1304,10 +1304,10 @@ def imsave(fname, arr, vmin=None, vmax=None, cmap=None, format=None, from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas from matplotlib.figure import Figure - figsize = [x / float(dpi) for x in (arr.shape[1], arr.shape[0])] - fig = Figure(figsize=figsize, dpi=dpi, frameon=False) + fig = Figure(dpi=dpi, frameon=False) canvas = FigureCanvas(fig) - im = fig.figimage(arr, cmap=cmap, vmin=vmin, vmax=vmax, origin=origin) + im = fig.figimage(arr, cmap=cmap, vmin=vmin, vmax=vmax, origin=origin, + resize=True) fig.savefig(fname, dpi=dpi, format=format, transparent=True) diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index 3c0c39153118..f45008686f32 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -919,7 +919,7 @@ def set_linewidth(self, w): ACCEPTS: float value in points """ - self._linewidth = w + self._linewidth = float(w) def set_linestyle(self, linestyle): """ diff --git a/lib/matplotlib/mathtext.py b/lib/matplotlib/mathtext.py index 049ed240a97d..9dafaaa1785d 100644 --- a/lib/matplotlib/mathtext.py +++ b/lib/matplotlib/mathtext.py @@ -2475,7 +2475,7 @@ def unknown_symbol(self, s, loc, toks): # The first 2 entires in the tuple are (font, char, sizescale) for # the two symbols under and over. The third element is the space # (in multiples of underline height) - r'AA' : ( ('rm', 'A', 1.0), (None, '\circ', 0.5), 0.0), + r'AA' : ( ('it', 'A', 1.0), (None, '\circ', 0.5), 0.0), } def c_over_c(self, s, loc, toks): @@ -2532,7 +2532,8 @@ def c_over_c(self, s, loc, toks): r'.' : r'\combiningdotabove', r'^' : r'\circumflexaccent', r'overrightarrow' : r'\rightarrow', - r'overleftarrow' : r'\leftarrow' + r'overleftarrow' : r'\leftarrow', + r'mathring' : r'\circ' } _wide_accents = set(r"widehat widetilde widebar".split()) @@ -2546,11 +2547,14 @@ def accent(self, s, loc, toks): raise ParseFatalException("Error parsing accent") accent, sym = toks[0] if accent in self._wide_accents: - accent = AutoWidthChar( + accent_box = AutoWidthChar( '\\' + accent, sym.width, state, char_class=Accent) else: - accent = Accent(self._accent_map[accent], state) - centered = HCentered([accent]) + accent_box = Accent(self._accent_map[accent], state) + if accent == 'mathring': + accent_box.shrink() + accent_box.shrink() + centered = HCentered([Hbox(sym.width / 4.0), accent_box]) centered.hpack(sym.width, 'exactly') return Vlist([ centered, diff --git a/lib/matplotlib/mlab.py b/lib/matplotlib/mlab.py index 9f0f7b0a9de8..aa5290540fa1 100644 --- a/lib/matplotlib/mlab.py +++ b/lib/matplotlib/mlab.py @@ -772,7 +772,15 @@ def _spectral_helper(x, y=None, NFFT=None, Fs=None, detrend_func=None, # Also include scaling factors for one-sided densities and dividing by # the sampling frequency, if desired. Scale everything, except the DC # component and the NFFT/2 component: - result[1:-1] *= scaling_factor + + # if we have a even number of frequencies, don't scale NFFT/2 + if not NFFT % 2: + slc = slice(1, -1, None) + # if we have an odd number, just don't scale DC + else: + slc = slice(1, None, None) + + result[slc] *= scaling_factor # MATLAB divides by the sampling frequency so that density function # has units of dB/Hz and can be integrated by the plotted frequency diff --git a/lib/matplotlib/patches.py b/lib/matplotlib/patches.py index da9f1c964ea1..0ed16cccca0f 100644 --- a/lib/matplotlib/patches.py +++ b/lib/matplotlib/patches.py @@ -334,7 +334,7 @@ def set_linewidth(self, w): """ if w is None: w = mpl.rcParams['patch.linewidth'] - self._linewidth = w + self._linewidth = float(w) def set_lw(self, lw): """alias for set_linewidth""" diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 1eef95867743..fa5f7c597e43 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -31,6 +31,7 @@ from matplotlib.cbook import _string_to_bool from matplotlib import docstring from matplotlib.backend_bases import FigureCanvasBase +import matplotlib.backend_managers as backend_managers from matplotlib.figure import Figure, figaspect from matplotlib.gridspec import GridSpec from matplotlib.image import imread as _imread @@ -151,7 +152,10 @@ def show(*args, **kw): described above. """ global _show - return _show(*args, **kw) + if hasattr(_backend_mod, 'Window'): # Can we use the new code? + return _pylab_helpers.Gcf.show_all(*args, **kw) + else: + _show(*args, **kw) def isinteractive(): @@ -425,13 +429,18 @@ def figure(num=None, # autoincrement if None, else integer from 1-N if get_backend().lower() == 'ps': dpi = 72 - figManager = new_figure_manager(num, figsize=figsize, - dpi=dpi, - facecolor=facecolor, - edgecolor=edgecolor, - frameon=frameon, - FigureClass=FigureClass, - **kwargs) + if hasattr(_backend_mod, 'Window'): # Can we use the MEP 27 code? + fig = FigureClass(figsize=figsize, dpi=dpi, facecolor=facecolor, + edgecolor=edgecolor, frameon=frameon, **kwargs) + figManager = backend_managers.FigureManager(fig, num) + else: + figManager = new_figure_manager(num, figsize=figsize, + dpi=dpi, + facecolor=facecolor, + edgecolor=edgecolor, + frameon=frameon, + FigureClass=FigureClass, + **kwargs) if figLabel: figManager.set_window_title(figLabel) @@ -444,7 +453,7 @@ def make_active(event): cid = figManager.canvas.mpl_connect('button_press_event', make_active) figManager._cidgcf = cid - _pylab_helpers.Gcf.set_active(figManager) + _pylab_helpers.Gcf.add_figure_manager(figManager) figManager.canvas.figure.number = num draw_if_interactive() diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index f7263ca7e383..b7ef4d3896ba 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -117,6 +117,15 @@ def validate_float_or_None(s): except ValueError: raise ValueError('Could not convert "%s" to float' % s) +def validate_dpi(s): + """confirm s is string 'figure' or convert s to float or raise""" + if s == 'figure': + return s + try: + return float(s) + except ValueError: + raise ValueError('"%s" is not string "figure" or' + ' could not convert "%s" to float' % (s, s)) def validate_int(s): """convert s to int or raise""" @@ -749,7 +758,7 @@ def __call__(self, s): closedmax=False)], ## Saving figure's properties - 'savefig.dpi': [100, validate_float], # DPI + 'savefig.dpi': [100, validate_dpi], # DPI 'savefig.facecolor': ['w', validate_color], # facecolor; white 'savefig.edgecolor': ['w', validate_color], # edgecolor; white 'savefig.frameon': [True, validate_bool], diff --git a/lib/matplotlib/tests/baseline_images/test_axes/bar_tick_label_multiple.png b/lib/matplotlib/tests/baseline_images/test_axes/bar_tick_label_multiple.png new file mode 100644 index 000000000000..a0ce8625befc Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_axes/bar_tick_label_multiple.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/bar_tick_label_single.png b/lib/matplotlib/tests/baseline_images/test_axes/bar_tick_label_single.png new file mode 100644 index 000000000000..e4742e58af9b Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_axes/bar_tick_label_single.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/barh_tick_label.png b/lib/matplotlib/tests/baseline_images/test_axes/barh_tick_label.png new file mode 100644 index 000000000000..e83d42a4290d Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_axes/barh_tick_label.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_69.pdf b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_69.pdf new file mode 100644 index 000000000000..09f7a773762e Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_69.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_69.png b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_69.png new file mode 100644 index 000000000000..9f2de65e96f2 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_69.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_69.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_69.svg new file mode 100644 index 000000000000..3221b4c2a8da --- /dev/null +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_69.svg @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_69.pdf b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_69.pdf new file mode 100644 index 000000000000..be7f1724d154 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_69.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_69.png b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_69.png new file mode 100644 index 000000000000..5d4b56171013 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_69.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_69.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_69.svg new file mode 100644 index 000000000000..a99fa3d1aee2 --- /dev/null +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_69.svg @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_69.pdf b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_69.pdf new file mode 100644 index 000000000000..870a8b1c6d3f Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_69.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_69.png b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_69.png new file mode 100644 index 000000000000..a909c167362c Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_69.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_69.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_69.svg new file mode 100644 index 000000000000..440d8ab4406b --- /dev/null +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_69.svg @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index fe6c5c4567a9..de71bd409275 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -131,7 +131,6 @@ def test_twinx_cla(): @image_comparison(baseline_images=["minorticks_on_rcParams_both"], extensions=['png']) def test_minorticks_on_rcParams_both(): - fig = plt.figure() matplotlib.rcParams['xtick.minor.visible'] = True matplotlib.rcParams['ytick.minor.visible'] = True @@ -1011,6 +1010,32 @@ def test_marker_edges(): ax.plot(x+0.2, np.sin(x), 'y.', ms=30.0, mew=2, mec='b') +@image_comparison(baseline_images=['bar_tick_label_single'], + extensions=['png']) +def test_bar_tick_label_single(): + # From 2516: plot bar with array of string labels for x axis + ax = plt.gca() + ax.bar(0, 1 , tick_label='a') + + +@image_comparison(baseline_images=['bar_tick_label_multiple'], + extensions=['png']) +def test_bar_tick_label_multiple(): + # From 2516: plot bar with array of string labels for x axis + ax = plt.gca() + ax.bar([1, 2.5], [1, 2], width=[0.2, 0.5], tick_label=['a', 'b'], + align='center') + + +@image_comparison(baseline_images=['barh_tick_label'], + extensions=['png']) +def test_barh_tick_label(): + # From 2516: plot barh with array of string labels for y axis + ax = plt.gca() + ax.barh([1, 2.5], [1, 2], height=[0.2, 0.5], tick_label=['a', 'b'], + align='center') + + @image_comparison(baseline_images=['hist_log'], remove_text=True) def test_hist_log(): diff --git a/lib/matplotlib/tests/test_mathtext.py b/lib/matplotlib/tests/test_mathtext.py index 6fb740e76256..47f04beef531 100644 --- a/lib/matplotlib/tests/test_mathtext.py +++ b/lib/matplotlib/tests/test_mathtext.py @@ -93,7 +93,8 @@ r"$? ! &$", # github issue #466 r'$\operatorname{cos} x$', # github issue #553 r'$\sum _{\genfrac{}{}{0}{}{0\leq i\leq m}{0