Square major/minor grid for axes with different limits
Asked Answered
M

2

1

I have a plot with a background grid. I need grid cells to be square (both major grid and minor grid cells) even though the limits of X and Y axes are different.

My current code is as follows:

import matplotlib.pyplot as plt
import matplotlib.ticker as plticker
import numpy as np

data = [0.014,  0.84,  0.95, -0.42, -0.79,  0.84, 0.98,  1.10,   0.56, -0.49]


fig, ax = plt.subplots(figsize=(20, 5))
ax.minorticks_on()

# Set major and minor grid lines on X
ax.set_xticks(np.arange(0, 10, 0.2))
ax.xaxis.set_minor_locator(plticker.MultipleLocator(base=0.2 / 5.))
for xmaj in ax.xaxis.get_majorticklocs():
        ax.axvline(x=xmaj, ls='-', color='red', linewidth=0.8)
for xmin in ax.xaxis.get_minorticklocs():
    ax.axvline(x=xmin, ls=':', color='red', linewidth=0.6)

# Set major and minor grid lines on Y
ylim = int(np.ceil(max(abs(min(data)), max(data))))
yticks = np.arange(-ylim, ylim + 0.5, 0.5)
ax.set_yticks(yticks)
ax.yaxis.set_minor_locator(plticker.MultipleLocator(base=0.5 / 5.))
for ymaj in ax.yaxis.get_majorticklocs():
        ax.axhline(y=ymaj, ls='-', color='red', linewidth=0.8)
for ymin in ax.yaxis.get_minorticklocs():
    ax.axhline(y=ymin, ls=':', color='red', linewidth=0.6)

ax.axis([0, 10, -ylim, ylim])
fig.tight_layout()

# Plot
ax.plot(data)

# Set equal aspect ratio NOT WORKING
plt.gca().set_aspect('equal', adjustable='box')
plt.show()

Which generates the following plot: enter image description here

Large grid cells contain 5 smaller cells each. However, the aspect ratio of large grid is not 1. Question: How can I make sure that large grid is square?

EDIT Current approach is to set same tick locations as suggested by @ImportanceOfBeingErnest, but change Y labels:

ylim = int(np.ceil(max(abs(min(data)), max(data))))
yticks = np.arange(-ylim, ylim + 0.2, 0.2)

ax.set_yticks(yticks)

labels = ['{:.1f}'.format(v if abs(v) < 1e-3 else (1 if v > 0 else -1)*((0.5 - abs(v)%0.5) + abs(v))) 
          if i%2==0 else "" for i, v in enumerate(np.arange(-ylim, ylim, 0.2))]
ax.set_yticklabels(labels)

Result: seems too hacky. enter image description here

Morice answered 1/8, 2018 at 11:24 Comment(2)
By "square" you mean square in data units or square in display units?Domineering
Square in display units.Morice
D
2

When using equal aspect ratio and aiming for a square grid you would need to use the same tickspacing for both axes. This can be achieved with a MultipleLocator where the interval needs to be the same for x and y axis.

In general, grids can be created with the grid command.

import matplotlib.pyplot as plt
import matplotlib.ticker as mticker
import numpy as np

data = [0.014,  0.84,  0.95, -0.42, -0.79,  0.84, 0.98,  1.10,   0.56, -0.49]


fig, ax = plt.subplots(figsize=(20, 5))
ax.minorticks_on()

# Set major and minor grid lines on X
ax.xaxis.set_major_locator(mticker.MultipleLocator(base=.5))
ax.xaxis.set_minor_locator(mticker.MultipleLocator(base=0.5 / 5.))

ax.yaxis.set_major_locator(mticker.MultipleLocator(base=.5))
ax.yaxis.set_minor_locator(mticker.MultipleLocator(base=0.5 / 5.))

ax.grid(ls='-', color='red', linewidth=0.8)
ax.grid(which="minor", ls=':', color='red', linewidth=0.6)

## Set limits
ylim = int(np.ceil(max(abs(min(data)), max(data))))
ax.axis([0, 10, -ylim, ylim])
plt.gca().set_aspect('equal', adjustable='box')
fig.tight_layout()

# Plot
ax.plot(data)

plt.show()

enter image description here

If you instead want to have different tick spacings with square major cells in the grid, you would need to give up the equal aspect ratio and instead set it to the quotient of the tick spacings.

import matplotlib.pyplot as plt
import matplotlib.ticker as mticker
import numpy as np

data = [0.014,  0.84,  0.95, -0.42, -0.79,  0.84, 0.98,  1.10,   0.56, -0.49]


fig, ax = plt.subplots(figsize=(20, 5))
ax.minorticks_on()

xm = 0.2
ym = 0.25

# Set major and minor grid lines on X
ax.xaxis.set_major_locator(mticker.MultipleLocator(base=xm))
ax.xaxis.set_minor_locator(mticker.MultipleLocator(base=xm / 5.))

ax.yaxis.set_major_locator(mticker.MultipleLocator(base=ym))
ax.yaxis.set_minor_locator(mticker.MultipleLocator(base=ym / 5.))

ax.grid(ls='-', color='red', linewidth=0.8)
ax.grid(which="minor", ls=':', color='red', linewidth=0.6)

## Set limits
ylim = int(np.ceil(max(abs(min(data)), max(data))))
ax.axis([0, 10, -ylim, ylim])
plt.gca().set_aspect(xm/ym, adjustable='box')
fig.tight_layout()


# Plot
ax.plot(data)

plt.show()

enter image description here

To then get rid of every second ticklabel, an option is

fmt = lambda x,p: "%.2f" % x if not x%(2*ym) else ""
ax.yaxis.set_major_formatter(mticker.FuncFormatter(fmt))
Domineering answered 1/8, 2018 at 11:52 Comment(5)
I agree with you about tickspacing. However, I need to have different tick values. Y axis values should go with step size of 0.5, while X axis values go with step size of 0.2. To achieve that I am trying to tinker with tick values at tick locations.Morice
You currently have one constraint too much. You could give up the equal aspect ratio of the plot. Is that what you mean?Domineering
I want both :) I don't want to give up equal aspect ratio. I am currently doing this:labels = ['{:.1f}'.format(v if v == 0 else (1 if v > 0 else -1)*((0.5 - v%0.5) + v)) if i%2==0 else "" for i, v in enumerate(np.arange(-ylim, ylim, 0.2))] ax.set_yticklabels(labels) Which sort of works, but there are some undesirable values on Y axis.Morice
Mathematics say: You cannot do both. But I will update the answer with how you could use non-equal aspect.Domineering
Could you please have a look at my edit of the question.Morice
H
0

You should be able to achieve this by using the same locator for the both axis. However matplotlib has a limitation currently, so here's a workaround:

# matplotlib doesnt (currently) allow two axis to share the same locator
# so make two wrapper locators and combine their view intervals
def share_locator(locator):
    class _SharedLocator(matplotlib.ticker.Locator):
        def tick_values(self, vmin, vmax):
            return locator.tick_values(vmin, vmax)

        def __call__(self):
            min0, max0 = shared_locators[0].axis.get_view_interval()
            min1, max1 = shared_locators[1].axis.get_view_interval()
            return self.tick_values(min(min0, min1), max(max0, max1))

    shared_locators = (_SharedLocator(), _SharedLocator())
    return shared_locators

Use like:

lx, ly = share_locator(matplotlib.ticker.AutoLocator())  # or any other locator
ax.xaxis.set_major_locator(lx)
ax.yaxis.set_major_locator(ly)
Hydrostat answered 28/11, 2020 at 13:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.