Imported from Quarrinexus/CMP-Plotter (
AGENTS.md). Install upstream withnpx skills add Quarrinexus/CMP-Plotter. Copyright stays with the author.
Notes for agents working on CMP Plotter
A Tk + matplotlib window for plotting columns of capacitance-bridge (CMP) data files against each other. README.md describes it from the user's side; this file is what isn't obvious from the code.
Running
uv run cmp-plotter
Python 3.13, managed by uv; dependencies are in pyproject.toml. There's no
test suite. Check changes by driving the real window from a script (below)
and by looking at a saved figure or a screenshot.
No data ships with the repo. On the author's machine it sits inside a
Huairou-CMP folder whose Analysis/data holds real runs
(Cambridge_Sep_26.00N.text) with a cmp-plotter.json profile; run 005
(a 28 T → 1 T sweep, 13k rows) and run 003 (24k rows, ~10k of them parked
at 28 T) are the useful ones, and Analysis/squiggle-finder.py is the
offline oscillation analysis the Smoothing, Background and FFT features
mirror. If that folder isn't around this repo, use whatever data folder
~/.config/cmp-plotter/settings.json points at, or ask for some.
Layout
| File | What's in it |
|---|---|
plotter.py |
the window: controls, drawing, zoom, saving |
model.py |
Line and Panel, colours, legend text, shared axis labels |
smoothing.py |
moving average, median, Savitzky–Golay; windows in points or x |
background.py |
polynomial fit in x, shown or subtracted |
spectrum.py |
FFT of a line against its plotted x, for FFT panels |
axis_functions.py |
the Function boxes (1/x, exp(y), ...) |
datasets.py, format_dialog.py, profile.py, columns.py |
reading files and per-folder profiles |
widgets.py |
colour picker, layout grid |
How a line is drawn
Plotter._draw_panel, per Line, via _line_data:
- read the columns, apply the axis functions (
_axis) - background (
background.apply): fit on the unsmoothed data - smoothing (
smoothing.smooth), on what's left - in an FFT panel only,
spectrum.spectrumof that against x - one
ax.plotcall
_line_data caches steps 1–3 per line, keyed on the line's settings, so a
data panel and its FFT panels do the work once; _reload_folder clears it.
Keep that order. Things that depend on it:
- One artist per
Line. The legend labels and_show_colourzipax.linesagainst the panel's lines, andself.artistsmaps each artist to a line index. A second artist per line breaks all three; that's why "show the fit" is a mode on a copied line, not an overlay. Line.shownis the tuple of what was last drawn:(run, x, x_fn, y, y_fn, smoothing, fitting).parts(),legend_labels,_default_nameand the zoom logic inapply_controlsall index into it, so a new per-line setting means updating each of them.- Settings in x units (
Line.span,fit_from,fit_to) are in the plotted x, after its function. They're cleared whenever x or its function changes, or on ⇅, since a value in T means nothing in 1/B. - Errors are stored in
Line.erroras"<Stage> error: message"; the text before the first": "becomes the popup title.
FFT panels are locked to a data panel
A Panel with a source is an FFT panel. Its lines is the source
panel's list, the same object, which is what keeps the two in sync: edits,
colours, added and removed lines all show in both with no copying. So:
- Redraw with
_redraw_selected, which redraws every panel in_linked(cell), data panel first: an FFT panel only draws lines whoseshownits data panel set, and leavesshownanderroralone._build_axeslikewise draws data panels before FFT panels. - Selecting a line goes through
_set_selected_line, so the linked panels select it too. - To break the link, use
_unlink, which gives the panel copies of the lines.set_layoutdoes that when a source panel is removed, andPanel.copyalways makes an independent data panel. - There are no FFTs of FFT panels, and no fit-range picking on them.
Linked data is a different link
Panel.link_group puts panels in a group that plots the same data: each
line's LINKED settings (model.py: run, x, x_fn, y, y_fn, colour) match
line by line, and the panels have the same number of lines. Unlike FFT panels
they have their own Line objects, so smoothing and background stay per panel, and
axis ranges and zoom are each panel's own. So:
- After changing a line's inputs or colour, call
_sync_inputs(cell), which copies them to the group (clearing a member's x-unit settings if its x changes);apply_controls,swapand the colour picker do. Add and remove lines throughadd_line/remove_line, which do it in every list in_group_lists. _tied(cell)is every panel a change shows in: the group plus each member's FFT or data panel._redraw_selectedredraws those.- A group's FFT panel shares one member's list, so
_group_listsdeduplicates by identity: never replace a list, only change it in place.
Things that look odd but are deliberate
- Row order, not sorted x. The field record jitters (hundreds of direction
reversals per run) and parks at the ends of sweeps, so smoothing windows
and x-unit windows follow the rows in the order they were taken.
smoothing.stretchesfinds, per row, the unbroken run of rows within the window in x. - No scipy. Savitzky–Golay is done in numpy (it matches
scipy.signal.savgol_filterto rounding error);background.fitusesnumpy.polynomial.Chebyshev.fitso high degrees stay well conditioned. - Drawn icons and triangles, not Unicode arrows: Tk's X core fonts can
show them as '®'. The same goes for text: in Tk widgets − (minus), – (en
dash) and → show as '®' and Δ as '∈'. matplotlib draws them fine, so plot
text keeps them and anything Tk shows goes through
plain()inplotter.py, or is written in ASCII (the "Savitzky-Golay" method name).·and×are fine. - Zoom survives redraws only for limits the user set (zooming turns
matplotlib's autoscale off).
_redraw_selected(keep)restores those and pushes the full view first so the toolbar's Home still works. - The controls column is a scrolling canvas (
self.side) with the Save area pinned below it; the scrollbar shows only when the column is taller than the window.
Testing by script
Build the window, patch the popups so they can't block, drive the controls, then read the state:
from cmp_plotter import plotter as P
P.messagebox.showerror = lambda title, message, parent=None: print(title, message)
app = P.Plotter() # uses ~/.config/cmp-plotter/settings.json
app.run.set(next(n for n in app.datasets if "005" in n)); app.apply_controls()
app.smooth.set("Savitzky-Golay"); app.apply_controls()
print(app.panel.line.shown, app.panel.line.error)
app.fig.savefig("/some/scratch/dir/check.png")
app.destroy()
Patch messagebox.askyesno too when making FFT panels. Wrap runs in
timeout: an unpatched popup waits forever. Don't call
choose_folder, which rewrites the user's settings file.
Style
- Match the surrounding code: short docstrings that say why, plain names, comments only where the reason isn't visible.
- UI text is plain and short; the left column is ~330 px wide, so check that
a new control doesn't widen it (
app.side_inner.winfo_reqwidth()). - Update README.md's Use section when behaviour changes.
- Commits: an imperative subject line, then a body saying what changed and
why, ending with the
Co-Authored-Byline the earlier commits use.
