7

I would like to modify the Y axis unit of the plot indicated below. Preferable would be the use of units like M (Million), k (Thousand) for large numbers. For example, the y Axis should look like: 50k, 100k, 150k, etc.

The plot below is generated by the following code snippet:

plt.autoscale(enable=True, axis='both')
plt.title("TTL Distribution")
plt.xlabel('TTL Value')
plt.ylabel('Number of Packets')
y = graphy  # data from a sqlite query
x = graphx  # data from a sqlite query
width = 0.5
plt.bar(x, y, width, align='center', linewidth=2, color='red', edgecolor='red')
fig = plt.gcf()
plt.show()

I saw this post and thought I could write my own formatting function:

def y_fmt(x, y):
    if max_y > 1000000:
        val = int(y)/1000000
        return '{:d} M'.format(val)
    elif max_y > 1000:
        val = int(y) / 1000
        return '{:d} k'.format(val)
    else:
        return y

But I missed that there is no plt.yaxis.set_major_formatter(tick.FuncFormatter(y_fmt)) function available for the bar plot I am using.

How I can achieve a better formatting of the Y axis?

[TTL Distribution Plot]

Community
  • 1
  • 1
Patrick
  • 1,046
  • 2
  • 10
  • 31

2 Answers2

15

In principle there is always the option to set custom labels via plt.gca().yaxis.set_xticklabels().

However, I'm not sure why there shouldn't be the possibility to use matplotlib.ticker.FuncFormatter here. The FuncFormatter is designed for exactly the purpose of providing custom ticklabels depending on the ticklabel's position and value. There is actually a nice example in the matplotlib example collection.

In this case we can use the FuncFormatter as desired to provide unit prefixes as suffixes on the axes of a matplotlib plot. To this end, we iterate over the multiples of 1000 and check if the value to be formatted exceeds it. If the value is then a whole number, we can format it as integer with the respective unit symbol as suffix. On the other hand, if there is a remainder behind the decimal point, we check how many decimal places are needed to format this number.

Here is a complete example:

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

def y_fmt(y, pos):
    decades = [1e9, 1e6, 1e3, 1e0, 1e-3, 1e-6, 1e-9 ]
    suffix  = ["G", "M", "k", "" , "m" , "u", "n"  ]
    if y == 0:
        return str(0)
    for i, d in enumerate(decades):
        if np.abs(y) >=d:
            val = y/float(d)
            signf = len(str(val).split(".")[1])
            if signf == 0:
                return '{val:d} {suffix}'.format(val=int(val), suffix=suffix[i])
            else:
                if signf == 1:
                    print val, signf
                    if str(val).split(".")[1] == "0":
                       return '{val:d} {suffix}'.format(val=int(round(val)), suffix=suffix[i]) 
                tx = "{"+"val:.{signf}f".format(signf = signf) +"} {suffix}"
                return tx.format(val=val, suffix=suffix[i])

                #return y
    return y


fig, ax = plt.subplots(ncols=3, figsize=(10,5))

x = np.linspace(0,349,num=350) 
y = np.sinc((x-66.)/10.3)**2*1.5e6+np.sinc((x-164.)/8.7)**2*660000.+np.random.rand(len(x))*76000.  
width = 1

ax[0].bar(x, y, width, align='center', linewidth=2, color='red', edgecolor='red')
ax[0].yaxis.set_major_formatter(FuncFormatter(y_fmt))

ax[1].bar(x[::-1], y*(-0.8e-9), width, align='center', linewidth=2, color='orange', edgecolor='orange')
ax[1].yaxis.set_major_formatter(FuncFormatter(y_fmt))

ax[2].fill_between(x, np.sin(x/100.)*1.7+100010, np.cos(x/100.)*1.7+100010, linewidth=2, color='#a80975', edgecolor='#a80975')
ax[2].yaxis.set_major_formatter(FuncFormatter(y_fmt))

for axes in ax:
    axes.set_title("TTL Distribution")
    axes.set_xlabel('TTL Value')
    axes.set_ylabel('Number of Packets')
    axes.set_xlim([x[0], x[-1]+1])

plt.show()

which provides the following plot:

enter image description here

ImportanceOfBeingErnest
  • 321,279
  • 53
  • 665
  • 712
  • Nice example, lot more complete than mine. But you write *"if there is a remainder behind the decimal point, we check how many decimal places are needed"*, but the cut-off is still a bit random? E.g. `y_fmt(100100, 0)` returns `100.1 k` but `y_fmt(100010, 0)` returns `100 k`. – Bart Nov 13 '16 at 12:35
  • 1
    @Bart Correct! I did think of that problem, but forgot about it afterwards. I updated the example to include such cases, where numbers are big but also close to each other. – ImportanceOfBeingErnest Nov 13 '16 at 13:39
  • 1
    There is also the built in `EngFormatter` which does something very similar out of the box http://matplotlib.org/examples/api/engineering_formatter.html – tacaswell Dec 11 '16 at 22:21
  • @tacaswell That's great! In which version was that introduced? The [normal api doc](http://matplotlib.org/api/ticker_api.html) doesn't have it (yet?). – ImportanceOfBeingErnest Dec 12 '16 at 08:01
5

You were pretty close; one (possibly) confusing thing about FuncFormatter is that the first argument is the tick value, and the second the tick position , which (when named x,y) can be confusing for the y-axis. For clarity, I renamed them in the example below.

The function should take in two inputs (tick value x and position pos) and return a string

(http://matplotlib.org/api/ticker_api.html#matplotlib.ticker.FuncFormatter)

Working example:

import numpy as np
import matplotlib.pylab as pl
import matplotlib.ticker as tick

def y_fmt(tick_val, pos):
    if tick_val > 1000000:
        val = int(tick_val)/1000000
        return '{:d} M'.format(val)
    elif tick_val > 1000:
        val = int(tick_val) / 1000
        return '{:d} k'.format(val)
    else:
        return tick_val

x = np.arange(300)
y = np.random.randint(0,2000000,x.size)

width = 0.5
pl.bar(x, y, width, align='center', linewidth=2, color='red', edgecolor='red')
pl.xlim(0,300)

ax = pl.gca()
ax.yaxis.set_major_formatter(tick.FuncFormatter(y_fmt))

enter image description here

Bart
  • 9,825
  • 5
  • 47
  • 73
  • 1
    thanks for your answer! However, the answer from ImportanceOfBeingErnest additionally extends the suffixes thus I marked it as 'accepted'. – Patrick Nov 14 '16 at 19:27