Follow matplotlib's API, and make liveplot easier to use - #15
Open
davidquarel wants to merge 2 commits into
Open
davidquarel wants to merge 2 commits into
davidquarel wants to merge 2 commits into
Conversation
…ubplots
- ax.plot("loss", fmt, **Line2D kwargs): one metric per call, returns [line] with
Line2D setters; two names raise, since matplotlib reads them as x and y
- axhline(y=0, xmin, xmax, **kw) / axvline(x=0, ymin, ymax, **kw): label is a
keyword, and only labelled lines (not "_"-prefixed) go in the legend
- one-sided limits (set_ylim(bottom=0)), ymin/ymax/xmin/xmax aliases
- set_xscale; any matplotlib scale, with scale kwargs; text kwargs on titles/labels
- the plot is the figure: plot.axes, suptitle/supxlabel/supylabel; broadcast
setters replaced by set_all(), plot-level axhline/axvline removed
- subplots: sharex/sharey, subplot_kw, gridspec_kw; returns a numpy array of panels
- imshow passes interpolation/aspect/... to matplotlib
- every setter is tried on a scratch Axes first, so mistakes raise matplotlib's
own error at the call site instead of killing the renderer
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s, fewer silent failures - one Axes class: plot.axes[i], subplots' axes, ax.twinx() and plot["acc"] are all Axes; twins keep their own y-settings and share the panel's (Panel/_Axis merged) - panels are strings only; the dict layout form is gone (the setters cover it) - plot.savefig(path, **kw) like Figure.savefig; figure() renamed snapshot() - outside a notebook, say once that nothing is drawn live - log() detaches tensors, so plot.log(loss=loss) needs no .item() and no torch warning - a legend only with two or more entries, or when ax.legend() asks for one - in a subplots() grid, warn when a logged metric has no ax.plot() (likely a typo) - figsize= on the constructor too: the whole figure, however many panels appear - refresh_seconds default 1.0 -> 0.2 (as on the stranded refresh-default branch) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 tasks
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two commits. The first makes liveplot follow matplotlib's API as closely as it can while the data comes from
log(). The second fixes the usability problems found while doing that. Nothing depends on liveplot yet, so the breaking changes are made now rather than kept for compatibility.1. matplotlib's API (0b8920a)
ax.plot("loss", fmt, **Line2D_kwargs)[line], and the line has Line2D's setters:(line,) = ax.plot("loss"); line.set_color("k")."r--","o","C2:","tab:orange"), and so doesax.lines.ax.plot("a", "b")used to draw two curves, but matplotlib reads it as b against a. It is now an error that says so and shows the one-call-per-metric spelling.axhline(y=0, xmin=0, xmax=1, **kw)/axvline(x=0, ymin=0, ymax=1, **kw)labelis a keyword; before, it was the second positional argument, which matplotlib reads asxmin._are left out.axvlinelabels are now legend entries instead of text drawn along the line.set_ylim(bottom=0), where the other end follows the data), as do theymin/ymax/xmin/xmaxaliases.set_xscaleis new, and any matplotlib scale can be used, with its kwargs. Titles and labels takefontdict,loc,pad/labelpadand text kwargs.subplots: takes matplotlib's arguments (sharex,sharey,subplot_kw,gridspec_kw,width_ratios,height_ratios,figsize,squeeze) and returns a numpy array of axes, soaxes.flatten(),axes[0, 1]andaxes.flatall work.plot.axes,suptitle,supxlabel,supylabel.plot.set_all(...)does the same thing explicitly, and also reaches panels created later.plot.axhline(metric=)andplot.axvline()are removed.imshow: other kwargs (interpolation,aspect, …) are passed to matplotlib.colr=orset_yscale("sqrt")raises matplotlib's own error on the line that caused it. Before, it killed the render process.2. Easier to use (1b4dd60)
Axestype:Paneland_Axisare merged.plot.axes[i], the axes fromsubplots,ax.twinx()andplot["acc"]are now allAxes.axhline, and the two share the panel's title, x-axis,axvline, legend and smoothing.{"metrics": [...], "ylim": ...}) is gone; the setters cover everything it did.plot.savefig(path, **kw)works likeFigure.savefig, andplot.figure()is renamedplot.snapshot().log()detaches tensors, soplot.log(loss=loss)needs no.item()and torch no longer warns.ax.legend()asks for one. A single-curve panel is already named by its title.subplotsgrid, a logged metric with noax.plot()for it triggers a warning, since it is usually a typo. Metrics that are not in a layout string still get their own panel with no warning, as documented.figsize=is accepted by the constructor too, and means the whole figure however many panels appear.refresh_secondsnow defaults to 0.2, the same one-line change as on the strandedrefresh-defaultbranch.Not included
ax.plot("lr", "loss")) would need a metric to appear on several axes.warm(), the docs site and the short README from Website: mkdocs from the .md files, built to gh-pages on every push to main #9–liveplot.warm(): keep a render process ready so plots appear immediately #12 were merged into stacked branches rather thanmain, so this PR doesn't include them. They will conflict with this PR inREADME.mdandliveplot/liveplot.py, and are best landed separately afterwards.Testing
pytest tests, matplotlib 3.11.2).set_all,suptitleandsupxlabel;Axestype and twins;savefig;figsizeon the constructor;sharex,gridspec_kwandsubplot_kw;imshowkwargs.examples/demo.pyandexamples/dcgan_synthetic.pyrun to completion as scripts.figsize, pinned limits and the twin-axis label.-W error,test_record_and_save_gifreports aResourceWarningfor a file the test leaves open. It does the same onmain.🤖 Generated with Claude Code
Verification (2026-10-02)
Environment: Python 3.12.3, matplotlib 3.11.2, numpy 2.5.3, IPython 9.17.1, torch 2.14.1+cu130 (CPU only),
MPLBACKEND=Agg. Basemain@f49ee1b, headpr15@1b4dd60(2 commits:0b8920a,1b4dd60). Fresh clone in the scratchpad. Scripts are inscratchpad/libs/scripts/liveplot-15/(claims.py,twonames.py,visual.py,axvline_default.py,hcolor.py).This is an API redesign, not a bug fix. "Bug on base" below means "the base lacks the claimed matplotlib-compatible behaviour". Each claim was probed with the same script on base and head (
claims.py; the exceptions it prints are real, not hidden).Bug on base
On base, liveplot's API departs from matplotlib's wherever the PR says it does:
main)ax.plot("loss", "r--")with a fmt string"r--"becomes a second metric, so two curves are drawn[<liveplot line 'loss'>](line,) = ax.plot("loss"); line.set_color("k")TypeError: cannot unpack non-iterable _Axis objectkin the rendered figureax.plot("c", color="tab:orange", alpha=0.5), plus"C2:"and"o"TypeError: Panel.plot() got an unexpected keyword argument 'color'C2/:, markero,tab:orange/alpha 0.5 all appear in the renderax.plot("lossD", "lossG")TypeError: ... one metric per call. matplotlib reads two names as x and y ...axhlinesignature(y, label=None, **kwargs)(y=0, xmin=0, xmax=1, **kwargs);axhline(0.7, 0.25, 0.75)draws x 0.25..0.75_hiddenaxhline in the legend['loss','acc','target','0.3','_hidden']['loss','acc','target']axvline(3, label="lr drop")lr drop, no textset_ylim(bottom=0)AssertionError: give both limits(0.0, 14.45), so the top follows the dataset_ylim(ymax=20)TypeError(4.55, 20.0)set_xscale("log")AttributeError(no such method)logset_yscale("symlog", linthresh=0.01)TypeError(no kwargs; only linear and log)set_yscale("sqrt")AssertionError: yscale must be 'linear' or 'log'(already at the call site)ValueError: 'sqrt' is not a valid value for scale.(matplotlib's own, at the call site)ax.plot("loss", colr="red")TypeErrorfrom liveplot's signatureAttributeError: Line2D.set() got an unexpected keyword argument 'colr', at the call siteset_title("hi", fontsize=20, loc="left")/set_ylabel(..., labelpad=, color=)TypeErrorsubplots(2, 2)return type_PanelGrid(no.flatten())numpy.ndarrayofAxes, shape (2, 2)subplots(2, 1, sharex=True, height_ratios=[2, 1])TypeErrorsuptitle/supxlabelAttributeErrorset_all(yscale="log", xlabel=...)before any panel existsAttributeErrorimshow(..., interpolation="bilinear")TypeErrorbilinearplot.savefig(path, dpi=50)AttributeErrorplot.log(loss=<tensor requiring grad>)UserWarning: Converting a tensor with requires_grad=True to a scalar ...['loss']ax.legend()brings it back as['loss']subplots()grid, logging a metric noax.plot()named (lsos)UserWarning: ... logged 'lsos', which no ax.plot() put on an axis ... A typo?LivePlot("loss", figsize=(8,3)), with 3 panels appearingTypeErrorsnapshot().get_size_inches() == (8, 3)refresh_secondsdefaultUserWarning: LivePlot: not in a notebook, so nothing is drawn live ..., once per process (emitted on the first probe, not repeated later)claims.pyon base (trimmed)Fixed on this PR
Every claimed behaviour in the table above works on head. One-sided limits and the axvline legend entry were also checked in the rendered PNG:
out_head/one_sided_ylim.pnghas y from 0 up to the data. Invisual_head.png, "lr drop" is a legend entry, wherevisual_base.pngwrites it as rotated text on both panels. Head also draws the axvline only on the panel it was called on; base drew a plot-wide line, so it appeared on thelrpanel too.claims.pyon head (trimmed)Tests
Panel,_Axis,_normalise_axhlinesremoved); of the rest, 8 failed / 34 passed. Every failure is a removed or renamed API: the dict layout form,plot.figure(), plot-levelset_title, the single-curve legend, and a_layoutindexgh pr checks)buildpass,pytestpass-W error -k save_gifResourceWarning: unclosed file ... run.gifon both base and head, so the PR's "pre-existing issue" note is accurateexamples/demo.py,examples/dcgan_synthetic.py,examples/make_gif.pyon head57 frames -> dcgan_synthetic.gif; make_gif final{'loss': 0.114, ...})The test diffs are API respellings (
figure()->snapshot(), dicts -> setters,panels->axes), plus new assertions. No assertion was loosened to make a test pass. One test was removed,test_axvline_skips_image_panels, which covered plot-wide axvlines; those no longer exist because axvline is per-panel now.test_normalise_axhlineswas replaced by a test that the dict form raises.Relevance to ARENA
The changes break the public API, but no ARENA code uses liveplot today, so nothing in ARENA breaks or needs updating when this merges.
git grep -il liveplotondavidquarel/ARENA_3.0tl3.9-port, fetched fresh at735a9a5a9(equal to the local worktree HEAD): 0 files.git grepover the ARENA_3.0 clone's 2934 local and remote-tracking refs did not finish: it timed out twice, so it is not evidence either way. Onlytl3.9-port, plus the checked-out files of every local worktree (next item), were actually searched.worktrees/matt-gmg/.../master_2_6.py. There,liveplotis a local variable holding the in-repo plotly helperLiveSubplots, not this library; there is noimport liveplot.gh search code liveplot --owner ARENA-educationreturned no hits outside the liveplot repo itself.So the PR's claim that "nothing depends on liveplot yet" checks out. The change is independent of the TL3.9 / transformers-5 path.
Notes
Mergeability:
MERGEABLE, and CI is green.Partly overclaimed: the "two names" error.
ax.plot("a", "b")raises only when the second name is not a valid matplotlib fmt string. If the second name is valid fmt ("b","r","o","k","g--", ...), it is silently read as a style:matplotlib has the same ambiguity, so this is defensible. Metric names that collide with fmt strings are rare, and the typo warning partly catches it. The body's wording ("It is now an error") is a little too strong, though.
A silent semantic change the body doesn't call out:
axvline()with nox. On base, noxmeant the current step, at the time of the call. On head it meansx=0, matplotlib's default:Old code like
ax.axvline(label="lr drop")still runs, but now draws at the origin; the new spelling isax.axvline(plot.step, label=...), which the tests and README use. Worth a sentence in the body.Removals the body doesn't list explicitly:
plot.panels: replaced byplot.axes; the body only saysplot.axesis "new".plot.set(...): removed along with the broadcast setters;set_allreplaces it.Panel.left/Panel.right: gone; useaxandax.twinx()._PanelGridtype: replaced by an ndarray."Panel and _Axis are merged" and "plot-level setters ... are removed" cover these in spirit.
Error location: "Before, it killed the render process" holds for the kwargs that base passed through to matplotlib. For
set_yscale("sqrt")specifically, base already raised at the call site (its ownAssertionError). What head adds there is matplotlib's own error message.Docs: README and
examples/on head use only the new API; grep found no stalefigure(), dict-layout,plot.axhlineor.panelsuses. The images underdocs/(gifs) were not regenerated or checked.Not tested: live drawing in a real Jupyter or Colab kernel; process mode was exercised only through the test suite's fake display handle.
Upstream status (2026-10-02)
ARENA-education/liveplothasfork=falseand no parent or source. ARENA created it on 2026-09-16, and it has 0 forks. Parts are adapted from tylerlum/live_plotter (MIT, credited inTHIRD_PARTY_LICENSES.md), but it isn't a fork of it. The PyPI nameliveplot(0.1.2, Philip Reinhold) is an unrelated project.