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. :^)
Related
When creating 2d-images with pyplot by using pcolor/pcolormesh I am using custom labels in my project. Unfortunately, some of those labels are too long to fit into their allocated space, and therefore spill over the next label, as shown below for a 2x2-matrix:
and a 4x4-matrix:
I tried to find solutions for that issue, and until now have found two possible approaches: Replacing all spaces with newlines (as proposed by https://stackoverflow.com/a/62521738/2546099), or using textwrap.wrap(), which needs a fixed text width (as proposed by https://stackoverflow.com/a/15740730/2546099 or https://medium.com/dunder-data/automatically-wrap-graph-labels-in-matplotlib-and-seaborn-a48740bc9ce)
I tested both approaches for both test matrices shown above, resulting in (when replacing space with newlines) for the 2x2-matrix:
and for the 4x4-matrix:
Similarly, I tested the second approach for both matrices, the 2x2-matrix, resulting in
and the 4x4-matrix, resulting in
Still, both approaches share the same issues:
When wrapping the label, a part of the label will extend into the image. Setting a fixed pad size will only work if the label size is known beforehand, to adjust the distance properly. Else, some labels might be spaced too far apart from the image, while others are still inside the image.
Wrapping the label with a fixed size might leave too much white space around. The best case might be figure 5 and 6: I used a wrap at 10 characters for both figures, but while the text placement works out nicely (from my point of view) for four columns, there is enough space to increase the wrap limit to 20 characters for two columns. For even more columns 10 characters might even be too much.
Therefore, are there other solutions which might solve my problem dynamically, depending on the size of the figure and the available space?
The code I used for generating the pictures above is:
import textwrap
import numpy as np
import matplotlib.pyplot as plt
imshow_size = 4
x_vec, y_vec = (
np.linspace(0, imshow_size - 1, imshow_size),
np.linspace(0, imshow_size - 1, imshow_size),
)
labels_to_plot = np.linspace(0, imshow_size, imshow_size + 1)
categories = ["This is a very long long label serving its job as place holder"] * (
imshow_size
)
## Idea 1: Replace space with \n, similar to https://stackoverflow.com/a/62521738/2546099
# categories = [category.replace(" ", "\n") for category in categories]
## Idea 2: Wrap labels at certain text length, similar to https://stackoverflow.com/a/15740730/2546099 or
## https://medium.com/dunder-data/automatically-wrap-graph-labels-in-matplotlib-and-seaborn-a48740bc9ce
categories = [textwrap.fill(category, 10) for category in categories]
X, Y = np.meshgrid(x_vec, y_vec)
Z = np.random.rand(imshow_size, imshow_size)
fig, ax = plt.subplots()
ax.pcolormesh(X, Y, Z)
ax.set_xticks(labels_to_plot[:-1])
ax.set_xticklabels(categories, va="center")
ax.set_yticks(labels_to_plot[:-1])
ax.set_yticklabels(categories, rotation=90, va="center")
fig.tight_layout()
plt.savefig("with_wrap_at_10_with_four_labels.png", bbox_inches="tight")
# plt.show()
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.
I am trying to plot a linear line with associated error.
I calculated values for slope (a) and intercepts (b). In addition, I calculated the error associated with these values. So I drew the line given by the typical formula below.
y=ax+b
However, in addition to the line, I also want to draw the associated error. I came up with the idea to draw the lines associated with these formulas and color the space between the lines gray.
y=(a+a_sd)x+(b+b_sd)
y=(a-a_sd)x+(b-b_sd)
Uisng the following piece of code, I am able to color part of the surface between the lines, but not the whole span (see included output).
I think this may be due to the fact that "distance" is not sorted, and fill_between is using distance[0] and distance[-1] as begin and end for the span, respectively.
As always, any help would be highly appreciated!
import matplotlib.pyplot as plt
distance=[0.35645334340084989, 0.55406894241607718, 0.10201413273193734, 0.13401365724625941, 0.71918808865838735, 0.14151335417722818]
time=[2.4004984846346171, 2.4909766335028447, 1.9852064018125195, 1.9083156734132103, 2.6380396934372863, 1.9114505780323543]
time_SD=[0.062393810960652669, 0.056945715242838917, 0.073960838867327183, 0.084111239062664475, 0.026912957190265499, 0.08595664694840538]
distance_SD=[0.035160608598240162, 0.032976715460514235, 0.02782911002465227, 0.035465701695038584, 0.043009444687382707, 0.038387585107200854]
a=1.17887019041
b=1.83339229489
a_sd=0.159771527859
b_sd=0.0762509747218
plt.errorbar(distance,time,yerr=time_SD, xerr=distance_SD, linestyle="None")
abline_values = [(a)*i + (b) for i in distance]
abline_values_plus = [(a+a_sd)*i + (b+b_sd) for i in distance]
abline_values_minus = [(a-a_sd)*i + (b-b_sd) for i in distance]
plt.plot(distance, abline_values,"r")
plt.fill_between(distance,abline_values_minus,abline_values_plus,facecolor='lightgrey', interpolate=True, edgecolors="None")
leg = plt.legend(loc="lower right", frameon=False, handlelength=0, handletextpad=0)
for item in leg.legendHandles:
item.set_visible(False)
plt.show()
In order to use pyplot.fill_between() the list to plot the horizontal coordinate should be sorted. Using an unsorted list of x values is possible, but can lead to undesired results.
Sorting a list can be done using sorted(list).
import matplotlib.pyplot as plt
distance=[0.35645334340084989, 0.55406894241607718, 0.10201413273193734, 0.13401365724625941, 0.71918808865838735, 0.14151335417722818]
time=[2.4004984846346171, 2.4909766335028447, 1.9852064018125195, 1.9083156734132103, 2.6380396934372863, 1.9114505780323543]
time_SD=[0.062393810960652669, 0.056945715242838917, 0.073960838867327183, 0.084111239062664475, 0.026912957190265499, 0.08595664694840538]
distance_SD=[0.035160608598240162, 0.032976715460514235, 0.02782911002465227, 0.035465701695038584, 0.043009444687382707, 0.038387585107200854]
a=1.17887019041
b=1.83339229489
a_sd=0.159771527859
b_sd=0.0762509747218
distance_sorted = sorted(distance)
plt.errorbar(distance,time,yerr=time_SD, xerr=distance_SD, linestyle="None")
abline_values = [(a)*i + (b) for i in distance_sorted]
abline_values_plus = [(a+a_sd)*i + (b+b_sd) for i in distance_sorted]
abline_values_minus = [(a-a_sd)*i + (b-b_sd) for i in distance_sorted]
plt.plot(distance_sorted, abline_values,"r")
plt.fill_between(distance_sorted,abline_values_minus,abline_values_plus, facecolor='lightgrey', edgecolors="None")
plt.show()
The documentation does not mention the requirement of x values being sorted. The reason is probably that fill_between actually works even with unsorted lists, just not the way one might expect. Maybe the following animation gives a more intuitive understanding on the issue:
You are right fill_between seems to expect the values to be sorted. The documentation is not clear about this behaviour though. The following example however shows the same effect:
import matplotlib.pyplot as plt
from numpy import random, array
#x = random.randn(20) #does not work
x = array(sorted(random.randn(20))) #works
a = 2
d = .5
y_h = x*(a+d)
y_l = x*(a-d)
plt.fill_between(x,y_h, y_l)
plt.show()
As a workaround just sort your values before calculating your errorlines using sorted.
I am plotting just a simple scatterplot with MPL 1.4.0. I want to control the number of dashes on the figures I am plotting because currently even though I set a linestyle, the dashes are too close to each other so it doesn't look like a properly dashed line.
#load cdeax,cdeay,gsix,gsiy,reich all are arrays of shape (380,)
figfit = plt.figure(); axfit = figfit.gca()
axfit.plot(cdeax,np.log(cdeay),'ko', alpha=.5); axfit.plot(gsix,np.log(gsiy), 'kx')
axfit.plot(cdeax,cdeafit,'k-'); axfit.plot(gsix,gsifit,'k:')
longevityregplot[1].plot(gsix,np.log(reich_l),'k-.')
#load cdeax,cdeay,gsix,gsiy,reich all are arrays of shape (380,)
figfit = plt.figure(); axfit = figfit.gca()
axfit.plot(cdeax,np.log(cdeay),'ko', alpha=.5); axfit.plot(gsix,np.log(gsiy), 'kx')
axfit.plot(cdeax,cdeafit,'k-',dashes = [10,10]); axfit.plot(gsix,gsifit,'k:',dashes=[10,10])
longevityregplot[1].plot(gsix,np.log(reich_l),'k-.')
However the above is what I get. Rather than a uniformly-dashed line, the lines get dashed at the ends to varying degrees but no matter what values I use for dashes, the dashing is never uniform.
I'm afraid I really don't know what the problem is here... Any ideas?
I have pasted the arrays I am using here: http://pastebin.com/rJ5Jjfmm
You should be able to just copy/paste them to your IDE for the above code to run.
Cheers!
EDIT:
Just with the single line plotted:
axfit.plot(cdeax,cdeafit,'k-',dashes = [10,10]);
EDIT2: pastebin link changed to include all data
EDIT3: Histogram of point density along the x axis:
I think what #cphlewis said is correct, you may have some x-axis backtracking. If I sort everything it looks ok to me (did my own fitting since I still don't see the fits on pastebin)
# import your data here
import math
figfit = plt.figure(); axfit = figfit.gca()
cdea = zip(cdeax,cdeay)
cdea = np.array(sorted(cdea, key = lambda x: x[0]))
gsi = zip(gsix,gsiy)
gsi = np.array(sorted(gsi, key = lambda x: x[0]))
cdeafit2 = np.polyfit(cdea[:,0],cdea[:,1],1)
gsifit2 = np.polyfit([x[0] for x in gsi],[math.log(x[1]) for x in gsi],1)
cdeafit = [x*cdeafit2[0] + cdeafit2[1] for x in cdea[:,0]]
gsifit = [math.exp(y) for y in [x*gsifit2[0] + gsifit2[1] for x in gsi[:,0]]]
axfit.plot(cdea[:,0],cdea[:,1],'ko', alpha=.5); axfit.plot(gsi[:,0],gsi[:,1], 'kx')
axfit.plot(cdea[:,0],cdeafit,'k-',dashes = [10,10]); axfit.plot(gsi[:,0],gsifit,'k:',dashes=[10,10])
#longevityregplot[1].plot(gsix,np.log(reich_l),'k-.') # not sure what this is
axfit.set_yscale('log')
plt.show()
How can the line width of the error bar caps in Matplotlib be changed?
I tried the following code:
(_, caplines, _) = matplotlib.pyplot.errorbar(
data['distance'], data['energy'], yerr=data['energy sigma'],
capsize=10, elinewidth=3)
for capline in caplines:
capline.set_linewidth(10)
capline.set_color('red')
pp.draw()
Unfortunately, this updates the color of the caps, but does not update the line width of the caps!
The resulting effect is similar to the "fat error bar lines / thin caps" in the following image:
It would be nice to have "fat" bar caps, in the case; how can this be done, in Matplotlib? Drawing the bar caps "manually", one by one with plot() would work, but a simpler alternative would be best.
EOL, you were very close..,
distance = [1,3,7,9]
energy = [10,20,30,40]
sigma = [1,3,2,5]
(_, caps, _) = plt.errorbar(distance, energy, sigma, capsize=20, elinewidth=3)
for cap in caps:
cap.set_color('red')
cap.set_markeredgewidth(10)
plt.show
set_markeredgewidth sets the width of the cap lines.
Matplotlib objects have so many attributes that often it is difficult to remember the right ones for a given object. IPython is a very useful tool for introspecting matplotlib. I used it to analyze the properties of the 2Dlines correponding to the error cap lines and I found that and other marker properties.
Cheers
This is based on #joaquin's answer, but a little more concise (if you just want plain error caps with no special styling):
distance = [1,3,7,9]
energy = [10,20,30,40]
sigma = [1,3,2,5]
plt.errorbar(distance,
energy,
sigma,
capsize=5,
elinewidth=2,
markeredgewidth=2)