mp-007 / kivy_matplotlib_widget

A fast matplotlib rendering for Kivy based on Kivy_matplotlib project and kivy scatter. Matplotlib used 'agg' backend
MIT License
33 stars 6 forks source link

Add Scroll Limiters #18

Closed GvozdevLeonid closed 5 months ago

GvozdevLeonid commented 1 year ago

class MatplotFigure(Widget): stop_x_scroll = ListProperty(None, allownone=True) stop_y_scroll = ListProperty(None, allownone=True)

self.appy_pan(...):
......
            cur_xlim -= dx/2
            if self.stop_x_scroll is not None:
                if cur_xlim[0] > self.stop_x_scroll[0] and cur_xlim[1] < self.stop_x_scroll[1]:
                    if inverted_x:
                        ax.set_xlim(cur_xlim[1],cur_xlim[0])
                    else:
                        ax.set_xlim(cur_xlim)
            else:
                if inverted_x:
                    ax.set_xlim(cur_xlim[1],cur_xlim[0])
                else:
                    ax.set_xlim(cur_xlim)

And same with Y axis

mp-007 commented 1 year ago

Maybe you can rewrite apply_pan function in your case and create a custom matplotlib graph

""" Custom  pan for MatplotFigure 
"""

import matplotlib
matplotlib.use('Agg')
from kivy_matplotlib_widget.uix.graph_widget import MatplotFigure 
from kivy.properties import ListProperty

class MatplotFigureCustomPan(MatplotFigure):
    """Custom pan for MatplotFigure
    """
    stop_x_scroll = ListProperty(None, allownone=True)
    stop_y_scroll = ListProperty(None, allownone=True)

    def __init__(self, **kwargs):
        super(MatplotFigureCustomPan, self).__init__(**kwargs)

    def apply_pan(self, ax, event, mode='pan'):
        """ pan method """

        trans = ax.transData.inverted()
        xdata, ydata = trans.transform_point((event.x-self.pos[0], event.y-self.pos[1]))
        xpress, ypress = trans.transform_point((self._last_touch_pos[event][0]-self.pos[0], self._last_touch_pos[event][1]-self.pos[1]))
        dx = xdata - xpress
        dy = ydata - ypress

        xleft,xright=self.axes.get_xlim()
        ybottom,ytop=self.axes.get_ylim()

        #check inverted data
        inverted_x = False
        if xleft>xright:
            inverted_x=True
            cur_xlim=(xright,xleft)
        else:
            cur_xlim=(xleft,xright)
        inverted_y = False
        if ybottom>ytop:
            inverted_y=True 
            cur_ylim=(ytop,ybottom)
        else:
            cur_ylim=(ybottom,ytop) 

        if self.interactive_axis and self.touch_mode=='pan' and not self.first_touch_pan=='pan':
            if (ydata < cur_ylim[0] and not inverted_y) or (ydata > cur_ylim[1] and inverted_y):
                left_anchor_zone= (cur_xlim[1] - cur_xlim[0])*.2 + cur_xlim[0]
                right_anchor_zone= (cur_xlim[1] - cur_xlim[0])*.8 + cur_xlim[0]
                if xdata < left_anchor_zone or xdata > right_anchor_zone:
                    mode = 'adjust_x'
                else:
                    mode = 'pan_x'
                self.touch_mode = mode
            elif (xdata < cur_xlim[0] and not inverted_x) or (xdata > cur_xlim[1] and inverted_x):
                bottom_anchor_zone=  (cur_ylim[1] - cur_ylim[0])*.2 + cur_ylim[0]
                top_anchor_zone= (cur_ylim[1] - cur_ylim[0])*.8 + cur_ylim[0]               
                if ydata < bottom_anchor_zone or ydata > top_anchor_zone:
                    mode = 'adjust_y'
                else:
                    mode= 'pan_y' 
                self.touch_mode = mode
            else:
                self.touch_mode = 'pan'

        if not mode=='pan_y' and not mode=='adjust_y':             
            if mode=='adjust_x':
                if self.anchor_x is None:
                    midpoint= (cur_xlim[1] + cur_xlim[0])/2
                    if xdata>midpoint:
                        self.anchor_x='left'
                    else:
                        self.anchor_x='right'
                if self.anchor_x=='left':                
                    if xdata> cur_xlim[0]:
                        cur_xlim -= dx/2
                        if inverted_x:
                            ax.set_xlim(cur_xlim[1],None)
                        else:
                            ax.set_xlim(None,cur_xlim[1])
                else:
                    if xdata< cur_xlim[1]:
                        cur_xlim -= dx/2
                        if inverted_x:
                            ax.set_xlim(None,cur_xlim[0])
                        else:
                            ax.set_xlim(cur_xlim[0],None)
            else:
                cur_xlim -= dx/2
                if self.stop_x_scroll is not None:
                    if cur_xlim[0] > self.stop_x_scroll[0] and cur_xlim[1] < self.stop_x_scroll[1]:
                        if inverted_x:
                            ax.set_xlim(cur_xlim[1],cur_xlim[0])
                        else:
                            ax.set_xlim(cur_xlim)
                else:
                    if inverted_x:
                        ax.set_xlim(cur_xlim[1],cur_xlim[0])
                    else:
                        ax.set_xlim(cur_xlim)

        if not mode=='pan_x' and not mode=='adjust_x':
            if mode=='adjust_y':
                if self.anchor_y is None:
                    midpoint= (cur_ylim[1] + cur_ylim[0])/2
                    if ydata>midpoint:
                        self.anchor_y='top'
                    else:
                        self.anchor_y='bottom'               

                if self.anchor_y=='top':
                    if ydata> cur_ylim[0]:
                        cur_ylim -= dy/2 
                        if inverted_y:
                            ax.set_ylim(cur_ylim[1],None)
                        else:
                            ax.set_ylim(None,cur_ylim[1])
                else:
                    if ydata< cur_ylim[1]:
                        cur_ylim -= dy/2  
                        if inverted_y:
                            ax.set_ylim(None,cur_ylim[0]) 
                        else:
                            ax.set_ylim(cur_ylim[0],None)
            else:            
                cur_ylim -= dy/2
                if inverted_y:
                    ax.set_ylim(cur_ylim[1],cur_ylim[0])
                else:
                    ax.set_ylim(cur_ylim)

        if self.first_touch_pan is None:
            self.first_touch_pan=self.touch_mode

        if self.fast_draw: 
            #use blit method               
            if self.background is None:
                self.background_patch_copy.set_visible(True)
                ax.figure.canvas.draw_idle()
                ax.figure.canvas.flush_events()                   
                self.background = ax.figure.canvas.copy_from_bbox(ax.figure.bbox)
                self.background_patch_copy.set_visible(False)  
            ax.figure.canvas.restore_region(self.background)                

            for line in ax.lines:
                ax.draw_artist(line)

            ax.figure.canvas.blit(ax.bbox)
            ax.figure.canvas.flush_events() 

        else:
            ax.figure.canvas.draw_idle()
            ax.figure.canvas.flush_events()

I don't know your purpose, but for me it's a specific case (not a general feature).

GvozdevLeonid commented 1 year ago

I use it for Spectrum Graph.

This is a very important feature. Sometimes it is very important to disable scrolling and zooming beyond the given limits (the user may lose sight of the data)

And thanks. I created a new widget based on your class, which disables scroll and zoom if additional limits are specified

mp-007 commented 1 year ago

@GvozdevLeonid Feel free to closed this issue if your new widget solve the problem.

I still available if you have any question regarding my code. I'm not good with commenting my code and sometime it can be hard understand what I'm doing.