Service Packs for the SAS9 Platform." />
This sample uses the GCHART and GPLOT procedures in conjunction with the Annotate facility in the SAS/GRAPH software to create a variety of indicators, and then uses the GREPLAY procedure to arrange the indicators into a dashboard. This version is intended for use as a webDAV content portlet in SAS Information Delivery Portal software.
Note: This dashboard is described on p. 199 of Information Dashboard Design (Few, Stephen. 2006. Sebastopol, CA. O'Reilly Media, Inc.).
For additional information about creating dashboard applications with SAS/GRAPH software, see SAS/GRAPH Dashboard Samples.
This sample creates a full-blown dashboard for a telesales organization. It creates the following custom charts:
It then uses the GREPLAY procedure with a custom layout template to place the charts onto a telesales dashboard. Here is the layout of the custom template:
This sample creates the following SAS data sets that correspond to the plots:
You can see the definition for these data sets near the top of the SAS program for this sample. The numbering for the plots and SAS data sets corresponds to the location of the charts in the dashboard layout.
Here are the variables that you must use in each of the data sets:
Data Set Name | Variable Name | Description |
---|---|---|
DATA1 |
CATEGORY | specifies the name of the data category. Can be Hold Time, Call Duration, or Abandonments. |
PERCENT_OF_TARGET | specifies the percent of the target. Can be 0 to 2.5 (0 to 250%). | |
ACTUAL_VALUE | specifies the actual numeric value. | |
DATA2 | CATEGORY | specifies the category. Can be 1, 2, or 3. 1 shows at the bottom. |
CATEGORY_NAME | specifies the category name. It is set based on the value of CATEGORY as follows : 1 = Hold Time, 2 = Call Duration, and 3 = Abandonments. | |
TIMESTAMP | specifies a numeric timestamp. | |
PERCENT_OF_TARGET | specifies the percent of the target as 0-n%. | |
DATA3 | BARNUM | specifies the bar number. Since only bar exists, always use 1. |
SEGMENT | specifies the three ranges. Can be 1, 2, or 3. | |
VALUE | specifies the percent utilization. Can be 0.75 to 1.00 (75% to 100%). | |
DATA4 | CATEGORY | specifies the category. Can be 1 or 2. |
CATEGORY_NAME | specifies the category name. It is set based on the value of CATEGORY as follows: 1 = Order Count and 2 = Call Count. | |
TIMESTAMP | specifies a timestamp as 1,2,...n. | |
PERCENT_OF_TARGET | specifies the percentage of the target value as 0-n%. | |
DATA4_TABLE | CAT_NAME | specifies the category name. Can be Call Count or Order Count. |
CATEGORY | specifies the category. Can be 1 or 2. | |
THIS_HOUR | specifies the count for the current hour. | |
TODAY | specifies the count for the current day. | |
THIS_MONTH | specifies the count for the current month. | |
DATA5 | CALLS_PER_REP | specifies the calls per representative as a character string range. Can be: '0-4','5-9','10-14','15-19','20-24','25-29', or '30-34'. |
REPS | specifies the number of representatives in this bar. | |
DATA6 | ORDERS_PER_REP | specifies the orders per representative as a character string range. Can be: '0-3','4-7','8-11','12-15','16-19','20-23', or '24-27'. |
REPS | specifies the number of representatives in this bar. | |
DATA7 | TEXT | specifies the representative's name preceded by a '.' if the representative is online or an 'X' if the representative is offline. |
NAME | specifies the representative's name without the '.' or 'X' prefix. | |
OFFLINE | specifies whether the representative is online or offline as follows: '.' = online, 'X' = offline. | |
ORDER_PER_HOUR | specifies the number of orders per hour for this representative. | |
CALLS_PER_HOUR | specifies the number of calls per hour for this representative. | |
CALL_DURATIONL | specifies the minimum call duration for the group. | |
CALL_DURATION1 | specifies the group call duration for the first quartile. | |
CALL_DURATIONM | specifies the median call duration for the group. | |
CALL_DURATION3 | specifies the group call duration for the third quartile. | |
CALL_DURATIONH | specifies the maximum call duration for the group. | |
SET_ORDER | specifies how the bars are to be sorted. It is computed as follows: |
You can change the data in the SAS program and rerun it to see how the changes affect the dashboard. You can also replace the hard coded data with a query that pulls similar data from your live databases to plot on the dashboard. However, the data from your databases must be similar and you must use the same variable names.
Here are some notes on how this sample generates the plots for the dashboard:
To generate PLOT1, the GCHART procedure is used to create a horizontal bar chart. The bars that need attention are annotated with orange and red dots to the left of the bars. The bars are also annotated with the actual value (data1_anno
) to the right of the bars. Custom titles are placed along the top of the chart using the Annotate function='label'
. Annotated areas are created behind the bar chart by specifying large annotated function='bar'
areas of specific colors behind the chart (data1_anno2
).
When the GCHART procedure is run, ref=1
is used to put a reference line at 100%, and the HTML= option is used to identify the variable that contains the HTML chart tip and drill-down information for the individual bars. The TITLE1, TITLE2, and TITLE3 statements are used to add white space around the chart in order to reserve room for the custom annotation. Notice that the midpoint axis is hard coded so that the order of the bars corresponds to the order of the sparklines in PLOT2. It is important to ensure that the order of the bar chart and sparklines match, since these charts complement one another.
To generate the Overall Rep Performance sparklines, the GPLOT procedure is used to overlay three lines on the same plot. To make the lines appear one above the other, an offset is added successively to the Y coordinate of each line. Axis statements are used to suppress the normal GPLOT axis. For the sparkline, a reference line is added on the vertical axis at the 100% target value to allow the viewer to see how the actual value compares to the target value over time.
Since the sparklines do not have markers, chart tips and drill-down links cannot be added to the the sparklines. To provide drill-down functionality, an invisible annotated bar is added around the entire graph area. The viewer can click on the bar to drill down to the detailed data.
To generate PLOT3, the GCHART procedure is used to generate a bar chart. The SUBGROUP option is used to shade the segments of the bar to correspond to the good, excessive, and critical ranges. A red or orange dot is annotated beside the bar chart if it needs attention (excessive or critical). The annotated dot is created by using the W character of the SAS/GRAPH marker software font and annotating that text using the label function.
Rather than the bar representing the actual value, in this case, the bar represents the good, excessive, and critical ranges. A line is annotated on the bar to represent the actual value. The Annotate MOVE and DRAW functions are used to create the annotated line. Annotated text labels and an annotated line are used to create the equation to the right of the bar chart. The Annotate MOVE and DRAW functions are used to draw the annotated line. An angled title with blank text and a large height is used to create the white space to the right of the bar chart. The GCHART HTML= option is used to create a drill-down link on the bar itself.
To generate PLOT4, the GCHART procedure is used to create sparklines that are similar to those that were created in PLOT2. Annotated text is then used to place the table to the left. The two rows in the table correspond to the two sparklines; therefore, the Annotate facility uses the data coordinate system (YSYS='2') and the same 'y_position' variable as the sparklines. Blank, angled titles are used to create white space around the plot to reserve space for annotations. A large amount of white space is reserved on the left side of the plot to reserve space for the annotated table.
As was done in PLOT2, because the sparklines have no markers, to create drill-down functionality, an invisible annotated bar is added around the entire graph area. The viewer can click on the bar to drill down to the detailed data.
To create PLOT5 and PLOT6, the GCHART procedure is used to create a vertical bar chart. Instead of letting the GCHART procedure group the bars, a character string in the data is used to group the data. Normally, the GCHART procedure arranges these character strings in alphabetical order. However, in this case, an AXIS statement is used to specify the desired order of the groups as follows:
order=('0-4' '5-9' '10-14' '15-19' '20-24' '25-29' '30-34')
The GCHART HTML= option is used to identify the variable that contains the HTML HREF tags for the drill-down and chart tip information.
To generate PLOT7, the GCHART procedure is used to draw the horizontal axes and to arrange the bars and boxes in the desired (ascending) order of Orders_Per_Hour+(Calls_Per_Hour/100), from top to bottom. Annotate is then used to draw the box plots. Here are the variables that are used to control the geometry of the custom annotated box plots:
CALL_DURATIONL | (group minimum) |
CALL_DURATION1 | (group first-quartile) |
CALL_DURATIONM | (group median) |
CALL_DURATION3 | (group third-quartile) |
CALL_DURATIONH | (group maximum) |
You can use the BOXPLOT procedure to calculate these variables from your raw data and then input them into this custom dashboard code.
The Annotate HTML variable is used to add drill-down links and chart tips to the the custom annotated box/bars. The user can click on them to drill down to the detailed data. On the box/bar midpoint axis, an AXIS statement is used to suppress the default values. Custom text labels for the name, orders, and calls are then annotated on the axis. The HTML variable in the Annotate data set is used to add a link on the name labels so that a viewer can click on a name to send a message to that representative.
In the dashboard in this sample, most of the overall title information is located at the bottom edge. The Annotate LABEL function is used to create these titles. Since the title slide is replayed into area 0 in the custom GREPLAY template, annotations can be added anywhere on the dashboard. The Annotate MOVE and DRAW functions are used to draw dividing lines to separate and group the pieces of the dashboard. Although the GREPLAY procedure can draw outlines around each individual GREPLAY area, Annotate provides more flexibility in grouping items visually. For example, notice that PLOT4, PLOT5, and PLOT6 are grouped by a simple outline that is drawn around all three of them. This cannot be done using the GREPLAY procedure.
When the plots are generated, the output is stored in GRSEGs. The GREPLAY procedure is then used to assemble the plots onto a single page using a custom GREPLAY template. The custom GREPLAY template is defined by specifying four X and Y coordinates for the rectangular area that will hold each graph. The units of the X and Y coordinates are the percentage of the total length of the axis where the coordinate is located, with 0,0 being at the bottom-left corner. Each rectangular area is assigned an ID number. Here are the coordinates that place the Rep Utilization chart in the middle-left of the dashboard in area 4:
4/llx = 0 lly =38 ulx = 0 uly =53 urx = 60 ury =53 lrx = 60 lry =38
where LLX
is the lower-left X coordinate, LLY
is the lower-left Y coordinate, and so on.
The GREPLAY procedure allows you to overlap the rectangular areas. Area 0 is defined as covering the entire dashboard. The GSLIDE procedure is used to place the title text near the top-right corner, and the Annotate facility is used to create a custom legend. Here are the coordinates:
0/llx = 0 lly = 0 ulx = 0 uly =100 urx =100 ury =100 lrx =100 lry = 0
Here is the GREPLAY procedure TREPLAY (template replay) action statement that draws the appropriate charts into the desired areas
of the custom GREPLAY template:
treplay 1:plot1 2:plot2 3:plot3 7:plot7 4:plot4 5:plot5 6:plot6 0:titles
Note: For convenience and code readability, spacing is used in the TREPLAY action statement to make the layout correspond to the actual location of the charts in relation to one another.
Notice that the PLOT7 GRSEG is played into area 7.
For each chart on the dashboard, this sample adds a drill-down link to a detailed table of the data for the chart. To keep this sample self-contained, the drill-down tables are placed on the same output page just below the dashboard, and each table is identified using an HTML anchor as follows:
ods html anchor="&anchor_name";
However, in your case, these drill-down links would likely be to separate detail tables or charts on a separate page.
In this sample, the anchor names are created when the bar charts are created as follows:
htmlvar= 'title='|| quote( 'Quarter: '|| trim(left(quarter)) ||'0D'x|| 'Target: '|| trim(left(put(target,&value_format))) ||'0D'x|| 'Actual: '|| trim(left(put(actual,&value_format))) ||'0D'x|| 'Actual as Percent of Target: '|| trim(left(put(percent_of_target,percent6.0))) ) || ' '|| 'href="#'||trim(left("&grseg_name"))||'"';
To keep the naming simple and consistent, the GRSEG names are used as the anchor names. While you are viewing the dashboard, if you mouse over each bar, the data value for the bar is displayed in the HTML chart tip. If you click a bar, your browser scrolls down to the HTML anchor to show you the table of the data for the bar.
These sample files and code examples are provided by SAS Institute Inc. "as is" without warranty of any kind, either express or implied, including but not limited to the implied warranties of merchantability and fitness for a particular purpose. Recipients acknowledge and agree that SAS Institute shall not be liable for any damages whatsoever arising out of their use of this material. In addition, SAS Institute will provide no support for the materials contained herein.
/* This example uses SAS/GRAPH software to generate a version of the telesales
performance dashboard described on p. 199 of "Information Dashboard Design"
(Few, Stephen. 2006. Sebastopol, CA. O'Reilly Media, Inc.). */
/* Specify the name for the output file. */
%let name=telesalesDashPortal;
filename odsout '.';
%let backcolor=cxFFFFEb;
%let textcolor=cxAEBB51;
%let good=graybb;
%let excessive=graydb;
%let critical=grayef;
%let red_dot=red;
%let orange_dot=orange;
/* Define fonts for indicator and dashboard titles and text. */
%let ftitle='swissb';
%let ftext='swissl';
/* Define the location of the HTML page that supplies drill-down details
for the indicator. If you don't have Internet access, you must put
the target HTML file where your browser can access it, then change the
following URL to point to your location. */
%let hardcoded_drilldown=http://support.sas.com/rnd/datavisualization/dashboards/generic_drilldown.htm;
/***************************************************************************/
/* Create sample data sources for the dashboard indicators. */
/* Data for plot1 (Overall Rep Performance) */
data data1;
format percent_of_target percentn7.0;
input category $ 1-15 percent_of_target actual_value;
datalines;
Hold Time .95 63
Call Duration 1.80 4756
Abandonments .80 27
run;
/* Data for plot2 (Today (compared to target) sparklines) */
data data2;
input category timestamp percent_of_target;
length category_name $20;
if (category eq 1) then category_name='Abandonments';
else if (category eq 2) then category_name='Call Duration';
else if (category eq 3) then category_name='Hold Time';
datalines;
1 1 .50
1 2 .75
1 3 .50
1 4 .60
1 5 1.25
1 6 .70
1 7 .66
1 8 .60
2 1 .80
2 2 .75
2 3 .70
2 4 1.00
2 5 1.20
2 6 .90
2 7 1.25
2 8 1.30
3 1 .75
3 2 .70
3 3 .65
3 4 .75
3 5 1.10
3 6 .90
3 7 .85
3 8 .80
;
run;
/* Data for plot3 (Rep Utilization) */
data data3;
format value percentn7.0;
input barnum segment value;
datalines;
1 1 .85
1 2 .05
1 3 .10
;
run;
/* data for plot4 sparklines (Volume) */
data data4;
input category timestamp percent_of_target;
length category_name $20;
if (category eq 1) then category_name='Order Count';
else if (category eq 2) then category_name='Call Count';
datalines;
2 1 .50
2 2 .95
2 3 .60
2 4 .60
2 5 1.15
2 6 1.10
2 7 1.0
2 8 1.0
1 1 .70
1 2 .75
1 3 .70
1 4 1.00
1 5 1.20
1 6 1.10
1 7 1.20
1 8 1.20
;
run;
/* Data for the table to the left of plot4 (Volume) */
data data4_table;
input cat_name $ 1-20 category this_hour today this_month;
datalines;
Call Count 2 373 1322 25934
Order Count 1 234 925 17834
;
run;
/* Data for plot5 (Mean Hourly Calls per Rep Today)*/
data data5;
input calls_per_rep $ 1-5 reps;
calls_per_rep=trim(left(calls_per_rep));
datalines;
0-4 0
5-9 0
10-14 5
15-19 10
20-24 8
25-29 1
30-34 0
;
run;
/* Data for plot6 (Mean Hourly Orders per Rep Today) */
data data6;
input orders_per_rep $ 1-5 reps;
orders_per_rep=trim(left(orders_per_rep));
datalines;
0-3 0
4-7 3
8-11 6
12-15 11
16-19 4
20-23 1
24-27 0
;
run;
/* Data for plot7 (Individual Rep Performance) */
/* Since SAS/GRAPH does not have a procedure to create horizontal boxplots,
this plot uses the Annotate facility to draw them. If real data points
were available, the numbers for the boxplots could be calculated as
follows:
proc boxplot data=data7;
plot Call_Duration*Name /
outhistory=data7_anno;
run;
Because actual data points are not available in this sample, estimated
values are used. */
data data7;
input Text $ 1-20 Orders_Per_Hour Calls_Per_Hour Call_DurationL Call_Duration1 Call_DurationM Call_Duration3 Call_DurationH;
Offline=substr(Text,1,1);
if Offline eq '.' then Offline=' ';
Name=trim(left(substr(Text,3,18)));
sort_order=Orders_Per_Hour+(Calls_Per_Hour/100);
datalines;
. Jacobs, S 5 10 0.5 6.1 8.2 11.0 17.5
. McKinsey, J 5 13 1.0 5.8 9.5 13.0 20.2
. Smith, V 6 12 3.0 8.8 11.0 13.1 17.2
. Wilcox, R 9 14 2.0 6.1 9.0 11.5 16.0
. Clark, P 10 14 1.5 5.5 9.0 12.6 18.7
. Simons, B 10 16 2.5 6.0 9.3 11.8 14.8
. Newman, A 11 15 3.0 6.0 8.5 10.5 14.0
. Bailey, S 11 16 2.9 6.8 9.0 11.0 18.1
. Barclay, T 11 17 0.8 4.5 7.0 12.0 17.0
. Jimenez, J 12 16 0.9 6.3 8.4 10.6 16.5
X Chou, A 12 17 2.0 4.5 5.9 9.0 19.0
. Kata, H 12 17 2.9 5.0 7.5 8.8 16.5
. Silverman, C 13 18 3.0 7.0 8.0 10.0 15.0
. Schuster, P 13 18 3.0 6.5 8.0 11.0 17.0
. Truman, M 13 19 2.3 6.1 7.8 10.5 16.3
X Pierce, B 14 19 0.4 5.0 7.0 10.0 15.0
. Fisher, J 14 20 2.7 4.5 6.3 9.0 14.3
. Jung, T 14 20 2.1 5.0 6.5 10.0 18.0
. English, S 15 21 2.2 6.8 7.5 10.0 14.0
. Wiley, P 15 21 2.0 5.7 8.5 12.0 17.0
. Johnson, N 16 21 3.0 5.0 8.7 13.0 15.0
X Lucas, J 16 22 3.0 5.5 8.3 13.0 17.0
. Forester, R 17 23 3.0 7.0 8.5 11.0 16.5
;
run;
/* The following macro variables are used by multiple plots. */
proc sql noprint;
select unique count(*) into :repcount from data7;
select unique count(*) into :reponline from data7 where offline^="X";
select unique &reponline/&repcount into :reppercent from data7;
quit;
/**************************************************************************/
goptions device=gif;
goptions cback=&backcolor;
goptions noborder;
/* Delete all GRSEGs in the current session to ensure that indicators use
the expected names. If a name is already in use, then an attempt to create
a new GRSEG using that name it will add a number to the name. In that case,
the subsequent GREPLAY will be placing the wrong GRSEGs into the dashboard.
Note: The macro code just checks whether there are any gsegs to delete. If
it tried to delete specific entries and none existed, then you would get an
error message: "ERROR: Member-name GSEG is unknown." */
%macro delcat(catname);
%if %sysfunc(cexist(&catname)) %then %do;
proc greplay nofs igout=&catname;
delete _all_;
run;
%end;
quit;
%mend delcat;
%delcat(work.gseg);
/**************************************************************************/
/* Create the individual dashboard indicators. */
/* Set the NODISPLAY option to save the plots as GRSEGS
without writing them to the output. */
goptions nodisplay;
/* plot1 - Overall Rep Performance (bar chart) */
/* Create chart tip and drill-down for the bars in plot1 */
data data1;
set data1;
length myhtml $ 250;
myhtml='title='||quote(
trim(left(category)) ||'0D'x||
trim(left(put(percent_of_target,percentn7.0)))||' = Percent of Target '||'0D'x||
trim(left(put(actual_value,comma12.0)))||' = Actual Value'
) ||' '|| 'href="'||"&hardcoded_drilldown"||'"';
run;
/* Create annotation that is specific to each individual bar. */
data data1_anno;
set data1;
length function $8 color style $20 text $40;
xsys='2'; ysys='2'; hsys='3';
ysys='2'; midpoint=category;
xsys='3'; x=0; position='6';
/* Annotate the bar values to the right of the plot.
Horizontal bar charts have some built-in capability to do this,
but this sample attempts to replicate Few's, so annotating the
values to gives more precise control. */
function='label'; style="&ftext"; size=5; color="black";
xsys='3'; x=93; position='4';
text=trim(left(put(actual_value,comma8.0)));
output;
/* Annotate colored dots beside bars that need attention.
The 'dot' is the 'W' character of the MARKER font supplied
with SAS/GRAPH software. (If this bar needs no attention,
then the dot is the same color as the background, which
makes it invisible.) */
xsys='3'; x=6; function='move';
output;
ysys='7'; y=8.0;
output;
function='label'; style='marker'; text='W'; size=7;
if percent_of_target > 1.75 then color="&red_dot";
else if percent_of_target > .90 then color="&orange_dot";
else color="&backcolor";
output;
run;
/* Create annotation that is not specific to the individual
bars, but more general to the whole graph. */
data data1_anno2;
length function $8 color style $20 text $40;
hsys='3'; when='a';
/* Annotate the main title and a few items of text over plot1. */
function='label';
color="&textcolor"; style="&ftext";
size=10;
xsys='3'; x=1;
ysys='3'; y=96;
position='6';
text='Overall Rep Performance';
output;
color="black"; style="&ftext";
size=6;
ysys='1'; y=100;
xsys='1'; x=0;
position='3';
text='% of Target';
output;
xsys='1'; x=100;
position='3';
text=' Actual';
output;
/* Annotate the three color-range areas behind the bar chart. */
xsys='2'; ysys='1'; when='b'; style='solid';
function='move'; x=0; y=0;
output;
function='bar'; color="&good"; line=1; size=.1; x=.9; y=100;
output;
function='move'; x=.9; y=0;
output;
function='bar'; color="&excessive"; line=1; size=.1; x=1.75; y=100;
output;
function='move'; x=1.75; y=0;
output;
function='bar'; color="&critical"; line=1; size=.1; x=2.50; y=100;
output;
run;
goptions xpixels=300 ypixels=175;
goptions gunit=pct ftitle=&ftitle ftext=&ftext htitle=4 htext=5.5;
title1 h=18 " ";
title2 a=90 h=11 " ";
title3 a=-90 h=18 " ";
footnote1 h=5 " ";
axis1 label=none order=('Hold Time' 'Call Duration' 'Abandonments') offset=(8,8);
axis2 label=none order=(0 to 2.5 by .5) value=(h=5) minor=none offset=(0,0);
pattern1 v=s c=gray55;
proc gchart data=data1 anno=data1_anno;
hbar category / nostats
maxis=axis1
raxis=axis2
caxis=gray99
ref=1 cref=black
width=3
space=3
type=sum sumvar=percent_of_target
anno=data1_anno2
html=myhtml
des=""
name="plot1";
run;
/* plot2 */
/* "Sparkline" graphs are challenging to create with SAS/GRAPH software.
This example uses the GPLOT procedure, and for each line an offset is
added so that the lines are plotted aboveor below each other, rather
than on top of each other. Also note that each line is plotted as a
percentage of the target, rather than as raw seconds. (If you plot as
raw seconds, it is impossible to scale the lines so they can be compared.) */
data data2; set data2;
y=(2*category)-1+percent_of_target;
run;
/* Annotate the title, to get it in the exact desired location,
to line up with some text in graph to the left */
data data2_anno;
length function $8 color style $20 text $40;
xsys='3'; ysys='3'; hsys='3'; when='a';
function='label'; position='5'; style="&ftext";
size=5.75; color="black";
x=50; y=82; text='Today (compared to target)';
output;
size=5.5; color="gray77";
x=50; y=16; text='(Sparklines scaled as % of target)';
output;
/* Annotate an invisible drill-down covering entire graph area (because
you can't do drill-downs on plot lines without markers. */
xsys='1'; ysys='1';
x=0; y=0; function='move';
output;
html='title="drilldown"' ||' '||
'href="'||"&hardcoded_drilldown"||'"';
x=100; y=100; function='bar'; style='empty'; when='b'; color="&backcolor";
output;
run;
goptions xpixels=200 ypixels=200;
goptions gunit=pct ftitle=&ftext ftext=&ftext htitle=8 htext=6.5;
axis1 label=none order=(0 to 8 by 2) value=none major=none minor=none style=0;
axis2 label=(' ') order=(1 to 12 by 2) value=none major=none minor=none style=0;
symbol v=none i=join width=.1 c=black repeat=100;
title1 h=8 " ";
title2 " ";
title3 a=90 h=5 " ";
title4 a=-90 h=5 " ";
footnote h=.5 " ";
proc gplot data=data2 anno=data2_anno;
plot y*timestamp=category /
noframe
vaxis=axis1
haxis=axis2
vref= 2 4 6
cvref=graycc
nolegend
des=""
name="plot2";
run;
/* plot3 */
data data3;
set data3;
length myhtml $ 250;
myhtml='title="drilldown"' ||' '||
'href="'||"&hardcoded_drilldown"||'"';
run;
data data3_anno;
length function $8 color style $20 text $40;
xsys='3'; ysys='3'; hsys='3'; when='a';
function='label';
xsys='3'; ysys='3';
x=1; y=84; position='6'; size=20; style="&ftext"; color="&textcolor";
text="Rep Utilization";
output;
xsys='3'; ysys='3';
color="black";
function='label'; size=12;
x=85;
y=70;
style="&ftext";
text="Reps Online: "; position='4';
output;
x=x+1;
style="&ftitle";
text=trim(left("&reponline")); position='6';
output;
x=85;
y=50;
style="&ftext";
text="Reps Today: "; position='4';
output;
x=x+1;
style="&ftitle";
text=trim(left("&repcount")); position='6';
output;
x=92.5;
y=60;
text="="; position='4';
output;
text=trim(left(put(&reppercent,percent7.0))); position='6';
output;
function='move'; x=86; y=58;
output;
function='draw'; x=x+2.5; color="black"; line=1; size=.1;
output;
/* Colored dot at left side of chart */
ysys='2'; midpoint=1;
xsys='3'; x=2;
function='move';
output;
function='label'; style='marker'; text='W'; size=14 ;
if &reppercent < .85 then color="&red_dot";
else if &reppercent < .90 then color="&orange_dot";
else color="&backcolor";
output;
position='5'; text=''; color="black";
function='move'; xsys='2'; x=&reppercent; ysys='1'; y=0;
output;
function='draw'; size=2.5; line=1; y=100;
output;
run;
goptions xpixels=600 ypixels=125;
goptions gunit=pct ftitle=&ftext ftext=&ftext htitle=15 htext=10;
axis1 label=none value=none offset=(8,8);
axis2 label=none order=(.75 to 1.00 by .05) minor=none offset=(0,0);
pattern1 v=s c=&critical;
pattern2 v=s c=&excessive;
pattern3 v=s c=&good;
title h=20 " ";
title2 a=-90 h=99 " ";
title3 a=-90 h=30 " ";
title4 a=90 h=13 " ";
footnote;
proc gchart data=data3 anno=data3_anno;
hbar barnum / discrete
type=sum
sumvar=value
subgroup=segment
width=8
space=0
coutline=gray55
maxis=axis1
raxis=axis2
caxis=gray99
nolegend
nostats
noframe
html=myhtml
des=""
name="plot3";
run;
/* plot4 */
/* Annotate the title to get it in the exact desired location,
so that it lines up with text in graph to the left. */
data data4_table; set data4_table;
y_position=(2*category)-1;
run;
data data4_anno; set data4_table;
length function $8 color style $20 text $40;
xsys='3'; ysys='3'; hsys='3'; when='a';
function='label'; style="&ftext";
if _n_ eq 1 then do;
x=1; y=90; position='6'; size=20; style="&ftext"; color="&textcolor";
text="Volume";
output;
position='5'; style="&ftext";
size=11; color="black";
xsys='1'; x=50; y=85; text='Per Hour Today';
output;
function='label';
size=11; color="black";
style="&ftitle";
xsys='3';
position='4';
ysys='3';
y=85;
x=35; text='This Hour';
output;
x=48; text='Today';
output;
x=62; text='This Month';
output;
end;
ysys='2'; y=y_position;
size=11;
style="&ftitle"; position='3';
x=12; text=trim(left(cat_name));
output;
size=12;
style="&ftext"; position='1';
x=35; text=trim(left(put(this_hour,comma12.0)));
output;
x=48; text=trim(left(put(today,comma12.0)));
output;
x=62; text=trim(left(put(this_month,comma12.0)));
output;
run;
data data4; set data4;
y_position=(2*category)-1+percent_of_target;
run;
/* Annotate an invisible drill-down covering the entire graph area (because
you can't do drill-downs on plot lines without markers. */
data data4_anno2;
xsys='3'; ysys='3';
x=0; y=0; function='move';
output;
html='title="drilldown"' ||' '||
'href="'||"&hardcoded_drilldown"||'"';
x=100; y=100; function='bar'; style='empty'; when='b'; color="&backcolor";
output;
run;
goptions xpixels=600 ypixels=125;
goptions gunit=pct ftitle=&ftext ftext=&ftext htitle=5 htext=4;
axis1 label=(h=10 ' ')
order=(0 to 6 by 2) value=none
major=none minor=none style=0;
axis2 label=none order=(1 to 12 by 2) value=none major=none minor=none style=0;
symbol v=none i=join width=.1 c=black repeat=100;
/* This controls the white-space around the sparkline plot.
In particular, the title2 statement adds a lot of space to the left. */
title1 h=1 " ";
title2 a=90 h=100 " ";
title3 a=-90 h=2 " ";
footnote h=.1 " ";
proc gplot data=data4 anno=data4_anno;
plot y_position*timestamp=category /
noframe
vaxis=axis1
haxis=axis2
vref= 2 4
cvref=graycc
nolegend
anno=data4_anno2
des=""
name="plot4";
run;
/* plot5 */
data data5; set data5;
length myhtml $ 250;
myhtml='title='||quote(
trim(left(reps)) ||' reps made '|| trim(left(calls_per_rep))||' mean hourly calls today.'
) ||' '|| 'href="'||"&hardcoded_drilldown"||'"';
run;
goptions xpixels=275 ypixels=200;
goptions gunit=pct ftitle=&ftitle ftext=&ftext htitle=6 htext=3.75;
title1 h=1 " ";
title2 h=3 a=-90 " ";
footnote;
axis1 label=(h=4 'Reps') order=(0 to 12 by 2) minor=none;
axis2 label=(h=4 'Mean Hourly Calls per Rep Today')
value=(h=3.2) order=('0-4' '5-9' '10-14' '15-19' '20-24' '25-29' '30-34');
pattern1 v=s c=graybb;
proc gchart data=data5;
vbar calls_per_rep /
type=sum
sumvar=reps
raxis=axis1
maxis=axis2
coutline=gray55
width=12
space=0
noframe
html=myhtml
des=""
name="plot5";
run;
/* plot6 */
data data6; set data6;
length myhtml $ 250;
myhtml='title='||quote(
trim(left(reps)) ||' reps had '|| trim(left(orders_per_rep))||' mean hourly orders today.'
) ||' '|| 'href="'||"&hardcoded_drilldown"||'"';
run;
goptions xpixels=275 ypixels=200;
goptions gunit=pct ftitle=&ftitle ftext=&ftext htitle=6 htext=3.75;
title1 h=1 " ";
title2 h=3 a=-90 " ";
footnote;
axis1 label=(h=4 'Reps') order=(0 to 12 by 2) minor=none;
axis2 label=(h=4 'Mean Hourly Orders per Rep Today')
value=(h=3.2) order=('0-3' '4-7' '8-11' '12-15' '16-19' '20-23' '24-27');
pattern1 v=s c=graybb;
proc gchart data=data6;
vbar orders_per_rep /
type=sum
sumvar=reps
raxis=axis1
maxis=axis2
coutline=gray55
width=12
space=0
noframe
html=myhtml
des=""
name="plot6";
run;
/* plot7 */
proc sort data=data7 out=data7;
by sort_order;
run;
data data7_anno;
set data7;
badrank=_n_;
run;
data data7_anno; set data7_anno;
xsys='2'; ysys='2'; hsys='3'; when='a';
length function color $8 style $20;
length html $ 250;
html='title='||quote(
trim(left(Name)) ||'0D'x||
trim(left(Orders_Per_Hour))||' Orders Per Hour '||'0D'x||
trim(left(Calls_Per_Hour))||' Calls Per Hour'||'0D'x||
'Median Call Duration: '|| trim(left(Call_DurationM))||' minutes'
) ||' '|| 'href="'||"&hardcoded_drilldown"||'"';
color="gray";
size=.1;
/* draw the outer box */
ysys='2';
midpoint=Name;
function='move'; x=Call_DurationL;
output;
function='move'; ysys='7'; y=-.3;
output;
function='bar'; line=0; style='solid'; x=Call_DurationH; y=+.6;
output;
/* draw the inner box */
ysys='2';
midpoint=Name;
function='move'; x=Call_Duration1;
output;
function='move'; ysys='7'; y=-.7;
output;
function='bar'; line=0; style='solid'; x=Call_Duration3; y=+1.4;
output;
/* Median Line (median is Call_DurationM, mean is Call_DurationX) */
color="white";
ysys='2';
midpoint=Name;
function='move'; x=Call_DurationM;
output;
function='move'; ysys='7'; y=-.7;
output;
function='bar'; line=0; style='solid'; x=Call_DurationM; y=+1.4;
output;
/* This sample sends email rather than an instant message. Notice that a
dummy email address is supplied.In a production dashboard, you would
want to have a field in your data with a real email address rather than
a static address in the code. */
length html $ 250;
html='title='||quote(
trim(left(Name)) ||'0D'x||
'Median Call Duration: '|| trim(left(Call_DurationM))||' minutes'
) ||' '|| 'href="mailto:'||trim(left(scan(Name,1,',')))||
'@some_telesales_company.com?subject=Mail from Telesales Dashboard"';
/* Labels along the left */
function='label'; size=2.1; color="black"; style="&ftext";
if badrank <= 4 then do;
color='red';
style="&ftext";
end;
ysys='2'; midpoint=Name;
xsys='3';
position='6';
x=2; text=trim(left(Offline));
output;
x=6; text=trim(left(Name));
output;
/* Annotate text labels */
html='';
position='4';
x=35; text=trim(left(Orders_Per_Hour));
output;
x=45; text=trim(left(Calls_Per_Hour));
output;
run;
data data7_anno2;
length function $8 color style $20 text $40;
xsys='3'; ysys='3'; hsys='3'; when='a';
function='label'; position='6';
y=99; x=2;
style="&ftext"; size=3.5; color="&textcolor"; text='Individual Rep Performance';
output;
style="&ftext"; size=2.0; color="black";
y=93;
x=25.5; text='Orders';
output;
x=40; text='Calls';
output;
y=91;
x=6; text='Name';
output;
x=26; text='Per Hr';
output;
x=40; text='Per Hr';
output;
x=58; text='Call Duration (minutes)';
output;
y=3; x=2; text='(X = Currently offline)';
output;
run;
goptions gunit=pct ftitle=&ftitle ftext=&ftext htitle=4 htext=2;
goptions xpixels=400 ypixels=685;
title1 ls=4.5 " ";
title2 a=90 h=20pct " ";
footnote;
axis1 offset=(2,2) label=none value=none;
axis2 order=(0 to 21 by 3) major=(height=.5) minor=none label=('Call Duration (minutes)') offset=(0,0);
pattern1 v=e c=white;
proc gchart data=data7 anno=data7_anno;
hbar Name /
type=sum sumvar=sort_order ascending nostats
maxis=axis1
raxis=axis2
caxis=gray99
anno=data7_anno2
des=""
name="plot7";
run;
/***************************************************************************/
data titlanno;
length function $8 color style $20 text $100;
length html $ 250;
xsys='3'; ysys='3'; hsys='3'; when='a';
/* Annotate the main title at the bottom/left of the dashboard. */
function='label'; position='6';
x=0; y=2.5; size=4.3; style="&ftext"; color="&textcolor"; text='Telesales Dashboard';
output;
/* Annotate the light green lines that group and separate the various parts
of the dashboard. */
line=1; size=.2; color="&textcolor";
function='move'; x=0; y=4.5;
output;
function='draw'; x=100;
output;
function='move'; x=0; y=55;
output;
function='draw'; x=60;
output;
function='move'; x=0; y=70;
output;
function='draw'; x=60;
output;
function='move'; x=60; y=4.5;
output;
function='draw'; y=100;
output;
/* Annotate a Help button in the bottom right of the dashboard.
Assign the HTML drill-down so that when the user clicks
the Help button, they will see information about the dashboard. */
html='title='||quote('Help')||' '||'href="'||"&hardcoded_drilldown"||'"';
function='move'; x=90; y=0;
output;
function='bar'; line=0; size=.1; style='solid'; color="&backcolor"; x=x+6; y=y+3;
output;
html='';
function='move'; x=90; y=0;
output;
function='bar'; line=0; size=.1; style='empty'; color="gray77"; x=x+6; y=y+3;
output;
function='label'; style="&ftext"; size=2.0; x=90+3; y=2; position='5'; text='Help';
output;
/* Annotate a helpful message below the list of rep names, to let the user know
they can click the rep name to send them a message. */
function='label'; style="&ftext"; size=2.0; x=62; y=1.5; position='6';
text='Click rep to send instant message';
output;
/* Annotate a custom color legend at the bottom middle of the dashboard.
This color legend is shared by the left top and left middle charts. */
function='label'; style="&ftext"; size=2.0;
x=35; y=1.5; position='6';
text='( Good; Excessive; Critical )';
output;
function='label'; size=1.1; positon='5'; y=1.1;
style='marker'; text='U';
x=36.3; color="&good";
output;
x=42.6; color="&excessive";
output;
x=51.8; color="&critical";
output;
style='markere'; color="gray77"; text='U';
x=36.3;
output;
x=42.6;
output;
x=51.8;
output;
run;
/* Use the GSLIDE procedure to display the custom text and graphics. */
title;
footnote;
goptions xpixels=925 ypixels=685;
proc gslide des="" name="titles" anno=titlanno;
run;
/**************************************************************************/
goptions device=gif;
goptions xpixels=925 ypixels=685;
goptions display;
goptions border;
ODS LISTING CLOSE;
ODS HTML path=odsout body="&name..htm" (title="Telesales Dashboard" no_top_matter no_bottom_matter)
style=minimal gtitle gfootnote;
/* Create a custom template, with seven areas for the plots
and one area for the title slide. */
proc greplay tc=tempcat nofs igout=work.gseg;
tdef murder des='Murder'
0/llx = 0 lly = 0
ulx = 0 uly =100
urx =100 ury =100
lrx =100 lry = 0
1/llx = 0 lly =70
ulx = 0 uly =100
urx = 40 ury =100
lrx = 40 lry =70
2/llx = 40 lly =70
ulx = 40 uly =100
urx = 60 ury =100
lrx = 60 lry =70
3/llx = 0 lly =55
ulx = 0 uly =70
urx = 60 ury =70
lrx = 60 lry =55
4/llx = 0 lly =38
ulx = 0 uly =53
urx = 60 ury =53
lrx = 60 lry =38
5/llx = 0 lly =5
ulx = 0 uly =40
urx = 30 ury =40
lrx = 30 lry =5
6/llx = 30 lly =5
ulx = 30 uly =40
urx = 60 ury =40
lrx = 60 lry =5
7/llx = 60 lly = 5
ulx = 60 uly =100
urx = 100 ury =100
lrx = 100 lry = 5
;
/* Replay the individual indicators and the title slide into the custom template. */
template = murder;
treplay
1:plot1 2:plot2
3:plot3 7:plot7
4:plot4
5:plot5 6:plot6
0:titles
des=""
name="&name";
run;
quit;
ODS HTML CLOSE;
ODS LISTING;
These sample files and code examples are provided by SAS Institute Inc. "as is" without warranty of any kind, either express or implied, including but not limited to the implied warranties of merchantability and fitness for a particular purpose. Recipients acknowledge and agree that SAS Institute shall not be liable for any damages whatsoever arising out of their use of this material. In addition, SAS Institute will provide no support for the materials contained herein.
Source: Stephen Few, Information Dashboard Design (Sebastopol, CA: O'Reilly Media, Inc., 2006), 199.
The following file contains SAS macro code to add the directory that contains the portlet version of the samples to a WebDAV repository. To download a copy, click on the link and select the Save option.
copyToWebDAV.sas
Type: | Sample |
Date Modified: | 2009-01-07 14:39:08 |
Date Created: | 2006-12-23 03:03:05 |
Product Family | Product | Host | SAS Release | |
Starting | Ending | |||
SAS System | SAS/GRAPH | All | 9.1 TS1M3 | 9.1 TS1M3 |