|
- #!/usr/bin/env python3
- # -*- coding: utf-8; mode: python; tab-width: 4; indent-tabs-mode: nil -*-
- # vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 fileencoding=utf-8
-
- from tkinter import ttk
- import tkinter as tk
- import queue
-
- from matplotlib.pyplot import autoscale
-
- from matplotlib.backends.backend_tkagg import (
- FigureCanvasTkAgg, NavigationToolbar2Tk)
- from matplotlib.figure import Figure
- import matplotlib.animation as animation
- import matplotlib.pyplot as plt
- import matplotlib
- matplotlib.use("TkAgg")
-
-
-
- #options:
- # {
- # 'subplots': [{'title': 'name', 'ylim': [-100, 100], 'xlim': [-100, 100], 'colors': ['red', 'blue'], 'labels': ['line1', 'line2'], 'xlabel': 'x', 'ylabel': 'y', 'autoscale': True}],
- # 'layout': {'row': 3, 'column': 3, 'pos': [[]]},
- # 'interval': 30
- # }
-
-
- class MySubFigure:
- def __init__(self, ax, config: dict):
- # Subplot
- self.__ax = ax
- self.__config = config
- self.__lines = []
- colors = []
-
- if 'colors' in self.__config:
- if 'labels' not in self.__config:
- raise Exception("colors and labels must be existent at the same time!")
-
- for i in self.__config['colors']:
- self.__lines.append([[], []])
- colors.append(i)
- self.__line_objs = []
-
- self.__autoscale = True if 'autoscale' in self.__config and self.__config['autoscale'] else False
- self.__xlim = [999999999, -999999990] # min, max
- self.__ylim = [999999999, -999999990] # min, max
-
- self.__ax.set_title(self.__config['title'])
- self.__ax.set_xlabel(self.__config['xlabel'])
- self.__ax.set_ylabel(self.__config['ylabel'])
-
- if 'xlim' in self.__config:
- self.__ax.set_xlim(self.__config['xlim'][0], self.__config['xlim'][1])
-
- if 'ylim' in self.__config:
- self.__ax.set_ylim(self.__config['ylim'][0], self.__config['ylim'][1])
-
- self.__ax.grid(True)
-
- if len(self.__lines) == 0:
- # Only one line
- self.__lines.append([[], []])
- self.__line_objs.append(self.__ax.plot(self.__lines[0][0], self.__lines[0][1])[0])
- else:
- for i_ in range(len(self.__lines)):
- self.__line_objs.append(self.__ax.plot(self.__lines[i][0], self.__lines[i][1], color=colors[i], label=self.__config['labels'][i])[0])
-
- self.___ax.legend(self.__line_objs, [l.get_label() for l in self.__line_objs])
-
- def update(self, timestamp, lines):
- for i in range(len(lines)):
- x = timestamp
- y = lines[i]
-
- self.__lines[i][0].append(x)
- self.__lines[i][1].append(y)
-
- if self._autoscale:
- # Update xlim and ylim
- if self.__xlim[0] > x:
- self.__xlim[0] = x
-
- if self.__xlim[1] < x:
- self.__xlim[1] = x
-
- if self.__ylim[0] > y:
- self.__ylim[0] = y
-
- if self.__ylim[1] < y:
- self.__ylim[1] = y
-
- self.__line_objs[i].set_data(self.__lines[i][0], self.__lines[i][1])
-
- self.__do_autoscale()
-
- return self.__line_objs
-
- def __do_autoscale(self):
- if not self.__autoscale:
- return
-
- lo, hi = self.__ax.get_xlim()
- bt, tp = self.__ax.get_ylim()
-
- need_update_x = False
- need_update_y = False
-
- if lo > self.__xlim[0]:
- lo = self.__xlim[0]
- need_update_x = True
-
- if hi < self.__xlim[1]:
- hi = self.__xlim[1]
- need_update_x = True
-
- if bt > self.__ylim[0]:
- bt = self.__ylim[0]
- need_update_y = True
-
- if tp < self.__ylim[1]:
- tp = self.__ylim[1]
- need_update_y = True
-
- if need_update_x:
- self.__ax.set_xlim(*self.__xlim)
-
- if need_update_y:
- self.__ax.set_ylim(*self.__ylim)
-
- @property
- def line_objs(self) -> list:
- return self.__line_objs
-
- def clear(self):
- for line in self.__lines:
- line[0].clear()
- line[1].clear()
-
-
- class MultiScope(tk.Frame):
- def __init__(self, parent, config, **options):
- tk.Frame.__init__(self, parent, **options)
- self.__config = config
- self._parent = parent
- self._sub_figs = []
- self.__line_objs = []
-
- if 's_ubplots' not in self.__config:
- raise Exception("subplots is required!")
-
- if 'layout' in self.__config:
- if len(self.__config['layout']['pos']) != len(self.__config['subplots']):
- raise Exception("Length of layout.pos={}, length of subplots={}, are not equal!".format(len(self.__config['layout']['pos']), len(self.__config['subplots'])))
-
- self.__interval = 30
-
- self._msg_queue = queue.Queue()
-
- if 'interval' in self.__config:
- self.__interval = self.__config['interval']
-
- self._setup_ui()
-
- def put_msg(self, timestamp, obj):
- """Caution: Always run in sub-thread"""
- self._msg_queue.put_nowait((timestamp, obj))
-
- def clear(self):
- for sub_fig in self._sub_figs:
- sub_fig.clear()
-
- self.__fig.canvas.draw()
- self.__fig.canvas.flush_events()
-
- def _setup_ui(self):
- self.__fig = plt.figure(figsize=(5, 4), constrained_layout=True, dpi=100)
-
- gs = self.__fig.add_gridspec(self.__config['layout']['row'], self.__config['layout']['column'])
-
- for i in range(len(self.__config['subplots'])):
- ax = self.__fig.add_subplot(gs[self.__config['layout']['pos'][i][0], self.__config['layout']['pos'][i][1]])
- my_sub_figure = MySubFigure(ax, self.__config['subplots'][i])
- self._sub_figs.append(my_sub_figure)
- self.__line_objs.extend(my_sub_figure.line_objs)
-
- self.show(_)
-
- def __update_data(self, _):
- while self._msg_queue.qsize() > 0:
- timestamp, dataset = self._msg_queue.get_nowait()
- for i in range(len(dataset)):
- self._sub_figs[i].update(timestamp, dataset[i])
-
- return self.__line_objs
-
- def show(self):
- canvas = FigureCanvasTkAgg(self.__fig, self)
-
- if 'toolbar' in self.__config and self.__config['toolbar']:
- toolbar = NavigationToolbar2Tk(canvas, self)
- toolbar.update()
-
- canvas.get_tk_widget().pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True)
-
- self.__animation = animation.FuncAnimation(
- fig=self.__fig, func=self.__update_data, interval=self.__interval, blit=True)
|