Skip to content

Question about 'text' (add lm summary to a plot)

19 messages · Christoph Buser, Marc Schwartz (via MN), Thomas Lumley +2 more

#
I would like to annotate my plot with a little box containing the slope,
intercept and R^2 of a lm on the data.

I would like it to look like...

 +----------------------------+
 | Slope     :   3.45 +- 0.34 |
 | Intercept : -10.43 +- 1.42 |
 | R^2       :   0.78         |
 +----------------------------+

However I can't make anything this neat, and I can't find out how to
combine this with symbols for R^2 / +- (plus minus).

Below is my best attempt (which is franky quite pour). Can anyone
improve on the below?

Specifically, 

aligned text and numbers, 
aligned decimal places, 
symbol for R^2 in the text (expression(R^2) seems to fail with
'paste') and +- 



Cheers,
Dan.


dat.lm <- lm(dat$AVG_CH_PAIRS ~ dat$CHAINS)

abline(coef(dat.lm),lty=2,lwd=1.5)


dat.lm.sum <- summary(dat.lm)
dat.lm.sum

attributes(dat.lm.sum)

my.text.1 <-
  paste("Slope : ",     round(dat.lm.sum$coefficients[2],2),
        "+/-",          round(dat.lm.sum$coefficients[4],2))

my.text.2 <-
  paste("Intercept : ", round(dat.lm.sum$coefficients[1],2),
        "+/-",          round(dat.lm.sum$coefficients[3],2))

my.text.3 <-
  paste("R^2 : ",       round(dat.lm.sum$r.squared,2))

my.text.1
my.text.2
my.text.3


## Add legend
text(x=3,
     y=300,
     paste(my.text.1,
           my.text.2,
           my.text.3,
           sep="\n"),
     adj=c(0,0),
     cex=1)
#
Dear Dan

I can only help you with your third problem, expression and
paste. You can use:

plot(1:5,1:5, type = "n")
text(2,4,expression(paste("Slope : ", 3.45%+-%0.34, sep = "")), pos = 4)
text(2,3.8,expression(paste("Intercept : ", -10.43%+-%1.42)), pos = 4)
text(2,3.6,expression(paste(R^2,": ", "0.78", sep = "")), pos = 4)

I do not have an elegant solution for the alignment.

Regards,

Christoph Buser

--------------------------------------------------------------
Christoph Buser <buser at stat.math.ethz.ch>
Seminar fuer Statistik, LEO C13
ETH (Federal Inst. Technology)	8092 Zurich	 SWITZERLAND
phone: x-41-44-632-4673		fax: 632-1228
http://stat.ethz.ch/~buser/
--------------------------------------------------------------


Dan Bolser writes:
 > 
 > I would like to annotate my plot with a little box containing the slope,
 > intercept and R^2 of a lm on the data.
 > 
 > I would like it to look like...
 > 
 >  +----------------------------+
 >  | Slope     :   3.45 +- 0.34 |
 >  | Intercept : -10.43 +- 1.42 |
 >  | R^2       :   0.78         |
 >  +----------------------------+
 > 
 > However I can't make anything this neat, and I can't find out how to
 > combine this with symbols for R^2 / +- (plus minus).
 > 
 > Below is my best attempt (which is franky quite pour). Can anyone
 > improve on the below?
 > 
 > Specifically, 
 > 
 > aligned text and numbers, 
 > aligned decimal places, 
 > symbol for R^2 in the text (expression(R^2) seems to fail with
 > 'paste') and +- 
 > 
 > 
 > 
 > Cheers,
 > Dan.
 > 
 > 
 > dat.lm <- lm(dat$AVG_CH_PAIRS ~ dat$CHAINS)
 > 
 > abline(coef(dat.lm),lty=2,lwd=1.5)
 > 
 > 
 > dat.lm.sum <- summary(dat.lm)
 > dat.lm.sum
 > 
 > attributes(dat.lm.sum)
 > 
 > my.text.1 <-
 >   paste("Slope : ",     round(dat.lm.sum$coefficients[2],2),
 >         "+/-",          round(dat.lm.sum$coefficients[4],2))
 > 
 > my.text.2 <-
 >   paste("Intercept : ", round(dat.lm.sum$coefficients[1],2),
 >         "+/-",          round(dat.lm.sum$coefficients[3],2))
 > 
 > my.text.3 <-
 >   paste("R^2 : ",       round(dat.lm.sum$r.squared,2))
 > 
 > my.text.1
 > my.text.2
 > my.text.3
 > 
 > 
 > ## Add legend
 > text(x=3,
 >      y=300,
 >      paste(my.text.1,
 >            my.text.2,
 >            my.text.3,
 >            sep="\n"),
 >      adj=c(0,0),
 >      cex=1)
 > 
 > ______________________________________________
 > R-help at stat.math.ethz.ch mailing list
 > https://stat.ethz.ch/mailman/listinfo/r-help
 > PLEASE do read the posting guide! http://www.R-project.org/posting-guide.html
#
On Thu, 21 Jul 2005, Christoph Buser wrote:

            
Cheers for this.

I was trying to get it to work, but the problem is that I need to replace
the values above with variables, from the following code...


dat.lm <- lm(dat$AVG_CH_PAIRS ~ dat$CHAINS)
dat.lm.sum <- summary(dat.lm)

my.slope.1 <- round(dat.lm.sum$coefficients[2],2)
my.slope.2 <- round(dat.lm.sum$coefficients[4],2)

my.inter.1 <- round(dat.lm.sum$coefficients[1],2)
my.inter.2 <- round(dat.lm.sum$coefficients[3],2)

my.Rsqua.1 <- round(dat.lm.sum$r.squared,2)


Anything I try results in either the words 'paste("Slope:", my.slope.1,
%+-%my.slope.2,sep="")' being written to the plot, or just
'my.slope.1+-my.slope2' (where the +- is correctly written).

I want to script it up and write all three lines to the plot with
'sep="\n"', rather than deciding three different heights.
Thanks very much for what you gave, its a good start for me to figure out 
how I am supposed to be telling R what to do!

Any way to just get fixed width fonts with text? (for the alignment
problem)


Cheers,
Dan.
#
Use bquote instead of expression, e.g.

trees.lm <- lm(Volume ~ Girth, trees)
trees.sm <- summary(trees.lm)
trees.co <- round(trees.sm$coefficients,2)
trees.rsq <- round(trees.sm$r.squared,2)

plot(Volume ~ Girth, trees)

text(10,60, bquote(Intercept : .(trees.co[1,1])%+-%.(trees.co[1,2])), pos = 4)
text(10,57, bquote(Slope : .(trees.co[2,1])%+-%.(trees.co[2,2])), pos = 4)
text(10,54,bquote(R^2 : .(trees.rsq)), pos = 4)
On 7/21/05, Dan Bolser <dmb at mrc-dunn.cam.ac.uk> wrote:
#
[Note: the initial posts have been re-arranged to attempt to maintain
the flow from top to bottom]

        
On Thu, 2005-07-21 at 19:55 +0100, Dan Bolser wrote:
Dan,

Here is one approach. It may not be the best, but it gets the job done.
You can certainly take this and encapsulate it in a function to automate
the text/box placement and to pass values as arguments.

A couple of quick concepts:

1. As far as I know, plotmath cannot do multiple lines, so each line in
your box needs to be done separately.

2. The horizontal alignment is a bit problematic when using expression()
or bquote() since I don't believe that multiple spaces are honored as
such after parsing. Thus I break up each component (label, ":" and
values) into separate text() calls. The labels are left justified.

3. The alignment for the numeric values are done with right
justification. So, as long as you use a consistent number of decimals in
the value outputs (2 here), you should be OK. This means you might need
to use formatC() or sprintf() to control the numeric output values on
either side of the +/- sign.

4. In the variable replacement, note the use of substitute() and the
list of x and y arguments as replacement values in the expressions.



# Set your values
my.slope.1 <- 3.45
my.slope.2 <- 0.34

my.inter.1 <- -10.43
my.inter.2 <- 1.42

my.Rsqua <- 0.78


# Create the initial plot as per Christoph's post
plot(1:5, 1:5, type = "n")


#-------------------------------------
# Do the Slope
#-------------------------------------

text(1, 4.5,  "Slope", pos = 4)
text(2, 4.5, ":")
text(3, 4.5, substitute(x %+-% y, 
                        list(x = my.slope.1, 
                             y = my.slope.2)),
     pos = 2)


#-------------------------------------
# Do the Intercept
#-------------------------------------

text(1, 4.25, "Intercept", pos = 4)
text(2, 4.25, ":")
text(3, 4.25, substitute(x %+-% y, 
                         list(x = my.inter.1, 
                              y = my.inter.2)),
     pos = 2)


#-------------------------------------
# Do R^2
#-------------------------------------

text(1, 4.0, expression(R^2), pos = 4)
text(2, 4.0, ":")
text(3, 4.0,  substitute(x, list(x = my.Rsqua)),
     pos = 2)


#-------------------------------------
# Do the Box
#-------------------------------------

rect(1, 3.75, 3, 4.75)


You can adjust the x,y coordinates for the various text elements as you
may require and can also calculate them based upon the xlim, ylim of
your actual plot. You can also modify the 'cex' argument to text() for
adjusting the sizes of the fonts in use.

BTW, to use a monospaced font, you can set par(family = "mono").
See ?par for more information.

HTH,

Marc Schwartz
#
On Thu, 21 Jul 2005, Marc Schwartz (via MN) wrote:

            
I have been trying many different combinations of the suggestions so
far...

bquote, substitute, expression, paste, etc.

Also I was trying to use the 'expression' part of the 'legend' function to
make thing easier (make new lines, make columns, make rectangles)...

So far all in vain. 

I am sure a fairly neat solution is possible using 'legend' and
'expression', but I cant get a legend which contains a variable
substituted value *and* a special symbol.

Can anyone show me an example of this simple step, and then I can try to
build on that.


Thanks all for suggestions, its one of those 'it seems so easy' problems
(for me).

P.S. Whats with the documentation for bquote?
#
Try as.expression(bquote(...whatever...))
On 7/22/05, Dan Bolser <dmb at mrc-dunn.cam.ac.uk> wrote:
#
On Fri, 22 Jul 2005, Gabor Grothendieck wrote:

            
Sob, wimper, etc.


my.slope.1 <-   3.22
my.slope.2 <-   0.13
my.inter.1 <- -10.66
my.inter.2 <-   1.96
my.Rsqua.1 <-   0.97



text(2,5,
  paste("Slope:  ",
        as.expression(bquote(.(my.slope.1)%+-%.(my.slope.2)))))


That puts the text and variable together, but not the symbol, which is
printed as '%+-%'

The following is nearly right, but wrong enough it makes my first attempt
look reasonable...


# Create the initial plot as per Christoph's post
plot(1:5, 1:5, type = "n")

legend(x=1,
       y=4.5,
       bg='white',
       ncol=3,
       c("Slope","Intercept",expression(R^2),
         ":",":",":",
         bquote( .(my.slope.1)%+-%.(my.slope.2)),
         bquote( .(my.inter.1)%+-%.(my.inter.2)),
         my.Rsqua.1
         )
       )

Anyone got any ideas on how to fix the above?

Namely 1) right align numbers, 2) remove excessive white space.

I like the above because it dosn't require me to calculate exactly where
to put each piece of text.


I just want to annotate a plot :(
#
On Fri, 22 Jul 2005, Dan Bolser wrote:

            
a<-7
   plot(1)
   legend("topleft",legend=do.call("expression",
                      list(bquote(alpha==.(a)),bquote(alpha^2+1==.(a^2+1)))))

works for me.  The trick is getting the inner calls to bquote 
evaluated, since expression doesn't evaluate its argument.


 	-thomas
#
On 7/22/05, Dan Bolser <dmb at mrc-dunn.cam.ac.uk> wrote:
Thomas Lumley has already answered with a solution but I 
thought I would address this specific construct.  The problem is
that the above converts it back to character.  You want to put
everything in the bquote so it stays an expression:

my.slope.1 <-   3.22
my.slope.2 <-   0.13

plot(1:5)
text(2,5, as.expression(bquote(Slope: .(my.slope.1)%+-%.(my.slope.2))))
#
On 7/22/05, Thomas Lumley <tlumley at u.washington.edu> wrote:
I think legend accepts a list argument directly so that could be
simplified to just:

  a<-7
  plot(1)
  L <- list(bquote(alpha==.(a)),bquote(alpha^2+1==.(a^2+1)))
  legend("topleft",legend=L)

The same comment seems to apply to my prior suggestion about
as.expression(bquote(...)), namely that one can just write the
following as text also supports a list argument:


my.slope.1 <-   3.22
my.slope.2 <-   0.13
my.inter.1 <- -10.66
my.inter.2 <-   1.96

plot(1:5)
L <- list(
  "Intercept:", bquote(.(my.inter.1)%+-%.(my.inter.2)),
  "Slope:", bquote(.(my.slope.1)%+-%.(my.slope.2))
)
text(2, c(4,4,4.25,4.25), L, pos = c(2,4,2,4))
#
There was an error in the second example.  It seems one can use
list in the way wanted in legend but not in text so for text one would
have to use the do.call approach of Thomas or sapply as shown here:

my.slope.1 <-   3.22
my.slope.2 <-   0.13
my.inter.1 <- -10.66
my.inter.2 <-   1.96

plot(1:5)
L <- list(
  "Intercept:", bquote(.(my.inter.1)%+-%.(my.inter.2)),
  "Slope:", bquote(.(my.slope.1)%+-%.(my.slope.2))
)
text(2, c(4,4,4.25,4.25), sapply(L, as.expression), pos = c(2,4,2,4))
On 7/22/05, Gabor Grothendieck <ggrothendieck at gmail.com> wrote:
#
On Fri, 22 Jul 2005, Gabor Grothendieck wrote:
Except that it wouldn't then work: the mathematical stuff comes out as 
text.
And this doesn't work either: you end up with %+-% rather than the 
plus-or-minus symbol.

The reason I gave the do.call() version is that I had tried these simpler 
versions and they didn't work.

 	-thomas
#
You are right.   One would have to use do.call as you did
or the sapply method of one of my previous posts:

a <- 7
plot(1)
L <- list(bquote(alpha==.(a)),bquote(alpha^2+1==.(a^2+1)))
legend("topleft",legend=sapply(L, as.expression))
On 7/22/05, Thomas Lumley <tlumley at u.washington.edu> wrote:
#
Ok guys,

So I played around with this a bit, going back to Dan's original
requirements and using Thomas' do.call() approach with legend(). Gabor's
approach using sapply() will also work here. I have the following:

# Note the leading spaces here for alignment in the table
# This could be automated with formatC() or sprintf()
my.slope.1 <-  "  3.22"
my.slope.2 <-  "0.13"
my.inter.1 <-  "-10.66"
my.inter.2 <-  "1.96"
my.Rsqua <-    "  0.97"

plot(1:5)

L <- list("Intercept:",
          "Slope:",
          bquote(paste(R^2, ":")),
          bquote(.(my.inter.1) %+-% .(my.inter.2)),
          bquote(.(my.slope.1) %+-% .(my.slope.2)),
          bquote(.(my.Rsqua)))

par(family = "mono")

legend("topleft", legend = do.call("expression", L), ncol = 2)



Note however, that while using the mono font helps with vertical
alignment of numbers, the +/- sign still comes out in the default font,
which is bold[er] than the text.

If one uses the default font, which is variable spaced, it is
problematic to get the proper alignment for the numbers. I even tried
using phantom(), but that didn't quite get it, since the spacing is
variable, as opposed to LaTeX's mono numeric spacing with default fonts.

Also, note that I am only using two columns, rather than three, since
trying to place the ":" as a middle column results in spacing that is
too wide, given that the text.width argument is a scalar and is set to
the maximum width of the character vectors.

Note also that even with mono spaced fonts, the exponent in R^2 is still
horizontally smaller than the other characters. Thus, spacing on that
line may also be affected depending upon what else one might attempt.

Not sure where else to go from here.

HTH,

Marc Schwartz
On Fri, 2005-07-22 at 14:01 -0400, Gabor Grothendieck wrote:
#
If one modifies legend by adding a vfont=c("serif", "plain") argument 
to the call to text (which is within the function text2 that is defined 
within legend) then one can do this:

my.slope.1 <-  "  3.22"
my.slope.2 <-  "0.13"
my.inter.1 <-  "-10.66"
my.inter.2 <-  "1.96"
my.Rsqua <-    "0.97"

plot(1:5)

tt <- c("Intercept:", "Slope:", "R\\S2:", 
	paste(my.inter.1, "\\+-", my.inter.2),
	paste(my.slope.2, "\\+-", my.slope.2), my.Rsqua)

# modified legend to accept vfont
my.legend("topleft", legend = tt, ncol = 2, adj = 0.1, vfont =
c("serif", "plain"))

which only requires character manipulation -- no plotmath,
expression or bquote; however, unfortunately it still does
not line up.  It undoubtedly would be possible to fix up
legend so that when used with Hershey fonts everything
lines up as expected.
#
On Fri, 22 Jul 2005, Marc Schwartz (via MN) wrote:

            
Ahhhhhh... So lovely! Thank you all so much!

I made a couple of tweeks to improve the overall appearance, using
"x.intersp = 0.1" tightens up the overall appearance, and using
"pch=c('','','',':',':',':')" adds the (aligned!) colons.

Here is the beauty...



my.slope.1 <-  "   3.22"
my.slope.2 <-  "0.13"
my.inter.1 <-  " -10.66"
my.inter.2 <-  "1.96"
my.Rsqua <-    "   0.97"

plot(1:5)

L <- list("Intercept",
          "Slope    ",
          bquote(paste(R^2)),
          bquote(.(my.inter.1) %+-% .(my.inter.2)),
          bquote(.(my.slope.1) %+-% .(my.slope.2)),
          bquote(.(my.Rsqua)))

par(family = "mono")

legend("topleft", #inset=-1,
       legend = do.call("expression", L),
       bg='white',
       ncol = 2,
       pch=c('','','',':',':',':'),
       x.intersp = 0.1,
       title="Yay! Thank You!"
       )


However (the final gripe ;) it seems 'inset=' dosn't work. Setting this to
anything (including the default) seems to surpress the legend without
error. But hey!

Thanks again,
#
On Sat, 23 Jul 2005, Dan Bolser wrote:

            
Oh....

Error in strwidth(legend, units = "user", cex = cex) :
        family mono not included in PostScript device
Execution halted
#
Here are a few tweaks.  We form a matrix and use round 
and format to automatically get the right number of 
decimals and widths right.  We have added extra digits
to show that they do not affect the result. Also the paste in the 
R^2 line was eliminated.  

nn <- matrix(c(-10.661, 1.961,                    # intercept
                 3.221, 0.131,                    # slope
                 0.971, NA), 3, 2, byrow = TRUE) # Rsquared
nnf <- apply(round(nn,2), 2, format)

plot(1:5)

L <- list("Intercept",
         "Slope",
         bquote(R^2),
         bquote(.(nnf[1,1]) %+-% .(nnf[1,2])),
         bquote(.(nnf[2,1]) %+-% .(nnf[2,2])),
         bquote(.(nnf[3,1])))

par(family = "mono")

legend("topleft", #inset=-1,
      legend = do.call("expression", L),
      bg='white',
      ncol = 2,
      pch=c('','','',':',':',':'),
      x.intersp = 0.4,
      title="Yay! Thank You!"
      )
On 7/23/05, Dan Bolser <dmb at mrc-dunn.cam.ac.uk> wrote: