Skip to content

Instantly share code, notes, and snippets.

@tukachev
Created April 4, 2026 08:03
Show Gist options
  • Select an option

  • Save tukachev/55ec883056a4ef03c743df53aa010695 to your computer and use it in GitHub Desktop.

Select an option

Save tukachev/55ec883056a4ef03c743df53aa010695 to your computer and use it in GitHub Desktop.
library(tidyverse)
library(lubridate)
library(ggtext)
library(glue)
URL <- "https://api.ooni.org/api/v1/aggregation?probe_cc=RU&since=2026-02-26&until=2026-04-03&time_grain=day&axis_x=measurement_start_day&test_name=telegram&format=CSV"
date_split <- ymd("2026-03-14")
status_levels <- c(
ok_count = "OK (Доступен)",
anomaly_count = "Anomaly (Аномалии)"
# , confirmed_count = "Confirmed (Подтверждено)"
)
status_colors <- c(
"OK (Доступен)" = "#4C78A8",
"Anomaly (Аномалии)" = "#D55E00",
"Confirmed (Подтверждено)" = "#CC79A7"
)
# Загрузка данных
ooni_data <- read_csv(URL, show_col_types = FALSE) %>%
mutate(measurement_start_day = ymd(measurement_start_day)) %>%
arrange(measurement_start_day)
ooni_data <- ooni_data %>%
mutate(
total_tests = ok_count + anomaly_count + confirmed_count,
anomaly_share = anomaly_count / total_tests
)
interval_stats <- ooni_data %>%
mutate(
period = case_when(
measurement_start_day < ymd("2026-03-14") ~ "До 14 марта",
measurement_start_day <= ymd("2026-03-19") ~ "14–19 марта",
TRUE ~ "20 мар–02 апр"
)
) %>%
mutate(period = factor(period,
levels = c("До 14 марта",
"14–19 марта",
"20 мар–02 апр"))) %>%
group_by(period) %>%
summarise(
mean_share = round(mean(anomaly_share, na.rm = TRUE), 2),
.groups = "drop"
) %>%
mutate(
mean_share_pct = scales::percent(mean_share, accuracy = 1)
)
interval_stats
vals <- interval_stats$mean_share_pct
subtitle_text <- glue::glue("
Доля <span style='color:#D55E00'><b>аномалий</b></span> в тестах проверки <span style='color:#4C78A8'><b>доступности</b></span> выросла<br>с ~{vals[1]}
до ~{vals[2]} в середине марта и до <b>~{vals[3]}</b> после <b>20 марта</b>.<br>
Рост числа аномалий указываeт на потенциальную блокировку."
)
caption <- "
Источник данных: OONI Explorer | 26.02 – 02.04.2026\n
Визуализация: Юрий Тукачев, апрель 2026 @weekly_charts"
ooni_long <- ooni_data %>%
pivot_longer(
cols = all_of(names(status_levels)),
names_to = "status",
values_to = "count"
) %>%
mutate(
status = recode(status, !!!status_levels),
status = factor(status, levels = unname(status_levels))
) %>%
filter(count > 0)
present_statuses <- levels(droplevels(ooni_long$status))
# Даты
dates <- as.Date(range(ooni_data$measurement_start_day))
date_split <- as.Date(ymd("2026-03-14"))
breaks_seq <- seq(dates[1], dates[2], by = "4 days")
breaks <- as.Date(c(breaks_seq, dates[1], date_split, dates[2]))
breaks <- sort(unique(breaks))
#метки дат
labels <- map_chr(breaks, \(d) {
label <- if (d == dates[1] || d == dates[2] || d == date_split) {
format(d, "%d<br>%b")
} else {
format(d, "%d")
}
if (d %in% c(dates[1], date_split, dates[2])) {
paste0("**", label, "**")
} else {
label
}
})
p <- ggplot(ooni_long, aes(measurement_start_day, count, fill = status)) +
geom_col(
width = 0.95,
color = "white",
linewidth = 0.20
) +
scale_fill_manual(
values = status_colors[present_statuses],
drop = TRUE
) +
scale_x_date(
breaks = breaks,
labels = labels,
expand = expansion(mult = c(0.01, 0.01)),
guide = guide_axis()
) +
scale_y_continuous(
breaks = seq(0, 1750, by = 250),
labels = scales::label_number(big.mark = " "),
expand = c(0, 0)
) +
annotate(
"text",
x = dates[1],
y = 1750,
label = "тестов в день",
hjust = 0.15,
vjust = 0.5,
size = 6,
color = "#34495e",
family = "PT Sans"
) +
coord_cartesian(ylim = c(0, 1750), clip = "off") +
labs(
title = "**Аномалии доступа к Telegram в России резко<br>участились после 14 марта**",
subtitle = subtitle_text,
y = NULL,
x = NULL,
caption = caption
) +
theme_minimal(base_family = "PT Sans", base_size = 16) +
theme(
plot.title.position = "plot",
plot.caption.position = "plot",
plot.title = element_markdown(family = "PT Sans", size = 28,
face = "bold", lineheight = 1.1,
margin = margin(b = 12)),
plot.subtitle = element_markdown(family = "PT Sans", size = 22,
color = "#2c3e50", lineheight = 1.1,
margin = margin(b = 35)),
plot.caption = element_text(family = "PT Sans", size = 14,
color = "#7f8c8d", lineheight = 0.7,
margin = margin(t = 25)),
axis.text.x.bottom = element_markdown(
size = 16,
color = "#34495e",
lineheight = 0.9
),
axis.text.y = element_text(family = "PT Sans", size = 16, color = "#34495e"),
legend.position = "none",
panel.grid = element_blank(),
plot.margin = margin(25, 25, 25, 25)
)
p
ggsave(
"ooni_telegram_ru.png",
p,
width = 9,
height = 9,
dpi = 300,
bg = "white"
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment