问题
I am currently creating a mobile app with KivyMD which serves for managing travel expense requests. The user will enter a desired requested amount for different types of expenses on an MDTextField. I want to add a donut graph made with patplotlib into an MDBoxLayout. Such graph should automatically update as the request is filled. (For clarity I will include a screenshot. The square in red is the desired location for my graph).
I created a method called update_method_graph and used fixed numbers and I can successfully create a Plot, however I have not been successful on adding such graph on the app. Once I can succesfully add the graph to my app I will link such values to the requests added by the user. For now my concern is to add the graph correctly. Of course the finished code will not include the plt.show() line, the graph should be updated directly on the app.
As for now, when I close the window of the graph, my code shows an error in
self.ids.expense_graph.add_widget(FigureCanvasKivyAgg(plt.gcf()))
File "kivy\properties.pyx", line 863, in kivy.properties.ObservableDict.__getattr__
AttributeError: 'super' object has no attribute '__getattr__'`
With the key error in expense_graph.
I have tried with from kivy.garden.matplotlib.backend_kivyagg import FigureCanvasKivyAgg
suggested in an answer to a similar question and with matplotlib.use('module://kivy.garden.matplotlib.backend_kivy')
, as done in examples of use in garden.matplotlib however I still can't get my app to work.
CODE FOR MINIMAL REPRODUCIBLE EXAMPLE
Python code:
from kivy.properties import ObjectProperty
from kivy.uix.screenmanager import ScreenManager, Screen
from kivymd.app import MDApp
from kivymd.uix.expansionpanel import MDExpansionPanel, MDExpansionPanelOneLine
from kivy.uix.boxlayout import BoxLayout
import matplotlib.pyplot as plt
from kivy.garden.matplotlib.backend_kivyagg import FigureCanvasKivyAgg
from kivy.uix.image import Image
class MyContentAliment(BoxLayout):
monto_alimento = 0
def apply_currency_format(self):
# if len <= 3
if len(self.ids.monto_aliment_viaje.text) <= 3 and self.ids.monto_aliment_viaje.text.isnumeric():
self.ids.monto_aliment_viaje.text = "$" + self.ids.monto_aliment_viaje.text + '.00'
# n,nnn
elif len(self.ids.monto_aliment_viaje.text) == 4 and self.ids.monto_aliment_viaje.text.isnumeric():
self.ids.monto_aliment_viaje.text = "$" + self.ids.monto_aliment_viaje.text[0] + "," + \
self.ids.monto_aliment_viaje.text[1:] + '.00'
# nn,nnn
elif len(self.ids.monto_aliment_viaje.text) == 5 and self.ids.monto_aliment_viaje.text.isnumeric():
self.ids.monto_aliment_viaje.text = "$" + self.ids.monto_aliment_viaje.text[:2] + "," + \
self.ids.monto_aliment_viaje.text[2:] + '.00'
def limit_currency(self):
if len(self.ids.monto_aliment_viaje.text) > 5 and self.ids.monto_aliment_viaje.text.startswith('$') == False:
self.ids.monto_aliment_viaje.text = self.ids.monto_aliment_viaje.text[:-1]
def sumar_gasto(self):
if self.ids.monto_aliment_viaje.text == "":
pass
elif self.ids.monto_aliment_viaje.text.startswith('$'):
pass
else:
travel_manager = MDApp.get_running_app().root.get_screen('travelManager')
monto_total = float(travel_manager.ids.suma_solic_viaje.text[2:])
monto_total += float(self.ids.monto_aliment_viaje.text)
travel_manager.ids.suma_solic_viaje.text = "$ " + str(monto_total)
self.apply_currency_format()
# USE THIS METHOD TO UPDATE THE VALUE OF ALIMENTOS (donut)
def update_requested_value(self):
MyContentAliment.monto_alimento = 0
if len(self.ids.monto_aliment_viaje.text) > 0:
MyContentAliment.monto_alimento = self.ids.monto_aliment_viaje.text
else:
MyContentAliment.monto_alimento = 0
TravelManagerWindow.update_donut_graph(MyContentAliment.monto_alimento)
class MyContentCasetas(BoxLayout):
monto_casetas = 0
def apply_currency_format(self):
# if len <= 3
if len(self.ids.monto_casetas_viaje.text) <= 3 and self.ids.monto_casetas_viaje.text.isnumeric():
self.ids.monto_casetas_viaje.text = "$" + self.ids.monto_casetas_viaje.text + '.00'
# n,nnn
elif len(self.ids.monto_casetas_viaje.text) == 4 and self.ids.monto_casetas_viaje.text.isnumeric():
self.ids.monto_casetas_viaje.text = "$" + self.ids.monto_casetas_viaje.text[0] + "," + \
self.ids.monto_casetas_viaje.text[1:] + '.00'
# nn,nnn
elif len(self.ids.monto_casetas_viaje.text) == 5 and self.ids.monto_casetas_viaje.text.isnumeric():
self.ids.monto_casetas_viaje.text = "$" + self.ids.monto_casetas_viaje.text[:2] + "," + \
self.ids.monto_casetas_viaje.text[2:] + '.00'
def limit_currency(self):
if len(self.ids.monto_casetas_viaje.text) > 5 and self.ids.monto_casetas_viaje.text.startswith('$') == False:
self.ids.monto_casetas_viaje.text = self.ids.monto_casetas_viaje.text[:-1]
def sumar_gasto(self):
if self.ids.monto_casetas_viaje.text == "":
pass
elif self.ids.monto_casetas_viaje.text.startswith('$'):
pass
else:
travel_manager = MDApp.get_running_app().root.get_screen('travelManager')
monto_total = float(travel_manager.ids.suma_solic_viaje.text[2:])
monto_total += float(self.ids.monto_casetas_viaje.text)
travel_manager.ids.suma_solic_viaje.text = "$ " + str(monto_total)
self.apply_currency_format()
# USE THIS METHOD TO UPDATE THE VALUE OF CASETAS (donut)
def update_requested_value(self):
MyContentCasetas.monto_casetas = 0
if len(self.ids.monto_casetas_viaje.text) > 0:
MyContentCasetas.monto_casetas = self.ids.monto_casetas_viaje.text
else:
MyContentCasetas.monto_casetas = 0
TravelManagerWindow.update_donut_graph(MyContentCasetas.monto_casetas)
class MyContentGasolina(BoxLayout):
monto_gasolina = 0
def apply_currency_format(self):
# if len <= 3
if len(self.ids.monto_gas_viaje.text) <= 3 and self.ids.monto_gas_viaje.text.isnumeric():
self.ids.monto_gas_viaje.text = "$" + self.ids.monto_gas_viaje.text + '.00'
# n,nnn
elif len(self.ids.monto_gas_viaje.text) == 4 and self.ids.monto_gas_viaje.text.isnumeric():
self.ids.monto_gas_viaje.text = "$" + self.ids.monto_gas_viaje.text[0] + "," + \
self.ids.monto_gas_viaje.text[1:] + '.00'
# nn,nnn
elif len(self.ids.monto_gas_viaje.text) == 5 and self.ids.monto_gas_viaje.text.isnumeric():
self.ids.monto_gas_viaje.text = "$" + self.ids.monto_gas_viaje.text[:2] + "," + \
self.ids.monto_gas_viaje.text[2:] + '.00'
def limit_currency(self):
if len(self.ids.monto_gas_viaje.text) > 5 and self.ids.monto_gas_viaje.text.startswith('$') == False:
self.ids.monto_gas_viaje.text = self.ids.monto_gas_viaje.text[:-1]
def sumar_gasto(self):
if self.ids.monto_gas_viaje.text == "":
pass
elif self.ids.monto_gas_viaje.text.startswith('$'):
pass
else:
travel_manager = MDApp.get_running_app().root.get_screen('travelManager')
monto_total = float(travel_manager.ids.suma_solic_viaje.text[2:])
monto_total += float(self.ids.monto_gas_viaje.text)
travel_manager.ids.suma_solic_viaje.text = "$ " + str(monto_total)
self.apply_currency_format()
# USE THIS METHOD TO UPDATE THE VALUE OF GASOLINA (donut)
def update_requested_value(self):
MyContentGasolina.monto_gasolina = 0
if len(self.ids.monto_gas_viaje.text) > 0:
MyContentGasolina.monto_gasolina = self.ids.monto_gas_viaje.text
else:
MyContentGasolina.monto_gasolina = 0
TravelManagerWindow.update_donut_graph \
(MyContentGasolina.monto_gasolina)
class LoginWindow(Screen):
pass
class TravelManagerWindow(Screen):
panel_container = ObjectProperty(None)
expense_graph = ObjectProperty(None)
# EXPANSION PANEL PARA SOLICITAR GV
def set_expansion_panel(self):
self.ids.panel_container.clear_widgets()
# FOOD PANEL
self.ids.panel_container.add_widget(MDExpansionPanel(icon="food", content=MyContentAliment(),
panel_cls=MDExpansionPanelOneLine(text="Alimentacion")))
# CASETAS PANEL
self.ids.panel_container.add_widget(MDExpansionPanel(icon="food", content=MyContentCasetas(),
panel_cls=MDExpansionPanelOneLine(text="Casetas")))
# GAS PANEL
self.ids.panel_container.add_widget(MDExpansionPanel(icon="food", content=MyContentGasolina(),
panel_cls=MDExpansionPanelOneLine(text="Gasolina")))
def update_donut_graph(self):
travel_manager = MDApp.get_running_app().root.get_screen('travelManager')
travel_manager.ids.expense_graph.clear_widgets()
# create data
names = 'Alimentación', 'Casetas', 'Gasolina',
data_values = [MyContentAliment.monto_alimento, MyContentCasetas.monto_casetas,
MyContentGasolina.monto_gasolina]
# Create a white circle for the center of the plot
my_circle = plt.Circle((0, 0), 0.65, color='white')
# Create graph, add and place percentage labels
# Add spaces to separate elements from the donut
explode = (0.05, 0.05, 0.05)
plt.pie(data_values, autopct="%.1f%%", startangle=0, pctdistance=0.80, labeldistance=1.2, explode=explode)
p = plt.gcf()
p.gca().add_artist(my_circle)
# Create and place legend of the graph
plt.legend(labels=names, loc="center")
# Add graph to Kivy App
plt.show()
# THE DESIRED RESULT IS TO ADD THE GRAPH TO THE APP WITH THE LINE OF CODE BELOW, INSTEAD OF THE plt.show() line
travel_manager.ids.expense_graph.add_widget(Image(source='donut_graph_image.png'))
# WINDOW MANAGER ################################
class WindowManager(ScreenManager):
pass
class ReprodExample3(MDApp):
travel_manager_window = TravelManagerWindow()
def build(self):
self.theme_cls.primary_palette = "Teal"
return WindowManager()
if __name__ == "__main__":
ReprodExample3().run()
KV Code:
<WindowManager>:
LoginWindow:
TravelManagerWindow:
<LoginWindow>:
name: 'login'
MDRaisedButton:
text: 'Enter'
pos_hint: {'center_x': 0.5, 'center_y': 0.5}
size_hint: None, None
on_release:
root.manager.transition.direction = 'up'
root.manager.current = 'travelManager'
<TravelManagerWindow>:
name:'travelManager'
on_pre_enter: root.set_expansion_panel()
MDRaisedButton:
text: 'Back'
pos_hint: {'center_x': 0.5, 'center_y': 0.85}
size_hint: None, None
on_release:
root.manager.transition.direction = 'down'
root.manager.current = 'login'
BoxLayout:
orientation: 'vertical'
size_hint:1,0.85
pos_hint: {"center_x": 0.5, "center_y":0.37}
adaptive_height:True
height: self.minimum_height
ScrollView:
adaptive_height:True
GridLayout:
size_hint_y: None
cols: 1
row_default_height: root.height*0.10
height: self.minimum_height
BoxLayout:
adaptive_height: True
orientation: 'horizontal'
GridLayout:
id: panel_container
size_hint_x: 0.6
cols: 1
adaptive_height: True
BoxLayout:
size_hint_x: 0.05
MDCard:
id: resumen_solicitud
size_hint: None, None
size: "250dp", "350dp"
pos_hint: {"top": 0.9, "center_x": .5}
elevation: 0.1
BoxLayout:
orientation: 'vertical'
canvas.before:
Color:
rgba: 0.8, 0.8, 0.8, 1
Rectangle:
pos: self.pos
size: self.size
MDLabel:
text: 'Monto Total Solicitado'
font_style: 'Button'
halign: 'center'
font_size: (root.width**2 + root.height**2) / 15.5**4
size_hint_y: 0.2
MDSeparator:
height: "1dp"
MDTextField:
id: suma_solic_viaje
text: "$ 0.00"
bold: True
line_color_normal: app.theme_cls.primary_color
halign: "center"
size_hint_x: 0.8
pos_hint: {'center_x': 0.5, 'center_y': 0.5}
MDSeparator:
height: "1dp"
# DESIRED LOCATION FOR THE MATPLOTLIB GRAPH
MDBoxLayout:
id: expense_graph
<MyContentAliment>:
adaptive_height: True
MDBoxLayout:
orientation:'horizontal'
adaptive_height:True
size_hint_x:self.width
pos_hint: {"center_x":0.5, "center_y":0.5}
spacing: dp(10)
padding_horizontal: dp(10)
MDLabel:
text: 'Monto:'
multiline: 'True'
halign: 'center'
pos_hint: {"x":0, "top":0.5}
size_hint_x: 0.15
font_style: 'Button'
font_size: 19
MDTextField:
id: monto_aliment_viaje
hint_text: 'Monto a solicitar'
pos_hint: {"x":0, "top":0.5}
halign: 'left'
size_hint_x: 0.3
helper_text: 'Ingresar el monto a solicitar'
helper_text_mode: 'on_focus'
write_tab: False
required: True
on_text: root.limit_currency()
MDRaisedButton:
id: boton_aliment_viaje
pos_hint: {"x":0, "top":0.5}
text:'Ingresar Gasto'
on_press:
root.update_requested_value()
on_release:
root.sumar_gasto()
### CASETAS
<MyContentCasetas>:
adaptive_height: True
MDBoxLayout:
orientation:'horizontal'
adaptive_height:True
size_hint_x:self.width
pos_hint: {"center_x":0.5, "center_y":0.5}
spacing: dp(10)
padding_horizontal: dp(10)
MDLabel:
text: 'Monto:'
multiline: 'True'
halign: 'center'
pos_hint: {"x":0, "top":0.5}
size_hint_x: 0.15
font_style: 'Button'
font_size: 19
MDTextField:
id: monto_casetas_viaje
hint_text: 'Monto a solicitar'
pos_hint: {"x":0, "top":0.5}
halign: 'left'
size_hint_x: 0.3
helper_text: 'Ingresar el monto a solicitar'
helper_text_mode: 'on_focus'
write_tab: False
#input_filter: 'float'
required: True
on_text: root.limit_currency()
MDRaisedButton:
id: boton_casetas_viaje
pos_hint: {"x":0, "top":0.5}
text:'Ingresar Gasto'
on_press:
root.update_requested_value()
on_release:
root.sumar_gasto()
BoxLayout:
size_hint_x: 0.05
### GASOLINA
<MyContentGasolina>:
adaptive_height: True
MDBoxLayout:
orientation:'horizontal'
adaptive_height:True
size_hint_x:self.width
pos_hint: {"center_x":0.5, "center_y":0.5}
spacing: dp(10)
padding_horizontal: dp(10)
MDLabel:
text: 'Monto:'
multiline: 'True'
halign: 'center'
pos_hint: {"x":0, "top":0.5}
size_hint_x: 0.15
font_style: 'Button'
font_size: 19
MDTextField:
id: monto_gas_viaje
hint_text: 'Monto a solicitar'
pos_hint: {"x":0, "top":0.5}
halign: 'left'
size_hint_x: 0.3
helper_text: 'Ingresar el monto a solicitar'
helper_text_mode: 'on_focus'
write_tab: False
required: True
on_text: root.limit_currency()
MDRaisedButton:
id: boton_gas_viaje
pos_hint: {"x":0, "top":0.5}
text:'Ingresar Gasto'
on_press:
root.update_requested_value()
on_release:
root.sumar_gasto()
BoxLayout:
size_hint_x: 0.05
Any suggestions or corrections of my code will be greatly appreciated. Thanks a lot in advance.
EDIT I managed to link the MDTextFields to the data values in the graph. So the graph will update as values are entered. Every time you add a value, an updated graph will appear so you can see it for yourself (code of minimal reproducible example is already updated). I am, nevertheless, still unable to add the graph to my App. I will greatly appreciate your help. Thanks a lot in advance!
EDIT #2
I changed my approach, I decided to convert the graph into an Image, and add the Image to a MDBoxLayout. (If the first approach is better please let me know). The code is already updated. However I get an error:
self.ids.expense_graph.add_widget(updated_graph)
AttributeError: 'str' object has no attribute 'ids'
I have searched on the web for different solutions to this error however I can't fix this.
EDIT 3
So I finally was able to solve the error code described on EDIT 2. I am able to add my graph correctly to the App. However the graph is not updated with new expenses (although the file does update and the plt.show() line of code does show an updated graph). Any idea why the graph in the app is failing to update? Code for Minimal Reproducible Example is already updated.
回答1:
I think you just need to rebuild the plot with each change. Try changing your update_donut_graph()
to:
def update_donut_graph(self):
plt.clf() # clear the plot
travel_manager = MDApp.get_running_app().root.get_screen('travelManager')
travel_manager.ids.expense_graph.clear_widgets()
# create data
names = 'Alimentación', 'Casetas', 'Gasolina',
data_values = [MyContentAliment.monto_alimento, MyContentCasetas.monto_casetas,
MyContentGasolina.monto_gasolina]
# Create a white circle for the center of the plot
my_circle = plt.Circle((0, 0), 0.65, color='white')
# Create graph, add and place percentage labels
# Add spaces to separate elements from the donut
explode = (0.05, 0.05, 0.05)
plt.pie(data_values, autopct="%.1f%%", startangle=0, pctdistance=0.80, labeldistance=1.2, explode=explode)
p = plt.gcf()
p.gca().add_artist(my_circle)
# Create and place legend of the graph
plt.legend(labels=names, loc="center")
# Add graph to Kivy App
# plt.show()
# THE DESIRED RESULT IS TO ADD THE GRAPH TO THE APP WITH THE LINE OF CODE BELOW, INSTEAD OF THE plt.show() line
travel_manager.ids.expense_graph.add_widget(FigureCanvasKivyAgg(figure=plt.gcf()))
回答2:
Thanks for your reply @John Anderson . As always, you've been of great help. I encounter however a simple problem. The size of my graph is modified, it is considerably smaller, yet the size of the legend remains the same. So instead of showing the graph how I desire, the legend now covers up the graph.
Is there a way I can prevent this? I've been thinking maybe a way to make the graph to take up the entire canvas size will be appropiate. As you can see in donut_graph_image.png screenshot, there is a lot of whitespace around the graph. Or a command that will help me maintain the sizes of the graph? I tried with Axes.set_aspect but this did worked.
Thanks again.
来源:https://stackoverflow.com/questions/65603075/add-a-matplotlib-graph-to-a-widget-in-kivymd