Skip to content

eyeris

3.3.0Package record

Flexible, Extensible, & Reproducible Pupillometry Preprocessing

0packages depend
4.1Kdownloads / year
61.1%test coverage
13/13flavors OK

Overview

About

Pupillometry offers a non-invasive window into the mind and has been used extensively as a psychophysiological readout of arousal signals linked with cognitive processes like attention, stress, and emotional states [Clewett et al. (2020) doi:10.1038/s41467-020-17851-9; Kret & Sjak-Shie (2018) doi:10.3758/s13428-018-1075-y; Strauch (2024) doi:10.1016/j.tins.2024.06.002]. Yet, despite decades of pupillometry research, many established packages and workflows to date lack design patterns based on Findability, Accessibility, Interoperability, and Reusability (FAIR) principles [see Wilkinson et al. (2016) doi:10.1038/sdata.2016.18]. 'eyeris' provides a modular, performant, and extensible preprocessing framework for pupillometry data with BIDS-like organization and interactive output reports [Esteban et al. (2019) doi:10.1038/s41592-018-0235-4; Gorgolewski et al. (2016) doi:10.1038/sdata.2016.44]. Development was supported, in part, by the Stanford Wu Tsai Human Performance Alliance, Stanford Ric Weiland Graduate Fellowship, Stanford Center for Mind, Brain, Computation and Technology, NIH National Institute on Aging Grants (R01-AG065255, R01-AG079345), NSF GRFP (DGE-2146755), McKnight Brain Research Foundation Clinical Translational Research Scholarship in Cognitive Aging and Age-Related Memory Loss, American Brain Foundation, and the American Academy of Neurology.

Changes in 3.3.0

This minor release changes how missing pupil data is handled, broadens the range of eye trackers eyeris can ingest, and refreshes every diagnostic figure. The headline **breaking change** is that interpolate() no longer interpolates across arbitrarily long gaps: stretches of consecutive missing samples longer than the new max_gap_ms limit (default 250 ms, following Kret & Sjak-Shie, 2018) are now left as NA, and the downstream steps that cannot operate on missing data (lpfilt(), downsample(), bin(), detrend()) now work *around* those gaps and restore them, rather than being skipped. New features include load_generic(), a tracker-agnostic loader that lets non-EyeLink data enter the pipeline (with glassbox() now accepting a pre-loaded eyeris object); a sampling-uniformity guardrail plus an automatic resample() step for hardware that silently *drops* samples instead of zero-filling them; spline detrending via detrend(method = "spline"); and simulate_eyeris()/sim_params() for generating seeded, synthetic, pipeline-compatible pupil data with known ground truth. Bug fixes restore the fitted detrend coefficients on the glassbox() output, repair per-run epoch CSV writing in bidsify() for both multi-run objects and run_num overrides, and resolve a multi-block crash on R >= 4.2. Under the hood, all diagnostic plotting migrated from base graphics to reaborn/ggplot2.

🚨 **Breaking changes & deprecations**

  • **BREAKING (#295)**: interpolate() now enforces a maximum gap duration for linear interpolation via a new max_gap_ms parameter. Gaps of consecutive missing (NA) samples that are **longer** than max_gap_ms are now left as NA rather than interpolated over. The default of 250 ms follows the recommendation of Kret & Sjak-Shie (2018), who advise against interpolating across long gaps where linear interpolation is unlikely to reflect the true underlying pupil signal. Previously (eyeris ≤ 3.2.0), **all** gaps were interpolated regardless of duration. This is a change in default behavior and may affect downstream results; eyeris now prints a console notice (at most once per glassbox() run) when the limit is in effect. To restore the previous behavior of interpolating across all gaps, set interpolate = list(max_gap_ms = Inf) in glassbox() (or max_gap_ms = Inf when calling interpolate() directly). The threshold can also be customized, e.g., glassbox(interpolate = list(max_gap_ms = 100)). The limit is specified in milliseconds and is internally converted to a number of samples using each recording's own sampling rate, so it behaves consistently across sampling frequencies, by @shawntz in #318.
  • **ENH (#295)**: The downstream glassbox() steps that cannot operate on missing data — low-pass filtering (lpfilt()), downsampling (downsample()), binning (bin()), and detrending (detrend()) — now work *around* the gaps that interpolate(max_gap_ms) intentionally leaves as NA. They filter/resample/fit over the available data (filling temporarily where needed) and then restore the gaps as NA, so the long-gap NAs are preserved through to the final preprocessed output instead of causing these steps to be skipped. When interpolation has not been run upstream, the filter/resample steps still raise the usual "interpolate first" error, by @shawntz in #318.
  • **ENH (#295)**: lpfilt() and downsample() now emit a warning when they filter over gaps longer than max_gap_ms (left as NA by interpolate()). Because a low-pass/anti-aliasing filter cannot run on missing data, these gaps are temporarily filled and then masked back to NA; the filter can therefore slightly bias the valid pupil samples immediately adjacent to each gap toward the (near-linear) interpolated values — analogous to the edge artifacts seen at the start/end of a filtered recording, but repeated at every long gap. The warning lets you decide whether to disable filtering and/or downsampling (lpfilt = FALSE and/or downsample = FALSE) to avoid introducing this systematic bias, at the cost of retaining more high-frequency content. The console warning fires at most once per glassbox() run, and the same information is recorded as a **"Data Quality Notes"** section in the HTML preprocessing report so it is captured as part of the data provenance, by @shawntz in #318.

🚀 New features

  • **NF**: **detrend() now supports spline detrending via a new method argument.** Detrending previously only removed a straight-line (linear) trend, which cannot capture the slow, *nonlinear* drift often present in longer recordings. detrend() now accepts method = "linear" (the default, preserving the previous behavior of regressing pupil size on time) or method = "spline", which removes a smooth, potentially nonlinear trend by fitting a natural cubic spline basis of time (splines::ns(time, df = spline_df)). The flexibility of the fitted spline is controlled by the new spline_df argument (degrees of freedom; default 5). Both models return the fitted trend and the residuals exactly as before, so all downstream steps, plots, and reports are unchanged aside from the trend shape. Enable it in the pipeline with glassbox(detrend = list(method = "spline")) (optionally spline_df = ...), or call detrend(method = "spline") directly, by @shawntz in #349.
  • **NF (#299)**: **Added load_generic(), a tracker-agnostic data loader for non-EyeLink eye trackers.** Until now, load_asc() (for SR Research EyeLink .asc files) was the only implemented loader, which prevented researchers using other eye trackers from running their data through eyeris. load_generic() accepts three standardized data frames — pupil (raw samples: timestamp + pupil size, optionally gaze eye_x/eye_y), events (timestamp + message text), and blinks (blink start/end intervals) — plus an optional fourth gaze data frame for the less common case where gaze is exported separately from pupil size (joined onto pupil by timestamp). These are assembled into the exact same eyeris S3 object that load_asc() produces, making the result a drop-in input to glassbox(), epoch(), bidsify(), and the plotting methods. Column names are configurable via the mapping argument; sampling rate is inferred from the timestamps when not supplied; timestamps may be provided in milliseconds (default) or seconds (time_unit = "s"); and block/eye/pupil_type/screen-dimension metadata mirror load_asc(). Native readers for specific tracker formats can now be layered on top of this function incrementally, by @shawntz in #311.
  • **NF**: **glassbox() now accepts a pre-loaded eyeris object** (e.g., from load_generic()), automatically skipping the file-loading step and running the remaining pipeline on the object directly. This makes load_generic(...) |> glassbox() work as expected, and also means an already-loaded load_asc() object piped into glassbox() now processes correctly instead of raising an error, by @shawntz in #311.
  • **ENH (#300)**: Added a hardware-quirk guardrail that detects non-uniform sampling intervals in the input timeseries. Different eye trackers behave differently when pupil data is missing: EyeLink zero-fills, but some hardware silently *drops* samples, leaving gaps in the otherwise evenly spaced time vector. Because downstream pipeline steps (e.g., detransient(), lpfilt(), downsample()) assume a fixed sampling rate, such gaps can silently distort results. The new internal check_uniform_sampling_intervals() helper infers the expected inter-sample interval from the data (robust to dropped samples and sub-millisecond timestamp rounding), checks each recording segment independently so that legitimate between-block gaps are not flagged, and emits an informative warning estimating the number of dropped samples when irregular intervals are detected. It also cross-checks the data-derived interval against the device's reported sampling rate to catch *systematic* dropout (e.g., every Nth sample missing), which leaves a uniform but coarser grid that gap detection alone cannot see. The check is wired into load_asc() so the quirk is surfaced early, before any preprocessing begins, by @shawntz in #308.
  • **ENH (#320)**: Added an automatic resample() step that repairs the **time axis** of recordings whose hardware *drops* samples (instead of zero-filling) when pupil data is missing — the dropped-sample quirk surfaced by the #300 guardrail. When glassbox() detects irregular sampling intervals (gated on the robust check_uniform_sampling_intervals() detector from #300), it places each block onto the expected uniform sampling grid in two stages: (1) it anchors the grid on the first *reliable* regular interval (rather than the first timestamp), so early sub-period timing jitter doesn't offset the whole grid, and back-extends the grid to cover any earlier samples; then (2) it resamples the observed data onto that grid — linearly interpolating across short/jittered intervals, and inserting NA rows for any interval longer than the expected period (dropped samples), flagged in a new logical is_resampled column and left for the existing interpolate() step to fill. Because a gap is now represented as explicit missing data (never fabricated values), interpolate() decides how much of a missing span to fill according to its own policy (see the #295 max_gap_ms limit). This turns the "dropped-sample" problem into the ordinary "missing-value" problem the rest of the pipeline already handles. It runs **automatically and by default**, but only when needed: it is a guaranteed no-op for already-uniform data (e.g., EyeLink) and deliberately leaves high-rate recordings that report integer-millisecond timestamps untouched (so it never collapses genuine sub-millisecond samples). Opt out with glassbox(resample = FALSE). summarize_confounds() now also reports n_resampled/prop_resampled per block, by @shawntz in #320.
  • **NF**: **Added simulate_eyeris() and sim_params(), a seeded synthetic pupil data generator with known ground truth.** eyeris previously had no way to produce synthetic pupil data, making it difficult to write self-contained tests, build reproducible documentation examples, or demonstrate how a preprocessing step behaves without shipping and loading a real recording. sim_params() builds the signal-model parameter list, with every component independently toggle-able and validated: tonic baseline, slow drift, hippus, Hoeks & Levelt phasic responses, blinks (missing cores plus occlusion flank spikes), transient spikes, broadband noise, and optional line noise. simulate_eyeris() then generates the signal under a confined RNG (withr::with_seed(), so the global .Random.seed is left untouched) and delegates to the same internal object constructor used by load_asc(), so the returned eyeris object is structurally identical to a real recording and flows through the entire pipeline unchanged (deblink() … zscore(), glassbox(), plot(), epoch(), summarize_confounds()). This addition is purely additive and changes no existing behavior, by @shawntz in #351.
Show all changes in 3.3.0Hide the rest of the changes in 3.3.0

🐛 Bugs fixed

  • **FF**: Fixed glassbox(detrend = TRUE) silently dropping the fitted detrend coefficients. The per-block detrend coefficients are computed inside pipeline_handler() and stored on the internal per-block object, but glassbox()'s block-recombine step copied back the timeseries, params, decimated sample rate, and pre-decimation data — never $detrend_coefs. As a result, $detrend_coefs was always absent from the object returned by glassbox(), even though it was correctly populated when calling detrend() directly. The recombine step (in both the main and per-eye/binocular paths) now preserves $detrend_coefs per block, so the fitted intercept/slope are available on the glassbox() output for provenance and inspection. It also clears any $detrend_coefs inherited from a pre-loaded input object at the start of each run, so the returned object retains only the coefficients produced in that run (and none survive when detrending is disabled or fails). The detrend_fitted_values column that drives the detrend diagnostic plots/reports was already preserved and is unaffected, by @shawntz in #350.
  • **FF**: Fixed an error in summarize_confounds() (via get_block_numbers()) that aborted with the condition has length > 1 when processing multi-block data on R ≥ 4.2. The internal is.na() guard now handles vectors of block numbers, so multi-block recordings flow through the confound summary step correctly. This latent bug was surfaced by the new multi-block load_generic() tests, by @shawntz in #311.
  • **FF**: Fixed multi-run epoch CSV files (*_desc-preproc_pupil_epoch-<label>.csv) not being written for each run. When a subject's data contained multiple blocks (runs), bidsify() built the per-epoch preprocessed output filename from the run_num argument — which is intentionally ignored (NULL) for multi-block objects — and packed the epoch label directly into the desc field without the run- and epoch- BIDS entities. As a result, every run wrote to the *same* filename and silently overwrote the previous one (last-run-wins), so the expected ..._run-NN_desc-preproc_pupil_epoch-<label>.csv files never materialized for multi-run inputs. The multi-run writer now derives the run number from each block and routes the epoch label through the BIDS filename builder, exactly mirroring the single-run path. The DuckDB/parquet outputs were already keyed by the correct per-run number and are unchanged — only the CSV filenames were affected. As part of the same fix, the multi-run raw-timeseries writers now key the run-NN token off each block's own block number (rather than a positional index, which mislabeled runs when blocks were not numbered sequentially), and several internal epoch/baseline metadata helpers no longer receive verbose in their block_name argument position (which crashed bidsify() under verbose = FALSE whenever epochs were present), by @alicexue and @shawntz in #322.
  • **FF**: Fixed the run_num override being ignored for epoch CSV and per-run metadata.json outputs when preprocessing separate single-block .asc files into a shared bids_dir. In that workflow the run number is not forced at load_asc(), so every file loads as block_1 and the per-file run_num is instead passed to bidsify(); two outputs ignored the block_1 → block_<run_num> rename and collapsed onto run-01, so every run's *_desc-preproc_pupil_epoch-<label>.csv overwrote itself and every run's *_metadata.json ended up holding the last run's source_file/call_stack. The epoch-block rename now writes the renamed block key back onto the epoch list (previously it updated only a local copy of the block-name vector), and per-run metadata regeneration in make_report() is scoped to the blocks present in the current object rather than every run-XX directory found on disk — so sibling runs written in earlier loop iterations keep their own metadata and are only read back for the combined report, by @shawntz in #334.

🔧 Under the hood

  • **ENH**: Migrated all diagnostic plotting from base graphics to reaborn — an R port of the 'Python' 'seaborn' library built on 'ggplot2'. plot.eyeris(), the gaze heatmap (plot_gaze_heatmap()), the binocular-correlation panels (plot_binocular_correlation()), the detrend overlay, the pupil-size distribution histograms, the progressive-summary report plot, and the per-epoch gallery figures are now rendered with reaborn and composited with patchwork, giving the interactive HTML reports a consistent, publication-quality seaborn aesthetic while preserving eyeris's colour palette and axis labels. Each plotting function now builds ggplot objects and prints them to the active graphics device, so the existing report/gallery capture flow (png()/jpeg() → draw → dev.off()) is unchanged and every public plotting API keeps its signature. Multi-panel time series previews are composited with patchwork (replacing base par(mfrow) layouts), missing-sample gaps are shaded as contiguous regions, and the binocu

The NEWS section is longer than shown here.

From NEWS.md in the release

Install

Health

CRAN checks
13OK
Slowest check: 10.8 min · r-oldrel-macos-x86_64
Status changes
  • OK13 OK2026-09-30
  • ERROR12 OK, 1 ERROR2026-09-04
  • OK13 OK2026-08-05
  • NOTE12 OK, 1 NOTE2026-08-01
  • OK13 OK2026-06-20
Show 9 earlier changesHide 9 earlier changes
  • ERROR12 OK, 1 ERROR2026-06-19
  • OK13 OK2026-06-09
  • ERROR12 OK, 1 ERROR2026-06-08
  • OK13 OK2026-05-24
  • ERROR12 OK, 1 ERROR2026-05-17
  • OK13 OK2026-05-15
  • ERROR12 OK, 1 ERROR2026-05-13
  • OK12 OK2026-04-25
  • ERROR11 OK, 2 NOTE, 1 ERROR2026-03-10
Advisories

none known

Documentation

In release 3.3.0
READMEYes · 2,687 wordsVignettesYes · 10 · dynamicNEWSYes · 100% structured · mentions 3.3.0Citation fileYes · cites a publicationWebsiteYes · pkgdown
Help pages
191 (152 internal)includes a package overview page
Examples that run
63%24 run in a plain check · 6 only when donttest examples are enabled (R CMD check --as-cran) · 8 never run
Exported functions with examples
89%
Documented parameters
99%
Return-value docs
100%
Help pages citing references
1%

Downloads

4.1K
CRAN downloads in the past year
Rank #7,868 · ~11/day · ~340/mo
Daily download trend is not available in this view yet.
46630 days
1.2K90 days
4.1K1 year
Compare downloads with other packages →
Also on125 r2u3 autocran

Repository

9Stars
6Forks
0Open issues
0Open PRs
30Releases
2,123Commits
7Commit identities
eye-trackingpipelinespreprocessingpupillometryr-packagereproducible-scienceworkflowscran
2,123 commits · Last activity 2026-10-11 · 0% stars, 30d · Discussions: 6 posts
Responsiveness
How these are sampled
PRs merged (all time)
98%
Median days to close, 50 most recently updated closed issues
6
Median days to close, 50 most recently updated closed PRs
0

Stars over time

2025-06-20 · 52026-09-02 · 9
PracticesBeta

18 development-tooling and community-health practices detected across 6 families in the upstream repository

Checks run against github.com/shawntz/eyeris on 2026-10-21.

Continuous integration (2)
GitHub ActionsR CMD check workflow

The R CMD check workflow runs on Linux, macOS and Windows, and on R-devel

Governance and community (5)
Code of conductContributing guidePR templateFunding

Funding links: GitHub Sponsors, Patreon, Buy Me a Coffee

The repository owner accepts GitHub Sponsors

Docs source (4)
README.Rmdpkgdown

GitHub Pages site last deployed 2026-10-07 · The repository has a site deploy workflow

pkgdown site last built 2026-10-07

Reproducibility and dev environment (2)
data-raw/Makefile
Show all practicesHide 2 practices
CRAN release process (2)
cran-comments.mdCRAN-SUBMISSION
Lint, format, editor (3)
Lint workflowAirRStudio project
How this is detected·Detection ruleset v4 (2026-10-20)
AI coding toolsBeta

This repository shows signs of AI coding tools.

  • GitHub Copilotfirst seen 2025-12-22
    Pull request: opened by its own account
    Commits: 14 by its own account · at least 1 crediting it in the message
  • Cursorfirst seen 2026-06-03
    Pull request it wrote: opened by a project member
    Commits: 5 by its own account
  • Claude Codefirst seen 2026-06-05
    Named in .Rbuildignore: .claude
    Pull request it wrote: opened by a project member
    Commits: at least 126 crediting it in the message
Models named in its commits
  • Claude Opus 4.893 commits · Jun to Jul 2026
  • Claude Opus 5.57 commits · Oct 2026

Some of these counts are a minimum, because the search stopped before the oldest commits.

Counts are commits, not lines of code. Commits by a tool's accounts count once across all of its accounts. Commits crediting it are the largest count among its credit lines, a minimum.How this is detected·Detection ruleset v4 (updated 2026-09-27)

Dependencies

Declared dependencies
27 external dependencies (excludes base and recommended)

Installing eyeris adds 72 packages (37 with compiled code, 70 MB of source tarballs). Heaviest: stringi 11 MB, ggplot2 6.0 MB, data.table 5.7 MB.

Depends (1)
R >= 4.1
LinkingTo (0)
none
Enhances (0)
none
Reverse dependencies
0direct
0indirect

Nothing depends on this yet.

People & History

People (5)
Author, Maintainer since 1.0.0 (2025)
Contributor · added in 1.1.0
Contributor · added in 1.1.0
Contributor · added in 1.2.0
Contributor · added in 1.1.0
Listed in earlier versions (10)
Show 8 of 10 peopleHide 8 people
NIH National Institute on Aging R01 AG065255
no longer listed · 1.1.0
Stanford Center for Mind, Brain, Computation and Technology
no longer listed · 1.1.0 to 1.2.1
Stanford Ric Weiland Graduate Fellowship
no longer listed · 1.1.0 to 1.2.1
Stanford Wu Tsai Human Performance Alliance
no longer listed · 1.1.0 to 1.2.1
American Academy of Neurology
no longer listed · 1.2.0 to 1.2.1
American Brain Foundation
no longer listed · 1.2.0 to 1.2.1
McKnight Brain Research Foundation Clinical Translational Research Scholarship in Cognitive Aging and Age-Related Memory Loss
no longer listed · 1.2.0 to 1.2.1
NIH National Institute on Aging R01-AG065255
no longer listed · 1.2.0 to 1.2.1
Releases (12)

Pick two releases to compare their code metrics.

  • 3.3.0Latest
    record
    2026-07-31 · current release · diff ↗ ·
    added 4 exports
    load_generic, resample, sim_params, simulate_eyeris
  • 3.2.0
    record
    2026-06-19 · diff ↗ ·
    added 1 export
    boilerplate
  • 3.1.0
    record
    2026-06-05 · diff ↗
  • R
    R 4.6.0 released · 2026-04-24
  • 3.0.1
    record
    2025-10-07 · diff ↗
  • 3.0.0
    record
    2025-09-17 · diff ↗ ·
    added 12 exports
    eyeris_db_collect, eyeris_db_connect, eyeris_db_disconnect, eyeris_db_list_tables, eyeris_db_read, eyeris_db_reconstruct_from_chunks, eyeris_db_split_for_sharing, eyeris_db_summary, eyeris_db_to_chunked_files, eyeris_db_to_parquet, process_chunked_query, read_eyeris_parquet
  • 2.1.1
    record
    2025-07-26 · diff ↗
  • 2.1.0
    record
    2025-07-22 · diff ↗ ·
    added 2 exports
    eyelink_asc_binocular_demo_dataset, plot_binocular_correlation
  • 2.0.0
    record
    2025-07-03 · diff ↗ ·
    added 5 exports
    bin, downsample, eyeris_color_palette, plot_gaze_heatmap, summarize_confounds
  • 1.2.1
    record
    2025-06-13 · diff ↗ ·
    added 1 export
    eyelogger
  • 1.2.0
    record
    2025-06-07 · diff ↗
  • 1.1.0
    record
    2025-04-24 · diff ↗ ·
    added 1 export
    eyelink_asc_demo_dataset
  • R
    R 4.5.0 released · 2025-04-11
  • 1.0.0
    record
    2025-03-31
  • R
    R 4.4.0 released · 2024-04-24

Package metadata

First published
2025-03-31
Total releases
12 / 1 yrs
License
MIT + file LICENSE OSI
Minimum R
≥ 4.1
Download size
3.5 MB

Cite

Cite this package

Run in R for the authors' entry:

citation("eyeris")

The package provides a citation file, so citation() returns the authors' own entry.

It points to 10.1101/2025.06.01.657312.

Cite the R Observatory

For a number measured here: a download total, a coverage figure, an archival date.

APA

Balamuta, J. J. (2026). R Observatory: Metrics for eyeris version 3.3.0 [Data set]. HJJB, LLC. Data release v2026-10-23. https://doi.org/10.5281/zenodo.21843040

From data release v2026-10-23, which the citation names so these numbers can be found later. More on citing and the projects behind them.

Report a problem with this page →

Privacy choices

These apply to this browser and are stored on this device only. Nothing about your choice is sent to us.

Read the privacy policy