%let name=unemployment_by_state; /* Set your current-working-directory (to read/write files), if you need to ... %let rc=%sysfunc(dlgcdir('c:\someplace\public_html')); */ filename odsout '.'; /* Using data from: https://www.bls.gov/web/laus.supp.toc.htm https://www.bls.gov/web/laus/ststdsadata.txt Imitating map from: https://www.bls.gov/web/laus/mstrtcr1.gif */ %let c1=cx4d9221; %let c2=cxa1d76a; %let c3=cxe6f5d0; %let c4=cxfde0ef; %let c5=cxe9a3c9; %let c6=cxc51b7d; /* ranges for annotated color-bars along the side */ data ranges; input range range_min range_max; datalines; 1 0 .03 2 .03 .04 3 .04 .05 4 .05 .06 5 .06 .07 6 .07 1.0 ; run; /* annotate the color ranges, on the left and right of graph */ data anno_ranges_left; set ranges; length label $300 function x1space y1space display fillcolor layer $50; if range=1 then fillcolor="&c1"; if range=2 then fillcolor="&c2"; if range=3 then fillcolor="&c3"; if range=4 then fillcolor="&c4"; if range=5 then fillcolor="&c5"; if range=6 then fillcolor="&c6"; function='polygon'; display="fill"; layer="front"; y1space='datavalue'; x1space='wallpercent'; x1=0; y1=range_min; output; function='polycont'; x1space='datapercent'; x1=0; y1=range_min; output; if range_max=1.0 then do; y1space='wallpercent'; range_max=100; end; x1space='datapercent'; x1=0; y1=range_max; output; x1space='wallpercent'; x1=0; y1=range_max; output; run; data anno_ranges_right; set anno_ranges_left; x1=100; run; data anno_ranges; set anno_ranges_left anno_ranges_right; run; /* seasonally adjusted data */ filename textfile "../democd98/ststdsadata.txt"; /* or, the not seasonally adjusted data */ /* filename textfile "../democd98/ststdnsadata.txt"; */ data my_data (drop = i whole_line); infile textfile lrecl=300 pad firstobs=17; length state_name $50; format date date9.; format civilian_noninst_population total_civilian_labor_force comma10.0; format percent_of_population percentn7.2; format total_employed comma10.0; format percent_employed percentn7.2; format total_unemployed comma10.0; format percent_unemployed percentn7.2; length month $20; input month year; do i = 1 to 57; input whole_line $ 1-300; if trim(left(whole_line))^='' then do; date=input('15'||substr(month,1,3)||trim(left(year)),date9.); state_name=trim(left(scan(whole_line,1,'.'))); civilian_noninst_population=.; civilian_noninst_population=input(scan(whole_line,-7,' '),comma10.0); total_civilian_labor_force=.; total_civilian_labor_force=input(scan(whole_line,-6,' '),comma10.0); percent_of_population=.; percent_of_population=scan(whole_line,-5,' ')/100; total_employed=.; total_employed=input(scan(whole_line,-4,' '),comma10.0); percent_employed=.; percent_employed=scan(whole_line,-3,' ')/100; total_unemployed=.; total_unemployed=input(scan(whole_line,-2,' '),comma10.0); percent_unemployed=.; percent_unemployed=scan(whole_line,-1,' ')/100; output; end; end; run; proc sql noprint; /* Merge in the state abbreviations */ create table my_data as select unique my_data.*, us_states_attr.statecode from my_data left join mapsgfk.us_states_attr on my_data.state_name=us_states_attr.idname; /* Determine the most recent date (month & year) in the data */ select max(date) format=monname20. into :maxmon separated by ' ' from my_data; select max(date) format=year4. into :maxyear separated by ' ' from my_data; quit; run; data my_data; set my_data (where=(statecode^='')); run; proc sort data=my_data out=my_data; by state_name date; run; %macro do_state(statecode); ods html anchor="&statecode"; ods graphics / imagename="&name._&statecode"; data temp; set my_data (where=(statecode="&statecode")); run; /* determine the lowest unemployment for this state */ proc sql noprint; select min(percent_unemployed) format=percentn7.2 into :lowest separated by ' ' from temp; create table anno_lowest as select * from temp having percent_unemployed=min(percent_unemployed); select unique(state_name) into :state separated by ' ' from temp; quit; run; /* Some states have the same lowest unemployment during multiple months. In those cases, take the most recent one (otherwise the graph might show overlapping labels, and look cluttered and confusing. */ proc sort data=anno_lowest out=anno_lowest; by statecode date; run; data anno_lowest; set anno_lowest; by statecode; if last.statecode then output; run; /* annotate some things to help the user easily see the lowest value */ ods escapechar='^'; data anno_lowest; set anno_lowest; length label $300 function x1space y1space anchor linecolor textcolor layer $50; /* blue dashed reference line */ function='line'; linethickness=1; linecolor='dodgerblue'; linepattern='shortdash'; x1space='wallpercent'; y1space='datavalue'; x2space='wallpercent'; y2space='datavalue'; x1=0; y1=percent_unemployed; x2=100; y2=y1; layer="back"; /* be sure to use 'nowall' with this */ output; /* blue arrow, pointing to the line */ function='text'; x1space='wallpercent'; y1space='datavalue'; x1=100; y1=percent_unemployed; anchor='left'; textcolor="dodgerblue"; textsize=10; textweight='normal'; width=100; widthunit='percent'; label="^{unicode '25c4'x}^ "||trim(left(put(percent_unemployed,percent7.1))); layer="front"; output; /* date, rotated 90 degrees, under the lowest unemployment */ function='text'; x1space='datavalue'; y1space='datavalue'; x1=date; y1=percent_unemployed-.004; anchor='right'; rotate=90; label=trim(left(substr(month,1,3)))||' '||trim(left(year)); output; /* circle marker, under lowest unemployment */ function='oval'; linethickness=2; linecolor='dodgerblue'; linepattern='solid'; height=11; width=11; heightunit='pixel'; widthunit='pixel'; anchor='center'; x1space='datavalue'; y1space='datavalue'; y1=percent_unemployed; x1=date; output; run; /* Since ods graphics footnote does not support url links yet, annotate the footnote (annotated text supports url links). */ data anno_footnote; length label $100 anchor x1space y1space function $50 textcolor $12; function='text'; x1=50; y1=3; x1space='wallpercent'; y1space='graphpercent'; anchor='center'; textcolor="gray99"; textsize=10; textweight='normal'; width=100; widthunit='percent'; url="https://www.bls.gov/web/laus/ststdsadata.txt"; label="Data source: BLS Local Area Unemployment Statistics (LAUS) through &maxmon &maxyear"; output; run; data anno_all; set anno_lowest anno_ranges anno_footnote; run; title1 h=16pt c=gray33 "Unemployment Rate in &state"; title2 h=12pt c=gray77 ls=.5 "seasonally adjusted monthly values"; title3 h=1pt ls=0 ' '; proc sgplot data=temp noborder pad=(right=12pct bottom=6pct) nowall sganno=anno_all; format date year4.; series y=percent_unemployed x=date / lineattrs=(color=blue) tip=(date percent_unemployed) tipformat=(monyy7. percent7.2); yaxis display=(nolabel noline) min=0 offsetmin=0 offsetmax=0 thresholdmax=1 grid gridattrs=(color=graydd); xaxis display=(noline nolabel) offsetmin=.03 offsetmax=.03 values=('01jan1975'd to '01jan2025'd by year5) grid gridattrs=(color=graydd); run; %mend; ODS LISTING CLOSE; ODS HTML path=odsout body="&name..htm" gpath="./unemployment_by_state_dir/" (title="Unemployment by state") style=htmlblue; ods graphics / noscale /* if you don't use this option, the text will be resized */ imagemap tipmax=2500 imagefmt=png width=800px height=500px noborder; proc sql noprint; create table loop_data as select unique statecode, fipnamel(stfips(statecode)) as statename from my_data order by statename; quit; run; /* proc sort data=loop_data out=loop_data; by statename; run; */ /* Create a table, with html links to the html anchor for each state graph. */ data table_data; set loop_data; length href link $300; label link='State'; href='href='||quote(trim(left('#'||trim(left(statecode))))); link='' || htmlencode(trim(statename)) || ''; run; title1 c=gray33 h=14pt "Click state name to see unemployment graph..."; proc print data=table_data label noobs style(data)={font_size=12pt} style(header)={font_size=12pt}; var link; run; /* Loop through, and make a plot for each manufacturer */ data _null_; set loop_data; call execute('%do_state('|| statecode ||');'); run; quit; ODS HTML CLOSE; ODS LISTING;