1616from kivy .vector import Vector
1717from matplotlib .backends .backend_agg import FigureCanvasAgg
1818from matplotlib import cbook
19+ from matplotlib .colors import to_hex
1920from weakref import WeakKeyDictionary
2021from kivy .metrics import dp
2122import numpy as np
23+ from kivy .utils import get_color_from_hex
24+
2225
2326class MatplotFigure (Widget ):
2427 """Widget to show a matplotlib figure in kivy.
@@ -57,6 +60,7 @@ class MatplotFigure(Widget):
5760 fast_draw = BooleanProperty (True ) #True will don't draw axis
5861 xsorted = BooleanProperty (False ) #to manage x sorted data
5962 minzoom = NumericProperty (dp (40 ))
63+ hover_instance = ObjectProperty (None , allownone = True )
6064
6165 def on_figure (self , obj , value ):
6266 self .figcanvas = _FigureCanvas (self .figure , self )
@@ -130,6 +134,10 @@ def __init__(self, **kwargs):
130134 self .anchor_x = None
131135 self .anchor_y = None
132136
137+ #manage hover data
138+ self .x_hover_data = None
139+ self .y_hover_data = None
140+
133141 #manage back and next event
134142 self ._nav_stack = cbook .Stack ()
135143 self .set_history_buttons ()
@@ -245,20 +253,53 @@ def hover(self, event) -> None:
245253 line = good_line [idx_best ]
246254 self .x_cursor , self .y_cursor = line .get_data ()
247255 x = self .x_cursor [good_index [idx_best ]]
248- y = self .y_cursor [good_index [idx_best ]]
249- self .set_cross_hair_visible (True )
256+ y = self .y_cursor [good_index [idx_best ]]
257+
258+ if not self .hover_instance :
259+ self .set_cross_hair_visible (True )
250260
251261 # update the cursor x,y data
252262 ax = line .axes
253263 self .horizontal_line .set_ydata (y )
254264 self .vertical_line .set_xdata (x )
255265
256266 #x y label
257- if self .cursor_xaxis_formatter :
258- x = self .cursor_xaxis_formatter .format_data (x )
259- if self .cursor_yaxis_formatter :
260- y = self .cursor_yaxis_formatter .format_data (y )
261- self .text .set_text (f"x={ x } , y={ y } " )
267+ if self .hover_instance :
268+ xy_pos = ax .transData .transform ([(x ,y )])
269+ self .x_hover_data = x
270+ self .y_hover_data = y
271+ self .hover_instance .x_hover_pos = float (xy_pos [0 ][0 ]) + self .x
272+ self .hover_instance .y_hover_pos = float (xy_pos [0 ][1 ]) + self .y
273+ self .hover_instance .show_cursor = True
274+
275+ if self .cursor_xaxis_formatter :
276+ x = self .cursor_xaxis_formatter .format_data (x )
277+ if self .cursor_yaxis_formatter :
278+ y = self .cursor_yaxis_formatter .format_data (y )
279+ self .hover_instance .label_x_value = f"{ x } "
280+ self .hover_instance .label_y_value = f"{ y } "
281+
282+ self .hover_instance .ymin_line = float (ax .bbox .bounds [1 ]) + self .y
283+ self .hover_instance .ymax_line = float (ax .bbox .bounds [1 ] + ax .bbox .bounds [3 ]) + self .y
284+
285+ self .hover_instance .custom_label = line .get_label ()
286+ self .hover_instance .custom_color = get_color_from_hex (to_hex (line .get_color ()))
287+
288+ if self .hover_instance .x_hover_pos > self .x + self .axes .bbox .bounds [2 ] + self .axes .bbox .bounds [0 ] or \
289+ self .hover_instance .x_hover_pos < self .x + self .axes .bbox .bounds [0 ] or \
290+ self .hover_instance .y_hover_pos > self .y + self .axes .bbox .bounds [1 ] + self .axes .bbox .bounds [3 ] or \
291+ self .hover_instance .y_hover_pos < self .y + self .axes .bbox .bounds [1 ]:
292+ self .hover_instance .hover_outside_bound = True
293+ else :
294+ self .hover_instance .hover_outside_bound = False
295+
296+ return
297+ else :
298+ if self .cursor_xaxis_formatter :
299+ x = self .cursor_xaxis_formatter .format_data (x )
300+ if self .cursor_yaxis_formatter :
301+ y = self .cursor_yaxis_formatter .format_data (y )
302+ self .text .set_text (f"x={ x } , y={ y } " )
262303
263304 #blit method (always use because same visual effect as draw)
264305 if self .background is None :
@@ -281,6 +322,12 @@ def hover(self, event) -> None:
281322 #if touch is too far, hide cross hair cursor
282323 else :
283324 self .set_cross_hair_visible (False )
325+ if self .hover_instance :
326+ self .hover_instance .x_hover_pos = self .x
327+ self .hover_instance .y_hover_pos = self .y
328+ self .hover_instance .show_cursor = False
329+ self .x_hover_data = None
330+ self .y_hover_data = None
284331
285332 def home (self ) -> None :
286333 """ reset data axis
@@ -411,6 +458,27 @@ def _draw_bitmap(self):
411458 self ._img_texture .blit_buffer (
412459 bytes (self ._bitmap ), colorfmt = "rgba" , bufferfmt = 'ubyte' )
413460 self ._img_texture .flip_vertical ()
461+
462+ if self .hover_instance :
463+ #update hover pos if needed
464+ if self .hover_instance .show_cursor and self .x_hover_data and self .y_hover_data :
465+ xy_pos = self .axes .transData .transform ([(self .x_hover_data ,self .y_hover_data )])
466+ self .hover_instance .x_hover_pos = float (xy_pos [0 ][0 ]) + self .x
467+ self .hover_instance .y_hover_pos = float (xy_pos [0 ][1 ]) + self .y
468+
469+ # ymin,ymax=self.axes.get_ylim()
470+ # ylim_pos = self.axes.transData.transform([(ymin,ymax)])
471+ self .hover_instance .ymin_line = float (self .axes .bbox .bounds [1 ]) + self .y
472+ self .hover_instance .ymax_line = float (self .axes .bbox .bounds [1 ] + self .axes .bbox .bounds [3 ] )+ self .y
473+
474+ if self .hover_instance .x_hover_pos > self .x + self .axes .bbox .bounds [2 ] + self .axes .bbox .bounds [0 ] or \
475+ self .hover_instance .x_hover_pos < self .x + self .axes .bbox .bounds [0 ] or \
476+ self .hover_instance .y_hover_pos > self .y + self .axes .bbox .bounds [1 ] + self .axes .bbox .bounds [3 ] or \
477+ self .hover_instance .y_hover_pos < self .y + self .axes .bbox .bounds [1 ]:
478+ self .hover_instance .hover_outside_bound = True
479+ else :
480+ self .hover_instance .hover_outside_bound = False
481+
414482
415483 def transform_with_touch (self , event ):
416484 """ manage touch behaviour. based on kivy scatter method"""
@@ -489,6 +557,26 @@ def transform_with_touch(self, event):
489557 changed = True
490558 return changed
491559
560+ def on_motion (self ,* args ):
561+ '''Kivy Event to trigger mouse event on motion
562+ `enter_notify_event`.
563+ '''
564+ pos = args [1 ]
565+ newcoord = self .to_widget (pos [0 ], pos [1 ])
566+ x = newcoord [0 ]
567+ y = newcoord [1 ]
568+ inside = self .collide_point (x ,y )
569+ if inside :
570+
571+ # will receive all motion events.
572+ if self .figcanvas and self .hover_instance :
573+ #avoid in motion if touch is detected
574+ if not len (self ._touches )== 0 :
575+ return
576+ FakeEvent .x = x
577+ FakeEvent .y = y
578+ self .hover (FakeEvent )
579+
492580 def on_touch_down (self , event ):
493581 """ Manage Mouse/touch press """
494582 x , y = event .x , event .y
@@ -819,6 +907,8 @@ def _onSize(self, o, size):
819907 self .figcanvas .draw ()
820908 if self .legend_instance :
821909 self .legend_instance .update_size ()
910+ if self .hover_instance :
911+ self .hover_instance .figwidth = self .width
822912
823913 def update_lim (self ):
824914 """ update axis lim if zoombox is used"""
@@ -984,6 +1074,10 @@ def blit(self, bbox=None):
9841074 self .widget .bt_h = h
9851075 self .widget ._draw_bitmap ()
9861076
1077+ class FakeEvent :
1078+ x :None
1079+ y :None
1080+
9871081from kivy .factory import Factory
9881082
9891083Factory .register ('MatplotFigure' , MatplotFigure )
0 commit comments