Skip to content

Legend placement bug #1235

Closed
Closed

Description

@dmcdougall

This script produces a legend that covers the red point:

import matplotlib.pyplot as plt

colors = ['b','g','r']
for n in range(3):
    plt.scatter([n,],[n,],color=colors[n])
plt.legend(['foo','foo','foo'],loc='best')
plt.gca().set_xlim(-0.5, 2.2)
plt.gca().set_ylim(-0.5, 2.2)
plt.show()

Activity

  1. pelson commented on Sep 13, 2012

    @pelson
    Member

    Fantastic simple, reproducible case.

    The problem is that the best placement only takes axes.lines into account. To get to the actual code, look at legend.py, and in particular the _find_best_position and _auto_legend_data methods.

  2. dmcdougall commented on Sep 13, 2012

    @dmcdougall
    MemberAuthor

    @pelson Thanks for that info. Given your comment, would it be too presumptuous to say we'd also see this behaviour for text and arrows under the legend? Perhaps _find_best_position and _auto_legend_data should look more broadly at the axes artists?

    Edit: Grammar.

  3. pelson commented on Sep 13, 2012

    @pelson
    Member

    Perhaps _find_best_position and _auto_legend_data should look more broadly at the axes artists

    In my eyes, that would make best better...

  4. dmcdougall commented on Sep 18, 2012

    @dmcdougall
    MemberAuthor

    So I'm trying to fix this just for the scatter case, so I can see what's going on. I'm having problems getting the bounding box for each of the points. This is my loop:

    for handle in ax.collections:
        paths = handle.get_paths()
        transforms = handle.get_transforms()
        for p, t in zip(paths, transforms):
            box = p.get_extents(transform=t)
            print(box)

    There are 3 PathCollections, presumably one for each point in the scatter plot, but I'm getting rubbish bounding boxes:

    Bbox('array([[-2.23606798, -2.23606798],\n       [ 2.23606798,  2.23606798]])')
    Bbox('array([[-2.23606798, -2.23606798],\n       [ 2.23606798,  2.23606798]])')
    Bbox('array([[-2.23606798, -2.23606798],\n       [ 2.23606798,  2.23606798]])')
    Bbox('array([[-2.23606798, -2.23606798],\n       [ 2.23606798,  2.23606798]])')
    Bbox('array([[-2.23606798, -2.23606798],\n       [ 2.23606798,  2.23606798]])')
    Bbox('array([[-2.23606798, -2.23606798],\n       [ 2.23606798,  2.23606798]])')
    

    Any idea what's going on? Did I do something silly?

  5. pelson commented on Sep 18, 2012

    @pelson
    Member

    There are 3 PathCollections, presumably one for each point in the scatter plot

    The way this code has been written, yes, there should be 3 collections. In most cases though, one would plot the 3 points in one PathCollection i.e. plt.scatter(range(3), range(3)). It may be that the bounding box for a collection cannot be broken down for each of its components.

    My knowledge of Collections is very limited, but you may be interested in the _offsets and _transOffset attributes on a Collection...

  6. dmcdougall commented on Sep 18, 2012

    @dmcdougall
    MemberAuthor

    Aha! Thank you. So a PathCollection is actually just a single path, together with a bunch of offsets. Then that's enough information to define the whole collection. So it should be easy to get the bounding box for each component in the collection: Work out the bounding box of the path. Then, for each offset, transform that bounding box by the offset amount.

  7. pelson commented on Sep 19, 2012

    @pelson
    Member

    So it should be easy to get the bounding box for each component in the collection: Work out the bounding box of the path. Then, for each offset, transform that bounding box by the offset amount.

    Easy when you put it like that :-)

  8. wmak commented on Mar 1, 2014

    @wmak

    Hey, I took a crack at solving this bug along with some help from @abalmeida7 and @ShaiMitchell
    I used the _offsets from the collections that @dmcdougall presented above, and just added one to the badness score if the point intersected with the legend box.

    @@ -716,6 +716,7 @@ class Legend(Artist):
             ax = self.parent
             bboxes = []
             lines = []
    +        points = []
    
             for handle in ax.lines:
                 assert isinstance(handle, Line2D)
    @@ -734,12 +735,15 @@ class Legend(Artist):
                     transform = handle.get_transform()
                     bboxes.append(handle.get_path().get_extents(transform))
    
    +        for handle in ax.collections:
    +            points.append(handle._offsets)
    +
             try:
                 vertices = np.concatenate([l.vertices for l in lines])
             except ValueError:
                 vertices = np.array([])
    
    -        return [vertices, bboxes, lines]
    +        return [vertices, bboxes, lines, points]
    
         def draw_frame(self, b):
             'b is a boolean.  Set draw frame to b'
    @@ -896,7 +900,7 @@ class Legend(Artist):
             # should always hold because function is only called internally
             assert self.isaxes
    
    -        verts, bboxes, lines = self._auto_legend_data()
    +        verts, bboxes, lines, points = self._auto_legend_data()
    
             bbox = Bbox.from_bounds(0, 0, width, height)
             if consider is None:
    @@ -927,12 +927 @@ class Legend(Artist):
                     if line.intersects_bbox(legendBox):
                         badness += 1
    
    +            for point in points:
    +                val = self.parent.transData.transform(np.vstack(point[0]).T)
    +                if legendBox.count_contains(val):
    +                    badness += 1
                 ox, oy = l, b
                 if badness == 0:
                     return ox, oy
    
                 candidates.append((badness, (l, b)))
    
  9. tacaswell commented on Mar 3, 2014

    @tacaswell
    Member

    @wmak Could you put that in a PR? It make reviewing proposed changes much easier (and lets the CI server have at it).

  10. added a commit that references this issue on Mar 3, 2014
  11. added a commit that references this issue on Mar 7, 2016
    da49463
  12. added 2 commits that reference this issue on Mar 7, 2016
    01c73b4
    869cc9d
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions