How to Stagger or Offset X-Axis Labels in Matplotlib

How do I stagger or offset x-axis labels in Matplotlib?

You can loop through your x axis ticks and increase the pad for every other tick so that they are lower than the other ticks. A minimal example would be:

import matplotlib.pyplot as plt

fig, ax = plt.subplots()

ax.plot([1,2,3,4,5])

ax.set_xticks([1,2,3,4,5])
ax.set_xticklabels(["A","B","C","D","E",])

# [1::2] means start from the second element in the list and get every other element
for tick in ax.xaxis.get_major_ticks()[1::2]:
tick.set_pad(15)

plt.show()

enter image description here

matplotlib: how to prevent x-axis labels from overlapping

  • The issue in the OP is the dates are formatted as string type. matplotlib plots every value as a tick label with the tick location being a 0 indexed number based on the number of values.
  • The resolution to this issue is to convert all values to the correct type, datetime in this case.
    • Once the axes have the correct type, there are additional matplotlib methods, which can be used to further customize the tick spacing.
  • The answers to What is plotted when string data is passed to the matplotlib API? explain in more detail what happens when string values are passed to matplotlib.
  • As of 2014-09-30, pandas has a read_sql function, which has a parse_dates parameter. You definitely want to use that instead.

Original Answer

Here's how you should convert your date string into real datetime objects:

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
data_tuples = [
('4084036', '1', '2006-12-22 22:46:35'),
('4084938', '1', '2006-12-23 16:19:13'),
('4084938', '2', '2006-12-23 16:20:23'),
('4084939', '1', '2006-12-23 16:29:14'),
('4084954', '1', '2006-12-23 16:28:28'),
('4250653', '1', '2007-02-12 21:58:53'),
('4250657', '1', '2007-03-12 21:58:53')]
datatypes = [('col1', 'i4'), ('col2', 'i4'), ('date', 'S20')]
data = np.array(data_tuples, dtype=datatypes)
col1 = data['col1']

# convert the dates to a datetime type
dates = mdates.num2date(mdates.datestr2num(data['date']))
fig, ax1 = plt.subplots()
ax1.bar(dates, col1)
fig.autofmt_xdate()

Sample Image

Getting a simple list of tuples out of your database cursor should be as simple as...

data_tuples = []
for row in cursor:
data_tuples.append(row)

However, I posted a version of a function that I use to take db cursors directly to record arrays or pandas dataframes here: How to convert SQL Query result to PANDAS Data Structure?

Hopefully that helps too.

How to add axis offset in matplotlib plot?

Answering this for the most general case first.
A dodge can be implemented by shifting the artists in the figure by some amount. It might be useful to use points as units of that shift. E.g. you may want to shift your markers on the plot by 5 points.

This shift can be accomplished by adding a translation to the data transform of the artist. Here I propose a ScaledTranslation.

Now to keep this most general, one may write a function which takes the plotting method, the axes and the data as input, and in addition some dodge to apply, e.g.

draw_dodge(ax.errorbar, X, y, yerr =y/4., ax=ax, dodge=d, marker="d" )

The full functional code:

import matplotlib.pyplot as plt
from matplotlib import transforms
import numpy as np
import pandas as pd


def draw_dodge(*args, **kwargs):
func = args[0]
dodge = kwargs.pop("dodge", 0)
ax = kwargs.pop("ax", plt.gca())
trans = ax.transData + transforms.ScaledTranslation(dodge/72., 0,
ax.figure.dpi_scale_trans)
artist = func(*args[1:], **kwargs)
def iterate(artist):
if hasattr(artist, '__iter__'):
for obj in artist:
iterate(obj)
else:
artist.set_transform(trans)
iterate(artist)
return artist

X = ["a", "b"]
Y = np.array([[1,2],[2,2],[3,2],[1,4]])

Dodge = np.arange(len(Y),dtype=float)*10
Dodge -= Dodge.mean()

fig, ax = plt.subplots()

for y,d in zip(Y,Dodge):
draw_dodge(ax.errorbar, X, y, yerr =y/4., ax=ax, dodge=d, marker="d" )

ax.margins(x=0.4)
plt.show()

enter image description here

You may use this with ax.plot, ax.scatter etc. However not with any of the seaborn functions, because they don't return any useful artist to work with.


Now for the case in question, the remaining problem is to get the data in a useful format. One option would be the following.

df1 = pd.DataFrame({'x':list('ffffssss'), 
'y':[1,2,3,4,5,6,7,8],
'h':list('abababab')})
df2 = df1.copy()
df2['y'] = df2['y']+0.5

N = len(np.unique(df1["x"].values))*len([df1,df2])
Dodge = np.linspace(-N,N,N)/N*10


fig, ax = plt.subplots()
k = 0
for df in [df1,df2]:
for (n, grp) in df.groupby("h"):
x = grp.groupby("x").mean()
std = grp.groupby("x").std()
draw_dodge(ax.errorbar, x.index, x.values,
yerr =std.values.flatten(), ax=ax,
dodge=Dodge[k], marker="o", label=n)
k+=1

ax.legend()
ax.margins(x=0.4)
plt.show()

enter image description here

How can I offset the x-axis ticks so every other is slightly lower/higher?

You are doing one fundamental mistake I think. You should append the new line character before the string because only then you will see the label text one line below. Otherwise, you are just sending the cursor to the next line without printing anything.

Moreover, you need label.get_text() to get the string of the tick-labels. I am showing a sample answer below. Adapt the same logic to your example. I cannot do that because you haven't provided a MCVE

import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots()

x = np.linspace(0, 10, 20)

plt.plot(x, x**2)
fig.canvas.draw()
labels_formatted = [label.get_text() if i%2==0 else '\n'+label.get_text() for i, label in enumerate(ax.xaxis.get_majorticklabels())]
ax.set_xticklabels(labels_formatted)
plt.show()

enter image description here

Matplotlib showing x-tick labels overlapping

Ok, finally got it working. The trick was to use plt.setp to manually rotate the tick labels. Using fig.autofmt_xdate() did not work as it does some unexpected things when you have multiple subplots in your figure. Here's the working code with its output:

for i, d in enumerate([360, 30, 7, 1]):
ax = axes.flatten()[i]
earlycut = now - relativedelta(days=d)
data = df.loc[df.index>=earlycut, :]
ax.plot(data.index, data['value'])

ax.get_xaxis().set_minor_locator(mpl.ticker.AutoMinorLocator())
ax.get_yaxis().set_minor_locator(mpl.ticker.AutoMinorLocator())

ax.grid(b=True, which='major', color='w', linewidth=1.5)
ax.grid(b=True, which='minor', color='w', linewidth=0.75)

plt.setp(ax.get_xticklabels(), rotation=30, horizontalalignment='right')

fig.tight_layout()

enter image description here

By the way, the comment earlier about some matplotlib things taking forever is very interesting here. I'm using a raspberry pi to act as a weather station at a remote location. It's collecting the data and serving the results via the web. And boy oh boy, it's really wheezing trying to put out these graphics.

How do you change an x axis that displays date and time to show only month name?

I had to change your code a little because it used a hourly range over 3 days, which results in all the months being June. Also, your temp line is unnecessary because you could equivalently create and directly assign the random integers to df['Value'] without first making a temp dataframe.

import numpy as np
import pandas as pd
df = pd.DataFrame(pd.date_range('2011-06-01','2012-06-04', freq='M'), columns=['date/time'])
df['Value'] = np.random.randint(5, 100, len(df.index))

fig = plt.figure()
ax = fig.add_subplot(111)

months_list = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
### Iterate over the TimeStamp objects, calling the month format of each
months = [months_list[dt.month - 1] for dt in df['date/time']]

ax.plot(months, df['Value'])
plt.show()

For the part about avoiding overlapping tick labels, you could do a basic rotation of the labels:

ax.tick_params(axis='x', labelrotation=30)

or refer to these answers that show two different ways to do this:

  1. Adds a label pad to alternate tick labels
  2. Fine-tune rotation and positions of tick labels

Here's the outcome:
Basic rotation

X-axis label on two rows

If you are okay with 90 degrees rotated text, you may try this code that is based on a very useful x-label rotating text tool, available here

Code

h=figure;
bar(randi(9,26,1),'EdgeColor','g') %// Assumed random data for demo
set(gca, 'XTickLabel',str, 'XTick',1:numel(str))
xticklabel_rotate([],90,str);%% File-exchange code %// xlabel text rotated
saveas(h, 'computing_time.png');

Sample plot with some random data

enter image description here

If you are okay with down-sampling the x-label text, i.e. for example show only every other
label, use this right before creating the figure handle -

str(1:2:end)={[]}

Rest of the code stays the same. The output plot would look like this -

enter image description here

If you still want to keep the data horizontal, you need to downsample the number of labels by a good factor. In your given sample case, a factor of 4 worked. The changes in the code is adding the following code right after declaring str and of course commenting the x-label rotating tool usage -

str1 = cell(1,numel(str));
str1(1:4:end) = str(1:4:end);
str = str1;

The trick here is to use empty cells for the x-labels that you want to skip.

Result -

enter image description here



Related Topics



Leave a reply



Submit