0

There is an example here for how to create a multi-colored text title.

However, I want to apply this to a plot that already has a figure in it.

For example, if I apply it to this (same code as with the example minus a few extras and with another figure)...:

plt.rcdefaults()
import matplotlib.pyplot as plt
%matplotlib inline
from matplotlib import transforms

fig = plt.figure(figsize=(4,3), dpi=300)

def rainbow_text(x,y,ls,lc,**kw):

    t = plt.gca().transData
    fig = plt.gcf()
    plt.show()

    #horizontal version
    for s,c in zip(ls,lc):
        text = plt.text(x,y," "+s+" ",color=c, transform=t, **kw)
        text.draw(fig.canvas.get_renderer())
        ex = text.get_window_extent()
        t = transforms.offset_copy(text._transform, x=ex.width, units='dots')

plt.figure()
rainbow_text(0.5,0.5,"all unicorns poop rainbows ! ! !".split(), 
        ['red', 'orange', 'brown', 'green', 'blue', 'purple', 'black'],
        size=40)

...the result is 2 plots with the title enlarged. This sort of makes sense to me because I'm using plt. two times. But how do I integrate it so that it only refers to the first instance of plt. in creating the title?

Also, about this line:

t = transforms.offset_copy(text._transform, x=ex.width, units='dots')

I notice it can alter the spacing between words, but when I play with the values of x, results are not predictable (spacing is inconsistent between words). How can I meaningfully adjust that value?

And finally, where it says "units='dots'", what are the other options? Are 'dots' 1/72nd of an inch (and is that the default for Matplotlib?)?

How can I convert units from dots to inches?

Thanks in advance!

Dance Party2
  • 7,214
  • 17
  • 59
  • 106

3 Answers3

1

In fact the bounding box of the text comes in units unlike the ones used, for example, in scatterplot. Text is a different kind of object that gets somehow redraw if you resize the window or change the ratio. By having a stabilized window you can ask the coordinates of the bounding box in plot units and build your colored text that way:

a = "all unicorns poop rainbows ! ! !".split()
c = ['red', 'orange', 'brown', 'green', 'blue', 'purple', 'black']

f = plt.figure(figsize=(4,3), dpi=120)
ax = f.add_subplot(111)

r = f.canvas.get_renderer()
space = 0.1
w = 0.5
counter = 0
for i in a:
    t = ax.text(w, 1.2, a[counter],color=c[counter],fontsize=12,ha='left')
    transf = ax.transData.inverted()
    bb = t.get_window_extent(renderer=f.canvas.renderer)
    bb = bb.transformed(transf)
    w = w + bb.xmax-bb.xmin + space
    counter = counter + 1
plt.ylim(0.5,2.5)
plt.xlim(0.6,1.6)
plt.show()

, which results in:

Matplotlib colored text

This, however, is still not ideal since you need to keep controlling the size of your plot axis to obtain the correct spaces between words. This is somewhat arbitrary but if you manage to do your program with such a control it's feasible to use plot units to achieve your intended purpose.

ORIGINAL POST:

plt. is just the call to the library. In truth you are creating an instance of plt.figure in the global scope (so it can be seen in locally in the function). Due to this you are overwriting the figure because you use the same name for the variable (so it's just one single instance in the end). To solve this try controlling the names of your figure instances. For example:

import matplotlib.pyplot as plt
#%matplotlib inline
from matplotlib import transforms

fig = plt.figure(figsize=(4,3), dpi=300)
#plt.show(fig)

def rainbow_text(x,y,ls,lc,**kw):

    t = plt.gca().transData
    figlocal = plt.gcf()

    #horizontal version
    for s,c in zip(ls,lc):
        text = plt.text(x,y," "+s+" ",color=c, transform=t, **kw)
        text.draw(figlocal.canvas.get_renderer())
        ex = text.get_window_extent()
        t = transforms.offset_copy(text._transform, x=ex.width, units='dots')

    plt.show(figlocal) #plt.show((figlocal,fig))

#plt.figure()
rainbow_text(0.5,0.5,"all unicorns poop rainbows ! ! !".split(), 
        ['red', 'orange', 'brown', 'green', 'blue', 'purple', 'black'],
        size=40,)

I've commented several instructions but notice I give a different name for the figure local to the function (figlocal). Also notice that in my examples of show I control directly which figure should be shown.

As for your other questions notice you can use other units as can be seen in the function documentation:

Return a new transform with an added offset.
      args:
        trans is any transform
      kwargs:
        fig is the current figure; it can be None if units are 'dots'
        x, y give the offset
        units is 'inches', 'points' or 'dots'

EDIT: Apparently there's some kind of problem with the extents of the bounding box for text that does not give the correct width of the word and thus the space between words is not stable. My advise is to use the latex functionality of Matplotlib to write the colors in the same string (so only one call of plt.text). You can do it like this:

import matplotlib
import matplotlib.pyplot as plt
matplotlib.use('pgf')
from matplotlib import rc

rc('text',usetex=True)
rc('text.latex', preamble=r'\usepackage{color}')

a = "all unicorns poop rainbows ! ! !".split()
c = ['red', 'orange', 'brown', 'green', 'blue', 'purple', 'black']
st = ''
for i in range(len(a)):
    st = st + r'\textcolor{'+c[i]+'}{'+a[i]+'}'
plt.text(0.5,0.5,st)
plt.show()

This however is not an ideal solution. The reason is that you need to have Latex installed, including the necessary packages (notice I'm using the color package). Take a look at Yann answer in this question: Partial coloring of text in matplotlib

Community
  • 1
  • 1
armatita
  • 12,825
  • 8
  • 48
  • 49
  • Thanks, @armatita but how do I control (really, regularize) the width for a given unit? I ask because these examples will produce inconsistent spacing between words, which is what I'm ultimately trying to fix (regarding second part of my question). – Dance Party2 Mar 30 '16 at 19:40
  • 1
    @Dance Party2 If that transform is not doing stuff the correct way just forget the transform. Define your font, alignment and plot each word using coordinates. Notice you are already obtaining the bounding box of the text (text.get_window_extent()) so you can declare that a space is, for example, 0.2 meaning word2 should be positioned in word1_width + 0.2 (and so on for word3) With this you will be building a kind of transform yourself. The thing is I'm not sure what that transform you are using is actually doing. Also: http://stackoverflow.com/questions/5320205/matplotlib-text-dimensions – armatita Mar 30 '16 at 21:11
  • Yes! Thank you so much! This will save me HOURS of time. If you want to post that as an answer, I'll gladly accept it. – Dance Party2 Apr 01 '16 at 13:29
  • One more thing, how can I get it to display the width in inches (1/72)? – Dance Party2 Apr 01 '16 at 13:42
  • 1
    @Dance Party2 I've tried doing a small example of my suggestion but the width of the bounding boxes of words is consistently over estimating the size of the word and thus giving bad results. This seems like a bug or at least some strange behavior in matplotlib (Sorry I was not expecting it, didn't mean to mislead you). I've edited my post with a suggestion. It's not ideal but it should work (if you are willing to use Latex together with matplotlib). – armatita Apr 02 '16 at 19:37
  • 1
    @armatita: Thanks for looking more into it. I am not allergic to LaTex, so to speak, but I'll have to look up how to install the packages (I'm using the Jupyter notebook via Anaconda). Is it a series of 'pip install's? I also use custom fonts from my computer's library. Will that work via MatPlotLib or will it need to be through LaTex? – Dance Party Apr 03 '16 at 17:31
  • 1
    @Dance Party2 LaTeX is an independent package from python itself and I doubt you can use anything like pip to install it. Try checking matplotlib channels for help (http://matplotlib.org/faq/troubleshooting_faq.html#getting-help) and put a question in the mailing list perhaps. It might also happen that matplotlib itself is not adequate to what you are trying to achieve. What is the final purpose for this code? – armatita Apr 03 '16 at 17:54
  • @armatita: My intention is to create a dashboard compose of bar charts for which bars have 2 different colors. Instead of creating a separate legend, I tend to integrate the legend into the title. For example, if the blue bars represent group 1 and the yellow bars represent group 2, the title might say "Yearly progress of group 1 vs group 2" where the text for group 1 is blue and the text for group 2 is yellow (and other text is black). – Dance Party2 Apr 04 '16 at 14:17
  • I'm taking a different approach based on something that I think might work. I've posted a new question here: http://stackoverflow.com/questions/36434152/matplotlib-get-left-and-right-coordinates-from-bbox-and-convert-to-points – Dance Party2 Apr 05 '16 at 18:24
  • 1
    @Dance Party2 You kind of gave me an idea. Let me try to edit my answer to this question before giving a try to the other. – armatita Apr 05 '16 at 19:31
  • 1
    @Dance Party2 I've added another piece of code to my answer. I still don't think it's the solution you are looking for but might be useful for anyone who comes across this question. – armatita Apr 05 '16 at 19:43
0

@armatita: I think your answer actually does what I need. I thought I needed display coordinates instead, but it looks like I can just use axis 1 coordinates, if that's what this is (I'm planning on using multiple axes via subplot2grid). Here's an example:

import matplotlib.pyplot as plt
%matplotlib inline
dpi=300
f_width=4
f_height=3
f = plt.figure(figsize=(f_width,f_height), dpi=dpi)

ax1 = plt.subplot2grid((100,115), (0,0), rowspan=95, colspan=25)
ax2 = plt.subplot2grid((100,115), (0,30), rowspan=95, colspan=20)
ax3 = plt.subplot2grid((100,115), (0,55), rowspan=95, colspan=35)
ax4 = plt.subplot2grid((100,115), (0,95), rowspan=95, colspan=20)
r = f.canvas.get_renderer()
t = ax1.text(.5, 1.1, 'a lot of text here',fontsize=12,ha='left')
space=0.1
w=.5

transf = ax1.transData.inverted()
bb = t.get_window_extent(renderer=f.canvas.renderer)
bb = bb.transformed(transf)

e = ax1.text(.5+bb.width+space, 1.1, 'text',fontsize=12,ha='left')

print(bb)
plt.show()

I'm not sure what you mean about controlling the axis size, though. Are you referring to using the code in different environments or exporting the image in different sizes? I plan on having the image used in the same environment and in the same size (per instance of using this approach), so I think it will be okay. Does my logic make sense? I have a weak grasp on what's really going on, so I hope so. I would use it with a function (via splitting the text) like you did, but there are cases where I need to split on other characters (i.e. when a word in parentheses should be colored, but not the parentheses). Maybe I can just put a delimiter in there like ','? I think I need a different form of .split() because it didn't work when I tried it. At any rate, if I can implement this across all of my charts, it will save me countless hours. Thank you so much!

Dance Party2
  • 7,214
  • 17
  • 59
  • 106
  • 1
    You are welcome. You've probably noticed that when you resize a matplolib plot the axis are redraw. This transformation does not follow for text (or it does but not in a way useful to you). So when you do a plot (with whatever customization works for you) and than try to resize it looses those precious equal spaces you are trying to maintain. This is the reason why I think it's not an ideal solution but if will have absolute control about the size of your plots than it should do the trick. – armatita Apr 06 '16 at 17:12
  • That's okay. I think I see what you mean. When I make the width 10 inches instead of 4, the spacing (which I set at 0.1) increases. For a given plot with a given width, It won't be much for me to adjust that value until I get something that works. – Dance Party2 Apr 06 '16 at 18:11
  • I found that if I split a string on commas like this, I don't have to worry about spacing because I can just include the spaces between delimiters: a = str("Group 1 ,vs. ,Group 2 (,sub1,) and (,sub2,)").split(',')...colors: c = ['black','red','black','green','black','blue','black'] – Dance Party2 Apr 07 '16 at 13:27
  • @armatita: This is so incredibly useful. If I had more time, I would try to create a method from it and recommend it on GitHub. Is that of interest to you? – Dance Party2 Apr 07 '16 at 13:33
  • I personally don't have a use for it but if you see it can be a useful matplotlib protocol why not. The developers will, however, probably want a structure similar to the transforms that already exist on matplotlib (perhaps there's already one obscure badly documented one doing just that. Wouldn't that be ironic?!). Best of luck. – armatita Apr 07 '16 at 13:37
  • Okay, if I can figure out how to get it into the transform structure (and cannot find any obscure one that already exists) then I'll pursue it for sure. That would be very ironic, indeed. – Dance Party2 Apr 07 '16 at 13:53
0

Here is an example where there are 2 plots and 2 instances of using the function for posterity:

import matplotlib.pyplot as plt
%matplotlib inline
dpi=300
f_width=4
f_height=3
f = plt.figure(figsize=(f_width,f_height), dpi=dpi)

ax1 = plt.subplot2grid((100,60), (0,0), rowspan=95, colspan=30)
ax2 = plt.subplot2grid((100,60), (0,30), rowspan=95, colspan=30)

f=f #Name for figure
string = str("Group 1 ,vs. ,Group 2 (,sub1,) and (,sub2,)").split(',')
color = ['black','red','black','green','black','blue','black']
xpos = .5
ypos = 1.2
axis=ax1
#No need to include space if incuded between delimiters above
#space = 0.1 
def colortext(f,string,color,xpos,ypos,axis):
#f=figure object name (i.e. fig, f, figure)
    r = f.canvas.get_renderer()
    counter = 0
    for i in string:
        t = axis.text(xpos, ypos, string[counter],color=color[counter],fontsize=12,ha='left')
        transf = axis.transData.inverted()
        bb = t.get_window_extent(renderer=f.canvas.renderer)
        bb = bb.transformed(transf)
        xpos = xpos + bb.xmax-bb.xmin
        counter = counter + 1
colortext(f,string,color,xpos,ypos,axis)

string2 = str("Group 1 part 2 ,vs. ,Group 2 (,sub1,) and (,sub2,)").split(',')
ypos2=1.1
colortext(f,string2,color,xpos,ypos2,axis)

plt.show()
Dance Party2
  • 7,214
  • 17
  • 59
  • 106