Matplotlib legend in separate figure with PolyCollection object - python

I need to create the legend as a separate figure, and more importantly separate instance that can be saved in a new file. My plot consists of lines and a filled in segment.
The problem is the fill_between element, I can not add it to the external figure/legend.
I realise, this is a different type of object, it is a PolyCollection, while to line-plots are Line2D elements.
How do I handle the PolyCollection so that I can use it in the external legend?
INFO: matplotlib version 3.3.2
import matplotlib.pyplot as plt
import numpy as np
# Dummy data
x = np.linspace(1, 100, 1000)
y = np.log(x)
y1 = np.sin(x)
# Create regular plot and plot everything
fig = plt.figure('Line plot')
ax = fig.add_subplot(111)
line1, = ax.plot(x, y)
line2, = ax.plot(x, y1)
fill = ax.fill_between(x, y, y1)
ax.legend([line1, line2, fill],['Log','Sin','Area'])
ax.plot()
# create new plot only for legend
legendFig = plt.figure('Legend plot')
legendFig.legend([line1, line2],['Log','Sin']) <----- This works
# legendFig.legend([line1, line2, fill],['Log','Sin', 'Area']) <----- This does not work

You forgot to mention what does not work means here.
Apparently, you get an error message: RuntimeError: Can not put single artist in more than one figure.
Matplotlib doesn't allow elements placed in one figure to be reused in another. It is just a lucky coincidence that the line don't give an error.
To use an element in another figure, you can create a new element, and that copy the style from the original element:
from matplotlib.lines import Line2D
from matplotlib.collections import PolyCollection
legendFig = plt.figure('Legend plot')
handle_line1 = Line2D([], [])
handle_line1.update_from(line1)
handle_line2 = Line2D([], [])
handle_line2.update_from(line2)
handle_fill = PolyCollection([])
handle_fill.update_from(fill)
legendFig.legend([handle_line1, handle_line2, handle_fill], ['Log', 'Sin', 'Area'])

Related

Add item to existing Matplotlib legend

Given the following setup:
from matplotlib import pyplot as plt
fig, ax = plt.subplots()
ax.plot([0,1,2,3,4,5,6], label='linear')
ax.plot([0,1,4,9,16,25,36], label='square')
lgd = ax.legend(loc='lower right')
If a function add_patch receives only lgd as an argument, can a custom legend item be added to the legend on top of the existing items, without changing the other properties of the legend?
I was able to add an item using:
def add_patch(legend):
from matplotlib.patches import Patch
ax = legend.axes
handles, labels = ax.get_legend_handles_labels()
handles.append(Patch(facecolor='orange', edgecolor='r'))
labels.append("Color Patch")
ax.legend(handles=handles, labels=labels)
But this does not preserve the properties of the legend like location. How can I add an item given only the legend object after lines have been plotted?
In principle a legend is not meant to be updated, but recreated instead.
The following would do what you're after, but beware that this is a hack which uses internal methods and is hence not guaranteed to work and might break in future releases. So don't use it in production code. Also, if you have set a title to the legend with a different font(size) than default, it will be lost upon updating. Also, if you have manipulated the order of handles and labels via markerfirst, this will be lost upon updating.
from matplotlib import pyplot as plt
fig, ax = plt.subplots()
ax.plot([0,1,2,3,4,5,6], label='linear')
ax.plot([0,1,4,9,16,25,36], label='square')
lgd = ax.legend(loc='lower right')
def add_patch(legend):
from matplotlib.patches import Patch
ax = legend.axes
handles, labels = ax.get_legend_handles_labels()
handles.append(Patch(facecolor='orange', edgecolor='r'))
labels.append("Color Patch")
legend._legend_box = None
legend._init_legend_box(handles, labels)
legend._set_loc(legend._loc)
legend.set_title(legend.get_title().get_text())
add_patch(lgd)
plt.show()
Is adding the color patch after the lines have been plotted but before adding the legend an option?
import matplotlib.pyplot as plt
from matplotlib.patches import Patch
fig, ax = plt.subplots()
line1 = ax.plot([0,1,2,3,4,5,6], label='linear')
line2 = ax.plot([0,1,4,9,16,25,36], label='square')
patch = Patch(facecolor='orange', edgecolor='r', label='Color patch')
lgd = ax.legend(handles=[line1, line2, patch], loc='lower right')

How to add a legend entry without a symbol/color into an existing legend?

I have a plot with a legend. I would like to add an entry into the legend box. This entry could be something like a fit parameter or something else descriptive of the data.
As an example, one can use the code below
import numpy as np
import matplotlib.pyplot as plt
x = np.linspace(0, 2*np.pi, 101)
y = np.sin(x)
plt.plot(x, y, color='r', label='y = sin(x)')
plt.plot(np.NaN, np.NaN, color='k', label='extra label')
plt.plot(np.NaN, np.NaN, color='b', linestyle=None, label='linestyle = None')
plt.plot(np.NaN, np.NaN, color='orange', marker=None, label='marker = None')
plt.plot(np.NaN, np.NaN, color=None, label='color = None')
plt.legend()
plt.show()
to generate the plot below
I would like instead to have a label "extra label" with only whitespace and no symbol. I tried changing the linestyle, marker, and color kwargs to None but without success. I've also tried plotting plt.plot([], []) instead of plotting plt.plot(np.NaN, np.NaN). I suppose some hacky workaround is to change color='k' to color='white'. But I'm hoping there is a more proper way to do this. How can I do this?
EDIT
My question is not a duplicate. The post that this is accused of being a duplicate of shows another way of producing a legend label, but not for one without a symbol. One can run the code below to test as the same problem from my original question applies.
import matplotlib.patches as mpatches
nan_patch = mpatches.Patch(color=None, label='The label for no data')
and modify this instance from plt.legend()
plt.legend(handles=[nan_patch])
You can add items to legend as shown in the (now removed) duplicate. Note that to have no color in the legend itself you must set color="none", e.g
empty_patch = mpatches.Patch(color='none', label='Extra label')
plt.legend(handles=[empty_patch])
In order to have this, as well as your existing legend entries, you can get a list of the existing legend handles and labels, add the extra ones to it, then plot the legend:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
x = np.linspace(0, 2*np.pi, 101)
y = np.sin(x)
plt.plot(x, y, color='r', label='y = sin(x)')
handles, labels = plt.gca().get_legend_handles_labels() # get existing handles and labels
empty_patch = mpatches.Patch(color='none', label='Extra label') # create a patch with no color
handles.append(empty_patch) # add new patches and labels to list
labels.append("Extra label")
plt.legend(handles, labels) # apply new handles and labels to plot
plt.show()
Which gives:

Moving Collections between axes

While playing with ImportanceOfBeingErnest's code to move artists between axes, I thought it would easy to extend it to collections (such as PathCollections generated by plt.scatter) as well. No such luck:
import matplotlib.pyplot as plt
import numpy as np
import pickle
x = np.linspace(-3, 3, 100)
y = np.exp(-x**2/2)/np.sqrt(2*np.pi)
a = np.random.normal(size=10000)
fig, ax = plt.subplots()
ax.scatter(x, y)
pickle.dump(fig, open("/tmp/figA.pickle", "wb"))
# plt.show()
fig, ax = plt.subplots()
ax.hist(a, bins=20, density=True, ec="k")
pickle.dump(fig, open("/tmp/figB.pickle", "wb"))
# plt.show()
plt.close("all")
# Now unpickle the figures and create a new figure
# then add artists to this new figure
figA = pickle.load(open("/tmp/figA.pickle", "rb"))
figB = pickle.load(open("/tmp/figB.pickle", "rb"))
fig, ax = plt.subplots()
for figO in [figA, figB]:
lists = [figO.axes[0].lines, figO.axes[0].patches, figO.axes[0].collections]
addfunc = [ax.add_line, ax.add_patch, ax.add_collection]
for lis, func in zip(lists, addfunc):
for artist in lis[:]:
artist.remove()
artist.axes = ax
# artist.set_transform(ax.transData)
artist.figure = fig
func(artist)
ax.relim()
ax.autoscale_view()
plt.close(figA)
plt.close(figB)
plt.show()
yields
Removing artist.set_transform(ax.transData) (at least when calling ax.add_collection) seems to help a little, but notice that the y-offset is still off:
How does one properly move collections from one axes to another?
The scatter has an IdentityTransform as master transform. The data transform is the internal offset transform. One would hence need to treat the scatter separately.
from matplotlib.collections import PathCollection
from matplotlib.transforms import IdentityTransform
# ...
if type(artist) == PathCollection:
artist.set_transform(IdentityTransform())
artist._transOffset = ax.transData
else:
artist.set_transform(ax.transData)
Unfortunately, there is no set_offset_transform method, such that one needs to replace the ._transOffset attribute to set the offset transform.

Want to change the bar chart in matplotlib using slider [duplicate]

I have bar chart, with a lot of custom properties ( label, linewidth, edgecolor)
import matplotlib.pyplot as plt
fig = plt.figure()
ax = plt.gca()
x = np.arange(5)
y = np.random.rand(5)
bars = ax.bar(x, y, color='grey', linewidth=4.0)
ax.cla()
x2 = np.arange(10)
y2 = np.random.rand(10)
ax.bar(x2,y2)
plt.show()
With 'normal' plots I'd use set_data(), but with barchart I got an error: AttributeError: 'BarContainer' object has no attribute 'set_data'
I don't want to simply update the heights of the rectangles, I want to plot totally new rectangles. If I use ax.cla(), all my settings (linewidth, edgecolor, title..) are lost too not only my data(rectangles), and to clear many times, and reset everything makes my program laggy. If I don't use ax.cla(), the settings remain, the program is faster (I don't have to set my properties all the time), but the rectangles are drawn of each other, which is not good.
Can you help me with that?
In your case, bars is only a BarContainer, which is basically a list of Rectangle patches. To just remove those while keeping all other properties of ax, you can loop over the bars container and call remove on all its entries or as ImportanceOfBeingErnest pointed out simply remove the full container:
import numpy as np
import matplotlib.pyplot as plt
fig = plt.figure()
ax = plt.gca()
x = np.arange(5)
y = np.random.rand(5)
bars = ax.bar(x, y, color='grey', linewidth=4.0)
bars.remove()
x2 = np.arange(10)
y2 = np.random.rand(10)
ax.bar(x2,y2)
plt.show()

Adding a legend to PyPlot in Matplotlib in the simplest manner possible

TL;DR -> How can one create a legend for a line graph in Matplotlib's PyPlot without creating any extra variables?
Please consider the graphing script below:
if __name__ == '__main__':
PyPlot.plot(total_lengths, sort_times_bubble, 'b-',
total_lengths, sort_times_ins, 'r-',
total_lengths, sort_times_merge_r, 'g+',
total_lengths, sort_times_merge_i, 'p-', )
PyPlot.title("Combined Statistics")
PyPlot.xlabel("Length of list (number)")
PyPlot.ylabel("Time taken (seconds)")
PyPlot.show()
As you can see, this is a very basic use of matplotlib's PyPlot. This ideally generates a graph like the one below:
Nothing special, I know. However, it is unclear what data is being plotted where (I'm trying to plot the data of some sorting algorithms, length against time taken, and I'd like to make sure people know which line is which). Thus, I need a legend, however, taking a look at the following example below(from the official site):
ax = subplot(1,1,1)
p1, = ax.plot([1,2,3], label="line 1")
p2, = ax.plot([3,2,1], label="line 2")
p3, = ax.plot([2,3,1], label="line 3")
handles, labels = ax.get_legend_handles_labels()
# reverse the order
ax.legend(handles[::-1], labels[::-1])
# or sort them by labels
import operator
hl = sorted(zip(handles, labels),
key=operator.itemgetter(1))
handles2, labels2 = zip(*hl)
ax.legend(handles2, labels2)
You will see that I need to create an extra variable ax. How can I add a legend to my graph without having to create this extra variable and retaining the simplicity of my current script?
Add a label= to each of your plot() calls, and then call legend(loc='upper left').
Consider this sample (tested with Python 3.8.0):
import numpy as np
import matplotlib.pyplot as plt
x = np.linspace(0, 20, 1000)
y1 = np.sin(x)
y2 = np.cos(x)
plt.plot(x, y1, "-b", label="sine")
plt.plot(x, y2, "-r", label="cosine")
plt.legend(loc="upper left")
plt.ylim(-1.5, 2.0)
plt.show()
Slightly modified from this tutorial: http://jakevdp.github.io/mpl_tutorial/tutorial_pages/tut1.html
You can access the Axes instance (ax) with plt.gca(). In this case, you can use
plt.gca().legend()
You can do this either by using the label= keyword in each of your plt.plot() calls or by assigning your labels as a tuple or list within legend, as in this working example:
import numpy as np
import matplotlib.pyplot as plt
x = np.linspace(-0.75,1,100)
y0 = np.exp(2 + 3*x - 7*x**3)
y1 = 7-4*np.sin(4*x)
plt.plot(x,y0,x,y1)
plt.gca().legend(('y0','y1'))
plt.show()
However, if you need to access the Axes instance more that once, I do recommend saving it to the variable ax with
ax = plt.gca()
and then calling ax instead of plt.gca().
Here's an example to help you out ...
fig = plt.figure(figsize=(10,5))
ax = fig.add_subplot(111)
ax.set_title('ADR vs Rating (CS:GO)')
ax.scatter(x=data[:,0],y=data[:,1],label='Data')
plt.plot(data[:,0], m*data[:,0] + b,color='red',label='Our Fitting
Line')
ax.set_xlabel('ADR')
ax.set_ylabel('Rating')
ax.legend(loc='best')
plt.show()
You can add a custom legend documentation
first = [1, 2, 4, 5, 4]
second = [3, 4, 2, 2, 3]
plt.plot(first, 'g--', second, 'r--')
plt.legend(['First List', 'Second List'], loc='upper left')
plt.show()
A simple plot for sine and cosine curves with a legend.
Used matplotlib.pyplot
import math
import matplotlib.pyplot as plt
x=[]
for i in range(-314,314):
x.append(i/100)
ysin=[math.sin(i) for i in x]
ycos=[math.cos(i) for i in x]
plt.plot(x,ysin,label='sin(x)') #specify label for the corresponding curve
plt.plot(x,ycos,label='cos(x)')
plt.xticks([-3.14,-1.57,0,1.57,3.14],['-$\pi$','-$\pi$/2',0,'$\pi$/2','$\pi$'])
plt.legend()
plt.show()
Add labels to each argument in your plot call corresponding to the series it is graphing, i.e. label = "series 1"
Then simply add Pyplot.legend() to the bottom of your script and the legend will display these labels.

Categories

Resources