Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hotfix contour #24

Merged
merged 24 commits into from
Jan 26, 2025
Merged

Hotfix contour #24

merged 24 commits into from
Jan 26, 2025

Conversation

cvanelteren
Copy link
Contributor

Addresses #23 partly reverts #13

Copy link
Collaborator

@beckermr beckermr left a comment

Choose a reason for hiding this comment

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

The PR that added this bit of code is #17.

The diff there removed this block of code:

                for contour in obj.collections:
                    contour.set_linestyle("-")
                    contour.set_linewidth(linewidth)
                    contour.set_edgecolor("face")

Presumably one needs something to set how the contours are drawn. I don't see how simply removing the offending code is actually fixing anything.

Copy link
Contributor Author

@cvanelteren cvanelteren left a comment

Choose a reason for hiding this comment

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

From my understanding, the collections property used to hold the lines for the different levels. This property is removed and you can now access the lines directly using get_paths which returns flat list of all the paths for all the levels. If I look at the api (here), the draw command sets the properties for the paths manually which makes the collections part redundant. Why? well my initial impulse would be to replace the collections with a similar command for the paths, but the api is already doing that on draw -- hence the removal.

@cvanelteren cvanelteren requested a review from beckermr January 19, 2025 11:09
@beckermr
Copy link
Collaborator

OK. Let's add a test against an older version of the code to ensure a plot that accesses these features comes out the same.

@cvanelteren
Copy link
Contributor Author

I went back to proplot and copied the tests -- for which many won't work -- to recreate the contour which generated:

image

I think, however, that this is wrong. The reason why I think this is that the contour shows high and lows, but the linestyles do not reflect these differences. The current unittests will produce
image
I think the major difference is created through the path effects doing something funky to the ContourSet. I can recreate the left subplot by merely setting the correct keywords in the function call of the contour on ax[1] to:

ax.contour(data, ls = '-', lw = 1.5, edgecolor = 'k')

producing

image

Digging a bit further, I went to the proplot docs to look for some tests. From the proplot docs I can generate the following figure
image

using this
import xarray as xr
import numpy as np
import pandas as pd

# DataArray
state = np.random.RandomState(51423)
linspace = np.linspace(0, np.pi, 20)
data = 50 * state.normal(1, 0.2, size=(20, 20)) * (
    np.sin(linspace * 2) ** 2
    * np.cos(linspace + np.pi / 2)[:, None] ** 2
)
lat = xr.DataArray(
    np.linspace(-90, 90, 20),
    dims=('lat',),
    attrs={'units': '\N{DEGREE SIGN}N'}
)
plev = xr.DataArray(
    np.linspace(1000, 0, 20),
    dims=('plev',),
    attrs={'long_name': 'pressure', 'units': 'hPa'}
)
da = xr.DataArray(
    data,
    name='u',
    dims=('plev', 'lat'),
    coords={'plev': plev, 'lat': lat},
    attrs={'long_name': 'zonal wind', 'units': 'm/s'}
)

# DataFrame
data = state.rand(12, 20)
df = pd.DataFrame(
    (data - 0.4).cumsum(axis=0).cumsum(axis=1)[::1, ::-1],
    index=pd.date_range('2000-01', '2000-12', freq='MS')
)
df.name = 'temperature (\N{DEGREE SIGN}C)'
df.index.name = 'date'
df.columns.name = 'variable (units)'
import ultraplot as pplt
fig = pplt.figure(refwidth=2.5, share=False, suptitle='Automatic subplot formatting')

# Plot DataArray
cmap = pplt.Colormap('PuBu', left=0.05)
ax = fig.subplot(121, yreverse=True)
ax.contourf(da, cmap=cmap, colorbar='t', lw=0.7, ec='k')

# Plot DataFrame
ax = fig.subplot(122, yreverse=True)
ax.contourf(df, cmap='YlOrRd', colorbar='t', lw=0.7, ec='k')
ax.format(xtickminor=False, yformatter='%b', ytickminor=False)
pplt.show(block = 1)

Which "looks" the same as on their docs.

Conclusion:

I think the current implementation correctly implements the contour that matplotlib intends, and prior versions of proplot do not.

I propose to set the baseline for this particular plot at the current version rather to check it further back. But I am open if someone has the time or gusto to check it with even older versions.

Subnote

I used the latest official build from proplot, with matplotlib 3.4.3 and manually rebuilding basemap -- I can share the env if one needs it.

@beckermr
Copy link
Collaborator

I don't follow your message at all. In the plot from the current unittests, the right-hand subplot looks really funky. The black contour lines only show when there is a contour label. This is fixed by the extra call specified and the fixed plot looks like the output from proplot previously.

I think we should include this call in the code so that the behavior is unchanged. Proplot deviates from matplotlib on purpose to produce better defaults and nicer plots. IDK why we would change that.

@cvanelteren
Copy link
Contributor Author

cvanelteren commented Jan 20, 2025

The unittest situation you mentioned - those were actually brought over from Ulraplot since Proplot didn't have any tests to begin with. Most of these unittests fail under proplot as they include changes from a newer matplotlib, making retroactive testing and generaing baselines hard -- unless we fix proplot (or the tests). This makes it hard to include a test that generates baselines without retroactively fixing proplot -- which I don't feel like is necessary as I will show next.

I've thoroughly checked the contour plotting and can confirm it's working properly. The main difference you're seeing is just in how the default styles work:

  • MPL normally doesn't show lines without labels
  • Proplot uses the same line style everywhere
  • Ultraplot's version uses dashed lines for negative values and solid for positive ones

The good news is you can still recreate those proplot-style images exactly like before. The changes are really just about giving users better default options out of the box.

This means that the proposed change itself, does not remove features or causes issue since we can recreate issues. The only thing we could consider is whether we want the default dashed line behavior or not.

Hopefully this clears the issues up!

@beckermr
Copy link
Collaborator

Can you send me the code that made the second plot above? That would help me understand what is going on.

@cvanelteren
Copy link
Contributor Author

import ultraplot as uplt

import numpy as np
state = np.random.RandomState(51423)
data = state.rand(5, 5) * 10 - 5
fig, axs = uplt.subplots(ncols=2)
ax = axs[0]
ax.contourf(
    data,
    edgecolor="k",
    linewidth=1.5,
    labels=True,
    labels_kw={"color": "k", "size": "large"},
)
ax = axs[1]
m = ax.contourf(data, ls = '-', lw = 1.5, edgecolor = 'k')
ax.clabel(m, colors="black", fontsize="large")  # looks finewithout this

import matplotlib.patheffects as pe

m.set_path_effects([pe.Stroke(linewidth=1.5, foreground="k), pe.Normal()])
uplt.show(block = 1)

@cvanelteren
Copy link
Contributor Author

Note it is from the unittest ;-)!

@beckermr
Copy link
Collaborator

Ah can you send a link to the unittest code?

@cvanelteren
Copy link
Contributor Author

def test_contour_labels():

@cvanelteren
Copy link
Contributor Author

I am merging this with devel. After locally testing the open PRs it all looks good for now.

@cvanelteren cvanelteren changed the base branch from main to devel January 26, 2025 09:11
@cvanelteren
Copy link
Contributor Author

is that ok @beckermr?

Copy link
Collaborator

@beckermr beckermr left a comment

Choose a reason for hiding this comment

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

Sorry for the delay here. One question about the python bound but otherwise LGTM.

pyproject.toml Outdated
@@ -19,14 +19,13 @@ maintainers = [
]
description = "A succinct matplotlib wrapper for making beautiful, publication-quality graphics."
readme = "README.rst"
requires-python = ">=3.6.0"
requires-python = ">=3.8, <3.12"
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why the upper bound here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

basemap won't install for python>=3.12. Eventhough we don't have a hard requirement for it some of the tests will fail. I contacted the people from basemap and they are working on a fix. I put a crude PR up there that monkey patches basemap for these higher versions, but it isn't (and will likely never be) merged.

I checked the code with this monkey patch for basemap and ultraplot will pass all the tests for these higher versions.

@beckermr
Copy link
Collaborator

Feel free to merge to main if you'd like.

@cvanelteren cvanelteren merged commit 1d57293 into Ultraplot:devel Jan 26, 2025
1 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants