matplotlib - Draw a heatmap/pixelmap with ability to edit individual pixel colours (different colormaps by row) - matplotlib

I'm trying to draw a heat map/pixelmap representation of a matrix using matplotlib. I currently have the following code which gives me the pixelmap as required (adapted from Heatmap in matplotlib with pcolor?):
import matplotlib.pyplot as plt
import numpy as np
column_labels = list('ABCD')
row_labels = list('0123')
data = np.array([[0,1,2,0],
[1,0,1,1],
[1,2,0,0],
[0,0,0,1]])
fig, ax = plt.subplots()
heatmap = ax.pcolor(data, cmap=plt.cm.Blues)
# put the major ticks at the middle of each cell
ax.set_xticks(np.arange(data.shape[0])+0.5, minor=False)
ax.set_yticks(np.arange(data.shape[1])+0.5, minor=False)
# want a more natural, table-like display
ax.invert_yaxis()
ax.xaxis.tick_top()
ax.set_xticklabels(row_labels, minor=False)
ax.set_yticklabels(column_labels, minor=False)
ax.yaxis.grid(True, which='minor', linestyle='-', color='k', linewidth = 0.3, alpha = 0.5)
ax.xaxis.grid(True, which='minor', linestyle='-', color='k', linewidth = 0.3, alpha = 0.5)
# Set the location of the minor ticks to the edge of pixels for the x grid
minor_locator = AutoMinorLocator(2)
ax.xaxis.set_minor_locator(minor_locator)
# Lets turn off the actual minor tick marks though
for tickmark in ax.xaxis.get_minor_ticks():
tickmark.tick1On = tickmark.tick2On = False
# Set the location of the minor ticks to the edge of pixels for the y grid
minor_locator = AutoMinorLocator(2)
ax.yaxis.set_minor_locator(minor_locator)
# Lets turn off the actual minor tick marks though
for tickmark in ax.yaxis.get_minor_ticks():
tickmark.tick1On = tickmark.tick2On = False
plt.show()
Which gives the following plot:
However I would like to extend this such that on mouse click I can highlight a 'row' in the pixelmap in green, e.g. if the user selected row 'C' I would have (I appreciate the green highlight is not clear for pixels with a 0 value):
I know how to deal with the mouse events but I'm not sure how to modify the colour of a single row in the pixelmap. It would also help if I could set labels for individual pixels of the pixel map to be retrieved on mouse click, as opposed to using the mouse x/y location to index the label lists.

I have figured out my own problem, with help from this question:
Plotting of 2D data : heatmap with different colormaps.
The code is below and the comments should explain the steps taken clearly.
import matplotlib.pyplot as plt
import numpy as np
from numpy.ma import masked_array
import matplotlib.cm as cm
from matplotlib.ticker import AutoMinorLocator
column_labels = list('ABCD')
row_labels = list('0123')
data = np.array([[0,1,2,0],
[1,0,1,1],
[1,2,0,0],
[0,0,0,1]])
fig, ax = plt.subplots()
# List to keep track of handles for each pixel row
pixelrows = []
# Lets create a normalizer for the whole data array
norm = plt.Normalize(vmin = np.min(data), vmax = np.max(data))
# Let's loop through and plot each pixel row
for i, row in enumerate(data):
# First create a mask to ignore all others rows than the current
zerosarray = np.ones_like(data)
zerosarray[i, :] = 0
plotarray = masked_array(data, mask=zerosarray)
# If we are not on the 3rd row down let's use the red colormap
if i != 2:
pixelrows.append(ax.matshow(plotarray, norm=norm, cmap=cm.Reds))
# Otherwise if we are at the 3rd row use the green colormap
else:
pixelrows.append(ax.matshow(plotarray, norm=norm, cmap=cm.Greens))
# put the major ticks at the middle of each cell
ax.set_xticks(np.arange(data.shape[0]), minor=False)
ax.set_yticks(np.arange(data.shape[1]), minor=False)
# want a more natural, table-like display
ax.xaxis.tick_top()
ax.set_xticklabels(row_labels, minor=False)
ax.set_yticklabels(column_labels, minor=False)
ax.yaxis.grid(True, which='minor', linestyle='-', color='k', linewidth = 0.3, alpha = 0.5)
ax.xaxis.grid(True, which='minor', linestyle='-', color='k', linewidth = 0.3, alpha = 0.5)
# Set the location of the minor ticks to the edge of pixels for the x grid
minor_locator = AutoMinorLocator(2)
ax.xaxis.set_minor_locator(minor_locator)
# Lets turn of the actual minor tick marks though
for tickmark in ax.xaxis.get_minor_ticks():
tickmark.tick1On = tickmark.tick2On = False
# Set the location of the minor ticks to the edge of pixels for the y grid
minor_locator = AutoMinorLocator(2)
ax.yaxis.set_minor_locator(minor_locator)
# Lets turn of the actual minor tick marks though
for tickmark in ax.yaxis.get_minor_ticks():
tickmark.tick1On = tickmark.tick2On = False
plt.show()

Related

Utilise a slider to update the position of legend in Matplotlib

I am trying to make a slider that can adjust the x and y coordinates of the legend anchor, but this does not seem to be updating on the plot. I keep getting the message in console "No artists with labels found to put in legend. Note that artists whose label start with an underscore are ignored when legend() is called with no argument", each time the slider value is updated.
Here is the code, taken from this example in the matplotlib docs
from cProfile import label
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import Slider, Button
# The parametrized function to be plotted
def f(t, amplitude, frequency):
return amplitude * np.sin(2 * np.pi * frequency * t)
t = np.linspace(0, 1, 1000)
# Define initial parameters
init_amplitude = 5
init_frequency = 3
# Create the figure and the line that we will manipulate
fig, ax = plt.subplots()
line, = ax.plot(t, f(t, init_amplitude, init_frequency), lw=2, label = "wave")
ax.set_xlabel('Time [s]')
# adjust the main plot to make room for the sliders
fig.subplots_adjust(left=0.25, bottom=0.25)
initx = 0.4
inity = 0.2
def l(x,y):
return (x,y)
legend = fig.legend(title = 'title', prop={'size': 8}, bbox_to_anchor = l(initx,inity))
legend.remove( )
# Make a horizontal slider to control the frequency.
axfreq = fig.add_axes([0.25, 0.1, 0.3, 0.3])
freq_slider = Slider(
ax=axfreq,
label='Frequency [Hz]',
valmin=0.1,
valmax=30,
valinit=init_frequency,
)
# Make a vertically oriented slider to control the amplitude
axamp = fig.add_axes([0.1, 0.25, 0.0225, 0.63])
amp_slider = Slider(
ax=axamp,
label="Amplitude",
valmin=0,
valmax=10,
valinit=init_amplitude,
orientation="vertical"
)
# The function to be called anytime a slider's value changes
def update(val):
legend = plt.legend(title = '$J_{xx}$', prop={'size': 8}, bbox_to_anchor= l(amp_slider.val, freq_slider.val))
legend.remove()
#line.set_ydata(f(t, amp_slider.val, freq_slider.val))
fig.canvas.draw_idle()
# register the update function with each slider
freq_slider.on_changed(update)
amp_slider.on_changed(update)
# Create a `matplotlib.widgets.Button` to reset the sliders to initial values.
resetax = fig.add_axes([0.8, 0.025, 0.1, 0.04])
button = Button(resetax, 'Reset', hovercolor='0.975')
def reset(event):
freq_slider.reset()
amp_slider.reset()
button.on_clicked(reset)
plt.show()
Is it even possible to update other matplotlib plot parameters like xticks/yticks or xlim/ylim with a slider, rather than the actual plotted data? I am asking so that I can speed up the graphing process, as I tend to lose a lot of time just getting the right plot parameters whilst making plots presentable, and would like to automate this in some way.

Matplotlib: how to automatically draw an axes title at the left-most position?

I'm drawing my axes title with the method ax.set_title("Horizontal Bars", ha="left", x=0, fontsize=16) and it draw as below:
How do I draw it in the left-most position, as the "title here" in red above? I know I can use a negative value for x, but I'd like to find this value automatically.
To dynamically generate the bounds you would do:
import matplotlib.pyplot as plt
import numpy as np
# Fixing random state for reproducibility
np.random.seed(19680801)
plt.rcdefaults()
fig, ax = plt.subplots()
# Example data
people = ('Tom', 'Dick', 'Harry', 'Slim', 'Jim')
y_pos = np.arange(len(people))
performance = 3 + 10 * np.random.rand(len(people))
error = np.random.rand(len(people))
ax.barh(y_pos, performance, xerr=error, align='center')
ax.set_yticks(y_pos)
ax.set_yticklabels(people)
ax.invert_yaxis() # labels read top-to-bottom
ax.set_xlabel('Performance')
# Get min x and max y
# get the inverse of the transformation from data coordinates to pixels
transf = ax.transData.inverted()
bb = plt.figure().get_window_extent(renderer = plt.figure().canvas.get_renderer())
bb_datacoords = bb.transformed(transf)
points = bb_datacoords.get_points()
x_lim = points[0][0]
y_lim = points[1][1]
ax.text(x=x_lim, y=y_lim, s="Horizontal Bars", weight="bold", fontsize=16) # <- Use text instead of title
which gives you an output of:

How can I get rid of this dummy mappable object and still draw my colorbar in Matplotlib?

I have the code below to plot circles add them to an ax.
I color the circles with respect to a colorbar.
However, to add the colorbar to my plot, I'm using sc=plot.scatter(...) and putting the colorbar using this dummy sc. Because plt.colorbar(sc,...) requires a mappable argument. How can I get rid of this dummy sc and still draw my colorbar?
import matplotlib
import numpy as np
import os
import matplotlib as mpl
from matplotlib.colors import Normalize
import matplotlib.cm as matplotlib_cm
from matplotlib import pyplot as plt
print(matplotlib.__version__)
row_list=['row1', 'row2', 'row3']
column_list=[2]
maxProcessiveGroupLength=2
index = column_list.index(maxProcessiveGroupLength)
plot1,panel1 = plt.subplots(figsize=(20+1.5*len(column_list), 10+1.5*len(row_list)))
plt.rc('axes', edgecolor='lightgray')
#make aspect ratio square
panel1.set_aspect(1.0)
panel1.text(0.1, 1.2, 'DEBUG', horizontalalignment='center', verticalalignment='top', fontsize=60, fontweight='bold', fontname='Arial',transform=panel1.transAxes)
if (len(column_list) > 1):
panel1.set_xlim([1, index + 1])
panel1.set_xticks(np.arange(0, index + 2, 1))
else:
panel1.set_xlim([0, len(column_list)])
panel1.set_xticks(np.arange(0, len(column_list)+1, 1))
if (len(row_list) > 1):
panel1.set_ylim([1, len(row_list)])
else:
panel1.set_ylim([0, len(row_list)])
panel1.set_yticks(np.arange(0, len(row_list) + 1, 1))
panel1.set_facecolor('white')
panel1.grid(color='black')
for edge, spine in panel1.spines.items():
spine.set_visible(True)
spine.set_color('black')
xlabels = None
if (index is not None):
xlabels = column_list[0:index + 1]
ylabels = row_list
cmap = matplotlib_cm.get_cmap('Blues') # Looks better
v_min = 2
v_max = 20
norm = Normalize(v_min, v_max)
bounds = np.arange(v_min, v_max+1, 2)
# Plot the circles with color
for row_index, row in enumerate(row_list):
for column_index, processive_group_length in enumerate(column_list):
radius=0.35
color=10+column_index*3+row_index*3
circle = plt.Circle((column_index + 0.5, row_index + 0.5), radius,color=cmap(norm(color)), fill=True)
panel1.add_patch(circle)
# Used for scatter plot
x = []
y = []
c = []
for row_index, processiveGroupLength in enumerate(row_list):
x.append(row_index)
y.append(row_index)
c.append(0.5)
# This code defines the ticks on the color bar
# plot the scatter plot
sc = plt.scatter(x, y, s=0, c=c, cmap=cmap, vmin=v_min, vmax=v_max, edgecolors='black')
# colorbar to the bottom
cb = plt.colorbar(sc ,orientation='horizontal') # this works because of the scatter
cb.ax.set_xlabel("colorbar label", fontsize=50, labelpad=25)
# common for horizontal colorbar and vertical colorbar
cbax = cb.ax
cbax.tick_params(labelsize=40)
text_x = cbax.xaxis.label
text_y = cbax.yaxis.label
font = mpl.font_manager.FontProperties(size=40)
text_x.set_font_properties(font)
text_y.set_font_properties(font)
# CODE GOES HERE TO CENTER X-AXIS LABELS...
panel1.set_xticklabels([])
mticks = panel1.get_xticks()
panel1.set_xticks((mticks[:-1] + mticks[1:]) / 2, minor=True)
panel1.tick_params(axis='x', which='minor', length=0, labelsize=50)
if xlabels is not None:
panel1.set_xticklabels(xlabels,minor=True)
panel1.xaxis.set_ticks_position('top')
plt.tick_params(
axis='x', # changes apply to the x-axis
which='major', # both major and minor ticks are affected
bottom=False, # ticks along the bottom edge are off
top=False) # labels along the bottom edge are off
# CODE GOES HERE TO CENTER Y-AXIS LABELS...
panel1.set_yticklabels([])
mticks = panel1.get_yticks()
panel1.set_yticks((mticks[:-1] + mticks[1:]) / 2, minor=True)
panel1.tick_params(axis='y', which='minor', length=0, labelsize=50)
panel1.set_yticklabels(ylabels, minor=True) # fontsize
plt.tick_params(
axis='y', # changes apply to the x-axis
which='major', # both major and minor ticks are affected
left=False) # labels along the bottom edge are off
plt.show()
From the documentation of colorbar:
Note that one can create a ScalarMappable "on-the-fly" to generate
colorbars not attached to a previously drawn artist
In your example, the following allows for creating the same colorbar without the scatter plot:
cb = plt.colorbar(mpl.cm.ScalarMappable(norm=norm, cmap=cmap), orientation='horizontal')

How to extend colorbar for 'out-of-range' values in Bokeh or Holoview?

In Matplotlib, there is the colorbar property extend that makes pointed end(s) for out-of- range values. How would you do the third subplots with Bokeh or Holoview?
I added a Matplotlib example below:
import numpy as np
import matplotlib.pyplot as plt
# setup some generic data
N = 37
x, y = np.mgrid[:N, :N]
Z = (np.cos(x*0.2) + np.sin(y*0.3))
# mask out the negative and positive values, respectively
Zpos = np.ma.masked_less(Z, 0)
Zneg = np.ma.masked_greater(Z, 0)
fig, (ax1, ax2, ax3) = plt.subplots(figsize=(13, 3), ncols=3)
# plot just the positive data and save the
# color "mappable" object returned by ax1.imshow
pos = ax1.imshow(Zpos, cmap='Blues', interpolation='none')
# add the colorbar using the figure's method,
# telling which mappable we're talking about and
# which axes object it should be near
fig.colorbar(pos, ax=ax1)
# repeat everything above for the negative data
neg = ax2.imshow(Zneg, cmap='Reds_r', interpolation='none')
fig.colorbar(neg, ax=ax2)
# Plot both positive and negative values between +/- 1.2
pos_neg_clipped = ax3.imshow(Z, cmap='RdBu', vmin=-1.2, vmax=1.2,
interpolation='none')
# Add minorticks on the colorbar to make it easy to read the
# values off the colorbar.
cbar = fig.colorbar(pos_neg_clipped, ax=ax3, extend='both')
cbar.minorticks_on()
plt.show()
Example plot, colorbar with pointed ends to point out higher values:
Bokeh has a current PR (not finished) to try to add functionality like this: https://github.com/bokeh/bokeh/pull/10781

grouped bar chart with broken axis in matplotlib [duplicate]

I'm trying to create a plot using pyplot that has a discontinuous x-axis. The usual way this is drawn is that the axis will have something like this:
(values)----//----(later values)
where the // indicates that you're skipping everything between (values) and (later values).
I haven't been able to find any examples of this, so I'm wondering if it's even possible. I know you can join data over a discontinuity for, eg, financial data, but I'd like to make the jump in the axis more explicit. At the moment I'm just using subplots but I'd really like to have everything end up on the same graph in the end.
Paul's answer is a perfectly fine method of doing this.
However, if you don't want to make a custom transform, you can just use two subplots to create the same effect.
Rather than put together an example from scratch, there's an excellent example of this written by Paul Ivanov in the matplotlib examples (It's only in the current git tip, as it was only committed a few months ago. It's not on the webpage yet.).
This is just a simple modification of this example to have a discontinuous x-axis instead of the y-axis. (Which is why I'm making this post a CW)
Basically, you just do something like this:
import matplotlib.pylab as plt
import numpy as np
# If you're not familiar with np.r_, don't worry too much about this. It's just
# a series with points from 0 to 1 spaced at 0.1, and 9 to 10 with the same spacing.
x = np.r_[0:1:0.1, 9:10:0.1]
y = np.sin(x)
fig,(ax,ax2) = plt.subplots(1, 2, sharey=True)
# plot the same data on both axes
ax.plot(x, y, 'bo')
ax2.plot(x, y, 'bo')
# zoom-in / limit the view to different portions of the data
ax.set_xlim(0,1) # most of the data
ax2.set_xlim(9,10) # outliers only
# hide the spines between ax and ax2
ax.spines['right'].set_visible(False)
ax2.spines['left'].set_visible(False)
ax.yaxis.tick_left()
ax.tick_params(labeltop='off') # don't put tick labels at the top
ax2.yaxis.tick_right()
# Make the spacing between the two axes a bit smaller
plt.subplots_adjust(wspace=0.15)
plt.show()
To add the broken axis lines // effect, we can do this (again, modified from Paul Ivanov's example):
import matplotlib.pylab as plt
import numpy as np
# If you're not familiar with np.r_, don't worry too much about this. It's just
# a series with points from 0 to 1 spaced at 0.1, and 9 to 10 with the same spacing.
x = np.r_[0:1:0.1, 9:10:0.1]
y = np.sin(x)
fig,(ax,ax2) = plt.subplots(1, 2, sharey=True)
# plot the same data on both axes
ax.plot(x, y, 'bo')
ax2.plot(x, y, 'bo')
# zoom-in / limit the view to different portions of the data
ax.set_xlim(0,1) # most of the data
ax2.set_xlim(9,10) # outliers only
# hide the spines between ax and ax2
ax.spines['right'].set_visible(False)
ax2.spines['left'].set_visible(False)
ax.yaxis.tick_left()
ax.tick_params(labeltop='off') # don't put tick labels at the top
ax2.yaxis.tick_right()
# Make the spacing between the two axes a bit smaller
plt.subplots_adjust(wspace=0.15)
# This looks pretty good, and was fairly painless, but you can get that
# cut-out diagonal lines look with just a bit more work. The important
# thing to know here is that in axes coordinates, which are always
# between 0-1, spine endpoints are at these locations (0,0), (0,1),
# (1,0), and (1,1). Thus, we just need to put the diagonals in the
# appropriate corners of each of our axes, and so long as we use the
# right transform and disable clipping.
d = .015 # how big to make the diagonal lines in axes coordinates
# arguments to pass plot, just so we don't keep repeating them
kwargs = dict(transform=ax.transAxes, color='k', clip_on=False)
ax.plot((1-d,1+d),(-d,+d), **kwargs) # top-left diagonal
ax.plot((1-d,1+d),(1-d,1+d), **kwargs) # bottom-left diagonal
kwargs.update(transform=ax2.transAxes) # switch to the bottom axes
ax2.plot((-d,d),(-d,+d), **kwargs) # top-right diagonal
ax2.plot((-d,d),(1-d,1+d), **kwargs) # bottom-right diagonal
# What's cool about this is that now if we vary the distance between
# ax and ax2 via f.subplots_adjust(hspace=...) or plt.subplot_tool(),
# the diagonal lines will move accordingly, and stay right at the tips
# of the spines they are 'breaking'
plt.show()
I see many suggestions for this feature but no indication that it's been implemented. Here is a workable solution for the time-being. It applies a step-function transform to the x-axis. It's a lot of code, but it's fairly simple since most of it is boilerplate custom scale stuff. I have not added any graphics to indicate the location of the break, since that is a matter of style. Good luck finishing the job.
from matplotlib import pyplot as plt
from matplotlib import scale as mscale
from matplotlib import transforms as mtransforms
import numpy as np
def CustomScaleFactory(l, u):
class CustomScale(mscale.ScaleBase):
name = 'custom'
def __init__(self, axis, **kwargs):
mscale.ScaleBase.__init__(self)
self.thresh = None #thresh
def get_transform(self):
return self.CustomTransform(self.thresh)
def set_default_locators_and_formatters(self, axis):
pass
class CustomTransform(mtransforms.Transform):
input_dims = 1
output_dims = 1
is_separable = True
lower = l
upper = u
def __init__(self, thresh):
mtransforms.Transform.__init__(self)
self.thresh = thresh
def transform(self, a):
aa = a.copy()
aa[a>self.lower] = a[a>self.lower]-(self.upper-self.lower)
aa[(a>self.lower)&(a<self.upper)] = self.lower
return aa
def inverted(self):
return CustomScale.InvertedCustomTransform(self.thresh)
class InvertedCustomTransform(mtransforms.Transform):
input_dims = 1
output_dims = 1
is_separable = True
lower = l
upper = u
def __init__(self, thresh):
mtransforms.Transform.__init__(self)
self.thresh = thresh
def transform(self, a):
aa = a.copy()
aa[a>self.lower] = a[a>self.lower]+(self.upper-self.lower)
return aa
def inverted(self):
return CustomScale.CustomTransform(self.thresh)
return CustomScale
mscale.register_scale(CustomScaleFactory(1.12, 8.88))
x = np.concatenate((np.linspace(0,1,10), np.linspace(9,10,10)))
xticks = np.concatenate((np.linspace(0,1,6), np.linspace(9,10,6)))
y = np.sin(x)
plt.plot(x, y, '.')
ax = plt.gca()
ax.set_xscale('custom')
ax.set_xticks(xticks)
plt.show()
Check the brokenaxes package:
import matplotlib.pyplot as plt
from brokenaxes import brokenaxes
import numpy as np
fig = plt.figure(figsize=(5,2))
bax = brokenaxes(
xlims=((0, .1), (.4, .7)),
ylims=((-1, .7), (.79, 1)),
hspace=.05
)
x = np.linspace(0, 1, 100)
bax.plot(x, np.sin(10 * x), label='sin')
bax.plot(x, np.cos(10 * x), label='cos')
bax.legend(loc=3)
bax.set_xlabel('time')
bax.set_ylabel('value')
A very simple hack is to
scatter plot rectangles over the axes' spines and
draw the "//" as text at that position.
Worked like a charm for me:
# FAKE BROKEN AXES
# plot a white rectangle on the x-axis-spine to "break" it
xpos = 10 # x position of the "break"
ypos = plt.gca().get_ylim()[0] # y position of the "break"
plt.scatter(xpos, ypos, color='white', marker='s', s=80, clip_on=False, zorder=100)
# draw "//" on the same place as text
plt.text(xpos, ymin-0.125, r'//', fontsize=label_size, zorder=101, horizontalalignment='center', verticalalignment='center')
Example Plot:
For those interested, I've expanded upon #Paul's answer and added it to the matplotlib wrapper proplot. It can do axis "jumps", "speedups", and "slowdowns".
There is no way currently to add "crosses" that indicate the discrete jump like in Joe's answer, but I plan to add this in the future. I also plan to add a default "tick locator" that sets sensible default tick locations depending on the CutoffScale arguments.
Adressing Frederick Nord's question how to enable parallel orientation of the diagonal "breaking" lines when using a gridspec with ratios unequal 1:1, the following changes based on the proposals of Paul Ivanov and Joe Kingtons may be helpful. Width ratio can be varied using variables n and m.
import matplotlib.pylab as plt
import numpy as np
import matplotlib.gridspec as gridspec
x = np.r_[0:1:0.1, 9:10:0.1]
y = np.sin(x)
n = 5; m = 1;
gs = gridspec.GridSpec(1,2, width_ratios = [n,m])
plt.figure(figsize=(10,8))
ax = plt.subplot(gs[0,0])
ax2 = plt.subplot(gs[0,1], sharey = ax)
plt.setp(ax2.get_yticklabels(), visible=False)
plt.subplots_adjust(wspace = 0.1)
ax.plot(x, y, 'bo')
ax2.plot(x, y, 'bo')
ax.set_xlim(0,1)
ax2.set_xlim(10,8)
# hide the spines between ax and ax2
ax.spines['right'].set_visible(False)
ax2.spines['left'].set_visible(False)
ax.yaxis.tick_left()
ax.tick_params(labeltop='off') # don't put tick labels at the top
ax2.yaxis.tick_right()
d = .015 # how big to make the diagonal lines in axes coordinates
# arguments to pass plot, just so we don't keep repeating them
kwargs = dict(transform=ax.transAxes, color='k', clip_on=False)
on = (n+m)/n; om = (n+m)/m;
ax.plot((1-d*on,1+d*on),(-d,d), **kwargs) # bottom-left diagonal
ax.plot((1-d*on,1+d*on),(1-d,1+d), **kwargs) # top-left diagonal
kwargs.update(transform=ax2.transAxes) # switch to the bottom axes
ax2.plot((-d*om,d*om),(-d,d), **kwargs) # bottom-right diagonal
ax2.plot((-d*om,d*om),(1-d,1+d), **kwargs) # top-right diagonal
plt.show()
This is a hacky but pretty solution for x-axis breaks.
The solution is based on https://matplotlib.org/stable/gallery/subplots_axes_and_figures/broken_axis.html, which gets rid of the problem with positioning the break above the spine, solved by How can I plot points so they appear over top of the spines with matplotlib?
from matplotlib.patches import Rectangle
import matplotlib.pyplot as plt
def axis_break(axis, xpos=[0.1, 0.125], slant=1.5):
d = slant # proportion of vertical to horizontal extent of the slanted line
anchor = (xpos[0], -1)
w = xpos[1] - xpos[0]
h = 1
kwargs = dict(marker=[(-1, -d), (1, d)], markersize=12, zorder=3,
linestyle="none", color='k', mec='k', mew=1, clip_on=False)
axis.add_patch(Rectangle(
anchor, w, h, fill=True, color="white",
transform=axis.transAxes, clip_on=False, zorder=3)
)
axis.plot(xpos, [0, 0], transform=axis.transAxes, **kwargs)
fig, ax = plt.subplots(1,1)
plt.plot(np.arange(10))
axis_break(ax, xpos=[0.1, 0.12], slant=1.5)
axis_break(ax, xpos=[0.3, 0.31], slant=-10)
if you want to replace an axis label, this would do the trick:
from matplotlib import ticker
def replace_pos_with_label(fig, pos, label, axis):
fig.canvas.draw() # this is needed to set up the x-ticks
labs = axis.get_xticklabels()
labels = []
locs = []
for text in labs:
x = text._x
lab = text._text
if x == pos:
lab = label
labels.append(lab)
locs.append(x)
axis.xaxis.set_major_locator(ticker.FixedLocator(locs))
axis.set_xticklabels(labels)
fig, ax = plt.subplots(1,1)
plt.plot(np.arange(10))
replace_pos_with_label(fig, 0, "-10", axis=ax)
replace_pos_with_label(fig, 6, "$10^{4}$", axis=ax)
axis_break(ax, xpos=[0.1, 0.12], slant=2)