luke_16
luke_16

Reputation: 187

Matplotlib: keep grid lines behind the graph but the y and x axis above

I am having a hard time plotting grid lines under my graphs without messing with the main x and y axis zorder:

import matplotlib.pyplot as plt
import numpy as np


N = 5
menMeans = (20, 35, 30, 35, 27)
menStd =   (2, 3, 4, 1, 2)

ind = np.arange(N)  # the x locations for the groups
width = 0.35       # the width of the bars

fig, ax = plt.subplots()
rects1 = ax.bar(ind, menMeans, width, color='r', yerr=menStd, alpha=0.9, linewidth = 0,zorder=3)

womenMeans = (25, 32, 34, 20, 25)
womenStd =   (3, 5, 2, 3, 3)
rects2 = ax.bar(ind+width, womenMeans, width, color='y', yerr=womenStd, alpha=0.9, linewidth = 0,zorder=3)

# add some
ax.set_ylabel('Scores')
ax.set_title('Scores by group and gender')
ax.set_xticks(ind+width)
ax.set_xticklabels( ('G1', 'G2', 'G3', 'G4', 'G5') )

ax.legend( (rects1[0], rects2[0]), ('Men', 'Women') )

fig.gca().yaxis.grid(True, which='major', linestyle='-', color='#D9D9D9',zorder=2, alpha = .9)
[line.set_zorder(4) for line in ax.lines]

def autolabel(rects):
    # attach some text labels
    for rect in rects:
        height = rect.get_height()
        ax.text(rect.get_x()+rect.get_width()/2., 1.05*height, '%d'%int(height),
                ha='center', va='bottom')

autolabel(rects1)
autolabel(rects2)

plt.show()

The example is taken from matplotlib's own, and I have tweaked a little to show how to make the problem appear. I cannot post images but if you run the code you will see that the bars are plotted above the horizontal grid lines as well as above the x and y axis. I do not want the x and y axis being hidden by the graph, especially when the ticks are also getting blocked.

Upvotes: 15

Views: 24364

Answers (6)

blaylockbk
blaylockbk

Reputation: 3311

borrowing from @tueda's MRE...

This produces the undesired result of the grid lines over the bars. As the question points out, increasing the bar zorder places the bars over the axis ticks.

import matplotlib.pyplot as plt

y = (20, 35, 30, 35, 27)
x = range(len(y))

fig, ax = plt.subplots()
ax.bar(x, y)
ax.grid(True)
ax.tick_params(direction="in", length=10)

enter image description here

Solution: Manually plot horizontal and vertical lines for each tick under the bar instead of using grid()...

import matplotlib.pyplot as plt

y = (20, 35, 30, 35, 27)
x = range(len(y))

fig, ax = plt.subplots()
ax.bar(x, y)
ax.tick_params(direction="in", length=10)

for i in ax.get_xticks():
    ax.axvline(i, color="g", zorder=-10)
for i in ax.get_yticks():
    ax.axhline(i, color="r", zorder=-10)

enter image description here

Upvotes: 1

tueda
tueda

Reputation: 982

I know this is quite an old question but the problem seems to essentially remain, so let me write down an MRE and my solution with Python 3.9.6 and Matplotlib 3.4.2.

Problem

As the OP was aware, the default zorder for bar plots is relatively low. The following code leads to a plot where both the tick lines and the grid lines are above the bars.

import matplotlib.pyplot as plt

y = (20, 35, 30, 35, 27)
x = range(len(y))

fig, ax = plt.subplots()
ax.bar(x, y)
ax.grid(True)
ax.tick_params(direction="in", length=10)

plt.show()

First example

It is possible to use ax.set_axisbelow(True) to move both the tick lines and the grid lines behind the bars, but then the ticks on the x-axis are hidden by the bars.

import matplotlib.pyplot as plt

y = (20, 35, 30, 35, 27)
x = range(len(y))

fig, ax = plt.subplots()
ax.bar(x, y)
ax.grid(True)
ax.tick_params(direction="in", length=10)
ax.set_axisbelow(True)  # This line added.

plt.show()

Second example

Solution

Redraw the tick lines after all elements in the plot are drawn. (I assume they are opaque and the redrawing is harmless.)

import matplotlib.artist
import matplotlib.backend_bases
import matplotlib.pyplot as plt


class TickRedrawer(matplotlib.artist.Artist):
    """Artist to redraw ticks."""

    __name__ = "ticks"

    zorder = 10

    @matplotlib.artist.allow_rasterization
    def draw(self, renderer: matplotlib.backend_bases.RendererBase) -> None:
        """Draw the ticks."""
        if not self.get_visible():
            self.stale = False
            return

        renderer.open_group(self.__name__, gid=self.get_gid())

        for axis in (self.axes.xaxis, self.axes.yaxis):
            loc_min, loc_max = axis.get_view_interval()

            for tick in axis.get_major_ticks() + axis.get_minor_ticks():
                if tick.get_visible() and loc_min <= tick.get_loc() <= loc_max:
                    for artist in (tick.tick1line, tick.tick2line):
                        artist.draw(renderer)

        renderer.close_group(self.__name__)
        self.stale = False


y = (20, 35, 30, 35, 27)
x = range(len(y))

fig, ax = plt.subplots()
ax.bar(x, y)
ax.grid(True)
ax.tick_params(direction="in", length=10)
ax.set_axisbelow(True)
ax.add_artist(TickRedrawer())  # This line added.

plt.show()

Solution

Upvotes: 4

Gabriel
Gabriel

Reputation: 9432

A easier and better solution is to copy the lines from the grid command and disable the grid and again add the lines again but with correct zorder:

ax.grid(True)
lines = ax.xaxis.get_gridlines().copy() + ax.yaxis.get_gridlines().copy()
ax.grid(False)
for l in lines:
    ax.add_line(l)
    l.set_zorder(0)

Upvotes: 1

nedim
nedim

Reputation: 1857

I had the same problem of axes getting drawn below the plot line when I have grid lines in the background:

ax.yaxis.grid()  # grid lines
ax.set_axisbelow(True)  # grid lines are behind the rest

The solution that worked for me was to set the zorder argument of the plot() function to a value between 1 and 2. It is not immediately clear, but the zorder value can be any number. From the documentation for the matplotlib.artist.Artist class:

set_zorder(level)

Set the zorder for the artist. Artists with lower zorder values are drawn first.

ACCEPTS: any number

Therefore:

for i in range(5):
    ax.plot(range(10), np.random.randint(10, size=10), zorder=i / 100.0 + 1)

I haven't checked for values outside this range, maybe they would also work.

Upvotes: 12

volvoxdiekugelamoebe
volvoxdiekugelamoebe

Reputation: 21

I had the same problem of plotting above the axes as @luke_16. In my case, it arose when using the option ax.set_axisbelow(True) to set the grind behind the plots.

My workaround for this bug is, not to use the onboard-grid, but to simulate it:

def grid_selfmade(ax,minor=False):
    y_axis=ax.yaxis.get_view_interval()
    for xx in ax.xaxis.get_ticklocs():
        ax.plot([xx,xx],y_axis,linestyle=':',linewidth=0.5,zorder=0)
    if minor==True:
        for xx in ax.xaxis.get_ticklocs(minor=True):
            ax.plot([xx,xx],y_axis,linestyle=':',linewidth=0.5,zorder=0)
    x_axis=ax.xaxis.get_view_interval()
    for yy in ax.yaxis.get_ticklocs():
        ax.plot(x_axis,[yy,yy],linestyle=':',linewidth=0.5,zorder=0,)
    if minor==True:
        for yy in ax.yaxis.get_ticklocs(minor=True):
            ax.plot(x_axis,[yy,yy],linestyle=':',linewidth=0.5,zorder=0)

This function only needs the current axis-instance and then draws an onboard-like grid behind everything else at the major ticks (optional also for minor ticks).

For having the axis and axis ticks on top of the graphs, it is necessary to leave ax.set_axisbelow(False) to False and not to use zorder>2 in plots. I managed the zorder of plots without any zorder-option by changing the order of plot-commands in code.

Upvotes: 2

Francesco Montesano
Francesco Montesano

Reputation: 8658

I have tried matplotlib 1.2.1, 1.3.1rc2 and master (commit 06d014469fc5c79504a1b40e7d45bc33acc00773)

To get the axis spines on top of the the bars you can do the following:

for k, spine in ax.spines.items():  #ax.spines is a dictionary
    spine.set_zorder(10)

EDIT

It seems that I can't make the tick lines to go on top of the bars. I've tried

1. ax.tick_params(direction='in', length=10, color='k', zorder=10)
   #This increases the size of the lines to 10 points, 
   #but the lines stays hidden behind  the bars
2. for l in ax.yaxis.get_ticklines():
       l.set_zorder(10)

and some other way with no results. It seems that when drawing the bars they are put on top and the zorder is ignored

A workaround could be to draw the tick lines outwards

ax.tick_params(direction='out', length=4, color='k', zorder=10)

or both in and outwards, using direction='inout'

EDIT2

I've done some test after @tcaswell comments.

If zorder in the ax.bar function is set to be <=2, the axis, ticklines and grid lines are drawn above the bars. If the valus is >2.01 (the default value for axis) the bars are drawn on top of the axis, ticklines and grid. Then it possible to set larger values to the spines (as above) but any attempt to change the zorder of the ticklines is simply ignored (although the values are updated on the corresponding artists).

I've tried the to use zorder=1 for the bar and zorder=0 for the grid and the grid is drawn on top of the bars. So zorder is ignored.

recap

It seems to me that ticklines and grid zorder are just ignored and kept to the default values. For me this is a bug somehow related with bar or some patches.

BTW, I do remember changing successfully the zorder in ticklines when using imshow

Upvotes: 6

Related Questions