Skip to content
Draft
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
d874c4d
Adding height scale functionality to skewt.py
Jun 18, 2025
4a745b0
Changing aspect
Jun 18, 2025
4afaa8c
Updating skew-t height axis internals
Jun 18, 2025
4c06fd6
Changing axis tick handling
Jun 20, 2025
c096ef6
Changed axis name to heightax
Jun 20, 2025
5502645
Added image comparison tests for height axis
Jun 20, 2025
bceeae9
Fixing code style
Jun 20, 2025
84f4675
Fixing code style
Jun 20, 2025
d57c0c0
Fixing code style
Jun 20, 2025
45d9627
Fixing code style (linting)
Jun 20, 2025
d80f260
Changing skewt height axis to a seperate function and updating tests …
Jun 23, 2025
af8c385
Updating height change coords test
Jun 23, 2025
af449b1
Updating code style
Jun 23, 2025
699cfcc
Updating code style
Jun 23, 2025
904abb5
Code style fixes
Jun 23, 2025
7c3c93a
Updating testing tolerance
Jun 23, 2025
35e2a50
Removed trailing whitespace
Jun 23, 2025
2bbe602
Fixing doc building error
Jun 23, 2025
4a05f55
Merging with unidata main
Jun 23, 2025
8b7cd8a
Cleaning up tick logic for heightax
Jun 24, 2025
7324860
Fixing style errors
Jun 24, 2025
6a2b441
Style errors
Jun 24, 2025
c8b598b
Style fixes
Jun 24, 2025
76c0261
Updating to twinx() bc log/linear axis on secondary_yaxis didn't work
Jun 24, 2025
1c446fe
im going to revert this anyways
blue-jaye-121 Jul 3, 2025
989e094
like i said gonna reset
blue-jaye-121 Jul 3, 2025
c7086da
Merge branch 'Unidata:main' into height_scale
blue-jaye-121 Jul 7, 2025
9fb5b6e
Merge branch 'height_scale' of https://github.com/blue-jaye-121/MetPy…
blue-jaye-121 Jul 30, 2025
3f8760f
Reverting skewt.py to my most successful attempt
blue-jaye-121 Jul 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 32 additions & 3 deletions src/metpy/plots/skewt.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@
from matplotlib.patches import Circle
from matplotlib.projections import register_projection
import matplotlib.spines as mspines
from matplotlib.ticker import MultipleLocator, NullFormatter, ScalarFormatter
from matplotlib.ticker import (MultipleLocator, FixedLocator, NullFormatter,

Check failure on line 20 in src/metpy/plots/skewt.py

View workflow job for this annotation

GitHub Actions / Run Lint Tools

[flake8] reported by reviewdog 🐶 I001 isort found an import in the wrong position Raw Output: ./src/metpy/plots/skewt.py:20:1: I001 isort found an import in the wrong position
ScalarFormatter, NullLocator)

Check failure on line 21 in src/metpy/plots/skewt.py

View workflow job for this annotation

GitHub Actions / Run Lint Tools

[flake8] reported by reviewdog 🐶 I001 isort found an import in the wrong position Raw Output: ./src/metpy/plots/skewt.py:21:1: I001 isort found an import in the wrong position
import matplotlib.transforms as transforms

Check failure on line 22 in src/metpy/plots/skewt.py

View workflow job for this annotation

GitHub Actions / Run Lint Tools

[flake8] reported by reviewdog 🐶 I005 isort found an unexpected missing import Raw Output: ./src/metpy/plots/skewt.py:22:1: I005 isort found an unexpected missing import

Check failure on line 22 in src/metpy/plots/skewt.py

View workflow job for this annotation

GitHub Actions / Run Lint Tools

[flake8] reported by reviewdog 🐶 I005 isort found an unexpected missing import Raw Output: ./src/metpy/plots/skewt.py:22:1: I005 isort found an unexpected missing import
import numpy as np

from ._util import colored_line
from ..calc import dewpoint, dry_lapse, el, lcl, moist_lapse, vapor_pressure
from ..calc import (dewpoint, dry_lapse, el, lcl, moist_lapse, vapor_pressure,

Check failure on line 26 in src/metpy/plots/skewt.py

View workflow job for this annotation

GitHub Actions / Run Lint Tools

[flake8] reported by reviewdog 🐶 I001 isort found an import in the wrong position Raw Output: ./src/metpy/plots/skewt.py:26:1: I001 isort found an import in the wrong position
pressure_to_height_std, height_to_pressure_std)

Check failure on line 27 in src/metpy/plots/skewt.py

View workflow job for this annotation

GitHub Actions / Run Lint Tools

[flake8] reported by reviewdog 🐶 I001 isort found an import in the wrong position Raw Output: ./src/metpy/plots/skewt.py:27:1: I001 isort found an import in the wrong position
from ..calc.tools import _delete_masked_points

Check failure on line 28 in src/metpy/plots/skewt.py

View workflow job for this annotation

GitHub Actions / Run Lint Tools

[flake8] reported by reviewdog 🐶 I005 isort found an unexpected missing import Raw Output: ./src/metpy/plots/skewt.py:28:1: I005 isort found an unexpected missing import

Check failure on line 28 in src/metpy/plots/skewt.py

View workflow job for this annotation

GitHub Actions / Run Lint Tools

[flake8] reported by reviewdog 🐶 I005 isort found an unexpected missing import Raw Output: ./src/metpy/plots/skewt.py:28:1: I005 isort found an unexpected missing import
from ..interpolate import interpolate_1d
from ..package_tools import Exporter
from ..units import concatenate, is_quantity, units
Expand Down Expand Up @@ -261,7 +263,8 @@

"""

def __init__(self, fig=None, rotation=30, subplot=None, rect=None, aspect=80.5):
def __init__(self, fig=None, rotation=30, subplot=None, rect=None,
aspect=80.5, show_heights=False):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would you feel about this interface,

skew = metpy.calc.SkewT(show_heights=True)

vs

skew = metpy.calc.SkewT()
skew.heightax()
# or
skew.add_heightax()
# or
skew.add_height_scale()

etc etc etc., or something like that? To me, a height axis almost exactly straddles the line of "plot config" vs "physical parameter", so I'm torn, personally.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dopplershift Mentioned on the original issue:

I'd really like to have it separated from the plotting call
In an ideal world, it'd be dynamically updated from the y-axis in pressure coords. I'm not sure if a
matplotlib spine is enough, or if maybe we do need the separate Axes, but we use matplotlib's event
handling to update the range on the height scale

Which to me points to a separate method like add_heightax(). I originally interpreted the issue this way because a height axis feels a bit more like a "plot config" but in theory it shouldn't be hard to switch it around; the issue will be making sure it can dynamically update. I'll bake this up (maybe in a separate branch for now) and see what it entails

r"""Create SkewT - logP plots.

Parameters
Expand All @@ -286,6 +289,10 @@
Aspect ratio (i.e. ratio of y-scale to x-scale) to maintain in the plot.
Defaults to 80.5. Passing the string ``'auto'`` tells matplotlib to handle
the aspect ratio automatically (this is not recommended for SkewT).
show_heights : boolean, optional
Copy link
Member

@dcamron dcamron Jun 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
show_heights : boolean, optional
show_heights : bool, optional

if we keep this interface, this should fix the doc builds yelling at you!

Flag for showing heights as a secondary y axis.
Calculated from pressure_to_height_std.
(defaults to false)

"""
if fig is None:
Expand Down Expand Up @@ -335,6 +342,28 @@
self.dry_adiabats = None
self.moist_adiabats = None

if (show_heights):
# Set a secondary axis with height from pressure_to_height_standard
# Requires direct and inverse fctns - pressure axis and height axis
def pressure_axis(p):
return pressure_to_height_std(units.Quantity(p, 'hPa')).m_as('km')

def height_axis(h):
return height_to_pressure_std(units.Quantity(h, 'km')).m
# Positions the axis .12 normalized units to the left of the pressure axis
self.heightax = self.ax.secondary_yaxis(-0.12,
functions=(pressure_axis, height_axis))
# Set ylim based on pressure limits
self.heightax.set_ylim(pressure_to_height_std(units.Quantity
(self.ax.get_ylim(), 'hPa')))
self.heightax.yaxis.set_units(units.km)
self.heightax.yaxis.set_minor_locator(NullLocator())
self.heightax.yaxis.set_major_formatter(ScalarFormatter())
# Create ticks on the height axis counting by 1 from min to max
ymin, ymax = self.heightax.get_ylim()
yticks = np.arange(ymin, ymax + 1, 1)
self.heightax.yaxis.set_major_locator(FixedLocator(yticks))

# Maintain a reasonable ratio of data limits.
self.ax.set_aspect(aspect, adjustable='box')

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
157 changes: 157 additions & 0 deletions tests/plots/test_skewt.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,49 @@ def test_skewt_api():
return fig


@pytest.mark.mpl_image_compare(style='default', tolerance=0.069)
def test_skewt_api_with_heights():
"""Test the SkewT API with height axis."""
with matplotlib.rc_context({'axes.autolimit_mode': 'data'}):
fig = plt.figure(figsize=(9, 9))
skew = SkewT(fig, aspect='auto', show_heights=True)

# Plot the data using normal plotting functions, in this case using
# log scaling in Y, as dictated by the typical meteorological plot
p = np.linspace(1000, 100, 10)
t = np.linspace(20, -20, 10)
u = np.linspace(-10, 10, 10)
skew.plot(p, t, 'r')
skew.plot_barbs(p, u, u)

skew.ax.set_xlim(-20, 30)
skew.ax.set_ylim(1000, 100)

# Add the relevant special lines
skew.plot_dry_adiabats()
skew.plot_moist_adiabats()
skew.plot_mixing_lines()

# Call again to hit removal statements
skew.plot_dry_adiabats()
skew.plot_moist_adiabats()
skew.plot_mixing_lines()

# You can't remove text from a secax with remove_text so do it manually
skew.heightax.set_ylabel('')
skew.heightax.set_yticklabels([])
skew.ax.set_title('')
skew.ax.set_xlabel('')
skew.ax.set_ylabel('')
skew.ax.set_xticklabels([])
skew.ax.set_yticklabels([])

# prevents label from being cut off by savefig
plt.tight_layout()

return fig


@pytest.mark.mpl_image_compare(remove_text=True, style='default', tolerance=0.32)
def test_skewt_api_units():
"""Test the SkewT API when units are provided."""
Expand All @@ -70,6 +113,42 @@ def test_skewt_api_units():
return fig


@pytest.mark.mpl_image_compare(style='default', tolerance=.32)
def test_skewt_api_units_heights():
"""Test the SkewT API when units are provided and show_heights is true."""
with matplotlib.rc_context({'axes.autolimit_mode': 'data'}):
fig = plt.figure(figsize=(9, 9))
skew = SkewT(fig, show_heights=True)
p = (np.linspace(950, 100, 10) * units.hPa).to(units.Pa)
t = (np.linspace(18, -20, 10) * units.degC).to(units.kelvin)
u = np.linspace(-20, 20, 10) * units.knots

skew.plot(p, t, 'r')
skew.plot_barbs(p, u, u)

# Add the relevant special lines
skew.plot_dry_adiabats()
skew.plot_moist_adiabats()
skew.plot_mixing_lines()

# This works around the fact that newer pint versions default to degrees_Celsius
skew.ax.set_xlabel('degC')

# You can't remove text from a secax with remove_text so do it manually
skew.heightax.set_ylabel('')
skew.heightax.set_yticklabels([])
skew.ax.set_title('')
skew.ax.set_xlabel('')
skew.ax.set_ylabel('')
skew.ax.set_xticklabels([])
skew.ax.set_yticklabels([])

# Prevents labels from being cut off by savefig
plt.tight_layout()

return fig


@pytest.mark.mpl_image_compare(tolerance=0., remove_text=True, style='default')
def test_skewt_default_aspect_empty():
"""Test SkewT with default aspect and no plots, only special lines."""
Expand Down Expand Up @@ -113,6 +192,24 @@ def test_skewt_subplot():
return fig


@pytest.mark.mpl_image_compare(tolerance=.811, style='default')
def test_skewt_subplot_heights():
"""Test using skewT on a sub-plot with height axis."""
fig = plt.figure(figsize=(9, 9))
skew = SkewT(fig, subplot=(2, 2, 1), aspect='auto', show_heights=True)

# You can't remove text from a secax with remove_text so do it manually
skew.heightax.set_ylabel('')
skew.heightax.set_yticklabels([])
skew.ax.set_title('')
skew.ax.set_xlabel('')
skew.ax.set_ylabel('')
skew.ax.set_xticklabels([])
skew.ax.set_yticklabels([])

return fig


@pytest.mark.mpl_image_compare(tolerance=0, remove_text=True, style='default')
def test_skewt_gridspec():
"""Test using SkewT on a GridSpec sub-plot."""
Expand All @@ -130,6 +227,24 @@ def test_skewt_with_grid_enabled():
plt.close(s.ax.figure)


@pytest.mark.mpl_image_compare(tolerance=0, style='default')
def test_skewt_gridspec_heights():
"""Test using SkewT on a GridSpec sub-plot."""
fig = plt.figure(figsize=(9, 9))
gs = GridSpec(1, 2)
skew = SkewT(fig, subplot=gs[0, 1], aspect='auto', show_heights=True)

# You can't remove text from a secax with remove_text so do it manually
skew.heightax.set_ylabel('')
skew.heightax.set_yticklabels([])
skew.ax.set_title('')
skew.ax.set_xlabel('')
skew.ax.set_ylabel('')
skew.ax.set_xticklabels([])
skew.ax.set_yticklabels([])
return fig


@pytest.mark.mpl_image_compare(tolerance=0., remove_text=True, style='default')
def test_skewt_arbitrary_rect():
"""Test placing the SkewT in an arbitrary rectangle."""
Expand All @@ -144,6 +259,23 @@ def test_skewt_subplot_rect_conflict():
SkewT(fig, rect=(0.15, 0.35, 0.8, 0.3), subplot=(1, 1, 1))


@pytest.mark.mpl_image_compare(tolerance=0., style='default')
def test_skewt_arbitrary_rect_heights():
"""Test placing the SkewT in an arbitrary rectangle with height axis."""
fig = plt.figure(figsize=(9, 9))
skew = SkewT(fig, rect=(0.15, 0.35, 0.8, 0.3), aspect='auto', show_heights=True)

# You can't remove text from a secax with remove_text so do it manually
skew.heightax.set_ylabel('')
skew.heightax.set_yticklabels([])
skew.ax.set_title('')
skew.ax.set_xlabel('')
skew.ax.set_ylabel('')
skew.ax.set_xticklabels([])
skew.ax.set_yticklabels([])
return fig


@pytest.mark.mpl_image_compare(tolerance=0.0198, remove_text=True, style='default')
def test_skewt_units():
"""Test that plotting with SkewT works with units properly."""
Expand Down Expand Up @@ -308,6 +440,31 @@ def test_skewt_wide_aspect_ratio(test_profile):
return fig


@pytest.mark.mpl_image_compare(tolerance=0.039, style='default')
def test_skewt_wide_aspect_ratio_heights(test_profile):
"""Test plotting a skewT with a wide aspect ratio with height axis."""
p, t, _, tp = test_profile

fig = plt.figure(figsize=(12.5, 3))
skew = SkewT(fig, aspect='auto', show_heights=True)
skew.plot(p, t, 'r')
skew.plot(p, tp, 'k')
skew.ax.set_xlim(-30, 50)
skew.ax.set_ylim(1050, 700)

# You can't remove text from a secax with remove_text so do it manually
skew.heightax.set_ylabel('')
skew.heightax.set_yticklabels([])
skew.ax.set_title('')
skew.ax.set_xlabel('')
skew.ax.set_xticklabels([])
skew.ax.set_yticklabels([])

# This works around the fact that newer pint versions default to degrees_Celsius
skew.ax.set_xlabel('degC')
return fig


@pytest.mark.mpl_image_compare(tolerance=0, remove_text=True)
def test_hodograph_api():
"""Basic test of Hodograph API."""
Expand Down
Loading