#!/usr/bin/env python3 """Plot instrument. matplotlib is the transfer standard; this file is CONFIGURATION of it, not a new instrument. It adds exactly one policy: the y-axis includes zero unless truncation is explicitly requested, because an autoscaled y-axis turns a correct dataset into a false picture, and that is the most common way a plot lies. """ import sys, csv, matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt import matplotlib.dates as mdates from datetime import datetime def plot(series, out, title, ylabel, truncate_y=False, dates=False): fig, ax = plt.subplots(figsize=(11, 5.2), dpi=160) for name, xs, ys, color in series: ax.plot(xs, ys, marker="o", ms=2.6, lw=1.4, color=color, label=name, zorder=3) if not truncate_y: ax.set_ylim(bottom=0) # the policy if dates: ax.xaxis.set_major_locator(mdates.AutoDateLocator()) ax.xaxis.set_major_formatter(mdates.DateFormatter("%b %d")) ax.set_ylabel(ylabel); ax.set_title(title, loc="left") ax.grid(True, lw=0.5, alpha=0.35, zorder=0) ax.legend(frameon=False, loc="upper left") for s in ("top","right"): ax.spines[s].set_visible(False) fig.tight_layout(); fig.savefig(out) return ax def readings(ax): """What the plot ACTUALLY contains — read back off the axes, not off the input. This is what makes the plot checkable.""" out = [] for ln in ax.get_lines(): d = ln.get_xydata() out.append({"label": ln.get_label(), "n": len(d), "xmin": float(d[:,0].min()), "xmax": float(d[:,0].max()), "ymin": float(d[:,1].min()), "ymax": float(d[:,1].max()), "last": (float(d[-1,0]), float(d[-1,1]))}) return out, ax.get_ylim()