How do I get the height of a wrapped text in Matplotlib? - python

I have a suptitle object that will sometimes be wrapped, using the built in wrapping functionality of Matplotlib. However, when trying to get the height of the suptitle, I seem to always get the height corresponding to one line. Where am I going wrong? This is what I'm trying with:
from matplotlib.figure import Figure
from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas
fig = Figure((4, 4))
FigureCanvas(fig)
text_1 = "I'm a short text"
text_2 = "I'm a longer text that will be wrapped autoamtically by Matplotlib, using wrap=True"
title = fig.suptitle(text_1, wrap=True)
fig.canvas.draw() # Draw text to find out how big it is
bbox = title.get_window_extent()
print(bbox.width) # 105
print(bbox.height) # 14
title = fig.suptitle(text_2, wrap=True)
fig.canvas.draw() # Draw text to find out how big it is
bbox = title.get_window_extent()
print(bbox.width) # 585 <-- This looks about right
print(bbox.height) # Still 14 even though this time the text is wrapped!
The same thing happens with Text objects (using something like fig.text(0.5, 0.5, text_1, wrap=True).

Thank you #ImportanceOfBeingErnest for pointing out that this is not really possible. Here is one workaround, that kind of works, by checking the number of lines the text is broken up into, and multiplying by the approximate line-height. This works when automatically inserted breaks are mixed with manually (i.e. there is an "\n" in the text), but will be off by a number of pixels. Any more precise suggestions welcome.
def get_text_height(fig, obj):
""" Get the approximate height of a text object.
"""
fig.canvas.draw() # Draw text to find out how big it is
t = obj.get_text()
r = fig.canvas.renderer
w, h, d = r.get_text_width_height_descent(t, obj._fontproperties,
ismath=obj.is_math_text(t))
num_lines = len(obj._get_wrapped_text().split("\n"))
return (h * num_lines)
text = "I'm a long text that will be wrapped automatically by Matplotlib, using wrap=True"
obj = fig.suptitle(text, wrap=True)
height = get_text_height(fig, obj)
print(height) # 28 <-- Close enough! (In reality 30)

Related

Text alignment control in Shady

I'm using Shady to write some text on screen, and I'm wondering what would be the simplest way to control the alignment of the string. From my understanding, the align parameter in a Shady text object controls the paragraph alignment, but I'm interested in controlling the alignment of a single line of text.
Essentially I'd like to replicate the behavior of the horizontalalignment, verticalalignment and rotation parameters of the matplotlib text function. But to do that I need to estimate the area (in pixels) that will be occupied by the string once rendered. Can I get that out of Shady somehow? In the manual it says that the rendering is done on the CPU and the rendered String is then pushed to the GPU, so it should be doable.
You're correct that the .text.align and .text.wrapping properties are to do with "alignment" only at the logical level of the text flow—i.e., how the lines of a multi-line text stimulus are aligned relative to each other in the coordinate-frame in which they're read (independent of which way up the whole stimulus is physically).
The properties you're talking about—rotation, "vertical alignment", and even what you call "horizontal alignment" if there's only one line of text in play—are not text-specific properties: they could apply equally well to any rectangular patch. For this reason, the properties you want to manipulate are stim.* level properties, not stim.text.*. Specifically, they are .anchor and .rotation as demonstrated here:
#!/usr/bin/env python -m Shady shell
import Shady, Shady.Text
w = Shady.World(fullScreenMode=False)
axes = w.Stimulus(Shady.PixelRuler(1000), anchor=Shady.LOWER_LEFT, size=600)
xlabel = w.Stimulus(text='x axis label', x=300, y=0, anchor=Shady.TOP)
ylabel = w.Stimulus(text='y axis label', x=0, y=300, anchor=Shady.BOTTOM, rotation=90)
speed = 30
msg = w.Stimulus(
xy = 300,
rotation = Shady.Integral( lambda t: speed ),
text = 'Change msg.anchor to anything\nbetween [-1,-1] and [+1,+1]\nand see what happens',
text_blockbg = [0, 0, 0, 0.5],
)
Shady.AutoFinish(w)
Somewhere in the undocumented functions of Shady.Text there is probably some way of estimating, in advance, what the size of a rendered text stimulus is going to be. In fact, on closer examination, it looks like the least annoying way to do it would be to actually make the texture array:
img = Shady.Text.MakeTextImage('hello world')
heightInPixels, widthInPixels, _ = img.shape
But hopefully with the appropriate usage of .anchor you should no longer need this.

"Button_Press_Event" Coordinates Being Called Before I Actually Click

I am plotting a fits image of a field of stars. I am then displaying circular apertures across the image on sufficiently bright stars. Next, I am trying to click on the star that I am interested in, and getting the brightness measurement out of the nearest circle. Finally, I want to use that brightness measurement for other calculations.
My problem is that the variable I declare to store the x,y coordinates of my click ("coords") seems to be getting called before I actually click, resulting in an empty array and errors.
A co-worker sent it to me many months ago, and once upon a time it worked flawlessly. But it seems that it's stopped working. It may be a result of updating some libraries/modules, but I can't be sure.
I have tried various combinations, including declaring "coords" in other function, in other locations, etc. I have tried changing the order of certain lines to perhaps get "coords" to be called later. I've also tried writing the code from the ground up, and have had many of the same errors. Because this specific code once worked, I attach it instead of my attempts.
To be honest, I don't fully understand the code, as I didn't write it. As a result, I don't understand what is being called when, or what any changes I've made actually do.
def plot_image(file, vmin=5, vmax=99, threshold=5, radius=25):
hdu=fits.open(file)
image = hdu[0].data
exptime = hdu[0].header['EXPTIME']
band = hdu[0].header['FILTERS']
airmass = hdu[0].header['AIRMASS']
readnoise = hdu[0].header['RN_01']
gain = hdu[0].header['GAIN_01']
obj = hdu[0].header['OBJECT']
sub = image[1500:2000,1500:2000]
bkg_sigma = mad_std(sub)
mean, median, std = sigma_clipped_stats(sub, sigma=3.0, maxiters=5)
daofind = photutils.DAOStarFinder(fwhm=2., threshold=threshold*bkg_sigma)
sources = daofind(sub - median)
positions = (sources['xcentroid'], sources['ycentroid'])
apertures = photutils.CircularAperture(positions, r=radius)
phot_table = photutils.aperture_photometry(sub - median, apertures)
pix = select(image, apertures)
print(pix)
if len(pix) == 2 or len(coords) == 2:
distance = np.sqrt((np.array(phot_table['xcenter'])-pix[0])**2 + (np.array(phot_table['ycenter'])-pix[1])**2)
star = np.argmin(dist)
counts = phot_table[star]['aperture_sum']
fluxfile = open('testfile.txt')
signal = (counts * gain) / exptime
err = np.sqrt(counts*gain + (readnoise**2*np.pi*radius**2))
else:
print('Pix length = 0')
def select(image, apertures, vmin = 5, vmax = 99):
global coords
coords = []
fig = plt.figure(figsize = (9,9))
ax = fig.add_subplot(111)
ax.imshow(image, cmap = 'gist_gray_r', origin='lower', vmin = np.percentile(image, vmin), vmax = np.percentile(image, vmax), interpolation='none')
apertures.plot(color='blue', lw=1.5, alpha=0.5, ax = ax)
ax.set_title('Hello')#label='Object: '+obj+'\nFilter: '+band)
cid = fig.canvas.mpl_connect('button_press_event', onclick)
plt.show()
fig.canvas.mpl_disconnect(cid)
if None in coords:
return [np.nan,np.nan]
else:
return np.round(coords)
def onclick(event):
x = event.xdata
y = event.ydata
global coords
coords = [x, y]
plt.close()
return
def closeonclick(event):
print('Close On Click')
plt.close()
return
Expected Result: The image is displayed with blue apertures overlaid. Then, I click on the desired star and the coordinates I click are stored to "coords" and printed to the console. The window displaying the image is closed alongside the previous step, as well. Finally, using those coordinates, it finds the nearest aperture and does some science with the resulting brightness.
Actual Result: "coords" is immediately printed (an empty list). Immediately after, the image is displayed. Clicking it does nothing. It doesn't change the value of "coords", nothing else is printed, nor does the window close.
I'll come back and delete this if it isn't right (I'd comment if I had the reputation), but it looks like you have to define a global variable outside any functions first, then use the keyword before the variable name inside a function to change its scope. Try moving "coords = []" outside your functions (the list will no longer be empty after the first call to "onclick", but each new click should replace the coordinates so it shouldn't be an issue).
Ref: https://www.programiz.com/python-programming/global-keyword

Ellipse patch equivalent to set_radius in the Circle Patch? Matplotlib

To summarise the important bit:
I have a function which plots a circle on a matplotlib graph. Every time I recall the function I simply resize the circle (using set_radius), as It always needs to be in the same positon on the graph (in the centre). This way It doesn't get too messy
I want to do the same thing with an ellipse patch but this time be able to change the height, width and angle it is at. However I can't find any equivalent of set_radius
def Moment_Of_Inertia(self):
"""Plot the moment of Inertia ellipse, with the ratio factor """
# my code to get ellipse/circle properties
self.limitradius = findSBradius(self.RawImage,self.SBLimit)[0]
MoIcall = mOinertia(self.RawImage,self.limitradius)
self.ratio=MoIcall[0] # get the axes ratio
self.height=1
Eigenvector = MoIcall[1]
self.EllipseAngle np.degrees(np.arctanh((Eigenvector[1]/Eigenvector[0])))
# This is the part I am not sure how to do
self.MoIellipse.set(width=self.ratio*15)
self.MoIellipse.set(height=self.height*15)
self.MoIellipse.set(angle= self.EllipseAngle)
# It works with a circle patch
self.circleLimit.set_radius(self.limitradius)
self.circleLimit.set_visible(True)
self.MoIellipse.set_visible(True)
self.canvas.draw()
If my code is a bit out of context I am happy to explain more, I am trying to embed a matplotlib graph in a tkinter window. both patches are already initialized in the constructor and I just want to resize them.
This answer assumes that the question is about the Ellipse from matplotlib.patches.Ellipse.
This has attributes width, height and angle. You can set those attributes as
ellipse = matplotlib.patches.Ellipse((0,0),.5,.5)
ellipse.width = 1
ellipse.height = 2
ellipse.angle = 60
As for any other python object, you can also use setattr, like
setattr(ellipse,"width", 2)
Some complete example:
import matplotlib.pyplot as plt
import matplotlib.widgets
class sliderellipse(matplotlib.widgets.Slider):
def __init__(self,*args,**kwargs):
self.ellipse = kwargs.pop("ellipse", None)
self.attr = kwargs.pop("attr", "width")
matplotlib.widgets.Slider.__init__(self,*args,**kwargs)
self.on_changed(self.update_me)
def update_me(self,val=None):
setattr(self.ellipse,self.attr, val)
self.ax.figure.canvas.draw_idle()
fig, axes = plt.subplots(nrows=4,
gridspec_kw={"height_ratios" : [1,.05,.05,.05],
"hspace" : 0.5})
axes[0].axis([-1,1,-1,1])
axes[0].set_aspect("equal")
ellipse = matplotlib.patches.Ellipse((0,0),.5,.5)
axes[0].add_patch(ellipse)
labels = ["width", "height","angle"]
maxs = [2,2,360]
sl = []
for ax,lab,m in zip(axes[1:],labels,maxs):
sl.append(sliderellipse(ax,lab,0,m,ellipse=ellipse,attr=lab))
plt.show()

Area Line Plot in Python using Report Lab

I am using Reportlab to create some graphs in my PDF reports. I was creating an Area Line Plot and got stuck at a point where I am not able to understand why am I not getting the output I would like to see.
Here is the code I had written for my output:
def standardLinePlot(data, width=200, height=200):
d = Drawing(width, height)
lp = AreaLinePlot()
lp.data=data
lp.width, lp.height = width, height
lp.xValueAxis.valueMin = 0
lp.xValueAxis.valueMax =36
lp.xValueAxis.valueSteps = [0,6,12,18,24,30,36]
lp.yValueAxis.valueMin = 0
lp.yValueAxis.valueMax =100
lp.strokeColor=colors.black
lp.fillColor=colors.grey
lp.reversePlotOrder = False
lp.joinedLines=1
d.add(lp)
return d
The output I am getting is:
My intended output is that grey color should be in place of red color which is the area under the line plot. The other problem is how can I add the axis title to this chart. For example, I need “Months” to be my X axis and “% of NAV” to be my Y axis.
To define the color for the lines it seems you need to access... well, the lines :). So, lp.lines[0].strokeColor = colors.grey instead of lp.strokeColor = colors.grey, as that one goes for the plot background color!
The question about the labels is a bit more tricky, though... ScatterPlot includes functionality to set labels for X and Y axis, but that's not the case for AreaLinePlot. Of course, you could derive a class from AreaLinePlot copying that functionality, if you're going to use it often.
Change
lp = AreaLinePlot()
to
lp = LinePlot()
and try that
lp.lines[0].strokeColor = colors.red
lp.lines[0].inFill = True
but the fill color will be the same as the line color.
credit goes to #Ricardo Cárdenes

converting text size into data coordinates

In matplotlib, what is a way of converting the text box size into data coordinates?
For example, in this toy script I'm fine-tuning the coordinates of the text box so that it's next to a data point.
#!/usr/bin/python
import matplotlib.pyplot as plt
xx=[1,2,3]
yy=[2,3,4]
dy=[0.1,0.2,0.05]
fig=plt.figure()
ax=fig.add_subplot(111)
ax.errorbar(xx,yy,dy,fmt='ro-',ms=6,elinewidth=4)
# HERE: can one get the text bbox size?
txt=ax.text(xx[1]-0.1,yy[1]-0.4,r'$S=0$',fontsize=16)
ax.set_xlim([0.,3.4])
ax.set_ylim([0.,4.4])
plt.show()
Is there a way of doing something like this pseudocode instead?
x = xx[1] - text_height
y = yy[1] - text_width/2
ax.text(x,y,text)
Generally speaking, you can't get the size of the text until after it's drawn (thus the hacks in #DSM's answer).
For what you're wanting to do, you'd be far better off using annotate.
E.g. ax.annotate('Your text string', xy=(x, y), xytext=(x-0.1, y-0.4))
Note that you can specify the offset in points as well, and thus offset the text by it's height (just specify textcoords='offset points')
If you're wanting to adjust vertical alignment, horizontal alignment, etc, just add those as arguments to annotate (e.g. horizontalalignment='right' or equivalently ha='right')
I'm not happy with it at all, but the following works; I was getting frustrated until I found this code for a similar problem, which suggested a way to get at the renderer.
import matplotlib.pyplot as plt
xx=[1,2,3]
yy=[2,3,4]
dy=[0.1,0.2,0.05]
fig=plt.figure()
figname = "out.png"
ax=fig.add_subplot(111)
ax.errorbar(xx,yy,dy,fmt='ro-',ms=6,elinewidth=4)
# start of hack to get renderer
fig.savefig(figname)
renderer = plt.gca().get_renderer_cache()
# end of hack
txt = ax.text(xx[1], yy[1],r'$S=0$',fontsize=16)
tbox = txt.get_window_extent(renderer)
dbox = tbox.transformed(ax.transData.inverted())
text_width = dbox.x1-dbox.x0
text_height = dbox.y1-dbox.y0
x = xx[1] - text_height
y = yy[1] - text_width/2
txt.set_position((x,y))
ax.set_xlim([0.,3.4])
ax.set_ylim([0.,4.4])
fig.savefig(figname)
OTOH, while this might get the text box out of the actual data point, it doesn't necessarily get the box out of the way of the marker, or the error bar. So I don't know how useful it'll be in practice, but I guess it wouldn't be that hard to loop over all the drawn objects and move the text until it's out of the way. I think the linked code tries something similar.
Edit: Please note that this was clearly a courtesy accept; I would use Joe Kington's solution if I actually wanted to do this, and so should everyone else. :^)

Categories

Resources