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

Legend placement bug #1235

Closed
dmcdougall opened this issue Sep 12, 2012 · 9 comments · Fixed by #6110
Closed

Legend placement bug #1235

dmcdougall opened this issue Sep 12, 2012 · 9 comments · Fixed by #6110

Comments

@dmcdougall
Copy link
Member

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()
@pelson
Copy link
Member

pelson commented Sep 13, 2012

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.

@dmcdougall
Copy link
Member Author

@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.

@pelson
Copy link
Member

pelson commented Sep 13, 2012

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

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

@dmcdougall
Copy link
Member Author

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?

@pelson
Copy link
Member

pelson commented Sep 18, 2012

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...

@dmcdougall
Copy link
Member Author

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.

@pelson
Copy link
Member

pelson commented Sep 19, 2012

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 :-)

@wmak
Copy link

wmak commented Mar 1, 2014

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)))

@tacaswell
Copy link
Member

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

wmak pushed a commit to wmak/matplotlib that referenced this issue Mar 3, 2014
@wmak wmak mentioned this issue Mar 3, 2014
dashed added a commit to nansonzheng/matplotlib that referenced this issue Mar 7, 2016
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