[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]

[tor-commits] [metrics-web/release] Simplify Rserve setup.



commit e82de493279a0e74b55e5fd66a4056a1cecf19c5
Author: Karsten Loesing <karsten.loesing@xxxxxxx>
Date:   Fri Jan 11 11:39:12 2019 +0100

    Simplify Rserve setup.
---
 src/main/R/rserver/Rserv.conf    |    2 -
 src/main/R/rserver/graphs.R      | 1539 ------------------------------------
 src/main/R/rserver/rserve-init.R | 1609 +++++++++++++++++++++++++++++++++++++-
 src/main/R/rserver/tables.R      |   58 --
 4 files changed, 1600 insertions(+), 1608 deletions(-)

diff --git a/src/main/R/rserver/Rserv.conf b/src/main/R/rserver/Rserv.conf
deleted file mode 100644
index 1fb3039..0000000
--- a/src/main/R/rserver/Rserv.conf
+++ /dev/null
@@ -1,2 +0,0 @@
-workdir /srv/metrics.torproject.org/metrics/website/rserve/workdir
-source rserve-init.R
diff --git a/src/main/R/rserver/graphs.R b/src/main/R/rserver/graphs.R
deleted file mode 100644
index 0d7a90c..0000000
--- a/src/main/R/rserver/graphs.R
+++ /dev/null
@@ -1,1539 +0,0 @@
-countrylist <- list(
-  "ad" = "Andorra",
-  "ae" = "the United Arab Emirates",
-  "af" = "Afghanistan",
-  "ag" = "Antigua and Barbuda",
-  "ai" = "Anguilla",
-  "al" = "Albania",
-  "am" = "Armenia",
-  "an" = "the Netherlands Antilles",
-  "ao" = "Angola",
-  "aq" = "Antarctica",
-  "ar" = "Argentina",
-  "as" = "American Samoa",
-  "at" = "Austria",
-  "au" = "Australia",
-  "aw" = "Aruba",
-  "ax" = "the Aland Islands",
-  "az" = "Azerbaijan",
-  "ba" = "Bosnia and Herzegovina",
-  "bb" = "Barbados",
-  "bd" = "Bangladesh",
-  "be" = "Belgium",
-  "bf" = "Burkina Faso",
-  "bg" = "Bulgaria",
-  "bh" = "Bahrain",
-  "bi" = "Burundi",
-  "bj" = "Benin",
-  "bl" = "Saint Bartelemey",
-  "bm" = "Bermuda",
-  "bn" = "Brunei",
-  "bo" = "Bolivia",
-  "bq" = "Bonaire, Sint Eustatius and Saba",
-  "br" = "Brazil",
-  "bs" = "the Bahamas",
-  "bt" = "Bhutan",
-  "bv" = "the Bouvet Island",
-  "bw" = "Botswana",
-  "by" = "Belarus",
-  "bz" = "Belize",
-  "ca" = "Canada",
-  "cc" = "the Cocos (Keeling) Islands",
-  "cd" = "the Democratic Republic of the Congo",
-  "cf" = "Central African Republic",
-  "cg" = "Congo",
-  "ch" = "Switzerland",
-  "ci" = "Côte d'Ivoire",
-  "ck" = "the Cook Islands",
-  "cl" = "Chile",
-  "cm" = "Cameroon",
-  "cn" = "China",
-  "co" = "Colombia",
-  "cr" = "Costa Rica",
-  "cu" = "Cuba",
-  "cv" = "Cape Verde",
-  "cw" = "Curaçao",
-  "cx" = "the Christmas Island",
-  "cy" = "Cyprus",
-  "cz" = "the Czech Republic",
-  "de" = "Germany",
-  "dj" = "Djibouti",
-  "dk" = "Denmark",
-  "dm" = "Dominica",
-  "do" = "the Dominican Republic",
-  "dz" = "Algeria",
-  "ec" = "Ecuador",
-  "ee" = "Estonia",
-  "eg" = "Egypt",
-  "eh" = "the Western Sahara",
-  "er" = "Eritrea",
-  "es" = "Spain",
-  "et" = "Ethiopia",
-  "fi" = "Finland",
-  "fj" = "Fiji",
-  "fk" = "the Falkland Islands (Malvinas)",
-  "fm" = "the Federated States of Micronesia",
-  "fo" = "the Faroe Islands",
-  "fr" = "France",
-  "ga" = "Gabon",
-  "gb" = "the United Kingdom",
-  "gd" = "Grenada",
-  "ge" = "Georgia",
-  "gf" = "French Guiana",
-  "gg" = "Guernsey",
-  "gh" = "Ghana",
-  "gi" = "Gibraltar",
-  "gl" = "Greenland",
-  "gm" = "Gambia",
-  "gn" = "Guinea",
-  "gp" = "Guadeloupe",
-  "gq" = "Equatorial Guinea",
-  "gr" = "Greece",
-  "gs" = "South Georgia and the South Sandwich Islands",
-  "gt" = "Guatemala",
-  "gu" = "Guam",
-  "gw" = "Guinea-Bissau",
-  "gy" = "Guyana",
-  "hk" = "Hong Kong",
-  "hm" = "Heard Island and McDonald Islands",
-  "hn" = "Honduras",
-  "hr" = "Croatia",
-  "ht" = "Haiti",
-  "hu" = "Hungary",
-  "id" = "Indonesia",
-  "ie" = "Ireland",
-  "il" = "Israel",
-  "im" = "the Isle of Man",
-  "in" = "India",
-  "io" = "the British Indian Ocean Territory",
-  "iq" = "Iraq",
-  "ir" = "Iran",
-  "is" = "Iceland",
-  "it" = "Italy",
-  "je" = "Jersey",
-  "jm" = "Jamaica",
-  "jo" = "Jordan",
-  "jp" = "Japan",
-  "ke" = "Kenya",
-  "kg" = "Kyrgyzstan",
-  "kh" = "Cambodia",
-  "ki" = "Kiribati",
-  "km" = "Comoros",
-  "kn" = "Saint Kitts and Nevis",
-  "kp" = "North Korea",
-  "kr" = "the Republic of Korea",
-  "kw" = "Kuwait",
-  "ky" = "the Cayman Islands",
-  "kz" = "Kazakhstan",
-  "la" = "Laos",
-  "lb" = "Lebanon",
-  "lc" = "Saint Lucia",
-  "li" = "Liechtenstein",
-  "lk" = "Sri Lanka",
-  "lr" = "Liberia",
-  "ls" = "Lesotho",
-  "lt" = "Lithuania",
-  "lu" = "Luxembourg",
-  "lv" = "Latvia",
-  "ly" = "Libya",
-  "ma" = "Morocco",
-  "mc" = "Monaco",
-  "md" = "the Republic of Moldova",
-  "me" = "Montenegro",
-  "mf" = "Saint Martin",
-  "mg" = "Madagascar",
-  "mh" = "the Marshall Islands",
-  "mk" = "Macedonia",
-  "ml" = "Mali",
-  "mm" = "Burma",
-  "mn" = "Mongolia",
-  "mo" = "Macau",
-  "mp" = "the Northern Mariana Islands",
-  "mq" = "Martinique",
-  "mr" = "Mauritania",
-  "ms" = "Montserrat",
-  "mt" = "Malta",
-  "mu" = "Mauritius",
-  "mv" = "the Maldives",
-  "mw" = "Malawi",
-  "mx" = "Mexico",
-  "my" = "Malaysia",
-  "mz" = "Mozambique",
-  "na" = "Namibia",
-  "nc" = "New Caledonia",
-  "ne" = "Niger",
-  "nf" = "Norfolk Island",
-  "ng" = "Nigeria",
-  "ni" = "Nicaragua",
-  "nl" = "the Netherlands",
-  "no" = "Norway",
-  "np" = "Nepal",
-  "nr" = "Nauru",
-  "nu" = "Niue",
-  "nz" = "New Zealand",
-  "om" = "Oman",
-  "pa" = "Panama",
-  "pe" = "Peru",
-  "pf" = "French Polynesia",
-  "pg" = "Papua New Guinea",
-  "ph" = "the Philippines",
-  "pk" = "Pakistan",
-  "pl" = "Poland",
-  "pm" = "Saint Pierre and Miquelon",
-  "pn" = "the Pitcairn Islands",
-  "pr" = "Puerto Rico",
-  "ps" = "the Palestinian Territory",
-  "pt" = "Portugal",
-  "pw" = "Palau",
-  "py" = "Paraguay",
-  "qa" = "Qatar",
-  "re" = "Reunion",
-  "ro" = "Romania",
-  "rs" = "Serbia",
-  "ru" = "Russia",
-  "rw" = "Rwanda",
-  "sa" = "Saudi Arabia",
-  "sb" = "the Solomon Islands",
-  "sc" = "the Seychelles",
-  "sd" = "Sudan",
-  "se" = "Sweden",
-  "sg" = "Singapore",
-  "sh" = "Saint Helena",
-  "si" = "Slovenia",
-  "sj" = "Svalbard and Jan Mayen",
-  "sk" = "Slovakia",
-  "sl" = "Sierra Leone",
-  "sm" = "San Marino",
-  "sn" = "Senegal",
-  "so" = "Somalia",
-  "sr" = "Suriname",
-  "ss" = "South Sudan",
-  "st" = "São Tomé and Príncipe",
-  "sv" = "El Salvador",
-  "sx" = "Sint Maarten",
-  "sy" = "the Syrian Arab Republic",
-  "sz" = "Swaziland",
-  "tc" = "Turks and Caicos Islands",
-  "td" = "Chad",
-  "tf" = "the French Southern Territories",
-  "tg" = "Togo",
-  "th" = "Thailand",
-  "tj" = "Tajikistan",
-  "tk" = "Tokelau",
-  "tl" = "East Timor",
-  "tm" = "Turkmenistan",
-  "tn" = "Tunisia",
-  "to" = "Tonga",
-  "tr" = "Turkey",
-  "tt" = "Trinidad and Tobago",
-  "tv" = "Tuvalu",
-  "tw" = "Taiwan",
-  "tz" = "the United Republic of Tanzania",
-  "ua" = "Ukraine",
-  "ug" = "Uganda",
-  "um" = "the United States Minor Outlying Islands",
-  "us" = "the United States",
-  "uy" = "Uruguay",
-  "uz" = "Uzbekistan",
-  "va" = "Vatican City",
-  "vc" = "Saint Vincent and the Grenadines",
-  "ve" = "Venezuela",
-  "vg" = "the British Virgin Islands",
-  "vi" = "the United States Virgin Islands",
-  "vn" = "Vietnam",
-  "vu" = "Vanuatu",
-  "wf" = "Wallis and Futuna",
-  "ws" = "Samoa",
-  "xk" = "Kosovo",
-  "ye" = "Yemen",
-  "yt" = "Mayotte",
-  "za" = "South Africa",
-  "zm" = "Zambia",
-  "zw" = "Zimbabwe")
-
-countryname <- function(country) {
-  res <- countrylist[[country]]
-  if (is.null(res))
-    res <- "no-man's-land"
-  res
-}
-
-# Helper function that takes date limits as input and returns major breaks as
-# output. The main difference to the built-in major breaks is that we're trying
-# harder to align major breaks with first days of weeks (Sundays), months,
-# quarters, or years.
-custom_breaks <- function(input) {
-  scales_index <- cut(as.numeric(max(input) - min(input)),
-    c(-1, 7, 12, 56, 180, 600, 2000, Inf), labels = FALSE)
-  from_print_format <- c("%F", "%F", "%Y-W%U-7", "%Y-%m-01", "%Y-01-01",
-    "%Y-01-01", "%Y-01-01")[scales_index]
-  from_parse_format <- ifelse(scales_index == 3, "%Y-W%U-%u", "%F")
-  by <- c("1 day", "2 days", "1 week", "1 month", "3 months", "1 year",
-    "2 years")[scales_index]
-  seq(as.Date(as.character(min(input), from_print_format),
-    format = from_parse_format), max(input), by = by)
-}
-
-# Helper function that takes date limits as input and returns minor breaks as
-# output. As opposed to the built-in minor breaks, we're not just adding one
-# minor break half way through between two major breaks. Instead, we're plotting
-# a minor break for every day, week, month, or quarter between two major breaks.
-custom_minor_breaks <- function(input) {
-  scales_index <- cut(as.numeric(max(input) - min(input)),
-    c(-1, 7, 12, 56, 180, 600, 2000, Inf), labels = FALSE)
-  from_print_format <- c("%F", "%F", "%F", "%Y-W%U-7", "%Y-%m-01", "%Y-01-01",
-    "%Y-01-01")[scales_index]
-  from_parse_format <- ifelse(scales_index == 4, "%Y-W%U-%u", "%F")
-  by <- c("1 day", "1 day", "1 day", "1 week", "1 month", "3 months",
-    "1 year")[scales_index]
-  seq(as.Date(as.character(min(input), from_print_format),
-    format = from_parse_format), max(input), by = by)
-}
-
-# Helper function that takes breaks as input and returns labels as output. We're
-# going all ISO-8601 here, though we're not just writing %Y-%m-%d everywhere,
-# but %Y-%m or %Y if all breaks are on the first of a month or even year.
-custom_labels <- function(breaks) {
-  if (all(format(breaks, format = "%m-%d") == "01-01", na.rm = TRUE)) {
-    format(breaks, format = "%Y")
-  } else {
-    if (all(format(breaks, format = "%d") == "01", na.rm = TRUE)) {
-      format(breaks, format = "%Y-%m")
-    } else {
-      format(breaks, format = "%F")
-    }
-  }
-}
-
-# Helper function to format numbers in non-scientific notation with spaces as
-# thousands separator.
-formatter <- function(x, ...) {
-  format(x, ..., scientific = FALSE, big.mark = " ")
-}
-
-theme_update(
-  # Make plot title centered, and leave some room to the plot.
-  plot.title = element_text(hjust = 0.5, margin = margin(b = 11)),
-
-  # Leave a little more room to the right for long x axis labels.
-  plot.margin = margin(5.5, 11, 5.5, 5.5)
-)
-
-# Set the default line size of geom_line() to 1.
-update_geom_defaults("line", list(size = 1))
-
-copyright_notice <- "The Tor Project - https://metrics.torproject.org/";
-
-stats_dir <- "/srv/metrics.torproject.org/metrics/shared/stats/"
-
-rdata_dir <- "/srv/metrics.torproject.org/metrics/shared/RData/"
-
-# Helper function that copies the appropriate no data object to filename.
-copy_no_data <- function(filename) {
-  len <- nchar(filename)
-  extension <- substr(filename, len - 3, len)
-  if (".csv" == extension) {
-    write("# No data available for the given parameters.", file=filename)
-  } else {
-    file.copy(paste(rdata_dir, "no-data-available", extension, sep = ""),
-      filename)
-  }
-}
-
-# Helper function wrapping calls into error handling.
-robust_call <- function(wrappee, filename) {
-  tryCatch(eval(wrappee), error = function(e) copy_no_data(filename),
-     finally = if (!file.exists(filename) || file.size(filename) == 0) {
-       copy_no_data(filename)
-       })
-}
-
-# Write the result of the given FUN, typically a prepare_ function, as .csv file
-# to the given path_p.
-write_data <- function(FUN, ..., path_p) {
-  FUN(...) %>%
-    write.csv(path_p, quote = FALSE, row.names = FALSE, na = "")
-}
-
-# Disable readr's automatic progress bar.
-options(readr.show_progress = FALSE)
-
-prepare_networksize <- function(start_p = NULL, end_p = NULL) {
-  read_csv(file = paste(stats_dir, "networksize.csv", sep = ""),
-      col_types = cols(
-        date = col_date(format = ""),
-        relays = col_double(),
-        bridges = col_double())) %>%
-    filter(if (!is.null(start_p)) date >= as.Date(start_p) else TRUE) %>%
-    filter(if (!is.null(end_p)) date <= as.Date(end_p) else TRUE)
-}
-
-plot_networksize <- function(start_p, end_p, path_p) {
-  prepare_networksize(start_p, end_p) %>%
-    gather(variable, value, -date) %>%
-    complete(date = full_seq(date, period = 1),
-      variable = c("relays", "bridges")) %>%
-    ggplot(aes(x = date, y = value, colour = variable)) +
-    geom_line() +
-    scale_x_date(name = "", breaks = custom_breaks,
-      labels = custom_labels, minor_breaks = custom_minor_breaks) +
-    scale_y_continuous(name = "", labels = formatter, limits = c(0, NA)) +
-    scale_colour_hue("", breaks = c("relays", "bridges"),
-        labels = c("Relays", "Bridges")) +
-    ggtitle("Number of relays") +
-    labs(caption = copyright_notice)
-  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
-}
-
-prepare_versions <- function(start_p = NULL, end_p = NULL) {
-  read_csv(paste(stats_dir, "versions.csv", sep = ""),
-      col_types = cols(
-        date = col_date(format = ""),
-        version = col_character(),
-        relays = col_double())) %>%
-    filter(if (!is.null(start_p)) date >= as.Date(start_p) else TRUE) %>%
-    filter(if (!is.null(end_p)) date <= as.Date(end_p) else TRUE)
-}
-
-plot_versions <- function(start_p, end_p, path_p) {
-  s <- prepare_versions(start_p, end_p)
-  known_versions <- unique(s$version)
-  getPalette <- colorRampPalette(brewer.pal(12, "Paired"))
-  colours <- data.frame(breaks = known_versions,
-    values = rep(brewer.pal(min(12, length(known_versions)), "Paired"),
-                 len = length(known_versions)),
-    stringsAsFactors = FALSE)
-  versions <- s[s$version %in% known_versions, ]
-  visible_versions <- sort(unique(versions$version))
-  versions <- versions %>%
-    complete(date = full_seq(date, period = 1), nesting(version)) %>%
-    ggplot(aes(x = date, y = relays, colour = version)) +
-    geom_line() +
-    scale_x_date(name = "", breaks = custom_breaks,
-      labels = custom_labels, minor_breaks = custom_minor_breaks) +
-    scale_y_continuous(name = "", labels = formatter, limits = c(0, NA)) +
-    scale_colour_manual(name = "Tor version",
-      values = colours[colours$breaks %in% visible_versions, 2],
-      breaks = visible_versions) +
-    ggtitle("Relay versions") +
-    labs(caption = copyright_notice)
-  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
-}
-
-prepare_platforms <- function(start_p = NULL, end_p = NULL) {
-  read_csv(file = paste(stats_dir, "platforms.csv", sep = ""),
-      col_types = cols(
-        date = col_date(format = ""),
-        platform = col_factor(levels = NULL),
-        relays = col_double())) %>%
-    filter(if (!is.null(start_p)) date >= as.Date(start_p) else TRUE) %>%
-    filter(if (!is.null(end_p)) date <= as.Date(end_p) else TRUE) %>%
-    mutate(platform = tolower(platform)) %>%
-    spread(platform, relays)
-}
-
-plot_platforms <- function(start_p, end_p, path_p) {
-  prepare_platforms(start_p, end_p) %>%
-    gather(platform, relays, -date) %>%
-    complete(date = full_seq(date, period = 1), nesting(platform)) %>%
-    ggplot(aes(x = date, y = relays, colour = platform)) +
-    geom_line() +
-    scale_x_date(name = "", breaks = custom_breaks,
-      labels = custom_labels, minor_breaks = custom_minor_breaks) +
-    scale_y_continuous(name = "", labels = formatter, limits = c(0, NA)) +
-    scale_colour_manual(name = "Platform",
-      breaks = c("linux", "macos", "bsd", "windows", "other"),
-      labels = c("Linux", "macOS", "BSD", "Windows", "Other"),
-      values = c("linux" = "#56B4E9", "macos" = "#333333", "bsd" = "#E69F00",
-          "windows" = "#0072B2", "other" = "#009E73")) +
-    ggtitle("Relay platforms") +
-    labs(caption = copyright_notice)
-  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
-}
-
-prepare_dirbytes <- function(start_p = NULL, end_p = NULL) {
-  read_csv(file = paste(stats_dir, "bandwidth.csv", sep = ""),
-      col_types = cols(
-        date = col_date(format = ""),
-        isexit = col_logical(),
-        isguard = col_logical(),
-        bwread = col_skip(),
-        bwwrite = col_skip(),
-        dirread = col_double(),
-        dirwrite = col_double())) %>%
-    filter(if (!is.null(start_p)) date >= as.Date(start_p) else TRUE) %>%
-    filter(if (!is.null(end_p)) date <= as.Date(end_p) else TRUE) %>%
-    filter(is.na(isexit)) %>%
-    filter(is.na(isguard)) %>%
-    mutate(dirread = dirread * 8 / 1e9,
-      dirwrite = dirwrite * 8 / 1e9) %>%
-    select(date, dirread, dirwrite)
-}
-
-plot_dirbytes <- function(start_p, end_p, path_p) {
-  prepare_dirbytes(start_p, end_p) %>%
-    gather(variable, value, -date) %>%
-    complete(date = full_seq(date, period = 1), nesting(variable)) %>%
-    ggplot(aes(x = date, y = value, colour = variable)) +
-    geom_line() +
-    scale_x_date(name = "", breaks = custom_breaks,
-      labels = custom_labels, minor_breaks = custom_minor_breaks) +
-    scale_y_continuous(name = "", labels = unit_format(unit = "Gbit/s"),
-      limits = c(0, NA)) +
-    scale_colour_hue(name = "",
-        breaks = c("dirwrite", "dirread"),
-        labels = c("Written dir bytes", "Read dir bytes")) +
-    ggtitle("Number of bytes spent on answering directory requests") +
-    labs(caption = copyright_notice) +
-    theme(legend.position = "top")
-  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
-}
-
-prepare_relayflags <- function(start_p = NULL, end_p = NULL, flag_p = NULL) {
-  read_csv(file = paste(stats_dir, "relayflags.csv", sep = ""),
-      col_types = cols(
-        date = col_date(format = ""),
-        flag = col_factor(levels = NULL),
-        relays = col_double())) %>%
-    filter(if (!is.null(start_p)) date >= as.Date(start_p) else TRUE) %>%
-    filter(if (!is.null(end_p)) date <= as.Date(end_p) else TRUE) %>%
-    filter(if (!is.null(flag_p)) flag %in% flag_p else TRUE)
-}
-
-plot_relayflags <- function(start_p, end_p, flag_p, path_p) {
-  prepare_relayflags(start_p, end_p, flag_p) %>%
-    complete(date = full_seq(date, period = 1), flag = unique(flag)) %>%
-    ggplot(aes(x = date, y = relays, colour = flag)) +
-    geom_line() +
-    scale_x_date(name = "", breaks = custom_breaks,
-      labels = custom_labels, minor_breaks = custom_minor_breaks) +
-    scale_y_continuous(name = "", labels = formatter, limits = c(0, NA)) +
-    scale_colour_manual(name = "Relay flags", values = c("#E69F00",
-        "#56B4E9", "#009E73", "#EE6A50", "#000000", "#0072B2"),
-        breaks = flag_p, labels = flag_p) +
-    ggtitle("Number of relays with relay flags assigned") +
-    labs(caption = copyright_notice)
-  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
-}
-
-prepare_torperf <- function(start_p = NULL, end_p = NULL, server_p = NULL,
-    filesize_p = NULL) {
-  read_csv(file = paste(stats_dir, "torperf-1.1.csv", sep = ""),
-      col_types = cols(
-        date = col_date(format = ""),
-        filesize = col_double(),
-        source = col_character(),
-        server = col_character(),
-        q1 = col_double(),
-        md = col_double(),
-        q3 = col_double(),
-        timeouts = col_skip(),
-        failures = col_skip(),
-        requests = col_skip())) %>%
-    filter(if (!is.null(start_p)) date >= as.Date(start_p) else TRUE) %>%
-    filter(if (!is.null(end_p)) date <= as.Date(end_p) else TRUE) %>%
-    filter(if (!is.null(server_p)) server == server_p else TRUE) %>%
-    filter(if (!is.null(filesize_p))
-        filesize == ifelse(filesize_p == "50kb", 50 * 1024,
-        ifelse(filesize_p == "1mb", 1024 * 1024, 5 * 1024 * 1024)) else
-        TRUE) %>%
-    transmute(date, filesize, source, server, q1 = q1 / 1e3, md = md / 1e3,
-      q3 = q3 / 1e3)
-}
-
-plot_torperf <- function(start_p, end_p, server_p, filesize_p, path_p) {
-  prepare_torperf(start_p, end_p, server_p, filesize_p) %>%
-    filter(source != "") %>%
-    complete(date = full_seq(date, period = 1), nesting(source)) %>%
-    ggplot(aes(x = date, y = md, ymin = q1, ymax = q3, fill = source)) +
-    geom_ribbon(alpha = 0.5) +
-    geom_line(aes(colour = source), size = 0.75) +
-    scale_x_date(name = "", breaks = custom_breaks,
-      labels = custom_labels, minor_breaks = custom_minor_breaks) +
-    scale_y_continuous(name = "", labels = unit_format(unit = "s"),
-      limits = c(0, NA)) +
-    scale_fill_hue(name = "Source") +
-    scale_colour_hue(name = "Source") +
-    ggtitle(paste("Time to complete",
-        ifelse(filesize_p == "50kb", "50 KiB",
-        ifelse(filesize_p == "1mb", "1 MiB", "5 MiB")),
-        "request to", server_p, "server")) +
-    labs(caption = copyright_notice) +
-    theme(legend.position = "top")
-  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
-}
-
-prepare_torperf_failures <- function(start_p = NULL, end_p = NULL,
-    server_p = NULL, filesize_p = NULL) {
-  read_csv(file = paste(stats_dir, "torperf-1.1.csv", sep = ""),
-      col_types = cols(
-        date = col_date(format = ""),
-        filesize = col_double(),
-        source = col_character(),
-        server = col_character(),
-        q1 = col_skip(),
-        md = col_skip(),
-        q3 = col_skip(),
-        timeouts = col_double(),
-        failures = col_double(),
-        requests = col_double())) %>%
-    filter(if (!is.null(start_p)) date >= as.Date(start_p) else TRUE) %>%
-    filter(if (!is.null(end_p)) date <= as.Date(end_p) else TRUE) %>%
-    filter(if (!is.null(filesize_p))
-        filesize == ifelse(filesize_p == "50kb", 50 * 1024,
-        ifelse(filesize_p == "1mb", 1024 * 1024, 5 * 1024 * 1024)) else
-        TRUE) %>%
-    filter(if (!is.null(server_p)) server == server_p else TRUE) %>%
-    filter(requests > 0) %>%
-    transmute(date, filesize, source, server, timeouts = timeouts / requests,
-        failures = failures / requests)
-}
-
-plot_torperf_failures <- function(start_p, end_p, server_p, filesize_p,
-    path_p) {
-  prepare_torperf_failures(start_p, end_p, server_p, filesize_p) %>%
-    filter(source != "") %>%
-    gather(variable, value, -c(date, filesize, source, server)) %>%
-    mutate(variable = factor(variable, levels = c("timeouts", "failures"),
-      labels = c("Timeouts", "Failures"))) %>%
-    ggplot(aes(x = date, y = value, colour = source)) +
-    geom_point(size = 2, alpha = 0.5) +
-    scale_x_date(name = "", breaks = custom_breaks,
-      labels = custom_labels, minor_breaks = custom_minor_breaks) +
-    scale_y_continuous(name = "", labels = percent, limits = c(0, NA)) +
-    scale_colour_hue(name = "Source") +
-    facet_grid(variable ~ .) +
-    ggtitle(paste("Timeouts and failures of",
-        ifelse(filesize_p == "50kb", "50 KiB",
-        ifelse(filesize_p == "1mb", "1 MiB", "5 MiB")),
-        "requests to", server_p, "server")) +
-    labs(caption = copyright_notice) +
-    theme(legend.position = "top")
-  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
-}
-
-prepare_onionperf_buildtimes <- function(start_p = NULL, end_p = NULL) {
-  read_csv(file = paste(stats_dir, "buildtimes.csv", sep = ""),
-      col_types = cols(
-        date = col_date(format = ""),
-        source = col_character(),
-        position = col_double(),
-        q1 = col_double(),
-        md = col_double(),
-        q3 = col_double())) %>%
-    filter(if (!is.null(start_p)) date >= as.Date(start_p) else TRUE) %>%
-    filter(if (!is.null(end_p)) date <= as.Date(end_p) else TRUE)
-}
-
-plot_onionperf_buildtimes <- function(start_p, end_p, path_p) {
-  prepare_onionperf_buildtimes(start_p, end_p) %>%
-    filter(source != "") %>%
-    mutate(date = as.Date(date),
-      position = factor(position, levels = seq(1, 3, 1),
-        labels = c("1st hop", "2nd hop", "3rd hop"))) %>%
-    complete(date = full_seq(date, period = 1), nesting(source, position)) %>%
-    ggplot(aes(x = date, y = md, ymin = q1, ymax = q3, fill = source)) +
-    geom_ribbon(alpha = 0.5) +
-    geom_line(aes(colour = source), size = 0.75) +
-    facet_grid(position ~ .) +
-    scale_x_date(name = "", breaks = custom_breaks,
-      labels = custom_labels, minor_breaks = custom_minor_breaks) +
-    scale_y_continuous(name = "", labels = unit_format(unit = "ms"),
-      limits = c(0, NA)) +
-    scale_fill_hue(name = "Source") +
-    scale_colour_hue(name = "Source") +
-    ggtitle("Circuit build times") +
-    labs(caption = copyright_notice) +
-    theme(legend.position = "top")
-  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
-}
-
-prepare_onionperf_latencies <- function(start_p = NULL, end_p = NULL,
-    server_p = NULL) {
-  read_csv(file = paste(stats_dir, "latencies.csv", sep = ""),
-      col_types = cols(
-        date = col_date(format = ""),
-        source = col_character(),
-        server = col_character(),
-        q1 = col_double(),
-        md = col_double(),
-        q3 = col_double())) %>%
-    filter(if (!is.null(start_p)) date >= as.Date(start_p) else TRUE) %>%
-    filter(if (!is.null(end_p)) date <= as.Date(end_p) else TRUE) %>%
-    filter(if (!is.null(server_p)) server == server_p else TRUE)
-}
-
-plot_onionperf_latencies <- function(start_p, end_p, server_p, path_p) {
-  prepare_onionperf_latencies(start_p, end_p, server_p) %>%
-    filter(source != "") %>%
-    complete(date = full_seq(date, period = 1), nesting(source)) %>%
-    ggplot(aes(x = date, y = md, ymin = q1, ymax = q3, fill = source)) +
-    geom_ribbon(alpha = 0.5) +
-    geom_line(aes(colour = source), size = 0.75) +
-    scale_x_date(name = "", breaks = custom_breaks,
-      labels = custom_labels, minor_breaks = custom_minor_breaks) +
-    scale_y_continuous(name = "", labels = unit_format(unit = "ms"),
-      limits = c(0, NA)) +
-    scale_fill_hue(name = "Source") +
-    scale_colour_hue(name = "Source") +
-    ggtitle(paste("Circuit round-trip latencies to", server_p, "server")) +
-    labs(caption = copyright_notice) +
-    theme(legend.position = "top")
-  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
-}
-
-prepare_connbidirect <- function(start_p = NULL, end_p = NULL) {
-  read_csv(file = paste(stats_dir, "connbidirect2.csv", sep = ""),
-      col_types = cols(
-        date = col_date(format = ""),
-        direction = col_factor(levels = NULL),
-        quantile = col_double(),
-        fraction = col_double())) %>%
-    filter(if (!is.null(start_p)) date >= as.Date(start_p) else TRUE) %>%
-    filter(if (!is.null(end_p)) date <= as.Date(end_p) else TRUE) %>%
-    mutate(quantile = paste("X", quantile, sep = ""),
-      fraction = fraction / 100) %>%
-    spread(quantile, fraction) %>%
-    rename(q1 = X0.25, md = X0.5, q3 = X0.75)
-}
-
-plot_connbidirect <- function(start_p, end_p, path_p) {
-  prepare_connbidirect(start_p, end_p) %>%
-    complete(date = full_seq(date, period = 1), nesting(direction)) %>%
-    ggplot(aes(x = date, y = md, ymin = q1, ymax = q3, fill = direction)) +
-    geom_ribbon(alpha = 0.5) +
-    geom_line(aes(colour = direction), size = 0.75) +
-    scale_x_date(name = "", breaks = custom_breaks,
-      labels = custom_labels, minor_breaks = custom_minor_breaks) +
-    scale_y_continuous(name = "", labels = percent, limits = c(0, NA)) +
-    scale_colour_hue(name = "Medians and interquartile ranges",
-                     breaks = c("both", "write", "read"),
-        labels = c("Both reading and writing", "Mostly writing",
-                   "Mostly reading")) +
-    scale_fill_hue(name = "Medians and interquartile ranges",
-                   breaks = c("both", "write", "read"),
-        labels = c("Both reading and writing", "Mostly writing",
-                   "Mostly reading")) +
-    ggtitle("Fraction of connections used uni-/bidirectionally") +
-    labs(caption = copyright_notice) +
-    theme(legend.position = "top")
-  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
-}
-
-prepare_bandwidth_flags <- function(start_p = NULL, end_p = NULL) {
-  advbw <- read_csv(file = paste(stats_dir, "advbw.csv", sep = ""),
-      col_types = cols(
-        date = col_date(format = ""),
-        isexit = col_logical(),
-        isguard = col_logical(),
-        advbw = col_double())) %>%
-    transmute(date, have_guard_flag = isguard, have_exit_flag = isexit,
-      variable = "advbw", value = advbw * 8 / 1e9)
-  bwhist <- read_csv(file = paste(stats_dir, "bandwidth.csv", sep = ""),
-      col_types = cols(
-        date = col_date(format = ""),
-        isexit = col_logical(),
-        isguard = col_logical(),
-        bwread = col_double(),
-        bwwrite = col_double(),
-        dirread = col_double(),
-        dirwrite = col_double())) %>%
-    transmute(date, have_guard_flag = isguard, have_exit_flag = isexit,
-      variable = "bwhist", value = (bwread + bwwrite) * 8 / 2e9)
-  rbind(advbw, bwhist) %>%
-    filter(if (!is.null(start_p)) date >= as.Date(start_p) else TRUE) %>%
-    filter(if (!is.null(end_p)) date <= as.Date(end_p) else TRUE) %>%
-    filter(!is.na(have_exit_flag)) %>%
-    filter(!is.na(have_guard_flag)) %>%
-    spread(variable, value)
-}
-
-plot_bandwidth_flags <- function(start_p, end_p, path_p) {
-  prepare_bandwidth_flags(start_p, end_p) %>%
-    gather(variable, value, c(advbw, bwhist)) %>%
-    unite(flags, have_guard_flag, have_exit_flag) %>%
-    mutate(flags = factor(flags,
-      levels = c("FALSE_TRUE", "TRUE_TRUE", "TRUE_FALSE", "FALSE_FALSE"),
-      labels = c("Exit only", "Guard and Exit", "Guard only",
-      "Neither Guard nor Exit"))) %>%
-    mutate(variable = ifelse(variable == "advbw",
-      "Advertised bandwidth", "Consumed bandwidth")) %>%
-    ggplot(aes(x = date, y = value, fill = flags)) +
-    geom_area() +
-    scale_x_date(name = "", breaks = custom_breaks,
-      labels = custom_labels, minor_breaks = custom_minor_breaks) +
-    scale_y_continuous(name = "", labels = unit_format(unit = "Gbit/s"),
-      limits = c(0, NA)) +
-    scale_fill_manual(name = "",
-      values = c("#03B3FF", "#39FF02", "#FFFF00", "#AAAA99")) +
-    facet_grid(variable ~ .) +
-    ggtitle("Advertised and consumed bandwidth by relay flags") +
-    labs(caption = copyright_notice) +
-    theme(legend.position = "top")
-  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
-}
-
-prepare_userstats_relay_country <- function(start_p = NULL, end_p = NULL,
-    country_p = NULL, events_p = NULL) {
-  read_csv(file = paste(stats_dir, "clients.csv", sep = ""),
-      col_types = cols(
-        date = col_date(format = ""),
-        node = col_character(),
-        country = col_character(),
-        transport = col_character(),
-        version = col_character(),
-        lower = col_double(),
-        upper = col_double(),
-        clients = col_double(),
-        frac = col_double()),
-      na = character()) %>%
-    filter(node == "relay") %>%
-    filter(if (!is.null(start_p)) date >= as.Date(start_p) else TRUE) %>%
-    filter(if (!is.null(end_p)) date <= as.Date(end_p) else TRUE) %>%
-    filter(if (!is.null(country_p))
-      country == ifelse(country_p == "all", "", country_p) else TRUE) %>%
-    filter(transport == "") %>%
-    filter(version == "") %>%
-    select(date, country, clients, lower, upper, frac) %>%
-    rename(users = clients)
-}
-
-plot_userstats_relay_country <- function(start_p, end_p, country_p, events_p,
-    path_p) {
-  u <- prepare_userstats_relay_country(start_p, end_p, country_p, events_p) %>%
-    complete(date = full_seq(date, period = 1))
-  plot <- ggplot(u, aes(x = date, y = users))
-  if (length(na.omit(u$users)) > 0 & events_p != "off" &
-      country_p != "all") {
-    upturns <- u[u$users > u$upper, c("date", "users")]
-    downturns <- u[u$users < u$lower, c("date", "users")]
-    if (events_p == "on") {
-      u[!is.na(u$lower) & u$lower < 0, "lower"] <- 0
-      plot <- plot +
-        geom_ribbon(data = u, aes(ymin = lower, ymax = upper), fill = "gray")
-    }
-    if (length(upturns$date) > 0)
-      plot <- plot +
-          geom_point(data = upturns, aes(x = date, y = users), size = 5,
-          colour = "dodgerblue2")
-    if (length(downturns$date) > 0)
-      plot <- plot +
-          geom_point(data = downturns, aes(x = date, y = users), size = 5,
-          colour = "firebrick2")
-  }
-  plot <- plot +
-    geom_line() +
-    scale_x_date(name = "", breaks = custom_breaks,
-      labels = custom_labels, minor_breaks = custom_minor_breaks) +
-    scale_y_continuous(name = "", labels = formatter, limits = c(0, NA)) +
-    ggtitle(paste("Directly connecting users",
-        ifelse(country_p == "all", "",
-        paste(" from", countryname(country_p))), sep = "")) +
-    labs(caption = copyright_notice)
-  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
-}
-
-prepare_userstats_bridge_country <- function(start_p = NULL, end_p = NULL,
-    country_p = NULL) {
-  read_csv(file = paste(stats_dir, "clients.csv", sep = ""),
-      col_types = cols(
-        date = col_date(format = ""),
-        node = col_character(),
-        country = col_character(),
-        transport = col_character(),
-        version = col_character(),
-        lower = col_double(),
-        upper = col_double(),
-        clients = col_double(),
-        frac = col_double()),
-      na = character()) %>%
-    filter(node == "bridge") %>%
-    filter(if (!is.null(start_p)) date >= as.Date(start_p) else TRUE) %>%
-    filter(if (!is.null(end_p)) date <= as.Date(end_p) else TRUE) %>%
-    filter(if (!is.null(country_p))
-      country == ifelse(country_p == "all", "", country_p) else TRUE) %>%
-    filter(transport == "") %>%
-    filter(version == "") %>%
-    select(date, country, clients, frac) %>%
-    rename(users = clients)
-}
-
-plot_userstats_bridge_country <- function(start_p, end_p, country_p, path_p) {
-  prepare_userstats_bridge_country(start_p, end_p, country_p) %>%
-    complete(date = full_seq(date, period = 1)) %>%
-    ggplot(aes(x = date, y = users)) +
-    geom_line() +
-    scale_x_date(name = "", breaks = custom_breaks,
-      labels = custom_labels, minor_breaks = custom_minor_breaks) +
-    scale_y_continuous(name = "", labels = formatter, limits = c(0, NA)) +
-    ggtitle(paste("Bridge users",
-        ifelse(country_p == "all", "",
-        paste(" from", countryname(country_p))), sep = "")) +
-    labs(caption = copyright_notice)
-  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
-}
-
-prepare_userstats_bridge_transport <- function(start_p = NULL, end_p = NULL,
-    transport_p = NULL) {
-  u <- read_csv(file = paste(stats_dir, "clients.csv", sep = ""),
-      col_types = cols(
-        date = col_date(format = ""),
-        node = col_character(),
-        country = col_character(),
-        transport = col_character(),
-        version = col_character(),
-        lower = col_double(),
-        upper = col_double(),
-        clients = col_double(),
-        frac = col_double())) %>%
-    filter(node == "bridge") %>%
-    filter(if (!is.null(start_p)) date >= as.Date(start_p) else TRUE) %>%
-    filter(if (!is.null(end_p)) date <= as.Date(end_p) else TRUE) %>%
-    filter(is.na(country)) %>%
-    filter(is.na(version)) %>%
-    filter(!is.na(transport)) %>%
-    select(date, transport, clients, frac)
-  if (is.null(transport_p) || "!<OR>" %in% transport_p) {
-    n <- u %>%
-      filter(transport != "<OR>") %>%
-      group_by(date, frac) %>%
-      summarize(clients = sum(clients))
-    u <- rbind(u, data.frame(date = n$date, transport = "!<OR>",
-                             clients = n$clients, frac = n$frac))
-  }
-  u %>%
-    filter(if (!is.null(transport_p)) transport %in% transport_p else TRUE) %>%
-    select(date, transport, clients, frac) %>%
-    rename(users = clients) %>%
-    arrange(date, transport)
-}
-
-plot_userstats_bridge_transport <- function(start_p, end_p, transport_p,
-    path_p) {
-  if (length(transport_p) > 1) {
-    title <- paste("Bridge users by transport")
-  } else {
-    title <- paste("Bridge users using",
-             ifelse(transport_p == "<??>", "unknown pluggable transport(s)",
-             ifelse(transport_p == "<OR>", "default OR protocol",
-             ifelse(transport_p == "!<OR>", "any pluggable transport",
-             ifelse(transport_p == "fte", "FTE",
-             ifelse(transport_p == "websocket", "Flash proxy/websocket",
-             paste("transport", transport_p)))))))
-  }
-  u <- prepare_userstats_bridge_transport(start_p, end_p, transport_p) %>%
-    complete(date = full_seq(date, period = 1), nesting(transport))
-  if (length(transport_p) > 1) {
-    plot <- ggplot(u, aes(x = date, y = users, colour = transport))
-  } else {
-    plot <- ggplot(u, aes(x = date, y = users))
-  }
-  plot <- plot +
-    geom_line() +
-    scale_x_date(name = "", breaks = custom_breaks,
-      labels = custom_labels, minor_breaks = custom_minor_breaks) +
-    scale_y_continuous(name = "", labels = formatter, limits = c(0, NA)) +
-    ggtitle(title) +
-    labs(caption = copyright_notice)
-  if (length(transport_p) > 1) {
-    plot <- plot +
-      scale_colour_hue(name = "", breaks = transport_p,
-            labels = ifelse(transport_p == "<??>", "Unknown PT",
-                     ifelse(transport_p == "<OR>", "Default OR protocol",
-                     ifelse(transport_p == "!<OR>", "Any PT",
-                     ifelse(transport_p == "fte", "FTE",
-                     ifelse(transport_p == "websocket", "Flash proxy/websocket",
-                     transport_p))))))
-  }
-  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
-}
-
-prepare_userstats_bridge_version <- function(start_p = NULL, end_p = NULL,
-    version_p = NULL) {
-  read_csv(file = paste(stats_dir, "clients.csv", sep = ""),
-      col_types = cols(
-        date = col_date(format = ""),
-        node = col_character(),
-        country = col_character(),
-        transport = col_character(),
-        version = col_character(),
-        lower = col_double(),
-        upper = col_double(),
-        clients = col_double(),
-        frac = col_double())) %>%
-    filter(node == "bridge") %>%
-    filter(if (!is.null(start_p)) date >= as.Date(start_p) else TRUE) %>%
-    filter(if (!is.null(end_p)) date <= as.Date(end_p) else TRUE) %>%
-    filter(is.na(country)) %>%
-    filter(is.na(transport)) %>%
-    filter(if (!is.null(version_p)) version == version_p else TRUE) %>%
-    select(date, version, clients, frac) %>%
-    rename(users = clients)
-}
-
-plot_userstats_bridge_version <- function(start_p, end_p, version_p, path_p) {
-  prepare_userstats_bridge_version(start_p, end_p, version_p) %>%
-    complete(date = full_seq(date, period = 1)) %>%
-    ggplot(aes(x = date, y = users)) +
-    geom_line() +
-    scale_x_date(name = "", breaks = custom_breaks,
-      labels = custom_labels, minor_breaks = custom_minor_breaks) +
-    scale_y_continuous(name = "", labels = formatter, limits = c(0, NA)) +
-    ggtitle(paste("Bridge users using IP", version_p, sep = "")) +
-    labs(caption = copyright_notice)
-  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
-}
-
-prepare_userstats_bridge_combined <- function(start_p = NULL, end_p = NULL,
-    country_p = NULL) {
-  if (!is.null(country_p) && country_p == "all") {
-    prepare_userstats_bridge_country(start_p, end_p, country_p)
-  } else {
-    read_csv(file = paste(stats_dir, "userstats-combined.csv", sep = ""),
-        col_types = cols(
-          date = col_date(format = ""),
-          node = col_skip(),
-          country = col_character(),
-          transport = col_character(),
-          version = col_skip(),
-          frac = col_double(),
-          low = col_double(),
-          high = col_double()),
-        na = character()) %>%
-      filter(if (!is.null(start_p)) date >= as.Date(start_p) else TRUE) %>%
-      filter(if (!is.null(end_p)) date <= as.Date(end_p) else TRUE) %>%
-      filter(if (!is.null(country_p)) country == country_p else TRUE) %>%
-      select(date, country, transport, low, high, frac) %>%
-      arrange(date, country, transport)
-  }
-}
-
-plot_userstats_bridge_combined <- function(start_p, end_p, country_p, path_p) {
-  if (country_p == "all") {
-    plot_userstats_bridge_country(start_p, end_p, country_p, path_p)
-  } else {
-    top <- 3
-    u <- prepare_userstats_bridge_combined(start_p, end_p, country_p)
-    a <- aggregate(list(mid = (u$high + u$low) / 2),
-                   by = list(transport = u$transport), FUN = sum)
-    a <- a[order(a$mid, decreasing = TRUE)[1:top], ]
-    u <- u[u$transport %in% a$transport, ] %>%
-      complete(date = full_seq(date, period = 1), nesting(country, transport))
-    title <- paste("Bridge users by transport from ",
-                   countryname(country_p), sep = "")
-    ggplot(u, aes(x = as.Date(date), ymin = low, ymax = high,
-      fill = transport)) +
-    geom_ribbon(alpha = 0.5, size = 0.5) +
-    scale_x_date(name = "", breaks = custom_breaks,
-      labels = custom_labels, minor_breaks = custom_minor_breaks) +
-    scale_y_continuous(name = "", limits = c(0, NA), labels = formatter) +
-    scale_colour_hue("Top-3 transports") +
-    scale_fill_hue("Top-3 transports") +
-    ggtitle(title) +
-    labs(caption = copyright_notice) +
-    theme(legend.position = "top")
-    ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
-  }
-}
-
-prepare_advbwdist_perc <- function(start_p = NULL, end_p = NULL, p_p = NULL) {
-  read_csv(file = paste(stats_dir, "advbwdist.csv", sep = ""),
-      col_types = cols(
-        date = col_date(format = ""),
-        isexit = col_logical(),
-        relay = col_skip(),
-        percentile = col_integer(),
-        advbw = col_double())) %>%
-    filter(if (!is.null(start_p)) date >= as.Date(start_p) else TRUE) %>%
-    filter(if (!is.null(end_p)) date <= as.Date(end_p) else TRUE) %>%
-    filter(if (!is.null(p_p)) percentile %in% as.numeric(p_p) else
-      percentile != "") %>%
-    transmute(date, percentile = as.factor(percentile),
-      variable = ifelse(is.na(isexit), "all", "exits"),
-      advbw = advbw * 8 / 1e9) %>%
-    spread(variable, advbw) %>%
-    rename(p = percentile)
-}
-
-plot_advbwdist_perc <- function(start_p, end_p, p_p, path_p) {
-  prepare_advbwdist_perc(start_p, end_p, p_p) %>%
-    gather(variable, advbw, -c(date, p)) %>%
-    mutate(variable = ifelse(variable == "all", "All relays",
-      "Exits only")) %>%
-    complete(date = full_seq(date, period = 1), nesting(p, variable)) %>%
-    ggplot(aes(x = date, y = advbw, colour = p)) +
-    facet_grid(variable ~ .) +
-    geom_line() +
-    scale_x_date(name = "", breaks = custom_breaks,
-      labels = custom_labels, minor_breaks = custom_minor_breaks) +
-    scale_y_continuous(name = "", labels = unit_format(unit = "Gbit/s"),
-      limits = c(0, NA)) +
-    scale_colour_hue(name = "Percentile") +
-    ggtitle("Advertised bandwidth distribution") +
-    labs(caption = copyright_notice)
-  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
-}
-
-prepare_advbwdist_relay <- function(start_p = NULL, end_p = NULL, n_p = NULL) {
-  read_csv(file = paste(stats_dir, "advbwdist.csv", sep = ""),
-      col_types = cols(
-        date = col_date(format = ""),
-        isexit = col_logical(),
-        relay = col_integer(),
-        percentile = col_skip(),
-        advbw = col_double())) %>%
-    filter(if (!is.null(start_p)) date >= as.Date(start_p) else TRUE) %>%
-    filter(if (!is.null(end_p)) date <= as.Date(end_p) else TRUE) %>%
-    filter(if (!is.null(n_p)) relay %in% as.numeric(n_p) else
-      relay != "") %>%
-    transmute(date, relay = as.factor(relay),
-      variable = ifelse(is.na(isexit), "all", "exits"),
-      advbw = advbw * 8 / 1e9) %>%
-    spread(variable, advbw) %>%
-    rename(n = relay)
-}
-
-plot_advbwdist_relay <- function(start_p, end_p, n_p, path_p) {
-  prepare_advbwdist_relay(start_p, end_p, n_p) %>%
-    gather(variable, advbw, -c(date, n)) %>%
-    mutate(variable = ifelse(variable == "all", "All relays",
-      "Exits only")) %>%
-    complete(date = full_seq(date, period = 1), nesting(n, variable)) %>%
-    ggplot(aes(x = date, y = advbw, colour = n)) +
-    facet_grid(variable ~ .) +
-    geom_line() +
-    scale_x_date(name = "", breaks = custom_breaks,
-      labels = custom_labels, minor_breaks = custom_minor_breaks) +
-    scale_y_continuous(name = "", labels = unit_format(unit = "Gbit/s"),
-      limits = c(0, NA)) +
-    scale_colour_hue(name = "n") +
-    ggtitle("Advertised bandwidth of n-th fastest relays") +
-    labs(caption = copyright_notice)
-  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
-}
-
-prepare_hidserv_dir_onions_seen <- function(start_p = NULL, end_p = NULL) {
-  read_csv(file = paste(stats_dir, "hidserv.csv", sep = ""),
-      col_types = cols(
-        date = col_date(format = ""),
-        type = col_factor(levels = NULL),
-        wmean = col_skip(),
-        wmedian = col_skip(),
-        wiqm = col_double(),
-        frac = col_double(),
-        stats = col_skip())) %>%
-    filter(if (!is.null(start_p)) date >= as.Date(start_p) else TRUE) %>%
-    filter(if (!is.null(end_p)) date <= as.Date(end_p) else TRUE) %>%
-    filter(type == "dir-onions-seen") %>%
-    transmute(date, onions = ifelse(frac >= 0.01, wiqm, NA), frac)
-}
-
-plot_hidserv_dir_onions_seen <- function(start_p, end_p, path_p) {
-  prepare_hidserv_dir_onions_seen(start_p, end_p) %>%
-    complete(date = full_seq(date, period = 1)) %>%
-    ggplot(aes(x = date, y = onions)) +
-    geom_line() +
-    scale_x_date(name = "", breaks = custom_breaks,
-      labels = custom_labels, minor_breaks = custom_minor_breaks) +
-    scale_y_continuous(name = "", limits = c(0, NA), labels = formatter) +
-    ggtitle("Unique .onion addresses") +
-    labs(caption = copyright_notice)
-  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
-}
-
-prepare_hidserv_rend_relayed_cells <- function(start_p = NULL, end_p = NULL) {
-  read_csv(file = paste(stats_dir, "hidserv.csv", sep = ""),
-      col_types = cols(
-        date = col_date(format = ""),
-        type = col_factor(levels = NULL),
-        wmean = col_skip(),
-        wmedian = col_skip(),
-        wiqm = col_double(),
-        frac = col_double(),
-        stats = col_skip())) %>%
-    filter(if (!is.null(start_p)) date >= as.Date(start_p) else TRUE) %>%
-    filter(if (!is.null(end_p)) date <= as.Date(end_p) else TRUE) %>%
-    filter(type == "rend-relayed-cells") %>%
-    transmute(date,
-      relayed = ifelse(frac >= 0.01, wiqm * 8 * 512 / (86400 * 1e9), NA), frac)
-}
-
-plot_hidserv_rend_relayed_cells <- function(start_p, end_p, path_p) {
-  prepare_hidserv_rend_relayed_cells(start_p, end_p) %>%
-    complete(date = full_seq(date, period = 1)) %>%
-    ggplot(aes(x = date, y = relayed)) +
-    geom_line() +
-    scale_x_date(name = "", breaks = custom_breaks,
-      labels = custom_labels, minor_breaks = custom_minor_breaks) +
-    scale_y_continuous(name = "", labels = unit_format(unit = "Gbit/s"),
-      limits = c(0, NA)) +
-    ggtitle("Onion-service traffic") +
-    labs(caption = copyright_notice)
-  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
-}
-
-prepare_webstats_tb <- function(start_p = NULL, end_p = NULL) {
-  read_csv(file = paste(stats_dir, "webstats.csv", sep = ""),
-      col_types = cols(
-        log_date = col_date(format = ""),
-        request_type = col_factor(levels = NULL),
-        platform = col_skip(),
-        channel = col_skip(),
-        locale = col_skip(),
-        incremental = col_skip(),
-        count = col_double())) %>%
-    filter(if (!is.null(start_p)) log_date >= as.Date(start_p) else TRUE) %>%
-    filter(if (!is.null(end_p)) log_date <= as.Date(end_p) else TRUE) %>%
-    filter(request_type %in% c("tbid", "tbsd", "tbup", "tbur")) %>%
-    group_by(log_date, request_type) %>%
-    summarize(count = sum(count)) %>%
-    spread(request_type, count) %>%
-    rename(date = log_date, initial_downloads = tbid,
-      signature_downloads = tbsd, update_pings = tbup,
-      update_requests = tbur)
-}
-
-plot_webstats_tb <- function(start_p, end_p, path_p) {
-  prepare_webstats_tb(start_p, end_p) %>%
-    gather(request_type, count, -date) %>%
-    mutate(request_type = factor(request_type,
-      levels = c("initial_downloads", "signature_downloads", "update_pings",
-        "update_requests"),
-      labels = c("Initial downloads", "Signature downloads", "Update pings",
-        "Update requests"))) %>%
-    ungroup() %>%
-    complete(date = full_seq(date, period = 1), nesting(request_type)) %>%
-    ggplot(aes(x = date, y = count)) +
-    geom_point() +
-    geom_line() +
-    facet_grid(request_type ~ ., scales = "free_y") +
-    scale_x_date(name = "", breaks = custom_breaks,
-      labels = custom_labels, minor_breaks = custom_minor_breaks) +
-    scale_y_continuous(name = "", labels = formatter, limits = c(0, NA)) +
-    theme(strip.text.y = element_text(angle = 0, hjust = 0, size = rel(1.5)),
-          strip.background = element_rect(fill = NA)) +
-    ggtitle("Tor Browser downloads and updates") +
-    labs(caption = copyright_notice)
-  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
-}
-
-prepare_webstats_tb_platform <- function(start_p = NULL, end_p = NULL) {
-  read_csv(file = paste(stats_dir, "webstats.csv", sep = ""),
-      col_types = cols(
-        log_date = col_date(format = ""),
-        request_type = col_factor(levels = NULL),
-        platform = col_factor(levels = NULL),
-        channel = col_skip(),
-        locale = col_skip(),
-        incremental = col_skip(),
-        count = col_double())) %>%
-    filter(if (!is.null(start_p)) log_date >= as.Date(start_p) else TRUE) %>%
-    filter(if (!is.null(end_p)) log_date <= as.Date(end_p) else TRUE) %>%
-    filter(request_type %in% c("tbid", "tbup")) %>%
-    group_by(log_date, platform, request_type) %>%
-    summarize(count = sum(count)) %>%
-    spread(request_type, count, fill = 0) %>%
-    rename(date = log_date, initial_downloads = tbid, update_pings = tbup)
-}
-
-plot_webstats_tb_platform <- function(start_p, end_p, path_p) {
-  prepare_webstats_tb_platform(start_p, end_p) %>%
-    gather(request_type, count, -c(date, platform)) %>%
-    mutate(request_type = factor(request_type,
-      levels = c("initial_downloads", "update_pings"),
-      labels = c("Initial downloads", "Update pings"))) %>%
-    ungroup() %>%
-    complete(date = full_seq(date, period = 1),
-      nesting(platform, request_type)) %>%
-    ggplot(aes(x = date, y = count, colour = platform)) +
-    geom_point() +
-    geom_line() +
-    scale_x_date(name = "", breaks = custom_breaks,
-      labels = custom_labels, minor_breaks = custom_minor_breaks) +
-    scale_y_continuous(name = "", labels = formatter, limits = c(0, NA)) +
-    scale_colour_hue(name = "Platform",
-        breaks = c("w", "m", "l", "o", ""),
-        labels = c("Windows", "macOS", "Linux", "Other", "Unknown")) +
-    facet_grid(request_type ~ ., scales = "free_y") +
-    theme(strip.text.y = element_text(angle = 0, hjust = 0, size = rel(1.5)),
-          strip.background = element_rect(fill = NA),
-          legend.position = "top") +
-    ggtitle("Tor Browser downloads and updates by platform") +
-    labs(caption = copyright_notice)
-  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
-}
-
-prepare_webstats_tb_locale <- function(start_p = NULL, end_p = NULL) {
-  read_csv(file = paste(stats_dir, "webstats.csv", sep = ""),
-      col_types = cols(
-        log_date = col_date(format = ""),
-        request_type = col_factor(levels = NULL),
-        platform = col_skip(),
-        channel = col_skip(),
-        locale = col_factor(levels = NULL),
-        incremental = col_skip(),
-        count = col_double())) %>%
-    filter(if (!is.null(start_p)) log_date >= as.Date(start_p) else TRUE) %>%
-    filter(if (!is.null(end_p)) log_date <= as.Date(end_p) else TRUE) %>%
-    filter(request_type %in% c("tbid", "tbup")) %>%
-    rename(date = log_date) %>%
-    group_by(date, locale, request_type) %>%
-    summarize(count = sum(count)) %>%
-    mutate(request_type = factor(request_type, levels = c("tbid", "tbup"))) %>%
-    spread(request_type, count, fill = 0) %>%
-    rename(initial_downloads = tbid, update_pings = tbup)
-}
-
-plot_webstats_tb_locale <- function(start_p, end_p, path_p) {
-  d <- prepare_webstats_tb_locale(start_p, end_p) %>%
-    gather(request_type, count, -c(date, locale)) %>%
-    mutate(request_type = factor(request_type,
-      levels = c("initial_downloads", "update_pings"),
-      labels = c("Initial downloads", "Update pings")))
-  e <- d
-  e <- aggregate(list(count = e$count), by = list(locale = e$locale), FUN = sum)
-  e <- e[order(e$count, decreasing = TRUE), ]
-  e <- e[1:5, ]
-  d <- aggregate(list(count = d$count), by = list(date = d$date,
-    request_type = d$request_type,
-    locale = ifelse(d$locale %in% e$locale, d$locale, "(other)")), FUN = sum)
-  d %>%
-    complete(date = full_seq(date, period = 1),
-      nesting(locale, request_type)) %>%
-    ggplot(aes(x = date, y = count, colour = locale)) +
-    geom_point() +
-    geom_line() +
-    scale_x_date(name = "", breaks = custom_breaks,
-      labels = custom_labels, minor_breaks = custom_minor_breaks) +
-    scale_y_continuous(name = "", labels = formatter, limits = c(0, NA)) +
-    scale_colour_hue(name = "Locale",
-        breaks = c(e$locale, "(other)"),
-        labels = c(as.character(e$locale), "Other")) +
-    facet_grid(request_type ~ ., scales = "free_y") +
-    theme(strip.text.y = element_text(angle = 0, hjust = 0, size = rel(1.5)),
-          strip.background = element_rect(fill = NA),
-          legend.position = "top") +
-    guides(col = guide_legend(nrow = 1)) +
-    ggtitle("Tor Browser downloads and updates by locale") +
-    labs(caption = copyright_notice)
-  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
-}
-
-prepare_webstats_tm <- function(start_p = NULL, end_p = NULL) {
-  read_csv(file = paste(stats_dir, "webstats.csv", sep = ""),
-      col_types = cols(
-        log_date = col_date(format = ""),
-        request_type = col_factor(levels = NULL),
-        platform = col_skip(),
-        channel = col_skip(),
-        locale = col_skip(),
-        incremental = col_skip(),
-        count = col_double())) %>%
-    filter(if (!is.null(start_p)) log_date >= as.Date(start_p) else TRUE) %>%
-    filter(if (!is.null(end_p)) log_date <= as.Date(end_p) else TRUE) %>%
-    filter(request_type %in% c("tmid", "tmup")) %>%
-    group_by(log_date, request_type) %>%
-    summarize(count = sum(count)) %>%
-    mutate(request_type = factor(request_type, levels = c("tmid", "tmup"))) %>%
-    spread(request_type, count, drop = FALSE, fill = 0) %>%
-    rename(date = log_date, initial_downloads = tmid, update_pings = tmup)
-}
-
-plot_webstats_tm <- function(start_p, end_p, path_p) {
-  prepare_webstats_tm(start_p, end_p) %>%
-    gather(request_type, count, -date) %>%
-    mutate(request_type = factor(request_type,
-      levels = c("initial_downloads", "update_pings"),
-      labels = c("Initial downloads", "Update pings"))) %>%
-    ungroup() %>%
-    complete(date = full_seq(date, period = 1), nesting(request_type)) %>%
-    ggplot(aes(x = date, y = count)) +
-    geom_point() +
-    geom_line() +
-    facet_grid(request_type ~ ., scales = "free_y") +
-    scale_x_date(name = "", breaks = custom_breaks,
-      labels = custom_labels, minor_breaks = custom_minor_breaks) +
-    scale_y_continuous(name = "", labels = formatter, limits = c(0, NA)) +
-    theme(strip.text.y = element_text(angle = 0, hjust = 0, size = rel(1.5)),
-          strip.background = element_rect(fill = NA)) +
-    ggtitle("Tor Messenger downloads and updates") +
-    labs(caption = copyright_notice)
-  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
-}
-
-prepare_relays_ipv6 <- function(start_p = NULL, end_p = NULL) {
-  read_csv(file = paste(stats_dir, "ipv6servers.csv", sep = ""),
-      col_types = cols(
-        valid_after_date = col_date(format = ""),
-        server = col_factor(levels = NULL),
-        guard_relay = col_skip(),
-        exit_relay = col_skip(),
-        announced_ipv6 = col_logical(),
-        exiting_ipv6_relay = col_logical(),
-        reachable_ipv6_relay = col_logical(),
-        server_count_sum_avg = col_double(),
-        advertised_bandwidth_bytes_sum_avg = col_skip())) %>%
-    filter(if (!is.null(start_p))
-        valid_after_date >= as.Date(start_p) else TRUE) %>%
-    filter(if (!is.null(end_p))
-        valid_after_date <= as.Date(end_p) else TRUE) %>%
-    filter(server == "relay") %>%
-    group_by(valid_after_date) %>%
-    summarize(total = sum(server_count_sum_avg),
-      announced = sum(server_count_sum_avg[announced_ipv6]),
-      reachable = sum(server_count_sum_avg[reachable_ipv6_relay]),
-      exiting = sum(server_count_sum_avg[exiting_ipv6_relay])) %>%
-    rename(date = valid_after_date)
-}
-
-plot_relays_ipv6 <- function(start_p, end_p, path_p) {
-  prepare_relays_ipv6(start_p, end_p) %>%
-    complete(date = full_seq(date, period = 1)) %>%
-    gather(category, count, -date) %>%
-    ggplot(aes(x = date, y = count, colour = category)) +
-    geom_line() +
-    scale_x_date(name = "", breaks = custom_breaks,
-      labels = custom_labels, minor_breaks = custom_minor_breaks) +
-    scale_y_continuous(name = "", labels = formatter, limits = c(0, NA)) +
-    scale_colour_hue(name = "", h.start = 90,
-      breaks = c("total", "announced", "reachable", "exiting"),
-      labels = c("Total (IPv4) OR", "IPv6 announced OR", "IPv6 reachable OR",
-        "IPv6 exiting")) +
-    ggtitle("Relays by IP version") +
-    labs(caption = copyright_notice) +
-    theme(legend.position = "top")
-  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
-}
-
-prepare_bridges_ipv6 <- function(start_p = NULL, end_p = NULL) {
-  read_csv(file = paste(stats_dir, "ipv6servers.csv", sep = ""),
-      col_types = cols(
-        valid_after_date = col_date(format = ""),
-        server = col_factor(levels = NULL),
-        guard_relay = col_skip(),
-        exit_relay = col_skip(),
-        announced_ipv6 = col_logical(),
-        exiting_ipv6_relay = col_skip(),
-        reachable_ipv6_relay = col_skip(),
-        server_count_sum_avg = col_double(),
-        advertised_bandwidth_bytes_sum_avg = col_skip())) %>%
-    filter(if (!is.null(start_p))
-        valid_after_date >= as.Date(start_p) else TRUE) %>%
-    filter(if (!is.null(end_p))
-        valid_after_date <= as.Date(end_p) else TRUE) %>%
-    filter(server == "bridge") %>%
-    group_by(valid_after_date) %>%
-    summarize(total = sum(server_count_sum_avg),
-      announced = sum(server_count_sum_avg[announced_ipv6])) %>%
-    rename(date = valid_after_date)
-}
-
-plot_bridges_ipv6 <- function(start_p, end_p, path_p) {
-  prepare_bridges_ipv6(start_p, end_p) %>%
-    complete(date = full_seq(date, period = 1)) %>%
-    gather(category, count, -date) %>%
-    ggplot(aes(x = date, y = count, colour = category)) +
-    geom_line() +
-    scale_x_date(name = "", breaks = custom_breaks,
-      labels = custom_labels, minor_breaks = custom_minor_breaks) +
-    scale_y_continuous(name = "", labels = formatter, limits = c(0, NA)) +
-    scale_colour_hue(name = "", h.start = 90,
-      breaks = c("total", "announced"),
-      labels = c("Total (IPv4) OR", "IPv6 announced OR")) +
-    ggtitle("Bridges by IP version") +
-    labs(caption = copyright_notice) +
-    theme(legend.position = "top")
-  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
-}
-
-prepare_advbw_ipv6 <- function(start_p = NULL, end_p = NULL) {
-  read_csv(file = paste(stats_dir, "ipv6servers.csv", sep = ""),
-      col_types = cols(
-        valid_after_date = col_date(format = ""),
-        server = col_factor(levels = NULL),
-        guard_relay = col_logical(),
-        exit_relay = col_logical(),
-        announced_ipv6 = col_logical(),
-        exiting_ipv6_relay = col_logical(),
-        reachable_ipv6_relay = col_logical(),
-        server_count_sum_avg = col_skip(),
-        advertised_bandwidth_bytes_sum_avg = col_double())) %>%
-    filter(if (!is.null(start_p))
-        valid_after_date >= as.Date(start_p) else TRUE) %>%
-    filter(if (!is.null(end_p))
-        valid_after_date <= as.Date(end_p) else TRUE) %>%
-    filter(server == "relay") %>%
-    mutate(advertised_bandwidth_bytes_sum_avg =
-        advertised_bandwidth_bytes_sum_avg * 8 / 1e9) %>%
-    group_by(valid_after_date) %>%
-    summarize(total = sum(advertised_bandwidth_bytes_sum_avg),
-      total_guard = sum(advertised_bandwidth_bytes_sum_avg[guard_relay]),
-      total_exit = sum(advertised_bandwidth_bytes_sum_avg[exit_relay]),
-      reachable_guard = sum(advertised_bandwidth_bytes_sum_avg[
-        reachable_ipv6_relay & guard_relay]),
-      reachable_exit = sum(advertised_bandwidth_bytes_sum_avg[
-        reachable_ipv6_relay & exit_relay]),
-      exiting = sum(advertised_bandwidth_bytes_sum_avg[
-        exiting_ipv6_relay])) %>%
-    rename(date = valid_after_date)
-}
-
-plot_advbw_ipv6 <- function(start_p, end_p, path_p) {
-  prepare_advbw_ipv6(start_p, end_p) %>%
-    complete(date = full_seq(date, period = 1)) %>%
-    gather(category, advbw, -date) %>%
-    ggplot(aes(x = date, y = advbw, colour = category)) +
-    geom_line() +
-    scale_x_date(name = "", breaks = custom_breaks,
-      labels = custom_labels, minor_breaks = custom_minor_breaks) +
-    scale_y_continuous(name = "", labels = unit_format(unit = "Gbit/s"),
-      limits = c(0, NA)) +
-    scale_colour_hue(name = "", h.start = 90,
-      breaks = c("total", "total_guard", "total_exit", "reachable_guard",
-        "reachable_exit", "exiting"),
-      labels = c("Total (IPv4) OR", "Guard total (IPv4)", "Exit total (IPv4)",
-        "Reachable guard IPv6 OR", "Reachable exit IPv6 OR", "IPv6 exiting")) +
-    ggtitle("Advertised bandwidth by IP version") +
-    labs(caption = copyright_notice) +
-    theme(legend.position = "top") +
-    guides(colour = guide_legend(nrow = 2, byrow = TRUE))
-  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
-}
-
-prepare_totalcw <- function(start_p = NULL, end_p = NULL) {
-  read_csv(file = paste(stats_dir, "totalcw.csv", sep = ""),
-      col_types = cols(
-        valid_after_date = col_date(format = ""),
-        nickname = col_character(),
-        have_guard_flag = col_logical(),
-        have_exit_flag = col_logical(),
-        measured_sum_avg = col_double())) %>%
-    filter(if (!is.null(start_p))
-        valid_after_date >= as.Date(start_p) else TRUE) %>%
-    filter(if (!is.null(end_p))
-        valid_after_date <= as.Date(end_p) else TRUE) %>%
-    group_by(valid_after_date, nickname) %>%
-    summarize(measured_sum_avg = sum(measured_sum_avg)) %>%
-    rename(date = valid_after_date, totalcw = measured_sum_avg) %>%
-    arrange(date, nickname)
-}
-
-plot_totalcw <- function(start_p, end_p, path_p) {
-  prepare_totalcw(start_p, end_p) %>%
-    mutate(nickname = ifelse(is.na(nickname), "consensus", nickname)) %>%
-    mutate(nickname = factor(nickname,
-      levels = c("consensus", unique(nickname[nickname != "consensus"])))) %>%
-    ungroup() %>%
-    complete(date = full_seq(date, period = 1), nesting(nickname)) %>%
-    ggplot(aes(x = date, y = totalcw, colour = nickname)) +
-    geom_line(na.rm = TRUE) +
-    scale_x_date(name = "", breaks = custom_breaks,
-      labels = custom_labels, minor_breaks = custom_minor_breaks) +
-    scale_y_continuous(name = "", labels = formatter, limits = c(0, NA)) +
-    scale_colour_hue(name = "") +
-    ggtitle("Total consensus weights across bandwidth authorities") +
-    labs(caption = copyright_notice)
-  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
-}
-
-
diff --git a/src/main/R/rserver/rserve-init.R b/src/main/R/rserver/rserve-init.R
index f160698..57e14f5 100644
--- a/src/main/R/rserver/rserve-init.R
+++ b/src/main/R/rserver/rserve-init.R
@@ -1,12 +1,1603 @@
-##Pre-loaded libraries and graphing functions to speed things up
+require(ggplot2)
+require(RColorBrewer)
+require(scales)
+require(dplyr)
+require(tidyr)
+require(readr)
 
-library("ggplot2")
-library("RColorBrewer")
-library("scales")
-library(dplyr)
-library(tidyr)
-library(readr)
+countrylist <- list(
+  "ad" = "Andorra",
+  "ae" = "the United Arab Emirates",
+  "af" = "Afghanistan",
+  "ag" = "Antigua and Barbuda",
+  "ai" = "Anguilla",
+  "al" = "Albania",
+  "am" = "Armenia",
+  "an" = "the Netherlands Antilles",
+  "ao" = "Angola",
+  "aq" = "Antarctica",
+  "ar" = "Argentina",
+  "as" = "American Samoa",
+  "at" = "Austria",
+  "au" = "Australia",
+  "aw" = "Aruba",
+  "ax" = "the Aland Islands",
+  "az" = "Azerbaijan",
+  "ba" = "Bosnia and Herzegovina",
+  "bb" = "Barbados",
+  "bd" = "Bangladesh",
+  "be" = "Belgium",
+  "bf" = "Burkina Faso",
+  "bg" = "Bulgaria",
+  "bh" = "Bahrain",
+  "bi" = "Burundi",
+  "bj" = "Benin",
+  "bl" = "Saint Bartelemey",
+  "bm" = "Bermuda",
+  "bn" = "Brunei",
+  "bo" = "Bolivia",
+  "bq" = "Bonaire, Sint Eustatius and Saba",
+  "br" = "Brazil",
+  "bs" = "the Bahamas",
+  "bt" = "Bhutan",
+  "bv" = "the Bouvet Island",
+  "bw" = "Botswana",
+  "by" = "Belarus",
+  "bz" = "Belize",
+  "ca" = "Canada",
+  "cc" = "the Cocos (Keeling) Islands",
+  "cd" = "the Democratic Republic of the Congo",
+  "cf" = "Central African Republic",
+  "cg" = "Congo",
+  "ch" = "Switzerland",
+  "ci" = "Côte d'Ivoire",
+  "ck" = "the Cook Islands",
+  "cl" = "Chile",
+  "cm" = "Cameroon",
+  "cn" = "China",
+  "co" = "Colombia",
+  "cr" = "Costa Rica",
+  "cu" = "Cuba",
+  "cv" = "Cape Verde",
+  "cw" = "Curaçao",
+  "cx" = "the Christmas Island",
+  "cy" = "Cyprus",
+  "cz" = "the Czech Republic",
+  "de" = "Germany",
+  "dj" = "Djibouti",
+  "dk" = "Denmark",
+  "dm" = "Dominica",
+  "do" = "the Dominican Republic",
+  "dz" = "Algeria",
+  "ec" = "Ecuador",
+  "ee" = "Estonia",
+  "eg" = "Egypt",
+  "eh" = "the Western Sahara",
+  "er" = "Eritrea",
+  "es" = "Spain",
+  "et" = "Ethiopia",
+  "fi" = "Finland",
+  "fj" = "Fiji",
+  "fk" = "the Falkland Islands (Malvinas)",
+  "fm" = "the Federated States of Micronesia",
+  "fo" = "the Faroe Islands",
+  "fr" = "France",
+  "ga" = "Gabon",
+  "gb" = "the United Kingdom",
+  "gd" = "Grenada",
+  "ge" = "Georgia",
+  "gf" = "French Guiana",
+  "gg" = "Guernsey",
+  "gh" = "Ghana",
+  "gi" = "Gibraltar",
+  "gl" = "Greenland",
+  "gm" = "Gambia",
+  "gn" = "Guinea",
+  "gp" = "Guadeloupe",
+  "gq" = "Equatorial Guinea",
+  "gr" = "Greece",
+  "gs" = "South Georgia and the South Sandwich Islands",
+  "gt" = "Guatemala",
+  "gu" = "Guam",
+  "gw" = "Guinea-Bissau",
+  "gy" = "Guyana",
+  "hk" = "Hong Kong",
+  "hm" = "Heard Island and McDonald Islands",
+  "hn" = "Honduras",
+  "hr" = "Croatia",
+  "ht" = "Haiti",
+  "hu" = "Hungary",
+  "id" = "Indonesia",
+  "ie" = "Ireland",
+  "il" = "Israel",
+  "im" = "the Isle of Man",
+  "in" = "India",
+  "io" = "the British Indian Ocean Territory",
+  "iq" = "Iraq",
+  "ir" = "Iran",
+  "is" = "Iceland",
+  "it" = "Italy",
+  "je" = "Jersey",
+  "jm" = "Jamaica",
+  "jo" = "Jordan",
+  "jp" = "Japan",
+  "ke" = "Kenya",
+  "kg" = "Kyrgyzstan",
+  "kh" = "Cambodia",
+  "ki" = "Kiribati",
+  "km" = "Comoros",
+  "kn" = "Saint Kitts and Nevis",
+  "kp" = "North Korea",
+  "kr" = "the Republic of Korea",
+  "kw" = "Kuwait",
+  "ky" = "the Cayman Islands",
+  "kz" = "Kazakhstan",
+  "la" = "Laos",
+  "lb" = "Lebanon",
+  "lc" = "Saint Lucia",
+  "li" = "Liechtenstein",
+  "lk" = "Sri Lanka",
+  "lr" = "Liberia",
+  "ls" = "Lesotho",
+  "lt" = "Lithuania",
+  "lu" = "Luxembourg",
+  "lv" = "Latvia",
+  "ly" = "Libya",
+  "ma" = "Morocco",
+  "mc" = "Monaco",
+  "md" = "the Republic of Moldova",
+  "me" = "Montenegro",
+  "mf" = "Saint Martin",
+  "mg" = "Madagascar",
+  "mh" = "the Marshall Islands",
+  "mk" = "Macedonia",
+  "ml" = "Mali",
+  "mm" = "Burma",
+  "mn" = "Mongolia",
+  "mo" = "Macau",
+  "mp" = "the Northern Mariana Islands",
+  "mq" = "Martinique",
+  "mr" = "Mauritania",
+  "ms" = "Montserrat",
+  "mt" = "Malta",
+  "mu" = "Mauritius",
+  "mv" = "the Maldives",
+  "mw" = "Malawi",
+  "mx" = "Mexico",
+  "my" = "Malaysia",
+  "mz" = "Mozambique",
+  "na" = "Namibia",
+  "nc" = "New Caledonia",
+  "ne" = "Niger",
+  "nf" = "Norfolk Island",
+  "ng" = "Nigeria",
+  "ni" = "Nicaragua",
+  "nl" = "the Netherlands",
+  "no" = "Norway",
+  "np" = "Nepal",
+  "nr" = "Nauru",
+  "nu" = "Niue",
+  "nz" = "New Zealand",
+  "om" = "Oman",
+  "pa" = "Panama",
+  "pe" = "Peru",
+  "pf" = "French Polynesia",
+  "pg" = "Papua New Guinea",
+  "ph" = "the Philippines",
+  "pk" = "Pakistan",
+  "pl" = "Poland",
+  "pm" = "Saint Pierre and Miquelon",
+  "pn" = "the Pitcairn Islands",
+  "pr" = "Puerto Rico",
+  "ps" = "the Palestinian Territory",
+  "pt" = "Portugal",
+  "pw" = "Palau",
+  "py" = "Paraguay",
+  "qa" = "Qatar",
+  "re" = "Reunion",
+  "ro" = "Romania",
+  "rs" = "Serbia",
+  "ru" = "Russia",
+  "rw" = "Rwanda",
+  "sa" = "Saudi Arabia",
+  "sb" = "the Solomon Islands",
+  "sc" = "the Seychelles",
+  "sd" = "Sudan",
+  "se" = "Sweden",
+  "sg" = "Singapore",
+  "sh" = "Saint Helena",
+  "si" = "Slovenia",
+  "sj" = "Svalbard and Jan Mayen",
+  "sk" = "Slovakia",
+  "sl" = "Sierra Leone",
+  "sm" = "San Marino",
+  "sn" = "Senegal",
+  "so" = "Somalia",
+  "sr" = "Suriname",
+  "ss" = "South Sudan",
+  "st" = "São Tomé and Príncipe",
+  "sv" = "El Salvador",
+  "sx" = "Sint Maarten",
+  "sy" = "the Syrian Arab Republic",
+  "sz" = "Swaziland",
+  "tc" = "Turks and Caicos Islands",
+  "td" = "Chad",
+  "tf" = "the French Southern Territories",
+  "tg" = "Togo",
+  "th" = "Thailand",
+  "tj" = "Tajikistan",
+  "tk" = "Tokelau",
+  "tl" = "East Timor",
+  "tm" = "Turkmenistan",
+  "tn" = "Tunisia",
+  "to" = "Tonga",
+  "tr" = "Turkey",
+  "tt" = "Trinidad and Tobago",
+  "tv" = "Tuvalu",
+  "tw" = "Taiwan",
+  "tz" = "the United Republic of Tanzania",
+  "ua" = "Ukraine",
+  "ug" = "Uganda",
+  "um" = "the United States Minor Outlying Islands",
+  "us" = "the United States",
+  "uy" = "Uruguay",
+  "uz" = "Uzbekistan",
+  "va" = "Vatican City",
+  "vc" = "Saint Vincent and the Grenadines",
+  "ve" = "Venezuela",
+  "vg" = "the British Virgin Islands",
+  "vi" = "the United States Virgin Islands",
+  "vn" = "Vietnam",
+  "vu" = "Vanuatu",
+  "wf" = "Wallis and Futuna",
+  "ws" = "Samoa",
+  "xk" = "Kosovo",
+  "ye" = "Yemen",
+  "yt" = "Mayotte",
+  "za" = "South Africa",
+  "zm" = "Zambia",
+  "zw" = "Zimbabwe")
 
-source('graphs.R')
-source('tables.R')
+countryname <- function(country) {
+  res <- countrylist[[country]]
+  if (is.null(res))
+    res <- "no-man's-land"
+  res
+}
+
+# Helper function that takes date limits as input and returns major breaks as
+# output. The main difference to the built-in major breaks is that we're trying
+# harder to align major breaks with first days of weeks (Sundays), months,
+# quarters, or years.
+custom_breaks <- function(input) {
+  scales_index <- cut(as.numeric(max(input) - min(input)),
+    c(-1, 7, 12, 56, 180, 600, 2000, Inf), labels = FALSE)
+  from_print_format <- c("%F", "%F", "%Y-W%U-7", "%Y-%m-01", "%Y-01-01",
+    "%Y-01-01", "%Y-01-01")[scales_index]
+  from_parse_format <- ifelse(scales_index == 3, "%Y-W%U-%u", "%F")
+  by <- c("1 day", "2 days", "1 week", "1 month", "3 months", "1 year",
+    "2 years")[scales_index]
+  seq(as.Date(as.character(min(input), from_print_format),
+    format = from_parse_format), max(input), by = by)
+}
+
+# Helper function that takes date limits as input and returns minor breaks as
+# output. As opposed to the built-in minor breaks, we're not just adding one
+# minor break half way through between two major breaks. Instead, we're plotting
+# a minor break for every day, week, month, or quarter between two major breaks.
+custom_minor_breaks <- function(input) {
+  scales_index <- cut(as.numeric(max(input) - min(input)),
+    c(-1, 7, 12, 56, 180, 600, 2000, Inf), labels = FALSE)
+  from_print_format <- c("%F", "%F", "%F", "%Y-W%U-7", "%Y-%m-01", "%Y-01-01",
+    "%Y-01-01")[scales_index]
+  from_parse_format <- ifelse(scales_index == 4, "%Y-W%U-%u", "%F")
+  by <- c("1 day", "1 day", "1 day", "1 week", "1 month", "3 months",
+    "1 year")[scales_index]
+  seq(as.Date(as.character(min(input), from_print_format),
+    format = from_parse_format), max(input), by = by)
+}
+
+# Helper function that takes breaks as input and returns labels as output. We're
+# going all ISO-8601 here, though we're not just writing %Y-%m-%d everywhere,
+# but %Y-%m or %Y if all breaks are on the first of a month or even year.
+custom_labels <- function(breaks) {
+  if (all(format(breaks, format = "%m-%d") == "01-01", na.rm = TRUE)) {
+    format(breaks, format = "%Y")
+  } else {
+    if (all(format(breaks, format = "%d") == "01", na.rm = TRUE)) {
+      format(breaks, format = "%Y-%m")
+    } else {
+      format(breaks, format = "%F")
+    }
+  }
+}
+
+# Helper function to format numbers in non-scientific notation with spaces as
+# thousands separator.
+formatter <- function(x, ...) {
+  format(x, ..., scientific = FALSE, big.mark = " ")
+}
+
+theme_update(
+  # Make plot title centered, and leave some room to the plot.
+  plot.title = element_text(hjust = 0.5, margin = margin(b = 11)),
+
+  # Leave a little more room to the right for long x axis labels.
+  plot.margin = margin(5.5, 11, 5.5, 5.5)
+)
+
+# Set the default line size of geom_line() to 1.
+update_geom_defaults("line", list(size = 1))
+
+copyright_notice <- "The Tor Project - https://metrics.torproject.org/";
+
+stats_dir <- "/srv/metrics.torproject.org/metrics/shared/stats/"
+
+rdata_dir <- "/srv/metrics.torproject.org/metrics/shared/RData/"
+
+# Helper function that copies the appropriate no data object to filename.
+copy_no_data <- function(filename) {
+  len <- nchar(filename)
+  extension <- substr(filename, len - 3, len)
+  if (".csv" == extension) {
+    write("# No data available for the given parameters.", file=filename)
+  } else {
+    file.copy(paste(rdata_dir, "no-data-available", extension, sep = ""),
+      filename)
+  }
+}
+
+# Helper function wrapping calls into error handling.
+robust_call <- function(wrappee, filename) {
+  tryCatch(eval(wrappee), error = function(e) copy_no_data(filename),
+     finally = if (!file.exists(filename) || file.size(filename) == 0) {
+       copy_no_data(filename)
+       })
+}
+
+# Write the result of the given FUN, typically a prepare_ function, as .csv file
+# to the given path_p.
+write_data <- function(FUN, ..., path_p) {
+  FUN(...) %>%
+    write.csv(path_p, quote = FALSE, row.names = FALSE, na = "")
+}
+
+# Disable readr's automatic progress bar.
+options(readr.show_progress = FALSE)
+
+prepare_networksize <- function(start_p = NULL, end_p = NULL) {
+  read_csv(file = paste(stats_dir, "networksize.csv", sep = ""),
+      col_types = cols(
+        date = col_date(format = ""),
+        relays = col_double(),
+        bridges = col_double())) %>%
+    filter(if (!is.null(start_p)) date >= as.Date(start_p) else TRUE) %>%
+    filter(if (!is.null(end_p)) date <= as.Date(end_p) else TRUE)
+}
+
+plot_networksize <- function(start_p, end_p, path_p) {
+  prepare_networksize(start_p, end_p) %>%
+    gather(variable, value, -date) %>%
+    complete(date = full_seq(date, period = 1),
+      variable = c("relays", "bridges")) %>%
+    ggplot(aes(x = date, y = value, colour = variable)) +
+    geom_line() +
+    scale_x_date(name = "", breaks = custom_breaks,
+      labels = custom_labels, minor_breaks = custom_minor_breaks) +
+    scale_y_continuous(name = "", labels = formatter, limits = c(0, NA)) +
+    scale_colour_hue("", breaks = c("relays", "bridges"),
+        labels = c("Relays", "Bridges")) +
+    ggtitle("Number of relays") +
+    labs(caption = copyright_notice)
+  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
+}
+
+prepare_versions <- function(start_p = NULL, end_p = NULL) {
+  read_csv(paste(stats_dir, "versions.csv", sep = ""),
+      col_types = cols(
+        date = col_date(format = ""),
+        version = col_character(),
+        relays = col_double())) %>%
+    filter(if (!is.null(start_p)) date >= as.Date(start_p) else TRUE) %>%
+    filter(if (!is.null(end_p)) date <= as.Date(end_p) else TRUE)
+}
+
+plot_versions <- function(start_p, end_p, path_p) {
+  s <- prepare_versions(start_p, end_p)
+  known_versions <- unique(s$version)
+  getPalette <- colorRampPalette(brewer.pal(12, "Paired"))
+  colours <- data.frame(breaks = known_versions,
+    values = rep(brewer.pal(min(12, length(known_versions)), "Paired"),
+                 len = length(known_versions)),
+    stringsAsFactors = FALSE)
+  versions <- s[s$version %in% known_versions, ]
+  visible_versions <- sort(unique(versions$version))
+  versions <- versions %>%
+    complete(date = full_seq(date, period = 1), nesting(version)) %>%
+    ggplot(aes(x = date, y = relays, colour = version)) +
+    geom_line() +
+    scale_x_date(name = "", breaks = custom_breaks,
+      labels = custom_labels, minor_breaks = custom_minor_breaks) +
+    scale_y_continuous(name = "", labels = formatter, limits = c(0, NA)) +
+    scale_colour_manual(name = "Tor version",
+      values = colours[colours$breaks %in% visible_versions, 2],
+      breaks = visible_versions) +
+    ggtitle("Relay versions") +
+    labs(caption = copyright_notice)
+  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
+}
+
+prepare_platforms <- function(start_p = NULL, end_p = NULL) {
+  read_csv(file = paste(stats_dir, "platforms.csv", sep = ""),
+      col_types = cols(
+        date = col_date(format = ""),
+        platform = col_factor(levels = NULL),
+        relays = col_double())) %>%
+    filter(if (!is.null(start_p)) date >= as.Date(start_p) else TRUE) %>%
+    filter(if (!is.null(end_p)) date <= as.Date(end_p) else TRUE) %>%
+    mutate(platform = tolower(platform)) %>%
+    spread(platform, relays)
+}
+
+plot_platforms <- function(start_p, end_p, path_p) {
+  prepare_platforms(start_p, end_p) %>%
+    gather(platform, relays, -date) %>%
+    complete(date = full_seq(date, period = 1), nesting(platform)) %>%
+    ggplot(aes(x = date, y = relays, colour = platform)) +
+    geom_line() +
+    scale_x_date(name = "", breaks = custom_breaks,
+      labels = custom_labels, minor_breaks = custom_minor_breaks) +
+    scale_y_continuous(name = "", labels = formatter, limits = c(0, NA)) +
+    scale_colour_manual(name = "Platform",
+      breaks = c("linux", "macos", "bsd", "windows", "other"),
+      labels = c("Linux", "macOS", "BSD", "Windows", "Other"),
+      values = c("linux" = "#56B4E9", "macos" = "#333333", "bsd" = "#E69F00",
+          "windows" = "#0072B2", "other" = "#009E73")) +
+    ggtitle("Relay platforms") +
+    labs(caption = copyright_notice)
+  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
+}
+
+prepare_dirbytes <- function(start_p = NULL, end_p = NULL) {
+  read_csv(file = paste(stats_dir, "bandwidth.csv", sep = ""),
+      col_types = cols(
+        date = col_date(format = ""),
+        isexit = col_logical(),
+        isguard = col_logical(),
+        bwread = col_skip(),
+        bwwrite = col_skip(),
+        dirread = col_double(),
+        dirwrite = col_double())) %>%
+    filter(if (!is.null(start_p)) date >= as.Date(start_p) else TRUE) %>%
+    filter(if (!is.null(end_p)) date <= as.Date(end_p) else TRUE) %>%
+    filter(is.na(isexit)) %>%
+    filter(is.na(isguard)) %>%
+    mutate(dirread = dirread * 8 / 1e9,
+      dirwrite = dirwrite * 8 / 1e9) %>%
+    select(date, dirread, dirwrite)
+}
+
+plot_dirbytes <- function(start_p, end_p, path_p) {
+  prepare_dirbytes(start_p, end_p) %>%
+    gather(variable, value, -date) %>%
+    complete(date = full_seq(date, period = 1), nesting(variable)) %>%
+    ggplot(aes(x = date, y = value, colour = variable)) +
+    geom_line() +
+    scale_x_date(name = "", breaks = custom_breaks,
+      labels = custom_labels, minor_breaks = custom_minor_breaks) +
+    scale_y_continuous(name = "", labels = unit_format(unit = "Gbit/s"),
+      limits = c(0, NA)) +
+    scale_colour_hue(name = "",
+        breaks = c("dirwrite", "dirread"),
+        labels = c("Written dir bytes", "Read dir bytes")) +
+    ggtitle("Number of bytes spent on answering directory requests") +
+    labs(caption = copyright_notice) +
+    theme(legend.position = "top")
+  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
+}
+
+prepare_relayflags <- function(start_p = NULL, end_p = NULL, flag_p = NULL) {
+  read_csv(file = paste(stats_dir, "relayflags.csv", sep = ""),
+      col_types = cols(
+        date = col_date(format = ""),
+        flag = col_factor(levels = NULL),
+        relays = col_double())) %>%
+    filter(if (!is.null(start_p)) date >= as.Date(start_p) else TRUE) %>%
+    filter(if (!is.null(end_p)) date <= as.Date(end_p) else TRUE) %>%
+    filter(if (!is.null(flag_p)) flag %in% flag_p else TRUE)
+}
+
+plot_relayflags <- function(start_p, end_p, flag_p, path_p) {
+  prepare_relayflags(start_p, end_p, flag_p) %>%
+    complete(date = full_seq(date, period = 1), flag = unique(flag)) %>%
+    ggplot(aes(x = date, y = relays, colour = flag)) +
+    geom_line() +
+    scale_x_date(name = "", breaks = custom_breaks,
+      labels = custom_labels, minor_breaks = custom_minor_breaks) +
+    scale_y_continuous(name = "", labels = formatter, limits = c(0, NA)) +
+    scale_colour_manual(name = "Relay flags", values = c("#E69F00",
+        "#56B4E9", "#009E73", "#EE6A50", "#000000", "#0072B2"),
+        breaks = flag_p, labels = flag_p) +
+    ggtitle("Number of relays with relay flags assigned") +
+    labs(caption = copyright_notice)
+  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
+}
+
+prepare_torperf <- function(start_p = NULL, end_p = NULL, server_p = NULL,
+    filesize_p = NULL) {
+  read_csv(file = paste(stats_dir, "torperf-1.1.csv", sep = ""),
+      col_types = cols(
+        date = col_date(format = ""),
+        filesize = col_double(),
+        source = col_character(),
+        server = col_character(),
+        q1 = col_double(),
+        md = col_double(),
+        q3 = col_double(),
+        timeouts = col_skip(),
+        failures = col_skip(),
+        requests = col_skip())) %>%
+    filter(if (!is.null(start_p)) date >= as.Date(start_p) else TRUE) %>%
+    filter(if (!is.null(end_p)) date <= as.Date(end_p) else TRUE) %>%
+    filter(if (!is.null(server_p)) server == server_p else TRUE) %>%
+    filter(if (!is.null(filesize_p))
+        filesize == ifelse(filesize_p == "50kb", 50 * 1024,
+        ifelse(filesize_p == "1mb", 1024 * 1024, 5 * 1024 * 1024)) else
+        TRUE) %>%
+    transmute(date, filesize, source, server, q1 = q1 / 1e3, md = md / 1e3,
+      q3 = q3 / 1e3)
+}
+
+plot_torperf <- function(start_p, end_p, server_p, filesize_p, path_p) {
+  prepare_torperf(start_p, end_p, server_p, filesize_p) %>%
+    filter(source != "") %>%
+    complete(date = full_seq(date, period = 1), nesting(source)) %>%
+    ggplot(aes(x = date, y = md, ymin = q1, ymax = q3, fill = source)) +
+    geom_ribbon(alpha = 0.5) +
+    geom_line(aes(colour = source), size = 0.75) +
+    scale_x_date(name = "", breaks = custom_breaks,
+      labels = custom_labels, minor_breaks = custom_minor_breaks) +
+    scale_y_continuous(name = "", labels = unit_format(unit = "s"),
+      limits = c(0, NA)) +
+    scale_fill_hue(name = "Source") +
+    scale_colour_hue(name = "Source") +
+    ggtitle(paste("Time to complete",
+        ifelse(filesize_p == "50kb", "50 KiB",
+        ifelse(filesize_p == "1mb", "1 MiB", "5 MiB")),
+        "request to", server_p, "server")) +
+    labs(caption = copyright_notice) +
+    theme(legend.position = "top")
+  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
+}
+
+prepare_torperf_failures <- function(start_p = NULL, end_p = NULL,
+    server_p = NULL, filesize_p = NULL) {
+  read_csv(file = paste(stats_dir, "torperf-1.1.csv", sep = ""),
+      col_types = cols(
+        date = col_date(format = ""),
+        filesize = col_double(),
+        source = col_character(),
+        server = col_character(),
+        q1 = col_skip(),
+        md = col_skip(),
+        q3 = col_skip(),
+        timeouts = col_double(),
+        failures = col_double(),
+        requests = col_double())) %>%
+    filter(if (!is.null(start_p)) date >= as.Date(start_p) else TRUE) %>%
+    filter(if (!is.null(end_p)) date <= as.Date(end_p) else TRUE) %>%
+    filter(if (!is.null(filesize_p))
+        filesize == ifelse(filesize_p == "50kb", 50 * 1024,
+        ifelse(filesize_p == "1mb", 1024 * 1024, 5 * 1024 * 1024)) else
+        TRUE) %>%
+    filter(if (!is.null(server_p)) server == server_p else TRUE) %>%
+    filter(requests > 0) %>%
+    transmute(date, filesize, source, server, timeouts = timeouts / requests,
+        failures = failures / requests)
+}
+
+plot_torperf_failures <- function(start_p, end_p, server_p, filesize_p,
+    path_p) {
+  prepare_torperf_failures(start_p, end_p, server_p, filesize_p) %>%
+    filter(source != "") %>%
+    gather(variable, value, -c(date, filesize, source, server)) %>%
+    mutate(variable = factor(variable, levels = c("timeouts", "failures"),
+      labels = c("Timeouts", "Failures"))) %>%
+    ggplot(aes(x = date, y = value, colour = source)) +
+    geom_point(size = 2, alpha = 0.5) +
+    scale_x_date(name = "", breaks = custom_breaks,
+      labels = custom_labels, minor_breaks = custom_minor_breaks) +
+    scale_y_continuous(name = "", labels = percent, limits = c(0, NA)) +
+    scale_colour_hue(name = "Source") +
+    facet_grid(variable ~ .) +
+    ggtitle(paste("Timeouts and failures of",
+        ifelse(filesize_p == "50kb", "50 KiB",
+        ifelse(filesize_p == "1mb", "1 MiB", "5 MiB")),
+        "requests to", server_p, "server")) +
+    labs(caption = copyright_notice) +
+    theme(legend.position = "top")
+  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
+}
+
+prepare_onionperf_buildtimes <- function(start_p = NULL, end_p = NULL) {
+  read_csv(file = paste(stats_dir, "buildtimes.csv", sep = ""),
+      col_types = cols(
+        date = col_date(format = ""),
+        source = col_character(),
+        position = col_double(),
+        q1 = col_double(),
+        md = col_double(),
+        q3 = col_double())) %>%
+    filter(if (!is.null(start_p)) date >= as.Date(start_p) else TRUE) %>%
+    filter(if (!is.null(end_p)) date <= as.Date(end_p) else TRUE)
+}
+
+plot_onionperf_buildtimes <- function(start_p, end_p, path_p) {
+  prepare_onionperf_buildtimes(start_p, end_p) %>%
+    filter(source != "") %>%
+    mutate(date = as.Date(date),
+      position = factor(position, levels = seq(1, 3, 1),
+        labels = c("1st hop", "2nd hop", "3rd hop"))) %>%
+    complete(date = full_seq(date, period = 1), nesting(source, position)) %>%
+    ggplot(aes(x = date, y = md, ymin = q1, ymax = q3, fill = source)) +
+    geom_ribbon(alpha = 0.5) +
+    geom_line(aes(colour = source), size = 0.75) +
+    facet_grid(position ~ .) +
+    scale_x_date(name = "", breaks = custom_breaks,
+      labels = custom_labels, minor_breaks = custom_minor_breaks) +
+    scale_y_continuous(name = "", labels = unit_format(unit = "ms"),
+      limits = c(0, NA)) +
+    scale_fill_hue(name = "Source") +
+    scale_colour_hue(name = "Source") +
+    ggtitle("Circuit build times") +
+    labs(caption = copyright_notice) +
+    theme(legend.position = "top")
+  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
+}
+
+prepare_onionperf_latencies <- function(start_p = NULL, end_p = NULL,
+    server_p = NULL) {
+  read_csv(file = paste(stats_dir, "latencies.csv", sep = ""),
+      col_types = cols(
+        date = col_date(format = ""),
+        source = col_character(),
+        server = col_character(),
+        q1 = col_double(),
+        md = col_double(),
+        q3 = col_double())) %>%
+    filter(if (!is.null(start_p)) date >= as.Date(start_p) else TRUE) %>%
+    filter(if (!is.null(end_p)) date <= as.Date(end_p) else TRUE) %>%
+    filter(if (!is.null(server_p)) server == server_p else TRUE)
+}
+
+plot_onionperf_latencies <- function(start_p, end_p, server_p, path_p) {
+  prepare_onionperf_latencies(start_p, end_p, server_p) %>%
+    filter(source != "") %>%
+    complete(date = full_seq(date, period = 1), nesting(source)) %>%
+    ggplot(aes(x = date, y = md, ymin = q1, ymax = q3, fill = source)) +
+    geom_ribbon(alpha = 0.5) +
+    geom_line(aes(colour = source), size = 0.75) +
+    scale_x_date(name = "", breaks = custom_breaks,
+      labels = custom_labels, minor_breaks = custom_minor_breaks) +
+    scale_y_continuous(name = "", labels = unit_format(unit = "ms"),
+      limits = c(0, NA)) +
+    scale_fill_hue(name = "Source") +
+    scale_colour_hue(name = "Source") +
+    ggtitle(paste("Circuit round-trip latencies to", server_p, "server")) +
+    labs(caption = copyright_notice) +
+    theme(legend.position = "top")
+  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
+}
+
+prepare_connbidirect <- function(start_p = NULL, end_p = NULL) {
+  read_csv(file = paste(stats_dir, "connbidirect2.csv", sep = ""),
+      col_types = cols(
+        date = col_date(format = ""),
+        direction = col_factor(levels = NULL),
+        quantile = col_double(),
+        fraction = col_double())) %>%
+    filter(if (!is.null(start_p)) date >= as.Date(start_p) else TRUE) %>%
+    filter(if (!is.null(end_p)) date <= as.Date(end_p) else TRUE) %>%
+    mutate(quantile = paste("X", quantile, sep = ""),
+      fraction = fraction / 100) %>%
+    spread(quantile, fraction) %>%
+    rename(q1 = X0.25, md = X0.5, q3 = X0.75)
+}
+
+plot_connbidirect <- function(start_p, end_p, path_p) {
+  prepare_connbidirect(start_p, end_p) %>%
+    complete(date = full_seq(date, period = 1), nesting(direction)) %>%
+    ggplot(aes(x = date, y = md, ymin = q1, ymax = q3, fill = direction)) +
+    geom_ribbon(alpha = 0.5) +
+    geom_line(aes(colour = direction), size = 0.75) +
+    scale_x_date(name = "", breaks = custom_breaks,
+      labels = custom_labels, minor_breaks = custom_minor_breaks) +
+    scale_y_continuous(name = "", labels = percent, limits = c(0, NA)) +
+    scale_colour_hue(name = "Medians and interquartile ranges",
+                     breaks = c("both", "write", "read"),
+        labels = c("Both reading and writing", "Mostly writing",
+                   "Mostly reading")) +
+    scale_fill_hue(name = "Medians and interquartile ranges",
+                   breaks = c("both", "write", "read"),
+        labels = c("Both reading and writing", "Mostly writing",
+                   "Mostly reading")) +
+    ggtitle("Fraction of connections used uni-/bidirectionally") +
+    labs(caption = copyright_notice) +
+    theme(legend.position = "top")
+  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
+}
+
+prepare_bandwidth_flags <- function(start_p = NULL, end_p = NULL) {
+  advbw <- read_csv(file = paste(stats_dir, "advbw.csv", sep = ""),
+      col_types = cols(
+        date = col_date(format = ""),
+        isexit = col_logical(),
+        isguard = col_logical(),
+        advbw = col_double())) %>%
+    transmute(date, have_guard_flag = isguard, have_exit_flag = isexit,
+      variable = "advbw", value = advbw * 8 / 1e9)
+  bwhist <- read_csv(file = paste(stats_dir, "bandwidth.csv", sep = ""),
+      col_types = cols(
+        date = col_date(format = ""),
+        isexit = col_logical(),
+        isguard = col_logical(),
+        bwread = col_double(),
+        bwwrite = col_double(),
+        dirread = col_double(),
+        dirwrite = col_double())) %>%
+    transmute(date, have_guard_flag = isguard, have_exit_flag = isexit,
+      variable = "bwhist", value = (bwread + bwwrite) * 8 / 2e9)
+  rbind(advbw, bwhist) %>%
+    filter(if (!is.null(start_p)) date >= as.Date(start_p) else TRUE) %>%
+    filter(if (!is.null(end_p)) date <= as.Date(end_p) else TRUE) %>%
+    filter(!is.na(have_exit_flag)) %>%
+    filter(!is.na(have_guard_flag)) %>%
+    spread(variable, value)
+}
+
+plot_bandwidth_flags <- function(start_p, end_p, path_p) {
+  prepare_bandwidth_flags(start_p, end_p) %>%
+    gather(variable, value, c(advbw, bwhist)) %>%
+    unite(flags, have_guard_flag, have_exit_flag) %>%
+    mutate(flags = factor(flags,
+      levels = c("FALSE_TRUE", "TRUE_TRUE", "TRUE_FALSE", "FALSE_FALSE"),
+      labels = c("Exit only", "Guard and Exit", "Guard only",
+      "Neither Guard nor Exit"))) %>%
+    mutate(variable = ifelse(variable == "advbw",
+      "Advertised bandwidth", "Consumed bandwidth")) %>%
+    ggplot(aes(x = date, y = value, fill = flags)) +
+    geom_area() +
+    scale_x_date(name = "", breaks = custom_breaks,
+      labels = custom_labels, minor_breaks = custom_minor_breaks) +
+    scale_y_continuous(name = "", labels = unit_format(unit = "Gbit/s"),
+      limits = c(0, NA)) +
+    scale_fill_manual(name = "",
+      values = c("#03B3FF", "#39FF02", "#FFFF00", "#AAAA99")) +
+    facet_grid(variable ~ .) +
+    ggtitle("Advertised and consumed bandwidth by relay flags") +
+    labs(caption = copyright_notice) +
+    theme(legend.position = "top")
+  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
+}
+
+prepare_userstats_relay_country <- function(start_p = NULL, end_p = NULL,
+    country_p = NULL, events_p = NULL) {
+  read_csv(file = paste(stats_dir, "clients.csv", sep = ""),
+      col_types = cols(
+        date = col_date(format = ""),
+        node = col_character(),
+        country = col_character(),
+        transport = col_character(),
+        version = col_character(),
+        lower = col_double(),
+        upper = col_double(),
+        clients = col_double(),
+        frac = col_double()),
+      na = character()) %>%
+    filter(node == "relay") %>%
+    filter(if (!is.null(start_p)) date >= as.Date(start_p) else TRUE) %>%
+    filter(if (!is.null(end_p)) date <= as.Date(end_p) else TRUE) %>%
+    filter(if (!is.null(country_p))
+      country == ifelse(country_p == "all", "", country_p) else TRUE) %>%
+    filter(transport == "") %>%
+    filter(version == "") %>%
+    select(date, country, clients, lower, upper, frac) %>%
+    rename(users = clients)
+}
+
+plot_userstats_relay_country <- function(start_p, end_p, country_p, events_p,
+    path_p) {
+  u <- prepare_userstats_relay_country(start_p, end_p, country_p, events_p) %>%
+    complete(date = full_seq(date, period = 1))
+  plot <- ggplot(u, aes(x = date, y = users))
+  if (length(na.omit(u$users)) > 0 & events_p != "off" &
+      country_p != "all") {
+    upturns <- u[u$users > u$upper, c("date", "users")]
+    downturns <- u[u$users < u$lower, c("date", "users")]
+    if (events_p == "on") {
+      u[!is.na(u$lower) & u$lower < 0, "lower"] <- 0
+      plot <- plot +
+        geom_ribbon(data = u, aes(ymin = lower, ymax = upper), fill = "gray")
+    }
+    if (length(upturns$date) > 0)
+      plot <- plot +
+          geom_point(data = upturns, aes(x = date, y = users), size = 5,
+          colour = "dodgerblue2")
+    if (length(downturns$date) > 0)
+      plot <- plot +
+          geom_point(data = downturns, aes(x = date, y = users), size = 5,
+          colour = "firebrick2")
+  }
+  plot <- plot +
+    geom_line() +
+    scale_x_date(name = "", breaks = custom_breaks,
+      labels = custom_labels, minor_breaks = custom_minor_breaks) +
+    scale_y_continuous(name = "", labels = formatter, limits = c(0, NA)) +
+    ggtitle(paste("Directly connecting users",
+        ifelse(country_p == "all", "",
+        paste(" from", countryname(country_p))), sep = "")) +
+    labs(caption = copyright_notice)
+  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
+}
+
+prepare_userstats_bridge_country <- function(start_p = NULL, end_p = NULL,
+    country_p = NULL) {
+  read_csv(file = paste(stats_dir, "clients.csv", sep = ""),
+      col_types = cols(
+        date = col_date(format = ""),
+        node = col_character(),
+        country = col_character(),
+        transport = col_character(),
+        version = col_character(),
+        lower = col_double(),
+        upper = col_double(),
+        clients = col_double(),
+        frac = col_double()),
+      na = character()) %>%
+    filter(node == "bridge") %>%
+    filter(if (!is.null(start_p)) date >= as.Date(start_p) else TRUE) %>%
+    filter(if (!is.null(end_p)) date <= as.Date(end_p) else TRUE) %>%
+    filter(if (!is.null(country_p))
+      country == ifelse(country_p == "all", "", country_p) else TRUE) %>%
+    filter(transport == "") %>%
+    filter(version == "") %>%
+    select(date, country, clients, frac) %>%
+    rename(users = clients)
+}
+
+plot_userstats_bridge_country <- function(start_p, end_p, country_p, path_p) {
+  prepare_userstats_bridge_country(start_p, end_p, country_p) %>%
+    complete(date = full_seq(date, period = 1)) %>%
+    ggplot(aes(x = date, y = users)) +
+    geom_line() +
+    scale_x_date(name = "", breaks = custom_breaks,
+      labels = custom_labels, minor_breaks = custom_minor_breaks) +
+    scale_y_continuous(name = "", labels = formatter, limits = c(0, NA)) +
+    ggtitle(paste("Bridge users",
+        ifelse(country_p == "all", "",
+        paste(" from", countryname(country_p))), sep = "")) +
+    labs(caption = copyright_notice)
+  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
+}
+
+prepare_userstats_bridge_transport <- function(start_p = NULL, end_p = NULL,
+    transport_p = NULL) {
+  u <- read_csv(file = paste(stats_dir, "clients.csv", sep = ""),
+      col_types = cols(
+        date = col_date(format = ""),
+        node = col_character(),
+        country = col_character(),
+        transport = col_character(),
+        version = col_character(),
+        lower = col_double(),
+        upper = col_double(),
+        clients = col_double(),
+        frac = col_double())) %>%
+    filter(node == "bridge") %>%
+    filter(if (!is.null(start_p)) date >= as.Date(start_p) else TRUE) %>%
+    filter(if (!is.null(end_p)) date <= as.Date(end_p) else TRUE) %>%
+    filter(is.na(country)) %>%
+    filter(is.na(version)) %>%
+    filter(!is.na(transport)) %>%
+    select(date, transport, clients, frac)
+  if (is.null(transport_p) || "!<OR>" %in% transport_p) {
+    n <- u %>%
+      filter(transport != "<OR>") %>%
+      group_by(date, frac) %>%
+      summarize(clients = sum(clients))
+    u <- rbind(u, data.frame(date = n$date, transport = "!<OR>",
+                             clients = n$clients, frac = n$frac))
+  }
+  u %>%
+    filter(if (!is.null(transport_p)) transport %in% transport_p else TRUE) %>%
+    select(date, transport, clients, frac) %>%
+    rename(users = clients) %>%
+    arrange(date, transport)
+}
+
+plot_userstats_bridge_transport <- function(start_p, end_p, transport_p,
+    path_p) {
+  if (length(transport_p) > 1) {
+    title <- paste("Bridge users by transport")
+  } else {
+    title <- paste("Bridge users using",
+             ifelse(transport_p == "<??>", "unknown pluggable transport(s)",
+             ifelse(transport_p == "<OR>", "default OR protocol",
+             ifelse(transport_p == "!<OR>", "any pluggable transport",
+             ifelse(transport_p == "fte", "FTE",
+             ifelse(transport_p == "websocket", "Flash proxy/websocket",
+             paste("transport", transport_p)))))))
+  }
+  u <- prepare_userstats_bridge_transport(start_p, end_p, transport_p) %>%
+    complete(date = full_seq(date, period = 1), nesting(transport))
+  if (length(transport_p) > 1) {
+    plot <- ggplot(u, aes(x = date, y = users, colour = transport))
+  } else {
+    plot <- ggplot(u, aes(x = date, y = users))
+  }
+  plot <- plot +
+    geom_line() +
+    scale_x_date(name = "", breaks = custom_breaks,
+      labels = custom_labels, minor_breaks = custom_minor_breaks) +
+    scale_y_continuous(name = "", labels = formatter, limits = c(0, NA)) +
+    ggtitle(title) +
+    labs(caption = copyright_notice)
+  if (length(transport_p) > 1) {
+    plot <- plot +
+      scale_colour_hue(name = "", breaks = transport_p,
+            labels = ifelse(transport_p == "<??>", "Unknown PT",
+                     ifelse(transport_p == "<OR>", "Default OR protocol",
+                     ifelse(transport_p == "!<OR>", "Any PT",
+                     ifelse(transport_p == "fte", "FTE",
+                     ifelse(transport_p == "websocket", "Flash proxy/websocket",
+                     transport_p))))))
+  }
+  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
+}
+
+prepare_userstats_bridge_version <- function(start_p = NULL, end_p = NULL,
+    version_p = NULL) {
+  read_csv(file = paste(stats_dir, "clients.csv", sep = ""),
+      col_types = cols(
+        date = col_date(format = ""),
+        node = col_character(),
+        country = col_character(),
+        transport = col_character(),
+        version = col_character(),
+        lower = col_double(),
+        upper = col_double(),
+        clients = col_double(),
+        frac = col_double())) %>%
+    filter(node == "bridge") %>%
+    filter(if (!is.null(start_p)) date >= as.Date(start_p) else TRUE) %>%
+    filter(if (!is.null(end_p)) date <= as.Date(end_p) else TRUE) %>%
+    filter(is.na(country)) %>%
+    filter(is.na(transport)) %>%
+    filter(if (!is.null(version_p)) version == version_p else TRUE) %>%
+    select(date, version, clients, frac) %>%
+    rename(users = clients)
+}
+
+plot_userstats_bridge_version <- function(start_p, end_p, version_p, path_p) {
+  prepare_userstats_bridge_version(start_p, end_p, version_p) %>%
+    complete(date = full_seq(date, period = 1)) %>%
+    ggplot(aes(x = date, y = users)) +
+    geom_line() +
+    scale_x_date(name = "", breaks = custom_breaks,
+      labels = custom_labels, minor_breaks = custom_minor_breaks) +
+    scale_y_continuous(name = "", labels = formatter, limits = c(0, NA)) +
+    ggtitle(paste("Bridge users using IP", version_p, sep = "")) +
+    labs(caption = copyright_notice)
+  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
+}
+
+prepare_userstats_bridge_combined <- function(start_p = NULL, end_p = NULL,
+    country_p = NULL) {
+  if (!is.null(country_p) && country_p == "all") {
+    prepare_userstats_bridge_country(start_p, end_p, country_p)
+  } else {
+    read_csv(file = paste(stats_dir, "userstats-combined.csv", sep = ""),
+        col_types = cols(
+          date = col_date(format = ""),
+          node = col_skip(),
+          country = col_character(),
+          transport = col_character(),
+          version = col_skip(),
+          frac = col_double(),
+          low = col_double(),
+          high = col_double()),
+        na = character()) %>%
+      filter(if (!is.null(start_p)) date >= as.Date(start_p) else TRUE) %>%
+      filter(if (!is.null(end_p)) date <= as.Date(end_p) else TRUE) %>%
+      filter(if (!is.null(country_p)) country == country_p else TRUE) %>%
+      select(date, country, transport, low, high, frac) %>%
+      arrange(date, country, transport)
+  }
+}
+
+plot_userstats_bridge_combined <- function(start_p, end_p, country_p, path_p) {
+  if (country_p == "all") {
+    plot_userstats_bridge_country(start_p, end_p, country_p, path_p)
+  } else {
+    top <- 3
+    u <- prepare_userstats_bridge_combined(start_p, end_p, country_p)
+    a <- aggregate(list(mid = (u$high + u$low) / 2),
+                   by = list(transport = u$transport), FUN = sum)
+    a <- a[order(a$mid, decreasing = TRUE)[1:top], ]
+    u <- u[u$transport %in% a$transport, ] %>%
+      complete(date = full_seq(date, period = 1), nesting(country, transport))
+    title <- paste("Bridge users by transport from ",
+                   countryname(country_p), sep = "")
+    ggplot(u, aes(x = as.Date(date), ymin = low, ymax = high,
+      fill = transport)) +
+    geom_ribbon(alpha = 0.5, size = 0.5) +
+    scale_x_date(name = "", breaks = custom_breaks,
+      labels = custom_labels, minor_breaks = custom_minor_breaks) +
+    scale_y_continuous(name = "", limits = c(0, NA), labels = formatter) +
+    scale_colour_hue("Top-3 transports") +
+    scale_fill_hue("Top-3 transports") +
+    ggtitle(title) +
+    labs(caption = copyright_notice) +
+    theme(legend.position = "top")
+    ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
+  }
+}
+
+prepare_advbwdist_perc <- function(start_p = NULL, end_p = NULL, p_p = NULL) {
+  read_csv(file = paste(stats_dir, "advbwdist.csv", sep = ""),
+      col_types = cols(
+        date = col_date(format = ""),
+        isexit = col_logical(),
+        relay = col_skip(),
+        percentile = col_integer(),
+        advbw = col_double())) %>%
+    filter(if (!is.null(start_p)) date >= as.Date(start_p) else TRUE) %>%
+    filter(if (!is.null(end_p)) date <= as.Date(end_p) else TRUE) %>%
+    filter(if (!is.null(p_p)) percentile %in% as.numeric(p_p) else
+      percentile != "") %>%
+    transmute(date, percentile = as.factor(percentile),
+      variable = ifelse(is.na(isexit), "all", "exits"),
+      advbw = advbw * 8 / 1e9) %>%
+    spread(variable, advbw) %>%
+    rename(p = percentile)
+}
+
+plot_advbwdist_perc <- function(start_p, end_p, p_p, path_p) {
+  prepare_advbwdist_perc(start_p, end_p, p_p) %>%
+    gather(variable, advbw, -c(date, p)) %>%
+    mutate(variable = ifelse(variable == "all", "All relays",
+      "Exits only")) %>%
+    complete(date = full_seq(date, period = 1), nesting(p, variable)) %>%
+    ggplot(aes(x = date, y = advbw, colour = p)) +
+    facet_grid(variable ~ .) +
+    geom_line() +
+    scale_x_date(name = "", breaks = custom_breaks,
+      labels = custom_labels, minor_breaks = custom_minor_breaks) +
+    scale_y_continuous(name = "", labels = unit_format(unit = "Gbit/s"),
+      limits = c(0, NA)) +
+    scale_colour_hue(name = "Percentile") +
+    ggtitle("Advertised bandwidth distribution") +
+    labs(caption = copyright_notice)
+  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
+}
+
+prepare_advbwdist_relay <- function(start_p = NULL, end_p = NULL, n_p = NULL) {
+  read_csv(file = paste(stats_dir, "advbwdist.csv", sep = ""),
+      col_types = cols(
+        date = col_date(format = ""),
+        isexit = col_logical(),
+        relay = col_integer(),
+        percentile = col_skip(),
+        advbw = col_double())) %>%
+    filter(if (!is.null(start_p)) date >= as.Date(start_p) else TRUE) %>%
+    filter(if (!is.null(end_p)) date <= as.Date(end_p) else TRUE) %>%
+    filter(if (!is.null(n_p)) relay %in% as.numeric(n_p) else
+      relay != "") %>%
+    transmute(date, relay = as.factor(relay),
+      variable = ifelse(is.na(isexit), "all", "exits"),
+      advbw = advbw * 8 / 1e9) %>%
+    spread(variable, advbw) %>%
+    rename(n = relay)
+}
+
+plot_advbwdist_relay <- function(start_p, end_p, n_p, path_p) {
+  prepare_advbwdist_relay(start_p, end_p, n_p) %>%
+    gather(variable, advbw, -c(date, n)) %>%
+    mutate(variable = ifelse(variable == "all", "All relays",
+      "Exits only")) %>%
+    complete(date = full_seq(date, period = 1), nesting(n, variable)) %>%
+    ggplot(aes(x = date, y = advbw, colour = n)) +
+    facet_grid(variable ~ .) +
+    geom_line() +
+    scale_x_date(name = "", breaks = custom_breaks,
+      labels = custom_labels, minor_breaks = custom_minor_breaks) +
+    scale_y_continuous(name = "", labels = unit_format(unit = "Gbit/s"),
+      limits = c(0, NA)) +
+    scale_colour_hue(name = "n") +
+    ggtitle("Advertised bandwidth of n-th fastest relays") +
+    labs(caption = copyright_notice)
+  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
+}
+
+prepare_hidserv_dir_onions_seen <- function(start_p = NULL, end_p = NULL) {
+  read_csv(file = paste(stats_dir, "hidserv.csv", sep = ""),
+      col_types = cols(
+        date = col_date(format = ""),
+        type = col_factor(levels = NULL),
+        wmean = col_skip(),
+        wmedian = col_skip(),
+        wiqm = col_double(),
+        frac = col_double(),
+        stats = col_skip())) %>%
+    filter(if (!is.null(start_p)) date >= as.Date(start_p) else TRUE) %>%
+    filter(if (!is.null(end_p)) date <= as.Date(end_p) else TRUE) %>%
+    filter(type == "dir-onions-seen") %>%
+    transmute(date, onions = ifelse(frac >= 0.01, wiqm, NA), frac)
+}
+
+plot_hidserv_dir_onions_seen <- function(start_p, end_p, path_p) {
+  prepare_hidserv_dir_onions_seen(start_p, end_p) %>%
+    complete(date = full_seq(date, period = 1)) %>%
+    ggplot(aes(x = date, y = onions)) +
+    geom_line() +
+    scale_x_date(name = "", breaks = custom_breaks,
+      labels = custom_labels, minor_breaks = custom_minor_breaks) +
+    scale_y_continuous(name = "", limits = c(0, NA), labels = formatter) +
+    ggtitle("Unique .onion addresses") +
+    labs(caption = copyright_notice)
+  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
+}
+
+prepare_hidserv_rend_relayed_cells <- function(start_p = NULL, end_p = NULL) {
+  read_csv(file = paste(stats_dir, "hidserv.csv", sep = ""),
+      col_types = cols(
+        date = col_date(format = ""),
+        type = col_factor(levels = NULL),
+        wmean = col_skip(),
+        wmedian = col_skip(),
+        wiqm = col_double(),
+        frac = col_double(),
+        stats = col_skip())) %>%
+    filter(if (!is.null(start_p)) date >= as.Date(start_p) else TRUE) %>%
+    filter(if (!is.null(end_p)) date <= as.Date(end_p) else TRUE) %>%
+    filter(type == "rend-relayed-cells") %>%
+    transmute(date,
+      relayed = ifelse(frac >= 0.01, wiqm * 8 * 512 / (86400 * 1e9), NA), frac)
+}
+
+plot_hidserv_rend_relayed_cells <- function(start_p, end_p, path_p) {
+  prepare_hidserv_rend_relayed_cells(start_p, end_p) %>%
+    complete(date = full_seq(date, period = 1)) %>%
+    ggplot(aes(x = date, y = relayed)) +
+    geom_line() +
+    scale_x_date(name = "", breaks = custom_breaks,
+      labels = custom_labels, minor_breaks = custom_minor_breaks) +
+    scale_y_continuous(name = "", labels = unit_format(unit = "Gbit/s"),
+      limits = c(0, NA)) +
+    ggtitle("Onion-service traffic") +
+    labs(caption = copyright_notice)
+  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
+}
+
+prepare_webstats_tb <- function(start_p = NULL, end_p = NULL) {
+  read_csv(file = paste(stats_dir, "webstats.csv", sep = ""),
+      col_types = cols(
+        log_date = col_date(format = ""),
+        request_type = col_factor(levels = NULL),
+        platform = col_skip(),
+        channel = col_skip(),
+        locale = col_skip(),
+        incremental = col_skip(),
+        count = col_double())) %>%
+    filter(if (!is.null(start_p)) log_date >= as.Date(start_p) else TRUE) %>%
+    filter(if (!is.null(end_p)) log_date <= as.Date(end_p) else TRUE) %>%
+    filter(request_type %in% c("tbid", "tbsd", "tbup", "tbur")) %>%
+    group_by(log_date, request_type) %>%
+    summarize(count = sum(count)) %>%
+    spread(request_type, count) %>%
+    rename(date = log_date, initial_downloads = tbid,
+      signature_downloads = tbsd, update_pings = tbup,
+      update_requests = tbur)
+}
+
+plot_webstats_tb <- function(start_p, end_p, path_p) {
+  prepare_webstats_tb(start_p, end_p) %>%
+    gather(request_type, count, -date) %>%
+    mutate(request_type = factor(request_type,
+      levels = c("initial_downloads", "signature_downloads", "update_pings",
+        "update_requests"),
+      labels = c("Initial downloads", "Signature downloads", "Update pings",
+        "Update requests"))) %>%
+    ungroup() %>%
+    complete(date = full_seq(date, period = 1), nesting(request_type)) %>%
+    ggplot(aes(x = date, y = count)) +
+    geom_point() +
+    geom_line() +
+    facet_grid(request_type ~ ., scales = "free_y") +
+    scale_x_date(name = "", breaks = custom_breaks,
+      labels = custom_labels, minor_breaks = custom_minor_breaks) +
+    scale_y_continuous(name = "", labels = formatter, limits = c(0, NA)) +
+    theme(strip.text.y = element_text(angle = 0, hjust = 0, size = rel(1.5)),
+          strip.background = element_rect(fill = NA)) +
+    ggtitle("Tor Browser downloads and updates") +
+    labs(caption = copyright_notice)
+  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
+}
+
+prepare_webstats_tb_platform <- function(start_p = NULL, end_p = NULL) {
+  read_csv(file = paste(stats_dir, "webstats.csv", sep = ""),
+      col_types = cols(
+        log_date = col_date(format = ""),
+        request_type = col_factor(levels = NULL),
+        platform = col_factor(levels = NULL),
+        channel = col_skip(),
+        locale = col_skip(),
+        incremental = col_skip(),
+        count = col_double())) %>%
+    filter(if (!is.null(start_p)) log_date >= as.Date(start_p) else TRUE) %>%
+    filter(if (!is.null(end_p)) log_date <= as.Date(end_p) else TRUE) %>%
+    filter(request_type %in% c("tbid", "tbup")) %>%
+    group_by(log_date, platform, request_type) %>%
+    summarize(count = sum(count)) %>%
+    spread(request_type, count, fill = 0) %>%
+    rename(date = log_date, initial_downloads = tbid, update_pings = tbup)
+}
+
+plot_webstats_tb_platform <- function(start_p, end_p, path_p) {
+  prepare_webstats_tb_platform(start_p, end_p) %>%
+    gather(request_type, count, -c(date, platform)) %>%
+    mutate(request_type = factor(request_type,
+      levels = c("initial_downloads", "update_pings"),
+      labels = c("Initial downloads", "Update pings"))) %>%
+    ungroup() %>%
+    complete(date = full_seq(date, period = 1),
+      nesting(platform, request_type)) %>%
+    ggplot(aes(x = date, y = count, colour = platform)) +
+    geom_point() +
+    geom_line() +
+    scale_x_date(name = "", breaks = custom_breaks,
+      labels = custom_labels, minor_breaks = custom_minor_breaks) +
+    scale_y_continuous(name = "", labels = formatter, limits = c(0, NA)) +
+    scale_colour_hue(name = "Platform",
+        breaks = c("w", "m", "l", "o", ""),
+        labels = c("Windows", "macOS", "Linux", "Other", "Unknown")) +
+    facet_grid(request_type ~ ., scales = "free_y") +
+    theme(strip.text.y = element_text(angle = 0, hjust = 0, size = rel(1.5)),
+          strip.background = element_rect(fill = NA),
+          legend.position = "top") +
+    ggtitle("Tor Browser downloads and updates by platform") +
+    labs(caption = copyright_notice)
+  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
+}
+
+prepare_webstats_tb_locale <- function(start_p = NULL, end_p = NULL) {
+  read_csv(file = paste(stats_dir, "webstats.csv", sep = ""),
+      col_types = cols(
+        log_date = col_date(format = ""),
+        request_type = col_factor(levels = NULL),
+        platform = col_skip(),
+        channel = col_skip(),
+        locale = col_factor(levels = NULL),
+        incremental = col_skip(),
+        count = col_double())) %>%
+    filter(if (!is.null(start_p)) log_date >= as.Date(start_p) else TRUE) %>%
+    filter(if (!is.null(end_p)) log_date <= as.Date(end_p) else TRUE) %>%
+    filter(request_type %in% c("tbid", "tbup")) %>%
+    rename(date = log_date) %>%
+    group_by(date, locale, request_type) %>%
+    summarize(count = sum(count)) %>%
+    mutate(request_type = factor(request_type, levels = c("tbid", "tbup"))) %>%
+    spread(request_type, count, fill = 0) %>%
+    rename(initial_downloads = tbid, update_pings = tbup)
+}
+
+plot_webstats_tb_locale <- function(start_p, end_p, path_p) {
+  d <- prepare_webstats_tb_locale(start_p, end_p) %>%
+    gather(request_type, count, -c(date, locale)) %>%
+    mutate(request_type = factor(request_type,
+      levels = c("initial_downloads", "update_pings"),
+      labels = c("Initial downloads", "Update pings")))
+  e <- d
+  e <- aggregate(list(count = e$count), by = list(locale = e$locale), FUN = sum)
+  e <- e[order(e$count, decreasing = TRUE), ]
+  e <- e[1:5, ]
+  d <- aggregate(list(count = d$count), by = list(date = d$date,
+    request_type = d$request_type,
+    locale = ifelse(d$locale %in% e$locale, d$locale, "(other)")), FUN = sum)
+  d %>%
+    complete(date = full_seq(date, period = 1),
+      nesting(locale, request_type)) %>%
+    ggplot(aes(x = date, y = count, colour = locale)) +
+    geom_point() +
+    geom_line() +
+    scale_x_date(name = "", breaks = custom_breaks,
+      labels = custom_labels, minor_breaks = custom_minor_breaks) +
+    scale_y_continuous(name = "", labels = formatter, limits = c(0, NA)) +
+    scale_colour_hue(name = "Locale",
+        breaks = c(e$locale, "(other)"),
+        labels = c(as.character(e$locale), "Other")) +
+    facet_grid(request_type ~ ., scales = "free_y") +
+    theme(strip.text.y = element_text(angle = 0, hjust = 0, size = rel(1.5)),
+          strip.background = element_rect(fill = NA),
+          legend.position = "top") +
+    guides(col = guide_legend(nrow = 1)) +
+    ggtitle("Tor Browser downloads and updates by locale") +
+    labs(caption = copyright_notice)
+  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
+}
+
+prepare_webstats_tm <- function(start_p = NULL, end_p = NULL) {
+  read_csv(file = paste(stats_dir, "webstats.csv", sep = ""),
+      col_types = cols(
+        log_date = col_date(format = ""),
+        request_type = col_factor(levels = NULL),
+        platform = col_skip(),
+        channel = col_skip(),
+        locale = col_skip(),
+        incremental = col_skip(),
+        count = col_double())) %>%
+    filter(if (!is.null(start_p)) log_date >= as.Date(start_p) else TRUE) %>%
+    filter(if (!is.null(end_p)) log_date <= as.Date(end_p) else TRUE) %>%
+    filter(request_type %in% c("tmid", "tmup")) %>%
+    group_by(log_date, request_type) %>%
+    summarize(count = sum(count)) %>%
+    mutate(request_type = factor(request_type, levels = c("tmid", "tmup"))) %>%
+    spread(request_type, count, drop = FALSE, fill = 0) %>%
+    rename(date = log_date, initial_downloads = tmid, update_pings = tmup)
+}
+
+plot_webstats_tm <- function(start_p, end_p, path_p) {
+  prepare_webstats_tm(start_p, end_p) %>%
+    gather(request_type, count, -date) %>%
+    mutate(request_type = factor(request_type,
+      levels = c("initial_downloads", "update_pings"),
+      labels = c("Initial downloads", "Update pings"))) %>%
+    ungroup() %>%
+    complete(date = full_seq(date, period = 1), nesting(request_type)) %>%
+    ggplot(aes(x = date, y = count)) +
+    geom_point() +
+    geom_line() +
+    facet_grid(request_type ~ ., scales = "free_y") +
+    scale_x_date(name = "", breaks = custom_breaks,
+      labels = custom_labels, minor_breaks = custom_minor_breaks) +
+    scale_y_continuous(name = "", labels = formatter, limits = c(0, NA)) +
+    theme(strip.text.y = element_text(angle = 0, hjust = 0, size = rel(1.5)),
+          strip.background = element_rect(fill = NA)) +
+    ggtitle("Tor Messenger downloads and updates") +
+    labs(caption = copyright_notice)
+  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
+}
+
+prepare_relays_ipv6 <- function(start_p = NULL, end_p = NULL) {
+  read_csv(file = paste(stats_dir, "ipv6servers.csv", sep = ""),
+      col_types = cols(
+        valid_after_date = col_date(format = ""),
+        server = col_factor(levels = NULL),
+        guard_relay = col_skip(),
+        exit_relay = col_skip(),
+        announced_ipv6 = col_logical(),
+        exiting_ipv6_relay = col_logical(),
+        reachable_ipv6_relay = col_logical(),
+        server_count_sum_avg = col_double(),
+        advertised_bandwidth_bytes_sum_avg = col_skip())) %>%
+    filter(if (!is.null(start_p))
+        valid_after_date >= as.Date(start_p) else TRUE) %>%
+    filter(if (!is.null(end_p))
+        valid_after_date <= as.Date(end_p) else TRUE) %>%
+    filter(server == "relay") %>%
+    group_by(valid_after_date) %>%
+    summarize(total = sum(server_count_sum_avg),
+      announced = sum(server_count_sum_avg[announced_ipv6]),
+      reachable = sum(server_count_sum_avg[reachable_ipv6_relay]),
+      exiting = sum(server_count_sum_avg[exiting_ipv6_relay])) %>%
+    rename(date = valid_after_date)
+}
+
+plot_relays_ipv6 <- function(start_p, end_p, path_p) {
+  prepare_relays_ipv6(start_p, end_p) %>%
+    complete(date = full_seq(date, period = 1)) %>%
+    gather(category, count, -date) %>%
+    ggplot(aes(x = date, y = count, colour = category)) +
+    geom_line() +
+    scale_x_date(name = "", breaks = custom_breaks,
+      labels = custom_labels, minor_breaks = custom_minor_breaks) +
+    scale_y_continuous(name = "", labels = formatter, limits = c(0, NA)) +
+    scale_colour_hue(name = "", h.start = 90,
+      breaks = c("total", "announced", "reachable", "exiting"),
+      labels = c("Total (IPv4) OR", "IPv6 announced OR", "IPv6 reachable OR",
+        "IPv6 exiting")) +
+    ggtitle("Relays by IP version") +
+    labs(caption = copyright_notice) +
+    theme(legend.position = "top")
+  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
+}
+
+prepare_bridges_ipv6 <- function(start_p = NULL, end_p = NULL) {
+  read_csv(file = paste(stats_dir, "ipv6servers.csv", sep = ""),
+      col_types = cols(
+        valid_after_date = col_date(format = ""),
+        server = col_factor(levels = NULL),
+        guard_relay = col_skip(),
+        exit_relay = col_skip(),
+        announced_ipv6 = col_logical(),
+        exiting_ipv6_relay = col_skip(),
+        reachable_ipv6_relay = col_skip(),
+        server_count_sum_avg = col_double(),
+        advertised_bandwidth_bytes_sum_avg = col_skip())) %>%
+    filter(if (!is.null(start_p))
+        valid_after_date >= as.Date(start_p) else TRUE) %>%
+    filter(if (!is.null(end_p))
+        valid_after_date <= as.Date(end_p) else TRUE) %>%
+    filter(server == "bridge") %>%
+    group_by(valid_after_date) %>%
+    summarize(total = sum(server_count_sum_avg),
+      announced = sum(server_count_sum_avg[announced_ipv6])) %>%
+    rename(date = valid_after_date)
+}
+
+plot_bridges_ipv6 <- function(start_p, end_p, path_p) {
+  prepare_bridges_ipv6(start_p, end_p) %>%
+    complete(date = full_seq(date, period = 1)) %>%
+    gather(category, count, -date) %>%
+    ggplot(aes(x = date, y = count, colour = category)) +
+    geom_line() +
+    scale_x_date(name = "", breaks = custom_breaks,
+      labels = custom_labels, minor_breaks = custom_minor_breaks) +
+    scale_y_continuous(name = "", labels = formatter, limits = c(0, NA)) +
+    scale_colour_hue(name = "", h.start = 90,
+      breaks = c("total", "announced"),
+      labels = c("Total (IPv4) OR", "IPv6 announced OR")) +
+    ggtitle("Bridges by IP version") +
+    labs(caption = copyright_notice) +
+    theme(legend.position = "top")
+  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
+}
+
+prepare_advbw_ipv6 <- function(start_p = NULL, end_p = NULL) {
+  read_csv(file = paste(stats_dir, "ipv6servers.csv", sep = ""),
+      col_types = cols(
+        valid_after_date = col_date(format = ""),
+        server = col_factor(levels = NULL),
+        guard_relay = col_logical(),
+        exit_relay = col_logical(),
+        announced_ipv6 = col_logical(),
+        exiting_ipv6_relay = col_logical(),
+        reachable_ipv6_relay = col_logical(),
+        server_count_sum_avg = col_skip(),
+        advertised_bandwidth_bytes_sum_avg = col_double())) %>%
+    filter(if (!is.null(start_p))
+        valid_after_date >= as.Date(start_p) else TRUE) %>%
+    filter(if (!is.null(end_p))
+        valid_after_date <= as.Date(end_p) else TRUE) %>%
+    filter(server == "relay") %>%
+    mutate(advertised_bandwidth_bytes_sum_avg =
+        advertised_bandwidth_bytes_sum_avg * 8 / 1e9) %>%
+    group_by(valid_after_date) %>%
+    summarize(total = sum(advertised_bandwidth_bytes_sum_avg),
+      total_guard = sum(advertised_bandwidth_bytes_sum_avg[guard_relay]),
+      total_exit = sum(advertised_bandwidth_bytes_sum_avg[exit_relay]),
+      reachable_guard = sum(advertised_bandwidth_bytes_sum_avg[
+        reachable_ipv6_relay & guard_relay]),
+      reachable_exit = sum(advertised_bandwidth_bytes_sum_avg[
+        reachable_ipv6_relay & exit_relay]),
+      exiting = sum(advertised_bandwidth_bytes_sum_avg[
+        exiting_ipv6_relay])) %>%
+    rename(date = valid_after_date)
+}
+
+plot_advbw_ipv6 <- function(start_p, end_p, path_p) {
+  prepare_advbw_ipv6(start_p, end_p) %>%
+    complete(date = full_seq(date, period = 1)) %>%
+    gather(category, advbw, -date) %>%
+    ggplot(aes(x = date, y = advbw, colour = category)) +
+    geom_line() +
+    scale_x_date(name = "", breaks = custom_breaks,
+      labels = custom_labels, minor_breaks = custom_minor_breaks) +
+    scale_y_continuous(name = "", labels = unit_format(unit = "Gbit/s"),
+      limits = c(0, NA)) +
+    scale_colour_hue(name = "", h.start = 90,
+      breaks = c("total", "total_guard", "total_exit", "reachable_guard",
+        "reachable_exit", "exiting"),
+      labels = c("Total (IPv4) OR", "Guard total (IPv4)", "Exit total (IPv4)",
+        "Reachable guard IPv6 OR", "Reachable exit IPv6 OR", "IPv6 exiting")) +
+    ggtitle("Advertised bandwidth by IP version") +
+    labs(caption = copyright_notice) +
+    theme(legend.position = "top") +
+    guides(colour = guide_legend(nrow = 2, byrow = TRUE))
+  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
+}
+
+prepare_totalcw <- function(start_p = NULL, end_p = NULL) {
+  read_csv(file = paste(stats_dir, "totalcw.csv", sep = ""),
+      col_types = cols(
+        valid_after_date = col_date(format = ""),
+        nickname = col_character(),
+        have_guard_flag = col_logical(),
+        have_exit_flag = col_logical(),
+        measured_sum_avg = col_double())) %>%
+    filter(if (!is.null(start_p))
+        valid_after_date >= as.Date(start_p) else TRUE) %>%
+    filter(if (!is.null(end_p))
+        valid_after_date <= as.Date(end_p) else TRUE) %>%
+    group_by(valid_after_date, nickname) %>%
+    summarize(measured_sum_avg = sum(measured_sum_avg)) %>%
+    rename(date = valid_after_date, totalcw = measured_sum_avg) %>%
+    arrange(date, nickname)
+}
+
+plot_totalcw <- function(start_p, end_p, path_p) {
+  prepare_totalcw(start_p, end_p) %>%
+    mutate(nickname = ifelse(is.na(nickname), "consensus", nickname)) %>%
+    mutate(nickname = factor(nickname,
+      levels = c("consensus", unique(nickname[nickname != "consensus"])))) %>%
+    ungroup() %>%
+    complete(date = full_seq(date, period = 1), nesting(nickname)) %>%
+    ggplot(aes(x = date, y = totalcw, colour = nickname)) +
+    geom_line(na.rm = TRUE) +
+    scale_x_date(name = "", breaks = custom_breaks,
+      labels = custom_labels, minor_breaks = custom_minor_breaks) +
+    scale_y_continuous(name = "", labels = formatter, limits = c(0, NA)) +
+    scale_colour_hue(name = "") +
+    ggtitle("Total consensus weights across bandwidth authorities") +
+    labs(caption = copyright_notice)
+  ggsave(filename = path_p, width = 8, height = 5, dpi = 150)
+}
+
+countrynames <- function(countries) {
+  sapply(countries, countryname)
+}
+
+write_userstats <- function(start, end, node, path) {
+  end <- min(end, as.character(Sys.Date()))
+  c <- read.csv(paste("/srv/metrics.torproject.org/metrics/shared/stats/",
+                "clients.csv", sep = ""), stringsAsFactors = FALSE)
+  c <- c[c$date >= start & c$date <= end & c$country != '' &
+         c$transport == '' & c$version == '' & c$node == node, ]
+  u <- data.frame(country = c$country, users = c$clients,
+                  stringsAsFactors = FALSE)
+  u <- u[!is.na(u$users), ]
+  u <- aggregate(list(users = u$users), by = list(country = u$country),
+                 mean)
+  total <- sum(u$users)
+  u <- u[!(u$country %in% c("zy", "??", "a1", "a2", "o1", "ap", "eu")), ]
+  u <- u[order(u$users, decreasing = TRUE), ]
+  u <- u[1:10, ]
+  u <- data.frame(
+    cc = as.character(u$country),
+    country = sub('the ', '', countrynames(as.character(u$country))),
+    abs = round(u$users),
+    rel = sprintf("%.2f", round(100 * u$users / total, 2)))
+  write.csv(u, path, quote = FALSE, row.names = FALSE)
+}
+
+write_userstats_relay <- function(start, end, path) {
+  write_userstats(start, end, 'relay', path)
+}
+
+write_userstats_bridge <- function(start, end, path) {
+  write_userstats(start, end, 'bridge', path)
+}
+
+write_userstats_censorship_events <- function(start, end, path) {
+  end <- min(end, as.character(Sys.Date()))
+  c <- read.csv(paste("/srv/metrics.torproject.org/metrics/shared/stats/",
+                "clients.csv", sep = ""), stringsAsFactors = FALSE)
+  c <- c[c$date >= start & c$date <= end & c$country != '' &
+         c$transport == '' & c$version == '' & c$node == 'relay', ]
+  r <- data.frame(date = c$date, country = c$country,
+                  upturn = ifelse(!is.na(c$upper) &
+                                  c$clients > c$upper, 1, 0),
+                  downturn = ifelse(!is.na(c$lower) &
+                                    c$clients < c$lower, 1, 0))
+  r <- aggregate(r[, c("upturn", "downturn")],
+    by = list(country = r$country), sum)
+  r <- r[(r$country %in% names(countrylist)), ]
+  r <- r[order(r$downturn, r$upturn, decreasing = TRUE), ]
+  r <- r[1:10, ]
+  r <- data.frame(cc = r$country,
+    country = sub('the ', '', countrynames(as.character(r$country))),
+    downturns = r$downturn,
+    upturns = r$upturn)
+  write.csv(r, path, quote = FALSE, row.names = FALSE)
+}
 
diff --git a/src/main/R/rserver/tables.R b/src/main/R/rserver/tables.R
deleted file mode 100644
index 28bd3d5..0000000
--- a/src/main/R/rserver/tables.R
+++ /dev/null
@@ -1,58 +0,0 @@
-countrynames <- function(countries) {
-  sapply(countries, countryname)
-}
-
-write_userstats <- function(start, end, node, path) {
-  end <- min(end, as.character(Sys.Date()))
-  c <- read.csv(paste("/srv/metrics.torproject.org/metrics/shared/stats/",
-                "clients.csv", sep = ""), stringsAsFactors = FALSE)
-  c <- c[c$date >= start & c$date <= end & c$country != '' &
-         c$transport == '' & c$version == '' & c$node == node, ]
-  u <- data.frame(country = c$country, users = c$clients,
-                  stringsAsFactors = FALSE)
-  u <- u[!is.na(u$users), ]
-  u <- aggregate(list(users = u$users), by = list(country = u$country),
-                 mean)
-  total <- sum(u$users)
-  u <- u[!(u$country %in% c("zy", "??", "a1", "a2", "o1", "ap", "eu")), ]
-  u <- u[order(u$users, decreasing = TRUE), ]
-  u <- u[1:10, ]
-  u <- data.frame(
-    cc = as.character(u$country),
-    country = sub('the ', '', countrynames(as.character(u$country))),
-    abs = round(u$users),
-    rel = sprintf("%.2f", round(100 * u$users / total, 2)))
-  write.csv(u, path, quote = FALSE, row.names = FALSE)
-}
-
-write_userstats_relay <- function(start, end, path) {
-  write_userstats(start, end, 'relay', path)
-}
-
-write_userstats_bridge <- function(start, end, path) {
-  write_userstats(start, end, 'bridge', path)
-}
-
-write_userstats_censorship_events <- function(start, end, path) {
-  end <- min(end, as.character(Sys.Date()))
-  c <- read.csv(paste("/srv/metrics.torproject.org/metrics/shared/stats/",
-                "clients.csv", sep = ""), stringsAsFactors = FALSE)
-  c <- c[c$date >= start & c$date <= end & c$country != '' &
-         c$transport == '' & c$version == '' & c$node == 'relay', ]
-  r <- data.frame(date = c$date, country = c$country,
-                  upturn = ifelse(!is.na(c$upper) &
-                                  c$clients > c$upper, 1, 0),
-                  downturn = ifelse(!is.na(c$lower) &
-                                    c$clients < c$lower, 1, 0))
-  r <- aggregate(r[, c("upturn", "downturn")],
-    by = list(country = r$country), sum)
-  r <- r[(r$country %in% names(countrylist)), ]
-  r <- r[order(r$downturn, r$upturn, decreasing = TRUE), ]
-  r <- r[1:10, ]
-  r <- data.frame(cc = r$country,
-    country = sub('the ', '', countrynames(as.character(r$country))),
-    downturns = r$downturn,
-    upturns = r$upturn)
-  write.csv(r, path, quote = FALSE, row.names = FALSE)
-}
-



_______________________________________________
tor-commits mailing list
tor-commits@xxxxxxxxxxxxxxxxxxxx
https://lists.torproject.org/cgi-bin/mailman/listinfo/tor-commits