2011-04-13 26 views
55

Sto provando a creare un grafico usando pyplot che ha un asse x discontinuo. Il solito modo questo viene disegnato è che l'asse avrà qualcosa di simile:Python/Matplotlib - Esiste un modo per creare un asse discontinuo?

(valori) ---- ---- // (successivi valori)

dove il // indica che si sta saltando tutto tra (valori) e (valori successivi).

Non sono stato in grado di trovare alcun esempio di questo, quindi mi chiedo se è persino possibile. So che puoi unire i dati su una discontinuità per, ad esempio, i dati finanziari, ma mi piacerebbe rendere più esplicito il salto sull'asse. Al momento sto solo usando le sottotrame ma mi piacerebbe davvero che tutto finisse sullo stesso grafico alla fine.

risposta

56

risposta di Paolo è un metodo perfettamente bene di fare questo.

Tuttavia, se non si desidera eseguire una trasformazione personalizzata, è possibile utilizzare solo due sottotrame per creare lo stesso effetto.

Invece di unire un esempio da zero, c'è lo an excellent example of this written by Paul Ivanov negli esempi matplotlib (è solo nell'attuale consiglio git, poiché è stato eseguito solo pochi mesi fa. Non è ancora nella pagina Web.).

Questa è solo una semplice modifica di questo esempio per avere un asse x discontinuo anziché l'asse y. (Che è il motivo per cui sto facendo questo post un CW)

In pratica, basta fare qualcosa di simile:

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

Per aggiungere le linee degli assi spezzate // effetto, possiamo fare questo (ancora una volta, modificato dal l'esempio di Paolo Ivanov):

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

+5

Non avrei potuto dirlo meglio anch'io;) –

+1

Il metodo per rendere l'effetto '//' sembra funzionare bene solo se il rapporto delle figure secondarie è 1: 1. Sai come farlo funzionare con qualsiasi rapporto introdotto da, ad es. 'GridSpec (width_ratio = [n, m])'? –

23

Vedo molti suggerimenti per questa funzione ma nessuna indicazione che sia stata implementata. Ecco una soluzione praticabile per il tempo. Applica una trasformazione della funzione passo all'asse x. È un sacco di codice, ma è abbastanza semplice dato che la maggior parte di esso è roba in scala personalizzata. Non ho aggiunto alcuna grafica per indicare la posizione della pausa, dal momento che è una questione di stile. Buona fortuna per finire il lavoro.

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

+0

credo che sarà solo hanno a che fare, per ora. Questa sarà la mia prima volta a fare scherzi con gli assi personalizzati, quindi dovremo solo vedere come va. –

+0

C'è un piccolo refuso in 'def transform' di' InvertedCustomTransform', dove dovrebbe leggere 'self.upper' invece di' upper'. Grazie per l'ottimo esempio, però! –

+0

puoi aggiungere un paio di linee per mostrare come usare la tua classe? –

0

Affrontare la domanda di Frederick Nord su come abilitare l'orientamento parallelo delle linee di "rottura" diagonale quando si utilizza un gridspec con rapporti ineguali 1: 1, possono essere utili le seguenti modifiche basate sulle proposte di Paul Ivanov e Joe Kington. Il rapporto larghezza può essere variato usando le variabili n e 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() 
6

controllare il pacchetto brokenaxes:

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

example from brokenaxes

+0

Non è possibile "' from brokenaxes import brokenaxes'' in Pycharm Community 2016.3.2 dopo l'installazione. @ ben.dichter –

+0

C'è stato un bug. L'ho riparato. Esegui 'pip install brokenaxes == 0.2' per installare la versione fissa del codice. –

+0

Sembra interagire male con ax.grid (True) – innisfree

Problemi correlati