40

I'd like to make a plot in Python and have x range display ticks in multiples of pi.

Is there a good way to do this, not manually?

I'm thinking of using matplotlib, but other options are fine.

EDIT 3: EL_DON's solution worked for me like this:

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

f,ax=plt.subplots(figsize=(20,10))
x=np.linspace(-10*np.pi, 10*np.pi,1000)
y=np.sin(x)

ax.plot(x/np.pi,y)

ax.xaxis.set_major_formatter(tck.FormatStrFormatter('%g $\pi$'))
ax.xaxis.set_major_locator(tck.MultipleLocator(base=1.0))

plt.style.use("ggplot")


plt.show()

giving:

nice sine graph

EDIT 2 (solved in EDIT 3!): EL_DON's answer doesn't seem to work right for me:

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

f,ax=plt.subplots(figsize=(20,10))
x=np.linspace(-10*np.pi, 10*np.pi)
y=np.sin(x)

ax.plot(x/np.pi,y)

ax.xaxis.set_major_formatter(tck.FormatStrFormatter('%g $\pi$'))
ax.xaxis.set_major_locator(tck.MultipleLocator(base=1.0))

plt.style.use("ggplot")

plt.show()

gives me

plot

which really doesn't look right

Zubo
  • 1,543
  • 2
  • 20
  • 26
  • It's not resolved because there aren't enough points in linspace. Try `x=np.linspace(-10*np.pi, 10*np.pi,1001)` where 1001 should be large enough that it looks smooth. – EL_DON Nov 16 '16 at 22:45
  • @EL_DON, yes, thanks, that did it! – Zubo Nov 16 '16 at 22:58
  • 1
    There is a better answer available now. I recommend changing the accepted answer to @ScottCentoni's answer. – EL_DON Dec 06 '18 at 18:53
  • @EL_DON thanks, done – Zubo Dec 12 '18 at 11:10
  • 1
    There is a solution in the [matplotlib docs](https://matplotlib.org/3.1.0/gallery/units/radian_demo.html). – Dan Jul 26 '19 at 23:00
  • @Dan nice! That should be the preferred solution, then? – Zubo Jul 30 '19 at 11:02
  • 1
    @Zubo I think so. Although, I don't know they have it as a separate file though instead of incorporating the function into the package. Weird. Also, it's not so nice to be forced to use their `cos` implementation rather then numpy's... – Dan Jul 30 '19 at 12:24

7 Answers7

45

This is inspired by Python Data Science Handbook, although Sage attempts to do without explicit parameters.

EDIT: I've generalized this to allow you to supply as optional parameters the denominator, the value of the unit, and the LaTeX label for the unit. A class definition is included if you find that helpful.

import numpy as np
import matplotlib.pyplot as plt

def multiple_formatter(denominator=2, number=np.pi, latex='\pi'):
    def gcd(a, b):
        while b:
            a, b = b, a%b
        return a
    def _multiple_formatter(x, pos):
        den = denominator
        num = np.int(np.rint(den*x/number))
        com = gcd(num,den)
        (num,den) = (int(num/com),int(den/com))
        if den==1:
            if num==0:
                return r'$0$'
            if num==1:
                return r'$%s$'%latex
            elif num==-1:
                return r'$-%s$'%latex
            else:
                return r'$%s%s$'%(num,latex)
        else:
            if num==1:
                return r'$\frac{%s}{%s}$'%(latex,den)
            elif num==-1:
                return r'$\frac{-%s}{%s}$'%(latex,den)
            else:
                return r'$\frac{%s%s}{%s}$'%(num,latex,den)
    return _multiple_formatter
​
class Multiple:
    def __init__(self, denominator=2, number=np.pi, latex='\pi'):
        self.denominator = denominator
        self.number = number
        self.latex = latex
​
    def locator(self):
        return plt.MultipleLocator(self.number / self.denominator)
​
    def formatter(self):
        return plt.FuncFormatter(multiple_formatter(self.denominator, self.number, self.latex))

This can be used very simply, without any parameters:

x = np.linspace(-np.pi, 3*np.pi,500)
plt.plot(x, np.cos(x))
plt.title(r'Multiples of $\pi$')
ax = plt.gca()
ax.grid(True)
ax.set_aspect(1.0)
ax.axhline(0, color='black', lw=2)
ax.axvline(0, color='black', lw=2)
ax.xaxis.set_major_locator(plt.MultipleLocator(np.pi / 2))
ax.xaxis.set_minor_locator(plt.MultipleLocator(np.pi / 12))
ax.xaxis.set_major_formatter(plt.FuncFormatter(multiple_formatter()))
plt.show()

plot of cos(x)

Or it can be used in a more sophisticated way:

tau = np.pi*2
den = 60
major = Multiple(den, tau, r'\tau')
minor = Multiple(den*4, tau, r'\tau')
x = np.linspace(-tau/60, tau*8/60,500)
plt.plot(x, np.exp(-x)*np.cos(60*x))
plt.title(r'Multiples of $\tau$')
ax = plt.gca()
ax.grid(True)
ax.axhline(0, color='black', lw=2)
ax.axvline(0, color='black', lw=2)
ax.xaxis.set_major_locator(major.locator())
ax.xaxis.set_minor_locator(minor.locator())
ax.xaxis.set_major_formatter(major.formatter())
plt.show()

plot of exp(-x)*cos(60*x)

Scott Centoni
  • 1,019
  • 11
  • 13
  • Does the while loop cause trouble if the user accidentally plots out to some huge number? It's presumably calling the tick formatter more than once, and if the ticks are at like 1e16, 2e16, etc. or something because it was accidentally used on the wrong plot, could this choke up for a minute? – EL_DON Dec 04 '18 at 05:03
  • No, it's fine. The major and minor locator have to be disabled to go out to large x (otherwise the exception will hit before you can get to the formatter, so you can go back and fix your code that tried to use this on a huge x range). Then the execution time is less than 100 ms on my laptop even out to 3e16. The last tick is at 95492965855137200 pi/3. :) – EL_DON Dec 04 '18 at 05:07
  • 1
    There is a limitation if you try to do small increments in the major ticks. That is, instead of `ax.xaxis.set_major_locator(plt.MultipleLocator(np.pi / 4))`, change the 4 to 18 or something. Since `den = 12`, it will not format well. Maybe `den` could be changed to 60 to support more choices of tick increments. (I want this because I made a function that takes major and minor tick increments as arguments instead of setting to just pi/4 and pi/12) – EL_DON Dec 06 '18 at 18:52
  • 3
    Yes, that was also bothering me, so I've made it more general. Now you can choose the denominator you want, or pick something other than pi. – Scott Centoni Dec 08 '18 at 22:15
24
f,ax=plt.subplots(1)
x=linspace(0,3*pi,1001)
y=sin(x)
ax.plot(x/pi,y)
ax.xaxis.set_major_formatter(FormatStrFormatter('%g $\pi$'))
ax.xaxis.set_major_locator(matplotlib.ticker.MultipleLocator(base=1.0))

enter image description here

I used info from these answers:

Community
  • 1
  • 1
EL_DON
  • 1,416
  • 1
  • 19
  • 34
  • @Zubo if this answers your question, you should accept it as the answer – scicalculator Nov 16 '16 at 21:28
  • @scicalculator yeah it actually shows some errors at the moment, so I'm looking into it – Zubo Nov 16 '16 at 21:39
  • Ok so I tried it with some slight modifications and there's some weird behaviour - I don't know where that's coming from. Posted it in the edit. – Zubo Nov 16 '16 at 21:47
  • Does anyone have a way to do it where I don't have to divide x by pi first? – EL_DON Nov 16 '16 at 23:14
  • @EL_DON I found a way to do this without dividing x first, and wrote it up as another answer. – Scott Centoni Dec 03 '18 at 02:54
  • @ScottCentoni Not dividing `x` first is definitely an advantage. The fractions are also nice. The only things better about my answer are that it's more compact and it doesn't have a while loop, which I'm not sure is actually a problem. @Zubo you might want to change the accepted answer. – EL_DON Dec 04 '18 at 04:59
11

If you want to avoid dividing x by pi in the plot command, this answer can be adjusted slightly using a FuncFormatter instead of a FormatStrFormatter:

import numpy as np
from matplotlib import pyplot as plt
from matplotlib.ticker import FuncFormatter, MultipleLocator

fig,ax = plt.subplots()

x = np.linspace(-5*np.pi,5*np.pi,100)
y = np.sin(x)/x
ax.plot(x,y)
#ax.xaxis.set_major_formatter(FormatStrFormatter('%g $\pi$'))
ax.xaxis.set_major_formatter(FuncFormatter(
   lambda val,pos: '{:.0g}$\pi$'.format(val/np.pi) if val !=0 else '0'
))
ax.xaxis.set_major_locator(MultipleLocator(base=np.pi))

plt.show()

gives the following image:

result of the above code

Thomas Kühn
  • 9,412
  • 3
  • 47
  • 63
5

Solution for pi fractions:

import numpy as np
import matplotlib.pyplot as plt

from matplotlib import rc
rc('text', usetex=True) # Use LaTeX font

import seaborn as sns
sns.set(color_codes=True)
  1. Plot your function:
fig, ax = plt.subplots(1)
x = np.linspace(0, 2*np.pi, 1001)
y = np.cos(x)
ax.plot(x, y)
plt.xlim(0, 2*np.pi)
  1. Modify the range of the grid so that it corresponds to the pi values:
ax.set_xticks(np.arange(0, 2*np.pi+0.01, np.pi/4))
  1. Change axis labels:
labels = ['$0$', r'$\pi/4$', r'$\pi/2$', r'$3\pi/4$', r'$\pi$',
          r'$5\pi/4$', r'$3\pi/2$', r'$7\pi/4$', r'$2\pi$']
ax.set_xticklabels(labels)

enter image description here

kabhel
  • 324
  • 3
  • 11
  • Worked a charm (+1). For those that are using plt rather than ax, use `x_ticks = np.arange(0, 2*np.pi+0.01, np.pi/4)` `labels = ['$0$', r'$\pi/4$', r'$\pi/2$', r'$3\pi/4$', r'$\pi$', r'$5\pi/4$', r'$3\pi/2$', r'$7\pi/4$', r'$2\pi$']` and then `plt.xticks(x_ticks, labels=labels)`. – Matthew Cassell Aug 07 '22 at 07:30
1
import numpy as np
import matplotlib.pyplot as plt
x=np.linspace(0,3*np.pi,1001)
plt.ylim(-3,3)
plt.xlim(0, 4*np.pi)
plt.plot(x, np.sin(x))
tick_pos= [0, np.pi , 2*np.pi]
labels = ['0', '$\pi$', '$2\pi$']
plt.xticks(tick_pos, labels)

enter image description here

Kolibril
  • 1,096
  • 15
  • 19
1

I created a PyPi Package that creates formatter and locator instances like Scott Centoni's answer.

"""Show a simple example of using MultiplePi."""

import matplotlib.pyplot as plt
import numpy as np

from matplot_fmt_pi import MultiplePi

fig = plt.figure(figsize=(4*np.pi, 2.4))
axes = fig.add_subplot(111)
x = np.linspace(-2*np.pi, 2*np.pi, 512)
axes.plot(x, np.sin(x))

axes.grid(True)
axes.axhline(0, color='black', lw=2)
axes.axvline(0, color='black', lw=2)
axes.set_title("MultiplePi formatting")

pi_manager = MultiplePi(2)
axes.xaxis.set_major_locator(pi_manager.locator())
axes.xaxis.set_major_formatter(pi_manager.formatter())

plt.tight_layout()
plt.savefig("./pi_graph.png", dpi=120)
0

Here is a version converting floats into fractions of pi. Just use your favorite formatter, then convert the float values it produced into pi fractions using function convert_to_pi_fractions(ax, axis='x'), specifying which spine must be converted (or both). You get that:

enter image description here

from that:

enter image description here

from fractions import Fraction
import numpy as np
from numpy import pi
import matplotlib.pyplot as plt
import matplotlib.ticker as tck

def convert_to_pi_fractions(ax, axis='x'):
    assert axis in ('x', 'y', 'both')
    if axis in ('x', 'both'):
        vals, labels = process_ticks(ax.get_xticks())
        if len(vals) > 0: ax.set_xticks(vals, labels)
    if axis in ('y', 'both'):
        vals, labels = process_ticks(ax.get_yticks())
        if len(vals) > 0: ax.set_yticks(vals, labels)

def process_ticks(ticks):
    vals = []
    labels = []
    for tick in ticks:
        frac = Fraction(tick/pi)
        if frac.numerator < 10 and frac.numerator < 10:
            if frac.numerator == 0: label = '0'
            elif frac.denominator == 1:
                if frac.numerator == 1: label = '$\pi$'
                elif frac.numerator == -1: label = '-$\pi$'
                else: label = f'{frac.numerator} $\pi$'
            elif frac.numerator == -1: label = f'-$\pi$/{frac.denominator}'
            elif frac.numerator == 1: label = f'$\pi$/{frac.denominator}'
            else: label = f'{frac.numerator}$\pi$/{frac.denominator}'
            vals.append(tick)
            labels.append(label)
    return vals, labels

# Generate data
w_fr = np.linspace(-0.5*pi, 3.1*pi, 60)
H_func = lambda h, w: np.sum(h * np.exp(-1j * w[:, None] * np.arange(len(h))), axis=1)
r_fr = H_func([1, -1], w_fr)

# Prepare figure
fig, ax = plt.subplots(figsize=(10, 4), layout='constrained')
ax.grid()
ax.set_title('Frequency response')
ax.set_xlabel('normalized radian frequency')
ax.xaxis.set_major_locator(tck.MultipleLocator(base=pi/2))
g_c, p_c = 'C0', 'C1'

# Plot gain
ax.set_ylabel('amplitude', c=g_c)
ax.plot(w_fr, abs(r_fr), label='gain', c=g_c)
ax.tick_params(axis='y', labelcolor=g_c)

# Plot phase shift
ax1 = ax.twinx()
ax1.set_ylabel('phase shift', c=p_c)
ax1.yaxis.set_major_locator(tck.MultipleLocator(base=pi/4))

ax1.plot(w_fr, np.unwrap(np.angle(r_fr), period=2*pi), label='phase shift', c=p_c)
ax1.tick_params(axis='y', labelcolor=p_c)

# Convert floats to pi fractions
convert_to_pi_fractions(ax)
convert_to_pi_fractions(ax1, axis='y')
mins
  • 6,478
  • 12
  • 56
  • 75