2

The objective is to modify the xticklabel upon plotting pcolormesh and scatter.

However, I am having difficulties accessing the existing xtick labels.

Simply

ax = plt.axes()
labels_x = [item.get_text() for item in ax.get_xticklabels()]

which produced:

['', '', '', '', '', '']

or

fig.canvas.draw()
xticks = ax.get_xticklabels()

which produced:

['', '', '', '', '', '']

does not return the corresponding label.

May I know how to properly access axis tick labels for a plt cases.

For readability, I split the code into two section.

  1. The first section to generate the data used for plotting
  2. Second section deal the plotting

Section 1: Generate data used for plotting

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import math

np.random.seed(0)
increment=120
max_val=172800

aran=np.arange(0,max_val,increment).astype(int)
arr=np.concatenate((aran.reshape(-1,1), np.random.random((aran.shape[0],4))), axis=1)
df=pd.DataFrame(arr,columns=[('lapse',''),('a','i'),('a','j'),('b','k'),('c','')])

ridx=df.index[df[('lapse','')] == 3600].tolist()[0]+1 # minus 1 so to allow 3600 start at new row


df[('event','')]=0
df.loc[[1,2,3,10,20,30],[('event','')]]=1

arr=df[[('a','i'),('event','')]].to_numpy()
col_len=ridx
v=arr[:,0].view()

nrow_size=math.ceil(v.shape[0]/col_len)
X=np.pad(arr[:,0].astype(float), (0, nrow_size*col_len - arr[:,0].size),
       mode='constant', constant_values=np.nan).reshape(nrow_size,col_len)

mask_append_val=0  # This value must equal to 1 for masking
arrshape=np.pad(arr[:,1].astype(float), (0, nrow_size*col_len - arr[:,1].size),
       mode='constant', constant_values=mask_append_val).reshape(nrow_size,col_len)

Section 2 Plotting

fig = plt.figure(figsize=(8,6))
plt.pcolormesh(X,cmap="plasma")

x,y = X.shape
xs,ys = np.ogrid[:x,:y]
# the non-zero coordinates
u = np.argwhere(arrshape)

plt.scatter(ys[:,u[:,1]].ravel()+.5,xs[u[:,0]].ravel()+0.5,marker='*', color='r', s=55)

plt.gca().invert_yaxis()

xlabels_to_use_this=df.loc[:30,[('lapse','')]].values.tolist()

# ax = plt.axes()
# labels_x = [item.get_text() for item in ax.get_xticklabels()]
# labels_y = [item.get_text() for item in ax.get_yticklabels()]

plt.xlabel('X-axis')
plt.ylabel('Y-axis')
plt.title("Plot 2D array")
plt.colorbar()
plt.tight_layout()
plt.show()

Expected output

b

mpx
  • 3,081
  • 2
  • 26
  • 56
  • 1
    Please describe what the specific problem is: what tick-values are you getting returned? It is also useful to mention what you're trying to achieve that you need the tick-values for. – mand Dec 16 '21 at 16:08
  • Well, the xtick positions can still change a lot as more data is added to the plot. You can force a draw, but the recommended solution is that you yourself decide where you wnat the ticks, and then call `ax.set_xticks(your_ticks)`. Note that "the `plt` case" is exactly the same as the `ax = plt.gca()` case. Internally both do the same. For example, you could do `plt.xticks(range(len(xlabels_to_use_this)), xlabels_to_use_this)`. Note that both setting the ticks as trying to receive the ticks should happen as late as possible in your code, as many plot commands change the ticks. – JohanC Dec 16 '21 at 16:32
  • Also note that `ax = plt.axes()` creates a new dummy plot on top of your existing plot. You should really avoid that. Use `ax = plt.gca()` instead to access the existing subplot. The preferred way to get correct ticks, is to use `plt.pcolormesh(x-values, y-values, color-values, ...)` – JohanC Dec 16 '21 at 16:36
  • Thanks for the detail explaination @JohanC, Maybe I understanding something, but use ax = plt.gca() followed by labels_x = [item.get_text() for item in ax.get_xticklabels()] still yield `['', '', '', '', '', '', '', '']` – mpx Dec 16 '21 at 16:46
  • There aren't any xtick labels set in your code. They are only shown at "show" time, and use something like a ticker formatter ([docs](https://matplotlib.org/stable/api/ticker_api.html)). You can get `plt.gca().get_xticks()` instead. But better, you use `ax.set_xticks(...)` together with `ax.set_xticklabels(...)`. Your code is extremely hard to understand with all the reshaping going on. Also, the colorbar needs an argument. E.g. `mesh = plt.pcolormesh(...)` and `plt.colorbar(mesh, ....)` – JohanC Dec 16 '21 at 17:11
  • Thanks for advice @JohanC. But, I end up with Seaborn for this issue. – mpx Dec 16 '21 at 17:46
  • The [docs](https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.get_xticklabels.html) mention the following: > Notes The tick label strings are not populated until a draw method has > been called. So, place `ax.get_xticklabels()` after `plt.show()`. Does that work? (also see [32700935](https://stackoverflow.com/questions/32700935/get-xticklabels-contains-empty-text-instances)) – mand Dec 16 '21 at 19:49

2 Answers2

1

This is how the plot could be generated using matplotlib's pcolormesh and scatter:

import matplotlib.pyplot as plt
from matplotlib.ticker import MultipleLocator
import pandas as pd
import numpy as np

np.random.seed(0)
increment = 120
max_val = 172800
aran = np.arange(0, max_val, increment).astype(int)
arr_df = np.concatenate((aran.reshape(-1, 1), np.random.random((aran.shape[0], 4))), axis=1)
df = pd.DataFrame(arr_df, columns=[('lapse', ''), ('a', 'i'), ('a', 'j'), ('b', 'k'), ('c', '')])
df[('event', '')] = 0
df.loc[[1, 2, 3, 10, 20, 30], [('event', '')]] = 1

col_len_lapse = 3600
col_len = df[df[('lapse', '')] == col_len_lapse].index[0]
nrow_size = int(np.ceil(v.shape[0] / col_len))

a_i_values = df[('a', 'i')].values
a_i_values_meshed = np.pad(a_i_values.astype(float), (0, nrow_size * col_len - len(a_i_values)),
                           mode='constant', constant_values=np.nan).reshape(nrow_size, col_len)

fig, ax = plt.subplots(figsize=(8, 6))
# the x_values indicate the mesh borders, subtract one half so the ticks can be at the centers
x_values = df[('lapse', '')][:col_len + 1].values - increment / 2
# divide lapses for y by col_len_lapse to get hours
y_values = df[('lapse', '')][::col_len].values / col_len_lapse - 0.5
y_values = np.append(y_values, 2 * y_values[-1] - y_values[-2])  # add the bottommost border (linear extension)

mesh = ax.pcolormesh(x_values, y_values, a_i_values_meshed, cmap="plasma")

event_lapses = df[('lapse', '')][df[('event', '')] == 1]
ax.scatter(event_lapses % col_len_lapse,
           np.floor(event_lapses / col_len_lapse),
           marker='*', color='red', edgecolor='white', s=55)

ax.xaxis.set_major_locator(MultipleLocator(increment * 5))
ax.yaxis.set_major_locator(MultipleLocator(5))
ax.invert_yaxis()
ax.set_xlabel('X-axis (s)')
ax.set_ylabel('Y-axis (hours)')
ax.set_title("Plot 2D array")
plt.colorbar(mesh)

plt.tight_layout()  # fit the labels nicely into the plot
plt.show()

pcolormesh with seconds in the x-direction and hours in the y-direction

With Seaborn things can be simplified, adding new columns for hours and seconds, and using pandas' pivot (which automatically fills unavailable data with NaNs). Adding xtick_labels=5 sets the labels every 5 positions. (The star for lapse=3600 is at 1 hour, 0 seconds).

import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
import numpy as np

# df created as before

df['hours'] = (df[('lapse', '')].astype(int) // 3600)
df['seconds'] = (df[('lapse', '')].astype(int) % 3600)

df_heatmap = df.pivot(index='hours', columns='seconds', values=('a', 'i'))
df_heatmap_markers = df.pivot(index='hours', columns='seconds', values=('event', '')).replace(
    {0: '', 1: '★', np.nan: ''})

fig, ax = plt.subplots(figsize=(8, 6))

sns.heatmap(df_heatmap, xticklabels=5, yticklabels=5,
            annot=df_heatmap_markers, fmt='s', annot_kws={'color': 'lime'}, ax=ax)
ax.tick_params(rotation=0)

plt.tight_layout()
plt.show()

sns.heatmap from pivoted dataframe

Instead of a 'seconds' column, a 'minutes' column also might be interesting.

Here is an attempt to add time information as suggested in the comments:

from matplotlib import patheffects # to add some outline effect

# df prepared as the other seaborn example

fig, ax = plt.subplots(figsize=(8, 6))

path_effect = patheffects.withStroke(linewidth=2, foreground='yellow')
sns.heatmap(df_heatmap, xticklabels=5, yticklabels=5,
            annot=df_heatmap_markers, fmt='s',
            annot_kws={'color': 'red', 'path_effects': [path_effect]},
            cbar=True, cbar_kws={'pad': 0.16}, ax=ax)
ax.tick_params(rotation=0)

ax2 = ax.twinx()
ax2.set_ylim(ax.get_ylim())
yticks = ax.get_yticks()
ax2.set_yticks(yticks)
ax2.set_yticklabels([str(pd.to_datetime('2019-01-15 7:00:00') + pd.to_timedelta(h, unit='h')).replace(' ', '\n')
                     for h in yticks])

seaborn heatmap with secondary axis

JohanC
  • 71,591
  • 8
  • 33
  • 66
0

I end up using Seaborn to address this issue.

Specifically, the following lines able to easily tweak the xticklabel

fig.canvas.draw()
new_ticks = [i.get_text() for i in g.get_xticklabels()]

i=[int(idx) for idx in new_ticks]
newlabel=xlabels_to_use_this[i]
newlabel=[np.array2string(x, precision=0) for x in newlabel]

The full code for plotting is as below

import seaborn as sns
fig, ax = plt.subplots()

sns.heatmap(X,ax=ax)
x,y = X.shape
xs,ys = np.ogrid[:x,:y]
# the non-zero coordinates
u = np.argwhere(arrshape)
g=sns.scatterplot(ys[:,u[:,1]].ravel()+.5,xs[u[:,0]].ravel()+0.5,marker='*', color='r', s=55)


fig.canvas.draw()
new_ticks = [i.get_text() for i in g.get_xticklabels()]

i=[int(idx) for idx in new_ticks]
newlabel=xlabels_to_use_this[i]
newlabel=[np.array2string(x, precision=0) for x in newlabel]

ax.set_xticklabels(newlabel)

ax.set_xticklabels(ax.get_xticklabels(),rotation = 90)

for ind, label in enumerate(g.get_xticklabels()):
    if ind % 2 == 0:  # every 10th label is kept
        label.set_visible(True)
    else:
        label.set_visible(False)


for ind, label in enumerate(g.get_yticklabels()):
    if ind % 4 == 0:  # every 10th label is kept
        label.set_visible(True)
    else:
        label.set_visible(False)
      
plt.xlabel('Elapsed (s)')
plt.ylabel('Hour (h)')
plt.title("Rastar Plot")
plt.tight_layout()
plt.show()

enter image description here

mpx
  • 3,081
  • 2
  • 26
  • 56
  • Note that you are grouping 31 times 120 seconds into your hours (as 172800 is a multiple of 3600, you shouldn't have any empty space at the end). – JohanC Dec 16 '21 at 18:16
  • 1
    With seaborn you should use `sns.heatmap(...., xticklabels=..., yticklabels=...)`, leaving empty strings for the unlabeled ticks. – JohanC Dec 16 '21 at 18:17