Skip to content

Annotations are not clipped properly #14354

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
2xB opened this issue May 28, 2019 · 19 comments
Closed

Annotations are not clipped properly #14354

2xB opened this issue May 28, 2019 · 19 comments

Comments

@2xB
Copy link

2xB commented May 28, 2019

Bug report

Bug summary

Drawing an annotation outside the visible region inline in jupyter notebook leads to whitespace added around the figure extending it to where the annotation would be.

Code for reproduction

import matplotlib.pyplot as plt
%matplotlib inline
plt.xlim(0,1)
plt.ylim(0,1)
plt.annotate(xy=(3, 3), s='test')

Actual outcome


grafik


Expected outcome


grafik


Matplotlib version

  • Operating system: KDE neon 5.15
  • Matplotlib version: 3.0.3
  • Matplotlib backend (print(matplotlib.get_backend())): module://ipykernel.pylab.backend_inline
  • Python version: 3.6.7
  • Jupyter version: 5.2.2

Everything is installed via pip, pip and python are installed via apt.

@2xB
Copy link
Author

2xB commented May 28, 2019

Notice that if one does not set xlim and ylim, the same happens, where autoscaling would be an expected outcome, as is part of issue #10497.

@ImportanceOfBeingErnest
Copy link
Member

ImportanceOfBeingErnest commented May 29, 2019

If the clip_on argument is set to True,

plt.annotate(xy=(3, 3), s='test', clip_on=True)

the output is as expected.

A confusing bit is that the annotations have a annotation_clip argument in addition to the clip_on. This allows some extra tweaking on when to show or not to show the annotation. But it is ignored for the tight_bbox calculation.

@2xB
Copy link
Author

2xB commented May 29, 2019

So this is purely an issue of setting defaults? I would then suggest to go from clip_on=False, annotation_clip=True to either clip_on=True, annotation_clip=True or clip_on=False, annotation_clip=False, because there is no reason to make place on the canvas for an annotation that is not shown.

@2xB
Copy link
Author

2xB commented May 29, 2019

@ImportanceOfBeingErnest There is a catch: Why does this not occur when using plt.savefig?

import matplotlib.pyplot as plt
plt.xlim(0,1)
plt.ylim(0,1)
plt.annotate(xy=(3, 3), s='test')
plt.savefig("test.png")

test2


@ImportanceOfBeingErnest
Copy link
Member

No, it's not an issue of the correct defaults. The issue is that annotation_clip is considered only for finding out whether or not to draw the annotation, but not for finding out if it is to be taken into account for the calculation of the tight_bbox.

The issue only occurs if you use plt.savefig("test.png", bbox_inches="tight") (which is what the jupyter/IPython inline backend does by default).

In principle the defaults are fine: clip_on=False and annotation_clip=None. Those ensure that annotations that are defined in coordinates other than data coordinates are still shown outside the axes, which is what most people would expect.

So possible solutions are:

  • In the tight_bbox calculations, check if an artist is an annotation, or has a _check_xy attribute, and include it or not depending on its return. Drawback of this: It makes drawing less efficient and is in principle bad style, because in all other cases we would rather ask the artist if it to be included or not through the clip_on property.
  • Synchronize the clip_on with the annotation_clip. Meaning, clip_on might change its state depending on the current value of annotation_clip and the coordinate system in use. This sounds a lot better, but is harder to implement. Also it's not clear which one should take precedence in case both are defined.

@2xB
Copy link
Author

2xB commented May 29, 2019

As of my understanding while quickly looking through the source, lib/matplotlib/artist.py uses clip_on in https://github.com/matplotlib/matplotlib/blob/master/lib/matplotlib/artist.py#L268, so more or less any value for clip_on other than None or False is True. Therefore, wouldn't it be possible to introduce a new state to clip_on, so it stores clip_on=True, clip_on=False or clip_on=Annotation.ANNOTATION_CLIP, where the latter replaces annotation_clip=True? Then you could not have a weird combination of clip_on and annotation_clip, as their functionality is combined into one parameter.
This idea probably needs polishment, but it would help not having to synchronize multiple parameters.

@ImportanceOfBeingErnest
Copy link
Member

Due to the drawbacks of both possible solutions to this, I don't think this will be solved anytime soon. Also, one may consider it as a documentation issue, clarifying the role of clip_on and annotation_clip.

Hence, relabeling and remilestoning.

@2xB
Copy link
Author

2xB commented Aug 20, 2019

In principle the defaults are fine: clip_on=False and annotation_clip=None. Those ensure that annotations that are defined in coordinates other than data coordinates are still shown outside the axes, which is what most people would expect.

Sorry for reacting so late to this, but in original example, the annotation is NOT shown outside the axes, although I do not change the defaults.

@ImportanceOfBeingErnest
Copy link
Member

Yes, but you can easily get the annotation to show up using

plt.annotate(xy=(3, 3), s='test', annotation_clip=False)

So the question is, whether it's worth putting in a hacky solution with some heuristics about whether or not to show the annotation in case bbox_inches="tight" is used.

@jklymak
Copy link
Member

jklymak commented Aug 20, 2019

annotation_clip : bool or None, optional
    Whether to draw the annotation when the annotation point *xy* is
    outside the axes area.

    - If *True*, the annotation will only be drawn when *xy* is
      within the axes.
    - If *False*, the annotation will always be drawn.
    - If *None*, the annotation will only be drawn when *xy* is
      within the axes and *xycoords* is 'data'.

So, by default its none, and its not drawn. But because its not clipped using the usual clipping argument, its still included in the tight_layout calculation. I'm not sure I understand the subtleties of why annotations have multiple clip arguments, or what behaviour is particularly desired here.

@2xB
Copy link
Author

2xB commented Aug 21, 2019

@ImportanceOfBeingErnest I'm not quite sure I get this. When the default for annotation_clip is not to show the annotation outside plot borders, should not then the default for clip_on be to clip outside plot borders? Not to show the annotation outside but reserving space for it seems like a confusing default.
This would also improve consistency with every other plot item (like scatterplots, line plots etc.), that are clipped outside plot borders.

@ImportanceOfBeingErnest
Copy link
Member

The point is, you do want to show the annotations outside the axes in cases the anchor point is in data coordinates and still inside the axes, or in cases where you have used coordinates, other than data coordinates. Maybe the below animation makes this clearer.

annotation

@2xB
Copy link
Author

2xB commented Aug 21, 2019

Thank you for the explanation and the work of doing a GIF!
I think I might understand the reasoning, and have a suggestion on how to fix this. To discuss it, I created PR #15096.

@ImportanceOfBeingErnest
Copy link
Member

In #15096 @jklymak asked

Is there any use to annotation_clip and clip_on to ever be different?

As answer, here are all combinations:

image

@jklymak
Copy link
Member

jklymak commented Aug 22, 2019

OK, but I don't think we ever want the clip_on=True result. Is there a practical use for that?

@ImportanceOfBeingErnest
Copy link
Member

I'm not sure. Maybe in cases where you don't have any arrow? What's the alternative, though? Giving clip_on a third state None that is only used by annotations? But that would still require to somehow special-casing annotations in the bbox_inches="tight"- algorithms, right?

@QuLogic QuLogic modified the milestones: v3.3.0, v3.4.0 May 7, 2020
@fperonaci
Copy link

Hi, just to give a feedback (I hope this is the appropriate place) I agree with @jklymak: the case clip_on=True is weird and, I would argue, actually harmful. Indeed, I thought that the behaviour of annotation_clip=True would be achieved with clip_on=True which I think is the reasonable expectation. I think it makes a lot of sense to consider the annotation as a whole.

@QuLogic QuLogic modified the milestones: v3.4.0, v3.5.0 Jan 27, 2021
@QuLogic QuLogic modified the milestones: v3.5.0, v3.6.0 Sep 25, 2021
@QuLogic QuLogic modified the milestones: v3.6.0, v3.6.1 Sep 14, 2022
@QuLogic QuLogic modified the milestones: v3.6.1, v3.6.2 Oct 6, 2022
@QuLogic QuLogic modified the milestones: v3.6.2, v3.6.3 Oct 27, 2022
@QuLogic QuLogic modified the milestones: v3.6.3, v3.7.0 Jan 11, 2023
@ksunden ksunden modified the milestones: v3.7.0, v3.7.1 Feb 14, 2023
@QuLogic QuLogic modified the milestones: v3.7.1, v3.7-doc Mar 4, 2023
@QuLogic QuLogic modified the milestones: v3.7-doc, future releases May 2, 2023
@rcomer
Copy link
Member

rcomer commented Jun 5, 2024

I do not know what changed but I just tried this with Matplotlib v3.8.4 in a notebook and got the expected result, i.e. the figure is not resized to include the invisible annotation.

import matplotlib.pyplot as plt

plt.xlim(0,1)
plt.ylim(0,1)
plt.annotate('test', xy=(3, 3))

@rcomer rcomer closed this as completed Jun 5, 2024
@QuLogic
Copy link
Member

QuLogic commented Jul 6, 2024

This was fixed by #18772.

@QuLogic QuLogic modified the milestones: future releases, v3.4.0 Jul 6, 2024
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.

8 participants