【问题标题】:Add a Matplotlib Graph to a Widget in KivyMD在 KivyMD 中将 Matplotlib 图形添加到小部件
【发布时间】:2021-04-12 15:08:10
【问题描述】:

我目前正在使用 KivyMD 创建一个移动应用程序,用于管理差旅费用请求。用户将在 MDTextField 上为不同类型的费用输入所需的请求金额。我想将用 patplotlib 制作的 donut 图添加到 MDBoxLayout 中。当请求被填充时,此类图表应自动更新。 (为清楚起见,我将附上屏幕截图。红色方块是我的图表所需的位置)。

我创建了一个名为 update_method_graph 的方法并使用了固定数字,我可以成功地创建一个 Plot,但是我在应用程序上添加这样的图表并没有成功。一旦我可以成功地将图表添加到我的应用程序中,我会将这些值链接到用户添加的请求。现在我关心的是正确添加图表。当然完成的代码不会包含 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__'`

expense_graph中有关键错误。

我已尝试使用 an answer to a similar question 中建议的 from kivy.garden.matplotlib.backend_kivyagg import FigureCanvasKivyAggmatplotlib.use('module://kivy.garden.matplotlib.backend_kivy'),就像在 examples of use in garden.matplotlib 中所做的那样,但我仍然无法让我的应用程序运行。

最小可重复示例的代码

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

KV 代码:

<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

对我的代码的任何建议或更正将不胜感激。提前非常感谢。

编辑 我设法将 MDTextFields 链接到图中的数据值。因此,图表将随着值的输入而更新。每次添加值时,都会出现一个更新的图表,以便您自己查看(最小可重现示例的代码已经更新)。尽管如此,我仍然无法将图表添加到我的应用程序中。我将非常感谢您的帮助。提前非常感谢!

编辑#2

我改变了方法,我决定将图形转换为图像,并将图像添加到 MDBoxLayout。 (如果第一种方法更好,请告诉我)。代码已经更新。但是我得到一个错误:

self.ids.expense_graph.add_widget(updated_graph)
 AttributeError: 'str' object has no attribute 'ids'

我已经在网上搜索了这个错误的不同解决方案,但是我无法解决这个问题。

编辑 3

所以我终于能够解决 EDIT 2 中描述的错误代码。我能够正确地将我的图表添加到应用程序中。然而,图表并没有更新新费用(尽管文件确实更新并且 plt.show() 代码行确实显示了更新的图表)。知道为什么应用程序中的图表无法更新吗? Minimal Reproducible Example 的代码已经更新。

【问题讨论】:

    标签: python matplotlib kivy kivymd matplotlib-widget


    【解决方案1】:

    我认为您只需要在每次更改时重建情节。尝试将您的 update_donut_graph() 更改为:

    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】:

      感谢您的回复@John Anderson。和往常一样,你帮了大忙。 但是我遇到了一个简单的问题。我的图表的大小已修改,它要小得多,但图例的大小保持不变。因此,图例不再显示我想要的图形,而是覆盖了图形。

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

      再次感谢。

      【讨论】:

      • 您可以使用fontsize参数,如plt.legend(labels=names, loc="center", fontsize='xx-small'),使图形的容器更大,或使图例更小。
      • 我最终使用了较小的字体大小,删除了图例的框架,并使用了 autoscale(tight=True),加上 subplots_adjust ,并给出了 wspace = 0 和 hspace = 0。看起来正是我想要的它。再次非常感谢约翰,祝你有美好的一天。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-02-11
      • 1970-01-01
      • 2022-01-17
      • 2021-01-18
      相关资源
      最近更新 更多