3

I want to make a dynamic plot every day for year process with Python.

The X axis of the axes will be 12 month, and the process will be a barh in Matplotlib, as code below:

import random
import datetime
import matplotlib.pyplot as plt

def get_percent():
    today = datetime.date.today()
    start = datetime.date(today.year, 1, 1)
    diff = today - start
    percent = diff.days/365.0
    return percent

fig = plt.figure(figsize=(8,2))
ax = fig.add_subplot(1,1,1)
percent = get_percent()

ax.axis([0, 12, 0, 1])
month = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug','Sep', 'Nov', 'Dec']
ax.set_xticklabels(month)
ax.set_xlim(0, 100)
ax.barh(bottom=0.5,width=int(percent*100),height=0.2)

plt.show()
plt.close()

But after plot, the xtick show july to be 100% of the year, which is not I want.

enter image description here

I searched the doc of matplotlib, but did not find the answer :(. How can I make the x axis tick show Jan - Dec, and the barh show the percentage of the year?

DavidG
  • 24,279
  • 14
  • 89
  • 82
jetorz
  • 157
  • 3
  • 9

2 Answers2

7

How can I make xtick different from xlim in Python matplotlib?

That is not possible. xtick is essentially a sub-domain within your xlim (your x-range). "Ticks" are nothing more than discrete points within your limits which are produced by your chosen Locator. Visually, the tick presents itself to you in two ways: First, the little orthogonal line, which I presume gives the "tick" its name, and second, the value which is shown close to it. However, how the value is presented to the user is, in turn, determined by the chosen Formatter. Setting custom labels comes down to using a FixedFormatter which simply returns arbitrary labels for local tick indices.

See https://matplotlib.org/api/ticker_api.html for further details.


What goes wrong in your code?

fig = plt.figure(figsize=(8,2))
ax = fig.add_subplot(1,1,1)
ax.axis([0, 12, 0, 1])
ax.get_xticks()
# returns array([  0.,   2.,   4.,   6.,   8.,  10.,  12.])

Settings the limits how you do it produces exactly 6 ticks:

Image 1

If you now assign more custom labels than the actual number of ticks, the excessive ones are simply ignored. This is the behavior you experienced.

Funny thing. What happens if you set the custom labels and increase the number of ticks (and by that also the range) afterwards?

fig = plt.figure(figsize=(8,2))
ax = fig.add_subplot(1,1,1)
ax.axis([0, 12, 0, 1])
ax.set_xticklabels(['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep','Oct', 'Nov', 'Dec'])
ax.set_xticks(range(24))  # [0, 1, ..., 22, 23]
ax.get_xlim()
# returns (0.0, 23.0)

It increases your x-range (!) and simply shows no labels for the exceeding 12 ticks:

Image 2

Note that you also changed the x-range explicitly after setting the labels. But because the underlying Formatter does not really care about the concrete values but rather the local tick index AND the new x-range had the same amount of ticks you could not really see what is going on. After setting xlim(0, 100) your ticks actually were [0, 20, 40, ... 100] and not [0 ... 12] anymore. This can easily cause bugs and that is why you should always try to operate consistently in one domain (months 0-12, percent 0-100/0-1).


Solving the problem: Choosing a domain!

For the sake of this example I choose the months as our domain which means we operate in the range 0 to 12. Note that 0, the lower bound, corresponds to January 1st and 12, the upper bound, corresponds to December 31st. We can now left-align our monthly ticks (equals ax.set_xticks(range(12))) or center them under the respective month (equals import numpy as np; ax.set_xticks(np.arange(0, 12, 1) + 0.5)). As we set the limits to be (0, 12) modifying the ticks will not change the limits.

Using months as the domain also implies that your percent value (0, 1) is relative to (0, 12) and not (0, 100). Consequently, you have to multiply it by 12 to obtain the width you desire.

Putting it all together:

import random
import datetime
import matplotlib.pyplot as plt

def get_percent():
    today = datetime.date.today()
    start = datetime.date(today.year, 1, 1)
    diff = today - start
    percent = diff.days/365.0
    return percent

fig = plt.figure(figsize=(8,2))
ax = fig.add_subplot(1,1,1)
percent = get_percent()

ax.axis([0, 12, 0, 1])
month = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dec']
ax.set_xticks(range(12))  # ticks placed on [0, 1, ..., 11]
ax.set_xticklabels(month)
ax.barh(bottom=0.5,width=int(percent*12),height=0.2)

plt.show()
plt.close()

Plot

When you also remove the typecast to int when setting your bar width you receive the following result:

Float Plot

P.S. Forgive me for "Okt" instead of "Oct". That was my German soul at work.. :)

Michael Hoff
  • 6,119
  • 1
  • 14
  • 38
0

There are of course easier ways to make a chart with months on the axes.

Here, the problem is that matplotlib wouldn't know at which position to draw the string "Feb". So appart from the labels you also need to set the xtick positions. This can be done, e.g. using

ax.set_xticks(numpy.linspace(0,365,13)/365.*100)

where we use numpy. You could also type in the days that correspond to months by hand and would get a slightly more accurate result, because not all months have the same number of days.

The other problem is that you forgot to take october with you in the list. :-)

Complete code:

import datetime
import matplotlib.pyplot as plt
import numpy as np

def get_percent():
    today = datetime.date.today()
    start = datetime.date(today.year, 1, 1)
    diff = today - start
    percent = diff.days/365.0
    return percent

fig = plt.figure(figsize=(8,2))
ax = fig.add_subplot(1,1,1)
percent = get_percent()

ax.axis([0, 12, 0, 1])
month = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug','Sep','Oct', 'Nov', 'Dec']
ax.set_xticks(np.linspace(0,365,13)/365.*100)
ax.set_xticklabels(month)
ax.set_xlim(0, 100)
ax.barh(bottom=0.5,width=int(percent*100),height=0.2)

plt.show()
plt.close()

enter image description here

ImportanceOfBeingErnest
  • 321,279
  • 53
  • 665
  • 712
  • If you replace `ax.axis([0, 12, 0, 1])` with `ax.axis([0, 100, 0, 1])` you don't need `ax.set_xlim(0, 100)` anymore. – Michael Hoff Apr 13 '17 at 13:20
  • Well, yes, if you use datetime objects from the beginning, you don't need to bother about percentages anymore... This is what the questioner asked for; so I answered the question. – ImportanceOfBeingErnest Apr 13 '17 at 13:25
  • Sure. I don't argue with that. I just wanted to point out that `set_xlim(..)` partially overrides what was supplied in `axis(..)` and that therefore `set_xlim(..)` is redundant. No offense :) – Michael Hoff Apr 13 '17 at 13:30