anicore (development version)
Breaking changes
-
The position frame is now
anipoint, andaniframeis the abstract parent of every frame class:anipoint,anisegment,anijointandanievent(#154).-
anipoint(),as_anipoint(),example_anipoint()andvalidate_anipoint()replace theaniframe()versions, which remain as soft-deprecated aliases for one release. -
is_aniframe()is nowTRUEfor any frame, including an anievent. Test the grain withis_anipoint()orensure_is_anipoint(); functions that need coordinates now reject other grains. - Re-cast objects saved with earlier versions with
as_anipoint()oras_anievent().
-
-
Metadata is grouped into categories —
recording,time,space,variablesandstructure— andspec_versionbecomes 3.0.0 for anipoint frames and 1.0.0 for anievents (#118). Access stays flat:get_metadata(x, "sampling_rate"),get_metadata(x)$sampling_rateandset_metadata(x, sampling_rate = 30)work as before, and a category name returns the whole category. Readingattr(x, "metadata")directly no longer works. Objects in the old layout are migrated when read.- An anievent has no
spacecategory: its spatial fields readNULL, and spatial fields passed toas_anievent()are dropped (#73). -
get_metadata()with several names returns a plain list, and setting a field toNULLerrors (useNA).
- An anievent has no
-
set_*()functions now only declare; operations that change values have their own verbs (#155).-
get_variables(),set_variables(),add_variables()andremove_variables()replace the sixteen*_variables_*()functions andset_axes(). They take a role and a slot, e.g.set_variables(x, when = list(keys = "trial")).get_variables(x, "when")now includes the index; the grouping columns areget_keys(). -
convert_unit_space(),convert_unit_time()andconvert_unit_angle()replaceset_unit_*(). Converting frompx,frameorunknownnow needs acalibration_factor, or, fromframe, a declaredsampling_rate. -
set_axis_directions()only records directions;reflect_axis()turns an axis over and reflects the data. - The unit and sampling-rate getters and setters,
get_/set_axis_extents(),set_handedness()andset_angle_direction()are removed; useget_metadata()andset_metadata().set_sampling_rate()also converted frames to seconds: useset_metadata(x, sampling_rate = r) |> convert_unit_time("s").
-
Structures replace connections (#154). An
anistructure()holds points, segments with optional expected lengths, and joints: angles between two segments, with optional limits. A frame can hold several named structures, including several over one variable, throughset_structure(),get_structure()andremove_structure(). The*_connections()functions are removed; stored connection tables become structures.Subsetting a frame so that it loses a column it is keyed, indexed or bounded by gives a plain tibble rather than a frame whose metadata names columns it does not have (#178). This covers
[,select(),distinct()and removing a column with$<-or[[<-.af[c("x", "y")]andselect(af, x, y)used to return an anipoint missing itstimeand keys, whichensure_is_anipoint()accepted and later steps tripped over; they now return the columns asked for, ready for plain data-frame work. Dropping a declared value column (a position, an orientation) still returns the frame, so it can be re-declared.Directions are signed, in
(-pi, pi], throughout (#181).wrap_angle()now wraps to(-pi, pi]by default (modulo = "pi"), andcirc_mean()andcirc_median()return that range, so a summary such asanimetric::summarise_aniframe()’smedian_coursematches the per-row directions it summarises: a course of a little below zero now has a median of a little below zero, not a little below2 * pi. It is the rangeatan2()gives, which per-row directions already used. The range does not change any direction, only how it is written. For the old range, wrap at the end withwrap_angle(x, "2pi"), and pass"2pi"explicitly to anywrap_angle()call that relied on the default.wrap_angle()’s help page documents the convention.
Added
-
A
source_sampling_ratemetadata field, inrecording, holds the rate the device recorded at, apart fromsampling_rate, the rate of the data as it is now (#190). Resampling changes the current rate and never the recorded one; filters,convert_unit_time()and the rate check keep usingsampling_rate.- The first rate declared fills both:
set_metadata(sampling_rate = )on a frame with neither rate set also setssource_sampling_rate, so readers that declare a rate record the camera’s without changing. This applies toas_anipoint(metadata = )too. After thatsampling_ratechanges on its own, so correcting a wrong camera rate means settingsource_sampling_ratein the same call. -
source_sampling_rate = NaNdeclares that the device has no fixed rate, such as an event-driven sensor whose readings a reader integrates into windows. The first declaration leaves it alone, whereNAmeans not declared yet. - The compact metadata print and the frame header show the recorded rate only when it differs:
sampling_rate: 50 Hz (recorded at 200 Hz). - Metadata saved before the field existed still validates, and a frame that already declared a rate does not fill it later, since its later rates may be resampled ones.
- The first rate declared fills both:
-
convert_unit_time()converts to"frame"(#190). Frame numbers a file records are data, and frames computed from a rate are only nominal, so it never invents them:- A frame with a column named
frame, not its index, holds the recorded frame numbers, and converting to"frame"makes that column the index again. The column it replaces is kept. - Otherwise frames are computed from the declared
sampling_rate, counting from 0 (seconds times the rate), and rounded to the nearest whole frame. The conversion refuses when the sampling is irregular, a gap more than 1% from a whole number of frames, or when rounding would put two times of the same keys on one frame. - A wrong rate can now be put right by converting back to frames, declaring the right rate and converting forward again, rather than with a hand-worked
calibration_factor.
- A frame with a column named
angle_to_rad()andangle_from_rad()convert angle values between radians and a frame’s declaredunit_angle, for functions that compute angles in radians and return them in the frame’s unit (#170). The frame’s unit, or a unit string read once for use insidemutate(), says which. A frame that declares no angular unit ("none", or an anievent) is read as radians.as_anijoint()andreflect_axis()use them in place of their own copies of the check.as_anisegment()gives one row per segment of a structure: itslengthand unit direction (#154).as_anipoint(seg, root = )rebuilds positions from segments, for example after holding lengths constant.as_anijoint()gives one angle per joint of a structure (#154).angle_between()is the underlying computation.Orientation can be declared alongside position:
yawin 2D, or a unit quaternion in 3D (#46).reflect_axis()andconvert_unit_angle()handle it, andeuler_sequenceandeuler_intrinsicrecord the Euler convention a source uses.convert_inf_to_na(), the sibling ofconvert_nan_to_na(), for sources that mark a missing observation with an infinity rather than aNaN. TRex is one — its own documentation masksnp.infout before plotting, and itsmissingflag is 1 in exactly those frames. Left in place anInfpropagates through arithmetic silently, so one untracked frame turns a mean or a speed intoInfrather than into a missing value (animovement/aniread#116).-
A
source_formatmetadata field, recording which export layout a file was read as (animovement/aniread#118). Tracking software changes its export layout between releases — FreeMoCap’s tidy CSV gained areprojection_errorcolumn at v1.8.0, and its wide per-model CSVs are a different layout again — sosourcealone does not say what was parsed. Readers set it to a short layout name such as"by_frame_9col".It is deliberately separate from
source_version, which stays reserved for a version the file actually states. Most tracking formats record none, and a layout inferred from a column count must not be stored as though it had been read. Where both are known, both are set. Circular descriptive statistics, beside the existing angle utilities:
circ_median()(Fisher’s median),circ_mean(),circ_sd()andcirc_mad(). Where two directions tie for the median, they are averaged on the circle: averaging them arithmetically returns their antipode when the tie straddles zero, which is 180 degrees from the answer. Angles have no smallest or largest value, so the ordinary median and standard deviation do not apply — the mean of 350 and 10 degrees is 0, not 180 (#147).-
circ_difference()andcirc_successive_difference(), moved here from anispace, where they werecalculate_angular_difference()anddiff_angle(). The first is the shortest signed distance between two angles you name; the second applies it along a series, comparing each angle with the one before it. Both are general-purpose primitives rather than spatial transforms —circ_difference()is the one the circular summaries are built on — so anicore owns them alongside the rest of the angle utilities (#147).The rename follows the convention these settle on:
circ_*()for functions that compute with the wraparound, where an ordinary mean or difference would give the wrong answer, and*_angle()orx_to_y()for manipulating how an angle is written.
Changed
set_index()declares the unit of the new index in the same call, withunit, and checks that the new index increases with the old one within each group of keys (#190). An index that counted frames is no longer left under its old name: it is renamedframe, as recorded frame numbers are data, andconvert_unit_time(x, "frame")makes that column the index again.set_index()refuses when anotherframecolumn is in the way, and a new index with missing values. Its help page shows how to index a frame by a camera’s timestamp log, matched to rows by frame number.validate_anipoint()allows the declaredsampling_rateand the measured spacing of the index to differ by 1% before warning, rather than by one part in a million (#190). The spacing of a real timestamp log never matches the nominal rate exactly, so the warning fired on every one: a camera log averaging 30.11 Hz against a declared 30 Hz. The newrate_toleranceargument sets the margin;rate_tolerance = 1e-6gives the old, strict check.-
The order of the identity keys (formerly
variables_what) no longer asserts a hierarchy (#140, #141). It was documented as coarse to fine, which reads naturally for the names that nest — a subject has tracks, a track has keypoints — but identity variables need not nest at all.sex,treatmentandgenotypepartition a population without containing one another, and there is no sense in which one is finer than the next.The order is now documented as what auto-detection emits, not something a frame asserts. Nothing should read a position among the identity keys as meaning a level; a function that needs to know which variable to act on asks for it —
animetric::add_point()takesacross,anispace::translate_coords()takeslevel.No behaviour changes. Detection emits the same order, and the order still carries through to column order and grouping, which is presentation: grouping by
(a, b)and(b, a)gives the same groups. Help pages now show each function’s lifecycle stage (animovement/.github#46); a function without a badge is stable. The structure family is marked experimental, since its design is still being worked out (#154, #162):
anistructure(),validate_anistructure(),set_structure(),get_structure(),remove_structure(),as_anisegment()andas_anijoint(), together with theanisegmentandanijointclasses they return, and therootargument ofas_anipoint(). Experimental functions may change without a deprecation cycle.circ_median(), and socirc_mad(), take O(n log n) time rather than O(n²), with the same results (#182). On 20,000 anglescirc_median()takes 0.02 s rather than 7.6 s, andanimetric::summarise_aniframe()on a 10-minute, 30 fps recording of 6 keypoints takes 0.4 s rather than 90 s.-
Metadata and structures print more compactly (#180).
- Metadata prints one
name: valueline per field, wrapped to the console width, with units where the metadata declares them (sampling_rate: 30 Hz,axis_extents: x = 1920 px). Fields that are not set are named on one closing line rather than printed as<NA>, and storage types, the allowed values of factor fields andspec_versionare left out.print(get_metadata(x), all = TRUE)shows everything. - A structure writes each segment once, as
from - to, with its name only when it has one of its own, and wraps its lists to the console width. Like a tibble’s rows, a list of more than 20 shows its first 10;print(x, n = Inf)shows all.
- Metadata prints one
Fixed
sampling_intervalfollows the rows and the index (#190).filter(),slice(),[, and changing the index or a key withmutate(),$<-,[<-or[[<-measure it again, so a 30 Hz recording thinned to every third frame reports the 10 Hz spacing it now has, andvalidate_anipoint()warns that itssampling_rateno longer matches. It used to keep the spacing measured when the frame was built. Verbs that leave the rows and the index alone, such as amutate()of another column,rename()orselect(), keep the stored value without measuring.convert_unit_time()convertssampling_intervalalong with the index (#185). It used to stay in the old unit, so frames at 30 Hz converted to seconds kept an interval of 1 rather than 1/30.sampling_rateis in Hz and is unchanged.wrap_angle()no longer returns the end of its range that the range excludes (#181). An angle a rounding error abovepiwrapped to-pi, and one a rounding error below zero wrapped to2 * piwithmodulo = "2pi"; both now land on the other end, which is the same angle.Renaming columns carries the new names into the metadata (#178).
rename(),rename_with(), renaming inselect()orrelocate(), andnames<-used to leave the keys, the index, the interval, the declared variables and each structure’svariablenaming the old columns, sorename(af, t = time)gave a framevalidate_anipoint()rejected.validate_anipoint()no longer warns thatcoordinate_systemis stale for a frame whose axis columns are named something other than their roles, as withas_anipoint(df, variables_where = c(x = "u", y = "v"))(#178). It compared the recorded system with one inferred from the column names rather than the axis roles.
anicore 0.8.0 (2026-08-28)
Changed
-
The package is renamed from
aniframetoanicore(#84). It is no longer one class’s home: it declaresaniframe,anieventand — as #84 settles — the types the domain packages build on, while producing almost none of them. A package named after one of its entries was going to keep getting stranger as angles (#83), orientation (#46) and masks (#11) arrive.The
aniframeclass keeps its name, as doas_aniframe(),is_aniframe()and every other function. Only the package changes:library(anicore),anicore::, andinstall.packages("anicore").Done now rather than at 1.0.0 because a rename only ever gets more expensive, and there are no external users yet to carry the cost.
?aniframeis the constructor again. The package documentation had claimed the same help topic, so the two were merged into one page; it is now?anicore.
Added
-
An aniframe can be indexed by a column that is not called
time(#109). Avariables_indexmetadata field names the single column each row is positioned by;get_index()reads it,set_index()changes it and re-orders the frame, andas_aniframe()andaniframe()take anindexargument. A frame has exactly one index, and it is never a grouping variable.set_unit_time(),set_sampling_rate()andto_anievent()act on the declared index rather than a column namedtime, andvalidate_aniframe()checks it is present and numeric.An
anieventhas no index, since a bout is delimited bystartandstop. Itsvariables_indexisNAandget_index()errors on it. -
get_sampling_interval()reports the spacing of the index, derived from the data at construction rather than declared, andis_sampling_regular()says whether that spacing is even (#114). Nothing in the stack could previously tell whether a frame was regularly sampled, which several downstream functions need — interpolating on row position rather than on time is only correct when it is.Measured per key, since the index restarts in each group and a frame regular within every track can look irregular pooled. Regularity is computed on demand rather than stored, because dropping rows changes the answer; its
toleranceis an argument, and relative, so floating-point timestamps are not called irregular over the last decimal place.validate_aniframe()warns when a declaredsampling_ratedisagrees with the measured spacing — the same shape as #98, where the metadata claimed a unit the data was not in. get_sampling_rate(),get_unit_space(),get_unit_time()andget_unit_angle()read the fields that already had setters (#121). Every field with a dedicated setter now has a dedicated getter, so reading one no longer means naming it as a string. The factor-backed ones return a bare character vector, which is what callers were doing withas.character()anyway.get_coordinate_system()reads the coordinate system a frame is in (#109). There is deliberately no setter: the field is derived from the axis roles, soset_axes()says what the columns mean andanispace’smap_to_*()functions convert the coordinates.An
axesmetadata field records which column carries which axis role, so coordinates may be carried by columns of any name (#109).get_axes()reads it,set_axes()changes it, andas_aniframe()andset_variables_where()accept the same mapping —c(x = "u", y = "v").coordinate_systemfollows from it, andset_unit_space()and the axis extents resolve through it. The roles are a closed set:x,y,z,rho,phi,theta, and one that forms no coordinate system is rejected by name at declaration. Declaring spatial columns without roles keeps its old meaning, the column name being the role.Axis directions and extents record how a frame is laid out, replacing
originandy_height(#124).axis_directionsmaps each axis role to one ofright,left,up,down,backorforward, read from where the recording was made;axis_extentsmaps each to how far it runs.get_axis_directions(),set_axis_directions(),get_axis_extents()andset_axis_extents()read and write them. Turning an axis to its opposite reflects that column: around the axis extent where one is declared, around zero where none is. The column is found by role, so a frame whose vertical axis is called something else is handled, and an angular frame refuses rather than leaving every stored angle facing the wrong way.get_angle_direction()says which way angles run, derived from the axis directions rather than recorded (#124).atan2(y, x)counts counter-clockwise, so the same physical heading comes out mirrored between a y-down and a y-up frame; nothing said which convention a number was in.get_handedness()andset_handedness()say whether a frame is right- or left-handed (#124). Three declared axis directions determine it and are read in preference to thehandednessfield, which carries the convention for a frame that states one without spelling the axes out.set_handedness()defaults to right-handed and completes the third axis when two are declared.set_angle_direction()asks for a sense of rotation and declares the axis directions that give it (#124), turning the vertical axis over when both are already declared.wrap_angle()andunwrap_angle()move here fromanispace(#128). They are angle arithmetic rather than coordinate transformation, and belong besidedeg_to_rad()andrad_to_deg(), which were already here.animetricre-exported both fromanispace, which is the sign of a primitive sitting one layer above the packages that need it.Turning an axis over on a frame that stores angles recomputes them, rather than refusing (#134). No column carries
x,yorzon a polar, cylindrical or spherical frame, but the angles are measured from those axes: turningxover takes the supplement ofphi, turningyover negates it, and turningzover takes the supplement oftheta. The result comes back in the unit and range the frame keeps its angles in, andrhonever moves. An axis with a declared extent still refuses, because reflecting around it would move every point’s distance from the origin.Every exported function now has a runnable example (#106).
Changed
Declaring an axis role that is carried by one column while a different, undeclared column has that role’s name now warns (#119). The frame is legal and the mapping is right, but
.data$xthen returns a column that is not the x axis. Silence it withoptions(aniframe.quiet = TRUE).coordinate_systemfollows from which axis roles are declared rather than from column names (#109). A frame whose coordinates are named something else is now inferred correctly, where it degraded tounknownand was refused by every spatial function.is_cartesian(),is_polar(),is_cylindrical(),is_spherical(), theis_cartesian_*d()variants and theirensure_guards readcoordinate_systemrather than matching column names, and require an aniframe (#107, #109). A frame whose coordinates are called something else now satisfies the predicate for the system it is in, and an undeclared column no longer decides the answer — a spherical frame that has droppedrhofrom its declaration is no longer reported as spherical. The guards say which system the frame is in and how to get to the one you need.add_variables_where()andremove_variables_where()carry the axis roles through (#109). They combined bare column names, so on a frame with declared roles every addition or removal reduced it tounknown. Removing an axis until the remainder forms no coordinate system warns rather than aborting; declaring such a set outright still aborts.validate_aniframe()warns when identity, temporal context and the index together do not name one observation per row (#49). A repeat means some variable that tells the rows apart is undeclared, and every grouped operation folds them together.variables_whenno longer names the column the frame is indexed by; read that fromvariables_index, viaget_index()(#109). It holds only the temporal context, so a frame with none hascharacter()where it hadc("time"), as doeslist_default_metadata().aniprocess::filter_across()andfilter_na_across()takevariables_when[1]as the time column and must swap toaniframe::get_index(). Code reading the grouping columns is unaffected, and no longer has anything to exclude: they arec(variables_what, variables_when).default_metadata()is renamedlist_default_metadata()(#121).originandy_heightare removed, along withset_origin()andset_y_height()(#124). Useset_axis_directions()andset_axis_extents().originrecorded a corner, but the origin is(0, 0)in both of its values — what differed was the direction y increases in, which is what is recorded now. It also had nothing to say for 3D data or for a recording with no frame corners at all. The deprecatedpoint_of_referencealias goes with the field it aliased.as_aniframe()no longer fills in an axis extent from the data (#124).y_heightfell back tomax(y), which is the highest tracked point rather than the frame height, so a frame that was never told its height reflected around the wrong place. A frame now declares no extent until given one, and an axis with no extent is negated when turned over rather than reflected around a guess.aniread’s readers supply the video height they know about.spec_versionmoves toaniframe = "2.0.0"andanievent = "0.3.0"(#109). Major foraniframe:variables_whenno longer names the index, which breaks a consumer reading it from there. Minor foranievent, which gainsvariables_indexasNA.
Fixed
set_unit_space()converts the axis extents along with the coordinates (#124). An extent is a length, so converting cm to m left the frame claiming a height in the unit it no longer used.set_unit_space()converts the length axes of the frame’s coordinate system rather than whichever ofx,yandzare present (#98).rhois a length on polar, cylindrical and spherical frames and was never converted, while the metadata was updated to claim the new unit. Angular axes remainset_unit_angle()’s to convert. Where the coordinate system isunknowna length cannot be told from an angle, and the function now warns rather than silently converting nothing.set_unit_space(),set_unit_angle(),set_unit_time()andset_sampling_rate()no longer re-inject akeypointcolumn and overwritevariables_whatwith it (#96). A frame given a custom identity such asidwas silently regrouped on a constant column.as_aniframe()keeps the roles a frame already declares rather than re-deriving them, so casting an aniframe is no longer destructive (#96). A declaration whose columns have since been dropped still falls through to detection, so a cast continues to repair a drifted frame.
anicore 0.7.0 (2026-08-18, as aniframe)
Added
set_variables_what(),set_variables_when(),set_variables_where()andset_variables_event()declare the variable roles, each withget_,add_andremove_verbs (#82). They declare the role and restructure the frame to match, so the metadata and the frame cannot drift apart.add_variables_*()appends, so adding one identity column no longer means restating the others.validate_aniframe()re-checks that the metadata still describes the frame: every declared column present,timeand the spatial columns numeric (#79). Counterpart tovalidate_anievent().is_spatial()andensure_is_spatial()test the columns named invariables_where, which theis_cartesian*()family does not — those look at column names only (#79).
Changed
set_metadata()no longer writes thevariables_*fields; use their dedicated setters (#82). Writing them as plain metadata left the frame typed, ordered and grouped as before, so operations silently integrated across identities. A complete metadata object can still be restored wholesale.as_aniframe()errors whenvariables_whatnames a column that is not in the data, as it already did forvariables_whenandvariables_where(#77).aniframeandanieventrecognise the same identity variables —model,individual,subject,track,keypoint— ordered coarse to fine (#77).spec_versionmoves toaniframe = "1.1.0"andanievent = "0.2.0".
Removed
-
as_aniframe()no longer adds akeypoint = "centroid"column to data that already has an identity column (#77). Results are unaffected — the column was constant — but it no longer appears in the frame or the print header.
Fixed
Downstream subclasses survive the class-preserving methods (#81).
animetric’saniframe_kinwas dropped by the firstfilter(),mutate()or[. Verbs that were never covered —distinct(), joins,bind_rows()— still drop it.An
anieventno longer claims spatial properties it cannot have, such as a BORIS export announcingorigin: bottom_left(#73).unit_angle,originandreference_framegain a"none"level.
anicore 0.6.0 (2026-06-24, as aniframe)
Added
anievent, a class for behavioural events in long format — one row per bout (state event) or instant (point event) (#67). A sibling ofaniframe: it shares the metadata substrate but does not inherit from it. Required columns arechannel,type,label,startandstop, with identity columns travelling viavariables_whatand an optionalmodifierslist-column.typeis derived per(channel, label)group at construction — a group is"point"only when all its bouts are instantaneous — and can be set explicitly where that misclassifies.anievent()andas_anievent()construct the class,is_anievent()andensure_is_anievent()test it, andvalidate_anievent()re-checks its invariants on demand (#68). Class-preserving dplyr and base-R methods keep the class through tidyverse pipelines.to_anievent()run-length-encodes per-frame data into bouts, as distinct fromas_anievent(), which casts data that is already bout-shaped. Methods fordata.frameandaniframe; the latter auto-detects each channel’s identity scope, so a label constant across keypoints does not produce a duplicate bout per keypoint.A
variables_eventmetadata field — a named listlist(state, point)declaring which columns hold per-frame event labels (#66). State columns are interval-valued, point columns instantaneous; both appear in the print header when populated.A
spec_versionmetadata field, keyed by class, so each class’s data contract can evolve independently of the package version (#65). Older serialised objects without it continue to validate.set_unit_time()andset_sampling_rate()are S3 generics withaniframeandanieventmethods. On an anievent the calibration factor applies tostartandstoprather thantime.as_aniframe()auto-detectsobservationas a temporal grouping column, alongsidesessionandtrial.New article, “The anievent data structure”, covering channels, state and point events, modifiers, validation and multi-observation data (#70).
Changed
validate_anievent()warns when two bouts of the samechanneloverlap within a group. A warning rather than an error: overlap is normal BORIS output and the long format handles it natively.set_metadata()accepts partialvariables_eventinput — supplying onlystateor onlypointis fine, andNAor empty entries read as “none” rather than erroring (#76).The metadata print heading reads “animovement metadata”, since the substrate is shared by both classes (#69). The S3 class name is unchanged.
anicore 0.5.0 (2026-05-04, as aniframe)
Added
set_origin()converts between thebottom_leftandtop_leftorigin conventions, reflectingyaround the recorded frame height (#52).set_y_height()sets the y-axis frame height thatset_origin()uses, validated against the data range, and ay_heightmetadata field to hold it. Readers populate it from the source;as_aniframe()falls back tomax(y)when missing, and never overwrites an existing value.A
connectionsmetadata field for skeletons and other variable-level networks (#6). Stored as a named list keyed by the relevant identity or temporal variable — typicallykeypoint, butindividualfor social networks — with each entry a 2-columnfrom/totibble whose order is preserved. Manage it withset_connections(),get_connections(),add_connections()andremove_connections(). Endpoints missing from the corresponding column warn but are kept.A “Time” row in the print summary showing the tracked interval as
HH:MM:SS to HH:MM:SS, or as absolute datetimes whenstart_datetimeis set (#50). Sub-second runs use millisecond precision.New articles introducing the data structure: “The aniframe data structure”, “Metadata on an aniframe” and “Connections”.
Changed
set_unit_angle()converts the spatial angular columnsphiandthetawhenever they are present, so polar, cylindrical and spherical coordinates stay consistent with the declaredunit_angle(#21). These were previously assumed to be radians and left alone. The signature becomesset_unit_angle(data, to_unit, cols = NULL), matchingset_unit_time(); passcolsonly for additional non-spatial angular columns. Positional callers need to swap their arguments.The print summary is driven by
variables_whatandvariables_whenrather than hard-coded column names (#51). Custom identity and temporal variables such astrackandmodelnow appear, rows are omitted when their column is absent, and single-track readers no longer emit “Unknown or uninitialised column:individual”.The metadata print renders as a single block with field names and values aligned in fixed-width columns (#48).
The
filenamemetadata field accepts a character vector of length one or more, for readers that load from multiple source files (#34).Renamed the
point_of_referencemetadata field toorigin, with permitted values"bottom_left"and"top_left".
Deprecated
-
point_of_referenceas a metadata field name.set_metadata()still accepts it, with a warning.
Fixed
-
as_aniframe()no longer mis-classifies cylindrical (rho,phi,z) and spherical (rho,phi,theta) data as Cartesian (#44). Detection recognises therho+phisignature first, so cylindrical data is no longer reduced tocartesian_1dby itszcolumn. Cylindrical spatial columns are now orderedrho,phi,z(#43).
anicore 0.4.1 (as aniframe)
Fixed
- Corrected metadata written by
as_aniframe().
anicore 0.4.0 (as aniframe)
Added
-
variables_what,variables_whenandvariables_wherearguments toas_aniframe()andexample_aniframe(), written into the frame’s metadata. These declare which columns carry identity, temporal position and spatial position, and are the basis for how the frame is typed, ordered and grouped.
anicore 0.3.5 (as aniframe)
anicore 0.3.0 (as aniframe)
Spatial transformations leave aniframe for anispace. aniframe keeps the coordinate system — what a frame is in, and how to test it — while converting between systems becomes anispace’s job.
Added
-
ensure_is_cartesian(), with_1d(),_2d()and_3d()variants, andensure_is_polar(),ensure_is_cylindrical()andensure_is_spherical()— guards to sit at the top of a function that requires a particular coordinate system. -
convert_nan_to_na()is exported.
Removed
- The coordinate transformations
map_to_cartesian(),map_to_polar(),map_to_cylindrical()andmap_to_spherical(), the component converterscartesian_to_rho(),cartesian_to_phi(),cartesian_to_theta(),polar_to_x(),polar_to_y()andspherical_to_z(), the rigid transformsrotate_coords(),translate_coords()andtransform_to_egocentric(), and the angle helperswrap_angle(),unwrap_angle(),diff_angle()andcirc_difference(). All are available from anispace.
anicore 0.2.4 (as aniframe)
Changed
-
set_metadata()accepts a partial metadata list, rather than requiring every field.
anicore 0.2.3 (as aniframe)
Added
-
is_cartesian(), with_1d(),_2d()and_3d()variants, andis_polar(),is_cylindrical()andis_spherical()to test a frame’s coordinate system.
anicore 0.2.2 (as aniframe)
Added
-
unwrap_angle(), the counterpart towrap_angle().
Changed
-
constrain_angles_radians()is renamedwrap_angle().
anicore 0.2.1 (as aniframe)
Added
- Unit handling:
set_unit_space(),set_unit_angle(),set_unit_time()andset_sampling_rate(). - Coordinate transformations:
map_to_cartesian(),map_to_polar(),map_to_cylindrical()andmap_to_spherical(), with the component converterscartesian_to_rho(),cartesian_to_phi(),cartesian_to_theta(),polar_to_x(),polar_to_y()andspherical_to_z(). - Rigid transformations:
rotate_coords(),translate_coords()andtransform_to_egocentric(). - Angle handling:
deg_to_rad(),rad_to_deg(),constrain_angles_radians(),circ_difference()anddiff_angle(). -
ensure_is_aniframe(), a guard for functions that require an aniframe. - Trackball calibration, via
get_trackball_calibration_factor().
anicore 0.1.0 (2025-10-13, as aniframe)
First release. aniframe provides the core data structure for the animovement suite: a tibble subclass carrying metadata that says which columns hold identity, time and position.
Added
-
aniframe()andas_aniframe()to construct a frame,is_aniframe()to test one, andexample_aniframe()to generate one. -
get_metadata(),set_metadata()anddefault_metadata()to read and write the metadata a frame carries.