Advanced ODS Graphics: A deeper dive into item stores

0

I was writing some new examples for the Customizing the Kaplan-Meier Survival Plot chapter of SAS/STAT software. I thought I would share some of the issues I think about as I write template modification code. We'll take a deeper dive into understanding item stores--the files in which compiled templates are stored--and ways in which you can access them. At the end, I will show you one of my new examples: displaying percentages in the failure plot.

You can do many things with ODS and ODS Graphics and yet know little or nothing about how the tables and graphs are created. As you move beyond simply running a procedure and saving the output, you might want to customize the output. That is when you need to know about templates and where they are stored. Every table and graph has a template. Developers at SAS write the templates, and we provide you with the template source code so that can use them and modify them. Templates contain instructions about formats, table headers, types of graphs, and many other things. You can customize everything.

SAS provides several item stores that contain compiled templates. SAS also provides tools in PROC TEMPLATE via the LIST and SOURCE statements to access these item stores and ensure you are always accessing the templates that SAS provides. You can write DATA step programs to modify the templates. Programmatic template modification ensures that you have a fully documented path between the templates that SAS provides and the templates that you use. These tools give you choices about how to proceed. This post explains some of those choices. Along the way you will see examples of some the interesting ways you can use PROC TEMPLATE to access templates. Every time I write about template modification, I find myself in a bit of a quandary. How much should I say about item stores, and which method of accessing those templates should I use? I usually rely on SAS default settings for item stores. My colleague Rick Wicklin recently wrote about other ways. This post explores some of the alternatives and reasons why you might choose different alternatives in different situations. Let's begin by submitting the following statement:

ods path show;

We are asking ODS to show us the template search path--the default places that ODS looks for templates.

The results are:

Current ODS PATH list is:
 
1. SASUSER.TEMPLAT(UPDATE)
2. SASHELP.TMPLMST(READ)

This says that there are two default item stores. Templates that SAS provides are stored in the SASHELP.TMPLMST item store. This is a read-access only file. This is important! Never set the access to anything else. You never want to alter templates that SAS provides in the SASHELP.TMPLMST item store.You always want to modify them somewhere else. So where? The other item store in the list is SASUSER.TEMPLAT. The access for that item store is "update." By default, when you modify a template, it is stored in SASUSER.TEMPLAT. The version in SASHELP.TMPLMST remains unchanged. The ODS template search path shows that SAS looks for templates first in SASUSER.TEMPLAT to see if there is a template there that you modified. If it does not find a template there, it searches SASHELP.TMPLMST. Templates in SASHELP.TMPLMST remain for the duration of your SAS release. Templates in SASUSER.TEMPLAT remain across SAS sessions until you delete them.

You can store templates that you modify in other places, too. The following statement adds an item store to the ODS search path:

ods path (prepend) work.templat(update);

If you submit the following statement, you can see the change:

ods path show;
Current ODS PATH list is:
 
1. WORK.TEMPLAT(UPDATE)
2. SASUSER.TEMPLAT(UPDATE)
3. SASHELP.TMPLMST(READ)

Now when you modify a template, the modification is stored in WORK.TEMPLAT. ODS first searches for templates in the item store WORK.TEMPLAT, which you can update. If ODS does not find the template there, then it searches SASUSER.TEMPLAT followed by SASHELP.TMPLMST. This is all very analogous to SAS data sets: item stores in SASUSER persist across SAS sessions, and item stores in WORK last only for the duration of the current SAS session. Furthermore, you can make your own permanent item stores (at least until you delete them). The following statements illustrate:

libname mytpls '.';
ods path (prepend) mytpls.template(update);
ods path show;
Current ODS PATH list is:
 
1. MYTPLS.TEMPLATE(UPDATE)
2. WORK.TEMPLAT(UPDATE)
3. SASUSER.TEMPLAT(UPDATE)
4. SASHELP.TMPLMST(READ)

Now the first item store is in the file template.sas7bitm, which is in my working directory. (The "template" in "template.sas7bitm" matches the "template" in "mytpls.template".) You might want to create multiple item stores and put item stores in different directories for different projects.

You can restore and display the default path by submitting these statements:

ods path reset;
ods path show;
Current ODS PATH list is:
 
1. SASUSER.TEMPLAT(UPDATE)
2. SASHELP.TMPLMST(READ)

While the ODS template search path provides a simple notation for specifying the templates that SAS provides, you will find that PROC TEMPLATE provides more granular control. In the early days of ODS, SAS provided precisely one template item store, SASHELP.TMPLMST. That is the origin for the current form of the template search path. Now, SAS ships several item stores. In the context of the template search path, SASHELP.TMPLMST still means all of the templates that SAS provides. That is not true in PROC TEMPLATE.

The following step lists 555 templates that SAS provides in SASHELP.TMPLMST:

proc template;
   list / store=sashelp.tmplmst where=(Type ne 'Dir');
quit;

They include some Base SAS templates such as templates for PROC CONTENTS, ODS style templates, tagsets, some templates that get shared across procedures, and a few others (some for no particular reason). This is 6.4% of the total templates that SAS provides. The following steps list the names of all of the item stores and the number of templates in each:

proc template;
   ods exclude stats;
   ods output stats=s;
   list / stats=store;
run;
 
proc freq data=s(where=(type ne 'Dir'));
   tables store;
run;
                                                         Cumulative   Cumulative
Store                             Frequency    Percent    Frequency     Percent
--------------------------------------------------------------------------------
SASHELP.TMPLACAS                       667       7.70          667        7.70  
SASHELP.TMPLAIR                         23       0.27          690        7.96  
SASHELP.TMPLBASE                       159       1.83          849        9.80  
SASHELP.TMPLCAS                         80       0.92          929       10.72  
SASHELP.TMPLCOMMON                      87       1.00         1016       11.72  
SASHELP.TMPLETS                       1589      18.34         2605       30.06  
SASHELP.TMPLHPA                         81       0.93         2686       30.99  
SASHELP.TMPLHPDM                        54       0.62         2740       31.62  
SASHELP.TMPLHPETS                      117       1.35         2857       32.97  
SASHELP.TMPLHPF                        172       1.98         3029       34.95  
SASHELP.TMPLHPHPF                        6       0.07         3035       35.02  
SASHELP.TMPLHPSTAT                     387       4.47         3422       39.49  
SASHELP.TMPLHPTM                         1       0.01         3423       39.50  
SASHELP.TMPLIML                         43       0.50         3466       40.00  
SASHELP.TMPLLASR                        99       1.14         3565       41.14  
SASHELP.TMPLMST                        555       6.40         4120       47.54  
SASHELP.TMPLNETWORK                     49       0.57         4169       48.11  
SASHELP.TMPLNETWORKCOMMON               33       0.38         4202       48.49  
SASHELP.TMPLNETWORKOPTIMIZATION         24       0.28         4226       48.77  
SASHELP.TMPLNETWORKSOCIAL               12       0.14         4238       48.90  
SASHELP.TMPLOPTGRAPH                     4       0.05         4242       48.95  
SASHELP.TMPLOPTIMIZATION                21       0.24         4263       49.19  
SASHELP.TMPLOPTMINER                    17       0.20         4280       49.39  
SASHELP.TMPLOPTNETWORK                  57       0.66         4337       50.05  
SASHELP.TMPLOR                          80       0.92         4417       50.97  
SASHELP.TMPLQC                         357       4.12         4774       55.09  
SASHELP.TMPLSTAT                      3888      44.86         8662       99.95  
SASHELP.TMPLTMINE                        4       0.05         8666      100.00

All are accessible through the ODS search path SASHELP.TMPLMST. However, in other contexts such as PROC TEMPLATE, you cannot specify SASHELP.TMPLMST as a surrogate for all templates that SAS provides. You can work through the series of short examples that follow to better understand how ODS stores and accesses templates.

You can list all of the templates in SASUSER.TEMPLAT as follows:

proc template;
   list / store=sasuser.templat;
quit;

This produces:

WARNING: Path 'SASUSER.TEMPLAT' does not exist!

Now define a simple template:

proc template;
   define table Base.Freq.Factoid;
      parent=Common.Factoid;
   end;
quit;

PROC TEMPLATE prints the note:

NOTE: TABLE 'Base.Freq.Factoid' has been saved to: SASUSER.TEMPLAT

Now list the contents of that item store again:

proc template;
   list / store=sasuser.templat;
quit;
Listing of: SASUSER.TEMPLAT       
Path Filter is: *                 
Sort by: PATH/ASCENDING           
 
Obs    Path                  Type 
----------------------------------
 1     Base                  Dir  
 2     Base.Freq             Dir  
 3     Base.Freq.Factoid     Table

You can see there is now one table and two directory levels. Now list the Base.Freq.Factoid template:

proc template;
   list Base.Freq.Factoid;
quit;
Listing of: SASUSER.TEMPLAT
Path Filter is: Base.Freq.Factoid
Sort by: PATH/ASCENDING
 
Obs    Path                  Type
----------------------------------
 1     Base.Freq.Factoid     Table
 
 
Listing of: SASHELP.TMPLBASE
Path Filter is: Base.Freq.Factoid
Sort by: PATH/ASCENDING
 
Obs    Path                  Type
----------------------------------
 1     Base.Freq.Factoid     Table

It is in two item stores. Now delete and list the template:

proc template;
   delete Base.Freq.Factoid;
   list Base.Freq.Factoid;
quit;
Listing of: SASHELP.TMPLBASE
Path Filter is: Base.Freq.Factoid
Sort by: PATH/ASCENDING
 
Obs    Path                  Type
----------------------------------
 1     Base.Freq.Factoid     Table

It is in one item store. Now try to delete it again and list it:

proc template;
   delete Base.Freq.Factoid;
   list Base.Freq.Factoid;
quit;

PROC TEMPLATE prints these messages:

WARNING: Path 'Base.Freq.Factoid' does not exist!
NOTE: Could not delete 'Base.Freq.Factoid' from template store!

Here is the listing:

Listing of: SASHELP.TMPLBASE      
Path Filter is: Base.Freq.Factoid 
Sort by: PATH/ASCENDING           
 
Obs    Path                  Type 
----------------------------------
 1     Base.Freq.Factoid     Table

It shows that the template still exists in a SASHELP item store since SASHELP.TMPLMST (a surrogate in the ODS template search path for SASHELP.TMPLBASE and all of the other SASHELP item stores) is read access only. Assuming that you never change the access SASHELP.TMPLMST to anything other than read access, you can safely delete templates by specifying their name in the DELETE statement. When you are writing a program that involves template changes, you might want to begin the program by deleting the template that you have not yet modified. This way you can be certain that as you develop your code (as you submit code subsets and make mistakes along the way) that you are always modifying the templates that SAS provides. This is illustrated in the last example.

Now let's look at a graph template:

ods trace on;
proc kde data=sashelp.class;
   bivar height weight / plots=scatter;
run;

I chose the scatter plot in PROC KDE because it has one of the simplest graph templates in all of SAS. The ODS TRACE output shows that the template name is Stat.KDE.Graphics.ScatterPlot. Now list the template:

proc template;
   list Stat.KDE.Graphics.ScatterPlot;
quit;
Listing of: SASHELP.TMPLSTAT                      
Path Filter is: Stat.KDE.Graphics.ScatterPlot     
Sort by: PATH/ASCENDING                           
 
Obs    Path                              Type     
--------------------------------------------------
 1     Stat.KDE.Graphics.ScatterPlot     Statgraph

It is in the item store SASHELP.TMPLSTAT.

Now display the template source:

proc template;
   source Stat.KDE.Graphics.ScatterPlot;
quit;

Here is the template source:

define statgraph Stat.KDE.Graphics.ScatterPlot;
   dynamic _VAR1NAME _VAR1LABEL _VAR2NAME _VAR2LABEL _byline_ _bytitle_
      _byfootnote_;
   BeginGraph;
      EntryTitle "Distribution of " _VAR1NAME " by " _VAR2NAME;
      Layout Overlay / xaxisopts=(offsetmin=0.05 offsetmax=0.05) yaxisopts=(
         offsetmin=0.05 offsetmax=0.05);
         ScatterPlot x=X y=Y / markerattrs=GRAPHDATADEFAULT;
      EndLayout;
      if (_BYTITLE_)
         entrytitle _BYLINE_ / textattrs=GRAPHVALUETEXT;
      else
         if (_BYFOOTNOTE_)
            entryfootnote halign=left _BYLINE_;
         endif;
      endif;
   EndGraph;
end;

Now explictly specify the item store when displaying the source:

proc template;
   source Stat.KDE.Graphics.ScatterPlot / store=sashelp.tmplmst;
quit;

PROC TEMPLATE displays this warning since SASHELP.TMPLMST is not a surrogate for SASHELP.TMPLSTAT in PROC TEMPLATE:

WARNING: Path 'Stat.KDE.Graphics.ScatterPlot' does not exist!

Now display the source, specifically naming the correct item store:

proc template;
   source Stat.KDE.Graphics.ScatterPlot / store=sashelp.tmplstat;
quit;

Here are the results:

define statgraph Stat.KDE.Graphics.ScatterPlot / store = SASHELP.TMPLSTAT;
   dynamic _VAR1NAME _VAR1LABEL _VAR2NAME _VAR2LABEL _byline_ _bytitle_
      _byfootnote_;
   BeginGraph;
      EntryTitle "Distribution of " _VAR1NAME " by " _VAR2NAME;
      Layout Overlay / xaxisopts=(offsetmin=0.05 offsetmax=0.05) yaxisopts=(
         offsetmin=0.05 offsetmax=0.05);
         ScatterPlot x=X y=Y / markerattrs=GRAPHDATADEFAULT;
      EndLayout;
      if (_BYTITLE_)
         entrytitle _BYLINE_ / textattrs=GRAPHVALUETEXT;
      else
         if (_BYFOOTNOTE_)
            entryfootnote halign=left _BYLINE_;
         endif;
      endif;
   EndGraph;
end;

Now you can start to see part of the quandary to which I referred near the top of this post. The following two SOURCE statements do not display the same thing:

proc template;
   source Stat.KDE.Graphics.ScatterPlot;
   source Stat.KDE.Graphics.ScatterPlot / store=sashelp.tmplstat;
quit;

The difference is subtle but important. The two DEFINE statements are as follows:

define statgraph Stat.KDE.Graphics.ScatterPlot;
define statgraph Stat.KDE.Graphics.ScatterPlot / store = SASHELP.TMPLSTAT;

The first SOURCE statement finds the template by using the template search path, and the second SOURCE statement explicitly names the item store.

When you modify a template you have two alternatives:
1) Ensure that there is not a modified copy already by deleting the template, then list the template SAS provides.
2) Specify specifically that you want to see the source for a template in a SASHELP item store, but you will need to remove the STORE= option from the template definition before submitting it.

Furthermore, you have three independent alternatives:
1) Store the modified template in the default SASUSER item store, where it will persist to future SAS sessions.
2) Store the modified template in the a WORK item store, where it will not persist beyond this SAS session.
3) Store the modified template in some other item store.

There are no wrong answers here, and this flexibility is valuable. If you want the title of your Kaplan-Meier plots to always say "Kaplan-Meier", make a permanent template change and either store it in SASUSER or some other library that you always use. If you want a more temporary template change, store it in SASUSER and take care to delete the modified template either before you do the modification (to be safe) or after you are done using it. Or instead, store it in WORK. Either way, as you are developing and resubmitting code, you will need to delete old modifications and ensure that you are modifying a template that SAS provides and not one that you just modified. Sometimes you will find it handy to specifically specify the item store in PROC TEMPLATE code, but be aware that you might need to delete it from the DEFINE statement before submitting it to SAS.

The following step stores the PROC KDE scatter plot template, the one that SAS provides in the SASHELP.TMPLSTAT item store, into the file tpl.tpl.

proc template;
   source Stat.KDE.Graphics.ScatterPlot / store=sashelp.tmplstat file='tpl.tpl';
quit;

You might need to specify the file name differently depending on your operating system and how you run SAS. I will return to that point later. The following step modifies the graph template to drop the item store specification and change the title to use variable labels instead of variable names:

data _null_;                                        /* Standard boilerplate   */
   infile 'tpl.tpl' end=eof;                        /* Standard boilerplate   */
   input;                                           /* Standard boilerplate   */
   if _n_ eq 1 then call execute('proc template;'); /* Standard boilerplate   */
   _infile_ = tranwrd(_infile_, '/ store = SASHELP.TMPLSTAT', ' ');
   _infile_ = tranwrd(_infile_, '_VAR1NAME " by " _VAR2NAME', 
                      '_VAR1LABEL" by " _VAR2LABEL');
   call execute(_infile_);                          /* Standard boilerplate   */
   if eof then call execute('quit;');               /* Standard boilerplate   */
run;                                                /* Standard boilerplate   */

The following step creates the scatter plot and uses the modified template:

proc kde data=sashelp.class;
   bivar height weight / plots=scatter;
   label height = 'Student Height' weight = 'Student Weight';
run;

This particular example only works this way because PROC KDE passes in the labels as dynamic variables. Not all procedures do this. This technique of modifying templates is discussed in my Avanced ODS Graphics Examples book and also in SAS Global Forum and PharmaSUG papers. My colleage Rick Wicklin recently took up the cause of publicizing this technique, and you can read a gentle introduction in his post A SAS programming technique to modify ODS templates. Rick chooses to prepend a WORK item store rather than using a SASUSER item store. He also shows how to create a temporary file to store the template, which I illustrate next:

filename tmplt TEMP;
proc template;
   source Stat.KDE.Graphics.ScatterPlot / store=sashelp.tmplstat file=tmplt;
quit;
 
data _null_;                                        /* Standard boilerplate   */
   infile tmplt end=eof;                            /* Standard boilerplate   */
   input;                                           /* Standard boilerplate   */
   if _n_ eq 1 then call execute('proc template;'); /* Standard boilerplate   */
   _infile_ = tranwrd(_infile_, '/ store = SASHELP.TMPLSTAT', ' ');
   _infile_ = tranwrd(_infile_, '_VAR1NAME " by " _VAR2NAME', 
                      '_VAR1LABEL" by " _VAR2LABEL');
   call execute(_infile_);                          /* Standard boilerplate   */
   if eof then call execute('quit;');               /* Standard boilerplate   */
run;                                                /* Standard boilerplate   */
 
proc kde data=sashelp.class;
   bivar height weight / plots=scatter;
   label height = 'Student Height' weight = 'Student Weight';
run;

For final production code, the temporary file has a lot to recommend it. It does not leave a stray file around when it is done and it does not require a hard-coded and operating-system specific file name. However, as you are developing the code, you must look at the original template source code. You cannot modify a template without first seeing the code that you are modifying. You can submit a SOURCE statement without a FILE= option and view the code in the log. However when you do that, the line size is smaller, so you might have different line breaks. If you are going to read the template from a file in a DATA step, you need to first look at the template in that file.

I will end by showing you one of the examples that motivated this discussion. The following steps modify PROC LIFETEST's Kaplan-Meier plot (the failure plot version) to display percentages rather than proportions:

proc template;
   delete Stat.Lifetest.Graphics.ProductLimitFailure2;
   source Stat.Lifetest.Graphics.ProductLimitFailure2 / file='tpl.tpl';
quit;
 
data _null_;
   infile 'tpl.tpl' end=eof;
   input;
   if _n_ eq 1 then call execute('proc template;');
   _infile_ = tranwrd(_infile_, 'viewmax=1', 'viewmax=100');
   _infile_ = tranwrd(_infile_, 'tickvaluelist=(0 .2 .4 .6 .8 1.0)', 
                     'tickvaluelist=(0 20 40 60 80 100)');
   _infile_ = tranwrd(_infile_, '1-', '100-100*');
   _infile_ = tranwrd(_infile_, 'Failure Probability', 'Failure Percentage');
   call execute(_infile_);
   if eof then call execute('quit;');
run;
 
proc lifetest data=sashelp.BMT 
   plots=survival(cb=hw failure test atrisk(outside maxlen=13));
   time T * Status(0);
   strata Group;
run;

Notice that I delete the template before writing it to a file knowing that the first time I run the code, the modified template will not yet exist. The DATA step changes the option VIEWMAX=1 to VIEWMAX=100, it changes the tick values to range from 0 to 100, and it changes the expression for many Y axis options such as Y=EVAL(1-SURVIVAL) in the STEPPLOT statement to Y=EVAL(100-100*SURVIVAL). Finally, the DATA step changes the Y axis label. Again, you cannot write code like this without first looking at the template stored in the file tpl.tpl. For example, you must correctly specify the case and spacing when you specify the string that is to be translated.

You can delete all templates in SASUSER.TEMPLAT by submitting the following statements:

ods path sashelp.tmplmst(read);
proc datasets library=sasuser nolist; delete templat(memtype=itemstor); run;
ods path reset;

You need to change and restore the ODS PATH because you cannot delete an item store while it is being used.

No matter how you access the templates that SAS provides, where you store your compiled modified template, or how you handle work files, SAS provides the tools you you need to customize your results.

Share

About Author

Warren F. Kuhfeld

Distinguished Research Statistician

Warren F. Kuhfeld is a distinguished research statistician developer in SAS/STAT R&D. He received his PhD in psychometrics from UNC Chapel Hill in 1985 and joined SAS in 1987. He has used SAS since 1979 and has developed SAS procedures since 1984.

Warren wrote the SAS/STAT documentation chapters “Using the Output Delivery System,” “Statistical Graphics Using ODS,” “ODS Graphics Template Modification,” and “Customizing the Kaplan-Meier Survival Plot.” He also wrote the free web books Basic ODS Graphics Examples and Advanced ODS Graphics Examples.

Related Posts

Leave A Reply

Back to Top