2011-01-14 13 views
11

Ho bisogno di aiuto per personalizzare i miei grafici. Voglio che il canvas assomigli approssimativamente al modello predefinito del grafico 2D da Grapher di MacOS (vedi screenshot).Centro origine in matplotlib

Per chiarire - devo

  • un asse centrata
  • una griglia (preferibilmente con un ulteriore scuro griglia ogni 1 unità)
  • axislines con frecce
  • sola zero all'originale (quando ho fatto del mio meglio, ho ottenuto uno zero dall'asse xe un secondo dall'asse y.), leggermente spostato a sinistra, quindi non è dietro il asse y

Ho davvero apprezzato il vostro aiuto!

+0

Sicuramente possibile con matplotlib, ma potrebbe essere una seccatura. TeX con TikZ potrebbe essere in grado di farlo più facilmente, se questa è un'opzione. Certamente gli assi centrati e la griglia sono facili in TikZ, almeno. –

risposta

31

Questo sicuramente rientra nella categoria di più problemi di quanti ne valga con Matplotlib, ma ecco qui. Inoltre, per il caso di base, dai uno sguardo allo centering spines demo in the documentation.

È possibile eseguire questa operazione in diversi modi, ma per ottenere il miglior effetto visivo, prendere in considerazione qualcosa in linea con quanto segue. E 'ben lungi dall'essere perfetto, ma è abbastanza flessibile:

import matplotlib.pyplot as plt 
import matplotlib as mpl 
import matplotlib.patheffects 
import numpy as np 

def center_spines(ax=None, centerx=0, centery=0): 
    """Centers the axis spines at <centerx, centery> on the axis "ax", and 
    places arrows at the end of the axis spines.""" 
    if ax is None: 
     ax = plt.gca() 

    # Set the axis's spines to be centered at the given point 
    # (Setting all 4 spines so that the tick marks go in both directions) 
    ax.spines['left'].set_position(('data', centerx)) 
    ax.spines['bottom'].set_position(('data', centery)) 
    ax.spines['right'].set_position(('data', centerx - 1)) 
    ax.spines['top'].set_position(('data', centery - 1)) 

    # Draw an arrow at the end of the spines 
    ax.spines['left'].set_path_effects([EndArrow()]) 
    ax.spines['bottom'].set_path_effects([EndArrow()]) 

    # Hide the line (but not ticks) for "extra" spines 
    for side in ['right', 'top']: 
     ax.spines[side].set_color('none') 

    # On both the x and y axes... 
    for axis, center in zip([ax.xaxis, ax.yaxis], [centerx, centery]): 
     # Turn on minor and major gridlines and ticks 
     axis.set_ticks_position('both') 
     axis.grid(True, 'major', ls='solid', lw=0.5, color='gray') 
     axis.grid(True, 'minor', ls='solid', lw=0.1, color='gray') 
     axis.set_minor_locator(mpl.ticker.AutoMinorLocator()) 

     # Hide the ticklabels at <centerx, centery> 
     formatter = CenteredFormatter() 
     formatter.center = center 
     axis.set_major_formatter(formatter) 

    # Add offset ticklabels at <centerx, centery> using annotation 
    # (Should probably make these update when the plot is redrawn...) 
    xlabel, ylabel = map(formatter.format_data, [centerx, centery]) 
    ax.annotate('(%s, %s)' % (xlabel, ylabel), (centerx, centery), 
      xytext=(-4, -4), textcoords='offset points', 
      ha='right', va='top') 

# Note: I'm implementing the arrows as a path effect rather than a custom 
#  Spines class. In the long run, a custom Spines class would be a better 
#  way to go. One of the side effects of this is that the arrows aren't 
#  reversed when the axes are reversed! 

class EndArrow(mpl.patheffects._Base): 
    """A matplotlib patheffect to add arrows at the end of a path.""" 
    def __init__(self, headwidth=5, headheight=5, facecolor=(0,0,0), **kwargs): 
     super(mpl.patheffects._Base, self).__init__() 
     self.width, self.height = headwidth, headheight 
     self._gc_args = kwargs 
     self.facecolor = facecolor 

     self.trans = mpl.transforms.Affine2D() 

     self.arrowpath = mpl.path.Path(
       np.array([[-0.5, -0.2], [0.0, 0.0], [0.5, -0.2], 
          [0.0, 1.0], [-0.5, -0.2]]), 
       np.array([1, 2, 2, 2, 79])) 

    def draw_path(self, renderer, gc, tpath, affine, rgbFace): 
     scalex = renderer.points_to_pixels(self.width) 
     scaley = renderer.points_to_pixels(self.height) 

     x0, y0 = tpath.vertices[-1] 
     dx, dy = tpath.vertices[-1] - tpath.vertices[-2] 
     azi = np.arctan2(dy, dx) - np.pi/2.0 
     trans = affine + self.trans.clear(
       ).scale(scalex, scaley 
       ).rotate(azi 
       ).translate(x0, y0) 

     gc0 = renderer.new_gc() 
     gc0.copy_properties(gc) 
     self._update_gc(gc0, self._gc_args) 

     if self.facecolor is None: 
      color = rgbFace 
     else: 
      color = self.facecolor 

     renderer.draw_path(gc0, self.arrowpath, trans, color) 
     renderer.draw_path(gc, tpath, affine, rgbFace) 
     gc0.restore() 

class CenteredFormatter(mpl.ticker.ScalarFormatter): 
    """Acts exactly like the default Scalar Formatter, but yields an empty 
    label for ticks at "center".""" 
    center = 0 
    def __call__(self, value, pos=None): 
     if value == self.center: 
      return '' 
     else: 
      return mpl.ticker.ScalarFormatter.__call__(self, value, pos) 

Ho volutamente non ho impostato il x e gli intervalli di graduazione principali y a 1, ma che è facile da fare. ax.xaxis.set_major_locator(MultipleLocator(1))

Ora si può chiamare center_spines a fare qualcosa di simile:

x = np.arange(-5, 5) 
y = x 

line, = plt.plot(x, y) 
center_spines() 
plt.axis('equal') 
plt.show() 

alt text

+2

Ben fatto! Sembra un sacco di lavoro; Sono contento di aver mollato presto. :-) –

+0

Wow, sembra davvero pesante. Grazie! Cosa consiglieresti invece di matplotlib? – 0sh

+0

@Steve - Grazie! @mewoshh - Potrebbe essere un po 'più semplice in gnuplot. Tuttavia, non so a mano a mano come creare frecce sulle linee degli assi in gnuplot (che si aggiorna quando la trama viene riscalata). Il resto (spine dorsali centrate) è più semplice in gnuplot, ma non è nemmeno così difficile in matplotlib. –