Have you already ran the the script ‘prepare-for-the-workshop!.R’? If no, do it now before proceeding further!
library(logLime)
library(dplyr, warn.conflicts = FALSE)
library(ggplot2)
library(ggdensity)
surveyResults <- readRDS("dataExp5.RDS")
Code below performs preprocessing, filters out respondent-screens for which some common problems were identified, computes and adds columns with relative positions and cursor move distances, and finally merges some respondent’s characteristics to the data regarding actions (events).
# creating log-data object
logData <-
separate_logdata_types(surveyResults |>
select(respid, starts_with("log")),
surveyStructure = c("limesurvey_survey_745195.txt",
"limesurvey_survey_817417.txt"),
questionNamesPrefix = "log")
#> Processing log-data streams with:
#> - respondent's id(s) stored in columns: respid;
#> - log-data streams stored in columns: logvac, logimmig, logread1, logread2, logread3, logitrust1, logitrust2, loggtrust, logbis11a, logbis11b, logbis11c, logdis19a, logdis19b, lognc01a, lognc01b, lognc01c, logipip20a, logipip20b, logmcatt, logmcint, logmcburd.
#>
#> Preprocessing log-data streams:
#> Pivoting data with log-streams in many columns to put it into one column...
#> Separating log-streams into rows...
#> Separating log-streams into columns...
#> Marking broken records...
#> Done.
#> Separating returns to survey screens.
#> Warning in separate_logdata_types(select(surveyResults, respid,
#> starts_with("log")), : Separating returns to survey screens is not possible for
#> log-data collected using 'logdataLimeSurvey' applet in versions earlier than
#> 1.1. Argument `separateReturns` was set to 'no' automatically.
#> Processing input positions.
#> Processing system information.
#> Processing actions:
#> Computing scroll lengths...
#> Transforming mousemove events into actual moves...
#> Labeling clicks and hovers...
#> Labelling types of elements...
#> Done.
#>
#> Separated data consists of:
#> - 326 respondents on 21 screens,
#> - 6'524 respondent-screen-entries,
#> - 1'814'090 actions,
#> out of which data about 153 is somehow broken.
# filtering out problematic records (respondent-screens)
logData <- lapply(logData, semi_join,
y = logData$systemInfo |>
filter(problemsAnyBroken != 1,
problemsLeftBrowser != 1,
problemsResized != 1,
problemsTimeStamps != 1,
inputsWidth > 0),
by = c("respid", "screen"))
# computing relative positions and distances
logData$actions <- compute_relative_positions(logData$actions,
logData$systemInfo)
# merging some respondent's characteristics
logData$actions <- logData$actions |>
left_join(surveyResults |>
select(respid, variant, gender),
by = "respid")
Now we may proceed to drawing.
One of the commonly used forms of visualizing cursor moves data is the preparation of so-called heatmaps. On this type of graph areas of the survey screen that the cursor was pointing at for a long time and areas that were hardly visited (pointed at) are marked with different colors. An interesting feature of this form of visualization is that it can be used to illustrate both the actions of a single respondent and aggregated tendencies among a group of respondents.
One may draw heatmaps using function
geom_density2d_filled() from the ggplot2 package.
However, data prepared by the separate log data types()
above can not (or, more specifically, should not) be used to create such
plots! That’s because the actions element of a log-data objects
contain mousemove events of different duration. When estimating
the time the indicator has been in a given position, this durations
should be treated as a weight. However, there is no way to provide
different weights for observations while computing 2-dimensional
densities in ggplot2. For this reason, the data must be
transformed into another form in which cursor positions are reported at
evenly spaced points in time. You may get the data of this form by using
function compute_cursor_positions():
(cursorPositions <- compute_cursor_positions(logData$actions,
timeSpan = 250) |>
# also, let's join this data with some respondent's characteristics
left_join(surveyResults |>
select(respid, variant, gender),
by = "respid"))
#> Preprocessing log-data streams.
#> Calculating cursor positions.
#> # A tibble: 668,583 × 9
#> respid screen timeStampRel pageX pageY pageX_rel pageY_rel variant gender
#> <int> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <chr> <chr>
#> 1 58172 bis11a 0 1201 431 0.589 0.165 Personalit… Male
#> 2 58172 bis11a 250 1192 322 0.573 -0.128 Personalit… Male
#> 3 58172 bis11a 500 1165 300 0.524 -0.187 Personalit… Male
#> 4 58172 bis11a 750 1165 300 0.524 -0.187 Personalit… Male
#> 5 58172 bis11a 1000 1165 300 0.524 -0.187 Personalit… Male
#> 6 58172 bis11a 1250 1162 291 0.519 -0.211 Personalit… Male
#> 7 58172 bis11a 1500 1186 280 0.562 -0.241 Personalit… Male
#> 8 58172 bis11a 1750 1186 280 0.562 -0.241 Personalit… Male
#> 9 58172 bis11a 2000 1186 280 0.562 -0.241 Personalit… Male
#> 10 58172 bis11a 2250 1186 280 0.562 -0.241 Personalit… Male
#> # ℹ 668,573 more rows
Now we are ready to prepare heatmaps.
Let’s start with drawing a heatmap for a single respondent-screen. In the code chunk below the screen containing a set of question regarding immigrants is selected and a single respondent is sampled.
respCursorPositions <- cursorPositions |>
filter(screen == "immig") |>
filter(respid %in% sample(unique(respid), 1))
Having the data selected you may use ggplot2 functions to plot a heatmap. Please note, that relative positions are used instead of regular (absolute) ones.
# declaring data and mapping of variables onto axis
ggplot(respCursorPositions,
aes(x = pageX_rel, y = pageY_rel)) +
# heatmap itself (using the ggdensity package)
geom_hdr() +
# reversing the Y axis because of the difference in coordinates system
# between web browsers and plotting packages
scale_y_reverse() +
# a decoration - marking the area within which INPUT elements were shown
geom_vline(xintercept = c(0, 1), color = "red", linetype = "dashed") +
geom_hline(yintercept = c(0, 1), color = "red", linetype = "dashed")
The lightest areas indicate no hovering and the darker it gets, the more time was spent by the cursor over a given area. Please note, that values in the legend indicate density, not simply the time (i.e. density is proportional to time, but given in a different unit).
Let’s suppose we want to compare heatmaps for several respondents on one graph. In the code chunk below 4 respondents with explicitly specified ids are selected.
respsCursorPositions <- logData$actions |>
filter(screen == "immig") |>
filter(respid %in% c(988464343, 987844729, 988421695, 988555104))
To put heatmaps describing different respondents on separate panels
of the graph, you need to add facet_wrap() call - see the
last line of the code chunk below.
# declaring data and mapping of variables onto axis
ggplot(respsCursorPositions,
aes(x = pageX_rel, y = pageY_rel)) +
# heatmap itself (using the ggdensity package)
geom_hdr() +
# reversing the Y axis because of the difference in coordinates system
# between web browsers and plotting packages
scale_y_reverse(limits = c(NA, -1)) +
# a decoration - marking the area within which INPUT elements were shown
geom_vline(xintercept = c(0, 1), color = "red", linetype = "dashed") +
geom_hline(yintercept = c(0, 1), color = "red", linetype = "dashed") +
# placing each respondent on a separate panel
facet_wrap(vars(respid))
#> Warning: Removed 58 rows containing non-finite outside the scale range
#> (`stat_hdr()`).
Please note, that the results visualized using the
geom_hdr() do not show potential differences regarding the
total time spent on the screen by different respondents, but only
differences in the average share of time spent over the different
regions! If you’re interested in the mean absolute time spent over the
regions of the survey screen, you have to turn to ggplot2’s
geom_density_2d_filled(contour_var = "density", breaks = vectorOfReasonablyChosenValues).
Comparing groups of respondents using heatmaps is also simple. You
need to provide the ggplot() with a data frame including
all the members of the groups you want to compare, and then use the
variable(s) defining the grouping as the faceting variable(s).
In the code chunk below function facet_grid() is used
instead of facet_wrap() to arrange results in a grid
constructed from crossing values of variables gender and
variant (this second variable describes whether question about
immigration was presented in the first or in the second block of the
questionnaire).
# density
# declaring data and mapping of variables onto axis
ggplot(cursorPositions |>
filter(screen == "immig"),
aes(x = pageX_rel, y = pageY_rel)) +
# heatmap itself
geom_hdr() +
# reversing the Y axis because of the difference in coordinates system
# between web browsers and plotting packages
scale_y_reverse(limits = c(NA, -1)) +
# a decoration - marking the area within which INPUT elements were shown
geom_vline(xintercept = c(0, 1), color = "red", linetype = "dashed") +
geom_hline(yintercept = c(0, 1), color = "red", linetype = "dashed") +
# defining that graphs should summarize results
# grouped by the crossing of variant and gender
facet_grid(vars(variant), vars(gender))
#> Warning: Removed 1137 rows containing non-finite outside the scale range
#> (`stat_hdr()`).
Please note, that the results visualized using the
geom_hdr() do not show potential differences regarding the
mean absolute time spent on the screen by the members of different
groups, but only differences in the average share of time spent over the
different regions! As with comparing individual respondents, you will
have to turn to ggplot2’s
geom_density_2d_filled(contour_var = "density", breaks = vectorOfReasonablyChosenValues)
if you’re interested in comparing mean absolute times.
Sometimes you want to illustrate the cursor trace for a specific respondent-screen. Such a graph will look more informative if you enrich with the position of clicks and place the position of the survey form INPUT elements in the background. However, to achieve this, you first need to prepare several separate data sets:
# some single respondent's id
respondent <- 988543469
# data on cursor trace
respCursorMoves <- logData$actions |>
filter(screen == "immig",
type %in% "mousemove") |>
filter(respid %in% respondent)
# data on clicks
respCursorClicks <- logData$actions |>
filter(type %in% "click") |>
semi_join(respCursorMoves,
by = c("respid", "screen"))
# data on INPUT elements' positions
respInputPositions <- logData$inputPositions |>
semi_join(respCursorMoves,
by = c("respid", "screen"))
Having the data prepared you may use ggplot2 to prepare a graph:
# no data specified, as it will be defined separately for each 'layer' of the plot
ggplot(mapping = aes(x = pageX_rel, y = pageY_rel)) +
# putting INPUT elements into background
geom_point(data = respInputPositions,
size = 4, color = "darkgrey") +
# drawing mouse trace
geom_path(data = respCursorMoves,
linewidth = 1.2) +
# drawing clicks
geom_point(data = respCursorClicks,
size = 5) +
# labeling start and end point
geom_label(data = respCursorMoves[c(1, nrow(respCursorMoves)), ] |>
mutate(label = c("start", "end")),
mapping = aes(label = label), alpha = 0.7) +
# reversing the Y axis because of the difference in coordinates system
# between web browsers and plotting packages
scale_y_reverse()
If you want to animate cursor trace, it is possible by using package gganimate (you will also need a package responsible for rendering an animation - here this will be the gifski package, that prepares a GIF file, although this is not the only possible choice).
library(gifski)
library(gganimate)
To prepare animation you need to assign a ggplot graph to an object:
g1 <- ggplot(NULL,
aes(x = pageX_rel, y = pageY_rel)) +
# putting INPUT elements into background
geom_point(data = respInputPositions,
size = 4, color = "darkgrey") +
# drawing mouse trace
geom_path(data = respCursorMoves,
linewidth = 1.2) +
# drawing clicks
geom_point(data = respCursorClicks, mapping = aes(group = timeStampRel),
size = 5) +
# labeling start and end point
geom_label(data = respCursorMoves |> #[c(1, nrow(respCursorMoves)), ] |>
mutate(label = c("start", rep(NA_character_, n() - 2), "end")),
mapping = aes(label = label, group = timeStampRel), alpha = 0.7) +
# reversing the Y axis because of the difference in coordinates system
# between web browsers and plotting packages
scale_y_reverse()
Please note, that for the animated geom_point and
geom_label layers you have to explicitely set the
group aesthetics so that each time point will be assigned to a
different group (by using group = timeStampRel within the
call to aes()).
Now you may turn this graph into an animation by specyfing an
animation type in the same way as if you will add another element to a
graph. When drawing a cursor trace, the transition_reveal()
function will provide the appropriate type of animation. Also, you need
to specify that the timeStampRel variable describes the
time:
a1 <- g1 + transition_reveal(timeStampRel)
Finally, you need to render animation by calling function
animate():
animate(a1, renderer = gifski_renderer())
#> `geom_path()`: Each group consists of only one observation.
#> ℹ Do you need to adjust the group aesthetic?
#> `geom_path()`: Each group consists of only one observation.
#> ℹ Do you need to adjust the group aesthetic?