I have a dataframe that has these columns:
document, user, month, views
I am using a selectInput to filter the data by document.
I want to plot a (Plotly) line chart of views per month, for each user, for the selected document.
E.g. If one filters to a document for which ten users exist, I want to display ten plots, each showing the relevant user's views per month.
At current:
- I filter the data to the selected document (dplyr).
- I pass the filtered data to a function.
- In the function, I loop through the current document's users.
- In each loop, I filter the data to the current user (dplyr), and append a Plotly output to a output list.
- At the end of the function, I return the output list.
- The result of the function is assigne to a UI output.
The app successfully runs, but where the plots should display, I get a Result must have length x, not y error.
How would you go about this? I appreciate any advice you can give me.
For security reasons I cannot share my existing code, sorry - I understand that it's not very useful.
Edit: I've created a minimal reproducible example, based on this.
The process has changed slightly from my original question, mainly that I'm not using a separate function.
library(plotly)
library(tidyverse)
# DATA
data <- data.frame(
document= c("doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc1","doc2","doc2","doc2","doc2","doc2","doc2","doc2","doc2","doc2","doc2","doc2","doc2","doc2","doc2","doc2","doc2","doc2","doc2","doc2","doc2","doc2","doc2","doc2","doc2","doc2","doc2","doc2","doc2","doc2","doc2","doc2","doc2","doc2","doc2","doc2","doc2","doc2","doc2","doc2","doc2","doc2","doc2","doc2","doc2","doc2","doc2","doc2","doc2","doc2","doc2","doc2","doc2","doc2","doc2","doc2","doc2","doc2"),
user= c("user2","user2","user2","user2","user2","user2","user2","user2","user2","user2","user2","user2","user2","user2","user2","user2","user2","user2","user2","user3","user3","user3","user3","user3","user3","user3","user3","user3","user3","user3","user3","user3","user3","user3","user3","user3","user3","user3","user4","user4","user4","user4","user4","user4","user4","user4","user4","user4","user4","user4","user4","user4","user4","user4","user4","user4","user4","user1","user1","user1","user1","user1","user1","user1","user1","user1","user1","user1","user1","user1","user1","user1","user1","user1","user1","user1","user2","user2","user2","user2","user2","user2","user2","user2","user2","user2","user2","user2","user2","user2","user2","user2","user2","user2","user2","user4","user4","user4","user4","user4","user4","user4","user4","user4","user4","user4","user4","user4","user4","user4","user4","user4","user4","user4"),
month= as.Date(c("2017-01-01","2017-02-01","2017-03-01","2017-04-01","2017-05-01","2017-06-01","2017-07-01","2017-08-01","2017-09-01","2017-10-01","2017-11-01","2017-12-01","2018-01-01","2018-02-01","2018-03-01","2018-04-01","2018-05-01","2018-06-01","2018-07-01","2017-01-01","2017-02-01","2017-03-01","2017-04-01","2017-05-01","2017-06-01","2017-07-01","2017-08-01","2017-09-01","2017-10-01","2017-11-01","2017-12-01","2018-01-01","2018-02-01","2018-03-01","2018-04-01","2018-05-01","2018-06-01","2018-07-01","2017-01-01","2017-02-01","2017-03-01","2017-04-01","2017-05-01","2017-06-01","2017-07-01","2017-08-01","2017-09-01","2017-10-01","2017-11-01","2017-12-01","2018-01-01","2018-02-01","2018-03-01","2018-04-01","2018-05-01","2018-06-01","2018-07-01","2017-01-01","2017-02-01","2017-03-01","2017-04-01","2017-05-01","2017-06-01","2017-07-01","2017-08-01","2017-09-01","2017-10-01","2017-11-01","2017-12-01","2018-01-01","2018-02-01","2018-03-01","2018-04-01","2018-05-01","2018-06-01","2018-07-01","2017-01-01","2017-02-01","2017-03-01","2017-04-01","2017-05-01","2017-06-01","2017-07-01","2017-08-01","2017-09-01","2017-10-01","2017-11-01","2017-12-01","2018-01-01","2018-02-01","2018-03-01","2018-04-01","2018-05-01","2018-06-01","2018-07-01","2017-01-01","2017-02-01","2017-03-01","2017-04-01","2017-05-01","2017-06-01","2017-07-01","2017-08-01","2017-09-01","2017-10-01","2017-11-01","2017-12-01","2018-01-01","2018-02-01","2018-03-01","2018-04-01","2018-05-01","2018-06-01","2018-07-01")),
views= c(19,39,34,3,25,5,1,16,37,21,46,34,23,0,8,10,46,3,47,16,32,4,44,42,12,8,27,39,28,30,26,45,49,38,32,32,1,16,23,34,41,46,37,0,23,44,10,3,43,43,22,38,1,33,11,15,8,21,37,17,7,29,1,33,47,45,37,20,9,41,37,18,30,46,24,45,48,42,49,3,10,17,46,6,12,29,13,6,4,44,37,26,43,5,19,28,44,20,35,40,32,20,41,46,25,47,35,3,25,25,41,5,26,32)
)
# SERVER
server <- shinyServer(function(input, output) {
output$plots <- renderUI({
doc_data <- filter(data, document == input$select_doc) # This is the breaking line
plot_output_list <- lapply(1:length(unique(doc_data$user)), function(i) {
plotname <- paste("plot", i, sep="")
plotlyOutput(plotname)
})
do.call(tagList, plot_output_list)
})
for (i in 1:length(unique(doc_data$user))) {
local({
local_i <- i
doc_users <- unique(doc_data$user)
plotname <- paste("plot", local_i, sep="")
plot_data <- filter(doc_data, user == doc_users[local_i]) %>%
arrange(month)
output[[plotname]] <- renderPlotly({
p <- plot_ly(x= plot_data$month, y= plot_data$views, type = 'scatter', mode = 'lines')
p$elementId <- NULL
p
})
})
}
})
# UI
ui <- shinyUI(pageWithSidebar(
headerPanel("Minimum reproducible example"),
sidebarPanel(
selectInput("select_doc", choices= unique(data$document), label="", selected= 'doc1')#,
),
mainPanel(
uiOutput("plots")
)
))
# RUN
shinyApp(ui, server)
I've created an R Shiny application to help me streamline some common data cleaning tasks for working with high dimensional chemical composition data. Specifically, this app uses the fluidPage ui and ggplot/plotly interface to create a biplot with user selected X and Y variables and color/symbol attributes. The event_data function allows users to see attributes associated with points they interactively select via the rectangular selection or lasso. I'm new to Shiny so the code is not very elegant but I've managed to do all of the above.
I'm hoping to add one additional feature and I'm stuck on the best way to approach this. Specifically I'd like to be able to change one field in the dataset for points that are currently selected on a given plot. My current idea is to have a text field input that will allow me to type in what I'd like the new value in the field and have the change execute with an actionButton.
I found the answers to the question linked here quite useful but I still haven't managed to get this to work. Below is my current application script and a screenshot of the output as it stands now.
Any help or suggestions for new approaches would be greatly appreciated.
library(plotly)
library(shiny)
library(knitr)
library(kableExtra)
myApp <- function(attributes,dat1) {
dataset <- cbind(attributes,dat1)
ui <- fluidPage(
plotlyOutput('plot', width='1000px', height='600px'),
fluidRow(
column(2,
selectInput('xvar','X',names(dat1)),
selectInput('yvar','Y',names(dat1))),
column(3,offset=0.5,
selectInput('Code','GROUP',names(attributes)),
checkboxInput('Conf','Confidence Hull',value=TRUE)),
column(3,offset=0.5,
actionButton('Change','Change Group Assignment'),
textInput('NewGroup', label = 'Enter new group designation')),
column(3,offset=0.5,
actionButton("exit", label = "Return to R and write data"))),
verbatimTextOutput('brush')
)
server <- function(input, output) {
data.sel <- reactive({
dataset[,c(input$xvar,input$yvar,input$Code)]
})
output$plot <- renderPlotly({
p <- ggplot(data.sel(), aes(x=data.sel()[,1], y=data.sel()[,2],
color=data.sel()[,3], shape=data.sel()[,3])) +
geom_point() +
labs(x=input$xvar,y=input$yvar)
if(input$Conf) {p <- p + stat_ellipse(level=0.95)}
ggplotly(p) %>% layout(dragmode = 'select')
})
output$brush <- renderPrint({
d <- event_data('plotly_selected')
dd <- round(cbind(d[[3]],d[[4]]),3)
vv <- attributes[which(round(data.sel()[,1],3) %in% dd[,1] &
round(data.sel()[,2],3) %in% dd[,2]),]
if (is.null(d)) 'Click and drag events (i.e., select/lasso) appear here
(double-click to clear)' else kable(vv)
})
observe({
if(input$exit > 0)
stopApp()})
}
runApp(shinyApp(ui, server))
return(dataset)
}
In order to test this you can use a modified version of the iris data as I show below. Essentially, I'd like to be able to change the text in the new variable I'm adding to the iris data.
iris2 <- cbind(iris,rep('A',150))
names(iris2)[6] <- 'Assignment'
myApp(iris2[,5:6],iris2[,-(5:6)])
Here is a screenshot of the app in action. I've included the buttons to go along with my proposed solution but they currently do nothing.
Screenshot:
I was able to get this working as I originally intended once I understood how scoping assignment works in Shiny in relation to reactive statements. This app now mostly does everything I want it do, though I feel the code is really just cobbled together at this point and needs to be fixed in many areas. In particular I have a very janky solution to finding the selected items in my original dataframe as I really don't like the curvenumber/pointnumber index system.
library(plotly)
library(shiny)
library(knitr)
library(kableExtra)
theme_set(theme_light())
myApp <- function(attributes,dat1) {
dataset <- cbind(attributes,dat1)
vv <- NULL
ui <- fluidPage(
plotlyOutput('plot', width='1000px', height='600px'),
fluidRow(
column(2,
selectInput('xvar','X',names(dat1),selected='cs'),
selectInput('yvar','Y',names(dat1),selected='ta')),
column(3,offset=0.5,
selectInput('Code','GROUP',names(attributes),selected='CORE'),
checkboxInput('Conf','Confidence Elipse',value=TRUE),
sliderInput('int.set','Set Confidence Interval',min=0.80,max=0.99,step=0.01,value=0.95)),
column(3,offset=0.5,
br(),
actionButton('Change','Change Group Assignment'),
textInput('NewGroup', label = 'Enter new group designation')),
column(3,offset=0.5,
br(),
actionButton('refresh', label='Refresh Plot with New Assignments'),
br(),br(),
actionButton("exit", label = "Return to R and write data"))),
verbatimTextOutput('brush')
)
server <- function(input, output) {
values <- reactiveValues(vv = NULL)
data.sel <- reactive({
dataset[,c(input$xvar,input$yvar,input$Code)]
})
output$plot <- renderPlotly({
g1 <- data.sel()
p <- ggplot(g1, aes(x=g1[,1], y=g1[,2], color=g1[,3], shape=g1[,3])) +
geom_point() +
labs(x=input$xvar,y=input$yvar,color=input$Code,shape=input$Code)
if(input$Conf) {p <- p + stat_ellipse(level=input$int.set)}
ggplotly(p) %>% layout(dragmode = 'select')
})
output$brush<- renderPrint({
g1 <- data.sel()
d <- event_data('plotly_selected')
dd <- round(cbind(d[[3]],d[[4]]),3)
vv <- attributes[which(round(g1[,1],3) %in% dd[,1] & round(g1[,2],3) %in% dd[,2]),]
vv <<- vv
if (is.null(vv)) "Click and drag events (i.e., select/lasso) appear here (double-click to clear)" else kable(vv)
})
observeEvent(input$Change > 0, {
if (!is.null(vv)) {
dataset[which(row.names(dataset) %in% row.names(vv)),]$CORE <<-
input$NewGroup
}})
observe({
if(input$exit > 0)
stopApp()})
}
runApp(shinyApp(ui, server))
return(dataset)
}
And some test data
data(iris)
iris2 <- cbind(iris,rep('a',nrow(iris)))
names(iris2)[6] <- 'CORE'
out <- myApp(iris2[,5:6],iris2[,1:4])
Background: I'm building a dashboard that interfaces with a MySQL database. The user specifies a coarse filter to pull data from the database and clicks "Submit", the data are plotted with ggvis, then the user is able to play with fine filters to affect what subset of data are plotted. These fine filters depend on the data pulled from the database, therefore I generate them from the data using uiOutput/renderUI.
Problem: My challenge is that I want the UI to be updated based on the data before the plot is updated. Otherwise the fine filters from the old dataset are applied to the new data, which results in an error when plotting.
Example: The following example roughly reproduces the problem using mtcars. To get the error, select 4 cylinders, click "Submit", then select 6 cylinders and click "Submit" again. In this case, when the 4 cylinder fine filter is applied to the 6 cylinder dataset only a single point is returned, which causes an error when trying to apply a smoother in ggvis. Not the same error as I'm getting, but close enough.
library(shiny)
library(dplyr)
library(ggvis)
ui <- fluidPage(
headerPanel("Example"),
sidebarPanel(
h2("Course Filter:"),
selectInput("cyl_input", "Cylinders", c(4, 6)),
actionButton("submit", "Submit"),
conditionalPanel(condition = "input.submit > 0",
h2("Fine Filter: "),
uiOutput("mpg_input")
)
),
mainPanel(
ggvisOutput("mtcars_plot")
)
)
server <- function(input, output) {
mycars <- eventReactive(input$submit, {
filter(mtcars, cyl == input$cyl_input)
})
output$mpg_input <- renderUI({
mpg_range <- range(mycars()$mpg)
sliderInput("mpg_input", "MPG: ",
min = mpg_range[1], max = mpg_range[2],
value = mpg_range,
step = 0.1)
})
observe({
if (!is.null(input$mpg_input)) {
mycars() %>%
filter(mpg >= input$mpg_input[1],
mpg <= input$mpg_input[2]) %>%
ggvis(~mpg, ~wt) %>%
layer_points() %>%
layer_smooths() %>%
bind_shiny("mtcars_plot")
}
})
}
shinyApp(ui = ui, server = server)
After many hours of messing around, I've found a very hacky workaround. I'm not very satisfied with it, so am hoping someone can offer an improvement.
To summarize, my realization was that the renderUI call was being executed when it was supposed to be, i.e. prior to the plot being generated. However, renderUI doesn't directly change the slider in the UI, rather it sends a message to the browser telling it to update the slider. Such messages are only executed once all observers have been run. In particular, this happens after the observer wrapping the call to ggvis is run. So, the sequence seems to be
Message sent to browser to update slider.
Plot generated based on values in slider, which are still the old values.
Browser updates slider. Sadly too late :(
So, to work around this I decided to create a new reactive variable storing the range of MPG values. Immediately after the coarse filter has been applied, and before the slider is updated in the browser, this variable references the new data frame directly. Afterwards, when playing with the slider directly, this reactive variable references the slider. This just requires setting a flag specifying whether to reference the data frame or the slider, then flipping the flag in a sensible location.
Here's the code:
library(shiny)
library(dplyr)
library(ggvis)
ui <- fluidPage(
headerPanel("Example"),
sidebarPanel(
h2("Course Filter:"),
selectInput("cyl_input", "Cylinders", c(4, 6)),
actionButton("submit", "Submit"),
conditionalPanel(condition = "input.submit > 0",
h2("Fine Filter: "),
uiOutput("mpg_input")
)
),
mainPanel(
ggvisOutput("mtcars_plot")
)
)
server <- function(input, output) {
# create variable to keep track of whether data was just updated
fresh_data <- TRUE
mycars <- eventReactive(input$submit, {
# data have just been refreshed
fresh_data <<- TRUE
filter(mtcars, cyl == input$cyl_input)
})
output$mpg_input <- renderUI({
mpgs <- range(mycars()$mpg)
sliderInput("mpg_input", "MPG: ",
min = mpgs[1], max = mpgs[2],
value = mpgs,
step = 0.1)
})
# make filtering criterion a reactive expression
# required because web page inputs not updated until after everything else
mpg_range <- reactive({
# these next two lines are required though them seem to do nothing
# from what I can tell they ensure that mpg_range depends reactively on
# these variables. Apparently, the reference to these variables in the
# if statement is not enough.
input$mpg_input
mycars()
# if new data have just been pulled reference data frame directly
if (fresh_data) {
mpgs <- range(mycars()$mpg)
# otherwise reference web inputs
} else if (!is.null(input$mpg_input)) {
mpgs <- input$mpg_input
} else {
mpgs <- NULL
}
return(mpgs)
})
observe({
if (!is.null(mpg_range())) {
mycars() %>%
filter(mpg >= mpg_range()[1],
mpg <= mpg_range()[2]) %>%
ggvis(~mpg, ~wt) %>%
layer_points() %>%
layer_smooths() %>%
bind_shiny("mtcars_plot")
}
# ui now updated, data no longer fresh
fresh_data <<- FALSE
})
}
shinyApp(ui = ui, server = server)