TrajSet is renamed to Tracks, its constructor to
the lowercase tracks(), and the loader functions to snake_case:
TrajSet_read() -> read_tracks(), TrajSet_read_dir() -> read_tracks_dir(),
TrajSet_read_format() -> read_tracks_format(), TrajSet_load_manifest() ->
load_manifest(). No behaviour, slot, or output changed. Bundled cpunctatus
is now a Tracks object.headings_frame is now a tibble subclass whose class and display convention
survive dplyr verbs (mutate/filter/select/bind_rows) via the
dplyr_reconstruct contract, so the orientation no longer leaks. New exported
hf_display()/hf_heading_col()/hf_colour_col()/hf_coords() accessors read
the metadata (and work on a plain data frame). derive_headings() now returns
a headings_frame; orientation is consolidated into a single display
attribute (the vestigial display_convention/angle_convention strings are
removed). See vignette("design").New circ_regression() fits Fisher-Lee circular-linear regression of a heading
on linear covariates (formula interface over circular::lm.circular), with
summary() (tidy coefficients), predict()/fitted(), and print() methods.
simulate_tracks() gains a per-condition mean_slope so the predictor shifts
the mean heading, enabling end-to-end recovery.
New fitted_directions() turns a circ_regression() fit into mean-direction
arrows for add_circ_mean(), so the fitted heading-vs-covariate relationship
can be drawn on the circular panel, colour-graded by the predictor.
simulate_tracks() is now modality- and shape-aware. Per-condition modality
(uniform / unimodal / axial / multimodal with n_modes) sets the
distribution of per-track headings, and track_shape (directed or
oscillatory) sets within-track geometry — oscillatory produces
back-and-forth axial tracks whose line-width (line_width) is independent of
tortuosity, so the per-track axial methods recover the axis at default
settings. The ground truth is recorded in new output columns and in the
TrajSet meta$sim_conditions. The default output is unchanged.
track_speed() and step_speed() report trajectory speed in real units
(distance per second), using the track's frame rate / timestamps. With the
default unit-arena coordinates that is arena-units (radii) per second.
instantaneous_speed() gives per-observation speed (the per-row sibling of
elapsed_seconds()), and radiate(track_colour = "speed") colours each path
by it. Arena-units per second with the default coordinates.
velocity_vector() gives per-observation velocity components (vx, vy;
distance-calibrated when a scale is set), and angular_velocity() the signed
turning rate (counter-clockwise positive; radians or degrees per second).
Per-track summaries: track_velocity() (net average velocity vector,
distance-calibrated) and track_turning() (turning-rate summary -- mean_abs
magnitude by default, plus mean/max_abs/median_abs; radians or degrees).
Optional distance calibration: set_distance_scale() / calibrate_distance()
attach a physical scale + unit, so track_length(), track_speed(),
instantaneous_speed() and radiate(track_colour = "speed") report real units
(e.g. mm, mm/s). Unset, everything stays in arena/coordinate units.
American spellings are now accepted throughout: every colour... argument and
the assign_colour_* / cycle_colours / hf_colour_col functions have
color... aliases (British remains canonical; supplying both errors).
Tracks can carry a capture frame rate: set_frame_rate() / frame_rate()
store fps in the object, elapsed_seconds() and track_duration() report real
time, and radiate(track_colour = "time") colours each path by elapsed time
(POSIXct time works without a frame rate). simulate_tracks(frame_rate=) sets it.
radiate() gains track_colour = "sequence" to colour each trajectory path by
its point's position from start to finish (a per-track normalized gradient with
a continuous "start -> finish" colourbar). The Shiny app's Results figure gains
a matching Track colour selector.
New circ_boxplot_stats() and add_circular_boxplot() implement the
Tukey-like circular boxplot of Buttarazzi, Pandolfo & Porzio (2018) for
circular and axial heading data: a depth-ranked box around the circular
median, a closed-form von Mises fence multiplier, whiskers, and far-out
values. Validated against the bpDir reference implementation.
The Results step is reorganised into Circular plots and Summary & stats sub-tabs, each with its own options sidebar (the circular figure + its reproduce/export controls on one, the summary and model-selection tables on the other).
The Results figure's Track colour selector gains "By elapsed time", with a frame-rate (fps) input; an unset/invalid frame rate falls back to sequence colouring with a note.
The Results figure's Track colour selector gains "By speed" (instantaneous speed), sharing the frame-rate input with "By elapsed time"; an unset/invalid frame rate falls back to sequence colouring with a note.
The Results figure gains a Circular boxplot overlay toggle (directional and axial), wired through the figure-code export so the emitted script reproduces it. The layer toggles now reflow into two columns when space allows, and a note explains when the boxplot is not drawn (near-uniform data or too few points).
The circular-statistics core gains an axial = TRUE option for bidirectional
(mod-pi) data: circ_summary(), circ_summarise(), circ_dispersion(),
test_uniformity(), test_mean_directions(), test_concentration(),
compute_circ_mean(), and compute_circ_interval() compute the axial mean (an
axis in [0, 180)), axial resultant length, and the corresponding tests via the
standard angle-doubling method. The directional default (axial = FALSE) is
unchanged.
New circ_model_select() ranks three candidate circular models -- uniform,
unimodal von Mises, and axial (symmetric bimodal) von Mises -- by AICc, with
Akaike weights, to identify whether a heading sample is best described as
uniform, directionally, or axially oriented. Reuses vonmises_fit() and
circular's densities; no new dependency.
The app's statistical summary gains an always-on Rao spacing omnibus row
alongside Rayleigh, flagging departures from uniformity (multimodal/clustered
patterns) that the focused Rayleigh test misses. Rao spacing yields only a
coarse significance level, so it is shown as a bracket (< 0.05, > 0.10, …),
computed on raw angles regardless of the Data model.
test_uniformity() gains test = "hermans_rasson", the Hermans-Rasson omnibus
test of circular uniformity (Landler, Ruxton & Malkemper 2019) — far more
powerful than Rayleigh against multimodal / non-symmetric alternatives. Its
p-value is by Monte-Carlo simulation (n_sim, default 9999; seed via
set.seed()), and it honours group_col, p_adjust, and axial.
vonmises_fit(), wrappedcauchy_fit(), add_vonmises_density(), and
add_wrappedcauchy_density() gain axial = TRUE to fit and draw axial
(bidirectional, mod-pi) distributions via the doubled-angle method: the mean is
reported as an axis in [0, pi) (with the concentration estimated in the
doubled-angle frame) and the density curve shows two equal antipodal peaks.
The mean-direction and interval overlays gain axial = TRUE for bidirectional
data: add_circ_mean()/add_heading_arrow() draw a double-headed axis through
the centre, add_circ_interval()/add_heading_interval() draw the CI at both
poles, and add_critical_v_line() mirrors its boundary.
The empirical individual-data layers gain axial = TRUE to draw bidirectional
(period-pi) data at both ends: add_heading_points(), add_heading_vectors(),
and add_stacked_headings() plot each datum at both poles, and the empirical
densities (compute_circular_density() / add_heading_density(),
add_circular_kde(), add_angle_rose()) estimate on the both-ends-augmented
sample. In the Shiny app the headings-only "Axial" checkbox becomes a "Data
model" (Directional / Axial) selector in the Configure step that applies to
both input types, soft-syncs from inherently axial heading methods
(movement-axis / PCA / RANSAC), mirrors the whole figure, and relabels the
summary's Rayleigh test as "Rayleigh (axial)".
reference() reads
it and set_reference() changes it, re-deriving the relative frame
(rel_theta/rel_x/rel_y) consistently so it cannot drift. New exported
derive_coords() is the single source of the unit-circle -> polar/relative
math (also used internally by the loader mapping). For a reference-frame
offset, prefer set_reference() over a manual apply_transform().as.data.frame(ts) now returns the full frame even when derived coordinate
columns (rel_theta/rel_x/rel_y/radius/trans_rho/abs_theta) are not
stored, computing any missing ones from the canonical position and the
trajectory reference. Internal analyses read through it, so they no longer
depend on those columns being physically present.TrajSet objects built by the loader pipeline no longer store the redundant
derived coordinate columns (trans_rho/abs_theta/rel_x/rel_y), and no
construction path stores a rho/radius column; as.data.frame(ts) computes
them on demand from the canonical position and the trajectory reference. The
canonical trans_x/trans_y/raw_* and the analysis angle rel_theta are
kept. Storage of the derived frame is now an implementation detail; existing
data (e.g. the bundled cpunctatus) that still carries the columns behaves
identically.animal_track parameter of get_trial_limits(),
get_tracked_object_pos(), and get_all_object_pos() is renamed to track
(breaking). Residual animal-specific framing in the documentation is
generalised to "subject". Example references to the bundled millipede/urchin
datasets and the column-name guesses used by importers are unchanged.TrajSet_read() and TrajSet_read_dir() now auto-detect the field separator
(comma, semicolon, tab, or pipe) and decimal mark from file content rather
than the extension, so semicolon-separated and European decimal-comma exports,
and tab-delimited files saved as .csv, load correctly. A new read_opts
argument (delim, decimal, sheet) overrides detection when needed. Excel
workbooks (.xlsx/.xls) can be read directly (first sheet by default, or
read_opts$sheet), via the soft-dependency readxl. In the Shiny app the
first-rows preview now reflects the actual detected parse, and a new Delimiter
control lets you correct a misdetected separator in place.TrajSet_read() now loads single-track CSVs with custom column names: column
guessing is case-insensitive and matches separator-suffixed coordinates
(e.g. Track1_X/Track1_Y); a missing id column is treated as a single
trajectory and a missing time/frame column falls back to row order (each with
a message), and rows with non-finite coordinates are dropped. New exported
guess_columns() reports the guessed role of each column. The Shiny app shows
pre-filled X/Y/Time/ID dropdowns for Generic CSV uploads.load_tracks() and get_trial_limits() now carry every column from the
manifest / file_tbl onto trials (and into the resulting TrajSet), rather
than a fixed arc/type/obstacle/id list left over from one experiment.
Structural file-reference columns (basename/landmark/track) are excluded.
Use load_tracks2(colnames = ) to rename or restrict the columns carried.apply_transform() applies a user-supplied transformation to a loaded
TrajSet (per-trajectory or whole-frame), returning a modified TrajSet and
recording the step in its transform_history. Two worked recipes are
documented: an edge-referenced -> centre-referenced reference offset (a
per-trial offset read from stimulus-width metadata) and a polarization
direction -> axis (angle-doubling) remap.type == "Herm") reference-angle
adjustment and the now-unused midpoint argument from get_trial_limits();
experiment-specific reference corrections are now expressed via
apply_transform(). The bundled cpunctatus data is unchanged.TrajSet row-order validity check no longer requires a global sort by id
(which depended on string collation / locale); it now checks the actual
invariant -- each trajectory's rows form a contiguous, time-ordered block.velocity_axis heading rule: derive_headings(ts, rule = "velocity_axis")
reduces each trajectory to its movement axis (the axial mean of step
directions, in [0, 180)), the bidirectional counterpart of velocity_mean.
Pair it with the axial statistics and plots (axial = TRUE). Available in the
Shiny app's heading-method dropdown.derive_headings() gains an on_missing argument ("warn" (default),
"error", or "quiet") and now reports how many trials produced no heading.
Failed trials are always retained as NA rows, and the result carries
n_total, n_missing, and missing_ids attributes. Rule-based failures are
often non-random and can bias circular statistics, so they are surfaced by
default.circ_summarise() gains requestable n_total and n_missing stats so the
excluded-trial denominator can travel with the summary (default output
unchanged).The heading-method dropdown is now grouped into Directional and Axial methods, and a contextual note flags a mismatch with the Data model -- folding a directional heading to an axis (informational), or analysing an axial method as Directional (a warning, since that biases the statistics).
The Summary panel gains a selectable omnibus test (Rao spacing or
Hermans-Rasson) and a model-selection readout -- a "Best model" column plus
a full AICc comparison card (circ_model_select(): uniform / unimodal / axial
with Akaike weights) -- so the data, not just a declared model, indicates
whether a sample is uniform, directional, or axial.
The Results screen now shows an attrition banner when trials were excluded --
loud, with a bias caveat, for headings derived from trajectories, and a
neutral note for provided headings. The summary table gains an Excluded
column.
radiate.headings_frame() now draws the same theme-responsive radial chrome
as radiate() for trajectories (circumference, ticks, degree labels, radial
grid, origin) instead of a fixed bold circle, and gains show_markers,
colour_col, legend, display, grid, grid_colour, circumference, and
origin arguments. show_markers = FALSE returns the radial frame only, so
callers can layer their own marker and statistic overlays.void) is now
drawn at theme_classic's axis-line weight (0.5) rather than a bold 1.2, so it
reads like an axis rather than a heavy frame.radiate(theme = ), mapped from that theme's axis.line / axis.ticks /
axis.text. Picking a different theme restyles them (colour, line width, label
size and family), and colour is kept legible against a dark panel. add_ticks()
gains colour, linewidth, length, and n; degree_labs() gains size and
family; add_circ() gains linetype and colour/linewidth aliases.
(Styling follows the theme passed to radiate(), not a + theme() added
afterwards, since the chrome is drawn as layers.)radiate(circumference = )). A
consequence: on light grid themes (minimal, bw, light) the boundary is now
a subtle grid-coloured ring rather than a bold dark circle -- add a bold ring
back with + add_circ(colour = "black", linewidth = 1.2) if desired.radiate(origin = ) now defaults to FALSE (the centre point is opt-in on all
themes) and, when drawn, takes the theme's axis ink colour.step and start_sep defaults the
app uses).bw, grey, light, dark,
linedraw, minimal), radiate() now replaces the Cartesian grid behind the
unit circle with a theme-styled radial grid -- a circular disc in the
theme's panel colour, quadrant crosshairs and a ring at 0.5 (major), and 45
degree diagonals plus rings at 0.25/0.75 (minor). Control it with the new
grid = c("radial", "cartesian", "none") argument (default "radial");
grid = "cartesian" restores the previous square grid. grid_colour overrides
the derived colour.add_radial_grid() and add_origin_point() composable layers,
so the radial grid can be hand-built or restyled like any ggplot2 layer. The
existing quadrants/rings arguments still add a-la-carte guides when
grid != "radial".normalize_xy = TRUE (the TrajSet() and TrajSet_read() default) now
arena-scales each trajectory instead of collapsing every point onto the unit
circle. Previously it replaced each point with its unit vector (radius → 1),
destroying trajectory shape: radius- and displacement-based heading rules
(net, distal, crossing, exit, origin_mean, straight, ...) returned
wrong or NA headings, and uploaded data in the Shiny app was affected. Each
trajectory is now centred on its bounding-box midpoint and scaled so its
furthest point sits at radius 1, preserving shape and placing the arena centre
at the origin (what the radius-based rules expect). Raw coordinates are still
kept in <x>_raw/<y>_raw, and rho now holds the relative radius rather than
a constant 1. Landmark-based mapping, when available, remains the accurate path;
this only improves the no-landmark fallback. Stored x/y/rho values change
for normalize_xy = TRUE; pass normalize_xy = FALSE to keep raw coordinates.normalize_xy = FALSE), preserving legitimate excursions past the arena boundary. Track points
that fall outside the boundary (radius > 1) are now reported once per dataset --
"N points across M trials exceeded the arena boundary (radius > 1)" -- instead
of a separate warning per trial.read_calibration(),
cal_model(), cam_cal_pt(), cam_cal_many(), calibrate_positions(), and the
CalModel class), along with its vignette. radiatR normalises each trajectory to
a unit arena, so its outputs (headings, mean direction, resultant length, circular
statistics) are scale-invariant and never needed metric calibration; lens-distortion
correction and scaling to real-world units are better handled in the upstream
tracking pipeline (the tracker's own calibration, or OpenCV undistort) before
import. This completes the narrowing begun in 0.2.0, which removed the calibration
estimator. The R.matlab, yaml, and jsonlite suggested dependencies remain —
they are still used by the loaders.ggplot2 and radiatR (and, through
radiatR, circular/boot/mvtnorm) are attached lazily the first time the
user loads data, rather than at app startup. Under the WebAssembly (shinylive)
build, attaching those packages is the bulk of the in-browser R boot, so the
initial upload screen now paints on shiny + bslib alone and the heavier
load happens when a file or the example is chosen.quadrants / rings to radiate(), and the R
code export emits them, so every plot control round-trips through the export
again.A large feature release that broadens radiatR from a focused plotting toolkit into an end-to-end pipeline for circular-arena tracking: many more data sources, a full circular-statistics layer, richer distribution overlays, and a no-code graphical interface.
#wcentroid / #centroid /
#pcentroid variants, and (cm) / (px) unit annotations), ANY-maze,
Tracktor, Ctrax (.mat via R.matlab, with ellipse theta/a/b),
TrackMate, idtracker.ai, ToxTrac, BORIS, and dtrack.bodypart_axis heading rule.register_loader_dialect().inst/extdata/) for DeepLabCut, SLEAP, and
Tracktor so every dialect can be tried without external data.pose_to_headings() derives many per-frame headings from multi-point pose
data — suited to tethered-animal / gaze-direction designs.register_heading_rule(),
including bodypart_axis and ellipse_axis.derive_headings(..., return_coords = TRUE) now also works for distal, net,
straight, and pca_axis (previously only crossing), attaching each rule's
construction coordinates so the geometry behind a heading can be drawn.bin_angles() snaps angles to fixed-width circular bin centres. phase = 0
(default) centres bins on the reference direction; phase = width / 2
reproduces the edge-aligned bins of circular::plot.circular; arbitrary phases
allow e.g. quadrant binning. Intended as the precursor to a stacked dot plot
(bin_angles() then stack_headings()).stack_headings() and add_stacked_headings() gain a start_sep argument (the
analogue of circular::plot.circular's start.sep): a radial offset of the
first dot from the reference circle, so a stack can abut the periphery rather
than straddle it. The existing step argument (the analogue of sep) sets the
gap between dots. Both default to circular's behaviour at the package level.stack_headings() and add_stacked_headings() gain a group argument that
stacks dots within each group independently (e.g. one stacking per facet).path_straightness(x, y) and straightness_index(ts) compute the path
straightness index (net displacement / path length, 0–1) per trajectory.path_tortuosity(x, y) and tortuosity_ratio(ts) compute the classic
tortuosity ratio (path length / net displacement, ≥ 1), the reciprocal of the
straightness index.vonmises_fit() (with standard errors and a CI on the mean
direction) and wrappedcauchy_fit().circ_dispersion() (mean direction, resultant length, circular SD) and
sector_summary() (per-sector dwell proportions).circ_cor() — circular-linear and circular-circular correlation.p_adjust: test_uniformity() (Rayleigh / Kuiper / Rao /
Watson), test_mean_directions() (Watson-Williams, omnibus or pairwise), and
test_concentration().radiate() plot with +:
add_angle_rose(), add_vonmises_density(), add_wrappedcauchy_density(),
and add_circular_kde().add_critical_r() draws the Rayleigh / V-test critical
circle, and add_critical_v_line() draws the V-test decision boundary (a
straight line perpendicular to the hypothesised direction).add_critical_r() gains a colour_by_group argument (default TRUE). With
per_group = TRUE the per-panel circles are mapped to the group by default;
set colour_by_group = FALSE to draw them in a fixed colour while still
attaching the group column so the circles facet -- avoiding a clash with an
existing colour scale (e.g. the trajectory colours in the Shiny app).radiate() gains an arrow_colour_col argument: when set, the built-in mean
resultant arrow is drawn once per level of that column (within each panel, if
faceted) and coloured by it, so the arrow can follow a colour grouping
independently of faceting. The composable add_heading_arrow() and
add_heading_interval() already take colour_col for the same effect on the
mean-direction arrow and its confidence interval; the circular-statistics
vignette now shows this grouped-overlay pattern.cycle_colours() -- the order-stable primitive behind
assign_cycle_colours() and radiate()'s colour_cycle. It maps a key to a
cycled 1:n colour index and accepts an explicit level order, so two data
frames sharing a key (e.g. tracks and an overlay) can be coloured identically.
assign_cycle_colours() now delegates to it, and the Shiny app uses it too, so
the colour-cycling logic has a single source of truth.assign_colour_key() attaches a shared colour-key column to a
TrajSet or data frame (cycled for the trajectory or a high-cardinality key, raw
values + legend for a low-cardinality grouping; reference keeps the key
consistent across frames), so tracks and overlays colour identically. It
replaces the Shiny app's internal colour helpers.radiate() gains an angle_labels argument: "degrees" (default, e.g.
45°), "none", or "radians" (now rendered as π fractions, e.g. π/4,
rather than decimals). degree_labs() gains a matching units argument.
degrees = FALSE remains as a back-compatible alias for angle_labels = "none".radiate() gains a quadrants argument (default
FALSE) to opt back in.radiate() gains a rings argument (default FALSE) drawing concentric
guide rings (the radial analogue of a grid). Both the quadrant lines and the
guide rings take their colour and width from the chosen theme's grid lines,
so they match the theme (and fall back to a subtle grey for themes without a
grid, e.g. void and classic).radiate() style argument ("classic"/"minimal") is
replaced by theme, named for the ggplot2 base themes: "void" (default),
"minimal", "classic", "bw", "grey", "light", "dark", and
"linedraw". Each gives the matching ggplot2::theme_*() appearance (panel
background, grid, border). The exported sparse_theme() and spartan_theme()
are removed in favour of the new radial_theme(name).add_ticks() and
degree_labs() gain a colour argument.radiate() gains a show_tracks argument (default TRUE) to draw the arena
and overlays without the trajectory paths, symmetric with show_arrow and
show_labels.launch_app() starts a browser-based wizard (Upload → Configure → Results)
for users who do not write R, including condition detection and a Rayleigh
test. The app also builds as a static shinylive site.cpunctatus example and jumps straight to the
configure step.crossing its two
detection rings, ring-crossing dots, and dashed vector to the rim; distal the
furthest-from-centre point; net the first-to-last chord; straight the
longest straight run; and pca_axis the principal axis. Remaining rules mark
just the derived heading point. Constructions and heading markers take each
trajectory's colour.crossing), with a
one-line description of the selected rule shown beneath it.add_heading_interval(), add_critical_r(), and
add_critical_v_line() that the R code panel emits, so every statistical
overlay on the Results figure is reproduced by the code export. The figure's
subtitle (heading method) and caption (path-metrics summary, in the no-headings
mode) are carried by the spec and emitted too, so the entire figure — body,
overlays, and annotations — is reproduced by the exported script.NA values (trials with no defined heading).
The internal angle-wrapping helper is now NA-safe.read_calibration() reads camera
intrinsics and Brown-Conrady distortion coefficients from the MATLAB
Computer Vision Toolbox (.mat), OpenCV FileStorage (YAML/JSON), or a
plain CSV, and cal_model() builds a CalModel from coefficients you
already hold. Both handle radiatR's transposed intrinsic-matrix convention and
the 1-based (MATLAB) vs 0-based (OpenCV) principal-point difference for you.calibration_session(), calibration_from_points(),
checkerboard_points(), the calibration_points_* / *_calibration_points
helpers, and calibration_switch_axes()). Estimating intrinsics from
checkerboard images is better served by the mature toolboxes above; the
bundled calibration_corners.csv / calibration_truth.csv fixtures are gone
with it. cam_cal_pt(), cam_cal_many(), and calibrate_positions() are
unchanged.add_angle_rose(), add_circular_kde(), add_vonmises_density(), and
add_wrappedcauchy_density() now honor the plot's display rotation (via a
new display argument, the input's display attribute, or the identity
default), so they stay aligned with the tracks and heading markers on a
rotated display (e.g. a clock-oriented plot) instead of being drawn ~90
degrees off. Behaviour on the default plot is unchanged.crossing heading rule now computes the heading as the bearing of the
inner→outer ring-crossing vector projected onto the arena boundary (the unit
circle) — the method described in the vignette and used in the original
analysis — rather than the slope of the crossing segment. The two agree for
radial tracks but differ for oblique ones, so crossing-based circular
statistics change for non-radial trajectories. derive_headings(rule = "crossing", return_coords = TRUE) now also returns the outer crossing
(x_outer/y_outer).compute_circ_mean() carries the input's display attribute onto its output
and the arrow is rotated with the rest of the plot, on screen and in the
exported code.as.data.frame() on a TrajSet now works for users of the installed
package. It had been defined as an S4 method on the base S3 generic, which is
only reachable from within the package's own namespace, so user code got
"cannot coerce class TrajSet to a data.frame". It is now a registered S3
method.cpunctatus (and the raw tracks
cpunctatus_tracks), a subset of the Cylindroiulus punctatus millipede
visual-orientation experiment of Kirwan & Nilsson (2019,
Vision Research 165:36–44). This replaces the previous Paracentrotus
lividus sea-urchin example.pkgdown site with a grouped reference index and a changelog.