Closed abedhammoud closed 1 year ago
I am closing this because after learning further about this widget, I found that it already does what I am asking for and does it fast. In general, for a function that generates a matplotlib figure with multiple axes (ex. asp_figure() used below), I use the following to display the figure in this widget:
def asp_graph(self, *args):
selection, *_ = args
if self.ids._dashfigure.figure:
self.ids._dashfigure.figure.clear()
fig = self.asp_figure(selection)
self.ids._dashfigure.figure = fig
self.ids._dashfigure.axes = fig.axes[0]
self.ids._dashfigure.fast_draw = False
self.ids._dashfigure.figure.canvas.draw_idle()
self.ids._dashfigure.figure.canvas.flush_events()
And it works perfectly. Note that you need to set the axes (in this case, I set to axes[0]) to any of the axes in the figure.
@abedhammoud I think only the left axis will update when you used the zoombox. Also, the pan/zoom will be only apply on left axis.
https://github.com/mp-007/kivy_matplotlib_widget/issues/12#issuecomment-1345746831 As a temporary solution, I have add a new example in the repo "navigation_bar_general_purpose". This general purpose example is using matplotlib home, pan and zoombox (using NavigationToolbar2 like this repo https://github.com/jeysonmc/kivy_matplotlib). You have also the matplotib cursor x,y of mouse. You don't have the speed as MatPlotFigure with 1 axis and the zoombox is different but it is working.
temporary solution link: https://github.com/mp-007/kivy_matplotlib_widget/tree/main/navigation_bar_general_purpose
@abedhammoud if you want to test, I add a new experimental widget to handle 2 axis with twinx with lines.
https://github.com/mp-007/kivy_matplotlib_widget/tree/main/example_twinx_experimental
Excellent work and a good complement to the single-axis widget. I have built a dashboard using the MatPlotFigure, and it is working well. My dashboard contains multiple axes. To provide the zoom/pan/ etc., I allow the user to select a given axes by name, as shown below (is there a way to index the axes by label? that would be better than what I am doing below):
def select_fig_axes(self, axes_name):
fig = self.ids._dashfigure.figure
for ax in fig.axes:
if ax.get_label() == axes_name:
self.ids._dashfigure.axes = ax
break
I will later implement the zoom/pan similar to the example you provided.
Now the user has to select the name of the axes from a spinner that I populate with the labels from the figure:
self.ids._axes_select.values = [ax.get_label() for ax in fig.axes]
It would be easier to allow the user to click on a given axes and have that axes selected. Is there a way to do so? i.e., catch the mouse click event and set the selected axes to the axes below the mouse click.
Thanks again,
I think you can set label name with set_label, but I think it will better to go with the title with get_title ans set_title. The user see the title in the graph so you can put all the axis title in a spinner for example. I'm not 100% sure if my code will work when you change the axis but you can give a try.
Hi @mp-007, couldn't help myself get started with adding home/zoom/pan/cursor. The user selectes the axes with the spinner:
def select_fig_axes(self, axes_name):
fig = self.ids._dashfigure.figure
for ax in fig.axes:
if ax.get_label() == axes_name:
self.ids._dashfigure.axes = ax
break
self.ids._dashfigure.register_lines(
self.ids._dashfigure.axes.lines
)
The the zoom works perfectly with whatever axes is selected.
The home however, doesn't work following a zoom (nothing happens). This is what I have in the kv file:
Button:
text: 'Home'
height: dp(40)
size_hint: (1, None)
background_color: light_blue
on_release: root.set_home()
and in the py file I have:
@trace
def set_home(self):
self.ids._dashfigure.home()
The Pan works perfect.
the curson however, crashes shortly after it draws the cross-hair.
Traceback (most recent call last):
File "C:\Users\abed\miniconda3\envs\alant\lib\runpy.py", line 196, in _run_module_as_main
return _run_code(code, main_globals, None,
File "C:\Users\abed\miniconda3\envs\alant\lib\runpy.py", line 86, in _run_code
exec(code, run_globals)
File "C:\Users\abed\OneDrive\Documents\dev\GitHub\alant-git\apps\dixisales-app\dixisales\__main__.py", line 2922, in <module>
DixisalesApp().run()
File "C:\Users\abed\miniconda3\envs\alant\lib\site-packages\kivy\app.py", line 955, in run
runTouchApp()
File "C:\Users\abed\miniconda3\envs\alant\lib\site-packages\kivy\base.py", line 574, in runTouchApp
EventLoop.mainloop()
File "C:\Users\abed\miniconda3\envs\alant\lib\site-packages\kivy\base.py", line 339, in mainloop
self.idle()
File "C:\Users\abed\miniconda3\envs\alant\lib\site-packages\kivy\base.py", line 383, in idle
self.dispatch_input()
File "C:\Users\abed\miniconda3\envs\alant\lib\site-packages\kivy\base.py", line 334, in dispatch_input
post_dispatch_input(*pop(0))
File "C:\Users\abed\miniconda3\envs\alant\lib\site-packages\kivy\base.py", line 296, in post_dispatch_input
wid.dispatch('on_touch_move', me)
File "kivy\_event.pyx", line 731, in kivy._event.EventDispatcher.dispatch
File "C:\Users\abed\miniconda3\envs\alant\lib\site-packages\kivy_matplotlib_widget\uix\graph_widget.py", line 445, in on_touch_move
if self.transform_with_touch(event):
File "C:\Users\abed\miniconda3\envs\alant\lib\site-packages\kivy_matplotlib_widget\uix\graph_widget.py", line 336, in transform_with_touch
self.hover(event)
File "C:\Users\abed\miniconda3\envs\alant\lib\site-packages\kivy_matplotlib_widget\uix\graph_widget.py", line 177, in hover
index = min(np.searchsorted(self.x_cursor, xdata), len(self.y_cursor) - 1)
TypeError: object of type 'numpy.float64' has no len()
The only issue I have with using the title is that the title is optional. So far what I am doing seems to work ok.
I think you should update the ymin and ymax value when you change the axis.
self.ids._dashfigure.ymin(newaxis.get_ylim()[0])
self.ids._dashfigure.ymax(newaxis.get_ylim()[1])
for the cursor, I think you need to update self.lines with the new axis lines. ~To do that, used the register_lines function (it will set the cursor to the next axis)~ Wait, try to change self.lines first, because I think register_lines will create multiple cursor :
self.ids._dashfigure.register_lines(newaxis.lines)
Thanks, will try by this weekend, and let you know.
for the cursor, I think in the end, you will need to remove cursor line from the axis with something like this
ax.lines.remove(self.ids._dashfigure.horizontal_line)
ax.lines.remove(self.ids._dashfigure.vertical_line)
and recreate the same things as register line but without the self.text
self.horizontal_line = newaxis.axhline(color='k', lw=0.8, ls='--', visible=False)
self.vertical_line = newaxis.axvline(color='k', lw=0.8, ls='--', visible=False)
#register lines
self.lines=newaxis.lines
@abedhammoud I have made some tests and it can work if you set fast_draw=False. The current blit method do not correctly work without this.
This is by main.py file
from kivy.utils import platform
#avoid conflict between mouse provider and touch (very important with touch device)
#no need for android platform
if platform != 'android':
from kivy.config import Config
Config.set('input', 'mouse', 'mouse,disable_on_activity')
from kivy.lang import Builder
from kivy.app import App
import matplotlib.pyplot as plt
import numpy as np
import matplotlib as mpl
from kivy.metrics import dp
#optimized draw on Agg backend
mpl.rcParams['path.simplify'] = True
mpl.rcParams['path.simplify_threshold'] = 1.0
mpl.rcParams['agg.path.chunksize'] = 1000
#define some matplotlib figure parameters
mpl.rcParams['font.family'] = 'Verdana'
mpl.rcParams['axes.spines.top'] = False
mpl.rcParams['axes.linewidth'] = 1.0
KV = '''
#:import MatplotFigure graph_widget
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:"pan"
on_release:
app.set_touch_mode('pan')
self.state='down'
ToggleButton:
group:'touch_mode'
text:"zoom box"
on_release:
app.set_touch_mode('zoombox')
self.state='down'
ToggleButton:
group:'touch_mode'
text:"cursor"
on_release:
app.set_touch_mode('cursor')
self.state='down'
MatplotFigure:
id:figure_wgt
BoxLayout:
size_hint_y:0.2
ToggleButton:
group:'choose_axis'
state:'down'
text:"left axis"
on_release:
app.set_axis('ax1')
self.state='down'
ToggleButton:
group:'choose_axis'
text:"right axis"
on_release:
app.set_axis('ax2')
self.state='down'
'''
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)
ax2 = ax1.twinx()
x = np.linspace(0,2*np.pi,100)
ax1.plot(x,np.sin(x),'b')
ax1.set_xlabel('Scaleable axis')
ax1.set_ylabel('Scaleable axis')
ax2.plot(x,np.sin(x+1),'r')
ax2.set_ylabel('Static axis',weight='bold')
self.screen.figure_wgt.figure = fig
self.screen.figure_wgt.fast_draw=False
self.lines=list(fig.axes[0].lines)
self.screen.figure_wgt.register_lines(self.lines)
def set_touch_mode(self,mode):
self.screen.figure_wgt.touch_mode=mode
def home(self):
self.screen.figure_wgt.home()
def set_axis(self,axis):
if axis=='ax1':
#left axis
newaxis=self.screen.figure_wgt.figure.axes[0]
self.screen.figure_wgt.axes=newaxis
#do not include cursor lines
left_axis_lines=[]
for left_axis_line in list(self.screen.figure_wgt.figure.axes[0].lines):
if left_axis_line != self.screen.figure_wgt.horizontal_line and \
left_axis_line != self.screen.figure_wgt.vertical_line:
left_axis_lines.append(left_axis_line)
self.screen.figure_wgt.lines = left_axis_lines
elif axis=='ax2':
#right axis
newaxis=self.screen.figure_wgt.figure.axes[1]
self.screen.figure_wgt.axes=newaxis
self.screen.figure_wgt.lines = list(self.screen.figure_wgt.figure.axes[1].lines)
#ymin/ymax from left axis data
ymin_list=[]
ymax_list=[]
for current_lines in self.screen.figure_wgt.lines:
ymin_list.append(min(current_lines.get_ydata()))
ymax_list.append(max(current_lines.get_ydata()))
yoffset = abs(max(ymax_list) - min(ymin_list)) * 0.01
self.screen.figure_wgt.ymin = min(ymin_list) - yoffset
self.screen.figure_wgt.ymax = max(ymax_list) + yoffset
old_axis=self.screen.figure_wgt.horizontal_line.axes
old_axis.lines.remove(self.screen.figure_wgt.horizontal_line)
old_axis.lines.remove(self.screen.figure_wgt.vertical_line)
self.screen.figure_wgt.horizontal_line = newaxis.axhline(color='k', lw=0.8, ls='--', visible=False)
self.screen.figure_wgt.vertical_line = newaxis.axvline(color='k', lw=0.8, ls='--', visible=False)
#register lines
self.lines=newaxis.lines
Test().run()
Thanks. I will have fun with it over the weekend. @mp-007 if you can publish an example where the figure contains two or more axes and be able to select/control each separately it would be of great help. For example, add a bar or pie plot to the plot example that your showing. Sorry if I am asking too much. Thanks.
@abedhammoud for bar and pie plot, the cursor will not work, so only the min/max need to be set. I know it will be great if the cursor work on all different graph type, but this part need a lot of work.
note that I have used graph_widget.py release on 14th december for my example.
@mp-007, is it ok if I email you my trials, and once I get all working, I can post the final version here. Many thanks.
yes, you can share me a link via discord as a private message.
Hi @mp-007, I couldn't send the whole file with discord so I will try again here. Basically, I am using your excellent widget as a dashboard for my BI application. For a given report, the dashboard normally contains 1 or more axes (plot, bar, pi, etc). Here is the code so far (I am trying to use it as a component in my own widget):
import kivy.config
from kivy.app import App
from kivy.lang import Builder
from kivy.core.window import Window
from kivy.uix.boxlayout import BoxLayout
import numpy as np
import matplotlib.pyplot as plt
Builder.load_string('''
#:import MatplotFigure kivy_matplotlib_widget
#:set light_blue (176/255, 192/255, 222/255, 1)
<Kivydash>:
dash: _dash
axes_selector: _axes_select
BoxLayout:
orientation: 'vertical'
padding: 0
spacing: 0
MatplotFigure:
id: _dash
size_hint: (1, 1)
GridLayout:
cols: 5
padding: 0
spacing: 0
size_hint: (1, None)
height: self.minimum_height
Spinner:
id: _axes_select
text: 'Select Axes'
height: dp(40)
size_hint: (1, None)
background_color: light_blue
on_text: root.select_fig_axes(self.text)
Button:
text: 'Home'
height: dp(40)
size_hint: (1, None)
background_color: light_blue
on_release: root.set_home()
ToggleButton:
text: 'Pan'
height: dp(40)
group: 'touch_mode'
state: 'down'
size_hint: (1, None)
background_color: light_blue
on_release:
root.set_touch_mode('pan')
self.state='down'
ToggleButton:
text: 'Zoom'
height: dp(40)
group: 'touch_mode'
size_hint: (1, None)
background_color: light_blue
on_release:
root.set_touch_mode('zoombox')
self.state='down'
ToggleButton:
text: 'Cursor'
height: dp(40)
group: 'touch_mode'
size_hint: (1, None)
background_color: light_blue
on_release:
root.set_touch_mode('cursor')
self.state='down'
''', filename='kivydash.kv')
class Kivydash(BoxLayout):
def __init__(self, **kwargs):
super().__init__(**kwargs)
def select_fig_axes(self, axes_label):
fig = self.dash.figure
self.dash.axes = fig.axes[int(axes_label)]
def set_touch_mode(self, mode):
self.dash.touch_mode = mode
def set_home(self):
self.dash.home()
class TestApp(App):
"""Test app."""
def build(self):
Window.bind(on_resize=self.on_resize)
return Kivydash()
def on_start(self, *args):
################
# create figure.
################
fig, ax = plt.subplots(2, 2)
# plot
x = np.linspace(0, 2 * np.pi, 100)
ax[0, 0].plot(x, np.sin(x), 'b')
# bar
langs = ['C', 'C++', 'Java', 'Python', 'PHP']
students = [23, 17, 35, 29, 12]
ax[0, 1].bar(langs,students)
# pie
ax[1, 0].pie(students, labels=langs)
#####################################
# plot multi axes figure on dashbord.
#####################################
self.root.dash.figure = fig
self.root.dash.fast_draw=False
self.root.axes_selector.values = ['0', '1', '2']
self.lines=list(fig.axes[0].lines)
# initialize to 1st axes
self.root.dash.axes = fig.axes[0]
def on_resize(self, instance, width, height):
"""Window resize callback."""
kivy.config.Config.set('graphics', 'width', width)
kivy.config.Config.set('graphics', 'height', height)
kivy.config.Config.write()
if __name__ == '__main__':
TestApp().run()
Here are the issues I have - thanks for any pointers:
Code updated:
import kivy.config
from kivy.app import App
from kivy.lang import Builder
from kivy.core.window import Window
from kivy.uix.boxlayout import BoxLayout
import numpy as np
import matplotlib.pyplot as plt
Builder.load_string('''
#:import MatplotFigure kivy_matplotlib_widget
#:set light_blue (176/255, 192/255, 222/255, 1)
<Kivydash>:
display: _display
axes_selector: _axes_select
BoxLayout:
orientation: 'vertical'
padding: 0
spacing: 0
MatplotFigure:
id: _display
GridLayout:
cols: 5
padding: 0
spacing: 0
size_hint: (1, None)
height: self.minimum_height
Spinner:
id: _axes_select
text: 'Select Axes'
height: dp(40)
size_hint: (1, None)
background_color: light_blue
on_text: root.select_fig_axes(self.text)
Button:
text: 'Home'
height: dp(40)
size_hint: (1, None)
background_color: light_blue
on_release: root.set_home()
ToggleButton:
text: 'Pan'
height: dp(40)
group: 'touch_mode'
state: 'down'
size_hint: (1, None)
background_color: light_blue
on_release:
root.set_touch_mode('pan')
self.state='down'
ToggleButton:
text: 'Zoom'
height: dp(40)
group: 'touch_mode'
size_hint: (1, None)
background_color: light_blue
on_release:
root.set_touch_mode('zoombox')
self.state='down'
ToggleButton:
text: 'Cursor'
height: dp(40)
group: 'touch_mode'
size_hint: (1, None)
background_color: light_blue
on_release:
root.set_touch_mode('cursor')
self.state='down'
''', filename='kivydash.kv')
class Kivydash(BoxLayout):
def __init__(self, **kwargs):
super().__init__(**kwargs)
def select_fig_axes(self, axes_label):
fig = self.display.figure
self.display.axes = fig.axes[int(axes_label)]
def set_touch_mode(self, mode):
self.display.touch_mode = mode
def set_home(self):
self.display.home()
def show(self, fig):
if self.display.figure and fig != self.display.figure:
self.display.figure.clear()
self.display.figure = fig
self.display.fast_draw = False
self.display.axes = fig.axes[0]
self.axes_selector.values = [ax.get_label() for ax in fig.axes]
# self.display.figure.canvas.draw_idle()
# self.display.figure.canvas.flush_events()
class TestApp(App):
"""Test app."""
def build(self):
Window.bind(on_resize=self.on_resize)
return Kivydash()
def on_start(self, *args):
################
# create figure.
################
fig, ax = plt.subplots(2, 2)
# plot
x = np.linspace(0, 2 * np.pi, 100)
ax[0, 0].plot(x, np.sin(x), 'b')
# bar
langs = ['C', 'C++', 'Java', 'Python', 'PHP']
students = [23, 17, 35, 29, 12]
ax[0, 1].bar(langs,students)
# pie
ax[1, 0].pie(students, labels=langs)
#####################################
# plot multi axes figure on dashbord.
#####################################
self.root.show(fig)
def on_resize(self, instance, width, height):
"""Window resize callback."""
kivy.config.Config.set('graphics', 'width', width)
kivy.config.Config.set('graphics', 'height', height)
kivy.config.Config.write()
if __name__ == '__main__':
TestApp().run()
I will have a look during the weekend, but I can already see 2 things:
If you are using subplots with multiple figures, you need to used the matplotlib navigation toolbar to make it interactive (see https://github.com/mp-007/kivy_matplotlib_widget/tree/main/navigation_bar_general_purpose). If you want to use MatplotFigure with all the fast interactive feature you will need to used multiple MatplotFigure. See this example on discord https://discord.com/channels/423249981340778496/423249981340778498/1051517483691032626
When you set a figure you need to set all min/max attribute to make the home functionnal. In develop branch (next release), it is done automatically when you set the figure, but if you used the pip version, you need to set the min/max manually. See commit https://github.com/mp-007/kivy_matplotlib_widget/commit/02082d5adb352faf4f12922a9f332c3c0ea88ab5#diff-f6097a6e5f0fb2258e6fce57db0cfbd2381f01a31a26bc6128913dcb25ab5c17R68
Thanks @mp-007 , I shortened the code, and integrated the nav tool bar, which I cannot seem to get it connected to the figure. Thanks for any pointers.
import logging
import kivy.config
from kivy.app import App
from kivy.lang import Builder
from kivy.core.window import Window
from kivy.uix.boxlayout import BoxLayout
import numpy as np
import matplotlib.pyplot as plt
Builder.load_string('''
#:import MatplotFigure kivy_matplotlib_widget
#:import MatplotNavToolbar navigation_bar_widget
<Kivydash>:
display: _figure_wgt
navbar: _navbar_wgt
BoxLayout:
orientation: 'vertical'
padding: [dp(10), 0, 0, dp(0)]
spacing: 0
MatplotFigure:
id: _figure_wgt
MatplotNavToolbar:
id: _navbar_wgt
size_hint: 1, 0.2
figure_widget: _figure_wgt
''', filename='kivydash.kv')
class Kivydash(BoxLayout):
def __init__(self, **kwargs):
super().__init__(**kwargs)
def show(self, fig):
logging.warning(f'{plt.get_fignums()=}')
logging.warning(f'{plt.gcf()=}')
if self.display.figure:
plt.close('all')
self.display.figure = fig
self.display.fast_draw = False
self.display.figure.canvas.draw_idle()
self.display.figure.canvas.flush_events()
self.display.axes = fig.axes[0]
# attach nav bar.
self.navbar._navtoolbar._init_toolbar()
class TestApp(App):
"""Test app."""
def build(self):
Window.bind(on_resize=self.on_resize)
return Kivydash()
def set_touch_mode(self, mode):
print(f'{mode=}')
self.display.touch_mode = mode
def home(self):
print('set home...')
self.display.home()
def on_start(self, *args):
# create figure.
fig, ax = plt.subplots(2, 2)
# plot
x = np.linspace(0, 2 * np.pi, 100)
ax[0, 0].plot(x, np.sin(x), 'b')
# bar
langs = ['C', 'C++', 'Java', 'Python', 'PHP']
students = [23, 17, 35, 29, 12]
ax[0, 1].bar(langs,students)
# pie
ax[1, 0].pie(students, labels=langs)
# hbar
ax[1, 1].barh(langs,students)
# plot multi axes figure on dashbord.
self.root.show(fig)
def on_resize(self, instance, width, height):
"""Window resize callback."""
kivy.config.Config.set('graphics', 'width', width)
kivy.config.Config.set('graphics', 'height', height)
kivy.config.Config.write()
if __name__ == '__main__':
TestApp().run()
you need to used the MatPlotFigureGenaral widget (only in develop branch, will be add on net release). Do something like this.
...
Builder.load_string('''
#:import MatplotFigureGeneral graph_widget_general
#:import MatplotNavToolbar navigation_bar_widget
<Kivydash>:
display: _figure_wgt
navbar: _navbar_wgt
BoxLayout:
orientation: 'vertical'
padding: [dp(10), 0, 0, dp(0)]
spacing: 0
MatplotFigureGeneral:
id: _figure_wgt
MatplotNavToolbar:
id: _navbar_wgt
size_hint: 1, 0.2
figure_widget: _figure_wgt
''', filename='kivydash.kv')
class Kivydash(BoxLayout):
def __init__(self, **kwargs):
super().__init__(**kwargs)
def show(self, fig):
logging.warning(f'{plt.get_fignums()=}')
logging.warning(f'{plt.gcf()=}')
if self.display.figure:
plt.close('all')
self.display.figure = fig
# self.display.fast_draw = False
self.display.figure.canvas.draw_idle()
self.display.figure.canvas.flush_events()
# self.display.axes = fig.axes[0]
...
Thanks, @mp-007, excellent work. Really appreciated. It works great.
Hi @mp-007, this is the latest with the changes you recommended above, and added more to explain my usecase.
# avoid conflict between mouse provider and touch - very important with
# touch device. no need for android platform
from kivy.utils import platform
if platform != 'android':
from kivy.config import Config
Config.set('input', 'mouse', 'mouse,disable_on_activity')
import logging
from kivy.app import App
from kivy.metrics import dp
from kivy.lang import Builder
from kivy.uix.button import Button
from kivy.uix.boxlayout import BoxLayout
import numpy as np
import matplotlib.pyplot as plt
from alant.utils.decotools import trace
# optimized draw on Agg backend
import matplotlib as mpl
mpl.rcParams['path.simplify'] = True
mpl.rcParams['agg.path.chunksize'] = 1000
mpl.rcParams['path.simplify_threshold'] = 1.0
# define some matplotlib figure parameters
# mpl.rcParams['axes.linewidth'] = 1.0
# mpl.rcParams['font.family'] = 'Verdana'
# mpl.rcParams['axes.spines.top'] = False
Builder.load_string('''
#:import MatplotFigureGeneral graph_widget_general
#:import MatplotNavToolbar navigation_bar_widget
<Kivydash>:
display: _figure_wgt
navbar: _navbar_wgt
BoxLayout:
orientation:'vertical'
# left, top, right, bottom
padding: [dp(10), 0, 0, 0]
spacing: 0
MatplotFigureGeneral:
id:_figure_wgt
MatplotNavToolbar:
id: _navbar_wgt
height: dp(60)
size_hint: 1, None
figure_widget: _figure_wgt
''', filename='kivydash.kv')
class Kivydash(BoxLayout):
def __init__(self, **kwargs):
super().__init__(**kwargs)
@trace
def show(self, fig):
logging.warning(f'{plt.get_fignums()=}')
logging.warning(f'{plt.gcf()=}')
if self.display.figure:
plt.close('all')
plt.tight_layout()
self.display.figure = fig
# attach navigation bar.
self.navbar._navtoolbar._init_toolbar()
####################################
# if user want to hide cursor label.
####################################
# self.navbar.ids.info_lbl.opacity=0
# self.navbar.ids.info_lbl.height=dp(0.01)
# self.navbar.ids.info_lbl.size_hint_y=None
class TestApp(App):
def build(self):
root = BoxLayout()
# dashboard
self._dash = Kivydash()
# control
control = BoxLayout(
orientation='vertical',
size_hint=(None, 1), width=dp(100)
)
control.add_widget(
Button(
text='Fig-0',
on_release=self.display_figure_0
)
)
control.add_widget(
Button(
text='Fig-1',
on_release=self.display_figure_1
)
)
control.add_widget(
Button(
text='Fig-2',
on_release=self.display_figure_2
)
)
root.add_widget(control)
root.add_widget(self._dash)
return root
def on_start(self, *args):
fig = self.build_figure_0()
self._dash.show(fig)
def display_figure_0(self, *args):
fig = self.build_figure_0()
self._dash.show(fig)
def display_figure_1(self, *args):
fig = self.build_figure_1()
self._dash.show(fig)
def display_figure_2(self, *args):
fig = self.build_figure_2()
self._dash.show(fig)
def build_figure_0(self):
fig, ax = plt.subplots(
2, 3,
figsize=(18, 11),
constrained_layout=True,
)
# twin axes plot
x = np.linspace(0, 2 * np.pi, 100)
ax[0, 0].plot(x, np.sin(x), 'b')
ax[0, 0].set_xlabel('Scaleable axis')
ax[0, 0].set_ylabel('Scaleable axis')
ax2 = ax[0, 0].twinx()
ax2.plot(x, np.sin( x + 1), 'r')
ax2.set_ylabel('Static axis', weight='bold')
# bar plots
langs = ['C', 'C++', 'Java', 'Python', 'PHP']
students = [23, 17, 35, 29, 12]
ax[0, 1].bar(langs, students)
ax[1, 0].barh(langs, students)
# plot
ax[1, 1].plot(x, np.cos(x))
# pie plot
ax[0, 2].pie(students, labels=langs)
ax[1, 2].pie(students, labels=langs)
return fig
def build_figure_1(self):
fig, ax = plt.subplots(
2, 2,
figsize=(18, 11),
constrained_layout=True,
)
# pie plot
students = [23, 17, 35, 29, 12]
langs = ['C', 'C++', 'Java', 'Python', 'PHP']
ax[0, 0].pie(students, labels=langs)
# twin axes plot
x = np.linspace(0, 2 * np.pi, 100)
ax[1, 1].plot(x, np.sin(x), 'b')
ax[1, 1].set_xlabel('Scaleable axis')
ax[1, 1].set_ylabel('Scaleable axis')
ax2 = ax[1, 1].twinx()
ax2.plot(x, np.sin( x + 1), 'r')
ax2.set_ylabel('Static axis', weight='bold')
# bar plots
ax[0, 1].bar(langs, students)
ax[1, 0].barh(langs, students)
return fig
def build_figure_2(self):
fig, ax = plt.subplots(
1, 1,
figsize=(18, 11),
constrained_layout=True,
)
# twin axes plot
x = np.linspace(0, 2 * np.pi, 100)
ax.plot(x, np.sin(x), 'b')
ax.set_xlabel('Scaleable axis')
ax.set_ylabel('Scaleable axis')
ax2 = ax.twinx()
ax2.plot(x, np.sin( x + 1), 'r')
ax2.set_ylabel('Static axis', weight='bold')
return fig
if __name__ == '__main__':
TestApp().run()
It allows me to use the widget as a drawing board that I draw on multiple figures. It is close to perfect for what I need, but I don't seem to get the navigation bar working 100% yet.
When I use the zoom on one of the axes, the shaded box is clamped and cannot cover the whole axes. It seems that I need to reinitialize the navigation bar every time the show(fig) method is called with a new figure.
Another issue I noticed is that sometimes pressing the home button takes you to the previously displayed figure and not reset the current figure.
I must not be initializing the navbar correctly between calls to show(figure)
Sorry for my long postings. I hope I am helping somehow.
Thanks. I look forward to the next release.
You can't really control the axis with matplotlib navigation. It do want matplotlib decide and you can't really customized the zoom box for example. I think the best option in your case is to used several MatplotFigure and control your navigation tools separately on every figure.
OK, thanks, I will try that.
@abedhammoud An example based on your code
# avoid conflict between mouse provider and touch - very important with
# touch device. no need for android platform
from kivy.utils import platform
if platform != 'android':
from kivy.config import Config
Config.set('input', 'mouse', 'mouse,disable_on_activity')
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.screenmanager import SlideTransition
import numpy as np
import matplotlib.pyplot as plt
from kivy.core.window import Window
# optimized draw on Agg backend
import matplotlib as mpl
mpl.rcParams['path.simplify'] = True
mpl.rcParams['agg.path.chunksize'] = 1000
mpl.rcParams['path.simplify_threshold'] = 1.0
# define some matplotlib figure parameters
# mpl.rcParams['axes.linewidth'] = 1.0
# mpl.rcParams['font.family'] = 'Verdana'
# mpl.rcParams['axes.spines.top'] = False
Builder.load_string('''
#:import MatplotFigure graph_widget
<Kivydash>:
BoxLayout
size_hint_x:None
width:dp(100)
orientation:'vertical'
Button:
text:'Fig-0'
on_release:
app.display_figure('screen1')
Button:
text:'Fig-1'
on_release:
app.display_figure('screen2')
ScreenManager:
id:sm
Screen1:
name:'screen1'
size: sm.size
pos: sm.pos
Screen2:
name:'screen2'
size: sm.size
pos: sm.pos
<Screen1@Screen>:
_figure_wgt:_figure_wgt
_figure_wgt2:_figure_wgt2
_figure_wgt3:_figure_wgt3
_figure_wgt4:_figure_wgt4
_figure_wgt5:_figure_wgt5
_figure_wgt6:_figure_wgt6
BoxLayout:
orientation:'vertical'
# left, top, right, bottom
padding: [dp(10), 0, 0, 0]
BoxLayout:
size_hint_y:0.2
Button:
text:"home"
on_release:app.home()
ToggleButton:
group:'touch_mode'
state:'down'
text:"pan"
on_release:
app.set_touch_mode('pan')
self.state='down'
ToggleButton:
group:'touch_mode'
text:"zoom box"
on_release:
app.set_touch_mode('zoombox')
self.state='down'
ToggleButton:
group:'touch_mode'
text:"cursor"
on_release:
app.set_touch_mode('cursor')
self.state='down'
BoxLayout:
orientation:'vertical'
BoxLayout:
BoxLayout:
orientation:'vertical'
MatplotFigure:
id:_figure_wgt
BoxLayout:
size_hint_y:0.1
ToggleButton:
group:'choose_axis1'
state:'down'
text:"left axis"
on_release:
app.set_axis(_figure_wgt,'ax1')
self.state='down'
ToggleButton:
group:'choose_axis1'
text:"right axis"
on_release:
app.set_axis(_figure_wgt,'ax2')
self.state='down'
BoxLayout:
orientation:'vertical'
MatplotFigure:
id:_figure_wgt2
Widget:
size_hint_y:0.1
BoxLayout:
orientation:'vertical'
MatplotFigure:
id:_figure_wgt3
Widget:
size_hint_y:0.1
BoxLayout:
MatplotFigure:
id:_figure_wgt4
MatplotFigure:
id:_figure_wgt5
MatplotFigure:
id:_figure_wgt6
<Screen2@Screen>:
_figure_wgt:_figure_wgt
_figure_wgt2:_figure_wgt2
_figure_wgt3:_figure_wgt3
_figure_wgt4:_figure_wgt4
BoxLayout:
orientation:'vertical'
padding: [dp(10), 0, 0, 0]
BoxLayout:
size_hint_y:0.2
Button:
text:"home"
on_release:app.home()
ToggleButton:
group:'touch_mode'
state:'down'
text:"pan"
on_release:
app.set_touch_mode('pan')
self.state='down'
ToggleButton:
group:'touch_mode'
text:"zoom box"
on_release:
app.set_touch_mode('zoombox')
self.state='down'
ToggleButton:
group:'touch_mode'
text:"cursor"
on_release:
app.set_touch_mode('cursor')
self.state='down'
BoxLayout:
orientation:'vertical'
BoxLayout:
BoxLayout:
orientation:'vertical'
MatplotFigure:
id:_figure_wgt
Widget:
size_hint_y:0.1
BoxLayout:
orientation:'vertical'
MatplotFigure:
id:_figure_wgt2
BoxLayout:
size_hint_y:0.1
ToggleButton:
group:'choose_axis2'
state:'down'
text:"left axis"
on_release:
app.set_axis(_figure_wgt2,'ax1')
self.state='down'
ToggleButton:
group:'choose_axis2'
text:"right axis"
on_release:
app.set_axis(_figure_wgt2,'ax2')
self.state='down'
BoxLayout:
MatplotFigure:
id:_figure_wgt3
MatplotFigure:
id:_figure_wgt4
''', filename='kivydash.kv')
class Kivydash(BoxLayout):
def __init__(self, **kwargs):
super().__init__(**kwargs)
class TestApp(App):
def build(self):
# dashboard
self._dash = Kivydash()
return self._dash
def on_start(self, *args):
self.build_figure_0()
self.build_figure_1()
Window.maximize()
def display_figure(self,screen):
self._dash.ids.sm.tansition = SlideTransition(duration=0.2)
self._dash.ids.sm.current=screen
def build_figure_0(self):
fig1, ax1 = plt.subplots(1,1)
fig1.subplots_adjust(left=0.13,top=0.93,right=0.93,bottom=0.2)
# twin axes plot
x = np.linspace(0, 2 * np.pi, 100)
ax1.plot(x, np.sin(x), 'b')
ax1.set_xlabel('Scaleable axis')
ax1.set_ylabel('Scaleable axis')
ax2 = ax1.twinx()
ax2.plot(x, np.sin( x + 1), 'r')
ax2.set_ylabel('Static axis', weight='bold')
fig2, ax3 = plt.subplots(1,1)
fig2.subplots_adjust(left=0.13,top=0.93,right=0.93,bottom=0.2)
# bar plots
langs = ['C', 'C++', 'Java', 'Python', 'PHP']
students = [23, 17, 35, 29, 12]
ax3.bar(langs, students)
fig3, ax4 = plt.subplots(1,1)
fig3.subplots_adjust(left=0.13,top=0.93,right=0.93,bottom=0.2)
ax4.barh(langs, students)
fig4, ax5 = plt.subplots(1,1)
fig4.subplots_adjust(left=0.13,top=0.93,right=0.93,bottom=0.2)
# plot
ax5.plot(x, np.cos(x))
fig5, ax6 = plt.subplots(1,1)
fig5.subplots_adjust(left=0.13,top=0.93,right=0.93,bottom=0.2)
# pie plot
ax6.pie(students, labels=langs)
fig6, ax7 = plt.subplots(1,1)
fig6.subplots_adjust(left=0.13,top=0.93,right=0.93,bottom=0.2)
ax7.pie(students, labels=langs)
screen1=self._dash.ids.sm.get_screen('screen1')
screen1._figure_wgt.figure=fig1
screen1._figure_wgt2.figure=fig2
screen1._figure_wgt2.fast_draw=False
screen1._figure_wgt3.figure=fig3
screen1._figure_wgt3.fast_draw=False
screen1._figure_wgt4.figure=fig4
screen1._figure_wgt5.figure=fig5
screen1._figure_wgt5.fast_draw=False
screen1._figure_wgt6.figure=fig6
screen1._figure_wgt6.fast_draw=False
screen1._figure_wgt.register_lines(list(screen1._figure_wgt.axes.lines))
screen1._figure_wgt.fast_draw=False
screen1._figure_wgt4.register_lines(list(screen1._figure_wgt4.axes.lines))
def build_figure_1(self):
fig1, ax1 = plt.subplots(1,1)
fig1.subplots_adjust(left=0.13,top=0.93,right=0.93,bottom=0.2)
# pie plot
students = [23, 17, 35, 29, 12]
langs = ['C', 'C++', 'Java', 'Python', 'PHP']
ax1.pie(students, labels=langs)
fig2, ax2 = plt.subplots(1,1)
fig2.subplots_adjust(left=0.13,top=0.93,right=0.93,bottom=0.2)
# twin axes plot
x = np.linspace(0, 2 * np.pi, 100)
ax2.plot(x, np.sin(x), 'b')
ax2.set_xlabel('Scaleable axis')
ax2.set_ylabel('Scaleable axis')
ax3 = ax2.twinx()
ax3.plot(x, np.sin( x + 1), 'r')
ax3.set_ylabel('Static axis', weight='bold')
# bar plots
fig3, ax4 = plt.subplots(1,1)
fig3.subplots_adjust(left=0.13,top=0.93,right=0.93,bottom=0.2)
ax4.bar(langs, students)
fig4, ax5 = plt.subplots(1,1)
fig4.subplots_adjust(left=0.13,top=0.93,right=0.93,bottom=0.2)
ax5.barh(langs, students)
screen2 = self._dash.ids.sm.get_screen('screen2')
screen2._figure_wgt.figure=fig1
screen2._figure_wgt.fast_draw=False
screen2._figure_wgt2.figure=fig2
screen2._figure_wgt3.figure=fig3
screen2._figure_wgt3.fast_draw=False
screen2._figure_wgt4.figure=fig4
screen2._figure_wgt4.fast_draw=False
screen2._figure_wgt2.register_lines(list(screen2._figure_wgt2.axes.lines))
screen2._figure_wgt2.fast_draw=False
def set_touch_mode(self,mode):
if self._dash.ids.sm.current=='screen1':
screen1=self._dash.ids.sm.get_screen('screen1')
if mode=='cursor':
screen1._figure_wgt.touch_mode=mode
screen1._figure_wgt4.touch_mode=mode
else:
screen1._figure_wgt.touch_mode=mode
screen1._figure_wgt2.touch_mode=mode
screen1._figure_wgt3.touch_mode=mode
screen1._figure_wgt4.touch_mode=mode
screen1._figure_wgt5.touch_mode=mode
screen1._figure_wgt6.touch_mode=mode
elif self._dash.ids.sm.current=='screen2':
screen2=self._dash.ids.sm.get_screen('screen2')
if mode=='cursor':
screen2._figure_wgt2.touch_mode=mode
else:
screen2._figure_wgt.touch_mode=mode
screen2._figure_wgt2.touch_mode=mode
screen2._figure_wgt3.touch_mode=mode
screen2._figure_wgt4.touch_mode=mode
def home(self):
if self._dash.ids.sm.current=='screen1':
screen1=self._dash.ids.sm.get_screen('screen1')
screen1._figure_wgt.home()
screen1._figure_wgt2.home()
screen1._figure_wgt3.home()
screen1._figure_wgt4.home()
screen1._figure_wgt5.home()
screen1._figure_wgt6.home()
elif self._dash.ids.sm.current=='screen2':
screen2=self._dash.ids.sm.get_screen('screen2')
screen2._figure_wgt.home()
screen2._figure_wgt2.home()
screen2._figure_wgt3.home()
screen2._figure_wgt4.home()
def set_axis(self,instance,axis):
if axis=='ax1':
#left axis
newaxis=instance.figure.axes[0]
instance.axes=newaxis
#do not include cursor lines
left_axis_lines=[]
for left_axis_line in list(instance.figure.axes[0].lines):
if left_axis_line != instance.horizontal_line and \
left_axis_line != instance.vertical_line:
left_axis_lines.append(left_axis_line)
instance.lines = left_axis_lines
elif axis=='ax2':
#right axis
newaxis=instance.figure.axes[1]
instance.axes=newaxis
instance.lines = list(instance.figure.axes[1].lines)
#ymin/ymax from left axis data
ymin_list=[]
ymax_list=[]
for current_lines in instance.lines:
ymin_list.append(min(current_lines.get_ydata()))
ymax_list.append(max(current_lines.get_ydata()))
instance.ymin = min(ymin_list)
instance.ymax = max(ymax_list)
yoffset = abs(max(ymax_list) - min(ymin_list)) * 0.01
instance.ymin = min(ymin_list) - yoffset
instance.ymax = max(ymax_list) + yoffset
# old_axis=instance.horizontal_line.axes
# old_axis.lines.remove(instance.horizontal_line)
# old_axis.lines.remove(instance.vertical_line)
# instance.horizontal_line = newaxis.axhline(color='k', lw=0.8, ls='--', visible=False)
# instance.vertical_line = newaxis.axvline(color='k', lw=0.8, ls='--', visible=False)
#register lines
self.lines=newaxis.lines
if __name__ == '__main__':
TestApp().run()
used the MatPlotFigure widget from master branch
Feel free to close this issue if you think handling multiple axis is working correctly.
Thanks. Excellent work.
Today, the widget provides a good level of interactive control for a single axes figure and does it well/fast. What would be good to have is the ability to display figures with multiple axes and either have the interactive features turned off or attached to one of the axes at a time. Thanks, @mp-007, for the excellent work.