ggcircular hex logo

ggcircular

ggcircular is a ggplot2 extension for circular, axial and directional data. It provides layers, scales, coordinate helpers, summaries, diagnostics and displays for circular-linear and circular-circular dependence.

The package is designed for exploratory graphics, teaching examples and reproducible statistical workflows involving directions, bearings, orientations, times of day, turn angles, circular-linear relationships and other periodic measurements.

Part of the research ecosystem

This repository is part of Aurélien Nicosia’s open research and teaching ecosystem in computational statistics, scientific R software, reproducible data science and statistical education.

Installation

CRAN

Install the CRAN version (currently 0.1.0):

install.packages("ggcircular")

GitHub release 0.2.0

Install version 0.2.0 from GitHub:

install.packages("remotes")
remotes::install_github("AurelienNicosiaULaval/ggcircular@v0.2.0")

Or clone with SSH and install locally:

git clone git@github.com:AurelienNicosiaULaval/ggcircular.git
cd ggcircular
R -q -e 'devtools::install(upgrade = "never")'

Quick Start

library(ggplot2)
library(dplyr)
library(ggcircular)
wind_directions |>
  filter(season == "winter") |>
  ggplot(aes(x = direction)) +
  geom_rose(aes(y = after_stat(density), fill = after_stat(density)), bins = 24, alpha = 0.78) +
  geom_circular_density(linewidth = 1.1, colour = "#123C4A") +
  geom_mean_direction(length = "resultant", colour = "#E4572E", linewidth = 1.1) +
  scale_x_circular_compass() +
  coord_circular(zero = "north", direction = "clockwise") +
  labs(fill = "density", title = "Winter wind directions") +
  theme_circular()

What It Does

Workflow Main helpers
Rose diagrams and circular histograms geom_rose(), stat_rose()
Circular density estimation geom_circular_density(), stat_circular_density()
Mean direction and concentration geom_mean_direction(), circular_summary(), estimate_kappa()
Circular confidence intervals and tests circular_mean_ci(), rayleigh_test(), watson_williams_test(), stat_circular_test()
Axial orientations modulo pi axial = TRUE in summaries and layers
Theoretical circular distributions stat_vonmises(), stat_wrapped_normal(), stat_uniform_circular()
Mixtures of von Mises components fit_vonmises_mixture(), stat_vonmises_mixture()
Movement and state-angle graphics mutate_directional_features(), geom_direction_arrow(), plot_state_angles()
Angular model diagnostics circular_residuals(), circular_model_diagnostics(), autoplot() methods
Spherical and posterior helpers spherical_summary(), as_circular_draws(), summarise_circular_draws()
Directional dependence on cylinders and tori stat_circular_topography(), geom_phase_loom(), stat_toroidal_topography(), stat_toroidal_ridge()

Design Principles

Conventions for Directions and Bearings

The default mathematical convention is zero = "east" with angles increasing counterclockwise. This matches the usual unit circle.

Compass bearings use zero = "north" with angles increasing clockwise. Use scale_x_circular_compass() together with coord_circular(zero = "north", direction = "clockwise") for bearing-like data such as wind direction or movement headings.

Axial data, such as unoriented lines, are different again: 0 and pi represent the same orientation. Use axial = TRUE in summaries and layers for these data.

Conditional Displays on Cylinders and Tori

All computational angles are in radians. The examples below use the mathematical convention, with zero at east and angles increasing counterclockwise. Smoothing controls the resolution of the conditional estimate: larger von Mises concentration values use more local angular neighbourhoods.

cyl <- simulate_cyl_diagnostic(n = 180, scenario = "multimodal", seed = 11)
tor <- simulate_tor_diagnostic(n = 200, scenario = "diagonal", seed = 12) |>
  mutate(
    theta_signed = angular_difference(theta, 0),
    phi_signed = angular_difference(phi, 0)
  )

stat_circular_topography() estimates the conditional density of a real response given an angle. geom_phase_loom() instead displays binned conditional masses.

ggplot(cyl, aes(theta, x)) +
  stat_circular_topography(n_theta = 48, n_x = 48, kappa = 14) +
  coord_circular() +
  scale_x_circular_radians() +
  theme_circular()
ggplot(cyl, aes(theta, x)) +
  geom_phase_loom(
    n_sectors = 18,
    n_x_bins = 10,
    mass_type = "conditional",
    min_mass = 0
  ) +
  coord_equal() +
  theme_void()

stat_toroidal_topography() estimates f(phi | theta), while stat_toroidal_ridge() extracts its first grid maximum and reports local concentration. The ridge also computes after_stat(ridge_ambiguous) from near-tied distinct local modes. A ridge is not a substitute for the full topography when multimodality is plausible.

ggplot(tor, aes(theta_signed, phi_signed)) +
  stat_toroidal_topography(
    n_theta = 48,
    n_phi = 48,
    kappa_theta = 14,
    kappa_phi = 14,
    conditional = TRUE
  ) +
  coord_equal()
ggplot(tor, aes(theta_signed, phi_signed)) +
  stat_toroidal_topography(
    n_theta = 48,
    n_phi = 48,
    conditional = TRUE,
    alpha = 0.65
  ) +
  stat_toroidal_ridge(
    n_theta = 48,
    n_phi = 48,
    tie_tolerance = 0.01,
    linewidth = 1
  ) +
  coord_equal()

Summaries

circular_summary() respects existing dplyr groups and returns mean direction, resultant length, circular variance, circular standard deviation and an estimated von Mises concentration parameter. estimate_kappa() is a descriptive piecewise approximation from the sample resultant length, not a full inferential fit.

wind_directions |>
  circular_summary(direction, season) |>
  mutate(
    mean_degrees = round(rad_to_deg(mean), 1),
    Rbar = round(Rbar, 3),
    kappa = round(kappa, 2)
  ) |>
  select(season, n, mean_degrees, Rbar, kappa)
#> # A tibble: 4 × 5
#>   season     n mean_degrees  Rbar kappa
#>   <chr>  <int>        <dbl> <dbl> <dbl>
#> 1 fall     131        310.  0.811  3
#> 2 spring   115        135.  0.802  2.89
#> 3 summer   138        223.  0.87   4.15
#> 4 winter   116         48.2 0.904  5.52

Axial Data

Axial observations identify opposite directions. For example, an orientation of 0 radians is equivalent to an orientation of pi radians. Use axial = TRUE to compute and display these data modulo pi.

ggplot(axial_orientations, aes(x = orientation, fill = group)) +
  geom_rose(bins = 18, axial = TRUE, alpha = 0.72) +
  geom_mean_direction(axial = TRUE, colour = "#123C4A", linewidth = 1) +
  scale_x_circular_degrees(limits = c(0, pi)) +
  coord_circular() +
  facet_wrap(~ group) +
  theme_circular()

Directional Movement

ggcircular includes helpers for bearings, turn angles and state-specific angular distributions.

animal_steps |>
  filter(!is.na(turn_angle)) |>
  ggplot(aes(x = turn_angle, fill = state)) +
  geom_rose(bins = 24, alpha = 0.72) +
  geom_mean_direction(colour = "#123C4A", linewidth = 1) +
  scale_x_circular_degrees(
    breaks = deg_to_rad(c(0, 90, 180, 270)),
    labels = c("0", "90", "180", "270")
  ) +
  coord_circular() +
  facet_wrap(~ state) +
  theme_circular()

Mixtures of von Mises Distributions

Finite mixtures are fitted with an expectation-maximization routine and can be drawn directly on top of empirical rose diagrams. These fits are descriptive and depend on initialization, so use seed, nstart and diagnostic output when the mixture is substantively important.

set.seed(2026)

fit_mix <- fit_vonmises_mixture(
  wind_directions$direction,
  k = 2,
  init = "spaced",
  nstart = 3,
  seed = 2026
)

ggplot(wind_directions, aes(x = direction)) +
  geom_rose(aes(y = after_stat(density)), bins = 24, alpha = 0.42) +
  stat_vonmises_mixture(fit = fit_mix, linewidth = 1.2, colour = "#123C4A") +
  scale_x_circular_degrees() +
  coord_circular() +
  theme_circular()
tidy_circular(fit_mix) |>
  mutate(
    mu_degrees = round(rad_to_deg(mu), 1),
    kappa = round(kappa, 2),
    proportion = round(proportion, 3)
  ) |>
  select(component, proportion, mu_degrees, kappa)
#> # A tibble: 2 × 4
#>   component proportion mu_degrees kappa
#>       <int>      <dbl>      <dbl> <dbl>
#> 1         1      0.328       51.6  1.45
#> 2         2      0.672      232.   0.77

Tests and Intervals

circular_mean_ci(
  wind_directions$direction,
  method = "bootstrap",
  R = 399,
  seed = 2026
) |>
  mutate(across(c(mean, lower, upper), rad_to_deg))
#> # A tibble: 1 × 7
#>    mean lower upper level method        n   Rbar
#>   <dbl> <dbl> <dbl> <dbl> <chr>     <int>  <dbl>
#> 1  235.  139.  354.  0.95 bootstrap   500 0.0494
rayleigh <- rayleigh_test(wind_directions$direction)

tibble::tibble(
  statistic = unname(rayleigh$statistic),
  n = unname(rayleigh$parameter),
  p_value = rayleigh$p.value,
  method = rayleigh$method
)
#> # A tibble: 1 × 4
#>   statistic     n p_value method
#>       <dbl> <int>   <dbl> <chr>
#> 1      1.22   500   0.295 Rayleigh test of circular uniformity

Optional Model Integrations

The package keeps heavier modeling ecosystems in Suggests. When available, these integrations add diagnostics without making them hard dependencies.

fit <- CircularRegression::consensus(direction ~ speed, data = wind_directions)

circular_model_diagnostics(fit)

autoplot(fit, type = "residuals_density")
autoplot(fit, type = "fitted_observed")

Optional helpers currently target:

Experimental Features

The following pieces are intentionally available but still experimental:

Experimental functions are documented and tested, but their return columns may still evolve as validation reveals better public contracts.

Statistical Limitations

ggcircular is primarily a visualization and diagnostics package. It does not replace specialist inference workflows for circular statistics.

CRAN Status

ggcircular is available from CRAN at https://CRAN.R-project.org/package=ggcircular. The CRAN page lists version 0.1.0, published 2026-06-04, source archives, binaries, vignettes and check results.

The development workflow continues to include:

Longer articles are built for pkgdown and excluded from the CRAN tarball.

Vignettes

Start with:

vignette("ggcircular", package = "ggcircular")

Then see the pkgdown articles:

Citation

To cite ggcircular from R, use:

citation("ggcircular")

Citation metadata is also provided in inst/CITATION.

Contributing and Support

Contributions are welcome through focused GitHub issues and pull requests. See CONTRIBUTING.md, SUPPORT.md and CODE_OF_CONDUCT.md for contribution, support and conduct guidelines.

Development Status

ggcircular is currently experimental. The public API is usable, tested and documented, but may still evolve as more angular model classes and validation cases are added.

Current checks:

References