Home > GSOC2015 > Tentative Matplotlib Integration

Tentative Matplotlib Integration

The purpose of this post is to introduce the main goal of my google summer of code project. The second goal is to support ink processing in a more easier way. Both together can make a difference from other backends implementations. First, I am going to start by summarizing what I know so far about how matplotlib works. The package matplotlib.pyplot is the root of a tree hierarchy, they call it “state-machine environment”. The next step is to create a figure which internally will create the structure to handle the rest of the objects inside the figure. The next level in the hierarchy is Axes, Axis and Artist.

Axes is the whole figure area where the Axis and plot are going to be rendered. As I can see it Axes is kind of a subplot since a Figure can handle more than one Axes but it needs at least one. Axes relevant methods are set_title(), set_xlabel() and set_ylabel(). Axis refers to the coordinates of the graph two main attributes of this class are Locator and Formatter. This class handles the ticks and labels over the coordinates as well as the mapping with the plotting area. Finally, Artist is the class that takes care of the style of the graph –> color, line styles, fonts, etc.

GIven this, a backend is an abstract class that allows to render matplotlib plots using different frontend platforms. I am going to be working on writting one for Kivy starting next week. There are two types of backends: user interface and non-interactive. The user interface one provides ways to visually interact with the data using different interaction techniques usually mouse and keyboard. The non-interactive are the ones that export the plot to a hard format such as png, pdf or svg. There are two rendering engines one given by vector renderers and one given by raster renderers. The main difference between both is that vector use primitive geometric elements such as lines and polygons and raster which are an array of pixels. The very first one can scale and the second one are resolution dependent.

For the implementation of kivy backend I am gonna start implementing a RendererBase which needs a GraphicsContext. To the best of my knowledge while creating the Agg implementation I would pass Agg’s graphics context otherwise I would pass kivy.graphics.context for this purpose. The methods to implement in this class are draw_path, draw_image, draw_gouraud_triangle, there are some other methods as well that can be implemented for optimization reasons.

A GraphicsContext needs to be instantiated and mapped to the matplotlib base classes methods and attributes. For instance map the color of a line with a function that gives what is that color in the kivy context. Another example could be to tell matplotlib use normal line or anti aliased implementation. FigureCanvas should map kivy canvas in addition with the set of events that would need to be overwritten in the FigureCanvas implementation. FigureManager creates the window for the look of it. There is a class called timer which seems to handle the delay time between events.

For the Show() one can overwrite the default mainloop to use the kivy mainloop App.run().

A very first outline can be seen here:


"""

This file was built taking as models backend_template.py, backend_gtk3.py, backend_wx.py and backend_qt5.py. This is just a more developed
template based on documentation reading. There are two things I am still unclear, the backend in most of the cases returns a whole App
with the information rendered. But in our case if we would like to generate a widget that can be added to another widget then we would 
need to return FigureCanvasKivy as a Widget. If this is the case we would need to pass the main kivy thread to the backend. If not we can
create a new Kivy thread.

"""
 
from __future__ import (absolute_import, division, print_function,
                        unicode_literals)
 
import six
 
try:
    import kivy
    from kivy.app import App
    from kivy.uix.widget import Widget
except ImportError:
    raise ImportError("this backend requires Kivy to be installed.")
 
try:
    kivy.require('1.9.0') # I would need to check which release of Kivy would be the best suitable.
except AttributeError:
    raise ImportError(
        "kivy version too old -- it must have require_version")
 
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 ShowBase
 
from matplotlib.cbook import is_string_like, is_writable_file_like
from matplotlib.colors import colorConverter
from matplotlib.figure import Figure
from matplotlib.widgets import SubplotTool
 
from matplotlib import lines
from matplotlib import cbook
from matplotlib import verbose
from matplotlib import rcParams
 
_debug = True
 
class TestApp(App):
    def build(self):
        pass
        #return FloatLayout(size=(300,300))
 
app = None
 
def _create_App():
    global app
    if app is None:
        if _debug:
            print("Starting up Kivy Application")
        app = TestApp()
 
class Show(ShowBase):
    def mainloop(self):
        global app
        app.run()
 
show = Show()
 
def draw_if_interactive():
    pass
 
def new_figure_manager(num, *args, **kwargs):
    """
    Create a new figure manager instance
    """
    _create_App()
    thisFig = Figure(*args, **kwargs)
    return new_figure_manager_given_figure(num, thisFig)
 
 
def new_figure_manager_given_figure(num, figure):
    """
    Create a new figure manager instance for the given figure.
    """
    canvas = FigureCanvasKivy(figure)
    manager = FigureManagerKivy(canvas, num)
    return manager
 
class RendererKivy(RendererBase):
	"""
	Handles drawing/rendering operations
	We can try to use Agg, GDK or Cairo or develop one from scratch
	"""
	def __init__(self, dpi):
        self.dpi = dpi
 
    def draw_path(self, gc, path, transform, rgbFace=None):
        pass
		
	def draw_markers(self, gc, marker_path, marker_trans, path, trans, rgbFace=None):
		pass
		
	def draw_image(self, gc, x, y, im):
        pass
 
    def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
        pass
 
    def flipy(self):
        return True
 
    def get_canvas_width_height(self):
        return 100, 100
 
    def get_text_width_height_descent(self, s, prop, ismath):
        return 1, 1, 1
 
    def new_gc(self):
        return GraphicsContextTemplate()
 
    def points_to_pixels(self, points):
		pass
	
class FigureCanvasKivy(Widget, FigureCanvasBase):
    def __init__(self, figure):
        if _debug:
            print('FigureCanvasKivy: ', figure)
		
		#Examples of connecting events predefined here to events declared on matplotlib
		self.connect('scroll_event',         self.scroll_event)
        self.connect('button_press_event',   self.button_press_event)
        self.connect('button_release_event', self.button_release_event)
        self.connect('configure_event',      self.configure_event)
        self.connect('expose_event',         self.expose_event)
        self.connect('key_press_event',      self.key_press_event)
        self.connect('key_release_event',    self.key_release_event)
        self.connect('motion_notify_event',  self.motion_notify_event)
        self.connect('leave_notify_event',   self.leave_notify_event)
        self.connect('enter_notify_event',   self.enter_notify_event)
 
        super(FigureCanvasKivy, self).__init__(figure=figure)
		self._renderer_init()
        self.figure = figure
        w, h = self.get_width_height()
		
	def _renderer_init(self):
		self._renderer = RendererKivy(self, self.figure.dpi)
 
    def __timerEvent(self, event):
        # hide until we can test and fix
        self.mpl_idle_event(event)
	
	def get_width_height():
		return (Widget.width, Widget.height)
 
    def enterEvent(self, event):
        FigureCanvasBase.enter_notify_event(self, guiEvent=event)
 
    def leaveEvent(self, event):
        pass
		
	#Example overriding event
    def mousePressEvent(self, event):
        x = event.pos().x()
        # flipy so y=0 is bottom of canvas
        y = self.figure.bbox.height - event.pos().y()
        button = self.buttond.get(event.button())
        if button is not None:
            FigureCanvasBase.button_press_event(self, x, y, button,
                                                guiEvent=event)
        if _debug:
            print('button pressed:', event.button())
 
    def mouseDoubleClickEvent(self, event):
        x = event.pos().x()
        # flipy so y=0 is bottom of canvas
        y = self.figure.bbox.height - event.pos().y()
        button = self.buttond.get(event.button())
        if button is not None:
            FigureCanvasBase.button_press_event(self, x, y,
                                                button, dblclick=True,
                                                guiEvent=event)
        if _debug:
            print('button doubleclicked:', event.button())
 
    def mouseMoveEvent(self, event):
        x = event.x()
        # flipy so y=0 is bottom of canvas
        y = self.figure.bbox.height - event.y()
        FigureCanvasBase.motion_notify_event(self, x, y, guiEvent=event)
        # if DEBUG: print('mouse move')
 
    def mouseReleaseEvent(self, event):
        x = event.x()
        # flipy so y=0 is bottom of canvas
        y = self.figure.bbox.height - event.y()
        button = self.buttond.get(event.button())
        if button is not None:
            FigureCanvasBase.button_release_event(self, x, y, button,
                                                  guiEvent=event)
        if _debug:
            print('button released')
 
    def wheelEvent(self, event):
        x = event.x()
        # flipy so y=0 is bottom of canvas
        y = self.figure.bbox.height - event.y()
        # from QWheelEvent::delta doc
        if event.pixelDelta().x() == 0 and event.pixelDelta().y() == 0:
            steps = event.angleDelta().y() / 120
        else:
            steps = event.pixelDelta().y()
 
        if steps != 0:
            FigureCanvasBase.scroll_event(self, x, y, steps, guiEvent=event)
            if _debug:
                print('scroll event: delta = %i, '
                      'steps = %i ' % (event.delta(), steps))
 
    def keyPressEvent(self, event):
        key = self._get_key(event)
        if key is None:
            return
        FigureCanvasBase.key_press_event(self, key, guiEvent=event)
        if _debug:
            print('key press', key)
 
    def keyReleaseEvent(self, event):
        key = self._get_key(event)
        if key is None:
            return
        FigureCanvasBase.key_release_event(self, key, guiEvent=event)
        if _debug:
            print('key release', key)
 
    def resizeEvent(self, event):
        #to implement
        pass
 
    def sizeHint(self):
        return Widget.size
 
    def minumumSizeHint(self):
        Widget.size = (10,10)
        return Widget.size
 
    def _get_key(self, event):
        #to implement
        pass
 
    def new_timer(self, *args, **kwargs):
        pass
        #return TimerKivy(*args, **kwargs)
 
    def flush_events(self):
        pass
        #global app
        #app.processEvents()
 
    def start_event_loop(self, timeout):
        FigureCanvasBase.start_event_loop_default(self, timeout)
 
    
    def stop_event_loop(self):
        FigureCanvasBase.stop_event_loop_default(self)
 
    def draw_idle(self):
        #to IMPLEMENTATION
        pass
 
import os
 
class FigureManagerKivy(FigureManagerBase):
 
	"""
	Here we have an interactive backend handle windows.
	"""
 
    def __init__(self, canvas, num):
        global app
        if _debug:
            print('FigureManagerKivy.%s' % fn_name())
        FigureManagerBase.__init__(self, canvas, num)
        self.canvas = canvas
        self.window = app._app_window
        #App Kivy events to implement
        self.window.closing.connect(canvas.close_event)
        self.window.closing.connect(self._widgetclosed)
        ##################################################
        
        self.window.set_title("Figure %d" % num)
        image = os.path.join(matplotlib.rcParams['datapath'],
                             'images', 'matplotlib.png')
        self.window.set_icon(app.get_application_icon())
 
 
    def full_screen_toggle(self):
        #toogle in kivy between fullscreen and non fullscreen
        pass
 
    def _widgetclosed(self):
        pass
        #check if window of the app is destroyed
 
    def _get_toolbar(self, canvas, parent):
        #Toolbar to implement on Kivy
        pass
 
    def resize(self, width, height):
        'set the canvas size in pixels'
        pass
 
    def show(self):
        self.window.show()
 
    def destroy(self, *args):
        #Delete components crated for the widget
        pass
 
    def get_window_title(self):
        return six.text_type(self.window.title)
 
    def set_window_title(self, title):
        self.window.set_title(title)
		
class GraphicsContextTemplate(GraphicsContextBase):
	pass
	"""
    The graphics context provides the color, line styles, etc...
	Rendering work, ref: backend_ps.py
	"""
 
FigureCanvas = FigureCanvasKivy
FigureManager = FigureManagerKivy

Advertisements
Categories: GSOC2015 Tags: ,
  1. No comments yet.
  1. No trackbacks yet.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: