-
Notifications
You must be signed in to change notification settings - Fork 50
/
backend_kivy.py
1314 lines (1135 loc) · 49.7 KB
/
backend_kivy.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
'''
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 _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