From 765987dffb6e07bbec0c9d4eb0eb2c8c33349ecb Mon Sep 17 00:00:00 2001 From: mp-007 Date: Tue, 3 Jun 2025 14:10:24 -0400 Subject: [PATCH 1/5] feature velocity pan (x axis only). See example velocity pan. --- examples/example_velocity_pan/main.py | 112 +++++++++ kivy_matplotlib_widget/__init__.py | 2 +- kivy_matplotlib_widget/tools/pan_effects.py | 81 +++++++ kivy_matplotlib_widget/uix/graph_widget.py | 251 ++++++++++++++++++-- 4 files changed, 428 insertions(+), 18 deletions(-) create mode 100644 examples/example_velocity_pan/main.py create mode 100644 kivy_matplotlib_widget/tools/pan_effects.py diff --git a/examples/example_velocity_pan/main.py b/examples/example_velocity_pan/main.py new file mode 100644 index 0000000..de47311 --- /dev/null +++ b/examples/example_velocity_pan/main.py @@ -0,0 +1,112 @@ +"""Basic example for kivy_matplotlib_widget + + This example don't use kivy_matplotlib_widget module in case you want to + incorporate some widgets in your project and you want to customized stuffs. + If you don't need custom used, please install the module with: + + pip install kivy-matplotlib-widget + +Note: + MatplotFigure is used when you have only 1 axis with lines only. + +""" + + +from kivy.utils import platform +from kivy.config import Config + +#avoid conflict between mouse provider and touch (very important with touch device) +#no need for android platform +if platform != 'android': + Config.set('input', 'mouse', 'mouse,disable_on_activity') +else: + #for android, we remove mouse input to not get extra touch + Config.remove_option('input', 'mouse') + +from kivy.lang import Builder +from kivy.app import App + +import matplotlib.pyplot as plt +import numpy as np +import kivy_matplotlib_widget #register all widgets to kivy register + +# Fixing random state for reproducibility +np.random.seed(19680801) + +dt = 0.01 +t = np.arange(0, 10, dt) +nse = np.random.randn(len(t)) +r = np.exp(-t / 0.05) + +cnse = np.convolve(nse, r) * dt +cnse = cnse[:len(t)] +s = 0.1 * np.sin(2 * np.pi * t) + cnse + +KV = ''' + +Screen + figure_wgt:figure_wgt + BoxLayout: + orientation:'vertical' + BoxLayout: + size_hint_y:0.2 + Button: + text:"home" + on_release:app.home() + ToggleButton: + group:'touch_mode' + state:'down' + text:"velocity pan (x axis only)" + on_release: + app.set_touch_mode('pan') + self.state='down' + figure_wgt.velocity_panx=True + ToggleButton: + group:'touch_mode' + text:"normal pan" + on_release: + app.set_touch_mode('pan') + self.state='down' + figure_wgt.velocity_panx=False + ToggleButton: + group:'touch_mode' + text:"zoom box" + on_release: + app.set_touch_mode('zoombox') + self.state='down' + MatplotFigure: + id:figure_wgt + fast_draw:False #recommanded for velocity pan + velocity_panx:True + auto_zoom:True +''' + + +class Test(App): + lines = [] + + def build(self): + self.screen=Builder.load_string(KV) + return self.screen + + def on_start(self, *args): + fig, ax1 = plt.subplots(1, 1) + ax1.plot(t,s) + ax1.set_xlim(t[0],t[50]) + ax1.set_ylim(min(s)-0.05,max(s)+0.05) + + self.screen.figure_wgt.figure = fig + + #set pan only in x + # self.set_touch_mode('pan_x') + + #set x lim (can pan or zoom over these values) + self.screen.figure_wgt.stop_xlimits = [min(t),max(t)] + + def set_touch_mode(self,mode): + self.screen.figure_wgt.touch_mode=mode + + def home(self): + self.screen.figure_wgt.home() + +Test().run() \ No newline at end of file diff --git a/kivy_matplotlib_widget/__init__.py b/kivy_matplotlib_widget/__init__.py index 4b84c44..ce8e9e0 100644 --- a/kivy_matplotlib_widget/__init__.py +++ b/kivy_matplotlib_widget/__init__.py @@ -14,6 +14,6 @@ import kivy_matplotlib_widget.factory_registers # NOQA -__version__ = "0.15.0" +__version__ = "0.16.0" __description__ = "A Matplotlib interactive widget for kivy" __author__ = "mp-007" diff --git a/kivy_matplotlib_widget/tools/pan_effects.py b/kivy_matplotlib_widget/tools/pan_effects.py new file mode 100644 index 0000000..b434449 --- /dev/null +++ b/kivy_matplotlib_widget/tools/pan_effects.py @@ -0,0 +1,81 @@ +from time import time +from kivy.effects.kinetic import KineticEffect +from kivy.uix.widget import Widget +from kivy.properties import NumericProperty, ObjectProperty + + +class PanEffect(KineticEffect): + '''PanEffect class for velocity pan + ''' + + drag_threshold = NumericProperty('20sp') + '''Minimum distance to travel before the movement is considered as a drag. + + :attr:`drag_threshold` is a :class:`~kivy.properties.NumericProperty` and + defaults to 20sp. + ''' + + pan = NumericProperty(0) + '''Computed value for panning. This value is different from + :py:attr:`kivy.effects.kinetic.KineticEffect.value` + in that it will return to one of the min/max bounds. + + :attr:`pan` is a :class:`~kivy.properties.NumericProperty` and defaults + to 0. + ''' + + target_widget = ObjectProperty(None, allownone=True, baseclass=Widget) + '''Widget to attach to this effect. Even if this class doesn't make changes + to the `target_widget` by default, subclasses can use it to change the + graphics or apply custom transformations. + + :attr:`target_widget` is a :class:`~kivy.properties.ObjectProperty` and + defaults to None. + ''' + + displacement = NumericProperty(0) + '''Cumulative distance of the movement during the interaction. This is used + to determine if the movement is a drag (more than :attr:`drag_threshold`) + or not. + + :attr:`displacement` is a :class:`~kivy.properties.NumericProperty` and + defaults to 0. + ''' + + def reset(self, pos): + '''(internal) Reset the value and the velocity to the `pos`. + Mostly used when the bounds are checked. + ''' + self.value = pos + self.velocity = 0 + self.is_manual = True + self.displacement = 0 + + if (history := self.history): + val = history[-1][1] + history.clear() + history.append((time(), val)) + + def on_value(self, *args): + + if not self.is_manual: + self.target_widget.apply_pan_w_vel(self.value) + + def start(self, val, t=None): + self.is_manual = True + self.displacement = 0 + return super(PanEffect, self).start(val, t) + + def update(self, val, t=None): + self.displacement += abs(val - self.history[-1][1]) + # print('allo') + return super(PanEffect, self).update(val, t) + + def stop(self, val, t=None): + # print('allo') + self.is_manual = False + self.displacement += abs(val - self.history[-1][1]) + if self.displacement <= self.drag_threshold: + self.velocity = 0 + return + return super(PanEffect, self).stop(val, t) \ No newline at end of file diff --git a/kivy_matplotlib_widget/uix/graph_widget.py b/kivy_matplotlib_widget/uix/graph_widget.py index 750f2a7..1b4be1e 100644 --- a/kivy_matplotlib_widget/uix/graph_widget.py +++ b/kivy_matplotlib_widget/uix/graph_widget.py @@ -30,6 +30,7 @@ from kivy.utils import get_color_from_hex from kivy.core.window import Window from kivy.clock import Clock +from kivy_matplotlib_widget.tools.pan_effects import PanEffect class MatplotFigure(Widget): """Widget to show a matplotlib figure in kivy. @@ -84,7 +85,16 @@ class MatplotFigure(Widget): autoscale_tight = BooleanProperty(False) desktop_mode = BooleanProperty(True) #change mouse hover for selector widget current_selector = OptionProperty("None", - options = ["None",'rectangle','lasso','ellipse','span','custom']) + options = ["None",'rectangle','lasso','ellipse','span','custom']) + stop_xlimits = ListProperty(None, allownone=True) + stop_ylimits = ListProperty(None, allownone=True) + velocity_panx = BooleanProperty(False) + pan_effect_cls = ObjectProperty(PanEffect, allownone=True) + + effect_x = ObjectProperty(None, allownone=True) + _effect_x_start_x = None + _update_effect_bounds_ev = None + last_touch_vel = 0 def on_figure(self, obj, value): self.figcanvas = _FigureCanvas(self.figure, self) @@ -197,6 +207,10 @@ def __init__(self, **kwargs): #selector management self.kv_post_done = False self.selector = None + + #manage pan effect + effect_cls = self.pan_effect_cls + self.effect_x = effect_cls(target_widget=self) self.bind(size=self._onSize) @@ -757,12 +771,18 @@ def transform_with_touch(self, event): if self._nav_stack() is None: self.push_current() self.apply_pan(self.axes, event) + + if self.velocity_panx: + self.effect_x.update(event.x) if self.touch_mode=='pan_x' or self.touch_mode=='pan_y' \ or self.touch_mode=='adjust_x' or self.touch_mode=='adjust_y': if self._nav_stack() is None: self.push_current() - self.apply_pan(self.axes, event, mode=self.touch_mode) + self.apply_pan(self.axes, event, mode=self.touch_mode) + + if self.velocity_panx: + self.effect_x.update(event.x) elif self.touch_mode=='drag_legend': if self.legend_instance: @@ -940,6 +960,14 @@ def on_touch_down(self, event): elif self.touch_mode=='minmax': self.min_max(event) + + elif self.velocity_panx and 'pan' in self.touch_mode: + self._effect_x_start_x = event.x + self.last_touch_vel = event.x + self.effect_x.start(event.x) + self.effect_x.trigger_velocity_update() + self._update_effect_bounds() + elif self.touch_mode=='selector': pass @@ -989,8 +1017,17 @@ def on_touch_up(self, event): self.touch_mode=='pan_x' or self.touch_mode=='pan_y' \ or self.touch_mode=='adjust_x' or self.touch_mode=='adjust_y' \ or self.touch_mode=='minmax': - + self.push_current() + + if self.velocity_panx: + self.effect_x.stop(event.x) + ev = self._update_effect_bounds_ev + if ev is None: + ev = self._update_effect_bounds_ev = Clock.create_trigger( + self._update_effect_bounds) + ev() + if self.interactive_axis: if self.touch_mode=='pan_x' or self.touch_mode=='pan_y' \ or self.touch_mode=='adjust_x' or self.touch_mode=='adjust_y': @@ -1039,6 +1076,10 @@ def apply_zoom(self, scale_factor, ax, anchor=(0, 0),new_line=None): cur_xlim = ax.get_xlim() cur_ylim = ax.get_ylim() + + if self.velocity_panx: + xdata = (cur_xlim[1] - cur_xlim[0])*.5 + cur_xlim[0] + ydata = (cur_ylim[1] - cur_ylim[0])*.5 + cur_ylim[0] scale=ax.get_xscale() yscale=ax.get_yscale() @@ -1073,7 +1114,23 @@ def apply_zoom(self, scale_factor, ax, anchor=(0, 0),new_line=None): if self.do_zoom_x: if scale == 'linear': - ax.set_xlim([xdata - new_width * (1 - relx), xdata + new_width * (relx)]) + if self.stop_xlimits is not None: + new_min = xdata - new_width * (1 - relx) + new_max = xdata + new_width * (relx) + if new_min > self.stop_xlimits[0] and new_max < self.stop_xlimits[1]: + ax.set_xlim([new_min, new_max]) + elif new_min <= self.stop_xlimits[0] and new_max >= self.stop_xlimits[1]: + ax.set_xlim([self.stop_xlimits[0],self.stop_xlimits[1]]) + ax.figure.canvas.draw_idle() + ax.figure.canvas.flush_events() + return + + elif new_min <= self.stop_xlimits[0]: + ax.set_xlim([self.stop_xlimits[0], self.stop_xlimits[0] + (new_max-new_min)]) + elif new_max >= self.stop_xlimits[1]: + ax.set_xlim([ self.stop_xlimits[1] - (new_max-new_min),self.stop_xlimits[1]]) + else: + ax.set_xlim([xdata - new_width * (1 - relx), xdata + new_width * (relx)]) else: new_min = xdata - new_width * (1 - relx) new_max = xdata + new_width * (relx) @@ -1083,9 +1140,23 @@ def apply_zoom(self, scale_factor, ax, anchor=(0, 0),new_line=None): new_min, new_max = min_, max_ if new_min <= 0. or new_max <= 0.: # Limit case new_min, new_max = min_, max_ - ax.set_xlim([new_min, new_max]) + if self.stop_xlimits is not None: + if new_min > self.stop_xlimits[0] and new_max < self.stop_xlimits[1]: + ax.set_xlim([new_min, new_max]) + elif new_min <= self.stop_xlimits[0] and new_max >= self.stop_xlimits[1]: + ax.set_xlim([self.stop_xlimits[0],self.stop_xlimits[1]]) + ax.figure.canvas.draw_idle() + ax.figure.canvas.flush_events() + return + + elif new_min <= self.stop_xlimits[0]: + ax.set_xlim([self.stop_xlimits[0], self.stop_xlimits[0] + (new_max-new_min)]) + elif new_max >= self.stop_xlimits[1]: + ax.set_xlim([ self.stop_xlimits[1] - (new_max-new_min),self.stop_xlimits[1]]) + else: + ax.set_xlim([new_min, new_max]) - if self.do_zoom_y: + if self.do_zoom_y or not self.velocity_panx: if yscale == 'linear': ax.set_ylim([ydata - new_height * (1 - rely), ydata + new_height * (rely)]) else: @@ -1223,12 +1294,20 @@ def apply_pan(self, ax, event, mode='pan'): self.inv_transform_eval((self.transform_eval(cur_xlim[1],ax.xaxis) - dx),ax.xaxis)] except (ValueError, OverflowError): cur_xlim = cur_xlim # Keep previous limits - if inverted_x: - ax.set_xlim(cur_xlim[1],cur_xlim[0]) + + if self.stop_xlimits is not None: + if cur_xlim[0] >= self.stop_xlimits[0] and cur_xlim[1] <= self.stop_xlimits[1]: + if inverted_x: + ax.set_xlim(cur_xlim[1],cur_xlim[0]) + else: + ax.set_xlim(cur_xlim) else: - ax.set_xlim(cur_xlim) + 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 not (mode=='pan_x' or self.velocity_panx) and not mode=='adjust_x': if mode=='adjust_y': if self.anchor_y is None: midpoint= (cur_ylim[1] + cur_ylim[0])/2 @@ -1278,10 +1357,18 @@ def apply_pan(self, ax, event, mode='pan'): self.inv_transform_eval((self.transform_eval(cur_ylim[1],ax.yaxis) - dy),ax.yaxis)] except (ValueError, OverflowError): cur_ylim = cur_ylim # Keep previous limits - if inverted_y: - ax.set_ylim(cur_ylim[1],cur_ylim[0]) + + if self.stop_ylimits is not None: + if cur_ylim[0] >= self.stop_ylimits[0] and cur_ylim[1] <= self.stop_ylimits[1]: + if inverted_y: + ax.set_ylim(cur_ylim[1],cur_ylim[0]) + else: + ax.set_ylim(cur_ylim) else: - ax.set_ylim(cur_ylim) + 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 @@ -1607,12 +1694,21 @@ def zoom_factory(self, event, ax, base_scale=1.1): newcoord = self.to_widget(event.x, event.y, relative=True) x = newcoord[0] y = newcoord[1] + + if self.velocity_panx: + ax=self.axes + x = (ax.bbox.bounds[2] - ax.bbox.bounds[0])/2 + y = (ax.bbox.bounds[3] - ax.bbox.bounds[1])/2 trans = ax.transData.inverted() - xdata, ydata = trans.transform_point((x, y)) - + xdata, ydata = trans.transform_point((x, y)) + cur_xlim = ax.get_xlim() cur_ylim = ax.get_ylim() + + if self.velocity_panx: + xdata = (cur_xlim[1] - cur_xlim[0])*.5 + cur_xlim[0] + ydata = (cur_ylim[1] - cur_ylim[0])*.5 + cur_ylim[0] scale=ax.get_xscale() yscale=ax.get_yscale() @@ -1658,7 +1754,25 @@ def zoom_factory(self, event, ax, base_scale=1.1): if self.do_zoom_x: if scale == 'linear': - ax.set_xlim([xdata - new_width * (1 - relx), xdata + new_width * (relx)]) + if self.stop_xlimits is not None: + new_min = xdata - new_width * (1 - relx) + new_max = xdata + new_width * (relx) + if new_min > self.stop_xlimits[0] and new_max < self.stop_xlimits[1]: + ax.set_xlim([new_min, new_max]) + elif new_min <= self.stop_xlimits[0] and new_max >= self.stop_xlimits[1]: + ax.set_xlim([self.stop_xlimits[0],self.stop_xlimits[1]]) + ax.figure.canvas.draw_idle() + ax.figure.canvas.flush_events() + return + + elif new_min <= self.stop_xlimits[0]: + ax.set_xlim([self.stop_xlimits[0], self.stop_xlimits[0] + (new_max-new_min)]) + elif new_max >= self.stop_xlimits[1]: + ax.set_xlim([ self.stop_xlimits[1] - (new_max-new_min),self.stop_xlimits[1]]) + else: + ax.set_xlim([xdata - new_width * (1 - relx), xdata + new_width * (relx)]) + + else: new_min = xdata - new_width * (1 - relx) new_max = xdata + new_width * (relx) @@ -1668,7 +1782,21 @@ def zoom_factory(self, event, ax, base_scale=1.1): new_min, new_max = min_, max_ if new_min <= 0. or new_max <= 0.: # Limit case new_min, new_max = min_, max_ - ax.set_xlim([new_min, new_max]) + if self.stop_xlimits is not None: + if new_min > self.stop_xlimits[0] and new_max < self.stop_xlimits[1]: + ax.set_xlim([new_min, new_max]) + elif new_min <= self.stop_xlimits[0] and new_max >= self.stop_xlimits[1]: + ax.set_xlim([self.stop_xlimits[0],self.stop_xlimits[1]]) + ax.figure.canvas.draw_idle() + ax.figure.canvas.flush_events() + return + + elif new_min <= self.stop_xlimits[0]: + ax.set_xlim([self.stop_xlimits[0], self.stop_xlimits[0] + (new_max-new_min)]) + elif new_max >= self.stop_xlimits[1]: + ax.set_xlim([ self.stop_xlimits[1] - (new_max-new_min),self.stop_xlimits[1]]) + else: + ax.set_xlim([new_min, new_max]) if self.do_zoom_y: if yscale == 'linear': @@ -1874,6 +2002,95 @@ def draw_box(self, event, x0, y0, x1, y1) -> None: self._box_pos = x0, y0 self._box_size = x1 - x0, y1 - y0 + def _update_effect_x_bounds(self, *args): + self.effect_x.value = self._effect_x_start_x + + def _update_effect_bounds(self, *args): + self._update_effect_x_bounds() + + def apply_pan_w_vel(self, val): + """ pan method """ + + ax = self.axes + + if len(self._touches)>=1: + self.effect_x.reset(val) + 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() + + self.update_hover() + + else: + ax.figure.canvas.draw_idle() + ax.figure.canvas.flush_events() + return + + trans = ax.transData.inverted() + xdata, ydata = trans.transform_point((val-self.pos[0], 0)) + xpress, ypress = trans.transform_point((self.last_touch_vel-self.pos[0], 0)) + + self.last_touch_vel = val + + dx = xdata - xpress + + xleft,xright=self.axes.get_xlim() + # if xleft>xright: + # return + + cur_xlim=(xleft,xright) + + + if self.stop_xlimits is not None: + if cur_xlim[0] > self.stop_xlimits[0] and cur_xlim[1] < self.stop_xlimits[1]: + cur_xlim -= dx + ax.set_xlim(cur_xlim) + # self.range = xright-xleft + else: + self.effect_x.reset(val) + + if cur_xlim[0] <= self.stop_xlimits[0]: + ax.set_xlim(left=self.stop_xlimits[0],right=self.stop_xlimits[0]+xright-xleft) + elif cur_xlim[1] >= self.stop_xlimits[1] : + ax.set_xlim(left=self.stop_xlimits[1] - (xright-xleft),right=self.stop_xlimits[1]) + else: + cur_xlim -= dx + ax.set_xlim(cur_xlim) + + 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() + + self.update_hover() + + else: + ax.figure.canvas.draw_idle() + ax.figure.canvas.flush_events() + class _FigureCanvas(FigureCanvasAgg): """Internal AGG Canvas""" From e40c13357abf19bc7e0e16063c7d559bf882b5f3 Mon Sep 17 00:00:00 2001 From: mp-007 Date: Tue, 3 Jun 2025 15:31:07 -0400 Subject: [PATCH 2/5] some additional info --- examples/example_velocity_pan/main.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/examples/example_velocity_pan/main.py b/examples/example_velocity_pan/main.py index de47311..d11a52a 100644 --- a/examples/example_velocity_pan/main.py +++ b/examples/example_velocity_pan/main.py @@ -1,13 +1,9 @@ -"""Basic example for kivy_matplotlib_widget +"""Velocity pan example for kivy_matplotlib_widget - This example don't use kivy_matplotlib_widget module in case you want to - incorporate some widgets in your project and you want to customized stuffs. - If you don't need custom used, please install the module with: - - pip install kivy-matplotlib-widget Note: MatplotFigure is used when you have only 1 axis with lines only. + Velocity pan is managed for xaxis only """ @@ -96,10 +92,7 @@ def on_start(self, *args): ax1.set_ylim(min(s)-0.05,max(s)+0.05) self.screen.figure_wgt.figure = fig - - #set pan only in x - # self.set_touch_mode('pan_x') - + #set x lim (can pan or zoom over these values) self.screen.figure_wgt.stop_xlimits = [min(t),max(t)] From 0d66580779ed6e3542095aa8b467dfc19e8238d0 Mon Sep 17 00:00:00 2001 From: mp-007 Date: Tue, 3 Jun 2025 19:08:09 -0400 Subject: [PATCH 3/5] remove some lines --- kivy_matplotlib_widget/uix/graph_widget.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/kivy_matplotlib_widget/uix/graph_widget.py b/kivy_matplotlib_widget/uix/graph_widget.py index 1b4be1e..841ed60 100644 --- a/kivy_matplotlib_widget/uix/graph_widget.py +++ b/kivy_matplotlib_widget/uix/graph_widget.py @@ -1694,11 +1694,6 @@ def zoom_factory(self, event, ax, base_scale=1.1): newcoord = self.to_widget(event.x, event.y, relative=True) x = newcoord[0] y = newcoord[1] - - if self.velocity_panx: - ax=self.axes - x = (ax.bbox.bounds[2] - ax.bbox.bounds[0])/2 - y = (ax.bbox.bounds[3] - ax.bbox.bounds[1])/2 trans = ax.transData.inverted() xdata, ydata = trans.transform_point((x, y)) From ce87a51f77f7775ef9285bf61b32e88400429d88 Mon Sep 17 00:00:00 2001 From: mp-007 Date: Thu, 5 Jun 2025 10:37:25 -0400 Subject: [PATCH 4/5] add more info an remove an unwanted condition --- examples/example_velocity_pan/main.py | 1 + kivy_matplotlib_widget/uix/graph_widget.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/example_velocity_pan/main.py b/examples/example_velocity_pan/main.py index d11a52a..17e120c 100644 --- a/examples/example_velocity_pan/main.py +++ b/examples/example_velocity_pan/main.py @@ -4,6 +4,7 @@ Note: MatplotFigure is used when you have only 1 axis with lines only. Velocity pan is managed for xaxis only + Velocity pan may not work on inverted axis """ diff --git a/kivy_matplotlib_widget/uix/graph_widget.py b/kivy_matplotlib_widget/uix/graph_widget.py index 841ed60..28f2a67 100644 --- a/kivy_matplotlib_widget/uix/graph_widget.py +++ b/kivy_matplotlib_widget/uix/graph_widget.py @@ -1078,6 +1078,7 @@ def apply_zoom(self, scale_factor, ax, anchor=(0, 0),new_line=None): cur_ylim = ax.get_ylim() if self.velocity_panx: + #zoom in the middle because pan is only in x axis xdata = (cur_xlim[1] - cur_xlim[0])*.5 + cur_xlim[0] ydata = (cur_ylim[1] - cur_ylim[0])*.5 + cur_ylim[0] @@ -1156,7 +1157,7 @@ def apply_zoom(self, scale_factor, ax, anchor=(0, 0),new_line=None): else: ax.set_xlim([new_min, new_max]) - if self.do_zoom_y or not self.velocity_panx: + if self.do_zoom_y: if yscale == 'linear': ax.set_ylim([ydata - new_height * (1 - rely), ydata + new_height * (rely)]) else: @@ -1702,6 +1703,7 @@ def zoom_factory(self, event, ax, base_scale=1.1): cur_ylim = ax.get_ylim() if self.velocity_panx: + #zoom in the middle because pan is only in x axis xdata = (cur_xlim[1] - cur_xlim[0])*.5 + cur_xlim[0] ydata = (cur_ylim[1] - cur_ylim[0])*.5 + cur_ylim[0] From 6e2a04d4a4c952078bc91e4e79ab0045e824b407 Mon Sep 17 00:00:00 2001 From: mp-007 Date: Tue, 29 Jul 2025 09:23:11 -0400 Subject: [PATCH 5/5] remove some lines and add a comment in velocity example --- examples/example_velocity_pan/main.py | 2 ++ kivy_matplotlib_widget/tools/pan_effects.py | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/example_velocity_pan/main.py b/examples/example_velocity_pan/main.py index 17e120c..30e75a3 100644 --- a/examples/example_velocity_pan/main.py +++ b/examples/example_velocity_pan/main.py @@ -87,6 +87,8 @@ def build(self): return self.screen def on_start(self, *args): + + #generate the plot fig, ax1 = plt.subplots(1, 1) ax1.plot(t,s) ax1.set_xlim(t[0],t[50]) diff --git a/kivy_matplotlib_widget/tools/pan_effects.py b/kivy_matplotlib_widget/tools/pan_effects.py index b434449..a64f4f0 100644 --- a/kivy_matplotlib_widget/tools/pan_effects.py +++ b/kivy_matplotlib_widget/tools/pan_effects.py @@ -68,11 +68,9 @@ def start(self, val, t=None): def update(self, val, t=None): self.displacement += abs(val - self.history[-1][1]) - # print('allo') return super(PanEffect, self).update(val, t) def stop(self, val, t=None): - # print('allo') self.is_manual = False self.displacement += abs(val - self.history[-1][1]) if self.displacement <= self.drag_threshold: