Страницы

Поиск по вопросам

понедельник, 25 марта 2019 г.

как обновлять график matplotlib в tkinter раз в 5 минут данными из базы данных?

Что нужно: Каждые 5 секунд, считываются координаты из БД и отображаются на графике matplotlib в окне tkinter.
Что сделано: рисуются только первый набор данных и не рисуются второй набор
ВОПРОС: как исправить чтобы рисовался сначала первый набор данных , а через 5 секунд второй?
Вот обновленный минимальный рабочий код:
from threading import Thread from queue import Empty, Queue import time import tkinter as tk from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg#, NavigationToolbar2TkAgg from matplotlib.figure import Figure
class tkChartGUI(tk.Frame):
def __init__(self, parent): tk.Frame.__init__(self, parent) self.parent = parent self.initUI()
def get_latest_data(self, dataid): x_array=[] y_array=[] if (dataid == 1): x_array=[1,2,3,4,5,6,7,8,9,10,11,12,13,14]; y_array=[0.5,0.7,0.3,1.0,0.6,0.9,0.5,0.2,0.1,0.5,0.33,0.55,0.3,0.6] if (dataid == 2): x_array=[1,2,3,4,5,6,7,8,9,10,11,12,13,14]; y_array=[0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1.0,1.1,1.1,1.2,1.3] return (x_array, y_array)
def initUI(self): self.parent.title("Simple chart") self.parent.geometry("800x600+300+100")
result_queue = Queue() Thread(target=self.get_latest_data, args=[result_queue], daemon=True).start()
x_array, y_array = self.get_latest_data(1)
f = Figure(figsize=(5, 3), dpi=150) a = f.add_subplot(111) a.set_xlabel("Values_X") a.set_ylabel("Values_Y") a.yaxis.grid(True, which='major') a.xaxis.grid(True, which='major') a.plot(x_array, y_array)
canvas = FigureCanvasTkAgg(f, master=self.parent) canvas.show() canvas.get_tk_widget().grid(row=0,column=0)
def display_result(a, q): x_array = [] y_array = [] try: x_array = q.get(block=False) # get data y_array = q.get(block=False) a.plot(x_array, y_array) canvas.draw() except Empty: #a.clear() timeout_millis = round(100 - (5000 * time.time()) % 100) self.parent.after(timeout_millis, display_result, a, q)
def get_result(q): x_array, y_array = self.get_latest_data(2) q.put(x_array) # put data in FIFO queue x coords array q.put(y_array) # put data in FIFO queue y coords array
display_result(a, result_queue)
def onExit(self): self.quit()
def main(): root = tk.Tk() my_gui = tkChartGUI(root) root.mainloop()
if __name__ == '__main__': main()


Ответ

Чтобы постоянно не считывать данные из базы данных (polling), можно определить trigger, чтобы вызвать функцию (callback), когда в БД интересное событие произойдёт. К примеру, когда в нужную таблицу новое значение добавляется (Launch a Python Script from a sqlite3 Trigger):
CREATE TRIGGER tt AFTER INSERT ON t BEGIN SELECT got_y(NEW.y); END
Обновления графика происходят в got_y() обратном вызове, определённом в make_callback() ниже. Код похож на ответ c loop(), определённой с помощью .after(), с той разницей, что здесь функция вызывается, когда нужные данные готовы, вместо того чтобы периодически непрерывно новые данные запрашивать без блокировки:
#!/usr/bin/env python3 import datetime as DT import sqlite3 import random import threading import time import tkinter as tk from collections import deque
import matplotlib.pyplot as plt from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg from matplotlib.dates import date2num
def generate_data(db): # generate dummy data for the example while True: time.sleep(.1) db.execute('insert into t values(?)', (random.randrange(100),))
def make_callback(root): # plot something # see @MaxU answer https://ru.stackoverflow.com/q/801923/23044 n = 300
def get_t(): return date2num(DT.datetime.now()) xx = deque([get_t()], maxlen=n) yy = deque([0], maxlen=n) fig, ax = plt.subplots() ax.set_ylim(0, 100) line, = ax.plot_date(xx, yy, marker='') format_time = '{:%Y-%m-%d %H:%M:%S}'.format time_text = ax.text(0.5, 0.9, '', transform=ax.transAxes)
# add to GUI canvas = FigureCanvasTkAgg(fig, master=root) # A tk.DrawingArea. canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
def got_y(y): # update plot xx.append(get_t()) yy.append(int(y)) line.set_data(xx, yy) ax.fill_between(xx, 0, yy, color='lightgrey') time_text.set_text(format_time(DT.datetime.now())) ax.relim() # update axes limits ax.autoscale_view(scaley=False) canvas.draw() return got_y
def main(): root = tk.Tk() # a dummy db for the example db = sqlite3.connect(':memory:', check_same_thread=False) db.execute('create table t(y)') db.create_function('got_y', 1, make_callback(root)) db.execute('CREATE TRIGGER tt AFTER INSERT ON t BEGIN SELECT got_y(NEW.y); END') threading.Thread(target=generate_data, args=[db], daemon=True).start() root.mainloop()
if __name__ == '__main__': main()
Здесь db.create_function() определяет какая Питон-функция используется в качестве got_y(y) обратного вызова. В примере, callback вызывается в дочернем потоке, что не всегда желаемо. Поток с generate_data() используется только для примера, фактически данные могут вставляться в базу данных из другого процесса или других машин. How to receive automatic notifications about changes in tables?

Для сравнения вот вариант с polling, где база данных постоянно опрашивается в фоновом потоке, используя простой цикл (про достоинства и недостатки цикла подробно описано в Как правильно сделать временный цикл?)
def poll_db(interval=5): while True: time.sleep(interval - time.time() % interval) # avoid drift emit(get_y_from_db())
здесь get_y_from_db() делает запрос к базе данных, а emit() генерирует событие для GUI:
#!/usr/bin/env python3 import random import threading import time import tkinter as tk
def get_y_from_db(): # generate dummy data time.sleep(random.random()) # emulate blocking function return random.randrange(100)
def poll_db(emit, interval=5): while True: time.sleep(interval - time.time() % interval) # avoid drift emit(get_y_from_db())
root = tk.Tk() root.bind('<>', lambda e, f=make_callback(root): f(e.y)) # subscribe threading.Thread(target=poll_db, args=[lambda y: root.event_generate('<>', when='tail', y=y)], daemon=True).start() root.mainloop()
Пример кода (где этот метод также используется): вывод процесса показывается в Tkinter GUI с помощью root.event_generate()

Не на всех реализациях можно root.event_generate() в фоновом потоке вызвать. В таких случаях можно использовать queue, чтобы данные между потоками передавать:
#!/usr/bin/env python3 import datetime as DT import random import threading import time import tkinter as tk from collections import deque from queue import Empty, Queue from time import time as timer
import matplotlib.pyplot as plt from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg from matplotlib.dates import date2num
def get_y_from_db(): # generate dummy data time.sleep(random.random()) # emulate blocking function return random.randrange(100)
def poll_db(emit, interval=1): # NOTE: interval is independant from the delay while True: time.sleep(interval) emit(get_y_from_db())
def start_polling_loop(root, q, delay): # plot something # see @MaxU answer https://ru.stackoverflow.com/q/801923/23044 n = 600
def get_t(): return date2num(DT.datetime.now()) xx = deque([get_t()], maxlen=n) yy = deque([0], maxlen=n) fig, ax = plt.subplots() ax.set_ylim(0, 100) line, = ax.plot_date(xx, yy, marker='') format_time = '{:%Y-%m-%d %H:%M:%S}'.format time_text = ax.text(0.5, 0.9, '', transform=ax.transAxes)
# add to GUI canvas = FigureCanvasTkAgg(fig, master=root) # A tk.DrawingArea. canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
# update in a loop def loop(): timeout_millis = round(delay - (1000 * timer()) % delay) root.after(timeout_millis, loop) # avoid drift
try: y = q.get(block=False) except Empty: return # no new data
# update plot xx.append(get_t()) yy.append(y) line.set_data(xx, yy) ax.fill_between(xx, 0, yy, color='lightgrey') time_text.set_text(format_time(DT.datetime.now())) ax.relim() # update axes limits ax.autoscale_view(scaley=False) canvas.draw() root.after_idle(loop) # start
root = tk.Tk() q = Queue() threading.Thread(target=poll_db, args=[q.put], daemon=True).start() start_polling_loop(root, q, delay=40) root.mainloop()
Упрощённый вариант (с одним обновлением данных из потока), см. в Как сделать постоянное обновление окна Tkinter? Как избежать подвисания на время ожидания ответа от сервера. Ещё пример кода (где этот метод используется): вывод процесса показывается в Tkinter GUI, используя widget.after(), q.get()/q.put()

Комментариев нет:

Отправить комментарий