I'm using matplotlib 1.3.0 and I have the following:
import matplotlib.pyplot as plt
cmap = plt.cm.jet
plt.contourf([[.12, .2], [.8, 2]], levels=[0, .1, .3, .5, 1, 3], cmap=cmap, vmin=0, vmax=3)
plt.colorbar()
which produces:
The bit that I don't understand is where did all of the other colors go? As I understand, by specifying vmin=0, vmax=3 then the color bar should use the full range of cmap like in this image:
which is produced without giving the vmin, vmax and levels arguments. So... what am I missing here?
EDIT 1
In response to tom10 & tcaswell. I would have expected it to be as you say, but... unfortunately it's not. Take a look at this:
plt.contourf([[.12, .2], [.8, 3.2]], levels=[0, .1, .3, .5, 1, 3], cmap=cmap, vmin=0, vmax=3)
plt.colorbar()
with:
Maybe to clarify this a bit: say I have data and the important features of it are around 0.1, but there are some around 3 let's say. So I give it a levels=[0, 0.005, 0.075, 0.1, 0.125, 0.15, 0.2, 1, 2.5, 2.75, 3, 3.25] and vmin=0, vmax=3.25. Now I would expect to see the full range of colors, but instead all of the important data-points 0.005 to 0.125 end up in the blue region (by using the standard plt.cm.jet color map). What I'm saying I guess is... if I give levels=[0, 1, 2, 3], vmin=0, vmax=3 for some data that goes from 0 to 3 I expect to see all the colors in the given color map, but if I give levels=[0, 0.9, 0.1, 0.11, 1, 3], vmi=0, vmax=3 I would expect the same, to see all the colors in the given color map, except mapped to the right intervals, instead I see the bunch of blues coloring the 0-0.11 region and some green / yellow coloring the other part of the region. Hope this makes it... a bit clear.
EDIT 2
The same happens even if I don't give any norm or vmin, vmax.
EDIT 3
Referring to tcaswell's comment, behaving the way it is... for me at least is counter-intuitive. I expected that the color would be independent of the data-points in a way. I would expect that the full range of colors from the colormap would be used all the time (except when the vmin, vmax are larger/smaller than the levels min, max values). In other words, looking at this code I did a while back (Python 3):
import matplotlib.colors as mc
def addNorm(cmapData):
cmapData['norm'] = mc.BoundaryNorm(cmapData['bounds'], cmapData['cmap'].N)
return True
def discretize(cmap, bounds):
resCmap = {}
resCmap['cmap'] = mc.ListedColormap( \
[cmap(i/len(bounds[1:])) for i in range(len(bounds[1:]))]
)
resCmap['bounds'] = bounds
addNorm(resCmap)
return resCmap
then use it as:
levels = [0, .1, .3, .5, 1, 3]
cmapData = discretize(plt.cm.jet, bounds=levels)
plt.contourf([[.12, .2], [.8, 3.2]], levels=levels, cmap=cmapData['cmap'], norm=cmapData['norm'])
plt.colorbar()
which gives the plot where you can actually distinguish the features (0.1-0.5), i.e. they are no longer in the blue region by using the above method with plt.cm.jet:
I mean, I know I solved this, and a while back too... but my question I guess is... how come the default in matplotlib is not this? I would have expected it to be this way... or maybe is it just a configuration / argument / something to enable this by default that I'm missing?
After playing around a bit it seems that the answer to this question is way easier than I ever thought. Just some explanation first. While reading the documentation on the normalizing classes from matplotlib.colors I figured... well, matplotlib.colors.BoundaryNorm should be used here! but something is wrong as you can see in the following example:
import matplotlib.pyplot as plt
import matplotlib.colors as mc
levels = [0, .1, .3, .5, 1, 3]
norm = mc.BoundaryNorm(levels, len(levels)-1)
plt.contourf([[.12, .2], [.8, 2]], levels=levels, norm=norm)
plt.colorbar()
plt.show()
which gives this:
and this is obviously something we don't want! And I was thinking... why would you have to give to the constructor of BoundaryNorm the number of colors to use?... Shouldn't BoundaryNorm use the full extent of the colormap? And then it struck me, with just a little change to the code above:
# use here 256 instead of len(levels)-1 becuase
# as it's mentioned in the documentation for the
# colormaps, the default colormaps use 256 colors in their
# definition: print(plt.cm.jet.N) for example
norm = mc.BoundaryNorm(levels, 256)
and we get:
which is exactly what we want!
Or you we can do:
cmap = # user define cmap
norm = mc.BoundaryNorm(levels, cmap.N)
# which is I guess a little bit more programatically (is this a word?!) correct
The color of the filled region is picked by mid point of the two lines it is filling between (iirc). The yellow you are seeing is the mapping of 2 under the color map and limits you set.
If you want to map the color by region index, do a bit of monkey patching:
def _process_colors_by_index(self):
"""
Color argument processing for contouring.
The color is based in the index in the level set, not
the actual value of the level.
"""
self.monochrome = self.cmap.monochrome
if self.colors is not None:
# Generate integers for direct indexing.
i0, i1 = 0, len(self.levels)
if self.filled:
i1 -= 1
# Out of range indices for over and under:
if self.extend in ('both', 'min'):
i0 = -1
if self.extend in ('both', 'max'):
i1 += 1
self.cvalues = list(range(i0, i1))
self.set_norm(colors.NoNorm())
else:
self.cvalues = range(len(self.levels))
self.set_array(range(len(self.levels)))
self.autoscale_None()
if self.extend in ('both', 'max', 'min'):
self.norm.clip = False
# self.tcolors are set by the "changed" method
orig = matplotlib.contour.ContourSet._process_colors
matplotlib.contour.ContourSet._process_colors = _process_colors_by_index
cmap = plt.cm.jet
figure()
out = plt.contourf([[.12, .2], [.8, 2]], levels=[0, .1, .3, .5, 1, 3], cmap=cmap)
plt.colorbar()
# fix what we have done
matplotlib.contour.ContourSet._process_colors = orig
You can probably do better and remove the shift by 1/2 as well.
You can also reach in and just change the color of existing contours. It looks like you need to change the values of out.cvalues and then call out.changed() on the object.
A less destructive version would be to write a custom norm by sub-classing matplotlib.colors.Normalize, see colors.py for a template.
The maximum value of your data is 2. In the plot in question you set vmax=3.
In more detail, vmax sets the range of colors used in the mapping. Since this is much bigger than your data range, when you plot the data, you don't see the full range of colors. This is further confused by the small number of levels that you chose, which isn't showing you all the colors that are available, since the colorbar only shows a single color for the whole 1 to 3 range, again, obscuring colors available beyond 2.
Actually I think the best solution yet is located at this place:
http://protracted-matter.blogspot.ie/2012/08/nonlinear-colormap-in-matplotlib.html
It defines this little class which solves all the problems:
class nlcmap(mc.LinearSegmentedColormap):
"""A nonlinear colormap"""
name = 'nlcmap'
def __init__(self, cmap, levels):
self.cmap = cmap
# #MRR: Need to add N for backend
self.N = cmap.N
self.monochrome = self.cmap.monochrome
self.levels = np.asarray(levels, dtype='float64')
self._x = self.levels / self.levels.max()
self._y = np.linspace(0.0, 1.0, len(self.levels))
##MRR Need to add **kw for 'bytes'
def __call__(self, xi, alpha=1.0, **kw):
yi = np.interp(xi, self._x, self._y)
return self.cmap(yi, alpha)
The script was originally developed by a guy named Robert Hetland. All the details are in the link above.
Related
I would like to create a Cityscapes_Palette_Map as a "colormap" for my semantic segmentation output.
The definition of color for each pixel value range from 0 to 22 is as shown in the link.
I see a lot of example of creating a "continuous" cmap but what I need is "discrete" cmap to map an int pixel value (class) to a specific color. I wonder if anyone can point me to the right reference to solve my problem. Thanks a lot.
Welcome to SO.
Matplotlib still doesn't have an easy way to map integers to colors.
Usually, the most straightforward way is to simply apply the mapping outside of matplotlib and then pass the color values to matplotlib.
import numpy as np
import matplotlib.pyplot as plt
n = 10
x = np.random.rand(n)
y = np.random.rand(n)
color_as_integer = np.random.randint(3, size=n)
colormap = {
0 : np.array([ 0, 0, 0, 255]), # unlabelled
1 : np.array([ 70, 70, 70, 255]), # building
2 : np.array([100, 40, 40, 255]), # fence
}
# matplotlib works with rbga values in the range 0-1
colormap = {k : v / 255. for k, v in colormap.items()}
color_as_rgb = np.array([colormap[ii] for ii in color_as_integer])
plt.scatter(x, y, s=100, c=color_as_rgb)
plt.show()
You can then use proxy artists to create a legend as outlined here.
The alternative is to use a combination of ListedColormap and BoundaryNorm to map integers to colors, as outlined in this answer.
In that case, you can also get a colorbar as outlined here (although making a proper legend is probably better in your case).
If you have a Colormap cmap, for example:
cmap = matplotlib.cm.get_cmap('Spectral')
How can you get a particular colour out of it between 0 and 1, where 0 is the first colour in the map and 1 is the last colour in the map?
Ideally, I would be able to get the middle colour in the map by doing:
>>> do_some_magic(cmap, 0.5) # Return an RGBA tuple
(0.1, 0.2, 0.3, 1.0)
You can do this with the code below, and the code in your question was actually very close to what you needed, all you have to do is call the cmap object you have.
import matplotlib
cmap = matplotlib.cm.get_cmap('Spectral')
rgba = cmap(0.5)
print(rgba) # (0.99807766255210428, 0.99923106502084169, 0.74602077638401709, 1.0)
For values outside of the range [0.0, 1.0] it will return the under and over colour (respectively). This, by default, is the minimum and maximum colour within the range (so 0.0 and 1.0). This default can be changed with cmap.set_under() and cmap.set_over().
For "special" numbers such as np.nan and np.inf the default is to use the 0.0 value, this can be changed using cmap.set_bad() similarly to under and over as above.
Finally it may be necessary for you to normalize your data such that it conforms to the range [0.0, 1.0]. This can be done using matplotlib.colors.Normalize simply as shown in the small example below where the arguments vmin and vmax describe what numbers should be mapped to 0.0 and 1.0 respectively.
import matplotlib
norm = matplotlib.colors.Normalize(vmin=10.0, vmax=20.0)
print(norm(15.0)) # 0.5
A logarithmic normaliser (matplotlib.colors.LogNorm) is also available for data ranges with a large range of values.
(Thanks to both Joe Kington and tcaswell for suggestions on how to improve the answer.)
In order to get rgba integer value instead of float value, we can do
rgba = cmap(0.5,bytes=True)
So to simplify the code based on answer from Ffisegydd, the code would be like this:
#import colormap
from matplotlib import cm
#normalize item number values to colormap
norm = matplotlib.colors.Normalize(vmin=0, vmax=1000)
#colormap possible values = viridis, jet, spectral
rgba_color = cm.jet(norm(400),bytes=True)
#400 is one of value between 0 and 1000
I once ran into a similar situation where I needed "n" no. of colors from a colormap so that I can assign each color to my data.
I have compiled a code to this in a package called "mycolorpy".
You can pip install it using:
pip install mycolorpy
You can then do:
from mycolorpy import colorlist as mcp
import numpy as np
Example: To create a list of 5 hex strings from cmap "winter"
color1=mcp.gen_color(cmap="winter",n=5)
print(color1)
Output:
['#0000ff', '#0040df', '#0080bf', '#00c09f', '#00ff80']
Another example to generate 16 list of colors from cmap bwr:
color2=mcp.gen_color(cmap="bwr",n=16)
print(color2)
Output:
['#0000ff', '#2222ff', '#4444ff', '#6666ff', '#8888ff', '#aaaaff', '#ccccff', '#eeeeff', '#ffeeee', '#ffcccc', '#ffaaaa', '#ff8888', '#ff6666', '#ff4444', '#ff2222', '#ff0000']
There is a python notebook with usage examples to better visualize this.
Say you want to generate a list of colors from a cmap that is normalized to a given data. You can do that using:
a=random.randint(1000, size=(200))
a=np.array(a)
color1=mcp.gen_color_normalized(cmap="seismic",data_arr=a)
plt.scatter(a,a,c=color1)
Output:
You can also reverse the color using:
color1=mcp.gen_color_normalized(cmap="seismic",data_arr=a,reverse=True)
plt.scatter(a,a,c=color1)
Output:
I had precisely this problem, but I needed sequential plots to have highly contrasting color. I was also doing plots with a common sub-plot containing reference data, so I wanted the color sequence to be consistently repeatable.
I initially tried simply generating colors randomly, reseeding the RNG before each plot. This worked OK (commented-out in code below), but could generate nearly indistinguishable colors. I wanted highly contrasting colors, ideally sampled from a colormap containing all colors.
I could have as many as 31 data series in a single plot, so I chopped the colormap into that many steps. Then I walked the steps in an order that ensured I wouldn't return to the neighborhood of a given color very soon.
My data is in a highly irregular time series, so I wanted to see the points and the lines, with the point having the 'opposite' color of the line.
Given all the above, it was easiest to generate a dictionary with the relevant parameters for plotting the individual series, then expand it as part of the call.
Here's my code. Perhaps not pretty, but functional.
from matplotlib import cm
cmap = cm.get_cmap('gist_rainbow') #('hsv') #('nipy_spectral')
max_colors = 31 # Constant, max mumber of series in any plot. Ideally prime.
color_number = 0 # Variable, incremented for each series.
def restart_colors():
global color_number
color_number = 0
#np.random.seed(1)
def next_color():
global color_number
color_number += 1
#color = tuple(np.random.uniform(0.0, 0.5, 3))
color = cmap( ((5 * color_number) % max_colors) / max_colors )
return color
def plot_args(): # Invoked for each plot in a series as: '**(plot_args())'
mkr = next_color()
clr = (1 - mkr[0], 1 - mkr[1], 1 - mkr[2], mkr[3]) # Give line inverse of marker color
return {
"marker": "o",
"color": clr,
"mfc": mkr,
"mec": mkr,
"markersize": 0.5,
"linewidth": 1,
}
My context is JupyterLab and Pandas, so here's sample plot code:
restart_colors() # Repeatable color sequence for every plot
fig, axs = plt.subplots(figsize=(15, 8))
plt.title("%s + T-meter"%name)
# Plot reference temperatures:
axs.set_ylabel("°C", rotation=0)
for s in ["T1", "T2", "T3", "T4"]:
df_tmeter.plot(ax=axs, x="Timestamp", y=s, label="T-meter:%s" % s, **(plot_args()))
# Other series gets their own axis labels
ax2 = axs.twinx()
ax2.set_ylabel(units)
for c in df_uptime_sensors:
df_uptime[df_uptime["UUID"] == c].plot(
ax=ax2, x="Timestamp", y=units, label="%s - %s" % (units, c), **(plot_args())
)
fig.tight_layout()
plt.show()
The resulting plot may not be the best example, but it becomes more relevant when interactively zoomed in.
To build on the solutions from Ffisegydd and amaliammr, here's an example where we make CSV representation for a custom colormap:
#! /usr/bin/env python3
import matplotlib
import numpy as np
vmin = 0.1
vmax = 1000
norm = matplotlib.colors.Normalize(np.log10(vmin), np.log10(vmax))
lognum = norm(np.log10([.5, 2., 10, 40, 150,1000]))
cdict = {
'red':
(
(0., 0, 0),
(lognum[0], 0, 0),
(lognum[1], 0, 0),
(lognum[2], 1, 1),
(lognum[3], 0.8, 0.8),
(lognum[4], .7, .7),
(lognum[5], .7, .7)
),
'green':
(
(0., .6, .6),
(lognum[0], 0.8, 0.8),
(lognum[1], 1, 1),
(lognum[2], 1, 1),
(lognum[3], 0, 0),
(lognum[4], 0, 0),
(lognum[5], 0, 0)
),
'blue':
(
(0., 0, 0),
(lognum[0], 0, 0),
(lognum[1], 0, 0),
(lognum[2], 0, 0),
(lognum[3], 0, 0),
(lognum[4], 0, 0),
(lognum[5], 1, 1)
)
}
mycmap = matplotlib.colors.LinearSegmentedColormap('my_colormap', cdict, 256)
norm = matplotlib.colors.LogNorm(vmin, vmax)
colors = {}
count = 0
step_size = 0.001
for value in np.arange(vmin, vmax+step_size, step_size):
count += 1
print("%d/%d %f%%" % (count, vmax*(1./step_size), 100.*count/(vmax*(1./step_size))))
rgba = mycmap(norm(value), bytes=True)
color = (rgba[0], rgba[1], rgba[2])
if color not in colors.values():
colors[value] = color
print ("value, red, green, blue")
for value in sorted(colors.keys()):
rgb = colors[value]
print("%s, %s, %s, %s" % (value, rgb[0], rgb[1], rgb[2]))
Colormaps come with their own normalize method, so if you have a plot already made you can access the color at a certain value.
import matplotlib.pyplot as plt
import numpy as np
cmap = plt.cm.viridis
cm = plt.pcolormesh(np.random.randn(10, 10), cmap=cmap)
print(cmap(cm.norm(2.2)))
For a quick and dirty you can use the map directly.
Or you can just do what #amaliammr says.
data_size = 23 # range 0..23
colors = plt.cm.turbo
color_normal = colours.N/data_size
for i in range(data_size):
col = colours.colors[int(i*color_normal)]
I want to completely remove white space around my axes during active plot (not save_fig as others asked).
Here we cannot use bbox_inches='tight'. I can use tight_layout(pad=0).
When axis is on, it works fine, it shows all the ticks and x-y labels.
However, in some cases, I set the axis off. What I expected is to see the contents expand to fill up the empty space where the axes are. However, this does not work. It still keep the padding as there are still x-y labels and axes.
How can I remove the white space of invisible axes objects?
edit:
I am aware that I can use ax.set_yticks([]) and ax.set_xticks([]) to turn those off. But this is clumsy, I have to remember the the ticks before I clear them. And if I remove-then-add those ticks. The ticks cannot automatically update any more.
I wonder is there any more straightforward way to do this?
We can still see there is a small border spacing even after removing all ticks. If someone can come up a way to remove that too. It will be fantastic.
I would also like to keep the title if there is one. Thus the hard-coded ax.set_position([0,0,1,x]) is not very good for this usage. Surely we can still try to get the top spacing when there is a title, but if someone can provide a more direct/simple way to handle this, it will be preferred.
Example code:
def demo_tight_layout(w=10, h=6, axisoff=False, removeticks=False):
fig,ax = plt.subplots()
fig.set_facecolor((0.8, 0.8, 0.8))
rect = patches.Rectangle((-w/2, -h/2), w, h, color='#00ffff', alpha=0.5)
ax.add_patch(rect)
ax.plot([-w/2,w/2], [-h/2,h/2])
ax.plot([-w/2,w/2], [h/2,-h/2])
ax.set_ylabel("ylabel")
ax.margins(0)
_texts = []
if axisoff:
ax.set_axis_off()
_texts.append("axisoff")
if removeticks:
ax.set_xticks([])
ax.set_yticks([])
ax.set_ylabel("")
_texts.append("removeticks")
fig.text(0.5, 0.6, " ".join(_texts))
fig.tight_layout(pad=0)
plt.show()
return fig, ax, text
You may adjust the subplot parameters depending on whether you turned the axis off or not.
import matplotlib.pyplot as plt
from matplotlib import patches
def demo_tight_layout(w=10, h=6, axisoff=False):
fig,ax = plt.subplots()
fig.set_facecolor((0.8, 0.8, 0.8))
rect = patches.Rectangle((-w/2, -h/2), w, h, color='#00ffff', alpha=0.5)
ax.add_patch(rect)
ax.plot([-w/2,w/2], [-h/2,h/2])
ax.plot([-w/2,w/2], [h/2,-h/2])
ax.set_ylabel("ylabel")
ax.margins(0)
_texts = []
fig.tight_layout()
if axisoff:
ax.set_axis_off()
_texts.append("axisoff")
params = dict(bottom=0, left=0, right=1)
if ax.get_title() == "":
params.update(top=1)
fig.subplots_adjust(**params)
fig.text(0.5, 0.6, " ".join(_texts))
plt.show()
Now demo_tight_layout(axisoff=True) produces
and demo_tight_layout(axisoff=False) produces
You need to set the axes position to fill the figure. If you create your figure and plot with
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.gca()
ax.plot(some_x_data, some_y_data)
you need to add the following line to fill the figure with the axes:
ax.set_position([0, 0, 1, 1], which='both')
This sets the axes location relative to the figure size in the following way:
[left, bottom, width, height]
So to completely fill the figure use [0, 0, 1, 1] as shown above.
So taking your code, it should look like this (using fill_figure bool to check):
def demo_tight_layout(w=10, h=6, axisoff=False, removeticks=False, fill_figure=False):
fig,ax = plt.subplots()
fig.set_facecolor((0.8, 0.8, 0.8))
rect = patches.Rectangle((-w/2, -h/2), w, h, color='#00ffff', alpha=0.5)
ax.add_patch(rect)
ax.plot([-w/2,w/2], [-h/2,h/2])
ax.plot([-w/2,w/2], [h/2,-h/2])
ax.set_ylabel("ylabel")
ax.margins(0)
_texts = []
if axisoff:
ax.set_axis_off()
_texts.append("axisoff")
if removeticks:
ax.set_xticks([])
ax.set_yticks([])
ax.set_ylabel("")
_texts.append("removeticks")
fig.text(0.5, 0.6, " ".join(_texts))
fig.tight_layout(pad=0)
if fill_figure:
ax.set_position([0, 0, 1, 1], which='both')
plt.show()
return fig, ax, text
ax.set_position needs to be after fig.tight_layout.
If a figure title is needed, there is no direct way to do it. This unluckily can't be avoided. You need to adapt the height parameters manually so that the title fits in the figure, for example with:
ax.set_position([0, 0, 1, .9], which='both')
I have something like
import matplotlib.pyplot as plt
import numpy as np
a=[0.05, 0.1, 0.2, 1, 2, 3]
plt.hist((a*2, a*3), bins=[0, 0.1, 1, 10])
plt.gca().set_xscale("symlog", linthreshx=0.1)
plt.show()
which gives me the following plot:
As one can see, the bar width is not equal. In the linear part (from 0 to 0.1), everything is find, but after this, the bar width is still in linear scale, while the axis is in logarithmic scale, giving me uneven widths for bars and spaces in between (the tick is not in the middle of the bars).
Is there any way to correct this?
Inspired by https://stackoverflow.com/a/30555229/635387 I came up with the following solution:
import matplotlib.pyplot as plt
import numpy as np
d=[0.05, 0.1, 0.2, 1, 2, 3]
def LogHistPlot(data, bins):
totalWidth=0.8
colors=("b", "r", "g")
for i, d in enumerate(data):
heights = np.histogram(d, bins)[0]
width=1/len(data)*totalWidth
left=np.array(range(len(heights))) + i*width
plt.bar(left, heights, width, color=colors[i], label=i)
plt.xticks(range(len(bins)), bins)
plt.legend(loc='best')
LogHistPlot((d*2, d*3, d*4), [0, 0.1, 1, 10])
plt.show()
Which produces this plot:
The basic idea is to drop the plt.hist function, compute the histogram by numpy and plot it with plt.bar. Than, you can easily use a linear x-axis, which makes the bar width calculation trivial. Lastly, the ticks are replaced by the bin edges, resulting in the logarithmic scale. And you don't even have to deal with the symlog linear/logarithmic botchery anymore.
You could use histtype='stepfilled' if you are okay with a plot where the data sets are plotted one behind the other. Of course, you'll need to carefully choose colors with alpha values, so that all your data can still be seen...
a = [0.05, 0.1, 0.2, 1, 2, 3] * 2
b = [0.05, 0.05, 0.05, 0.15, 0.15, 2]
colors = [(0.2, 0.2, 0.9, 0.5), (0.9, 0.2, 0.2, 0.5)] # RGBA tuples
plt.hist((a, b), bins=[0, 0.1, 1, 10], histtype='stepfilled', color=colors)
plt.gca().set_xscale("symlog", linthreshx=0.1)
plt.show()
I've changed your data slightly for a better illustration. This gives me:
For some reason the overlap color seems to be going wrong (matplotlib 1.3.1 with Python 3.4.0; Is this a bug?), but it's one possible solution/alternative to your problem.
Okay, I found out the real problem: when you create the histogram with those bin-edge settings, the histogram creates bars which have equal size, and equal outside-spacing on the non-log scale.
To demonstrate, here's a zoomed-in version of the plot in the question, but in non-log scale:
Notice how the first two bars are centered around (0 + 0.1) / 2 = 0.05, with a gap of 0.1 / 10 = 0.01 at the edges, while the next two bars are centered around (0.1 + 1.0) / 2 = 0.55, with a gap of 1.1 / 10 = 0.11 at either edge.
When converting things to log scale, bar widths and edge widths all go for a huge toss. This is compounded further by the fact that you have a linear scale from 0 to 0.1, after which things become log-scale.
I know no way of fixing this, other than to do everything manually. I've used the geometric means of the bin-edges in order to compute what the bar edges and bar widths should be. Note that this piece of code will work only for two datasets. If you have more datasets, you'll need to have some function that fills in the bin-edges with a geometric series appropriately.
import numpy as np
import matplotlib.pyplot as plt
def geometric_means(a):
"""Return pairwise geometric means of adjacent elements."""
return np.sqrt(a[1:] * a[:-1])
a = [0.05, 0.1, 0.2, 1, 2, 3] * 2
b = [0.05, 0.1, 0.2, 1, 2, 3] * 3
# Find frequencies
bins = np.array([0, 0.1, 1, 10])
a_hist = np.histogram(a, bins=bins)[0]
b_hist = np.histogram(b, bins=bins)[0]
# Find log-scale mid-points for bar-edges
mid_vals = np.hstack((np.array([0.05,]), geometric_means(bins[1:])))
# Compute bar left-edges, and bar widths
a_x = np.empty(mid_vals.size * 2)
a_x = bins[:-1]
a_widths = mid_vals - bins[:-1]
b_x = np.empty(mid_vals.size * 2)
b_x = mid_vals
b_widths = bins[1:] - mid_vals
plt.bar(a_x, a_hist, width=a_widths, color='b')
plt.bar(b_x, b_hist, width=b_widths, color='g')
plt.gca().set_xscale("symlog", linthreshx=0.1)
plt.show()
And the final result:
Sorry, but the neat gaps between the bars get killed. Again, this can be fixed by doing the appropriate geometric interpolation, so that everything is linear on log-scale.
Just in case someone stumbles upon this problem:
This solution looks much more like the way it should be
plotting a histogram on a Log scale with Matplotlib
The following screenshot shows my x-axis.
I added some labels and rotated them by 90 degrees in order to better read them. However, pyplot truncates the bottom such that I'm not able to completely read the labels.
How do I extend the bottom margin in order to see the complete labels?
Two retroactive ways:
fig, ax = plt.subplots()
# ...
fig.tight_layout()
Or
fig.subplots_adjust(bottom=0.2) # or whatever
Here's a subplots_adjust example: http://matplotlib.org/examples/pylab_examples/subplots_adjust.html
(but I prefer tight_layout)
A quick one-line solution that has worked for me is to use pyplot's auto tight_layout method directly, available in Matplotlib v1.1 onwards:
plt.tight_layout()
This can be invoked immediately before you show the plot (plt.show()), but after your manipulations on the axes (e.g. ticklabel rotations, etc).
This convenience method avoids manipulating individual figures of subplots.
Where plt is the standard pyplot from:
import matplotlib.pyplot as plt
fig.savefig('name.png', bbox_inches='tight')
works best for me, since it doesn't reduce the plot size compared to
fig.tight_layout()
Subplot-adjust did not work for me, since the whole figure would just resize with the labels still out of bounds.
A workaround I found was to keep the y-axis always a certain margin over the highest or minimum y-values:
x1,x2,y1,y2 = plt.axis()
plt.axis((x1,x2,y1 - 100 ,y2 + 100))
fig, ax = plt.subplots(tight_layout=True)
This is rather complicated, but it gives a general and neat solution.
import numpy as np
value1 = 3
xvalues = [0, 1, 2, 3, 4]
line1 = [2.0, 3.0, 2.0, 5.0, 4.0]
stdev1 = [0.1, 0.2, 0.1, 0.4, 0.3]
line2 = [1.7, 3.1, 2.5, 4.8, 4.2]
stdev2 = [0.12, 0.18, 0.12, 0.3, 0.35]
max_times = [max(line1+stdev1),max(line2+stdev2)]
min_times = [min(line1+stdev1),min(line2+stdev2)]
font_size = 25
max_total = max(max_times)
min_total = min(min_times)
max_minus_min = max_total - min_total
step_size = max_minus_min/10
head_space = (step_size*3)
plt.figure(figsize=(15, 15))
plt.errorbar(xvalues, line1, yerr=stdev1, fmt='', color='b')
plt.errorbar(xvalues, line2, yerr=stdev2, fmt='', color='r')
plt.xlabel("xvalues", fontsize=font_size)
plt.ylabel("lines 1 and 2 Test "+str(value1), fontsize=font_size)
plt.title("Let's leave space for the legend Experiment"+ str(value1), fontsize=font_size)
plt.legend(("Line1", "Line2"), loc="upper left", fontsize=font_size)
plt.tick_params(labelsize=font_size)
plt.yticks(np.arange(min_total, max_total+head_space, step=step_size) )
plt.grid()
plt.tight_layout()
Result: