Reputation: 217
I have a plot with two different series of curves which I am going to plot them using points and lines. I would like to have a legend such that the line and point markers share the same label.
I have tried this suggestion which works well if my both series of plots have different point types, instead of line and points. The code that I am currently using, with improper legend, is
import numpy as np
import matplotlib.pyplot as plt
Vs = np.array([0.5, 1, 1.5, 2])
Xs = np.array([[ 0.5, 0.2, 0.7],
[ 0.5, 0.3, 0.9],
[ 0.5, 0.5, 0.4],
[ 0.5, 0.7, 0.4],
[ 0.5, 0.9, 0.7],
[ 1, 0.15, 0.9],
[ 1, 0.35, 0.6],
[ 1, 0.45, 0.6],
[ 1, 0.67, 0.5],
[ 1, 0.85, 0.9],
[ 1.5, 0.1, 0.9],
[ 1.5, 0.3, 0.7],
[ 1.5, 0.76, 0.3],
[ 1.5, 0.98, 0.4],
[ 2, 0.21, 0.5],
[ 2, 0.46, 0.4],
[ 2, 0.66, 0.3],
[ 2, 0.76, 0.5],
[ 2, 0.88, 0.4],
[ 2, 0.99, 0.4]])
f, axs = plt.subplots(1, 1, figsize=(2.5,3))
#-------------------------------------
axs.set_xlim(0.38,1.0)
axs.set_ylim(0.0,4.0)
colors = plt.cm.gist_ncar(np.linspace(0,1,max(Vs)+3))
for idx,Val in enumerate(Vs):
axs.plot(Xs[Xs[:,0] == Val ,1], Xs[Xs[:,0] == Val ,2],'s',label=r"$Y={}$".format(Val), ms=3, color=colors[idx])
axs.plot(Xs[Xs[:,0] == Val ,1], Xs[Xs[:,0] == Val ,2]*Val/0.3,'-', label=r"$Y={}$".format(Val), ms=3, color=colors[idx])
axs.set_ylim(0.0,4.0)
axs.set_ylabel(r"$Y$ ", labelpad=2)
axs.set_xlabel(r"$X$ ", labelpad=2)
axs.set_yticks([0,0.5,1.0,1.5,2.0, 2.5, 3.0, 3.5, 4.0])
axs.set_xticks([0,0.5,1.0])
axs.legend(fontsize=6, loc=2, numpoints = 1, labelspacing=0.2,handletextpad=0.2, frameon=False)
f.savefig("tmp.pdf")
plt.show()
Do you any suggestions to resolve this issue?
Upvotes: 1
Views: 1914
Reputation: 339112
Applying my answer to How to create two legend objects for a single plot instance? in this case:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib
Vs = np.array([0.5, 1, 1.5, 2])
Xs = np.array([[ 0.5, 0.2, 0.7], [ 0.5, 0.3, 0.9], [ 0.5, 0.5, 0.4],
[ 0.5, 0.7, 0.4],[ 0.5, 0.9, 0.7], [ 1, 0.15, 0.9],
[ 1, 0.35, 0.6], [ 1, 0.45, 0.6], [ 1, 0.67, 0.5],
[ 1, 0.85, 0.9], [ 1.5, 0.1, 0.9], [ 1.5, 0.3, 0.7],
[ 1.5, 0.76, 0.3], [ 1.5, 0.98, 0.4], [ 2, 0.21, 0.5],
[ 2, 0.66, 0.3], [ 2, 0.76, 0.5], [ 2, 0.88, 0.4],
[ 2, 0.99, 0.4]])
f, axs = plt.subplots(1, 1, figsize=(2.5,3))
axs.set_xlim(0.38,1.0)
axs.set_ylim(0.0,4.0)
colors = plt.cm.gist_ncar(np.linspace(0,1,max(Vs)+3))
for idx,Val in enumerate(Vs):
axs.plot(Xs[Xs[:,0] == Val ,1], Xs[Xs[:,0] == Val ,2],'s',label=r"$Y={}$".format(Val), ms=3, color=colors[idx])
axs.plot(Xs[Xs[:,0] == Val ,1], Xs[Xs[:,0] == Val ,2]*Val/0.3,'-', label=r"$Y={}$".format(Val), ms=3, color=colors[idx])
axs.set_ylim(0.0,4.0)
axs.set_ylabel(r"$Y$ ", labelpad=2)
axs.set_xlabel(r"$X$ ", labelpad=2)
axs.set_yticks([0,0.5,1.0,1.5,2.0, 2.5, 3.0, 3.5, 4.0])
axs.set_xticks([0,0.5,1.0])
h, l = axs.get_legend_handles_labels()
axs.legend(handles=zip(h[::2], h[1::2]), labels=l[::2],
handler_map = {tuple: matplotlib.legend_handler.HandlerTuple(None)})
plt.show()
Upvotes: 3
Reputation: 9810
I would go with creating custom lines to be shown in your legend. You can go about it by saving the output of each plot command (a line plot returns a matplotlib.lines.Line2D
object which stores the line style, marker style, color, etc). You can then loop over the saved lines and create new Line2D
objects that combine the properties of the two lines with the same color. Saving these new Line2D
objects in a list, say handles
, you can then pass that list to the ax.legend()
call:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.lines import Line2D
Vs = np.array([0.5, 1, 1.5, 2])
Xs = np.array([[ 0.5, 0.2, 0.7],
[ 0.5, 0.3, 0.9],
[ 0.5, 0.5, 0.4],
[ 0.5, 0.7, 0.4],
[ 0.5, 0.9, 0.7],
[ 1, 0.15, 0.9],
[ 1, 0.35, 0.6],
[ 1, 0.45, 0.6],
[ 1, 0.67, 0.5],
[ 1, 0.85, 0.9],
[ 1.5, 0.1, 0.9],
[ 1.5, 0.3, 0.7],
[ 1.5, 0.76, 0.3],
[ 1.5, 0.98, 0.4],
[ 2, 0.21, 0.5],
[ 2, 0.46, 0.4],
[ 2, 0.66, 0.3],
[ 2, 0.76, 0.5],
[ 2, 0.88, 0.4],
[ 2, 0.99, 0.4]])
f, axs = plt.subplots(1, 1, figsize=(2.5,3))
#-------------------------------------
axs.set_xlim(0.38,1.0)
axs.set_ylim(0.0,4.0)
colors = plt.cm.gist_ncar(np.linspace(0,1,max(Vs)+3))
##saving the Line2D objects:
lines = []
points = []
for idx,Val in enumerate(Vs):
point, = axs.plot(
Xs[Xs[:,0] == Val ,1], Xs[Xs[:,0] == Val ,2],'s',
label=r"$Y={}$".format(Val), ms=3, color=colors[idx]
)
line, = axs.plot(
Xs[Xs[:,0] == Val ,1], Xs[Xs[:,0] == Val ,2]*Val/0.3,'-',
label=r"$Y={}$".format(Val), ms=3, color=colors[idx]
)
points.append(point)
lines.append(line)
axs.set_ylim(0.0,4.0)
axs.set_ylabel(r"$Y$ ", labelpad=2)
axs.set_xlabel(r"$X$ ", labelpad=2)
axs.set_yticks([0,0.5,1.0,1.5,2.0, 2.5, 3.0, 3.5, 4.0])
axs.set_xticks([0,0.5,1.0])
#axs.legend(fontsize=6, loc=2, numpoints = 1, labelspacing=0.2,handletextpad=0.2, frameon=False)
#f.savefig("tmp.pdf")
##generating the legend handles, with linestyle, markerstyle, color, and label
##copied from the plotted lines:
handles = [
Line2D(
[],[], marker=point.get_marker(), linestyle=line.get_linestyle(),
color = line.get_color(),
label = line.get_label(),
) for line, point in zip(lines,points)
]
##passing handles as argument to the `legend()` call:
axs.legend(
handles=handles,
fontsize=6, loc=2, numpoints = 1, labelspacing=0.2,
handletextpad=0.2, frameon=False,
)
plt.show()
The resulting picture looks like this:
EDIT:
Following the example that is linked in the question, one can design a handler object that generates the wanted legend handles. Replacing the last part of the above code with the following:
##a dedicated class that holds the lines to be included in the legend entry
class LineContainer:
def __init__(self, *args):
args = [line for line in args if isinstance(line,Line2D)]
if len(args) < 0:
raise ValueError('At least one line must be passed')
self._lines = list(args)
def get_lines(self):
return self._lines
def get_label(self):
##assuming here that all lines have the same label
return self._lines[0].get_label()
##adapted from https://stackoverflow.com/a/31530393/2454357
class data_handler(object):
def legend_artist(self, legend, orig_handle, fontsize, handlebox):
scale = fontsize / 22
x0, y0 = handlebox.xdescent, handlebox.ydescent
width, height = handlebox.width, handlebox.height
##use these two lines to control the lengths of the individual line
##segments and the spacing between them:
##width for individual artists
l = 0.7*width/len(orig_handle.get_lines())
##distance between individual artists
l0 = 0.3*width/len(orig_handle.get_lines())
result = []
for i, line in enumerate(orig_handle.get_lines()):
new_line = Line2D([],[])
new_line.update_from(line)
##if no linestyle is defined, plot only the marker:
if new_line.get_linestyle() in ['None', None]:
new_line.set_data(
[x0+l*(i+0.5)], [y0+height/2]
)
##else plot markers and lines:
else:
new_line.set_data(
[x0+l*i+l0/2, x0+l*(i+1)-l0/2],
[y0+height/2, y0+height/2]
)
new_line.set_transform(handlebox.get_transform())
handlebox.add_artist(new_line)
result.append(new_line)
return result
##generating the handles
handles = [
LineContainer(line, point) for line, point in zip(lines, points)
]
axs.legend(
handles = handles,
handler_map={LineContainer: data_handler()},
fontsize=6, loc=2, numpoints = 1, labelspacing=0.2,
handletextpad=0.2, frameon=False,
)
plt.show()
gives the following image:
Upvotes: 0