15

I need help customizing my plots. I want the canvas to look approximately like the default 2D-graph template from MacOS's Grapher (see screenshot).

To clarify - I need

  • a centered axis
  • a grid (preferably with an additional darker grid every 1 unit)
  • axislines with arrows
  • only one zero at the origo (when I tried my best, I got one zero from the x-axis and a second one from the y-axis.), slightly moved to the left so it's not behind the y-axis

I really appreciate your help!

Joe Kington
  • 275,208
  • 71
  • 604
  • 463
Milo Wielondek
  • 4,164
  • 3
  • 33
  • 45
  • Surely possible with matplotlib, but could be a hassle. TeX with TikZ may be able to do this more easily, if that is an option. Certainly the centered axes and grid is easy in TikZ, at least. – Steve Tjoa Jan 14 '11 at 18:54

2 Answers2

41

This definitely falls under the category of more trouble than it's worth with matplotlib, but here you go. Also, for the basic case, have a look at the centering spines demo in the documentation.

You can do this in a few different ways, but for the best visual effect, consider something along the lines of the following. It's far from perfect, but it's reasonably flexible:

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)

I deliberately didn't set the x and y major tick intervals to 1, but that's easy to do. ax.xaxis.set_major_locator(MultipleLocator(1))

Now you can just call center_spines to do something like this:

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

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

alt text

Joe Kington
  • 275,208
  • 71
  • 604
  • 463
  • @Steve - Thanks! @mewoshh - It might be a bit easier in gnuplot. However, I don't know offhand how to make arrows on the axis lines in gnuplot (that update when the plot is rescaled). The rest (centered axis spines) is easier in gnuplot, but isn't that difficult in matplotlib, either. – Joe Kington Jan 18 '11 at 19:16
  • 6
    This really ought to be built into matplotlib. It may not be as useful for scientific plots but it's damn useful for educational purposes and homework. – Iguananaut Mar 08 '13 at 00:54
  • center_spines() is nice looking, but it seems xlabel and ylabel are not showing any more. Also could you modify the code so as to always make both axes appearing? This would mean that if the origin is out of the plot area, the two arrowed axes would cross in the nearest corner to it. – user1850133 Apr 26 '13 at 19:12
  • Unfortunately, currently it doesn't work: cannot find `mpl.patheffects._Base`. – Ilya V. Schurov Sep 19 '20 at 15:24
  • Not working for me either: `/usr/local/bin/python3 /Users/me/code/python3/moonbooks.org-2.py Traceback (most recent call last): File "/Users/me/code/python3/moonbooks.org.py", line 54, in class EndArrow(mpl.patheffects._Base): AttributeError: module 'matplotlib.patheffects' has no attribute '_Base'` – DevonDahon Oct 12 '20 at 07:25
3

Centered axes, see an example here (look for "zeroed spines"): http://matplotlib.sourceforge.net/examples/pylab_examples/spine_placement_demo.html

grid: ax.grid(True)

Get rid of the zero at the origin: see set_ticks(...) here: http://matplotlib.sourceforge.net/api/axis_api.html

What else did you look for?

ev-br
  • 24,968
  • 9
  • 65
  • 78