Source code for fsleyes.plotting.plotcanvas

#
# plotcanvas.py - The PlotCanvas class.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""This module provides the :class:`PlotCanvas` class, which plots
:class:`.DataSeries` instances on a ``matplotlib`` canvas. The ``PlotCanvas``
is used by the :class:`.TimeSeriesPanel`, :class:`.HistogramPanel`, and
:class:`.PowerSpectrumPanel` views, and potentially by other FSLeyes control
panels.
"""


import logging
import collections

import numpy                             as np
import scipy.interpolate                 as interp
import matplotlib.pyplot                 as plt
import matplotlib.backends.backend_wxagg as wxagg

import fsl.utils.idle                    as idle
import fsleyes_props                     as props
import fsleyes_widgets                   as fwidgets

import fsleyes.strings                   as strings


log = logging.getLogger(__name__)


[docs]class PlotCanvas(props.HasProperties): """The ``PlotCanvas`` can be used to plot :class:`.DataSeries` instances onto a ``matplotlib`` ``FigureCanvasWxAgg`` canvas. The ``PlotCanvas`` is used by all *FSLeyes views* which display some sort of 2D data plot, such as the :class:`.TimeSeriesPanel`, and the :class:`.HistogramPanel`. The ``PlotCanvas`` uses :mod:`matplotlib` for its plotting. The ``matplotlib`` ``Figure``, ``Axis``, and ``Canvas`` instances can be accessed via the :meth:`figure`, :meth:`axis`, and :meth:`canvas` methods, if they are needed. Various display settings can be configured through ``PlotCanvas`` properties, including :attr:`legend`, :attr:`smooth`, etc. **Basic usage** After you have created a ``PlotCanvas``, you can add :class:`.DataSeries` instances to the :attr:`dataSeries` property, then call :meth:`draw` or :meth:`asyncDraw`. The :meth:`draw` method simply calls :meth:`drawDataSeries` and :meth:`drawArtists`, so you can alternately call those methods directly, or pass your own ``drawFunc`` when creating a ``PlotCanvas``. The ``PlotCanvas`` itself is not a ``wx`` object, so cannot be displayed - the ``matplotilb Canvas`` object, accessible through the :meth:`canvas` method, is what you should add to a ``wx`` parent object. **Data series** A ``PlotCanvas`` instance plots data contained in one or more :class:`.DataSeries` instances; all ``DataSeries`` classes are defined in the :mod:`.plotting` sub-package. ``DataSeries`` objects can be plotted by passing them to the :meth:`drawDataSeries` method. Or, if you want one or more ``DataSeries`` to be *held*, i.e. plotted every time, you can add them to the :attr:`dataSeries` list. The ``DataSeries`` in the :attr:`dataSeries` list will be plotted on every call to :meth:`drawDataSeries` (in addition to any ``DataSeries`` passed directly to :meth:`drawDataSeries`) until they are removed from the :attr:`dataSeries` list. **The draw queue** The ``PlotCanvas`` uses a :class:`.async.TaskThread` to asynchronously extract and prepare data for plotting, This is because data preparation may take a long time for certain types of ``DataSeries`` (e.g. :class:`TimeSeries` which are retrieving data from large :class:`.Image` overlays), and the main application thread should not be blocked while this is occurring. The ``TaskThread`` instance is accessible through the :meth:`getDrawQueue` method, in case anything needs to be scheduled on it. """ dataSeries = props.List() """This list contains :class:`.DataSeries` instances which are plotted on every call to :meth:`drawDataSeries`. ``DataSeries`` instances can be added/removed directly to/from this list. """ artists = props.List() """This list contains any ``matplotlib.Artist`` instances which are plotted every call to :meth:`drawArtists`. """ legend = props.Boolean(default=True) """If ``True``, a legend is added to the plot, with an entry for every ``DataSeries`` instance in the :attr:`dataSeries` list. """ xAutoScale = props.Boolean(default=True) """If ``True``, the plot :attr:`limits` for the X axis are automatically updated to fit all plotted data. """ yAutoScale = props.Boolean(default=True) """If ``True``, the plot :attr:`limits` for the Y axis are automatically updated to fit all plotted data. """ xLogScale = props.Boolean(default=False) """Toggle a :math:`log_{10}` x axis scale. """ yLogScale = props.Boolean(default=False) """Toggle a :math:`log_{10}` y axis scale. """ invertX = props.Boolean(default=False) """Invert the plot along the X axis. """ invertY = props.Boolean(default=False) """Invert the plot along the Y axis. """ xScale = props.Real(default=1) """Scale to apply to the X axis data. """ yScale = props.Real(default=1) """Scale to apply to the Y axis data. """ xOffset = props.Real(default=0) """Offset to apply to the X axis data. """ yOffset = props.Real(default=0) """Offset to apply to the Y axis data. """ ticks = props.Boolean(default=True) """Toggle axis ticks and tick labels on/off.""" grid = props.Boolean(default=True) """Toggle an axis grid on/off.""" gridColour = props.Colour(default=(1, 1, 1)) """Grid colour (if :attr:`grid` is ``True``).""" bgColour = props.Colour(default=(0.8, 0.8, 0.8)) """Plot background colour.""" smooth = props.Boolean(default=False) """If ``True`` all plotted data is up-sampled, and smoothed using spline interpolation. """ xlabel = props.String() """A label to show on the x axis. """ ylabel = props.String() """A label to show on the y axis. """ limits = props.Bounds(ndims=2) """The x/y axis limits. If :attr:`xAutoScale` and :attr:`yAutoScale` are ``True``, these limit values are automatically updated on every call to :meth:`drawDataSeries`. """ showPreparingMessage = props.Boolean(default=True) """Show a message on the canvas whilst data is being prepared for plotting. """
[docs] def __init__(self, parent, drawFunc=None, prepareFunc=None): """Create a ``PlotCanvas``. :arg parent: The :mod:`wx` parent object. :arg drawFunc: Custon function to call instead of :meth:`draw`. :arg prepareFunc: Custom function to call instead of :meth:`prepareDataSeries`. """ figure = plt.Figure() axis = figure.add_subplot(111) canvas = wxagg.FigureCanvasWxAgg(parent, -1, figure) figure.subplots_adjust(top=1.0, bottom=0.0, left=0.0, right=1.0) figure.patch.set_visible(False) self.__name = '{}_{}'.format(type(self).__name__, id(self)) self.__figure = figure self.__axis = axis self.__canvas = canvas self.__prepareFunc = prepareFunc self.__drawFunc = drawFunc self.__destroyed = False # Accessing data from large compressed # files may take time, so we maintain # a queue of plotting requests. The # functions executed on this task # thread are used to prepare data for # plotting - the plotting occurs on # the main WX event loop. # # The drawDataSeries method sets up the # asynchronous data preparation, and the # __drawDataSeries method does the actual # plotting. self.__drawQueue = idle.TaskThread() self.__drawQueue.daemon = True self.__drawQueue.start() # Whenever a new request comes in to # draw the plot, we can't cancel any # pending requests, as they are running # on separate threads and out of our # control (and could be blocking on I/O). # # Instead, we keep track of the total # number of pending requests. The # __drawDataSeries method (which does the # actual plotting) will only draw the # plot if there are no pending requests # (because otherwise it would be drawing # out-of-date data). self.__drawRequests = 0 # The getDrawnDataSeries method returns # data as it is shown on the plot - some # pre/post-processing may be applied to # the data as retrieved by DataSeries # instances, so this dictionary is used # to keep copies of the mpl Artist object # which contains the data with all # processing applied, that is currrently # on the plot (and accessible via # getDrawnDataSeries). self.__drawnDataSeries = collections.OrderedDict() # Redraw whenever any property changes, for propName in ['legend', 'xAutoScale', 'yAutoScale', 'xLogScale', 'yLogScale', 'invertX', 'invertY', 'xScale', 'yScale', 'xOffset', 'yOffset', 'ticks', 'grid', 'gridColour', 'bgColour', 'smooth', 'xlabel', 'ylabel']: self.addListener(propName, self.__name, self.asyncDraw) # custom listeners for a couple of properties self.addListener('dataSeries', self.__name, self.__dataSeriesChanged) self.addListener('artists', self.__name, self.__artistsChanged) self.addListener('limits', self.__name, self.__limitsChanged)
[docs] def destroy(self): """Removes some property listeners, and clears references to all :class:`.DataSeries`, ``matplotlib`` and ``wx`` objects. """ for propName in ['dataSeries', 'artists', 'limits', 'legend', 'xAutoScale', 'yAutoScale', 'xLogScale', 'yLogScale', 'invertX', 'invertY', 'xScale', 'yScale', 'xOffset', 'yOffset', 'ticks', 'grid', 'gridColour', 'bgColour', 'smooth', 'xlabel', 'ylabel']: self.removeListener(propName, self.__name) for ds in self.dataSeries: for propName in ds.redrawProperties(): ds.removeListener(propName, self.__name) ds.destroy() self.__drawQueue.stop() self.__drawQueue = None self.__drawnDataSeries = None self.dataSeries = [] self.artists = [] self.__figure = None self.__axis = None self.__destroyed = True
@property def destroyed(self): """Returns True if :meth:`destroy` has been called, ``False`` otherwise. """ return self.__destroyed @property def figure(self): """Returns the ``matplotlib`` ``Figure`` instance.""" return self.__figure @property def axis(self): """Returns the ``matplotlib`` ``Axis`` instance.""" return self.__axis @property def canvas(self): """Returns the ``matplotlib`` ``Canvas`` instance.""" return self.__canvas
[docs] def getDrawQueue(self): """Returns the :class`.idle.TaskThread` instance used for data preparation. """ return self.__drawQueue
[docs] def draw(self, *a): """Call :meth:`drawDataSeries` and then :meth:`drawArtists`. Or, if a ``drawFunc`` was provided, calls that instead. You will generally want to call :meth:`asyncDraw` instead of this method. """ if self.destroyed: return if self.__drawFunc: self.__drawFunc(*a) else: self.drawDataSeries() self.drawArtists()
[docs] def asyncDraw(self, *a): """Schedules :meth:`draw` to be run asynchronously. This method should be used in preference to calling :meth:`draw` directly in most cases, particularly where the call occurs within a property callback function. This method is automatically called called whenever a :class:`.DataSeries` is added to the :attr:`dataSeries` list, or when any plot display properties change. """ # don't run the task if it's # already scheduled on the idle loop idleName = '{}.draw'.format(id(self)) if not self.destroyed and not idle.idleLoop.inIdle(idleName): idle.idle(self.draw, name=idleName)
[docs] def message(self, msg, clear=True, border=False): """Displays the given message in the centre of the figure. This is a convenience method provided for use by subclasses. """ axis = self.axis if clear: self.__drawnDataSeries.clear() axis.clear() axis.set_xlim((0.0, 1.0)) axis.set_ylim((0.0, 1.0)) if border: bbox = {'facecolor' : '#ffffff', 'edgecolor' : '#cdcdff', 'boxstyle' : 'round,pad=1'} else: bbox = None axis.text(0.5, 0.5, msg, ha='center', va='center', transform=axis.transAxes, bbox=bbox) self.canvas.draw()
[docs] def getArtist(self, ds): """Returns the ``matplotlib.Artist`` (typically a ``Line2D`` instance) associated with the given :class:`.DataSeries` instance. A ``KeyError`` is raised if there is no such artist. """ return self.__drawnDataSeries[ds]
[docs] def getDrawnDataSeries(self): """Returns a list of tuples, each tuple containing the ``(DataSeries, x, y)`` data for one ``DataSeries`` instance as it is shown on the plot. """ return [(ds, np.array(l.get_xdata()), np.array(l.get_ydata())) for ds, l in self.__drawnDataSeries.items()]
[docs] def prepareDataSeries(self, ds): """Prepares the data from the given :class:`.DataSeries` so it is ready to be plotted. Called by the :meth:`__drawOneDataSeries` method for any ``extraSeries`` passed to the :meth:`drawDataSeries` method (but **not** applied to :class:`.DataSeries` that have been added to the :attr:`dataSeries` list). This implementation just returns :class:`.DataSeries.getData` - you can pass a ``prepareFunc`` to ``__init__`` to perform any custom preprocessing. """ if self.__prepareFunc: return self.__prepareFunc(ds) else: return ds.getData()
[docs] def drawArtists(self, refresh=True, immediate=False): """Draw all ``matplotlib.Artist`` instances in the :attr:`artists` list, then refresh the canvas. :arg refresh: If ``True`` (default), the canvas is refreshed. """ axis = self.axis canvas = self.canvas def realDraw(): # Just in case this PlotPanel is destroyed # before this task gets executed if not fwidgets.isalive(self.__canvas): return for artist in self.artists: if artist not in axis.findobj(type(artist)): axis.add_artist(artist) if immediate: realDraw() else: self.__drawQueue.enqueue(idle.idle, realDraw) def refreshCanvas(): if not self.destroyed: canvas.draw() if refresh: if immediate: refreshCanvas() else: self.__drawQueue.enqueue(idle.idle, refreshCanvas)
[docs] def drawDataSeries(self, extraSeries=None, refresh=False, **plotArgs): """Queues a request to plot all of the :class:`.DataSeries` instances in the :attr:`dataSeries` list. This method does not do the actual plotting - it is performed asynchronously, to avoid locking up the GUI: 1. The data for each ``DataSeries`` instance is prepared on separate threads (using :func:`.idle.run`). 2. A call to :func:`.idle.wait` is enqueued on a :class:`.TaskThread`. 3. This ``wait`` function waits until all of the data preparation threads have completed, and then passes all of the data to the :meth:`__drawDataSeries` method. :arg extraSeries: A sequence of additional ``DataSeries`` to be plotted. These series are passed through the :meth:`prepareDataSeries` method before being plotted. :arg refresh: If ``True``, the canvas is refreshed. Otherwise, you must call ``getCanvas().draw()`` manually. Defaults to ``False`` - the :meth:`drawArtists` method will refresh the canvas, so if you call :meth:`drawArtists` immediately after calling this method (which you should), then you don't need to manually refresh the canvas. :arg plotArgs: Passed through to the :meth:`__drawDataSeries` method. .. note:: This method must only be called from the main application thread (the ``wx`` event loop). """ if extraSeries is None: extraSeries = [] canvas = self.canvas axis = self.axis toPlot = self.dataSeries[:] toPlot = [ds for ds in toPlot if ds.enabled] extraSeries = [ds for ds in extraSeries if ds.enabled] toPlot = extraSeries + toPlot preprocs = [True] * len(extraSeries) + [False] * len(toPlot) if len(toPlot) == 0: self.__drawnDataSeries.clear() axis.clear() canvas.draw() return # Before clearing/redrawing, save # a copy of the x/y axis limits - # the user may have changed them # via panning/zooming and, if # autoLimit is off, we will want # to preserve the limits that the # user set. These are passed to # the __drawDataSeries method. # # Make sure the limits are ordered # as (min, max), as they won't be # if invertX/invertY are active. axxlim = list(sorted(self.limits.x)) axylim = list(sorted(self.limits.y)) # Here we are preparing the data for # each data series on separate threads, # as data preparation can be time # consuming for large images. We # display a message on the canvas # during preparation. tasks = [] allXdata = [None] * len(toPlot) allYdata = [None] * len(toPlot) # Create a separate function # for each data series for idx, (ds, preproc) in enumerate(zip(toPlot, preprocs)): def getData(d=ds, p=preproc, i=idx): if not d.enabled: return if p: xdata, ydata = self.prepareDataSeries(d) else: xdata, ydata = d.getData() allXdata[i] = xdata allYdata[i] = ydata tasks.append(getData) # Run the data preparation tasks, # a separate thread for each. tasks = [idle.run(t) for t in tasks] # Show a message while we're # preparing the data. if self.showPreparingMessage: self.message(strings.messages[self, 'preparingData'], clear=False, border=True) # Wait until data preparation is # done, then call __drawDataSeries. self.__drawRequests += 1 self.__drawQueue.enqueue(idle.wait, tasks, self.__drawDataSeries, toPlot, allXdata, allYdata, axxlim, axylim, refresh, taskName='{}.wait'.format(id(self)), wait_direct=True, **plotArgs)
def __drawDataSeries( self, dataSeries, allXdata, allYdata, oldxlim, oldylim, refresh, xlabel=None, ylabel=None, **plotArgs): """Called by :meth:`__drawDataSeries`. Plots all of the data associated with the given ``dataSeries``. :arg dataSeries: The list of :class:`.DataSeries` instances to plot. :arg allXdata: A list of arrays containing X axis data, one for each ``DataSeries``. :arg allYdata: A list of arrays containing Y axis data, one for each ``DataSeries``. :arg oldxlim: X plot limits from the previous draw. If ``xAutoScale`` is disabled, this limit is preserved. :arg oldylim: Y plot limits from the previous draw. If ``yAutoScale`` is disabled, this limit is preserved. :arg refresh: Refresh the canvas - see :meth:`drawDataSeries`. :arg xlabel: If provided, overrides the value of the :attr:`xlabel` property. :arg ylabel: If provided, overrides the value of the :attr:`ylabel` property. :arg plotArgs: Remaining arguments passed to the :meth:`__drawOneDataSeries` method. """ # Avoid spursious post-destruction # notifications (occur sporadically # during testing) if self.destroyed: return # Only draw the plot if there are no # pending draw requests. Otherwise # we would be drawing out-of-date data. self.__drawRequests -= 1 if self.__drawRequests != 0: return axis = self.axis canvas = self.canvas width, height = canvas.get_width_height() self.__drawnDataSeries.clear() axis.clear() xlims = [] ylims = [] for ds, xdata, ydata in zip(dataSeries, allXdata, allYdata): if any((ds is None, xdata is None, ydata is None)): continue if not ds.enabled: continue xdata = self.xOffset + self.xScale * xdata ydata = self.yOffset + self.yScale * ydata xlim, ylim = self.__drawOneDataSeries(ds, xdata, ydata, **plotArgs) if np.any(np.isclose([xlim[0], ylim[0]], [xlim[1], ylim[1]])): continue xlims.append(xlim) ylims.append(ylim) if len(xlims) == 0: xmin, xmax = 0.0, 0.0 ymin, ymax = 0.0, 0.0 else: (xmin, xmax), (ymin, ymax) = self.__calcLimits( xlims, ylims, oldxlim, oldylim, width, height) # x/y axis labels if xlabel is None: xlabel = self.xlabel if ylabel is None: ylabel = self.ylabel if xlabel is None: xlabel = '' if ylabel is None: ylabel = '' xlabel = xlabel.strip() ylabel = ylabel.strip() if xlabel != '': axis.set_xlabel(xlabel, va='bottom') axis.xaxis.set_label_coords(0.5, 10.0 / height) if ylabel != '': axis.set_ylabel(ylabel, va='top') axis.yaxis.set_label_coords(10.0 / width, 0.5) # Ticks if self.ticks: axis.tick_params(direction='in', pad=-5) axis.tick_params(axis='both', which='both', length=3) for ytl in axis.yaxis.get_ticklabels(): ytl.set_horizontalalignment('left') for xtl in axis.xaxis.get_ticklabels(): xtl.set_verticalalignment('bottom') else: # we clear the labels, but # leave the ticks, so the # axis grid gets drawn xlabels = ['' for i in range(len(axis.xaxis.get_ticklabels()))] ylabels = ['' for i in range(len(axis.yaxis.get_ticklabels()))] axis.set_xticklabels(xlabels) axis.set_yticklabels(ylabels) axis.tick_params(axis='both', which='both', length=0) # Limits if xmin != xmax: if self.invertX: axis.set_xlim((xmax, xmin)) else: axis.set_xlim((xmin, xmax)) if self.invertY: axis.set_ylim((ymax, ymin)) else: axis.set_ylim((ymin, ymax)) # legend labels = [ds.label for ds in dataSeries if ds.label is not None] if len(labels) > 0 and self.legend: handles, labels = axis.get_legend_handles_labels() legend = axis.legend( handles, labels, loc='upper right', fontsize=10, handlelength=3, fancybox=True) legend.get_frame().set_alpha(0.6) if self.grid: axis.grid(linestyle='-', color=self.gridColour, linewidth=0.5, zorder=0) else: axis.grid(False) axis.spines['right'] .set_visible(False) axis.spines['left'] .set_visible(False) axis.spines['top'] .set_visible(False) axis.spines['bottom'].set_visible(False) axis.set_axisbelow(True) axis.patch.set_facecolor(self.bgColour) self.figure.patch.set_alpha(0) if refresh: canvas.draw() def __drawOneDataSeries(self, ds, xdata, ydata, **plotArgs): """Plots a single :class:`.DataSeries` instance. This method is called by the :meth:`drawDataSeries` method. :arg ds: The ``DataSeries`` instance. :arg xdata: X axis data. :arg ydata: Y axis data. :arg plotArgs: May be used to customise the plot - these arguments are all passed through to the ``Axis.plot`` function. """ if ds.alpha == 0: return (0, 0), (0, 0) if len(xdata) != len(ydata) or len(xdata) == 0: log.debug('{}: data series length mismatch, or ' 'no data points (x: {}, y: {})'.format( ds.overlay.name, len(xdata), len(ydata))) return (0, 0), (0, 0) xdata = np.asarray(xdata, dtype=float) ydata = np.asarray(ydata, dtype=float) log.debug('Drawing {} for {}'.format(type(ds).__name__, ds.overlay)) # Note to self: If the smoothed data is # filled with NaNs, it is possibly due # to duplicate values in the x data, which # are not handled very well by splrep. if self.smooth: tck = interp.splrep(xdata, ydata) xdata = np.linspace(xdata[0], xdata[-1], len(xdata) * 5, dtype=np.float32) ydata = interp.splev(xdata, tck) nans = ~(np.isfinite(xdata) & np.isfinite(ydata)) xdata[nans] = np.nan ydata[nans] = np.nan if self.xLogScale: xdata[xdata <= 0] = np.nan if self.yLogScale: ydata[ydata <= 0] = np.nan if np.all(np.isnan(xdata) | np.isnan(ydata)): return (0, 0), (0, 0) kwargs = plotArgs kwargs['lw'] = kwargs.get('lw', ds.lineWidth) kwargs['alpha'] = kwargs.get('alpha', ds.alpha) kwargs['color'] = kwargs.get('color', ds.colour) kwargs['label'] = kwargs.get('label', ds.label) kwargs['ls'] = kwargs.get('ls', ds.lineStyle) axis = self.axis line = axis.plot(xdata, ydata, **kwargs)[0] self.__drawnDataSeries[ds] = line if self.xLogScale: axis.set_xscale('log') posx = xdata[xdata > 0] xlimits = np.nanmin(posx), np.nanmax(posx) else: xlimits = np.nanmin(xdata), np.nanmax(xdata) if self.yLogScale: axis.set_yscale('log') posy = ydata[ydata > 0] ylimits = np.nanmin(posy), np.nanmax(posy) else: ylimits = np.nanmin(ydata), np.nanmax(ydata) return xlimits, ylimits def __dataSeriesChanged(self, *a): """Called when the :attr:`dataSeries` list changes. Adds listeners to any new :class:`.DataSeries` instances, and then calls :meth:`asyncDraw`. """ for ds in self.dataSeries: for propName in ds.redrawProperties(): ds.addListener(propName, self.__name, self.asyncDraw, overwrite=True) self.asyncDraw() def __artistsChanged(self, *a): """Called when the :attr:`artists` list changes. Calls :meth:`asyncDraw`. """ self.asyncDraw() def __limitsChanged(self, *a): """Called when the :attr:`limits` change. Updates the axis limits accordingly. """ axis = self.axis axis.set_xlim(self.limits.x) axis.set_ylim(self.limits.y) self.asyncDraw() def __calcLimits(self, dataxlims, dataylims, axisxlims, axisylims, axWidth, axHeight): """Calculates and returns suitable axis limits for the current plot. Also updates the :attr:`limits` property. This method is called by the :meth:`drawDataSeries` method. If :attr:`xAutoScale` or :attr:`yAutoScale` are enabled, the limits are calculated from the data range, using the canvas width and height to maintain consistent padding around the plotted data, irrespective of the canvas size. . Otherwise, the existing axis limits are retained. :arg dataxlims: A tuple containing the (min, max) x data range. :arg dataylims: A tuple containing the (min, max) y data range. :arg axisxlims: A tuple containing the current (min, max) x axis limits. :arg axisylims: A tuple containing the current (min, max) y axis limits. :arg axWidth: Canvas width in pixels :arg axHeight: Canvas height in pixels """ if self.xAutoScale: xmin = min([lim[0] for lim in dataxlims]) xmax = max([lim[1] for lim in dataxlims]) lPad = (xmax - xmin) * (50.0 / axWidth) rPad = (xmax - xmin) * (50.0 / axWidth) xmin = xmin - lPad xmax = xmax + rPad else: xmin = axisxlims[0] xmax = axisxlims[1] if self.yAutoScale: ymin = min([lim[0] for lim in dataylims]) ymax = max([lim[1] for lim in dataylims]) bPad = (ymax - ymin) * (50.0 / axHeight) tPad = (ymax - ymin) * (50.0 / axHeight) ymin = ymin - bPad ymax = ymax + tPad else: ymin = axisylims[0] ymax = axisylims[1] self.disableListener('limits', self.__name) self.limits[:] = [xmin, xmax, ymin, ymax] self.enableListener('limits', self.__name) return (xmin, xmax), (ymin, ymax)