Hi,
I'm trying to return a chart generated by Highcharts & Plotly. However I'm getting the following error.
<simpleError: No method asJSON S3 class: htmlwidget>
Error in run(timeoutMs) : index out of bounds
In addition: Warning message:
In li["message"] <- err :
number of items to replace is not a multiple of replacement length
Here is the portion of code which generates the plot. First one which returns a PNG works fine, but 2&3 fails
#* @get /salesgraph
makePlot <- function(){
par(mar = rep(2, 4)) # margins
dates <- seq(as.Date("2015-10-01"),
as.Date("2015-10-31"), by=1)
sold <- 1:31
plot(dates, sold, type="b", main="Sales")
}
#* @get /highgraph
highPlot <- function(){
data(diamonds, economics_long, mpg, package = "ggplot2")
library(dplyr)
library(highcharter)
return(hchart(mpg, "scatter", hcaes(x = displ, y = hwy, group = class)))
}
#* @get /plotly
plotlygraph <- function(){
library(plotly)
set.seed(100)
d <- diamonds[sample(nrow(diamonds), 1000), ]
plot_ly(d, x = ~carat, y = ~price, color = ~carat,
size = ~carat, text = ~paste("Clarity: ", clarity
}
Good question. (First, in your example above you were missing a @png annotation on the image that instructs plumber to return an image, but it sounds like you already figured that out.)
In the same way that you have to instruct plumber that your @png endpoint is a special case that's going to return something other than JSON, you need to do the same for HTML widgets. In your example you're just returning an S3 object (I believe -- I'm not terribly familiar with either highcharter or plotly) which rightly has problems getting rendered to JSON.
What you actually want is to return the HTML of the htmlwidget. That requires rendering the widget into a single self-contained HTML file that includes all the dependencies that the widget needs in order to load in the browser. The best function I could find is htmlwidgets::saveWidget. Unfortunately this requires writing the file out to disk only to be read back in, but that's not going to be the most expensive part of this operation anyways, so let's not sweat it.
I think this might merit a new serializer in plumber, but without that you could use htmlwidgets::saveWidget() to write out the singular HTML file that you need, then combine that with the @serializer html annotation on your function to tell plumber "don't try to render this as a big JSON string. Just serve it as HTML."
That would make your example file something like the following, which now serves all three examples properly.
#* @get /salesgraph
#* @png
makePlot <- function(){
par(mar = rep(2, 4)) # margins
dates <- seq(as.Date("2015-10-01"),
as.Date("2015-10-31"), by=1)
sold <- 1:31
plot(dates, sold, type="b", main="Sales")
}
#* @get /highgraph
#* @serializer html
highPlot <- function(){
data(diamonds, economics_long, mpg, package = "ggplot2")
library(dplyr)
library(highcharter)
ch <- hchart(mpg, "scatter", hcaes(x = displ, y = hwy, group = class))
# It looks like htmlwidgets is picky about the file you write to. It has to end in
# .html or else it will silently not write a selfcontained file?
file <- tempfile(fileext=".html")
# Must write a self-contained file. We're not serving a directory of assets
# in response to this request, just one HTML file.
htmlwidgets::saveWidget(ch, file, selfcontained=TRUE)
# Read the file back in as a single string and return.
paste(readLines(file), collapse="")
}
#* @get /plotly
#* @serializer html
plotlygraph <- function(){
library(plotly)
set.seed(100)
d <- diamonds[sample(nrow(diamonds), 1000), ]
ch <- plot_ly(d, x = ~carat, y = ~price, color = ~carat,
size = ~carat, text = ~paste("Clarity: ", clarity))
# It looks like htmlwidgets is picky about the file you write to. It has to end in
# .html or else it will silently not write a selfcontained file?
file <- tempfile(fileext=".html")
# Must write a self-contained file. We're not serving a directory of assets
# in response to this request, just one HTML file.
htmlwidgets::saveWidget(ch, file, selfcontained=TRUE)
# Read the file back in as a single string and return.
paste(readLines(file), collapse="")
}
I actually went ahead and added a custom serializer for this so that you don't have to save the widget yourself. These functions can now be simplified to:
#* @get /plotly
#* @serializer htmlwidget
plotlygraph <- function(){
library(plotly)
set.seed(100)
d <- diamonds[sample(nrow(diamonds), 1000), ]
plot_ly(d, x = ~carat, y = ~price, color = ~carat,
size = ~carat, text = ~paste("Clarity: ", clarity))
}
If you add the @serializer htmlwidget annotation, plumber will now take care of saving the widget out to HTML and returning it for you.
Most helpful comment
I actually went ahead and added a custom serializer for this so that you don't have to save the widget yourself. These functions can now be simplified to:
If you add the
@serializer htmlwidgetannotation, plumber will now take care of saving the widget out to HTML and returning it for you.