I want to add a legend for the blue vertical dashed lines and black vertical dashed lines with label long entry points and short entry points respectively. The other two lines (benchmark and manual strategy portfolio) came from the dataframe.
How do I add a legend for the two vertical line styles?
Here is my existing code and the corresponding graph. The dataframe is a two column dataframe of values that share date indices (the x) and have y values. The blue_x_coords and black_x_coords are the date indices for the vertical lines, as you would expect. Thanks in advance!
ax = df.plot(title=title, fontsize=12, color=["tab:purple", "tab:red"])
ax.set_xlabel(xlabel)
ax.set_ylabel(ylabel)
for xc in blue_x_coords:
plt.axvline(x=xc, color="blue", linestyle="dashed", label="Long Entry points")
for xc in black_x_coords:
plt.axvline(x=xc, color="black", linestyle="dashed", label="Short Entry points")
plt.savefig("./images/" + filename)
plt.clf()
You can do this by simply specifying the legend yourself instead of relying on pandas to do it for you.
Each call to ax.axvline will add another entry to your legend, so the only trick we'll need to do is deduplicate legend entries who share the same label. From there we simply call ax.legend with the corresponding handles and labels.
from matplotlib.pyplot import subplots, show
from pandas import DataFrame, date_range, to_datetime
from numpy.random import default_rng
from matplotlib.dates import DateFormatter
rng = default_rng(0)
df = DataFrame({
'Benchmark': rng.normal(0, .1, size=200),
'Manual Strategy Portfolio': rng.uniform(-.1, .1, size=200).cumsum(),
}, index=date_range('2007-12', freq='7D', periods=200))
ax = df.plot(color=['tab:purple', 'tab:red'])
blue_x_coords = to_datetime(['2008-07', '2009-11', '2010-10-12'])
black_x_coords = to_datetime(['2008-02-15', '2009-01-15', '2011-09-23'])
for xc in blue_x_coords:
blue_vline = ax.axvline(x=xc, color="blue", linestyle="dashed", label="Long Entry points")
for xc in black_x_coords:
black_vline = ax.axvline(x=xc, color="black", linestyle="dashed", label="Short Entry points")
# De-duplicate all legend entries based on their label
legend_entries = {label: artist for artist, label in zip(*ax.get_legend_handles_labels())}
# Restructure data to pass into ax.legend
labels, handles = zip(*legend_entries.items())
ax.legend(labels=labels, handles=handles, loc='center left', bbox_to_anchor=(1.02, .5))
You can just do plt.legend() before plt.show() but here you need to use vlines() here ymin and ymax are required
ax=df.plot(color=["green","red"])
ax.set_title("Test")
ax.set_xlabel("X")
ax.set_ylabel("Y")
ax.vlines(range(0,100,25),label="Long Entry points",linestyle="--",ymin=0,ymax=100,color="blue")
# you can pass blue_x_coords instead of range
ax.vlines(range(0,100,15),label="Short Entry points",linestyle="--",ymin=0,ymax=100,color="black")
# you can pass black_x_coords instead of range
plt.legend()
plt.show()
Output:
If using axvline then you can follow this approach:
You can add new legend with Axes.add_artist() to add new legend in the existing plot.
plt.legend() will work here as you have added label in axvline() but there's a catch as it's added via loop then that many label are added.
Removed label from plt.axvline as it is being added multiple time and thus there will be that many different label in legend.
While adding new legend you need to pass loc also or else it will be at default place only.
It will be added as another legend and not merged in same legend (I don't know method to add in same legend if someone knows please show)
ax=df.plot(color=["green","red"])
ax.set_title("Test")
ax.set_xlabel("X")
ax.set_ylabel("Y")
for xc in range(0,100,25):
line1=plt.axvline(x=xc, color="blue", linestyle="dashed")
for xc in range(0,100,15):
line2=plt.axvline(x=xc, color="black", linestyle="dashed")
new_legend=plt.legend([line1,line2],["Long Entry points","Short Entry points"],loc="lower right")
ax.add_artist(new_legend)
plt.legend()
plt.show()
Output:
Answer: Seems like the easiest way is to replace the for loops:
ax.vlines(x=blue_x_coords, colors="blue", ymin=bottom, ymax=top, linestyles="--", label="Long Entry Points")
ax.vlines(x=black_x_coords, colors="black", ymin=bottom, ymax=top, linestyles="--", label="Short Entry Points")
ax.legend()
Related
I am plotting the same type of information, but for different countries, with multiple subplots with Matplotlib. That is, I have nine plots on a 3x3 grid, all with the same for lines (of course, different values per line).
However, I have not figured out how to put a single legend (since all nine subplots have the same lines) on the figure just once.
How do I do that?
There is also a nice function get_legend_handles_labels() you can call on the last axis (if you iterate over them) that would collect everything you need from label= arguments:
handles, labels = ax.get_legend_handles_labels()
fig.legend(handles, labels, loc='upper center')
figlegend may be what you're looking for: matplotlib.pyplot.figlegend
An example is at Figure legend demo.
Another example:
plt.figlegend(lines, labels, loc = 'lower center', ncol=5, labelspacing=0.)
Or:
fig.legend(lines, labels, loc = (0.5, 0), ncol=5)
TL;DR
lines_labels = [ax.get_legend_handles_labels() for ax in fig.axes]
lines, labels = [sum(lol, []) for lol in zip(*lines_labels)]
fig.legend(lines, labels)
I have noticed that none of the other answers displays an image with a single legend referencing many curves in different subplots, so I have to show you one... to make you curious...
Now, if I've teased you enough, here it is the code
from numpy import linspace
import matplotlib.pyplot as plt
# each Axes has a brand new prop_cycle, so to have differently
# colored curves in different Axes, we need our own prop_cycle
# Note: we CALL the axes.prop_cycle to get an itertoools.cycle
color_cycle = plt.rcParams['axes.prop_cycle']()
# I need some curves to plot
x = linspace(0, 1, 51)
functs = [x*(1-x), x**2*(1-x),
0.25-x*(1-x), 0.25-x**2*(1-x)]
labels = ['$x-x²$', '$x²-x³$',
'$\\frac{1}{4} - (x-x²)$', '$\\frac{1}{4} - (x²-x³)$']
# the plot,
fig, (a1,a2) = plt.subplots(2)
for ax, f, l, cc in zip((a1,a1,a2,a2), functs, labels, color_cycle):
ax.plot(x, f, label=l, **cc)
ax.set_aspect(2) # superfluos, but nice
# So far, nothing special except the managed prop_cycle. Now the trick:
lines_labels = [ax.get_legend_handles_labels() for ax in fig.axes]
lines, labels = [sum(lol, []) for lol in zip(*lines_labels)]
# Finally, the legend (that maybe you'll customize differently)
fig.legend(lines, labels, loc='upper center', ncol=4)
plt.show()
If you want to stick with the official Matplotlib API, this is
perfect, otherwise see note no.1 below (there is a private
method...)
The two lines
lines_labels = [ax.get_legend_handles_labels() for ax in fig.axes]
lines, labels = [sum(lol, []) for lol in zip(*lines_labels)]
deserve an explanation, see note 2 below.
I tried the method proposed by the most up-voted and accepted answer,
# fig.legend(lines, labels, loc='upper center', ncol=4)
fig.legend(*a2.get_legend_handles_labels(),
loc='upper center', ncol=4)
and this is what I've got
Note 1
If you don't mind using a private method of the matplotlib.legend module ... it's really much much much easier
from matplotlib.legend import _get_legend_handles_labels
...
fig.legend(*_get_legend_handles_and_labels(fig.axes), ...)
Note 2
I have encapsulated the two tricky lines in a function, just four lines of code, but heavily commented
def fig_legend(fig, **kwdargs):
# Generate a sequence of tuples, each contains
# - a list of handles (lohand) and
# - a list of labels (lolbl)
tuples_lohand_lolbl = (ax.get_legend_handles_labels() for ax in fig.axes)
# E.g., a figure with two axes, ax0 with two curves, ax1 with one curve
# yields: ([ax0h0, ax0h1], [ax0l0, ax0l1]) and ([ax1h0], [ax1l0])
# The legend needs a list of handles and a list of labels,
# so our first step is to transpose our data,
# generating two tuples of lists of homogeneous stuff(tolohs), i.e.,
# we yield ([ax0h0, ax0h1], [ax1h0]) and ([ax0l0, ax0l1], [ax1l0])
tolohs = zip(*tuples_lohand_lolbl)
# Finally, we need to concatenate the individual lists in the two
# lists of lists: [ax0h0, ax0h1, ax1h0] and [ax0l0, ax0l1, ax1l0]
# a possible solution is to sum the sublists - we use unpacking
handles, labels = (sum(list_of_lists, []) for list_of_lists in tolohs)
# Call fig.legend with the keyword arguments, return the legend object
return fig.legend(handles, labels, **kwdargs)
I recognize that sum(list_of_lists, []) is a really inefficient method to flatten a list of lists, but ① I love its compactness, ② usually is a few curves in a few subplots and ③ Matplotlib and efficiency? ;-)
For the automatic positioning of a single legend in a figure with many axes, like those obtained with subplots(), the following solution works really well:
plt.legend(lines, labels, loc = 'lower center', bbox_to_anchor = (0, -0.1, 1, 1),
bbox_transform = plt.gcf().transFigure)
With bbox_to_anchor and bbox_transform=plt.gcf().transFigure, you are defining a new bounding box of the size of your figureto be a reference for loc. Using (0, -0.1, 1, 1) moves this bounding box slightly downwards to prevent the legend to be placed over other artists.
OBS: Use this solution after you use fig.set_size_inches() and before you use fig.tight_layout()
You just have to ask for the legend once, outside of your loop.
For example, in this case I have 4 subplots, with the same lines, and a single legend.
from matplotlib.pyplot import *
ficheiros = ['120318.nc', '120319.nc', '120320.nc', '120321.nc']
fig = figure()
fig.suptitle('concentration profile analysis')
for a in range(len(ficheiros)):
# dados is here defined
level = dados.variables['level'][:]
ax = fig.add_subplot(2,2,a+1)
xticks(range(8), ['0h','3h','6h','9h','12h','15h','18h','21h'])
ax.set_xlabel('time (hours)')
ax.set_ylabel('CONC ($\mu g. m^{-3}$)')
for index in range(len(level)):
conc = dados.variables['CONC'][4:12,index] * 1e9
ax.plot(conc,label=str(level[index])+'m')
dados.close()
ax.legend(bbox_to_anchor=(1.05, 0), loc='lower left', borderaxespad=0.)
# it will place the legend on the outer right-hand side of the last axes
show()
If you are using subplots with bar charts, with a different colour for each bar, it may be faster to create the artefacts yourself using mpatches.
Say you have four bars with different colours as r, m, c, and k, you can set the legend as follows:
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
labels = ['Red Bar', 'Magenta Bar', 'Cyan Bar', 'Black Bar']
#####################################
# Insert code for the subplots here #
#####################################
# Now, create an artist for each color
red_patch = mpatches.Patch(facecolor='r', edgecolor='#000000') # This will create a red bar with black borders, you can leave out edgecolor if you do not want the borders
black_patch = mpatches.Patch(facecolor='k', edgecolor='#000000')
magenta_patch = mpatches.Patch(facecolor='m', edgecolor='#000000')
cyan_patch = mpatches.Patch(facecolor='c', edgecolor='#000000')
fig.legend(handles = [red_patch, magenta_patch, cyan_patch, black_patch], labels=labels,
loc="center right",
borderaxespad=0.1)
plt.subplots_adjust(right=0.85) # Adjust the subplot to the right for the legend
To build on top of gboffi's and Ben Usman's answer:
In a situation where one has different lines in different subplots with the same color and label, one can do something along the lines of:
labels_handles = {
label: handle for ax in fig.axes for handle, label in zip(*ax.get_legend_handles_labels())
}
fig.legend(
labels_handles.values(),
labels_handles.keys(),
loc = "upper center",
bbox_to_anchor = (0.5, 0),
bbox_transform = plt.gcf().transFigure,
)
Using Matplotlib 2.2.2, this can be achieved using the gridspec feature.
In the example below, the aim is to have four subplots arranged in a 2x2 fashion with the legend shown at the bottom. A 'faux' axis is created at the bottom to place the legend in a fixed spot. The 'faux' axis is then turned off so only the legend shows. Result:
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
# Gridspec demo
fig = plt.figure()
fig.set_size_inches(8, 9)
fig.set_dpi(100)
rows = 17 # The larger the number here, the smaller the spacing around the legend
start1 = 0
end1 = int((rows-1)/2)
start2 = end1
end2 = int(rows-1)
gspec = gridspec.GridSpec(ncols=4, nrows=rows)
axes = []
axes.append(fig.add_subplot(gspec[start1:end1, 0:2]))
axes.append(fig.add_subplot(gspec[start2:end2, 0:2]))
axes.append(fig.add_subplot(gspec[start1:end1, 2:4]))
axes.append(fig.add_subplot(gspec[start2:end2, 2:4]))
axes.append(fig.add_subplot(gspec[end2, 0:4]))
line, = axes[0].plot([0, 1], [0, 1], 'b') # Add some data
axes[-1].legend((line,), ('Test',), loc='center') # Create legend on bottommost axis
axes[-1].set_axis_off() # Don't show the bottom-most axis
fig.tight_layout()
plt.show()
This answer is a complement to user707650's answer on the legend position.
My first try on user707650's solution failed due to overlaps of the legend and the subplot's title.
In fact, the overlaps are caused by fig.tight_layout(), which changes the subplots' layout without considering the figure legend. However, fig.tight_layout() is necessary.
In order to avoid the overlaps, we can tell fig.tight_layout() to leave spaces for the figure's legend by fig.tight_layout(rect=(0,0,1,0.9)).
Description of tight_layout() parameters.
All of the previous answers are way over my head, at this state of my coding journey, so I just added another Matplotlib aspect called patches:
import matplotlib.patches as mpatches
first_leg = mpatches.Patch(color='red', label='1st plot')
second_leg = mpatches.Patch(color='blue', label='2nd plot')
thrid_leg = mpatches.Patch(color='green', label='3rd plot')
plt.legend(handles=[first_leg ,second_leg ,thrid_leg ])
The patches aspect put all the data i needed on my final plot (it was a line plot that combined three different line plots all in the same cell in Jupyter Notebook).
Result
(I changed the names form what I named my own legend.)
I want to plot 2 different graphs in one plot. One graph is just one line, so no problem with labeling the legend. In df_2_plot is a list of tickers that is delivered, so more lines and more tickers within legend. If I label them like this, I only receive the list several times in the legend, instead of the right ticker for each line.
I tried to work with for loops but can't find a solution.
def func_plot_DataFrame(df_2_plot, legend_lst):
y1 = df_2_plot
y2 = df_infektionsgeschehen
fig, ax1 = plt.subplots()
ax2 = ax1.twinx()
ax1.plot(y1, label = legend_lst)
ax2.plot(y2, 'grey', linewidth=2, alpha=0.3, label = 'Neuinfektionen')
plt.show()
You need to use ax1.legend instead of using the label argument directly in ax1.plot
ax1.legend(labels=legend_lst)
Have a look at the official documentation here
I have the following code to print out columns from a pandas dataframe as two histograms:
df = pd.read_csv('fairview_Procedure_combined.csv')
ax = df.hist(column=['precision', 'recall'], bins=25, grid=False, figsize=(12,8), color='#86bf91', zorder=2, rwidth=0.9)
ax = ax[0]
for x in ax:
# Despine
x.spines['right'].set_visible(False)
x.spines['top'].set_visible(False)
x.spines['left'].set_visible(False)
# Switch off ticks
x.tick_params(axis="both", which="both", bottom="off", top="off", labelbottom="on", left="off", right="off", labelleft="on")
# Draw horizontal axis lines
vals = x.get_yticks()
for tick in vals:
x.axhline(y=tick, linestyle='dashed', alpha=0.4, color='#eeeeee', zorder=1)
# Remove title
x.set_title("")
# Set x-axis label
x.set_xlabel("test", labelpad=20, weight='bold', size=12)
# Set y-axis label
x.set_ylabel("count", labelpad=20, weight='bold', size=12)
# Format y-axis label
x.yaxis.set_major_formatter(StrMethodFormatter('{x:,g}'))
which gives the attached output:
I would like however to have different labels on the x-axis (in particular, those listed in my column list, that is, precision and recall)
Also, I have a grouping column (semantic_type) I would like to use to generate a bunch of paired graphs, but when I pass the by keyword in my hist method to group the histograms by semantic_type, I get an error of color kwarg must have one color per data set. 18 data sets and 1 colors were provided)
I figured it out using subplots... piece of cake.
I would like to create a single legend entry that shows two markers, one of them open, the other filled. I have tried to do this with both plt.scatter and plt.plot but I'm running in to trouble with either:
With plt.scatter: there appears to be a slight vertical offset between the points in the legend when using scatterpoints > 2. Is it possible to have them on the same horizontal?
With plt.plot: Is possible to modify the legend markers individually to have one open and one filled?
# plot random data to host the legend:
# scatter option
plt.scatter(-1, -1,
facecolor='k', edgecolor='k',
label='scatter')
# plot option
plt.plot(-1, -1, lw=0,
marker='o', markerfacecolor='k', markeredgecolor='k', label='plot')
# add legend
leg = plt.legend(scatterpoints=2, numpoints=2)
# modify scatter points -- have an unwanted vertical offset?
leg.legendHandles[1].set_facecolors(['None', 'k'])
leg.legendHandles[1].set_edgecolors(['k', 'k'])
# modify plot points -- cannot change individual markers?
leg.legendHandles[0].set_markeredgecolor(['None', 'k'])
leg.legendHandles[0].set_markeredgecolor(['k', 'k'])
You can use a HandlerTuple:
import matplotlib.pyplot as plt
from matplotlib.legend_handler import HandlerTuple
l1, = plt.plot(-1, -1, lw=0, marker="o",
markerfacecolor='k', markeredgecolor='k')
l2, = plt.plot(-0.5, -1, lw=0, marker="o",
markerfacecolor="none", markeredgecolor='k')
plt.legend([(l1, l1), (l1, l2)], ["test 1", "test 2"], handler_map={tuple: HandlerTuple(2)})
plt.show()
It is possible to align the scatterpoints horizontally by setting the scatteryoffsets parameter of plt.legend:
leg = plt.legend(scatterpoints=2, scatteryoffsets=[0.5])
From the Documentation:
scatteryoffsets : iterable of floats
The vertical offset (relative to the font size) for the markers created for a scatter plot legend entry. 0.0 is at the base the legend text, and 1.0 is at the top. To draw all markers at the same height, set to [0.5]. Default is [0.375, 0.5, 0.3125].
I would like to identify two different symbols (with different colors) on the same line in a legend. Below, I tried doing this with Proxy Artists, but the result is that they get stacked on top of each other in the legend. I want them next to each other or one above the other-- so they are both visible.
from pylab import *
import matplotlib.lines as mlines
#define two colors, one for 'r' data, one for 'a' data
rcolor=[69./255 , 115./255, 50.8/255 ]
acolor=[202./255, 115./255, 50.8/255 ]
#Plot theory:
ax2.plot(rho, g_r, '-',color=rcolor,lw=2)
ax2.plot(rho, g_a, '-',color=acolor,lw=2)
#Plot experiment:
ax2.scatter(X_r, Y_r,s=200, marker='s', facecolors='none', edgecolors=rcolor);
ax2.scatter(X_a, Y_a,s=200, marker='^', facecolors='none', edgecolors=acolor);
#Create Proxy Artists for legend
expt_r = mlines.Line2D([], [], fillstyle='none', color=rcolor, marker='s', linestyle='', markersize=15)
expt_a = mlines.Line2D([], [], fillstyle='none', color=acolor, marker='^', linestyle='', markersize=15)
thry_r = mlines.Line2D([], [], fillstyle='none', color=rcolor, marker='', markersize=15)
thry_a = mlines.Line2D([], [], fillstyle='none', color=acolor, marker='', markersize=15)
#Add legend
ax2.legend(((expt_r,expt_a),(thry_r,thry_a)), ('Experiment','Theory'))
I think my problem is almost exactly like this one: (Matplotlib, legend with multiple different markers with one label), but it seems like the problem is unsolved since the answer there just plots one patch on top of the other, which is exactly what happens for me too. I feel like maybe I need to make a composite patch somehow, but I had trouble finding how to do this. Thanks!
Also, I haven't found how to make the legend symbols look the same (line thickness, size) as the scatter symbols. Thanks again.
The problem of overlapping patches (aka artists) lies in how you have defined the handles and labels when creating the legend. To quote the matplotlib legend guide:
the default handler_map has a special tuple handler (legend_handler.HandlerTuple) which simply plots the handles on top of one another for each item in the given tuple
Let's first examine the structure of the legend you have given as an example. Any iterable object can be used for handles and labels, so I choose to store them as lists, in line with some examples given in the legend guide and to make the code clearer:
ax2.legend([(expt_r, expt_a), (thry_r, thry_a)], ['Experiment', 'Theory'])
handles_list = [(expt_r, expt_a), (thry_r, thry_a)]
handles1 = (expt_r, expt_a) # tuple of 2 handles (aka legend keys) representing the markers
handles2 = (thry_r, thry_a) # same, these represent the lines
labels_list = ['Experiment', 'Theory']
label1 = 'Experiment'
label2 = 'Theory'
Whatever the number of handles contained in handles1 or in handles2, they will all be drawn on top of one another by the corresponding label1 and label2, seeing as they are contained in a single tuple. To solve this issue and have the keys/symbols drawn separately, you must take them out of the tuples like this:
handles_list = [expt_r, expt_a, thry_r, thry_a]
But now you face the issue that only the expt_r, expt_a handles will be drawn because the labels list contains only two labels. Yet the goal here is to avoid needlessly repeating these labels. Here is an example of how to solve this issue. It is built on the code sample you have provided and makes use of the legend parameters:
import numpy as np # v 1.19.2
import matplotlib.pyplot as plt # v 3.3.2
# Set data parameters
rng = np.random.default_rng(seed=1)
data_points = 10
error_scale = 0.2
# Create variables
rho = np.arange(0, data_points)
g_r = rho**2
g_r_error = rng.uniform(-error_scale, error_scale, size=g_r.size)*g_r
g_a = rho**2 + 50
g_a_error = rng.uniform(-error_scale, error_scale, size=g_a.size)*g_a
X_r = rho
Y_r = g_r + g_r_error
X_a = rho
Y_a = g_a + g_a_error
# Define two colors, one for 'r' data, one for 'a' data
rcolor = [69./255 , 115./255, 50.8/255 ]
acolor = [202./255, 115./255, 50.8/255 ]
# Create figure with single axes
fig, ax = plt.subplots(figsize=(9,5))
# Plot theory: notice the comma after each variable to unpack the list
# containing one Line2D object returned by the plotting function
# (because it is possible to plot several lines in one function call)
thry_r, = ax.plot(rho, g_r, '-', color=rcolor, lw=2)
thry_a, = ax.plot(rho, g_a, '-', color=acolor, lw=2)
# Plot experiment: no need for a comma as the PathCollection object
# returned by the plotting function is not contained in a list
expt_r = ax.scatter(X_r, Y_r, s=100, marker='s', facecolors='none', edgecolors=rcolor)
expt_a = ax.scatter(X_a, Y_a, s=100, marker='^', facecolors='none', edgecolors=acolor)
# Create custom legend: input handles and labels in desired order and
# set ncol=2 to line up the legend keys of the same type.
# Note that seeing as the labels are added here with explicitly defined
# handles, it is not necessary to define the labels in the plotting functions.
ax.legend(handles=[thry_r, expt_r, thry_a, expt_a],
labels=['', '', 'Theory','Experiment'],
loc='upper left', ncol=2, handlelength=3, edgecolor='black',
borderpad=0.7, handletextpad=1.5, columnspacing=0)
plt.show()
The problem is solved but the code can be simplified to automate the legend creation. It is possible to avoid storing the output of each plotting function as a new variable by making use of the get_legend_handles_labels function. Here is an example built on the same data. Note that a third type of plot (error band) is added to make the processing of handles and labels more clear:
# Define parameters used to process handles and labels
nb_plot_types = 3 # theory, experiment, error band
nb_experiments = 2 # r and a
# Create figure with single axes
fig, ax = plt.subplots(figsize=(9,5))
# Note that contrary to the previous example, here it is necessary to
# define a label in the plotting functions seeing as the returned
# handles/artists are this time not stored as variables. No labels means
# no handles in the handles list returned by the
# ax.get_legend_handles_labels() function.
# Plot theory
ax.plot(rho, g_r, '-', color=rcolor, lw=2, label='Theory')
ax.plot(rho, g_a, '-', color=acolor, lw=2, label='Theory')
# Plot experiment
ax.scatter(X_r, Y_r, s=100, marker='s', facecolors='none',
edgecolors=rcolor, label='Experiment')
ax.scatter(X_a, Y_a, s=100, marker='^', facecolors='none',
edgecolors=acolor, label='Experiment')
# Plot error band
g_r_lower = g_r - error_scale*g_r
g_r_upper = g_r + error_scale*g_r
ax.fill_between(X_r, g_r_lower, g_r_upper,
color=rcolor, alpha=0.2, label='Uncertainty')
g_a_lower = g_a - error_scale*g_a
g_a_upper = g_a + error_scale*g_a
ax.fill_between(X_a, g_a_lower, g_a_upper,
color=acolor, alpha=0.2, label='Uncertainty')
# Extract handles and labels and reorder/process them for the custom legend,
# based on the number of types of plots and the number of experiments.
# The handles list returned by ax.get_legend_handles_labels() appears to be
# always ordered the same way with lines placed first, followed by collection
# objects in alphabetical order, regardless of the order of the plotting
# functions calls. So if you want to display the legend keys in a different
# order (e.g. put lines on the bottom line) you will have to process the
# handles list in another way.
handles, labels = ax.get_legend_handles_labels()
handles_ordered_arr = np.array(handles).reshape(nb_plot_types, nb_experiments).T
handles_ordered = handles_ordered_arr.flatten()
# Note the use of asterisks to unpack the lists of strings
labels_trimmed = *nb_plot_types*[''], *labels[::nb_experiments]
# Create custom legend with the same format as in the previous example
ax.legend(handles_ordered, labels_trimmed,
loc='upper left', ncol=nb_experiments, handlelength=3, edgecolor='black',
borderpad=0.7, handletextpad=1.5, columnspacing=0)
plt.show()
Additional documentation: legend class, artist class
I do not answer your main question, sorry.
However, regarding your last point
how to make the legend symbols look the same (line thickness, size) as the scatter symbols
you can use the keyword markerscale of the legend command. So for equal size
ax2.legend( ...<handles_and_labels>... , markerscale=1)
or a change of legend.markerscale in the rcParams should do.