Sprache auswählen

Immer mal wieder möchte man den Verlauf von beliebigen Messwerten anzeigen. Dazu wird in der Pythonwelt ziemlich oft die Matplotlib verwendet. Da es für Kivy auch gleich noch ein passendes Backend gibt, mit dem man Matplotlib Graphen in Kivy darstellen kann, ist das sehr komfortabel.

Voraussetzungen

Wer sich bisher werder Kivy, noch die Matplotlib, noch das Backend installiert hat, der startet hier, bevor er mit dem Artikel weitermacht:

Kivy installieren

Kivy Garden Matplotlib installieren

Die Messwerte

Das Widget liest seine Daten aus einer JSON Datei. Der Aufbau und der Inhalt für unser Widget ist hier dargestellt.

[{"time_code": "2019-03-01 00:02:38", "temperature": "23", "humidity": "42"}, {"time_code": "2019-03-02 00:02:38", "temperature": "24", "humidity": "55"}, {"time_code": "2019-03-03 00:02:38", "temperature": "22", "humidity": "40"}]

Jeder Datensatz beinhaltet einen Zeitstempel (time_code), der angibt, wann der Datensatz erfasst wurde, die Temperatur (temperature) und die Luftfeuchtigkeit (humidity).

Das KV File

Wie immer beschreibt unser .kv file den grafischen Aufbau der GUI.

Unser Layout besteht aus einem BoxLayout, das als einziges Widget unseren Plot enthält. Über die Eigenschaften Units, titles und ax_colors wird beeinflusst, was und wie das Widget dargestellt wird.

Die Einheiten (units) sind in meinem Fall °C und %rH, da wir wie ein JSON file mit Temperatur- und Luftfeuchtigkeitswerten haben. Es können aber natürlich beliebige andere Daten dargestellt werden, bspw. Drehzal und Geschwindigkeit. Die Eigenschaft titles ist schlussendlich die in den Plots dargestellt Überschrift. Die Eigenschaft ax_colors beeinflusst die Farbe, in der die Plots gezeichnet werden.

Über das Event on_touch_down sorgen wir dafür, dass beim Anklicken des Fensters die Plots neu gezeichnet werden.

<MyRootLayout>:
    orientation: 'vertical'
    on_touch_down: self.ids.widget1.update_plot()
    TwoPlotsWidget:
        id: widget1
        units : ['°C','%rH']
        titles : ['Temperatur','Luftfeuchtigkeit']
        ax_colors : ['r','b']
 

<TwoPlotsWidget>:

 Das Python File

Unsere Klasse TwoPlotsWidget ist diesmal abgeleitet vom FigureCanvasKivyAgg. Dieses Backend stellt Matplotlib Grapen dar. Es ist wichtig, dass man bei der Initialisierung eine gültige Figure übergibt. Man muss unbedingt beachten, dass man diese Figure im weiteren nicht durch eine andere ersetzen kann, um den Plot zu erneuern. Man muss jedes mal die existierende Figure erst bereinigen und dann neu aufbauen.

Die Eigenschaften, die wir über das kv File beeinflussen können, binden wir auch in diesem Beispiel über das Observer Pattern an Variablen.

  • units, definiert die Einheiten, welche an den y-Achsen angezeigt werden. Falls im kv file nichts definiert wird, wird -- angezeigt
  • titles, ist der Titel, welcher über jedem der Plots angezeigt wird. Falls im kv file nichts definiert wird, wird -- angezeigt
  • ax_colors, legt die Farbe fest, in der die Plots dargestellt werden. Falls im kv file nichts definiert wird, wird jeder Plot mittels einer grünen Linie dargestellt.

Die Plots sollen eine gemeinsame x-Achse haben. Das bedeutet, dass die Beschriftungen nur an einer Achse und nicht an beiden Achsen dargestellt werden. Das spart auf kleinen Bildschirmen etwas Platz und sieht auch ansprechend aus. Man sollte hier beachten, dass es leider ein kleiner Bug/Unschöne Eigenschaft der Matplotlib ist, dass man beiden Achsen die Formatierung mitgeben muss, ansonsten kommt es zu ziemlich komischen Darstellungen. Da ich in diesem Beispiel die Achsen mittels einer Schleife bearbeite, ist das recht einfach. 

from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.properties import NumericProperty, ObjectProperty, StringProperty
import json
from kivy.garden.matplotlib.backend_kivyagg import FigureCanvasKivyAgg
import matplotlib.pyplot as plt
import matplotlib.dates as dt
from datetime import datetime
from time import gmtime, asctime, strftime, mktime, strptime


class TwoPlotsWidget(FigureCanvasKivyAgg):

    plt.style.use('dark_background')

    timestamp = []
    temperature = []
    humidity = []
    units = ObjectProperty(['--','--'])
    titles = ObjectProperty(['--','--'])
    ax_colors = ObjectProperty(['g','g'])
    notification = StringProperty('')

    def __init__(self, **kwargs):
        '''__init__ takes the figure the backend is going to work with'''
        super(TwoPlotsWidget, self).__init__(plt.gcf(), **kwargs)
        pass

    def update_plot(self, *args, **kwargs):
        '''reads the latest data, updates the figure and plots it'''

        #Read the data to show from a file and store it
        with open('graph.json', 'r') as read_file:
            data = json.load(read_file)

        #clear old data
        self.timestamp.clear()
        self.temperature.clear()
        self.humidity.clear()

        for index in data:
            self.timestamp.append(datetime.fromtimestamp(mktime(strptime(index['time_code'],'%Y-%m-%d %H:%M:%S'))))
            self.temperature.append(float(index['temperature']))
            self.humidity.append(float(index['humidity']))

        data = [self.temperature, self.humidity]

        #Clear the figure
        myfigure = plt.gcf()
        myfigure.clf()

        #Add two subplots to the figure with a shared x axis
        axes = myfigure.subplots(2,1, sharex=True)

        #Add the data to the axes and modify their axis
        for n in range(len(axes)):
            axes[n].plot_date(self.timestamp, data[n], self.ax_colors[n], xdate=True)             
            axes[n].set_ylabel(self.units[n])
            axes[n].set_title(self.titles[n])
            plt.ylim(min(data[n])-2,max(data[n])+2)
            axes[n].xaxis.set_minor_locator(dt.HourLocator(byhour=range(0,24,12)))   #show minor ticks with a step width of 12 hours
            axes[n].xaxis.set_minor_formatter(dt.DateFormatter('%H:%M'))
            axes[n].xaxis.set_major_locator(dt.DayLocator(bymonthday=range(0,31,1))) #show major ticks witha step widht of 1 day
            axes[n].xaxis.set_major_formatter(dt.DateFormatter('%d-%B'))
            axes[n].xaxis.set_tick_params(which='major', pad=15)                     #set spacing between major and minor labels

        #the axis labels for the first subplot are made invisible
        plt.setp(axes[0].get_xticklabels(which='both'), visible=False)

        #draw the figure
        myfigure.canvas.draw_idle()

class MyRootLayout(BoxLayout):
    pass

class MyWidgetApp(App):
    def build(self):
        return MyRootLayout()

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

Das Ergebnis

Fertig ist der Plot. Temperatur und Feuchtigkeit mit den entsprechenden Einheiten und Überschriften über den Plots.

 

Sonstiges

Das Beispiel hier wurde zum Plotten von Daten über mehrere Tage hinweg optimiert. Daher wird beim Erzeugen der Achsen die Funktion

axes[n].plot_date()  

verwendet. Die ist speziell dafür gemacht. Wer andere Daten darstellen möchte, sollte sich über die Funktion plot schlau machen.

Wichtig zu wissen ist auch, dass der Weg nicht der einzige ist, um Plots anzuzeigen, bzw. zu modifizieren. Eine weitere Möglichkeit ist, das TwoPlotWidget vom Typ BoxLayout zu machen. Anschließend fügt man bei jedem Update ein neues FigureKanvasKivyAgg als Widget hinzu. Bei dieser Variante kann man jedes mal eine neue Figure erstellen und ist etwas freier in den Funktionen, die man verwendet. Beispiele dafür findet man im DocString des Backend.

Da ich in meiner App generell einen schwarzen Hintergrund habe, verwende ich das Stylesheet dark_background. Es lassen sich aber hier natürlich alle andern vordefinierten Stylesheets oder ein selbst gemachtes verwenden. Um das Standard Stylesheet zu verwenden einfach die Zeile auskommentieren oder 'default' eintragen.

plt.style.use('default')

Quellcode auf GitHub

https://github.com/HaikoKrais/TwoPlotsWidget