Matplotlib Calculate Axis Coordinate Extents Given String - python

I am trying to center an axis text object by:
Getting the width in coordinates of the text divided by 2.
Subtracting that value from the center (provided) location on the x-axis.
Using the resulting value as the x starting position (with ha='left').
I have seen examples of how to get x-coordinates (bounds) after plotting a string like this:
import matplotlib as plt
f = plt.figure()
r = f.canvas.get_renderer()
t = plt.text(0, 0, 'test')
bb = t.get_window_extent(renderer=r)
width = bb.width
However, I would like to know the width (in axis coordinates) of a string before plotting so that I can anticipate an adjustment to make.
I've tried the following, but it did not return the correct axis coordinates and I think a transformation may need to occur:
t = matplotlib.textpath.TextPath((0,0), 'test', size=9)
bb = t.get_extents()
w = bb.width #16.826132812499999
Here's a sample to work with (last 3 lines show what I want to do):
import matplotlib.pyplot as plt
import matplotlib
import numpy as np
%matplotlib inline
prf=[60,70,65,83,77,70,71]
figsize=3.5,4
fig, ax = plt.subplots(1, 1, figsize = figsize, dpi=300)
ind=np.arange(len(prf))
p=ax.bar(ind,prf,color=colrs,edgecolor='none')
t = matplotlib.textpath.TextPath((0,0), 'test', size=9)
bb = t.get_extents()
w = bb.width
center=len(ind)/2
xposition=center-w/2
ax.text(xposition,110,'test',size=9)
This question is a follow-up from this post.
I know I can use ha='center', but this is actually for a more complex text (multi-colored), which does not provide that option.
Thanks in advance!

You can create a text object, obtain its bounding box and then remove the text again. You may transform the bounding box into data coordinates (I assume that you mean data coordinates, not axes coordinates in the question) and use those to create a left aligned text.
import matplotlib.pyplot as plt
ax = plt.gca()
ax.set_xlim(0,900)
#create centered text
text = ax.text(400,0.5, "string", ha="center", color="blue")
plt.gcf().canvas.draw()
bb = text.get_window_extent()
# remove centered text
text.remove()
del text
# create left aligned text from position of centered text
bb2 = bb.transformed(ax.transData.inverted())
text = ax.text(bb2.x0,0.5, "string", ha="left", color="red")
plt.show()

Related

How to fix overlapping matplotlib y-axis tick labels or autoscale the plot? [duplicate]

I am trying to make a series of matplotlib plots that plot timespans for different classes of objects. Each plot has an identical x-axis and plot elements like a title and a legend. However, which classes appear in each plot differs; each plot represents a different sampling unit, each of which only contains only a subset of all the possible classes.
I am having a lot of trouble determining how to set the figure and axis dimensions. The horizontal size should always remain the same, but the vertical dimensions need to be scaled to the number of classes represented in that sampling unit. The distance between each entry on the y-axis should be equal for every plot.
It seems that my difficulties lie in the fact that I can set the absolute size (in inches) of the figure with plt.figure(figsize=(w,h)), but I can only set the size of the axis with relative dimensions (e.g., fig.add_axes([0.3,0.05,0.6,0.85]) which leads to my x-axis labels getting cut off when the number of classes is small.
Here is an MSPaint version of what I'd like to get vs. what I'm getting.
Here is a simplified version of the code I have used. Hopefully it is enough to identify the problem/solution.
import pandas as pd
import matplotlib.pyplot as plt
import pylab as pl
from matplotlib import collections as mc
from matplotlib.lines import Line2D
import seaborn as sns
# elements for x-axis
start = 1
end = 6
interval = 1 # x-axis tick interval
xticks = [x for x in range(start, end, interval)] # create x ticks
# items needed for legend construction
lw_bins = [0,10,25,50,75,90,100] # bins for line width
lw_labels = [3,6,9,12,15,18] # line widths
def make_proxy(zvalue, scalar_mappable, **kwargs):
color = 'black'
return Line2D([0, 1], [0, 1], color=color, solid_capstyle='butt', **kwargs)
for line_subset in data:
# create line collection for this run through loop
lc = mc.LineCollection(line_subset)
# create plot and set properties
sns.set(style="ticks")
sns.set_context("notebook")
############################################################
# I think the problem lies here
fig = plt.figure(figsize=(11, len(line_subset.index)*0.25))
ax = fig.add_axes([0.3,0.05,0.6,0.85])
############################################################
ax.add_collection(lc)
ax.set_xlim(left=start, right=end)
ax.set_xticks(xticks)
ax.xaxis.set_ticks_position('bottom')
ax.margins(0.05)
sns.despine(left=True)
ax.set_yticks(line_subset['order_y'])
ax.set(yticklabels=line_subset['ylabel'])
ax.tick_params(axis='y', length=0)
# legend
proxies = [make_proxy(item, lc, linewidth=item) for item in lw_labels]
leg = ax.legend(proxies, ['0-10%', '10-25%', '25-50%', '50-75%', '75-90%', '90-100%'], bbox_to_anchor=(1.0, 0.9),
loc='best', ncol=1, labelspacing=3.0, handlelength=4.0, handletextpad=0.5, markerfirst=True,
columnspacing=1.0)
for txt in leg.get_texts():
txt.set_ha("center") # horizontal alignment of text item
txt.set_x(-23) # x-position
txt.set_y(15) # y-position
You can start by defining the margins on top and bottom in units of inches. Having a fixed unit of one data unit in inches allows to calculate how large the final figure should be.
Then dividing the margin in inches by the figure height gives the relative margin in units of figure size, this can be supplied to the figure using subplots_adjust, given the subplots has been added with add_subplot.
A minimal example:
import numpy as np
import matplotlib.pyplot as plt
data = [np.random.rand(i,2) for i in [2,5,8,4,3]]
height_unit = 0.25 #inch
t = 0.15; b = 0.4 #inch
for d in data:
height = height_unit*(len(d)+1)+t+b
fig = plt.figure(figsize=(5, height))
ax = fig.add_subplot(111)
ax.set_ylim(-1, len(d))
fig.subplots_adjust(bottom=b/height, top=1-t/height, left=0.2, right=0.9)
ax.barh(range(len(d)),d[:,1], left=d[:,0], ec="k")
ax.set_yticks(range(len(d)))
plt.show()

Seaborn & Matplotlib Adding Text Relative to Axes

Trying to use seaborn and matplotlib to plot some data, need to add some descriptive text to my plot, normally I'd just use the matplotlib command text, and place it where I wanted relative to the axes, but it doesn't appear to work at all, I get no text showing beyond the default stuff on the axes, ticks, etc. What I want is some custom text showing in the top left corner of the plot area.
import seaborn as sns
import matplotlib.pyplot as plt
import pandas as pd
df is my pandas dataframe, it just contains some columns of time and coordinate data with a tag "p" which is an identifier.
ax2 = sns.scatterplot("t","x", data = df, hue = "p")
ax2.text(0.1, 0.9, r"$s = {}, F = {}, N = {}$".format(value1, valu2, value3))
plt.show()
Anyone know how I can get some text to show, relatively positioned, the "value" items are just the variables with the data I want to print. Thanks.
You want to position a text "in the top left corner of the plot area". The "plot area" is called axes. Three solutions come to mind:
Text in axes coordinates
You could specify the text in axes coordinates. Those range from (0,0) in the lower left corner of the axes to (1,1) in the top right corner of the axes. The corresponding transformation is obtained via ax.transAxes.
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
ax.text(0.02, 0.98, "Text", ha="left", va="top", transform=ax.transAxes)
plt.show()
Annotation with offset
In the above the distance between the text and the top left corner will be dependent on the size of the axes. It might hence be beneficial to position the text exactly at the top left corner (i.e. (0,1) in axes coordinates) and then offset it by some points, i.e in absolute units.
ax.annotate("Text", xy=(0,1), xycoords="axes fraction",
xytext=(5,-5), textcoords="offset points",
ha="left", va="top")
The result here looks similar to the above, but is independent of the axes or figure size; the text will always be 5 pts away from the top left corner.
Text at an anchored position
Finally, you may not actually want to specify any coordinates at all. After all "upper left" should be enough as positioning information. This would be achieved via an AnchoredText as follows.
import matplotlib.pyplot as plt
from matplotlib.offsetbox import AnchoredText
fig, ax = plt.subplots()
anc = AnchoredText("Text", loc="upper left", frameon=False)
ax.add_artist(anc)
plt.show()
In order to position the text in the upper left corner for a plot without knowing the limits beforehand you can query the x and y limits of the axis and use that to position the text relative to the bounds of the plot. Consider this example (where I have also included code to generate some random data for demonstration)
import seaborn as sns
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
np.random.seed(1)
df = pd.DataFrame({'t':np.linspace(1,2,100),
'x':np.random.randn(100)})
value1 = 1
value2 = 2
value3 = 3
ax2 = sns.scatterplot("t","x", data = df)
tl = ((ax2.get_xlim()[1] - ax2.get_xlim()[0])*0.010 + ax2.get_xlim()[0],
(ax2.get_ylim()[1] - ax2.get_ylim()[0])*0.95 + ax2.get_ylim()[0])
ax2.text(tl[0], tl[1], r"$s = {}, F = {}, N = {}$".format(value1, value2, value3))
plt.show()
This will output
and changing the bounds will not change the position of the text, i.e.
You may need to adjust the multipliers 0.01 and 0.95 as you want based on exactly how close to the corner you want the text.

Aligning annotated text with colorbar label text

I'd like to find a way to make an annotation that automatically aligns with the label text of a colorbar. Take this example:
import matplotlib.pyplot as plt
import numpy as np
fig, ax = plt.subplots(figsize=(5,10))
data = np.arange(1000, 0, -10).reshape(10, 10)
im = ax.imshow(data, cmap='Blues')
clb = plt.colorbar(im, shrink=0.4)
clb.ax.annotate('text', xy=(1, -0.075), xycoords='axes fraction')
I want to have the last t of "text" to be on the same x coordinate as the last 0 of 1000 in the colorbar label. I can do so manually by adjusting the xy parameter in annotate, but I have to do this for many graphs and would like to find a way to get the parameter from somewhere automatically.
How can I get the maximum x coordinate of the text labes and annotate in a way where the annotation ends on that coordinate? Could someone point me in the right direction? Thanks a lot!
Since the labels are left-aligned, but you want to align your additional text according to the end of that label, I fear there is no other choice than to find out the coordinates from the drawn figure and place the label accordingly.
import matplotlib.pyplot as plt
from matplotlib import transforms
import numpy as np
fig, ax = plt.subplots(figsize=(5,4))
data = np.arange(1000, 0, -10).reshape(10, 10)
im = ax.imshow(data, cmap='Blues')
cbar = plt.colorbar(im)
# draw figure first to be able to retrieve coordinates
fig.canvas.draw()
# get the bounding box of the last label
bbox = cbar.ax.get_yticklabels()[-1].get_window_extent()
# calculate pixels back to axes coords
labx,_ = cbar.ax.transAxes.inverted().transform([bbox.x1,0])
ax.annotate('text', xy=(labx, -0.075), xycoords=cbar.ax.transAxes,
ha = "right")
plt.show()
Note that this approach will fail once you change the figure size afterwards or change the layout in any other way. It should hence always come last in your code.

Polar grid on left hand side of rectangular plot

I am trying to reproduce a plot like this:
So the requirements are actually that the grid (that is to be present just on the left side) behaves just like a grid, that is, if we zoom in and out, it is always there present and not dependent on specific x-y limits for the actual data.
Unfortunately there is no diagonal version of axhline/axvline (open issue here) so I was thinking about using the grid from polar plots.
So for that I have two problems:
This answer shows how to overlay a polar axis on top of a rectangular one, but it does not match the origins and x-y values. How can I do that?
I also tried the suggestion from this answer for having polar plots using ax.set_thetamin/max but I get an AttributeError: 'AxesSubplot' object has no attribute 'set_thetamin' How can I use these functions?
This is the code I used to try to add a polar grid to an already existing rectangular plot on ax axis:
ax_polar = fig.add_axes(ax, polar=True, frameon=False)
ax_polar.set_thetamin(90)
ax_polar.set_thetamax(270)
ax_polar.grid(True)
I was hoping I could get some help from you guys. Thanks!
The mpl_toolkits.axisartist has the option to plot a plot similar to the desired one. The following is a slightly modified version of the example from the mpl_toolkits.axisartist tutorial:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.cbook as cbook
from mpl_toolkits.axisartist import SubplotHost, ParasiteAxesAuxTrans
from mpl_toolkits.axisartist.grid_helper_curvelinear import GridHelperCurveLinear
import mpl_toolkits.axisartist.angle_helper as angle_helper
from matplotlib.projections import PolarAxes
from matplotlib.transforms import Affine2D
# PolarAxes.PolarTransform takes radian. However, we want our coordinate
# system in degree
tr = Affine2D().scale(np.pi/180., 1.) + PolarAxes.PolarTransform()
# polar projection, which involves cycle, and also has limits in
# its coordinates, needs a special method to find the extremes
# (min, max of the coordinate within the view).
# 20, 20 : number of sampling points along x, y direction
extreme_finder = angle_helper.ExtremeFinderCycle(20, 20,
lon_cycle=360,
lat_cycle=None,
lon_minmax=None,
lat_minmax=(0, np.inf),)
grid_locator1 = angle_helper.LocatorDMS(36)
tick_formatter1 = angle_helper.FormatterDMS()
grid_helper = GridHelperCurveLinear(tr,
extreme_finder=extreme_finder,
grid_locator1=grid_locator1,
tick_formatter1=tick_formatter1
)
fig = plt.figure(1, figsize=(7, 4))
fig.clf()
ax = SubplotHost(fig, 1, 1, 1, grid_helper=grid_helper)
# make ticklabels of right invisible, and top axis visible.
ax.axis["right"].major_ticklabels.set_visible(False)
ax.axis["right"].major_ticks.set_visible(False)
ax.axis["top"].major_ticklabels.set_visible(True)
# let left axis shows ticklabels for 1st coordinate (angle)
ax.axis["left"].get_helper().nth_coord_ticks = 0
# let bottom axis shows ticklabels for 2nd coordinate (radius)
ax.axis["bottom"].get_helper().nth_coord_ticks = 1
fig.add_subplot(ax)
## A parasite axes with given transform
## This is the axes to plot the data to.
ax2 = ParasiteAxesAuxTrans(ax, tr)
## note that ax2.transData == tr + ax1.transData
## Anything you draw in ax2 will match the ticks and grids of ax1.
ax.parasites.append(ax2)
intp = cbook.simple_linear_interpolation
ax2.plot(intp(np.array([150, 230]), 50),
intp(np.array([9., 3]), 50),
linewidth=2.0)
ax.set_aspect(1.)
ax.set_xlim(-12, 1)
ax.set_ylim(-5, 5)
ax.grid(True, zorder=0)
wp = plt.Rectangle((0,-5),width=1,height=10, facecolor="w", edgecolor="none")
ax.add_patch(wp)
ax.axvline(0, color="grey", lw=1)
plt.show()

How to calculate the dimensions of a text object in python's matplotlib

I'm trying to calculate the dimensions of a text object, given the characters in the array, point size and font. This is to place the text string in such a way that it's centered within a plot when using the matplotlib package in python, and will have to use the same units as the data being plotted.
As pointed out in the comments, matplotlib allows for centering text (and other alignments). See documentation here.
If you really need the dimensions of a text object, here is a quick solution that relies on drawing the text once, getting its dimensions, converting them to data dimensions, deleting the original text, then re-plotting the text centered in data coordinates. This question provides a useful explanation.
import matplotlib.pyplot as plt
plt.ion()
fig = plt.figure()
ax = fig.add_subplot(111)
xlim = ax.get_xlim()
ylim = ax.get_ylim()
textToPlot = 'Example'
t = ax.text(.5*(xlim[0] + xlim[1]), .5*(ylim[0] + ylim[1]), textToPlot)
transf = ax.transData.inverted()
bb = t.get_window_extent(renderer = fig.canvas.renderer)
bb_datacoords = bb.transformed(transf)
newX = .5*(xlim[1] - xlim[0] - (bb_datacoords.x1 - bb_datacoords.x0))
newY = .5*(ylim[1] - ylim[0] - (bb_datacoords.y1 - bb_datacoords.y0))
t.remove()
ax.text(newX, newY, textToPlot)
ax.set_xlim(xlim)
ax.set_ylim(ylim)
The result of this script looks like this:

Categories