def __init__(self, *args, **kwargs): """Identical to Slider.__init__, except for the "increment" kwarg. "increment" specifies the step size that the slider will be discritized to.""" self.inc = kwargs.pop('increment', 0.5) self.discrete_val = round(kwargs['valinit'] / self.inc) * self.inc Slider.__init__(self, *args, **kwargs)
def __init__(self, *args, **kwargs): """Identical to Slider.__init__, except for the "increment" kwarg. "increment" specifies the step size that the slider will be discritized to.""" self.inc = kwargs.pop("increment", 1.0) Slider.__init__(self, *args, **kwargs) self.dval = int(self.val + 0.5)
def __init__(self, *args, **kwargs): """ Identical to Slider.__init__, except for the "increment" kwarg. "increment" specifies the step size that the slider will be discretized to. """ self.inc = kwargs.pop('increment', 0.5) Slider.__init__(self, *args, **kwargs)
def __init__(self, *args, **kwargs): """ Identical to Slider.__init__, except for the new keyword 'allowed_vals'. This keyword specifies the allowed positions of the slider """ self.allowed_vals = kwargs.pop('allowed_vals', None) self.previous_val = kwargs['valinit'] Slider.__init__(self, *args, **kwargs) if self.allowed_vals == None: self.allowed_vals = [self.valmin, self.valmax]
def __init__(self, *args, **kwargs): self.inc = kwargs.pop('increment', 1.0) self.param = kwargs.pop('param') self.item = kwargs.pop('item') self.val = None Slider.__init__(self,*args, **kwargs) self.on_changed( self.replot ) return
def __init__(self, *args, **kwargs): self.previous_val = kwargs['valinit'] Slider.__init__(self, *args, **kwargs)
def __init__(self, *args, **kwargs): """ "increment" specifies the step size that the slider will be discritized to.""" self.inc = kwargs.pop('increment', 0.5) Slider.__init__(self, *args, **kwargs)
def __init__( self, ax, sliderLabel, depths, updateFunc, selectedDepthColor, fontsize=8, valInit=0, **kwargs, ): # The color of the currently displayed depth page. self.selectedDepthColor = selectedDepthColor self.nonSelectedDepthColor = "w" self.depths = depths # Make the selection depth buttons self.depthSelections = [] numDepths = float(len(depths)) rectangleBot = 0 textYCoord = 0.5 # startBoundaries go from zero to just below 1. leftBoundary = [i / numDepths for i, _depths in enumerate(depths)] for leftBoundary, depth in zip(leftBoundary, depths): # First depth (leftBoundary==0) is on, rest are off. if leftBoundary == 0: color = self.selectedDepthColor else: color = self.nonSelectedDepthColor depthSelectBox = matplotlib.patches.Rectangle( (leftBoundary, rectangleBot), 1.0 / numDepths, 1, transform=ax.transAxes, facecolor=color, ) ax.add_artist(depthSelectBox) self.depthSelections.append(depthSelectBox) # Make text halfway into box textXCoord = leftBoundary + 0.5 / numDepths ax.text( textXCoord, textYCoord, "{:.1f}".format(depth), ha="center", va="center", transform=ax.transAxes, fontsize=fontsize, ) # Make forward and backward button backwardArrow, forwardArrow = "$\u25C0$", "$\u25B6$" divider = axes_grid1.make_axes_locatable(ax) buttonWidthPercent = "5%" backwardAxes = divider.append_axes("right", size=buttonWidthPercent, pad=0.03) forwardAxes = divider.append_axes("right", size=buttonWidthPercent, pad=0.03) self.backButton = matplotlib.widgets.Button( backwardAxes, label=backwardArrow, color=self.nonSelectedDepthColor, hovercolor=self.selectedDepthColor, ) self.backButton.label.set_fontsize(fontsize) self.backButton.on_clicked(self.previous) self.forwardButton = matplotlib.widgets.Button( forwardAxes, label=forwardArrow, color=self.nonSelectedDepthColor, hovercolor=self.selectedDepthColor, ) self.forwardButton.label.set_fontsize(fontsize) self.forwardButton.on_clicked(self.next) # init at end since slider will set val to 0, and it needs to have state # setup before doing that Slider.__init__(self, ax, sliderLabel, 0, len(depths), valinit=0, **kwargs) self.on_changed(updateFunc) self.set_val(valInit) # need to set after updateFunc is added. # Turn off value visibility since the buttons text shows the value self.valtext.set_visible(False)
def __init__(self, *args, **kwargs): self.inc = kwargs.pop('increment', 1) self.valfmt = '%s' Slider.__init__(self, *args, **kwargs)
def __init__(self, *args, **kwargs): """Identical to Slider.__init__, except for the "increment" kwarg. "increment" specifies the step size that the slider will be discritized to.""" self.inc = kwargs.pop('increment') Slider.__init__(self, *args, **kwargs)
def __init__(self, *args, **kwargs): """Identical to Slider.__init__, except for the "steps" kwarg. "steps" is a list of discretized values for the slider.""" self.steps = kwargs.pop('steps', None) Slider.__init__(self, *args, **kwargs)
def __init__(self, *args, **kwargs): self.inc = kwargs.pop('increment', 1) self.valfmt = '%s' Slider.__init__(self, *args, **kwargs) self.changecallback = lambda v: ()
def __init__(self, *args, **kwargs): self.previous_val = kwargs['valinit'] Slider.__init__(self, *args, **kwargs)
class guiMenu: fig = None gauss_params = [33, 7] logTextLabels = [] logText = [] axMisc = None axAlgorithm = None axSaveOptions = None axGauss = None axLog = None axRadio = None axLogLabels = None line_gaussian = None ax_window_size = None slider_window_size = None ax_sigma = None slider_sigma = None originalStdOut = None #------------------------------------------------------------------------------ # Class initialization # def __init__(self): self.strTitle = 'Gesture Analysis Configuration - ' + settings.appVersion self.fig = plt.figure() self.fig.canvas.set_window_title(self.strTitle) self.fig.set_size_inches((settings.screen_cx * 0.49) / self.fig.dpi, (settings.screen_cy * 0.465) / self.fig.dpi) self.fig.canvas.mpl_connect('resize_event', self.onresize) self.fig.canvas.mpl_connect('close_event', self.onclose) self.fig.edgecolor = 'blue' self.fig.set_facecolor('0.90') left = 0.03 # the left side of the subplots of the figure right = 0.97 # the right side of the subplots of the figure bottom = 0.04 # the bottom of the subplots of the figure top = 0.92 # the top of the subplots of the figure wspace = 0.3 # the amount of width reserved for blank space between subplots hspace = 0.7 # the amount of height reserved for white space between subplots plt.subplots_adjust(left=left, top=top, right=right, bottom=bottom, wspace=wspace, hspace=hspace) self.axMisc = plt.subplot2grid((6, 4), (0, 0), rowspan=1, colspan=1, aspect='auto', anchor='NW') self.axAlgorithm = plt.subplot2grid((6, 4), (1, 0), rowspan=2, colspan=1, aspect='auto', anchor='NW') self.axSaveOptions = plt.subplot2grid((6, 4), (3, 0), rowspan=3, colspan=1, aspect='equal', anchor='NW') self.axGauss = plt.subplot2grid((6, 4), (0, 1), rowspan=4, colspan=3) self.axLog = plt.subplot2grid((6, 4), (5, 1), rowspan=1, colspan=3, aspect='auto', anchor='NW') # create the various groups of UI controls self.createUIMiscellaneous() self.createUILog() self.createUIAlgorithm() self.createUIGaussianFilterControls() self.createUIMiscellaneousControls() # set settings.tkGuiCanvas for use for modal dialogs try: if (self.fig is not None) and (self.fig.canvas is not None) and ( self.fig.canvas._tkcanvas is not None): settings.tkGuiCanvas = self.fig.canvas._tkcanvas except Exception as e: pass #------------------------------------------------------------------------------ # def get_aspect(self, ax): # Total figure size figW, figH = ax.get_figure().get_size_inches() # Axis size on figure _, _, w, h = ax.get_position().bounds # Ratio of display units disp_ratio = (figH * h) / (figW * w) # Ratio of data units # Negative over negative because of the order of subtraction data_ratio = sub(*ax.get_ylim()) / sub(*ax.get_xlim()) return disp_ratio / data_ratio #------------------------------------------------------------------------------ # def createUIAlgorithm(self): algorithm = settings.application.algorithm.lower() defaultAlgoritmIdx = 0 if (algorithm == 'total'): defaultAlgoritmIdx = 0 elif (algorithm == 'parallel'): defaultAlgoritmIdx = 1 elif (algorithm == 'naive'): defaultAlgoritmIdx = 2 self.axAlgorithm.set_title('Algorithm', x=0, horizontalalignment='left') # create an axis to host to radio buttons. make its aspect ratio # equal so the radiobuttons stay round aspect = self.get_aspect(self.axAlgorithm) rect = [0, 0, 1.0 * aspect, 1.0] ip = InsetPosition(self.axAlgorithm, rect) self.axRadio = plt.axes(rect) self.axRadio.set_axes_locator(ip) self.axRadio.axis('off') self.radioAlgorithm = RadioButtons( self.axRadio, ('Total Energy', 'Parallel Energy', 'Naive (no filter)'), active=defaultAlgoritmIdx) self.radioAlgorithm.on_clicked(self.onClickAlgorithm) #------------------------------------------------------------------------------ # def createUIGaussianFilterControls(self): axcolor = 'lightgoldenrodyellow' rect = [0.82, -0.158, 0.14, 0.07] ax_btnapply = plt.axes(rect) ip = InsetPosition(self.axGauss, rect) #posx, posy, width, height ax_btnapply.set_axes_locator(ip) self.btnApply = Button(ax_btnapply, 'Apply') self.btnApply.on_clicked(self.onclickApply) rect = [0.82, -0.245, 0.14, 0.07] ax_btnreset = plt.axes(rect) ip = InsetPosition(self.axGauss, rect) #posx, posy, width, height ax_btnreset.set_axes_locator(ip) self.btnReset = Button(ax_btnreset, 'Reset', color='0.950', hovercolor='0.975') self.btnReset.on_clicked(self.onclickReset) rect = [0.1, -0.155, 0.55, 0.04] self.ax_window_size = plt.axes(rect, facecolor=axcolor) ip = InsetPosition(self.axGauss, rect) #posx, posy, width, height self.ax_window_size.set_axes_locator(ip) self.slider_window_size = Slider(self.ax_window_size, 'Window Size', 1, (self.gauss_params[0] + 1) * 2 + 1, valinit=self.gauss_params[0], valstep=2) self.slider_window_size.on_changed(self.updateGaussianFilter) rect = [0.1, -0.235, 0.55, 0.04] self.ax_sigma = plt.axes(rect, facecolor=axcolor) ip = InsetPosition(self.axGauss, rect) #posx, posy, width, height self.ax_sigma.set_axes_locator(ip) self.slider_sigma = Slider(self.ax_sigma, 'Sigma', 1, (self.gauss_params[1] + 1) * 2, valinit=self.gauss_params[1], valstep=1) self.slider_sigma.on_changed(self.updateGaussianFilter) self.updateGaussianFilter() #------------------------------------------------------------------------------ # def createUIMiscellaneous(self): self.axMisc.set_title('', x=0, horizontalalignment='left') # removing top and right borders self.axMisc.xaxis.set_visible(False) self.axMisc.yaxis.set_visible(False) # remove ticks self.axSaveOptions.set_xticks([]) self.axSaveOptions.set_yticks([]) # remove ticks self.axAlgorithm.set_xticks([]) self.axAlgorithm.set_yticks([]) bbox = self.axMisc.get_window_extent() self.axMisc.set_xlim(0, bbox.width) self.axMisc.set_ylim(0, bbox.height) #------------------------------------------------------------------------------ # def createUILog(self): self.axLog.set_title('Console Log', x=0, horizontalalignment='left') # removing top and right borders self.axLog.xaxis.set_visible(False) self.axLog.yaxis.set_visible(False) self.resetLogLabels() # redirect console messages to gui's log self.originalStdOut = sys.stdout sys.stdout = CaptureOutput() # ----------------------------------------------------------------------------- # def resetLogLabels(self): cnt = len(self.logTextLabels) for i in range(0, cnt): self.logTextLabels[i].remove() self.logTextLabels = [] bbox = self.axLog.get_window_extent() self.axLog.set_xlim(0, bbox.width) self.axLog.set_ylim(0, bbox.height) aspect = self.get_aspect(self.axLog) rect = [0, 0, 1.0 * aspect, 1.0] ip = InsetPosition(self.axLog, rect) if (self.axLogLabels is None): self.axLogLabels = plt.axes(rect) else: self.axLogLabels.set_position(rect) self.axLogLabels.set_axes_locator(ip) self.axLogLabels.axis('off') aspectLog = 1.0 / self.get_aspect(self.axLog) strText = 'Tyg' tmp, self.logTextHeight = settings.getTextExtent(self.axLog, strText) self.logTextHeight = self.logTextHeight * aspectLog # * self.fig.dpi # pre-create empty log label placeholders self.logTextLabels = [] y = (self.logTextHeight / 4.0) cy = bbox.height idx = len(self.logText) - 1 while (y < cy): str = self.logText[idx] if (idx >= 0) else '' idx = idx - 1 lbl = self.axLogLabels.text( 8.0, y, str, horizontalalignment='left', verticalalignment='bottom', color='dimgray', clip_on=True, transform=self.axLog.transData ) #, bbox={'facecolor':'lightgray', 'alpha':0.7, 'pad':0.0}) self.logTextLabels.append(lbl) y += self.logTextHeight # ----------------------------------------------------------------------------- # def createUIMiscellaneousControls(self): rect = [0.06, 0.30, 0.70, 0.40] ip = InsetPosition(self.axMisc, rect) #posx, posy, width, height ax_btnbrowse = plt.axes(rect) ax_btnbrowse.set_axes_locator(ip) self.btnBrowse = Button(ax_btnbrowse, 'Input File') self.btnBrowse.on_clicked(self.onclickBrowse) self.axSaveOptions.set_title('Output Files', x=0, horizontalalignment='left') x = 0.06 dx = 0.70 # 0.80 dy = 0.10 # 0.17 cy = 0.14 # 0.24 y = 0.80 rect = [x, y, dx, dy] ip = InsetPosition(self.axSaveOptions, rect) #posx, posy, width, height ax_btn = plt.axes(rect) ax_btn.set_axes_locator(ip) self.btnSaveJSON = Button(ax_btn, 'JSON') self.btnSaveJSON.on_clicked(self.onclickSaveJSON) rect = [x, y - 1 * cy, dx, dy] ip = InsetPosition(self.axSaveOptions, rect) #posx, posy, width, height ax_btn = plt.axes(rect) ax_btn.set_axes_locator(ip) self.btnSaveTXT = Button(ax_btn, 'Text') self.btnSaveTXT.on_clicked(self.onclickSaveTXT) rect = [x, y - 2 * cy, dx, dy] ip = InsetPosition(self.axSaveOptions, rect) #posx, posy, width, height ax_btn = plt.axes(rect) ax_btn.set_axes_locator(ip) self.btnSaveFilterView = Button(ax_btn, 'Filter Graph') self.btnSaveFilterView.on_clicked(self.onclickSaveFilterView) rect = [x, y - 3 * cy, dx, dy] ip = InsetPosition(self.axSaveOptions, rect) #posx, posy, width, height ax_btn = plt.axes(rect) ax_btn.set_axes_locator(ip) self.btnSaveSkeletonView = Button(ax_btn, '3D Joint Data') self.btnSaveSkeletonView.on_clicked(self.onclickSaveSkeletonView) rect = [x, y - 4 * cy, dx, dy] ip = InsetPosition(self.axSaveOptions, rect) #posx, posy, width, height ax_btn = plt.axes(rect) ax_btn.set_axes_locator(ip) self.btnSaveScoreView = Button(ax_btn, 'Score View') self.btnSaveScoreView.on_clicked(self.onclickSaveScoreView) rect = [x, y - 5 * cy, dx, dy] ip = InsetPosition(self.axSaveOptions, rect) #posx, posy, width, height ax_btn = plt.axes(rect) ax_btn.set_axes_locator(ip) self.btnSavePNG = Button(ax_btn, 'Full Score') self.btnSavePNG.on_clicked(self.onclickSaveImage) #------------------------------------------------------------------------------ # canvas resize event # def onresize(self, event): # plt.tight_layout() # keep tha radio buttons round... if (self.axRadio is not None) and (self.axAlgorithm is not None): aspect = self.get_aspect(self.axAlgorithm) rect = [0, 0, 1.0 * aspect, 1.0] ip = InsetPosition(self.axAlgorithm, rect) self.axRadio.set_axes_locator(ip) self.axRadio.set_position(rect) self.resetLogLabels() # ----------------------------------------------------------------------------- # canvas close event # def onclose(self, event): self.fig = None # if user closes this figure, let the main application know and to exit settings.application.close() # ----------------------------------------------------------------------------- # def updateUIControls(self, algorithm): algorithm = algorithm.lower() fEnable = False if (algorithm == 'naive') else True alpha = 0.2 if (algorithm == 'naive') else 1.0 self.btnApply.set_active(fEnable) self.btnReset.set_active(fEnable) self.btnApply.label.set_alpha(alpha) self.btnReset.label.set_alpha(alpha) fUpdateGaussianPlot = False if (self.gauss_params[0] != self.slider_window_size.val): self.ax_window_size.clear() self.slider_window_size.__init__( self.ax_window_size, 'Window Size', valmin=1, valmax=(self.gauss_params[0] + 1) * 2 + 1, valinit=self.gauss_params[0], valstep=2) self.slider_window_size.on_changed(self.updateGaussianFilter) fUpdateGaussianPlot = True if (self.gauss_params[1] != self.slider_sigma.val): self.ax_sigma.clear() self.slider_sigma.__init__(self.ax_sigma, 'Sigma', valmin=1, valmax=(self.gauss_params[1] + 1) * 2, valinit=self.gauss_params[1], valstep=1) self.slider_sigma.on_changed(self.updateGaussianFilter) fUpdateGaussianPlot = True if (fUpdateGaussianPlot): self.updateGaussianFilter() self.line_gaussian.set_alpha(alpha) self.ax_window_size.patch.set_alpha(alpha) if (self.slider_window_size.poly): self.slider_window_size.poly.set_alpha(alpha) self.slider_window_size.set_active(fEnable) for r in self.slider_window_size.ax.texts: r.set_alpha(alpha) self.ax_sigma.patch.set_alpha(alpha) if (self.slider_sigma.poly): self.slider_sigma.poly.set_alpha(alpha) self.slider_sigma.set_active(fEnable) for r in self.slider_sigma.ax.texts: r.set_alpha(alpha) self.fig.canvas.draw_idle() # ----------------------------------------------------------------------------- # def updateInputName(self): self.fig.canvas.set_window_title( self.strTitle + ' - [' + settings.application.strBeautifiedInputFile + ']') # ----------------------------------------------------------------------------- # def getAlgorithmSelection(self): if (self.radioAlgorithm.value_selected == 'Total Energy'): return 'Total' elif (self.radioAlgorithm.value_selected == 'Parallel Energy'): return 'Parallel' return 'Naive' # ----------------------------------------------------------------------------- # def onClickAlgorithm(self, label): algorithm = self.getAlgorithmSelection() if (settings.application.labanotation is not None): self.gauss_params = settings.application.labanotation.getGaussianParameters( algorithm) self.updateUIControls(algorithm) settings.application.applyAlgoritm(algorithm) #------------------------------------------------------------------------------ # updateGaussianFilter() has an unused parameter, though needs it because the # sliders use this function as their update callback... def updateGaussianFilter(self, val=0): # remove current gaussian lines if (self.line_gaussian is not None): self.line_gaussian.remove() del self.line_gaussian self.line_gaussian = None gauss_params = (int(self.slider_window_size.val), int(self.slider_sigma.val)) self._t = np.arange(-gauss_params[0] / 2.0 + 0.5, gauss_params[0] / 2.0 + 0.5, 1.0) s = wf.gaussFilter(gauss_params[0], gauss_params[1]) self.line_gaussian, = self.axGauss.plot(self._t, s, marker="o", linestyle='-', color='red', lw=1) # for i, txt in enumerate(s): # self.axGauss.annotate("{:0.2f}".format(txt), (self._t[i], s[i])) wnd = int(gauss_params[0] / 2) + 1 self.axGauss.set_xlim(-wnd, wnd) self.axGauss.set_ylim(0, 0.42) # np.max(s)) self.fig.canvas.draw_idle() #------------------------------------------------------------------------------ # def onclickApply(self, event): algorithm = self.getAlgorithmSelection() self.gauss_params = (int(self.slider_window_size.val), int(self.slider_sigma.val)) if (settings.application.labanotation is not None): settings.application.labanotation.setGaussianParameters( algorithm, self.gauss_params) settings.application.applyAlgoritm(algorithm) #------------------------------------------------------------------------------ # def onclickSaveJSON(self, event): settings.application.saveJSON() #------------------------------------------------------------------------------ # def onclickSaveTXT(self, event): settings.application.saveTXT() #------------------------------------------------------------------------------ # def onclickSaveImage(self, event): settings.application.saveImage() #------------------------------------------------------------------------------ # def onclickSaveFilterView(self, event): settings.application.saveFilterView() #------------------------------------------------------------------------------ # def onclickSaveSkeletonView(self, event): settings.application.saveSkeletonView() #------------------------------------------------------------------------------ # def onclickSaveScoreView(self, event): settings.application.saveScoreView() #------------------------------------------------------------------------------ # def onclickReset(self, event): self.slider_window_size.reset() self.slider_sigma.reset() #------------------------------------------------------------------------------ # def onclickBrowse(self, event): file = self.selectInputFile() if (file is None): return settings.application.openAndProcessInputfile(file) #------------------------------------------------------------------------------ # def selectInputFile(self): fTyp = [("Kinect Joint Data File", "*.csv")] splitInput = os.path.split( os.path.abspath(settings.application.inputFilePath)) options = {} options['filetypes'] = fTyp options['initialdir'] = splitInput[0].replace('/', os.sep) if (settings.tkGuiCanvas is not None): options['parent'] = settings.tkGuiCanvas file = tkFileDialog.askopenfilename(**options) if not file: return None return file #------------------------------------------------------------------------------ # def logMessage(self, str, ioRedirect=False): # also write message to console if (self.originalStdOut is not None): extra = "\r\n" if (ioRedirect is False) else "" self.originalStdOut.write(str + extra) self.logText.append(str) cnt = len(self.logTextLabels) if (cnt > 0): for i in range(cnt - 1, 0, -1): self.logTextLabels[i].set_text( self.logTextLabels[i - 1].get_text()) self.logTextLabels[0].set_text(str) self.fig.canvas.draw_idle()
class DGILibPlot(object): """DGILibPlot The `DGILibPlot` class is responsible with plotting the electrical current (Amperes) data and gpio state data (values of `1`/`0`) obtained from an Atmel board. The X axis represents time in seconds, while the Y axis represents the electrical current in Amperes. There are two ways that the gpio pins state can be shown along with the electrical current. One is the `line` method and one is the `highlight` method. The `line` method shows a square waveform, a typical byproduct of the digital signal that gpio pins usually have. The `highlight` method highlights only particular parts of the plot with semi-transparent coloured areas, where the pins have a value of interest (set using the ``plot_pins_values`` argument of the class). Below are shown some examples of `DGILibPlot` and the two methods of drawing the gpio pins (`line`/`highlight`). **Example plots using "line" method:** .. figure:: images/plot_line_1.png :scale: 60% Figure 1: Example of plot with the 'line' method chosen for the drawing of pins. All of the pins are being plotted, so you can always see their `True`/`False` values. .. figure:: images/plot_line_2.png :scale: 60% Figure 2: The same plot with the same data as figure 1, only zoomed in. .. figure:: images/plot_line_3.png :scale: 60% Figure 3: The same plot with the same data as figure 1 and 2, only even more zoomed in. Here we can clearly see that gpio pins 0, 2 and 3 have defaulted on a single value all along the program's execution on the board. We can however clearly see the toggling of pin 1, represented in orange. **Example plots using "highlight" method:** .. figure:: images/plot_highlight_1.png :scale: 60% Figure 4: Example of plot with the 'highlight' method chosen for the drawing of pins. The time the pins are holding the value of interest (in this case, `True` value) is small every time. This is why we can see the highlighted areas looking more like vertical lines when zoomed out. The only pin being toggled by the program on the board is pin 1, hence it's why we only see one color of highlighted areas. .. figure:: images/plot_highlight_2.png :scale: 60% Figure 5: The same plot with the same data as figure 1, only zoomed in. .. figure:: images/plot_highlight_3.png :scale: 60% Figure 6: The same plot with the same data as figure 1 and 2, only even more zoomed in. Now we can see one of the the highlight area in its proper form. **Parameters** The parameters listed in the section below can be passed as arguments when initializing the `DGILibPlot` object, or as arguments to a `DGILibExtra` object. They can be included in a configuration dictionary (:py:class:`dict` object) and then unwrapped in the initialization function call of either the `DGILibPlot` object or the `DGILibExtra` object (by doing: ``dgilib_plot = DGILibPlot(**config_dict)`` or ``with DGILibExtra(**config_dict) as dgilib: ...``). Parameters ---------- dgilib_extra : DGILibExtra A `DGILibExtra` object can be specified, from where the plot can obtain the electrical current and gpio state data. If a `DGILibExtra` object is not desired to be specified however, then it should be set to `None`. (the default is `None`, meaning that measurements data (in the form of a :class:`DGILibData` should be manually called as function updates)) fig : matplotlib.pyplot.figure, optional If it is wanted so that the data is to be plotted on an already existing `matplotlib` figure, then the object representing the already instantiated figure can be specified for this parameter. For example, the electrical current data and gpio state data can be plotted in a subplot of a figure window that holds other plots as well. (the default is `None`, meaning that a new figure object will be created internally) ax : matplotlib.pyplot.axes, optional If it is wanted so that the data is to be plotted on an already existing `matplotlib` axes, then the object representing the already instantiated axes can be specified for this parameter. (the default is `None`, meaning that a new axes object will be created internally) ln : matplotlib.pyplot.lines2d, optional If it is wanted so that the data is to be plotted on an already existing `matplotlib` `Lines2D` object, then the object representing the already instantiated `Lines2D` object can be specified for this parameter. (the default is `None`, meaning that a new `Lines2D` object will be created internally) window_title : str, optional If another window title than the default is desired to be used, it can be specified here. (the default is ``Plot of current (in amperes) and gpio pins``) plot_xmax : int, optional This *initializes* the figure view to a maximum of `plot_xmax` on the X axis, where the data to be plotted. Later, the user can change the figure view using the bottom sliders of the plot figure. (the default is an arbitrary `10`) plot_ymax : int, optional This *initializes* the figure view to a maximum of `plot_xmax` on the Y axis, where the data to be plotted. Later, the user can change the figure view using the bottom sliders of the plot figure. (the default is `0.005`, meaning 5 mA, so that something as energy consuming as a blinking LED can be shown by a `DGILibPlot` with default settings) plot_pins : list(bool, bool, bool, bool), optional Set the pins to be drawn in the plot, out of the 4 GPIO available pins that the Atmel board gives data about to be sent through the computer through the Atmel Embedded Debugger (EDBG) Data Gateway Interface (DGI). (the default is `[True, True, True, True]`, meaning all pins are drawn) plot_pins_method : str, optional Set the *method* of drawing the pin. The values can be either ``"line"`` or ``"highlight"``. Refer to the above figures to see the differences. (the default is `"highlight"`) plot_pins_colors : list(str, str, str, str), optional Set the colors of the semi-transparent highlight areas drawn when using the `highlight` method, or the lines when using the `lines` method of drawing pins. (the default is `["red", "orange", "blue", "green"]`, meaning that pin 0 will have a `red` semi-transparent highlight area or line, pin 1 will have `orange` ones, and so on) automove_method : str, optional When the plot is receiving data live from the measurements taken in real-time from the board (as opposed to receiving all the data to be plotted at once, say, when reading the data from saved csv files), and `plot_xmax` is smaller than the expected size of the data in the end, then at some point the data will update past the figure view. `DGILibPlot` automatically moves the figure view to the last timestamp of the latest data received, and it can do so in two ways, depending on the value of ``automove_method``. The `page` method changes the figure view in increments of ``plot_ymax``, when the data updates past the figure view, as if the plot is turning one "page" at a time. The `latest_data` method makes the figure view always have the latest data in view, meaning that it moves in small increments so that it always keeps the latest data point `0.15` seconds away from the right edge of the figure view. The `0.15` seconds value is an arbitrary hard-coded value, chosen after some experiments. (the default is 'latest_data', meaning the plot's figure view will follow the latest data in smaller increments, keeping the latest data always on the right side of the plot) verbose : int Specifies verbosity: - 0: Silent - 1: Currently prints if data is missing, either as an object or as \ values, when :func:`update_plot` is being called. (the default is `0`) Attributes ---------- axvspans : list(4 x list(matplotlib.pyplot.axvspan)) The way the semi-transparent coloured areas for the gpio pins are drawn on the plot is by using ``matplotlib.pyplot.axvspan`` objects. The `axvspan` objects are collected in a list for potential use for later. (e.g.: such as to delete them, using the :func:`clear_pins` method). (the default is 4 empty lists, meaning no highlighting of areas of interest has occured yet) annotations : list(4 x list(str)) As we have seen in figures 4, 5, 6, for the `highlight` method of drawing pins, the counting or iteration of the highlighted areas are also showed on the plot. There are 4 pins, so therefore 4 lists of the counting/iteration stored as strings are saved by the `DGILibPlot` for later use by developers (e.g.: to replace from numbers to actual descriptions and then call the redrawing of pins). (the default is 4 empty lists, meaning no annotations for the highlighted areas of interest were placed yet) preprocessed_averages_data : list(4 x list(tuple(int, \ tuple(float, float), int, float))) As the `highlight` method draws the pins on the plot with the help of the :class:`HoldTimes` class, that means the plot knows afterwards the time intervals in which the pins have values of interest. This can be valuable for a subsequent averaging function that wants to calculate faster the average current or energy consumption of the the board activity only where it was highlighted on the plot. As such, `DGILibPlot` prepares a list of 4 lists of tuples, each for every pin, the tuple containing the iteration index of the highlighted area of interest the pins kept the same value consecutively (called a `hold` time), another tuple containing the timestamps with respect to the electrical current data, which says the beginning and end times of that `hold` interval, an index in the list of the electrical current data points where the respective hold time starts and, lastly, a `None` value, which then should be replaced with a :py:class:`float` value for the the average current or charge during that hold time. (the default is 4 empty lists, meaning no gathered preprocessed averages data yet) iterations : list(4 x int) As the plot is being updated live, the `iterations` list holds the number of highlighed areas that have been drawn already for for each pin. As the whole measurement data gets plotted, the iterations list practically holds the number of iterations the of all the areas of interest for each pin (which can be, for example, the number of `for` loops that took place in the program itself running on the board). (the default are 4 ``0`` values, meaning no areas of interest on the gpio data has been identified) last_xpos : float As the plot is being updated live, the figure view moves along with the latest data to be shown on the plot. If the user desires to stop this automatic movement and focus on some specific area of the plot, while the data is still being updated in real-time, the figure needs to detect that it should stop following the data to not disturb the user. It does so by comparing the current x-axis position value of the x axis shown, with the last x-axis position value saved, before doing any automatic movement of the figure view. If they are different, no automatic movement is being done from now on. xylim_mutex : Lock As there are many ways to move, zoom and otherwise manipulate the figure view to different sections of the plot, using the `matplotlib`'s implicit movement and zoom controls, or the freshly built `DGILibPlot` sliders appearing at the bottom (See figures), or the automatic following of the latest data feature, there is a multi-threading aspect involved and therefore a mutex should be involved, in the form of a `Lock` object, to prevent anomalies. hold_times_obj: HoldTimes Only instantiated when `highlight` method is being used for drawing pins information, the :class:`HoldTimes` class holds the algorithm that works on the live data to obtain the timestamps of areas of interest of the gpio data, in order to be highlighted on the plot. As the algorithm works on live updating data, the areas of interst can be cut off between updates of data. As such, the :class:`HoldTimes` keeps information for the algorithm to work with, in order to advert this. """ def __init__(self, dgilib_extra=None, *args, **kwargs): self.dgilib_extra = dgilib_extra # Maybe the user wants to put the power figure along with # other figures he wants self.fig = kwargs.get("fig") if self.fig is None: self.fig = plt.figure(figsize=(8, 6)) # Set window title if supplied, if not set a default self.window_title = kwargs.get( "window_title", "Plot of current (in amperes) and" + "gpio pins") self.fig.canvas.set_window_title(self.window_title) self.ax = kwargs.get("ax") if self.ax is None: self.ax = self.fig.add_subplot(1, 1, 1) self.ax.set_xlabel('Time [s]') self.ax.set_ylabel('Current [A]') # We need the Line2D object as well, to update it if (len(self.ax.lines) == 0): self.ln, = self.ax.plot([], [], 'r-', label="Power") else: self.ln = self.ax.lines[0] self.ln.set_xdata([]) self.ln.set_ydata([]) # Initialize xmax, ymax of plot initially self.plot_xmax = kwargs.get("plot_xmax", None) if self.plot_xmax is None: self.plot_xauto = True self.plot_xmax = 10 else: self.plot_xauto = False self.plot_ymax = kwargs.get("plot_ymax", None) if self.plot_ymax is None: self.plot_yauto = True self.plot_ymax = 0.005 else: self.plot_yauto = False self.plot_pins = kwargs.get("plot_pins", [True, True, True, True]) self.plot_pins_values = kwargs.get("plot_pins_values", [True, True, True, True]) self.plot_pins_method = kwargs.get("plot_pins_method", "highlight") # or "line" self.plot_pins_colors = kwargs.get("plot_pins_colors", ["red", "orange", "blue", "green"]) self.automove_method = kwargs.get("automove_method", "latest_data") # or "page" self.axvspans = [[], [], [], []] self.annotations = [[], [], [], []] self.preprocessed_averages_data = [[], [], [], []] #self.total_average = [0,0,0,0] self.iterations = [0, 0, 0, 0] self.last_xpos = 0.0 self.xylim_mutex = Lock() #self.refresh_plot_pause_secs = kwargs.get("refresh_plot_pause_secs", 0.00000001) if self.plot_pins_method == "highlight": self.hold_times_obj = HoldTimes() if self.plot_pins_method == "line": self.ax_pins = self.ax.twinx() self.ax_pins.set_ylabel('Pin Value') self.ax_pins.set_ylim([-0.1, 1.1]) self.ax_pins.set_yticks([0, 1]) self.ax_pins.set_yticklabels(["Low", "High"]) self.ln_pins = list( self.plot_pins) # Instantiate as copy of plot_pins for pin, plot_pin in enumerate(self.plot_pins): if plot_pin: self.ln_pins[pin] = self.ax_pins.plot( [], [], label=f"gpio{pin}")[0] self.ax_pins.legend(handles=[ ln_pin for ln_pin in self.ln_pins if not isinstance(ln_pin, bool) ] + [self.ln]) # Should actually check if it is a lines instance # Hardwiring these values to 0 self.plot_xmin = 0 self.plot_ymin = 0 # We need these values from the user (or from the superior class), # hence no default values # TODO: Are they really needed though? self.plot_xdiv = kwargs.get("plot_xdiv", min(5, self.plot_xmax)) self.plot_xstep = kwargs.get("plot_xstep", 0.5) self.plot_xstep_default = self.plot_xstep # self.duration = kwargs.get("duration", max(self.plot_xmax, 5)) # We'll have some sliders to zoom in and out of the plot, as well as a cursor to move around when zoomed in # Leave space for sliders at the bottom plt.subplots_adjust(bottom=0.3) # Show grid self.ax.grid() # Slider color self.axcolor = 'lightgoldenrodyellow' # Title format # Should use this https://matplotlib.org/gallery/recipes/placing_text_boxes.html self.title_avg = "Total avg curr: %1" self.title_vis = "Visible avg curr: %1" self.title_pin = "Avg curr in %1: %2" # "calculate_average(data[INTERFACE_POWER])*1e3:.4} mA" self.title_time = "Time spent in %1: %2" self.ax.set_title("Waiting for data...") # Hold times for pins list, we're going to collect them self.hold_times_sum = 0.00 self.hold_times_next_index = 0 # Will be incremented to 0 later self.hold_times_already_drawn = [] self.verbose = kwargs.get("verbose", 0) self.initialize_sliders() def initialize_sliders(self): self.axpos = plt.axes([0.25, 0.1, 0.65, 0.03], facecolor=self.axcolor) self.axwidth = plt.axes([0.25, 0.15, 0.65, 0.03], facecolor=self.axcolor) self.resetax = plt.axes([0.8, 0.025, 0.1, 0.04]) self.spos = Slider(self.axpos, 'x', 0, self.plot_xmax, valinit=0, valstep=self.plot_xstep) self.swidth = Slider(self.axwidth, 'xmax', 0, self.plot_xmax, valinit=self.plot_xdiv, valstep=self.plot_xstep) self.resetbtn = Button(self.resetax, 'Reset', color=self.axcolor, hovercolor='0.975') #TODO: Change to pixel sizes self.xleftax = plt.axes([0.4, 0.025, 0.095, 0.04]) # x_pos, y_pos, width, height self.xrightax = plt.axes([0.5, 0.025, 0.095, 0.04]) self.xmaxleftax = plt.axes([0.6, 0.025, 0.095, 0.04]) self.xmaxrightax = plt.axes([0.7, 0.025, 0.095, 0.04]) self.xstepupax = plt.axes([0.3, 0.025, 0.095, 0.04]) self.xstepdownax = plt.axes([0.2, 0.025, 0.095, 0.04]) self.xleftbtn = Button(self.xleftax, '<x', color=self.axcolor, hovercolor='0.975') self.xrightbtn = Button(self.xrightax, 'x>', color=self.axcolor, hovercolor='0.975') self.xmaxleftbtn = Button(self.xmaxleftax, 'xmax-', color=self.axcolor, hovercolor='0.975') self.xmaxrightbtn = Button(self.xmaxrightax, 'xmax+', color=self.axcolor, hovercolor='0.975') self.xstepupbtn = Button(self.xstepupax, 'xstep+', color=self.axcolor, hovercolor='0.975') self.xstepdownbtn = Button(self.xstepdownax, 'xstep-', color=self.axcolor, hovercolor='0.975') self.xstepax = plt.axes([0.1, 0.025, 0.095, 0.04]) self.xsteptb = TextBox(self.xstepax, 'xstep', initial=str(self.plot_xstep)) def xstep_submit(text): self.plot_xstep = float(text) self.xsteptb.on_submit(xstep_submit) def increase_x(event): #if ((self.spos.val + self.plot_xstep) <= (self.plot_xmax - self.swidth.val)): self.spos.set_val(self.spos.val + self.plot_xstep) update_pos(self.spos.val) def decrease_x(event): #if ((self.spos.val - self.plot_xstep) >= (self.plot_xmin)): self.spos.set_val(self.spos.val - self.plot_xstep) update_pos(self.spos.val) def increase_xmax(event): #if ((self.swidth.val + self.plot_xstep) <= self.plot_xmax): self.swidth.set_val(self.swidth.val + self.plot_xstep) update_width(self.swidth.val) def decrease_xmax(event): if ((self.swidth.val - self.plot_xstep) >= (self.plot_xmin + 0.000001)): self.swidth.set_val(self.swidth.val - self.plot_xstep) update_width(self.swidth.val) def increase_xstep(event): val = float(self.xsteptb.text) if ((val + 0.05) < (self.swidth.val - 0.000001)): self.xsteptb.set_val("{0:.2f}".format(val + 0.05)) xstep_submit(self.xsteptb.text) def decrease_xstep(event): val = float(self.xsteptb.text) if ((val - 0.05) >= (0.05)): self.xsteptb.set_val("{0:.2f}".format(val - 0.05)) xstep_submit(self.xsteptb.text) self.xleftbtn.on_clicked(decrease_x) self.xrightbtn.on_clicked(increase_x) self.xmaxleftbtn.on_clicked(decrease_xmax) self.xmaxrightbtn.on_clicked(increase_xmax) self.xstepupbtn.on_clicked(increase_xstep) self.xstepdownbtn.on_clicked(decrease_xstep) # I'm not sure how to detach these without them forgetting their parents (sliders) def update_pos(val): if self.xylim_mutex.acquire(False): pos = self.spos.val width = self.swidth.val #self.set_axis(pos, pos + width, self.plot_ymin, self.plot_ymax, "update_pos function") self.ax.axis( [pos, pos + width, self.plot_ymin, self.plot_ymax]) self.xylim_mutex.release() def update_width(val): if self.xylim_mutex.acquire(False): pos = self.spos.val width = self.swidth.val self.axpos.clear() self.spos.__init__(self.axpos, 'x', 0, width, valinit=pos, valstep=self.plot_xstep) self.spos.on_changed(update_pos) self.spos.set_val(pos) #self.set_axis(pos, pos + width, self.plot_ymin, self.plot_ymax, "update_width function") self.ax.axis( [pos, pos + width, self.plot_ymin, self.plot_ymax]) self.xylim_mutex.release() self.spos.on_changed(update_pos) self.swidth.on_changed(update_width) # TODO: This def see_all(event): if self.xylim_mutex.acquire(False): #self.set_axis(0, self.last_timestamp, self.plot_ymin, self.plot_ymax, "see_all function") self.ax.axis( [0, self.last_timestamp, self.plot_ymin, self.plot_ymax]) self.last_xpos = -1 self.xylim_mutex.release() def reset(event): if self.xylim_mutex.acquire(False): self.swidth.set_val(self.plot_xmax) self.axpos.clear() self.spos.__init__(self.axpos, 'x', 0, self.plot_xmax, valinit=0, valstep=self.plot_xstep_default) self.spos.on_changed(update_pos) self.xsteptb.set_val(str(self.plot_xstep_default)) #self.set_axis(self.plot_xmin, self.plot_xmax, self.plot_ymin, self.plot_ymax, "reset function") self.ax.axis([ self.plot_xmin, self.plot_xmax, self.plot_ymin, self.plot_ymax ]) self.last_xpos = -1 self.xylim_mutex.release() self.resetbtn.on_clicked(reset) self.spos.set_val(0) self.swidth.set_val(self.plot_xmax) # Auto-change the sliders/buttons when using plot tools def on_xlims_change(axes): if self.xylim_mutex.acquire(False): # Non-blocking xlim_left = self.ax.get_xlim()[0] xlim_right = self.ax.get_xlim()[1] pos = xlim_left width = xlim_right - xlim_left self.spos.set_val(pos) self.swidth.set_val(width) self.last_xpos = -1 self.xylim_mutex.release() def on_ylims_change(axes): print(self.ax.get_ylim()) self.ax.callbacks.connect('xlim_changed', on_xlims_change) #self.ax.callbacks.connect('ylim_changed', on_ylims_change) def update_plot(self, data): verbose = self.verbose if data is None: data = self.dgilib_extra.data if (not data): if verbose: print( "dgilib_plot.update_plot: Expected 'data' containing interfaces. Got 'data' with no interfaces. Returning from call with no work done." ) return if (not data.power): if verbose: print( "dgilib_plot.update_plot: Expected 'data' containing power data. Got 'data' with interfaces but no power timestamp & value pairs. Returning from call with no work done." ) return if (not data.gpio): if verbose: print( "dgilib_plot.update_plot: Expected 'data' containing gpio data. Got 'data' with interfaces but no gpio timestamp & value pairs." ) return if not plt.fignum_exists(self.fig.number): plt.show() else: plt.draw() self.refresh_plot() #TODO: ln might have an update_callback and then it can listen to the data being updated instead of updating data here self.ln.set_xdata(data.power.timestamps) self.ln.set_ydata(data.power.values) automove = True current_xpos = self.ax.get_xlim()[0] if self.last_xpos != current_xpos: automove = False if automove and self.xylim_mutex.acquire(False): last_timestamp = data.power.timestamps[-1] pos = self.spos.val width = self.swidth.val if (last_timestamp > (pos + width)): if self.automove_method == "page": self.spos.set_val(pos + width) elif self.automove_method == "latest_data": arbitrary_amount = 0.15 if last_timestamp > width: self.spos.set_val(last_timestamp + arbitrary_amount - width) pos = self.spos.val width = self.swidth.val self.ax.axis([pos, pos + width, self.plot_ymin, self.plot_ymax]) self.last_xpos = pos self.xylim_mutex.release() self.refresh_plot() self.draw_pins(data) def clear_pins(self): """ Clears the highlighted areas on the plot that represent the state of the gpio pins (as seen in figures 4, 5, 6). Using this method only makes sense if the `highlight` method of drawing pins was used. """ if self.axvspans is None: return for axvsp in self.axvspans: axvsp.remove() def draw_pins(self, data): """draw_pins [summary] Raises ------ ValueError Raised when `plot_pins_method` member of class has another string value than the ones available (`highlight`, `line`) Parameters ---------- data : DGILibData :class:`DGILibData` object that contains the GPIO data to be drawn on the plot. """ # Here we set defaults (with 'or' keyword ...) ax = self.ax plot_pins = self.plot_pins plot_pins_values = self.plot_pins_values #plot_pins_method = self.plot_pins_method or "highlight" plot_pins_colors = self.plot_pins_colors # Here we do checks and stop drawing pins if something is unset if ax is None: return if plot_pins is None: return verbose = self.verbose no_of_pins = len(self.plot_pins) if self.plot_pins_method == "highlight": for pin_idx in range(no_of_pins): # For every pin number (0,1,2,3) if plot_pins[pin_idx] == True: # If we want them plotted hold_times = self.hold_times_obj.identify_hold_times( pin_idx, plot_pins_values[pin_idx], data.gpio) if hold_times is not None: for ht in hold_times: axvsp = ax.axvspan(ht[0], ht[1], color=plot_pins_colors[pin_idx], alpha=0.25) self.axvspans[pin_idx].append(axvsp) x_halfway = (ht[1] - ht[0]) / 4 + ht[0] y_halfway = (self.plot_ymax - self.plot_ymin) / 2 + self.plot_ymin annon = ax.annotate(str(self.iterations[pin_idx] + 1), xy=(x_halfway, y_halfway)) self.annotations[pin_idx].append(annon) self.iterations[pin_idx] += 1 # TODO: The start and stop indexes of the data points that are area of interest # might be more useful for an averaging function, but currently the plot uses # the coordinates of the X axis(the start/stop timestamps) in order to highlight # the areas of interest. self.preprocessed_averages_data[pin_idx].append( (self.iterations[pin_idx], ht, 0, None)) # This should be in update_plot() self.ax.set_title( f"Logging. Collected {len(data.power)} power samples and {len(data.gpio)} gpio samples." ) elif self.plot_pins_method == "line": extend_gpio = data.gpio.timestamps[-1] < data.power.timestamps[-1] for pin, plot_pin in enumerate(self.plot_pins): if plot_pin: self.ln_pins[pin].set_xdata(data.gpio.timestamps + extend_gpio * [data.power.timestamps[-1]]) self.ln_pins[pin].set_ydata( data.gpio.get_select_in_value(pin) + extend_gpio * [data.gpio.values[-1][pin]]) self.ax.set_title( f"Logging. Collected {len(data.power)} power samples and {len(data.gpio)} gpio samples." ) self.fig.show() else: raise ValueError( f"Unrecognized plot_pins_method: {self.plot_pins_method}") def plot_still_exists(self): """plot_still_exists Can be used in a boolean (e.g.: `if`, `while`) clause to check if the plot is still shown inside a window (e.g.: to unpause program functionality if the plot is closed). Also used in the :func:`keep_plot_alive` member function of the class. Returns ------- bool Returns `True` if the plot still exists and `False` otherwise. """ return plt.fignum_exists(self.fig.number) def refresh_plot(self): """refresh_plot Makes a few `matplotlib` specific calls to refresh and redraw the plot (especially when new data is to be drawn). Is used in :func:`update_plot` member function of the class. """ self.ax.relim() # recompute the data limits self.ax.autoscale_view() # automatic axis scaling self.fig.canvas.flush_events() def keep_plot_alive(self): while self.plot_still_exists(): self.refresh_plot() """keep_plot_alive Pauses program functionality until the plot is closed. Does so by refreshing the plot using :func:`refresh_plot`. """ def pause(self, time=0.000001): """pause Calls `matplotlib's` `pause` function for an amount of seconds. Parameters ---------- time : float The number of seconds the plot should have time to refresh itself and pause program functionality. """ plt.pause(time)
def __init__(self, *args, **kwargs): Slider.__init__(self, *args, **kwargs)