# A Grammar Of Interactive Graphics

### `loon.ggplot`

#### R. W. Oldford and Zehao Xu

#### 2025-10-10

Just as `ggplot2` provides a layered implementation of a grammar of graphics, `loon.ggplot` provides a layered implementation of a grammar of interactive graphics.

With `loon.ggplot`, data analysts can easily switch between the elegant and beautiful static graphics of `ggplot2` and the powerful direct manipulation interactive graphics of `loon`, using each where it is most natural.

# A working dataset – `airquality`

To provide a working example, consider the `airquality` dataset:

    data("airquality")
    summary(airquality)
    #>      Ozone           Solar.R           Wind             Temp
    #>  Min.   :  1.00   Min.   :  7.0   Min.   : 1.700   Min.   :56.00
    #>  1st Qu.: 18.00   1st Qu.:115.8   1st Qu.: 7.400   1st Qu.:72.00
    #>  Median : 31.50   Median :205.0   Median : 9.700   Median :79.00
    #>  Mean   : 42.13   Mean   :185.9   Mean   : 9.958   Mean   :77.88
    #>  3rd Qu.: 63.25   3rd Qu.:258.8   3rd Qu.:11.500   3rd Qu.:85.00
    #>  Max.   :168.00   Max.   :334.0   Max.   :20.700   Max.   :97.00
    #>  NA's   :37       NA's   :7
    #>      Month            Day
    #>  Min.   :5.000   Min.   : 1.0
    #>  1st Qu.:6.000   1st Qu.: 8.0
    #>  Median :7.000   Median :16.0
    #>  Mean   :6.993   Mean   :15.8
    #>  3rd Qu.:8.000   3rd Qu.:23.0
    #>  Max.   :9.000   Max.   :31.0
    #>

It has missing data (in `Ozone` and `Solar.R`) and the variable `Month` appears as numerical. The date information can easily be made more meaningful:

    airquality$Date <- with(airquality,
                            as.Date(paste("1973", Month, Day, sep = "-")))
    # Could also look up the day of the week for each date and add it
    airquality$Weekday <- factor(weekdays(airquality$Date),
                                 levels = c("Monday", "Tuesday", "Wednesday",
                                            "Thursday", "Friday",
                                            "Saturday", "Sunday"))

`Month` can also be turned it into a factor so that its levels are three character month abbreviations (arranged to match calendar order):

    airquality$Month <- factor(month.abb[airquality$Month],
                               levels = month.abb[unique(airquality$Month)])

The data now look like

    head(airquality, n = 3)
    #>   Ozone Solar.R Wind Temp Month Day       Date   Weekday
    #> 1    41     190  7.4   67   May   1 1973-05-01   Tuesday
    #> 2    36     118  8.0   72   May   2 1973-05-02 Wednesday
    #> 3    12     149 12.6   74   May   3 1973-05-03  Thursday

With its mix of continuous and categorical variables (some with missing data) this transformed data will be used to illustrate `loon.ggplot`’s grammar of interactive graphics.

# The trick: `ggplot()` becomes `l_ggplot()`

The interactive grammar begins by simply replacing `ggplot()` by `l_ggplot()`, wherever it appears in the layered grammar. The same arguments (e.g., `data`, `mapping`, etc.) and clauses (e.g. geoms, scales, coordinates, etc.) are used, but now to create an interactive plot.

For example,

    lgp <- l_ggplot(airquality,
                    mapping = aes(x = Wind, y = Ozone)) +
      ggtitle("Air quality in New York (1973)") +
      geom_point(size = 3)

Like `ggplot()`, `l_ggplot()` produces a data structure containing the information needed to create a plot. No plot is actually yet displayed; rather `lgp` has the potential to produce a plot on demand.

A look at its class

    class(lgp)
    #> [1] "l_ggplot"        "ggplot2::ggplot" "ggplot"          "ggplot2::gg"
    #> [5] "S7_object"       "gg"

suggests that it is both an `l_ggplot` and `ggplot2` classes.

In `ggplot2` identical methods have been written for both the `plot()` and the `print()` function to show the `ggplot` on the current device.
In `loon.ggplot` these functions are different for an `l_ggplot`:

- `plot(lgp)` displays the `lgp` structure as a static `ggplot` on the current device
- `print(lgp)` displays it as an interactive `loon` plot in the `R` session.

For example, to see the `ggplot`:

    plot(lgp)

And to see the interactive `loon` plot, simply print it:

    lgp                   # or print(lgp)

which will look something like (as rendered in `grid` graphics).

Each plot (the static `ggplot` and the interactive `loon` plot) presents the same information, but in slightly different form (e.g., different choices on title placement, white space padding, etc.)

Though the data information is identical in both plots, the `loon` plot appears a little more spartan. This is because an interactive plot is dynamic and can change in real time by direct interaction; it is enough that the analyst appreciates the data content of the plot without too much concern over display details. In contrast, the `ggplot` is more often meant to be shared in print and so demands more flexibility to lay out its plot elements in an elegant display.

Note: In `ggplot2`, every time a `ggplot` is printed, a new plot is produced on the current device. Similarly, in `loon.ggplot`, every time an `l_ggplot` is printed, a new (interactive) `loon` plot is produced; every time it is plotted, a new (static) `ggplot` is produced.

## Programmatic access of the interactive plot

At times, as when creating this document, it will be handy to have programmatic access to the `loon` plot. This can be done by assigning the `loon` plot to a variable, say `lp`, in any one of several different ways:

When it was first printed it, the interactive plot would have returned a string. For example, it was

    #> [1] ".l0.ggplot.plot"

which is of the form `".lXX.ggplot.plot"` where `XX` is a non-negative integer.

This is the `tcltk` “path” to the `loon` plot and uniquely identifies the `loon` structure. The data structure is now accessed through `l_getFromPath(pathname)` as below.

    lp <- l_getFromPath(".lXX.ggplot.plot")  # replace XX by whatever number appeared

Of course, this requires you to have noticed and recovered the string pathname for that plot when it first appeared. Fortunately, that is not necessary. In the title bar of the window containing the `loon` plot, the string following `"path: "` can also be used. In the present case, this will be of the form `".lXX.ggplot"` (as before but without the additional `".plot"` suffix). The call

    lp <- l_getFromPath(".lXX.ggplot")  # replace XX by whatever number appeared

will return the interactive plot as before.

Finally, if you have the foresight to know that you would like to have programmatic access to the interactive plot from the start, you could assign it to a variable when it was first created.
There are two ways to do this.

One is

    lp <- print(lgp)

The other uses a powerful function called `loon.ggplot()` (more on this below):

    lp <- loon.ggplot(lgp)

Note: that unlike `l_getFromPath()`, either of the above calls will produce a new interactive plot from `lgp` and assign it to `lp`.

## The loon-ggplot duality

Using `l_ggplot()` in place of `ggplot()` extends the graphics grammar of `ggplot2` to produce interactive `loon` plots.

- use `l_ggplot()` in place of `ggplot()`

  - plotting an `l_ggplot` produces a static `ggplot` display

  - printing an `l_ggplot` produces an interactive `loon` display.

- the `l_ggplot` is not the interactive `loon` plot; it is an enhanced `ggplot` and can be augmented just as any other `ggplot`.

- Changes to the `ggplot` have no effect on the interactive plot

  For example,

      plot(lgp + geom_smooth())

  behaves like any other `ggplot` with no effect on the interactive plot.

- Changes to the interactive `loon` plot have no effect on the static `l_ggplot`

  Make whatever changes you like interactively to the `loon` plot `lp`, or programmatically as below:

      # Change glyph aesthetics of ALL points
      lp["color"] <- "lightgrey"
      lp["glyph"] <- "ctriangle"  # closed triangle
      lp["size"]  <- 10           # proportional to area in loon

      # Dynamically change the scaling (magnify or zoom in and out)
      for (mag in rep(c(0.8, 1, 1.2), times = 5)){
         lp["zoomX"] <- mag
         lp["zoomY"] <- mag
         Sys.sleep(0.1) # slow down to see effect
       }
      # Settle on
      lp["zoomX"] <- 1.2
      lp["zoomY"] <- 1.2
      #
      # Or, similarly, change the location/origin of the plot
      xlocs <- seq(min(lp["x"]),
                   median(lp["x"]),
                   length.out = 10)

      ylocs <- seq(min(lp["y"]),
                   median(lp["y"]),
                   length.out = 10)
      # Dynamically change the origin
      for (i in 1:length(xlocs)){
        lp["panX"] <- xlocs[i]
        lp["panY"] <- ylocs[i]
        Sys.sleep(0.1) # slow down to see effect
      }
      # and back
      xlocs <- rev(xlocs)
      ylocs <- rev(ylocs)
      # dynamically
      for (i in 1:length(xlocs)){
        lp["panX"] <- xlocs[i]
        lp["panY"] <- ylocs[i]
        Sys.sleep(0.1) # slow down to see effect
      }
      # Perhaps settle on
      lp["panX"] <- 7
      lp["panY"] <- 0

  And now observe the effect on each of the plots:

      # First the l_ggplot
      plot(lgp)

  No change. It is unaffected by any change to the interactive plot.

      # Now the l_plot
      plot(lp)

  Reflects the changes made on the interactive plot.

## The function `loon.ggplot()`

Earlier, `loon.ggplot()` was recommended as a means to produce an interactive plot from an `l_ggplot` and to assign it to a variable, as in

    lp <- loon.ggplot(lgp)

This makes it seem essentially equivalent to `print(lgp)`, but it is not.

Instead, `loon.ggplot()` is a powerful two way bridge between `ggplot`s and `loon` plots.

When called on

- an `l_ggplot` (an extended `ggplot`) `loon.ggplot(lgp)` produces an interactive `loon` plot.

- an interactive `loon` plot, it produces a static `ggplot`

      loon.ggplot(lp)

  This is a `ggplot` with parameters to make it look like the `loon` plot it was built from (e.g. the title is centred in this `ggplot`).

  `loon.ggplot()` gives us a second way to produce a static version of an interactive `loon` plot.

  - `plot(lp)` produces a `grid` graphic object (or `grob`)

  - `loon.ggplot(lp)` produces a `ggplot` graphic object.

  Either reproduces the current `loon` plot as a static snapshot of its present appearence (ideally wysiwyg).

  Which is preferred depends on the use intended for the static plot .

  The `ggplot` version is easier to work with and trivial to adapt using the grammar.

      gp <- loon.ggplot(lp)  # loon to ggplot
      class(gp)
      #> [1] "ggplot2::ggplot" "ggplot"          "ggplot2::gg"     "S7_object"
      #> [5] "gg"
      gp + geom_smooth()

  Alternatively, a new interactive plot can be generated from an ordinary `ggplot` as

      new_lp <- loon.ggplot(gp + geom_smooth())

  and this interactive plot is turned into a new static `ggplot` with the same function, as shown below

      loon.ggplot(new_lp)

  Note that the grey polygon emphasizing the confidence region does not appear. It is there, but as a hidden `layer` of the `loon` plot that can be revealed at any time (via the `loon` inspector or programmatically). The polygon is not shown by default because colours do not (yet) have an alpha channel (for transparency) in `tcltk`.

The bridge: `loon.ggplot()` turns `ggplot`s to interactive `loon` plots and `loon` plots to `ggplot`s.

# The interactive grammar

`loon.ggplot` extends the grammar of graphics by adding several new clauses.

## `+ linking()`

`+ linking(linkingGroup = NULL, linkingKey = NULL, linkedStates = NULL, sync = NULL)`

`loon` implements a group-key-state linking model.

Interactive plots having the same `linkingGroup` are linked, in that each plot changes its display in response to display changes of the `linkedStates` of any plot in the same `linkingGroup`. Only those `linkedStates` in common are changed and display elements are matched by values of the `linkingKey`.

- `linkingGroup`

  A string naming the group or `NULL` for no linking.

- `linkedStates`

  A character vector of the states to be linked.

  By default these include `selected`, `active`, and others peculiar to each type of plot such as `color` and `size`.

- `linkingKey`

  A length `n` character vector of unique strings, one for each observation; default is `"0"`, `"1"`, …, `"n-1"`.

- `sync`

  Specifies the direction of synchronizing the linked states at the time the plot is created.
  The value `"pull"` pulls the values of the linked states from plots in the linking group and assigns them to the new plot; value `"push"` pushes the newly created plot’s linked states values out to the others in the linking group.

  Default is `"pull"` unless some aesthetics matching the linked states are specified in the plot creation; then the default will be `"push"`.

Each plot will propogate, and respond to, only changes in those `state`s named in its `linkedStates`. Display elements associated with the unique set of keys in each plot’s `linkingKey` will change together.

## `+ hover()`

`+ hover(itemLabel = NULL, showItemLabels = NULL)`

Provides a pop up display as the mouse hovers over a plot element in the interactive plot.

- `itemLabel`

  A character vector of length `n` with a string to be used to pop up when the mouse hovers above that element.

- `showItemLabels`

  A single logical value: `TRUE` if pop up labels are to appear on hover, `FALSE` (the default) if they are not.

## `+ selection()`

`+ selection(selected = NULL, selectBy = NULL, selectionLogic = NULL)`

Set which elements (i.e., observations) are `"selected"`. These are shown highlighted in the plot.

- `selected`

  a logical or a logical vector of length n that determines which observations are selected (TRUE and hence appear highlighted in the plot) and which are not. Default is FALSE and no points are highlighted.

- `selectBy`

  A string determining how selection will occur in the interactive plot. Default is `"sweeping"` where a rectangular region is reshaped or “swept” out to select observations.; alternately `"brushing"` will indicate that a fixed rectangular region is moved about the display to select observations.

- `selectionLogic`

  One of `"select"` (the default), `"deselect"`, and `"invert"`. The first highlights observations as selected, the second downlights them, and the third inverts them (downlighting highlighted observations and highlighting downlighted ones).

## `+ active()`

`+ active(active = NULL, activeGeomLayers = NULL)`

Set `active` and/or `activeGeomLayers`

- `active`

  a logical, or a logical vector of length `n`, determining which observations are active (hence appear in the plot) and which are inactive (`FALSE` and hence do not appear). Default is `TRUE`.

- `activeGeomLayers`

  determine which geom layer is interactive by its `geom_...` position in the grammar of the expression. Currently, only `geom_point()` and `geom_histogram()` can be set as the active geom layer(s) so far. (N.B. more than one `geom_point()` layer can be set as an active layer, but only one `geom_histogram()` can be set as an active geom layer and it can be the only active layer)

## `+ zoom()`

`+ zoom(layerId = NULL, scaleToFun = NULL)`

Change the visible plot region by scaling to different elements of the display.

- `layerId`

  numerical; which layer to scale the plot by. If the layerId is set as `NULL` (default), the region of the interactive graphics loon will be determined by the ggplot object (i.e. `coord_cartesian`, `xlim`, etc); else one can use `scaleToFun` to modify the region of the layer.

- `scaleToFun`

  scale function to be used. If `NULL` (default), based on different layers, different scale functions will be applied. For example, if the layer is the main graphic model, i.e. `l_plot` `l_hist`, then the default `scaleToFun` is `l_scaleto_plot`; else if the layer is a general `l_layer` widget, the default `scaleToFun` would be `loon::l_scaleto_layer`.

  If it is not `NULL`, any of the following `scaleToFun` functions could be used

  scale to | Subfunction
  --- | ---
  plot | l_scaleto_plot
  world | l_scaleto_world
  active | l_scaleto_active
  selected | l_scaleto_selected
  layer | l_scaleto_layer

  Alternatively, `scaleToFun` can be any function whose arguments match those of the functions above.

## `+ interactivity()`

    + interactivity(linkingGroup, linkingKey, linkedStates, sync,  # linking
                    active, activeGeomLayers,                      # active
                    selected, selectBy, selectionLogic,            # selection
                    layerId, scaleToFun,                           # zoom
                    itemLabel,  showItemLabels,                    # hover
                    ... )

Set interactive components (e.g. linking, selection, etc) in one clause. All named arguments are as described in the other clauses.

- `...` other named arguments to modify loon plot states. See `l_info_states()`.

# The trick: `l_ggplot()` becomes `ggplot()`

We began with the recommendation that to have interactive `ggplot`s, all we need do is replace `ggplot()` in the grammar by `l_ggplot()` with all the usual arguments. All clauses of the `ggplot` grammar can be used as before plus the new interactive clauses. The result is an `l_ggplot` that prints as an interactive `loon` plot and plots as a `ggplot2` plot. The advantage is that using `l_ggplot()` makes it clear that an ordinary `ggplot` is not being produced and emphasizes the loon-ggplot duality.

The final trick is that, with `loon.ggplot`, one does not actually need to use `l_ggplot()`, simply use `ggplot()`. If no clauses are interactive, then an ordinary `ggplot` will be produced; if an interactive clause is added, an `l_ggplot` will be produced.

For example, the following produces an ordinary `ggplot`.

    ggp <- ggplot(airquality, mapping = aes(Solar.R, Temp)) +
      geom_point(size = 3) +
      ggtitle("Air quality in New York (1973)")
    # which is an ordinary ggplot and prints as one
    ggp

Adding an interactive clause, turns the result into an `l_ggplot`.

    lggp <- ggp +
      linking(linkingGroup = "airquwqality") +
      selection(selected = airquality$Solar.R < 100) +
      zoom(layerId = 1, scaleToFun = l_scaleto_selected) +
      geom_smooth()
    # which is an interactive loon and prints as one
    lggp
    #> [1] ".l3.ggplot.plot"
    #> attr(,"class")
    #> [1] "l_plot" "loon"
    # but plots as a ggplot
    plot(lggp)

Note that the interactive effects do not appear in `plot(l_ggp)`; this is because this is still an `l_ggplot`. To print the interactive plot, a handle to it must be found, for example using `l_getFromPath(".l3.ggplot")`

    l_ggp <- l_getFromPath(".l3.ggplot")
    # Alternatively, the loon plot could have been captured when first
    # created by using loon.ggplot(lggp) in stead of print(lggp) as follows
    #
    # l_ggp <- loon.ggplot(lggp)
    #
    # Either way, it will look like the following as a grid graphics plot
    plot(l_ggp)
    # and as below when presented as a ggplot
    loon.ggplot(l_ggp)

The final trick is that either `l_ggplot()` or `ggplot()` cam be produce interactive plots using the `ggplot` grammar.

It’s largely a matter of taste.