2012-05-07 25 views
16

Necesito dos conjuntos de datos superpuestos con diferentes escalas del eje Y en Matplotlib. Los datos contienen valores positivos y negativos. Quiero que los dos ejes compartan un origen, pero Matplotlib no alinea las dos escalas por defecto.Eje Matplotlib con dos escalas origen compartido

import numpy as np 
import matplotlib.pyplot as plt 

fig = plt.figure() 
ax1 = fig.add_subplot(111) 
ax2 = ax1.twinx() 

ax1.bar(range(6), (2, -2, 1, 0, 0, 0)) 
ax2.plot(range(6), (0, 2, 8, -2, 0, 0)) 
plt.show() 

supongo que es posible realizar algún cálculo con .get_ylim() y .set_ylim() dos alinear las dos escalas. ¿Hay una solución más fácil?

Output from the sample above

Respuesta

27

utilizar la función align_yaxis():

import numpy as np 
import matplotlib.pyplot as plt 

def align_yaxis(ax1, v1, ax2, v2): 
    """adjust ax2 ylimit so that v2 in ax2 is aligned to v1 in ax1""" 
    _, y1 = ax1.transData.transform((0, v1)) 
    _, y2 = ax2.transData.transform((0, v2)) 
    inv = ax2.transData.inverted() 
    _, dy = inv.transform((0, 0)) - inv.transform((0, y1-y2)) 
    miny, maxy = ax2.get_ylim() 
    ax2.set_ylim(miny+dy, maxy+dy) 


fig = plt.figure() 
ax1 = fig.add_subplot(111) 
ax2 = ax1.twinx() 

ax1.bar(range(6), (2, -2, 1, 0, 0, 0)) 
ax2.plot(range(6), (0, 2, 8, -2, 0, 0)) 

align_yaxis(ax1, 0, ax2, 0) 
plt.show() 

enter image description here

14

el fin de garantizar que el Y-límites se mantienen (así que no hay puntos de datos se desplazan fuera de la parcela) , y para equilibrar el ajuste de ambos ejes y, realicé algunas adiciones a la respuesta de @ HYRY:

def align_yaxis(ax1, v1, ax2, v2): 
    """adjust ax2 ylimit so that v2 in ax2 is aligned to v1 in ax1""" 
    _, y1 = ax1.transData.transform((0, v1)) 
    _, y2 = ax2.transData.transform((0, v2)) 
    adjust_yaxis(ax2,(y1-y2)/2,v2) 
    adjust_yaxis(ax1,(y2-y1)/2,v1) 

def adjust_yaxis(ax,ydif,v): 
    """shift axis ax by ydiff, maintaining point v at the same location""" 
    inv = ax.transData.inverted() 
    _, dy = inv.transform((0, 0)) - inv.transform((0, ydif)) 
    miny, maxy = ax.get_ylim() 
    miny, maxy = miny - v, maxy - v 
    if -miny>maxy or (-miny==maxy and dy > 0): 
     nminy = miny 
     nmaxy = miny*(maxy+dy)/(miny+dy) 
    else: 
     nmaxy = maxy 
     nminy = maxy*(miny+dy)/(maxy+dy) 
    ax.set_ylim(nminy+v, nmaxy+v) 
3

@ respuesta de drevicko falla para mí cuando el trazado de las siguientes dos secuencias de puntos:

l1 = [0.03, -0.6, 1, 0.05] 
l2 = [0.8, 0.9, 1, 1.1] 
fig, ax1 = plt.subplots() 
ax1.plot(l1) 
ax2 = ax1.twinx() 
ax2.plot(l2, color='r') 
align_yaxis(ax1, 0, ax2, 0) 

enter image description here

... así que aquí está mi versión:

def align_yaxis(ax1, ax2): 
    """Align zeros of the two axes, zooming them out by same ratio""" 
    axes = (ax1, ax2) 
    extrema = [ax.get_ylim() for ax in axes] 
    tops = [extr[1]/(extr[1] - extr[0]) for extr in extrema] 
    # Ensure that plots (intervals) are ordered bottom to top: 
    if tops[0] > tops[1]: 
     axes, extrema, tops = [list(reversed(l)) for l in (axes, extrema, tops)] 

    # How much would the plot overflow if we kept current zoom levels? 
    tot_span = tops[1] + 1 - tops[0] 

    b_new_t = extrema[0][0] + tot_span * (extrema[0][1] - extrema[0][0]) 
    t_new_b = extrema[1][1] - tot_span * (extrema[1][1] - extrema[1][0]) 
    axes[0].set_ylim(extrema[0][0], b_new_t) 
    axes[1].set_ylim(t_new_b, extrema[1][1]) 

Hay, en principio infinita diferente posibilidades de alinear los ceros (u otros valores, que las otras soluciones proporcionadas aceptan): siempre que coloque cero en el eje y, puede acercar cada una de las dos series para que encaje. Simplemente escogemos la posición de manera que, después de la transformación, los dos cubran un intervalo vertical de la misma altura. O en otros términos, los minimizamos de un mismo factor en comparación con el gráfico no alineado. (Esto hace no quiere decir que 0 se encuentra en la mitad de la trama: Esto sucederá, por ejemplo, si una parcela todo es negativo y el otro todo es positivo.)

Numpy versión:

def align_yaxis_np(ax1, ax2): 
    """Align zeros of the two axes, zooming them out by same ratio""" 
    axes = np.array([ax1, ax2]) 
    extrema = np.array([ax.get_ylim() for ax in axes]) 
    tops = extrema[:,1]/(extrema[:,1] - extrema[:,0]) 
    # Ensure that plots (intervals) are ordered bottom to top: 
    if tops[0] > tops[1]: 
     axes, extrema, tops = [a[::-1] for a in (axes, extrema, tops)] 

    # How much would the plot overflow if we kept current zoom levels? 
    tot_span = tops[1] + 1 - tops[0] 

    extrema[0,1] = extrema[0,0] + tot_span * (extrema[0,1] - extrema[0,0]) 
    extrema[1,0] = extrema[1,1] + tot_span * (extrema[1,0] - extrema[1,1]) 
    [axes[i].set_ylim(*extrema[i]) for i in range(2)] 
0

I' he cocinado una solución a partir de lo anterior que alineará cualquier número de ejes:

def align_yaxis_np(axes): 
    """Align zeros of the two axes, zooming them out by same ratio""" 
    axes = np.array(axes) 
    extrema = np.array([ax.get_ylim() for ax in axes]) 

    # reset for divide by zero issues 
    for i in range(len(extrema)): 
     if np.isclose(extrema[i, 0], 0.0): 
      extrema[i, 0] = -1 
     if np.isclose(extrema[i, 1], 0.0): 
      extrema[i, 1] = 1 

    # upper and lower limits 
    lowers = extrema[:, 0] 
    uppers = extrema[:, 1] 

    # if all pos or all neg, don't scale 
    all_positive = False 
    all_negative = False 
    if lowers.min() > 0.0: 
     all_positive = True 

    if uppers.max() < 0.0: 
     all_negative = True 

    if all_negative or all_positive: 
     # don't scale 
     return 

    # pick "most centered" axis 
    res = abs(uppers+lowers) 
    min_index = np.argmin(res) 

    # scale positive or negative part 
    multiplier1 = abs(uppers[min_index]/lowers[min_index]) 
    multiplier2 = abs(lowers[min_index]/uppers[min_index]) 

    for i in range(len(extrema)): 
     # scale positive or negative part based on which induces valid 
     if i != min_index: 
      lower_change = extrema[i, 1] * -1*multiplier2 
      upper_change = extrema[i, 0] * -1*multiplier1 
      if upper_change < extrema[i, 1]: 
       extrema[i, 0] = lower_change 
      else: 
       extrema[i, 1] = upper_change 

     # bump by 10% for a margin 
     extrema[i, 0] *= 1.1 
     extrema[i, 1] *= 1.1 

    # set axes limits 
    [axes[i].set_ylim(*extrema[i]) for i in range(len(extrema))] 

ejemplo: aligned axes

Cuestiones relacionadas