Skip to contents

anicore (development version)

Breaking changes

  • The position frame is now anipoint, and aniframe is the abstract parent of every frame class: anipoint, anisegment, anijoint and anievent (#154).

  • Metadata is grouped into categories — recording, time, space, variables and structure — and spec_version becomes 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_rate and set_metadata(x, sampling_rate = 30) work as before, and a category name returns the whole category. Reading attr(x, "metadata") directly no longer works. Objects in the old layout are migrated when read.

    • An anievent has no space category: its spatial fields read NULL, and spatial fields passed to as_anievent() are dropped (#73).
    • get_metadata() with several names returns a plain list, and setting a field to NULL errors (use NA).
  • set_*() functions now only declare; operations that change values have their own verbs (#155).

  • 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, through set_structure(), get_structure() and remove_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")] and select(af, x, y) used to return an anipoint missing its time and keys, which ensure_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"), and circ_mean() and circ_median() return that range, so a summary such as animetric::summarise_aniframe()’s median_course matches 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 below 2 * pi. It is the range atan2() 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 with wrap_angle(x, "2pi"), and pass "2pi" explicitly to any wrap_angle() call that relied on the default. wrap_angle()’s help page documents the convention.

Added

  • A source_sampling_rate metadata field, in recording, holds the rate the device recorded at, apart from sampling_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 using sampling_rate.

    • The first rate declared fills both: set_metadata(sampling_rate = ) on a frame with neither rate set also sets source_sampling_rate, so readers that declare a rate record the camera’s without changing. This applies to as_anipoint(metadata = ) too. After that sampling_rate changes on its own, so correcting a wrong camera rate means setting source_sampling_rate in the same call.
    • source_sampling_rate = NaN declares 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, where NA means 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.
  • 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.
  • angle_to_rad() and angle_from_rad() convert angle values between radians and a frame’s declared unit_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 inside mutate(), says which. A frame that declares no angular unit ("none", or an anievent) is read as radians. as_anijoint() and reflect_axis() use them in place of their own copies of the check.

  • as_anisegment() gives one row per segment of a structure: its length and 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: yaw in 2D, or a unit quaternion in 3D (#46). reflect_axis() and convert_unit_angle() handle it, and euler_sequence and euler_intrinsic record the Euler convention a source uses.

  • convert_inf_to_na(), the sibling of convert_nan_to_na(), for sources that mark a missing observation with an infinity rather than a NaN. TRex is one — its own documentation masks np.inf out before plotting, and its missing flag is 1 in exactly those frames. Left in place an Inf propagates through arithmetic silently, so one untracked frame turns a mean or a speed into Inf rather than into a missing value (animovement/aniread#116).

  • A source_format metadata 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 a reprojection_error column at v1.8.0, and its wide per-model CSVs are a different layout again — so source alone 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() and circ_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() and circ_successive_difference(), moved here from anispace, where they were calculate_angular_difference() and diff_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() or x_to_y() for manipulating how an angle is written.

Changed

  • set_index() declares the unit of the new index in the same call, with unit, 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 renamed frame, as recorded frame numbers are data, and convert_unit_time(x, "frame") makes that column the index again. set_index() refuses when another frame column 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 declared sampling_rate and 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 new rate_tolerance argument sets the margin; rate_tolerance = 1e-6 gives 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, treatment and genotype partition 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() takes across, anispace::translate_coords() takes level.

    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() and as_anijoint(), together with the anisegment and anijoint classes they return, and the root argument of as_anipoint(). Experimental functions may change without a deprecation cycle.

  • circ_median(), and so circ_mad(), take O(n log n) time rather than O(n²), with the same results (#182). On 20,000 angles circ_median() takes 0.02 s rather than 7.6 s, and animetric::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: value line 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 and spec_version are 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.

Fixed

  • sampling_interval follows the rows and the index (#190). filter(), slice(), [, and changing the index or a key with mutate(), $<-, [<- or [[<- measure it again, so a 30 Hz recording thinned to every third frame reports the 10 Hz spacing it now has, and validate_anipoint() warns that its sampling_rate no 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 a mutate() of another column, rename() or select(), keep the stored value without measuring.

  • convert_unit_time() converts sampling_interval along 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_rate is 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 above pi wrapped to -pi, and one a rounding error below zero wrapped to 2 * pi with modulo = "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 in select() or relocate(), and names<- used to leave the keys, the index, the interval, the declared variables and each structure’s variable naming the old columns, so rename(af, t = time) gave a frame validate_anipoint() rejected.

  • validate_anipoint() no longer warns that coordinate_system is stale for a frame whose axis columns are named something other than their roles, as with as_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 aniframe to anicore (#84). It is no longer one class’s home: it declares aniframe, anievent and — 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 aniframe class keeps its name, as do as_aniframe(), is_aniframe() and every other function. Only the package changes: library(anicore), anicore::, and install.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.

  • ?aniframe is 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). A variables_index metadata field names the single column each row is positioned by; get_index() reads it, set_index() changes it and re-orders the frame, and as_aniframe() and aniframe() take an index argument. A frame has exactly one index, and it is never a grouping variable.

    set_unit_time(), set_sampling_rate() and to_anievent() act on the declared index rather than a column named time, and validate_aniframe() checks it is present and numeric.

    An anievent has no index, since a bout is delimited by start and stop. Its variables_index is NA and get_index() errors on it.

  • get_sampling_interval() reports the spacing of the index, derived from the data at construction rather than declared, and is_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 tolerance is an argument, and relative, so floating-point timestamps are not called irregular over the last decimal place.

    validate_aniframe() warns when a declared sampling_rate disagrees 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() and get_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 with as.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, so set_axes() says what the columns mean and anispace’s map_to_*() functions convert the coordinates.

  • An axes metadata 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, and as_aniframe() and set_variables_where() accept the same mapping — c(x = "u", y = "v"). coordinate_system follows from it, and set_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 origin and y_height (#124). axis_directions maps each axis role to one of right, left, up, down, back or forward, read from where the recording was made; axis_extents maps each to how far it runs. get_axis_directions(), set_axis_directions(), get_axis_extents() and set_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() and set_handedness() say whether a frame is right- or left-handed (#124). Three declared axis directions determine it and are read in preference to the handedness field, 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() and unwrap_angle() move here from anispace (#128). They are angle arithmetic rather than coordinate transformation, and belong beside deg_to_rad() and rad_to_deg(), which were already here. animetric re-exported both from anispace, 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, y or z on a polar, cylindrical or spherical frame, but the angles are measured from those axes: turning x over takes the supplement of phi, turning y over negates it, and turning z over takes the supplement of theta. The result comes back in the unit and range the frame keeps its angles in, and rho never 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$x then returns a column that is not the x axis. Silence it with options(aniframe.quiet = TRUE).

  • coordinate_system follows 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 to unknown and was refused by every spatial function.

  • is_cartesian(), is_polar(), is_cylindrical(), is_spherical(), the is_cartesian_*d() variants and their ensure_ guards read coordinate_system rather 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 dropped rho from 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() and remove_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 to unknown. 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_when no longer names the column the frame is indexed by; read that from variables_index, via get_index() (#109). It holds only the temporal context, so a frame with none has character() where it had c("time"), as does list_default_metadata(). aniprocess::filter_across() and filter_na_across() take variables_when[1] as the time column and must swap to aniframe::get_index(). Code reading the grouping columns is unaffected, and no longer has anything to exclude: they are c(variables_what, variables_when).

  • default_metadata() is renamed list_default_metadata() (#121).

  • origin and y_height are removed, along with set_origin() and set_y_height() (#124). Use set_axis_directions() and set_axis_extents(). origin recorded 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 deprecated point_of_reference alias goes with the field it aliased.

  • as_aniframe() no longer fills in an axis extent from the data (#124). y_height fell back to max(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_version moves to aniframe = "2.0.0" and anievent = "0.3.0" (#109). Major for aniframe: variables_when no longer names the index, which breaks a consumer reading it from there. Minor for anievent, which gains variables_index as NA.

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 of x, y and z are present (#98). rho is a length on polar, cylindrical and spherical frames and was never converted, while the metadata was updated to claim the new unit. Angular axes remain set_unit_angle()’s to convert. Where the coordinate system is unknown a 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() and set_sampling_rate() no longer re-inject a keypoint column and overwrite variables_what with it (#96). A frame given a custom identity such as id was 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() and set_variables_event() declare the variable roles, each with get_, add_ and remove_ 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, time and the spatial columns numeric (#79). Counterpart to validate_anievent().

  • is_spatial() and ensure_is_spatial() test the columns named in variables_where, which the is_cartesian*() family does not — those look at column names only (#79).

Changed

  • set_metadata() no longer writes the variables_* 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 when variables_what names a column that is not in the data, as it already did for variables_when and variables_where (#77).

  • aniframe and anievent recognise the same identity variables — model, individual, subject, track, keypoint — ordered coarse to fine (#77).

  • spec_version moves to aniframe = "1.1.0" and anievent = "0.2.0".

Removed

  • as_aniframe() no longer adds a keypoint = "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’s aniframe_kin was dropped by the first filter(), mutate() or [. Verbs that were never covered — distinct(), joins, bind_rows() — still drop it.

  • An anievent no longer claims spatial properties it cannot have, such as a BORIS export announcing origin: bottom_left (#73). unit_angle, origin and reference_frame gain 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 of aniframe: it shares the metadata substrate but does not inherit from it. Required columns are channel, type, label, start and stop, with identity columns travelling via variables_what and an optional modifiers list-column. type is 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() and as_anievent() construct the class, is_anievent() and ensure_is_anievent() test it, and validate_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 from as_anievent(), which casts data that is already bout-shaped. Methods for data.frame and aniframe; 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_event metadata field — a named list list(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_version metadata 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() and set_sampling_rate() are S3 generics with aniframe and anievent methods. On an anievent the calibration factor applies to start and stop rather than time.

  • as_aniframe() auto-detects observation as a temporal grouping column, alongside session and trial.

  • 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 same channel overlap within a group. A warning rather than an error: overlap is normal BORIS output and the long format handles it natively.

  • set_metadata() accepts partial variables_event input — supplying only state or only point is fine, and NA or 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 the bottom_left and top_left origin conventions, reflecting y around the recorded frame height (#52).

  • set_y_height() sets the y-axis frame height that set_origin() uses, validated against the data range, and a y_height metadata field to hold it. Readers populate it from the source; as_aniframe() falls back to max(y) when missing, and never overwrites an existing value.

  • A connections metadata field for skeletons and other variable-level networks (#6). Stored as a named list keyed by the relevant identity or temporal variable — typically keypoint, but individual for social networks — with each entry a 2-column from/to tibble whose order is preserved. Manage it with set_connections(), get_connections(), add_connections() and remove_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 when start_datetime is 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 columns phi and theta whenever they are present, so polar, cylindrical and spherical coordinates stay consistent with the declared unit_angle (#21). These were previously assumed to be radians and left alone. The signature becomes set_unit_angle(data, to_unit, cols = NULL), matching set_unit_time(); pass cols only for additional non-spatial angular columns. Positional callers need to swap their arguments.

  • The print summary is driven by variables_what and variables_when rather than hard-coded column names (#51). Custom identity and temporal variables such as track and model now 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 filename metadata field accepts a character vector of length one or more, for readers that load from multiple source files (#34).

  • Renamed the point_of_reference metadata field to origin, with permitted values "bottom_left" and "top_left".

Deprecated

  • point_of_reference as 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 the rho + phi signature first, so cylindrical data is no longer reduced to cartesian_1d by its z column. Cylindrical spatial columns are now ordered rho, phi, z (#43).

anicore 0.4.1 (as aniframe)

Fixed

anicore 0.4.0 (as aniframe)

Added

  • variables_what, variables_when and variables_where arguments to as_aniframe() and example_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.

Changed

  • Identity and temporal variables are coerced to integer, with the exception of time, which stays numeric.
  • time is required. A frame without it is no longer a valid aniframe.
  • An unrecognised set of spatial columns is accepted, with coordinate_system recorded as "unknown", rather than refused.

anicore 0.3.5 (as aniframe)

Added

  • A NEWS.md file, to track changes to the package.
  • Smaller units: ns (nanosecond), us (microsecond), nm (nanometre) and um (micrometre).

Removed

  • get_trackball_calibration_factor(), following the move of trackball handling to aniread.

anicore 0.3.4 (as aniframe)

Removed

  • Trackball calibration. It reads hardware output rather than describing a frame, and belongs with the readers in aniread.

anicore 0.3.3 (as aniframe)

Fixed

  • NA and NaN handling in metadata and coercion.
  • An NA datetime is no longer given a class, which had made empty start_datetime values print oddly.

anicore 0.3.2 (as aniframe)

Changed

  • "cartesian" is no longer a permitted coordinate_system value; the dimensioned forms cartesian_1d, cartesian_2d and cartesian_3d replace it.
  • Metadata printing and the start_datetime class were tidied.

anicore 0.3.1 (as aniframe)

Removed

  • add_centroid(). Deriving a centroid is a metric rather than a property of the frame, and belongs in animetric.

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

Removed

  • The coordinate transformations map_to_cartesian(), map_to_polar(), map_to_cylindrical() and map_to_spherical(), the component converters cartesian_to_rho(), cartesian_to_phi(), cartesian_to_theta(), polar_to_x(), polar_to_y() and spherical_to_z(), the rigid transforms rotate_coords(), translate_coords() and transform_to_egocentric(), and the angle helpers wrap_angle(), unwrap_angle(), diff_angle() and circ_difference(). All are available from anispace.

anicore 0.2.5 (as aniframe)

Fixed

  • map_to_cartesian() no longer adds a z column when converting from polar coordinates.

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

Changed

  • model is recognised as an identity column, alongside individual and keypoint.

anicore 0.2.2 (as aniframe)

Added

Changed

anicore 0.2.1 (as aniframe)

Added

  • Unit handling: set_unit_space(), set_unit_angle(), set_unit_time() and set_sampling_rate().
  • Coordinate transformations: map_to_cartesian(), map_to_polar(), map_to_cylindrical() and map_to_spherical(), with the component converters cartesian_to_rho(), cartesian_to_phi(), cartesian_to_theta(), polar_to_x(), polar_to_y() and spherical_to_z().
  • Rigid transformations: rotate_coords(), translate_coords() and transform_to_egocentric().
  • Angle handling: deg_to_rad(), rad_to_deg(), constrain_angles_radians(), circ_difference() and diff_angle().
  • ensure_is_aniframe(), a guard for functions that require an aniframe.
  • Trackball calibration, via get_trackball_calibration_factor().

anicore 0.2.0 (2025-10-23, as aniframe)

Changed

  • tbl_sum.aniframe() is registered as an S3 method rather than exported.

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