Search code examples
pythonplotkivykivy-language

Python Kivy Plots with MeshLinePlot confusion


I am trying to implement real time plots for measurements to a Kivy application, but I do not understand the inner workings of the kivy garden library.

My goal that I want to achieve: I want to have multiple plots inside a ScrollView such that I can add multiple real time plots programmatically from a dictionary and scroll through them if they occupy more space than one screen height. The main problem I have is that the Graph does not behave like an ordinary Widget but rather behaves like a canvas. I already tried to implement this with matplotlib as backend_kivyagg, but I failed to have fixed size for every subplot that I created.

There are multiple things that I do not understand why they happen like they happen.

from math import sin, cos

from kivy.app import App
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label
from kivy.uix.scrollview import ScrollView
from kivy.uix.widget import Widget

from kivy.garden.graph import Graph, MeshLinePlot


class Plot(Widget):
    def __init__(self):
        super(Plot, self).__init__()
        self.graph = Graph(xlabel="x", ylabel="y", x_ticks_minor=5, x_ticks_major=25, y_ticks_major=1,
                           y_grid_label=True, x_grid_label=True, x_grid=True, y_grid=True,
                           xmin=-0, xmax=100, ymin=-1, ymax=1, draw_border=False)
        # graph.size = (1200, 400)
        # self.graph.pos = self.center

        self.plot = MeshLinePlot(color=[1, 1, 1, 1])
        self.plot.points = [(x, sin(x / 10.)) for x in range(0, 101)]
        self.plot2 = MeshLinePlot(color=[1, 0, 0, 1])
        self.plot2.points = [(x, cos(x / 10.)) for x in range(0, 101)]
        self.add_widget(self.graph)

        self.graph.add_plot(self.plot)
        self.graph.add_plot(self.plot2)


class GraphLayoutApp(App):

    def build(self):
        scroll_view = ScrollView()
        grid_layout = GridLayout(cols=1, row_force_default=True, padding=20, spacing=20)
        graph = Plot()
        graph2 = Plot()
        label = Label(text="Hello World!")
        label2 = Label(text="Hello World!")
        grid_layout.add_widget(label)
        grid_layout.add_widget(graph)
        grid_layout.add_widget(label2)
        grid_layout.add_widget(graph2)
        scroll_view.add_widget(grid_layout)

        return scroll_view


if __name__ == '__main__':
    GraphLayoutApp().run()

Questions:

  1. Inside the class GraphLayoutApp I create two objects graph and graph2 but if I add them to the GridLayout() only one does appear? How can I add multiple graphs?

  2. Also Inside the build method of the class GraphLayoutApp I create two lables. I wanted to first display the first label then the graph and then the second label label2. But it seems to me that the graph is always displayed in the lower left corner. I guess this has to do with the canvas on which it is drawn, but I cannot solve it.


Solution

  • Here is a modified version with some things fixed:

    The issue here is that you made your Plot inherit from Widget, which really the most basic widget in the framework, and as it's not a layout, doesn't manage anything in the children you add to it, so the Graph widget you added to it was left at the default size/position (respectively [100, 100] and [0, 0]), and so they stacked on each others, i used RelativeLayout, which sets children by default to its own size and position, so the Graph follows the widget. Another solution is to simply make Plot inherit from Graph, since it's the only child, and to use "self.add_plot" instead of "self.graph.add_plot", and to override the Graph parameters, the best solution is probably to create a KV rule for the Plot class. But the first solution was the minimal change from your code.

    The general principle you missed is that in kivy, widgets are by default not constrained to any position/size, unless their parents are layouts that specifically manage that (like GridLayout did for your labels).

    I also made the GridLayout automatically size itself to the minimum_size (determined by the hardcoded size i put in all widgets), so you actually have something to scroll on.

    This code is also very python-driven, in kivy, you usually want to do more things using KV, rather than using add_widget for static things.

    from math import sin, cos
    
    from kivy.app import App
    from kivy.uix.gridlayout import GridLayout
    from kivy.uix.label import Label
    from kivy.uix.scrollview import ScrollView
    from kivy.uix.widget import Widget
    from kivy.uix.relativelayout import RelativeLayout
    
    from kivy_garden.graph import Graph, MeshLinePlot
    
    
    class Plot(RelativeLayout):
        def __init__(self, **kwargs):
            super(Plot, self).__init__(**kwargs)
            self.graph = Graph(xlabel="x", ylabel="y", x_ticks_minor=5, x_ticks_major=25, y_ticks_major=1,
                               y_grid_label=True, x_grid_label=True, x_grid=True, y_grid=True,
                               xmin=-0, xmax=100, ymin=-1, ymax=1, draw_border=False)
            # graph.size = (1200, 400)
            # self.graph.pos = self.center
    
            self.plot = MeshLinePlot(color=[1, 1, 1, 1])
            self.plot.points = [(x, sin(x / 10.)) for x in range(0, 101)]
            self.plot2 = MeshLinePlot(color=[1, 0, 0, 1])
            self.plot2.points = [(x, cos(x / 10.)) for x in range(0, 101)]
            self.add_widget(self.graph)
    
            self.graph.add_plot(self.plot)
            self.graph.add_plot(self.plot2)
    
    
    class GraphLayoutApp(App):
    
        def build(self):
            scroll_view = ScrollView()
            grid_layout = GridLayout(cols=1, padding=20, spacing=20, size_hint_y=None)
            grid_layout.bind(minimum_size=grid_layout.setter('size'))
            graph = Plot(size_hint_y=None, height=500)
            graph2 = Plot(size_hint_y=None, height=500)
            label = Label(text="Hello World!", size_hint_y=None)
            label2 = Label(text="Hello World!", size_hint_y=None)
            grid_layout.add_widget(label)
            grid_layout.add_widget(graph)
            grid_layout.add_widget(label2)
            grid_layout.add_widget(graph2)
            scroll_view.add_widget(grid_layout)
    
            # return grid_layout
            return scroll_view
    
    
    if __name__ == '__main__':
        GraphLayoutApp().run()