A Tour of the Plotting Package#
Estimated reading time: 10 minutes
tephpy.plotting is the matplotlib layer: what turns a sounding into a figure. It
publishes two names — TephigramAxes, registered as the "tephigram" projection,
and add_logo — and keeps the rest private. Underneath it sits tephpy.transforms,
which owns the temperature-entropy mathematics; beside it sit tephpy.sounding and
tephpy.calc, which it consumes and which never import it back. The thermodynamics is
MetPy’s throughout.
This page is a map, not an account. Every mechanism it names is specified in a design specification or documented on the object itself, and is cited here rather than restated. Where this page and a specification disagree, the specification is right and this page is the defect — so follow the citation whenever you need the argument rather than the rule.
The Modules#
Module |
What it owns |
Where its rules live |
|---|---|---|
|
the package’s two public names, and nothing else |
— |
|
the projection and its transforms, the plotting accessors, edge ownership, the cursor readout, and the side-panel layout |
|
|
the five background families: their members, option resolution, the zoom ladder, and their labels |
|
|
the CAPE and CIN region geometry |
|
|
the wind-barb gutter, its staff, and the thinning of levels |
|
|
the branding artist, which has a specification of its own |
logo spec §3.2, §3.3, §3.4, §3.5, §3.6 |
|
one answer to “what colour is the canvas”, shared by the logo and the inline labels |
axes.py and isopleths.py carry roughly three quarters of the package between
them. The other five are small enough to read whole, and the table above is the only
place that has to change when a module arrives or leaves.
How the Isopleths Draw#
The five families — isotherms, isobars, dry adiabats, moist adiabats and humidity
mixing-ratio lines — are drawn by default, one IsoplethFamily artist each. Members
are built lazily on the first draw and cached; every draw then clips that cache to
the view rectangle, selects the members the zoom ladder calls for, and re-places the
labels (spec §3.2.1).
That is why pan, zoom, resize and set_extent need no special handling: matplotlib
calls draw on every render, and nothing caches a decision that depends on the view.
A change that moves work out of draw and into construction is a change that breaks
zoom, quietly.
Settings resolve in three tiers — accessor keywords, then tephpy.config, then
_constants — re-read on every resolve, so a configuration change can move a family
the call never mentions (spec §3.5). Passing values or interval explicitly fixes
the member set and turns the zoom ladder off.
Labels go inline or onto an edge, per family (spec §3.2.2). Which edges suit which family is a measured question rather than a matter of taste: the per-family edge-crossing counts, and the pairings they recommend, are in spec §3.2.7.
Clear Is the Constructor#
TephigramAxes.clear is where every piece of projection-owned state is built — the
transform, the equal aspect, the hidden native axes, the five families, the edges they
claim, and the default extent. Matplotlib calls it twice over: once from Axes.__init__
and again on every ax.clear(). There is no separate constructor to read, and anything
that must survive a clear has to be built here.
The order matters in one place. All five families are constructed before the first edge
sync runs, because each arms its on_change as it is built and a sync running mid-loop
would see a partial set. The extent lands last.
A family’s configure notifies on_change only when it succeeds, so a call that
raises leaves both the family and the diagram’s edges as they were (spec §3.2.3).
Who Owns an Edge#
An edge carries one family’s labels, or none. Whichever way a family is reconfigured — an
accessor, a direct configure, an Artist.set_visible — the same four steps run:
the family resolves its new options;
_check_label_edgesrejects a claim another family already holds;_sync_edge_labelscompares what the five families now ask for against what is currently claimed; and_claim_edgeor_release_edgeapplies the difference.
Validation sits at step 2 rather than inside the family because the axes owns all five families and is therefore the only object that can see a collision. Handing it to the family as a hook puts the rejection inside that family’s own rollback (spec §3.2.3).
Three properties are easy to break from inside any one of those steps.
A claim installs identity, never presentation. Locator, formatter, visibility, colour,
title. How the ticks look is stamped once, when the edge axis is created, and belongs to
the caller from the claim onwards — so ax.edge_axis("top").set_tick_params(labelsize=12)
survives everything short of a release (spec §3.2.3).
The tick-colour memory is keyed by owner as well as colour. It outlives a release, so comparing colour alone would suppress a new owner’s claim whenever its colour happened to match the previous owner’s, leaving the ticks in a colour that ties them to nothing.
The sync is re-entrancy guarded. Nothing on the sync path resolves options, so a nested call would have nothing new to apply. The guard makes that structural rather than a standing bet on matplotlib’s axis internals.
The Side Panels#
Two panels can sit to the right of the diagram: the wind-barb gutter and the indices panel, in that order, inside out (spec §3.2.7).
One divider per axes, created once and cached. A second make_axes_locatable call
would build a fresh divider and replace the parent locator, detaching the earlier panel so
that it draws over the newcomer.
Relayout, not teardown. _relayout_side_panels rebuilds the divider’s horizontal
stack and reassigns every locator whenever a panel appears, so the inside-out order holds
whichever order plot_barbs and annotate_indices were called in. Removing and
re-appending would not do: append_axes only ever appends to the size stack, so the
removed panel’s width slot stays behind as a phantom gap.
The pad widens when the right edge carries ticks. Isopleth tick labels are wider than the gutter’s own pad, so the panel nearest the diagram takes a wider one while the right edge is claimed. That is why a right-edge claim relayouts the panels at all, and it is the one place the two flows on this page meet.
Who removes the panels on a clear depends on who called it. They are the diagram’s to
remove on a direct ax.clear(), and the figure’s on a figure clear, where the diagram’s
teardown stands down rather than racing it: Figure.clear clears and deletes each entry
of a snapshot of figure.axes, so an axes removing a sibling from inside its own
clear orphans an entry the figure is still about to visit. The two cases are told apart
by the calling frame — _figure_is_clearing — because the figure’s state is identical
either way (spec §3.2.7).
Where to Look#
Changing |
Read first |
|---|---|
a family’s geometry, members or labels |
the |
which edge a family labels |
spec §3.2.3, then |
emphasis styling of individual members |
|
what an accessor accepts, or a new accessor |
spec §3.2.5, then the accessor’s own docstring |
the cursor readout |
spec §3.2.6, then |
the default view, or which edges a family can tick |
spec §3.2.7 for the crossing counts, framing spec §3.1 for what an extent frames |
a side panel’s width, order or pad |
spec §3.2.7, then |
anything that must survive |
|
the logo |
logo spec, which is a specification of its own |
The tests for all of this live in tests/plotting/, mirroring the package
(Testing).