1

I am trying to draw a custom legend, where on one line I have some circles of increasing radius, followed by the word "Income". I consider this a beautiful way to show that a circle's size corresponds to the subject's income.

The legend has to be manually drawn. Here is how I achieve that:

class AnyObjectHandler(HandlerBase):
    def create_artists(self, legend, orig_handle,
                       x0, y0, width, height, fontsize, trans):

        legend.numpoints = 1
        l1 = plt.Line2D([x0 - 40, y0 + width], [0.3 * height, 0.3 * height], color='blue',
                        marker='o', markersize=10, markerfacecolor="blue")

        return [l1]

fig.legend([object], ['Income'], numpoints=1,
           handler_map={object: AnyObjectHandler()})

The problem is that even though I tried to specify numpoints == 1 twice, the legend still comes with the default 2 markers per line. A related question (where I found how to set numpoints to 1 is this: matplotlib Legend Markers Only Once

This is what the above code yields:

Currently produced legen

Instead of this, I would like the line to only show one circle. Doesn't matter which one.

pilu
  • 720
  • 5
  • 16
  • 1
    1) You do not use `numpoints` anywhere in your `Line2D`s, how should they know that you want it to use it? 2) You create a line with 2 points. Which of those 2 points should have the marker? – ImportanceOfBeingErnest Jun 21 '18 at 12:23
  • The `Line2D` does not accept any argument that has to do with points. Its just a line filled with points, how many they are is normally controlled by the `numpoints` argument of the `legend`. This works for me as long as the legend is not custom. – pilu Jun 21 '18 at 12:24
  • Check the linked question on my edit. – pilu Jun 21 '18 at 12:30
  • 1
    Line2D should allow to use `markevery`. Alternatively, you may create the points manually. In either case, it is your duty to actually use the numpoints somoewhere in the code you create. If you can tell how the legend should look like in dependence of `numpoints`, I could of course provide a solution. – ImportanceOfBeingErnest Jun 21 '18 at 12:33
  • I added an image of what I am getting and an explanation of what I want achieved. To simplify let's only consider a single line. To reproduce just create a random plot and use this code to make the legend, as the legend is completely unrelated to the underlying graph. – pilu Jun 21 '18 at 12:40
  • I commented about the option to use `markevery`. This is only possible when using an axes legend (`ax.legend`) but not, as in this case, with a figure legend (`fig.legend`). For details see discussion in [this PR](https://github.com/matplotlib/matplotlib/pull/11358), which will eventually introduce `markevery` as the default for legends in the long run. – ImportanceOfBeingErnest Jun 21 '18 at 13:23

1 Answers1

2

You may create a second Line2D inside the Handler's create_artist method. This allows to have a Line2D for the line and another one for the marker.

import matplotlib.pyplot as plt
from matplotlib.legend_handler import HandlerBase

class AnyObjectHandler(HandlerBase):
    def create_artists(self, legend, orig_handle,
                       x0, y0, width, height, fontsize, trans):

        l1 = plt.Line2D([x0 - 40, x0 + width], [0.3 * height, 0.3 * height], 
                        color='blue', marker='None')

        m1 = plt.Line2D([x0 - 40], [0.3 * height], color='blue', linestyle="",
                        marker='o', markersize=10, markerfacecolor="blue")

        return [l1, m1]


fig, ax = plt.subplots()

fig.legend([object], ['Income'],
           handler_map={object: AnyObjectHandler()})

plt.show()

enter image description here

This solution is independent of the numpoints argument and is probably useful in case you already know that you will always only need one point.

Alternatively you may access the numpoints to be able to specify how many points you want to use. This would best be done by subclassing a handler that actually knows about the numpoints argument.

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.legend_handler import HandlerNpoints

class AnyObjectHandler(HandlerNpoints):
    def create_artists(self, legend, orig_handle,
                       x0, y0, width, height, fontsize, trans):

        l1 = plt.Line2D([x0 - 40, x0 + width], [0.3 * height, 0.3 * height], 
                        color='blue', marker='None')

        num = self.get_numpoints(legend)
        if num == 1:
            xdata = [x0 - 40]
        else:
            xdata = np.linspace(x0 - 40, x0 + width, num)
        m1 = plt.Line2D(xdata, [0.3 * height]*len(xdata), color='blue', 
                        linestyle="", marker='o', markersize=10)

        return [l1, m1]


fig, ax = plt.subplots()

fig.legend([object], ['Income'], numpoints=1,
           handler_map={object: AnyObjectHandler()})

plt.show()

For numpoints=1 this gives that same result as the above, but you may then specify numpoints=2

enter image description here

or numpoints=3

enter image description here

etc.

ImportanceOfBeingErnest
  • 321,279
  • 53
  • 665
  • 712