From f52ab00652fa52405ac93928aab45354515c6bde Mon Sep 17 00:00:00 2001 From: Christoph Urlacher Date: Sat, 12 Sep 2026 13:41:14 +0200 Subject: [PATCH] Add wordcounts plot script --- Justfile | 4 ++ plot_wordcounts.R | 84 ++++++++++++++++++++++++++++++++++++ wordcounts.csv | 107 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 195 insertions(+) create mode 100644 plot_wordcounts.R create mode 100644 wordcounts.csv diff --git a/Justfile b/Justfile index 3e73fad..1600c94 100644 --- a/Justfile +++ b/Justfile @@ -16,3 +16,7 @@ format: clean-format: rm thesis.fmt thesis.log thesis.tex.bbl thesis.tex.blg + +# Plot wordcounts.svg with ticks every n days (default: 7). +plot-wordcounts days="7": + Rscript plot_wordcounts.R {{quote(days)}} diff --git a/plot_wordcounts.R b/plot_wordcounts.R new file mode 100644 index 0000000..99db2cc --- /dev/null +++ b/plot_wordcounts.R @@ -0,0 +1,84 @@ +#!/usr/bin/env Rscript +# Usage: Rscript plot_wordcounts.R [tick_days] [input.csv] [output.svg] +# By default, label every seven days, independently of the data points. + +library(ggplot2) + +args <- commandArgs(trailingOnly = TRUE) +if (length(args) > 3L) { + stop("Usage: Rscript plot_wordcounts.R [tick_days] [input.csv] [output.svg]") +} +tick_days <- if (length(args) >= 1L) { + as.numeric(args[1]) +} else { + 7 +} + +script_arg <- grep("^--file=", commandArgs(), value = TRUE)[1] +script_dir <- dirname(normalizePath(sub("^--file=", "", script_arg))) +input <- if (length(args) >= 2L) { + args[2] +} else { + file.path(script_dir, "wordcounts.csv") +} +output <- if (length(args) >= 3L) { + args[3] +} else { + file.path(script_dir, "wordcounts.svg") +} + +counts <- read.csv(input, colClasses = "character") + +timezone <- Sys.getenv("TZ", unset = "Europe/Berlin") +if (!nzchar(timezone)) { + timezone <- "Europe/Berlin" +} +normalized <- sub("([+-][0-9]{2}):([0-9]{2})$", "\\1\\2", counts$date) +counts$timestamp <- as.POSIXct( + normalized, + format = "%Y-%m-%dT%H:%M:%S%z", + tz = timezone +) +date_only <- grepl("^[0-9]{4}-[0-9]{2}-[0-9]{2}$", counts$date) +counts$timestamp[date_only] <- as.POSIXct( + counts$date[date_only], + format = "%Y-%m-%d", + tz = timezone +) +counts$wordcount <- as.numeric(counts$wordcount) +counts <- counts[order(counts$timestamp), ] + +# Fixed time intervals +start <- as.POSIXct( + format(min(counts$timestamp), "%Y-%m-%d", tz = timezone), + tz = timezone +) +interval <- tick_days * 86400 +span <- as.numeric(difftime(max(counts$timestamp), start, units = "secs")) +ticks <- start + seq(0, max(1, ceiling(span / interval))) * interval + +plot <- ggplot(counts, aes(x = timestamp, y = wordcount)) + + scale_x_datetime( + breaks = ticks, + limits = range(ticks), + date_labels = "%Y-%m-%d", + timezone = timezone, + minor_breaks = NULL + ) + + scale_y_continuous(labels = scales::label_comma()) + + labs( + title = "Thesis Word Count", + x = "Date", + y = "Words" + ) + + theme_minimal(base_size = 12) + + theme( + axis.text.x = element_text(angle = 45, hjust = 1), + panel.grid.minor = element_blank() + ) +if (nrow(counts) > 1L) { + plot <- plot + geom_line(linewidth = 0.5) +} + +ggsave(output, plot, width = 10, height = 5, dpi = 180, bg = "white") +message("Saved ", output) diff --git a/wordcounts.csv b/wordcounts.csv new file mode 100644 index 0000000..77d7353 --- /dev/null +++ b/wordcounts.csv @@ -0,0 +1,107 @@ +date,wordcount +2026-06-23T12:00:00+02:00,496 +2026-06-30T12:00:00+02:00,2210 +2026-07-01T10:00:00+02:00,3745 +2026-07-01T12:00:00+02:00,4587 +2026-07-02T12:00:00+02:00,5058 +2026-07-05T12:00:00+02:00,5783 +2026-07-07T12:00:00+02:00,5538 +2026-09-11T11:24:33+02:00,8603 +2026-09-11T11:26:13+02:00,8603 +2026-09-11T11:31:20+02:00,8596 +2026-09-11T11:42:48+02:00,8727 +2026-09-11T11:43:08+02:00,8727 +2026-09-11T11:47:47+02:00,8746 +2026-09-11T12:01:42+02:00,8812 +2026-09-11T12:03:40+02:00,8860 +2026-09-11T12:07:01+02:00,8887 +2026-09-11T12:07:14+02:00,8888 +2026-09-11T12:07:31+02:00,8884 +2026-09-11T12:08:51+02:00,8892 +2026-09-11T12:09:39+02:00,8894 +2026-09-11T12:11:27+02:00,8913 +2026-09-11T12:12:29+02:00,8924 +2026-09-11T12:13:22+02:00,8920 +2026-09-11T12:13:42+02:00,8927 +2026-09-11T12:14:31+02:00,8927 +2026-09-11T12:15:46+02:00,8946 +2026-09-11T12:20:47+02:00,9070 +2026-09-11T12:23:46+02:00,9099 +2026-09-11T12:24:02+02:00,9102 +2026-09-11T12:24:42+02:00,9109 +2026-09-11T12:26:15+02:00,9109 +2026-09-11T12:26:25+02:00,9109 +2026-09-11T12:26:38+02:00,9109 +2026-09-11T12:30:20+02:00,9137 +2026-09-11T12:30:35+02:00,9137 +2026-09-11T12:31:24+02:00,9115 +2026-09-11T12:34:33+02:00,9124 +2026-09-11T12:36:17+02:00,9129 +2026-09-11T12:36:44+02:00,9113 +2026-09-11T12:42:09+02:00,9167 +2026-09-11T12:42:35+02:00,9167 +2026-09-11T12:53:02+02:00,9235 +2026-09-11T12:53:46+02:00,9234 +2026-09-11T12:54:27+02:00,9235 +2026-09-11T12:55:42+02:00,9235 +2026-09-11T12:56:42+02:00,9250 +2026-09-11T12:58:59+02:00,9379 +2026-09-11T12:59:54+02:00,9389 +2026-09-11T13:00:27+02:00,9389 +2026-09-11T13:00:43+02:00,9389 +2026-09-11T13:04:39+02:00,9427 +2026-09-11T13:04:55+02:00,9485 +2026-09-11T13:06:04+02:00,9488 +2026-09-11T13:07:00+02:00,9496 +2026-09-11T13:09:11+02:00,9494 +2026-09-11T13:10:20+02:00,9504 +2026-09-11T13:10:37+02:00,9509 +2026-09-11T13:10:45+02:00,9508 +2026-09-11T13:12:12+02:00,9510 +2026-09-11T13:17:28+02:00,9639 +2026-09-11T13:20:11+02:00,9686 +2026-09-11T13:20:35+02:00,9686 +2026-09-11T13:30:23+02:00,9704 +2026-09-11T13:31:28+02:00,9713 +2026-09-11T13:34:02+02:00,9740 +2026-09-11T13:34:11+02:00,9738 +2026-09-11T13:36:51+02:00,9771 +2026-09-11T13:37:45+02:00,9768 +2026-09-11T13:39:55+02:00,9778 +2026-09-11T13:40:19+02:00,9779 +2026-09-11T13:40:55+02:00,9779 +2026-09-11T13:41:22+02:00,9779 +2026-09-11T13:44:40+02:00,9800 +2026-09-11T13:44:47+02:00,9801 +2026-09-11T13:45:09+02:00,9801 +2026-09-11T13:47:18+02:00,9801 +2026-09-11T13:47:55+02:00,9801 +2026-09-11T13:49:17+02:00,9769 +2026-09-11T13:50:04+02:00,9906 +2026-09-11T16:31:06+02:00,9952 +2026-09-11T16:32:21+02:00,9967 +2026-09-11T16:40:24+02:00,10123 +2026-09-11T16:40:56+02:00,10123 +2026-09-11T16:41:08+02:00,10127 +2026-09-11T16:43:54+02:00,10166 +2026-09-11T16:44:23+02:00,10170 +2026-09-11T16:44:54+02:00,10170 +2026-09-11T16:45:46+02:00,10170 +2026-09-11T16:46:07+02:00,10170 +2026-09-12T11:57:09+02:00,10170 +2026-09-12T12:05:20+02:00,10170 +2026-09-12T12:05:36+02:00,10169 +2026-09-12T13:09:05+02:00,10172 +2026-09-12T13:10:19+02:00,10188 +2026-09-12T13:10:53+02:00,10197 +2026-09-12T13:12:10+02:00,10201 +2026-09-12T13:14:00+02:00,10242 +2026-09-12T13:14:23+02:00,10228 +2026-09-12T13:15:51+02:00,10227 +2026-09-12T13:17:15+02:00,10230 +2026-09-12T13:18:49+02:00,10224 +2026-09-12T13:25:07+02:00,10225 +2026-09-12T13:26:35+02:00,10236 +2026-09-12T13:27:44+02:00,10236 +2026-09-12T13:28:26+02:00,10237 +2026-09-12T13:58:35+02:00,10237