Related
I tried in a first time to set manually the location for the main legend of a main plot produced by Getdist tool.
The plot below represents the 1/2 sigma confidence levels coming from a covariance matrix with joint distributions. It is produced by Getdist tool.
The main routine that generates this plot is :
# g.settings
g = plots.get_subplot_plotter()
g.settings.figure_legend_frame = True
g.settings.legend_fontsize = 21
g.triangle_plot([matrix1, matrix2],
names,
filled = True,
contour_colors = ['darkblue','red'],
line_args = [{'lw':2, 'color':'darkblue'},
{'lw':2, 'color':'red'}]
)
g.add_legend(['Opt. Flat. No Gamma. - cross - standard situation - Criterion taking into accound a = 200',\
'Pess. Flat. No Gamma. - cross - standard situation - Criterion taking into account a = 300' ],\
bbox_to_anchor = [1.5, 8.5])
The value 1.5 seems to correspond to the x-coordinate (width) 8.5 corresponds to the y-coordinate of legend (height).
Now, I would like to automatically do this process instead of set manual at each time the position of the legend.
I want the top right of the legend to be positioned at the top border of the first left upper box (just at the level of top line border below the "1sigma ± 0.0012" title).
I would like also the legend to be pushed to the right of the figure (up to the right border for the right lower box of the figure: identified by sigma8 "1sigma ± 0.001" title ; Caution: I want it located before the 1.0 and 0.0 xticks, just at the x-coordinate of right line border).
Here what I tried to get the global coordinates (the entire plot) of the top border for this left upper box :
# First, get y coordinates of top border for first Likelihood
box1 = g.subplots[0,0]
box1_coords = box1._position.bounds
print('box1_coords = ', box1_coords)
and I get at the execution the following values :
box1_coords = (0.125, 0.7860975609756098, 0.09451219512195125, 0.09390243902439022)
As you can see, these values seem to be normalized, so I don't know how to handle if I want to insert these values into :
bbox_to_anchor = [box1_coords[0], box1_coords[1]]
This line of code produces a bad position for legend, as expected.
So, how can I manage to automatically assign the good values for bbox_to_anchor to get what I want (y-coordinate at level of top border of left upper box identified by the "1sigma ± 0.0012" title) and pushed on the right side up to the right border of right lower box (x-coordinate identified by sigma8 with "1sigma ± 0.001" title)?
Update 1
I tried to adapt them to my case, but issue still occurs. Here what I have done:
# g.settings
g = plots.get_subplot_plotter()
# get the max y position of the top left axis
top_left_plot = g.subplots[0,0].axes.get_position().ymax
# get the max x position of the bottom right axis
# it is -1 to reference the last plot
bottom_right_plot = g.subplots[-1,-1].axes.get_position().xmax
I don't know why the values of top_left_plot and bottom_right_plot are not the good ones.
I think that subplots[0,0] (for top y-coordinate of legend) refers to the top left subplot and subplots[-1,-1] to the bottom right subplot (for right x-coordinate of legend) but considering this, it doesn't work.
For example :
# g.settings
g = plots.get_subplot_plotter()
# Call triplot
g.triangle_plot([matrix1, matrix2],
names,
filled = True,
legend_labels = [],
contour_colors = ['darkblue','red'],
line_args = [{'lw':2, 'color':'darkblue'},
{'lw':2, 'color':'red'}])
g.add_legend(['Opt. Flat. No Gamma. - cross - standard situation - Criterion taking into accound a = 200',
'Pess. Flat. No Gamma. - cross - standard situation - Criterion taking into account a = 300'],
legend_loc='upper right',
bbox_to_anchor=(bottom_right_plot, top_left_plot)
)
I get :
legend_coords y_max, x_max 0.88 0.9000000000000001
I can't understand why these values (seems to be comprised between 0.0 and 1.0) are not taken into account with g.add_legend.
With #mullinscr's solution, I get the following figure :
If I take for the coordinates of legend position by forcing :
top_left_plot = 8.3
bottom_right_plot = 1.0
This looks like to the first figure of this post. But these 2 values are not comprised between 0.0 and 1.0 like it should.
Update 2
#mullinscr, thanks, I have followed your update and always get an issue. If I apply the same code snippet directly in my script, i.e :
g.add_legend(['An example legend - item 1'],
legend_loc='upper right', # we want to specify the location of this point
bbox_to_anchor=(bottom_right_plot, top_left_plot),
bbox_transform=plt.gcf().transFigure, # this is the x and y co-ords we extracted above
borderaxespad=0, # this means there is no padding around the legend
edgecolor='black')
Then I get the following figure :
As you can see, the coordinates are not really what is really expected : a slight shift on x-coordinate and y-coordinate is present.
If I apply your code snippet for my legend text, I get:
I give you the link of my entire script, this will be easier maybe for you to see an error compared what is expected:
My entire Python script
Here's my answer, it's the same as #scleronomic's answer, but I'll point out some of the things that tripped me up when figuring this out.
Below is my code to reproduce your desired positioning, I've tried to create the same subplot layout to you, but through matplotlib not getdist -- same result though.
As you discovered, the trick lies in extracting the position data of the first and last axes (top-left and lower-right), to reference from. The bounds method that you used returns: the x0, y0, width and height of the axes (see the docs). However what we want is the maximum x and y, so that our legend corner is in the top right. This can be achieved by using the xmax and ymax method:
axes.flatten()[-1].get_position().xmax
axes.flatten()[0].get_position().ymax
Once we have these variables they can be passed into the bbox_to_anchor parameter of the add_legend() function, as you did. However, if we use loc='upper right' too, it tells matplotlib that we want the upper right of the legend to be pinned to this top right corner. Finally, we need to set borderaxespad=0 otherwise the legend won't sit exactly where we want it to due to default padding.
Please see my example code below, as well as the resulting picture. Note that I left the top-right plot in so you can see that it lines up correctly.
Also, note that as #scleronomic says, calls to plt.tight_layout() etc will mess this positioning up.
import matplotlib.pyplot as plt
# code to layout subplots as in your example:
# --------------------------------------------
g, axes = plt.subplots(nrows=7, ncols=7,figsize=(10,10))
unwanted = [1,2,3,4,5,9,10,11,12,13,17,
18,19,20,25,26,27,33,34,41]
for ax in axes.flatten():
ax.plot([1,2], [1,2])
ax.set_yticks([])
ax.set_xticks([])
for n, ax in enumerate(axes.flatten()):
if n in unwanted:
ax.remove()
# Code to answer your question:
# ------------------------------
# get the max y position of the top left axis
top_left_plot = axes.flatten()[0].get_position().ymax
# get the max x position of the bottom right axis
# it is -1 to reference the last plot
bottom_right_plot = axes.flatten()[-1].get_position().xmax
# I'm using the matplotlib so it is g.legend() not g.add_legend
# but g.add_legend() should work the same as it is a wrapper of th ematplotlib func
g.legend(['Opt. Flat. No Gamma. - cross - standard situation - Criterion taking into accound a = 200',
'Pess. Flat. No Gamma. - cross - standard situation - Criterion taking into account a = 300'],
loc='upper right', # we want to specify the location of this point
bbox_to_anchor=(bottom_right_plot, top_left_plot), # this is the x and y co-ords we extracted above
borderaxespad=0, # this means there is no padding around the legend
edgecolor='black') # I set it black for this example
plt.show()
Update
After #youpilat13's comments, I investigated some more and installed getdist to try and recreate with that tool. Initially I got the same results, but found the trick is, unlike if you were making this in matplotlib, you have to transform the legend's coordinates to figure coordinates. This can be achieved with the following in the g.add_legend() call:
bbox_transform=plt.gcf().transFigure
Here is a complete example:
import getdist
from getdist import plots, MCSamples
from getdist.gaussian_mixtures import GaussianND
covariance = [[0.001**2, 0.0006*0.05, 0], [0.0006*0.05, 0.05**2, 0.2**2], [0, 0.2**2, 2**2]]
mean = [0.02, 1, -2]
gauss=GaussianND(mean, covariance)
g = plots.get_subplot_plotter(subplot_size=3)
g.triangle_plot(gauss,filled=True)
top_left_plot = g.subplots.flatten()[0].get_position().ymax
bottom_right_plot = g.subplots.flatten()[-1].get_position().xmax
g.add_legend(['An example legend - item 1'],
legend_loc='upper right', # we want to specify the location of this point
bbox_to_anchor=(bottom_right_plot, top_left_plot),
bbox_transform=plt.gcf().transFigure, # this is the x and y co-ords we extracted above
borderaxespad=0, # this means there is no padding around the legend
edgecolor='black')
And the resulting image:
It basically works as you described. The bboxes (xmin, ymin, width, height) of the axes are given in fractions of the figure and plt.legend() uses the same format so the two are compatible. By setting the upper right corner of the legend to the corner defined by the outer most axes you get the clean layout and don't have to worry about the exact size of the legend.
import matplotlib.pyplot as plt
n = 4
# Create the subplot grid
# Alternative: fig, ax = plt.subplots(n, n); ax[i, j].remove() for j > i
fig = plt.figure()
gs = fig.add_gridspec(nrows=n, ncols=n)
ax = np.zeros((n, n), dtype=object)
for i in range(n):
for j in range(n):
if j <= i:
ax[i, j] = fig.add_subplot(gs[i, j])
# add this to make the position of the legend easier to spot
ax[0, -1] = fig.add_subplot(gs[0, -1])
# Plot some dummy data
ax[0, 0].plot(range(10), 'b-o', label='Dummy Label 4x4')
# Set the legend
y_max = ax[0][0].get_position().ymax
x_max = ax[-1][-1].get_position().xmax
fig.legend(loc='upper right', bbox_to_anchor=(x_max, y_max),
borderaxespad=0)
plt.show()
Some pitfalls could be using the Constrained Layout
or using bbox_inches='tight' when saving the file as both screw up the position of the legend in unexpected ways.
For some more examples of legend placement I found this collection
very helpful.
In the pyplot document for scatter plot:
matplotlib.pyplot.scatter(x, y, s=20, c='b', marker='o', cmap=None, norm=None,
vmin=None, vmax=None, alpha=None, linewidths=None,
faceted=True, verts=None, hold=None, **kwargs)
The marker size
s:
size in points^2. It is a scalar or an array of the same length as x and y.
What kind of unit is points^2? What does it mean? Does s=100 mean 10 pixel x 10 pixel?
Basically I'm trying to make scatter plots with different marker sizes, and I want to figure out what does the s number mean.
This can be a somewhat confusing way of defining the size but you are basically specifying the area of the marker. This means, to double the width (or height) of the marker you need to increase s by a factor of 4. [because A = WH => (2W)(2H)=4A]
There is a reason, however, that the size of markers is defined in this way. Because of the scaling of area as the square of width, doubling the width actually appears to increase the size by more than a factor 2 (in fact it increases it by a factor of 4). To see this consider the following two examples and the output they produce.
# doubling the width of markers
x = [0,2,4,6,8,10]
y = [0]*len(x)
s = [20*4**n for n in range(len(x))]
plt.scatter(x,y,s=s)
plt.show()
gives
Notice how the size increases very quickly. If instead we have
# doubling the area of markers
x = [0,2,4,6,8,10]
y = [0]*len(x)
s = [20*2**n for n in range(len(x))]
plt.scatter(x,y,s=s)
plt.show()
gives
Now the apparent size of the markers increases roughly linearly in an intuitive fashion.
As for the exact meaning of what a 'point' is, it is fairly arbitrary for plotting purposes, you can just scale all of your sizes by a constant until they look reasonable.
Edit: (In response to comment from #Emma)
It's probably confusing wording on my part. The question asked about doubling the width of a circle so in the first picture for each circle (as we move from left to right) it's width is double the previous one so for the area this is an exponential with base 4. Similarly the second example each circle has area double the last one which gives an exponential with base 2.
However it is the second example (where we are scaling area) that doubling area appears to make the circle twice as big to the eye. Thus if we want a circle to appear a factor of n bigger we would increase the area by a factor n not the radius so the apparent size scales linearly with the area.
Edit to visualize the comment by #TomaszGandor:
This is what it looks like for different functions of the marker size:
x = [0,2,4,6,8,10,12,14,16,18]
s_exp = [20*2**n for n in range(len(x))]
s_square = [20*n**2 for n in range(len(x))]
s_linear = [20*n for n in range(len(x))]
plt.scatter(x,[1]*len(x),s=s_exp, label='$s=2^n$', lw=1)
plt.scatter(x,[0]*len(x),s=s_square, label='$s=n^2$')
plt.scatter(x,[-1]*len(x),s=s_linear, label='$s=n$')
plt.ylim(-1.5,1.5)
plt.legend(loc='center left', bbox_to_anchor=(1.1, 0.5), labelspacing=3)
plt.show()
Because other answers here claim that s denotes the area of the marker, I'm adding this answer to clearify that this is not necessarily the case.
Size in points^2
The argument s in plt.scatter denotes the markersize**2. As the documentation says
s : scalar or array_like, shape (n, ), optional
size in points^2. Default is rcParams['lines.markersize'] ** 2.
This can be taken literally. In order to obtain a marker which is x points large, you need to square that number and give it to the s argument.
So the relationship between the markersize of a line plot and the scatter size argument is the square. In order to produce a scatter marker of the same size as a plot marker of size 10 points you would hence call scatter( .., s=100).
import matplotlib.pyplot as plt
fig,ax = plt.subplots()
ax.plot([0],[0], marker="o", markersize=10)
ax.plot([0.07,0.93],[0,0], linewidth=10)
ax.scatter([1],[0], s=100)
ax.plot([0],[1], marker="o", markersize=22)
ax.plot([0.14,0.86],[1,1], linewidth=22)
ax.scatter([1],[1], s=22**2)
plt.show()
Connection to "area"
So why do other answers and even the documentation speak about "area" when it comes to the s parameter?
Of course the units of points**2 are area units.
For the special case of a square marker, marker="s", the area of the marker is indeed directly the value of the s parameter.
For a circle, the area of the circle is area = pi/4*s.
For other markers there may not even be any obvious relation to the area of the marker.
In all cases however the area of the marker is proportional to the s parameter. This is the motivation to call it "area" even though in most cases it isn't really.
Specifying the size of the scatter markers in terms of some quantity which is proportional to the area of the marker makes in thus far sense as it is the area of the marker that is perceived when comparing different patches rather than its side length or diameter. I.e. doubling the underlying quantity should double the area of the marker.
What are points?
So far the answer to what the size of a scatter marker means is given in units of points. Points are often used in typography, where fonts are specified in points. Also linewidths is often specified in points. The standard size of points in matplotlib is 72 points per inch (ppi) - 1 point is hence 1/72 inches.
It might be useful to be able to specify sizes in pixels instead of points. If the figure dpi is 72 as well, one point is one pixel. If the figure dpi is different (matplotlib default is fig.dpi=100),
1 point == fig.dpi/72. pixels
While the scatter marker's size in points would hence look different for different figure dpi, one could produce a 10 by 10 pixels^2 marker, which would always have the same number of pixels covered:
import matplotlib.pyplot as plt
for dpi in [72,100,144]:
fig,ax = plt.subplots(figsize=(1.5,2), dpi=dpi)
ax.set_title("fig.dpi={}".format(dpi))
ax.set_ylim(-3,3)
ax.set_xlim(-2,2)
ax.scatter([0],[1], s=10**2,
marker="s", linewidth=0, label="100 points^2")
ax.scatter([1],[1], s=(10*72./fig.dpi)**2,
marker="s", linewidth=0, label="100 pixels^2")
ax.legend(loc=8,framealpha=1, fontsize=8)
fig.savefig("fig{}.png".format(dpi), bbox_inches="tight")
plt.show()
If you are interested in a scatter in data units, check this answer.
You can use markersize to specify the size of the circle in plot method
import numpy as np
import matplotlib.pyplot as plt
x1 = np.random.randn(20)
x2 = np.random.randn(20)
plt.figure(1)
# you can specify the marker size two ways directly:
plt.plot(x1, 'bo', markersize=20) # blue circle with size 10
plt.plot(x2, 'ro', ms=10,) # ms is just an alias for markersize
plt.show()
From here
It is the area of the marker. I mean if you have s1 = 1000 and then s2 = 4000, the relation between the radius of each circle is: r_s2 = 2 * r_s1. See the following plot:
plt.scatter(2, 1, s=4000, c='r')
plt.scatter(2, 1, s=1000 ,c='b')
plt.scatter(2, 1, s=10, c='g')
I had the same doubt when I saw the post, so I did this example then I used a ruler on the screen to measure the radii.
I also attempted to use 'scatter' initially for this purpose. After quite a bit of wasted time - I settled on the following solution.
import matplotlib.pyplot as plt
input_list = [{'x':100,'y':200,'radius':50, 'color':(0.1,0.2,0.3)}]
output_list = []
for point in input_list:
output_list.append(plt.Circle((point['x'], point['y']), point['radius'], color=point['color'], fill=False))
ax = plt.gca(aspect='equal')
ax.cla()
ax.set_xlim((0, 1000))
ax.set_ylim((0, 1000))
for circle in output_list:
ax.add_artist(circle)
This is based on an answer to this question
If the size of the circles corresponds to the square of the parameter in s=parameter, then assign a square root to each element you append to your size array, like this: s=[1, 1.414, 1.73, 2.0, 2.24] such that when it takes these values and returns them, their relative size increase will be the square root of the squared progression, which returns a linear progression.
If I were to square each one as it gets output to the plot: output=[1, 2, 3, 4, 5]. Try list interpretation: s=[numpy.sqrt(i) for i in s]
How can I plot a dendrogram right on top of a matrix of values, reordered appropriately to reflect the clustering, in Python? An example is the following figure:
This is Figure 6 from: A panel of induced pluripotent stem cells from chimpanzees: a resource for comparative functional genomics
I use scipy.cluster.dendrogram to make my dendrogram and perform hierarchical clustering on a matrix of data. How can I then plot the data as a matrix where the rows have been reordered to reflect a clustering induced by the cutting the dendrogram at a particular threshold, and have the dendrogram plotted alongside the matrix? I know how to plot the dendrogram in scipy, but not how to plot the intensity matrix of data with the right scale bar next to it.
The question does not define matrix very well: "matrix of values", "matrix of data". I assume that you mean a distance matrix. In other words, element D_ij in the symmetric nonnegative N-by-N distance matrix D denotes the distance between two feature vectors, x_i and x_j. Is that correct?
If so, then try this (edited June 13, 2010, to reflect two different dendrograms).
Tested in python 3.10 and matplotlib 3.5.1
import numpy as np
import matplotlib.pyplot as plt
import scipy.cluster.hierarchy as sch
from scipy.spatial.distance import squareform
# Generate random features and distance matrix.
np.random.seed(200) # for reproducible data
x = np.random.rand(40)
D = np.zeros([40, 40])
for i in range(40):
for j in range(40):
D[i,j] = abs(x[i] - x[j])
condensedD = squareform(D)
# Compute and plot first dendrogram.
fig = plt.figure(figsize=(8, 8))
ax1 = fig.add_axes([0.09, 0.1, 0.2, 0.6])
Y = sch.linkage(condensedD, method='centroid')
Z1 = sch.dendrogram(Y, orientation='left')
ax1.set_xticks([])
ax1.set_yticks([])
# Compute and plot second dendrogram.
ax2 = fig.add_axes([0.3, 0.71, 0.6, 0.2])
Y = sch.linkage(condensedD, method='single')
Z2 = sch.dendrogram(Y)
ax2.set_xticks([])
ax2.set_yticks([])
# Plot distance matrix.
axmatrix = fig.add_axes([0.3, 0.1, 0.6, 0.6])
idx1 = Z1['leaves']
idx2 = Z2['leaves']
D = D[idx1,:]
D = D[:,idx2]
im = axmatrix.matshow(D, aspect='auto', origin='lower', cmap=plt.cm.YlGnBu)
axmatrix.set_xticks([]) # remove axis labels
axmatrix.set_yticks([]) # remove axis labels
# Plot colorbar.
axcolor = fig.add_axes([0.91, 0.1, 0.02, 0.6])
plt.colorbar(im, cax=axcolor)
plt.show()
fig.savefig('dendrogram.png')
Edit: For different colors, adjust the cmap attribute in imshow. See the scipy/matplotlib docs for examples. That page also describes how to create your own colormap. For convenience, I recommend using a preexisting colormap. In my example, I used YlGnBu.
Edit: add_axes (see documentation here) accepts a list or tuple: (left, bottom, width, height). For example, (0.5,0,0.5,1) adds an Axes on the right half of the figure. (0,0.5,1,0.5) adds an Axes on the top half of the figure.
Most people probably use add_subplot for its convenience. I like add_axes for its control.
To remove the border, use add_axes([left,bottom,width,height], frame_on=False). See example here.
If in addition to the matrix and dendrogram it is required to show the labels of the elements, the following code can be used, that shows all the labels rotating the x labels and changing the font size to avoid overlapping on the x axis. It requires moving the colorbar to have space for the y labels:
axmatrix.set_xticks(range(40))
axmatrix.set_xticklabels(idx1, minor=False)
axmatrix.xaxis.set_label_position('bottom')
axmatrix.xaxis.tick_bottom()
pylab.xticks(rotation=-90, fontsize=8)
axmatrix.set_yticks(range(40))
axmatrix.set_yticklabels(idx2, minor=False)
axmatrix.yaxis.set_label_position('right')
axmatrix.yaxis.tick_right()
axcolor = fig.add_axes([0.94,0.1,0.02,0.6])
The result obtained is this (with a different color map):
Hi
I'm trying to create plots which incorporate parallel conversion scales for two sets of units on the y-axis; using the two different styles of:
offset ('parasitic') y-axes and
overlaid/shared y-axes
to replicate the style of the left-hand y-axes in the attached example images.
I'd like to find the simplest generic way of producing both of the above example plots, which also allows me to generate the y-axis conversion scales by defining the relationship between the two sets of units as a function (in this example: mmHg = kPa * 7.5).
If it's possible to add the third right-hand y axes (vapour concentration and water content) shown in these examples, which are unrelated to the left hand scales, this would be a bonus.
I've read related stackoverflow.com postings and examples on using multiple x and y axes using the twinx and twiny functions - e.g.
here - as well as the Matplotlib cookbook, but I can't find an example which addresses this particular problem.
I'd be very grateful for any minimal working examples or links.
I'm using Matplotlib in Spyder 2.2.1 / Python 2.7.5
Many thanks in anticipation
Dave
For the first plot, I recommend axisartist. The automatic scaling of the two y-axis on the left-hand-side is achieved through a simple scaling factor that applies to the specified y-limits. This first example is based on the explanations on parasite axes:
import numpy as np
from mpl_toolkits.axes_grid1 import host_subplot
import mpl_toolkits.axisartist as AA
import matplotlib.pyplot as plt
# initialize the three axis:
host = host_subplot(111, axes_class=AA.Axes)
plt.subplots_adjust(left=0.25)
par1 = host.twinx()
par2 = host.twinx()
# secify the offset for the left-most axis:
offset = -60
new_fixed_axis = par2.get_grid_helper().new_fixed_axis
par2.axis["right"] = new_fixed_axis(loc="left", axes=par2, offset=(offset, 0))
par2.axis["right"].toggle(all=True)
# data ratio for the two left y-axis:
y3_to_y1 = 1/7.5
# y-axis limits:
YLIM = [0.0, 150.0,
0.0, 150.0]
# set up dummy data
x = np.linspace(0,70.0,70.0)
y1 = np.asarray([xi**2.0*0.032653 for xi in x])
y2 = np.asarray([xi**2.0*0.02857 for xi in x])
# plot data on y1 and y2, respectively:
host.plot(x,y1,'b')
par1.plot(x,y2,'r')
# specify the axis limits:
host.set_xlim(0.0,70.0)
host.set_ylim(YLIM[0],YLIM[1])
par1.set_ylim(YLIM[2],YLIM[3])
# when specifying the limits for the left-most y-axis
# you utilize the conversion factor:
par2.set_ylim(YLIM[2]*y3_to_y1,YLIM[3]*y3_to_y1)
# set y-ticks, use np.arange for defined deltas
# add a small increment to the last ylim value
# to ensure that the last value will be a tick
host.set_yticks(np.arange(YLIM[0],YLIM[1]+0.001,10.0))
par1.set_yticks(np.arange(YLIM[2],YLIM[3]+0.001,10.0))
par2.set_yticks(np.arange(YLIM[2]*y3_to_y1,YLIM[3]*y3_to_y1+0.001, 2.0))
plt.show()
You will end up with this plot:
You can try to modify the above example to give you the second plot, too. One idea is, to reduce offset to zero. However, with the axisartist, certain tick functions are not supported. One of them is specifying if the ticks go inside or outside the axis.
Therefore, for the second plot, the following example (based on matplotlib: overlay plots with different scales?) is appropriate.
import numpy as np
import matplotlib.pyplot as plt
# initialize the three axis:
fig = plt.figure()
ax1 = fig.add_subplot(111)
ax2 = ax1.twinx()
ax3 = ax1.twinx()
# data ratio for the two left y-axis:
y3_to_y1 = 1/7.5
# y-axis limits:
YLIM = [0.0, 150.0,
0.0, 150.0]
# set up dummy data
x = np.linspace(0,70.0,70.0)
y1 = np.asarray([xi**2.0*0.032653 for xi in x])
y2 = np.asarray([xi**2.0*0.02857 for xi in x])
# plot the data
ax1.plot(x,y1,'b')
ax2.plot(x,y2,'r')
# define the axis limits
ax1.set_xlim(0.0,70.0)
ax1.set_ylim(YLIM[0],YLIM[1])
ax2.set_ylim(YLIM[2],YLIM[3])
# when specifying the limits for the left-most y-axis
# you utilize the conversion factor:
ax3.set_ylim(YLIM[2]*y3_to_y1,YLIM[3]*y3_to_y1)
# move the 3rd y-axis to the left (0.0):
ax3.spines['right'].set_position(('axes', 0.0))
# set y-ticks, use np.arange for defined deltas
# add a small increment to the last ylim value
# to ensure that the last value will be a tick
ax1.set_yticks(np.arange(YLIM[0],YLIM[1]+0.001,10.0))
ax2.set_yticks(np.arange(YLIM[2],YLIM[3]+0.001,10.0))
ax3.set_yticks(np.arange(YLIM[2]*y3_to_y1,YLIM[3]*y3_to_y1+0.001, 2.0))
# for both letf-hand y-axis move the ticks to the outside:
ax1.get_yaxis().set_tick_params(direction='out')
ax3.get_yaxis().set_tick_params(direction='out')
plt.show()
This results in this figure:
Again, the set_tick_params(direction='out') does not work with the axisartist from the first example.
Somewhat counter-intuitive, both the y1 and y3 ticks have to be set to 'out'. For y1, this makes sense, and for y3 you have to remember that it started as a right-hand-side axis. Therefore, those ticks would appear outside (with the default 'in' setting) when the axis is moved to the left.
I am not able to draw a simple, vertical arrow in the following log-log plot:
#!/usr/bin/python2
import matplotlib.pyplot as plt
import matplotlib as mpl
plt.yscale('log')
plt.xscale('log')
plt.ylim((1e-20,1e-10))
plt.xlim((1e-12,1))
plt.arrow(0.00006666, 1e-20, 0, 1e-8 - 1e-20, length_includes_head=True)
plt.savefig('test.pdf')
It just doesn't show. From the documentation it appears as if all the arguments, like width, height and so on relate to the scale of the axis. This is very counter-intuitive. I tried using twin() of the axisartist package to define an axis on top of mine with limits (0,1), (0,1) to have more control over the arrow's parameters, but I couldn't figure out how to have a completely independent axis on top of the primary one.
Any ideas?
I was looking for an answer to this question, and found a useful answer! You can specify any "mathtext" character (matplotlib's version of LaTeX) as a marker. Try:
plt.plot(x,y, 'ko', marker=r'$\downarrow$', markersize=20)
This will plot a downward pointing, black arrow at position (x,y) that looks good on any plot (even log-log).
See: matplotlib.org/users/mathtext.html#mathtext-tutorial for more symbols you can use.
Subplots approach
After creating the subplots do the following
Align the positions
Use set_axis_off() to turn the axis off (ticks, labels, etc)
Draw the arrow!
So a few lines gets whats you want!
E.g.
#!/usr/bin/python2
import matplotlib.pyplot as plt
hax = plt.subplot(1,2,1)
plt.yscale('log')
plt.xscale('log')
plt.ylim((1e-20,1e-10))
plt.xlim((1e-12,1))
hax2 = plt.subplot(1,2,2)
plt.arrow(0.1, 1, 0, 1, length_includes_head=True)
hax.set_position([0.1, 0.1, 0.8, 0.8])
hax2.set_position([0.1, 0.1, 0.8, 0.8])
hax2.set_axis_off()
plt.savefig('test.pdf')
Rescale data
Alternatively a possibly easier approach, though the axis labels may be tricky, is to rescale the data.
i.e.
import numpy
# Other import commands and data input
plt.plot(numpy.log10(x), numpy.log10(y)))
Not a great solution, but a decent result if you can handle the tick labels!
I know this thread has been dead for a long time now, but I figure posting my solution might be helpful for anyone else trying to figure out how to draw arrows on log-scale plots efficiently.
As an alternative to what others have already posted, you could use a transformation object to input the arrow coordinates not in the scale of the original axes but in the (linear) scale of the "axes coordinates". What I mean by axes coordinates are those that are normalized to [0,1] (horizontal range) by [0,1] (vertical range), where the point (0,0) would be the bottom-left corner and the point (1,1) would be the top-right, and so on. Then you could simply include an arrow by:
plt.arrow(0.1, 0.1, 0.9, 0.9, transform=plot1.transAxes, length_includes_head=True)
This gives an arrow that spans diagonally over 4/5 of the plot's horizontal and vertical range, from the bottom-left to the top-right (where plot1 is the subplot name).
If you want to do this in general, where exact coordinates (x0,y0) and (x1,y1) in the log-space can be specified for the arrow, this is not too difficult if you write two functions fx(x) and fy(y) that transform from the original coordinates to these "axes" coordinates. I've given an example of how the original code posted by the OP could be modified to implement this below (apologies for not including the images the code produces, I don't have the required reputation yet).
#!/usr/bin/python3
import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
# functions fx and fy take log-scale coordinates to 'axes' coordinates
ax = 1E-12 # [ax,bx] is range of horizontal axis
bx = 1E0
def fx(x):
return (np.log(x) - np.log(ax))/(np.log(bx) - np.log(ax))
ay = 1E-20 # [ay,by] is range of vertical axis
by = 1E-10
def fy(y):
return (np.log(y) - np.log(ay))/(np.log(by) - np.log(ay))
plot1 = plt.subplot(111)
plt.xscale('log')
plt.yscale('log')
plt.xlim(ax, bx)
plt.ylim(ay, by)
# transformed coordinates for arrow from (1E-10,1E-18) to (1E-4,1E-16)
x0 = fx(1E-10)
y0 = fy(1E-18)
x1 = fx(1E-4) - fx(1E-10)
y1 = fy(1E-16) - fy(1E-18)
plt.arrow(
x0, y0, x1, y1, # input transformed arrow coordinates
transform = plot1.transAxes, # tell matplotlib to use axes coordinates
facecolor = 'black',
length_includes_head=True
)
plt.grid(True)
plt.savefig('test.pdf')