We’ll use test data supplied by SR Research (which I found in the cili package for Python). The test data can be found in the extdata/ directory of the package.

require(eyelinker)
require(dplyr)

#Look for file 
fpath <- system.file("extdata/mono500.asc.gz",package="eyelinker")

asc files can be gigantic, so it’s a good idea to compress them, R doesn’t mind (here they’re compressed in gzip format, hence the .gz).

To read the file just call read.asc:

dat <- read.asc(fpath)

dat is a list with fields:

names(dat)
## [1] "raw"    "msg"    "sacc"   "fix"    "blinks" "info"

Meta-data

Some meta-data can be read from the “SAMPLES” lines in the asc file.

str(dat$info)
## List of 8
##  $ velocity  : logi FALSE
##  $ resolution: logi FALSE
##  $ htarg     : logi FALSE
##  $ input     : logi FALSE
##  $ left      : logi TRUE
##  $ right     : logi FALSE
##  $ cr        : logi TRUE
##  $ mono      : logi TRUE

Here we have a monocular recording of the left eye.

What are the units?

Depending on how the Eyelink is set up, positions can be reported in pixels or degrees, relative to the head, the screen or the camera. I’m guessing the most common case is to use screen coordinates, but I don’t know whether the coordinate system is stored in a predictable manner in asc files. If you have any suggestions please email me. I’ll assume you know what the relevant units are.

Raw data

The raw data has a simple structure:

raw <- dat$raw
head(raw,3)
##      time    xp    yp   ps cr.info block
## 1 7196720 512.8 394.5 1063     ...     1
## 2 7196722 513.3 395.4 1064     ...     1
## 3 7196724 513.9 397.0 1066     ...     1

In a binocular recording the raw data has the following structure:

dat.bi <- system.file("extdata/bino1000.asc.gz",package="eyelinker") %>% read.asc

head(dat.bi$raw,3)
##      time   xpl   ypl  psl   xpr   ypr  psr cr.info block
## 1 7427362 502.3 411.1 1103 512.8 395.9 1094   .....     1
## 2 7427363 500.2 411.7 1103 511.7 395.6 1094   .....     1
## 3 7427364 498.0 412.3 1104 510.5 394.5 1094   .....     1

The variables are the same as before, with the addition of a postfix corresponding to the eye (i.e. xpl is the x position of the left eye).

Tidying up raw data

It’s sometimes more convenient for plotting and analysis if the raw data are in “long” rather than “wide” format, as in the following example:

library(tidyr)
## 
## Attaching package: 'tidyr'
## The following object is masked from 'package:magrittr':
## 
##     extract
raw.long <- dplyr::select(raw,time,xp,yp,block) %>% gather("coord","pos",xp,yp)
head(raw.long,2)
##      time block coord   pos
## 1 7196720     1    xp 512.8
## 2 7196722     1    xp 513.3
tail(raw.long,2)
##         time block coord   pos
## 3667 7205382     4    yp 364.6
## 3668 7205384     4    yp 364.9

The eye position is now in a single column rather than two, and the column “coord” tells us if the valuye corresponds to the x or y position. The benefits may not be obvious now, but it does make plotting the traces via ggplot2 a lot easier:

require(ggplot2)
## Loading required package: ggplot2
raw.long <- mutate(raw.long,ts=(time-min(time))/1e3) #let's have time in sec. 
ggplot(raw.long,aes(ts,pos,col=coord))+geom_point()

In this particular file there are four separate recording periods, corresponding to different “blocks” in the asc file, which we can check using:

ggplot(raw.long,aes(ts,pos,col=coord))+geom_line()+facet_wrap(~ block)

Saccades

The Eyelink automatically detects saccades in an online fashion. The results are converted to a data.frame:

sac <- dat$sac
head(sac,2)
##     stime   etime dur   sxp   syp   exp   eyp ampl  pv eye block
## 1 7197124 7197134  12 513.8 395.9 509.2 380.4 0.46  57   L     1
## 2 7197510 7197546  38 510.8 383.0 735.8 373.2 6.38 313   L     1

Each line corresponds to a saccade, and the different columns are:

In the binocular case, we have:

head(dat.bi$sac,3)
##     stime   etime dur   sxp   syp   exp   eyp ampl  pv eye block
## 1 7428104 7428157  54 494.3 401.6 224.1 367.4 7.68 400   L     1
## 2 7428104 7428157  54 508.4 403.5 245.3 389.6 7.43 348   R     1
## 3 7430690 7430726  37 518.7 394.2 805.6 396.3 8.08 442   R     2

The only difference is in the “eye” column, which tells you in which eye the saccade was first recorded.

Labelling saccades in the raw traces

To see if the saccades have been labelled correctly, we’ll have to find the corresponding time samples in the raw data.

The easiest way to achieve this is to view the detected saccades as a set of temporal intervals, with endpoints given by stime and etime. We’ll use function “%In%” to check if each time point in the raw data can be found in one of these intervals.

Sac <- cbind(sac$stime,sac$etime) #Define a set of intervals with these endpoints
#See also: intervals package
raw <- mutate(raw,saccade=time %In% Sac)
head(raw,3)
##      time    xp    yp   ps cr.info block saccade
## 1 7196720 512.8 394.5 1063     ...     1   FALSE
## 2 7196722 513.3 395.4 1064     ...     1   FALSE
## 3 7196724 513.9 397.0 1066     ...     1   FALSE
mean(raw$saccade)*100 #6% of time samples correspond to saccades
## [1] 6.161396

Now each time point labelled with “saccade==TRUE” corresponds to a saccade detected by the eye tracker.

Let’s plot traces again:

mutate(raw.long,saccade=time %In% Sac) %>% filter(block==1) %>% ggplot(aes(ts,pos,group=coord,col=saccade))+geom_line()

Fixations

Fixations are stored in a very similar way to saccades:

fix <- dat$fix
head(fix,3)
##     stime   etime dur   axp   ayp  aps eye block
## 1 7196724 7197122 400 515.1 396.3 1050   L     1
## 2 7197136 7197508 374 512.6 384.3  988   L     1
## 3 7197548 7197696 150 734.0 375.8  918   L     1

Each line is a fixation, and the columns are:

Labelling fixations in the raw traces

We can re-use essentially the same code to label fixations as we did to label saccades:

Fix <- cbind(fix$stime,fix$etime) #Define a set of intervals 
mutate(raw.long,fixation=time %In% Fix) %>% filter(block==1) %>% ggplot(aes(ts,pos,group=coord,col=fixation))+geom_line()

We can get a fixation index using whichInterval:

mutate(raw,fix.index=whichInterval(time,Fix)) %>% head(4)
##      time    xp    yp   ps cr.info block saccade fix.index
## 1 7196720 512.8 394.5 1063     ...     1   FALSE        NA
## 2 7196722 513.3 395.4 1064     ...     1   FALSE        NA
## 3 7196724 513.9 397.0 1066     ...     1   FALSE         1
## 4 7196726 513.2 397.6 1064     ...     1   FALSE         1

Let’s check that the average x and y positions are correct:

raw <- mutate(raw,fix.index=whichInterval(time,Fix))
fix.check <- filter(raw,!is.na(fix.index)) %>% group_by(fix.index) %>% summarise(axp=mean(xp),ayp=mean(yp)) %>% ungroup
head(fix.check,3)
## Source: local data frame [3 x 3]
## 
##   fix.index      axp      ayp
##       (int)    (dbl)    (dbl)
## 1         1 515.0930 396.2695
## 2         2 512.6283 384.2984
## 3         3 734.0107 375.8333

We grouped all time samples according to fixation index, and computed mean x and y positions.

We verify that we recovered the right values:

all.equal(fix.check$axp,fix$axp)
## [1] "Mean relative difference: 4.48531e-05"
all.equal(fix.check$ayp,fix$ayp)
## [1] "Mean relative difference: 7.397594e-05"

Messages

The last data structure we need to cover contains messages:

head(dat$msg)
##       time                                                          text
## 1 12134177                                                   -8 SYNCTIME
## 2 12134177 -8 !V DRAW_LIST ../../runtime/dataviewer/js/graphics/VC_1.vcl
## 3 12134177     -7 !V IAREA FILE ../../runtime/dataviewer/js/aoi/IA_1.ias
## 4 12152026                                               -8 blank_screen
## 5 12153648                                                   -3 SYNCTIME
## 6 12153648 -2 !V DRAW_LIST ../../runtime/dataviewer/js/graphics/VC_2.vcl
##   block
## 1     1
## 2     1
## 3     1
## 4     1
## 5     2
## 6     2

The lines correspond to “MSG” lines in the original asc file. Since messages can be anything read.asc leaves them unparsed. If you’re interested in certain event types (e.g., time stamps), you’ll have to parse msg$text yourself. Here for example we extract all messages that contain the words “Saccade_target”:

library(stringr)
filter(dat$msg,str_detect(text,fixed("blank_screen"))) 
##       time             text block
## 1 12152026  -8 blank_screen     1
## 2 12175944  -5 blank_screen     2
## 3 12198433 -15 blank_screen     3
## 4 12223186 -10 blank_screen     4