Analyse and Visualise Headings and Trajectories on the Unit Disk

A complete pipeline for analysing directional data -- headings and trajectories on the unit disk. Supply a table of angles directly, or reconstruct headings from movement trajectories imported from tracking tools ('EthoVision', 'DeepLabCut', 'SLEAP', 'TRex', 'ANY-maze', 'TrackMate', 'idtracker.ai', and others) via a unified dialect-based loader system, including multi-bodypart pose estimation with likelihood-weighted centroid. Derives per-trial heading directions using multiple rules (ring-crossing, distal point, pose body-axis, velocity-based, and more). Computes circular statistics including mean direction, resultant length, von Mises and wrapped Cauchy parametric fits, circular correlation (circular-linear and circular-circular), circular-linear regression (Fisher and Lee (1992) ), model selection among orientation models (Fitak and Johnsen (2017) ), and uniformity and multi-sample hypothesis tests (Rayleigh, Watson-Williams, Hermans-Rasson (Landler, Ruxton and Malkemper (2019) ), and others) with multiple-comparison correction. Path tortuosity follows Benhamou (2004) . Renders publication-quality 'ggplot2' radial plots with overlaid rose diagrams, parametric density curves, non-parametric kernel density estimates, and circular boxplots (Buttarazzi, Pandolfo and Porzio (2018) ). Includes a browser-based 'shiny' graphical interface requiring no R coding via launch_app().


radiatR radiatR website

R-CMD-check r-universe

An R package for analysing and visualising headings and trajectories on the unit disk — directional (angular) data of any kind. It covers the full pipeline: angle/trajectory input → coordinate transformation → circular statistics → ggplot2 visualisation. Bring a table of headings directly, or reconstruct them from movement trajectories, with built-in import for DeepLabCut, SLEAP, EthoVision, TrackMate and other tracking tools.

Key Features

  • Flexible loader framework — read exports from DeepLabCut, SLEAP, EthoVision, TrackMate, idtracker.ai and other tracking tools, via registered dialects; register a loader for any other format
  • Pose-estimation support — multi-bodypart centroid and body-axis heading from DeepLabCut, SLEAP and EthoVision body points
  • Heading rule registry — derive per-trial headings by any of 15+ built-in rules or register your own
  • Circular statistics — mean direction, resultant length, concentration, and grouped summaries via the circular package
  • ggplot2 visualisation — radiate() renders unit-circle plots of headings and trajectories with concentric guides, mean-direction arrows, tick marks, and faceting; composable with add_* layer helpers

Supported Tracking Tools

Dialect Tool
deeplabcut DeepLabCut predictions CSV, single- or multi-animal (single bodypart or likelihood-weighted centroid)
sleap SLEAP (analysis CSV; single node or score-weighted centroid)
idtrackerai_wide idtracker.ai
ethovision EthoVision XT text export (centre point, or named body points)
trackmate TrackMate (Fiji)
toxtrac ToxTrac
trex TRex (positional CSV; plain X/Y or #wcentroid/#centroid/#pcentroid variants)
anymaze ANY-maze CSV export (centre position)

Register additional formats with register_loader_dialect() or the declarative register_loader_format().

Heading Rules

derive_headings() dispatches to any of the built-in rules or a registered custom rule:

Rule Method
crossing Angle at ring-crossing event (two radii)
distal Angle at frame of maximum radial distance
exit Velocity direction at ring crossing
net Start-to-end vector
velocity_mean Circular mean of step directions (pauses left out; weight_by = "step_length" weights by step length)
vm_fit von Mises MLE of the mean step direction (same heading as velocity_mean)
ransac_straight RANSAC-fitted straight-segment direction
origin_mean Mean bearing of the positions from the origin (r_power weights by distance)
bodypart_axis Axis between two tracked keypoints (pose data)
ellipse_axis Pre-computed orientation angle column (e.g. a theta column)
… straight, window_net, goal_bias, pca_axis, maxspeed_window, entry, ring_tangent

Register custom rules with register_heading_rule().

Circular statistics

Beyond summaries, radiatR covers the common circular-statistics workflow — all returning tidy data frames, and most accepting axial = TRUE for bidirectional (mod-180°) data such as orientation or polarization axes.

  • Uniformity / modality tests — test_uniformity() runs the Rayleigh (default), Kuiper, Rao spacing, Watson, and Hermans–Rasson (Monte-Carlo, powerful against multimodal alternatives) tests.
  • Parametric fits — vonmises_fit() and wrappedcauchy_fit() (each with an axial mode), overlaid on plots with add_vonmises_density() / add_wrappedcauchy_density().
  • Model selection — circ_model_select() ranks uniform vs unimodal vs axial von Mises by AICc with Akaike weights, so the data indicate the modality.
  • Summaries & association — circ_summary() / circ_summarise(), circ_dispersion(), and circ_cor() (circular–linear and circular–circular).
hd <- derive_headings(cpunctatus, rule = "distal")

# Is the sample non-uniform, and how is it best described?
test_uniformity(hd, test = "hermans_rasson")   # omnibus; catches multimodality
circ_model_select(hd)                            # uniform / unimodal / axial by AICc

The graphical app surfaces these too: a selectable omnibus test and a model-selection readout alongside the summary table.

Circular regression

Model a heading on linear covariates (Fisher-Lee circular-linear regression):

# simulate a known predictor -> mean-heading effect, then recover it
cond <- data.frame(condition = "demo", n_trials = 150, ref_mean = 0,
                   concentration_base = 12, mean_slope = 0.6,
                   predictor_mean = 0, predictor_sd = 1)
s  <- simulate_tracks(conditions = cond, n_points = 8, seed = 1)
hd <- s[!duplicated(s$trial_id), c("predictor", "final_heading")]
names(hd)[2] <- "heading"

fit <- circ_regression(hd, heading ~ predictor)
summary(fit)            # tidy coefficient table (recovers the positive slope)
predict(fit, data.frame(predictor = c(-1, 0, 1)))

# draw the fitted sweep on the circular panel
radiate(headings_frame(hd, heading, units = "radians")) +
  add_circ_mean(fitted_directions(fit, at = seq(-2, 2, length.out = 7)),
                colour_col = "predictor")

Typical Workflow

library(radiatR)

# 1. Load tracking data (example: DeepLabCut with head + thorax bodyparts)
ts <- read_tracks(
  "my_tracks.csv",
  dialect      = "deeplabcut",
  dialect_args = list(bodypart = c("head", "thorax"))
)

# 2. Derive headings
hd <- derive_headings(ts, rule = "crossing",
                      circ0 = 0.3, circ1 = 0.6,
                      coords = "absolute")

# 3. Circular summary per condition
compute_circ_mean(hd, group_col = "condition")

# 4. Visualise
radiate(ts,
        group_col  = "trial_id",
        colour_col = "condition",
        facets     = "condition",
        show_arrow = TRUE) +
  add_heading_points(hd, colour_col = "condition")

Coordinates. radiatR does not calibrate your data. By default, (x, y) are used exactly as supplied, so headings, mean direction, resultant length, and other circular statistics are only as arena-accurate as your input coordinates. Correct lens distortion and any scaling to real-world units in your tracking pipeline (e.g. your tracker's own calibration, or OpenCV undistort) before importing. normalize_xy = TRUE is available when only trajectory shape matters (each trajectory is independently centred on its own bounding box and scaled to the unit circle) -- it is not a substitute for calibration and does not preserve bearing relative to a fixed arena origin. When a per-trial reference direction is available, the landmark-relative frame (rel_x/rel_y, coords = "relative" in derive_headings()) is the accurate option.

Label the circumference in domain units

The circumference can be labelled in domain units instead of degrees — handy for time-of-year or compass data. angle_labels selects a built-in scale ("cardinal", "hours", "months", "seconds") and aligns the tick count to it:

# Label the circle in months instead of degrees
radiate(ts, angle_labels = "months")

Colour tracks by position

track_colour = "sequence" shades each trajectory along its own length, so the direction of travel is visible at a glance — a per-track gradient from start (dark) to finish (bright), with a continuous "start → finish" colourbar:

# colour each track from start (dark) to finish (bright)
radiate(cpunctatus, show_tracks = TRUE, track_colour = "sequence")

Representing time

Attach a capture frame rate to a Tracks object and the time aspect of frame-indexed tracks can be reported in real seconds and shown on the plot. track_duration() gives the elapsed seconds per trajectory, and track_colour = "time" colours each path by elapsed time (POSIXct time works without a frame rate):

# attach a capture rate, then represent the time aspect
ts <- set_frame_rate(cpunctatus, fps = 30)
track_duration(ts)                                       # seconds per track
radiate(ts, show_tracks = TRUE, track_colour = "time")   # colour by elapsed time

A frame rate also lets track_speed() report trajectory speed in real units (radii per second):

# speed in radii per second once a frame rate is set
ts <- set_frame_rate(cpunctatus, fps = 30)
track_speed(ts)              # mean speed per track
track_speed(ts, stat = "max")
# colour each path by its instantaneous speed (needs a frame rate)
ts <- set_frame_rate(cpunctatus, fps = 30)
radiate(ts, show_tracks = TRUE, track_colour = "speed")

If you know a physical scale, calibrate distances so lengths and speeds report in real units. The scale is physical units per coordinate unit (e.g. mm per radius); unset, everything stays in coordinate units (radii).

# calibrate distance (optional): 50 mm per coordinate unit (radius)
ts <- set_distance_scale(set_frame_rate(cpunctatus, 30), 50, unit = "mm")
track_length(ts)            # path length per track, in mm
track_speed(ts)             # mean speed per track, in mm/s
# or from two measured landmarks:
# ts <- calibrate_distance(ts, coord_distance = 0.8, real_distance = 40, unit = "mm")
# velocity components and turning rate (need a frame rate; vx/vy use the distance scale)
ts <- set_frame_rate(cpunctatus, fps = 30)
head(velocity_vector(ts))                 # vx, vy per observation
head(angular_velocity(ts, units = "degrees"))   # turning rate, deg/s (CCW +)
# per-track summaries (need a frame rate; velocity uses the distance scale)
ts <- set_frame_rate(cpunctatus, fps = 30)
track_velocity(ts)                       # net velocity vector (vx, vy) per track
track_turning(ts, units = "degrees")     # typical turning rate (deg/s) per track
# non-circular kinematics: speed (or turning rate) along each track over time
ts <- set_frame_rate(cpunctatus, fps = 30)
plot_profile(ts, metric = "speed")
plot_profile(ts, metric = "turning", units = "degrees")

Circular boxplot

A Tukey-like boxplot for circular data (Buttarazzi, Pandolfo & Porzio, 2018): the box spans the central 50% around the circular median, whiskers reach a concentration-adjusted fence, and far-out values are marked individually. Position-based axial data is supported via axial = TRUE (drawn at both poles).

hd <- derive_headings(cpunctatus, rule = "crossing", circ0 = 0.2, circ1 = 0.4)
radiate(cpunctatus) + add_circular_boxplot(hd)

# the summary on its own (median, hinges, fences, far-out, fence multiplier)
circ_boxplot_stats(hd)$constant

Simulate Data

simulate_tracks() generates synthetic circular trajectories for testing pipelines and teaching:

# Three default conditions differing in concentration and tortuosity
ts <- simulate_tracks(seed = 42, output = "trajset")
radiate(ts, group_col = "trial_id", facets = "condition", show_arrow = TRUE)

# Custom conditions table
conds <- data.frame(
  condition          = c("low", "high"),
  n_trials           = 20L,
  concentration_base = c(1.5, 8),
  tortuosity_base    = c(0.12, 0.03)
)
ts2 <- simulate_tracks(conditions = conds, seed = 1, output = "trajset")

Each condition can also carry a known directional ground truth. Set a modality (uniform / unimodal / axial / multimodal, with n_modes) to control the distribution of per-track headings, and a track shape (directed or oscillatory) to control within-track geometry — oscillatory tracks move back and forth along an axis, the kind of data the position-based axial heading methods (pca_axis, ransac_straight) are built for:

# Oscillatory axial tracks: each moves back-and-forth along a ~0.6 rad axis
osc <- data.frame(condition = "axial_osc", n_trials = 40L, ref_mean = 0.6,
                  concentration_base = 50, track_shape = "oscillatory",
                  n_reversals = 4L)
ts <- simulate_tracks(conditions = osc, output = "trajset", seed = 1)

# the position-based axial methods recover the ~0.6 rad axis (read as an
# axial mean) at default settings:
ax <- derive_headings(ts, rule = "pca_axis")
circ_summarise(ax, "heading", axial = TRUE, units = "radians", stats = "mean_dir")

# a directional rule cannot: the back-and-forth directions cancel
net <- derive_headings(ts, rule = "net")
circ_summarise(net, "heading", units = "radians", stats = "resultant_R")

Installation

# From r-universe (pre-built binaries)
install.packages("radiatR",
                 repos = c("https://johnkirwan.r-universe.dev",
                           "https://cloud.r-project.org"))

# From GitHub
remotes::install_github("JohnKirwan/radiatR")

# From local clone
devtools::install_local(".")

The graphical app

A browser-based interface covers the common workflow — upload a tracking file, choose a heading method, and view circular plots and summary statistics — with no R coding required.

Try it live: https://019f950c-e3cd-3ba0-a8cb-2a8a76a14dd5.share.connect.posit.cloud/

There are two ways to reach it:

1. Run locally (works after installing the package):

radiatR::launch_app()

2. Hosted on Posit Connect Cloud (recommended for collaborators) — https://019f950c-e3cd-3ba0-a8cb-2a8a76a14dd5.share.connect.posit.cloud/. A real Shiny server runs R server-side, so the page loads like any normal web app. Connect Cloud is git-backed: it is linked to this GitHub repo and rebuilds inst/app on every push, resolving R package dependencies from the committed inst/app/manifest.json — there are no deploy secrets. Regenerate that manifest with the connect-manifest workflow (Actions → Run workflow) whenever the app's dependencies or pinned R version change, then merge the PR it opens. To host your own copy, connect your fork in the Connect Cloud dashboard (content directory inst/app, primary file app.R). For a one-off manual deploy to any Shiny server instead:

rsconnect::deployApp(system.file("app", package = "radiatR"))

Optional Dependencies

Package Used for
ggrepel Non-overlapping track labels
readr / data.table Faster CSV reading
jsonlite / yaml JSON and YAML loader format specs
arrow Parquet/Feather file support

Reference manual

It appears you don't have a PDF plugin for this browser. You can click here to download the reference manual.

install.packages("radiatR")

0.1.3 by John D. Kirwan, 4 hours ago


https://johnkirwan.github.io/radiatR/, https://github.com/JohnKirwan/radiatR


Report a bug at https://github.com/JohnKirwan/radiatR/issues


Browse source code at https://github.com/cran/radiatR


Authors: John D. Kirwan [aut, cre, cph] , A. & M.W. Mathis Labs [cph] (test fixture tests/testthat/fixtures/real/dlc_CollectedData_Pranav.csv , from 'DeepLabCut') , Talmo Pereira [cph] (example file inst/extdata/sleap_example.csv , from 'SLEAP') , The Trustees of Princeton University [cph] (example file inst/extdata/sleap_example.csv , from 'SLEAP')


Documentation:   PDF Manual  


GPL (>= 3) license


Imports rlang, ggplot2, tibble, vctrs, circular, methods, utils, stats

Suggests bpDir, dplyr, testthat, knitr, rmarkdown, ggrepel, arrow, data.table, jsonlite, readr, readxl, yaml, withr, writexl, shiny, bslib, pkgload, png, jpeg, svglite, shinytest2, chromote, CircMLE, vdiffr, sphunif


See at CRAN