Matplotlib Rectangle With Color Gradient Fill - python

I want to draw a rectangle, with a gradient color fill from left to right, at an arbitrary position with arbitrary dimensions in my axes instance (ax1) coordinate system.
My first thought was to create a path patch and somehow set its fill as a color gradient. But according to THIS POST there isn't a way to do that.
Next I tried using a colorbar. I created a second axes instance ax2 using fig.add_axes([left, bottom, width, height]) and added a color bar to that.
ax2 = fig.add_axes([0, 0, width, height/8])
colors = [grad_start_color, grad_end_color]
index = [0.0, 1.0]
cm = LinearSegmentedColormap.from_list('my_colormap', zip(index, colors))
colorbar.ColorbarBase(ax2, cmap=cm, orientation='horizontal')
But the positional parameters passed to fig.add_axes() are in the coordinate system of fig, and don't match up with the coordinate system of ax1.
How can I do this?

I have been asking myself a similar question and spent some time looking for the answer to find in the end that this can quite easily be done by imshow:
from matplotlib import pyplot
pyplot.imshow([[0.,1.], [0.,1.]],
cmap = pyplot.cm.Greens,
interpolation = 'bicubic'
)
It is possible to specify a colormap, what interpolation to use and much more. One additional thing, I find very interesting, is the possibility to specify which part of the colormap to use. This is done by means of vmin and vmax:
pyplot.imshow([[64, 192], [64, 192]],
cmap = pyplot.cm.Greens,
interpolation = 'bicubic',
vmin = 0, vmax = 255
)
Inspired by this example
Additional Note:
I chose X = [[0.,1.], [0.,1.]] to make the gradient change from left to right. By setting the array to something like X = [[0.,0.], [1.,1.]], you get a gradient from top to bottom. In general, it is possible to specify the colour for each corner where in X = [[i00, i01],[i10, i11]], i00, i01, i10 and i11 specify colours for the upper-left, upper-right, lower-left and lower-right corners respectively. Increasing the size of X obviously allows to set colours for more specific points.

did you ever solve this? I wanted the same thing and found the answer using the coordinate mapping from here,
#Map axis to coordinate system
def maptodatacoords(ax, dat_coord):
tr1 = ax.transData.transform(dat_coord)
#create an inverse transversion from display to figure coordinates:
fig = ax.get_figure()
inv = fig.transFigure.inverted()
tr2 = inv.transform(tr1)
#left, bottom, width, height are obtained like this:
datco = [tr2[0,0], tr2[0,1], tr2[1,0]-tr2[0,0],tr2[1,1]-tr2[0,1]]
return datco
#Plot a new axis with a colorbar inside
def crect(ax,x,y,w,h,c,**kwargs):
xa, ya, wa, ha = maptodatacoords(ax, [(x,y),(x+w,y+h)])
fig = ax.get_figure()
axnew = fig.add_axes([xa, ya, wa, ha])
cp = mpl.colorbar.ColorbarBase(axnew, cmap=plt.get_cmap("Reds"),
orientation='vertical',
ticks=[],
**kwargs)
cp.outline.set_linewidth(0.)
plt.sca(ax)
Hopefully this helps anyone in the future who needs similar functionality. I ended up using a grid of patch objects instead.

Related

How to make an inset plot with mollweide projection?

I want to make a skymap using the Mollweide projection for a main set of axes and for an inset axes. This is easy for the main axes but not for the inset. I've tried a few different things but it doesn't work for the inset. Please help!
Here you can find the latitude and longitude data, and here you can find the sky location probability density data.
First, I make the main plot:
xmin = min(l)
xmax = max(l)
ymin = min(b)
ymax = max(b)
X, Y = np.meshgrid(np.linspace(xmin, xmax, 100), np.linspace(ymin, ymax, 100))
mpl.rcParams["text.usetex"] = True
fig = plt.figure(1)
fig.set_figheight(8)
fig.set_figwidth(8)
ax = plt.axes(projection='mollweide')
ax.grid()
# skypost is the sky location probability-density data accessible above
plt.contour(X, Y, skypost, colors='blue', levels=[5, 50, 95])
which works fine. Next, I define the inset axes and plot the contours, however there seems to be no way that completely works for this. What I want is for the inset to zoom-in on the contours while keeping the mollweide projection. I've tried to do as the example on ligo.skymaps, i.e.,
axesinset = plt.axes(
[0.0, 0.2, 0.25, 0.25],
projection='astro degrees zoom',
center='110d +20d',
radius='10 deg' )
plt.sca(axesinset)
axesinset.contour(X, Y, skypost, colors='blue', levels=[5, 50, 95])
axesinset.grid()
but this doesn't work since the contours don't even appear! I don't understand why they don't appear. I also do not understand why the x-axis of the inset is backwards?
Instead, I've tried just plotting a new mollweide projection in the inset, and restricting the xlim and ylim, but it says these options are not supported for the mollweide projection. Is there a way around this to restrict the axes limits?
Lastly, I've tried just doing a regular inset without the mollweide, which works, however the shape of the contours are distorted relative to the contours on the main mollweide plot which is physically relevant for my case. So this is very sub-optimal.
Any suggestions and advice are greatly appreciated.
To have the axis in the correct way, you can rotate the subplot by using rotate.
Concerning the fact that your contour are not shown, it is probably because you have to add the transform keyword. If you don't specify it, it is plotted in pixel coordinates by default (https://docs.astropy.org/en/stable/visualization/wcsaxes/overlays.html).
The example below shows that the desired point (in blue) is obtained by adding ax.get_transform("world").
The blue and green points are in the lower right corner because of the rotate.
I guess that it should be the same for contour.
ax = plt.subplot(111, projection='geo degrees zoom',
center="0d - 0d", radius='10 deg', rotate='180 deg')
ax.grid()
ax.set_xlabel(r"$\phi \, [deg]$")
ax.set_ylabel(r"$\theta \, [deg]$")
ax.scatter(0,0, color = "blue")
ax.scatter(100,0, color = "green")
ax.scatter(0,0, color = "red", transform = ax.get_transform("world"))
I'm a bit late to the party, but I thought its worth mentioning that I've created a nice inset-map functionality for EOmaps...
It lets you create inset-maps in arbitrary projections and you can add whatever features you want!
from eomaps import Maps
m = Maps(Maps.CRS.Mollweide())
m.add_feature.preset.coastline()
# create a rectangular inset-map that shows a 5 degree rectangle
# centered around a given point
inset = m.new_inset_map(xy=(6, 43), xy_crs=4326,
radius=5, radius_crs=4326,
inset_crs=Maps.CRS.Mollweide(),
shape="rectangles")
inset.add_feature.preset.coastline()
inset.add_feature.preset.ocean()
inset.add_feature.cultural_10m.urban_areas(fc="r", ec="none")
m.apply_layout(
{'0_map': [0.01, 0.17333, 0.98, 0.65333],
'1_map': [0.05, 0.11667, 0.43341, 0.76667]})

Python equivalent for Matlab's Demcmap (elevation +/- appropriate colormap)

I am looking for a way to get an elevation appropriate colormap for matplotlib.
the cmap 'terrain' looks great but the colorscaling isn't based around zero (i.e. if the scale is 0->5000m, the 0->1000m range may be shades of blue, which you would assume to be for below sea-level)
for example:
The Matlab function equivalent is:
demcmap
What is the best way to get matplotlib to shift a terrain colormap's greens/browns and blues around the zero elevation mark?
Unfortunaly, matplotlib does not provide the functionality of Matlab's demcmap.
There might actually be some build-in features in the python basemap package, of which I'm not aware.
So, sticking to matplotlib on-board options, we can subclass Normalize to build a color normalization centered around a point in the middle of the colormap. This technique can be found in another question on StackOverflow and adapted to the specific needs, namely to set a sealevel (which is probably best chosen as 0) and the value in the colormap col_val (ranging between 0 and 1) to which this sealevel should correspond. In the case of the terrain map, it seems that 0.22, corresponding to a turqoise color, might be a good choice.
The Normalize instance can then be given as an argument to imshow. The resulting figures can be seen down below in the first row of the picture.
Due to the smooth transition around the sealevel the values around 0 appear in a turqoise color, making it hard to distinguish between land and sea.
We can therefore change the terrain map a bit and cut out those colors, such that the coastline is better visible. This is done by combining two parts of the map, ranging from 0 to 0.17 and from 0.25 to 1, and thus cutting out a part of it.
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.colors
class FixPointNormalize(matplotlib.colors.Normalize):
"""
Inspired by https://stackoverflow.com/questions/20144529/shifted-colorbar-matplotlib
Subclassing Normalize to obtain a colormap with a fixpoint
somewhere in the middle of the colormap.
This may be useful for a `terrain` map, to set the "sea level"
to a color in the blue/turquise range.
"""
def __init__(self, vmin=None, vmax=None, sealevel=0, col_val = 0.21875, clip=False):
# sealevel is the fix point of the colormap (in data units)
self.sealevel = sealevel
# col_val is the color value in the range [0,1] that should represent the sealevel.
self.col_val = col_val
matplotlib.colors.Normalize.__init__(self, vmin, vmax, clip)
def __call__(self, value, clip=None):
x, y = [self.vmin, self.sealevel, self.vmax], [0, self.col_val, 1]
return np.ma.masked_array(np.interp(value, x, y))
# Combine the lower and upper range of the terrain colormap with a gap in the middle
# to let the coastline appear more prominently.
# inspired by https://stackoverflow.com/questions/31051488/combining-two-matplotlib-colormaps
colors_undersea = plt.cm.terrain(np.linspace(0, 0.17, 56))
colors_land = plt.cm.terrain(np.linspace(0.25, 1, 200))
# combine them and build a new colormap
colors = np.vstack((colors_undersea, colors_land))
cut_terrain_map = matplotlib.colors.LinearSegmentedColormap.from_list('cut_terrain', colors)
# invent some data (height in meters relative to sea level)
data = np.linspace(-1000,2400,15**2).reshape((15,15))
# plot example data
fig, ax = plt.subplots(nrows = 2, ncols=3, figsize=(11,6) )
plt.subplots_adjust(left=0.08, right=0.95, bottom=0.05, top=0.92, hspace = 0.28, wspace = 0.15)
plt.figtext(.5, 0.95, "Using 'terrain' and FixedPointNormalize", ha="center", size=14)
norm = FixPointNormalize(sealevel=0, vmax=3400)
im = ax[0,0].imshow(data+1000, norm=norm, cmap=plt.cm.terrain)
fig.colorbar(im, ax=ax[0,0])
norm2 = FixPointNormalize(sealevel=0, vmax=3400)
im2 = ax[0,1].imshow(data, norm=norm2, cmap=plt.cm.terrain)
fig.colorbar(im2, ax=ax[0,1])
norm3 = FixPointNormalize(sealevel=0, vmax=0)
im3 = ax[0,2].imshow(data-2400.1, norm=norm3, cmap=plt.cm.terrain)
fig.colorbar(im3, ax=ax[0,2])
plt.figtext(.5, 0.46, "Using custom cut map and FixedPointNormalize (adding hard edge between land and sea)", ha="center", size=14)
norm4 = FixPointNormalize(sealevel=0, vmax=3400)
im4 = ax[1,0].imshow(data+1000, norm=norm4, cmap=cut_terrain_map)
fig.colorbar(im4, ax=ax[1,0])
norm5 = FixPointNormalize(sealevel=0, vmax=3400)
im5 = ax[1,1].imshow(data, norm=norm5, cmap=cut_terrain_map)
cbar = fig.colorbar(im5, ax=ax[1,1])
norm6 = FixPointNormalize(sealevel=0, vmax=0)
im6 = ax[1,2].imshow(data-2400.1, norm=norm6, cmap=cut_terrain_map)
fig.colorbar(im6, ax=ax[1,2])
for i, name in enumerate(["land only", "coast line", "sea only"]):
for j in range(2):
ax[j,i].text(0.96,0.96,name, ha="right", va="top", transform=ax[j,i].transAxes, color="w" )
plt.show()

How to rotate tick labels in floating cylindrical axes?

http://matplotlib.org/mpl_toolkits/axes_grid/users/overview.html
Check out the VERY bottom of this link. I'm interested in that axes in the middle, where the axis objects are curved into the shape of a quarter-washer. If you check the sourcecode, this axes object is made by setup_axes2:
def setup_axes2(fig, rect):
"""
With custom locator and formatter.
Note that the extreme values are swapped.
"""
tr = PolarAxes.PolarTransform()
pi = np.pi
angle_ticks = [(0, r"$0$"),
(.25*pi, r"$\frac{1}{4}\pi$"),
(.5*pi, r"$\frac{1}{2}\pi$")]
grid_locator1 = FixedLocator([v for v, s in angle_ticks])
tick_formatter1 = DictFormatter(dict(angle_ticks))
grid_locator2 = MaxNLocator(2)
grid_helper = floating_axes.GridHelperCurveLinear(
tr, extremes=(.5*pi, 0, 2, 1),
grid_locator1=grid_locator1,
grid_locator2=grid_locator2,
tick_formatter1=tick_formatter1,
tick_formatter2=None)
ax1 = floating_axes.FloatingSubplot(fig, rect, grid_helper=grid_helper)
fig.add_subplot(ax1)
# create a parasite axes whose transData in RA, cz
aux_ax = ax1.get_aux_axes(tr)
aux_ax.patch = ax1.patch # for aux_ax to have a clip path as in ax
ax1.patch.zorder = 0.9 # but this has a side effect that the patch is
# drawn twice, and possibly over some other
# artists. So, we decrease the zorder a bit to
# prevent this.
return ax1, aux_ax
When I label the ticks in the theta axis, the labels are always upside down. I don't know how to flip them. I also don't know how to flip the axis labels upside down. Does anyone know about these confusing floating axes?
The hint was in setup_axes3() from the example you linked. The individual axes in the FloatingSubplot are referred to like ax.axis[side] where side is one of ["top","bottom","left","right"]. From there you get the usual.
ax = ax2.axis["bottom"]
ax.major_ticklabels.set_rotation(180)
ax.set_label("foo")
ax.label.set_rotation(180)
ax.LABELPAD += 10
Just do dir(ax) to see what you have access to.

Select starting color in matplotlib colormap

I have the figure shown below. Presently the figure's colorscheme uses the entire range of the colormap (mpl.cm.Paired). What I want to do, and have been unable to figure out, is how to limit matplotlib to use only a subset of the colormap. In this case I am trying to get the starting color to be a darker shade of blue. Here's the plotting section of my code:
Figure = plt.figure(figsize=(22,10))
Map = Basemap(projection='robin', lon_0=0, resolution='l')
x, y = Map(LONS, LATS)
levels = np.arange(0, 4100, 100)
fcp = Map.contourf(x, y, data, levels, interpolation="bicubic", cmap=mpl.cm.Paired)
cb = Map.colorbar(fcp, "bottom", size="5%", pad='5%', extendrect=False)
cb.ax.tick_params(labelsize=18)
cb.solids.set_edgecolor("face")
cb.set_label("metres",fontsize=18)
cb.ax.set_aspect(0.047)
Map.drawcoastlines(linewidth=1)
Map.drawmapboundary(linewidth=1)
Map.drawmeridians([-150,-100,-50,0,50,100, 150],labels=[1,1,1,0],fontsize=18)
Map.drawparallels([-60,-30,0,30,60],labels=[1,1,1,1],fontsize=18)
One way to do this would be to call the function mpl.cm.Paired() for a subset of the normalised range (i.e., [0-1]) and then use the list of colors that it returns to define a new colormap:
import matplotlib.colors as mcol
lvTmp = np.linspace(0.1,1.0,len(levels)-1)
cmTmp = mlp.cm.Paired(lvTmp)
newCmap = mcol.ListedColormap(cmTmp)
You'll need to fiddle about with the 0.1 value in that linspace to get the start color that you want from the built in colormap.

Understanding matplotlib verts

I'm trying to create custom markers in matplotlib for a scatter plot, where the markers are rectangles with fix height and varying width. The width of each marker is a function of the y-value. I tried it like this using this code as a template and assuming that if verts is given a list of N 2-D tuples it plots rectangles with the width of the corresponing first value and the height of the second (maybe this is already wrong, but then how else do I accomplish that?).
I have a list of x and y values, each containing angles in degrees. Then, I compute the width and height of each marker by
field_size = 2.
symb_vec_x = [(field_size / np.cos(i * np.pi / 180.)) for i in y]
symb_vec_y = [field_size for i in range(len(y))]
and build the verts list and plot everything with
symb_vec = list(zip(symb_vec_x, symb_vec_y))
fig = plt.figure(1, figsize=(14.40, 9.00))
ax = fig.add_subplot(1,1,1)
sc = ax.scatter(ra_i, dec_i, marker='None', verts=symb_vec)
But the resulting plot is empty, no error message however. Can anyone tell me what I did wrong with defining the verts and how to do it right?
Thanks!
As mentioned 'marker='None' need to be removed then the appropriate way to specify a rectangle with verts is something like
verts = list(zip([-10.,10.,10.,-10],[-5.,-5.,5.,5]))
ax.scatter([0.5,1.0],[1.0,2.0], marker=(verts,0))
The vertices are defined as ([x1,x2,x3,x4],[y1,y2,y3,y4]) so attention must be paid to which get minus signs etc.
This (verts,0) is mentioned in the docs as
For backward compatibility, the form (verts, 0) is also accepted,
but it is equivalent to just verts for giving a raw set of vertices
that define the shape.
However I find using just verts does not give the correct shape.
To automate the process you need to do something like
v_val=1.0
h_val=2.0
verts = list(zip([-h_val,h_val,h_val,-h_val],[-v_val,-v_val,v_val,v_val]))
Basic example:
import pylab as py
ax = py.subplot(111)
v_val=1.0
h_val=2.0
verts = list(zip([-h_val,h_val,h_val,-h_val],[-v_val,-v_val,v_val,v_val]))
ax.scatter([0.5,1.0],[1.0,2.0], marker=(verts,0))
*
edit
Individual markers
So you need to manually create a vert for each case. This will obviously depend on how you want your rectangles to change point to point. Here is an example
import pylab as py
ax = py.subplot(111)
def verts_function(x,y,r):
# Define the vertex's multiplying the x value by a ratio
x = x*r
y = y
return [(-x,-y),(x,-y),(x,y),(-x,y)]
n=5
for i in range(1,4):
ax.scatter(i,i, marker=(verts_function(i,i,0.3),0))
py.show()
so in my simple case I plot the points i,i and draw rectangles around them. The way the vert markers are specified is non intuitive. In the documentation it's described as follows:
verts: A list of (x, y) pairs used for Path vertices. The center of
the marker is located at (0,0) and the size is normalized, such that
the created path is encapsulated inside the unit cell.
Hence, the following are equivalent:
vert = [(-300.0, -1000), (300.0, -1000), (300.0, 1000), (-300.0, 1000)]
vert = [(-0.3, -1), (0.3, -1), (0.3, 1), (-0.3, 1)]
e.g they will produce the same marker. As such I have used a ratio, this is where you need to do put in the work. The value of r (the ratio) will change which axis remains constant.
This is all getting very complicated, I'm sure there must be a better way to do this.
I got the solution from Ryan of the matplotlib users mailing list. It's quite elegant, so I will share his example here:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
from matplotlib.collections import PatchCollection
n = 100
# Get your xy data points, which are the centers of the rectangles.
xy = np.random.rand(n,2)
# Set a fixed height
height = 0.02
# The variable widths of the rectangles
widths = np.random.rand(n)*0.1
# Get a color map and make some colors
cmap = plt.cm.hsv
colors = np.random.rand(n)*10.
# Make a normalized array of colors
colors_norm = colors/colors.max()
# Here's where you have to make a ScalarMappable with the colormap
mappable = plt.cm.ScalarMappable(cmap=cmap)
# Give it your non-normalized color data
mappable.set_array(colors)
rects = []
for p, w in zip(xy, widths):
xpos = p[0] - w/2 # The x position will be half the width from the center
ypos = p[1] - height/2 # same for the y position, but with height
rect = Rectangle( (xpos, ypos), w, height ) # Create a rectangle
rects.append(rect) # Add the rectangle patch to our list
# Create a collection from the rectangles
col = PatchCollection(rects)
# set the alpha for all rectangles
col.set_alpha(0.3)
# Set the colors using the colormap
col.set_facecolor( cmap(colors_norm) )
# No lines
col.set_linewidth( 0 )
#col.set_edgecolor( 'none' )
# Make a figure and add the collection to the axis.
fig = plt.figure()
ax = fig.add_subplot(111)
ax.add_collection(col)
# Add your ScalarMappable to a figure colorbar
fig.colorbar(mappable)
plt.show()
Thank you, Ryan, and everyone who contributed their ideas!

Categories