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