Skip to content

[Bug]: Issue with setting axis limits on 3D plots #25804

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

Closed
nbfazel opened this issue May 1, 2023 · 20 comments · Fixed by #27349
Closed

[Bug]: Issue with setting axis limits on 3D plots #25804

nbfazel opened this issue May 1, 2023 · 20 comments · Fixed by #27349
Assignees

Comments

@nbfazel
Copy link

nbfazel commented May 1, 2023

Bug summary

I have created 3D surface plots using matplotlib as described [here]. Everything works except when I set the axis limits. Then the plots look very strange. It appears that commands such as Axes.set_xlim() (whether or not Axes.set_xbound()is used) limit the axis but not the data. The data is plotted and appears to continue beyond the axis limits.

Code for reproduction

axes = []
fig0, ax0 = plt.subplots(subplot_kw={"projection": "3d"})
axes.append(ax0)

surf0 = axes[0].plot_surface(DS_ee, DS_cc, DS_array, cmap=cm.jet, linewidth=0, antialiased=False)

axes[0].xaxis.set_major_locator(MultipleLocator(10))
axes[0].xaxis.set_minor_locator(MultipleLocator(2))
axes[0].yaxis.set_major_locator(MultipleLocator(1))
axes[0].yaxis.set_minor_locator(MultipleLocator(0.2))

axes[0].set_zlabel('Default Spectrum [photons/keV/pixel]', fontsize=7)
axes[0].set_xlim(left=0, right=10)
axes[0].set_zlim(0, 3000)
# axes[0].set_xbound(0, 10)
# axes[0].set_zbound(0, 3000)

for t in axes[0].xaxis.get_major_ticks(): t.label.set_fontsize(6)
for t in axes[0].yaxis.get_major_ticks(): t.label.set_fontsize(6)
for t in axes[0].zaxis.get_major_ticks(): t.label.set_fontsize(6)

axes[0].grid(False)
axes[0].view_init(elev=elev_angle, azim=azim_angle)
axes[0].set(xlabel='E [keV]', ylabel=ylabel)

Actual outcome

Surface plot after using Axes.set_xlim(left=0, right=10):

Screenshot 2023-04-28 at 2 53 45 PM

Surface plot after using using Axes.set_xlim(left=0, right=10) and axes[0].set_zlim(0, 3000):

Screenshot 2023-04-28 at 2 54 18 PM

Expected outcome

The image should look like the following but with the E axis ending at 10 keV and the vertical axis ending at 3000:

Screenshot 2023-04-28 at 2 51 16 PM

Additional information

I'm running the code that imports matplotlib in Spyder. I can share upon request the full code and input files needed to reproduce the error.

I tried all the suggestions in this post and also this one and none worked. For example, I tried:

axes[0].set_xlim(left=0, right=10)
axes[0].set_zlim(0, 3000)

Also

axes[0].set_xlim(min=0, max=10)
axes[0].set_zlim(0, 3000)

and

axes[0].set_xlim(left=0, right=10)
axes[0].set_zlim(0, 3000)
axes[0].set_xbound(0, 10)
axes[0].set_zbound(0, 3000)

and

axes[0].set_xlim3d(left=0, right=10)
axes[0].set_zlim3d(0, 3000)

Operating system

OS/X Ventura 13.3.1

Matplotlib Version

3.5.2

Matplotlib Backend

Qt5Agg

Python version

3.9.13

Jupyter version

No response

Installation

None

@scottshambaugh
Copy link
Contributor

scottshambaugh commented May 2, 2023

I think that the axes not "truncating" the plotted surfaces is the intended behavior, though an option to crop data to the box inside the axis planes would be useful. Right now clip_box only clips the 2D projection, and I think clip_box_3d or something similar would be a worthwhile feature.

Somewhat related to #23450, where the issue is reversed - there we want to be clipping based on the 3d box, not the 2d one.

@scottshambaugh
Copy link
Contributor

scottshambaugh commented May 2, 2023

A workaround in your case might be to edit your data so that points higher than 10 / 3000 on the respective axes are set to np.nan and are thus hidden. This may require calculating the interpolated contour line at 3000 and manually adding those points back to your dataset, so you don't get a jagged "sawtooth" there.

@nbfazel
Copy link
Author

nbfazel commented May 3, 2023

Thanks. I really don't understand why this is the intended behavior because it sure doesn't make sense and I wasted a lot of time trying to change the behavior. I just want to be able to set the axis limit without modifying the data. Any chance this could be fixed?

@scottshambaugh
Copy link
Contributor

scottshambaugh commented May 5, 2023

Hi @nbfazel thank you for raising the issue! I'll try to take a look at this, but fair warning that it might not be soon and matplotlib only releases a new version every 6-12 months or so.

I see two ways to implement this:

  1. Extend bounding boxes to 3D, which I think would be a good bit more work but is a little more flexible.
  2. Have a clip_to_axis_bounds flag (or similar) which does specifically this behavior.

I think (2) is definitely much easier and covers most of the desired use cases here, and could always be a special case of (1) if that ever gets implemented.

@nbfazel
Copy link
Author

nbfazel commented May 5, 2023

Thank you @scottshambaugh. I appreciate that. I will look for a interim solution while this is being implemented.

@scottshambaugh
Copy link
Contributor

scottshambaugh commented May 9, 2023

I did some digging into this and think it will end up being rather complicated for surfaces plots like your example. The problem is that matplotlib's 3D renderer does not use a ray-tracing algorithm, so you cannot easily threshold drawing only part of a face for a polygon between specific bounds.

Take (A) as your initial set of points. It would be fairly easy to threshold your points / vertices based on bounds, and I think that would work well for scatter plots. In the case of surfaces however, this will likely mean jagged sawtooth edges since you would not be drawing the polygons which have a vertex right outside your bounds, as in (B). Perhaps that's okay as a first step implementation.

The way to get around this would be to fill in the gaps with new polygons created on the fly to edge match with the bounds, as in (C). That however could get very complicated especially in 3D near the corners/edges of the bounds from multiple axes, or in cases where your bounds are tighter than the extent of a single polygon.

image

@nbfazel
Copy link
Author

nbfazel commented May 11, 2023

Thanks for the update. I wonder how this is done Matlab or Mathematica.

@nbfazel
Copy link
Author

nbfazel commented May 21, 2023

In this example (scroll down to the answer), limits are set for all axes, without running into the issues I described above. I thought maybe it was because it also set axes to off. I tried that and it didn't make a difference. I'm not sure why setting axis limits works in the example. (I ran the example and produce the same image.)

@nbfazel
Copy link
Author

nbfazel commented Aug 1, 2023

Just checking to see if we know why in the example provided above (on May 21), they don't run into the problem after setting axis limit in 3D. Thanks!

@scottshambaugh
Copy link
Contributor

scottshambaugh commented Aug 1, 2023

The issue persists in the example above. If you don't turn the axes off, then this is more visible:
image

If you zoom out, you can see that the limits weren't truncated for the data (500/(9*pi) = 17.68):
image

MATLAB and Mathematica use their own renderers, I expect automatically cropping data in 3D space would be quite a bit of effort to bring into matplotlib.

@nbfazel
Copy link
Author

nbfazel commented Aug 1, 2023

Thank you so much. So do you think I should just go with the advice you gave on May 2? ("edit your data so that points higher than 10 / 3000 on the respective axes are set to np.nan")

@scottshambaugh
Copy link
Contributor

scottshambaugh commented Aug 1, 2023

If you need to make these plots any time soon, then yes. With matplotlib as a 99%-volunteer effort it can take quite a long time to implement feature requests, and the number of people familiar with the 3D code is a smaller fraction than the 2D plotting. If you want to take a shot at adding this though, I think that an option to clip 3D data to the axis limits would be a welcome feature. :)

@nbfazel
Copy link
Author

nbfazel commented Aug 2, 2023

Thanks! Recently I tried to fix an issue with SciPy (that I had reported) and even though the fix wasn't complicated, I spent a lot of time trying to set up my development environment and ultimately gave up. This is more challenging since I would first need to familiarize myself with 3D rendering and then again try to set up my development environment.

@scottshambaugh
Copy link
Contributor

scottshambaugh commented Sep 18, 2023

I did a dive into stackoverflow, and it looks like this a very common use case. I think we should go ahead and implement a flag to clip to 3D axis bounds now, and we can figure out how to clean up jagged edges later.

This question shows how to do this manually using MaskedArrays, which I think will make a for a good approach here. Will have to think about recalculating this on zoom changes, might slow down interactivity quite a bit. https://stackoverflow.com/questions/56411587/matplotlib-3d-plot-how-to-use-set-zlim-correctly as a way to do this manually

I found the following issues where this behavior was desired:

@scottshambaugh scottshambaugh self-assigned this Sep 18, 2023
@harish-srini
Copy link

LIke the suggestion by @scottshambaugh what if matplotlib chose the make the plots using the copy of the original numpy array with data points set to np.nan based on the choice of limits? Would that be easy to implement within matplotlib itself?

@scottshambaugh
Copy link
Contributor

scottshambaugh commented Nov 20, 2023

Draft PR is up to add this functionality, I'll be fleshing it out over the next week.

That PR will implement option (B) from my comment above, with jagged edges at the boundary. I'm going to say that getting straight/smooth edges rather than jagged edges is a "won't do" feature for now, as it would require some pretty drastic changes to how the renderer works, so (C) won't happen and I'll close this issue once the PR is merged. For further tracking of the smooth edges question, that bit can be considered a duplicate of this question: #8902

@nbfazel
Copy link
Author

nbfazel commented Nov 20, 2023

OK, thank you. I'll give it a try when it becomes available.

@scottshambaugh
Copy link
Contributor

#27349 to implement dynamic clipping in 3D plots is now ready for review if you'd like to play with it!

@nbfazel
Copy link
Author

nbfazel commented Nov 26, 2023

Thanks! What do I need to do to sync up and pull the version of the code with the fix?

@scottshambaugh
Copy link
Contributor

scottshambaugh commented Nov 26, 2023

@nbfazel you can find the branch that is getting merged here: https://github.com/scottshambaugh/matplotlib/tree/3d_dynamic_masking

And this stackoverflow answer has good directions on the commands to do that: https://stackoverflow.com/questions/14383212/git-pulling-a-branch-from-another-repository

But hopefully that issue has enough info to evaluate the behavior without having to download and test locally.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants