Python Matplotlib: Align text on far left of graph - python

I'm trying to figure out how to align text with the far left of a graph in matplotlib. I can accomplish this by hard coding the value (in the example below x=-.98), however this takes a lot of trial and error.
Is there away to return the coordinates of the figure border (not the graph border) or to set the text to start at the far left of the figure?
# matplotlib example of text align
import matplotlib.pyplot as plt
# x and y axis data
y_axis_labels = ['y-label-1','y-label-2','y-label-3','y-label-4']
x_axis_labels = [1,2,3,4]
# create horizontal bar plot
fig, ax = plt.subplots()
ax.barh(y_axis_labels, x_axis_labels)
# Align the text with the far left of the y_axis labels
plt.text(x=-.98,y=4, s='start at far left', color='red')
plt.show()
Link to example image (I want the red text to align with blue line)
Thanks!

Replace
plt.text(x=-.98,y=4, s='start at far left', color='red')
with
ax.set_title('start at far left',loc='right',color='red' )

Related

Right and left justified text in a saved `matplotlib`/`seaborn` figure

I have made a heatmap in seaborn, and I need to have text in the corners.
Have:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
np.random.seed(2021)
data = pd.DataFrame(np.random.randint(0, 5, (3, 5)))\
.rename({0: "Supercalifragilisticexpialidocious",
1: "Humuhumunukunukuapua'a",
2: "Hippopotomonstrosesquipedaliophobia"
})
sns.heatmap(data)
plt.savefig("dave.png")
Want:
The plt.figtext command has worked for this in the past, but I am frustrated in formatting the right and left justification (and distance from the top and bottom), so I just want to have a standard distance from the edges, which sounds like justification. It sounds like that is a matter of changing the coordinates in figtext, which I have not figured out how to do, but I think that is not quite enough. Since the plot can extend very far to the left, I need the GHI and JKL to be to the left of the saved image, not just of the plotting area. The lengths of those words on the left can vary from plot to plot, and I want GHI and JKL left-justified no matter what, whether the long word is "Hippopotomonstrosesquipedaliophobia" or "Dave" (but it shouldn't be way to the left, beyond the left edge of the words, when those words are short).
What would be the way to execute this?
I suppose it would be nice to know how to have such an image appear in a Jupyter Notebook or pop up when I run my script from the command line, but I mostly care about saving the images with those ABC, DEF, GHI, and JKL comments.
One approach is to use a Gridspec. Define a grid with 4 rows and columns and create axes for each plot position in the grid.
In the center of the grid you plot the heatmap and in the borders of the grid you plot the labels.
The borders will define a 1x1 plot, you can adjust the labels position inside each individual plot with axis.text() or give more space to the labels by adding more rows and columns to the grid.
At the end hide the axis of each plot label with ax.axis('off') and you get the desire output
Full example
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
np.random.seed(2021)
data = pd.DataFrame(np.random.randint(0, 5, (3, 5)))\
.rename(
{
0: "Supercalifragilisticexpialidocious",
1: "Humuhumunukunukuapua'a",
2: "Hippopotomonstrosesquipedaliophobia"
}
)
gs = gridspec.GridSpec(4, 4)
# Upper left label
ax1 = plt.subplot(gs[:1, :1])
ax1.text(0.5, 0.5, 'GHI', fontsize='xx-large')
ax1.axis('off')
# Lower left label
ax2 = plt.subplot(gs[3:4, :1])
ax2.text(0.5, 0.5, 'JKL', fontsize='xx-large')
ax2.axis('off')
# Upper right label
ax3 = plt.subplot(gs[:1, 3:4])
ax3.text(0, 0.5, 'ABC', fontsize='xx-large')
ax3.axis('off')
# Lower Right label
ax4 = plt.subplot(gs[3:4, 3:4])
ax4.text(0, 0.5, 'DEF', fontsize='xx-large')
ax4.axis('off')
ax5 = plt.subplot(gs[1:3, 1:3])
sns.heatmap(data, ax=ax5)
References
Center the third subplot in the middle of second row python
Hiding axis text in matplotlib plots
Putting text in top left corner of matplotlib plot
To put the text at the corners of your plot you need to
Make some room around your plot for the text
Get the extent (bounding box) of your total plot for the text positions.
For 1) you can specify a constrained layout with some pads large enough to accomodate the text. For 2) you need to get the bounding boxes of the heatmap itself and its colorbar in figure coordinates. Then you take the union of these two boxes and place your texts with the corresponding alignments at the four corners of this unified bounding box.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
np.random.seed(2021)
data = pd.DataFrame(np.random.randint(0, 5, (3, 5)))\
.rename({0: "Supercalifragilisticexpialidocious",
1: "Humuhumunukunukuapua'a",
2: "Hippopotomonstrosesquipedaliophobia"
})
fig, ax = plt.subplots(constrained_layout={'w_pad': .5, 'h_pad': .5})
sns.heatmap(data, ax=ax)
fig.draw_without_rendering()
cb = fig.axes[1]
ax_extent = fig.transFigure.inverted().transform_bbox(ax.get_tightbbox(fig.canvas.get_renderer()))
cb_extent = fig.transFigure.inverted().transform_bbox(cb.get_tightbbox(fig.canvas.get_renderer()))
total_extent = ax_extent.union([ax_extent, cb_extent])
# uncomment the following to show axes and colorbar extents
#from matplotlib.patches import Rectangle
#for extent in (ax_extent, cb_extent):
# fig.add_artist(Rectangle(extent.p0, extent.width, extent.height, ec='.6', ls='dotted', fill=False))
fig.text(total_extent.x1, total_extent.y1, 'ABC', ha='center', va='bottom', fontsize=20)
fig.text(total_extent.x1, total_extent.y0, 'DEF', ha='center', va='top', fontsize=20)
fig.text(total_extent.x0, total_extent.y1, 'GHI', ha='center', va='bottom', fontsize=20)
fig.text(total_extent.x0, total_extent.y0, 'KLM', ha='center', va='top', fontsize=20)
This also works without any change for short y labels, there's no need to fiddle with text positions:
Depending on your likings you can change the horizontal alignment of the texts from 'center' to 'right' and 'left' respectively.
To better understand how it works, you can uncomment the three lines for visualizing the bounding boxes. From here you'll see that we need the union of the two boxes to neatly put the texts at the same y position as the colorbar extends a bit beyond the heatmap:

Limiting the size of legend in MatPlotLib in python, then allowing scrolling within the legend

I would like to limit the size of the legend in MatPlotLib to scale with a figure. After this, I would like to enable scrolling within the legend to see any cut off data. This is regarding two legends, each corresponding to a subplot. A picture is attached to show the exact setup:
In this image, you can see that the legends are overlapping each other, as well as being cut off by the bottom of the frame.
Here is the python code used to obtain the figure:
import matplotlib.pyplot as plt
from matplotlib.transforms import Bbox
fig, axes = plt.subplots(2, figsize=(9,6))
x = [i for i in range(100)]
y_data = []
for i in range(1,15):
temp = []
for j in x:
temp.append(i * j)
y_data.append(temp)
for line in y_data:
axes[0].plot(x, line, '.')
axes[1].plot(x, line, '.')
axes[0].legend(x, bbox_to_anchor=(1.02, 0, 0.07, 1))
axes[1].legend(x, bbox_to_anchor=(1.02, 0, 0.07, 1))
plt.show()
I would like to modify this code so that the legend is smaller, and so that if there are a great amount of lines in the legend, the data that is not within the confines of the legend can be scrolled to.
I attempted to use the ideas here: Fix size of legend in matplotlib, but it did not seem to scale with two subplots. Setting the height and width in bbox_to_anchor also did not seem to constrain the legend - only move it.
How can I accomplish this?

How to adjust heatmap cell height/width?

I have plotted a simple heatmap with the below code in python. How would I go about adjusting the height/width of the individual heatmap cells with matplotlib?
def HeatMap(data):
# plot the figure
figure = plt.figure()
sub_figure = figure.add_subplot(111)
heatmap = sub_figure.imshow(data, interpolation='nearest',cmap='jet')
# add a color bar
cbar = figure.colorbar(ax=sub_figure, mappable=one_box, orientation='horizontal')
cbar.set_label('Scores')
plt.show()
I have tried figure = plt.figure(figsize=(10,10)) for example but it increases/decreases the whole figure rather than the individual cells. So far I have this:
I want the cells to be smaller in height so a bit squished.

adjust matplotlib subplot spacing after tight_layout

I would like to minimize white space in my figure. I have a row of sub plots where four plots share their y-axis and the last plot has a separate axis.
There are no ylabels or ticklabels for the shared axis middle panels.
tight_layout creates a lot of white space between the the middle plots as if leaving space for tick labels and ylabels but I would rather stretch the sub plots. Is this possible?
import matplotlib.gridspec as gridspec
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
fig = plt.figure()
gs = gridspec.GridSpec(1, 5, width_ratios=[4,1,4,1,2])
ax = fig.add_subplot(gs[0])
axes = [ax] + [fig.add_subplot(gs[i], sharey=ax) for i in range(1, 4)]
axes[0].plot(np.random.randint(0,100,100))
barlist=axes[1].bar([1,2],[1,20])
axes[2].plot(np.random.randint(0,100,100))
barlist=axes[3].bar([1,2],[1,20])
axes[0].set_ylabel('data')
axes.append(fig.add_subplot(gs[4]))
axes[4].plot(np.random.randint(0,5,100))
axes[4].set_ylabel('other data')
for ax in axes[1:4]:
plt.setp(ax.get_yticklabels(), visible=False)
sns.despine();
plt.tight_layout(pad=0, w_pad=0, h_pad=0);
Setting w_pad = 0 is not changing the default settings of tight_layout. You need to set something like w_pad = -2. Which produces the following figure:
You could go further, to say -3 but then you would start to get some overlap with your last plot.
Another way could be to remove plt.tight_layout() and set the boundaries yourself using
plt.subplots_adjust(left=0.065, right=0.97, top=0.96, bottom=0.065, wspace=0.14)
Though this can be a bit of a trial and error process.
Edit
A nice looking graph can be achieved by moving the ticks and the labels of the last plot to the right hand side. This answer shows you can do this by using:
ax.yaxis.tick_right()
ax.yaxis.set_label_position("right")
So for your example:
axes[4].yaxis.tick_right()
axes[4].yaxis.set_label_position("right")
In addition, you need to remove sns.despine(). Finally, there is now no need to set w_pad = -2, just use plt.tight_layout(pad=0, w_pad=0, h_pad=0)
Using this creates the following figure:

Matplotlib - axvspan vs subplots

I'm writing a pythonic script for a coastal engineering application which should output, amongst other things, a figure with two subplots.
The problem is that I would like to shade a section of both subplots using plt.axvspan() but for some reason it only shades one of them.
Please find below an excerpt of the section of the code where I set up the plots as well as the figure that it's currently outputting (link after code).
Thanks for your help, and sorry if this is a rookie question (but it just happens that I am indeed a rookie in Python... and programming in general) but I couldn't find an answer for this anywhere else.
Feel free to add any comments to the code.
# PLOTTING
# now we generate a figure with the bathymetry vs required m50 and another figure with bathy vs Hs
#1. Generate plots
fig = plt.figure() # Generate Figure
ax = fig.add_subplot(211) # add the first plot to the figure.
depth = ax.plot(results[:,0],results[:,1]*-1,label="Depth [mDMD]") #plot the first set of data onto the first set of axis.
ax2 = ax.twinx() # generate a secondary vertical axis with the same horizontal axis as the first
m50 = ax2.plot(results[:,0],results[:,6],"r",label="M50 [kg]") # plot the second set of data onto the second vertical axis
ax3 = fig.add_subplot(212) # generate the second subplot
hs = ax3.plot(results[:,0],results[:,2],"g",label="Hs(m)")
#Now we want to find where breaking starts to occur so we shade it on the plot.
xBreakingDistance = results[numpy.argmax(breakingIndex),0]
# and now we plot a box from the origin to the depth of breaking.
plt.axvspan(0,xBreakingDistance,facecolor="b",alpha=0.1) # this box is called a span in matplotlib (also works for axhspan)
# and then we write BREAKING ZONE in the box we just created
yLimits = ax.get_ylim() # first we get the range of y being plotted
yMiddle = (float(yLimits[1])-float(yLimits[0])) / 2 + yLimits[0] # then we calculate the middle value in y (to center the text)
xMiddle = xBreakingDistance / 2 # and then the middle value in x (to center the text)
#now we write BREAKING ZONE in the center of the box.
ax.text(xMiddle,yMiddle,"BREAKING ZONE",fontweight="bold",rotation=90,verticalalignment="center",horizontalalignment="center")
#FIGURE FORMATTING
ax.set_xlabel("Distance [m]") # define x label
ax.set_ylabel("Depth [mDMD]") # define y label on the first vertical axis (ax)
ax2.set_ylabel("M50 [kg]") # define y label on the second vertical axis (ax2)
ax.grid() # show grid
ax3.set_xlabel("Distance[m]") #define x label
ax3.set_ylabel("Hs[m]") # define y label
ax3.grid()
plt.tight_layout() # minimize subplot labels overlapping
# generating a label on a plot with 2 vertical axis is not very intuitive. Normally we would just write ax.label(loc=0)
combined_plots = depth+m50 #first we need to combine the plots in a vector
combined_labels = [i.get_label() for i in combined_plots] # and then we combine the labels
ax.legend(combined_plots,combined_labels,loc=0) # and finally we plot the combined_labels of the combined_plots
plt.savefig("Required M50(kg) along the trench.png",dpi=1000)
plt.close(fig)
Output Figure:
By just calling plt.axvspan, you are telling matplotlib to create the axvspan on the currently active axes (i.e. in this case, the last one you created, ax3)
You need to plot the axvspan on both of the axes you would like for it to appear on. In this case, ax and ax3.
So, you could do:
ax.axvspan(0,xBreakingDistance,facecolor="b",alpha=0.1)
ax3.axvspan(0,xBreakingDistance,facecolor="b",alpha=0.1)
or in one line:
[this_ax.axvspan(0,xBreakingDistance,facecolor="b",alpha=0.1) for this_ax in [ax,ax3]]
It's difficult to analyze your code and not being able to reproduce it. I advise you to build a minimal example. In any case notice that you are calling "plt.axvspan(" which is general call to the library.
You need to specifically state that you want this in both "ax" and "ax2" (i think).
Also if you need more control consider using Patches (I don't know axvspan):
import matplotlib.pyplot as plt
import matplotlib.patches as patches
fig1 = plt.figure()
ax1 = fig1.add_subplot(111, aspect='equal')
ax1.add_patch(
patches.Rectangle(
(0.1, 0.1), # (x,y)
0.5, # width
0.5, # height
)
)
fig1.savefig('rect1.png', dpi=90, bbox_inches='tight')
See that call to "ax1" in the example? Just make something similar to yours. Or just add axvspan to each of your plots.

Categories

Resources