I using matplotlib to plot some data in python and the plots require a standard colour bar. The data consists of a series of NxM matrices containing frequency information so that a simple imshow() plot gives a 2D histogram with colour describing frequency. Each matrix contains data in different, but overlapping ranges. Imshow normalizes the data in each matrix to the range 0-1 which means that, for example, the plot of matrix A, will appear identical to the plot of the matrix 2*A (though the colour bar will show double the values). What I would like is for the colour red, for example, to correspond to the same frequency in all of the plots. In other words, a single colour bar would suffice for all the plots. Any suggestions would be greatly appreciated.
Not to steal #ianilis's answer, but I wanted to add an example...
There are multiple ways, but the simplest is just to specify the vmin and vmax kwargs to imshow. Alternately, you can make a matplotlib.cm.Colormap instance and specify it, but that's more complicated than necessary for simple cases.
Here's a quick example with a single colorbar for all images:
import numpy as np
import matplotlib.pyplot as plt
# Generate some data that where each slice has a different range
# (The overall range is from 0 to 2)
data = np.random.random((4,10,10))
data *= np.array([0.5, 1.0, 1.5, 2.0])[:,None,None]
# Plot each slice as an independent subplot
fig, axes = plt.subplots(nrows=2, ncols=2)
for dat, ax in zip(data, axes.flat):
# The vmin and vmax arguments specify the color limits
im = ax.imshow(dat, vmin=0, vmax=2)
# Make an axis for the colorbar on the right side
cax = fig.add_axes([0.9, 0.1, 0.03, 0.8])
fig.colorbar(im, cax=cax)
plt.show()
Easiest solution is to call clim(lower_limit, upper_limit) with the same arguments for each plot.
This only answer half of the question, or rather starts a new one.
If you change
data *= np.array([0.5, 1.0, 1.5, 2.0])[:,None,None]
to
data *= np.array([2.0, 1.0, 1.5, 0.5])[:,None,None]
your colorbar will go from 0 to 0.5 which in this case is dark blue to slightly lighter blue and will not cover the whole range (0 to 2).
The colorbar will only show the colors from the last image or contour regardless of vmin and vmax.
I wasn't happy with the solutions that suggested to manually set vmin and vmax, so I decided to read the limits of each plot and automatically set vmin and vmax.
The example below shows three plots of samples taken from normal distributions with increasing mean value.
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1 import ImageGrid
import numpy as np
numberOfPlots = 3
data = []
for i in range(numberOfPlots):
mean = i
data.append(np.random.normal(mean, size=(100,100)))
fig = plt.figure()
grid = ImageGrid(fig, 111, nrows_ncols=(1,numberOfPlots), cbar_mode='single')
ims = []
for i in range(numberOfPlots):
ims.append(grid[i].imshow(data[i]))
grid[i].set_title("Mean = " + str(i))
clims = [im.get_clim() for im in ims]
vmin = min([clim[0] for clim in clims])
vmax = max([clim[1] for clim in clims])
for im in ims:
im.set_clim(vmin=np.floor(vmin),vmax=np.ceil(vmax))
grid[0].cax.colorbar(ims[0]) # with cbar_mode="single", cax attribute of all axes are identical
fig.show()
Related
How do I use colorbar attributes such as in this snippet:
import seaborn as sns
uniform_data = np.random.rand(10, 12) # random data
ax = sns.heatmap(uniform_data)
cbar = ax.collections[0].colorbar
plt.show()
To shrink the colorbar and put it to the bottom and anchored to the lower left corner (that is, NOT centered)?
Something like this, but with the colorbar shrunk to, let's say 70% and anchored to the bottom left
I am unsure how to search for the methods as cbar.set_location() is not available.
If you want infinite customizability, you need to go more low level than you will get with seaborn, which gives convenience, but can't have knobs for everything.
The most straightforward way to get what you want is to place the colorbar axes manually. Note that you will need to play with the y offset, which I set here to -0.2.
import matplotlib.pyplot as plt
import numpy as np
uniform_data = np.random.rand(10, 12) # random data
fig, ax = plt.subplots(layout='constrained')
pc = ax.imshow(uniform_data)
cbax = ax.inset_axes([0, -0.2, 0.7, 0.05], transform=ax.transAxes)
fig.colorbar(pc, ax=ax, cax=cbax, shrink=0.7, orientation='horizontal')
plt.show()
You could create the colorbar via seaborn, extract its position, adapt it and set it again:
from matplotlib import pyplot as plt
import seaborn as sns
import numpy as np
uniform_data = np.random.rand(10, 12)
ax = sns.heatmap(uniform_data, cmap='rocket_r', cbar_kws={'orientation': 'horizontal', 'ticks': np.linspace(0, 1, 6)})
cax = ax.collections[0].colorbar.ax # get the ax of the colorbar
pos = cax.get_position() # get the original position
cax.set_position([pos.x0, pos.y0, pos.width * 0.6, pos.height]) # set a new position
cax.set_frame_on(True)
cax.invert_xaxis() # invert the direction of the colorbar
for spine in cax.spines.values(): # show the colorbar frame again
spine.set(visible=True, lw=.8, edgecolor='black')
plt.show()
Note that you need cbar_kws={'orientation': 'horizontal'} for a horizontal colorbar that by default is aligned with the x-axis.
After using .set_position, something like plt.tight_layout() won't work anymore.
About your new questions:
cax.invert_xaxis() doesn't invert the colorbar direction
Yes it does. You seem to want to reverse the colormap. Matplotlib's convention is to append _r to the colormap name. In this case, seaborn is using the rocket colormap, rocket_r would be the reverse. Note that changing the ticks doesn't work the way you try it, as these are just numeric positions which will be sorted before they are applied.
If you want to show 0 and 1 in the colorbar (while the values in the heatmap are e.g. between 0.001 and 0.999, you could use vmin and vmax. E.g. sns.heatmap(..., vmin=0, vmax=1). vmin and vmax are one way to change the mapping between the values and the colors. By default, vmin=data.min() and vmax=data.max().
To show the colorbar outline: Add a black frame around a colorbar
ax.collections[0].colorbar is a colorbar, which in the latest versions also supports some functions to set ticks
ax.collections[0].colorbar.ax is an Axes object (a subplot). Matplotlib creates a small subplot on which the colorbar will be drawn. axs support a huge number of functions to change how the subplot looks or to add new elements. Note that a stackoverflow answer isn't meant to put of full matplotlib tutorial. The standard tutorials could be a starting point.
I have a problem with the colorbar for contour plots with user-defined levels. As you can see in the figure below, the color for the highest level (red) is not shown well in the colorbar. To solve this, I would like to change the range of the colorbar (e.g. from 0 to 1), while not changing the contour levels or range of the colormap.
So far, I have tried to manually set the ticks of the colorbar in the colorbar-call, but that doesn't change the range. (That is, ticks outside 0.3-0.8 will just not show up)
Minimum working example:
import numpy as np
import matplotlib.pyplot as plt
z = np.random.rand(10,10)
im = plt.contour(z, levels = [0.3,0.5,0.8], cmap="rainbow")
plt.colorbar(im)
I would like to change the range of the colorbar (e.g. from 0 to 1)
Save the colorbar object, change its boundaries and the ticks' position, finally update the plot to reflect our wishes.
import numpy as np
import matplotlib.pyplot as plt
z = np.random.rand(10,10)
im = plt.contour(z, levels = [0.3,0.5,0.8], cmap="rainbow")
cb = plt.colorbar(im)
cb.boundaries = np.array((0.0, 0.5, 1.0))
cb.set_ticks((0, 0.3, 0.5, 0.8, 1))
cb.update_normal(im)
I am quite new to python programming. I have a script with me that plots out a heat map using matplotlib. Range of X-axis value = (-180 to +180) and Y-axis value =(0 to 180). The 2D heatmap colours areas in Rainbow according to the number of points occuring in a specified area in the x-y graph (defined by the 'bin' (see below)).
In this case, x = values_Rot and y = values_Tilt (see below for code).
As of now, this script colours the 2D-heatmap in the linear scale. How do I change this script such that it colours the heatmap in the log scale? Please note that I only want to change the heatmap colouring scheme to log-scale, i.e. only the number of points in a specified area. The x and y-axis stay the same in linear scale (not in logscale).
A portion of the code is here.
rot_number = get_header_number(headers, AngleRot)
tilt_number = get_header_number(headers, AngleTilt)
psi_number = get_header_number(headers, AnglePsi)
values_Rot = []
values_Tilt = []
values_Psi = []
for line in data:
try:
values_Rot.append(float(line.split()[rot_number]))
values_Tilt.append(float(line.split()[tilt_number]))
values_Psi.append(float(line.split()[psi_number]))
except:
print ('This line didnt work, it may just be a blank space. The line is:' + line)
# Change the values here if you want to plot something else, such as psi.
# You can also change how the data is binned here.
plt.hist2d(values_Rot, values_Tilt, bins=25,)
plt.colorbar()
plt.show()
plt.savefig('name_of_output.png')
You can use a LogNorm for the colors, using plt.hist2d(...., norm=LogNorm()). Here is a comparison.
To have the ticks in base 2, the developers suggest adding the base to the LogLocator and the LogFormatter. As in this case the LogFormatter seems to write the numbers with one decimal (.0), a StrMethodFormatter can be used to show the number without decimals. Depending on the range of numbers, sometimes the minor ticks (shorter marker lines) also get a string, which can be suppressed assigning a NullFormatter for the minor colorbar ticks.
Note that base 2 and base 10 define exactly the same color transformation. The position and the labels of the ticks are different. The example below creates two colorbars to demonstrate the different look.
import matplotlib.pyplot as plt
from matplotlib.ticker import NullFormatter, StrMethodFormatter, LogLocator
from matplotlib.colors import LogNorm
import numpy as np
from copy import copy
# create some toy data for a standalone example
values_Rot = np.random.randn(100, 10).cumsum(axis=1).ravel()
values_Tilt = np.random.randn(100, 10).cumsum(axis=1).ravel()
fig, (ax1, ax2) = plt.subplots(ncols=2, figsize=(15, 4))
cmap = copy(plt.get_cmap('hot'))
cmap.set_bad(cmap(0))
_, _, _, img1 = ax1.hist2d(values_Rot, values_Tilt, bins=40, cmap='hot')
ax1.set_title('Linear norm for the colors')
fig.colorbar(img1, ax=ax1)
_, _, _, img2 = ax2.hist2d(values_Rot, values_Tilt, bins=40, cmap=cmap, norm=LogNorm())
ax2.set_title('Logarithmic norm for the colors')
fig.colorbar(img2, ax=ax2) # default log 10 colorbar
cbar2 = fig.colorbar(img2, ax=ax2) # log 2 colorbar
cbar2.ax.yaxis.set_major_locator(LogLocator(base=2))
cbar2.ax.yaxis.set_major_formatter(StrMethodFormatter('{x:.0f}'))
cbar2.ax.yaxis.set_minor_formatter(NullFormatter())
plt.show()
Note that log(0) is minus infinity. Therefore, the zero values in the left plot (darkest color) are left empty (white background) on the plot with the logarithmic color values. If you just want to use the lowest color for these zeros, you need to set a 'bad' color. In order not the change a standard colormap, the latest matplotlib versions wants you to first make a copy of the colormap.
PS: When calling plt.savefig() it is important to call it before plt.show() because plt.show() clears the plot.
Also, try to avoid the 'jet' colormap, as it has a bright yellow region which is not at the extreme. It may look nice, but can be very misleading. This blog article contains a thorough explanation. The matplotlib documentation contains an overview of available colormaps.
Note that to compare two plots, plt.subplots() needs to be used, and instead of plt.hist2d, ax.hist2d is needed (see this post). Also, with two colorbars, the elements on which the colorbars are based need to be given as parameter. A minimal change to your code would look like:
from matplotlib.ticker import NullFormatter, StrMethodFormatter, LogLocator
from matplotlib.colors import LogNorm
from matplotlib import pyplot as plt
from copy import copy
# ...
# reading the data as before
cmap = copy(plt.get_cmap('magma'))
cmap.set_bad(cmap(0))
plt.hist2d(values_Rot, values_Tilt, bins=25, cmap=cmap, norm=LogNorm())
cbar = plt.colorbar()
cbar.ax.yaxis.set_major_locator(LogLocator(base=2))
cbar.ax.yaxis.set_major_formatter(StrMethodFormatter('{x:.0f}'))
cbar.ax.yaxis.set_minor_formatter(NullFormatter())
plt.savefig('name_of_output.png') # needs to be called prior to plt.show()
plt.show()
I am using matplotlib.pyplot.specgram and matplotlib.pyplot.pcolormesh to make spectrogram plots of a seismic signal.
Background information -The reason for using pcolormesh is that I need to do arithmitic on the spectragram data array and then replot the resulting spectrogram (for a three-component seismogram - east, north and vertical - I need to work out the horizontal spectral magnitude and divide the vertical spectra by the horizontal spectra). It is easier to do this using the spectrogram array data than on individual amplitude spectra
I have found that the plots of the spectrograms after doing my arithmetic have unexpected values. Upon further investigation it turns out that the spectrogram plot made using the pyplot.specgram method has different values compared to the spectrogram plot made using pyplot.pcolormesh and the returned data array from the pyplot.specgram method. Both plots/arrays should contain the same values, I cannot work out why they do not.
Example:
The plot of
plt.subplot(513)
PxN, freqsN, binsN, imN = plt.specgram(trN.data, NFFT = 20000, noverlap = 0, Fs = trN.stats.sampling_rate, detrend = 'mean', mode = 'magnitude')
plt.title('North')
plt.xlabel('Time [s]')
plt.ylabel('Frequency [Hz]')
plt.clim(0, 150)
plt.colorbar()
#np.savetxt('PxN.txt', PxN)
looks different to the plot of
plt.subplot(514)
plt.pcolormesh(binsZ, freqsZ, PxN)
plt.clim(0,150)
plt.colorbar()
even though the "PxN" data array (that is, the spectrogram data values for each segment) is generated by the first method and re-used in the second.
Is anyone aware why this is happening?
P.S. I realise that my value for NFFT is not a square number, but it's not important at this stage of my coding.
P.P.S. I am not aware of what the "imN" array (fourth returned variable from pyplot.specgram) is and what it is used for....
First off, let's show an example of what you're describing so that other folks
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(1)
# Brownian noise sequence
x = np.random.normal(0, 1, 10000).cumsum()
fig, (ax1, ax2) = plt.subplots(nrows=2, figsize=(8, 10))
values, ybins, xbins, im = ax1.specgram(x, cmap='gist_earth')
ax1.set(title='Specgram')
fig.colorbar(im, ax=ax1)
mesh = ax2.pcolormesh(xbins, ybins, values, cmap='gist_earth')
ax2.axis('tight')
ax2.set(title='Raw Plot of Returned Values')
fig.colorbar(mesh, ax=ax2)
plt.show()
Magnitude Differences
You'll immediately notice the difference in magnitude of the plotted values.
By default, plt.specgram doesn't plot the "raw" values it returns. Instead, it scales them to decibels (in other words, it plots the 10 * log10 of the amplitudes). If you'd like it not to scale things, you'll need to specify scale="linear". However, for looking at frequency composition, a log scale is going to make the most sense.
With that in mind, let's mimic what specgram does:
plotted = 10 * np.log10(values)
fig, ax = plt.subplots()
mesh = ax.pcolormesh(xbins, ybins, plotted, cmap='gist_earth')
ax.axis('tight')
ax.set(title='Plot of $10 * log_{10}(values)$')
fig.colorbar(mesh)
plt.show()
Using a Log Color Scale Instead
Alternatively, we could use a log norm on the image and get a similar result, but communicate that the color values are on a log scale more clearly:
from matplotlib.colors import LogNorm
fig, ax = plt.subplots()
mesh = ax.pcolormesh(xbins, ybins, values, cmap='gist_earth', norm=LogNorm())
ax.axis('tight')
ax.set(title='Log Normalized Plot of Values')
fig.colorbar(mesh)
plt.show()
imshow vs pcolormesh
Finally, note that the examples we've shown have had no interpolation applied, while the original specgram plot did. specgram uses imshow, while we've been plotting with pcolormesh. In this case (regular grid spacing) we can use either.
Both imshow and pcolormesh are very good options, in this case. However,imshow will have significantly better performance if you're working with a large array. Therefore, you might consider using it instead, even if you don't want interpolation (e.g. interpolation='nearest' to turn off interpolation).
As an example:
extent = [xbins.min(), xbins.max(), ybins.min(), ybins.max()]
fig, ax = plt.subplots()
mesh = ax.imshow(values, extent=extent, origin='lower', aspect='auto',
cmap='gist_earth', norm=LogNorm())
ax.axis('tight')
ax.set(title='Log Normalized Plot of Values')
fig.colorbar(mesh)
plt.show()
I am using matplotlib.pyplot.specgram and matplotlib.pyplot.pcolormesh to make spectrogram plots of a seismic signal.
Background information -The reason for using pcolormesh is that I need to do arithmitic on the spectragram data array and then replot the resulting spectrogram (for a three-component seismogram - east, north and vertical - I need to work out the horizontal spectral magnitude and divide the vertical spectra by the horizontal spectra). It is easier to do this using the spectrogram array data than on individual amplitude spectra
I have found that the plots of the spectrograms after doing my arithmetic have unexpected values. Upon further investigation it turns out that the spectrogram plot made using the pyplot.specgram method has different values compared to the spectrogram plot made using pyplot.pcolormesh and the returned data array from the pyplot.specgram method. Both plots/arrays should contain the same values, I cannot work out why they do not.
Example:
The plot of
plt.subplot(513)
PxN, freqsN, binsN, imN = plt.specgram(trN.data, NFFT = 20000, noverlap = 0, Fs = trN.stats.sampling_rate, detrend = 'mean', mode = 'magnitude')
plt.title('North')
plt.xlabel('Time [s]')
plt.ylabel('Frequency [Hz]')
plt.clim(0, 150)
plt.colorbar()
#np.savetxt('PxN.txt', PxN)
looks different to the plot of
plt.subplot(514)
plt.pcolormesh(binsZ, freqsZ, PxN)
plt.clim(0,150)
plt.colorbar()
even though the "PxN" data array (that is, the spectrogram data values for each segment) is generated by the first method and re-used in the second.
Is anyone aware why this is happening?
P.S. I realise that my value for NFFT is not a square number, but it's not important at this stage of my coding.
P.P.S. I am not aware of what the "imN" array (fourth returned variable from pyplot.specgram) is and what it is used for....
First off, let's show an example of what you're describing so that other folks
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(1)
# Brownian noise sequence
x = np.random.normal(0, 1, 10000).cumsum()
fig, (ax1, ax2) = plt.subplots(nrows=2, figsize=(8, 10))
values, ybins, xbins, im = ax1.specgram(x, cmap='gist_earth')
ax1.set(title='Specgram')
fig.colorbar(im, ax=ax1)
mesh = ax2.pcolormesh(xbins, ybins, values, cmap='gist_earth')
ax2.axis('tight')
ax2.set(title='Raw Plot of Returned Values')
fig.colorbar(mesh, ax=ax2)
plt.show()
Magnitude Differences
You'll immediately notice the difference in magnitude of the plotted values.
By default, plt.specgram doesn't plot the "raw" values it returns. Instead, it scales them to decibels (in other words, it plots the 10 * log10 of the amplitudes). If you'd like it not to scale things, you'll need to specify scale="linear". However, for looking at frequency composition, a log scale is going to make the most sense.
With that in mind, let's mimic what specgram does:
plotted = 10 * np.log10(values)
fig, ax = plt.subplots()
mesh = ax.pcolormesh(xbins, ybins, plotted, cmap='gist_earth')
ax.axis('tight')
ax.set(title='Plot of $10 * log_{10}(values)$')
fig.colorbar(mesh)
plt.show()
Using a Log Color Scale Instead
Alternatively, we could use a log norm on the image and get a similar result, but communicate that the color values are on a log scale more clearly:
from matplotlib.colors import LogNorm
fig, ax = plt.subplots()
mesh = ax.pcolormesh(xbins, ybins, values, cmap='gist_earth', norm=LogNorm())
ax.axis('tight')
ax.set(title='Log Normalized Plot of Values')
fig.colorbar(mesh)
plt.show()
imshow vs pcolormesh
Finally, note that the examples we've shown have had no interpolation applied, while the original specgram plot did. specgram uses imshow, while we've been plotting with pcolormesh. In this case (regular grid spacing) we can use either.
Both imshow and pcolormesh are very good options, in this case. However,imshow will have significantly better performance if you're working with a large array. Therefore, you might consider using it instead, even if you don't want interpolation (e.g. interpolation='nearest' to turn off interpolation).
As an example:
extent = [xbins.min(), xbins.max(), ybins.min(), ybins.max()]
fig, ax = plt.subplots()
mesh = ax.imshow(values, extent=extent, origin='lower', aspect='auto',
cmap='gist_earth', norm=LogNorm())
ax.axis('tight')
ax.set(title='Log Normalized Plot of Values')
fig.colorbar(mesh)
plt.show()