How to Make a Discontinuous Axis in Matplotlib

Is there a way to make a discontinuous axis in Matplotlib?

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()

Sample Image

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()

Sample Image

Break // in x axis of matplotlib

You could adapt the matplotlib example for a break in the x-axis directly:

"""
Broken axis example, where the x-axis will have a portion cut out.
"""
import matplotlib.pylab as plt
import numpy as np

x = np.linspace(0,10,100)
x[75:] = np.linspace(40,42.5,25)

y = np.sin(x)

f,(ax,ax2) = plt.subplots(1,2,sharey=True, facecolor='w')

# plot the same data on both axes
ax.plot(x, y)
ax2.plot(x, y)

ax.set_xlim(0,7.5)
ax2.set_xlim(40,42.5)

# 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(labelright='off')
ax2.yaxis.tick_right()

# 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)
ax.plot((1-d,1+d),(1-d,1+d), **kwargs)

kwargs.update(transform=ax2.transAxes) # switch to the bottom axes
ax2.plot((-d,+d), (1-d,1+d), **kwargs)
ax2.plot((-d,+d), (-d,+d), **kwargs)

# 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()

matplotlib broken x-axis example

For your purposes, just plot your data twice (once on each axis, ax and ax2 and set your xlims appropriately. The "break lines" should move to match the new break because they are plotted in relative axis coordinates rather than data coordinates.

The break lines are just unclipped plot lines drawn between a pair of points. E.g. ax.plot((1-d,1+d), (-d,+d), **kwargs) plots the break line between point (1-d,-d) and (1+d,+d) on the first axis: this is the bottom righthand one. If you want to change the graident, change these values appropriately. For example, to make this one steeper, try ax.plot((1-d/2,1+d/2), (-d,+d), **kwargs)

Matplotlib multiple broken axis

You can define the width of the different subplots using the gridspec_kw argument to plt.subplots. In there, we define the width_ratios. In this case, you have the first and third subplots twice as wide as the middle one, so we can use (2,1,2) for the ratios.

We then need to make sure we turn off the correct spines: so for the left axes (ax1), we turn off the right spine. In the middle (ax2) we turn off both left and right, and on the right axes (ax3), we just turn off the left spine.

On the right axes, I move the y-axis ticks to the right hand side using ax3.yaxis.tick_right()

In the middle axes, I hide the ticks with ax2.tick_params(axis='y', length=0) --- note we can't just use something like ax2.set_yticks([]) here because that would affect the other axes, since we use sharey=True.

I've then taken the code to draw the diagonal lines from my other answer here, and added extra lines for the second break in the x axis.

All together, that looks like this:

import matplotlib.pyplot as plt
import numpy as np

days = list(range(0,500))
values = list(np.random.randint(low = 10,high=100,size=len(days)))

# use width_ratios to define the width of each subplot
# depending on the range we want to plot
f, (ax1, ax2, ax3) = plt.subplots(1, 3, sharey=True, facecolor='w',
gridspec_kw={'width_ratios': (2, 1, 2)})

ax1.plot(days, values)
ax2.plot(days, values)
ax3.plot(days, values)

ax1.set_xlim(0,100) # x-axis range limited to 0 - 100
ax2.set_xlim(250, 300) # x-axis range limited to 250 - 300
ax3.set_xlim(400, 500) # x-axis range limited to 400 - 500

# hide the spines between ax and ax2
ax1.spines['right'].set_visible(False)
ax2.spines['left'].set_visible(False)
ax2.spines['right'].set_visible(False)
ax3.spines['left'].set_visible(False)

# Move right hand axes ticks to right hand side
ax3.yaxis.tick_right()

# Turn off ticks on middle axes; so we don't affect the other
# axes ticks, let's just set the length to 0 here
ax2.tick_params(axis='y', length=0)

# Draw the diagonal lines to show broken axes
d = 2. # proportion of vertical to horizontal extent of the slanted line
kwargs = dict(marker=[(-1, -d), (1, d)], markersize=12,
linestyle="none", color='k', mec='k', mew=1, clip_on=False)
ax1.plot([1, 1], [0, 1], transform=ax1.transAxes, **kwargs)
ax2.plot([0, 0], [0, 1], transform=ax2.transAxes, **kwargs)
ax2.plot([1, 1], [0, 1], transform=ax2.transAxes, **kwargs)
ax3.plot([0, 0], [0, 1], transform=ax3.transAxes, **kwargs)

plt.savefig('2brokenaxes.png')

Sample Image

Create broken axis in graph

Borrowing heavily from the example here, we can modify it slightly to move to a broken x axis instead of y axis.

The main changes from that example to here:

  • change to sharey=True
  • turn off left/right spines instead of bottom/top
  • change position/angle of diagonal lines to show broken axes
  • change proportion of two axes since the left axes spans a shorter range

Note for the last point, here I have made them a 1:10 ratio, because I have made the second axes span 40 months, and the first axes space 4 months. You will likely want a slightly different ratio, so will need to adjust the xlim for each axes accordingly, and also the width_ratios parameter.

import matplotlib.dates as mdates
import pandas as pd
import numpy as np

drange = pd.date_range('2015-01-01', periods=78, freq='M')
data = pd.DataFrame({'Datum': drange, 'Percentage': np.random.randn(len(drange))})

fig, (ax1, ax2) = plt.subplots(ncols=2, sharey=True, gridspec_kw={'width_ratios': (1, 10)})
fig.subplots_adjust(wspace=0.05)

ax1.plot('Datum', 'Percentage', data=data)
ax2.plot('Datum', 'Percentage', data=data)

fmt_half_year1 = mdates.MonthLocator(interval=6)
fmt_half_year2 = mdates.MonthLocator(interval=6)
ax1.xaxis.set_major_locator(fmt_half_year1)
ax2.xaxis.set_major_locator(fmt_half_year2)

fmt_month1 = mdates.MonthLocator()
fmt_month2 = mdates.MonthLocator()
ax1.xaxis.set_minor_locator(fmt_month1)
ax2.xaxis.set_minor_locator(fmt_month2)

ax1.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m'))
ax2.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m'))

ax1.format_xdata = mdates.DateFormatter('%Y-%m')
ax2.format_xdata = mdates.DateFormatter('%Y-%m')

ax1.grid(True)
ax2.grid(True)

# hide the spines between ax1 and ax2
ax1.spines.right.set_visible(False)
ax2.spines.left.set_visible(False)
ax2.yaxis.tick_right()
ax2.tick_params(labelright=False) # don't put tick labels at the top
ax1.xaxis.tick_bottom()

datemin1 = np.datetime64(data['Datum'][0], 'M')
datemax1 = np.datetime64(data['Datum'][4], 'M')
ax1.set_xlim(datemin1, datemax1)

datemin2 = np.datetime64(data['Datum'][37], 'M')
datemax2 = np.datetime64(data['Datum'][77], 'M')
ax2.set_xlim(datemin2, datemax2)

fig.text(s='Datum', x=0.5, y=0.05)
ax1.set_ylabel('Percentage')
fig.suptitle('Percentage Trained')

fig.autofmt_xdate()

d = 2. # proportion of vertical to horizontal extent of the slanted line
kwargs = dict(marker=[(-1, -d), (1, d)], markersize=12,
linestyle="none", color='k', mec='k', mew=1, clip_on=False)
ax1.plot([1, 1], [0, 1], transform=ax1.transAxes, **kwargs)
ax2.plot([0, 0], [0, 1], transform=ax2.transAxes, **kwargs)

plt.show()

Sample Image

Matplotlib improve broken axis in subplots

Your task was twofold: first, you wanted the horizontal ratio of the subplot to be 8 to 2. Second, you made the assumption that you wanted to set up a broken axis. The horizontal ratio of the subplot can be handled by gridspec_ratios=[4,1] as shown in the comments; the second is to apply the broken axes from the Broken Axis. Vertical lines have been omitted.

import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import matplotlib.ticker as ticker
import datetime as dt
from datetime import datetime

csv = './data/Data_20220114.csv'
df = pd.read_csv(csv, header=None, names=['time', 'tide', 'ho', 'tp', 'lo', 'mwd'])
xp = [datetime.strptime(d, "%d/%m/%YT%H:%M") for d in df['time']]
xs = mdates.date2num(xp)
months_fmt = mdates.DateFormatter('%d/%m/%Y\n')

fig, (ax11, ax12) = plt.subplots(1, 2, figsize=(12,3), gridspec_kw=dict(width_ratios=[4,1]), sharex=False)
fig.subplots_adjust(wspace=0.05)

ax11.plot(xs, df['ho'], linewidth = 1, color = 'k')
ax11.xaxis.set_major_locator(mdates.MonthLocator(interval=3))
ax11.xaxis.set_major_formatter(months_fmt)
plt.setp(ax11.get_xticklabels(), rotation=45, ha='right')
ax11.set_ylabel ('$H_o\ [m]$', size = 12)
ax11.grid(True, ls='--')

ax12.plot(xs[-1440:], df['ho'][-1440:], linewidth = 1, color = 'k')
ax12.xaxis.set_major_locator(mdates.MonthLocator(interval=1))
ax12.xaxis.set_major_formatter(months_fmt)
plt.setp(ax12.get_xticklabels(), rotation=45, ha='right')
ax12.grid(True, ls='--')

ax11.spines.right.set_visible(False)
ax12.spines.left.set_visible(False)
ax11.yaxis.tick_left()
ax12.tick_params(labelleft=False)
ax12.yaxis.tick_right()

d = 0.8 # proportion of vertical to horizontal extent of the slanted line
kwargs = dict(marker=[(-1, -d), (1, d)], markersize=12,
linestyle="none", color='k', mec='k', mew=1, clip_on=False)
ax11.plot([1, 1], [1, 0], transform=ax11.transAxes, **kwargs)
ax12.plot([0, 0], [1, 0], transform=ax12.transAxes, **kwargs)

plt.show()

Sample Image

Formatting a broken y axis in python matplotlib

OK, well I have made some edits and it now works (just not quite as I'd originally intended) and there is a new solution here which should be pushed to the matplotlib page soon.

The key code is this section

# arguments to pass to plot, just so we don't keep repeating them
kwargs = dict(transform=ax.transAxes, color='k', clip_on=False)
ax.plot((-d, +d), (-d, +d), **kwargs) # top-left diagonal
ax.plot((1 - d, 1 + d), (-d, +d), **kwargs) # top-right diagonal

kwargs.update(transform=ax2.transAxes) # switch to the bottom axes
ax2.plot((-d, +d), (1 - d, 1 + d), **kwargs) # bottom-left diagonal
ax2.plot((1 - d, 1 + d), (1 - d, 1 + d), **kwargs) # bottom-right diagonal

You can amend it to

axis_break1 = 450
axis_break2 = 9951
x_min = -0.75
x_max = len(data.index)
l = 0.2 # "break" line length
kwargs = dict(color="k", clip_on=False, linewidth=1)
ax.plot((x_min - l, x_min + l), (axis_break2, axis_break2), **kwargs)# top-left
ax.plot((x_max - l, x_max + l), (axis_break2, axis_break2), **kwargs)# top-right
ax2.plot((x_min - l, x_min + l), (axis_break1, axis_break1), **kwargs)# bottom-left
ax2.plot((x_max - l, x_max + l), (axis_break1, axis_break1), **kwargs)# bottom-right

Which leaves us with a neat (if slightly less fancy) result.
resulting graph

Or a revised (and more elegant) version (from ImportanceOfBeingErnest):

d = .25  # proportion of vertical to horizontal extent of the slanted line
kwargs = dict(marker=[(-1, -d), (1, d)], markersize=12,
linestyle="none", color='k', mec='k', mew=1, clip_on=False)
ax.plot([0, 1], [0, 0], transform=ax.transAxes, **kwargs)
ax2.plot([0, 1], [1, 1], transform=ax2.transAxes, **kwargs)

which results in diagonal lines as originally intended.
Sample Image

discontinous axis in subplot - python matplotlib

You could use gridspec to layout the shape and location of the axes:

import numpy as np
import matplotlib.gridspec as gridspec
import matplotlib.pyplot as plt

gs = gridspec.GridSpec(4, 2)
ax00 = plt.subplot(gs[:2, 0])
ax01 = plt.subplot(gs[:2, 1])
ax10a = plt.subplot(gs[2, 0])
ax10b = plt.subplot(gs[3, 0])
ax11 = plt.subplot(gs[2:, 1])

x = np.linspace(-1, 1, 500)
y = 100*np.cos(10*x)**2*np.exp(-x**2)
for ax in (ax00, ax01, ax10a, ax10b, ax11):
ax.plot(x, y)

ax10a.set_ylim(60, 110)
ax10b.set_ylim(0, 10)

ax10a.spines['bottom'].set_visible(False)
ax10b.spines['top'].set_visible(False)
ax10a.xaxis.tick_top()
ax10a.tick_params(labeltop='off') # don't put tick labels at the top
ax10b.xaxis.tick_bottom()

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=ax10a.transAxes, color='k', clip_on=False)
ax10a.plot((-d,+d),(-d,+d), **kwargs) # top-left diagonal
ax10a.plot((1-d,1+d),(-d,+d), **kwargs) # top-right diagonal

kwargs.update(transform=ax10b.transAxes) # switch to the bottom axes
ax10b.plot((-d,+d),(1-d,1+d), **kwargs) # bottom-left diagonal
ax10b.plot((1-d,1+d),(1-d,1+d), **kwargs) # bottom-right diagonal

plt.tight_layout()
plt.show()

Sample Image

matplotlib create broken axis in subplot

I might suggest to use only two subplots, one at the top and one at the bottom. Then, divide the upper one into two via mpl_toolkits.axes_grid1.make_axes_locatable.

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1 import make_axes_locatable

x = np.random.uniform(0, 10, 40)
y = np.concatenate([np.random.uniform(0, 1, 30), np.random.uniform(0, 100, 10)])
y2 = np.random.uniform(0, 1, 40)

fig, axes = plt.subplots(nrows=2, sharex=True)

ax = axes[0]
divider = make_axes_locatable(ax)
ax2 = divider.new_vertical(size="100%", pad=0.1)
fig.add_axes(ax2)

ax.scatter(x, y)
ax.set_ylim(0, 1)
ax.spines['top'].set_visible(False)
ax2.scatter(x, y)
ax2.set_ylim(10, 100)
ax2.tick_params(bottom=False, labelbottom=False)
ax2.spines['bottom'].set_visible(False)

# From https://matplotlib.org/examples/pylab_examples/broken_axis.html
d = .015 # how big to make the diagonal lines in axes coordinates
# arguments to pass to plot, just so we don't keep repeating them
kwargs = dict(transform=ax2.transAxes, color='k', clip_on=False)
ax2.plot((-d, +d), (-d, +d), **kwargs) # top-left diagonal
ax2.plot((1 - d, 1 + d), (-d, +d), **kwargs) # top-right diagonal

kwargs.update(transform=ax.transAxes) # switch to the bottom axes
ax.plot((-d, +d), (1 - d, 1 + d), **kwargs) # bottom-left diagonal
ax.plot((1 - d, 1 + d), (1 - d, 1 + d), **kwargs) # bottom-right diagonal

#create bottom subplot as usual
axes[1].scatter(x, y2)

plt.show()

Sample Image

Draw matplotlib broken axis on left side only

  1. Not breaking the right axis Since the way to break the axis is to make two sets of axes, the only way I can think of the do this is to draw a line going from the top right corner of the lower axis to the bottom right corner of the upper axis. This can be done using transforms to get the relevant corners of the two axes in the axes coordinates of one of the axes
# Get the points from the lower-right corner of ax1 
# to the top-right corner of ax2 in the axes coordinates
# of ax1
low = (1, 0)
high = ax1.transAxes.inverted().transform(
ax2.transAxes.transform((1, 1))
)

and then plotting them with transform=ax1.transAxes

ax1.plot(
*list(zip(low, high)), "-k",
transform=ax1.transAxes, clip_on=False,
lw=ax1.spines.right.get_linewidth(),
)

2 and 3. Adjust the whitespace and position of the break This can be achieved using any way of creating different axes in matplotlib, such as passing gridspec_kw to plt.subplots e.g.

gridspec_kw = dict(height_ratios=(1, 4), hspace=0.30)
fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True, gridspec_kw=gridspec_kw)

As the name suggest height_ratios defines the ratios of the heights of the axes. hspace sets the vertical space between the two sets of axes (if you want to break the x-axis instead you can use wspace)

Applying these changes to the example you link, and removing the diagonal lines on the right hand side by changing

ax1.plot([0, 1], [0, 0], transform=ax1.transAxes, **kwargs)
ax2.plot([0, 1], [1, 1], transform=ax2.transAxes, **kwargs)

to

ax1.plot(0, 0, transform=ax1.transAxes, **kwargs)
ax2.plot(0, 1, transform=ax2.transAxes, **kwargs)

you get the following plot
Sample Image



Related Topics



Leave a reply



Submit