---
title: "Drawing log-data (UseR! 2026 workshop)"
author: "Tomasz Żółtak"
date: "`r Sys.Date()`"
output:
  html_document:
    toc: yes
    toc_float: yes
editor_options:
  chunk_output_type: console
---

```{r, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>",
  fig.width = 6,
  fig.height = 6
)
```

# Data and setup

**Have you already ran the the script 'prepare-for-the-workshop!.R'?**
If no, do it now before proceeding further!

## Loading packages

```{r}
library(logLime)
library(dplyr, warn.conflicts = FALSE)
library(ggplot2)
library(ggdensity)
```

## Reading the data

```{r}
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).

```{r}
# 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")
# 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.

# Drawing heatmaps

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.

## Data preparation

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()`:

```{r}
(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"))
```

Now we are ready to prepare heatmaps.

## Heatmap for a single respondent-screen

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.

```{r}
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.

```{r}
# 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).

## Several heatmaps on one graph

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.

```{r}
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.

```{r}
# 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))
```

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)`.

## Heatmaps comparing groups of respondents

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).

```{r}
# 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))
```

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.

# Drawing cursor traces and clicks

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:

```{r}
# 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:

```{r}
# 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()
```

# Animating cursor traces and clicks

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).

```{r}
library(gifski)
library(gganimate)
```

To prepare animation you need to assign a *ggplot* graph to an object:

```{r}
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:

```{r}
a1 <- g1 + transition_reveal(timeStampRel)
```

Finally, you need to *render* animation by calling function `animate()`:

```{r warning=FALSE}
animate(a1, renderer = gifski_renderer())
```
