Related
I'd like to use a custom (blue) color on a plot where the data is zero. I have tried the set_under method, but failed. The desired output would be a blue line at the bottom and a two blue square at the upper part of the graph. Any help is appreciated.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import LinearSegmentedColormap,TwoSlopeNorm
# TwoSlopeNorm see:
# https://matplotlib.org/devdocs/tutorials/colors/colormapnorms.html#sphx-glr-tutorials-colors-colormapnorms-py
red2orange = np.array([np.linspace(1, 1, 256),
np.linspace(0, 165/256, 256),
np.linspace(0, 0, 256),
np.ones(256)]).T
grey2black = np.array([np.linspace(0.75, 0.25, 256),
np.linspace(0.75, 0.25, 256),
np.linspace(0.75, 0.25, 256),
np.ones(256)]).T
all_colors = np.vstack((grey2black,red2orange))
cmap = LinearSegmentedColormap.from_list('two_slope_cmap', all_colors)
divnorm = TwoSlopeNorm(vmin=1, vcenter=400, vmax=1000)
# seting bad and under
cmap.set_bad('mediumspringgreen')
cmap.set_under('blue')
#fake data
data = np.arange(1400)[:,None]* np.ones(200)
data[ 1100:1150, 50:150] = np.nan # bad data
data[ 1200:1250, 50:150] = 0 # zero data
data[ 1300:1350, 50:150] = -1 # under data
# plot
f,a = plt.subplots()
raster = a.pcolormesh(data,cmap=cmap, norm=divnorm)
cbar = f.colorbar(raster,ax=a, extend='both')
For some unknown reason, TwoSlopeNorm doesn't seem to honor the under nor over colors. Changing the code to use plt.Normalize() instead of TwoSlopeNorm() indicates that for that norm, the under color works as expected.
A workaround is to draw the pcolormesh a second time, only for the under color. A drawback is that the under color isn't shown in the colorbar extension.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import LinearSegmentedColormap, ListedColormap, TwoSlopeNorm
red2orange = np.array([np.linspace(1, 1, 256),
np.linspace(0, 165 / 256, 256),
np.linspace(0, 0, 256),
np.ones(256)]).T
grey2black = np.array([np.linspace(0.75, 0.25, 256),
np.linspace(0.75, 0.25, 256),
np.linspace(0.75, 0.25, 256),
np.ones(256)]).T
all_colors = np.vstack((grey2black, red2orange))
cmap = LinearSegmentedColormap.from_list('two_slope_cmap', all_colors)
divnorm = TwoSlopeNorm(vmin=1, vcenter=400, vmax=1000)
# seting bad and under
cmap.set_bad('mediumspringgreen')
cmap.set_under('dodgerblue') # this doesn't seem to be used with a TwoSlopeNorm
# fake data
data = np.arange(1400)[:, None] * np.ones(200)
data[1100:1150, 50:150] = np.nan # bad data
data[1200:1250, 50:150] = 0 # zero data
data[1300:1350, 50:150] = -1 # under data
# plot
f, a = plt.subplots()
raster = a.pcolormesh(data, cmap=cmap, norm=divnorm)
cbar = f.colorbar(raster, ax=a, extend='both')
# draw the mesh a second time, only for the under color
a.pcolormesh(np.where(data < 1, 0, np.nan), cmap=ListedColormap([cmap.get_under()]))
plt.show()
Another workaround is to change the colormap itself, setting the lowest color to the desired under color. A drawback is that vmin needs to move a bit. (The distance seems to be 1/127 of the distance between vmin and vcenter. Internally, the colormap maintains 256 colors, with the color for vcenter at position 128.)
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import LinearSegmentedColormap, TwoSlopeNorm, to_rgba
red2orange = np.array([np.linspace(1, 1, 256),
np.linspace(0, 165 / 256, 256),
np.linspace(0, 0, 256),
np.ones(256)]).T
grey2black = np.array([np.linspace(0.75, 0.25, 256),
np.linspace(0.75, 0.25, 256),
np.linspace(0.75, 0.25, 256),
np.ones(256)]).T
all_colors = np.vstack((grey2black, red2orange))
all_colors[0, :] = to_rgba('dodgerblue')
cmap = LinearSegmentedColormap.from_list('two_slope_cmap', all_colors)
vmin = 1
vcenter = 400
vmax = 1000
divnorm = TwoSlopeNorm(vmin=vmin-(vcenter-vmin)/127, vcenter=vcenter, vmax=vmax)
# seting bad and under
cmap.set_bad('mediumspringgreen')
cmap.set_under('red') # this doesn't seem to be used with a TwoSlopeNorm
# fake data
data = np.arange(1400)[:, None] * np.ones(200)
data[1100:1150, 50:150] = np.nan # bad data
data[1200:1250, 50:150] = 0 # zero data
data[1300:1350, 50:150] = -1 # under data
# plot
f, a = plt.subplots()
raster = a.pcolormesh(data, cmap=cmap, norm=divnorm)
cbar = f.colorbar(raster, ax=a, extend='both')
plt.show()
PS: The following code tries to visualize how the two norms treat the under color differently:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import TwoSlopeNorm
fig, (ax1, ax2) = plt.subplots(ncols=2, figsize=(10, 3))
data = np.arange(0, 100)[:, None]
cmap = plt.get_cmap('viridis').copy()
cmap.set_under('crimson')
cmap.set_over('skyblue')
norm1 = plt.Normalize(20, 80)
im1 = ax1.imshow(data, cmap=cmap, norm=norm1, aspect='auto', origin='lower', interpolation='nearest')
plt.colorbar(im1, ax=ax1, extend='both')
ax1.set_title('using plt.Normalize()')
norm2 = TwoSlopeNorm(vmin=20, vcenter=30, vmax=80)
im2 = ax2.imshow(data, cmap=cmap, norm=norm2, aspect='auto', origin='lower', interpolation='nearest')
plt.colorbar(im2, ax=ax2, extend='both')
ax2.set_title('using TwoSlopeNorm')
plt.show()
PPS: Looking into the source of TwoSlopeNorm at github, the problem seems to be solved for the next version of matplotlib (current version is 3.4.3). So, you could try to install the development version. (The change involves adding left=-np.inf, right=np.inf as parameter to np.interp in the __call__ method of the TwoSlopeNorm class in colors.py.
This question leads on from a previous question I asked yesterday that got me most of the way to what I am after:
Rotate transformation on matplotlib axis in subplot
I want to create a two by two array of graphs where the bottom-left is a scatter plot and the other three are histograms projecting the x, y, and x-y data from that plot. That final histogram I want to have at a 45 degree angle, and it is the positioning of that plot which I am trying to adjust.
Currently, I have this:
from matplotlib.transforms import Affine2D
import mpl_toolkits.axisartist.floating_axes as floating_axes
import matplotlib.pyplot as plt
def setup_axes(fig, rect, rotation, axisScale, axisLimits, doShift):
tr_rot = Affine2D().scale(axisScale[0], axisScale[1]).rotate_deg(rotation)
# This seems to do nothing
if doShift:
tr_trn = Affine2D().translate(-90,-5)
else:
tr_trn = Affine2D().translate(0,0)
tr = tr_rot + tr_trn
grid_helper = floating_axes.GridHelperCurveLinear(tr, extremes=axisLimits)
ax = floating_axes.FloatingSubplot(fig, rect, grid_helper=grid_helper)
fig.add_subplot(ax)
aux_ax = ax.get_aux_axes(tr)
return ax, aux_ax
fig = plt.figure(1, figsize=(8, 8))
axes = []
axisOrientation = [0, 0, 270, -45]
axisScale = [[1,1],[6,1],[6,1],[6,1]]
axisPosition = [223,221,224,222]
axisLimits = [(-0.5, 4.5, -0.5, 4.5),
(-0.5, 4.5, 0, 12),
(-0.5, 4.5, 0, 12),
(-3.5, 3.5, 0, 12)]
doShift = [False, False, False, True]
label_axes = []
for i in range(0, len(axisOrientation)):
ax, aux_ax = setup_axes(fig, axisPosition[i], axisOrientation[i],
axisScale[i], axisLimits[i], doShift[i])
axes.append(aux_ax)
label_axes.append(ax)
numPoints = 100
x = []
y = []
for i in range(0,numPoints):
x.append(np.random.rand() + i/100.0)
y.append(np.random.rand() + i/100.0 + np.mod(i,2)*2)
axes[0].plot(x,y,ls='none',marker='x')
label_axes[0].axis["bottom"].label.set_text('Variable 1')
label_axes[0].axis["left"].label.set_text('Variable 2')
b = np.linspace(-0.5,4.5,50)
axes[1].hist(x, bins = b)
axes[2].hist(y, bins = b)
b = np.linspace(-3.5,3.5,50)
axes[3].hist(np.array(x)-np.array(y), bins=b)
for i in range(1,len(label_axes)):
for axisLoc in ['top','left','right']:
label_axes[i].axis[axisLoc].set_visible(False)
label_axes[i].axis['bottom'].toggle(ticklabels=False)
fig.subplots_adjust(wspace=-0.30, hspace=-0.30, left=0.00, right=0.99, top=0.99, bottom=0.0)
plt.show()
Which gives:
As you can see in the code, I try to shift the position of that top-right plot with an Affine2D().translate() but it seems to have no effect. Does anybody know how I might move this plot so that its x-axis almost-touches the top-right corner of the bottom-left plot's axes?
Edit:
I have also just noticed that the bottom-right plot is upside-down compared to how it should be. It needs to be top-bottom mirrored somehow.
Edit 2:
This code before fig.subplots_adjust() will fix that:
label_axes[2].invert_yaxis()
I am plotting changes in mean and variance of some data with the following code
import matplotlib.pyplot as pyplot
import numpy
vis_mv(data, ax = None):
if ax is None: ax = pyplot.gca()
cmap = pyplot.get_cmap()
colors = cmap(numpy.linspace(0, 1, len(data)))
xs = numpy.arange(len(data)) + 1
means = numpy.array([ numpy.mean(x) for x in data ])
varis = numpy.array([ numpy.var(x) for x in data ])
vlim = max(1, numpy.amax(varis))
# variance
ax.imshow([[0.,1.],[0.,1.]],
cmap = cmap, interpolation = 'bicubic',
extent = (1, len(data), -vlim, vlim), aspect = 'auto'
)
ax.fill_between(xs, -vlim, -varis, color = 'white')
ax.fill_between(xs, varis, vlim, color = 'white')
# mean
ax.plot(xs, means, color = 'white', zorder = 1)
ax.scatter(xs, means, color = colors, edgecolor = 'white', zorder = 2)
return ax
This works perfectly fine:
but now I would like to be able to use this visualisation also in a vertical fashion as some kind of advanced color bar kind of thingy next to another plot. I hoped it would be possible to rotate the entire axis with all of its contents,
but I could only find this question, which does not really have a solid answer yet either. Therefore, I tried to do it myself as follows:
from matplotlib.transforms import Affine2D
ax = vis_mv()
r = Affine2D().rotate_deg(90) + ax.transData
for x in ax.images + ax.lines + ax.collections:
x.set_transform(r)
old = ax.axis()
ax.axis(old[2:4] + old[0:2])
This almost does the trick (note how the scattered points, which used to lie along the white line, are blown up and not rotated as expected).
Unfortunately the PathCollection holding the result of the scattering does not act as expected. After trying out some things, I found that scatter has some kind of offset transform, which seems to be the equivalent of the regular transform in other collections.
x = numpy.arange(5)
ax = pyplot.gca()
p0, = ax.plot(x)
p1 = ax.scatter(x,x)
ax.transData == p0.get_transform() # True
ax.transData == p1.get_offset_transform() # True
It seems like I might want to change the offset transform instead for the scatter plot, but I did not manage to find any method that allows me to change that transform on a PathCollection. Also, it would make it a lot more inconvenient to do what I actually want to do.
Would anyone know if there exists a possibility to change the offset transform?
Thanks in advance
Unfortunately the PathCollection does not have a .set_offset_transform() method, but one can access the private _transOffset attribute and set the rotating transformation to it.
import matplotlib.pyplot as plt
from matplotlib.transforms import Affine2D
from matplotlib.collections import PathCollection
import numpy as np; np.random.seed(3)
def vis_mv(data, ax = None):
if ax is None: ax = plt.gca()
cmap = plt.get_cmap()
colors = cmap(np.linspace(0, 1, len(data)))
xs = np.arange(len(data)) + 1
means = np.array([ np.mean(x) for x in data ])
varis = np.array([ np.var(x) for x in data ])
vlim = max(1, np.amax(varis))
# variance
ax.imshow([[0.,1.],[0.,1.]],
cmap = cmap, interpolation = 'bicubic',
extent = (1, len(data), -vlim, vlim), aspect = 'auto' )
ax.fill_between(xs, -vlim, -varis, color = 'white')
ax.fill_between(xs, varis, vlim, color = 'white')
# mean
ax.plot(xs, means, color = 'white', zorder = 1)
ax.scatter(xs, means, color = colors, edgecolor = 'white', zorder = 2)
return ax
data = np.random.normal(size=(9, 9))
ax = vis_mv(data)
r = Affine2D().rotate_deg(90)
for x in ax.images + ax.lines + ax.collections:
trans = x.get_transform()
x.set_transform(r+trans)
if isinstance(x, PathCollection):
transoff = x.get_offset_transform()
x._transOffset = r+transoff
old = ax.axis()
ax.axis(old[2:4] + old[0:2])
plt.show()
I am generating 2D heat map plots of a set of 3D data. I would like to be able to have a mechanism to interactively page through each pane. Below is a simple sample code, I would like to be able to interactively view both panes (ie, z = [0,1]) via a slider bar (or some other means). Is this possible with matplotlib or is this something I'll need to do post processing after generating the image files?
import numpy as np
from matplotlib import pyplot as plt
data = np.random.randint(10, size=(5, 5, 2))
data_slice = np.zeros((5,5))
for i in range(0, 5):
for j in range(0, 5):
data_slice[i][j] = data[i][j][0]
plt.imshow(data_slice, cmap='hot', interpolation='nearest')
plt.show()
Edit : I want to be able to do this interactively and it appears that the possible duplicate is trying to do this automatically.
The solution could indeed be to use a Slider as in the excellent answer by #hashmuke. In his answer he mentioned that
"The slider is continuous while the layer index is a discrete integer [...]"
This brought me to think about a solution that wouldn't have this restriction and have
a more page-like look and feel.
The outcome is PageSlider. Subclassing Slider it makes use of the slider functionality, but displays the slider in integer steps starting at 1. It takes the number of pages numpages as init argument, but except of that works as Slider seen from the outside. Additionally it also provides a back- and forward button.
An example, similar to the one from #hashmuke, is given below the class.
import matplotlib.widgets
import matplotlib.patches
import mpl_toolkits.axes_grid1
class PageSlider(matplotlib.widgets.Slider):
def __init__(self, ax, label, numpages = 10, valinit=0, valfmt='%1d',
closedmin=True, closedmax=True,
dragging=True, **kwargs):
self.facecolor=kwargs.get('facecolor',"w")
self.activecolor = kwargs.pop('activecolor',"b")
self.fontsize = kwargs.pop('fontsize', 10)
self.numpages = numpages
super(PageSlider, self).__init__(ax, label, 0, numpages,
valinit=valinit, valfmt=valfmt, **kwargs)
self.poly.set_visible(False)
self.vline.set_visible(False)
self.pageRects = []
for i in range(numpages):
facecolor = self.activecolor if i==valinit else self.facecolor
r = matplotlib.patches.Rectangle((float(i)/numpages, 0), 1./numpages, 1,
transform=ax.transAxes, facecolor=facecolor)
ax.add_artist(r)
self.pageRects.append(r)
ax.text(float(i)/numpages+0.5/numpages, 0.5, str(i+1),
ha="center", va="center", transform=ax.transAxes,
fontsize=self.fontsize)
self.valtext.set_visible(False)
divider = mpl_toolkits.axes_grid1.make_axes_locatable(ax)
bax = divider.append_axes("right", size="5%", pad=0.05)
fax = divider.append_axes("right", size="5%", pad=0.05)
self.button_back = matplotlib.widgets.Button(bax, label=ur'$\u25C0$',
color=self.facecolor, hovercolor=self.activecolor)
self.button_forward = matplotlib.widgets.Button(fax, label=ur'$\u25B6$',
color=self.facecolor, hovercolor=self.activecolor)
self.button_back.label.set_fontsize(self.fontsize)
self.button_forward.label.set_fontsize(self.fontsize)
self.button_back.on_clicked(self.backward)
self.button_forward.on_clicked(self.forward)
def _update(self, event):
super(PageSlider, self)._update(event)
i = int(self.val)
if i >=self.valmax:
return
self._colorize(i)
def _colorize(self, i):
for j in range(self.numpages):
self.pageRects[j].set_facecolor(self.facecolor)
self.pageRects[i].set_facecolor(self.activecolor)
def forward(self, event):
current_i = int(self.val)
i = current_i+1
if (i < self.valmin) or (i >= self.valmax):
return
self.set_val(i)
self._colorize(i)
def backward(self, event):
current_i = int(self.val)
i = current_i-1
if (i < self.valmin) or (i >= self.valmax):
return
self.set_val(i)
self._colorize(i)
if __name__ == "__main__":
import numpy as np
from matplotlib import pyplot as plt
num_pages = 23
data = np.random.rand(9, 9, num_pages)
fig, ax = plt.subplots()
fig.subplots_adjust(bottom=0.18)
im = ax.imshow(data[:, :, 0], cmap='viridis', interpolation='nearest')
ax_slider = fig.add_axes([0.1, 0.05, 0.8, 0.04])
slider = PageSlider(ax_slider, 'Page', num_pages, activecolor="orange")
def update(val):
i = int(slider.val)
im.set_data(data[:,:,i])
slider.on_changed(update)
plt.show()
You can either animate the layers as suggested by Andrew's comment or you can manually walk through the the layers using a slider as follow:
import numpy as np
from matplotlib import pyplot as plt
from matplotlib.widgets import Slider
# generate a five layer data
data = np.random.randint(10, size=(5, 5, 5))
# current layer index start with the first layer
idx = 0
# figure axis setup
fig, ax = plt.subplots()
fig.subplots_adjust(bottom=0.15)
# display initial image
im_h = ax.imshow(data[:, :, idx], cmap='hot', interpolation='nearest')
# setup a slider axis and the Slider
ax_depth = plt.axes([0.23, 0.02, 0.56, 0.04])
slider_depth = Slider(ax_depth, 'depth', 0, data.shape[2]-1, valinit=idx)
# update the figure with a change on the slider
def update_depth(val):
idx = int(round(slider_depth.val))
im_h.set_data(data[:, :, idx])
slider_depth.on_changed(update_depth)
plt.show()
The slider is continues while the layer index is discrete integer, I hope that is not a problem. Here is the resulting figure,
I am generating 2D heat map plots of a set of 3D data. I would like to be able to have a mechanism to interactively page through each pane. Below is a simple sample code, I would like to be able to interactively view both panes (ie, z = [0,1]) via a slider bar (or some other means). Is this possible with matplotlib or is this something I'll need to do post processing after generating the image files?
import numpy as np
from matplotlib import pyplot as plt
data = np.random.randint(10, size=(5, 5, 2))
data_slice = np.zeros((5,5))
for i in range(0, 5):
for j in range(0, 5):
data_slice[i][j] = data[i][j][0]
plt.imshow(data_slice, cmap='hot', interpolation='nearest')
plt.show()
Edit : I want to be able to do this interactively and it appears that the possible duplicate is trying to do this automatically.
The solution could indeed be to use a Slider as in the excellent answer by #hashmuke. In his answer he mentioned that
"The slider is continuous while the layer index is a discrete integer [...]"
This brought me to think about a solution that wouldn't have this restriction and have
a more page-like look and feel.
The outcome is PageSlider. Subclassing Slider it makes use of the slider functionality, but displays the slider in integer steps starting at 1. It takes the number of pages numpages as init argument, but except of that works as Slider seen from the outside. Additionally it also provides a back- and forward button.
An example, similar to the one from #hashmuke, is given below the class.
import matplotlib.widgets
import matplotlib.patches
import mpl_toolkits.axes_grid1
class PageSlider(matplotlib.widgets.Slider):
def __init__(self, ax, label, numpages = 10, valinit=0, valfmt='%1d',
closedmin=True, closedmax=True,
dragging=True, **kwargs):
self.facecolor=kwargs.get('facecolor',"w")
self.activecolor = kwargs.pop('activecolor',"b")
self.fontsize = kwargs.pop('fontsize', 10)
self.numpages = numpages
super(PageSlider, self).__init__(ax, label, 0, numpages,
valinit=valinit, valfmt=valfmt, **kwargs)
self.poly.set_visible(False)
self.vline.set_visible(False)
self.pageRects = []
for i in range(numpages):
facecolor = self.activecolor if i==valinit else self.facecolor
r = matplotlib.patches.Rectangle((float(i)/numpages, 0), 1./numpages, 1,
transform=ax.transAxes, facecolor=facecolor)
ax.add_artist(r)
self.pageRects.append(r)
ax.text(float(i)/numpages+0.5/numpages, 0.5, str(i+1),
ha="center", va="center", transform=ax.transAxes,
fontsize=self.fontsize)
self.valtext.set_visible(False)
divider = mpl_toolkits.axes_grid1.make_axes_locatable(ax)
bax = divider.append_axes("right", size="5%", pad=0.05)
fax = divider.append_axes("right", size="5%", pad=0.05)
self.button_back = matplotlib.widgets.Button(bax, label=ur'$\u25C0$',
color=self.facecolor, hovercolor=self.activecolor)
self.button_forward = matplotlib.widgets.Button(fax, label=ur'$\u25B6$',
color=self.facecolor, hovercolor=self.activecolor)
self.button_back.label.set_fontsize(self.fontsize)
self.button_forward.label.set_fontsize(self.fontsize)
self.button_back.on_clicked(self.backward)
self.button_forward.on_clicked(self.forward)
def _update(self, event):
super(PageSlider, self)._update(event)
i = int(self.val)
if i >=self.valmax:
return
self._colorize(i)
def _colorize(self, i):
for j in range(self.numpages):
self.pageRects[j].set_facecolor(self.facecolor)
self.pageRects[i].set_facecolor(self.activecolor)
def forward(self, event):
current_i = int(self.val)
i = current_i+1
if (i < self.valmin) or (i >= self.valmax):
return
self.set_val(i)
self._colorize(i)
def backward(self, event):
current_i = int(self.val)
i = current_i-1
if (i < self.valmin) or (i >= self.valmax):
return
self.set_val(i)
self._colorize(i)
if __name__ == "__main__":
import numpy as np
from matplotlib import pyplot as plt
num_pages = 23
data = np.random.rand(9, 9, num_pages)
fig, ax = plt.subplots()
fig.subplots_adjust(bottom=0.18)
im = ax.imshow(data[:, :, 0], cmap='viridis', interpolation='nearest')
ax_slider = fig.add_axes([0.1, 0.05, 0.8, 0.04])
slider = PageSlider(ax_slider, 'Page', num_pages, activecolor="orange")
def update(val):
i = int(slider.val)
im.set_data(data[:,:,i])
slider.on_changed(update)
plt.show()
You can either animate the layers as suggested by Andrew's comment or you can manually walk through the the layers using a slider as follow:
import numpy as np
from matplotlib import pyplot as plt
from matplotlib.widgets import Slider
# generate a five layer data
data = np.random.randint(10, size=(5, 5, 5))
# current layer index start with the first layer
idx = 0
# figure axis setup
fig, ax = plt.subplots()
fig.subplots_adjust(bottom=0.15)
# display initial image
im_h = ax.imshow(data[:, :, idx], cmap='hot', interpolation='nearest')
# setup a slider axis and the Slider
ax_depth = plt.axes([0.23, 0.02, 0.56, 0.04])
slider_depth = Slider(ax_depth, 'depth', 0, data.shape[2]-1, valinit=idx)
# update the figure with a change on the slider
def update_depth(val):
idx = int(round(slider_depth.val))
im_h.set_data(data[:, :, idx])
slider_depth.on_changed(update_depth)
plt.show()
The slider is continues while the layer index is discrete integer, I hope that is not a problem. Here is the resulting figure,