An introduction to eyetools

Matthew Ivory, Tom Beesley

The eyetools package is designed to provide a consistent set of functions that helps researchers perform straightforward, yet powerful, steps in the analysis of eye-data. The suite of functions will allow the user to go from relatively unprocessed data to trial level summaries that are ready for statistical analysis.

You can install eyetools using the following code:

install.packages('eyetools')

A quick note before starting: the following tutorial is written using only the dependencies contained within eyetools, so that means that if you are planning on following this tutorial through, you only need to install eyetools.

library(eyetools)

Data Manipulation and Preprocessing

Eyetools has built in example data that can be used while getting to grips with the package. For the purpose of this tutorial, we will start with a small dataset that contains binocular eye data from two participants (and six trials) from a simple contingency learning task (the data are from Beesley, Nguyen, Pearson, & Le Pelley, 2015)1. In this task there are two stimuli that appear simultaneously on each trial (to the left and right in the top half of the screen). Participants look at these cues and then make a decision by selecting an “outcome response” button.

Screenshot of a trial from Beesley et al. 2015

Let’s load in this binocular data and explore the format. As we can see, the dataset is formed of 31,041 rows and 7 variables

data(HCL, package = "eyetools")

dim(HCL)
## [1] 31041     7

To get a basic idea of the contents of the data, we can look at the first 10 observations in the data

We can see that our seven variables contain the participant identifier (pNum), the timepoint of the trial (time), the left eye x and y coordinates (left_x and left_y), the right eye coordinates (right_x and right_y), as well as a trial identifier (trial).

By default eyetools assumes that the resolution of eye data is in pixels (and a default screen size of 1920x1080), however the functions should work with any units (with no guarantee as it is not tested with units other than pixels).

head(HCL[HCL$pNum == 118,], 10)
## # A tibble: 10 × 7
##    pNum   time left_x left_y right_x right_y trial
##    <chr> <dbl>  <dbl>  <dbl>   <dbl>   <dbl> <dbl>
##  1 118       0   909.   826.   1003.    808.     1
##  2 118       3   912.   829.   1001.    812.     1
##  3 118       7   912.   826.   1010.    813.     1
##  4 118      10   908.   824.   1006.    807.     1
##  5 118      13   912.   824.   1005.    805.     1
##  6 118      17   912.   826.   1000.    802.     1
##  7 118      20   910.   826.    997.    806.     1
##  8 118      23   912.   825.   1005.    806.     1
##  9 118      27   911.   819.   1006.    808.     1
## 10 118      30   914.   821.   1013.    804.     1

eyetools functions can accept either single-participant data or multi-participant data. By default, most functions assume that data is of a single participant unless a participant identifier column called participant_ID is present,in which case the data is handled as multi-participant. In cases where participants are identified with a different variable, functions accept a parameter of participant_ID that takes a character string of the identifying column. In situations where participant_ID is not declared, and when duplicated non-consecutive trials are detected (that implies multi-participant data being input as single-participant), the function will error. This is a good point to then check your data structure, ensure that it is labelled correctly and ordered by trial and time.

Converting binocular data to monocular data

We need to combine the left and right eye x and y coordinates to get a single pair of [x,y] coordinates for each timepoint. The eyetools function combine_eyes() can do this for us. The method parameter gives the option to either “average” the two eyes, or we can use the “best_eye”. For “average”, the result is based on the average of the two eyes for each sample, or for samples where there is data from only a single eye, that eye is used. For “best_eye”, a summary of the proportion of missing samples is computed, and the eye with the fewest missing samples is used. Here we use the default parameter “average”.

combine_eyes() is one of the few core eyetools functions that doesn’t handle multi-participant/single-participant data differently. Primarily because of the function’s construction and its intended useage, it does not need to do so.

data <- combine_eyes(HCL)

The above code returns a flattened list of all participants data, and if we take a look at just one participant, we are returned with a dataframe that has x and y variables in place of the left_* and right_* variables. This is the data format needed by many of the other eyetools functions: time, x, y, and trial. The ordering of the variables should not matter, however most of the functions will impose an ordering in how the information is returned.

head(data) # participant 118
##   pNum time trial        x        y
## 1  118    0     1 955.8583 816.5646
## 2  118    3     1 956.5178 820.6221
## 3  118    7     1 960.7383 819.7616
## 4  118   10     1 956.9727 815.3331
## 5  118   13     1 958.6214 814.0815
## 6  118   17     1 956.0035 814.1564

Fixing missing data and repairing data

The next stage of the process would be to remove missing data within continuous streams of eye data which are likely to be caused by blinking. We can do this using the interpolate() function. The maxgap parameter specifies the maximum number of consecutive NAs to fill. Any longer gaps will be left unchanged. This is set as default to 25. The method parameter can either be “approx” (default) or “spline” which are both calls to zoo::na.approx and zoo::na.spline respectively.

Note that as the participant identifier column is not “participant_ID” (as eyetools expects as default), it needs to be specified in the function call.

data <- interpolate(data, maxgap = 25, method = "approx", participant_ID = "pNum")

You can also request a report of the differences in NA values present before and after the interpolation as well

interpolate_report <- interpolate(data, maxgap = 25, method = "approx", participant_ID = "pNum", report = TRUE)

interpolate_report[[2]]
##   pNum missing_perc_before missing_perc_after
## 1  118          0.02278151         0.02278151
## 2  119          0.01086325         0.01086325

An additional step that can be beneficial is to pass the eye data through the smoother() function. This removes particularly jerky transitions between samples and is critical for the analysis of velocities in the eye-movements. For now, let’s store the smoothed data in a new object. We can also ask for a plot of the data so that we can visually inspect it to see how well it fits the data.

set.seed(0410) #set seed to show same participant and trials in both chunks

data_smooth <- smoother(data,
                        span = .1, # default setting. This controls the degree of smoothing
                        participant_ID = "pNum", 
                        plot = TRUE) # whether to plot or not, FALSE as default
## Showing trials: 4, 6 for participant 119

The plot above shows the difference between the raw and smoothed data for a randomly selected participant and two random trials (simply for visualisation purposes and to keep the amount of plotting to a minimum).

With the default smoothing setting, we can see that the smoothed data does not track the actual data as closely as it could. The lower the value of span, the closer the smoothed data represents the raw data. A visual inspection of the plot suggests that a span of .02 is a good value for this data example. It is important that the fixations and saccades are matched closely to ensure data quality. Oversmooth and you end up with significant data loss, undersmooth and all the jerky eye movement is preserved rendering the use of smoother() meaningless, so a good inspection and testing of values can be useful.

set.seed(0410) #set seed to show same participant and trials in both chunks

data_smooth <- smoother(data, 
                        span = .02,
                        participant_ID = "pNum",
                        plot = TRUE)
## Showing trials: 4, 6 for participant 119

Counterbalancing positions

Many psychology experiments will position stimuli on the screen in a counterbalanced fashion. For example, in the example data we are using, there are two stimuli, with one of these appearing on the left and one on the right. In our design, one of the cue stimuli is a “target” and one is a “distractor”, and the experiment counterbalances whether these are positioned on the left or right across trials.

Eyetools has a built in function which allows us to transform the x (or y) values of the stimuli to take into account a counterbalancing variable: conditional_transform(). This function currently allows for a single-dimensional flip across either the horizontal or vertical midline. It can be used on raw data or fixation data. It requires the spatial coordinates (x, y) and a specification of the counterbalancing variable. The result is a normalised set of data, in which the x (and/or y) position is consistent across counterbalanced conditions (e.g., in our example, we can transform the data so that the target cue is always on the left). This transformation is especially useful for future visualisations and calculation of time on areas of interest. Note that conditional_transform() is another function that does not discriminate between multi-participant and single-participant data and so no participant_ID parameter is required.

The keen-eyed will notice that the present data does not contain a variable to specify the counterbalanced positions. This is contained in a separate dataset that holds the behavioural data, including the response times, the outcome, accuracy, and cue_order which tells us whether the target cue was on the left (coded as 1) or on the right (coded as 2).

data_behavioural <- HCL_behavioural # behavioural data

head(data_behavioural)
## # A tibble: 6 × 8
##   pNum  trial P_cue NP_cue cue_order correct_out accuracy     RT
##   <chr> <int> <dbl>  <dbl>     <dbl>       <dbl>    <dbl>  <dbl>
## 1 118       1     2      5         2           2        0 13465 
## 2 118       2     2      6         2           2        0  7796.
## 3 118       3     1      6         2           1        1  5267.
## 4 118       4     1      5         2           1        1  9911.
## 5 118       5     2      5         2           2        1  4424.
## 6 118       6     2      6         2           2        1  5224.

First we need to combine the two datasets based upon the participant identifier. Once the data has been joined we can use conditional_transform() to transform the x coordinates across the midline.

data <- merge(data_smooth, data_behavioural) # merges with the common variables pNum and trial

data <- conditional_transform(data, 
                              flip = "x", #flip across x midline
                              cond_column = "cue_order", #this column holds the counterbalance information
                              cond_values = "2",#which values in cond_column to flip
                              message = FALSE) #suppress message that would repeat "Flipping across x midline" 

Fixation Detection

In this next stage, we can start to explore the functions available for determining fixations within the data. The two main functions here are fixation_dispersion() and fixation_VTI(). Alongside these, is the option to compare_algorithms() which produces a small number of metrics and plots to help visualise the two fixation algorithms. We will first demonstrate and explain the two algorithms before demonstrating compare_algorithms() as this relies on the two fixation algorithms.

Dispersion Algorithm

fixation_dispersion() detects fixations by assessing the dispersion of the eye position using a method similar to that proposed by Salvucci and Goldberg (1996)2. This evaluates the maximum dispersion (distance) between x/y coordinates across a window of data, and looks for sufficient periods in which this maximum dispersion is below the specified dispersion tolerance. NAs are considered breaks in the data and are not permitted within a valid fixation period. By default, it runs the interpolation algorithm and this can be switched off using the relevant parameter.

data_fixations_disp <- fixation_dispersion(data,
                                           min_dur = 150, # Minimum duration (in milliseconds) of period over which fixations are assessed
                                           disp_tol = 100, # Maximum tolerance (in pixels) for the dispersion of values allowed over fixation period
                                           run_interp = FALSE, # the default is true, but we have already run interpolate()
                                           NA_tol = 0.25, # the proportion of NAs tolerated within any window of samples evaluated as a fixation
                                           progress = FALSE, # whether to display a progress bar or not
                                           participant_ID = "pNum") 

The resultant data output from fixation_dispersion() presents data by trial and fixation. It gives the start and end time for these fixations along with their duration and the x,y coordinates for the entry of the fixation.

head(data_fixations_disp) # show sample of output data
##   pNum trial fix_n start  end duration    x   y prop_NA min_dur disp_tol
## 1  118     1     1     0  173      173  959 811       0     150      100
## 2  118     1     2   197  397      200  961 590       0     150      100
## 3  118     1     3   400  653      253  958 490       0     150      100
## 4  118     1     4   803 1083      280 1372 839       0     150      100
## 5  118     1     5  1233 1386      153  997 545       0     150      100
## 6  118     1     6  1390 1700      310  969 478       0     150      100

VTI Algorithm

The fixation_VTI() function operates differently to fixation_dispersion(). It determines fixations by assessing the velocity of eye-movements, using a method that is similar to that proposed by Salvucci & Goldberg (1996). This applies the algorithm used in VTI_saccade() (detailed below) and removes the identified saccades before assessing whether separated fixations are outside of the dispersion tolerance. If they are outside of this tolerance, the fixation is treated as a new fixation regardless of the length of saccade separating them. Compared to fixation_dispersion(), fixation_VTI() is more conservative in determining a fixation as smaller saccades are discounted and the resulting data is treated as a continued fixation (assuming it is within the pixel tolerance set by disp_tol).

In simple terms, fixation_VTI() calculates the saccades within the data and identifies fixations as (essentially) non-saccade periods. To avoid eye gaze drift, it applies a dispersion tolerance parameter as well to ensure that fixations can be appropriately localised to an x,y coordinate pair. One current limitation to fixation_VTI() that is not present in fixation_dispersion() is the need for data to be complete with no NAs present, otherwise it cannot compute the saccades.

The fixation_VTI() works best on unsmoothed data (with default settings), as the smoothing process alters the velocity of the eye movement. When working with smoothed data, lowering the default threshold parameter is recommended as the “jerky” saccadic starts are less sudden and so the entry point of a saccade is sooner.

data_fixations_VTI <- fixation_VTI(data,
                                   threshold = 80, #smoothed data, so use a lower threshold
                                   min_dur = 150, # Minimum duration (in milliseconds) of period over which fixations are assessed
                                   min_dur_sac = 20, # Minimum duration (in milliseconds) for saccades to be determined
                                   disp_tol = 100, # Maximum tolerance (in pixels) for the dispersion of values allowed over fixation period
                                   run_interp = TRUE,
                                   smooth = FALSE,
                                   progress = FALSE, # whether to display a progress bar or not, when running multiple participants 
                                   participant_ID = "pNum")
head(data_fixations_VTI) # show sample of output data for participant 118
##   pNum trialNumber fix_n start  end duration         x        y min_dur
## 1  118           1     1     0  717      717  958.6070 600.0978     150
## 2  118           1     2   810 1167      357 1379.3236 836.9181     150
## 3  118           1     3  1243 1696      453  977.4735 499.0165     150
## 4  118           1     4  1703 2150      447  958.6533 174.2973     150
## 5  118           1     5  2270 2660      390  376.3810 833.4158     150
## 6  118           1     6  2763 3633      870  969.5746 233.2405     150
##   disp_tol
## 1      100
## 2      100
## 3      100
## 4      100
## 5      100
## 6      100

Saccades

This is also a sensible point to briefly highlight the underlying saccade detection process. This can be accessed directly using saccade_VTI(). This uses the velocity threshold algorithm from Salvucci & Goldberg (1996) to determine saccadic eye movements. It calculates the length of a saccade based on the velocity of the eye being above a certain threshold.

saccades <- saccade_VTI(data, participant_ID = "pNum")

head(saccades)
##   pNum trial sac_n start  end duration  origin_x origin_y terminal_x terminal_y
## 1  118     1     1  2180 2240       60  833.2688 296.7871   487.3967   705.9158
## 2  118     1     2  2710 2750       40  614.5028 605.7001   862.3837   408.3421
## 3  118     1     3  3673 3726       53  885.6256 253.4150   558.1883   655.7776
## 4  118     1     4  4213 4233       20  460.3286 722.8386   577.2034   617.8567
## 5  118     1     5  4243 4283       40  628.1155 574.9734   877.0888   378.3259
## 6  118     1     6  5353 5376       23 1142.1215 616.8508  1269.8306   723.8116
##   mean_velocity peak_velocity
## 1      225.0736      331.8455
## 2      200.3353      263.8863
## 3      243.7927      340.3059
## 4      195.6512      251.7763
## 5      202.2794      266.9614
## 6      185.0751      228.9918

Comparing the algorithms

As mentioned above, a supplementary function exists to compare the two fixation algorithms, the imaginatively named compare_algorithms(). To demonstrate this, we apply it against a reduced dataset of just one participant. It takes a combination of the parameters from both the fixation algorithms, and by default prints a summary table that is also stored in the returned list. It is recommended to store this in an object as the output can be quite long depending on the number of trials.

#some functions are best with single-participant data
data_118 <- data[data$pNum == 118,]

comparison <- compare_algorithms(data_118,
                                 plot_fixations = TRUE,
                                 print_summary = TRUE,
                                 sample_rate = NULL,
                                 threshold = 80, #lowering the default threshold produces a better result when using smoothed data
                                 min_dur = 150,
                                 min_dur_sac = 20,
                                 disp_tol = 100,
                                 NA_tol = 0.25,
                                 run_interp = TRUE,
                                 smooth = FALSE)

##     algorithm trial  percent fix_n     corr.r        corr.p    corr.t
## 1         vti     1 89.40332    17 0.71222884  0.000000e+00 64.468030
## 2  dispersion     1 86.01139    30 0.71222884  0.000000e+00 64.468030
## 3         vti     2 81.69376    14 0.77587488  0.000000e+00 59.439708
## 4  dispersion     2 83.87511    22 0.77587488  0.000000e+00 59.439708
## 5         vti     3 85.18049    13 0.09599458  1.332863e-04  3.829771
## 6  dispersion     3 82.64725    16 0.09599458  1.332863e-04  3.829771
## 7         vti     4 86.41103    16 0.72680619  0.000000e+00 57.678336
## 8  dispersion     4 84.19105    23 0.72680619  0.000000e+00 57.678336
## 9         vti     5 88.76320    12 0.57213575 3.834637e-116 25.383123
## 10 dispersion     5 81.67421    14 0.57213575 3.834637e-116 25.383123
## 11        vti     6 82.37548    12 0.83553811  0.000000e+00 60.140951
## 12 dispersion     6 85.18519    15 0.83553811  0.000000e+00 60.140951

Areas of Interest

Once we have collected our fixation data (we will proceed using the fixations_disp dataset), we can start looking at Areas of Interest (AOIs) and then plots of the fixations.

For the AOI_ “family” of functions, we need to specify where our AOIs were presented on the screen. This will enable us to determine when a participant enters or exits these areas.

# set areas of interest
AOI_areas <- data.frame(matrix(nrow = 3, ncol = 4))
colnames(AOI_areas) <- c("x", "y", "width_radius", "height")

AOI_areas[1,] <- c(460, 840, 400, 300) # Left cue
AOI_areas[2,] <- c(1460, 840, 400, 300) # Right cue
AOI_areas[3,] <- c(960, 270, 300, 500) # outcomes

AOI_areas
##      x   y width_radius height
## 1  460 840          400    300
## 2 1460 840          400    300
## 3  960 270          300    500

AOI_time() analyses the total time on defined AOI regions across trials. Works with fixation and raw data as the input (must use one or the other, not both). This gives a cumulative total time spent for each trial.

data_AOI_time <- AOI_time(data = data_fixations_disp, 
                          data_type = "fix",
                          AOIs = AOI_areas,
                          participant_ID = "pNum")

head(data_AOI_time)
##   pNum trial AOI_1 AOI_2 AOI_3
## 1  118     1  1405  3351  5289
## 2  118     2  1076  1327  3755
## 3  118     3   733   569  2843
## 4  118     4  3466  2442  2351
## 5  118     5  1090   246  2123
## 6  118     6   879   783  2465

The returned data show the time in milliseconds on each area of interest, per trial. It is also possible to specify names for the different areas of interest. Or you can request that the function returns the time spent in AOIs as a proportion of overall time in the trial, which requires an input vector that has the values, luckily this is something contained in the HCL_behavioural obect.

AOI_time(data = data_fixations_disp,
                          data_type = "fix",
                          AOIs = AOI_areas,
                          participant_ID = "pNum", 
                          as_prop = TRUE, 
                          trial_time = HCL_behavioural$RT) #vector of trial times
##    pNum trial     AOI_1      AOI_2     AOI_3
## 1   118     1 0.1043446 0.24886743 0.3927961
## 2   118     2 0.1380248 0.17022205 0.4816758
## 3   118     3 0.1391737 0.10803524 0.5397965
## 4   118     4 0.3497089 0.24639041 0.2372088
## 5   118     5 0.2463722 0.05560327 0.4798608
## 6   118     6 0.1682554 0.14987941 0.4718426
## 7   119     1 0.1597284 0.34258439 0.2219270
## 8   119     2 0.1640896 0.19781477 0.3960240
## 9   119     3 0.2570097 0.27748062 0.2881783
## 10  119     4 0.2134123 0.44471937 0.1516641
## 11  119     5 0.2516355 0.35280374 0.2214953
## 12  119     6 0.2243610 0.25205112 0.3718839

As mentioned, it also works with raw data too:

AOI_time(data = data_118, data_type = "raw", AOIs = AOI_areas,
         participant_ID = "pNum")
##   pNum trial AOI_1 AOI_2 AOI_3
## 1  118     1  1666  3573  5872
## 2  118     2  1353  1413  4163
## 3  118     3   797   593  3063
## 4  118     4  3733  2529  2879
## 5  118     5  1150   340  2356
## 6  118     6   923   817  2743

When working with raw data, you can also take advantage of AOI_time_binned(), which enables data binning based on time, and calculating time spent in AOIs as a result. Using bin_length, you specify a desired length of bin (say 100ms) and splits data into these bins, with ant remaining data being dropped from the analysis (as it does not form a complete bin which could skew analyses). As with AOI_time() you can specify either absolute or proportional time spent.

AOI_time_binned(data = data_118,
         AOIs = AOI_areas,
         participant_ID = "pNum", 
         bin_length = 100,
         max_time = 2000,
         as_prop = TRUE)
##     pNum trial bin_n AOI_1 AOI_2 AOI_3
## 1    118     1     1  0.00  0.00  0.00
## 2    118     1     2  0.00  0.00  0.00
## 3    118     1     3  0.00  0.00  0.00
## 4    118     1     4  0.00  0.00  0.00
## 5    118     1     5  0.00  0.00  0.57
## 6    118     1     6  0.00  0.00  1.00
## 7    118     1     7  0.00  0.00  1.00
## 8    118     1     8  0.00  0.13  0.27
## 9    118     1     9  0.00  1.00  0.00
## 10   118     1    10  0.00  1.00  0.00
## 11   118     1    11  0.00  1.00  0.00
## 12   118     1    12  0.00  0.83  0.00
## 13   118     1    13  0.00  0.00  0.00
## 14   118     1    14  0.00  0.00  0.23
## 15   118     1    15  0.00  0.00  1.00
## 16   118     1    16  0.00  0.00  1.00
## 17   118     1    17  0.00  0.00  1.00
## 18   118     1    18  0.00  0.00  1.00
## 19   118     1    19  0.00  0.00  1.00
## 20   118     1    20  0.00  0.00  1.00
## 21   118     2     1  0.00  0.00  0.00
## 22   118     2     2  0.00  0.00  0.00
## 23   118     2     3  0.00  0.00  0.00
## 24   118     2     4  0.17  0.00  0.00
## 25   118     2     5  1.00  0.00  0.00
## 26   118     2     6  1.00  0.00  0.00
## 27   118     2     7  1.00  0.00  0.00
## 28   118     2     8  1.00  0.00  0.00
## 29   118     2     9  1.00  0.00  0.00
## 30   118     2    10  1.00  0.00  0.00
## 31   118     2    11  1.00  0.00  0.00
## 32   118     2    12  0.63  0.00  0.10
## 33   118     2    13  0.00  0.00  1.00
## 34   118     2    14  0.00  0.00  1.00
## 35   118     2    15  0.00  0.00  1.00
## 36   118     2    16  0.00  0.00  1.00
## 37   118     2    17  0.00  0.00  1.00
## 38   118     2    18  0.00  0.00  1.00
## 39   118     2    19  0.00  0.00  1.00
## 40   118     2    20  0.00  0.00  0.80
## 41   118     3     1  0.00  0.00  0.00
## 42   118     3     2  0.00  0.00  0.00
## 43   118     3     3  0.00  0.00  0.00
## 44   118     3     4  0.00  0.00  0.03
## 45   118     3     5  0.00  0.00  1.00
## 46   118     3     6  0.00  0.00  1.00
## 47   118     3     7  0.00  0.00  1.00
## 48   118     3     8  0.00  0.00  1.00
## 49   118     3     9  0.00  0.00  1.00
## 50   118     3    10  0.33  0.00  0.40
## 51   118     3    11  1.00  0.00  0.00
## 52   118     3    12  1.00  0.00  0.00
## 53   118     3    13  0.10  0.57  0.00
## 54   118     3    14  0.00  1.00  0.00
## 55   118     3    15  0.00  1.00  0.00
## 56   118     3    16  0.00  1.00  0.00
## 57   118     3    17  0.00  1.00  0.00
## 58   118     3    18  0.00  1.00  0.00
## 59   118     3    19  0.00  0.37  0.37
## 60   118     3    20  0.00  0.00  1.00
## 61   118     4     1  0.00  0.00  0.00
## 62   118     4     2  0.13  0.00  0.00
## 63   118     4     3  1.00  0.00  0.00
## 64   118     4     4  1.00  0.00  0.00
## 65   118     4     5  1.00  0.00  0.00
## 66   118     4     6  0.40  0.00  0.20
## 67   118     4     7  0.00  0.00  1.00
## 68   118     4     8  0.00  0.00  1.00
## 69   118     4     9  0.00  0.00  1.00
## 70   118     4    10  0.00  0.00  1.00
## 71   118     4    11  0.00  0.00  0.70
## 72   118     4    12  0.00  0.93  0.00
## 73   118     4    13  0.00  1.00  0.00
## 74   118     4    14  0.00  1.00  0.00
## 75   118     4    15  0.00  1.00  0.00
## 76   118     4    16  0.00  0.50  0.00
## 77   118     4    17  0.63  0.00  0.00
## 78   118     4    18  1.00  0.00  0.00
## 79   118     4    19  1.00  0.00  0.00
## 80   118     4    20  1.00  0.00  0.00
## 81   118     5     1  0.00  0.00  0.00
## 82   118     5     2  0.00  0.00  0.00
## 83   118     5     3  0.00  1.00  0.00
## 84   118     5     4  0.00  1.00  0.00
## 85   118     5     5  0.00  0.30  0.40
## 86   118     5     6  0.00  0.00  1.00
## 87   118     5     7  0.00  0.00  1.00
## 88   118     5     8  0.00  0.00  0.67
## 89   118     5     9  0.00  0.00  0.00
## 90   118     5    10  0.00  0.50  0.00
## 91   118     5    11  0.03  0.60  0.00
## 92   118     5    12  1.00  0.00  0.00
## 93   118     5    13  1.00  0.00  0.00
## 94   118     5    14  1.00  0.00  0.00
## 95   118     5    15  1.00  0.00  0.00
## 96   118     5    16  1.00  0.00  0.00
## 97   118     5    17  0.73  0.00  0.00
## 98   118     5    18  0.00  0.00  1.00
## 99   118     5    19  0.00  0.00  1.00
## 100  118     5    20  0.00  0.00  1.00
## 101  118     6     1  0.00  0.00  0.00
## 102  118     6     2  0.00  0.00  0.00
## 103  118     6     3  0.00  0.00  0.00
## 104  118     6     4  0.00  0.00  0.60
## 105  118     6     5  0.00  0.00  1.00
## 106  118     6     6  0.00  0.00  0.80
## 107  118     6     7  0.93  0.00  0.00
## 108  118     6     8  1.00  0.00  0.00
## 109  118     6     9  1.00  0.00  0.00
## 110  118     6    10  1.00  0.00  0.00
## 111  118     6    11  1.00  0.00  0.00
## 112  118     6    12  0.70  0.00  0.00
## 113  118     6    13  0.00  0.97  0.00
## 114  118     6    14  0.00  1.00  0.00
## 115  118     6    15  0.00  1.00  0.00
## 116  118     6    16  0.00  0.30  0.43
## 117  118     6    17  0.00  0.00  1.00
## 118  118     6    18  0.00  0.00  1.00
## 119  118     6    19  0.00  0.00  1.00
## 120  118     6    20  0.00  0.00  1.00

The AOI_seq() function analyses the sequence of entries into defined AOI regions across trials. This works with fixation data.

data_AOI_sequence <- AOI_seq(data_fixations_disp,
                             AOI_areas,         
                             AOI_names = NULL,         
                             participant_ID = "pNum")   

head(data_AOI_sequence)
##   pNum trial AOI start  end duration entry_n
## 1  118     1   3   400  653      253       1
## 2  118     1   2   803 1083      280       2
## 3  118     1   3  1390 2120      730       3
## 4  118     1   1  2260 2666      406       4
## 5  118     1   3  2760 3646      886       5
## 6  118     1   1  3753 4116      363       6

The returned data provide a list of the entries into the AOIs, across each trial. By default the data is returned in long format, with one row per entry.

Plotting Functions

Finally, eyetools contains plot_* functions, plot_seq() plot_spatial(), and plot_AOI_growth(). These functions are not designed to accommodate multi-participant data and work best with single trials.

plot_seq() is a tool for visualising the timecourse of raw data over a single trial. If data from multiple trials are present, then a single trial will be sampled at random. Alternatively, the trial_number can be specified. Data can be plotted across the whole trial, or can be split into bins to present distinct plots for each time window.

The most simple use is to just pass it raw data:

plot_seq(data_118, trial_number = 1)

But the parameters of plot_seq() can help exploration be more informative. We can also add a background image and/or the AOIs we have defined:

plot_seq(data_118, trial_number = 1, bg_image = "data/HCL_sample_image.jpg") # add background image

plot_seq(data_118, trial_number = 1, AOIs = AOI_areas) # add AOIs

You also have the option to split the time into bins to reduce the amount of data plotted

plot_seq(data_118, trial_number = 1, AOIs = AOI_areas, bin_time = 1000)

plot_spatial() is a tool for visualising raw eye-data, processed fixations, and saccades. Fixations can be labeled in the order they were made. You can also overlay areas of interest (AOIs) and customise the resolution. It takes separate parameters for fixations, raw data, and saccades so they can be plotted simultaneously. You can also specify the trial_number to plot.

plot_spatial(raw_data = data_118, trial_number = 6)

plot_spatial(fix_data = fixation_dispersion(data_118), trial_number = 6)

plot_spatial(sac_data = saccade_VTI(data_118), trial_number = 6)

Or as mentioned, simultaneously:

plot_spatial(raw_data = data_118,
             fix_data = fixation_dispersion(data_118),
             sac_data = saccade_VTI(data_118),
             trial_number = 6)

The function plot_AOI_growth() helps visualise how attention is directed across the development of a trial. It presents a line graph of the change in proportion of time spent in each AOI region across the trial time.

#standard plot with absolute time
plot_AOI_growth(data = data_118, AOIs = AOI_areas, type = "abs", trial_number = 1)

#standard plot with proportional time
plot_AOI_growth(data = data_118, AOIs = AOI_areas, type = "prop", trial_number = 1)

#only keep predictive and non-predictive cues rather than the target AOI
plot_AOI_growth(data = data_118, AOIs = AOI_areas, type = "prop", AOI_names = c("Predictive", "Non Predictive", NA))
## Multiple trials detected: randomly sampled - trial:3


  1. Beesley, T., Nguyen, K. P., Pearson, D., & Le Pelley, M. E. (2015). Uncertainty and predictiveness determine attention to cues during human associative learning. Quarterly Journal of Experimental Psychology, 68(11), 2175-2199.↩︎

  2. Salvucci, D. D., & Goldberg, J. H. (2000). Identifying fixations and saccades in eye-tracking protocols. Proceedings of the Symposium on Eye Tracking Research & Applications - ETRA ’00, 71–78.↩︎