
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.
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.
CircularRegression,
donnees-bleues
Install the CRAN version (currently 0.1.0):
install.packages("ggcircular")
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")'
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()

| 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()
|
2 * pi.pi through axial = TRUE.ggplot objects, tibbles or familiar test
objects.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.
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()

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 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()

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()

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
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
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:
CircularRegression-style angular, consensus and two-step objects
through S3 class support.momentuHMM state probabilities and Viterbi states.posterior draw objects through posterior::as_draws_df().circular tests when classical circular test implementations are
available.The following pieces are intentionally available but still experimental:
momentuHMM state-angle adapters;Experimental functions are documented and tested, but their return columns may still evolve as validation reveals better public contracts.
ggcircular is primarily a visualization and diagnostics package. It
does not replace specialist inference workflows for circular statistics.
circular_mean_ci() is unreliable when the mean resultant length is
close to zero because the mean direction is weakly identified.rayleigh_test() is mainly sensitive to unimodal departures from
uniformity.watson_williams_test() relies on strong assumptions about group
concentration and uses the optional circular implementation.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:
R CMD check --as-cran on the final source tarball;--run-donttest checks;_R_CHECK_FORCE_SUGGESTS_=false;Longer articles are built for pkgdown and excluded from the CRAN tarball.
Start with:
vignette("ggcircular", package = "ggcircular")
Then see the pkgdown articles:
To cite ggcircular from R, use:
citation("ggcircular")
Citation metadata is also provided in inst/CITATION.
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.
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:
devtools::test() passes.devtools::check(document = FALSE, args = "--as-cran", build_args = "--no-manual")
is used before release commits._R_CHECK_FORCE_SUGGESTS_=false and full-suggests checks when
optional packages are available.pkgdown builds and publishes the website from main.