Reputation: 311
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:
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
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)
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
Reputation: 53
I built upon Joe's answer to resolve his solution's limitations
Limitations:
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");
}'
Upvotes: 0
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:
{format: "png", height: 400, width: 800}
does not seem to explicitly define the size of the copied chart.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