# Building Your Own Custom Pipeline Extensions

One key strength of the preprocessing framework within `eyeris` is its modularity.

While we encourage most users to use the `glassbox()` function for simplicity and reproducibility, advanced users can create custom preprocessing steps that seamlessly integrate into the pipeline.

This vignette walks you through the structure required to write your own `eyeris`-compatible preprocessing functions.

## 🧩 How the Pipeline Works

Under the hood, each preprocessing function in `eyeris` is a wrapper around a core operation that gets tracked, versioned, and stored using the `pipeline_handler()`.

Custom pipeline steps must conform to the `eyeris` protocol for maximum compatibility with the downstream functions we provide.

Following the `eyeris` protocol also ensures: - all operations follow a predictable structure, and - that new pupil data columns based on previous operations in the chain are able to be dynamically constructed within the core `timeseries` data frame.

For instance: `pupil_raw -> pupil_raw_deblink -> pupil_raw_deblink_detransient -> ...`

If you’re unfamiliar with how these columns are structured and tracked, first check out the companion vignette: 📦 Anatomy of an `eyeris` Object.

## 🛠️Creating a Custom Extension for `eyeris`

Let’s say you want to write a new `eyeris` extension function called `winsorize()` to apply winsorization to extreme pupil values.

### 1) Write the core operation function

This function should accept a data frame `x`, a string `prev_op` (i.e., the name of the previous pupil column), and any custom parameters.

#### To illustrate:

    winsorize_pupil <- function(x, prev_op, lower = 0.01, upper = 0.99) {
      vec <- x[[prev_op]]
      q <- quantile(vec, probs = c(lower, upper), na.rm = TRUE)
      vec[vec < q[1]] <- q[1]
      vec[vec > q[2]] <- q[2]
      vec
    }

### 2) Create the wrapper using the `eyeris::pipeline_handler()`

The `pipeline_handler()` enables your function to automatically:

- track your function within the `eyeris` list object’s `params` field,
- append a new column to each block within the `timeseries` list of data frames, and
- update the object’s `latest` pointer.

#### To illustrate:

    #' Winsorize pupil values
    #'
    #' Applies winsorization to extreme pupil values within each block.
    #'
    #' @param eyeris An `eyeris` object created by [load_asc()].
    #' @param lower Lower quantile threshold. Default is 0.01.
    #' @param upper Upper quantile threshold. Default is 0.99.
    #'
    #' @return Updated `eyeris` object with new winsorized pupil column.
    winsorize <- function(eyeris, lower = 0.01, upper = 0.99) {
      pipeline_handler(
        eyeris,
        winsorize_pupil,
        "winsorize",
        lower = lower,
        upper = upper
      )
    }

1. Here, the first argument is always the `eyeris` class object.
2. Then, is the name of the function (the one you created in step 1 above).
3. Third, is the internal label you want `eyeris` to refer to (i.e., the one for the column name, plots, etc.).
4. Fourth position++ are the parameters you created after `prev_op` when creating the function `winsorize_pupil` in step 1.

## 🎉 And that’s it!

You should now be able to use your new function extension as a component within a new custom `eyeris` pipeline declaration. To illustrate:

    system.file("extdata", "memory.asc", package = "eyeris") |>
      eyeris::load_asc(block = "auto") |>
      eyeris::deblink(extend = 50) |>
      winsorize()

## 💪 Best Practices

- Use consistent naming: i.e., match your suffix (e.g. `"winsorize"`) to the column and log structure:
  - If you call `pipeline_handler(..., "winsorize")`
  - Then, your function names should reflect that:
    - the public facing `winsorize()` wrapper function, and
    - the private `winsorize_pupil()` logic implementation function.
  - You will then see `pupil_raw_*_winsorize` as the new output column!
- Respect previous steps: Your custom function should rely only on `prev_op`, not on any hardcoded column names!
- Return the expected data type: Be sure that you private function always returns a modified vector type, as the underlying `pipeline_handler()` is looking out for a vector it can transpose into the new column that will be added to the `timeseries` data frame within the resulting `eyeris` object.
- Test on individual blocks: Always try your private function logic on a single data block (e.g., `eyeris$timeseries[[1]]`) to debug before integrating it with the `eyeris` pipeline protocol.

## ✨ Summary

We hope you are now convinced at the power and extensibility the `eyeris` protocol enables! As we demonstrated here, with just a little bit of structure, you can create custom extension steps tailored to your specific analysis needs – all while preserving the reproducibility and organizational core principles `eyeris` was designed and built around.

If you’d like to contribute new steps to `eyeris`, feel free to open a pull request or discussion on GitHub!

## 📚 Citing `eyeris`

If you use the `eyeris` package in your research, please cite it!

Run the following in R to get the citation:

    citation("eyeris")
    #> To cite package 'eyeris' in publications use:
    #>
    #>   Schwartz S (2025). _eyeris: Flexible, Extensible, & Reproducible
    #>   Processing of Pupil Data_. R package version 1.0.0,
    #>   https://github.com/shawntz/eyeris/,
    #>   <https://shawnschwartz.com/eyeris/>.
    #>
    #> A BibTeX entry for LaTeX users is
    #>
    #>   @Manual{,
    #>     title = {eyeris: Flexible, Extensible, & Reproducible Processing of Pupil Data},
    #>     author = {Shawn Schwartz},
    #>     year = {2025},
    #>     note = {R package version 1.0.0, https://github.com/shawntz/eyeris/},
    #>     url = {https://shawnschwartz.com/eyeris/},
    #>   }