Search code examples
javascriptrhtmlwidgetsggiraph

R ggiraph dynamically setting tooltip text without Shiny


What: Dynamically set the contents of ggiraph tooltips in rmarkdown knitted to html on page load.

Why: Using embedded png's tooltips can be made to be graphics which is valuable for certain biological structures where text is insufficient. Here is a minimal example of what I'm currently doing:

encodedImage = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABoAAAAUCAIAAAAcIrrpAAAACXBIWXMAAASdAAAEnQF8NGuhAAADGElEQVR4nI2Uy08TURTG+VuMJmCCoIKIxq2ujEQT9y40yKsiILrSGITIzoVxZUwDFQlKQiC0ttMpMwXsi9JSSkvftJ0WWkuhBVtoAb+ZKeNQy+PkZjJz5t7fPfec892yg1K2u7u7tbW1vr4ejUYDgYDL5fJ4PHhhGCYej+NXyVWwsqLvnZ0dULAsFAphvdfrBcvhcNjt9oWFhfn5eZPJpNfr8b62tra/v38sLpPJxGIxTML+q6urIAaDQZ/P53a7nU4ncFar1Ww2G41GnU43MzND0/T09DR2LYHb3t4OcxaJRAQiPq2u8KI36vDHXKGEJ5xkX/yMPxCYm5sjSVKlUikUCmx5BIdcrHCGcIBAXCBig3w+X95E1HVR4nG9i7rWSd3opls+mX+ZrJOTkxMTE8hsAYc1vkPz+/2AIsBcLsf/vtKuOfdEWXJcaFTdeTPLMJHx8fGxsTFknMVtbGwgOx7OkHhEhw2E4E/AYZQ3E8Na9iijo6M2m43FIZfLnKGC4KZSKXFqBVxFs/rRB/PDfuP9PgPOe75RxfsbevWYRlEUksjigHBwhvIBKg5NjKvvpgVnNJm9+nyK9996pYUHJxsZGWFxSPySyI7DIf3h35lg/A+en4mVKgnJ++/26DANJyMIgsVtbm4ucmbjLJFIlMRVtpIoa23HFJ6VbaRQjfc/3JimVCoRCovb29vDm5Uzi8WCU0MYZyxF/Qs6nclDPENDQ+l0utB30AOig4DQ9OhPcNF0J+NqO6l77/RMIgO1DA4OouePqCKbzUKGEJDBYIAkISOUBVUWcOXNaqSp5rAC1c80PTITOm5gYAASKhbZASd+hA0W9KjVajUaDTJyqZXg19d1aHC7qHVL1ZKC52KT/Mt3dTKZFOe6+EaBHiA1EKFHuVxe2fKTX3y5TS6TyaRSaXv/14qnCt55+/Vs0fJiHG+4eZA+XDDVEjW/sqZdjcZEzyOchl5dodxtJGGNn477v7LiNrYHU8gd77/5khZfeqfgqiQlcLAHfQZBtlIyeFbc448WSBWjZ3hZ7HeG07wf4+23f7/+An2Gx2N7QYsiAAAAAElFTkSuQmCC"

g = ggplot()+
  geom_point_interactive(aes(x=1,y=1,tooltip=sprintf('<img src=\"%s\" />',encodedImage)))
girafe(code=print(g))

The downside to this is that for every plot using the tooltip graphic the encoded image is repeated, resulting in a file size that is too large to store many of.

How: To mitigate the increase in file size with the increase in use of tooltips I'm looking to assign the embedded image text into a json object, then dynamically update all tooltips to be the embedded image using javascript.

Things I've Done: I'm able to get the embedded image stored in a json by simply including script tags and outputing json text inside of those tags. For testing heres the hard-coded example with a simple replace text:

<script type="application/json" id="lookuptable">
{"ID1":"ReplaceText"}
</script>

What I can't do is replace the text of the tooltips. Essentially I was planning on setting the tooltip to and ID of some sort and using that to match the embedded image to the corresponding point. The tooltip text is stored in a json by ggiraph in something like:

<script type="application/json" data-for="htmlwidget-b8ceca7828d4dd46f692"> {x:{"html":.....}}</script>

In this json all html components (<,",>,etc) are "escaped," I believe this "htmlwidget" data is passed to what is essentially a mini-html page embedded in the html page, the "htmlwidget".

I've tried wrapping my temporary tooltips in <div> tags, but since they are tied up in a JSON they aren't seen in the DOM.

I've tried just naively replacing all instances if ID in any script tags:

<script type="application/javascript">
  console.log(document.getElementById('lookuptable').innerHTML)
  var lookuptable = JSON.parse(document.getElementById('lookuptable').innerHTML);
  
  var scriptTags = document.getElementsByTagName("script");
  for (s=0; s < scriptTags.length; s++){
    var item = scriptTags[s];
    for(var k in lookuptable){
      console.log(item.innerHTML);
      item.innerHTML.replace(k,lookuptable[k])
    }
  }
</script>

However it seems that by the time that this script runs the json no longer has the tooltip text in it (though it's in the html source).

This is where I currently am stuck. I'll update or answer this question if I make any more progress.

Also I'm well aware this would be trivial with Shiny, unfortunately that is not an option as the html page needs to be entirely standalone as the real rmarkdown I'm building is meant to be a standalone report.

Finally here is a complete, reproducible example. The final knitted project should result in the tooltip being "ReplaceText" (fix R code blocks by removing \):

---
title: "Demo"
author: "Zachary Klamer"
date: "12/9/2021"
output: html_document
---

\```{r setup, include=FALSE}
knitr::opts_chunk$set(echo = TRUE)
library(ggplot2)
library(ggiraph)
library(rjson)
\```

\```{r,echo=FALSE}
lookup = list()
lookup$FullID1 = "ReplaceText"
jsonData = toJSON(lookup)
\```
<script type="application/json" id="lookuptable">
`r jsonData`
</script>



\```{r,echo=FALSE}
g = ggplot()+
  geom_point_interactive(aes(x=1,y=1,tooltip='FullID1'))
girafe(code=print(g))
\```

<script type="application/javascript">
  console.log(document.getElementById('lookuptable').innerHTML)
  var lookuptable = JSON.parse(document.getElementById('lookuptable').innerHTML);

  var scriptTags = document.getElementsByTagName("script");
  for (s=0; s < scriptTags.length; s++){
    var item = scriptTags[s];
    for(var k in lookuptable){
      console.log(item.innerHTML);
      item.innerHTML.replace(k,lookuptable[k])
    }
  }
</script>

Solution

  • There may be several different ways to achieve this goal, in particular I suspect that using the htmlwidgets "onRender" function may be able to achieve this more cleanly but I never got that to work.

    What I've found is that any editing of the innerHTML of the htmlwidget or htmlwidget data breaks the mouseover text completely, because it breaks the event listener which powers the mouseover text.

    Instead I've found I can edit the resulting svg of the htmlwidget by wrapping my ending script in a $(window).load(function(){ ... }) call. If I find all svg elements (in this case circles!) and edit the title property of those svg objects I can preserve the event listeners and change the title contents (to be an image!).

    See complete example below which has 1000 1kb images as tooltips but gives no increase in file size:

    ---
    title: "Demo"
    author: "Zachary Klamer"
    date: "12/9/2021"
    output: html_document
    ---
    
    \```{r setup, include=FALSE}
    knitr::opts_chunk$set(echo = TRUE)
    library(ggplot2)
    library(ggiraph)
    library(rjson)
    library(htmlwidgets)
    \```
    
    \```{r,echo=FALSE}
    lookup = list()
    
    lookup$FullID1 = '<img src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABoAAAAUCAIAAAAcIrrpAAAACXBIWXMAAASdAAAEnQF8NGuhAAADGElEQVR4nI2Uy08TURTG+VuMJmCCoIKIxq2ujEQT9y40yKsiILrSGITIzoVxZUwDFQlKQiC0ttMpMwXsi9JSSkvftJ0WWkuhBVtoAb+ZKeNQy+PkZjJz5t7fPfec892yg1K2u7u7tbW1vr4ejUYDgYDL5fJ4PHhhGCYej+NXyVWwsqLvnZ0dULAsFAphvdfrBcvhcNjt9oWFhfn5eZPJpNfr8b62tra/v38sLpPJxGIxTML+q6urIAaDQZ/P53a7nU4ncFar1Ww2G41GnU43MzND0/T09DR2LYHb3t4OcxaJRAQiPq2u8KI36vDHXKGEJ5xkX/yMPxCYm5sjSVKlUikUCmx5BIdcrHCGcIBAXCBig3w+X95E1HVR4nG9i7rWSd3opls+mX+ZrJOTkxMTE8hsAYc1vkPz+/2AIsBcLsf/vtKuOfdEWXJcaFTdeTPLMJHx8fGxsTFknMVtbGwgOx7OkHhEhw2E4E/AYZQ3E8Na9iijo6M2m43FIZfLnKGC4KZSKXFqBVxFs/rRB/PDfuP9PgPOe75RxfsbevWYRlEUksjigHBwhvIBKg5NjKvvpgVnNJm9+nyK9996pYUHJxsZGWFxSPySyI7DIf3h35lg/A+en4mVKgnJ++/26DANJyMIgsVtbm4ucmbjLJFIlMRVtpIoa23HFJ6VbaRQjfc/3JimVCoRCovb29vDm5Uzi8WCU0MYZyxF/Qs6nclDPENDQ+l0utB30AOig4DQ9OhPcNF0J+NqO6l77/RMIgO1DA4OouePqCKbzUKGEJDBYIAkISOUBVUWcOXNaqSp5rAC1c80PTITOm5gYAASKhbZASd+hA0W9KjVajUaDTJyqZXg19d1aHC7qHVL1ZKC52KT/Mt3dTKZFOe6+EaBHiA1EKFHuVxe2fKTX3y5TS6TyaRSaXv/14qnCt55+/Vs0fJiHG+4eZA+XDDVEjW/sqZdjcZEzyOchl5dodxtJGGNn477v7LiNrYHU8gd77/5khZfeqfgqiQlcLAHfQZBtlIyeFbc448WSBWjZ3hZ7HeG07wf4+23f7/+An2Gx2N7QYsiAAAAAElFTkSuQmCC\" />'
    jsonData = toJSON(lookup)
    \```
    <script type="application/json" id="lookuptable">
    `r jsonData`
    </script>
    
    
    \```{r,echo=FALSE}
    g = ggplot()+
      geom_point_interactive(aes(x=1:1000,y=1:1000,tooltip='FullID1'))
     girafe(code=print(g))
    \```
    
    <script type="application/javascript">
      $(window).load(function(){
        var lookuptable = JSON.parse(document.getElementById('lookuptable').innerHTML);
        var keys = Object.keys(lookuptable);
        var circleTags = document.getElementsByTagName("circle");
        for (s=0; s < circleTags.length; s++){
          var item = circleTags[s];
          var itemTitle = item.attributes.title.nodeValue;
          console.log(itemTitle);
          if (keys.includes(itemTitle)){
            item.attributes.title.nodeValue=lookuptable[itemTitle];
          }
          console.log(item.attributes.title.nodeValue);
        }
      });
    </script>