Second y scale repeating axis ticks - python

I have some code below which plots 3 sets of random numbers by adding them to a plot (simulating real world data gathered from say a temperature sensor).
I am attempting to make 2 scales on the same plot.
Here, y2List is negative and this is the data set that I would like to create the second axis for. I figured out how to do this using other questions on here.
The problem is that when each data point is added, the second y axis ticks are shown again so that the second y axis is very crowded with numbers. I can get round this by setting a limit on the second y axis, which produces an image like this:
The second y axis is slightly darker than the others, and this is because python is plotting the same numbers on top of the existing ones after each point is plotted (I can tell because the numbers get darker as each point is plotted)
My question... is there a way to make the second y axis only plot the second scale only once? This is obviously just to make the plot aesthetically pleasing but every little helps!
My code is below:
plt.ion() # enable interactivity
def makeFig():
ax.plot(xList, yList, color='blue', label='something1' if x == 0 else '')
ax.plot(xList, y1List, color='red', label='something2' if x == 0 else '')
ax2 = ax.twinx()
ax2.plot(xList, y2List, color='orange', label='something else' if x == 0 else '')
ax2.set_ylim(-20,0)
xList=list()
yList=list()
y1List=list()
y2List=list()
x=0
while x<11:
fig1=plt.figure(1)
ax = fig1.add_subplot(111)
x_1 = datetime.datetime.now()
date_formatter = DateFormatter('%H:%M:%S')
y=np.random.random()
y1=np.random.random() *3
y2=np.random.random() *(-13)
xList.append(x_1)
yList.append(y)
y1List.append(y1)
y2List.append(y2)
makeFig()
plt.gcf().autofmt_xdate()
ax = plt.gca()
ax.xaxis.set_major_formatter(date_formatter)
max_xticks = 10
xloc = plt.MaxNLocator(max_xticks)
ax.xaxis.set_major_locator(xloc)
plt.get_current_fig_manager().window.wm_geometry("940x700+5+0")
plt.draw()
plt.legend(loc=2, bbox_to_anchor=(1, 0.5), prop={'size':10})
x+=1
plt.pause(0.5)

You should move the creation of the figure and the twin axes outside of your loop. They only need to be done once.
Specifically, move fig1=plt.figure(1), ax = fig1.add_subplot(111) and ax2 = ax.twinx() outside the loop.

Related

Legend position for figures with variable size

My plot function creates horizontal bars per year for data with different size. I have to change the figure size for each set of subplots.
I need to place my two legends on lower center of each figure below the x axis label. The positions need to vary depending on the figure size and remain consistent. So for all produced figures, the legends would look like this figure.
Find a snippet of my dataframe here. I have tried to simplify the code as much as I could and I know the plot is missing some element, but I just want to get to my question's answer, not to create a perfect plot here. I understand probably I need to create a variable for my anchor bounding box but I don't know how. Here is my code:
def plot_bars(data,ax):
""" Plots a single chart of work plan for a specific routeid
data: dataframe with section length and year
Returns: None"""
ax.barh(df['year'], df['sec_len'] , left = df['sec_begin'])
ax.set_yticklabels('')
def plot_fig(df):
# Draw the plots
ax_set = df[['routeid','num_bars']].drop_duplicates('routeid')
route_set = ax_set['routeid'].values
h_ratios = ax_set['num_bars'].values
len_ratio = h_ratios.sum()/BARS_PER_PAGE # Global constant set to 40 based on experiencing
fig, axes = plt.subplots(len(route_set), 1, squeeze=False, sharex=True
, gridspec_kw={'height_ratios':h_ratios}
, figsize=(10.25,7.5*len_ratio))
for i, r in enumerate(route_set):
plot_bars(df[df['routeid']==r], axes[i,0])
plt.xlabel('Section length')
## legends
fig.legend(labels=['Legend2'], loc=8, bbox_to_anchor=(0.5, -0.45))
fig.legend( labels=['Legend1'], loc = 8, bbox_to_anchor=(0.5, -0.3))
## Title
fig.suptitle('title', fontsize=16, y=1)
fig.subplots_adjust(hspace=0, top = 1-0.03/len_ratio)
for df in df_list:
plot_fig(df)
The problem is when the figure size changes, the legends move as in these pictures:
here
here
I think the problem boils down to having the correct relative position with respect to the xlabel, so are right that you need to calculate the bbox_to_anchor using the position of the xlabel and the height/width of the axes. Something like this:
fig, (ax, ax1) = plt.subplots(nrows=2, figsize=(5, 4), gridspec_kw={'height_ratios':[4, 1]})
ax.plot(range(10), range(10), label="myLabel")
ax.set_xlabel("xlabel")
x, y = ax.xaxis.get_label().get_position() # position of xlabel
h, w = ax.bbox.height, ax.bbox.width # height and width of the Axes
leg_pos = [x + 0 / w, y - 55 / h] # this needs to be adjusted according to your needs
fig.legend(loc="lower center", bbox_to_anchor=leg_pos, bbox_transform=ax.transAxes)
plt.show()

Matplotlib not showing correct and desired x-axis

I have a barplot I am trying to plot without the x-axis ticks overlapping. I have settled on an angle of 45 degrees, and a max. number of ticks of 50, as this is about the max. of what can be shown without overlapping (IF the ticks are tilted at 45 degrees).
However, in my attempts I ran into the problem of Matplotlib not setting the x-axis to what I desire, whatever I try. I need to plot multiple datasets, for all of which the time runs from -15.8 through somewhere around 1200-1800.
I tried several solutions I found online, but all to no avail. The code below does not work, as it does not show the correct ticks. The range stops well before the last number in the timepoints list.
import numpy as np
from matplotlib import pyplot as plt
# Mock data
timepoints = list(np.arange(-15.8, 1276.2, 4))
patient_counts = np.random.randint(300, 600, len(timepoints))
x_tick_pos = [i + 0.5 for i in range(len(timepoints))]
# Plot barplot
fig, ax = plt.subplots(figsize=(16, 10))
ax.bar(x_tick_pos, patient_counts, align='center', width=1.0)
# Set x axis ticks
ax.set_xticklabels(timepoints, rotation=45)
ax.locator_params(axis='x', nbins=20)
plt.show()
Clearly, the x-axis does not come close to the expected values.
EDIT
To expand, this question is a follow-up from this thread. The code based on the answer in that question is as follows
# Plot barplot
fig, ax = plt.subplots(figsize=(16, 10))
ax.bar(x_tick_pos, patient_counts, align='center', width=1.0)
# Set x axis ticks
ax.set_xticks(x_tick_pos)
ax.set_xticklabels(x_ticks, rotation=45)
This appears to set the right x-ticks, except they overlap a lot- hence why I want only a max of 50 ticks to show:
This might be a simple case of fixing the x_tick_pos list expression. In your mock example, if we print them out ...
x_tick_pos = [i + 0.5 for i in range(len(timepoints))]
print(x_tick_pos[:5], x_tick_pos[-5:])
... we get what your figure reflects:
[0.5, 1.5, 2.5, 3.5, 4.5] [318.5, 319.5, 320.5, 321.5, 322.5]
Changing the assignment to
x_tick_pos = [i + 0.5 for i timepoints]
would appear to give the expected ticks.
The issue is that the positioning of the ticks is written so that they line up with another graph above this one, as per this post.
There are two solutions:
forget about positioning the ticks relative to another graph, in case this bar plot is plotted in a standalone fashion
resetting the ticks after plotting the bar plot to give them correct labels:
# Plot barplot
fig, ax = plt.subplots(figsize=(16, 10))
ax.bar(x_tick_pos, patient_counts, align='center', width=1.0)
# Set x axis ticks
ticks_step = int(len(missings_df.index) / 50) # 50 here is the max. nr of ticks
x_ticks = [missings_df.index[i] for i in range(0, len(missings_df.index), int(len(missings_df)/50))]
x_tick_pos = [i + 0.5 for i in range(0, len(missings_df.index), int(len(missings_df)/50))]
ax.set_xticks(x_tick_pos)
ax.set_xticklabels(x_ticks, rotation=45)
This correctly plots the x-axis:

Can't Get Axis to Align Right on MatPlotLib 3d

I'm trying to do a 3d matplot graph. I'm having trouble getting the full axis to show with nicely aligned labels. I've outlined the steps I've tried below.
1) I can set the y-axis labels using:
yTicks = list(range(0,90,5)
ax.set_yticks(range(len(yTicks)), True)
However, as you can see, the labels are very badly aligned. It also isn't matching what I've actually defined, which should have been ticks counting by 5, not 10.
2) If I try using set_yticklabels as well, though, the alignment fixes but it only prints part of the axis. Here is the code and image:
ax.set_yticklabels(yTicks, verticalalignment='baseline',
horizontalalignment='left')
Notice how the y-axis went from 80 to 40.
3) And if I get rid of the True in set_yticks, everything squishes together:
4) Finally, if I use both set_yticks and set_yticklabels calling get_yticks() in the labels function, it almost works but you can see the axis lines extend beyond the "surface" of the graph:
ax.set_yticks(range(len(yTicks)), True)
ax.set_yticklabels(ax.get_yticks(), verticalalignment='baseline',
horizontalalignment='left')
5) Here is a more complete version of my code for reference:
plt.clf()
ax = plt.axes(projection='3d')
ax.bar3d(x,y,z,
1,1,[val*-1 if val != 0 else 0 for val in z])
xTicks = list(range(0,25,2))
yTicks = list(range(30,90,5))
ax.set_zlim(0, 1)
ax.set_xticks(range(len(xTicks)), True)
ax.set_yticks(range(len(yTicks)), True)
ax.set_xticklabels(ax.get_xticks(),
verticalalignment='baseline',
horizontalalignment='left')
ax.set_yticklabels(ax.get_yticks(),
verticalalignment='baseline',
horizontalalignment='left')
plt.savefig(file_path)
How can I get it to show my full axis (0-90) at intervals of 5 and have it aligned well?
6) UPDATE: Per the conversation below with #ImportanceOfBeingErnest, here is the issue I'm still experiencing using the following code:
x=[15,28,20]; y=[30,50,80]; z=[1,1,1]
plt.clf()
ax = plt.axes(projection='3d')
ax.bar3d(x,y,z,
1,1,[val*-1 if val != 0 else 0 for val in z])
xTicks = list(range(0,25,2))
yTicks = list(range(30,90,5))
ax.set_xticks(xTicks)
ax.set_yticks(yTicks)
ax.set_yticklabels(ax.get_yticks(),
verticalalignment='baseline',
horizontalalignment='left')
ax.set_zlim(0, 1)
plt.savefig(getSaveGraphPath(save_name))
As commented, you can set the ticks via ax.set_yticks.
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
x=[15,28,20]; y=[30,50,80]; z=[1,1,1]
ax = plt.axes(projection='3d')
ax.bar3d(x,y,z,
1,1,[val*-1 if val != 0 else 0 for val in z])
yTicks = list(range(30,90,5))
ax.set_yticks(yTicks)
ax.set_yticklabels(ax.get_yticks(),
verticalalignment='baseline',
horizontalalignment='left')
ax.set_zlim(0, 1)
plt.show()
This will show the desired 5 unit steps on the y axis.
So after much trial-and-error, the only way I could get the graph to render axes appropriately in various limit cases is as follows. I'm not completely happy with it (notice how the last y-tick label doesn't appear) but it is the only version that has the numbers actually next to their tick marks). I had to let x and y limits be effective only if the data didn't exceed their values, whereas the z boundary is a hard limit. I don't claim to understand why these permutations are all necessary (this all is only an issue with 3D plotting), but this is the solution that works for me:
plt.clf()
ax = plt.axes(projection='3d')
# Need to force include fake NaN data at the axis limits to make sure labels
# render correctly
#
# xLims and yLims create boundaries only if data doesn't stretch beyond them
xstart, xend = xLims
ystart, yend = yLims
x = [xstart] + x + [xend]
y = [ystart] + y + [yend]
z = [numpy.nan] + z + [numpy.nan]
# Plot graph
ax.bar3d(x,y,z,1,1,[val*-1 if val != 0 else 0 for val in z])
# Set z boundary (after graph creation)
ax.set_zbound(zBounds)
# Need to adjust labels slightly to make sure they align properly
use_x_ticks = ax.get_xticks()
### ON SOME SYSTEMS, use_x_ticks = ax.get_xticks()[1:] is needed for correct alignment ###
ax.set_xticklabels([int(x) if x==int(x) else x for x in use_x_ticks],
horizontalalignment='right',
verticalalignment='baseline')
ax.set_yticklabels([int(y) if y==int(y) else y for y in ax.get_yticks()],
verticalalignment='top')
# Save graph
plt.savefig(file_save_path)
As you can see below, everything is nicely aligned:

Python - Set limits at Scatter matrix from pandas

I am doing a scatter matrix plot, but when I try to set the xlim and ylim, the diagonal terms of the plot get wrecked.
My code is:
axS=scatter_matrix(dfS, alpha=0.5, figsize=(10, 10),
diagonal='kde',color="black")
for i in range(5):
for j in range(5):
axS[i,j].set_xlim(0.0,1.0)
axS[i,j].set_ylim(0.0,1.0)
plt.suptitle('Separable')
plt.show()
Without the limits (i.e. without the 'for i in range...'), the image I get is
Notice that the x and y limits are not the same in all the subplots.
Now, if I add the limits, the image I get is
Now I get to scale the same subplot to the same limits. However, neither the limit labels are right nor the diagonal plots.
Is there another way of setting the limits that won't mess the whole picture?
Thank you.
The problem is that you actually don't want the y axis of your KDE plots to have the range (0,1).
Try this:
axS=scatter_matrix(dfS, alpha=0.5, figsize=(10, 10),
diagonal='kde',color="black")
for i in range(5):
for j in range(5):
axS[i,j].set_xlim(0.0,1.0)
if i != j:
axS[i,j].set_ylim(0.0,1.0)
plt.suptitle('Separable')
plt.show()

Recalculate x y values after zoom based on current ylim and xlim in Matplotlib

Dear all I want to recalculate the x y values written in the tick labeling of my figure after i have zoomed in it in such a way that the origin is always at (0,0) and obviously the relative distances of the values on the x and y axis stay the same.
I think I need to track the limits of my figure after having zoomed in to it and than simply subtract the current xmin and ymin from the actual x y tick values.
I guess this can be achieved with the event handling API
Event handling
as i have learned here :
Source1
This is also the place where I got the start of my MWE:
import matplotlib.pyplot as plt
#
# Some toy data
x_seq = [x / 100.0 for x in xrange(1, 100)]
y_seq = [x**2 for x in x_seq]
#
# Scatter plot
fig, ax = plt.subplots(1, 1)
ax.scatter(x_seq, y_seq)
#
# Declare and register callbacks
def on_xlims_change(axes):
a=axes.get_xlim()
print "updated xlims: ", axes.get_xlim()
return a
def on_ylims_change(axes):
a=axes.get_ylim()
print "updated ylims: ", axes.get_ylim()
return a
ax.callbacks.connect('xlim_changed', on_xlims_change)
ax.callbacks.connect('ylim_changed', on_ylims_change)
#
# Show
plt.show()
But I do not really know how I should go from here? Do i have to do the calculation inside the on_xlims_change function and change the x and y tick labels there? Again, I think I really only need to change the value given in the label, right? or would it be easier to change the actual value of the coordinates such that the automatic tick labeling still works?
This may not be as easy as it sounds. When changing the limits, you would change the limits, such that the callback runs infinitly, making your window crash.
I would hence opt for another solution, using a second axes. So let's say you have two axes:
ax2 is the axes to plot to. But is has no frame and no ticklabels. This is the axes you can change the limits with.
ax is empty. It initially has the same limits as ax2. And it will show the ticklabels.
Once you zoom in on ax2 the callback function can change the limits of ax to your liking. This is then what is shown on the screen.
import matplotlib.pyplot as plt
# Some toy data
x_seq = [x / 100.0 for x in xrange(1, 100)]
y_seq = [x**2 for x in x_seq]
# ax is empty
fig, ax = plt.subplots()
ax.set_navigate(False)
# ax2 will hold the plot, but has invisible labels
ax2 = fig.add_subplot(111,zorder=2)
ax2.scatter(x_seq, y_seq)
ax2.axis("off")
ax.set_xlim(ax2.get_xlim())
ax.set_ylim(ax2.get_ylim())
#
# Declare and register callbacks
def on_lims_change(axes):
# change limits of ax, when ax2 limits are changed.
a=ax2.get_xlim()
ax.set_xlim(0, a[1]-a[0])
a=ax2.get_ylim()
ax.set_ylim(0, a[1]-a[0])
ax2.callbacks.connect('xlim_changed', on_lims_change)
ax2.callbacks.connect('ylim_changed', on_lims_change)
# Show
plt.show()
What you want to do can be achieved by updating your limits within on_ylims_change. However, we need to make sure not to end up with an infinite callback loop. One way to do this is to disconnect the callback while we are updating xlim and ylim, and then reconnecting it immediately afterward.
import matplotlib.pyplot as plt
#
# Some toy data
x_seq = [x / 100.0 for x in range(1, 100)]
y_seq = [x**2 for x in x_seq]
#
# Scatter plot
fig, ax = plt.subplots(1, 1)
ax.scatter(x_seq, y_seq)
#
# Declare and register callbacks
def on_lims_change(event_ax):
cid_list = list(event_ax.callbacks.callbacks['ylim_changed'].keys())
for cid in cid_list:
event_ax.callbacks.disconnect(cid)
a = ax.get_xlim()
span = a[1]-a[0]
event_ax.set_xlim(-span/2, span/2)
a = ax.get_ylim()
span = a[1]-a[0]
event_ax.set_ylim(-span/2, span/2)
event_ax.callbacks.connect('ylim_changed', on_lims_change)
ax.callbacks.connect('ylim_changed', on_lims_change)
#
# Show
plt.show()
Generally we only want to connect to the ylim_changed event since this will be called after xlim_changed for a standard zoom event. This way we have the final zoom results before starting to make changes.

Categories