Data visualization with SAS programming

Proc lifetest - at risk table preventing x axis from starting at 0

New Contributor
Posts: 2

Proc lifetest - at risk table preventing x axis from starting at 0

[ Edited ]

Hello SAS community! I'm using SAS University.


I have run proc lifetest to generate Kaplan Meier curves, and I managed to use Proc template to change the format of my x-axis, y-axis, entrytitle, etc. I used viewmin, viewmax, offsetmin, and offsetmax to better control my axes. I want both the x-axis and y-axis to start at 0, so I set viewmin=0 and offsetmin=0 for both the x-axis and y-axis. However, I have realized that my at risk table is causing the x-axis to start a little before 0. I'm attaching an image that shows what my axis shows.


Does anyone know what proc template language would allow me to either adjust my y-axis or at risk table so that the x-axis min is exactly set to 0?


Thank you for your help!

Posts: 496

Re: Proc lifetest - at risk table preventing x axis from starting at 0

[ Edited ]

In fact this is one of my examples for this year's PharmaSUG paper.  You need to display the at risk table via SG annotation or DRAW statements.  I can post more details after I get to work.  

Posts: 496

Re: Proc lifetest - at risk table preventing x axis from starting at 0

Posted in reply to WarrenKuhfeld

I extracted this code from one of my 2018 PharmaSUG examples.  My actual example is more complicated.  Come to Seattle to see the entire presentation.  My abstract is below the code.  The code might have a few superfluous lines left, but it illustrates the principles involved.  As you note, there is no way to have an axis table and a 0,0 origin, but there is a way to create the at risk table without using an axis table.  It relies on the graph modification method that I call "highly customized graphs".  I have written about it at SAS Global Forum, PharmaSUG, in the Graphically Speaking blog, , and elsewhere including here:

data simdata(drop=n);
   Treatment = 'Drug   ';
   do until(n = 500);
      Month  = -100 * log(uniform(368));
      status = not (uniform(368) > 0.10 or (Month > 15 and uniform(368) > 0.2));
      if Month < 18 then do; output; n + 1; end;
   n = 0;
   Treatment = 'Placebo';
   do until(n = 500);
      Month  = -150 * log(uniform(368));
      status = not (uniform(368) > 0.16 or (Month > 15 and uniform(368) > 0.1));
      if Month < 18 then do; output; n + 1; end;

proc template;
   delete Stat.Lifetest.Graphics.ProductLimitFailure  / store=sasuser.templat;
   source Stat.Lifetest.Graphics.ProductLimitFailure  / file='tpl.tpl';

* Modify template for full-size graph;
data _null_;
   infile 'tpl.tpl' end=eof;

   * Add PROC TEMPLATE statement;
   if _n_ = 1 then call execute('options nosource; proc template;');

   * Remove offset, make the Y axis start precisely at zero;
   _infile_ = tranwrd(_infile_, 'yaxisopts=(', 'yaxisopts=(offsetmin=0 offsetmax=0 ');

   * Remove offset, make the X axis start precisely at zero;
   _infile_ = tranwrd(_infile_, 'offsetmin=.05', 'offsetmin=0 offsetmax=0');

   * Remove the at-risk table, change Boolean expression to unconditional false;
   _infile_ = tranwrd(_infile_, 'PLOTATRISK=1', '0');

   * Find beginning of LAYOUT OVERLAY statement;
   if find(_infile_, 'layout overlay / xaxisopts') then lo + 1;

   * Find end of LAYOUT OVERLAY statement;
   if lo and index(_infile_, ';') then do;

      * Reset layout flag;
      lo = 0;
      * Add padding;
      _infile_ = tranwrd(_infile_, ';', 'pad=(bottom=10% left=5%);');

      * Write original or modified template line;
      call execute(_infile_);

      %* Add ANNOTATE statement to LAYOUT OVERLAY and give it an ID;
      _infile_ = 'annotate / id="id";';

   * Write original, new, or modified template line;
   call execute(_infile_);

   * End PROC TEMPLATE, display source again;
   if eof then call execute('quit; options source;');

* Create data object for graph (failureplot=fp);
ods document name=MyDoc (write);
ods graphics on;
proc lifetest data=simdata notable
     plot=survival(failure nocensor atrisk(outside atrisktickonly)=0 to 18 by 3);
   time Month * status(0);
   strata treatment;
   ods output failureplot=fp homtests=p;
ods document close;

* List document contents, need to copy the path from the output;
proc document name=MyDoc;
   list / levels=all;

* Using failure plot path from the ODS document, output dynamic variables;
proc document name=MyDoc;
   ods exclude dynamics;
   ods output dynamics=dynamics;
   obdynam \Lifetest#1\FailurePlot#1;

* SG annotation data set;
data anno(drop=atrisk tatrisk stratum stratumnum left probchisq);
   retain Function 'Text'        /* All annotations add text                 */
          X1Space 'DataValue   ' /* Most X coordinates are data values       */
          Y1Space 'GraphPercent' /* Most Y coordinates are graph percentages */
          ID 'id'                /* Constant ID. Matches ANNOTATE stmt ID.   */
          Width 100              /* Width=100% ensures no splitting          */
          left -3;               /* Extrapolated data value for label column */
                                 /* of axis table                            */
   * Extract axis table from graph data object;
   set fp(drop=_: censored event survival time);
   by stratumnum;
   if _n_ eq 1 then do;
      Label = 'No. at Risk';     /* Header for axis table row labels         */
      x1 = left;                 /* Ad hoc X coordinate                      */
      y1 = 9;                    /* Ad hoc Y coordinate                      */
      Anchor = 'Left  ';         /* Left-justify header                      */
      TextWeight = 'Bold';       /* Bold text                                */
      TextStyleElement = 'GraphDefault'; /* Text style element               */
      output;                    /* Write header                             */
      textweight = ' ';          /* Back to normal font                      */
      TextSize = .;              /* Restore default text size                */
                                 /* Set colors for axis table rows           */
   textstyleelement = cats('GraphData', stratumnum);
   textweight = ' ';             /* Normal font (not bold)                   */
   if first.stratumnum then do;  /* Set up axis table row labels             */
      label = stratum;           /* Row label                                */
      x1 = left;                 /* Ad hoc X coordinate                      */
      y1 = (3 - stratumnum) * 3; /* Ad hoc Y coordinate                      */
      anchor = 'Left';           /* Left-justify header                      */
      output;                    /* Write row label                          */
      anchor = 'Right';          /* Right-justify                            */
      y1space = 'DataValue';     /* Use data value Y coordinate              */
      x1 = 97.5;                 /* Ad hoc Y coordinate                      */
      y1 = ifn(stratumnum eq 1, 75, 90); /* Ad hoc Y coordinates             */
      TextSize = 7;              /* Text size for ad hoc curve labels        */
      x1space = 'GraphPercent';  /* Graph percentage X coordinates           */
      output;                    /* Write out ad hoc curve labels            */
      textsize = .;              /* Restore default text size                */
      x1space = 'DataValue';     /* Restore X coordinate space               */
   if n(tatrisk);                /* If this is part of the at-risk table     */
   label = put(atrisk, 6. -L);   /* Format numeric value to string           */
   x1 = tatrisk;                 /* X coordinate from data set               */
   y1 = (3 - stratumnum) * 3;    /* Ad hoc Y coordinate                      */
   y1space = 'GraphPercent';     /* Y coordinate is graph percentage         */
   anchor = 'Center';            /* Center values relative to ticks          */
   output;                       /* Write out body of axis table             */

* Display SG annotation data set;
proc print noobs; run;

ods html body='b.html';

* Write SGRENDER code, populate dynamics;
data _null_;

   * Read dynamics;
   set dynamics(where=(label1 ne '___NOBS___')) end=eof;

   * Write PROC statement, start DYNAMIC statement;
   if _n_ = 1 then do;
      call execute('proc sgrender data=fp sganno=anno ' ||
      call execute('dynamic');

   * Write name/value pairs. Numeric: name=formatted-value.
     Character: name=quoted formatted-value;
   if cvalue1 ne ' ' then
      call execute(catx(' ', label1, '=',
                   ifc(n(nvalue1), cvalue1,

   * End the DYNAMIC statement, end the PROC SGRENDER call;
   if eof then call execute('; run;');
ods html close;

proc template;
   delete Stat.Lifetest.Graphics.ProductLimitFailure  / store=sasuser.templat;

Creating a Publication-Quality Graph Embedded in Another Graph

Warren F. Kuhfeld, SAS Institute Inc., Cary, NC
John King, Ouachita Clinical Data Services, Inc.

Several recent papers in the New England Journal of Medicine (NEJM) display graphs inside of graphs (Marso et al.
2016b, a; Zinman et al. 2015; Holman et al. 2017). Typically, both graphs are Kaplan-Meier plots. The larger graph
has a Y axis that ranges from 0 to 100. The failure curves occupy a relatively small part of the graph and extend
perhaps no more than 10%–20% of the way up the Y axis. Inside this graph, in the open white space, you can place
another graph in which the Y axis extends from 0 to slightly above the maximum value. The outer graph shows the
failure probability in the full 0%–100% context, whereas the inner graph zooms in to the results and provides additional
text and annotation, such as curve labels, the hazard ratio, and p-values. The outer graph also displays the number of
subjects at risk. The format for the NEJM is quite specific. This paper illustrates how to use ODS Graphics to create a
graph inside a graph that is suitable for publication in the NEJM.

Posts: 1,265

Re: Proc lifetest - at risk table preventing x axis from starting at 0

Clearly, Warren's solution will work.


Alternatively, using SGPLOT, it is possible to do a few other arrangements without annotation.  Note, the y-axis ticks are get bit longer. 


This first one fixes the "0" issue, and still puts the table of subjects at risk at the bottom.  Note, the y-axis label is still pushed out to the left.


Survival Plot_Ref2_SG_V94.png


This one fixes the "0" and puts the subjects at risk closer to the curves.  Note, the y-axis label is closer to the y-axis tick values.


Survival Plot_SG_V94_Inside_2.png


I will write up a blog article to discuss the alternatives.


New Contributor
Posts: 2

Re: Proc lifetest - at risk table preventing x axis from starting at 0

Posted in reply to Sanjay_SAS

Warren and Sanjay, thank you both!


Warren, thanks for the extensive code. I'm still pretty new to SAS (this is my first time generating Kaplan Meier curves in SAS), and I've never used annotations in SAS before. I'm trying to finish these plots within the next few days, so I'm going to look for an easier (probably less elegant) solution for now, but I am definitely going to comb through your code later for my learning.


Sanjay, would it be possible for you to share the code that produced the first of the two plots that you posted?


Thank you both again!

Posts: 496

Re: Proc lifetest - at risk table preventing x axis from starting at 0

Posts: 1,265

Re: Proc lifetest - at risk table preventing x axis from starting at 0

[ Edited ]

Motivated by your communities question, I wrote up a blog post on Customizing Survival Plot in Graphically Speaking.  This blog is also a good reference for graphs using SAS in general.  You can enter a search term on the right to find articles on topics of interest.


Note, in this article I avoid the usage of annotation, a powerful and advanced technique.  Annotation allows you to customize the graph very extensively, but needs some learning.  In the examples, I use SGPLOT statements and options to achieve similar results, though your mileage may vary based on your requirements.


In this solution, I do not actually reduce the x-axis offset.  I just remove the y-axis line and limit the display of the x-axis line up to the "0" value so it "appears" as if the offset is removed.  You can add a reference line at  x=0 to create the appearance of a y-axis line.

Posts: 496

Re: Proc lifetest - at risk table preventing x axis from starting at 0

Posted in reply to Sanjay_SAS

Inspired by Sanjay's creative solutions, here is another one.  It might require tweaking and work better for some situations than others.  It uses an axis table to draw the ticks along with some of Sanjay's tricks.  It deliberately skips the 0 tick on the y axis.


ods graphics on;

/*--Get survival plot data from LIFETEST procedure--*/
proc lifetest data=sashelp.BMT 
   ods output Survivalplot=SurvivalPlotData;
   plots=survival(outside atrisk(maxlen=13)=0 to 2500 by 500);
   time T * Status(0);
   strata Group / test=logrank adjust=sidak;

data SurvivalPlotData2;
  set SurvivalPlotData end=last;
  if last then do;
    call missing (time, survival, atrisk, event, censored, tatrisk, stratum, stratumnum);
    do ytick = 0.2 to 1 by 0.2; output; end;
fornat ytick 3.1;

title 'Product-Limit Survival Estimates';
title2  h=0.8 'With Number of Subjects at Risk';
footnote j=l h=6pt italic 'This visual is for discussion of graph features only.'  
         '  The actual details should be customized by user to suit their application.';
proc sgplot data=SurvivalPlotData2 noborder noautolegend;
  styleattrs axisextent=data;
  refline 0 / axis=x;
  step x=time y=survival / group=stratum lineattrs=(pattern=solid) name='s';
  scatter x=time y=censored / markerattrs=(symbol=plus) name='c';
  scatter x=time y=censored / markerattrs=(symbol=plus) GROUP=stratum;
  yaxistable ytick / y=ytick location=inside position=left nolabel 
  xaxistable atrisk / x=tatrisk location=outside class=stratum colorgroup=stratum 
             valueattrs=(size=7 weight=bold) labelattrs=(size=8) nomissingclass;
  yaxis min=0 offsetmin=0 display=none grid;

Ask a Question
Discussion stats
  • 7 replies
  • 3 in conversation