Introduction

This is a brief tutorial on retrieving and preparing data for generating a heatmap in R using genomic data. We’ll be using publicly available data from “Comparative transcriptomic and proteomic analyses provide insights into the key genes involved in high-altitude adaptation in the Tibetan pig” [Scientific Reports 2017]. This is a comparative analysis of the transcriptomic and proteomic profiles of heart tissues obtained from Tibetan and Yorkshire pigs raised at high (TH and YH) and low (TL and YL) altitudes. The study aimed to identify key genes and molecular mechanisms involved in the high-altitude adaptations of the Tibetan pig.

The full text of the study is available through PubMed Central. The raw and processed data is deposited in GEO under series GSE92981.

Load packages

Load the relevant packages.

library(tidyverse)
library(magrittr)
library(pheatmap)
library(RColorBrewer)
library(rio)

If you get an error there is no package, you need to install some packages first. If you install some, others should install automatically as dependencies. Uncomment (remove #) the relevant commands to install.

# install.packages("tidyverse")
# install.packages("pheatmap")
# install.packages("rio")

Retrieve data

Download values

FPKMs (Fragments Per Kilobase of transcript per Million mapped reads) are a common RNA-seq expression unit. First, download the FPKM table. In the GEO submission, this is the supplementary file “GSE92981_expression_all_FPKM.txt.gz”.

fpkm_full = read_tsv("ftp://ftp.ncbi.nlm.nih.gov/geo/series/GSE92nnn/GSE92981/suppl/GSE92981_expression_all_FPKM.txt.gz")
Parsed with column specification:
cols(
  .default = col_double(),
  gene_id = col_character(),
  gene_name = col_character(),
  GeneID = col_character(),
  description = col_character(),
  locus = col_character(),
  `TH/YH` = col_character(),
  `TH/TL` = col_character(),
  `TL/YL` = col_character(),
  `YH/YL` = col_character()
)
See spec(...) for full column specifications.

Let’s check the dimensions of the downloaded FPKM table.

dim(fpkm_full)
[1] 18975    29

We should also check the contents of the table. It’s a good idea to use head() when dealing with potentially large tables to prevent excessive output.

head(fpkm_full)

Download stats

We have the FPKMs, but the table contains the full gene list. We probably want to use just an interesting subset. The study also provides a table of differentially expressed genes. It’s not in GEO, but it’s in the supplemental data (Supplementary Table S4). Unfortunately, it’s an Excel file, which makes it slightly more difficult to import. We’ll use the “Table S4-1” sheet (DEGs of TH vs YH).

stats_full = import("https://www.ncbi.nlm.nih.gov/pmc/articles/PMC5473931/bin/41598_2017_3976_MOESM2_ESM.xls", sheet = "Table S4-1", skip = 1)

Check the dimensions of the stats table.

dim(stats_full)
[1] 299   6

Check the contents of the stats table.

head(stats_full)

Prepare data

Clean up values

The FPKM table has a lot of columns. Let’s check just the FPKM table column names.

colnames(fpkm_full)
 [1] "gene_id"     "gene_name"   "GeneID"      "description"
 [5] "locus"       "TH1_FPKM"    "TH2_FPKM"    "TL1_FPKM"   
 [9] "TL2_FPKM"    "YH1_FPKM"    "YH2_FPKM"    "YL1_FPKM"   
[13] "YL2_FPKM"    "TH1_count"   "TH2_count"   "TL1_count"  
[17] "TL2_count"   "YH1_count"   "YH2_count"   "YL1_count"  
[21] "YL2_count"   "TH"          "TL"          "YH"         
[25] "YL"          "TH/YH"       "TH/TL"       "TL/YL"      
[29] "YH/YL"      

The magrittr package adds the ability to use %>% to “pipe” the expression forward, allowing the conversion of nested function calls into a simple pipeline of operations. This makes the code easier to read and work with if you want to perform multiple operations. For example, we can repeat the previous colnames() operation with a pipe.

fpkm_full %>% colnames()
 [1] "gene_id"     "gene_name"   "GeneID"      "description"
 [5] "locus"       "TH1_FPKM"    "TH2_FPKM"    "TL1_FPKM"   
 [9] "TL2_FPKM"    "YH1_FPKM"    "YH2_FPKM"    "YL1_FPKM"   
[13] "YL2_FPKM"    "TH1_count"   "TH2_count"   "TL1_count"  
[17] "TL2_count"   "YH1_count"   "YH2_count"   "YL1_count"  
[21] "YL2_count"   "TH"          "TL"          "YH"         
[25] "YL"          "TH/YH"       "TH/TL"       "TL/YL"      
[29] "YH/YL"      

Let’s clean up the table. The dplyr package has many helper functions to make this easier. We’ll create a new column with both gene names and gene IDs using mutate(), keep only relevant columns with select(), convert to a standard data frame (from tibble), and set gene column as row names.

fpkm_clean = fpkm_full %>%
  mutate(gene = paste0(gene_id, ":", gene_name)) %>%
  select(gene, ends_with("FPKM")) %>%
  as.data.frame() %>%
  column_to_rownames("gene")
head(fpkm_clean)

We can get rid of “FPKM” in column names to keep them simple.

colnames(fpkm_clean) = gsub("_FPKM", "", colnames(fpkm_clean))
head(fpkm_clean)

In addition to the shape of the table, it’s important to check the contents. With thousands of rows, that is hard to do manually. A boxplot can quickly show the distribution.

# "las = 2" makes the sample labels perpendicular
boxplot(fpkm_clean, las = 2)

The range of values is so high, the boxplots are not even visible. We can log-transform the values to make variation more similar across orders of magnitude.

fpkm_log2 = log2(fpkm_clean + 1)
boxplot(fpkm_log2, las = 2)

Clean up stats

As we did with FPKMs, we’ll create a new column with both gene names and gene IDs so that we have that in common.

stats_full = stats_full %>% mutate(gene = paste0(`Gene ID`, ":", `Gene Name`))
head(stats_full)

We can sort by significance.

stats_full %>% arrange(`P-value`) %>% head

Get the list of most significant genes.

top_genes = stats_full %>% arrange(`P-value`) %>% head(50) %$% gene
head(top_genes)
[1] "ENSSSCG00000025268:CKM"    "ENSSSCG00000018092:MT-ND6"
[3] "ENSSSCG00000003431:NPPB"   "ENSSSCG00000003430:NPPA"  
[5] "ENSSSCG00000018084:MT-ND3" "ENSSSCG00000009668:CLU"   

Generate heatmaps

Basic heatmaps

Select 100 random genes to use. We’ll only use these genes for the next few examples to save time.

random_genes = sample(rownames(fpkm_clean), 100)
head(random_genes)
[1] "ENSSSCG00000003168:ALDH16A1" "ENSSSCG00000023857:ARHGEF40"
[3] "ENSSSCG00000002996:RAB4B"    "ENSSSCG00000024394:IKZF4"   
[5] "ENSSSCG00000029664:-"        "ENSSSCG00000003138:HSD17B14"

Generate a simple heatmap using the original values for 100 random genes.

pheatmap(fpkm_clean[random_genes, ])

Generate a simple heatmap using the log-transformed values for 100 random genes.

pheatmap(fpkm_log2[random_genes, ])

Center and scale in the row direction.

pheatmap(fpkm_log2[random_genes, ], scale = "row")

Color options

Manually create color range.

my_colors = c("green", "black", "red")
my_colors
[1] "green" "black" "red"  

Expand the color range from 3 to 50 values.

my_colors = colorRampPalette(my_colors)(50)
my_colors
 [1] "#00FF00" "#00F400" "#00EA00" "#00DF00" "#00D500" "#00CA00"
 [7] "#00C000" "#00B600" "#00AB00" "#00A100" "#009600" "#008C00"
[13] "#008200" "#007700" "#006D00" "#006200" "#005800" "#004E00"
[19] "#004300" "#003900" "#002E00" "#002400" "#001A00" "#000F00"
[25] "#000500" "#050000" "#0F0000" "#1A0000" "#240000" "#2E0000"
[31] "#390000" "#430000" "#4E0000" "#580000" "#620000" "#6D0000"
[37] "#770000" "#820000" "#8C0000" "#960000" "#A10000" "#AB0000"
[43] "#B60000" "#C00000" "#CA0000" "#D50000" "#DF0000" "#EA0000"
[49] "#F40000" "#FF0000"

Generate a heatmap with a green/red color scheme.

pheatmap(fpkm_log2[random_genes, ], scale = "row", color = my_colors)

RColorBrewer provides a collection of pre-made color palettes.

display.brewer.all()

Use an RColorBrewer color palette.

my_colors = brewer.pal(n = 11, name = "RdBu")
my_colors = colorRampPalette(my_colors)(50)
my_colors = rev(my_colors)
my_colors
 [1] "#053061" "#0A3B70" "#10467F" "#16518E" "#1B5C9E" "#2166AC"
 [7] "#2870B1" "#2F79B5" "#3682BA" "#3D8BBF" "#4695C4" "#569FC9"
[13] "#66A9CF" "#76B3D4" "#86BDDA" "#95C6DF" "#A2CDE2" "#AFD4E6"
[19] "#BCDAEA" "#C9E1ED" "#D4E6F0" "#DBEAF2" "#E3EDF3" "#EBF1F4"
[25] "#F3F5F6" "#F7F4F2" "#F8EEE8" "#FAE8DE" "#FBE3D4" "#FCDDCA"
[31] "#FBD4BE" "#FAC9B0" "#F8BEA2" "#F6B394" "#F4A886" "#EF9B7A"
[37] "#E98D6F" "#E37E64" "#DD7059" "#D7624F" "#D05447" "#C84540"
[43] "#C13639" "#BA2832" "#B2192B" "#A41328" "#940E26" "#850923"
[49] "#760421" "#67001F"

Generate a heatmap with a blue/red color scheme.

pheatmap(fpkm_log2[random_genes, ], scale = "row", color = my_colors)

Significant genes

Visualize the most significant genes.

pheatmap(fpkm_log2[top_genes, ], scale = "row", color = my_colors)

Remove the border around each cell and reduce the row label font size.

pheatmap(fpkm_log2[top_genes, ],
         scale = "row",
         color = my_colors,
         border_color = NA,
         fontsize_row = 4)

Additional notes

This is just one approach to generating a heatmap. There are many other ways. You can even do it in Excel (sort of). Check this listing of graphical (no coding needed) and R-based alternatives: http://bit.ly/heatmapoptions.

LS0tCnRpdGxlOiAiQ3JlYXRpbmcgSGVhdG1hcHMgaW4gUiIKb3V0cHV0OgogIGh0bWxfbm90ZWJvb2s6CiAgICB0aGVtZTogcmVhZGFibGUKICAgIHRvYzogeWVzCiAgICB0b2NfZmxvYXQ6IHllcwogIGh0bWxfZG9jdW1lbnQ6CiAgICB0b2M6IHllcwotLS0KCgojIyBJbnRyb2R1Y3Rpb24KClRoaXMgaXMgYSBicmllZiB0dXRvcmlhbCBvbiByZXRyaWV2aW5nIGFuZCBwcmVwYXJpbmcgZGF0YSBmb3IgZ2VuZXJhdGluZyBhIGhlYXRtYXAgaW4gUiB1c2luZyBnZW5vbWljIGRhdGEuIFdlJ2xsIGJlIHVzaW5nIHB1YmxpY2x5IGF2YWlsYWJsZSBkYXRhIGZyb20gIkNvbXBhcmF0aXZlIHRyYW5zY3JpcHRvbWljIGFuZCBwcm90ZW9taWMgYW5hbHlzZXMgcHJvdmlkZSBpbnNpZ2h0cyBpbnRvIHRoZSBrZXkgZ2VuZXMgaW52b2x2ZWQgaW4gaGlnaC1hbHRpdHVkZSBhZGFwdGF0aW9uIGluIHRoZSBUaWJldGFuIHBpZyIgW1NjaWVudGlmaWMgUmVwb3J0cyAyMDE3XS4gVGhpcyBpcyBhIGNvbXBhcmF0aXZlIGFuYWx5c2lzIG9mIHRoZSB0cmFuc2NyaXB0b21pYyBhbmQgcHJvdGVvbWljIHByb2ZpbGVzIG9mIGhlYXJ0IHRpc3N1ZXMgb2J0YWluZWQgZnJvbSBUaWJldGFuIGFuZCBZb3Jrc2hpcmUgcGlncyByYWlzZWQgYXQgaGlnaCAoVEggYW5kIFlIKSBhbmQgbG93IChUTCBhbmQgWUwpIGFsdGl0dWRlcy4gVGhlIHN0dWR5IGFpbWVkIHRvIGlkZW50aWZ5IGtleSBnZW5lcyBhbmQgbW9sZWN1bGFyIG1lY2hhbmlzbXMgaW52b2x2ZWQgaW4gdGhlIGhpZ2gtYWx0aXR1ZGUgYWRhcHRhdGlvbnMgb2YgdGhlIFRpYmV0YW4gcGlnLgoKVGhlIFtmdWxsIHRleHRdKGh0dHBzOi8vd3d3Lm5jYmkubmxtLm5paC5nb3YvcG1jL2FydGljbGVzL1BNQzU0NzM5MzEvKSBvZiB0aGUgc3R1ZHkgaXMgYXZhaWxhYmxlIHRocm91Z2ggUHViTWVkIENlbnRyYWwuIFRoZSByYXcgYW5kIHByb2Nlc3NlZCBkYXRhIGlzIGRlcG9zaXRlZCBpbiBHRU8gdW5kZXIgc2VyaWVzIFtHU0U5Mjk4MV0oaHR0cHM6Ly93d3cubmNiaS5ubG0ubmloLmdvdi9nZW8vcXVlcnkvYWNjLmNnaT9hY2M9R1NFOTI5ODEpLgoKCiMjIExvYWQgcGFja2FnZXMKCkxvYWQgdGhlIHJlbGV2YW50IHBhY2thZ2VzLgoKYGBge3J9CmxpYnJhcnkodGlkeXZlcnNlKQpsaWJyYXJ5KG1hZ3JpdHRyKQpsaWJyYXJ5KHBoZWF0bWFwKQpsaWJyYXJ5KFJDb2xvckJyZXdlcikKbGlicmFyeShyaW8pCmBgYAoKSWYgeW91IGdldCBhbiBlcnJvciBgdGhlcmUgaXMgbm8gcGFja2FnZWAsIHlvdSBuZWVkIHRvIGluc3RhbGwgc29tZSBwYWNrYWdlcyBmaXJzdC4gSWYgeW91IGluc3RhbGwgc29tZSwgb3RoZXJzIHNob3VsZCBpbnN0YWxsIGF1dG9tYXRpY2FsbHkgYXMgZGVwZW5kZW5jaWVzLiBVbmNvbW1lbnQgKHJlbW92ZSBgI2ApIHRoZSByZWxldmFudCBjb21tYW5kcyB0byBpbnN0YWxsLgoKYGBge3J9CiMgaW5zdGFsbC5wYWNrYWdlcygidGlkeXZlcnNlIikKIyBpbnN0YWxsLnBhY2thZ2VzKCJwaGVhdG1hcCIpCiMgaW5zdGFsbC5wYWNrYWdlcygicmlvIikKYGBgCgoKIyMgUmV0cmlldmUgZGF0YQoKCiMjIyBEb3dubG9hZCB2YWx1ZXMKCkZQS01zIChGcmFnbWVudHMgUGVyIEtpbG9iYXNlIG9mIHRyYW5zY3JpcHQgcGVyIE1pbGxpb24gbWFwcGVkIHJlYWRzKSBhcmUgYSBjb21tb24gUk5BLXNlcSBleHByZXNzaW9uIHVuaXQuIEZpcnN0LCBkb3dubG9hZCB0aGUgRlBLTSB0YWJsZS4gSW4gdGhlIEdFTyBzdWJtaXNzaW9uLCB0aGlzIGlzIHRoZSBzdXBwbGVtZW50YXJ5IGZpbGUgIkdTRTkyOTgxX2V4cHJlc3Npb25fYWxsX0ZQS00udHh0Lmd6Ii4KCmBgYHtyfQpmcGttX2Z1bGwgPSByZWFkX3RzdigiZnRwOi8vZnRwLm5jYmkubmxtLm5paC5nb3YvZ2VvL3Nlcmllcy9HU0U5Mm5ubi9HU0U5Mjk4MS9zdXBwbC9HU0U5Mjk4MV9leHByZXNzaW9uX2FsbF9GUEtNLnR4dC5neiIpCmBgYAoKTGV0J3MgY2hlY2sgdGhlIGRpbWVuc2lvbnMgb2YgdGhlIGRvd25sb2FkZWQgRlBLTSB0YWJsZS4KCmBgYHtyfQpkaW0oZnBrbV9mdWxsKQpgYGAKCldlIHNob3VsZCBhbHNvIGNoZWNrIHRoZSBjb250ZW50cyBvZiB0aGUgdGFibGUuIEl0J3MgYSBnb29kIGlkZWEgdG8gdXNlIGBoZWFkKClgIHdoZW4gZGVhbGluZyB3aXRoIHBvdGVudGlhbGx5IGxhcmdlIHRhYmxlcyB0byBwcmV2ZW50IGV4Y2Vzc2l2ZSBvdXRwdXQuCgpgYGB7cn0KaGVhZChmcGttX2Z1bGwpCmBgYAoKCiMjIyBEb3dubG9hZCBzdGF0cwoKV2UgaGF2ZSB0aGUgRlBLTXMsIGJ1dCB0aGUgdGFibGUgY29udGFpbnMgdGhlIGZ1bGwgZ2VuZSBsaXN0LiBXZSBwcm9iYWJseSB3YW50IHRvIHVzZSBqdXN0IGFuIGludGVyZXN0aW5nIHN1YnNldC4gVGhlIHN0dWR5IGFsc28gcHJvdmlkZXMgYSB0YWJsZSBvZiBkaWZmZXJlbnRpYWxseSBleHByZXNzZWQgZ2VuZXMuIEl0J3Mgbm90IGluIEdFTywgYnV0IGl0J3MgaW4gdGhlIHN1cHBsZW1lbnRhbCBkYXRhIChTdXBwbGVtZW50YXJ5IFRhYmxlIFM0KS4gVW5mb3J0dW5hdGVseSwgaXQncyBhbiBFeGNlbCBmaWxlLCB3aGljaCBtYWtlcyBpdCBzbGlnaHRseSBtb3JlIGRpZmZpY3VsdCB0byBpbXBvcnQuIFdlJ2xsIHVzZSB0aGUgIlRhYmxlIFM0LTEiIHNoZWV0IChERUdzIG9mIFRIIHZzIFlIKS4KCmBgYHtyfQpzdGF0c19mdWxsID0gaW1wb3J0KCJodHRwczovL3d3dy5uY2JpLm5sbS5uaWguZ292L3BtYy9hcnRpY2xlcy9QTUM1NDczOTMxL2Jpbi80MTU5OF8yMDE3XzM5NzZfTU9FU00yX0VTTS54bHMiLCBzaGVldCA9ICJUYWJsZSBTNC0xIiwgc2tpcCA9IDEpCmBgYAoKQ2hlY2sgdGhlIGRpbWVuc2lvbnMgb2YgdGhlIHN0YXRzIHRhYmxlLgoKYGBge3J9CmRpbShzdGF0c19mdWxsKQpgYGAKCkNoZWNrIHRoZSBjb250ZW50cyBvZiB0aGUgc3RhdHMgdGFibGUuCgpgYGB7cn0KaGVhZChzdGF0c19mdWxsKQpgYGAKCgojIyBQcmVwYXJlIGRhdGEKCgojIyMgQ2xlYW4gdXAgdmFsdWVzCgpUaGUgRlBLTSB0YWJsZSBoYXMgYSBsb3Qgb2YgY29sdW1ucy4gTGV0J3MgY2hlY2sganVzdCB0aGUgRlBLTSB0YWJsZSBjb2x1bW4gbmFtZXMuCgpgYGB7cn0KY29sbmFtZXMoZnBrbV9mdWxsKQpgYGAKClRoZSBgbWFncml0dHJgIHBhY2thZ2UgYWRkcyB0aGUgYWJpbGl0eSB0byB1c2UgYCU+JWAgdG8gInBpcGUiIHRoZSBleHByZXNzaW9uIGZvcndhcmQsIGFsbG93aW5nIHRoZSBjb252ZXJzaW9uIG9mIG5lc3RlZCBmdW5jdGlvbiBjYWxscyBpbnRvIGEgc2ltcGxlIHBpcGVsaW5lIG9mIG9wZXJhdGlvbnMuIFRoaXMgbWFrZXMgdGhlIGNvZGUgZWFzaWVyIHRvIHJlYWQgYW5kIHdvcmsgd2l0aCBpZiB5b3Ugd2FudCB0byBwZXJmb3JtIG11bHRpcGxlIG9wZXJhdGlvbnMuIEZvciBleGFtcGxlLCB3ZSBjYW4gcmVwZWF0IHRoZSBwcmV2aW91cyBgY29sbmFtZXMoKWAgb3BlcmF0aW9uIHdpdGggYSBwaXBlLgoKYGBge3J9CmZwa21fZnVsbCAlPiUgY29sbmFtZXMoKQpgYGAKCkxldCdzIGNsZWFuIHVwIHRoZSB0YWJsZS4gVGhlIGBkcGx5cmAgcGFja2FnZSBoYXMgbWFueSBoZWxwZXIgZnVuY3Rpb25zIHRvIG1ha2UgdGhpcyBlYXNpZXIuIFdlJ2xsIGNyZWF0ZSBhIG5ldyBjb2x1bW4gd2l0aCBib3RoIGdlbmUgbmFtZXMgYW5kIGdlbmUgSURzIHVzaW5nIGBtdXRhdGUoKWAsIGtlZXAgb25seSByZWxldmFudCBjb2x1bW5zIHdpdGggYHNlbGVjdCgpYCwgY29udmVydCB0byBhIHN0YW5kYXJkIGRhdGEgZnJhbWUgKGZyb20gdGliYmxlKSwgYW5kIHNldCBgZ2VuZWAgY29sdW1uIGFzIHJvdyBuYW1lcy4KCmBgYHtyfQpmcGttX2NsZWFuID0gZnBrbV9mdWxsICU+JQogIG11dGF0ZShnZW5lID0gcGFzdGUwKGdlbmVfaWQsICI6IiwgZ2VuZV9uYW1lKSkgJT4lCiAgc2VsZWN0KGdlbmUsIGVuZHNfd2l0aCgiRlBLTSIpKSAlPiUKICBhcy5kYXRhLmZyYW1lKCkgJT4lCiAgY29sdW1uX3RvX3Jvd25hbWVzKCJnZW5lIikKaGVhZChmcGttX2NsZWFuKQpgYGAKCldlIGNhbiBnZXQgcmlkIG9mICJGUEtNIiBpbiBjb2x1bW4gbmFtZXMgdG8ga2VlcCB0aGVtIHNpbXBsZS4KCmBgYHtyfQpjb2xuYW1lcyhmcGttX2NsZWFuKSA9IGdzdWIoIl9GUEtNIiwgIiIsIGNvbG5hbWVzKGZwa21fY2xlYW4pKQpoZWFkKGZwa21fY2xlYW4pCmBgYAoKSW4gYWRkaXRpb24gdG8gdGhlIHNoYXBlIG9mIHRoZSB0YWJsZSwgaXQncyBpbXBvcnRhbnQgdG8gY2hlY2sgdGhlIGNvbnRlbnRzLiBXaXRoIHRob3VzYW5kcyBvZiByb3dzLCB0aGF0IGlzIGhhcmQgdG8gZG8gbWFudWFsbHkuIEEgYm94cGxvdCBjYW4gcXVpY2tseSBzaG93IHRoZSBkaXN0cmlidXRpb24uCgpgYGB7cn0KIyAibGFzID0gMiIgbWFrZXMgdGhlIHNhbXBsZSBsYWJlbHMgcGVycGVuZGljdWxhcgpib3hwbG90KGZwa21fY2xlYW4sIGxhcyA9IDIpCmBgYAoKVGhlIHJhbmdlIG9mIHZhbHVlcyBpcyBzbyBoaWdoLCB0aGUgYm94cGxvdHMgYXJlIG5vdCBldmVuIHZpc2libGUuIFdlIGNhbiBsb2ctdHJhbnNmb3JtIHRoZSB2YWx1ZXMgdG8gbWFrZSB2YXJpYXRpb24gbW9yZSBzaW1pbGFyIGFjcm9zcyBvcmRlcnMgb2YgbWFnbml0dWRlLgoKYGBge3J9CmZwa21fbG9nMiA9IGxvZzIoZnBrbV9jbGVhbiArIDEpCmJveHBsb3QoZnBrbV9sb2cyLCBsYXMgPSAyKQpgYGAKCgojIyMgQ2xlYW4gdXAgc3RhdHMKCkFzIHdlIGRpZCB3aXRoIEZQS01zLCB3ZSdsbCBjcmVhdGUgYSBuZXcgY29sdW1uIHdpdGggYm90aCBnZW5lIG5hbWVzIGFuZCBnZW5lIElEcyBzbyB0aGF0IHdlIGhhdmUgdGhhdCBpbiBjb21tb24uCgpgYGB7cn0Kc3RhdHNfZnVsbCA9IHN0YXRzX2Z1bGwgJT4lIG11dGF0ZShnZW5lID0gcGFzdGUwKGBHZW5lIElEYCwgIjoiLCBgR2VuZSBOYW1lYCkpCmhlYWQoc3RhdHNfZnVsbCkKYGBgCgpXZSBjYW4gc29ydCBieSBzaWduaWZpY2FuY2UuCgpgYGB7cn0Kc3RhdHNfZnVsbCAlPiUgYXJyYW5nZShgUC12YWx1ZWApICU+JSBoZWFkCmBgYAoKR2V0IHRoZSBsaXN0IG9mIG1vc3Qgc2lnbmlmaWNhbnQgZ2VuZXMuCgpgYGB7cn0KdG9wX2dlbmVzID0gc3RhdHNfZnVsbCAlPiUgYXJyYW5nZShgUC12YWx1ZWApICU+JSBoZWFkKDUwKSAlJCUgZ2VuZQpoZWFkKHRvcF9nZW5lcykKYGBgCgoKIyMgR2VuZXJhdGUgaGVhdG1hcHMKCgojIyMgQmFzaWMgaGVhdG1hcHMKClNlbGVjdCAxMDAgcmFuZG9tIGdlbmVzIHRvIHVzZS4gV2UnbGwgb25seSB1c2UgdGhlc2UgZ2VuZXMgZm9yIHRoZSBuZXh0IGZldyBleGFtcGxlcyB0byBzYXZlIHRpbWUuCgpgYGB7cn0KcmFuZG9tX2dlbmVzID0gc2FtcGxlKHJvd25hbWVzKGZwa21fY2xlYW4pLCAxMDApCmhlYWQocmFuZG9tX2dlbmVzKQpgYGAKCkdlbmVyYXRlIGEgc2ltcGxlIGhlYXRtYXAgdXNpbmcgdGhlIG9yaWdpbmFsIHZhbHVlcyBmb3IgMTAwIHJhbmRvbSBnZW5lcy4KCmBgYHtyfQpwaGVhdG1hcChmcGttX2NsZWFuW3JhbmRvbV9nZW5lcywgXSkKYGBgCgpHZW5lcmF0ZSBhIHNpbXBsZSBoZWF0bWFwIHVzaW5nIHRoZSBsb2ctdHJhbnNmb3JtZWQgdmFsdWVzIGZvciAxMDAgcmFuZG9tIGdlbmVzLgoKYGBge3J9CnBoZWF0bWFwKGZwa21fbG9nMltyYW5kb21fZ2VuZXMsIF0pCmBgYAoKQ2VudGVyIGFuZCBzY2FsZSBpbiB0aGUgcm93IGRpcmVjdGlvbi4KCmBgYHtyfQpwaGVhdG1hcChmcGttX2xvZzJbcmFuZG9tX2dlbmVzLCBdLCBzY2FsZSA9ICJyb3ciKQpgYGAKCgojIyMgQ29sb3Igb3B0aW9ucwoKTWFudWFsbHkgY3JlYXRlIGNvbG9yIHJhbmdlLgoKYGBge3J9Cm15X2NvbG9ycyA9IGMoImdyZWVuIiwgImJsYWNrIiwgInJlZCIpCm15X2NvbG9ycwpgYGAKCkV4cGFuZCB0aGUgY29sb3IgcmFuZ2UgZnJvbSAzIHRvIDUwIHZhbHVlcy4KCmBgYHtyfQpteV9jb2xvcnMgPSBjb2xvclJhbXBQYWxldHRlKG15X2NvbG9ycykoNTApCm15X2NvbG9ycwpgYGAKCkdlbmVyYXRlIGEgaGVhdG1hcCB3aXRoIGEgZ3JlZW4vcmVkIGNvbG9yIHNjaGVtZS4KCmBgYHtyfQpwaGVhdG1hcChmcGttX2xvZzJbcmFuZG9tX2dlbmVzLCBdLCBzY2FsZSA9ICJyb3ciLCBjb2xvciA9IG15X2NvbG9ycykKYGBgCgpSQ29sb3JCcmV3ZXIgcHJvdmlkZXMgYSBjb2xsZWN0aW9uIG9mIHByZS1tYWRlIGNvbG9yIHBhbGV0dGVzLgoKYGBge3J9CmRpc3BsYXkuYnJld2VyLmFsbCgpCmBgYAoKVXNlIGFuIFJDb2xvckJyZXdlciBjb2xvciBwYWxldHRlLgoKYGBge3J9Cm15X2NvbG9ycyA9IGJyZXdlci5wYWwobiA9IDExLCBuYW1lID0gIlJkQnUiKQpteV9jb2xvcnMgPSBjb2xvclJhbXBQYWxldHRlKG15X2NvbG9ycykoNTApCm15X2NvbG9ycyA9IHJldihteV9jb2xvcnMpCm15X2NvbG9ycwpgYGAKCkdlbmVyYXRlIGEgaGVhdG1hcCB3aXRoIGEgYmx1ZS9yZWQgY29sb3Igc2NoZW1lLgoKYGBge3J9CnBoZWF0bWFwKGZwa21fbG9nMltyYW5kb21fZ2VuZXMsIF0sIHNjYWxlID0gInJvdyIsIGNvbG9yID0gbXlfY29sb3JzKQpgYGAKCgojIyMgU2lnbmlmaWNhbnQgZ2VuZXMKClZpc3VhbGl6ZSB0aGUgbW9zdCBzaWduaWZpY2FudCBnZW5lcy4KCmBgYHtyfQpwaGVhdG1hcChmcGttX2xvZzJbdG9wX2dlbmVzLCBdLCBzY2FsZSA9ICJyb3ciLCBjb2xvciA9IG15X2NvbG9ycykKYGBgCgpSZW1vdmUgdGhlIGJvcmRlciBhcm91bmQgZWFjaCBjZWxsIGFuZCByZWR1Y2UgdGhlIHJvdyBsYWJlbCBmb250IHNpemUuCgpgYGB7cn0KcGhlYXRtYXAoZnBrbV9sb2cyW3RvcF9nZW5lcywgXSwKICAgICAgICAgc2NhbGUgPSAicm93IiwKICAgICAgICAgY29sb3IgPSBteV9jb2xvcnMsCiAgICAgICAgIGJvcmRlcl9jb2xvciA9IE5BLAogICAgICAgICBmb250c2l6ZV9yb3cgPSA0KQpgYGAKCgojIyBBZGRpdGlvbmFsIG5vdGVzCgpUaGlzIGlzIGp1c3Qgb25lIGFwcHJvYWNoIHRvIGdlbmVyYXRpbmcgYSBoZWF0bWFwLiBUaGVyZSBhcmUgbWFueSBvdGhlciB3YXlzLiBZb3UgY2FuIGV2ZW4gZG8gaXQgaW4gRXhjZWwgKHNvcnQgb2YpLiBDaGVjayB0aGlzIGxpc3Rpbmcgb2YgZ3JhcGhpY2FsIChubyBjb2RpbmcgbmVlZGVkKSBhbmQgUi1iYXNlZCBhbHRlcm5hdGl2ZXM6CltodHRwOi8vYml0Lmx5L2hlYXRtYXBvcHRpb25zXShodHRwOi8vYml0Lmx5L2hlYXRtYXBvcHRpb25zKS4K