Search code examples
pythonmatplotlibplot-annotations

How can I keep an annotation within the visible window?


How can I keep an annotation within the window, in Matplotlib? Basically, I want to fix this so that the yellow box will be visible:

enter image description here

Which was generated by this mcve:

from matplotlib import pyplot

point = pyplot.scatter(0, 0)
s = '''This is some really long text to go into the annotation that is way
too long to fit inside the figure window.'''

annotation = pyplot.annotate(
    s=s, xy=(0, 0), xytext=(-10, 10), textcoords='offset points',
    ha='right', va='bottom', bbox=dict(boxstyle='round,pad=0.5', fc='yellow')
)

pyplot.show()

I intend for the annotation to be a mouseover, but it's not necessary to implement that code to show the problem (I already figured out how to do that). However, perhaps there is a way to add a tooltip to it using tkinter, and that would probably completely fix my problem.

Perhaps there is a way to detect that the annotation is outside of the window. If there was such a way, this psuedocode would do what I want:

if annotation.is_not_within(window):
    draw_annotation_to_go_the_other_way(annotation)

Unfortunately, I haven't been able to find such a way.

Also, perhaps there is a built-in method that keeps the annotation within the bounds automagically. I also have been unable to find such a way.

How can I keep the annotation within the bounds of the figure window?


Solution

  • Since you said you could do it if you knew if the annotation was within the frame, here's a way to do so:

    def in_frame(annotation):
        pyplot.show(block=False) # we need this to get good data on the next line
        bbox = annotation.get_bbox_patch()
        uleft_coord = (0, bbox.get_height())
        pnt = bbox.get_data_transform().transform_point(uleft_coord)
        return pyplot.gcf().get_window_extent().contains(*pnt)
    

    The idea is that we grab a point from the bbox (bounding box) of the annotation, and ask the bbox of the figure (frame) if it contains that point. This only checks the upper-left-hand point, but we could get better by checking more corners. For your image, this returns False. Here are some examples that return true:

    enter image description here enter image description here

    And another that returns false:

    enter image description here

    Let's explain the function in more detail so you can work off of it:

    pyplot.show(block=False)             # display the figure, so that the annotation bbox
                                         # will return the actual box. Otherwise it says the
                                         # bbox has 1 pixel width and height.
    bbox = annotation.get_bbox_patch()
    uleft_coord = (0, bbox.get_height())
    
    # kind of scary, so let's break this down:
    pnt = bbox.get_data_transform().transform_point(uleft_coord)
          bbox.get_data_transform()      # this gives us a transform that can
                                         # convert a pixel from the coordinates
                                         # of the bbox to coordinates that the
                                         # figure uses.
    pnt = (...).transform_point(uleft_coord) # this converts the uleft_coord into
                                             # the coordinate system of the figure
    # This is that all together:
    pnt = bbox.get_data_transform().transform_point(uleft_coord)
    
    # here's another of those scary lines:
    return pyplot.gcf().get_window_extent().contains(*pnt)
           pyplot.gcf()                     # this calls "get current figure"
           pyplot.gcf().get_window_extent() # and now let's get its bbox
           (       ...     ).contains(*pnt) # and ask it if it contains pnt
    return pyplot.gcf().get_window_extent().contains(*pnt)
    

    Links: