Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python/Matplotlib - Is there a way to make a discontinuous axis?

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.

like image 252
Justin S Avatar asked Apr 13 '11 23:04

Justin S


People also ask

How do I restrict axis in matplotlib?

Import matplotlib. To set x-axis scale to log, use xscale() function and pass log to it. To plot the graph, use plot() function. To set the limits of the x-axis, use xlim() function and pass max and min value to it. To set the limits of the y-axis, use ylim() function and pass top and bottom value to it.

How do I avoid scientific notation in matplotlib?

If you want to disable both the offset and scientific notaion, you'd use ax. ticklabel_format(useOffset=False, style='plain') .

How do you change axis increments in Python?

To change the range of X and Y axes, we can use xlim() and ylim() methods.


2 Answers

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

enter image description here

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

enter image description here

like image 122
2 revs Avatar answered Oct 13 '22 06:10

2 revs


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

enter image description here

like image 42
Paul Avatar answered Oct 13 '22 05:10

Paul