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=