Skip to contents

The goal of tsspiral is to create spiral version of time series plots.

Installation

You can install the development version of tsspiral from GitHub with:

# install.packages("pak")
pak::pak("thiyangt/tsspiralplot")

Applications

Monthly data

library(tsspiral)
library(ggplot2)
library(viridis)
#> Loading required package: viridisLite

monthly_data <- data.frame(
  date = seq.Date(
    as.Date("2020-01-01"),
    as.Date("2024-12-01"),
    by = "month"
  )
)

monthly_data$value <- rnorm(
  nrow(monthly_data),
  mean = 100,
  sd = 20
)

ggplot(
  monthly_data,
  aes(
    x = date,
    y = value,
    fill = value
  )
) +
  geom_tsspiral(
    ring_spacing = 2
  ) +
  scale_fill_viridis_c() +
  theme_void()


ggplot(
  monthly_data,
  aes(
    x = date,
    y = value,
    fill = value
  )
) +
  geom_tsspiral(
    ring_spacing = 1
  ) +
  scale_fill_viridis_c() +
  theme_void()

monthly_data <- data.frame(
  date = seq.Date(
    as.Date("2020-02-01"),
    as.Date("2024-12-01"),
    by = "month"
  )
)

monthly_data$value <- rnorm(
  nrow(monthly_data),
  mean = 100,
  sd = 20
)

ggplot(
  monthly_data,
  aes(
    x = date,
    y = value,
    fill = value
  )
) +
  geom_tsspiral(
    ring_spacing = 2
  ) +
  scale_fill_viridis_c() +
  theme_void()

Quarterly

quarterly_data <- data.frame(
  date = seq.Date(
    as.Date("2020-01-01"),
    as.Date("2025-10-01"),
    by = "quarter"
  )
)

quarterly_data$value <- rnorm(
  nrow(quarterly_data),
  100,
  20
)

ggplot(
  quarterly_data,
  aes(
    x = date,
    y = value,
    fill = value
  )
) +
  geom_tsspiral(
    ring_spacing = 2
  ) +
  scale_fill_viridis_c() +
  theme_void()

Weekly

weekly_data <- data.frame(
  date = seq.Date(
    as.Date("2023-01-01"),
    as.Date("2025-12-28"),
    by = "week"
  )
)

weekly_data$value <- rnorm(
  nrow(weekly_data),
  100,
  20
)

ggplot(
  weekly_data,
  aes(
    x = date,
    y = value,
    fill = value
  )
) +
  geom_tsspiral(
    ring_spacing = 2
  ) +
  scale_fill_viridis_c() +
  theme_void()

Daily

set.seed(123)

dat <- data.frame(
  date = seq.Date(
   as.Date("2020-01-01"),
   as.Date("2023-12-31"),
    by = "day"
   )
 )

dat$value <- 100 +
 20 * sin(
    2 * pi *
       lubridate::yday(dat$date) / 365
   ) +
  rnorm(
     nrow(dat),
     sd = 5
   )

 ggplot(
  dat,
  aes(
    x = date,
    y = value,
   fill = value
  ) ) +
  geom_tsspiral(
    ring_spacing = 2
   ) +
   scale_fill_viridis_c() +
   theme_void()

Spiral Barchart

set.seed(123)

dat <- data.frame(
  date = seq.Date(
    as.Date("2020-01-01"),
    as.Date("2023-12-31"),
    by = "day"
  )
)

doy <- lubridate::yday(dat$date)

dat$value <-
  20 +
  15 * sin(2 * pi * doy / 365) +
  35 * exp(-((doy - 100) / 20)^2) +
  25 * exp(-((doy - 280) / 30)^2) +
  rnorm(nrow(dat), 0, 3)

ggplot(
  dat,
  aes(
    x = date,
    y = value,
    fill = value
  )
) +
  geom_tsspiralbar(
    ring_spacing = 2
  ) +
  scale_fill_viridis_c() +
  theme_void()

library(ggplot2)

set.seed(123)

dat <- data.frame(
  date = seq.Date(
    as.Date("2020-01-01"),
    as.Date("2023-12-31"),
    by = "day"
  )
)

dat$value <- 100 +
  30 * sin(
    2 * pi * lubridate::yday(dat$date) / 365
  ) +
  rnorm(
    nrow(dat),
    sd = 8
  )

ggplot(
  dat,
  aes(
    x = date,
    y = value,
    fill = value
  )
) +
  geom_tsspiralbar(
    ring_spacing = 2
  ) +
  scale_fill_viridis_c() +
  theme_void()

Missing Value Display Plot


library(ggplot2)

set.seed(123)

# Generate a daily time series
spiral_dat <- data.frame(
  date = seq.Date(
    from = as.Date("2020-01-01"),
    to = as.Date("2023-12-31"),
    by = "day"
  )
)

# Generate a seasonal time series
spiral_dat$value <-
  100 +
  25 * sin(
    2 * pi * lubridate::yday(spiral_dat$date) / 365
  ) +
  rnorm(
    nrow(spiral_dat),
    sd = 5
  )

# Create missing periods
missing_dates <- c(
  seq.Date(
    as.Date("2020-03-01"),
    as.Date("2020-03-20"),
    by = "day"
  ),
  seq.Date(
    as.Date("2021-07-10"),
    as.Date("2021-08-05"),
    by = "day"
  ),
  seq.Date(
    as.Date("2023-01-15"),
    as.Date("2023-02-10"),
    by = "day"
  )
)

# Remove missing observations
spiral_dat <- spiral_dat[
  !spiral_dat$date %in% missing_dates,
]

# Plot missingness
ggplot(
  spiral_dat,
  aes(x = date)
) +
  geom_tsspiralmiss(
    ring_spacing = 2,
    observed_colour = "grey85",
    missing_colour = "black"
  ) +
  theme_void()

Calendar plot for hourly time series

# =========================================================
# Example: Two months of hourly data
# =========================================================

set.seed(123)

hourly_data <- tibble::tibble(
  datetime = seq(
    from = as.POSIXct("2026-01-01 00:00"),
    to   = as.POSIXct("2026-02-28 23:00"),
    by   = "hour"
  )
) |>
  dplyr::mutate(
    hour = lubridate::hour(datetime),
    day = lubridate::day(datetime),

    temperature =
      25 +
      4 * sin(2 * pi * hour / 24) +
      0.05 * day +
      stats::rnorm(
        length(datetime),
        0,
        0.8
      )
  )
# January
calendar_hourly_polar(
  hourly_data,
  datetime,
  temperature,
  month = "2026-01-01"
)

# February
calendar_hourly_polar(
  hourly_data,
  datetime,
  temperature,
  month = "2026-02-01"
)

Temporal Hierachy

set.seed(123)

dat <- data.frame(
  date = seq.Date(
    from = as.Date("2020-01-01"),
    to = as.Date("2023-12-31"),
    by = "day" ))

dat$value <- 20 +
  5 * sin(
    2 * pi * lubridate::yday(dat$date) / 365.25
  ) +
   rnorm(nrow(dat), 0, 1)

temporal_hierarchical(
 data = dat,
 date = date,
 value = value)