''' Backend Kivy ===== .. image:: images/backend_kivy_example.jpg :align: right The :class:`FigureCanvasKivy` widget is used to create a matplotlib graph. This widget has the same properties as :class:`kivy.ext.mpl.backend_kivyagg.FigureCanvasKivyAgg`. FigureCanvasKivy instead of rendering a static image, uses the kivy graphics instructions :class:`kivy.graphics.Line` and :class:`kivy.graphics.Mesh` to render on the canvas. Installation ------------ The matplotlib backend for kivy can be used by using the garden extension in kivy following this .. _link: http://kivy.org/docs/api-kivy.garden.html :: garden install matplotlib Or if you want to include it directly on your application :: cd myapp garden install --app matplotlib Initialization -------------- A backend can be initialized in two ways. The first one is using pure pyplot as explained .. _here: http://matplotlib.org/faq/usage_faq.html#what-is-a-backend:: import matplotlib matplotlib.use('module://kivy.garden.matplotlib.backend_kivy') Once this is done, any figure instantiated after will be wrapped by a :class:`FigureCanvasKivy` ready to use. From here there are two options to continue with the development. 1. Use the :class:`FigureCanvasKivy` attribute defined as canvas from Figure, to embed your matplotlib graph in your own Kivy application as can be seen in the first example in the following section. .. warning:: One can create a matplotlib widget by importing FigureCanvas:: from kivy.garden.matplotlib.backend_kivyagg import FigureCanvas or from kivy.garden.matplotlib.backend_kivy import FigureCanvas and then instantiate an object:: fig, ax = plt.subplots() my_mpl_kivy_widget = FigureCanvas(fig) which will certainly work but a problem will arise if events were connected before the FigureCanvas is instantiated. If this approach is taken please connect matplotlib events after generating the matplotlib kivy widget object :: fig, ax = plt.subplots() fig.canvas.mpl_connect('button_press_event', callback_handler) my_mpl_kivy_widget = FigureCanvas(fig) In this scenario button_press_event won't be connected with the object being created in line 3, because will be connected to the default canvas set by matplotlib. If this approach is taken be sure of connecting the events after instantiation like the following: :: fig, ax = plt.subplots() my_mpl_kivy_widget = FigureCanvas(fig) fig.canvas.mpl_connect('button_press_event', callback_handler) 2. Use pyplot to write the application following matplotlib sintax as can be seen in the second example below. In this case a Kivy application will be created automatically from the matplotlib instructions and a NavigationToolbar will be added to the main canvas. Examples -------- 1. Example of a simple Hello world matplotlib App:: fig, ax = plt.subplots() ax.text(0.6, 0.5, "hello", size=50, rotation=30., ha="center", va="center", bbox=dict(boxstyle="round", ec=(1., 0.5, 0.5), fc=(1., 0.8, 0.8), ) ) ax.text(0.5, 0.4, "world", size=50, rotation=-30., ha="right", va="top", bbox=dict(boxstyle="square", ec=(1., 0.5, 0.5), fc=(1., 0.8, 0.8), ) ) canvas = fig.canvas The object canvas can be added as a widget into the kivy tree widget. If a change is done on the figure an update can be performed using :meth:`~kivy.ext.mpl.backend_kivyagg.FigureCanvasKivyAgg.draw`.:: # update graph canvas.draw() The plot can be exported to png with :meth:`~kivy.ext.mpl.backend_kivyagg.FigureCanvasKivyAgg.print_png`, as an argument receives the `filename`.:: # export to png canvas.print_png("my_plot.png") 2. Example of a pyplot application using matplotlib instructions:: import numpy as np import matplotlib.pyplot as plt N = 5 menMeans = (20, 35, 30, 35, 27) menStd = (2, 3, 4, 1, 2) ind = np.arange(N) # the x locations for the groups width = 0.35 # the width of the bars figure, ax = plt.subplots() rects1 = ax.bar(ind, menMeans, width, color='r', yerr=menStd) womenMeans = (25, 32, 34, 20, 25) womenStd = (3, 5, 2, 3, 3) rects2 = ax.bar(ind + width, womenMeans, width, color='y', yerr=womenStd) ax.set_ylabel('----------------------Scores------------------') ax.set_title('Scores by group and gender') ax.set_xticks(ind + width) ax.set_yticklabels(('Ahh', '--G1--', 'G2', 'G3', 'G4', 'G5', 'G5', 'G5', 'G5'), rotation=90) ax.legend((rects1[0], rects2[0]), ('Men', 'Women')) plt.draw() plt.savefig("test.png") plt.show() Navigation Toolbar ----------------- If initialized by the first step a :class:`NavigationToolbarKivy` widget can be created as well by instantiating an object with a :class:`FigureCanvasKivy` as parameter. The actual widget is stored in its actionbar attribute. This can be seen in test_backend.py example :: bl = BoxLayout(orientation="vertical") my_mpl_kivy_widget1 = FigureCanvasKivy(fig1) my_mpl_kivy_widget2 = FigureCanvasKivy(fig2) nav1 = NavigationToolbar2Kivy(my_mpl_kivy_widget1) nav2 = NavigationToolbar2Kivy(my_mpl_kivy_widget2) bl.add_widget(nav1.actionbar) bl.add_widget(my_mpl_kivy_widget1) bl.add_widget(nav2.actionbar) bl.add_widget(my_mpl_kivy_widget2) Connecting Matplotlib events to Kivy Events ----------------------- All matplotlib events are available: `button_press_event` which is raised on a mouse button clicked or on touch down, `button_release_event` which is raised when a click button is released or on touch up, `key_press_event` which is raised when a key is pressed, `key_release_event` which is raised when a key is released, `motion_notify_event` which is raised when the mouse is on motion, `resize_event` which is raised when the dimensions of the widget change, `scroll_event` which is raised when the mouse scroll wheel is rolled, `figure_enter_event` which is raised when mouse enters a new figure, `figure_leave_event` which is raised when mouse leaves a figure, `close_event` which is raised when the window is closed, `draw_event` which is raised on canvas draw, `pick_event` which is raised when an object is selected, `idle_event` (deprecated), `axes_enter_event` which is fired when mouse enters axes, `axes_leave_event` which is fired when mouse leaves axes.:: def press(event): print('press released from test', event.x, event.y, event.button) def release(event): print('release released from test', event.x, event.y, event.button) def keypress(event): print('key down', event.key) def keyup(event): print('key up', event.key) def motionnotify(event): print('mouse move to ', event.x, event.y) def resize(event): print('resize from mpl ', event) def scroll(event): print('scroll event from mpl ', event.x, event.y, event.step) def figure_enter(event): print('figure enter mpl') def figure_leave(event): print('figure leaving mpl') def close(event): print('closing figure') fig.canvas.mpl_connect('button_press_event', press) fig.canvas.mpl_connect('button_release_event', release) fig.canvas.mpl_connect('key_press_event', keypress) fig.canvas.mpl_connect('key_release_event', keyup) fig.canvas.mpl_connect('motion_notify_event', motionnotify) fig.canvas.mpl_connect('resize_event', resize) fig.canvas.mpl_connect('scroll_event', scroll) fig.canvas.mpl_connect('figure_enter_event', figure_enter) fig.canvas.mpl_connect('figure_leave_event', figure_leave) fig.canvas.mpl_connect('close_event', close) ''' from __future__ import (absolute_import, division, print_function, unicode_literals) import six import os import matplotlib import matplotlib.transforms as transforms from matplotlib._pylab_helpers import Gcf from matplotlib.backend_bases import RendererBase, GraphicsContextBase,\ FigureManagerBase, FigureCanvasBase, NavigationToolbar2, TimerBase from matplotlib.figure import Figure from matplotlib.transforms import Bbox, Affine2D from matplotlib.backend_bases import ShowBase, Event from matplotlib.backends.backend_agg import FigureCanvasAgg from matplotlib.mathtext import MathTextParser from matplotlib import rcParams from hashlib import md5 from matplotlib import _png from matplotlib import _path try: import kivy except ImportError: raise ImportError("this backend requires Kivy to be installed.") from kivy.app import App from kivy.graphics.texture import Texture from kivy.graphics import Rectangle from kivy.uix.widget import Widget from kivy.uix.label import Label from kivy.uix.floatlayout import FloatLayout from kivy.uix.behaviors import FocusBehavior from kivy.uix.actionbar import ActionBar, ActionView, \ ActionButton, ActionToggleButton, \ ActionPrevious, ActionOverflow, ActionSeparator from kivy.base import EventLoop from kivy.core.text import Label as CoreLabel from kivy.core.image import Image from kivy.graphics import Color, Line from kivy.graphics import Rotate, Translate from kivy.graphics.instructions import InstructionGroup from kivy.graphics.tesselator import Tesselator from kivy.graphics.context_instructions import PopMatrix, PushMatrix from kivy.graphics import StencilPush, StencilPop, StencilUse,\ StencilUnUse from kivy.logger import Logger from kivy.graphics import Mesh from kivy.resources import resource_find from kivy.uix.stencilview import StencilView from kivy.core.window import Window from kivy.uix.button import Button from kivy.uix.boxlayout import BoxLayout from kivy.uix.floatlayout import FloatLayout from kivy.uix.relativelayout import RelativeLayout from kivy.uix.popup import Popup from kivy.properties import ObjectProperty from kivy.uix.textinput import TextInput from kivy.lang import Builder from kivy.logger import Logger from kivy.clock import Clock from distutils.version import LooseVersion _mpl_ge_1_5 = LooseVersion(matplotlib.__version__) >= LooseVersion('1.5.0') _mpl_ge_2_0 = LooseVersion(matplotlib.__version__) >= LooseVersion('2.0.0') import numpy as np import io import textwrap import uuid import numbers from functools import partial from math import cos, sin, pi kivy.require('1.9.1') toolbar = None my_canvas = None class SaveDialog(FloatLayout): save = ObjectProperty(None) text_input = ObjectProperty(None) cancel = ObjectProperty(None) class MPLKivyApp(App): '''Creates the App initializing a FloatLayout with a figure and toolbar widget. ''' figure = ObjectProperty(None) toolbar = ObjectProperty(None) def build(self): EventLoop.ensure_window() layout = FloatLayout() if self.figure: self.figure.size_hint_y = 0.9 layout.add_widget(self.figure) if self.toolbar: self.toolbar.size_hint_y = 0.1 layout.add_widget(self.toolbar) return layout def draw_if_interactive(): '''Handle whether or not the backend is in interactive mode or not. ''' if matplotlib.is_interactive(): figManager = Gcf.get_active() if figManager: figManager.canvas.draw_idle() class Show(ShowBase): '''mainloop needs to be overwritten to define the show() behavior for kivy framework. ''' def mainloop(self): app = App.get_running_app() if app is None: app = MPLKivyApp(figure=my_canvas, toolbar=toolbar) app.run() show = Show() def new_figure_manager(num, *args, **kwargs): '''Create a new figure manager instance for the figure given. ''' # if a main-level app must be created, this (and # new_figure_manager_given_figure) is the usual place to # do it -- see backend_wx, backend_wxagg and backend_tkagg for # examples. Not all GUIs require explicit instantiation of a # main-level app (egg backend_gtk, backend_gtkagg) for pylab FigureClass = kwargs.pop('FigureClass', Figure) thisFig = FigureClass(*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) global my_canvas global toolbar toolbar = manager.toolbar.actionbar if manager.toolbar else None my_canvas = canvas return manager class RendererKivy(RendererBase): '''The kivy renderer handles drawing/rendering operations. A RendererKivy should be initialized with a FigureCanvasKivy widget. On initialization a MathTextParser is instantiated to generate math text inside a FigureCanvasKivy widget. Additionally a list to store clip_rectangles is defined for elements that need to be clipped inside a rectangle such as axes. The rest of the render is performed using kivy graphics instructions. ''' def __init__(self, widget): super(RendererKivy, self).__init__() self.widget = widget self.dpi = widget.figure.dpi self._markers = {} # Can be enhanced by using TextToPath matplotlib, textpath.py self.mathtext_parser = MathTextParser("Bitmap") self.list_goraud_triangles = [] self.clip_rectangles = [] self.labels_inside_plot = [] def contains(self, widget, x, y): '''Returns whether or not a point is inside the widget. The value of the point is defined in x, y as kivy coordinates. ''' left = widget.x bottom = widget.y top = widget.y + widget.height right = widget.x + widget.width return (left <= x <= right and bottom <= y <= top) def handle_clip_rectangle(self, gc, x, y): '''It checks whether the point (x,y) collides with any already existent stencil. If so it returns the index position of the stencil it collides with. if the new clip rectangle bounds are None it draws in the canvas otherwise it finds the correspondent stencil or creates a new one for the new graphics instructions. The point x,y is given in matplotlib coordinates. ''' x = self.widget.x + x y = self.widget.y + y collides = self.collides_with_existent_stencil(x, y) if collides > -1: return collides new_bounds = gc.get_clip_rectangle() if new_bounds: x = self.widget.x + int(new_bounds.bounds[0]) y = self.widget.y + int(new_bounds.bounds[1]) w = int(new_bounds.bounds[2]) h = int(new_bounds.bounds[3]) collides = self.collides_with_existent_stencil(x, y) if collides == -1: cliparea = StencilView(pos=(x, y), size=(w, h)) self.clip_rectangles.append(cliparea) self.widget.add_widget(cliparea) return len(self.clip_rectangles) - 1 else: return collides else: return -2 def draw_path_collection(self, gc, master_transform, paths, all_transforms, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position): '''Draws a collection of paths selecting drawing properties from the lists *facecolors*, *edgecolors*, *linewidths*, *linestyles* and *antialiaseds*. *offsets* is a list of offsets to apply to each of the paths. The offsets in *offsets* are first transformed by *offsetTrans* before being applied. *offset_position* may be either "screen" or "data" depending on the space that the offsets are in. ''' len_path = len(paths[0].vertices) if len(paths) > 0 else 0 uses_per_path = self._iter_collection_uses_per_path( paths, all_transforms, offsets, facecolors, edgecolors) # check whether an optimization is needed by calculating the cost of # generating and use a path with the cost of emitting a path in-line. should_do_optimization = \ len_path + uses_per_path + 5 < len_path * uses_per_path if not should_do_optimization: return RendererBase.draw_path_collection( self, gc, master_transform, paths, all_transforms, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position) # Generate an array of unique paths with the respective transformations path_codes = [] for i, (path, transform) in enumerate(self._iter_collection_raw_paths( master_transform, paths, all_transforms)): transform = Affine2D(transform.get_matrix()).scale(1.0, -1.0) if _mpl_ge_2_0: polygons = path.to_polygons(transform, closed_only=False) else: polygons = path.to_polygons(transform) path_codes.append(polygons) # Apply the styles and rgbFace to each one of the raw paths from # the list. Additionally a transformation is being applied to # translate each independent path for xo, yo, path_poly, gc0, rgbFace in self._iter_collection( gc, master_transform, all_transforms, path_codes, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position): list_canvas_instruction = self.get_path_instructions(gc0, path_poly, closed=True, rgbFace=rgbFace) for widget, instructions in list_canvas_instruction: widget.canvas.add(PushMatrix()) widget.canvas.add(Translate(xo, yo)) widget.canvas.add(instructions) widget.canvas.add(PopMatrix()) def collides_with_existent_stencil(self, x, y): '''Check all the clipareas and returns the index of the clip area that contains this point. The point x, y is given in kivy coordinates. ''' idx = -1 for cliparea in self.clip_rectangles: idx += 1 if self.contains(cliparea, x, y): return idx return -1 def get_path_instructions(self, gc, polygons, closed=False, rgbFace=None): '''With a graphics context and a set of polygons it returns a list of InstructionGroups required to render the path. ''' instructions_list = [] points_line = [] for polygon in polygons: for x, y in polygon: x = x + self.widget.x y = y + self.widget.y points_line += [float(x), float(y), ] tess = Tesselator() tess.add_contour(points_line) if not tess.tesselate(): Logger.warning("Tesselator didn't work :(") return newclip = self.handle_clip_rectangle(gc, x, y) if newclip > -1: instructions_list.append((self.clip_rectangles[newclip], self.get_graphics(gc, tess, points_line, rgbFace, closed=closed))) else: instructions_list.append((self.widget, self.get_graphics(gc, tess, points_line, rgbFace, closed=closed))) return instructions_list def get_graphics(self, gc, polygons, points_line, rgbFace, closed=False): '''Return an instruction group which contains the necessary graphics instructions to draw the respective graphics. ''' instruction_group = InstructionGroup() if isinstance(gc.line['dash_list'], tuple): gc.line['dash_list'] = list(gc.line['dash_list']) if rgbFace is not None: if len(polygons.meshes) != 0: instruction_group.add(Color(*rgbFace)) for vertices, indices in polygons.meshes: instruction_group.add(Mesh( vertices=vertices, indices=indices, mode=str("triangle_fan") )) instruction_group.add(Color(*gc.get_rgb())) if _mpl_ge_1_5 and (not _mpl_ge_2_0) and closed: points_poly_line = points_line[:-2] else: points_poly_line = points_line if gc.line['width'] > 0: instruction_group.add(Line(points=points_poly_line, width=int(gc.line['width'] / 2), dash_length=gc.line['dash_length'], dash_offset=gc.line['dash_offset'], dash_joint=gc.line['join_style'], dash_list=gc.line['dash_list'])) return instruction_group def draw_image(self, gc, x, y, im): '''Render images that can be displayed on a matplotlib figure. These images are generally called using imshow method from pyplot. A Texture is applied to the FigureCanvas. The position x, y is given in matplotlib coordinates. ''' # Clip path to define an area to mask. clippath, clippath_trans = gc.get_clip_path() # Normal coordinates calculated and image added. x = self.widget.x + x y = self.widget.y + y bbox = gc.get_clip_rectangle() if bbox is not None: l, b, w, h = bbox.bounds else: l = 0 b = 0 w = self.widget.width h = self.widget.height h, w = im.get_size_out() rows, cols, image_str = im.as_rgba_str() texture = Texture.create(size=(w, h)) texture.blit_buffer(image_str, colorfmt='rgba', bufferfmt='ubyte') if clippath is None: with self.widget.canvas: Color(1.0, 1.0, 1.0, 1.0) Rectangle(texture=texture, pos=(x, y), size=(w, h)) else: if _mpl_ge_2_0: polygons = clippath.to_polygons(clippath_trans, closed_only=False) else: polygons = clippath.to_polygons(clippath_trans) list_canvas_instruction = self.get_path_instructions(gc, polygons, rgbFace=(1.0, 1.0, 1.0, 1.0)) for widget, instructions in list_canvas_instruction: widget.canvas.add(StencilPush()) widget.canvas.add(instructions) widget.canvas.add(StencilUse()) widget.canvas.add(Color(1.0, 1.0, 1.0, 1.0)) widget.canvas.add(Rectangle(texture=texture, pos=(x, y), size=(w, h))) widget.canvas.add(StencilUnUse()) widget.canvas.add(StencilPop()) def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): '''Render text that is displayed in the canvas. The position x, y is given in matplotlib coordinates. A `GraphicsContextKivy` is given to render according to the text properties such as color, size, etc. An angle is given to change the orientation of the text when needed. If the text is a math expression it will be rendered using a MathText parser. ''' if mtext: transform = mtext.get_transform() ax, ay = transform.transform_point(mtext.get_position()) angle_rad = mtext.get_rotation() * np.pi / 180. dir_vert = np.array([np.sin(angle_rad), np.cos(angle_rad)]) if mtext.get_rotation_mode() == "anchor": # if anchor mode, rotation is undone first v_offset = np.dot(dir_vert, [(x - ax), (y - ay)]) ax = ax + v_offset * dir_vert[0] ay = ay + v_offset * dir_vert[1] w, h, d = self.get_text_width_height_descent(s, prop, ismath) ha, va = mtext.get_ha(), mtext.get_va() if ha == "center": ax -= w / 2 elif ha == "right": ax -= w if va == "top": ay -= h elif va == "center": ay -= h / 2 if mtext.get_rotation_mode() != "anchor": # if not anchor mode, rotation is undone last v_offset = np.dot(dir_vert, [(x - ax), (y - ay)]) ax = ax + v_offset * dir_vert[0] ay = ay + v_offset * dir_vert[1] x, y = ax, ay x += self.widget.x y += self.widget.y if ismath: self.draw_mathtext(gc, x, y, s, prop, angle) else: font = resource_find(prop.get_name() + ".ttf") color = gc.get_rgb() if font is None: plot_text = CoreLabel(font_size=prop.get_size_in_points(), color=color) else: plot_text = CoreLabel(font_size=prop.get_size_in_points(), font_name=prop.get_name(), color=color) plot_text.text = six.text_type("{}".format(s)) if prop.get_style() == 'italic': plot_text.italic = True if self.weight_as_number(prop.get_weight()) > 500: plot_text.bold = True plot_text.refresh() with self.widget.canvas: if isinstance(angle, float): PushMatrix() Rotate(angle=angle, origin=(int(x), int(y))) Rectangle(pos=(int(x), int(y)), texture=plot_text.texture, size=plot_text.texture.size) PopMatrix() else: Rectangle(pos=(int(x), int(y)), texture=plot_text.texture, size=plot_text.texture.size) def draw_mathtext(self, gc, x, y, s, prop, angle): '''Draw the math text using matplotlib.mathtext. The position x,y is given in Kivy coordinates. ''' ftimage, depth = self.mathtext_parser.parse(s, self.dpi, prop) w = ftimage.get_width() h = ftimage.get_height() texture = Texture.create(size=(w, h)) if _mpl_ge_1_5: texture.blit_buffer(ftimage.as_rgba_str()[0][0], colorfmt='rgba', bufferfmt='ubyte') else: texture.blit_buffer(ftimage.as_rgba_str(), colorfmt='rgba', bufferfmt='ubyte') texture.flip_vertical() with self.widget.canvas: Rectangle(texture=texture, pos=(x, y), size=(w, h)) def draw_path(self, gc, path, transform, rgbFace=None): '''Produce the rendering of the graphics elements using :class:`kivy.graphics.Line` and :class:`kivy.graphics.Mesh` kivy graphics instructions. The paths are converted into polygons and assigned either to a clip rectangle or to the same canvas for rendering. Paths are received in matplotlib coordinates. The aesthetics is defined by the `GraphicsContextKivy` gc. ''' if _mpl_ge_2_0: polygons = path.to_polygons(transform, self.widget.width, self.widget.height, closed_only=False) else: polygons = path.to_polygons(transform, self.widget.width, self.widget.height) list_canvas_instruction = self.get_path_instructions(gc, polygons, closed=True, rgbFace=rgbFace) for widget, instructions in list_canvas_instruction: widget.canvas.add(instructions) def draw_markers(self, gc, marker_path, marker_trans, path, trans, rgbFace=None): '''Markers graphics instructions are stored on a dictionary and hashed through graphics context and rgbFace values. If a marker_path with the corresponding graphics context exist then the instructions are pulled from the markers dictionary. ''' if not len(path.vertices): return # get a string representation of the path path_data = self._convert_path( marker_path, marker_trans + Affine2D().scale(1.0, -1.0), simplify=False) # get a string representation of the graphics context and rgbFace. style = str(gc._get_style_dict(rgbFace)) dictkey = (path_data, str(style)) # check whether this marker has been created before. list_instructions = self._markers.get(dictkey) # creating a list of instructions for the specific marker. if list_instructions is None: if _mpl_ge_2_0: polygons = marker_path.to_polygons(marker_trans, closed_only=False) else: polygons = marker_path.to_polygons(marker_trans) self._markers[dictkey] = self.get_path_instructions(gc, polygons, rgbFace=rgbFace) # Traversing all the positions where a marker should be rendered for vertices, codes in path.iter_segments(trans, simplify=False): if len(vertices): x, y = vertices[-2:] for widget, instructions in self._markers[dictkey]: widget.canvas.add(PushMatrix()) widget.canvas.add(Translate(x, y)) widget.canvas.add(instructions) widget.canvas.add(PopMatrix()) def flipy(self): return False def _convert_path(self, path, transform=None, clip=None, simplify=None, sketch=None): if clip: clip = (0.0, 0.0, self.width, self.height) else: clip = None if _mpl_ge_1_5: return _path.convert_to_string( path, transform, clip, simplify, sketch, 6, [b'M', b'L', b'Q', b'C', b'z'], False).decode('ascii') else: return _path.convert_to_svg(path, transform, clip, simplify, 6) def get_canvas_width_height(self): '''Get the actual width and height of the widget. ''' return self.widget.width, self.widget.height def get_text_width_height_descent(self, s, prop, ismath): '''This method is needed specifically to calculate text positioning in the canvas. Matplotlib needs the size to calculate the points according to their layout ''' if ismath: ftimage, depth = self.mathtext_parser.parse(s, self.dpi, prop) w = ftimage.get_width() h = ftimage.get_height() return w, h, depth font = resource_find(prop.get_name() + ".ttf") if font is None: plot_text = CoreLabel(font_size=prop.get_size_in_points()) else: plot_text = CoreLabel(font_size=prop.get_size_in_points(), font_name=prop.get_name()) plot_text.text = six.text_type("{}".format(s)) plot_text.refresh() return plot_text.texture.size[0], plot_text.texture.size[1], 1 def new_gc(self): '''Instantiate a GraphicsContextKivy object ''' return GraphicsContextKivy(self.widget) def points_to_pixels(self, points): return points / 72.0 * self.dpi def weight_as_number(self, weight): ''' Replaces the deprecated matplotlib function of the same name ''' # Return if number if isinstance(weight, numbers.Number): return weight # else use the mapping of matplotlib 2.2 elif weight == 'ultralight': return 100 elif weight == 'light': return 200 elif weight == 'normal': return 400 elif weight == 'regular': return 400 elif weight == 'book': return 500 elif weight == 'medium': return 500 elif weight == 'roman': return 500 elif weight == 'semibold': return 600 elif weight == 'demibold': return 600 elif weight == 'demi': return 600 elif weight == 'bold': return 700 elif weight == 'heavy': return 800 elif weight == 'extra bold': return 800 elif weight == 'black': return 900 else: raise ValueError('weight ' + weight + ' not valid') class NavigationToolbar2Kivy(NavigationToolbar2): '''This class extends from matplotlib class NavigationToolbar2 and creates an action bar which is added to the main app to allow the following operations to the figures. Home: Resets the plot axes to the initial state. Left: Undo an operation performed. Right: Redo an operation performed. Pan: Allows to drag the plot. Zoom: Allows to define a rectangular area to zoom in. Configure: Loads a pop up for repositioning elements. Save: Loads a Save Dialog to generate an image. ''' def __init__(self, canvas, **kwargs): self.actionbar = ActionBar(pos_hint={'top': 1.0}) super(NavigationToolbar2Kivy, self).__init__(canvas) self.rubberband_color = (1.0, 0.0, 0.0, 1.0) self.lastrect = None self.save_dialog = Builder.load_string(textwrap.dedent('''\ <SaveDialog>: text_input: text_input BoxLayout: size: root.size pos: root.pos orientation: "vertical" FileChooserListView: id: filechooser on_selection: text_input.text = self.selection and\ self.selection[0] or '' TextInput: id: text_input size_hint_y: None height: 30 multiline: False BoxLayout: size_hint_y: None height: 30 Button: text: "Cancel" on_release: root.cancel() Button: text: "Save" on_release: root.save(filechooser.path,\ text_input.text) ''')) def _init_toolbar(self): '''A Toolbar is created with an ActionBar widget in which buttons are added with a specific behavior given by a callback. The buttons properties are given by matplotlib. ''' basedir = os.path.join(rcParams['datapath'], 'images') actionview = ActionView() actionprevious = ActionPrevious(title="Navigation", with_previous=False) actionoverflow = ActionOverflow() actionview.add_widget(actionprevious) actionview.add_widget(actionoverflow) actionview.use_separator = True self.actionbar.add_widget(actionview) id_group = uuid.uuid4() for text, tooltip_text, image_file, callback in self.toolitems: if text is None: actionview.add_widget(ActionSeparator()) continue fname = os.path.join(basedir, image_file + '.png') if text in ['Pan', 'Zoom']: action_button = ActionToggleButton(text=text, icon=fname, group=id_group) else: action_button = ActionButton(text=text, icon=fname) action_button.bind(on_press=getattr(self, callback)) actionview.add_widget(action_button) def configure_subplots(self, *largs): '''It will be implemented later.''' pass def dismiss_popup(self): self._popup.dismiss() def show_save(self): '''Displays a popup widget to perform a save operation.''' content = SaveDialog(save=self.save, cancel=self.dismiss_popup) self._popup = Popup(title="Save file", content=content, size_hint=(0.9, 0.9)) self._popup.open() def save(self, path, filename): self.canvas.export_to_png(os.path.join(path, filename)) self.dismiss_popup() def save_figure(self, *args): self.show_save() def draw_rubberband(self, event, x0, y0, x1, y1): w = abs(x1 - x0) h = abs(y1 - y0) rect = [int(val)for val in (min(x0, x1) + self.canvas.x, min(y0, y1) + self.canvas.y, w, h)] if self.lastrect is None: self.canvas.canvas.add(Color(*self.rubberband_color)) else: self.canvas.canvas.remove(self.lastrect) self.lastrect = InstructionGroup() self.lastrect.add(Line(rectangle=rect, width=1.0, dash_length=5.0, dash_offset=5.0)) self.lastrect.add(Color(1.0, 0.0, 0.0, 0.2)) self.lastrect.add(Rectangle(pos=(rect[0], rect[1]), size=(rect[2], rect[3]))) self.canvas.canvas.add(self.lastrect) def release_zoom(self, event): self.lastrect = None return super(NavigationToolbar2Kivy, self).release_zoom(event) class GraphicsContextKivy(GraphicsContextBase, object): '''The graphics context provides the color, line styles, etc... All the mapping between matplotlib and kivy styling is done here. The GraphicsContextKivy stores colors as a RGB tuple on the unit interval, e.g., (0.5, 0.0, 1.0) such as in the Kivy framework. Lines properties and styles are set accordingly to the kivy framework definition for Line. ''' _capd = { 'butt': 'square', 'projecting': 'square', 'round': 'round', } line = {} def __init__(self, renderer): super(GraphicsContextKivy, self).__init__() self.renderer = renderer self.line['cap_style'] = self.get_capstyle() self.line['join_style'] = self.get_joinstyle() self.line['dash_offset'] = None self.line['dash_length'] = None self.line['dash_list'] = [] def set_capstyle(self, cs): '''Set the cap style based on the kivy framework cap styles. ''' GraphicsContextBase.set_capstyle(self, cs) self.line['cap_style'] = self._capd[self._capstyle] def set_joinstyle(self, js): '''Set the join style based on the kivy framework join styles. ''' GraphicsContextBase.set_joinstyle(self, js) self.line['join_style'] = js def set_dashes(self, dash_offset, dash_list): GraphicsContextBase.set_dashes(self, dash_offset, dash_list) # dash_list is a list with numbers denoting the number of points # in a dash and if it is on or off. if dash_list is not None: self.line['dash_list'] = dash_list if dash_offset is not None: self.line['dash_offset'] = int(dash_offset) def set_linewidth(self, w): GraphicsContextBase.set_linewidth(self, w) self.line['width'] = w def _get_style_dict(self, rgbFace): '''Return the style string. style is generated from the GraphicsContext and rgbFace ''' attrib = {} forced_alpha = self.get_forced_alpha() if rgbFace is None: attrib['fill'] = 'none' else: if tuple(rgbFace[:3]) != (0, 0, 0): attrib['fill'] = str(rgbFace) if len(rgbFace) == 4 and rgbFace[3] != 1.0 and not forced_alpha: attrib['fill-opacity'] = str(rgbFace[3]) if forced_alpha and self.get_alpha() != 1.0: attrib['opacity'] = str(self.get_alpha()) offset, seq = self.get_dashes() if seq is not None: attrib['line-dasharray'] = ','.join(['%f' % val for val in seq]) attrib['line-dashoffset'] = six.text_type(float(offset)) linewidth = self.get_linewidth() if linewidth: rgb = self.get_rgb() attrib['line'] = str(rgb) if not forced_alpha and rgb[3] != 1.0: attrib['line-opacity'] = str(rgb[3]) if linewidth != 1.0: attrib['line-width'] = str(linewidth) if self.get_joinstyle() != 'round': attrib['line-linejoin'] = self.get_joinstyle() if self.get_capstyle() != 'butt': attrib['line-linecap'] = _capd[self.get_capstyle()] return attrib class TimerKivy(TimerBase): ''' Subclass of :class:`backend_bases.TimerBase` that uses Kivy for timer events. Attributes: * interval: The time between timer events in milliseconds. Default is 1000 ms. * single_shot: Boolean flag indicating whether this timer should operate as single shot (run once and then stop). Defaults to False. * callbacks: Stores list of (func, args) tuples that will be called upon timer events. This list can be manipulated directly, or the functions add_callback and remove_callback can be used. ''' def _timer_start(self): # Need to stop it, otherwise we potentially leak a timer id that will # never be stopped. self._timer_stop() self._timer = Clock.schedule_interval(self._on_timer, self._interval / 1000.0) def _timer_stop(self): if self._timer is not None: Clock.unschedule(self._timer) self._timer = None def _timer_set_interval(self): # Only stop and restart it if the timer has already been started if self._timer is not None: self._timer_stop() self._timer_start() def _on_timer(self, dt): super(TimerKivy, self)._on_timer() class FigureCanvasKivy(FocusBehavior, Widget, FigureCanvasBase): '''FigureCanvasKivy class. See module documentation for more information. ''' def __init__(self, figure, **kwargs): Window.bind(mouse_pos=self._on_mouse_pos) self.bind(size=self._on_size_changed) self.bind(pos=self._on_pos_changed) self.entered_figure = True self.figure = figure super(FigureCanvasKivy, self).__init__(figure=self.figure, **kwargs) def draw(self): '''Draw the figure using the KivyRenderer ''' self.clear_widgets() self.canvas.clear() self._renderer = RendererKivy(self) self.figure.draw(self._renderer) def on_touch_down(self, touch): '''Kivy Event to trigger the following matplotlib events: `motion_notify_event`, `scroll_event`, `button_press_event`, `enter_notify_event` and `leave_notify_event` ''' newcoord = self.to_widget(touch.x, touch.y, relative=True) x = newcoord[0] y = newcoord[1] if super(FigureCanvasKivy, self).on_touch_down(touch): return True if self.collide_point(*touch.pos): self.motion_notify_event(x, y, guiEvent=None) touch.grab(self) if 'button' in touch.profile and touch.button in ("scrollup", "scrolldown",): self.scroll_event(x, y, 5, guiEvent=None) else: self.button_press_event(x, y, self.get_mouse_button(touch), dblclick=False, guiEvent=None) if self.entered_figure: self.enter_notify_event(guiEvent=None, xy=None) else: if not self.entered_figure: self.leave_notify_event(guiEvent=None) return False def on_touch_move(self, touch): '''Kivy Event to trigger the following matplotlib events: `motion_notify_event`, `enter_notify_event` and `leave_notify_event` ''' newcoord = self.to_widget(touch.x, touch.y, relative=True) x = newcoord[0] y = newcoord[1] inside = self.collide_point(touch.x, touch.y) if inside: self.motion_notify_event(x, y, guiEvent=None) if not inside and not self.entered_figure: self.leave_notify_event(guiEvent=None) self.entered_figure = True elif inside and self.entered_figure: self.enter_notify_event(guiEvent=None, xy=(x, y)) self.entered_figure = False return False def get_mouse_button(self, touch): '''Translate kivy convention for left, right and middle click button into matplotlib int values: 1 for left, 2 for middle and 3 for right. ''' if 'button' in touch.profile: if touch.button == "left": return 1 elif touch.button == "middle": return 2 elif touch.button == "right": return 3 return -1 def on_touch_up(self, touch): '''Kivy Event to trigger the following matplotlib events: `scroll_event` and `button_release_event`. ''' newcoord = self.to_widget(touch.x, touch.y, relative=True) x = newcoord[0] y = newcoord[1] if touch.grab_current is self: if 'button' in touch.profile and touch.button in ("scrollup", "scrolldown",): self.scroll_event(x, y, 5, guiEvent=None) else: self.button_release_event(x, y, self.get_mouse_button(touch), guiEvent=None) touch.ungrab(self) else: return super(FigureCanvasKivy, self).on_touch_up(touch) return False def keyboard_on_key_down(self, window, keycode, text, modifiers): '''Kivy event to trigger matplotlib `key_press_event`. ''' self.key_press_event(keycode[1], guiEvent=None) return super(FigureCanvasKivy, self).keyboard_on_key_down(window, keycode, text, modifiers) def keyboard_on_key_up(self, window, keycode): '''Kivy event to trigger matplotlib `key_release_event`. ''' self.key_release_event(keycode[1], guiEvent=None) return super(FigureCanvasKivy, self).keyboard_on_key_up(window, keycode) def _on_mouse_pos(self, *args): '''Kivy Event to trigger the following matplotlib events: `motion_notify_event`, `leave_notify_event` and `enter_notify_event`. ''' pos = args[1] newcoord = self.to_widget(pos[0], pos[1], relative=True) x = newcoord[0] y = newcoord[1] inside = self.collide_point(*pos) if inside: self.motion_notify_event(x, y, guiEvent=None) if not inside and not self.entered_figure: self.leave_notify_event(guiEvent=None) self.entered_figure = True elif inside and self.entered_figure: self.enter_notify_event(guiEvent=None, xy=(pos[0], pos[1])) self.entered_figure = False def enter_notify_event(self, guiEvent=None, xy=None): event = Event('figure_enter_event', self, guiEvent) self.callbacks.process('figure_enter_event', event) def leave_notify_event(self, guiEvent=None): event = Event('figure_leave_event', self, guiEvent) self.callbacks.process('figure_leave_event', event) def _on_pos_changed(self, *args): self.draw() def _on_size_changed(self, *args): '''Changes the size of the matplotlib figure based on the size of the widget. The widget will change size according to the parent Layout size. ''' w, h = self.size dpival = self.figure.dpi winch = float(w) / dpival hinch = float(h) / dpival self.figure.set_size_inches(winch, hinch, forward=False) self.resize_event() self.draw() def callback(self, *largs): self.draw() def blit(self, bbox=None): '''If bbox is None, blit the entire canvas to the widget. Otherwise blit only the area defined by the bbox. ''' self.blitbox = bbox filetypes = FigureCanvasBase.filetypes.copy() filetypes['png'] = 'Portable Network Graphics' def print_png(self, filename, *args, **kwargs): '''Call the widget function to make a png of the widget. ''' fig = FigureCanvasAgg(self.figure) FigureCanvasAgg.draw(fig) l, b, w, h = self.figure.bbox.bounds texture = Texture.create(size=(w, h)) texture.blit_buffer(bytes(fig.get_renderer().buffer_rgba()), colorfmt='rgba', bufferfmt='ubyte') texture.flip_vertical() img = Image(texture) img.save(filename) def get_default_filetype(self): return 'png' def new_timer(self, *args, **kwargs): """ Creates a new backend-specific subclass of :class:`backend_bases.Timer`. This is useful for getting periodic events through the backend's native event loop. Implemented only for backends with GUIs. optional arguments: *interval* Timer interval in milliseconds *callbacks* Sequence of (func, args, kwargs) where func(*args, **kwargs) will be executed by the timer every *interval*. """ return TimerKivy(*args, **kwargs) class FigureManagerKivy(FigureManagerBase): '''The FigureManager main function is to instantiate the backend navigation toolbar and to call show to instantiate the App. ''' def __init__(self, canvas, num): super(FigureManagerKivy, self).__init__(canvas, num) self.canvas = canvas self.toolbar = self._get_toolbar() def show(self): pass def get_window_title(self): return Window.title def set_window_title(self, title): Window.title = title def resize(self, w, h): if (w > 0) and (h > 0): Window.size = w, h def _get_toolbar(self): if rcParams['toolbar'] == 'toolbar2': toolbar = NavigationToolbar2Kivy(self.canvas) else: toolbar = None return toolbar '''Now just provide the standard names that backend.__init__ is expecting ''' FigureCanvas = FigureCanvasKivy FigureManager = FigureManagerKivy NavigationToolbar = NavigationToolbar2Kivy