EMACS as an end-user GUI to an R process

by Mortimer B. Cladwell III

How do you make available to your computer illiterate colleagues a useful R script that you have developed? As part of the algorithm certain variables may need to be set, a working directory identified, a file selected, etc. In the past I have blocked off a section of the script at the very beginning, where, using comments, I instruct the user how to set certain variables, how to copy and paste the entire script into Rgui, and interpret the results (e.g. find any data files that may be created etc.). Though this works, it is kind of a kludge, and may scare off more timid users. What is needed is a simple interface with text boxes, drop downs, radio buttons etc. Not only does this make it easier for the end-user, but allows the program to validate and/or restrict selections.

A simple interface is provided by the EMACS widget library. Though sparse in the variety of widgets supplied, those provided will satisfy about 99% of my needs. The added benefit is that R will interface directly with EMACS through ESS.

  1. Install components
  2. Startup
  3. Mode definition
  4. Main user interface file
  5. R script
  6. A HELP form
  7. Screenshot




Install components

Look at the
software setup page and follow directions for installing R, EMACS, and ESS in that order.

R example (reg) code

Optionally install the "reg" (R e.g.) software that will be discussed in the tutorial. Install in your emacs site-lisp directory e.g. c:\emacs-23.2\site-lisp\reg\. Should you install elsewhere, you will need to change the home-dir variable in the reg-main form, as well as add your installation directory to the EMACS load path. Critical code components are part of the tutorial, so you do not need to install the sample code if you are reading to understand only.

[Contents]


Startup

Start EMACS. If ESS and R have been properly installed, you should see:

"Finding all versions of R on your system..."

in the minibuffer during startup. Launch R with the command "M-x R". You should be able to enter R commands and see the output.

To make working with R more convenient, I include the following in my .emacs file:

;;__________________________________________________________________________
;;;; R  Setup
(require 'ess-site)

(defun my-ess-mode ()
  (interactive)
  (cond ((string= "R" ess-dialect)
	 (message "my-r-mode")
	 ;; customization for ESS[R] here
	 (define-key ess-mode-map '[f5] 'ess-eval-region-and-go))))

;and then hook this into the appropriate place:

(add-hook 'ess-mode-hook 'my-ess-mode)    

;;__________________________________________________________________________
;;;;    Programming - Elisp

(add-hook 'emacs-lisp-mode-hook
	  '(lambda ()
	     (interactive)
	     (require 'eldoc)
	     (turn-on-eldoc-mode)
	     (define-key emacs-lisp-mode-map [f5] 'eval-region)
	     (define-key emacs-lisp-mode-map [f4] 'eval-buffer)))

This allows me to use F5 to evaluate a region whether I am in R or Lisp. I also define the following function key shortcuts to streamline the loading of the main file and interface. If you decide not to define these keys, you will need to load the files manually and activate the main interface with "M-x reg-mode".

(global-set-key (kbd "<F6>") 'load-and-run-fave-file)

(defun load-and-run-fave-file()
  (interactive)
  (load "reg-mode.el"))

With the above key definition I can use F6 to load the example. If you don't want to sacrifice your F6 key, you can manually invoke the commands in the minibuffer:

M-x reg-mode

[Contents]


Mode definition

For a good discussion of the details of creating a major mode refer to Writing GNU EMACS Extensions by Bob Glickstein. The advantage of utilizing a major mode is that while you are in the mode specific buffer, you can have specific menu items and key chords that are in effect only while in that buffer. The commands for creating the mode along with helper functions and load statements can go into a file called reg-mode.el. My file also includes code for working with the widget library:
;primer-db.el sets up mode and provides widget interface and menu map
;;Must use widget keymap to have functional radio buttons

(require 'widget)
(require 'derived)
(eval-when-compile
       (require 'wid-edit))
(require 'cl)
(require 'info); needed for hot links to work

Define a hook variable and menu map. Add elements to the menu map. Note that each menu item is defined by a dotted pair, the car being the text in the menu and the cdr being an associated function. Often that function will launch a form to be used to enter data.
(defconst home-dir "c:/emacs-23.2/site-lisp/reg/" 
  "Working directory.") 


(defvar reg-mode-hook nil
  "*List of functions to call when entering reg mode*")

(defvar reg-menu-map nil
  "Menu for reg mode.")

(defvar r-results-buffer "*R-results*"
  "A buffer separate from *R* to hold results")

(defvar transferred-variable nil
  "A variable, the value of which will be transferred into the R process")

(if reg-menu-map
    nil
  (setq reg-menu-map (make-sparse-keymap "reg"))
  (define-key reg-menu-map [reg-main]
    '("Main form" . reg-main))
  (define-key reg-menu-map [reg-help]
    '("Help with R example" . reg-help)))


(defvar reg-mode-map nil
  "Keybindings for reg mode")

(if reg-mode-map
    nil
  (setq reg-mode-map (copy-keymap widget-keymap)))

(define-key reg-mode-map [menu-bar reg]
  (cons "reg" reg-menu-map))

(define-derived-mode reg-mode text-mode "reg"
  "Major mode for the R example code.
Special commands:
\\{reg-mode-map}")


(load "reg-forms")

Load any library files. I also define an ESS hook function that allows me to configure ESS. I want R to start in the background without any prompts. You will have to change the ess-directory if yours is different. I use format to format R commands and then submit to R via ESS using the ess-execute command. Comint commands are used to manipulate the processes. I also create here the buffer r-results-buffer that I will work with later.
 

(defun reg-ESS-hook ()
  (setf inferior-R-args "--no-restore --no-save")
  (setf ess-ask-for-ess-directory nil)    ;you can't be prompting the user here because this is performed in the background
  (setf ess-execute-in-process-buffer 1)  ;directs ess-execute commands to R window

  ;;all files in R loaded from the home directory c:/emacs-23.2/site-lisp/reg/
  (setf ess-directory home-dir))

(add-hook 'ess-pre-run-hook 'reg-ESS-hook)
(R) ;start R in its own buffer
;initialize R e.g. load the init file which will set variables, load libraries

(ess-execute  "rm(list=ls(all=TRUE))")
(ess-execute ( format "working.dir <-\"%s\"" home-dir))
(ess-execute (format "source\(\"reg-init-r.R\"\)"  ))

(get-buffer-create r-results-buffer)

(provide 'reg);provide the major mode "reg"
(reg-main); launches the main user interface
;;end reg mode
[Contents]

Main user interface file

This is the form the user will see when the reg software is started (in reg-main.el).

;;reg-forms.el
;;Must use widget keymap to have functional radio buttons
;;;;;

(defvar data-set nil)
(defvar reg-operation nil)

(defvar r-script "primer-search.r" 
  "File containing R code.") 


(defun reg-main()
  (interactive)
(setq buffer (get-buffer-create "*R-example-form*"))     
(switch-to-buffer buffer)     
      
(let ((inhibit-read-only t))     
  (erase-buffer))     
(remove-overlays)     
(reg-mode)     

Note that I invoke reg-mode, which allows me to provide a menu item with options that will help users. Next are the interface widgets which will be used to collect values for variables and then launch methods that will perfom the data manipulations of interest. I use boolean variables to determine which second dropdown to present to the user, depending on selections made in the first dropdown. See the sqlite tutorial for an example of how to populate a dropdown on the fly with database content using a macro.


(widget-insert (propertize "Example form for interaction with an R process" 'face '(:family "tahoma" :height 150 :underline t)))

(widget-insert "\n\n")     
(widget-insert "\n\n")
     
(widget-create 'menu-choice     
     		 :tag "Select data set"     
     		 :value data-set     
     		 :help-echo "Click to view dropdown"     
                 :notify (lambda (widget &rest ignore)     
                           (select-data-set (widget-value widget)))
	 
		 '(item "PlantGrowth")      
		 '(item "Indometh"))     
          
(widget-insert "\n\n")     

(if (equal data-set "Indometh") 
    (widget-create 'menu-choice     
		   :tag "Select operation"     
		   :value reg-operation     
		   :help-echo "Click to view dropdown"     
		   :notify (lambda (widget &rest ignore)     
			     (select-operation (widget-value widget)))
		   '(item "reshape")  
		   '(item "select")))     
 
(if (equal data-set "PlantGrowth") 
    (widget-create 'menu-choice     
		   :tag "Select operation"     
		   :value reg-operation     
		   :help-echo "Click to view dropdown"     
		   :notify (lambda (widget &rest ignore)     
			     (select-operation (widget-value widget)))
		   '(item "plot")  
		   '(item "write")))     
 

 
  (widget-insert "\n\n")          
(if reg-operation (progn 
		    (widget-insert "\n\n")
		    (widget-create 'push-button
				   :button-face 'custom-button
				   :notify (lambda (&rest ignore)
					     (run-script))
				   " Run Script ")
		    (widget-insert "     ")     
		    (widget-create 'push-button
				   :button-face 'custom-button
				   :notify (lambda (&rest ignore)
					     (clear-form))
				   " Clear Form ")))

(widget-setup))     
  

(defun select-data-set ( sel-data-set )
  (setf data-set sel-data-set)
  (reg-main))


(defun select-operation ( sel-operation )
  (setq reg-operation sel-operation)
  (reg-main))

run-script is the Lisp method that will be invoked when the user presses run. I demonstrate multiple strategies that can be used to interact with R. Submit a command to R and:

The condition statement selects the operation of interest depending on user input through the form.



(defun run-script ()
 ; (ess-execute (format "working.dir <- \"%s\"" home-dir))
  ;(ess-execute (format "source\(\"reg-library.R\"\)"   ))

  (cond ((equal reg-operation "reshape")
	 (ess-execute "reshape.indometh()"))

For select I redirect the output to a newly created buffer r-results-buffer using the comint-redirect-send-command-to-process command. The following accept-process-output command is used to force EMACS to wait for the R output before printing the reference. If you leave out the accept-process-output command, the buffer will contain the reference before the data output. Here I use one second as the wait time. It is often convenient to set the wait time with a variable that is a number calculated based on the number of rows to be processed in a data set, if known.

	((equal reg-operation "select")
	 (switch-to-buffer r-results-buffer)
	 (erase-buffer)
	 (insert "Subject 1 selected from the Indometh data set[1]     \n ")
	 (insert "----------------------------------------------\n")
	 (comint-redirect-send-command-to-process "select.indometh()" r-results-buffer "*R*" nil nil)
	 (accept-process-output (get-buffer-process r-results-buffer) 1)  
	 (insert "\n\n[1] Kwan, Breault, Umbenhauer, McMahon and Duggan (1976), Kinetics of\nIndomethicin absorption, elimination, and enterohepatic circulation in man.\nJournal of Pharmacokinetics and Biopharmaceutics, 4, 255-280.\n" ))
	((equal reg-operation "plot")
	 (ess-execute "plot.plantgrowth()"))

Here I use the R method write.plantgrowth() to write out the data to a file. An alternative strategy would be to redirect the data to an EMACS buffer and write from EMACS. This is especially desirable should any formatting need to be done to the text/data. In general I strive to handle text in EMACS and data in R.

	((equal reg-operation "write")
	 (ess-execute "write.plantgrowth()"))))


(defun clear-form ()
  (setf data-set nil)
  (setf reg-operation nil)
  (reg-main))

;;end reg-main
[Contents]

R script

The R script is loaded at the time the mode is defined. It can set variables, define methods, load libraries, etc.



reshape.indometh <- function(){ reshape(Indometh, v.names="conc", idvar="Subject",
                timevar="time", direction="wide")}

select.indometh <- function(){ Indometh[ Indometh$Subject==1,] }

plot.plantgrowth<- function(){ boxplot( PlantGrowth$weight, PlantGrowth$group, data=PlantGrowth)}

write.plantgrowth <- function(){
  file.out<- paste( working.dir,  "PlantGrowthData.txt", sep="")
  write.table( PlantGrowth, file = file.out, append = FALSE, sep = "\t", row.names=FALSE, na = "NA", quote=FALSE, col.names = TRUE)}



At any point in the program, new code can be sourced using the R source command.

[Contents]


A HELP form

To assist users with the application, I provide a help form accessible through the menu. I include here some pointers on how to work with EMACS buffers.

;;begin help interface

(defun reg-help()
  (interactive)
(setq buffer (get-buffer-create "*R-example-help-form*"))     
(switch-to-buffer buffer)     
      
(let ((inhibit-read-only t))     
  (erase-buffer))     
(remove-overlays)     
(reg-mode)     

(widget-insert (propertize "R example help form" 'face '(:family "tahoma" :height 150 :underline t)))

(widget-insert "\n\n")
(widget-insert "Here is where you can provide the users with customized help.\n")
(widget-insert "Hyperlinks to backgound information, definitions, software navigation hints are useful here.\n")
(widget-insert "\n\n")

(widget-insert "Go to the main ")
   (widget-create 'link
		  :button-prefix ""
                 :button-suffix ""
                 :button-face 'info-xref
     		 :action (lambda (&rest ignore)
     			   (reg-main))
     		 "R example page.")

(widget-insert "\n\n")   

(widget-insert "\n\n")   
(widget-insert "Useful EMACS navigation commands\n")   
(widget-insert "--------------------------------\n")   
(widget-insert "Ctrl-x b switch to buffer.  [TAB] to open completions buffer. [TAB] to autocompelete.\n")   
(widget-insert "Ctrl-x 1 make the current buffer the only one visible (i.e one window in the frame).\n")   
(widget-insert "Ctrl-x 2 split the current window into 2 horizontally.\n")   

(widget-insert "\n\n")   
(widget-setup))     

;;end primer-help
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

[Contents]

Screenshot

A screenshot with the form in the top pane and results for the "Indometh" and "Select" selections in the bottom pane.



[Contents]