Read height of legend in Python - python

I have some plots with a lot of information and lines, so sometimes I tend to put the legend outside the plot itself using bbox_to_anchor. I also prefer to have a title of the plot, but this will positionally coincide with the legend in that case. The following example below is just an illustration of the problem.
import numpy as np
import matplotlib.pyplot as plt
t = np.arange(0.0, 2.0, 0.01)
s = 1 + np.sin(2 * np.pi * t)
r = 1 + np.sin(4 * np.pi * t)
q = 1 + np.sin(6 * np.pi * t)
fig, ax = plt.subplots()
ax.plot(t, s, label='S')
ax.plot(t, r, label='R')
ax.plot(t, q, label='Q')
leg = ax.legend(loc=3, ncol=3, bbox_to_anchor=(.0, 1.02, 1., .102), borderaxespad=0., mode='expand')
ax.set_title('SIMPLE PLOT', y=1.1)
plt.show()
To avoid this, I set some kind of y-value (e.g. y=1.1). I would like to automate this process because I keep updating the same plot with new data, so the legend grows in size, and I need to adjust the position of the title accordingly.
Is there a way to automate this process?
Is there a function in Python that is able to read the height of the legend so that this can be used to adjust the title position?

The height of the legend is determined at draw time. You can get it after having drawn the figure via legend.get_window_extent(). The resulting bounding box is in units of pixels. In order to find the offset of the title, you will need to subtract the upper limit of the legend from the upper limit of the axes. So you need to get the axes position in pixels as well.
The title can be offset either in figure coordinates (y=1.1) or points (pad=20). I would suggest to use points here, to make it independent of the size of the axes. So you can calculate the difference in upper positions, convert from pixels to points (i.e. distance [pixels] * ppi / dpi) and add some constant offset in points (because usually you would not want the title to sit exactly on the border of the legend). Then use that number as pad.
import numpy as np
import matplotlib.pyplot as plt
fig, ax = plt.subplots(constrained_layout=True)
ax.plot([1,2,3], np.random.rand(3,5), label='Label')
leg = ax.legend(loc="lower center", ncol=3, bbox_to_anchor=(.0, 1.02, 1., 1.02),
borderaxespad=0, mode='expand')
fig.canvas.draw()
leg_box = leg.get_window_extent()
ax_box = ax.get_position().transformed(fig.transFigure)
pad = (leg_box.y1 - ax_box.y1)*72./fig.dpi + 6
ax.set_title('SIMPLE PLOT', pad=pad)
plt.show()
Note that here I also used constrained_layout to have the title not cropped by the figure boundaries.

Related

matplotlib how to set plot size (with dpi), not figure size

I could find a way to set a figure size with dpi
px = 1/plt.rcParams['figure.dpi']
fig = plt.figure(figsize=(1580*px, 25*px))
(reference: https://matplotlib.org/stable/gallery/subplots_axes_and_figures/figure_size_units.html)
fig = plt.figure(figsize=(1580*px, 25*px))
plt.plot(xx, y[0], label='min')
plt.plot(xx, y[1], label='max')
plt.yticks(y_ticks, y_tick_labels)
plt.ylim(top=y_max)
plt.legend()
However, how do you set the plot size?
I want my plot or graph to be full of (1580px, 25px)
but if I set the figure size and plot graphs using the above code, then the graph does not fit the figure (1580px, 25px). Even worse, labels or ticks are not shown well in the figure like below.
I want my graph size to be the above white space size( for example, 1580px, 25px) and then draw ticks and labels outside the white space (then figure size should be bigger than the given plot size). But I couldn't find a way to set the plot size. I could only find a way to set the figure size.
import matplotlib.pyplot as plt
import numpy as np
def axes_with_pixels(width, height, margin=0.2):
px = 1/plt.rcParams['figure.dpi']
fig_width, fig_height = np.array([width, height]) / (1 - 2 * margin)
fig, ax = plt.subplots(figsize=(fig_width*px, fig_height*px))
fig.subplots_adjust(left=margin, right=1-margin,
bottom=margin, top=1-margin)
return fig, ax
fig, ax = axes_with_pixels(580, 80) # Specify the Axes size in pixels
X = np.linspace(0, 10, 10)
Y0 = np.sin(X)
Y1 = np.cos(X)
plt.plot(X, Y0, label='min')
plt.plot(X, Y1, label='max')
plt.legend()
As you can see, the Axes (plot area) is exactly 580 * 80 pixels. (Note, the shown width of 581 pixels is due to the offset of the right edge.)
However, axes_with_pixels can be only used to set a single Axes with a specified pixels. If you want a figure to have multiple Axes with some specified pixels, then you have to consider wspace and hspace in subplots_adjust to get the figure size.

Draw markers at intersections of grid's minor ticks

I would like to produce a plot with a grid, so that a full line is drawn at major ticks, and intersections of minor ticks are marked by squares (or any customisable marker).
Here is an example of what I'm trying to achieve:
I generated this plot with the following code, using RegularPolyCollection:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.collections import RegularPolyCollection
# Define dimensions and create plotting objects
width_squares = 6
height_squares = 6
figure = plt.figure()
ax = figure.add_subplot(111)
# Define ticks
x_minors = np.linspace(0, width_squares, 5 * width_squares + 1)
x_majors = np.linspace(0, width_squares, width_squares + 1)
y_minors = np.linspace(0, height_squares, 5 * height_squares + 1)
y_majors = np.linspace(0, height_squares, height_squares + 1)
# Set ticks
ax.set_xticks(x_majors)
ax.set_xticks(x_minors, minor=True)
ax.set_yticks(y_majors)
ax.set_yticks(y_minors, minor=True)
# Define window
ax.set_xlim((0, 6))
ax.set_ylim((0, 6))
# Draw the point collection: squares rotated by 45°
offsets = [(x, y) for x in x_minors for y in y_minors]
points = RegularPolyCollection(
4,
sizes=(1,),
offsets=offsets,
color=('lightgray',),
transOffset=ax.transData,
rotation=0.7857
)
ax.add_collection(points)
# Draw the grid at major ticks
ax.grid(True, which='major', axis='both', color='lightgray')
plt.show()
However, the plot I'm actually trying to produce are way bigger, and performance is at stake.
Unfortunately, drawing a large collection of points is very time consuming.
I also investigated based on this question, and I produced a similar result by drawing vertical lines with linestyle set to "None", so that only intersections are marked, but the time consumption is similar to the collection approach.
I suspect there should be a combination of parameters for the plt.grid function that would produce what I want, but I could not understand the effect of markevery and other keyword arguments (while I do understand their meaning when used with Line2D objects).
Is there a standard way to produce such a grid? If so, is it possible to make it little time-consuming?
I am not sure if you tried the version from one of the answers provided in the link you shared. The main modification I had to do was to turn on the minor ticks while getting the x-tick and y-tick data. Do you have any numbers comparing the time complexity for this approach and the Line2D?
# Draw the grid at major ticks
ax.grid(True, which='major', axis='both')
ax.set_aspect('equal')
def set_grid_cross(ax):
xticks = ax.get_xticks(minor=True)
yticks = ax.get_yticks(minor=True)
xgrid, ygrid = np.meshgrid(xticks, yticks)
kywds = dict()
grid_lines = ax.plot(xgrid, ygrid, 'o', ms=2, color='lightgray', alpha=0.5)
set_grid_cross(ax)

Long vertical bar plot with matplotlib

I need to create a Python script that plots a list of (sorted) value as a vertical bar plot. I'd like to plot all the values and save it as a long vertical plot, so that both the yticks labels and bars are clearly visible. That is, I'd like a "long" verticalplot. The number of elements in the list varies (e.g. from 500 to 1000), so the use of figsize does not help as I don't know how long that should be.
import matplotlib.pyplot as plt
import numpy as np
fig, ax = plt.subplots()
# Example data
n = 500
y_pos = np.arange(n)
performance = 3 + 10 * np.random.rand(n)
ax.barh(y_pos, np.sort(performance), align='center', color='green', ecolor='black')
ax.set_yticks(y_pos)
ax.set_yticklabels([str(x) for x in y_pos])
ax.set_xlabel('X')
How can I modify the script so that I can stretch the figure vertically and make it readable?
Change the figsize depending on the number of data values. Also, manage the y-axis limit accordingly.
The following works perfectly:
n = 500
fig, ax = plt.subplots(figsize=(5,n//5)) # Changing figsize depending upon data
# Example data
y_pos = np.arange(n)
performance = 3 + 10 * np.random.rand(n)
ax.barh(y_pos, np.sort(performance), align='center', color='green', ecolor='black')
ax.set_yticks(y_pos)
ax.set_yticklabels([str(x) for x in y_pos])
ax.set_xlabel('X')
ax.set_ylim(0, n) # Manage y-axis properly
Given below is the output picture for n=200

Bargraph relative to a fixed point Matplotlib

I'm trying to plot a set of points relative to 1; I can do this on Excel but I'm struggling to do this in Python. The hardest part for me is to keep it into log scale for the Y-axis. If I have values
0.15 0.7 1.3 0.5 1.7,
How can I use matplotlib to achieve the same effect as shown below (done by Excel)? The best I've come up with so far is to subtract 1 from each value to actually center it around 0, but this ends up messing up the scaling.
This is what I want:
This is a failed attempt I get from Python:
Another failed attempt at Python gives me this; even though the Y-axis is in the log-range, notice that everything starts from the bottom, when I really want the values to be going up/down with respect to the centre, or 1, on the Y-axis
Does this work for you:
# our imports
import numpy as np
import matplotlib.pyplot as plt
# define the sample size and draw a random sample
N = 5
ind = np.arange(N)
sample = np.random.uniform(low=-1, high=1, size=(N))
# initialize our bar width and the subplot
width = 0.35
fig, ax = plt.subplots()
# plot our indexes, sample using a blue color
rects1 = ax.bar(ind, sample, width, color='blue')
# Set our axes labels, title, tick marks, and then our x ticks.
ax.set_ylabel('Scores')
ax.set_title('Scores Example')
ax.set_xticks(ind + width)
ax.set_xticklabels(('E1', 'E2', 'E3', 'E4', 'E5'))
# Create a horizontal line at the origin
ax.axhline(y=0, color='black')
# Show our plot, do whatever
plt.show()
More examples and references can be found here and here.
You can also use hex codes and set no outline for the bar graphs, and give some spacing for the actual plot, along with adding log axes:
Here is the final code to give a representation similar to your own:
# our imports
import numpy as np
import matplotlib.pyplot as plt
# define the sample size and draw a random sample
N = 5
ind = np.arange(N)
sample = 10 ** np.random.uniform(low=-2, high=2, size=(N))
# initialize our bar width and the subplot
width = 0.35
fig, ax = plt.subplots()
# plot our indexes, sample using a hex color and a 0 linewidth to get rid
# of the plot edges
rects1 = ax.bar(ind, sample, width, color='#50A6C2', linewidth=0)
# Set our axes labels, title, tick marks, and then our x ticks.
ax.set_ylabel('Scores')
ax.set_title('Scores Example')
ax.set_xticks(ind + width)
ax.set_xticklabels(('E1', 'E2', 'E3', 'E4', 'E5'))
# Create a horizontal line at the origin
ax.axhline(y=1, color='black')
# Set our limits
ax.set_xlim(-1, 5)
ax.set_yscale('log')
ax.set_ylim(0.01, 100)
# Show our plot, do whatever
plt.show()

Multiple y-scales but only one enabled for pan and zoom

Consider the following python code for plotting a matplotlib figure:
import matplotlib.pylab as pp
import numpy as np
alpha = np.linspace(0, 2 * np.pi, 400)
sig1 = np.sin(alpha)
sig2 = np.sin(2 * alpha) + 2 * (alpha > np.pi)
ax1 = pp.subplot(111)
ax2 = ax1.twinx()
ax1.plot(alpha, sig1, color='b')
ax2.plot(alpha, sig2, color='r')
ax1.set_ylabel('sig1 value', color='b')
ax2.set_ylabel('sig2 value', color='r')
pp.grid()
pp.show()
Giving me a nice plot
I would like to find out how to disable one of the axes for panning / zooming, so when I use the pan / zoom tool, only ax2 will rescale for example. Is there a way to do this? I want to do it programmatically.
You can do this using ax2.set_navigate(False):
from matplotlib.pyplot import *
import numpy as np
fig,ax1 = subplots(1,1)
ax2 = ax1.twinx()
ax2.set_navigate(False)
x = np.linspace(0,2*np.pi,100)
ax1.plot(x,np.sin(x),'b')
ax1.set_xlabel('Scaleable axis')
ax1.set_ylabel('Scaleable axis')
ax2.plot(x,np.sin(x+1),'r')
ax2.set_ylabel('Static axis',weight='bold')
A slightly more complex example with two plot areas and three vertical axes. Only the common horizontal axis and the left vertical axis of the lower subplot are interactive.
fig, ax_left = plt.subplots()
ax_right = ax_left.twinx()
ax_status = make_axes_locatable(ax_left).append_axes('top', size=1.2, pad=0., sharex=ax_left)
ax_status.xaxis.set_tick_params(labelbottom=False)
ax_right.set_navigate(False)
ax_status.set_navigate(False)
Before I added set_navigate(False) according to ali_m's answer, the two vertical axes of the lower plot were both affected by dragging the mouse vertically in the lower plot, while the status axis was unaffected as it should but only after the first mouse gesture. Dragging the mouse for the first time, all axes are affected. This seems to be a bug in matplotlib, just reported as #12613.

Categories

Resources