How might I plot multiple lines (same data, different styles) with a single plotting command in matplotlib? - matplotlib

In my example, I want to plot a "highlighting" line (a heavy black line with a narrower yellow line over it to create a black outline) behind the actual data (the blue 'Pitch Angle' line in my example).
My function highlight does this for elements in pandas Series x where h is True and also finally plots x. highlight returns unique identifiers for the highlighting lines created and the actual line styles used to allow the legend entries to be edited to produce what you see in the image, but as far as I can tell that has to be done after all plot commands, i.e. outside of highlight, assuming other things will be added to the Axes e.g. the 'Fine pitch' and '10 degrees' lines in my example.
def highlight(x, h, hlw=8, hc='#FFFBCC', hll='Highlighted', **kwargs):
"""
Plot data in Series x, highlighting the elements correponding to True in h.
"""
# For passing to extra the highlighter lines, drop any passed kwargs which
# would clobber their appearance. Keep a copy of the full set for the data.
# The extra lines are plotted first so they appear underneath the actual
# data.
fullkwargs = kwargs.copy()
for k in ['lw', 'linewidth', 'linestyle', 'ls', 'color', 'c', 'label']:
kwargs.pop(k, None)
# Generate a probably unique ID so that the legend entries of these lines
# can be identified by the caller.
import random
gid = random.random()
# Plot the highlighting lines. First, plot a heavier black line to provide
# a clear outline to the bright highlighted region.
x.where(h).plot(lw=hlw+2, c='k', label='_dont_put_me_in_legend', **kwargs)
# Plot the bright highlighter line on top of that. Give this line an id.
x.where(h).plot(lw=hlw, c=hc, label=hll, gid=gid, **kwargs)
# Plot the actual data.
a = x.plot(**fullkwargs)
# Generate the custom legend entry artists.
import matplotlib.lines as mlines
l1 = mlines.Line2D([], [], lw=hlw+2, c='k')
l2 = mlines.Line2D([], [], lw=hlw, c=hc)
# Return the unique identifier of this call, and the tuple of line styles to
# go to HandlerTuple (see
# http://matplotlib.org/users/legend_guide.html#legend-handlers). This
# makes it possible to update all "highlighter" legend entries with the
# correct color-on-black visual style produced here.
return {gid: (l1, l2)}
I manage to generate an accurate legend entry for the highlighting by making use of HandlerTuple when calling plt.legend() by looping over all legend entries and replacing legend keys where gid is one of the values returned by highlight.
handles,labels = a.get_legend_handles_labels()
newhandles = []
for h in handles:
if h.get_gid() in hi:
newhandles.append(hi[h.get_gid()])
else:
newhandles.append(h)
a.legend(newhandles, labels)
Is there a way in matplotlib to define a "compound" line style such that one plot command produces the desired appearance and has only one legend entry?

Related

Using a subfigure as textbody fails on tight layout with a lot of text

Using a subfigure as textbody fails on tight layout with a lot of text. As seen in the provided example, the bound of a figure are overreached by one subfigure, as if the text was not wrapped.
import pandas as pd
from matplotlib import pyplot as plt
# Paramters for A4 Paper
fullheight = 11.69
fullwidth = 8.27
# how the subfigures dive the space
fig0_factor = 0.7
fig1_factor = 0.3
# generate the figure
fig = plt.figure(constrained_layout = True, figsize=(fullwidth, fullheight)) #
# generate 2 subfigures
subfigs = fig.subfigures(2,height_ratios=[fig0_factor,fig1_factor])
# fill the fist subfigure
axes = subfigs[0].subplots(nrows=1)
# some plot
ax = plt.plot([0,0],[-1,1], lw=1, c='b', figure=subfigs[0])
# some text
subfigs[0].text(0.55, 0.00001, 'Figure 1', ha='center', va='center', rotation='horizontal',weight='bold')
# fill the second subfigure
text_ax = subfigs[1].subplots(1)
# make its background transparent
subfigs[1].patch.set_alpha(0.0)
# remove the axis, not removing it makes no difference regarding the problem
text_ax.set_axis_off()
# generate some text
message = ['word']*50 # 50 is enough to make the problem visable, my usecase has a way longer text
message = ' '.join(message)
# fill in the Text and wrap it
text_ax.text(0.00001, 0.8, message, horizontalalignment='left', verticalalignment='top',size=7, wrap=True)
# call tight layout
# this is neccecary for the whole plot, that is not shown for simplicity reasons
# explaination: subfigure one would be an array of subplots with different scales and descriptions,
# ever changing depending on the data that is to be plotted, so tight_layout mittigates
# a lot of tideos formatting
fig.tight_layout()
Please notice the figures right edge reaches for the whole panel, as seen in the second image
# omitting the wrap, it is clear why the figure is out of bound: somehow the layout gets its information from the unwrapped text
text_ax.text(0.00001, 0.8, message, horizontalalignment='left', verticalalignment='top',size=7, wrap=False)
Is there another way to have the text rendered in the subplot with it correctly noticing the wrapped bounds instead of the unwrapped bounds?
If you modify your code above to remove fig.tight_layout() and to take the text out of the layout then everything works as expected.
# fill in the Text and wrap it
thetext = text_ax.text(0.00001, 0.8, message, horizontalalignment='left',
verticalalignment='top',size=7, wrap=True)
thetext.set_in_layout(False)
There should be nothing that tight_layout does that layout='constrained' cannot do, and constrained layout is much more flexible.
BTW, you might consider subfigs[0].supxlabel() instead of the text command for "Figure 1"

Creating a colorbar in matplotlib given the code below

I'm having a really hard time creating a colourbar given the code below. I had the idea of colouring the lines one by one as my code adds the plot lines for each cycle iteration. What matters is that for each main cycle iteration (i in stim_strength_list) a new line is added for each of the subplots. And the colour values for such line get continuously updated.
Now the issue of such a code is that I'm having an extremely hard time creating a colourbar sitting to the right of all the subplot. Most importantly because I have no ScalarMappable object that can be fed into the plt.colorbar() function. And I don't know of any other function that could get the job done properly.
I see a lot of tutorials on-line where no parameters are given at all to the function and everything works just fine but I don't know why it isn't the case for me. I just began programming recently so I'm really sure that there is something really simple flyin over my head, but despite having searched all over stack overflow I haven't managed to find anything that could fix my specific problem.
stim_strength_list = [int(x)*10 for x in range(0,81)]
stp_list = [int(x) for x in range(51,71)]
area_list = ['V1', 'TO', 'AT', 'PFL', 'PML', 'M1L', 'A1', 'AB', 'PB', 'PFI', 'PMI', 'M1I']
# CREATE THE FIGURE
fig, axis = plt.subplots(2, 6, sharey=True, sharex=True, squeeze=True, figsize=(30, 22.5))
fig.suptitle("Activation plots for each area at different levels of strength, spiking architecture", fontsize=25.0, y=0.99)
# COLOURS
red = 0.0
blue = 1.0
for i in stim_strength_list:
# CREATING VALUES TO BE PLOTTED
a = df_s.drop(df_s[df_s['stim_strength'] != i].index)
b = a.groupby('stp').mean()
for j in area_list: # here a create an empty list for each of the 12 areas
globals()[j]=[] # i.e.: V1 = [], TO = [], AT = [], ...
for j in stp_list: # here I cycle through the time steps..
for k in area_list:
globals()[k].append(b.loc[j, k]) # to append values to each area called upon
# PLOTTING
for row in range(0, 2): # given the plt.subplots() methods refers to the subplots via a matrix coordinate system...
for col in range (0, 6): # ...I use this nested loop to plot values in each list in its respective subplot
if row == 0:
axis[row, col].plot(stp_list, globals()[area_list[col]], color=(red, 0, blue), linewidth=1)
else: # about the col + 6 indexing, if I'm in the second row of the subplots I just add 6 to the current value of col to fetch the appropriate brain area this is just
axis[row, col].plot(stp_list, globals()[area_list[col + 6]], color=(red, 0, blue), linewidth=1)
blue -= 1/len(stim_strength_list) # this is to make it that the first line is fully blue...
red += 1/len(stim_strength_list) # ... and the final one is fully red
# TITLING
for row in range(0, 2): #same logic as above, I did an external nested loop simply because the titling would be done for len(stp_list)*12 times
for col in range (0, 6):
if row == 0:
axis[row, col].set_title(area_list[col], fontsize=25)
else:
axis[row, col].set_title(area_list[col + 6], fontsize=25)
# LABELLING
axis[0, 0].set_ylabel('Amount of cells', fontsize=20)
axis[1, 0].set_ylabel('Amount of cells', fontsize=20)
for i in range(0, 6):
axis[1, i].set_xlabel('Time step', fontsize=20)
fig.tight_layout()
plt.show()
This is the code which creates everything. What is missing is the dataframe from which data is taken but i think that you can get an idea by looking at the result.
Final plot
What I would love to achieve is to have a colorbar which displays the colors employed to color the different lines within each subplot. Such transition is determined by progressively increasing and decreasing the red and blue values within the color attribute in .plot() method.
If I just add plt.colorbar() I will get the following error:
Traceback (most recent call last):
  at block x, line y
  at /opt/python/envs/default/lib/python3.8/site-packages/matplotlib/pyplot.py, line 2084, in colorbar(mappable, cax, ax, **kw)
RuntimeError: No mappable was found to use for colorbar creation. First define a mappable such as an image (with imshow) or a contour set (with contourf).
What would you do with the code available to achieve the result I described above?

Why does get_legend_handles_labels() return empty lists when legend labels are added via ax.legend() [duplicate]

This one is blowing my mind. I'm creating a little customize legend function where you put in some text and properties and it'll make it for you. So, before this is reachable in the GUI, a figure with a legend has already been created by this point which has "run5" and "run6" in it.
I wrote something that deletes the existing legend and calls legend on that same axis again with new handles/labels. However, when I do ax.get_legend_handles_labels() right afterwards it returns the deleted legend's handle and labels, completely ignoring the legend call I just did.
I've tried removing the legend and then just recreating it. But clearly ax is holding onto the previous legend's data.
from matplotlib.lines import Line2D
ax = self.axes[ind] #just the axis handle
custom_lines, custom_strings = [], []
try:
ax.get_legend().remove()
except:
# Means no legend exists
pass
for idx, i in enumerate(self.lgndRowWidget):
if not i.isHidden():
#The below five lines are grabbing data from GUI
lineText = self.lgndStr[idx].text() # Is "test" here
lineType = self.lgndLine[idx].currentText()
lineWidth = self.lgndWidth[idx].value()
lineMrkr = self.lgndMrkr[idx].currentText()
lineClr = self.lgndClr[idx].text()
custom_lines.append(Line2D([0],[0],
lw=lineWidth,
ls=lineType,
marker=lineMrkr,
color=lineClr))
custom_strings.append(lineText)
if len(custom_lines) != 0:
print(custom_strings)
self.legList = ax.legend(custom_lines, custom_strings)
a,b = ax.get_legend_handles_labels()
print(b)
self.fig.canvas.draw()
print(custom_strings) returns whatever I input. In this case "test".
print(b) returns what was previously in the legend that I can't seem to get rid of: the initial "run5" and "run6". It SHOULD be "test".
You might have misunderstood the functionality of ax.get_legend_handles_labels().
What it does is to look for artists (like lines, collections etc.) that have a label.
It then returns those artists and their respective labels. Hence
ax.legend() is roughly equivalent to
handles, labels = ax.get_legend_handles_labels()
ax.legend(handles=handles, labels=labels)
.get_legend_handles_labels() does not know about whether there is a legend present, because it returns what is supposed to be in the legend, not what currently is in it.

ax.lines contains line objects without x- and y-coordinates

For my project, I use sns.FacetGrid() to plot multiple subplots each containing multiple lines. My general goal is to draw a mean line for each line in each subplot. My idea was to extract the x- and y-data for each line. For this I iterate over each subplot and then over each line object in each subplot (as described in this stackoverflow post). The problem: Each of the subjects seems to contain four 'empty' line objects, but my subplots contain only three lines each. So my expected output is a list of six tuples, each containing an array for my x- and y-data. Does anyone know where these four empty line objects come from and how to get only the x- and y-data for each of the existing (aka. visible) lines?
Here's my code:
import numpy as np
import pandas as pd
import seaborn as sns
# simulate data frames #########################################################
n_outer_folds = 10
plot_df_1 = pd.DataFrame({'Outer Fold':np.linspace(start=1,stop=10,num=n_outer_folds),
'train_BAC':np.random.uniform(low=0.6,high=1.0,size=n_outer_folds).tolist(),
'train_SPEC':np.random.uniform(low=0.6,high=1.0,size=n_outer_folds).tolist(),
'test_BAC':np.random.uniform(low=0.1,high=0.8,size=n_outer_folds).tolist(),
'test_SPEC':np.random.uniform(low=0.1,high=0.8,size=n_outer_folds).tolist()
})
plot_df_2 = pd.DataFrame({'Outer Fold':np.linspace(start=1,stop=10,num=n_outer_folds),
'train_BAC':np.random.uniform(low=0.6,high=1.0,size=n_outer_folds).tolist(),
'train_SPEC':np.random.uniform(low=0.6,high=1.0,size=n_outer_folds).tolist(),
'test_BAC':np.random.uniform(low=0.1,high=0.8,size=n_outer_folds).tolist(),
'test_SPEC':np.random.uniform(low=0.1,high=0.8,size=n_outer_folds).tolist()
})
plot_df_list = [plot_df_1,plot_df_2]
# append 'Model' column to make each plot df identifiable
for idx,plot_df in enumerate(plot_df_list):
plot_df['Model'] = idx
# concatenate all plot dfs
plot_df = pd.concat(plot_df_list)
# create a plotable Dataframe
plot_df_melt = pd.melt(plot_df,
id_vars=['Outer Fold','Model'],
value_vars=['train_BAC','test_BAC','train_SPEC'],
var_name ='Scores',
value_name='Score'
)
# plot data
g = sns.FacetGrid(plot_df_melt,col="Model",height=4,aspect=2,col_wrap=1)
g.map(sns.lineplot,'Outer Fold','Score','Scores')
# get line data
axes_data = []
ax_lines_data = []
for ax in g.axes.flat:
axes_data.append(ax)
for line in ax.lines:
ax_lines_data.append((line.get_xdata(),line.get_ydata()))
Plotting n different hue categories in a seaborn lineplot gives you 2*n+1 lines in the axes.
A minimal example:
df = pd.DataFrame({"x" : [1,2,2,4], "y" : [1,2,3,4], "hue" : list("ABAB")})
ax = sns.lineplot("x", "y", "hue", data=df)
print([line.get_label() for line in ax.lines])
prints
['_line0', '_line1', 'hue', 'A', 'B']
Here, '_line0', '_line1' are the lines shown in the image. They contain the data. Their label starts with an underscore, such that they would not appear in a legend.
The remaining 'hue', 'A', 'B' do not contain any data. Their sole purpose is to make up the legend. 'hue' is the "legend title", which is a normal legend entry as well; 'A', 'B' are the legend entries.
This is a consequence of how seaborn creates legends. Possible options are to filter the lines. E.g. one could take only the ones which have an underscore as first character in their label,
[line for line in ax.lines if line.get_label()[0] == "_"]

Pandas / Plot.plt Line Graph, X Values horizontal

How can I pass an argument to show all of the x value so one could read it? As well, how can I show many lines of data, and with alegend?
plt. plot(a, b, linewidth=2.0 )
You could display only n-th x-ticks and make a plot bigger to accomodate more labels.
df = pd.DataFrame(data=np.random.rand(10), index=pd.Series(np.random.rand(10)).astype(str)+'_index')
0
0.007017115173211574_index 0.963285
0.434969747965131_index 0.547248
0.18021258326382017_index 0.719402
0.7815848046772174_index 0.061448
0.8856299613744312_index 0.771062
0.16840431221766328_index 0.524256
0.8662531211345982_index 0.528706
0.6389453277004077_index 0.287410
0.7444490769967744_index 0.513631
0.8965709043061524_index 0.892011
plt.subplots(figsize=(20,15)) # make plot bigger
plt.plot(df.index, df[0]*2, linewidth=.9) # plot several lines
plt.plot(df.index, df[0].rename('1'),linewidth=.9) # plot several lines
plt.xticks(df.index[::2]) # tick every 2nd label on x axis.
plt.legend() # show legend