I have a very specific set of requirements by my customer.
We are comparing customer usage patterns against a benchmark set of data.
They require two different colored bars for each service type.
They want the percentage usage printed at the end of each bar. I have used annotation files to add text and commentary to each pair of bars.
I have two issues:
The customer wants the legend to be off to the right. This is legend1 in my example. position=(top right outside)
When I use legend1 in my gchart procedure, the graph is destroyed.
However, if I use legend2, position= (top center outside) the legend starts too far to the right for my customer's taste.
I have added a label of blank spaces '20'x to the legend to force the centered legend to display more toward the right, but I would like to know why the legend position is changing the available graph data space.
The second issue is that I can't seem to place the percentage value at the end of the bar. The examples I have found make it look like I just have to add the percentage (length of the bar) to the start location and the print location will be just past the end of the bar. I found that that is not working and the printed location ends up in the middle of the bar. I have kludged the annotation command to multiply the percentage amount and adding a pad of additional fixed spaces.
Possibly some of the issues are due to using a proportional font (Calibri) which is our corporate standard instead of a fixed font.
But if there is an explanation, or if this group can recommend some solutions, I would be grateful.
I am attaching sample code that demonstrates the issue. And examples of the resulting graphs based upon the legend selected.
The sample code should run standalone as I build the data in the example.
/* *****************************************************************/
/* Slide presentation Member Utilization vs Benchmark BarChart */
/* *****************************************************************/
ods html close;
data sample_slide_usage;
infile datalines delimiter= ',';
input
usage_text : $60. source : $45. my_order : $40. GROUP_NUMBER : $10.
COUNT PERCENT comment_text : $80. Service_Type1 Service_Type2 Service_Type3 ;
datalines ;
Service Type 1 has a long description,Benchmark,1-Service Type 1,BENCH,3334399,31.468822649, members had this type of service,1,0,0
Service Type 1 has a long description,Your Members' Utilization,1-Service Type 1,0009999,3506,32.705223881, members had this type of service,1,0,0
Service Type 2 medium,Benchmark,2-Service Type 2,BENCH,193769,1.828720047, members had this type of service,0,1,0
Service Type 2 medium,Your Members' Utilization,2-Service Type 2,0009999,247,2.3041044776, members had this type of service,0,1,0
Service Type 3 medium,Benchmark,3-Service Type 3,BENCH,3150839,29.736451363, members had this type of service,0,0,1
Service Type 3 medium,Your Members' Utilization,3-Service Type 3,0009999,3598,33.563432836, members had this type of service,0,0,1
Type 4 short,Benchmark,4-Service Type 4,BENCH,3916874,36.966005941, members had this type of service,0,0,0
Type 4 short,Your Members' Utilization,4-Service Type 4,0009999,3369,31.427238806, members had this type of service,0,0,0
;
run;
data pages;
page_count=0;
call symputx('pageno',page_count);
run;
/* macro to update page numbers before printing */
%macro print_page_no;
/* update page number */
data pages;
set pages;
page_count+1;
call symputx('pageno',page_count);
run;
/* print a page number in the middle of the green bar on the page */
ods region x=5.5in y=7.75in ; /* page number */
ods pdf text= "^S={font_weight=bold color=white background=A43b02aFF font_size=11pt} &pageno" ;
%mend;
%let pdfile = 'C:\Data\testgraph.pdf';
ODS PDF FILE="&pdfile."
/*style=styles.Wellness_Slide4 */
nogfootnote startpage=yes;
/* Title has a different color background */
/* add a page title */
ods layout absolute ;
ods region x=0.95in y=0.70in ; /* page title */
/*goptions transparency;*/
goptions reset=all border noaltdesc ;
data slide4;
length slide4text $ 2000.;
slide4text=catx(" ","^S={font_weight=medium foreground=white background=A43b02aFF font_size=28pt}Member Utilization at a Glance ",
" ");
call symputx("slide4text",slide4text);
run;
proc report data=slide4 nowindows noheader;
columns slide4text;
define slide4text / style(column)=[bordertopcolor=A43b02aFF borderleftcolor=A43b02aFF borderrightcolor=A43b02aFF borderbottomcolor=A43b02aFF];
run;
/* Set the graphics environment */
goptions reset=all noborder vsize=4.5in hsize= 8in
/* Set colors */
cback=white colors=( A43b02aFF black ) htext=12pt htitle=12pt
/* space between bars - in graph definition */
/* set font */
ftext='Calibri/bold'
fontres=PRESENTATION
noaltdesc
;
ods region x=0.95in y=2.0in ; /* New Chart */
/* Data is all collected slide4_usage- Generate chart */
/* %annomac;*/
/* %helpano(all);*/
/* annotate commands to add text to left of bar and number value to end of bar */
/* also like to add a text line between the bars */
data annobars; /* just get something on the page */
length function color $10 text $50 client_count $ 10 ;
retain function 'label' xsys '4' ysys hsys '3' when 'A' x xtitle 45 yadjust 2.0 ytitle 82.5 y0 y1 size 4 client_count;
/*set sample_slide_usage;*/
set sample_slide_usage;
/*xtitle=25;*/
/* main title */
if _n_ = 1 then do;
ytitle=82.5;
end;
/* for each line get the percentage number */
function= 'label';
color = 'Black';
style='Calibri/bold';
group= my_order;
/* allow for text descriptions ??? */
/*midpoint=usage_text;*/
y0= ytitle+yadjust ;
y1= ytitle-yadjust-2.5;
/* size of text - yes! but compared to what? */
size=4;
/* every other line starting with the benchmark data */
if (mod(_n_,2) = 1) then do;
/* write first percentage benchmark */
y=y0;
midpoint=source;
x=xtitle+(percent*1.8) + 3 ;
if (percent < 15) then x=x+3;
text = '20'x||put(percent, comma7.2) || '%' ;
color='Black';
position = '6'; /* < */
output;
/* write title - try to center on all bars in series */
y= ytitle;
x = xtitle;
text = usage_text;
midpoint=usage_text;
color='black';
/* midpoint=midvar;*/
position= '4';
output;
end;
/* print client percentage just a bit below but horizontal position */
else if (mod(_n_,2) = 0) then do;
y=y1;
x=xtitle+(percent*1.8) + 3 ;
if (percent < 15) then x=x+3;
text = '20'x||put(percent, comma7.2) || '%' ;
midpoint=source; /* cfr */
color='Black';
/* add strip after put to remove leading and trailing spaces */
client_count = strip(put(count, comma8.0) ) ;
position = '6'; /* < */
output;
/* add custom text for the comment field */
/* add the count for the client */
color='A43b02aFF';
style=('Calibri/bold');
/* move down to the comment location */
y= y-(8);
x= xtitle+7;
/* format the client count with commas */
text = client_count ;
/* position = 0 means stop hear and wait for more date to continue line */
position='0';
output;
/* add text to a comment line */
color='black';
style=('Calibri');
text = " " || comment_text;
/* pause the text to get client count for this attribute and print the number at end of the prior line */
x=.;
/* position='0';*/
output;
ytitle= ytitle-22.0;
end;
run;
/* Specify axis characteristics */
axis1 label=none ;
axis2 label=none ;
axis3 label=none;
/* Add a title to the graph */
/*title1 'Member Utilization at a Glance'; */
title1 ; /* turn off title as it is on the page already */
/* Define the legend options */
;
/* SET UP LEGEND ON RIGHT SIDE - LIMITS LENGTH OF BARS - WHY? */
legend1 noframe
/* set the legend on the page */
/*position=( top center outside)*/
position=( top right outside)
/* make the legend a small box */
shape=bar(.1in,.1in)
/* remove legend label */
label=none
/* following two options used to set legend position - commented out until I figure out how they work*/
/*mode=protect*/
/*origin=(50,90)*/
/*(font = 'Calibri/bold')*/
/* set the order of bars for the legend we want the benchmark on top */
order=( "Benchmark" "Your Members' Utilization" )
;
/* SET UP ALTERNATE LEGEND WITH CENTER OPTION - BARS ARE A REASONABLE LENGTH */
legend2 noframe
/* set the legend on the page */
/*position=( top center outside)*/
position=( top center outside)
/*cborder = 'black'*/
/* make the legend a small box */
shape=bar(.1in,.1in)
/* remove legend label */
/*label=('20'x'20'x'20'x'20'x'20'x'20'x'20'x'20'x'20'x'20'x'20'x'20'x'20'x'20'x'20'x'20'x'20'x )*/
label=none
/* following two options used to set legend position - commented out until I figure out how they work*/
/*mode=protect*/
/*origin=(50,90)*/
/*(font = 'Calibri/bold')*/
/* set the order of bars for the legend we want the benchmark on top */
order=( "Benchmark" "Your Members' Utilization" )
;
/* Create the graph */
/* move legend to the top of graph */
/*ods html;*/
proc gchart data=sample_slide_usage ;
hbar source /
noframe
/* move legend to the top of graph - NO LEGEND -manually created in annotations */
legend=legend2
/* remove outline around bars */
coutline=same
/* remove space between bars */
space=0
gspace=4
/* group by order or usage_text - trying to move bars over for more space for text - looking for start optiion in graphics space */
group= my_order
subgroup=source
/* remove column labels */
type=sum
sumvar=percent
/*sumlabel='Percentage'*/
/* sort bars in order specified */
midpoints= "Benchmark" "Your Members' Utilization"
/*maxis=axis1*/
raxis=axis1
/*gaxis=axis1*/
/* allow axis while debugging then turn it off for final */
noaxis
nostats
/*annnotate=none*/
annotate=annobars
;
run;
quit;
%print_page_no;
ods layout end;
ods pdf startpage=now; /* this closes current page and moves to the next page which can have a different background */
run;
/* close PDF document */
ODS PDF CLOSE;
I have seen a lot of comments for others that say to "change to Sgplot" but all the examples of SGPLOT seem to be use ODS GRAPHICS rather than ODS PDF and I don't see how to put the resulting graph on my PDF. Probably simple to fix but I don't know how to change my charts from GCHART to SGPLOT.
Thanks in advance.
I've trimmed out the pdf stuff (just outputting to png for now), and a few other things, and tried to simplify it down to the essence of the bar chart you want. In particular, I'm using mode=share in combination with offset= for the legend placement. I also pre-pend 'a0'x blanks and '\' and then use split='\' on the group axis to get it re-positioned a little lower (more centered) so you don't have to annotate it.
Hopefully this will make a good starting point for you!
%let name=testpng;
filename odsout '.';
data sample_slide_usage;
infile datalines delimiter= ',';
input
usage_text : $60. source : $45. my_order : $40. GROUP_NUMBER : $10.
COUNT PERCENT comment_text : $80. Service_Type1 Service_Type2 Service_Type3 ;
/* insert some blanks and '\', to use as the 'split' character in the axis */
usage_text='a0'x||'\'||'a0'x||'\'||trim(left(usage_text));
datalines ;
Service Type 1 has a long description,Benchmark,1-Service Type 1,BENCH,3334399,31.468822649, members had this type of service,1,0,0
Service Type 1 has a long description,Your Members' Utilization,1-Service Type 1,0009999,3506,32.705223881, members had this type of service,1,0,0
Service Type 2 medium,Benchmark,2-Service Type 2,BENCH,193769,1.828720047, members had this type of service,0,1,0
Service Type 2 medium,Your Members' Utilization,2-Service Type 2,0009999,247,2.3041044776, members had this type of service,0,1,0
Service Type 3 medium,Benchmark,3-Service Type 3,BENCH,3150839,29.736451363, members had this type of service,0,0,1
Service Type 3 medium,Your Members' Utilization,3-Service Type 3,0009999,3598,33.563432836, members had this type of service,0,0,1
Type 4 short,Benchmark,4-Service Type 4,BENCH,3916874,36.966005941, members had this type of service,0,0,0
Type 4 short,Your Members' Utilization,4-Service Type 4,0009999,3369,31.427238806, members had this type of service,0,0,0
;
run;
proc sort data=sample_slide_usage out=sample_slide_usage;
by my_order source;
run;
data my_anno; set sample_slide_usage;
length function $8 text $100;
xsys='2'; ysys='2';
hsys='3'; when='a';
function='label'; color='gray33';
/* Annotate the values at the ends of the bars */
group=usage_text;
yc=source;
x=PERCENT;
function='label'; position='>';
text='a0'x||put(percent/100,percent7.2);
output;
/* Annotate count & comment below each group of bars */
if (mod(_n_,2) = 0) then do;
xsys='2'; ysys='2';
group=usage_text;
yc=source;
y=.;
x=PERCENT;
function='move'; output;
xsys='1';
x=1;
function='move'; output;
ysys='7';
y=-7;
function='move'; output;
function='label'; position='>';
text=trim(left(put(count, comma7.0)))||' '||trim(left(comment_text));
output;
end;
run;
goptons device=png xpixels=800 ypixels=500;
goptions gunit=pct htext=9pt;
ODS LISTING CLOSE;
ODS HTML path=odsout body="&name..htm" style=htmlblue;
axis1 label=none value=none;
axis2 label=none value=(justify=right font='albany amt/bold' h=12pt) split='\' offset=(3,2);
axis3 label=none value=none major=none minor=none style=0 offset=(0,5);
legend1 label=none noframe across=1 position=(top right outside) mode=share
shape=bar(.1in,.1in) offset=(-5,8);
pattern1 v=s c=cx48af35;
pattern2 v=s c=gray33;
title1 h=16pt font='albany amt/bold' ls=1.5
box=1 bcolor=cx48af35 color=white 'Member Utilization at a Glance';
proc gchart data=sample_slide_usage anno=my_anno;
hbar source / type=sum sumvar=percent nostats
midpoints= "Benchmark" "Your Members' Utilization"
maxis=axis1 gaxis=axis2 raxis=axis3 noframe
space=0 gspace=4 group=usage_text
legend=legend1 coutline=same subgroup=source
des='' name="&name";
run;
quit;
ODS HTML CLOSE;
ODS LISTING;
To place any sgraphics output you put the procedure code in the place you want between ODS PDF and ODS PDF close statement. Inside a layout block if you are using those.
Did you by any chance consider MODE=SHARE with the legend?
No data so I can't test your code but if you use the default, which is PRESERVE then you get the documented behavior:
RESERVE
takes space for the legend from the procedure output area, thereby reducing the amount of space available for the graph. If
I had included data needed to replicate the issue in the question.
I appreciate confirmation that I can still place SGPLOT graphics within ODS PDF commands and that it will work the same as GCHART.
I had tried some MODE commands but my experience was that it cut space out of the graphic which would be unacceptable to my client. I will keep this in mind for future reference.
Thank you.
I've trimmed out the pdf stuff (just outputting to png for now), and a few other things, and tried to simplify it down to the essence of the bar chart you want. In particular, I'm using mode=share in combination with offset= for the legend placement. I also pre-pend 'a0'x blanks and '\' and then use split='\' on the group axis to get it re-positioned a little lower (more centered) so you don't have to annotate it.
Hopefully this will make a good starting point for you!
%let name=testpng;
filename odsout '.';
data sample_slide_usage;
infile datalines delimiter= ',';
input
usage_text : $60. source : $45. my_order : $40. GROUP_NUMBER : $10.
COUNT PERCENT comment_text : $80. Service_Type1 Service_Type2 Service_Type3 ;
/* insert some blanks and '\', to use as the 'split' character in the axis */
usage_text='a0'x||'\'||'a0'x||'\'||trim(left(usage_text));
datalines ;
Service Type 1 has a long description,Benchmark,1-Service Type 1,BENCH,3334399,31.468822649, members had this type of service,1,0,0
Service Type 1 has a long description,Your Members' Utilization,1-Service Type 1,0009999,3506,32.705223881, members had this type of service,1,0,0
Service Type 2 medium,Benchmark,2-Service Type 2,BENCH,193769,1.828720047, members had this type of service,0,1,0
Service Type 2 medium,Your Members' Utilization,2-Service Type 2,0009999,247,2.3041044776, members had this type of service,0,1,0
Service Type 3 medium,Benchmark,3-Service Type 3,BENCH,3150839,29.736451363, members had this type of service,0,0,1
Service Type 3 medium,Your Members' Utilization,3-Service Type 3,0009999,3598,33.563432836, members had this type of service,0,0,1
Type 4 short,Benchmark,4-Service Type 4,BENCH,3916874,36.966005941, members had this type of service,0,0,0
Type 4 short,Your Members' Utilization,4-Service Type 4,0009999,3369,31.427238806, members had this type of service,0,0,0
;
run;
proc sort data=sample_slide_usage out=sample_slide_usage;
by my_order source;
run;
data my_anno; set sample_slide_usage;
length function $8 text $100;
xsys='2'; ysys='2';
hsys='3'; when='a';
function='label'; color='gray33';
/* Annotate the values at the ends of the bars */
group=usage_text;
yc=source;
x=PERCENT;
function='label'; position='>';
text='a0'x||put(percent/100,percent7.2);
output;
/* Annotate count & comment below each group of bars */
if (mod(_n_,2) = 0) then do;
xsys='2'; ysys='2';
group=usage_text;
yc=source;
y=.;
x=PERCENT;
function='move'; output;
xsys='1';
x=1;
function='move'; output;
ysys='7';
y=-7;
function='move'; output;
function='label'; position='>';
text=trim(left(put(count, comma7.0)))||' '||trim(left(comment_text));
output;
end;
run;
goptons device=png xpixels=800 ypixels=500;
goptions gunit=pct htext=9pt;
ODS LISTING CLOSE;
ODS HTML path=odsout body="&name..htm" style=htmlblue;
axis1 label=none value=none;
axis2 label=none value=(justify=right font='albany amt/bold' h=12pt) split='\' offset=(3,2);
axis3 label=none value=none major=none minor=none style=0 offset=(0,5);
legend1 label=none noframe across=1 position=(top right outside) mode=share
shape=bar(.1in,.1in) offset=(-5,8);
pattern1 v=s c=cx48af35;
pattern2 v=s c=gray33;
title1 h=16pt font='albany amt/bold' ls=1.5
box=1 bcolor=cx48af35 color=white 'Member Utilization at a Glance';
proc gchart data=sample_slide_usage anno=my_anno;
hbar source / type=sum sumvar=percent nostats
midpoints= "Benchmark" "Your Members' Utilization"
maxis=axis1 gaxis=axis2 raxis=axis3 noframe
space=0 gspace=4 group=usage_text
legend=legend1 coutline=same subgroup=source
des='' name="&name";
run;
quit;
ODS HTML CLOSE;
ODS LISTING;
Thank you. I will work with this example as I fold my original data and formatting back in.
I see that you switched the font to Albany AMT, a monotype (fixed) font, where each character has the same spacing. The Calibri font we use is proportional and I get warnings from SAS noting that this may be an issue. It usually is, but that is the corporate standard font that is required. I suspect that may be the issue behind many of my spacing issues.
I will play with this answer and try changing bits of it one by one to see the differences between your solution and mine.
Once again, thanks for the help and quick reply.
SAS Innovate 2025 is scheduled for May 6-9 in Orlando, FL. Sign up to be first to learn about the agenda and registration!
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.