Draft
The 2020 demographics were projections at the time of the original work (2017). I hope to get actuals and redo this. The data sources were very messy, and a different set of work was peformed to combine and tidy the data that are input to this visualization process.
Visualize the change in demographics through time in each North Carolina Wake County. Data includes population counts by district, race, ethnicity, year, and age range. The age ranges go from birth to greater than 85. The age breaks are in five year increments. Race and ethnicity break down the same population. Races are black, white, and other. Ethnicities are Hispanic and non-Hispanic.
Source CSV can be downloaded at https://docs.google.com/spreadsheets/d/1PguhEERjgX901MRtJ9mNMSpZwM_wOeRbd5r3ruzxRuU/edit?usp=sharing
# data manipulation
library(tidyr)
library(dplyr)
# weighted median implementation
library(matrixStats)
# plot animation
require(plotly)
Functions
The function reshapecolumn takes a column, extracts data from the column name, enriches with additional columns, and returns a normalized structure that can be bound to other normalized columns as rows to create a long data frame.
This function will be applied to all of the columns of interest in the source data. The function is very much ad hoc, reflecting the definition of the input file.
reshapecolumn <- function(colname) {
parts <- colname %>%
strsplit("[_]") %>%
unlist()
# divisor column to calculate percentage
divisorcol <- parts %>%
(function(x) (paste("totalpop",x[2],x[3],sep="_")))
# total population for the year will be used for plotting
totalpop_year <- paste("totalpop",parts[3],sep="_")
# extract data from the column names
colsfromname <- data.frame("classification" = as.character(parts[1]),
"agerange" = as.character(parts[2]),
"year" = as.integer(parts[3]))
# build the frame
newdataframe <- cbind(rawdemo["nc_senate"],
colsfromname,
round(rawdemo[colname] / rawdemo[divisorcol] * 100),
round(rawdemo[colname]))
# set the new column names (must precede next step)
names(newdataframe)[5] <- "percent"
names(newdataframe)[6] <- "count"
# add the total population for the nc senate district to the rows
newdataframe <- inner_join(newdataframe,
rawdemo[,c("nc_senate", totalpop_year)],
by="nc_senate")
# set the column name
names(newdataframe)[7] <- "totalpop_district_year"
newdataframe$totalpop_district_year <- round(newdataframe$totalpop_district_year)
return(newdataframe)
}
Select and munge the source data
The file is selected and read. Its name is printed for tracking what was done.
csvfilter <- matrix(c("CSV", "*.csv"), ncol=2)
rawdemo <- choose.files(filters = csvfilter,
caption = "Select demographics source file",
multi=FALSE) %T>%
print() %>%
read.csv()
[1] "C:\\Users\\wmc\\Documents\\vote\\NCSenateDemographics_1990_2020_FULL FILE.csv"
rawdemo
The data columns of interest (those with classified demographic data) are identified by a common name pattern (the argument to grep). The identified column names are captured in the vector cohorts. The reshapecolumn function is applied to each element of the vector, with each returned data frame bound as rows to the next. This process transforms the wide structure to a long structure, with enrichment by the function.
cohorts <- names(rawdemo)[grep("(hisp|white|black|other).*_age",
(names(rawdemo)))]
head(cohorts, 30)
[1] "white_age0_1990" "white_age5_1990" "white_age10_1990" "white_age15_1990"
[5] "white_age20_1990" "white_age25_1990" "white_age30_1990" "white_age35_1990"
[9] "white_age40_1990" "white_age45_1990" "white_age50_1990" "white_age55_1990"
[13] "white_age60_1990" "white_age65_1990" "white_age70_1990" "white_age75_1990"
[17] "white_age80_1990" "white_age85_1990" "black_age0_1990" "black_age5_1990"
[21] "black_age10_1990" "black_age15_1990" "black_age20_1990" "black_age25_1990"
[25] "black_age30_1990" "black_age35_1990" "black_age40_1990" "black_age45_1990"
[29] "black_age50_1990" "black_age55_1990"
length(cohorts)
[1] 360
demolong <-
cohorts %>%
lapply(FUN=reshapecolumn) %>% # feeds each name into the function
# and builds a list of tables for each
# named column
do.call('rbind', .) # binds the result into a single data frame
head(demolong,20)
nrow(demolong)
[1] 18000
The age column values identify an age range for the age grouping. The values are mapped to an age midpoint integer for the group. The values are joined to the rows so the values can be used for weighted means.
agemeans <- data.frame(agerange = unique(demolong$agerange),
midpoint_age = seq(from=2, by = 5,length.out = 18))
demolong <- inner_join(demolong, agemeans, by="agerange")
The data is restructured again to group and summarize. The summarization defines weighted means for age and population percentage.
demosumm <- demolong %>%
group_by(year, nc_senate, classification, totalpop_district_year) %>%
summarise(mean_age = round(weighted.mean(midpoint_age,count)),
median_age = round(weightedMedian(midpoint_age,count)),
totalpop_class = sum(count)
)
`summarise()` has grouped output by 'year', 'nc_senate', 'classification'. You can override using the `.groups` argument.
demosumm$percent <- round((demosumm$totalpop_class / demosumm$totalpop_district_year) * 100, 1)
demosumm$identity <- paste("dist", demosumm$nc_senate, demosumm$classification, sep = "_")
# show the structure of the result, using the first few rows
head(demosumm,10)
Visualization
A specific district and a related set of classification is used to create a motion plot through multiple years.
wake <- subset(demosumm,
nc_senate %in% c(14,15,16,17))
wake$nc_senate <- as.factor(wake$nc_senate)
wake
Changes to Hispanic versus non-Hispanic
Plot changes to population size and percentage of Hispanic and non-Hispanics.
Uses ggplot (for plots) and plotly (for animation). https://plotly-r.com/animating-views.html
gg <- ggplot(subset(wake, classification %in% c("hispanic","nonhisp")),
aes(mean_age,
totalpop_district_year,
shape = classification)) +
geom_point(aes(size = percent,
frame = year,
ids = identity,
color = nc_senate
)
)
ggplotly(gg) %>%
animation_opts(
frame = 2000,
redraw = FALSE
)
Changes to racial makeup
Plot changes to population size and percentage of black, white, and other classifications.
Uses ggplot (for plots) and plotly (for animation). https://plotly-r.com/animating-views.html
gg <- ggplot(subset(wake, classification %in% c("white","black","other")),
aes(mean_age,
totalpop_district_year,
shape = classification)) +
geom_point(aes(size = percent,
frame = year,
ids = identity,
color = nc_senate
)
)
ggplotly(gg) %>%
animation_opts(
frame = 2000,
redraw = FALSE
)
LS0tDQp0aXRsZTogIkRlbW9ncmFwaGljIGNoYW5nZXMgaW4gV2FrZSBDb3VudHkgTkMgU2VuYXRlIERpc3RyaWN0cyBhY3Jvc3Mgdm90aW5nIHllYXJzIg0KYXV0aG9yOiAiTWFyayBDb25ub2xseSINCmRhdGU6ICJgciBTeXMuRGF0ZSgpYCINCm91dHB1dDogaHRtbF9ub3RlYm9vaw0KLS0tDQojIyBEcmFmdA0KVGhlIDIwMjAgZGVtb2dyYXBoaWNzIHdlcmUgcHJvamVjdGlvbnMgYXQgdGhlIHRpbWUgb2YgdGhlIG9yaWdpbmFsIHdvcmsgKDIwMTcpLiAgSSBob3BlIHRvIGdldCBhY3R1YWxzIGFuZCByZWRvIHRoaXMuICBUaGUgZGF0YSBzb3VyY2VzIHdlcmUgdmVyeSBtZXNzeSwgYW5kIGEgZGlmZmVyZW50IHNldCBvZiB3b3JrIHdhcyBwZWZvcm1lZCB0byBjb21iaW5lIGFuZCB0aWR5IHRoZSBkYXRhIHRoYXQgYXJlIGlucHV0IHRvIHRoaXMgdmlzdWFsaXphdGlvbiBwcm9jZXNzLg0KDQpWaXN1YWxpemUgdGhlIGNoYW5nZSBpbiBkZW1vZ3JhcGhpY3MgdGhyb3VnaCB0aW1lIGluIGVhY2ggTm9ydGggQ2Fyb2xpbmEgV2FrZSBDb3VudHkuICBEYXRhIGluY2x1ZGVzIHBvcHVsYXRpb24gY291bnRzIGJ5IGRpc3RyaWN0LCByYWNlLCBldGhuaWNpdHksIHllYXIsIGFuZCBhZ2UgcmFuZ2UuICBUaGUgYWdlIHJhbmdlcyBnbyBmcm9tIGJpcnRoIHRvIGdyZWF0ZXIgdGhhbiA4NS4gIFRoZSBhZ2UgYnJlYWtzIGFyZSBpbiBmaXZlIHllYXIgaW5jcmVtZW50cy4gUmFjZSBhbmQgZXRobmljaXR5IGJyZWFrIGRvd24gdGhlIHNhbWUgcG9wdWxhdGlvbi4gIFJhY2VzIGFyZSBibGFjaywgd2hpdGUsIGFuZCBvdGhlci4gIEV0aG5pY2l0aWVzIGFyZSBIaXNwYW5pYyBhbmQgbm9uLUhpc3BhbmljLg0KDQpTb3VyY2UgQ1NWIGNhbiBiZSBkb3dubG9hZGVkIGF0IGh0dHBzOi8vZG9jcy5nb29nbGUuY29tL3NwcmVhZHNoZWV0cy9kLzFQZ3VoRUVSamdYOTAxTVJ0SjltTk1TcFp3TV93T2VSYmQ1cjNydXp4UnVVL2VkaXQ/dXNwPXNoYXJpbmcNCg0KDQpgYGB7ciBzZXR1cCwgd2FybmluZz1GQUxTRX0NCiMgZGF0YSBtYW5pcHVsYXRpb24gDQpsaWJyYXJ5KHRpZHlyKQ0KbGlicmFyeShkcGx5cikNCg0KIyB3ZWlnaHRlZCBtZWRpYW4gaW1wbGVtZW50YXRpb24NCmxpYnJhcnkobWF0cml4U3RhdHMpDQoNCiMgcGxvdCBhbmltYXRpb24NCnJlcXVpcmUocGxvdGx5KQ0KYGBgDQojIyBGdW5jdGlvbnMgDQpUaGUgZnVuY3Rpb24gYGBgcmVzaGFwZWNvbHVtbmBgYCB0YWtlcyBhIGNvbHVtbiwgZXh0cmFjdHMgZGF0YSBmcm9tIHRoZSBjb2x1bW4gbmFtZSwgZW5yaWNoZXMgd2l0aCBhZGRpdGlvbmFsIGNvbHVtbnMsIGFuZCByZXR1cm5zIGEgbm9ybWFsaXplZCBzdHJ1Y3R1cmUgdGhhdCBjYW4gYmUgYm91bmQgdG8gb3RoZXIgbm9ybWFsaXplZCBjb2x1bW5zIGFzIHJvd3MgdG8gY3JlYXRlIGEgbG9uZyBkYXRhIGZyYW1lLg0KDQpUaGlzIGZ1bmN0aW9uIHdpbGwgYmUgYXBwbGllZCB0byBhbGwgb2YgdGhlIGNvbHVtbnMgb2YgaW50ZXJlc3QgaW4gdGhlIHNvdXJjZSBkYXRhLiAgVGhlIGZ1bmN0aW9uIGlzIHZlcnkgbXVjaCBhZCBob2MsIHJlZmxlY3RpbmcgdGhlIGRlZmluaXRpb24gb2YgdGhlIGlucHV0IGZpbGUuDQoNCmBgYHtyfQ0KcmVzaGFwZWNvbHVtbiA8LSBmdW5jdGlvbihjb2xuYW1lKSB7DQogIA0KICBwYXJ0cyA8LSBjb2xuYW1lICU+JSANCiAgICAgICAgICAgc3Ryc3BsaXQoIltfXSIpICU+JSANCiAgICAgICAgICAgdW5saXN0KCkNCiAgDQogICMgZGl2aXNvciBjb2x1bW4gdG8gY2FsY3VsYXRlIHBlcmNlbnRhZ2UNCiAgZGl2aXNvcmNvbCA8LSBwYXJ0cyAlPiUNCiAgICAgICAgICAgICAgICAoZnVuY3Rpb24oeCkgKHBhc3RlKCJ0b3RhbHBvcCIseFsyXSx4WzNdLHNlcD0iXyIpKSkNCiAgDQogICMgdG90YWwgcG9wdWxhdGlvbiBmb3IgdGhlIHllYXIgd2lsbCBiZSB1c2VkIGZvciBwbG90dGluZw0KICB0b3RhbHBvcF95ZWFyIDwtIHBhc3RlKCJ0b3RhbHBvcCIscGFydHNbM10sc2VwPSJfIikNCiAgDQogICMgZXh0cmFjdCBkYXRhIGZyb20gdGhlIGNvbHVtbiBuYW1lcw0KICBjb2xzZnJvbW5hbWUgPC0gZGF0YS5mcmFtZSgiY2xhc3NpZmljYXRpb24iID0gYXMuY2hhcmFjdGVyKHBhcnRzWzFdKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImFnZXJhbmdlIiA9IGFzLmNoYXJhY3RlcihwYXJ0c1syXSksDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ5ZWFyIiA9IGFzLmludGVnZXIocGFydHNbM10pKQ0KDQogICMgYnVpbGQgdGhlIGZyYW1lIA0KICBuZXdkYXRhZnJhbWUgPC0gY2JpbmQocmF3ZGVtb1sibmNfc2VuYXRlIl0sIA0KICAgICAgICAgICAgICAgICAgICAgICAgY29sc2Zyb21uYW1lLCANCiAgICAgICAgICAgICAgICAgICAgICAgIHJvdW5kKHJhd2RlbW9bY29sbmFtZV0gLyByYXdkZW1vW2Rpdmlzb3Jjb2xdICogMTAwKSwNCiAgICAgICAgICAgICAgICAgICAgICAgIHJvdW5kKHJhd2RlbW9bY29sbmFtZV0pKQ0KICANCiAgIyBzZXQgdGhlIG5ldyBjb2x1bW4gbmFtZXMgKG11c3QgcHJlY2VkZSBuZXh0IHN0ZXApDQogIG5hbWVzKG5ld2RhdGFmcmFtZSlbNV0gPC0gInBlcmNlbnQiDQogIG5hbWVzKG5ld2RhdGFmcmFtZSlbNl0gPC0gImNvdW50Ig0KICANCiAgIyBhZGQgdGhlIHRvdGFsIHBvcHVsYXRpb24gZm9yIHRoZSBuYyBzZW5hdGUgZGlzdHJpY3QgdG8gdGhlIHJvd3MgDQogIG5ld2RhdGFmcmFtZSA8LSBpbm5lcl9qb2luKG5ld2RhdGFmcmFtZSwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgIHJhd2RlbW9bLGMoIm5jX3NlbmF0ZSIsIHRvdGFscG9wX3llYXIpXSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYnk9Im5jX3NlbmF0ZSIpDQogIA0KICAjIHNldCB0aGUgY29sdW1uIG5hbWUNCiAgbmFtZXMobmV3ZGF0YWZyYW1lKVs3XSA8LSAidG90YWxwb3BfZGlzdHJpY3RfeWVhciINCiAgDQogIG5ld2RhdGFmcmFtZSR0b3RhbHBvcF9kaXN0cmljdF95ZWFyIDwtIHJvdW5kKG5ld2RhdGFmcmFtZSR0b3RhbHBvcF9kaXN0cmljdF95ZWFyKQ0KICANCiAgcmV0dXJuKG5ld2RhdGFmcmFtZSkNCn0NCmBgYA0KDQojIyBTZWxlY3QgYW5kIG11bmdlIHRoZSBzb3VyY2UgZGF0YQ0KVGhlIGZpbGUgaXMgc2VsZWN0ZWQgYW5kIHJlYWQuICBJdHMgbmFtZSBpcyBwcmludGVkIGZvciB0cmFja2luZyB3aGF0IHdhcyBkb25lLiAgDQoNCmBgYHtyfQ0KDQpjc3ZmaWx0ZXIgPC0gbWF0cml4KGMoIkNTViIsICIqLmNzdiIpLCBuY29sPTIpDQoNCnJhd2RlbW8gPC0gY2hvb3NlLmZpbGVzKGZpbHRlcnMgPSBjc3ZmaWx0ZXIsDQogICAgICAgICAgICAgICAgICAgICAgICBjYXB0aW9uID0gIlNlbGVjdCBkZW1vZ3JhcGhpY3Mgc291cmNlIGZpbGUiLA0KICAgICAgICAgICAgICAgICAgICAgICAgbXVsdGk9RkFMU0UpICAlVD4lDQogICAgICAgICAgIHByaW50KCkgJT4lICAgICAgICAgIA0KICAgICAgICAgICByZWFkLmNzdigpDQpyYXdkZW1vDQpgYGANCg0KDQpUaGUgZGF0YSBjb2x1bW5zIG9mIGludGVyZXN0ICh0aG9zZSB3aXRoIGNsYXNzaWZpZWQgZGVtb2dyYXBoaWMgZGF0YSkgYXJlIGlkZW50aWZpZWQgYnkgYSBjb21tb24gbmFtZSBwYXR0ZXJuICh0aGUgYXJndW1lbnQgdG8gYGBgZ3JlcGBgYCkuIFRoZSBpZGVudGlmaWVkIGNvbHVtbiBuYW1lcyBhcmUgY2FwdHVyZWQgaW4gdGhlIHZlY3RvciBgYGBjb2hvcnRzYGBgLiAgVGhlIGBgYHJlc2hhcGVjb2x1bW5gYGAgZnVuY3Rpb24gaXMgYXBwbGllZCB0byBlYWNoIGVsZW1lbnQgb2YgdGhlIHZlY3Rvciwgd2l0aCBlYWNoIHJldHVybmVkIGRhdGEgZnJhbWUgYm91bmQgYXMgcm93cyB0byB0aGUgbmV4dC4gIFRoaXMgcHJvY2VzcyB0cmFuc2Zvcm1zIHRoZSB3aWRlIHN0cnVjdHVyZSB0byBhIGxvbmcgc3RydWN0dXJlLCB3aXRoIGVucmljaG1lbnQgYnkgdGhlIGZ1bmN0aW9uLg0KDQpgYGB7cn0NCmNvaG9ydHMgPC0gbmFtZXMocmF3ZGVtbylbZ3JlcCgiKGhpc3B8d2hpdGV8YmxhY2t8b3RoZXIpLipfYWdlIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAobmFtZXMocmF3ZGVtbykpKV0NCmhlYWQoY29ob3J0cywgMzApIA0KbGVuZ3RoKGNvaG9ydHMpDQpgYGANCg0KYGBge3J9DQpkZW1vbG9uZyA8LSANCiAgY29ob3J0cyAlPiUNCiAgDQogIGxhcHBseShGVU49cmVzaGFwZWNvbHVtbikgJT4lICAgICMgZmVlZHMgZWFjaCBuYW1lIGludG8gdGhlIGZ1bmN0aW9uDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICMgYW5kIGJ1aWxkcyBhIGxpc3Qgb2YgdGFibGVzIGZvciBlYWNoIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAjIG5hbWVkIGNvbHVtbg0KICANCiAgZG8uY2FsbCgncmJpbmQnLCAuKSAgICAgICAgICAgICAgIyBiaW5kcyB0aGUgcmVzdWx0IGludG8gYSBzaW5nbGUgZGF0YSBmcmFtZQ0KDQpoZWFkKGRlbW9sb25nLDIwKQ0KbnJvdyhkZW1vbG9uZykNCmBgYA0KDQpUaGUgYWdlIGNvbHVtbiB2YWx1ZXMgaWRlbnRpZnkgYW4gYWdlIHJhbmdlIGZvciB0aGUgYWdlIGdyb3VwaW5nLiAgVGhlIHZhbHVlcyBhcmUgbWFwcGVkIHRvIGFuIGFnZSBtaWRwb2ludCBpbnRlZ2VyIGZvciB0aGUgZ3JvdXAuICBUaGUgdmFsdWVzIGFyZSBqb2luZWQgdG8gdGhlIHJvd3Mgc28gdGhlIHZhbHVlcyBjYW4gYmUgdXNlZCBmb3Igd2VpZ2h0ZWQgbWVhbnMuDQoNCmBgYHtyfQ0KYWdlbWVhbnMgPC0gZGF0YS5mcmFtZShhZ2VyYW5nZSAgPSB1bmlxdWUoZGVtb2xvbmckYWdlcmFuZ2UpLA0KICAgICAgICAgICAgICAgICAgICAgICBtaWRwb2ludF9hZ2UgPSBzZXEoZnJvbT0yLCBieSA9IDUsbGVuZ3RoLm91dCA9IDE4KSkNCg0KZGVtb2xvbmcgPC0gaW5uZXJfam9pbihkZW1vbG9uZywgYWdlbWVhbnMsIGJ5PSJhZ2VyYW5nZSIpDQpgYGANCg0KVGhlIGRhdGEgaXMgcmVzdHJ1Y3R1cmVkIGFnYWluIHRvIGdyb3VwIGFuZCBzdW1tYXJpemUuICBUaGUgc3VtbWFyaXphdGlvbiBkZWZpbmVzIHdlaWdodGVkIG1lYW5zIGZvciBhZ2UgYW5kIHBvcHVsYXRpb24gcGVyY2VudGFnZS4NCg0KYGBge3J9DQpkZW1vc3VtbSA8LSBkZW1vbG9uZyAlPiUgDQogIGdyb3VwX2J5KHllYXIsIG5jX3NlbmF0ZSwgY2xhc3NpZmljYXRpb24sIHRvdGFscG9wX2Rpc3RyaWN0X3llYXIpICU+JQ0KICBzdW1tYXJpc2UobWVhbl9hZ2UgPSByb3VuZCh3ZWlnaHRlZC5tZWFuKG1pZHBvaW50X2FnZSxjb3VudCkpLA0KICAgICAgICAgICAgbWVkaWFuX2FnZSA9IHJvdW5kKHdlaWdodGVkTWVkaWFuKG1pZHBvaW50X2FnZSxjb3VudCkpLA0KICAgICAgICAgICAgdG90YWxwb3BfY2xhc3MgPSBzdW0oY291bnQpDQogICAgICAgICAgICApDQoNCmRlbW9zdW1tJHBlcmNlbnQgPC0gcm91bmQoKGRlbW9zdW1tJHRvdGFscG9wX2NsYXNzIC8gZGVtb3N1bW0kdG90YWxwb3BfZGlzdHJpY3RfeWVhcikgKiAxMDAsIDEpDQpkZW1vc3VtbSRpZGVudGl0eSA8LSBwYXN0ZSgiZGlzdCIsIGRlbW9zdW1tJG5jX3NlbmF0ZSwgZGVtb3N1bW0kY2xhc3NpZmljYXRpb24sIHNlcCA9ICJfIikNCg0KIyBzaG93IHRoZSBzdHJ1Y3R1cmUgb2YgdGhlIHJlc3VsdCwgdXNpbmcgdGhlIGZpcnN0IGZldyByb3dzDQpoZWFkKGRlbW9zdW1tLDEwKQ0KYGBgDQoNCg0KIyMjIFZpc3VhbGl6YXRpb24NCkEgc3BlY2lmaWMgZGlzdHJpY3QgYW5kIGEgcmVsYXRlZCBzZXQgb2YgY2xhc3NpZmljYXRpb24gaXMgdXNlZCB0byBjcmVhdGUgYSBtb3Rpb24gcGxvdCB0aHJvdWdoIG11bHRpcGxlIHllYXJzLg0KDQpgYGB7cn0NCndha2UgPC0gc3Vic2V0KGRlbW9zdW1tLA0KICAgICAgICAgICAgICAgbmNfc2VuYXRlICVpbiUgYygxNCwxNSwxNiwxNykpIA0Kd2FrZSRuY19zZW5hdGUgPC0gYXMuZmFjdG9yKHdha2UkbmNfc2VuYXRlKQ0KDQp3YWtlDQpgYGANCg0KIyMjIENoYW5nZXMgdG8gSGlzcGFuaWMgdmVyc3VzIG5vbi1IaXNwYW5pYw0KUGxvdCBjaGFuZ2VzIHRvIHBvcHVsYXRpb24gc2l6ZSBhbmQgcGVyY2VudGFnZSBvZiBIaXNwYW5pYyBhbmQgbm9uLUhpc3Bhbmljcy4NCg0KVXNlcyBgYGBnZ3Bsb3RgYGAgKGZvciBwbG90cykgYW5kIGBgYHBsb3RseWBgYCAoZm9yIGFuaW1hdGlvbikuIGh0dHBzOi8vcGxvdGx5LXIuY29tL2FuaW1hdGluZy12aWV3cy5odG1sDQoNCmBgYHtyLCB3YXJuaW5nPUZBTFNFLCBmaWcud2lkdGg9OCwgZmlnLmhlaWdodD02fQ0KZ2cgPC0gZ2dwbG90KHN1YnNldCh3YWtlLCBjbGFzc2lmaWNhdGlvbiAlaW4lIGMoImhpc3BhbmljIiwibm9uaGlzcCIpKSwNCiAgICAgICAgICAgICBhZXMobWVhbl9hZ2UsIA0KICAgICAgICAgICAgICAgICB0b3RhbHBvcF9kaXN0cmljdF95ZWFyLA0KICAgICAgICAgICAgICAgICBzaGFwZSA9IGNsYXNzaWZpY2F0aW9uKSkgKw0KICAgICAgICBnZW9tX3BvaW50KGFlcyhzaXplID0gcGVyY2VudCwgDQogICAgICAgICAgICAgICAgICAgICAgIGZyYW1lID0geWVhciwgDQogICAgICAgICAgICAgICAgICAgICAgIGlkcyA9IGlkZW50aXR5LA0KICAgICAgICAgICAgICAgICAgICAgICBjb2xvciA9IG5jX3NlbmF0ZQ0KICAgICAgICAgICAgICAgICAgICAgICApDQogICAgICAgICAgICAgICAgICAgKQ0KZ2dwbG90bHkoZ2cpICU+JSANCiAgYW5pbWF0aW9uX29wdHMoDQogICAgZnJhbWUgPSAyMDAwLCANCiAgICByZWRyYXcgPSBGQUxTRQ0KICApDQpgYGANCg0KIyMjIENoYW5nZXMgdG8gcmFjaWFsIG1ha2V1cA0KUGxvdCBjaGFuZ2VzIHRvIHBvcHVsYXRpb24gc2l6ZSBhbmQgcGVyY2VudGFnZSBvZiBibGFjaywgd2hpdGUsIGFuZCBvdGhlciBjbGFzc2lmaWNhdGlvbnMuDQoNClVzZXMgYGBgZ2dwbG90YGBgIChmb3IgcGxvdHMpIGFuZCBgYGBwbG90bHlgYGAgKGZvciBhbmltYXRpb24pLiBodHRwczovL3Bsb3RseS1yLmNvbS9hbmltYXRpbmctdmlld3MuaHRtbA0KDQpgYGB7ciwgd2FybmluZz1GQUxTRSwgZmlnLndpZHRoPTgsIGZpZy5oZWlnaHQ9Nn0NCmdnIDwtIGdncGxvdChzdWJzZXQod2FrZSwgY2xhc3NpZmljYXRpb24gJWluJSBjKCJ3aGl0ZSIsImJsYWNrIiwib3RoZXIiKSksDQogICAgICAgICAgICAgYWVzKG1lYW5fYWdlLCANCiAgICAgICAgICAgICAgICAgdG90YWxwb3BfZGlzdHJpY3RfeWVhciwNCiAgICAgICAgICAgICAgICAgc2hhcGUgPSBjbGFzc2lmaWNhdGlvbikpICsNCiAgICAgICAgZ2VvbV9wb2ludChhZXMoc2l6ZSA9IHBlcmNlbnQsIA0KICAgICAgICAgICAgICAgICAgICAgICBmcmFtZSA9IHllYXIsIA0KICAgICAgICAgICAgICAgICAgICAgICBpZHMgPSBpZGVudGl0eSwNCiAgICAgICAgICAgICAgICAgICAgICAgY29sb3IgPSBuY19zZW5hdGUNCiAgICAgICAgICAgICAgICAgICAgICAgKQ0KICAgICAgICAgICAgICAgICAgICkNCmdncGxvdGx5KGdnKSAlPiUgDQogIGFuaW1hdGlvbl9vcHRzKA0KICAgIGZyYW1lID0gMjAwMCwgDQogICAgcmVkcmF3ID0gRkFMU0UNCiAgKQ0KYGBgDQo=