Hello SAS experts,
I am trying to create some plots with annotations using PROC TEMPLATE. Currently, I have the annotations set up using an annotation dataset (rather than incorporating the annotations into the PROC TEMPLATE procedure itself using statements such as drawtext). I am getting it close to working correctly, but the annotations are appearing for all BY groups in each plot. That is, BY group=1 shows annotations for all BY groups, even though I have the BY group variable in the annotations dataset.
Is there a way I can have the annotations show only for each corresponding BY group? Sample code to reproduce my plot is below:
data graphdata;
input rpt_strat $ est delta level CI;
format Est CI percentn11.2;
datalines;
20-to-44 -.0383 -0.002374357 0.05 -.0572
20-to-44 . -0.002374357 0.05 -.0195
45-to-64 -.0317 -0.004262066 0.05 -.0525
45-to-64 . -0.004262066 0.05 -.0109
;
run;
ods path(prepend) Work.Templat(update);
%SGANNO;
data anno;
set graphdata;
if Est ~= .;
%SGTEXT(label="Estimate", x1=Est, y1=75, x1space='datavalue', y1space='graphpercent', anchor='center',width=200, widthunit='pixel');
keep rpt_strat;
run;
proc template;
define statgraph CIPlot;
begingraph / designwidth=800px designheight=200px;
layout overlay / yaxisOpts=(display=none)
xAxisOpts=(label="Rate");
annotate;
scatterplot x=Est y=level / markerattrs=(symbol=circlefilled size=10) datalabel=Est datalabelposition=top;
seriesplot x=CI y =level / display=(markers) markerattrs=(symbol=ibeam size=10) datalabel=CI datalabelposition=top;
referenceline x=delta / lineattrs=(pattern=2);
referenceline x=0 / lineattrs=(pattern=1);
endlayout;
endgraph;
end;
run;
proc sgrender data=graphdata template=CIPlot sganno=anno;
by rpt_strat;
title1 "Non-inferiority Confidence Interval for Measure";
title2 "Ages #byval1";
run;
Note that "Estimate" is being displayed twice on each graph: it should only be displayed once per graph, above the big blue dot. Is there a way to fix this?
Also, if anyone knows how to suppress the automatic BY group heading "rpt_strat=20-to-44" and "rpt_strat=45-to-64" I'd appreciate that too!
I'm using SAS 9.4 TS1M5
Thank you,
Brian
@bstarr wrote:
Thank you, I was afraid of that. I will populate the annotations within PROC TEMPLATE itself. I get that's how it works, but it seems like it should work in a way that the annotation is done separately for each BY group. Just one of those things, I suppose! 🙂
Something else: a better label variable, use the splitchar options.
data graphdata; infile datalines missover; input rpt_strat $ est delta level CI ; format Est CI percentn11.2; if est ~= . then do; Text=catx('*',"Estimate",put(est,percentn11.2)); end; datalines; 20-to-44 -.0383 -0.002374357 0.05 -.0572 20-to-44 . -0.002374357 0.05 -.0195 45-to-64 -.0317 -0.004262066 0.05 -.0525 45-to-64 . -0.004262066 0.05 -.0109 ; run; proc template; define statgraph CIPlot; begingraph / designwidth=800px designheight=200px; layout overlay / yaxisOpts=(display=none) xAxisOpts=(label="Rate"); annotate; scatterplot x=Est y=level / markerattrs=(symbol=circlefilled size=10) datalabel=text DATALABELSPLIT=TRUE datalabelsplitchar='*' datalabelposition=top; seriesplot x=CI y =level / display=(markers) markerattrs=(symbol=ibeam size=10) datalabel=CI datalabelposition=top; referenceline x=delta / lineattrs=(pattern=2); referenceline x=0 / lineattrs=(pattern=1); endlayout; endgraph; end; run; proc sgrender data=graphdata template=CIPlot ; by rpt_strat; title1 "Non-inferiority Confidence Interval for Measure"; title2 "Ages #byval1"; run;title;
You can set some datalabelattrs to change text to some extent.
The behavior is as expected: EACH plot will plot the entire Annotate set, or at least the bits that would fit within the plot area.
I suggest adding another variable and using a TEXTPLOT statement with the text and desired positions to match the by variables.
I'm not aware of another way to get annotate sets to match on by variables only.
Thank you, I was afraid of that. I will populate the annotations within PROC TEMPLATE itself. I get that's how it works, but it seems like it should work in a way that the annotation is done separately for each BY group. Just one of those things, I suppose! 🙂
@bstarr wrote:
Thank you, I was afraid of that. I will populate the annotations within PROC TEMPLATE itself. I get that's how it works, but it seems like it should work in a way that the annotation is done separately for each BY group. Just one of those things, I suppose! 🙂
Something else: a better label variable, use the splitchar options.
data graphdata; infile datalines missover; input rpt_strat $ est delta level CI ; format Est CI percentn11.2; if est ~= . then do; Text=catx('*',"Estimate",put(est,percentn11.2)); end; datalines; 20-to-44 -.0383 -0.002374357 0.05 -.0572 20-to-44 . -0.002374357 0.05 -.0195 45-to-64 -.0317 -0.004262066 0.05 -.0525 45-to-64 . -0.004262066 0.05 -.0109 ; run; proc template; define statgraph CIPlot; begingraph / designwidth=800px designheight=200px; layout overlay / yaxisOpts=(display=none) xAxisOpts=(label="Rate"); annotate; scatterplot x=Est y=level / markerattrs=(symbol=circlefilled size=10) datalabel=text DATALABELSPLIT=TRUE datalabelsplitchar='*' datalabelposition=top; seriesplot x=CI y =level / display=(markers) markerattrs=(symbol=ibeam size=10) datalabel=CI datalabelposition=top; referenceline x=delta / lineattrs=(pattern=2); referenceline x=0 / lineattrs=(pattern=1); endlayout; endgraph; end; run; proc sgrender data=graphdata template=CIPlot ; by rpt_strat; title1 "Non-inferiority Confidence Interval for Measure"; title2 "Ages #byval1"; run;title;
You can set some datalabelattrs to change text to some extent.
Nifty, thank you! I will work with this.
Another way is to use the ID column in your Anno data set to subset your annotate observations. In this case, the ID column value would be the same as the rpt_strat column. To create the ID column, add parameter id=rpt_strat to the %SGTEXT macro call in your Anno DATA step:
data anno; set graphdata; if Est ~= .; %SGTEXT(id=rpt_strat, label="Estimate", x1=Est, y1=75, x1space='datavalue', y1space='graphpercent', anchor='center',width=200, widthunit='pixel'); keep rpt_strat; run;
In your CIPlot template code, to get the BY-Group variable value into your template, create dynamic variable _BYVAL_, and then in your ANNOTATE statement, add option ID = _BYVAL_:
proc template; define statgraph CIPlot; dynamic _BYVAL_; begingraph / designwidth=800px designheight=200px; layout overlay / yaxisOpts=(display=none) xAxisOpts=(label="Rate"); annotate / id=_BYVAL_; scatterplot x=Est y=level / markerattrs=(symbol=circlefilled size=10) datalabel=Est datalabelposition=top; seriesplot x=CI y =level / display=(markers) markerattrs=(symbol=ibeam size=10) datalabel=CI datalabelposition=top; referenceline x=delta / lineattrs=(pattern=2); referenceline x=0 / lineattrs=(pattern=1); endlayout; endgraph; end; run;
Finally, to suppress the automatic BY-line, specify system option NOBYLINE:
options nobyline; proc sgrender data=graphdata template=CIPlot sganno=anno; by rpt_strat; title1 "Non-inferiority Confidence Interval for Measure"; title2 "Ages #byval1"; run; options byline; /* Restore BY-line */
You can find information about subsetting annotate observations in Subsetting Annotations in SAS Graph Template Language: User’s Guide. Hope this helps.
Steve
Thank you, Steve. This is excellent! Does just what I need and still uses the annotation dataset.
I had never seen or thought of Steve's trick. Very nice! Inspired by it, I will write a blog for graphically speaking (https://blogs.sas.com/content/graphicallyspeaking/) in the next few days that shows how to specify the graph syntax in PROC SGPLOT with a BY statement, slightly post-process the template, then create the graphs with custom SG annotations for each BY group. The latter part is based on my multiple blogs and papers on highly customized graphs. Here is a preview of the code (again, giving credit to Steve for the idea of adding an ID variable that matches the BY variable). I'll explore other ways too.
proc sort data=sashelp.class out=c;
by sex;
run;
data anno;
x1 = 20; y1 = 85; function = 'Text'; dataspace = 'GraphPercent'; width = 100;
label = 'Female Students'; id = 'F'; output;
label = 'Male Students'; id = 'M'; output;
run;
proc sgplot data=c tmplout='tmp.tmp';
ods exclude sgplot;
scatter y=weight x=height;
by sex;
run;
data _null_;
infile 'tmp.tmp';
input;
_infile_ = tranwrd(_infile_, 'sgplot;', 'by;');
if _n_ eq 1 then call execute('proc template;');
call execute(_infile_);
if find(_infile_, 'layout overlay') then
call execute('dynamic _byval_; annotate / id=_byval_;');
run;
ods html body='b.html';
title;
options nobyline;
proc sgrender data=c template=by sganno=anno; by sex; run;
options byline;
ods html close;
Excellent, I'm glad this sparked some interest! Thank you for sharing the code sample and I look forward to reading the blog post!
Join us for SAS Innovate 2025, our biggest and most exciting global event of the year, in Orlando, FL, from May 6-9. Sign up by March 14 for just $795.
Learn how use the CAT functions in SAS to join values from multiple variables into a single value.
Find more tutorials on the SAS Users YouTube channel.
Ready to level-up your skills? Choose your own adventure.