Search code examples
javascriptrggplot2r-markdownr-plotly

User inputs for R ggplot or plotly without shiny


I have an Rmarkdown with a simple scatter plot (a map for instance), and I would like users to be able to provide some arbitrary x and y coordinates via an input and have those plotted on the graph (in red in the example below). The problem is, I don't have a shiny server so I cannot rely on that option. Is there a implement this, for instance, via javascript or something?

This is what I have:

---
title: "Untitled"
output: html_document
---

```{r setup, include=FALSE}
library(ggplot2)
library(plotly)
```

```{r fig.height=4, fig.width=4}
X <- data.frame(x = 1:10, y = 1:10)
gg <- ggplot(X, aes(x, y)) + geom_point()
ggplotly(gg)
```

This is what I am looking for:

enter image description here


Edit
The example above is a simplification. In reality, the grid is 360x240 and the coordinates can only be integers.

Edit 2 @JohanRosa already provided a nice answer by rebuilding the plot entirely on plotly.js. However, my ggplot is in fact quite complexe and I have many of them. It would therefore be quite complicated for me to rebuild each of them into plotly.js. This is the reason I am looking for an solution that can work directly on the ggplot(ly) that I have.


Solution

  • We can use htmlwidgets::onRender to inject custom JS code into your ggplotly object.

    I reused @JohanRosa's inputs (thanks! +1) and provided an id to the container div to listen on the inputs. Furthermore I'm using Plotly.restyle to avoid redrawing the plot.

    Please check the following:

    ---
    title: "ggplotly user inputs"
    output: html_document
    ---
        
    :::{#inputcontainerid .input-container}
    
    :::{.xs}
    ### X coordinate
    <input type='number' value=5 id='x1' class='x'>
    <input type='number' value=2.5 id='x2' class='x'>
    <input type='number' value=7.5 id='x3' class='x'>
    :::
    
    :::{.ys}
    ### Y coordinate
    <input type='number' value=10 id='y1'>
    <input type='number' value=5 id='y2'>
    <input type='number' value=2.5 id='y3'>
    :::
    
    :::
    
    <!-- css configuration to arrange the inputs -->
    ```{css, echo = FALSE}
    input {
      display: block;
    }
    
    .xs, .ys {
      display: inline-block;
    }
    ```
    
    ```{r setup, include=FALSE}
    library(ggplot2)
    library(plotly)
    library(htmlwidgets)
    ```
    
    ```{r out.width='100%', echo=FALSE}
    X <- data.frame(x = 1:10, y = 1:10)
    
    JS <- "
    function(el, x){
      var id = el.getAttribute('id');
      var gd = document.getElementById(id);
      
      let defaultInputs = {
        x: [$('#x1').val(), $('#x2').val(), $('#x3').val()],
        y: [$('#y1').val(), $('#y2').val(), $('#y3').val()],
        mode: 'markers',
        type: 'scatter',
        name: 'user'
      };
      
      Plotly.addTraces(gd, defaultInputs);
      
      document.getElementById('inputcontainerid').addEventListener('input', function(event){
        let userInputs = {
          x: [[$('#x1').val(), $('#x2').val(), $('#x3').val()]],
          y: [[$('#y1').val(), $('#y2').val(), $('#y3').val()]]
        };
        Plotly.restyle(gd, userInputs, 1);
      });
    }
    "
    
    gg <- ggplot(X, aes(x, y)) + geom_point()
    ggplotly(gg) %>% 
      layout(xaxis = list(autorange = TRUE), yaxis = list(autorange = TRUE)) %>%
      onRender(jsCode = JS)
    ```
    

    result

    For additional infos please see chapter 5 "Event handling in JavaScript" from Carson Sievert's book Interactive web-based data visualization with R, plotly, and shiny.