Position font relative to axis using ax.text, matplotlib - python

I'm not sure how to properly position font relative to an axis object using matplotlib.
Example:
fig, axes = plt.subplots(nrows=1, ncols=3, figsize=(10, 4), dpi=100)
x = [1, 2]
y = [3, 4]
y_loc = 4.1
x_loc = 0.95
fs = 12
ax = axes[0]
ax.plot(x, y)
_ = ax.text(x=x_loc, y=y_loc, s="Plot 1", fontsize=fs)
ax = axes[1]
ax.plot(x, y)
_ = ax.text(x=x_loc, y=y_loc, s="Plot 2", fontsize=fs)
ax = axes[2]
_ = ax.plot(x, y)
_ = ax.text(x=x_loc, y=y_loc, s="Plot 3", fontsize=fs)
Which gives:
The use of values:
y_loc = 4.1
x_loc = 0.95
makes me think that there should be a better approach to this.
Note - I would like to use ax.text here, not title, and the question is mainly about how best to position text relative to a particular axis within a subplot. Ideally it would extend to a grid plot as well if it was just relative to a particular axis.

Default, ax.text uses "data coordinates", i.e. with x and y as shown on the ticks of the axes. To plot relative to the rectangle defined by the axes, use transform=ax.transAxes. Here 0,0 will be the point at the bottom left and 1,1 the point at the top right. (This kind of coordinates is also very useful when positioning a legend.)
from matplotlib import pyplot as plt
import numpy as np
fig, axes = plt.subplots(nrows=1, ncols=3, figsize=(10, 4), dpi=100)
for ind, ax in enumerate(axes):
ax.plot(np.random.randint(0, 10, 2), np.random.randint(0, 10, 2))
ax.text(x=0, y=1.05, s=f"Plot {ind+1}", fontsize=12, transform=ax.transAxes)
plt.show()

Related

End ticks missing on colorbars in matplotlib

I want to make contour plots with colorbars that are symmetric about zero and have ticks at the maximum and minimum values. I am having a problem where the end ticks on my colorbars are not showing.
Here is an example:
fig, ax = plt.subplots()
A = np.random.random((10,10))*10-5
x = np.arange(0, A.shape[1])
y = np.arange(0, A.shape[0])
minval=-5
maxval=5
im1 = ax.contourf(x,y,A,150, vmin=minval, vmax=maxval,cmap="BrBG",extend='both')
cbar = ax.figure.colorbar(
im1,
ax=ax,
ticks=[minval, minval/2, 0, maxval/2, maxval],
orientation="vertical",
)
Which results in this figure (it will not let me embed the image, see link), that has tick marks at 0 and +/-2.5 but not +/-5:
contour plot with a colorbar that has ticks at -2.5, 0 and 2.5 but not at -5 or 5
I tried these following add-ons with no avail:
im1.set_clim(minval, maxval)
cbar.ax.set_xticklabels([minval, minval/2, '0', maxval/2, maxval])
plt.show()
I'm nearly positive that this used to work fine but has recently been skipping out on the end tick marks. I'm running it in a jupyter notebook. Ideas?
If you call contourf(..., levels=150, ...), matplotlib will create 151 equally spaced boundaries between the minimum and maximum of the data. The way np.random.random works, the minimum of A is slightly larger than -5 and the maximum slightly smaller than 5. So, these extreme values don't belong to the level boundaries. And won't be visible in the colorbar. To have them visible, the level boundaries could be set explicitly to include these values:
from matplotlib import pyplot as plt
import numpy as np
fig, ax = plt.subplots()
A = np.random.random((10, 10)) * 10 - 5
x = np.arange(0, A.shape[1])
y = np.arange(0, A.shape[0])
minval = -5
maxval = 5
im1 = ax.contourf(x, y, A, levels=np.linspace(minval, maxval, 151), cmap="BrBG", extend='both')
cbar = fig.colorbar(
im1,
ax=ax,
ticks=[minval, minval/2, 0, maxval/2, maxval],
orientation="vertical"
)
cbar.ax.set_yticklabels([minval, minval/2, "0", maxval/2, maxval])
plt.show()
Also note that for a vertical colorbar, the y-direction is used (cbar.ax.set_yticklabels).
Do you specifically need to be using contourf? If you use pcolormesh instead, almost your exact code will give you the ticks where you want (you just have to move "extend" to the cbar call vs the pcolormesh call). That doesn't answer your question, but dodges the issue...
fig, ax = plt.subplots()
A = np.random.random((10,10))*10-5
x = np.arange(0, A.shape[1])
y = np.arange(0, A.shape[0])
minval=-5
maxval=5
# im1 = ax.contourf(x,y,A,150, vmin=minval-10, vmax=maxval+10,cmap=plt.cm.get_cmap("BrBG",32),extend='both')
im1 = ax.pcolormesh(x,y,A,vmin=minval,vmax=maxval,cmap=plt.cm.get_cmap('BrBG'))
cbar = fig.colorbar(
im1,
ax=ax,
ticks=[minval, minval/2, 0, maxval/2, maxval],
orientation="vertical", extend='both'
)

How to add a subplot to each rectangle in a Tree Map?

I created this tree map using Matplotlib and Squarify:
Now I would like to add a line plot on each rectangle in the tree map. Is that possible?
Squarify's plot is a convenience function to directly plot a treemap given values and labels. But, this process can also be executed step-by-step. One of the steps is to calculate the positions of the rectangles, for which we suppose a figure which has coordinates from 0,0 to 1,1 from lower left to upper right.
With these rectangles we can manually position axes to draw on. It is unclear whether ticks are needed. If needed, they can be placed inside. Or the axes could be moved completely to the center of each subplot. Or only have ticks without labels.
Here is some demonstrating code:
import numpy as np
import matplotlib.pyplot as plt
import squarify
values = [500, 433, 331, 254, 119]
values.sort(reverse=True) # values must be sorted descending (and positive)
# the sum of the values must equal the total area to be laid out; i.e., sum(values) == width * height
values = squarify.normalize_sizes(values, 1, 1)
rects = squarify.squarify(values, 0, 0, 1, 1)
fig = plt.figure(figsize=(7, 5))
axes = [fig.add_axes([rect['x'], rect['y'], rect['dx'], rect['dy'], ]) for rect in rects]
for ax, color in zip(axes, plt.cm.Pastel1.colors):
x = np.linspace(0, 10, 100)
y = np.random.normal(0.01, 0.1, 100).cumsum()
ax.plot(x, y)
ax.tick_params(axis="y", direction="in", pad=-15)
ax.tick_params(axis="x", direction="in", pad=-15)
plt.setp(ax.get_yticklabels(), ha="left")
ax.set_facecolor(color)
plt.show()
Here is another example resembling the image in the question, with a main plot and a colorbar. The default mplcursors gets confused with all these axes, but annotations while hovering can also be added manually.
import numpy as np
import matplotlib.pyplot as plt
import squarify
values = [4000, 1500, 1500, 1200, 1000, 500]
fig, mainax = plt.subplots(figsize=(6, 4))
mainax.set_xlim(0, 1000)
mainax.set_ylim(0, 1000)
mainax.grid(False)
cmap = plt.cm.get_cmap('Greens')
norm = plt.Normalize(vmin=0, vmax=max(values))
plt.colorbar(plt.cm.ScalarMappable(cmap=cmap, norm=norm))
pos = mainax.get_position()
values.sort(reverse=True)
normalized_values = squarify.normalize_sizes(values, pos.width, pos.height)
rects = squarify.squarify(normalized_values, pos.x0, pos.y0, pos.width, pos.height)
axes = [fig.add_axes([rect['x'], rect['y'], rect['dx'], rect['dy'], ]) for rect in rects]
for ax, val in zip(axes, values):
x = np.linspace(0, 10, 100)
y = np.random.normal(0.01, 0.1, 100).cumsum()
ax.plot(x, y)
ax.set_xticks([])
ax.set_yticks([])
ax.set_facecolor(cmap(norm(val)))
mainax.set_facecolor('none') # prevent that the mainax blocks the other axes
mainax.set_zorder(20) # high z-order because the annotations are drawn using this ax
labels = ['a', 'b', 'c', 'd', 'e', 'f']
sum_val = sum(values)
annotations = [mainax.annotate(f"'{lbl}': {val}\n{val * 100.0 / sum_val:.1f} %",
xy=(0, 0), xycoords='figure pixels',
xytext=(0, 0), textcoords='offset points',
bbox=dict(boxstyle='round', fc='lemonchiffon'),
ha='center', va='bottom')
for ax, val, lbl in zip(axes, values, labels)]
for annot in annotations:
annot.set_visible(False)
def hover(event):
for ax, annot in zip(axes, annotations):
if ax.bbox.contains(event.x, event.y):
annot.xy = (event.x, event.y)
annot.set_visible(True)
else:
annot.set_visible(False)
fig.canvas.draw_idle()
fig.canvas.mpl_connect("motion_notify_event", hover)
plt.show()
Yes it is possible. You will have to write the code to extract the exact positions where you want to place the new plot in.
You need to set the position of the new figure using f.canvas.manager.window.SetPosition
This answer will greatly help https://stackoverflow.com/a/37999370/4551984

Python - Stacking two histograms with a scatter plot

Having an example code for a scatter plot along with their histograms
x = np.random.rand(5000,1)
y = np.random.rand(5000,1)
fig = plt.figure(figsize=(7,7))
ax = fig.add_subplot(111)
ax.scatter(x, y, facecolors='none')
ax.set_xlim(0,1)
ax.set_ylim(0,1)
fig1 = plt.figure(figsize=(7,7))
ax1 = fig1.add_subplot(111)
ax1.hist(x, bins=25, fill = None, facecolor='none',
edgecolor='black', linewidth = 1)
fig2 = plt.figure(figsize=(7,7))
ax2 = fig2.add_subplot(111)
ax2.hist(y, bins=25 , fill = None, facecolor='none',
edgecolor='black', linewidth = 1)
What I'm wanting to do is to create this graph with the histograms attached to their respected axis almost like this example
I'm familiar with stacking and merging the x-axis
f, (ax1, ax2, ax3) = plt.subplots(3)
ax1.scatter(x, y)
ax2.hist(x, bins=25, fill = None, facecolor='none',
edgecolor='black', linewidth = 1)
ax3.hist(y, bins=25 , fill = None, facecolor='none',
edgecolor='black', linewidth = 1)
f.subplots_adjust(hspace=0)
plt.setp([a.get_xticklabels() for a in f.axes[:-1]], visible=False)
But I have no idea how to attach the histograms to the y axis and x axis like in the picture I posted above, and on top of that, how to vary the size of the graphs (ie make the scatter plot larger and the histograms smaller in comparison)
Seaborn is the way to go for quick statistical plots. But if you want to avoid another dependency you can use subplot2grid to place the subplots and the keywords sharex and sharey to make sure the axes are synchronized.
import numpy as np
import matplotlib.pyplot as plt
x = np.random.randn(100)
y = np.random.randn(100)
scatter_axes = plt.subplot2grid((3, 3), (1, 0), rowspan=2, colspan=2)
x_hist_axes = plt.subplot2grid((3, 3), (0, 0), colspan=2,
sharex=scatter_axes)
y_hist_axes = plt.subplot2grid((3, 3), (1, 2), rowspan=2,
sharey=scatter_axes)
scatter_axes.plot(x, y, '.')
x_hist_axes.hist(x)
y_hist_axes.hist(y, orientation='horizontal')
You should always look at the matplotlib gallery before asking how to plot something, chances are that it will save you a few keystrokes -- I mean you won't have to ask. There are actually two plots like this in the gallery. Unfortunately the code is old and does not take advantage of subplot2grid, the first one uses rectangles and the second one uses axes_grid, which is a somewhat weird beast. That's why I posted this answer.
I think it's hard to do this solely with matplotlib but you can use seaborn which has jointplot function.
import numpy as np
import pandas as pd
import seaborn as sns
sns.set(color_codes=True)
x = np.random.rand(1000,1)
y = np.random.rand(1000,1)
data = np.column_stack((x,y))
df = pd.DataFrame(data, columns=["x", "y"])
sns.jointplot(x="x", y="y", data=df);

How to add one custom tickline in matplotlib axes

Is there a programmatic way to force the appearance of a single tickline at the additional tick location shown below?
Requirements:
Tickline should be pointing down from x-axis
Tickline should extend to label 103 regardless of padding
Tickline should be the same color and thickness of axes
No changes to other ticks (ticklines or tick labels)
Code and sample image:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
fig, ax = plt.subplots(figsize=(6, 8 / 3))
fig.tight_layout()
x_limit = 103
x = [0, 10, 50, 100, x_limit]
y = [6, 2, 5, 2, 20]
ax.plot(x, y)
# Add a tick which represents the maximum x-value
xticks = ax.xaxis.get_majorticklocs()
ax.xaxis.set_ticks(np.append(xticks, x_limit))
# Change padding of tick in the event other ticks get too close
tick = ax.get_xaxis().get_major_ticks()[-1]
tick.set_pad(14)
tick.label1 = tick._get_text1()
# Set tight axes bounds around data
ax.set_ylim(0, max(y) + 1)
ax.set_xlim(0, x_limit)
EDIT: Tried tcaswell's solution and ended up with an annotation in the right place. I do notice some aliasing as if it doesn't look like an extension of the y-axis. Any ideas on how to clean this up?
You can do this with annotate and a bit of transform magic.
import matplotlib.transforms as mtransforms
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(9, 3), tight_layout=True)
ax.set_xlim(90, 105)
trans = mtransforms.blended_transform_factory(ax.transData, ax.transAxes)
tt = ax.annotate('103', (103, 0), xytext=(0, -12), transform=trans,
arrowprops={'arrowstyle': '-'}, ha='center', va='top',
textcoords='offset points')

Matplotlib different size subplots

I need to add two subplots to a figure. One subplot needs to be about three times as wide as the second (same height). I accomplished this using GridSpec and the colspan argument but I would like to do this using figure so I can save to PDF. I can adjust the first figure using the figsize argument in the constructor, but how do I change the size of the second plot?
As of matplotlib 3.6.0, width_ratios and height_ratios can now be passed directly as keyword arguments to plt.subplots and subplot_mosaic, as per What's new in Matplotlib 3.6.0 (Sep 15, 2022).
f, (a0, a1) = plt.subplots(1, 2, width_ratios=[3, 1])
f, (a0, a1, a2) = plt.subplots(3, 1, height_ratios=[1, 1, 3])
Another way is to use the subplots function and pass the width ratio with gridspec_kw
matplotlib Tutorial: Customizing Figure Layouts Using GridSpec and Other Functions
matplotlib.gridspec.GridSpec has available gridspect_kw options
import numpy as np
import matplotlib.pyplot as plt
# generate some data
x = np.arange(0, 10, 0.2)
y = np.sin(x)
# plot it
f, (a0, a1) = plt.subplots(1, 2, gridspec_kw={'width_ratios': [3, 1]})
a0.plot(x, y)
a1.plot(y, x)
f.tight_layout()
f.savefig('grid_figure.pdf')
Because the question is canonical, here is an example with vertical subplots.
# plot it
f, (a0, a1, a2) = plt.subplots(3, 1, gridspec_kw={'height_ratios': [1, 1, 3]})
a0.plot(x, y)
a1.plot(x, y)
a2.plot(x, y)
f.tight_layout()
You can use gridspec and figure:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import gridspec
# generate some data
x = np.arange(0, 10, 0.2)
y = np.sin(x)
# plot it
fig = plt.figure(figsize=(8, 6))
gs = gridspec.GridSpec(1, 2, width_ratios=[3, 1])
ax0 = plt.subplot(gs[0])
ax0.plot(x, y)
ax1 = plt.subplot(gs[1])
ax1.plot(y, x)
plt.tight_layout()
plt.savefig('grid_figure.pdf')
I used pyplot's axes object to manually adjust the sizes without using GridSpec:
import matplotlib.pyplot as plt
import numpy as np
x = np.arange(0, 10, 0.2)
y = np.sin(x)
# definitions for the axes
left, width = 0.07, 0.65
bottom, height = 0.1, .8
bottom_h = left_h = left+width+0.02
rect_cones = [left, bottom, width, height]
rect_box = [left_h, bottom, 0.17, height]
fig = plt.figure()
cones = plt.axes(rect_cones)
box = plt.axes(rect_box)
cones.plot(x, y)
box.plot(y, x)
plt.show()
Probably the simplest way is using subplot2grid, described in Customizing Location of Subplot Using GridSpec.
ax = plt.subplot2grid((2, 2), (0, 0))
is equal to
import matplotlib.gridspec as gridspec
gs = gridspec.GridSpec(2, 2)
ax = plt.subplot(gs[0, 0])
so bmu's example becomes:
import numpy as np
import matplotlib.pyplot as plt
# generate some data
x = np.arange(0, 10, 0.2)
y = np.sin(x)
# plot it
fig = plt.figure(figsize=(8, 6))
ax0 = plt.subplot2grid((1, 3), (0, 0), colspan=2)
ax0.plot(x, y)
ax1 = plt.subplot2grid((1, 3), (0, 2))
ax1.plot(y, x)
plt.tight_layout()
plt.savefig('grid_figure.pdf')
In a simple way, different size sub plotting can also be done without gridspec:
plt.figure(figsize=(12, 6))
ax1 = plt.subplot(2,3,1)
ax2 = plt.subplot(2,3,2)
ax3 = plt.subplot(2,3,3)
ax4 = plt.subplot(2,1,2)
axes = [ax1, ax2, ax3, ax4]
A nice way of doing this was added in matplotlib 3.3.0, subplot_mosaic.
You can make a nice layout using an "ASCII art" style.
For example
fig, axes = plt.subplot_mosaic("ABC;DDD")
will give you three axes on the top row and one spanning the full width on the bottom row like below
A nice thing about this method is that the axes returned from the function is a dictionary with the names you define, making it easier to keep track of what is what e.g.
axes["A"].plot([1, 2, 3], [1, 2, 3])
You can also pass a list of lists to subplot_mosaic if you want to use longer names
fig, axes = plt.subplot_mosaic(
[["top left", "top centre", "top right"],
["bottom row", "bottom row", "bottom row"]]
)
axes["top left"].plot([1, 2, 3], [1, 2, 3])
will produce the same figure

Categories

Resources