2

I am trying to modify the legend of a figure that contains two overlayed scatter plots. More specifically, I want two legend handles and labels: the first handle will contain multiple points (each colored differently), while the other handle consists of a single point.

As per this related question, I can modify the legend handle to show multiple points, each one being a different color.

As per this similar question, I am aware that I can change the number of points shown by a specified handle. However, this applies the change to all handles in the legend. Can it be applied to one handle only?

My goal is to combine both approaches. Is there a way to do this?

In case it isn't clear, I would like to modify the embedded figure (see below) such that Z vs X handle shows only one-point next to the corresponding legend label, while leaving the Y vs X handle unchanged.

My failed attempt at producing such a figure is below:

Attempt at desired figure

To replicate this figure, one can run the code below:

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.legend_handler import HandlerTuple, HandlerRegularPolyCollection

class ScatterHandler(HandlerRegularPolyCollection):

    def update_prop(self, legend_handle, orig_handle, legend):
        """ """
        legend._set_artist_props(legend_handle)
        legend_handle.set_clip_box(None)
        legend_handle.set_clip_path(None)

    def create_collection(self, orig_handle, sizes, offsets, transOffset):
        """ """
        p = type(orig_handle)([orig_handle.get_paths()[0]], sizes=sizes, offsets=offsets, transOffset=transOffset, cmap=orig_handle.get_cmap(), norm=orig_handle.norm)
        a = orig_handle.get_array()
        if type(a) != type(None):
            p.set_array(np.linspace(a.min(), a.max(), len(offsets)))
        else:
            self._update_prop(p, orig_handle)
        return p

x = np.arange(10)
y = np.sin(x)
z = np.cos(x)

fig, ax = plt.subplots()
hy = ax.scatter(x, y, cmap='plasma', c=y, label='Y vs X')
hz = ax.scatter(x, z, color='k', label='Z vs X')
ax.grid(color='k', linestyle=':', alpha=0.3)
fig.subplots_adjust(bottom=0.2)
handler_map = {type(hz) : ScatterHandler()}
fig.legend(mode='expand', ncol=2, loc='lower center', handler_map=handler_map, scatterpoints=5)

plt.show()
plt.close(fig)

One solution that I do not like is to create two legends - one for Z vs X and one for Y vs X. But, my actual use case involves an optional number of handles (which can exceed two) and I would prefer not having to calculate the optimal width/height of each legend box. How else can this problem be approached?

1 Answers1

1

This is a dirty trick and not an elegant solution, but you can set the sizes of other points for Z-X legend to 0. Just change your last two lines to the following.

leg = fig.legend(mode='expand', ncol=2, loc='lower center', handler_map=handler_map, scatterpoints=5)
# The third dot of the second legend stays the same size, others are set to 0
leg.legendHandles[1].set_sizes([0,0,leg.legendHandles[1].get_sizes()[2],0,0])

The result is as shown.

enter image description here

ilke444
  • 2,641
  • 1
  • 17
  • 31
  • Your solution seems to work perfectly. But, I'm still trying to wrap my head around why. Running `print(leg.legendHandles[1].get_sizes())` shows `[36. 36. 36. 36. 36.]`. I do not understand what these correspond to, and why you use `leg.legendHandles[1].get_sizes()[2]`. If you don't mind, can you explain what is happening? –  Mar 06 '20 at 13:42
  • 1
    `legendHandles[0]` corresponds to the legend on the left, and `legendHandles[1]` corresponds to the one on the right. `get_sizes()` returns the area for each of the 5 points. I set the area of 1,2,4,5th points to 0 by `set_sizes[0,0, , 0,0]`, and 3rd one to whatever it was before, by `leg.legendHandles[1].get_sizes()[2]`. I hope this is more clear. – ilke444 Mar 06 '20 at 17:52