Joe
Joe

Reputation: 311

How can I create a custom JS function to copy plotly image to clipboard in R shiny

I would like to implement a button on a plotly chart that will copy the plot to the user's clipboard similar to how the snapshot button downloads a png of the plot as a file.

I've referenced this documentation to create a custom modebar button, but I'm not familiar enough with Javascript to know how to write that snippet (or if its even possible).

Below is the R code I wrote to try it, but it doesn't work. A button does appear (although the image is not visible, but I can tell its there if I hover in the upper rightmost corner). But when I click it, the plot is not copied to the clipboard and the chrome console says:

Uncaught TypeError: Plotly.execCommand is not a function at Object.eval [as click] (eval at tryEval ((index):258), :2:15) at HTMLAnchorElement. (:7:2055670)

Figure:

Plotly modebar with my desired button

My code:

library(plotly)
library(shiny)

d <- data.frame(xaxis = c(1,2,3,4,5), 
                ydata = c(10,40,60,30,25))

p <- plot_ly() %>%
  add_trace(data = d,
            x = ~xaxis,
            y = ~ydata,
            type = "scatter", mode = "lines")

plotClip <- list(
  name = "plotClip",
  icon = list(
    path = "plotClip.svg",
    transform = 'matrix(1 0 0 1 -2 -2) scale(0.7)'
  ),
  click = htmlwidgets::JS(
    'function(gd) {
       Plotly.execCommand("copy");
       alert("Copied the plot");
    }'
  )
)

p <- p %>%
  config(modeBarButtonsToAdd = list(plotClip),
         displaylogo = FALSE,
         toImageButtonOptions= list(filename = "plot.png",
                                    format = "png",
                                    width = 800, height = 400))

ui <- fluidPage(
      plotlyOutput(outputId = "myplot")
)

server <- function(input, output) {
  output$myplot <- renderPlotly({
    p
  })
}

shinyApp(ui, server)

Thanks for any insight on this!

Upvotes: 3

Views: 1857

Answers (3)

St&#233;phane Laurent
St&#233;phane Laurent

Reputation: 84519

I don't know how to deal with the toolbar, but here is how to copy the image to the clipboard by clicking a button:

library(shiny)
library(plotly)

d <- data.frame(X1 = rnorm(50,mean=50,sd=10), 
                X2 = rnorm(50,mean=5,sd=1.5), 
                Y = rnorm(50,mean=200,sd=25))

ui <- fluidPage(
  title = 'Copy Plotly to clipboard',
  sidebarLayout(
    
    sidebarPanel(
      helpText(),
      actionButton('copy', "Copy")
    ),
    
    mainPanel(
      plotlyOutput('regPlot'),
      tags$script('
        async function copyImage(url) {
          try {
            const data = await fetch(url);
            const blob = await data.blob();
            await navigator.clipboard.write([
              new ClipboardItem({
                [blob.type]: blob
              })
            ]);
            console.log("Image copied.");
          } catch (err) {
            console.error(err.name, err.message);
          } 
        }
        document.getElementById("copy").onclick = function() {
          var gd = document.getElementById("regPlot");
          Plotly.Snapshot.toImage(gd, {format: "png"}).once("success", function(url) {
            copyImage(url);
          });
        }')
    )
  )
)

server <- function(input, output, session) {
  
  regPlot <- reactive({
    plot_ly(d, x = d$X1, y = d$X2, mode = "markers")
  })
  
  output$regPlot <- renderPlotly({
    regPlot()
  })
  
}

shinyApp(ui = ui, server = server)

EDIT

I found how to deal with the toolbar. This even doesn't require Shiny.

library(plotly)

asd <- data.frame(
  week = c(1, 2, 3), 
  a    = c(12, 41, 33), 
  b    = c(43, 21, 23), 
  c    = c(43, 65, 43), 
  d    = c(33, 45, 83)
)

js <- c(
  'function (gd) {',
  '  Plotly.Snapshot.toImage(gd, { format: "png" }).once(',
  '    "success",',
  '    async function (url) {',
  '      try {',
  '        const data = await fetch(url);',
  '        const blob = await data.blob();',
  '        await navigator.clipboard.write([',
  '          new ClipboardItem({',
  '            [blob.type]: blob',
  '          })',
  '        ]);',
  '        console.log("Image copied.");',
  '      } catch (err) {',
  '        console.error(err.name, err.message);',
  '      }',
  '    }',
  '  );',
  '}'
)

Copy_SVGpath <- "M97.67,20.81L97.67,20.81l0.01,0.02c3.7,0.01,7.04,1.51,9.46,3.93c2.4,2.41,3.9,5.74,3.9,9.42h0.02v0.02v75.28 v0.01h-0.02c-0.01,3.68-1.51,7.03-3.93,9.46c-2.41,2.4-5.74,3.9-9.42,3.9v0.02h-0.02H38.48h-0.01v-0.02 c-3.69-0.01-7.04-1.5-9.46-3.93c-2.4-2.41-3.9-5.74-3.91-9.42H25.1c0-25.96,0-49.34,0-75.3v-0.01h0.02 c0.01-3.69,1.52-7.04,3.94-9.46c2.41-2.4,5.73-3.9,9.42-3.91v-0.02h0.02C58.22,20.81,77.95,20.81,97.67,20.81L97.67,20.81z M0.02,75.38L0,13.39v-0.01h0.02c0.01-3.69,1.52-7.04,3.93-9.46c2.41-2.4,5.74-3.9,9.42-3.91V0h0.02h59.19 c7.69,0,8.9,9.96,0.01,10.16H13.4h-0.02v-0.02c-0.88,0-1.68,0.37-2.27,0.97c-0.59,0.58-0.96,1.4-0.96,2.27h0.02v0.01v3.17 c0,19.61,0,39.21,0,58.81C10.17,83.63,0.02,84.09,0.02,75.38L0.02,75.38z M100.91,109.49V34.2v-0.02h0.02 c0-0.87-0.37-1.68-0.97-2.27c-0.59-0.58-1.4-0.96-2.28-0.96v0.02h-0.01H38.48h-0.02v-0.02c-0.88,0-1.68,0.38-2.27,0.97 c-0.59,0.58-0.96,1.4-0.96,2.27h0.02v0.01v75.28v0.02h-0.02c0,0.88,0.38,1.68,0.97,2.27c0.59,0.59,1.4,0.96,2.27,0.96v-0.02h0.01 h59.19h0.02v0.02c0.87,0,1.68-0.38,2.27-0.97c0.59-0.58,0.96-1.4,0.96-2.27L100.91,109.49L100.91,109.49L100.91,109.49 L100.91,109.49z"

CopyImage <- list(
  name = "Copy",
  icon = list(
    path = Copy_SVGpath,
    width = 111,
    height = 123
  ),
  click = htmlwidgets::JS(js)
)

plot_ly(
  asd, x = ~week, y = ~`a`, name = "a", type = "scatter", mode = "lines"
) %>%
  add_trace(y = ~`b`, name = "b", mode = "lines") %>%
  layout(
    xaxis = list(title = "Week", showgrid = FALSE, rangemode = "normal"),
    yaxis = list(title = "", showgrid = FALSE, rangemode = "normal"),
    hovermode = "x unified"
  ) %>%
  config(modeBarButtonsToAdd = list(CopyImage))

Upvotes: 3

user3408956
user3408956

Reputation: 53

I built upon Joe's answer to resolve his solution's limitations

  • Used Plotly.toImage instead of Plotly.Snapshot.toImage to enable users to either choose copied plot's size via toImageButtonOptions or default to whatever size is shown in shiny app.
  • added svg copy icon

Limitations:

  • only works in localhost or shinyapps.io or shiny server pro, because all browers will block clipboard access unless website uses HTTPS or is localhost
library(tidyverse)
library(plotly)

# only works in shinyapps.io or localhost due to all browsers only allowing clipboard access over HTTPS or localhost
plotly_add_copy_button <- function(pl) {
  
  # can also be htmlwidgets::JS("Plotly.Icons.disk")
  # download svg from eg https://uxwing.com/files-icon/
  icon_copy_svg <- list(
    path = str_c(
      "M102.17,29.66A3,3,0,0,0,100,26.79L73.62,1.1A3,3,0,0,0,71.31,0h-46a5.36,5.36,0,0,0-5.36,5.36V20.41H5.36A5.36,5.36,0,0,0,0,25.77v91.75a5.36,",
      "5.36,0,0,0,5.36,5.36H76.9a5.36,5.36,0,0,0,5.33-5.36v-15H96.82a5.36,5.36,0,0,0,5.33-5.36q0-33.73,0-67.45ZM25.91,20.41V6h42.4V30.24a3,3,0,0,0,",
      "3,3H96.18q0,31.62,0,63.24h-14l0-46.42a3,3,0,0,0-2.17-2.87L53.69,21.51a2.93,2.93,0,0,0-2.3-1.1ZM54.37,30.89,72.28,47.67H54.37V30.89ZM6,116.89V26.",
      "37h42.4V50.65a3,3,0,0,0,3,3H76.26q0,31.64,0,63.24ZM17.33,69.68a2.12,2.12,0,0,1,1.59-.74H54.07a2.14,2.14,0,0,1,1.6.73,2.54,2.54,0,0,1,.63,1.7,2.",
      "57,2.57,0,0,1-.64,1.7,2.16,2.16,0,0,1-1.59.74H18.92a2.15,2.15,0,0,1-1.6-.73,2.59,2.59,0,0,1,0-3.4Zm0,28.94a2.1,2.1,0,0,1,1.58-.74H63.87a2.12,2.12,",
      "0,0,1,1.59.74,2.57,2.57,0,0,1,.64,1.7,2.54,2.54,0,0,1-.63,1.7,2.14,2.14,0,0,1-1.6.73H18.94a2.13,2.13,0,0,1-1.59-.73,2.56,2.56,0,0,1,0-3.4ZM63.87,83.",
      "41a2.12,2.12,0,0,1,1.59.74,2.59,2.59,0,0,1,0,3.4,2.13,2.13,0,0,1-1.6.72H18.94a2.12,2.12,0,0,1-1.59-.72,2.55,2.55,0,0,1-.64-1.71,2.5,2.5,0,0,1,.65",
      "-1.69,2.1,2.1,0,0,1,1.58-.74ZM17.33,55.2a2.15,2.15,0,0,1,1.59-.73H39.71a2.13,2.13,0,0,1,1.6.72,2.61,2.61,0,0,1,0,3.41,2.15,2.15,0,0,1-1.59.73H18.92a2.",
      "14,2.14,0,0,1-1.6-.72,2.61,2.61,0,0,1,0-3.41Zm0-14.47A2.13,2.13,0,0,1,18.94,40H30.37a2.12,2.12,0,0,1,1.59.72,2.61,2.61,0,0,1,0,3.41,2.13,2.13,0,0,1-1.58.",
      "73H18.94a2.16,2.16,0,0,1-1.59-.72,2.57,2.57,0,0,1-.64-1.71,2.54,2.54,0,0,1,.65-1.7ZM74.3,10.48,92.21,27.26H74.3V10.48Z"
    ),
    transform = 'scale(0.12)'
  )
  
  plotly_copy_button <- list(
    name = "Copy to Clipboard",
    icon = icon_copy_svg,
    click = htmlwidgets::JS('function(gd) {copyPlot(gd)}') # JS function defined by us and added in ui.R
  )
  
  pl <- pl %>%
    config(
      modeBarButtonsToAdd = list(plotly_copy_button),
      displaylogo = FALSE,
      toImageButtonOptions= list(format = "png", width = NULL, height = NULL)
    )
  
  pl
}


# based on https://stackoverflow.com/questions/64721568/how-can-i-create-a-custom-js-function-to-copy-plotly-image-to-clipboard-in-r-shi
# stackoverlow used Plotly.Snapshot.toImage, but need to use Plotly.toImage to control height width: https://github.com/plotly/plotly.js/issues/83
# see https://github.com/plotly/plotly.js/blob/master/src/plot_api/to_image.js for optional arguments
# this JS function needs to be added to ui.R
copy_plot_js <- 'function copyPlot(gd) {
  var toImageButtonOptions = gd._context.toImageButtonOptions;
  var opts = {
    format: toImageButtonOptions.format || "png",
    width: toImageButtonOptions.width || null,
    height: toImageButtonOptions.height || null
  };
  Plotly.toImage(gd, opts).then(async function(url) {
    try {
      const data = await fetch(url);
      const blob = await data.blob();
      await navigator.clipboard.write([
        new ClipboardItem({
          [blob.type]: blob
        })
      ]);
      console.log("Image copied.");
    } catch (err) {
      console.error(err.name, err.message);
    }
  });
  alert("Copied the plot");
}'

enter image description here

Upvotes: 0

Joe
Joe

Reputation: 311

For anyone looking for how to do this in the toolbar/modebar specifically, I modified Stephane Laurent's answer from below to get it to work.

However, the issues that remain are:

  1. The initial click of the button copies the chart to clipboard but the size is different from what is shown on screen initially. If you change the chart at all, even by simply changing the browser window size, then click the button again, the copied chart looks exactly like it does in browser (ideal behavior).
  2. Setting {format: "png", height: 400, width: 800} does not seem to explicitly define the size of the copied chart.
  3. The icon does not appear on the button despite the file being in the same dir as the app.

full code:

library(plotly)
library(shiny)

d <- data.frame(xaxis = c(1,2,3,4,5), 
                ydata = c(10,40,60,30,25))

p <- plot_ly() %>%
  add_trace(data = d,
            x = ~xaxis,
            y = ~ydata,
            type = "scatter", mode = "lines")

plotClip <- list(
  name = "plotClip",
  icon = list(
    path = "plotClip.svg",
    transform = 'matrix(1 0 0 1 -2 -2) scale(0.7)'
  ),
  click = htmlwidgets::JS(
    'function(gd) {
       Plotly.Snapshot.toImage(gd, {format: "png"}).once("success", function(url) {
            copyImage(url);
          });
       alert("Copied the plot");
    }'
  )
)

p <- p %>%
  config(modeBarButtonsToAdd = list(plotClip),
         displaylogo = FALSE,
         toImageButtonOptions= list(filename = "plot.png",
                                    format = "png",
                                    width = 800, height = 400))

copyImgTag <- tags$script(
  'async function copyImage(url) {
          try {
            const data = await fetch(url);
            const blob = await data.blob();
            await navigator.clipboard.write([
              new ClipboardItem({
                [blob.type]: blob
              })
            ]);
            console.log("Image copied.");
          } catch (err) {
            console.error(err.name, err.message);
          } 
        }'
)

ui <- tagList(
  fluidPage(
    plotlyOutput(outputId = "myplot")
    ), 
  copyImgTag
)

server <- function(input, output) {
  output$myplot <- renderPlotly({
    p
  })
}

shinyApp(ui, server)

Upvotes: 2

Related Questions