--- title: "GGPlot2 Introduction" author: "Arno Kimeswenger" output: html_document: code_folding: none # no hide/show butten in html document number_sections: true css: "styles.css" editor_options: markdown: wrap: 72 --- ```{r setup, include=FALSE} knitr::opts_chunk$set( warning = TRUE, message = TRUE, error = TRUE ) options(warn = 1) ``` # Preliminary Work ```{r} library("tidyverse") ``` # Load Data Have a look at variables and scale levels: ```{r, tidy = FALSE} data_toy_properties <- tribble( ~variable_name, ~values, ~scale_level, ~quantity1, ~quantity2, ~r_type, "austrian", "TRUE and FALSE", "nominal", "discrete", 2, "logical", "marital_status", "married, single, divorced, widowed", "nominal", "discrete", 4, "factor", "name", "string", "nominal", "discrete", Inf, "character", "?", "?", "nominal", "continuous", Inf, "?", "age_cut", "child, adolescent, adult", "ordinal", "discrete", 3, "ordered factor", "?", "?", "ordinal", "continuous", Inf, "?", "year_birth", "2023, 2024, etc.", "metric (interval-scaled)", "discrete", Inf, "integer", "temperature", "[20, 50] °C", "metric (interval-scaled)", "continuous", Inf, "double", "test_score", "{0, 1, ..., 100} points", "metric (ratio-scaled)", "discrete", 101, "integer", "height", "[0, 300] cm", "metric (ratio-scaled)", "continuous", Inf, "double" ) data_toy_properties ``` Or more compact: ```{r} data_toy_properties_wide_1 <- data_toy_properties |> select(variable_name, scale_level, quantity1) |> pivot_wider( names_from = quantity1, # take values from quantity1 (discrete and continuous) and make columns values_from = variable_name, # these values are inserted in the new columns discrete and continuous # there is more than one value per cell to insert, therefore a function is used in each cell values_fn = ~ paste(.x, collapse = ", ") # short form of anonymous function # values_fn = function(x) paste(x, collapse = ", ") # long form of anonymous function ) |> rename( "scale level \\ quantity" = scale_level # rename column scale_level ) data_toy_properties_wide_1 ``` The following is the "inverse" map with pivot_longer. pivot_longer is used often to prepare data for plots. ```{r} data_toy_properties_wide_1 |> pivot_longer( cols = c("discrete", "continuous"), # chose columns names_to = "quantity_1", # the names (discrete and continuous) will be stored in new variable quantity_1 values_to = "variable_name" # the values (austrian, marital_status, name) ) |> separate_rows(variable_name, sep = "\\s*,\\s*") |> # to get for e.g. austrian, marital_status, name 3 rows instead of one rename("scale_level" = "scale level \\ quantity") |> # rename column name select(variable_name, scale_level, quantity_1) # new ordering ``` And how it is used in R: ```{r} data_toy_properties |> select(r_type, scale_level, quantity1) |> pivot_wider( names_from = quantity1, # take values from quantitiy1 (discrecte and continuous) and make columns values_from = r_type, # these values are inserted in the new columns discrete and continuous # there is more than one value per cell to insert, therefore a function is used in each cell values_fn = ~ paste(.x, collapse = ", ") # short form of anonymous function # values_fn = function(x) paste(x, collapse = ", ") # long form of anonymous function ) |> rename( "scale level \\ quantity" = scale_level ) ``` Read data with warning, because we do not specify the column types: ```{r} data_toy_temp <- read_csv("toy.csv") data_toy_temp |> head(3) ``` Store values for marital_status: ```{r} marital_status_values <- data_toy_temp |> pull(marital_status) |> unique() marital_status_values ``` Store values for age.cat: ```{r} age_cat_values <- data_toy_temp |> pull(age_cat) |> unique() age_cat_values ``` Change ordering (maybe it was already ok) and erase NA ```{r} age_cat_values <- age_cat_values[c(3, 2, 1)] age_cat_values ``` Read data again: ```{r} data_coltypes <- cols( name = col_character(), austrian = col_logical(), marital_status = col_factor(marital_status_values), year_birth = col_integer(), age_cat = col_factor(levels = age_cat_values, ordered = TRUE), temperature = col_double(), test_score = col_integer() ) data_toy <- read_csv("toy.csv", col_types = data_coltypes) data_toy |> head(3) ``` ```{r} data_toy |> glimpse() ``` Levels of all factor variables: ```{r} data_toy |> select(where(is.factor)) |> summary() ``` Levels of all ordered variables: ```{r} data_toy |> select(where(is.ordered)) |> summary() ``` Full summary: ```{r} data_toy |> summary() ``` # Some Boxplots ## Simple boxplot year_birth has na-values hence warnings: ```{r, fig.width=5} data_toy |> # data ggplot(aes(y = year_birth)) + # aesthetics (define what is on y axis) is used for all geom objects geom_boxplot() # define geom object (plot type here boxplot) ``` We can define aesthetics in geom_boxplot instead of ggplot. If we have more geom objects, then this can be necessary. Here it makes no difference. ```{r, fig.width=5} data_toy |> # data ggplot() + # empty command geom_boxplot(aes(y = year_birth)) # aesthetics is defined only for this geom object ``` To avoid warning, we could use filter to exclude rows where year_birth is NA. Or we use na.rm = TRUE in ggplot to do the same. ```{r, fig.width=5} data_toy |> ggplot(aes(y = year_birth)) + geom_boxplot(na.rm = TRUE) ``` Change dimensions in code chunk with e.g. {r, fig.width=1, fig.height=1.2} (the real text size stays the same at 11pt). Be careful: Here in r chunk we can only use inch not cm. So if we want to get the same text/plot ratio in e.g. pdf, we should also use inch later in ggsave. ```{r, fig.width=1, fig.height=1.2} data_toy |> ggplot(aes(y = year_birth)) + geom_boxplot(na.rm = TRUE) ``` Or with {r, fig.width=2, fig.height=2.4} ```{r, fig.width=2, fig.height=2.4} data_toy |> ggplot(aes(y = year_birth)) + geom_boxplot(na.rm = TRUE) ``` Save last plot. If we use width and height we change again the text/plot ratio because the textsize stays constant at 11pt. If we want to have the same text/plot ratio as in above r chunk, we should use inch in ggsave. Formats png and jpg are no vector graphics, I prefer pdf and svg in most cases. ```{r} ggsave("my_first_plot_a.pdf", width = 2, height = 2.4, units = "in") # compare text/plot ratio, should be as in last plot ggsave("my_first_plot_b.pdf", width = 1, height = 1.2, units = "in") # compare text/plot ratio, should be as in plot above last plot ggsave("my_first_plot_c.pdf", width = 2, height = 2.4, units = "in", dpi = 100) # compare file size, does not change for pdf (vector graphics) so dpi is unnecessary ggsave("my_first_plot_d.pdf", width = 2, height = 2.4, units = "in", dpi = 600) # compare file size, does not change for pdf (vector graphics) so dpi is unnecessary ggsave("my_first_plot_e.png", width = 2, height = 2.4, units = "in", dpi = 100) # here dpi makes a difference ggsave("my_first_plot_f.png", width = 2, height = 2.4, units = "in", dpi = 600) # here dpi makes a difference compare file size with plot e ``` Or change text size for (ideally all) plots in document: ```{r, fig.width=2, fig.height=2.4} data_toy |> ggplot(aes(y = year_birth)) + geom_boxplot(na.rm = TRUE) + theme_grey(base_size = 16) ``` ```{r} ggsave("my_first_plot_g.pdf", width = 2, height = 2.5, units = "in") ``` It is also possible to store the plot as variable: ```{r} plot1 <- data_toy |> ggplot(aes(y = year_birth)) + geom_boxplot(na.rm = TRUE) ``` Next we can show the plot: ```{r, fig.width=5} plot1 ``` Modify the plot e.g. theme: ```{r, fig.width=5} plot1 + # theme_classic() # theme_dark() # theme_minimal() # theme_grey() theme_bw() ``` And of course save the plot: ```{r} ggsave("my_first_plot_h.pdf", plot1, width = 2, height = 2.4, units = "in") ``` ## Colors and transparency ```{r, fig.height=2, fig.width=10} fh1 <- rgb(r = 97 / 255, g = 164 / 255, b = 215 / 255) # fh lightblue fh1a <- rgb(r = 97 / 255, g = 164 / 255, b = 215 / 255, alpha = 0.15) # transparancy: fh1 15% and background (maybe white) 85% factor <- 0.15 fh1b <- rgb(t(col2rgb(fh1) / 255 * factor + 1 * (1 - factor))) # fh1 15% and white 85% fh2 <- rgb(r = 22 / 255, g = 48 / 255, b = 114 / 255) # fh darkblue ggplot() + geom_rect(aes(xmin = 0, xmax = 1, ymin = 0, ymax = 1), fill = fh1) + # fh1 geom_rect(aes(xmin = 1, xmax = 2, ymin = 0, ymax = 1), fill = fh1a) + # fh1a, grids are darker, because background is gray and not white geom_rect(aes(xmin = 2, xmax = 3, ymin = 0, ymax = 1), fill = fh1b) + # fh1b, no transparency, so we do not see the grids geom_rect(aes(xmin = 3, xmax = 4, ymin = 0, ymax = 1), fill = fh2) + # fh2 theme_bw() # it is important to use white background, otherwise transparancy of fh1a is different, see grid lines ``` ## Some properties ```{r} data_toy |> ggplot(aes(y = year_birth)) + geom_boxplot( color = fh2, # lines are plotted in fh2 fill = fh1, # areas are plotted in fh1 linewidth = 1, # width of lines median.linewidth = 1, # with of median line outlier.shape = 23, # shape of outlier outlier.size = 3, # size of outlier outlier.stroke = 1, # width of outlier lines na.rm = TRUE ) + labs( title = "Year of Birth", y = "" ) + scale_x_continuous(breaks = NULL) + # This ensures no x-axis labels or ticks scale_y_continuous( # control the position of breaks, normally I keep standard values breaks = 1920 + (0:4) * 20, minor_breaks = 1930 + (0:4) * 20 ) + theme( panel.background = element_rect(fill = fh1b), # background color panel.grid.major.y = element_line(color = "black", linetype = "solid", linewidth = 0.1), # major y-grid panel.grid.minor.y = element_line(color = "black", linetype = "dashed", linewidth = 0.05), # minor y-grid # panel.grid.minor.y = element_blank(), # or if we do not like to see minor grid text = element_text(face = "bold", size = 11), # font properties axis.ticks = element_blank() # no ticks e.g. right of values 1940, 1960, ... on y-axis ) ``` ## More than one geom in one plot We could store the above plot as variable e.g. plot2 and reuse it here. but it can make a difference which geom object (violin, boxplot) and gridlines etc. are used first. ```{r} data_toy |> ggplot(aes(x = 0, y = year_birth)) + # geom_violin and geom_point need x value geom_violin( # violin plot linewidth = 1, color = fh2, fill = fh1, alpha = 0.3, width = 4.5, na.rm = TRUE ) + geom_boxplot( # boxplot color = fh2, fill = fh1, linewidth = 1, median.linewidth = 1, outlier.shape = 23, outlier.size = 3, outlier.stroke = 1, na.rm = TRUE ) + stat_boxplot( # error bars geom = "errorbar", color = fh2, linewidth = 1, na.rm = TRUE ) + geom_point( # data points position = position_jitter(width = 0.2), # jittered is used (randomly x value) color = fh2, na.rm = TRUE ) + labs( title = "Year of Birth", x = "", y = "" ) + scale_x_continuous( breaks = NULL # This ensures no x-axis labels or ticks ) + scale_y_continuous( # control the position of breaks, normally I keep standard values breaks = 1920 + (0:4) * 20, minor_breaks = 1930 + (0:4) * 20 ) + theme( panel.background = element_rect(fill = fh1b), # background color panel.grid.major.y = element_line(color = "black", linetype = "solid", linewidth = 0.1), # major y-grid panel.grid.minor.y = element_line(color = "black", linetype = "dashed", linewidth = 0.05), # minor y-grid # panel.grid.minor.y = element_blank(), # or if we do not like to see minor grid text = element_text(face = "bold", size = 11), # font properties axis.ticks = element_blank() # no ticks e.g. right of values 1940, 1960, ... on y-axis ) ``` ## Plot by factor variable Plot by marital_status ```{r, fig.width=8} data_toy |> ggplot(aes(x = marital_status, y = year_birth)) + # we get 4 boxplot because marital_status has 4 values geom_boxplot( color = fh2, fill = fh1, linewidth = 1, median.linewidth = 1, outlier.shape = 23, outlier.size = 3, outlier.stroke = 1, na.rm = TRUE ) + stat_boxplot( geom = "errorbar", color = fh2, linewidth = 1, na.rm = TRUE ) + labs( title = "Year of Birth\nBy marital status", x = "", y = "" ) + scale_y_continuous( # control the position of breaks, normally I keep standard values breaks = 1920 + (0:4) * 20, minor_breaks = 1930 + (0:4) * 20 ) + theme( panel.background = element_rect(fill = fh1b), panel.grid.major.y = element_line(color = "black", linetype = "solid", linewidth = 0.1), panel.grid.major.x = element_blank(), panel.grid.minor.y = element_line(color = "black", linetype = "dashed", linewidth = 0.05), text = element_text(face = "bold", size = 11), axis.ticks = element_blank() ) ``` ## Using variable for property We again plot by marital_status, but furthermore we use this variable for color and fill. There is no real advantage in using different colors here for the 3 boxes. ```{r, fig.width=8} factor2 <- 0.3 darkred_alpha <- rgb(t(col2rgb("darkred") / 255 * factor2 + 1 * (1 - factor2))) darkgreen_alpha <- rgb(t(col2rgb("darkgreen") / 255 * factor2 + 1 * (1 - factor2))) darkblue_alpha <- rgb(t(col2rgb("darkblue") / 255 * factor2 + 1 * (1 - factor2))) gray42_alpha <- rgb(t(col2rgb("gray42") / 255 * factor2 + 1 * (1 - factor2))) data_toy |> ggplot(aes(x = marital_status, y = year_birth)) + geom_boxplot(aes(color = marital_status, fill = marital_status), # each boxplot gets its own color, we do not use default colors, therefore we use scale_color_manual and scale_fill_manual later linewidth = 1, median.linewidth = 1, outlier.shape = 23, outlier.size = 3, outlier.stroke = 1, na.rm = TRUE ) + stat_boxplot(aes(color = marital_status), geom = "errorbar", linewidth = 1, na.rm = TRUE ) + labs( title = "Year of Birth\nBy marital status", x = "", y = "" ) + scale_color_manual(values = c("single" = "darkred", "married" = "darkblue", "divorced" = "darkgreen", "widowed" = "gray42")) + # use our own color selection for color scale_fill_manual(values = c("single" = darkred_alpha, "married" = darkblue_alpha, "divorced" = darkgreen_alpha, "widowed" = gray42_alpha)) + # use our own color selection for fill theme( legend.position = "none", # legend is not necessary here axis.ticks = element_blank() ) ``` ## Using facet grid Here we use marital_status again: ```{r, fig.width=8} data_toy |> ggplot(aes(y = year_birth)) + # we do not use x="marital_status" because we use facets geom_boxplot(aes(color = marital_status, fill = marital_status), linewidth = 1, median.linewidth = 1, outlier.shape = 23, outlier.size = 3, na.rm = TRUE ) + stat_boxplot(aes(color = marital_status), geom = "errorbar", linewidth = 1, na.rm = TRUE ) + labs( title = "Geburtsjahr\nnach Familienstand", x = "", y = "" ) + facet_grid(. ~ marital_status, # facet creates again 4 boxplots labeller = as_labeller(c("single" = "ledig", "married" = "verheiratet", "divorced" = "geschieden", "widowed" = "verwitwet")) # be careful here! ) + scale_x_continuous( breaks = NULL # This ensures no x-axis labels or ticks ) + scale_y_continuous( # control the position of breaks, normally I keep standard values breaks = 1920 + (0:4) * 20, minor_breaks = 1930 + (0:4) * 20 ) + scale_color_manual(values = c("single" = "darkred", "married" = "darkblue", "divorced" = "darkgreen", "widowed" = "gray42")) + # use our own color selection for color scale_fill_manual(values = c("single" = darkred_alpha, "married" = darkblue_alpha, "divorced" = darkgreen_alpha, "widowed" = gray42_alpha)) + # use our own color selection for fill theme( legend.position = "none", # we do not need legend here axis.ticks = element_blank() # no ticks on x-axis ) ``` But we can use another factor to get a "matrix". Here we use austrian for rows and marital_status for columns. ```{r, fig.width=8} data_toy |> filter(!is.na(austrian)) |> # we do not want to get a row for austrian == NA ggplot(aes(y = year_birth)) + # we do not use x="marital_status" because we use facet geom_boxplot(aes(color = marital_status, fill = marital_status), linewidth = 1, median.linewidth = 1, outlier.shape = 23, outlier.size = 3, na.rm = TRUE ) + stat_boxplot(aes(color = marital_status), geom = "errorbar", linewidth = 1, na.rm = TRUE ) + labs( title = "Geburtsjahr\nnach Staatsbürgerschaft und Familienstand", x = "", y = "" ) + facet_grid(austrian ~ marital_status, labeller = as_labeller(c( "single" = "ledig", "married" = "verheiratet", "divorced" = "geschieden", "widowed" = "verwitwet", # be careful here "TRUE" = "Österreicher/in", "FALSE" = "kein/e Österreicher/in" # be careful here )) ) + scale_x_continuous( breaks = NULL # This ensures no x-axis labels or ticks ) + scale_y_continuous( # control the position of breaks, normally I keep standard values breaks = 1920 + (0:4) * 20, minor_breaks = 1930 + (0:4) * 20 ) + scale_color_manual(values = c("single" = "darkred", "married" = "darkblue", "divorced" = "darkgreen", "widowed" = "gray42")) + # use our own color selection for color scale_fill_manual(values = c("single" = darkred_alpha, "married" = darkblue_alpha, "divorced" = darkgreen_alpha, "widowed" = gray42_alpha)) + # use our own color selection for fill theme( legend.position = "none", # we do not need legend here axis.ticks = element_blank() # no ticks on x-axis ) ``` Maybe we want to use different colors for Austrian as well. For that we define a new variable in our data_toy. ```{r} # for demonstration only data_toy |> mutate( color = interaction(marital_status, austrian, sep = "_") ) |> distinct(color) ``` ```{r, fig.width=8} factor2 <- 0.3 # blend with white darkred_alpha <- rgb(t(col2rgb("darkred") / 255 * factor2 + 1 * (1 - factor2))) darkgreen_alpha <- rgb(t(col2rgb("darkgreen") / 255 * factor2 + 1 * (1 - factor2))) darkblue_alpha <- rgb(t(col2rgb("darkblue") / 255 * factor2 + 1 * (1 - factor2))) gray42_alpha <- rgb(t(col2rgb("gray42") / 255 * factor2 + 1 * (1 - factor2))) factor3 <- 0.5 # blend with black darkred_alpha2 <- rgb(t(col2rgb("darkred") / 255 * factor3 + 0 * (1 - factor3))) darkgreen_alpha2 <- rgb(t(col2rgb("darkgreen") / 255 * factor3 + 0 * (1 - factor3))) darkblue_alpha2 <- rgb(t(col2rgb("darkblue") / 255 * factor3 + 0 * (1 - factor3))) gray42_alpha2 <- rgb(t(col2rgb("gray42") / 255 * factor3 + 0 * (1 - factor3))) # blend with white darkred_alpha3 <- rgb(t(col2rgb(darkred_alpha2) / 255 * factor2 + 1 * (1 - factor2))) darkgreen_alpha3 <- rgb(t(col2rgb(darkgreen_alpha) / 255 * factor2 + 1 * (1 - factor2))) darkblue_alpha3 <- rgb(t(col2rgb(darkblue_alpha2) / 255 * factor2 + 1 * (1 - factor2))) gray42_alpha3 <- rgb(t(col2rgb(gray42_alpha2) / 255 * factor2 + 1 * (1 - factor2))) data_toy |> filter(!is.na(austrian)) |> mutate(color = interaction(marital_status, austrian, sep = "_")) |> # add the color e.g. single_FALSE ggplot(aes(y = year_birth)) + # we do not use x = "marital_status" because we use facet geom_boxplot(aes(color = color, fill = color), linewidth = 1, median.linewidth = 1, outlier.shape = 23, outlier.size = 3, na.rm = TRUE ) + stat_boxplot(aes(color = color), geom = "errorbar", linewidth = 1, na.rm = TRUE ) + labs( title = "Geburtsjahr\nnach Staatsbürgerschaft und Familienstand", x = "", y = "" ) + facet_grid(austrian ~ marital_status, labeller = as_labeller(c( "single" = "ledig", "married" = "verheiratet", "divorced" = "geschieden", "widowed" = "verwitwet", # be careful here "TRUE" = "Österreicher/in", "FALSE" = "kein/e Österreicher/in" # be careful here )) ) + scale_x_continuous( breaks = NULL # This ensures no x-axis labels or ticks ) + scale_y_continuous( # control the position of breaks, normally I keep standard values breaks = 1920 + (0:4) * 20, minor_breaks = 1930 + (0:4) * 20 ) + scale_color_manual(values = c( "single_FALSE" = "darkred", "single_TRUE" = darkred_alpha2, "married_FALSE" = "darkblue", "married_TRUE" = darkblue_alpha2, "divorced_FALSE" = "darkgreen", "divorced_TRUE" = darkgreen_alpha2, "widowed_FALSE" = "gray42", "widowed_TRUE" = gray42_alpha2 )) + # use our own color selection for color scale_fill_manual(values = c( "single_FALSE" = darkred_alpha, "single_TRUE" = darkred_alpha3, "married_FALSE" = darkblue_alpha, "married_TRUE" = darkblue_alpha3, "divorced_FALSE" = darkgreen_alpha, "divorced_TRUE" = darkgreen_alpha3, "widowed_FALSE" = gray42_alpha, "widowed_TRUE" = gray42_alpha3 )) + # use our own color selection for fill theme( legend.position = "none", # we do not need legend here axis.ticks = element_blank() # no ticks on x-axis ) ```