├── .gitignore ├── README.md ├── __init__.py ├── border.png └── kivy_matplotlib.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | *.py~ 4 | *.zip 5 | *.rar 6 | *.html~ 7 | *.htm~ 8 | *.css~ 9 | !.gitignore 10 | *.profile 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Kivy_Matplotlib 2 | =============== 3 | 4 | Simple widget to display a Matplotlib Figure in kivy. Also a basic toolbar widget to pan and zoom the plot. 5 | **Android/IOS not supported (until matplotlib can be ported to those systems)** 6 | 7 | Usage 8 | ====== 9 | **Basic example** 10 | ```python 11 | import matplotlib as mpl 12 | from kivy_matplotlib import MatplotFigure 13 | from kivy.base import runTouchApp 14 | 15 | # Make plot 16 | fig = mpl.figure.Figure(figsize=(2, 2)) 17 | fig.gca().plot([1, 2, 3]) 18 | 19 | # MatplotFigure (Kivy widget) 20 | fig_kivy = MatplotFigure(fig) 21 | 22 | runTouchApp(fig_kivy) 23 | ``` 24 | 25 | **Example with Toolbar** 26 | 27 | ```python 28 | import matplotlib as mpl 29 | from kivy.app import App 30 | import numpy as np 31 | from kivy.lang import Builder 32 | from kivy_matplotlib import MatplotFigure, MatplotNavToolbar 33 | kv = """ 34 | BoxLayout: 35 | orientation: 'vertical' 36 | 37 | MatplotFigure: 38 | id: figure_wgt 39 | size_hint: 1, 0.7 40 | MatplotNavToolbar: 41 | id: navbar_wgt 42 | size_hint: 1, 0.3 43 | figure_widget: figure_wgt 44 | """ 45 | 46 | class testApp(App): 47 | title = "Test Matplotlib" 48 | 49 | def build(self): 50 | 51 | # Matplotlib stuff, figure and plot 52 | fig = mpl.figure.Figure(figsize=(2, 2)) 53 | t = np.arange(0.0, 100.0, 0.01) 54 | s = np.sin(0.08 * np.pi * t) 55 | axes = fig.gca() 56 | axes.plot(t, s) 57 | axes.set_xlim(0, 50) 58 | axes.grid(True) 59 | 60 | # Kivy stuff 61 | root = Builder.load_string(kv) 62 | figure_wgt = root.ids['figure_wgt'] # MatplotFigure 63 | figure_wgt.figure = fig 64 | 65 | return root 66 | 67 | testApp().run() 68 | ``` 69 | 70 | Requirements 71 | ============ 72 | 73 | - Kivy (>1.8) 74 | - Matplotlib (>1.4) 75 | 76 | Credits 77 | ======= 78 | Jeyson Molina (jeyson.mco at gmail.com) 79 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | from kivy_matplotlib import MatplotFigure, MatplotNavToolbar 2 | -------------------------------------------------------------------------------- /border.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeysonm82/kivy_matplotlib/8d227351193cf43cc868091bc2a44c98a096dcc4/border.png -------------------------------------------------------------------------------- /kivy_matplotlib.py: -------------------------------------------------------------------------------- 1 | """Simple widget to display a matplolib figure in kivy""" 2 | from kivy.uix.widget import Widget 3 | from matplotlib.backends.backend_agg import FigureCanvasAgg 4 | from matplotlib.backend_bases import NavigationToolbar2 5 | from kivy.graphics.texture import Texture 6 | from kivy.properties import ObjectProperty, ListProperty 7 | from kivy.base import EventLoop 8 | from kivy.lang import Builder 9 | from kivy.uix.boxlayout import BoxLayout 10 | import math 11 | 12 | 13 | class MatplotFigure(Widget): 14 | 15 | """Widget to show a matplotlib figure in kivy. 16 | The figure is rendered internally in an AGG backend then 17 | the rgb data is obtained and blitted into a kivy texture. 18 | """ 19 | 20 | figure = ObjectProperty(None) 21 | _box_pos = ListProperty([0, 0]) 22 | _box_size = ListProperty([0, 0]) 23 | _img_texture = ObjectProperty(None) 24 | _bitmap = None 25 | _pressed = False 26 | figcanvas = ObjectProperty(None) 27 | # I Chose composition over MI because of name clashes 28 | 29 | def on_figure(self, obj, value): 30 | self.figcanvas = _FigureCanvas(self.figure, self) 31 | self.figcanvas._isDrawn = False 32 | l, b, w, h = self.figure.bbox.bounds 33 | w = int(math.ceil(w)) 34 | h = int(math.ceil(h)) 35 | self.width = w 36 | self.height = h 37 | 38 | # Texture 39 | self._img_texture = Texture.create(size=(w, h)) 40 | 41 | def __init__(self, figure=None, *args, **kwargs): 42 | super(MatplotFigure, self).__init__(*args, **kwargs) 43 | self.figure = figure 44 | # Event binding 45 | EventLoop.window.bind(mouse_pos=self.on_mouse_move) 46 | self.bind(size=self._onSize) 47 | 48 | def _draw_bitmap(self): 49 | if self._bitmap is None: 50 | print "No bitmap!" 51 | return 52 | self._img_texture = Texture.create(size=(self.bt_w, self.bt_h)) 53 | self._img_texture.blit_buffer( 54 | self._bitmap, colorfmt="rgb", bufferfmt='ubyte') 55 | self._img_texture.flip_vertical() 56 | 57 | def on_mouse_move(self, window, mouse_pos): 58 | """ Mouse move """ 59 | if self._pressed: # Do not process this event if there's a touch_move 60 | return 61 | x, y = mouse_pos 62 | if self.collide_point(x, y): 63 | real_x, real_y = x - self.pos[0], y - self.pos[1] 64 | self.figcanvas.motion_notify_event(x, real_y, guiEvent=None) 65 | 66 | def on_touch_down(self, event): 67 | x, y = event.x, event.y 68 | 69 | if self.collide_point(x, y): 70 | self._pressed = True 71 | real_x, real_y = x - self.pos[0], y - self.pos[1] 72 | self.figcanvas.button_press_event(x, real_y, 1, guiEvent=event) 73 | 74 | def on_touch_move(self, event): 75 | """ Mouse move while pressed """ 76 | x, y = event.x, event.y 77 | if self.collide_point(x, y): 78 | real_x, real_y = x - self.pos[0], y - self.pos[1] 79 | self.figcanvas.motion_notify_event(x, real_y, guiEvent=event) 80 | 81 | def on_touch_up(self, event): 82 | x, y = event.x, event.y 83 | if self._box_size[0] > 1 or self._box_size[1] > 1: 84 | self.reset_box() 85 | if self.collide_point(x, y): 86 | pos_x, pos_y = self.pos 87 | real_x, real_y = x - pos_x, y - pos_y 88 | self.figcanvas.button_release_event(x, real_y, 1, guiEvent=event) 89 | self._pressed = False 90 | 91 | def new_timer(self, *args, **kwargs): 92 | pass # TODO 93 | 94 | def _onSize(self, o, size): 95 | if self.figure is None: 96 | return 97 | # Creat a new, correctly sized bitmap 98 | self._width, self._height = size 99 | self._isDrawn = False 100 | 101 | if self._width <= 1 or self._height <= 1: 102 | return 103 | 104 | dpival = self.figure.dpi 105 | winch = self._width / dpival 106 | hinch = self._height / dpival 107 | self.figure.set_size_inches(winch, hinch) 108 | self.figcanvas.resize_event() 109 | self.figcanvas.draw() 110 | 111 | def reset_box(self): 112 | self._box_size = 0, 0 113 | self._box_pos = 0, 0 114 | 115 | def draw_box(self, event, x0, y0, x1, y1): 116 | pos_x, pos_y = self.pos 117 | # Kivy coords 118 | y0 = pos_y + y0 119 | y1 = pos_y + y1 120 | self._box_pos = x0, y0 121 | self._box_size = x1 - x0, y1 - y0 122 | 123 | 124 | class _FigureCanvas(FigureCanvasAgg): 125 | 126 | """Internal AGG Canvas""" 127 | 128 | def __init__(self, figure, widget, *args, **kwargs): 129 | self.widget = widget 130 | super(_FigureCanvas, self).__init__(figure, *args, **kwargs) 131 | 132 | def draw(self): 133 | """ 134 | Render the figure using agg. 135 | """ 136 | super(_FigureCanvas, self).draw() 137 | agg = self.get_renderer() 138 | w, h = agg.width, agg.height 139 | self._isDrawn = True 140 | 141 | self.widget.bt_w = w 142 | self.widget.bt_h = h 143 | self.widget._bitmap = agg.tostring_rgb() 144 | self.widget._draw_bitmap() 145 | 146 | def blit(self, bbox=None): 147 | # TODO bbox 148 | agg = self.get_renderer() 149 | w, h = agg.width, agg.height 150 | self.widget._bitmap = agg.tostring_rgb() 151 | self.widget.bt_w = w 152 | self.widget.bt_h = h 153 | self.widget._draw_bitmap() 154 | 155 | def print_figure(self, filename, *args, **kwargs): 156 | super(self.print_figure, self).print_figure(filename, *args, **kwargs) 157 | if self._isDrawn: 158 | self.draw() 159 | 160 | 161 | class MatplotNavToolbar(BoxLayout): 162 | 163 | """Figure Toolbar""" 164 | pan_btn = ObjectProperty(None) 165 | zoom_btn = ObjectProperty(None) 166 | home_btn = ObjectProperty(None) 167 | info_lbl = ObjectProperty(None) 168 | _navtoolbar = None # Internal NavToolbar logic 169 | figure_widget = ObjectProperty(None) 170 | 171 | def __init__(self, figure_widget=None, *args, **kwargs): 172 | super(MatplotNavToolbar, self).__init__(*args, **kwargs) 173 | self.figure_widget = figure_widget 174 | 175 | def on_figure_widget(self, obj, value): 176 | self.figure_widget.bind(figcanvas=self._canvas_ready) 177 | 178 | def _canvas_ready(self, obj, value): 179 | self._navtoolbar = _NavigationToolbar(value, self) 180 | self._navtoolbar.figure_widget = obj 181 | 182 | 183 | class _NavigationToolbar(NavigationToolbar2): 184 | figure_widget = None 185 | 186 | def __init__(self, canvas, widget): 187 | self.widget = widget 188 | super(_NavigationToolbar, self).__init__(canvas) 189 | 190 | def _init_toolbar(self): 191 | self.widget.home_btn.bind(on_press=self.home) 192 | self.widget.pan_btn.bind(on_press=self.pan) 193 | self.widget.zoom_btn.bind(on_press=self.zoom) 194 | 195 | def dynamic_update(self): 196 | self.canvas.draw() 197 | 198 | def draw_rubberband(self, event, x0, y0, x1, y1): 199 | self.figure_widget.draw_box(event, x0, y0, x1, y1) 200 | 201 | def set_message(self, s): 202 | self.widget.info_lbl.text = s 203 | 204 | from kivy.factory import Factory 205 | Factory.register('MatplotFigure', MatplotFigure) 206 | Factory.register('MatplotNavToolbar', MatplotNavToolbar) 207 | 208 | Builder.load_string(''' 209 | 210 | canvas: 211 | Color: 212 | rgb: (1, 1, 1) 213 | Rectangle: 214 | pos: self.pos 215 | size: self.size 216 | texture: self._img_texture 217 | Color: 218 | rgba: 1, 0, 0, 0.5 219 | BorderImage: 220 | source: 'border.png' 221 | pos: self._box_pos 222 | size: self._box_size 223 | border: 3, 3, 3, 3 224 | 225 | : 226 | orientation: 'vertical' 227 | home_btn: home_btn 228 | pan_btn: pan_btn 229 | zoom_btn: zoom_btn 230 | info_lbl: info_lbl 231 | 232 | BoxLayout: 233 | size_hint: 1, 0.7 234 | Button: 235 | id: home_btn 236 | text: "Home" 237 | 238 | ToggleButton: 239 | id: pan_btn 240 | text: "Pan" 241 | group: "toolbar_btn" 242 | ToggleButton: 243 | id: zoom_btn 244 | text: "Zoom" 245 | group: "toolbar_btn" 246 | Label: 247 | id: info_lbl 248 | size_hint: 1, 0.3 249 | ''') 250 | 251 | if __name__ == '__main__': 252 | # Example 253 | import matplotlib as mpl 254 | from kivy.app import App 255 | import numpy as np 256 | from kivy.lang import Builder 257 | kv = """ 258 | BoxLayout: 259 | orientation: 'vertical' 260 | 261 | MatplotFigure: 262 | id: figure_wgt 263 | size_hint: 1, 0.7 264 | MatplotNavToolbar: 265 | id: navbar_wgt 266 | size_hint: 1, 0.3 267 | figure_widget: figure_wgt 268 | """ 269 | 270 | class testApp(App): 271 | title = "Test Matplotlib" 272 | 273 | def build(self): 274 | 275 | # Matplotlib stuff, figure and plot 276 | fig = mpl.figure.Figure(figsize=(2, 2)) 277 | t = np.arange(0.0, 100.0, 0.01) 278 | s = np.sin(0.08 * np.pi * t) 279 | axes = fig.gca() 280 | axes.plot(t, s) 281 | axes.set_xlim(0, 50) 282 | axes.grid(True) 283 | 284 | # Kivy stuff 285 | root = Builder.load_string(kv) 286 | figure_wgt = root.ids['figure_wgt'] # MatplotFigure 287 | figure_wgt.figure = fig 288 | 289 | return root 290 | 291 | testApp().run() 292 | --------------------------------------------------------------------------------