Set the maximum number of rows in legend - python

I need to draw several datasets within a single plot. The number of datasets varies, so I don't know a priori how many there will be.
If I just draw the legends, I get this (MCVE below):
How can I tell plt.legend() to only draw say the first 10 legends? I've looked around the plt.legends() class but there seems to be no argument to set such a value.
MCVE:
import numpy as np
import matplotlib.pyplot as plt
dataset = []
for _ in range(20):
dataset.append(np.random.uniform(0, 1, 2))
lbl = ['adfg', 'dfgb', 'cgfg', 'rtbd', 'etryt', 'frty', 'jklg', 'jklh',
'ijkl', 'dfgj', 'kbnm', 'bnmbl', 'qweqw', 'fghfn', 'dfg', 'hjt', 'dfb',
'sdgdas', 'werwe', 'dghfg']
for i, xy in enumerate(dataset):
plt.scatter(xy[0], xy[1], label=lbl[i])
plt.legend()
plt.savefig('test.png')

You can just limit the number of labels shown.
import matplotlib.pyplot as plt
maxn = 16
for i in range(25):
plt.scatter(.5, .5, label=(i//maxn)*"_"+str(i))
plt.legend()
plt.show()
This method works also for text labels of course:
import numpy as np
import matplotlib.pyplot as plt
labels = ["".join(np.random.choice(list("ABCDEFGHIJK"), size=8)) for k in range(25)]
maxn = 16
for i,l in enumerate(labels):
plt.scatter(.5, .5, label=(i//maxn)*"_"+l)
plt.legend()
plt.show()
The reason this works is that labels starting with "_" are ignored in the legend. This is used internally to give objects a label without showing them in the legend but can of course also be used by us to limit the number of elements in the legend.

I would like to suggest an alternative way to get your desired output, which I feel relies less on a "hack" of the legend labels.
You can use the function Axes.get_legend_handles_labels() to get a list of the handles and the labels of the objects that are to be put in the legend.
You can truncate these lists however you feel like, before passing them to plt.legend(). For instance:
import numpy as np
import matplotlib.pyplot as plt
dataset = []
for _ in range(20):
dataset.append(np.random.uniform(0, 1, 2))
lbl = ['adfg', 'dfgb', 'cgfg', 'rtbd', 'etryt', 'frty', 'jklg', 'jklh',
'ijkl', 'dfgj', 'kbnm', 'bnmbl', 'qweqw', 'fghfn', 'dfg', 'hjt', 'dfb',
'sdgdas', 'werwe', 'dghfg']
fig, ax = plt.subplots()
for i, xy in enumerate(dataset):
ax.scatter(xy[0], xy[1], label=lbl[i])
h,l = ax.get_legend_handles_labels()
plt.legend(h[:3], l[:3]) # <<<<<<<< This is where the magic happens
plt.show()
You could even display every other label plt.legend(h[::2], l[::2]) or whatever else you want.

Related

How to align text with ylabel in matplotlib?

I want to add a text "a)", "b)" and "c)" in subfigures and align it with the yaxis label.
Lets say we have a simple plot, with variable y-axis tick labels "0.8", "0.08", "0.016". This would increase the distance between ax[i].transAxes=0 and ylabel.
import matplotlib.pyplot as plt
import numpy as np
x=np.arange(0,1,0.01)
y=np.sin(x)
fig,ax=plt.subplots(1,3,figsize=(6,4))
ax[0].plot(x,y)
ax[1].plot(x,y*0.1)
ax[2].plot(x,y*0.02)
for i in range(3):
ax[i].spines['top'].set_visible(False)
ax[i].spines['right'].set_visible(False)
ax[i].set_ylabel('Sine')
ax[i].set_xlabel('x')
plt.tight_layout()
abc='abc'
for i in range(3):
ax[i].text(0,1,abc[i]+')',transform=ax[i].transAxes)
plt.show()
Currently I am trying to find the (x,y) position by trial and error to right align "a)", "b)" or "c)" with the ylabel. Is there a better way to do this?
If I try to get the position of ylabel using,
ylbl=ax[i].set_ylabel('Sine')
print(ylbl.get_position())
I get (0,0.5), which is not really helpful.
More bizzare is when I do a tight_layout.
ylbls=[]
for i in range(3):
ylbls.append(ax[i].set_ylable('Sine'))
plt.tight_layout()
for i in range(3):
print(ylbls[i].get_position()
I get values (37.7,0.5), (201.8,0.5), (365.9,0.5). I have no idea what these 37.7, 201.8, 365.9 imply and if I can use them to align ylabel with my text somehow?
First, get the y-label text box information, and then use the blend transform method, x is from y-label box info, y is from ax[i].transAxes.
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.transforms import IdentityTransform
import matplotlib.transforms as transforms
x=np.arange(0,1,0.01)
y=np.sin(x)
fig,ax=plt.subplots(1,3,figsize=(7,5))
ax[0].plot(x,y)
ax[1].plot(x,y*0.1)
ax[2].plot(x,y*0.02)
for i in range(3):
ax[i].spines['top'].set_visible(False)
ax[i].spines['right'].set_visible(False)
ax[i].set_ylabel('Sine')
ax[i].set_xlabel('x')
plt.tight_layout()
fig.canvas.draw()
abc = r'abc'
for i in range(3):
iax = ax[i]
trans = transforms.blended_transform_factory(IdentityTransform(), iax.transAxes)
bb = iax.yaxis.label.get_window_extent()
iax.text(bb.x0-3,1.01,abc[i]+')',ha='left',fontsize=14,transform=trans)
plt.savefig('output_text.png',dpi=300)

matplotlib: Add AxesSubplot instances to a figure

I'm going insane here ... this should be a simple exercise but I'm stuck:
I have a Jupyter notebook and am using the ruptures Python package. All I want to do is, take the figure or AxesSubplot(s) that the display() function returns and add it to a figure of my own, so I can share the x-axis, have a single image, etc.:
import pandas as pd
import matplotlib.pyplot as plt
myfigure = plt.figure()
l = len(df.columns)
for index, series in enumerate(df):
data = series.to_numpy().astype(int)
algo = rpt.KernelCPD(kernel='rbf', min_size=4).fit(data)
result = algo.predict(pen=3)
myfigure.add_subplot(l, 1, index+1)
rpt.display(data, result)
plt.title(series.name)
plt.show()
What I get is a figure with the desired number of subplots (all empty) and n separate figures from ruptures:
When instead I want want the subplots to be filled with the figures ...
I basically had to recreate the plot that ruptures.display(data,result) produces, to get my desired figure:
import pandas as pd
import numpy as np
import ruptures as rpt
import matplotlib.pyplot as plt
from matplotlib.ticker import EngFormatter
fig, axs = plt.subplots(len(df.columns), figsize=(22,20), dpi=300)
for index, series in enumerate(df):
resampled = df[series].dropna().resample('6H').mean().pad()
data = resampled.to_numpy().astype(int)
algo = rpt.KernelCPD(kernel='rbf', min_size=4).fit(data)
result = algo.predict(pen=3)
# Create ndarray of tuples from the result
result = np.insert(result, 0, 0) # Insert 0 as first result
tuples = np.array([ result[i:i+2] for i in range(len(result)-1) ])
ax = axs[index]
# Fill area beween results alternating blue/red
for i, tup in enumerate(tuples):
if i%2==0:
ax.axvspan(tup[0], tup[1], lw=0, alpha=.25)
else:
ax.axvspan(tup[0], tup[1], lw=0, alpha=.25, color='red')
ax.plot(data)
ax.set_title(series)
ax.yaxis.set_major_formatter(EngFormatter())
plt.subplots_adjust(hspace=.3)
plt.show()
I've wasted more time on this than I can justify, but it's pretty now and I can sleep well tonight :D

Pyplot set_xticks doesn't work as expected

I want to set the x tick density by specifying how many ticks to skip each time. For example, if the x axis is labelled by 100 consecutive dates, and I want to skip every 10 dates, then I will do something like
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
ts = pd.period_range("20060101", periods=100).strftime("%Y%m%d")
y = np.random.randn(100)
ax = plt.subplot(1, 1, 1)
ax.plot(ts, y)
xticks = ax.get_xticks()
ax.set_xticks(xticks[::10])
plt.xticks(rotation="vertical")
plt.show()
However the output is out of place. Pyplot only picks the first few ticks and place them all in the wrong positions, although the spacing is correct:
What can I do to get the desired output? Namely the ticks should be instead:
['20060101' '20060111' '20060121' '20060131' '20060210' '20060220'
'20060302' '20060312' '20060322' '20060401']
#klim's answer seems to put the correct marks on the axis, but the labels still won't show. An example where the date axis is correctly marked yet without labels:
Set xticklabels also. Like this.
xticks = ax.get_xticks()
xticklabels = ax.get_xticklabels()
ax.set_xticks(xticks[::10])
ax.set_xticklabels(xticklabels[::10], rotation=90)
Forget the above, which doesn't work.
How about this?
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
ts = pd.period_range("20060101", periods=100).strftime("%Y%m%d")
x = np.arange(len(ts))
y = np.random.randn(100)
ax = plt.subplot(1, 1, 1)
ax.plot(x, y)
ax.set_xticks(x[::10])
ax.set_xticklabels(ts[::10], rotation="vertical")
plt.show()
This works on my machine.

Go to the next color of ax._get_lines.prop_cycler.next() continuously by loop

I want to use the next color for each plot that I plot by for loop. But when I call ax1._get_lines.prop_cycler.next(), it cannot go to the third color. How to do something like .next().next() or ax1._get_lines.prop_cycler.next()[k] in the loop line by using k in xrange() ?
a = np.cumsum(np.cumsum(np.random.randn(7,4), axis=0), axis=1)
lab = np.array(["A","B","C","E"])
for k in xrange(4):
ax1 = plt.subplot2grid((4, 1), (k, 0), rowspan=1, colspan=4)
ax1._get_lines.prop_cycler.next()
ax1.plot(a[:,k])
plt.legend(labels=lab[k:k+1])
plt.show()
In the code from the question, each subplot has its own colorcycle, such that next gives you the second color of the axes' colorcycle in each subplot.
A solution would be to share the same color cycle among all subplots.
For the case where you create the subplots inside the loop, you might use
import matplotlib.pyplot as plt
import numpy as np
a = np.cumsum(np.cumsum(np.random.randn(7,4), axis=0), axis=1)
lab = np.array(["A","B","C","E"])
for k in xrange(4):
ax1 = plt.subplot2grid((4, 1), (k, 0))
if k==0:
c = ax1._get_lines.prop_cycler
else:
ax1._get_lines.prop_cycler = c
ax1.plot(a[:,k])
plt.legend(labels=lab[k:k+1])
plt.show()
Or in a case where subplots are created all at once,
import matplotlib.pyplot as plt
import numpy as np
a = np.cumsum(np.cumsum(np.random.randn(7,4), axis=0), axis=1)
lab = np.array(["A","B","C","E"])
fig, axes = plt.subplots(4,1)
for k, ax in enumerate(axes):
ax._get_lines.prop_cycler = axes[0]._get_lines.prop_cycler
ax.plot(a[:,k])
ax.legend(labels=lab[k])
plt.show()
Of course you could also use the color cycler from each axes individually and propagate it to the next color
import matplotlib.pyplot as plt
import numpy as np
a = np.cumsum(np.cumsum(np.random.randn(7,4), axis=0), axis=1)
lab = np.array(["A","B","C","E"])
fig, axes = plt.subplots(4,1)
for k, ax in enumerate(axes):
for i in range(k):
next(ax._get_lines.prop_cycler)
ax.plot(a[:,k])
ax.legend(labels=lab[k])
plt.show()
In all cases the result would look like this:
The problem with your code is that you create a new axes instance in every loop and then you do ax1._get_lines.prop_cycler.next(). Since ax1 is a new object every time, you always get the same color.
The best solution is to explicitly pass the color to the plot. You should be able to obtain what you want with the following code, inspired by this example:
import numpy as np
import matplotlib.pyplot as plt
a = np.cumsum(np.cumsum(np.random.randn(7,4), axis=0), axis=1)
lab = np.array(["A","B","C","E"])
colors = plt.rcParams['axes.prop_cycle']
colors = colors.by_key()['color']
for (k, label), color in zip(enumerate(lab), colors):
ax1 = plt.subplot2grid((4, 1), (k, 0), rowspan=1, colspan=4)
ax1.plot(a[:,k], color=color)
plt.legend(labels=label)
plt.show()
Note: my version of the code also avoids xrange and the next() method that fail in python3.
A warning: in your example, you use ax1._get_lines. Usually names starting with an underscore are considered private: therefore the matplotlib developers might change or remove the attribute _get_lines at any time without notice and break your code. I suggest you to make use of the public API as much as possible.

Using Colormaps to set color of line in matplotlib

How does one set the color of a line in matplotlib with scalar values provided at run time using a colormap (say jet)? I tried a couple of different approaches here and I think I'm stumped. values[] is a storted array of scalars. curves are a set of 1-d arrays, and labels are an array of text strings. Each of the arrays have the same length.
fig = plt.figure()
ax = fig.add_subplot(111)
jet = colors.Colormap('jet')
cNorm = colors.Normalize(vmin=0, vmax=values[-1])
scalarMap = cmx.ScalarMappable(norm=cNorm, cmap=jet)
lines = []
for idx in range(len(curves)):
line = curves[idx]
colorVal = scalarMap.to_rgba(values[idx])
retLine, = ax.plot(line, color=colorVal)
#retLine.set_color()
lines.append(retLine)
ax.legend(lines, labels, loc='upper right')
ax.grid()
plt.show()
The error you are receiving is due to how you define jet. You are creating the base class Colormap with the name 'jet', but this is very different from getting the default definition of the 'jet' colormap. This base class should never be created directly, and only the subclasses should be instantiated.
What you've found with your example is a buggy behavior in Matplotlib. There should be a clearer error message generated when this code is run.
This is an updated version of your example:
import matplotlib.pyplot as plt
import matplotlib.colors as colors
import matplotlib.cm as cmx
import numpy as np
# define some random data that emulates your indeded code:
NCURVES = 10
np.random.seed(101)
curves = [np.random.random(20) for i in range(NCURVES)]
values = range(NCURVES)
fig = plt.figure()
ax = fig.add_subplot(111)
# replace the next line
#jet = colors.Colormap('jet')
# with
jet = cm = plt.get_cmap('jet')
cNorm = colors.Normalize(vmin=0, vmax=values[-1])
scalarMap = cmx.ScalarMappable(norm=cNorm, cmap=jet)
print scalarMap.get_clim()
lines = []
for idx in range(len(curves)):
line = curves[idx]
colorVal = scalarMap.to_rgba(values[idx])
colorText = (
'color: (%4.2f,%4.2f,%4.2f)'%(colorVal[0],colorVal[1],colorVal[2])
)
retLine, = ax.plot(line,
color=colorVal,
label=colorText)
lines.append(retLine)
#added this to get the legend to work
handles,labels = ax.get_legend_handles_labels()
ax.legend(handles, labels, loc='upper right')
ax.grid()
plt.show()
Resulting in:
Using a ScalarMappable is an improvement over the approach presented in my related answer:
creating over 20 unique legend colors using matplotlib
I thought it would be beneficial to include what I consider to be a more simple method using numpy's linspace coupled with matplotlib's cm-type object. It's possible that the above solution is for an older version. I am using the python 3.4.3, matplotlib 1.4.3, and numpy 1.9.3., and my solution is as follows.
import matplotlib.pyplot as plt
from matplotlib import cm
from numpy import linspace
start = 0.0
stop = 1.0
number_of_lines= 1000
cm_subsection = linspace(start, stop, number_of_lines)
colors = [ cm.jet(x) for x in cm_subsection ]
for i, color in enumerate(colors):
plt.axhline(i, color=color)
plt.ylabel('Line Number')
plt.show()
This results in 1000 uniquely-colored lines that span the entire cm.jet colormap as pictured below. If you run this script you'll find that you can zoom in on the individual lines.
Now say I want my 1000 line colors to just span the greenish portion between lines 400 to 600. I simply change my start and stop values to 0.4 and 0.6 and this results in using only 20% of the cm.jet color map between 0.4 and 0.6.
So in a one line summary you can create a list of rgba colors from a matplotlib.cm colormap accordingly:
colors = [ cm.jet(x) for x in linspace(start, stop, number_of_lines) ]
In this case I use the commonly invoked map named jet but you can find the complete list of colormaps available in your matplotlib version by invoking:
>>> from matplotlib import cm
>>> dir(cm)
A combination of line styles, markers, and qualitative colors from matplotlib:
import itertools
import matplotlib as mpl
import matplotlib.pyplot as plt
N = 8*4+10
l_styles = ['-','--','-.',':']
m_styles = ['','.','o','^','*']
colormap = mpl.cm.Dark2.colors # Qualitative colormap
for i,(marker,linestyle,color) in zip(range(N),itertools.product(m_styles,l_styles, colormap)):
plt.plot([0,1,2],[0,2*i,2*i], color=color, linestyle=linestyle,marker=marker,label=i)
plt.legend(bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0.,ncol=4);
UPDATE: Supporting not only ListedColormap, but also LinearSegmentedColormap
import itertools
import matplotlib.pyplot as plt
Ncolors = 8
#colormap = plt.cm.Dark2# ListedColormap
colormap = plt.cm.viridis# LinearSegmentedColormap
Ncolors = min(colormap.N,Ncolors)
mapcolors = [colormap(int(x*colormap.N/Ncolors)) for x in range(Ncolors)]
N = Ncolors*4+10
l_styles = ['-','--','-.',':']
m_styles = ['','.','o','^','*']
fig,ax = plt.subplots(gridspec_kw=dict(right=0.6))
for i,(marker,linestyle,color) in zip(range(N),itertools.product(m_styles,l_styles, mapcolors)):
ax.plot([0,1,2],[0,2*i,2*i], color=color, linestyle=linestyle,marker=marker,label=i)
ax.legend(bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0.,ncol=3,prop={'size': 8})
U may do as I have written from my deleted account (ban for new posts :( there was). Its rather simple and nice looking.
Im using 3-rd one of these 3 ones usually, also I wasny checking 1 and 2 version.
from matplotlib.pyplot import cm
import numpy as np
#variable n should be number of curves to plot (I skipped this earlier thinking that it is obvious when looking at picture - sorry my bad mistake xD): n=len(array_of_curves_to_plot)
#version 1:
color=cm.rainbow(np.linspace(0,1,n))
for i,c in zip(range(n),color):
ax1.plot(x, y,c=c)
#or version 2: - faster and better:
color=iter(cm.rainbow(np.linspace(0,1,n)))
c=next(color)
plt.plot(x,y,c=c)
#or version 3:
color=iter(cm.rainbow(np.linspace(0,1,n)))
for i in range(n):
c=next(color)
ax1.plot(x, y,c=c)
example of 3:
Ship RAO of Roll vs Ikeda damping in function of Roll amplitude A44

Categories