Source code for kapteyn.mplutil

"""
Module mplutil
==============

.. sectionauthor:: Hans Terlouw <gipsy@astro.rug.nl>
.. highlight:: python
   :linenothreshold: 1000

Utilities for use with matplotlib.
Classes :class:`AxesCallback`, :class:`CanvasCallback`, :class:`TimeCallback`
and :class:`VariableColormap`
and module-internal function :func:`KeyPressFilter`.

Class AxesCallback
------------------

.. autoclass:: AxesCallback

Class CanvasCallback
--------------------

.. autoclass:: CanvasCallback

Class TimeCallback
------------------

.. autoclass:: TimeCallback

Class VariableColormap
----------------------

.. autoclass:: VariableColormap

Key press filter
----------------

Via its internal function :func:`KeyPressFilter` the module filters key_press
events for the backend in which the application displays its contents.
By default all key_press events are discarded by the filter and do not reach
the backend. This behaviour can be changed by assigning a list of acceptable
keys to KeyPressFilter's attribute *allowed*. E.g.,
``KeyPressFilter.allowed = ['g', 'f']`` will allow characters ``g`` and ``f``
to reach the backend so that the backend's grid- and full-screen toggles
will be available again.
The filtering can be completely switched on and off by assigning True or False
to KeyPressFilter's attribute *enabled*. E.g.,
``KeyPressFilter.enabled = False``.

GIPSY keyword event connection
------------------------------

.. autofunction:: gipsy_connect

Matplotlib backends work-arounds
--------------------------------

This module provides work-arounds for limitations of the matplotlib
Qt4 and Qt4Agg backends. They will become available when :mod:`mplutil`
is imported. No other action is required.

Special keys
............

By default, the Qt4 backend does not return a number of special key codes
in key_press_event objects. This work-around makes the following key codes
available: 'pageup', 'pagedown', 'left', 'right', 'up', 'down', 'home'
and 'end'.

Resize events
.............

By default, the Qt4Agg backend does not report resize events. This work-around
takes care of this.
 
"""
# VOG: Next lines are only to confirm the backend
#from sys import stdout
#from matplotlib import rcParams
#backend = rcParams['backend'].upper()
#print("Backend mplutil:", backend)
#stdout.flush()


# Work-around for the qt4 backend which does not support the PageUp
# and PageDown keys.
# VOG: For current versions of PyQt/Matplotlib this is not needed and 
# it causes crashes also.
"""
try:
   from PyQt4 import QtCore
   from matplotlib.backends.backend_qt4 import FigureCanvasQT
   FigureCanvasQT.keyvald[QtCore.Qt.Key_PageUp]   = 'pageup'
   FigureCanvasQT.keyvald[QtCore.Qt.Key_PageDown] = 'pagedown'
   FigureCanvasQT.keyvald[QtCore.Qt.Key_Left]     = 'left'
   FigureCanvasQT.keyvald[QtCore.Qt.Key_Right]    = 'right'
   FigureCanvasQT.keyvald[QtCore.Qt.Key_Up]       = 'up'
   FigureCanvasQT.keyvald[QtCore.Qt.Key_Down]     = 'down'
   FigureCanvasQT.keyvald[QtCore.Qt.Key_Home]     = 'home'
   FigureCanvasQT.keyvald[QtCore.Qt.Key_End]      = 'end'
except:
   pass


# Work-arounds for the qt4agg backend.
#

try:
   from matplotlib.backends import backend_qt4agg

#
#  Enable reporting resize events which the qt4agg normally doesn't do.
#
   def resizeEvent( self, e ):
      backend_qt4agg.FigureCanvasQT.resizeEvent( self, e )
      self.resize_event()

#
#  Allow keypress events also when auto-repeating. By default these
#  are suppressed as of matplotlib version 1.1.0.
#
   def _get_key( self, event ):
###        if event.isAutoRepeat():
###            return None
        if event.key() < 256:
            key = str(event.text())
        elif event.key() in self.keyvald:
            key = self.keyvald[ event.key() ]
        else:
            key = None
          
        return key 


   backend_qt4agg.FigureCanvasQTAgg.resizeEvent = resizeEvent
   backend_qt4agg.FigureCanvasQTAgg._get_key = _get_key
except:
   pass
"""


import weakref
   
# ==========================================================================
#                             class AxesCallback
# --------------------------------------------------------------------------
[docs]class AxesCallback(object): """ :class:`AxesCallback` has been built on top of matplotlib's event handling mechanism. Objects of this class provide a more powerful mechanism for handling events from :class:`LocationEvent` and derived classes than matplotlib provides itself. This class allows the programmer to register a callback function with an event type combined with an Axes object. Whenever the event occurs within the specified Axes object, the callback function is called with the AxesCallback object as its single argument. Different from matplotlib-style event handlers, it is possible to handle overlapping Axes objects. An AxesCallback object will not be deleted as long as it is scheduled ("active"), so it is not always necessary to keep a reference to it. :param proc: the function to be called upon receiving an event of the specified type and occurring in the specified Axes object. It is called with one argument: the current AxesCallback object. If it returns a value which evaluates to True, processing of the current event stops, i.e., no further callback functions will be called for this event. :param axes: the matplotlib Axes object. :param eventtype: the matplotlib event type such as 'motion_notify_event' or 'key_press_event'. :param schedule: indicates whether the object should start handling events immediately. Default True. :param attr: keyword arguments each resulting in an attribute with the same name. **Attributes:** .. attribute:: axes The specified axes object. .. attribute:: canvas The FigureCanvas object to which `axes` belongs. .. attribute:: eventtype The specified event type. .. attribute:: active True if callback is scheduled, False otherwise. .. attribute:: xdata, ydata The cursor position in data coordinates within the specified Axes object. These values may be different from the attributes with the same name of the event object. .. attribute:: event The Event object delivered by matplotlib. **Methods:** .. automethod:: schedule .. automethod:: deschedule **Example:** :: #!/usr/bin/env python from matplotlib.pyplot import figure, show from kapteyn.mplutil import AxesCallback def draw_cb(cb): if cb.event.button: if cb.pos is not None: cb.axes.plot((cb.pos[0], cb.xdata), (cb.pos[1], cb.ydata), cb.c) cb.canvas.draw() cb.pos = (cb.xdata, cb.ydata) else: cb.pos = None def colour_cb(cb): cb.drawer.c = cb.event.key fig = figure() frame = fig.add_axes((0.1, 0.1, 0.8, 0.8)) frame.set_autoscale_on(False) draw = AxesCallback(draw_cb, frame, 'motion_notify_event', pos=None, c='r') setc = AxesCallback(colour_cb, frame, 'key_press_event', drawer=draw) show() The above code implements a complete, though very simple, drawing program. It first creates a drawing frame and then connects two :class:`AxesCallback` objects to it. The first object, `draw`, connects to the callback function :func:`draw_cb`, which will draw line segments as long as the mouse is moved with a button down. The previous position is "remembered" by `draw` via its attribute :attr:`pos`. The drawing colour is determined by `draw`'s attribute :attr:`c` which can be modified by the callback function :func:`colour_cb` by typing one of the letters 'r', 'g', 'b', 'y', 'm', 'c', 'w' or 'k'. This callback function is called via the second AxesCallback object `setc` which has the first :class:`AxesCallback` object `draw` as an attribute. """ __scheduled = [] # currently scheduled callbacks __handlers = {} # currently active event handlers def __init__(self, proc, axes, eventtype, schedule=True, **attr): self.proc = proc self.axes = weakref.proxy(axes) self.axref = weakref.ref(axes) self.eventtype = eventtype self.canvas = axes.get_figure().canvas for name in list(attr.keys()): self.__dict__[name] = attr[name] self.active = False if schedule: self.schedule()
[docs] def schedule(self): """ Activate the object so that it will start receiving matplotlib events and calling the callback function. If the object is already active, it will be put in front of the list of active objects so that its callback function will be called before others. """ if self.axref() is None: raise Exception('Axes object does not exist anymore') if self.active: self.__scheduled.remove(self) # remove from current position .. self.__scheduled.insert(0, self) # .. and move to front of list return # no further action # Try to find a handler and increment the number of # registrations for this canvas-eventtype combination. # If no handler can be found, connect this event type # to __handler() and register this combination. try: id, numreg = self.__handlers[self.canvas, self.eventtype] self.__handlers[self.canvas, self.eventtype] = id, numreg+1 except KeyError: id = self.canvas.mpl_connect(self.eventtype, self.__handler()) self.__handlers[self.canvas,self.eventtype] = id, 1 self.active = True # mark active self.__scheduled.insert(0, self) # insert in active list
[docs] def deschedule(self): """ Deactivate the object so that it does not receive matplotlib events anymore and will not call its callback function. If the object is already inactive, nothing will be done. """ if not self.active: return # no action, stays inactive id, numreg = self.__handlers[self.canvas, self.eventtype] numreg -= 1 # decrement number of callbacks if numreg==0: # was this the last one? del self.__handlers[self.canvas, self.eventtype] # remove registration self.canvas.mpl_disconnect(id) # disconnect handler else: self.__handlers[self.canvas, self.eventtype] = id, numreg self.active = False # mark inactive self.__scheduled.remove(self) # remove from active list
def __handler(): def __handler(event): if event.canvas.widgetlock.locked(): return for callback in AxesCallback.__scheduled: axes = callback.axref() if axes is None: callback.deschedule() continue try: # VOG: Added on 15-11-2017 # because pressing a key outside the MPL canvas could still # generate an event. This reults in TypeError: a float is required if event.canvas is callback.canvas and \ event.name==callback.eventtype and \ axes.contains(event)[0]: callback.event = event callback.xdata, callback.ydata = \ axes.transData.inverted().transform((event.x, event.y)) if callback.proc(callback): break except: pass return __handler __handler = staticmethod(__handler)
# ========================================================================== # class CanvasCallback # --------------------------------------------------------------------------
[docs]class CanvasCallback(object): """ :class:`CanvasCallback` has been built on top of matplotlib's event handling mechanism. Objects of this class provide a more powerful mechanism for handling events than matplotlib provides itself. This class allows the programmer to register a callback function with an event type combined with an FigureCanvas object. Whenever the event occurs within the specified FigureCanvas object, the callback function is called with the CanvasCallback object as its single argument. A CanvasCallback object will not be deleted as long as it is scheduled ("active"), so it is not always necessary to keep a reference to it. This class is a simplified version of :class:`AxesCallback` and is intended for situations where either no Axes object is available or the event type is not a :class:`LocationEvent`, i.e., there is no position involved. :param proc: the function to be called upon receiving an event of the specified type and occurring in the specified FigureCanvas. It is called with one argument: the current CanvasCallback object. If it returns a value which evaluates to True, processing of the current event stops, i.e., no further callback functions will be called for this event. :param canvas: the matplotlib FigureCanvas object. :param eventtype: the matplotlib event type such as 'resize_event' or 'motion_notify_event'. :param schedule: indicates whether the object should start handling events immediately. Default True. :param attr: keyword arguments each resulting in an attribute with the same name. **Attributes:** .. attribute:: canvas The specified FigureCanvas object. .. attribute:: eventtype The specified event type. .. attribute:: active True if callback is scheduled, False otherwise. .. attribute:: event The Event object delivered by matplotlib. **Methods:** .. automethod:: schedule .. automethod:: deschedule """ __scheduled = [] # currently scheduled callbacks __handlers = {} # currently active event handlers def __init__(self, proc, canvas, eventtype, schedule=True, **attr): self.proc = proc self.canvas = canvas self.eventtype = eventtype for name in list(attr.keys()): self.__dict__[name] = attr[name] self.active = False if schedule: self.schedule()
[docs] def schedule(self): """ Activate the object so that it will start receiving matplotlib events and calling the callback function. If the object is already active, it will be put in front of the list of active objects so that its callback function will be called before others. """ if self.active: self.__scheduled.remove(self) # remove from current position .. self.__scheduled.insert(0, self) # .. and move to front of list return # no further action # Try to find a handler and increment the number of # registrations for this canvas-eventtype combination. # If no handler can be found, connect this event type # to __handler() and register this combination. try: id, numreg = self.__handlers[self.canvas, self.eventtype] self.__handlers[self.canvas, self.eventtype] = id, numreg+1 except KeyError: id = self.canvas.mpl_connect(self.eventtype, self.__handler()) self.__handlers[self.canvas,self.eventtype] = id, 1 self.active = True # mark active self.__scheduled.insert(0, self) # insert in active list
[docs] def deschedule(self): """ Deactivate the object so that it does not receive matplotlib events anymore and will not call its callback function. If the object is already inactive, nothing will be done. """ if not self.active: return # no action, stays inactive id, numreg = self.__handlers[self.canvas, self.eventtype] numreg -= 1 # decrement number of callbacks if numreg==0: # was this the last one? del self.__handlers[self.canvas, self.eventtype] # remove registration self.canvas.mpl_disconnect(id) # disconnect handler else: self.__handlers[self.canvas, self.eventtype] = id, numreg self.active = False # mark inactive self.__scheduled.remove(self) # remove from active list
def __handler(): def __handler(event): for callback in CanvasCallback.__scheduled: if event.canvas is callback.canvas and \ event.name==callback.eventtype: callback.event = event if callback.proc(callback): break return __handler __handler = staticmethod(__handler)
# ========================================================================== # class VariableColormap # -------------------------------------------------------------------------- import numpy, math, glob from os.path import basename from numpy import ma from matplotlib.colors import Colormap try: from matplotlib import cm, colormaps except: from matplotlib import cm from kapteyn.tabarray import tabarray from kapteyn import package_dir
[docs]class VariableColormap(Colormap): """ :class:`VariableColormap` is a subclass of :class:`matplotlib.colors.Colormap` with special methods that allow the colormap to be modified. A VariableColormap can be constructed from any other matplotlib colormap object, from a NumPy array with one RGB triplet per row or from a textfile with one RGB triplet per line. Values should be between 0.0 and 1.0. :param source: the object from which the VariableColormap is created. Either an other colormap object or its registered name, a NumPy array or the name of a text file containing RGB triplets. A number of colormap files is available within the package. A list of names can be obtained with class method :meth:`luts`. :param name: the name of the color map. **Attributes:** .. attribute:: auto Indicates whether Axes objects registered with method :meth:`add_frame` will be automatically updated when the colormap changes. Default True. .. attribute:: slope The colormap slope as specified with method :meth:`modify`. .. attribute:: shift The colormap shift as specified with method :meth:`modify`. .. attribute:: scale The colormap's current scale as specified with method :meth:`set_scale`. .. attribute:: source The object (string or colormap) from which the colormap is currently derived. **Methods** .. automethod:: modify .. automethod:: set_scale .. automethod:: set_source .. automethod:: set_length .. automethod:: add_frame .. automethod:: remove_frame .. automethod:: update .. automethod:: luts """
[docs] @classmethod def luts(cls): "Return a list with filenames of colormaps available within the package." maps = [basename(lut) for lut in glob.glob(package_dir + '/lut/*.lut')] maps.sort() return maps
def __init__(self, source, name='Variable'): self.name = None # new VariableColormap object self.bad_set = False self.set_source(source) self.monochrome = False Colormap.__init__(self, name, self.worklut.shape[0]-3) self.canvases = {} self.frames = set() self.slope = 1.0 self.shift = 0.0 self.invrt = 1.0 self.scale = 'LINEAR' self.auto = True self.callback = None def __call__(self, X, alpha=1.0, bytes=False): if self.bad_set: if not isinstance(X, numpy.ma.masked_array): X = numpy.ma.asarray(X) X.mask = ma.make_mask(~numpy.isfinite(X)) return Colormap.__call__(self, X, alpha, bytes) def __len__(self): return self.N def set_bad(self, color='k', alpha = 1.0): self.bad_set = True self.bad_val = (color, alpha) Colormap.set_bad(self, color, alpha) if self.auto: self.update()
[docs] def set_length(self, length): """ Change the colormap's number of entries. The new set of entries is derived from the current set by linear interpolation. The current length can be obtained with the function :func:`len`. For best results, the new length should be chosen such that the original colormap entries are represented unmodified in the new set. This can be achieved by setting :math:`n_{new} = kn_{old}-k+1`, where :math:`n_i` is the colormap's length and :math:`k` is integer. For normal work, the 'standard' length of 256 is usually sufficient, but in special cases increasing the colormap's length can be helpful to eliminate false contours. """ ncolors = len(self) lut_tail = self.baselut[ncolors:] newmap = numpy.zeros((length,3), numpy.float) factor = float(ncolors-1)/(length-1) xdest = numpy.array(list(range(length)), numpy.float)*factor xsrc = list(range(ncolors)) for primary in [0,1,2]: primap = numpy.interp(xdest, xsrc, self.baselut[:ncolors,primary]) newmap[:,primary] = primap self.set_source(newmap) self.baselut[length:] = lut_tail if self.bad_set: badcolor, badalpha = self.bad_val self.set_bad(badcolor, badalpha)
[docs] def set_source(self, source): """ Define an alternative source for the colormap. *source* can be any other matplotlib colormap object or its registered name, a NumPy array with one RGB triplet per row or the name of a textfile with one RGB triplet per line. Values should be between 0.0 and 1.0. """ self.source = source try: # VOG 26-06-2023, deprecated from MPL 3.7: source = cm.get_cmap(source) try: source = colormaps[source] except: source = cm.get_cmap(source) # To remain compatible with MPL versions < 3.7 if source is None: source = self.source # restore source except: pass if isinstance(source, Colormap): if not source._isinit: source._init() self.baselut = source._lut self.N = source.N elif isinstance(source, numpy.ndarray): ncolors = source.shape[0] self.baselut = numpy.ones((ncolors+3,4), dtype=float) self.baselut[:ncolors,:3] = source self.N = ncolors # may have changed else: try: colors = tabarray(source) except: colors = tabarray(package_dir + '/lut/' + source) ncolors = colors.shape[0] self.baselut = numpy.ones((ncolors+3,4), dtype=float) self.baselut[:ncolors,:3] = colors self.N = ncolors # may have changed self._i_under = self.N self._i_over = self.N+1 self._i_bad = self.N+2 self.worklut = self.baselut.copy() self._lut = self.worklut.copy() # existing may be inadequate if self.bad_set: badcolor, badalpha = self.bad_val self.set_bad(badcolor, badalpha) # restore bad if self.name is not None: # existing VariableColormap object? self.set_scale(self.scale)
def _init(self): self._lut = self.worklut.copy() self._isinit = True self._set_extremes()
[docs] def modify(self, slope, shift): """ Apply a slope and a shift to the colormap. Defaults are 1.0 and 0.0. If one or more Axes objects have been registered with method :meth:`add_frame`, the images in them will be updated and the corresponding canvases will be redrawn. """ if not self._isinit: self._init() self.slope = slope self.shift = shift ncolors = self.N lut = self._lut worklut = self.worklut slope = slope*self.invrt for i in range(ncolors): x = (float(i)/float(ncolors-1))-0.5 y = slope*(x-shift)+0.5 if y>1.0: y = 1.0 elif y<0.0: y = 0.0 m = int(float(ncolors-1)*y+0.5) lut[i] = worklut[m] if self.auto: self.update()
def set_inverse(self, inverse=False): if inverse: self.invrt = -1.0 else: self.invrt = 1.0 self.modify(self.slope, self.shift)
[docs] def set_scale(self, scale='LINEAR'): """ Apply a scale to this colormap. *scale* can be one of: 'LINEAR', 'LOG', 'EXP', 'SQRT' and 'SQUARE'. """ scale = scale.upper() ncolors = self.N baselut = self.baselut worklut = self.worklut if scale=='LOG': fac = float(ncolors-1)/math.log(ncolors) for i in range(ncolors): worklut[i] = baselut[int(fac*math.log(i+1))] elif scale=='EXP': fac = float(ncolors-1)/math.pow(10.0, (ncolors-1)/100.0 -1.0) for i in range(ncolors): worklut[i] = baselut[int(fac*math.pow(10.0, i/100.0-1.0))] elif scale=='SQRT': fac = float(ncolors-1)/math.sqrt(ncolors) for i in range(ncolors): worklut[i] = baselut[int(fac*math.sqrt(i))] elif scale=='SQUARE': fac = float(ncolors-1)/(ncolors*ncolors) for i in range(ncolors): worklut[i] = baselut[int(fac*i*i)] elif scale=='LINEAR': worklut[:] = baselut[:] else: raise Exception('invalid colormap scale') self.scale = scale self.modify(self.slope, self.shift)
[docs] def add_frame(self, frame): """ Associate matplotlib Axes object *frame* with this colormap. If the colormap is subsequently modified, images in this frame will be updated and *frame*'s canvas will be redrawn. """ self.frames.add(frame) canvas = frame.figure.canvas if canvas not in self.canvases: self.canvases[canvas] = 1 else: self.canvases[canvas] += 1
[docs] def remove_frame(self, frame): """ Disassociate matplotlib Axes object *frame* from this colormap. """ self.frames.remove(frame) canvas = frame.figure.canvas self.canvases[canvas] -= 1 if self.canvases[canvas]==0: del self.canvases[canvas]
[docs] def update(self): """ Redraw all images in the Axes objects registered with method :meth:`add_frame`. update() is called automatically when the colormap changes while :attr:`auto` is True. """ for frame in self.frames: for image in frame.get_images(): image.changed() for canvas in self.canvases: canvas.draw() if self.callback is not None: self.callback()
# ========================================================================== # function KeyPressFilter # # VOG, 05-07-2023: Made function unusable and override behaviour with rcParams # See maputils.py # -------------------------------------------------------------------------- from matplotlib.backend_bases import FigureManagerBase #__key_press = FigureManagerBase.key_press def KeyPressFilter(fmb_obj, event): return #if not KeyPressFilter.enabled or event.key in KeyPressFilter.allowed: # __key_press(fmb_obj, event) KeyPressFilter.allowed = [] KeyPressFilter.enabled = True FigureManagerBase.key_press = KeyPressFilter # ========================================================================== # class TimeCallback # -------------------------------------------------------------------------- from matplotlib import rcParams
[docs]class TimeCallback(object): """ Objects of this class are responsible for handling timer events. Timer events occur periodically whenever a predefined period of time expires. A TimeCallback object will not be deleted as long as it is scheduled ("active"), so it is not always necessary to keep a reference to it. This class is backend-dependent. Currently supported backends are GTKAgg, GTK, Qt4Agg, Qt5Agg and TkAgg. :param proc: the function to be called upon receiving an event of the specified type and occurring in the specified Axes object. It is called with one argument: the current TimeCallback object. :param interval: the time interval in seconds. :param schedule: indicates whether the object should start handling events immediately. Default True. :param attr: keyword arguments each resulting in an attribute with the same name. **Attribute:** .. attribute:: active True if callback is scheduled, False otherwise. **Methods:** .. automethod:: schedule .. automethod:: deschedule .. automethod:: set_interval **Example:** :: #/usr/bin/env python from matplotlib import pyplot from kapteyn.mplutil import VariableColormap, TimeCallback import numpy from matplotlib import mlab def colour_cb(cb): slope = cb.cmap.slope shift = cb.cmap.shift if shift>0.5: shift = -0.5 cb.cmap.modify(slope, shift+0.01) # change colormap figure = pyplot.figure(figsize=(8,8)) frame = figure.add_axes([0.05, 0.05, 0.85, 0.85]) colormap = VariableColormap('jet') colormap.add_frame(frame) TimeCallback(colour_cb, 0.1, cmap=colormap) # change every 0.1 s x = y = numpy.arange(-3.0, 3.0, 0.025) X, Y = numpy.meshgrid(x, y) Z1 = mlab.bivariate_normal(X, Y, 1.0, 1.0, 0.0, 0.0) # Gaussian 1 Z2 = mlab.bivariate_normal(X, Y, 1.5, 0.5, 1, 1) # Gaussian 2 Z = Z2-Z1 # difference img = frame.imshow(Z, origin="lower", cmap=colormap) pyplot.show() This code displays an image composed of 2 Gaussians and continuously modifies its colormap's shift value between -0.5 and 0.5 in steps of 0.01. These steps take place at 0.1 second intervals. """ supported = {} scheduled = [] def __new__(cls, *args, **kwds): backend = rcParams['backend'].upper() if backend in TimeCallback.supported: return object.__new__(TimeCallback.supported[backend]) else: raise Exception('TimeCallback not supported for backend %s' % backend) def __init__(self, proc, interval, schedule=True, **attr): self.proc = proc self.interval = interval for name in list(attr.keys()): self.__dict__[name] = attr[name] self.id = 0 self.active = False if schedule: self.schedule()
[docs] def schedule(self): """ Activate the object so that it will start calling the callback function periodically. If the object is already active, nothing will be done. """ pass
[docs] def deschedule(self): """ Deactivate the object so that it stops calling its callback function. If the object is already inactive, nothing will be done. """ pass
[docs] def set_interval(self, interval): """ Changes the object's time interval in seconds. """ self.interval = interval if self.active: self.deschedule() self.schedule()
# -------------------------------------------------------------------------- # backend-dependent implementations # -------------------------------------------------------------------------- # GTK, GTKAgg: # try: import gobject class TimeCallback_GTK(TimeCallback): def schedule(self): if self.id: return milliseconds = max(1,int(round(self.interval*1000.0))) self.id = gobject.timeout_add(milliseconds, self.reached) self.active = True self.scheduled.append(self) def deschedule(self): if not self.id: return gobject.source_remove(self.id) self.id = 0 self.active = False self.scheduled.remove(self) def reached(self): self.proc(self) return True TimeCallback.supported['GTKAGG'] = TimeCallback_GTK TimeCallback.supported['GTK'] = TimeCallback_GTK except: pass # Qt4Agg: # try: from PyQt4 import QtCore class TimeCallback_QT4(TimeCallback): def __init__(self, proc, interval, schedule=True, **attr): self.proc = proc self.interval = interval for name in list(attr.keys()): self.__dict__[name] = attr[name] self.active = False self.timer = QtCore.QTimer() QtCore.QObject.connect(self.timer, QtCore.SIGNAL("timeout()"), self.reached) if schedule: self.schedule() def schedule(self): if self.active: return self.set_interval(self.interval) self.timer.start() self.active = True self.scheduled.append(self) def deschedule(self): if not self.active: return self.timer.stop() self.active = False self.scheduled.remove(self) def set_interval(self, interval): self.interval = interval milliseconds = max(1,int(round(self.interval*1000.0))) self.timer.setInterval(milliseconds) def reached(self): self.proc(self) TimeCallback.supported['QT4AGG'] = TimeCallback_QT4 except: pass # Qt5Agg: # try: from PyQt5.QtCore import QTimer class TimeCallback_QT5(TimeCallback): def __init__(self, proc, interval, schedule=True, **attr): self.proc = proc self.interval = interval for name in list(attr.keys()): self.__dict__[name] = attr[name] self.active = False self.timer = QTimer() #QtCore.QObject.connect(self.timer, QtCore.SIGNAL("timeout()"), # self.reached) self.timer.timeout.connect(self.reached) if schedule: self.schedule() def schedule(self): if self.active: return self.set_interval(self.interval) self.timer.start() self.active = True self.scheduled.append(self) def deschedule(self): if not self.active: return self.timer.stop() self.active = False self.scheduled.remove(self) def set_interval(self, interval): self.interval = interval milliseconds = max(1,int(round(self.interval*1000.0))) self.timer.setInterval(milliseconds) def reached(self): self.proc(self) TimeCallback.supported['QT5AGG'] = TimeCallback_QT5 # VOG, 17-07-2023: THe backend in QT5 does report as QTAGG, so we add it again. TimeCallback.supported['QTAGG'] = TimeCallback_QT5 except: pass # TkAgg: # try: from matplotlib.pyplot import get_current_fig_manager class TimeCallback_TKAGG(TimeCallback): def schedule(self): if self.id: return self.window = get_current_fig_manager().window self.milliseconds = max(1,int(round(self.interval*1000.0))) self.id = self.window.after(self.milliseconds, self.reached) self.active = True self.scheduled.append(self) def deschedule(self): if not self.id: return self.window.after_cancel(self.id) self.id = 0 self.active = False self.scheduled.remove(self) def reached(self): if self.id: self.proc(self) if self.active: self.id = self.window.after(self.milliseconds, self.reached) TimeCallback.supported['TKAGG'] = TimeCallback_TKAGG except: pass
[docs]def gipsy_connect(): """ Function only to be used by GIPSY tasks. It should be called by matplotlib programs when GIPSY's keyword events need to be handled, i.e., when the task uses the class KeyCallback. Here is an example:: #!/usr/bin/env python import gipsy from matplotlib.pyplot import figure, show from kapteyn.mplutil import AxesCallback, gipsy_connect def key_handler(cb): gipsy.anyout('Event: %s %s' % (cb.key, gipsy.usertext(cb.key))) gipsy.init() fig = figure() frame = fig.add_axes((0.1, 0.1, 0.8, 0.8)) gipsy_connect() gipsy.KeyCallback(key_handler, 'TESTKEY=') show() gipsy.finis() """ import gipsy backend = rcParams['backend'].upper() if backend in ['GTK', 'GTKAGG']: gipsy.gtkconnect() elif backend in ['QT4AGG', 'QT5AGG']: gipsy.qtconnect() #gipsy.anyout("qtconnect") #gipsy.anyout(backend) elif backend in ['TKAGG']: import tkinter def _tkio(fd, mask): gipsy.hersignal() window = get_current_fig_manager().window fd = gipsy.herconnect() window.tk.createfilehandler(fd, tkinter.READABLE, _tkio) else: raise RuntimeError('Unsupported matplotlib backend for GIPSY connect')