def save_and_show_if_needed(folder_to_save: str, to_show: bool, fig: plt.figure, data_path: Optional[str] = None, name_prefix: str = '') -> None: if folder_to_save: save_plot(folder_to_save, fig, name_prefix, data_path) if to_show: log.info('Showing ' + data_path) fig.show()
def make_plots_mpl(data, timeDeltaDict=None): times = [d[0] for d in data] nTplot = 1 + ((len(supply.temperatures) - 1) // 2) nax = len(aNames) + len(rNames) plotWidth = 3.6 plotHeight = 0.8 * (len(aNames) + len(rNames) + nTplot) minPlotHeight = len(supply.outPins) * 0.42 # inch plotHeight = max(plotHeight, minPlotHeight) dpi = 100 fig = Figure(figsize=(plotWidth * 1.6, plotHeight * 1.6), dpi=dpi) fig.set_facecolor(facecolor) if not isTest: canvas = FigureCanvasAgg(fig) # noqa kw = {} if versiontuple(mpl.__version__) >= versiontuple("2.0.0"): kw['facecolor'] = facecolor else: kw['axisbg'] = facecolor gs = gridspec.GridSpec(nax + 2, 1, height_ratios=[2 * nTplot] + [2 for i in range(nax)] + [1]) ax0 = fig.add_subplot(gs[0], **kw) axi = [fig.add_subplot(gs[i], sharex=ax0, **kw) for i in range(1, nax + 2)] for ax in [ax0] + axi: for spine in ['bottom', 'top', 'left', 'right']: ax.spines[spine].set_color(spinecolor) ax.xaxis.label.set_color(spinecolor) ax.yaxis.label.set_color(spinecolor) ax.tick_params(axis='x', colors=spinecolor) ax.tick_params(axis='y', colors=spinecolor) tp = dict(bottom=True, top=True, labelbottom=False) for ax in axi: ax.tick_params(axis="x", labeltop=False, **tp) ax0.tick_params(axis="x", labeltop=True, **tp) axi[-1].set_xlabel(u'date time', fontsize=13) axt = ax0 # temperature axes axs = axi[-1] # state axes axd = axi[:-1] # other sensor axes axNames = ['temperature'] + aNames + rNames + ['ios'] axUnits = [supply.temperatureUnit] + aUnits + rUnits + [''] for it, color in enumerate(tColors): datat = [d[it + 2] for d in data] lo = supply.temperatureOutlierLimits times0 = [t for t, d in zip(times, datat) if lo[0] < d < lo[1]] datat0 = [d for t, d in zip(times, datat) if lo[0] < d < lo[1]] # td = [(t, d) for t, d in zip(times, datat) if lo[0] < d < lo[1]] # times0, datat0 = zip(*td) axt.plot(times0, datat0, 'o-', lw=lw, color=color, alpha=alpha, ms=ms, markeredgecolor=color) for it, (ax, color) in enumerate(zip(axd, aColors + rColors)): datao = [d[it + 2 + len(tNames)] for d in data] ax.plot(times, datao, 'o-', lw=lw, color=color, alpha=alpha, ms=ms, markeredgecolor=color) ioNames = supply.plotPins onVals = [1.15 - i * 0.1 for i in range(len(ioNames))] ioColors = [] ioDisplayed = [] ioState = [] for name, onVal in zip(ioNames, onVals): if name in ioKeys: ioDisplayed.append(name) iio = ioKeys.index(name) dataIO = [onVal if d[1] & 2**iio else 1 - onVal for d in data] ioState.append(1 if dataIO[-1] > 0.5 else 0) color = pColors[iio] ioColors.append(color) axs.plot(times, dataIO, 'o-', lw=lw, color=color, alpha=alpha, ms=ms, markeredgecolor=color) if timeDeltaDict is not None: now = datetime.datetime.now() ax0.set_xlim([now - datetime.timedelta(**timeDeltaDict), now]) if yrange == "user": axt.set_ylim(supply.temperatureDisplayLimits) elif yrange == "auto": axt.set_ylim([None, None]) axs.set_ylim(-0.2, 1.2) axs.set_yticks([0, 1]) axs.set_yticklabels(['off', 'on']) for ax, lims in zip(axd, aLimits + rLimits): if yrange == "user": ax.set_ylim(*lims) elif yrange == "auto": ax.set_ylim([None, None]) # fig.autofmt_xdate() fig.canvas.draw() if versiontuple(mpl.__version__) < versiontuple("2.0.0"): tlabels = [item.get_text() for item in axi[-1].get_xticklabels()] posdot = tlabels[0].find('.0') if posdot > 0: tlabels = [item[:posdot] for item in tlabels] axi[-1].set_xticklabels(tlabels) fig.subplots_adjust(left=0.09, bottom=0.04, right=0.98, top=0.94, hspace=0.04) for ax, name, unit in zip([ax0] + axi, axNames, axUnits): label = u'{0} ({1})'.format(name, unit) if unit else name if name == 'ios': color_text(0.01, 0.65, ioDisplayed, ioState, ioColors, ax, va='top', fontsize=13, alpha=0.7) else: ax.text(0.01, 0.95, label, transform=ax.transAxes, va='top', fontsize=13, color=spinecolor, alpha=0.7) if timeDeltaDict is not None: ax0.annotate('', xy=(1, 1), xycoords='axes fraction', xytext=(1, 1.25), textcoords='axes fraction', arrowprops=dict(color=spinecolor, width=1, headwidth=7, headlength=10)) fig.text(0.01, 0.01, '{0} time points'.format(len(times)), color=spinecolor, alpha=0.25) xticklabels = ax0.get_xticklabels() ax0.set_xticklabels(xticklabels[:-1], rotation=20, ha="left") if isTest: fig.show() else: buf = BytesIO() fig.savefig(buf, format="png", facecolor=facecolor) return (base64.b64encode(buf.getvalue()).decode("ascii"), plotHeight * dpi)
class Plotter(object): """This class is responsible for managing the layout of a figure, and also implementing the plotting commands for simple graphs, hopefully making it even easier than using matplotlib directly.""" ACTIVE_AXES = "active axes" # A constant used as default target of the plotting methods default_hspace = 1.0 / 8.0 default_vspace = 1.0 / 8.0 default_hpadding = 0.05 default_vpadding = 0.05 def __init__(self, title=None, rows=0, cols=0, hspace=None, vspace=None, hpadding=None, vpadding=None): if hspace is None: hspace = self.default_hspace if vspace is None: vspace = self.default_vspace if hpadding is None: hpadding = self.default_hpadding if vpadding is None: vpadding = self.default_vpadding self.figure = Figure() self.figure.plotter = self self.title = self.figure.suptitle("" if title is None else title) self.rows = rows self.cols = cols self.hspace = hspace self.vspace = vspace self.hpadding = hpadding self.vpadding = vpadding self.active_axes = None def update(self): """Show changes in the figure.""" self.figure.show() def save(self, *args, **kwargs): """Save the figure. Please refer to matplotlib's documentation.""" self.figure.savefig(*args, **kwargs) def close(self): close_figure(self.figure) def layout(self, rows=None, cols=None, update=True): """Change the layout of the figure, i.e. the number of rows and columns.""" n = len(self.figure.axes) if rows is None and cols is None: rows = int(round(sqrt(n))) cols = rows + (1 if n > rows**2 else 0) elif rows is None: rows = int(ceil(float(n) / cols)) elif cols is None: cols = int(ceil(float(n) / rows)) elif rows * cols < n: raise ValueError("insufficient cells") self.rows = rows self.cols = cols self.redraw(update) def spacing(self, hspace=None, vspace=None, update=True): """Change the spacing between axes in the figure.""" if hspace is None and vspace is None: return if hspace is not None: if not 0.0 <= hspace <= 1.0: raise ValueError("illegal horizontal spacing (must be in [0, 1])") self.hspace = hspace if vspace is not None: if not 0.0 <= vspace <= 1.0: raise ValueError("illegal vertical spacing (must be in [0, 1])") self.vspace = vspace self.redraw(update) def padding(self, hpadding=None, vpadding=None, update=True): if hpadding is None and vpadding is None: return if hpadding is not None: if not 0.0 <= hpadding < 0.5: raise ValueError("illegal horizontal spacing (must be in [0, 0.5))") self.hpadding = hpadding if vpadding is not None: if not 0.0 <= vpadding < 0.5: raise ValueError("illegal vertical spacing (must be in [0, 0.5))") self.vpadding = vpadding self.redraw(update) def redraw(self, update=True): """Redraw the axes in the figure. This is usually used only after changes to layout or spacing in the figure.""" total_width = self.cols * (1.0 + 2.0*self.hspace) total_height = self.rows * (1.0 + 2.0*self.vspace) plot_width = (1.0 - 2.0*self.hpadding) / total_width plot_height = (1.0 - 2.0*self.vpadding) / total_height space_width = self.hspace * plot_width space_height = self.vspace * plot_height # Reposition the axes according to the new dimensions n = len(self.figure.axes) index = 0 y_pos = 1.0 - plot_height - space_height - self.vpadding for _ in xrange(self.rows): x_pos = space_width + self.hpadding for x in xrange(self.cols): axes = self.figure.axes[index] axes.set_position([x_pos, y_pos, plot_width, plot_height]) index += 1 x_pos += plot_width + 2 * space_width if index == n: break y_pos -= plot_height + 2 * space_height if index == n: break if update: self.update() def set_title(self, title, update=True): """Set the title of the figure (not axes!).""" self.title.set_text("" if title is None else title) if update: self.update() def set_size(self, width, height, inches=False): """Sets the size of the image in pixels or inches (if 'inches' is true).""" dpi = self.figure.get_dpi() if not inches: width /= dpi height /= dpi self.figure.set_size_inches(width, height, forward=True) def add_axes(self, make_active=True): """Add a new axes to the figure and place it in the right position. If the current grid of graphs cannot accommodate the new axes, the figure layout is recalculated.""" axes = self.figure.add_subplot(1, 1, 1, label=str(len(self.figure.axes))) if make_active: self.active_axes = axes rows, cols = None, None if self.rows * self.cols >= len(self.figure.axes): rows, cols = self.rows, self.cols self.layout(rows, cols, update=False) return axes def config_axes(self, axes=ACTIVE_AXES, title=None, xlabel=None, ylabel=None, xlimit=None, ylimit=None, legend=None, grid=None, update=True): """A many-in-one configuration method. Saves a few boring lines of matplotlib code.""" axes = self.get_axes(axes) if title is not None: axes.set_title(title) if xlabel is not None: axes.set_xlabel(xlabel) if ylabel is not None: axes.set_ylabel(ylabel) if xlimit is not None: axes.set_xlim(xlimit) if ylimit is not None: axes.set_ylim(ylimit) if legend is not None: axes.legend(loc=legend) if grid is not None: axes.grid(bool(grid)) if update: self.update() def get_axes(self, axes=ACTIVE_AXES): """This method can be used to check if a given axes belongs to the figure, retrieve the currently active axes, or add new axes to the figure.""" if axes is Plotter.ACTIVE_AXES: if self.active_axes is None: self.add_axes(make_active=True) return self.active_axes if axes is None: return self.add_axes(make_active=False) if axes in self.figure.axes: return axes raise Exception("axes does not belong to this Plotter") def set_active(self, axes): """Set the plotter's active axes, i.e. the default target of plotting commands.""" if axes not in self.figure.axes: raise Exception("axes does not belong to this Plotter") self.active_axes = axes # ------------------------------------------------- # Plotting methods @contextmanager def plotting_on(self, axes, update=True): yield self.get_axes(axes) if update: self.update() def legend(self, axes=ACTIVE_AXES, update=True, **kwargs): """Add a legend to the given axes.""" with self.plotting_on(axes, update) as axes: axes.legend(**kwargs) return axes def pie_chart(self, values, freqs, axes=ACTIVE_AXES, update=True, **kwargs): with self.plotting_on(axes, update) as axes: axes.pie(freqs, labels=values, **kwargs) return axes def bar_chart(self, values, freqs, axes=ACTIVE_AXES, update=True, **kwargs): with self.plotting_on(axes, update) as axes: data = self.__prepare_bar_chart(values, freqs) axes.xaxis.set_ticks(data.xtick_locs) axes.xaxis.set_ticklabels(data.xtick_labels) axes.bar(data.left, data.height, width=data.bar_width, **kwargs) return axes def pareto_chart(self, values, freqs, axes=ACTIVE_AXES, update=True, **kwargs): with self.plotting_on(axes, update) as axes: data = self.__prepare_pareto_chart(values, freqs) axes.xaxis.set_ticks(data.xtick_locs) axes.xaxis.set_ticklabels(data.xtick_labels) axes.bar(data.left, data.height, width=data.bar_width, **kwargs) axes.plot(data.xs, data.ys, "r-", label="Cumulative frequency") return axes def histogram(self, values, freqs=None, bins=10, axes=ACTIVE_AXES, update=True, **kwargs): with self.plotting_on(axes, update) as axes: data = self.__prepare_histogram(values, freqs, bins) axes.bar(data.left, data.height, width=data.bar_width, **kwargs) return axes def box_plot(self, values, axes=ACTIVE_AXES, update=True, **kwargs): with self.plotting_on(axes, update) as axes: axes.boxplot(values, **kwargs) return axes def run_chart(self, times, values, numeric=True, axes=ACTIVE_AXES, update=True, **kwargs): """A run chart of a time series.""" with self.plotting_on(axes, update) as axes: data = self.__prepare_run_chart(list(times), list(values), numeric) if not numeric: axes.yaxis.set_ticks(data.ytick_locs) axes.yaxis.set_ticklabels(data.ytick_labels) axes.plot(data.xs, data.ys, **kwargs) return axes def line_plot(self, xs, ys, axes=ACTIVE_AXES, update=True, **kwargs): """A simple 2D line plot.""" with self.plotting_on(axes, update) as axes: axes.plot(list(xs), list(ys), **kwargs) return axes def function_plot(self, function, start=0, stop=1.0, observations=100, axes=ACTIVE_AXES, update=True, **kwargs): """Make a quick plot of a function on a given interval.""" xs, ys = [], [] dx = float(stop - start) / (observations - 1) x = start for i in xrange(observations): xs.append(x) ys.append(function(x)) x += dx return self.line_plot(xs, ys, axes=axes, update=True, **kwargs) # ------------------------------------------------- # Preparation of data for plotting def __prepare_bar_chart(self, values, freqs, bar_width=1.0, bar_space=0.5): left = [(bar_width + bar_space) * x for x in xrange(len(freqs))] xtick_locs = [l + bar_width / 2.0 for l in left] return Namespace(left=left, height=freqs, bar_width=bar_width, xtick_locs=xtick_locs, xtick_labels=values) def __prepare_pareto_chart(self, values, freqs, bar_width=1.0): total = float(sum(freqs)) items = sorted([(f / total, v) for f, v in zip(freqs, values)], reverse=True) height = [] left = [bar_width * x for x in xrange(len(items))] xtick_locs = [l + bar_width / 2.0 for l in left] xtick_labels = [] xs = [bar_width * x for x in xrange(len(items) + 1)] ys = [0.0] for f, v in items: height.append(f) xtick_labels.append(v) ys.append(ys[-1] + f) return Namespace(left=left, height=height, xs=xs, ys=ys, bar_width=bar_width, xtick_locs=xtick_locs, xtick_labels=xtick_labels) def __prepare_histogram(self, values, freqs, bins): if freqs is None: freqs = [1.0] * len(values) items = sorted(zip(values, freqs)) minimum = items[ 0][0] maximum = items[-1][0] total_freq = sum(freqs) bin_span = float(maximum - minimum) / bins bin_end = [minimum + bin_span * (x + 1) for x in xrange(bins)] bin_end[-1] = maximum bin_freq = [0.0] * bins cur_bin = 0 for v, f in items: while v > bin_end[cur_bin]: cur_bin += 1 bin_freq[cur_bin] += f / (bin_span * total_freq) return Namespace(left=[end - bin_span for end in bin_end], height=bin_freq, bar_width=bin_span) def __prepare_run_chart(self, times, values, numeric): xs = [] ys = [] prev_y = values[0] for y, t in zip(values, times): xs.extend((t, t)) ys.extend((prev_y, y)) prev_y = y ytick_locs = None ytick_labels = None if not numeric: # map objects to integer y values if the tseries is not numeric y_set = sorted(set(ys)) y_mapping = dict(zip(y_set, xrange(len(y_set)))) ys = [y_mapping[y] for y in ys] # prepare y ticks explaining the translation from objects to integers yticks = sorted((i, v) for v, i in y_mapping.iteritems()) ytick_locs = [i for i, _ in yticks] ytick_labels = [v for _, v in yticks] return Namespace(xs=xs, ys=ys, ytick_locs=ytick_locs, ytick_labels=ytick_labels)