Matplotlib bar chart for negative numbers going above x-axis [duplicate] - python

This question already has answers here:
Modify tick label text
(13 answers)
Closed 4 years ago.
I am trying to make a bar chart of negative values where the baseline x-axis is at -10 instead of 0 and the values, because they are all -10<x<0, extend up from the baseline.
If I plot it as is, the bars extend downward:
import matplotlib.pyplot as plt
vals = [-4, -6, -8, -6, -5]
plt.bar(range(len(vals)), vals)
I could fake it in some sense by adding 10 to the data, but then I would have to remove the y-tick values, and I need to keep them.
new_vals = [val + 10 for val in vals]
plt.yticks([])
plt.bar(range(len(new_vals)), new_vals)
So how can I make the second image with the y-ticks of the first image and, preferably, without "faking" any of the data?

Following https://matplotlib.org/gallery/ticks_and_spines/custom_ticker1.html example, you can also do like this:
from matplotlib.ticker import FuncFormatter
def neg_tick(x, pos):
return '%.1f' % (-x if x else 0) # avoid negative zero (-0.0) labels
formatter = FuncFormatter(neg_tick)
fig, ax = plt.subplots()
ax.yaxis.set_major_formatter(formatter)
plt.bar(range(len(vals)), [-v for v in vals]) # or -numpy.asarray(vals)
# or, let Python enumerate bars:
# plt.bar(*zip(*enumerate(-v for v in vals)))
plt.show()
You cannot not "fake" data. Bar charts plot bars up for positive data and down for negative. If you want things differently, you need to trick bar chart somehow.
IMO, the above solution is better than https://stackoverflow.com/a/11250884/8033585 because it does not need the trick with drawing canvas, changing labels, etc.
If you do want to have "reverted" bar length (as it is in your example), then you can do the following:
from matplotlib.ticker import FuncFormatter
import numpy as np
# shifted up:
vals = np.asarray(vals)
minval = np.amin(vals)
minval += np.sign(minval) # "add" 1 so that "lowest" bar is still drawn
def neg_tick(x, pos):
return '%.1f' % (x + minval if x != minval else 0)
formatter = FuncFormatter(neg_tick)
fig, ax = plt.subplots()
ax.yaxis.set_major_formatter(formatter)
plt.bar(*zip(*enumerate(-minval + vals)))
plt.show()

Try matplotlib.axes.Axes.invert_yaxis
from matplotlib import pyplot as plt
vals = [-4, -6, -8, -6, -5]
plt.bar(range(len(vals)), vals)
fig, ax1 = plt.subplots(1,1)
ax1.bar(range(len(vals)), vals)
ax1.invert_yaxis()

Related

Set matplotlib tick locators, but specify spacing around them

For example if I have the following:
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
fig, ax = plt.subplots()
ax.set_xlim(left=0, right=11)
ax.xaxis.set_major_locator(ticker.MultipleLocator(1))
Which looks as:
Here the range is set to include 0 and 11 so that there's some spacing around the plotted values, but the data only contains values of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] - so I'd like to not have 0 and 11 on the xaxis.
Looking at the documentation for ticker.MultipleLocator (here) it's not clear how this should be done (of if it can be with a locator). I tried to use the view_limits method but it just seems to return a tuple.
Ideally the values 0 and 11 would be gone, and the plot would look as:
edit 1
The following "works"
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
fig, ax = plt.subplots()
ax.set_xlim(left=1e-2, right=11 - 1e-2)
ax.xaxis.set_major_locator(ticker.MultipleLocator(1))
Though removing fractional amounts in order to not display something isn't a solution here as it's constrained to the base value of the MultipleLocator.
For example - what if I wanted the following:
ax.set_xlim(left=1e-2, right=11 - 1e-2)
ax.xaxis.set_major_locator(ticker.MultipleLocator(0.5))
Which looks as:
Then I have 10.5 and so on, whereas I might still like the spacing to be a particular value not limited to the MultipleLocator size.
Getting the lower limit is something that is a bit tricky and not by default included in any of the Matplotlib ticker. However, taking a look at the source code
(https://github.com/matplotlib/matplotlib/blob/v3.5.1/lib/matplotlib/ticker.py#L2734-L2751) gives a good hint how to implement this by deriving a new MyMultipleLocator class from MultipleLocator by yourself. Here is a piece of code that should work:
import matplotlib.pyplot as plt
from matplotlib.ticker import MultipleLocator, _Edge_integer
import numpy as np
class MyMultipleLocator(MultipleLocator):
def __init__(self, base=1.0, offset=0.):
self._edge = _Edge_integer(base, 0)
self._offset = offset
def tick_values(self, vmin, vmax):
# we HAVE to re-implement this method as it is called by
# xaxis.set_major_locator(...)
vmin = self._edge.ge(vmin)
step = self._edge.step
n = (vmax - vmin + 0.001 * step) // step
locs = self._offset + vmin - step + np.arange(n + 3) * step
return self.raise_if_exceeds(locs)
fig, ax = plt.subplots()
x = np.arange(1, 11)
ax.plot(x, np.random.randint(-3, 3, size=x.size))
tick_spacing = 2
ax.xaxis.set_major_locator(MyMultipleLocator(base=tick_spacing, offset=0))
You can now additionally change the offset manually in the last line. You could even do this automatically by checking if the minimum (the nearest int, that is) is an odd or an even number, but if I understand the question correctly then this is not required here.
Output from the above code:
example 1
fig, ax = plt.subplots()
ax.xaxis.set_major_locator(MyMultipleLocator(base=1, offset=0))
ax.set_xlim(0.5, 10.5)
Which gives:
example 2
fig, ax = plt.subplots()
ax.xaxis.set_major_locator(MyMultipleLocator(base=2, offset=0.5))
ax.set_xlim(0., 11)
Which gives:

Matplotlib: Automatic coloured legend for all subplots using subplot line labels

The code below achieves what I want to do, but does so in a very roundabout way. I have looked around for a succinct way to produce a single legend for a figure that includes multiple subplots that takes into account their labels, to no avail. plt.figlegend() requires you to pass in labels and lines, and plt.legend() requires only handles (slightly better).
My example below illustrates what I want. I have 9 vectors, each with one of 3 categories. I want to plot each vector on a separate sub plot, label it, and plot a legend which indicates (using colour) what the label means; this is the automatic behaviour on a single plot.
Do you know of a better way of achieving the plot below?
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
nr_lines = 9
nr_cats = 3
np.random.seed(1337)
# Data
X = np.random.randn(nr_lines, 100)
labels = ['Category {}'.format(ii) for ii in range(nr_cats)]
y = np.random.choice(labels, nr_lines)
# Ideally wouldn't have to manually pick colours
clrs = matplotlib.rcParams['axes.prop_cycle'].by_key()['color']
clrs = [clrs[ii] for ii in range(nr_cats)]
lab_clr = {k: v for k, v in zip(labels, clrs)}
fig, ax = plt.subplots(3, 3)
ax = ax.flatten()
for ii in range(nr_lines):
ax[ii].plot(X[ii,:], label=y[ii], color=lab_clr[y[ii]])
lines = [a.lines[0] for a in ax]
l_labels = [l.get_label() for l in lines]
# the hack - get a single occurance of each label
idx_list = [l_labels.index(lab) for lab in labels]
lines_ = [lines[idx] for idx in idx_list]
#l_labels_ = [l_labels[idx] for idx in idx_list]
plt.legend(handles=lines_, bbox_to_anchor=[2, 2.5])
plt.tight_layout()
plt.savefig('/home/james/Downloads/stack_figlegend_example.png',
bbox_inches='tight')
You could use a dictionary to collect them using the label as a key. For example:
handles = {}
for ii in range(nr_lines):
l1, = ax[ii].plot(X[ii,:], label=y[ii], color=lab_clr[y[ii]])
if y[ii] not in handles:
handles[y[ii]] = l1
plt.legend(handles=handles.values(), bbox_to_anchor=[2, 2.5])
You only add a handle to the dictionary if the category isn't already present.

Remove axis scale

I've spent some time fruitlessly searching for an answer to my question, so I think a new question is in order. Consider this plot:
The axes labels use scientific notation. On the y-axis, all is well. However, I have tried and failed to get rid off the scaling factor that Python added in the lower-right corner. I would like to either remove this factor completely and simply indicate it by the units in the axis title or have it multiplied to every tick label. Everything would look better than this ugly 1e14.
Here's the code:
import numpy as np data_a = np.loadtxt('exercise_2a.txt')
import matplotlib as mpl
font = {'family' : 'serif',
'size' : 12}
mpl.rc('font', **font)
import matplotlib.pyplot as plt
fig = plt.figure()
subplot = fig.add_subplot(1,1,1)
subplot.plot(data_a[:,0], data_a[:,1], label='$T(t)$', linewidth=2)
subplot.set_yscale('log')
subplot.set_xlabel("$t[10^{14}s]$",fontsize=14)
subplot.set_ylabel("$T\,[K]$",fontsize=14)
plt.xlim(right=max(data_a [:,0]))
plt.legend(loc='upper right')
plt.savefig('T(t).pdf', bbox_inches='tight')
Update: Incorporating Will's implementation of scientificNotation into my script, the plot now looks like
Much nicer if you ask me. Here's the complete code for anyone wanting to adopt some part of it:
import numpy as np
data = np.loadtxt('file.txt')
import matplotlib as mpl
font = {'family' : 'serif',
'size' : 16}
mpl.rc('font', **font)
import matplotlib.pyplot as plt
fig = plt.figure()
subplot = fig.add_subplot(1,1,1)
subplot.plot(data[:,0], data[:,1], label='$T(t)$', linewidth=2)
subplot.set_yscale('log')
subplot.set_xlabel("$t[s]$",fontsize=20)
subplot.set_ylabel("$T\,[K]$",fontsize=20)
plt.xlim(right=max(data [:,0]))
plt.legend(loc='upper right')
def scientificNotation(value):
if value == 0:
return '0'
else:
e = np.log10(np.abs(value))
m = np.sign(value) * 10 ** (e - int(e))
return r'${:.0f} \cdot 10^{{{:d}}}$'.format(m, int(e))
formatter = mpl.ticker.FuncFormatter(lambda x, p: scientificNotation(x))
plt.gca().xaxis.set_major_formatter(formatter)
plt.savefig('T(t).pdf', bbox_inches='tight', transparent=True)
Just divide the x-values by 1e14:
subplot.plot(data_a[:,0] / 1e14, data_a[:,1], label='$T(t)$', linewidth=2)
If you want to add the label to each individual tick, you'll have to provide a custom formatter, like in tom's answer.
If you want it to look like as nice as the ticks on your y-axis, you could provide a function to format it with LaTeX:
def scientificNotation(value):
if value == 0:
return '0'
else:
e = np.log10(np.abs(value))
m = np.sign(value) * 10 ** (e - int(e))
return r'${:.0f} \times 10^{{{:d}}}$'.format(m, int(e))
# x is the tick value; p is the position on the axes.
formatter = mpl.ticker.FuncFormatter(lambda x, p: scientificNotation(x))
plt.gca().xaxis.set_major_formatter(formatter)
Of course, this will clutter your x-axis up quite a bit, so you might end up needing to display them at an angle, for example.
You can also change the tick formatter with the ticker module.
An example would be to use a FormatStrFormatter:
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
fig,ax = plt.subplots()
ax.semilogy(np.linspace(0,5e14,50),np.logspace(3,7,50),'b-')
ax.xaxis.set_major_formatter(ticker.FormatStrFormatter('%.0e'))
Also see the answers here with lots of good ideas for ways to solve this.
In addition to the good answer from Will Vousden, you can set what you write in your ticks with:
plt.xticks(range(6), range(6))
the first range(6) is the location and the second is the label.

Matplotlib log scale tick label number formatting

With matplotlib when a log scale is specified for an axis, the default method of labeling that axis is with numbers that are 10 to a power eg. 10^6. Is there an easy way to change all of these labels to be their full numerical representation? eg. 1, 10, 100, etc.
Note that I do not know what the range of powers will be and want to support an arbitrary range (negatives included).
Sure, just change the formatter.
For example, if we have this plot:
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
ax.axis([1, 10000, 1, 100000])
ax.loglog()
plt.show()
You could set the tick labels manually, but then the tick locations and labels would be fixed when you zoom/pan/etc. Therefore, it's best to change the formatter. By default, a logarithmic scale uses a LogFormatter, which will format the values in scientific notation. To change the formatter to the default for linear axes (ScalarFormatter) use e.g.
from matplotlib.ticker import ScalarFormatter
for axis in [ax.xaxis, ax.yaxis]:
axis.set_major_formatter(ScalarFormatter())
I've found that using ScalarFormatter is great if all your tick values are greater than or equal to 1. However, if you have a tick at a number <1, the ScalarFormatter prints the tick label as 0.
We can use a FuncFormatter from the matplotlib ticker module to fix this issue. The simplest way to do this is with a lambda function and the g format specifier (thanks to #lenz in comments).
import matplotlib.ticker as ticker
ax.yaxis.set_major_formatter(ticker.FuncFormatter(lambda y, _: '{:g}'.format(y)))
Note in my original answer I didn't use the g format, instead I came up with this lambda function with FuncFormatter to set numbers >= 1 to their integer value, and numbers <1 to their decimal value, with the minimum number of decimal places required (i.e. 0.1, 0.01, 0.001, etc). It assumes that you are only setting ticks on the base10 values.
import matplotlib.ticker as ticker
import numpy as np
ax.yaxis.set_major_formatter(ticker.FuncFormatter(lambda y,pos: ('{{:.{:1d}f}}'.format(int(np.maximum(-np.log10(y),0)))).format(y)))
For clarity, here's that lambda function written out in a more verbose, but also more understandable, way:
def myLogFormat(y,pos):
# Find the number of decimal places required
decimalplaces = int(np.maximum(-np.log10(y),0)) # =0 for numbers >=1
# Insert that number into a format string
formatstring = '{{:.{:1d}f}}'.format(decimalplaces)
# Return the formatted tick label
return formatstring.format(y)
ax.yaxis.set_major_formatter(ticker.FuncFormatter(myLogFormat))
I found Joe's and Tom's answers very helpful, but there are a lot of useful details in the comments on those answers. Here's a summary of the two scenarios:
Ranges above 1
Here's the example code like Joe's, but with a higher range:
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
ax.axis([1, 10000, 1, 1000000])
ax.loglog()
plt.show()
That shows a plot like this, using scientific notation:
As in Joe's answer, I use a ScalarFormatter, but I also call set_scientific(False). That's necessary when the scale goes up to 1000000 or above.
import matplotlib.pyplot as plt
from matplotlib.ticker import ScalarFormatter
fig, ax = plt.subplots()
ax.axis([1, 10000, 1, 1000000])
ax.loglog()
for axis in [ax.xaxis, ax.yaxis]:
formatter = ScalarFormatter()
formatter.set_scientific(False)
axis.set_major_formatter(formatter)
plt.show()
Ranges below 1
As in Tom's answer, here's what happens when the range goes below 1:
import matplotlib.pyplot as plt
from matplotlib.ticker import ScalarFormatter
fig, ax = plt.subplots()
ax.axis([0.01, 10000, 1, 1000000])
ax.loglog()
for axis in [ax.xaxis, ax.yaxis]:
formatter = ScalarFormatter()
formatter.set_scientific(False)
axis.set_major_formatter(formatter)
plt.show()
That displays the first two ticks on the x axis as zeroes.
Switching to a FuncFormatter handles that. Again, I had problems with numbers 1000000 or higher, but adding a precision to the format string solved it.
import matplotlib.pyplot as plt
from matplotlib.ticker import FuncFormatter
fig, ax = plt.subplots()
ax.axis([0.01, 10000, 1, 1000000])
ax.loglog()
for axis in [ax.xaxis, ax.yaxis]:
formatter = FuncFormatter(lambda y, _: '{:.16g}'.format(y))
axis.set_major_formatter(formatter)
plt.show()
regarding these questions
What if I wanted to change the numbers to, 1, 5, 10, 20?
– aloha Jul 10 '15 at 13:26
I would like to add ticks in between, like 50,200, etc.., How can I do
that? I tried, set_xticks[50.0,200.0] but that doesn't seem to work!
– ThePredator Aug 3 '15 at 12:54
But with ax.axis([1, 100, 1, 100]), ScalarFormatter gives 1.0, 10.0, ... which is not what I desire. I want it to give integers...
– CPBL Dec 7 '15 at 20:22
you can solve those issue like this with MINOR formatter:
ax.yaxis.set_minor_formatter(matplotlib.ticker.ScalarFormatter())
ax.yaxis.set_minor_formatter(matplotlib.ticker.FormatStrFormatter("%.8f"))
ax.set_yticks([0.00000025, 0.00000015, 0.00000035])
in my application I'm using this format scheme, which I think solves most issues related to log scalar formatting; the same could be done for data > 1.0 or x axis formatting:
plt.ylabel('LOGARITHMIC PRICE SCALE')
plt.yscale('log')
ax.yaxis.set_major_formatter(matplotlib.ticker.ScalarFormatter())
ax.yaxis.set_major_formatter(matplotlib.ticker.FormatStrFormatter("%.8f"))
ax.yaxis.set_minor_formatter(matplotlib.ticker.ScalarFormatter())
ax.yaxis.set_minor_formatter(matplotlib.ticker.FormatStrFormatter("%.8f"))
#####################################################
#force 'autoscale'
#####################################################
yd = [] #matrix of y values from all lines on plot
for n in range(len(plt.gca().get_lines())):
line = plt.gca().get_lines()[n]
yd.append((line.get_ydata()).tolist())
yd = [item for sublist in yd for item in sublist]
ymin, ymax = np.min(yd), np.max(yd)
ax.set_ylim([0.9*ymin, 1.1*ymax])
#####################################################
z = []
for i in [0.0000001, 0.00000015, 0.00000025, 0.00000035,
0.000001, 0.0000015, 0.0000025, 0.0000035,
0.00001, 0.000015, 0.000025, 0.000035,
0.0001, 0.00015, 0.00025, 0.00035,
0.001, 0.0015, 0.0025, 0.0035,
0.01, 0.015, 0.025, 0.035,
0.1, 0.15, 0.25, 0.35]:
if ymin<i<ymax:
z.append(i)
ax.set_yticks(z)
for comments on "force autoscale" see: Python matplotlib logarithmic autoscale
which yields:
then to create a general use machine:
# user controls
#####################################################
sub_ticks = [10,11,12,14,16,18,22,25,35,45] # fill these midpoints
sub_range = [-8,8] # from 100000000 to 0.000000001
format = "%.8f" # standard float string formatting
# set scalar and string format floats
#####################################################
ax.yaxis.set_major_formatter(matplotlib.ticker.ScalarFormatter())
ax.yaxis.set_major_formatter(matplotlib.ticker.FormatStrFormatter(format))
ax.yaxis.set_minor_formatter(matplotlib.ticker.ScalarFormatter())
ax.yaxis.set_minor_formatter(matplotlib.ticker.FormatStrFormatter(format))
#force 'autoscale'
#####################################################
yd = [] #matrix of y values from all lines on plot
for n in range(len(plt.gca().get_lines())):
line = plt.gca().get_lines()[n]
yd.append((line.get_ydata()).tolist())
yd = [item for sublist in yd for item in sublist]
ymin, ymax = np.min(yd), np.max(yd)
ax.set_ylim([0.9*ymin, 1.1*ymax])
# add sub minor ticks
#####################################################
set_sub_formatter=[]
for i in sub_ticks:
for j in range(sub_range[0],sub_range[1]):
set_sub_formatter.append(i*10**j)
k = []
for l in set_sub_formatter:
if ymin<l<ymax:
k.append(l)
ax.set_yticks(k)
#####################################################
yields:
The machinery outlined in the accepted answer works great, but sometimes a simple manual override is easier. To get ticks at 1, 10, 100, 1000, for example, you could say:
ticks = 10**np.arange(4)
plt.xticks(ticks, ticks)
Note that it is critical to specify both the locations and the labels, otherwise matplotlib will ignore you.
This mechanism can be used to obtain arbitrary formatting. For instance:
plt.xticks(ticks, [ f"{x:.0f}" for x in ticks ])
or
plt.xticks(ticks, [ f"10^{int(np.log10(x))}" for x in ticks ])
or
plt.xticks(ticks, [ romannumerals(x) for x in ticks ])
(where romannumerals is an imagined function that converts its argument into Roman numerals).
As an aside, this technique also works if you want ticks at arbitrary intervals, e.g.,
ticks = [1, 2, 5, 10, 20, 50, 100]
etc.
import matplotlib.pyplot as plt
plt.rcParams['axes.formatter.min_exponent'] = 2
plt.xlim(1e-5, 1e5)
plt.loglog()
plt.show()
This will become default for all plots in a session.
See also: LogFormatter tickmarks scientific format limits

Stop matplotlib repeating labels in legend

Here is a very simplified example:
xvalues = [2,3,4,6]
for x in xvalues:
plt.axvline(x,color='b',label='xvalues')
plt.legend()
The legend will now show 'xvalues' as a blue line 4 times in the legend.
Is there a more elegant way of fixing this than the following?
for i,x in enumerate(xvalues):
if not i:
plt.axvline(x,color='b',label='xvalues')
else:
plt.axvline(x,color='b')
plt.legend takes as parameters
A list of axis handles which are Artist objects
A list of labels which are strings
These parameters are both optional defaulting to plt.gca().get_legend_handles_labels().
You can remove duplicate labels by putting them in a dictionary before calling legend. This is because dicts can't have duplicate keys.
For example:
For Python versions < 3.7
from collections import OrderedDict
import matplotlib.pyplot as plt
handles, labels = plt.gca().get_legend_handles_labels()
by_label = OrderedDict(zip(labels, handles))
plt.legend(by_label.values(), by_label.keys())
For Python versions > 3.7
As of Python 3.7, dictionaries retain input order by default. Thus, there is no need for OrderedDict form the collections module.
import matplotlib.pyplot as plt
handles, labels = plt.gca().get_legend_handles_labels()
by_label = dict(zip(labels, handles))
plt.legend(by_label.values(), by_label.keys())
Docs for plt.legend
handles, labels = ax.get_legend_handles_labels()
handle_list, label_list = [], []
for handle, label in zip(handles, labels):
if label not in label_list:
handle_list.append(handle)
label_list.append(label)
plt.legend(handle_list, label_list)
I don't know if this can be considered "elegant", but you can have your label a variable that gets set to "_nolegend_" after first usage:
my_label = "xvalues"
xvalues = [2,3,4,6]
for x in xvalues:
plt.axvline(x, color='b', label=my_label)
my_label = "_nolegend_"
plt.legend()
This can be generalized using a dictionary of labels if you have to put several labels:
my_labels = {"x1" : "x1values", "x2" : "x2values"}
x1values = [1, 3, 5]
x2values = [2, 4, 6]
for x in x1values:
plt.axvline(x, color='b', label=my_labels["x1"])
my_labels["x1"] = "_nolegend_"
for x in x2values:
plt.axvline(x, color='r', label=my_labels["x2"])
my_labels["x2"] = "_nolegend_"
plt.legend()
(Answer inspired by https://stackoverflow.com/a/19386045/1878788)
Problem - 3D Array
Questions: Nov 2012, Oct 2013
import numpy as np
a = np.random.random((2, 100, 4))
b = np.random.random((2, 100, 4))
c = np.random.random((2, 100, 4))
Solution - dict uniqueness
For my case _nolegend_ (bli and DSM) would not work, nor would label if i==0. ecatmur's answer uses get_legend_handles_labels and reduces the legend down with collections.OrderedDict. Fons demonstrates this is possible without an import.
Inline with these answers, I suggest using dict for unique labels.
# Step-by-step
ax = plt.gca() # Get the axes you need
a = ax.get_legend_handles_labels() # a = [(h1 ... h2) (l1 ... l2)] non unique
b = {l:h for h,l in zip(*a)} # b = {l1:h1, l2:h2} unique
c = [*zip(*b.items())] # c = [(l1 l2) (h1 h2)]
d = c[::-1] # d = [(h1 h2) (l1 l2)]
plt.legend(*d)
Or
plt.legend(*[*zip(*{l:h for h,l in zip(*ax.get_legend_handles_labels())}.items())][::-1])
Maybe less legible and memorable than Matthew Bourque's solution. Code golf welcome.
Example
import numpy as np
a = np.random.random((2, 100, 4))
b = np.random.random((2, 100, 4))
import matplotlib.pyplot as plt
fig, ax = plt.subplots(1)
ax.plot(*a, 'C0', label='a')
ax.plot(*b, 'C1', label='b')
ax.legend(*[*zip(*{l:h for h,l in zip(*ax.get_legend_handles_labels())}.items())][::-1])
# ax.legend() # Old, ^ New
plt.show()
Based on answer https://stackoverflow.com/a/13589144/9132798 and https://stackoverflow.com/a/19386045/9132798
plt.gca().get_legend_handles_labels()[1] gives a list of names, it is possible to check if the label is already in the list while in the loop plotting (label= name[i] if name[i] not in plt.gca().get_legend_handles_labels()[1] else '').
For the given example this solution would look like:
import matplotlib.pyplot as plt
xvalues = [2,3,4,6]
for x in xvalues:
plt.axvline(x,color='b',\
label= 'xvalues' if 'xvalues' \
not in plt.gca().get_legend_handles_labels()[1] else '')
plt.legend()
Which is much shorter than https://stackoverflow.com/a/13589144/9132798 and more flexible than https://stackoverflow.com/a/19386045/9132798 as it could be use for any kind of loop any plot function in the loop individually.
However, for many cycles it probably slower than https://stackoverflow.com/a/13589144/9132798.
These code snippets didn't work for me personally. I was plotting two different groups in two different colors. The legend would show two red markers and two blue markers, when I only wanted to see one per color. I'll paste a simplified version of what did work for me:
Import statements
import matplotlib.pyplot as plt
from matplotlib.legend_handler import HandlerLine2D
Plot data
points_grp, = plt.plot(x[grp_idx], y[grp_idx], color=c.c[1], marker=m, ms=4, lw=0, label=leglab[1])
points_ctrl, = plt.plot(x[ctrl_idx], y[ctrl_idx], color=c.c[0], marker=m, ms=4, lw=0, label=leglab[0])
Add legend
points_dict = {points_grp: HandlerLine2D(numpoints=1),points_ctrl: HandlerLine2D(numpoints=1)}
leg = ax.legend(fontsize=12, loc='upper left', bbox_to_anchor=(1, 1.03),handler_map=points_dict)

Categories

Resources