Here's a SAS ODS Graphics take on a neat New York Times tile grid U.S. map that graced nearly a full page of the Jan 12th paper and was much-liked on Twitter. No details, unfortunately, on what the talented NYT folks used to prep the data, generate the chart and add the text, so I took a crack at the whole soup-to-nuts process with SAS code.
Note: To view a larger image, right-click above image and select Open image in new tab. To view a full-resolution image, visit Twitter post, click on thumbnail, then right-click next image and select Open image in new tab.
* Creating A New York Times-Like Tile Grid U.S. Map With SAS ODS Graphics
Attempt to knockoff full-page graphic in 1-12-2022 NYT - see twitter.com/AlbertoCairo/status/1481289889421398026;
data NytMap; * Generate coordinates for NYT tile map of U.S.;
do r=1 to 8; * Eight rows;
input;
do c=1 to 11; * Eleven columns;
p=r*100+c; * Assign 4-digit # to 88 cells of panel chart;
st=input(substr(_infile_,(c-1)*3+1,2),$2.); * Grab 2-character state abbreviations;
output;
end;
end;
datalines; * NYT tile grid map "template";
ME
VT NH
WA ID MT ND MN IL WI MI NY RI MA
OR NV WY SD IA IN OH PA NJ CT
CA UT CO NE MO KY WV VA MD DE
AZ NM KS AR TN NC SC DC
OK LA MS AL GA
AK HI TX FL
; * Read NYT COVID-19 state-level daily data;
filename nytdata url 'https://raw.githubusercontent.com/nytimes/covid-19-data/master/rolling-averages/us-states.csv';
proc import out=NYTdata(where=(date<='10jan2022'd)) file=nytdata dbms=csv replace; * NYT chart we're trying to replicate was thru Jan 10;
proc sql; * Merge COVID-19 data with map coordinates and derive several fields;
create table NytDataMap as
select d.st, d.state, d.date, d.cases_avg_per_100k, m.p, md.maxdate,
case when date=md.maxdate then d.cases_avg_per_100k end as EndingCases,
case when date=md.maxdate then put(d.cases_avg_per_100k, 4.) end as EndingCasesX,
case when pcm.prevMonthsMaxCases<currMonthMaxCases then 'Record Cases in January' else 'Cases Rising' end as color
from NytMap m
left join (select fipstate(input(scan(geoid,2),2.)) as st, state, date, cases_avg_per_100k from nytdata) d on d.st=m.st
left join (select state, max(date) as maxdate from nytdata group by 1) md on d.state=md.state
left join (select state, max(case when date < '01jan2022'd then cases_avg_per_100k end) as prevMonthsMaxCases,
max(case when date between '01jan2022'd and '31jan2022'd then cases_avg_per_100k end) as currMonthMaxCases
from nytdata group by 1) pcm on pcm.state=d.state
order by m.p, d.st, d.date;
data years; * Show years the data covers (but only on WA cells);
st='WA'; p=3*100+1; yrY=.;
do yrX='01jan2020'd, '01jan2021'd, '01jan2022'd;
yr="'"||substr(put(yrX,year4.),3);
output;
end;
data NytDataMapYears; set nytdatamap years; * Merge the NYT data and the year annotations;
proc sort data=nytdatamapyears; by p st date;
data attrmap; * Create an attribute to assign colors based on cases in January;
ID="id"; value="Record Cases in January"; markercolor="red "; linecolor="red "; output;
ID="id"; value="Cases Rising "; markercolor="gray"; linecolor="gray"; output;
*==> Use PROC SGPANEL with series and scatter plots to create the U.S. tile map;
ods listing image_dpi=200 gpath='/home/ted.conway/';
ods graphics / border=off reset antialias height=8.5in width=11in imagename="nytmap" antialiasmax=35000;
proc sgpanel data=nytdatamapyears(where=(p^=.)) noautolegend subpixel dattrmap=attrmap;
panelby p / start=topleft sort=(ascending descending) noborder novarname nowall columns=11 rows=9 missing noheader onepanel;
inset st / nolabel position=topleft textattrs=(color=black size=9.5pt weight=bold); * Show 2-digit state codes in insets;
series x=date y=cases_avg_per_100k / group=color attrid=id smoothconnect lineattrs=(pattern=solid thickness=1.5pt); * Show cases by day trend lines;
scatter x=date y=EndingCases / labelstrip datalabel datalabelattrs=(color=black weight=bold) datalabelpos=top markerattrs=(symbol=circlefilled size=11pt color=white) markeroutlineattrs=(color=white); * Create marker separated from line for ending date;
scatter x=date y=EndingCases / group=color attrid=id markerattrs=(symbol=circlefilled) markeroutlineattrs=(color=white);
refline yrx / axis=x label=yr labelpos=min labelattrs=(color=black size=7pt weight=bold) transparency=.8 lineattrs=(color=black pattern=shortdash thickness=1.5pt); * Reference lines showing years (WA only);
colaxis display=none offsetmin=0 offsetmax=0;
rowaxis display=none offsetmin=0;
format EndingCases 4.;
*==> Use GTL to overlay text atop tile map image (one might just do this instead with editing software!);
data dummy; * Need "dummy" data for plot (one point);
retain x y .5;
proc template; * Specify text to overlay on map background image;
define statgraph image;
begingraph / subpixel=on border=false opaque=false;
layout overlay / xaxisopts=(display=none) yaxisopts=(display=none) walldisplay=none pad=0 outerpad=(top=0pt bottom=0pt left=0pt right=0pt);
scatterplot x=x y=y / markerattrs=(size=0); * Dummy scatter plot (invisible marker);
* Set background to U.S. tile map created in prior step;
drawimage "/home/ted.conway/nytmap.png" / border=false height=100 layer=back heightunit=percent drawspace=graphpercent;
* Put headings + narrative + legend in space at top left of map image;
Layout Gridded / rows=10 order=columnmajor border=false autoalign=(topleft) pad=0 outerpad=0;
entry halign=left textattrs=(size=19pt weight=bold) "What to Make of Those Soaring Covid Counts " / pad=0 outerpad=0 ;
entry halign=left textattrs=(size=9pt weight=bold style=italic) halign=left "Note:" textattrs=(size=9pt style=italic weight=normal) " This is a knockoff of a 1-12-2022 New York Times graphic " / pad=0 outerpad=0 ;
entry halign=left textattrs=(size=8.5pt) "Coronavirus case counts have reached record highs in the United States and continue to climb. Hospitalizations have surpassed last winter's " / pad=0 outerpad=0 ;
entry halign=left textattrs=(size=8.5pt) "wave. Deaths are also beginning to rise. The overall pattern is familiar, but a fresh perspective on how to interpret these metrics may be " / pad=0 outerpad=0;
entry halign=left textattrs=(size=8.5pt) "necessary as a faster but less severe variant tears through the country. Here's how to interpret the data in the coming days and weeks. " / pad=0 outerpad=0 ;
entry halign=left " " / pad=0 outerpad=0;
entry halign=left textattrs=(size=13pt weight=bold) "Average daily cases per 100,000 people " / pad=0 outerpad=0 ;
entry halign=left textattrs=(size=16pt color=red) {unicode '25cf'x} textattrs=(size=11pt color=black) " Record cases in January " textattrs=(size=16pt color=gray) {unicode '25cf'x} textattrs=(size=11pt color=black) " Cases rising " / pad=0 outerpad=0 ;
Endlayout;
* Put notes in space at bottom right of map image;
Layout Gridded / rows=5 order=columnmajor border=false autoalign=(bottomright) pad=0 outerpad=0;
entry halign=left textattrs=(size=7.5pt) " Note: All figures are 7-day " / pad=0 outerpad=0 ;
entry halign=left textattrs=(size=7.5pt) " rolling averages as of Jan 10. " / pad=0 outerpad=0 ;
entry halign=left textattrs=(size=7.5pt) " Source: New York Times " / pad=0 outerpad=0 ;
entry halign=left textattrs=(size=7.5pt) " database of reports from state " / pad=0 outerpad=0 ;
entry halign=left textattrs=(size=7.5pt) " and local health agencies. " / pad=0 outerpad=0 ;
Endlayout;
endlayout;
endgraph;
end;
run;
* Create the full map + text image!;
ods listing image_dpi=200 gpath='/home/ted.conway/';
ods graphics on / reset antialias width=11in height=8.5in imagename="nytpage" border=off;
proc sgrender data=dummy template=image;
run;
Although I don't really like this kind of map, good job with the imitation (and subtle improvements) 🙂
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.