Python 将Matplotlib图添加到KivyMD中的小部件

Python 将Matplotlib图添加到KivyMD中的小部件,python,matplotlib,kivy,kivymd,matplotlib-widget,Python,Matplotlib,Kivy,Kivymd,Matplotlib Widget,我目前正在使用KivyMD创建一个移动应用程序,用于管理差旅费用申请。用户将在MDTEXT字段中输入不同类型费用的所需请求金额。我想将使用patplotlib制作的圆环图添加到MDBoxLayout中。当请求被填写时,这样的图表应该自动更新。(为了清晰起见,我将包括一个屏幕截图。红色的正方形是我的图形所需的位置) 我创建了一个名为update_method_graph的方法,并使用了固定的数字,我可以成功地创建一个绘图,但是我没有成功地在应用程序中添加这样的图形。一旦我能成功地将图表添加到我的

我目前正在使用KivyMD创建一个移动应用程序,用于管理差旅费用申请。用户将在MDTEXT字段中输入不同类型费用的所需请求金额。我想将使用patplotlib制作的圆环图添加到MDBoxLayout中。当请求被填写时,这样的图表应该自动更新。(为了清晰起见,我将包括一个屏幕截图。红色的正方形是我的图形所需的位置)

我创建了一个名为update_method_graph的方法,并使用了固定的数字,我可以成功地创建一个绘图,但是我没有成功地在应用程序中添加这样的图形。一旦我能成功地将图表添加到我的应用程序中,我会将这些值链接到用户添加的请求。现在我关心的是正确地添加图表。当然,完成的代码不包括plt.show()行,图形应该直接在应用程序上更新

现在,当我关闭图形窗口时,我的代码在

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__'`
费用图中的关键错误

我已尝试使用kivy.garden.matplotlib.backend中的
_kivyaggimport figure canvaskivyagg
以及
matplotlib.use('module://kivy.garden.matplotlib.backend_kivy)
,如中所述,但我仍然无法使我的应用程序正常运行

最小可重复示例代码

Python代码:

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()
<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
我已经在网上搜索了这个错误的不同解决方案,但是我无法修复这个问题

编辑3

因此,我终于能够解决Edit2中描述的错误代码。我能够将我的图表正确添加到应用程序中。但是,图形不会用新的费用更新(尽管文件会更新,并且plt.show()代码行会显示更新的图形)。你知道为什么应用程序中的图表无法更新吗?最小可复制示例的代码已更新


我认为您只需要根据每次更改重新生成绘图。尝试将
更新甜甜圈图()
更改为:

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()))

感谢您的回复@johnanderson。一如既往,你帮了大忙。 然而,我遇到了一个简单的问题。我的图表的大小被修改,它相当小,但图例的大小保持不变。因此,图例不再显示我所希望的图形,而是覆盖了图形

有什么办法可以防止这种情况发生吗?我一直在想,也许一种让图形占据整个画布大小的方法是合适的。正如您在donut_graph_image.png屏幕截图中所看到的,图形周围有很多空白。或者是一个可以帮助我维护图形大小的命令?我尝试了Axis.set_方面,但这确实有效

再次感谢


您可以通过使用
fontsize
参数,如
plt.legend(labels=names,loc=“center”,fontsize='xx-small')
使图形的容器变大,或使图例变小。最后,我使用了较小的字体大小,删除了图例的框架,并使用了自动缩放(tight=True)以及子批次调整,并给出wspace=0和hspace=0。看起来正是我想要的。再次非常感谢你,约翰,祝你今天愉快。
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()))