/*
/ Program : rgpp.sas
/ Version : 4.0
/ Author : Roland Rashleigh-Berry
/ Date : 15-May-2011
/ Purpose : Create html graphical patient profiles
/ SubMacros : %rannomac %attrn %nobs %words %getvalue
/ Notes : This macro relies on there being datasets present by the
/ names of rgpp_style, rgpp_global, rgpp_patients and rgpp_data.
/ The first two datasets are effectively macro parameter datasets
/ and their contents will be turned into local macro variables.
/ Because dataset parameters are used then this macro has no
/ parameters to set. The "dummy" parameter does nothing and is
/ only there to make sure "call symputs" create local macro
/ variables rather than global macro variables.
/
/ See the RGPP documentation for how to set up these four
/ datasets for RGPP version 4.0 as this is a complicated process
/ and it is not possible to describe it here. You should ensure
/ that the documentation for version 4.0 is being followed as the
/ datasets differ in design for different versions of this macro.
/
/ Note that the _rggp_anno data step closely follows the Jackson
/ Structured Programming methodology which is why the coding
/ style will be unfamiliar to most sas programmers. More
/ information about this can be found using an Internet search.
/ The authoratative book on this subject is "Principles of
/ Program Design" by author Michael A. Jackson (not the singer).
/ Following this methodology is very helpful for writing correct
/ and maintainable code for this complex case. Note the use of
/ subroutines in this data step to simplify and aid the clarity
/ of the main logic loop.
/
/ Usage : %rgpp
/===============================================================================
/ PARAMETERS:
/-------name------- -------------------------description------------------------
/ dummy (pos) dummy parameter that does nothing
/===============================================================================
/ AMENDMENT HISTORY:
/ init --date-- mod-id ----------------------description------------------------
/ rrb 15May11 Source code for RGPP version 4.0 released
/===============================================================================
/ Copyright (C) 2011, Roland Rashleigh-Berry. Use of this software is by license
/ only commencing 01 Jan 2012 although permission is granted to use these macros
/ for educational or demonstration purposes and by drug regulatory agencies.
/
/ Users should ensure this software is suitable for the purpose to which it is
/ put and to provide adequate checks on the accuracy of any values produced as
/ no guarantee can be given that the results displayed by this software are as
/ expected and no liability is accepted for any damage caused through use of any
/ incorrect output produced. Only use this software if you agree to these terms.
/=============================================================================*/
%put MACRO CALLED: rgpp v4.0;
%macro rgpp(dummy);
%local i patient;
/***************************************
Create local macro variables
***************************************/
*-- Turn rgpp_style and rgpp_global datasets into local macro variables. --;
data _null_;
*- "merge" without "by", not "set", is used as we want only one observation -;
merge rgpp_style rgpp_global;
array _num {*} _numeric_;
array _char {*} _character_;
length __y $ 32;
do __i=1 to dim(_char);
__y=vname(_char(__i));
call symput(__y,trim(left(_char(__i))));
end;
do __i=1 to dim(_num);
__y=vname(_num(__i));
call symput(__y,trim(left(_num(__i))));
end;
run;
/***************************************
Compile the annotate macros
***************************************/
%rannomac;
/***********************************************
***********************************************
Define the graphics annotate macro
***********************************************
***********************************************/
%macro rgpp_anno(dsin=,
htmlfile=,
gifname=,
minscaleday=,
mindataday=,
maxdataday=,
maxscaleday=
);
*- select only the data in range for non-demog data -;
data _rgpp_render_data;
set &dsin;
*- reject or alter non-demography data not in range -;
if not blockisdemog and missing(pctdata1) and missing(pctdata2) then do;
if .&maxdataday then delete;
*- impute day1 where it is less than start day -;
if day1<&minscaleday then do;
day1=&minscaleday;
day1_imp=1;
end;
*- impute day2 where it is greater than end day -;
if day2>&maxscaleday then do;
day2=&maxscaleday;
day2_imp=1;
end;
end;
run;
*- prepare the ods html output file -;
ods html path=_webout body="&htmlfile..html";
data _rgpp_anno;
length day1 day2 pctdata1 pctdata2 8;
array tickdays {20} _temporary_ (20*999999);
array ticklabels {20} $ 24 _temporary_ (20*" ");
retain lowtickx hightickx maxticks tickspacing maxletters 0 ;
retain holdy itemcount holdx1 0;
retain scale "&scale" siteno "&syssite";
length workhtml $ 1024 worktext $ 32 bit $ 40 workcolor $ 8;
%dclannovars
KEEP day1 day2 pctdata1 pctdata2;
set _rgpp_render_data;
by blockseq itemseq;
*====================== FIRST TIME THROUGH =================;
*- Do for first time through - fill the tick arrays and draw the patient number -;
if _n_=1 then do;
storeday1=day1;
link filltickra;
day1=storeday1;
holdy=&vpos;
holdy=holdy-0.5;
%fillbar(y1=holdy,x1=&hposdmean-(&wpatientbg/2),
y2=holdy-&hpatientbg,x2=&hposdmean+(&wpatientbg/2),
fillcolor="&cpatientbg");
%text(x=&hposdmean,y=holdy-(&hpatientbg/2),
text=trim(left(&patvar)),height=&hpatient,
color="&cpatient",font="&fpatient");
holdy=holdy-&hpatientbg+1;
end;
*- End of do-for-first-time-through -;
*===================== FIRST IN THE BLOCK ===================;
*- Do for first blockseq - draw the block description -;
if first.blockseq then do;
holdy=holdy-2;
*- leave an extra blank line for a message block -;
if blockismsg then holdy=holdy-1;
%fillbar(y1=holdy,x1=&hposdmean-(&wblockdescbg/2),
y2=holdy-&hblockdescbg,x2=&hposdmean+(&wblockdescbg/2),
fillcolor="&cblockdescbg");
%text(x=&hposdmean,y=holdy-(&hblockdescbg/2),
text=trim(left(blockdesc)),height=&hblockdesc,
color="&cblockdesc",html=blockhtml,font="&fblockdesc");
itemcount=0;
holdy=holdy-&hblockdescbg-2;
end;
*- End of do-for-first-blockseq -;
*- Do if not blockismsg -;
if not blockismsg then do;
*============== FIRST OF AN ITEM SEQUENCE NUMBER ============;
*- Do for first item in itemseq - draw a striping bar and -;
*- write the item description to the left of the date area. -;
if first.itemseq then do;
itemcount=itemcount+1;
*- set holdx1 to a low value -;
holdx1=-99;
*- for alternate lines, stripe across -;
*- Do for stripeonfirst -;
if &stripeonfirst then do;
if mod(itemcount,2) EQ 1 then do;
%fillbar(y1=holdy-0.5,x1=&hposdmin,
y2=holdy+0.5,x2=&hposdmax,
fillcolor="&cstripe");
end;
end;
*- end of do-for-stripe-on-first -;
*- Do for if not stripeonfirst -;
else do;
if mod(itemcount,2) EQ 0 then do;
%fillbar(y1=holdy-0.5,x1=&hposdmin,
y2=holdy+0.5,x2=&hposdmax,
fillcolor="&cstripe");
end;
end;
*- end of do-for-if-not-stripeonfirst -;
*- Check against the maximum number of characters that can -;
*- be displayed in the space available. For text all in -;
*- upper case, a smaller number is appropriate. -;
if scan(itemdesc,1,"(")=upcase(scan(itemdesc,1,"(")) then maxletters=&maxucletters;
else maxletters=&maxmcletters;
if missing(fitemdesc) then fitemdesc="&fitemdesc";
if missing(citemdesc) then citemdesc="&citemdesc";
if missing(hitemdesc) then hitemdesc=&hitemdesc;
*- If the text is too long to fit then put the full text -;
*- in an html hotspot and display the truncated text from -;
*- the left (which helps the hotspot to be located). -;
*- do for length(itemdesc) GT maxletters -;
if length(itemdesc) GT maxletters then do;
workhtml='ALT="" TITLE="'||trim(itemdesc)||'"';
%text(y=holdy,x=0,position=">",html=workhtml,
text=substr(itemdesc,1,maxletters-2)||"...",
height=hitemdesc,color="&citemdesctrunc",
font=fitemdesc);
end;
*- end of do-for-if-length(itemdesc)-GT-maxletters -;
*- do if not length(itemdesc) GT maxletters -;
else do;
%text(y=holdy,x=&hposdmin-0.3,position="<",text=itemdesc,
height=hitemdesc,color=citemdesc,font=fitemdesc);
end;
*- end of do-if-not-length(itemdesc)-GT-maxletters -;
end;
*=============================================================;
*=================== DEAL WITH ITEM DATA =====================;
*=============================================================;
if citemline=" " then citemline="&citemline";
if missing(witemline) then witemline=&witemline;
if mitemfill=" " then mitemfill="mempty";
*============ PERIOD DATA ===========;
*- Do if not missing(day2) -;
if not missing(day2) then do;
if citemtext=" " then citemtext="&cfigure";
link calcx1;
link calcx2;
*- do if (x2-x1) LE hposmingap -;
if (x2-x1) LE &hposmingap then do;
x1=(x1+x2)/2;
link doabox;
end;
*- end of do-if-(x2-x1)-LE-hposmingap -;
*- do if (x2-x1) not LE hposmingap -;
else do;
*- do if usearrows -;
if &usearrows then do;
*- Use an arrow head to signify an imputed day. -;
*- if day1 and day2 are imputed then use a double arrow shape -;
if day1_imp and day2_imp then do;
%dblarrow(y=holdy,x1=x1,x2=x2,height=&hfigure,linewidth=witemline,
fillcolor=citemtext,linecolor=citemline,html=itemhtml,
fillpattern=mitemfill);
end;
*- else if just day1 is imputed then use a left arrow shape -;
else if day1_imp then do;
%larrow(y=holdy,x1=x1,x2=x2,height=&hfigure,linewidth=witemline,
fillcolor=citemtext,linecolor=citemline,html=itemhtml,
fillpattern=mitemfill);
end;
*- else if just day2 is imputed then use a right arrow shape -;
else if day2_imp then do;
%rarrow(y=holdy,x1=x1,x2=x2,height=&hfigure,linewidth=witemline,
fillcolor=citemtext,linecolor=citemline,html=itemhtml,
fillpattern=mitemfill);
end;
else do;
%rod(y=holdy,x1=x1,x2=x2,height=&hfigure,linewidth=witemline,
fillcolor=citemtext,linecolor=citemline,html=itemhtml,
fillpattern=mitemfill);
end;
end;
*- end of do-if-usearrows -;
*- do if not usearrows -;
else do;
%rod(y=holdy,x1=x1,x2=x2,height=&hfigure,linewidth=witemline,
fillcolor=citemtext,linecolor=citemline,html=itemhtml,
fillpattern=mitemfill);
end;
*- end of do-if-not-usearrows -;
end;
*- end of do-if-(x2-x1)-not-LE-hposmingap -;
end;
*========== DEMOGRAPHY DATA ===========;
*- do if blockisdemog -;
else if blockisdemog then do;
if citemtext=" " then citemtext="&citemtext";
if missing(hitemtext) then hitemtext=&hitemtext;
if missing(fitemtext) then fitemtext="&fitemtext";
*- Demography data is handled differently in that itemtext -;
*- needs to be left aligned within the date area. -;
%text(y=holdy,x=&hposdmin+0.3,text=trim(left(itemtext)),font=fitemtext,
position=">",color=citemtext,html=itemhtml,height=hitemtext);
end;
*- end of do-if-block-is-demog -;
*========== TIMEPOINT DATA ============;
*- do if not blockisdemog -;
else do;
if missing(hitemtext) then hitemtext=&hitemtext;
if missing(fitemtext) then fitemtext="&fitemtext";
link calcx1;
*- do if text exists draw the text -;
if not missing(itemtext) then do;
*- if far enough away from previous one then show as text -;
if (x1-holdx1) GT &hposmingap then do;
if citemtext=" " then citemtext="&citemtext";
%text(y=holdy,x=x1,text=trim(itemtext),height=hitemtext,
position="+",color=citemtext,html=itemhtml,
font=fitemtext);
end;
*- else if too close to previous one then show as a box -;
else do;
if citemtext=" " then citemtext="&cfigure";
link doabox;
end;
end;
*- end of do-if-text-exists-draw-the-text -;
else do;
if citemtext=" " then citemtext="&cfigure";
link doabox;
end;
end;
*- end of do-if-not-blockisdemog -;
/*==============================================================*/
/*================ END OF DEAL-WITH-ITEM-DATA ==================*/
/*==============================================================*/
holdx1=x1;
*- Decrement "holdy" after all the items have been -;
*- displayed for an item sequence number. -;
if last.itemseq then do;
holdy=holdy-1;
end;
*- Once a block of items is finished then draw a box around it -;
if last.blockseq then do;
if not blockismsg then do;
if &drawblockbox then do;
%bigbox(linecolor="&cblockbox",x1=&hposdmin,y1=holdy,
x2=&hposdmax,y2=holdy+itemcount+1);
end;
*- additionally, draw a date scale if the flag variable is set -;
if blockscale then do;
link drawscale;
end;
end;
end;
end;
*- end of do-if-block-is-msg -;
return;
/*===============================================================*/
/*========================= LINK ROUTINES =======================*/
/*===============================================================*/
doabox:
%box(y=holdy,x=x1,width=&hposminfigwidth,height=&hfigure,
linewidth=witemline,fillcolor=citemtext,linecolor=citemline,
html=itemhtml,fillpattern=mitemfill);
return;
propx1:
*- calculate x1 for proportional time ticks -;
x1=((day1-&minscaleday)/(&maxscaleday-&minscaleday))*(&hposdmax-&hposdmin)+&hposdmin;
return;
propx2:
*- calculate x2 for proportional time ticks -;
x2=((day2-&minscaleday)/(&maxscaleday-&minscaleday))*(&hposdmax-&hposdmin)+&hposdmin;
return;
equalx1:
*- calculate x1 for equi-distant time ticks -;
i=1;
do while(day1 GE tickdays(i));
i=i+1;
end;
if i<2 or i>maxticks then link propx1;
else x1=lowtickx+(i-2)*tickspacing+
(day1-tickdays(i-1))/(tickdays(i)-tickdays(i-1))*tickspacing;
return;
equalx2:
*- calculate x2 for equi-distant time ticks -;
i=1;
do while(day2 GE tickdays(i));
i=i+1;
end;
if i<2 or i>maxticks then link propx2;
else x2=lowtickx+(i-2)*tickspacing+
(day2-tickdays(i-1))/(tickdays(i)-tickdays(i-1))*tickspacing;
return;
calcx1:
*- calculate x1 depending on the tick scaling -;
if not missing(pctdata1) then x1=(&hposdmax-&hposdmin)*pctdata1/100+&hposdmin;
else do;
if &uniformscale then link equalx1;
else link propx1;
end;
return;
calcx2:
*- calculate x2 depending on the tick scaling -;
if not missing(pctdata2) then x2=(&hposdmax-&hposdmin)*pctdata2/100+&hposdmin;
else do;
if &uniformscale then link equalx2;
else link propx2;
end;
return;
filltickra:
*- Fill the tick arrays with information in the scale and -;
*- calculate other useful information concerning the ticks -;
i=1;
maxticks=0;
bit=scan(scale,i,"|");
do while(bit ne " ");
day1=input(scan(bit,1,"#"),6.);
worktext=left(scan(bit,2,"#"));
if day1 LE &maxdataday then do;
link propx1;
maxticks=maxticks+1;
if i=1 then lowtickx=x1;
else hightickx=x1;
tickdays(i)=day1;
ticklabels(i)=worktext;
end;
i=i+1;
bit=scan(scale,i,"|");
end;
tickspacing=(hightickx-lowtickx)/(maxticks-1);
return;
drawscale:
*- Routine to draw a date scale below the block box -;
workcolor="&cblockbox";
if not &drawblockbox then do;
workcolor="&cscale";
*- we have to draw the axis line if drawblockbox ne 1 -;
%drawline(x1=&hposdmin,x2=&hposdmax,y1=holdy,y2=holdy,
linecolor=workcolor);
end;
*- drop a line for the the scale text -;
holdy=holdy-1;
*- do for each of the tick labels -;
pctdata1=.;
pctdata2=.;
do j=1 to maxticks;
day1=tickdays(j);
link calcx1;
*- draw the tick label -;
%text(y=holdy,x=x1,position="+",text=trim(ticklabels(j)),
color=workcolor,height=&hscaletext,font="&fscaletext");
*- draw the tick drop line -;
%drawline(x1=x1,x2=x1,y1=holdy+0.6,y2=holdy+1,linecolor=workcolor);
end;
*- end of do-for-each-of-the-tick-labels -;
return;
*================================================================;
*====================== END OF LINK ROUTINES ====================;
*================================================================;
run;
*- Call proc ganno and set the description to a space to avoid -;
*- having a hotspot applied to the whole graphics output area. -;
proc ganno annotate=_rgpp_anno description=" " name="&gifname";
run;
*- Delete the grseg member so that a rerun can use the same name. -;
proc greplay igout=work.gseg nofs;
delete &gifname;
run;
quit;
%mend rgpp_anno;
/**************************************
Sort input data into correct order
**************************************/
proc sort data=rgpp_patients;
by &patvar;
run;
proc sort data=rgpp_data;
by &patvar blockseq itemseq day1 day2 pctdata1 pctdata2;
run;
/***************************************
Create a patient-ids-only dataset
***************************************/
*- This dataset will be used for selecting one obs at a -;
*- time from rgpp_patients and symputting its variables. -;
proc sort nodupkey data=rgpp_patients(keep=&patvar)
out=_rgpp_patonly;
by &patvar;
run;
/********************************************
Set up goptions and other graphics stuff
********************************************/
*- set the goptions -;
goptions reset=all &transparency gsfmode=replace device=gif &border
xpixels=&xpixels ypixels=&ypixels
hpos=&hpos vpos=&vpos cback=&cback
htext=&hitemdesc ftext=&fitemdesc ctext=&citemdesc
;
*- This "ods listing close" prevents graphics output -;
*- being written to where we do not want it. -;
ods listing close;
*- set up the fileref for the html and gif output folder -;
filename _webout "&webout";
/*********************************************
Call the annotate macro for each patient
*********************************************/
%do i=1 %to %nobs(_rgpp_patonly);
%let patient=%getvalue(_rgpp_patonly,&patvar,&i);
data _null_;
set rgpp_patients(where=(&patvar=&patient));
call symput('htmlfile',trim(left(htmlfile)));
call symput('gifname',trim(left(gifname)));
call symput('minscaleday',trim(left(minscaleday)));
call symput('mindataday',trim(left(mindataday)));
call symput('maxdataday',trim(left(maxdataday)));
call symput('maxscaleday',trim(left(maxscaleday)));
run;
%rgpp_anno(dsin=rgpp_data(where=(&patvar=&patient)),
htmlfile=&htmlfile,
gifname=&gifname,
minscaleday=&minscaleday,
mindataday=&mindataday,
maxdataday=&maxdataday,
maxscaleday=&maxscaleday
);
%end;
/**********************
Tidy up and exit
**********************/
filename _webout clear;
ods html close;
ods listing;
%mend rgpp;