The ProPlot Python package adds additional features to the Matplotlib library, including colourmap manipulations. One feature that is particularly attractive to me is the ability to rotate/shift colourmaps. To give you an example:
import proplot as pplot
import matplotlib.pyplot as plt
import numpy as np
state = np.random.RandomState(51423)
data = state.rand(30, 30).cumsum(axis=1)
fig, axes = plt.subplots(ncols=3, figsize=(9, 4))
fig.patch.set_facecolor("white")
axes[0].pcolormesh(data, cmap="Blues")
axes[0].set_title("Blues")
axes[1].pcolormesh(data, cmap="Blues_r")
axes[1].set_title("Reversed Blues")
axes[2].pcolormesh(data, cmap="Blues_s")
axes[2].set_title("Rotated Blues")
plt.tight_layout()
plt.show()
In the third column, you see the 180° rotated version of Blues. Currently ProPlot suffers from a bug that doesn't allow the user to revert the plotting style to Matplotlib's default style, so I was wondering if there was an easy way to rotate a colourmap in Matplotlib without resorting to ProPlot. I always found cmap manipulations in Matplotlib a bit arcane, so any help would be much appreciated.
If what you are trying to do is shift the colormaps, this can be done (relatively) easily:
def shift_cmap(cmap, frac):
"""Shifts a colormap by a certain fraction.
Keyword arguments:
cmap -- the colormap to be shifted. Can be a colormap name or a Colormap object
frac -- the fraction of the colorbar by which to shift (must be between 0 and 1)
"""
N=256
if isinstance(cmap, str):
cmap = plt.get_cmap(cmap)
n = cmap.name
x = np.linspace(0,1,N)
out = np.roll(x, int(N*frac))
new_cmap = matplotlib.colors.LinearSegmentedColormap.from_list(f'{n}_s', cmap(out))
return new_cmap
demonstration:
x = np.linspace(0,1,100)
x = np.vstack([x,x])
cmap1 = plt.get_cmap('Blues')
cmap2 = shift_cmap(cmap1, 0.25)
fig, (ax1, ax2) = plt.subplots(2,1)
ax1.imshow(x, aspect='auto', cmap=cmap1)
ax2.imshow(x, aspect='auto', cmap=cmap2)
To reverse a ListedColormap, there is a built-in reversed() but for the intended rotation, we have to create our own function.
#fake data generation
import numpy as np
np.random.seed(123)
#numpy array containing x, y, and color
arr = np.random.random(30).reshape(3, 10)
from matplotlib import pyplot as plt
from matplotlib.colors import ListedColormap
def rotate_cm(co_map, deg=180):
#define a function where the colormap is rotated by a certain degree
#180° shifts by 50%, 360° no change
n = co_map.N
#if rotating in the opposite direction feels more intuitive, reverse the sign here
deg = -deg%360
if deg < 0:
deg += 360
cutpoint = n * deg // 360
new_col_arr = [co_map(i) for i in range(cutpoint, n)] + [co_map(i) for i in range(cutpoint)]
return ListedColormap(new_col_arr)
fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(21,7))
#any listed colormap
my_cm = plt.cm.get_cmap("inferno")
#normal color map
cb1 = ax1.scatter(*arr[:2,:], c=arr[2,:], cmap=my_cm, marker="o")
plt.colorbar(cb1, ax=ax1)
ax1.set_title("regular colormap")
#reversed colormap
cb2 = ax2.scatter(*arr[:2,:], c=arr[2,:], cmap=my_cm.reversed(), marker="o")
plt.colorbar(cb2, ax=ax2)
ax2.set_title("reversed colormap")
#rotated colormap
cb3 = ax3.scatter(*arr[:2,:], c=arr[2,:], cmap=rotate_cm(my_cm, 90), marker="o")
#you can also combine the rotation with reversed()
#cb3 = ax3.scatter(*arr[:2,:], c=arr[2,:], cmap=rotate_cm(my_cm, 90).reversed(), marker="o")
plt.colorbar(cb3, ax=ax3)
ax3.set_title("colormap rotated by 90°")
plt.show()
Sample output:
Related
I'm trying to plot a series of frequency spectra in a 3D space using PolyCollection. My goal is to set "facecolors" as a gradient, i.e., the higher the magnitude, the lighter the color.
Please see this image for reference (I am not looking for the fancy design, just the gradients).
I tried to use the cmap argument of the PollyCollection, but I was unsuccessful.
I came this far with the following code adapted from here:
import matplotlib.pyplot as plt
from matplotlib.collections import PolyCollection
from mpl_toolkits.mplot3d import axes3d
import numpy as np
from scipy.ndimage import gaussian_filter1d
def plot_poly(magnitudes):
freq_data = np.arange(magnitudes.shape[0])[:,None]*np.ones(magnitudes.shape[1])[None,:]
mag_data = magnitudes
rad_data = np.linspace(1,magnitudes.shape[1],magnitudes.shape[1])
verts = []
for irad in range(len(rad_data)):
xs = np.concatenate([[freq_data[0,irad]], freq_data[:,irad], [freq_data[-1,irad]]])
ys = np.concatenate([[0],mag_data[:,irad],[0]])
verts.append(list(zip(xs, ys)))
poly = PolyCollection(verts, edgecolor='white', linewidths=0.5, cmap='Greys')
poly.set_alpha(.7)
fig = plt.figure(figsize=(24, 16))
ax = fig.add_subplot(111, projection='3d', proj_type = 'ortho')
ax.add_collection3d(poly, zs=rad_data, zdir='y')
ax.set_xlim3d(freq_data.min(), freq_data.max())
ax.set_xlabel('Frequency')
ax.set_ylim3d(rad_data.min(), rad_data.max())
ax.set_ylabel('Measurement')
ax.set_zlabel('Magnitude')
# Remove gray panes and axis grid
ax.xaxis.pane.fill = False
ax.xaxis.pane.set_edgecolor('white')
ax.yaxis.pane.fill = False
ax.yaxis.pane.set_edgecolor('white')
ax.zaxis.pane.fill = False
ax.zaxis.pane.set_edgecolor('white')
ax.view_init(50,-60)
plt.show()
sample_data = np.random.rand(2205, 4)
sample_data = gaussian_filter1d(sample_data, sigma=10, axis=0) # Just to smoothe the curves
plot_poly(sample_data)
Besides the missing gradients I am happy with the output of the code above.
Lets say I have the following dataset:
import numpy as np
import matplotlib.pyplot as plt
x_bins = np.arange(10)
y_bins = np.arange(10)
z = np.random.random((9,9))
I can easily plot this data with
plt.pcolormesh(x_bins, y_bins, z, cmap = 'viridis)
However, let's say I now add some alpha value for each point:
a = np.random.random((9,9))
How can I change the alpha value of each box in the pcolormesh plot to match the corresponding value in array "a"?
The mesh created by pcolormesh can only have one alpha for the complete mesh. To set an individual alpha for each cell, the cells need to be created one by one as rectangles.
The code below shows the pcolormesh without alpha at the left, and the mesh of rectangles with alpha at the right. Note that on the spots where the rectangles touch, the semi-transparency causes some unequal overlap. This can be mitigated by not drawing the cell edge (edgecolor='none'), or by longer black lines to separate the cells.
The code below changes the x dimension so easier verify that x and y aren't mixed up. relim and autoscale are needed because with matplotlib's default behavior the x and y limits aren't changed by adding patches.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle, Patch
x_bins = np.arange(12)
y_bins = np.arange(10)
z = np.random.random((9, 11))
a = np.random.random((9, 11))
cmap = plt.get_cmap('inferno')
norm = plt.Normalize(z.min(), z.max())
fig, (ax1, ax2) = plt.subplots(ncols=2)
ax1.pcolormesh(x_bins, y_bins, z, cmap=cmap, norm=norm)
for i in range(len(x_bins) - 1):
for j in range(len(y_bins) - 1):
rect = Rectangle((x_bins[i], y_bins[j]), x_bins[i + 1] - x_bins[i], y_bins[j + 1] - y_bins[j],
facecolor=cmap(norm(z[j, i])), alpha=a[j, i], edgecolor='none')
ax2.add_patch(rect)
# ax2.vlines(x_bins, y_bins.min(), y_bins.max(), edgecolor='black')
# ax2.hlines(y_bins, x_bins.min(), x_bins.max(), edgecolor='black')
ax2.relim()
ax2.autoscale(enable=True, tight=True)
plt.show()
I have an patch collection that I'd like to display a color map for. Because of some manipulations I do on top of the colormap, it's not possible for me to define it using a matplotlib.colorbar instance. At least not as far as I can tell; doing so strips some manipulations I do with my colors that blank out patches lacking data:
cmap = matplotlib.cm.YlOrRd
colors = [cmap(n) if pd.notnull(n) else [1,1,1,1]
for n in plt.Normalize(0, 1)([nullity for _, nullity in squares])]
# Now we draw.
for i, ((min_x, max_x, min_y, max_y), _) in enumerate(squares):
square = shapely.geometry.Polygon([[min_x, min_y], [max_x, min_y],
[max_x, max_y], [min_x, max_y]])
ax0.add_patch(descartes.PolygonPatch(square, fc=colors[i],
ec='white', alpha=1, zorder=4))
So I define a matplotlib.colorbar.ColorbarBase instance instead, which works:
matplotlib.colorbar.ColorbarBase(ax1, cmap=cmap, orientation='vertical',
norm=matplotlib.colors.Normalize(vmin=0, vmax=1))
Which results in e.g.:
The problem I have is that I want to reduce the size of this colorbar (specifically, the shrink it down to a specific vertical size, say, 500 pixels), but I don't see any obvious way of doing this. If I had a colorbar instance, I could adjust this easily using its axis property arguments, but ColorbarBase lacks these.
For further reference:
The example my implementation is based on.
The source code in question (warning: lengthy).
The size and shape is defined with the axis. This is a snippet from code I have where I group 2 plots together and add a colorbar at the top independently. I played with the values in that add_axes instance until I got a size that worked for me:
cax = fig.add_axes([0.125, 0.925, 0.775, 0.0725]) #has to be as a list - starts with x, y coordinates for start and then width and height in % of figure width
norm = mpl.colors.Normalize(vmin = low_val, vmax = high_val)
mpl.colorbar.ColorbarBase(cax, cmap = self.cmap, norm = norm, orientation = 'horizontal')
The question may be a bit old, but I found another solution that can be of help for anyone who is not willing to manually create a colorbar axes for the ColorbarBase class.
The solution below uses the matplotlib.colorbar.make_axes class to create a dependent sub_axes from the given axes. That sub_axes can then be supplied for the ColorbarBase class for the colorbar creation.
The code is derived from the matplotlib code example describe in here
Here is a snippet code:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import LinearSegmentedColormap
import matplotlib.colorbar as mcbar
from matplotlib import ticker
import matplotlib.colors as mcolors
# Make some illustrative fake data:
x = np.arange(0, np.pi, 0.1)
y = np.arange(0, 2 * np.pi, 0.1)
X, Y = np.meshgrid(x, y)
Z = np.cos(X) * np.sin(Y) * 10
colors = [(1, 0, 0), (0, 1, 0), (0, 0, 1)] # R -> G -> B
n_bins = [3, 6, 10, 100] # Discretizes the interpolation into bins
cmap_name = 'my_list'
fig, axs = plt.subplots(2, 2, figsize=(9, 7))
fig.subplots_adjust(left=0.02, bottom=0.06, right=0.95, top=0.94, wspace=0.05)
for n_bin, ax in zip(n_bins, axs.ravel()):
# Create the colormap
cm = LinearSegmentedColormap.from_list(cmap_name, colors, N=n_bin)
# Fewer bins will result in "coarser" colomap interpolation
im = ax.imshow(Z, interpolation='nearest', origin='lower', cmap=cm)
ax.set_title("N bins: %s" % n_bin)
cax, cbar_kwds = mcbar.make_axes(ax, location = 'right',
fraction=0.15, shrink=0.5, aspect=20)
cbar = mcbar.ColorbarBase(cax, cmap=cm,
norm=mcolors.Normalize(clip=False),
alpha=None,
values=None,
boundaries=None,
orientation='vertical', ticklocation='auto', extend='both',
ticks=n_bins,
format=ticker.FormatStrFormatter('%.2f'),
drawedges=False,
filled=True,
extendfrac=None,
extendrect=False, label='my label')
if n_bin <= 10:
cbar.locator = ticker.MaxNLocator(n_bin)
cbar.update_ticks()
else:
cbar.locator = ticker.MaxNLocator(5)
cbar.update_ticks()
fig.show()
So I'm trying to show the entire range of values plotted on the colorbar (in this case a np.linspace of 0-1).
I set the normalization so the color for values above 0.5 is always that given for 0.5 (i.e. flat top). What I want is a colorbar extending from 0 to 1 and showing the colors for this range. So you should be able to see the flat colorspace after value=0.5.
However, I can't find a way to do this. The default behaviour is to cut off the colorbar range at clim. The extend keyword doesn't seem to be physical/related to the data array and all set_clim does is change the color limits within the established limits.
import matplotlib.pyplot as plt
from matplotlib import cm
from matplotlib import colorbar
from matplotlib.colors import Normalize
import numpy as np
def plotcb(vals, plims, cax):
vmin, vmax = np.percentile(vals, plims)
cmap = cm.get_cmap('plasma')
cmap.set_bad('w', 1.)
cmap_scalar = cm.ScalarMappable(norm=Normalize(vmin, vmax), cmap=cmap)
cmap_scalar.set_array(np.ma.array(vals, mask=np.isnan(vals)))
cb = colorbar.Colorbar(cax, cmap_scalar)
return cmap_scalar.to_rgba(vals)
f, cax = plt.subplots()
all_values = np.linspace(0,1,100)
print plotcb(all_values, [0, 50], cax)
plt.show()
Any ideas?
Thanks
If I understand what you want, any value above 0.5 should have the same colour... The following code will do this, sorry not quite the same as your example and plasma colormap missing for me but hopefully idea should be helpful,
import matplotlib.pyplot as plt
from matplotlib import cm
from matplotlib import colorbar
from matplotlib.colors import Normalize
import numpy as np
class nlcmap(LinearSegmentedColormap):
"""A nonlinear colormap"""
name = 'nlcmap'
def __init__(self, cmap, levels):
self.cmap = cmap
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))
def __call__(self, xi, alpha=1.0, **kw):
yi = np.interp(xi, self._x, self._y)
return self.cmap(yi, alpha)
def plotcb(vals, levels, cax):
#cmap = cm.get_cmap('plasma')
cmap = cm.get_cmap('RdBu')
cmap_nl = nlcmap(cmap, levels)
cmap_scalar = cm.ScalarMappable(cmap=cmap_nl)
cmap_scalar.set_array(np.ma.array(vals, mask=np.isnan(vals)))
cb = colorbar.Colorbar(cax, cmap_scalar)
return cmap_scalar.to_rgba(vals)
f, cax = plt.subplots()
all_values = np.linspace(0,1,100)
#Set levels so top 50 are all the same
levels = all_values.copy()
levels[50:] = levels[-1]
print plotcb(all_values, levels, cax)
plt.show()
How would I have to proceed to obtain the following plot in Python :
For each angle I have a given value and I would like to plot it in a ring, any ideas ?
Something along these lines might work for you
import matplotlib.pyplot as plt
from matplotlib.patches import Wedge
import numpy as np
theta = np.linspace(0, 360, 100)
fig = plt.figure(figsize=(10,10))
ax = fig.add_subplot(111, frameon=False)
for i in range(len(theta)-1):
ax.add_artist(
Wedge((0, 0), 1, theta[i], theta[i+1], width=0.2, color=str(np.random.rand()))
)
ax.set_xlim((-2,2))
ax.set_ylim((-2,2))
ax.axes.get_xaxis().set_visible(False)
ax.axes.get_yaxis().set_visible(False)
fig.show()
An alternative approach would be to create a pcolormesh inside a set of polar axes:
from matplotlib import pyplot as plt
import numpy as np
def polar_heat(values, thetas=None, radii=None, ax=None, fraction=0.3,
**kwargs):
values = np.atleast_2d(values)
if thetas is None:
thetas = np.linspace(0, 2*np.pi, values.shape[1]).reshape(1, -1)
if radii is None:
radii = np.linspace(0, 1, values.shape[0] + 1).reshape(-1, 1)
if ax is None:
fig, ax = plt.subplots(1, 1, subplot_kw={'polar':True})
mesh = ax.pcolormesh(thetas, radii, values, **kwargs)
radrange = radii.ptp()
ax.set_rlim(radrange * (1 - 1. / fraction), radrange)
ax.set_axis_off()
return mesh
For example:
thetas = np.linspace(0, 2*np.pi, 180)
values = np.sin(6 * thetas)
polar_heat(values, thetas, fraction=0.3)
You could easily have multiple nested rings:
values2 = np.vstack([np.sin(3 * thetas), np.cos(6 * thetas)])
polar_heat(values2, fraction=0.6)
You may want to use pie function from matplotlib.pyplot.
You can plot a standard pie chart and place a white circle in the center then, so that it looks like a donut diagram.
See this tutorial for an example of what I'm talking about.
You can also experiment with Vega (format for visualization), namely with Vincent library for Python. See examples with pie/donut charts here.