How to Add Reactivity to Dynamically Created Plots From Dynamically Created sliderInputs? - r

I'm sorry my code is too complex to create a MRE. I am currently trying to dynamically output n numbers of plots & inputSliders based on the number of calculation columns inputted. I have looked almost everywhere, however I can not seem to find a previously posted question that connects dynamically produced plots & sliders.
Goals: Upload n plots & inputSlider based up a file upload. Two reactive vertical lines on top of the plot that move based on the respective inputSlider range.
What actually happens: The correct number of plots & sliderInputs output, however the inputSliders aren't reactive to the created plots AND the vertical line doesn't appear.
I don't receive any error messages, however I am almost certain that the issues lies in that my inputSlider information returns NULL.
I've tried to change the possible inputs for the ggplot code to hopefully show the respective plots by doing:
...+geom_vline(xintercept = input$plotSlider[1])+ geom_vline(xintercept = input$plotSlider[2])
...+geom_vline(xintercept = output$plotSlider[1])+geom_vline(xintercept = output$plotSlider[2])
..+geom_vline(xintercept = plotSlider[1]) +geom_vline(xintercept = plotSlider[2])
I also have tried rendering the sliders before the plots, since the input variable wouldn't have been created yet.
This here is a sample csv file:
structure(list(X10.9 = c(11.1, 11.6, 12, 12.5, 13, 13.4), X = c(NA,
NA, NA, NA, NA, NA), X.0.095 = c(-0.0911, -0.07, -0.0891, -0.1021,
-0.1019, -0.1019), X.1 = c(NA, NA, NA, NA, NA, NA), X1.4241 = c(1.4396,
1.4439, 1.4454, 1.4498, 1.4513, 1.4513), X.2 = c(NA, NA, NA,
NA, NA, NA), X1.4353 = c(1.4498, 1.4648, 1.474, 1.4819, 1.485,
1.4866), X.3 = c(NA, NA, NA, NA, NA, NA), X0.6736 = c(0.6943,
0.7066, 0.7141, 0.7179, 0.7193, 0.7182)), row.names = c(NA, 6L
), class = "data.frame")
My Code so far:
library(shiny)
library(dplyr, warn.conflicts = FALSE)
library(ggplot2)
#library(MeltR)
library(shiny)
library(glue)
# Define UI ----
ui <- navbarPage(title = "MeltShiny",
id = "navbar",
navbarMenu("File",
tabPanel("Add Data",
fluidPage(
sidebarLayout(
sidebarPanel(
textInput(label="Enter the Pathlength for each Absorbance Reading(separated by commas)",
placeholder = "E.g: 2,5,3,2,...",
inputId = "pathlengths"),
fileInput(label = "Add Data",
inputId = "inputFile",
multiple = FALSE,
accept = ".csv")
),
mainPanel(
tableOutput("contents")
)
)
)
)
),tabPanel(
value = "vizPanel",
title = "Data Visualization",
uiOutput("sliders"),
uiOutput("plots")
)
)
server <- function(input,output){
#Reactive list variable
values <- reactiveValues(masterFrame=NULL,up=NULL,loaded=NULL)
plots <- reactiveValues()
#Upload Project File
upload <- observeEvent(eventExpr =input$inputFile,
handlerExpr = {
req(input$inputFile)
#Declaring variables
pathlengths <- c(unlist(strsplit(input$pathlengths,",")))
req(input$inputFile)
fileName = input$inputFile$datapath
cd <- read.csv(file = fileName,header=FALSE)
df <- cd %>% select_if(~ !any(is.na(.)))
#Creating temporary frame to store sample data
columns <- c("Sample", "Pathlength", "Temperature", "Absorbance")
tempFrame <- data.frame(matrix(nrow = 0, ncol = 4))
colnames(tempFrame) <- columns
readings <- ncol(df)
#Loop that appends sample data
counter <- 1
for (x in 2:readings){
col <- df[x]
sample<-rep(c(counter),times=nrow(df[x]))
pathlength<-rep(c(pathlengths[counter]),times=nrow(df[x]))
col <- df[x]
t <- data.frame(sample,pathlength,df[1],df[x])
names(t) <- names(tempFrame)
tempFrame <- rbind(tempFrame, t)
counter <- counter + 1
}
values$numReadings <- counter-1
values$masterFrame <- tempFrame
values$up <- 1
}
)
output$contents <- renderTable({
return(values$masterFrame)})
observeEvent(eventExpr = input$navbar == "vizPanel",
handlerExpr = {
req(input$inputFile)
print("Observe Triggered")
for(i in 1:values$numReadings){
local({
myI <- i
plotName = paste0("plot",myI)
plotSlider = paste0("plotSlider",myI)
output[[plotName]] <- renderPlot({
data = values$masterFrame[values$masterFrame$Sample == myI,]
ggplot(data, aes(x = Temperature,
y = Absorbance,
color = factor(Sample))) +geom_point() +theme_classic()+geom_vline(xintercept = input$plotSlider[1]) +geom_vline(xintercept = input$plotSlider[2])
})
})
values$loaded <- 1
}
}
)
output$plots <- renderUI({
req(values$loaded)
plot_output_list <- lapply(1:values$numReadings, function(i){
plotName <- paste0("plot",i)
plotOutput(plotName,height=280,width=250)
})
do.call(tagList,plot_output_list)
})
output$sliders <- renderUI({
req(input$inputFile)
print("slider")
slider_output_list <- lapply(1:values$numReadings, function(i){
plotSlider <- paste0("plotSlider",i)
data = values$masterFrame[values$masterFrame$Sample == i,]
xmin = min(data$Temperature)
xmax = max(data$Temperature)
sliderInput(plotSlider,"Range of values",min=xmin,max=xmax,value=c(xmin,xmax))
})
do.call(tagList,slider_output_list)
})
}
# Run the app
shinyApp(ui = ui, server = server)
Any suggestions would be greatly appreciated!

Appropriate syntax for input$plotSlider will make it work. Try this
library(shiny)
library(dplyr, warn.conflicts = FALSE)
library(ggplot2)
#library(MeltR)
library(shiny)
library(glue)
# Define UI ----
ui <- navbarPage(title = "MeltShiny",
id = "navbar",
navbarMenu("File",
tabPanel("Add Data",
fluidPage(
sidebarLayout(
sidebarPanel(
textInput(label="Enter the Pathlength for each Absorbance Reading(separated by commas)",
placeholder = "E.g: 2,5,3,2,...",
inputId = "pathlengths"),
fileInput(label = "Add Data",
inputId = "inputFile",
multiple = FALSE,
accept = ".csv")
),
mainPanel(
tableOutput("contents")
)
)
)
)
),tabPanel(
value = "vizPanel",
title = "Data Visualization",
uiOutput("sliders"),
uiOutput("plots")
)
)
server <- function(input,output){
#Reactive list variable
values <- reactiveValues(masterFrame=NULL,up=NULL,loaded=NULL)
plots <- reactiveValues()
#Upload Project File
upload <- observeEvent(eventExpr =input$inputFile,
handlerExpr = {
req(input$inputFile)
#Declaring variables
pathlengths <- c(unlist(strsplit(input$pathlengths,",")))
req(input$inputFile)
fileName = input$inputFile$datapath
cd <- read.csv(file = fileName,header=TRUE)
df <- cd %>% select_if(~ !any(is.na(.)))
#Creating temporary frame to store sample data
columns <- c("Sample", "Pathlength", "Temperature", "Absorbance")
tempFrame <- data.frame(matrix(nrow = 0, ncol = 4))
colnames(tempFrame) <- columns
readings <- ncol(df)
#Loop that appends sample data
counter <- 1
for (x in 2:readings){
# local({
# x <- x
col <- df[x]
sample<-rep(c(counter),times=nrow(df[x]))
pathlength<-rep(c(pathlengths[counter]),times=nrow(df[x]))
col <- df[x]
t <- data.frame(sample,pathlength,df[1],df[x])
names(t) <- names(tempFrame)
tempFrame <- rbind(tempFrame, t)
counter <- counter + 1
#})
}
values$numReadings <- counter-1
values$masterFrame <- tempFrame
values$up <- 1
}
)
output$contents <- renderTable({
return(values$masterFrame)})
# observeEvent(eventExpr = input$navbar == "vizPanel",
# handlerExpr = {
observe({
req(input$inputFile)
#print(input[[paste0("plotSlider1")]])
print("Observe Triggered")
for(i in 1:values$numReadings){
local({
myI <- i
plotName = paste0("plot",myI)
plotSlider = paste0("plotSlider",myI)
output[[plotName]] <- renderPlot({
data = values$masterFrame[values$masterFrame$Sample == myI,]
ggplot(data, aes(x = Temperature,
y = Absorbance,
color = factor(Sample))) +
geom_point() + theme_classic()+
geom_vline(xintercept = input[[paste0("plotSlider",myI)]][1]) +
geom_vline(xintercept = input[[paste0("plotSlider",myI)]][2])
})
})
values$loaded <- 1
}
})
output$plots <- renderUI({
req(values$loaded)
plot_output_list <- lapply(1:values$numReadings, function(i){
plotName <- paste0("plot",i)
plotOutput(plotName,height=280,width=250)
})
do.call(tagList,plot_output_list)
})
output$sliders <- renderUI({
req(input$inputFile)
slider_output_list <- lapply(1:values$numReadings, function(i){
plotSlider <- paste0("plotSlider",i)
data = values$masterFrame[values$masterFrame$Sample == i,]
xmin = min(data$Temperature)
xmax = max(data$Temperature)
sliderInput(plotSlider,"Range of values",min=xmin,max=xmax,value=c(xmin,xmax))
})
do.call(tagList,slider_output_list)
})
}
# Run the app
shinyApp(ui = ui, server = server)

Related

Display the count of clicks in a plot using shiny

I want to build a shiny app that counts the number of clicks I make on any image, but I don't know how to make the counter increase, it just plots the number 1
I tried to create loops inside renderPlot but it doesn't work.
It is necessary to change the path of the files to a directory that contains .jpg images
library(shiny)
ui <- fluidPage(
titlePanel("Click Count"),
sidebarPanel(selectInput("IMAGE", "Sample image:",
list.files(path = "~",
pattern = ".jpg",
full.names = TRUE,
include.dirs = FALSE))),
fluidRow(
plotOutput("IMG", click = "countClick", "100%", "500px")
),
verbatimTextOutput("info")
)
server <- function(input, output, session){
# Creating a reactive variable that recognizes the selected image
img <- reactive({
f <- input$IMAGE
imager::load.image(f)
})
# Creating a spot where i can store reactive values
initX <- 1
initY <- 2
source_coords <- reactiveValues(xy = c(x=initX,y=initY))
# Coords
dest_coords <- reactiveValues(x=initX, y = initY)
observeEvent(plot_click(),{
dest_coords$x <- c(dest_coords$x, floor(plot_click()$x))
dest_coords$y <- c(dest_coords$y, floor(plot_click()$y))
})
plot_click <- debounce(reactive(input$countClick), 300)
output$IMG <- renderPlot({
plot(img(), axes = FALSE)
n <- 0
ex <- expression(n+1)
text(dest_coords$x, dest_coords$y,eval(ex),cex = 1 ,col = 'red')
})
output$info <- renderPrint({
req(input$countClick)
x <- round(input$countClick$x,2)
y <- round(input$countClick$y,2)
cat("[", x, ", ", y, "]", sep = "")
})
}
shinyApp(ui, server)
countClick is not a good name because input$countClick does not contain the numbers of clicks.
Not tested:
numberOfClicks <- reactiveVal(0)
dest_coords <- reactiveValues(x = initX, y = initY)
observeEvent(plot_click(),{
numberOfClicks(numberOfClicks() + 1)
dest_coords$x <- c(dest_coords$x, floor(plot_click()$x))
dest_coords$y <- c(dest_coords$y, floor(plot_click()$y))
})
plot_click <- debounce(reactive(input$countClick), 300)
output$IMG <- renderPlot({
plot(img(), axes = FALSE)
n <- numberOfClicks()
text(dest_coords$x, dest_coords$y, n, cex = 1 ,col = 'red')
})

plot visibility in shiny

I have this simple app - user should pick a starting hour and some values are generated from the starting hour and plotted.
library(shiny)
ui <- fluidPage(
titlePanel("Values by time generator"),
sidebarLayout(
sidebarPanel(
sliderInput("work_start",
"Starting hour:",
min = 7,
max = 17,
value = 9),
),
mainPanel(
plotOutput("plot")
)
)
)
server <- function(input, output) {
df_updated <- reactive({
starting_hour = 7
end_hour = 17
df_optim <- data.frame(time = as.double(), value = as.double())
df_optim[nrow(df_optim) + length(starting_hour:end_hour) ,] <- NA
df_optim[["time"]] <- starting_hour:end_hour
df_optim[["value"]] <- ifelse(df_optim[["time"]] < as.numeric(input$work_start), 0, rnorm(length(starting_hour:end_hour)))
output$plot <- renderPlot({
tmp <- df_updated()
plot(tmp[["time"]], tmp[["value"]], xlab = "time", ylab = "value")
})
})
}
shinyApp(ui = ui, server = server)
But when I start my app there is no plot generated. There is no error.
I checked names of values, but I have no idea how to fix this problem.
You plot should be outside the reactive object. Try this
server <- function(input, output) {
df_updated <- reactive({
starting_hour = 7
end_hour = 17
df_optim <- data.frame(time = as.double(), value = as.double())
df_optim[nrow(df_optim) + length(starting_hour:end_hour) ,] <- NA
df_optim[["time"]] <- starting_hour:end_hour
df_optim[["value"]] <- ifelse(df_optim[["time"]] < as.numeric(input$work_start), 0, rnorm(length(starting_hour:end_hour)))
df_optim
})
output$plot <- renderPlot({
tmp <- df_updated()
plot(tmp[["time"]], tmp[["value"]], xlab = "time", ylab = "value")
})
}

In R Shiny, how to read additional user inputs into a function and plot the results?

The below "MWE code 1" works as intended. It interpolates the values the user inputs into the matrix (id = input2) over the slider input periods (id = input1). Additional scenarios are generated with the click of the single action button which triggers a modal (for later purposes). For illustrative purposes, each scenario is linearly adjusted by a random variable.
I'm trying to adapt the above where additional user inputs into the matrix (always in column groupings of 2, for the 2 values to interpolate) are automatically added to the results function and plotted, without clicking the action button.
The below "MWE code 2" is my beginning of this implementation, and I end at my current knowledge. (Note the input matrix which expands in groups of 2 columns, and the elimination of the runif() inflator since presumably each added scenario will be different). How can I modify MWE code 2 to accomplish this? I am stuck.
MWE code 1:
library(shiny)
library(tidyverse)
library(ggplot2)
library(shinyMatrix)
interpol <- function(a,b){ # a = periods, b = matrix inputs
c <- rep(NA,a)
c[1] <- b[1]
c[a] <- b[2]
c <- approx(seq_along(c)[!is.na(c)],c[!is.na(c)],seq_along(c))$y # this interpolates
return(c)}
ui <- fluidPage(
sliderInput('input1','Periods to interpolate (input1):',min=2,max=10,value=10),
matrixInput("input2",
label = "Values to interpolate (input2):",
value = matrix(c(1,5),1,2,dimnames = list(NULL,c("Value 1","Value 2"))),
rows = list(names = FALSE),
class = "numeric"),
actionButton("add", "Add scenario"),
plotOutput("plot")
)
server <- function(input, output, session) {
results <- function(){interpol(req(input$input1),req(input$input2))}
numScenarios <- reactiveValues(numS=1)
observeEvent(input$add, {showModal(modalDialog(footer = modalButton("Close")))
numScenarios$numS <- (numScenarios$numS+1)})
output$plot <- renderPlot({
req(input$input1,input$input2)
v <- lapply(1:numScenarios$numS,
function(i) tibble(Scenario=i,X=1:input$input1,Y=runif(1)+results())
) %>% bind_rows()
v %>% ggplot() + geom_line(aes(x=X, y=Y, colour=as.factor(Scenario)))
})
}
shinyApp(ui, server)
MWE code 2:
library(shiny)
library(tidyverse)
library(ggplot2)
library(shinyMatrix)
interpol <- function(a,b){ # a = periods, b = matrix inputs
c <- rep(NA,a)
c[1] <- b[1]
c[a] <- b[2]
c <- approx(seq_along(c)[!is.na(c)],c[!is.na(c)],seq_along(c))$y # this interpolates
return(c)}
ui <- fluidPage(
sliderInput('input1','Periods to interpolate (input1):',min=2,max=10,value=10),
matrixInput("input2",
label = "Values to interpolate (input2) where first row lists scenario number:",
value = matrix(c(1,5),1,2,dimnames = list(NULL,c("Value 1","Value 2"))),
cols = list(extend = TRUE, delta = 2, delete = TRUE, names = TRUE,
editableNames = FALSE, multiheader=TRUE),
rows = list(names = FALSE),
class = "numeric"),
actionButton("add", "Add scenario"),
plotOutput("plot")
)
server <- function(input, output, session) {
results <- function(){interpol(req(input$input1),req(input$input2))}
numScenarios <- reactiveValues(numS=1)
observeEvent(input$add, {showModal(modalDialog(footer = modalButton("Close")))
numScenarios$numS <- (numScenarios$numS+1)})
output$plot <- renderPlot({
req(input$input1,input$input2)
v <- lapply(1:numScenarios$numS,
function(i) tibble(Scenario=i,X=1:input$input1,Y=results())
) %>% bind_rows()
v %>% ggplot() + geom_line(aes(x=X, y=Y, colour=as.factor(Scenario)))
})
observe({
req(input$input2)
mm <- input$input2
colnames(mm) <- trunc(1:ncol(mm)/2)+1
isolate(updateMatrixInput(session, "input2", mm))
})
}
shinyApp(ui, server)
See explanatory images below:
Edit: I'd suggest using a row-based matrixInput. This makes your life much easier, as you don't have to reshape the matrix before passing it to your custom function etc.
Please check the following:
library(shiny)
library(shinyMatrix)
library(dplyr)
library(ggplot2)
interpol <- function(a, b) {
# a = periods, b = matrix inputs
c <- rep(NA, a)
c[1] <- b[1]
c[a] <- b[2]
c <- approx(seq_along(c)[!is.na(c)], c[!is.na(c)], seq_along(c))$y # this interpolates
return(c)
}
ui <- fluidPage(
titlePanel("myMatrixInput"),
sidebarLayout(
sidebarPanel(
matrixInput(
"myMatrixInput",
label = "Values to interpolate (myMatrixInput) where first row lists scenario number:",
value = matrix(c(10, 1, 5), 1, 3, dimnames = list("Scenario 1", c("Periods", "Value 1", "Value 2"))),
cols = list(
extend = FALSE,
names = TRUE,
editableNames = FALSE
),
rows = list(names = TRUE,
delete = TRUE,
extend = TRUE,
delta = 1),
class = "numeric"
),
actionButton("add", "Add scenario")
),
mainPanel(
plotOutput("plot")
)
)
)
server <- function(input, output, session) {
sanitizedMat <- reactiveVal()
observeEvent(input$myMatrixInput, {
if(any(rownames(input$myMatrixInput) == "")){
tmpMatrix <- input$myMatrixInput
rownames(tmpMatrix) <- paste("Scenario", seq_len(nrow(input$myMatrixInput)))
updateMatrixInput(session, inputId = "myMatrixInput", value = tmpMatrix)
}
sanitizedMat(na.omit(input$myMatrixInput))
})
plotData <- reactive({
req(dim(sanitizedMat())[1] >= 1)
lapply(seq_len(nrow(sanitizedMat())),
function(i){
tibble(
Scenario = rownames(sanitizedMat())[i],
X = seq_len(sanitizedMat()[i, 1]),
Y = interpol(sanitizedMat()[i, 1], sanitizedMat()[i, 2:3])
)
}) %>% bind_rows()
})
output$plot <- renderPlot({
req(nrow(plotData()) > 0)
plotData() %>% ggplot() + geom_line(aes(
x = X,
y = Y,
colour = as.factor(Scenario)
))
})
}
shinyApp(ui, server)
Initial Answer
There is no need to calulate the numScenarios as they are defined by the dimensions of your matrix. The same applies to the modal you'll add later - just monitor the dimensions of the data to change the plot - no matter which input changes the reactive dataset.
As a general advice I'd recommend working with data.frames in long format instead of a matrix to prepare plots (using e.g. ggplot or plotly). See my answer here for an example.
Please check the following:
library(shiny)
library(shinyMatrix)
library(dplyr)
library(ggplot2)
interpol <- function(a, b) {
# a = periods, b = matrix inputs
c <- rep(NA, a)
c[1] <- b[1]
c[a] <- b[2]
c <- approx(seq_along(c)[!is.na(c)], c[!is.na(c)], seq_along(c))$y # this interpolates
return(c)
}
ui <- fluidPage(
sliderInput(
'mySliderInput',
'Periods to interpolate (mySliderInput):',
min = 2,
max = 10,
value = 10
),
matrixInput(
"myMatrixInput",
label = "Values to interpolate (myMatrixInput):",
value = matrix(c(1, 5), 1, 2, dimnames = list(NULL, c("Value 1", "Value 2"))),
cols = list(
extend = TRUE,
delta = 2,
delete = TRUE
),
rows = list(names = FALSE),
class = "numeric"
),
actionButton("add", "Add scenario"),
plotOutput("plot")
)
server <- function(input, output, session) {
observeEvent(input$add, {
showModal(modalDialog(footer = modalButton("Close")))
})
plotData <- reactive({
req(dim(input$myMatrixInput)[2] >= 2)
# req(dim(input$myMatrixInput)[2]%%2 == 0)
req(input$mySliderInput)
if(as.logical(dim(input$myMatrixInput)[2]%%2)){
myVector <- head(as.vector(input$myMatrixInput), -1)
} else {
myVector <- as.vector(input$myMatrixInput)
}
myMatrix <- matrix(myVector, ncol = 2)
lapply(seq_len(length(myVector)/2),
function(i){
tibble(
Scenario = i,
X = seq_len(input$mySliderInput),
Y = interpol(req(input$mySliderInput), req(myMatrix[i,]))
)
}) %>% bind_rows()
})
output$plot <- renderPlot({
req(nrow(plotData()) > 0)
plotData() %>% ggplot() + geom_line(aes(
x = X,
y = Y,
colour = as.factor(Scenario)
))
})
}
shinyApp(ui, server)
The above Edit works BEAUTIFULLY. Wow. Now the below simple edit of your edit simply pull the periods to interpolate out of the input matrix and back into a single slider input since in the full model this is meant for, modeled periods have to be the same for all input variables. However your 3 column matrix inputs also help me on another matter so THANK YOU. Also, I removed the "Add scenarios" action button since it is no longer needed with the automatically expanding input matrix. I sure learned a lot with this.
Edit of your edit:
ui <- fluidPage(
titlePanel("myMatrixInput"),
sidebarLayout(
sidebarPanel(
sliderInput('periods','Periods to interpolate:',min=2,max=10,value=10),
matrixInput(
"myMatrixInput",
label = "Values to interpolate (myMatrixInput):",
value = matrix(c(1, 5), 1, 2, dimnames = list("Scenario 1", c("Value 1", "Value 2"))),
cols = list(extend = FALSE,
names = TRUE,
editableNames = FALSE),
rows = list(names = TRUE,
delete = TRUE,
extend = TRUE,
delta = 1),
class = "numeric"
),
),
mainPanel(
plotOutput("plot")
)
)
)
server <- function(input, output, session) {
sanitizedMat <- reactiveVal()
observeEvent(input$myMatrixInput, {
if(any(rownames(input$myMatrixInput) == "")){
tmpMatrix <- input$myMatrixInput
rownames(tmpMatrix) <- paste("Scenario", seq_len(nrow(input$myMatrixInput)))
updateMatrixInput(session, inputId = "myMatrixInput", value = tmpMatrix)
}
sanitizedMat(na.omit(input$myMatrixInput))
})
plotData <- reactive({
req(dim(sanitizedMat())[1] >= 1)
lapply(seq_len(nrow(sanitizedMat())),
function(i){
tibble(
Scenario = rownames(sanitizedMat())[i],
X = 1:input$periods,
Y = interpol(input$periods, sanitizedMat()[i, 1:2])
)
}) %>% bind_rows()
})
output$plot <- renderPlot({
req(nrow(plotData()) > 0)
plotData() %>% ggplot() + geom_line(aes(
x = X,
y = Y,
colour = as.factor(Scenario)
))
})
}
shinyApp(ui, server)

Restyling traces using plotlyProxy in a scatterplot is unstable when points are colored according to category

I have a Shiny app that builds a scatterplot and highlights the clicked points by restyling the marker outline via plotlyProxy.
The app also subsets the data and moves the entries corresponding to the clicked points from the original "Data table" to an "Outlier table".
This seems to work fine when the markers are all the same color, or when they are colored by a continuous variable. But when I color the points by a categorical variable (like "Species"), it has a weird behavior, restyling a marker from each category instead of the clicked one. The data subsets correctly.
I think the restyle function should update all traces unless specified otherwise, so I am not sure where exactly lies the problem.
Here is my code:
library(plotly)
library(DT)
ui <- fluidPage(
mainPanel(
fluidRow(
div(
column(
width = 2,
uiOutput('chartOptions')),
column(width = 5,
h3("Scatter plot"),
plotlyOutput("scatterplot"),
verbatimTextOutput("click")
)
)
),
hr(),
div(
column(width = 6,
h2("Data Table"),
div(
DT::dataTableOutput(outputId = "table_keep"),
style = "height:auto; overflow-y: scroll;overflow-x: scroll;")),
column(width = 6,
h2("Outlier Data"),
div(
DT::dataTableOutput(outputId = "table_outliers"),
style = "height:auto; overflow-y: scroll;overflow-x: scroll;"))
)
))
server <- function(input, output, session){
datasetInput <- reactive({
df <- iris
return(df)
})
output$chartOptions <- renderUI({#choose variables to plot
if(is.null(datasetInput())){}
else {
list(
selectizeInput("xAxisSelector", "X Axis Variable",
colnames(datasetInput())),
selectizeInput("yAxisSelector", "Y Axis Variable",
colnames(datasetInput())),
selectizeInput("colorBySelector", "Color By:",
c(c("Do not color",colnames(datasetInput()))))
)
}
})
vals <- reactiveValues(#define reactive values for:
data = NULL,
data_keep = NULL,
data_exclude = NULL)
observe({
vals$data <- datasetInput()
vals$data_keep <- datasetInput()
})
## Datatable
output$table_keep <- renderDT({
vals$data_keep
},options = list(pageLength = 5))
output$table_outliers <- renderDT({
vals$data_exclude
},options = list(pageLength = 5))
# mechanism for managing selected points
keys <- reactiveVal()
observeEvent(event_data("plotly_click", source = "outliers", priority = "event"), {
req(vals$data)
is_outlier <- NULL
key_new <- event_data("plotly_click", source = "outliers")$key
key_old <- keys()
if (key_new %in% key_old){
keys(setdiff(key_old, key_new))
} else {
keys(c(key_new, key_old))
}
is_outlier <- rownames(vals$data) %in% keys()
vals$data_keep <- vals$data[!is_outlier, ]
vals$data_exclude <- vals$data[is_outlier, ]
plotlyProxy("scatterplot", session) %>%
plotlyProxyInvoke(
"restyle",
list(marker.line = list(
color = as.vector(ifelse(is_outlier,'black','grey')),
width = 2
))
)
})
observeEvent(event_data("plotly_doubleclick", source = "outliers"), {
req(vals$data)
keys(NULL)
vals$data_keep <- vals$data
vals$data_exclude <- NULL
plotlyProxy("scatterplot", session) %>%
plotlyProxyInvoke(
"restyle",
list(marker.line = list(
color = 'grey',
width = 2
)
))
})
output$scatterplot <- renderPlotly({
req(vals$data,input$xAxisSelector,input$yAxisSelector)
dat <- vals$data
key <- rownames(vals$data)
x <- input$xAxisSelector
y <- input$yAxisSelector
if(input$colorBySelector != "Do not color"){
color <- dat[, input$colorBySelector]
}else{
color <- "orange"
}
scatterplot <- dat %>%
plot_ly(x = dat[,x], y = dat[,y], source = "outliers") %>%
add_markers(key = key,color = color,
marker = list(size = 10, line = list(
color = 'grey',
width = 2
))) %>%
layout(showlegend = FALSE)
return(scatterplot)
})
output$click <- renderPrint({#click event data
d <- event_data("plotly_click", source = "outliers")
if (is.null(d)) "click events appear here (double-click to clear)" else d
})
}
shinyApp(ui, server)
The problem with your above code is that no traceIndices argument is provided for restyle. Please see this.
In your example, once you switch coloring to the factor Species plotly no longer creates one trace, but three. This happens in JS so counting is done from 0 to 2.
To restyle those traces you can address them via curveNumber (in this case 0:2) and pointNumber (50 data points in each trace 0:49)
With a single trace your example works as your key and your trace have the same length (150).
As your provided code is pretty long I just focused on the "Species" problem. It won't work in all other cases, but you should be able to deduce a more general approach from it:
library(shiny)
library(plotly)
library(DT)
ui <- fluidPage(
mainPanel(
fluidRow(
div(
column(
width = 2,
uiOutput('chartOptions')),
column(width = 5,
h3("Scatter plot"),
plotlyOutput("scatterplot"),
verbatimTextOutput("click")
)
)
),
hr(),
div(
column(width = 6,
h2("Data Table"),
div(
DT::dataTableOutput(outputId = "table_keep"),
style = "height:auto; overflow-y: scroll;overflow-x: scroll;")),
column(width = 6,
h2("Outlier Data"),
div(
DT::dataTableOutput(outputId = "table_outliers"),
style = "height:auto; overflow-y: scroll;overflow-x: scroll;"))
)
))
server <- function(input, output, session){
datasetInput <- reactive({
df <- iris
df$is_outlier <- FALSE
return(df)
})
output$chartOptions <- renderUI({#choose variables to plot
if(is.null(datasetInput())){}
else {
list(
selectizeInput("xAxisSelector", "X Axis Variable",
colnames(datasetInput())),
selectizeInput("yAxisSelector", "Y Axis Variable",
colnames(datasetInput())),
selectizeInput("colorBySelector", "Color By:",
c(c("Do not color",colnames(datasetInput()))))
)
}
})
vals <- reactiveValues(#define reactive values for:
data = NULL,
data_keep = NULL,
data_exclude = NULL)
observe({
vals$data <- datasetInput()
vals$data_keep <- datasetInput()
})
## Datatable
output$table_keep <- renderDT({
vals$data_keep
},options = list(pageLength = 5))
output$table_outliers <- renderDT({
vals$data_exclude
},options = list(pageLength = 5))
# mechanism for managing selected points
keys <- reactiveVal()
myPlotlyProxy <- plotlyProxy("scatterplot", session)
observeEvent(event_data("plotly_click", source = "outliers", priority = "event"), {
req(vals$data)
is_outlier <- NULL
plotlyEventData <- event_data("plotly_click", source = "outliers")
key_new <- plotlyEventData$key
key_old <- keys()
if (key_new %in% key_old){
keys(setdiff(key_old, key_new))
} else {
keys(c(key_new, key_old))
}
vals$data[keys(),]$is_outlier <- TRUE
is_outlier <- vals$data$is_outlier
vals$data_keep <- vals$data[!is_outlier, ]
vals$data_exclude <- vals$data[is_outlier, ]
print(paste("pointNumber:", plotlyEventData$pointNumber))
print(paste("curveNumber:", plotlyEventData$curveNumber))
plotlyProxyInvoke(
myPlotlyProxy,
"restyle",
list(marker.line = list(
color = as.vector(ifelse(vals$data[vals$data$Species %in% vals$data[plotlyEventData$key, ]$Species, ]$is_outlier,'black','grey')),
width = 2
)), plotlyEventData$curveNumber
)
})
observeEvent(event_data("plotly_doubleclick", source = "outliers"), {
req(vals$data)
keys(NULL)
vals$data_keep <- vals$data
vals$data_exclude <- NULL
plotlyProxyInvoke(
myPlotlyProxy,
"restyle",
list(marker.line = list(
color = 'grey',
width = 2
)
))
})
output$scatterplot <- renderPlotly({
req(datasetInput(),input$xAxisSelector,input$yAxisSelector)
dat <- datasetInput()
key <- rownames(dat)
x <- input$xAxisSelector
y <- input$yAxisSelector
if(input$colorBySelector != "Do not color"){
color <- dat[, input$colorBySelector]
}else{
color <- "orange"
}
scatterplot <- dat %>%
plot_ly(x = dat[,x], y = dat[,y], source = "outliers") %>%
add_markers(key = key,color = color,
marker = list(size = 10, line = list(
color = 'grey',
width = 2
))) %>%
layout(showlegend = FALSE)
return(scatterplot)
})
output$click <- renderPrint({#click event data
d <- event_data("plotly_click", source = "outliers")
if (is.null(d)) "click events appear here (double-click to clear)" else d
})
}
shinyApp(ui, server)
As a quick workaround, to avoid creating 3 traces, I simply converted the categorical variable assigned to color to numeric, and I hid the colorbar, so the output looks like this:
output$scatterplot <- renderPlotly({
req(vals$data,input$xAxisSelector,input$yAxisSelector)
dat <- vals$data
key <- rownames(vals$data)
x <- input$xAxisSelector
y <- input$yAxisSelector
if(input$colorBySelector != "Do not color"){
color <- as.numeric(dat[, input$colorBySelector])
}else{
color <- "orange"
}
scatterplot <- dat %>%
plot_ly(x = dat[,x], y = dat[,y], source = "outliers") %>%
add_markers(key = key,color = color,
marker = list(size = 10, line = list(
color = 'grey',
width = 2
))) %>%
layout(showlegend = FALSE) %>%
hide_colorbar()%>%
event_register("plotly_click")
return(scatterplot)
})
Update:
Another solution that I found is to make a loop of plotly proxies for each trace / category in the click event.
So the click event looks like this:
observeEvent(event_data("plotly_click", source = "outliers", priority = "event"), {
req(vals$data)
is_outlier <- NULL
key_new <- event_data("plotly_click", source = "outliers")$key
key_old <- keys()
#keys(c(key_new, key_old))
if (key_new %in% key_old){
keys(setdiff(key_old, key_new))
} else {
keys(c(key_new, key_old))
}
is_outlier <- rownames(vals$data) %in% keys()
vals$data_keep <- vals$data[!is_outlier, ]
vals$data_exclude <- vals$data[is_outlier, ]
indices <- list()
p <- plotlyProxy("scatterplot", session)
if(input$colorBySelector != "Do not color"){
if(is.factor(vals$data[,input$colorBySelector])){
for (i in 1:length(levels(vals$data[,input$colorBySelector]))){
indices[[i]] <- rownames(vals$data[which(vals$data[,input$colorBySelector] == levels(vals$data[,input$colorBySelector])[i]), ]) #retrieve indices for each category
plotlyProxyInvoke(p,
"restyle",
list(marker.line = list(
color = as.vector(ifelse(is_outlier[as.numeric(indices[[i]])],'black','grey')),
width = 2
)), c(i-1) #specify the trace (traces are indexed from 0)
)
}
}else{
p %>%
plotlyProxyInvoke(
"restyle",
list(marker.line = list(
color = as.vector(ifelse(is_outlier,'black','grey')),
width = 2
))
)
}
}else{
p %>%
plotlyProxyInvoke(
"restyle",
list(marker.line = list(
color = as.vector(ifelse(is_outlier,'black','grey')),
width = 2
))
)
}
})

Dynamically change plots based on user input in Shiny

I'm trying to create a shiny app that generates plots based on the user selection of a subset of a loaded dataframe. For example, I have the following dataset:
library(shiny)
library(data.table)
df <- rbind(
data.table( cat = rep('X', 40), grp = rep(LETTERS[1:4], each=10), x = rep(1:10, times=4), y = rnorm(40) ),
data.table( cat = rep('Y', 30), grp = rep(LETTERS[1:3], each=10), x = rep(1:10, times=3), y = rnorm(30) ),
data.table( cat = rep('Z', 20), grp = rep(LETTERS[4:6], each=10), x = rep(1:10, times=2), y = rnorm(20) )
)
Based on the value for cat that the user selects in the UI, I want shiny to produce charts for each value of grp. So, if the user selects 'X', then there will be 4 plots produced; if they select 'Y' there will be three, and if they select 'Z' there will be 3.
I also want to specify how each chart is generated based on the value of grp. So if grp is A,D or E I want it produce a line plot, otherwise it should produce a scatterplot (only if that grp has that value of course).
Below is the code for my (broken) shiny app:
server <- function(input, output) {
rv <- reactiveValues(
i = NULL,
df = NULL
)
observe({ rv$i <- input$i })
observe({ rv$df <- df[cat == rv$i] })
output$test <- renderUI({
plotList <- lapply( LETTERS[1:6], function(x) plotOutput(x) )
do.call( tagList, unlist(plotList, recursive=FALSE))
})
for(i in LETTERS[1:6]){
local({
my_i <- i
output[[my_i]] <- renderPlot({
if( my_i %in% c('A','D','E')) {
with(rv$df[grp == my_i], plot(x,y, type='l'))
} else {
with(rv$df[grp == my_i], plot(x,y))
}
})
})
}
}
ui <- fluidPage(
titlePanel('Title'),
sidebarLayout(
sidebarPanel(
helpText('Select the Category you would like to view.'),
selectInput('i', 'Category', c('X','Y','Z'), selectize=TRUE)
),
mainPanel(
uiOutput('test')
)
)
)
shinyApp(ui, server)
A reproducible example can be found at the bottom.
A few hints:
1) Using reactive contexts:
In your for Loop at the bottom of the Server Code you are using the reactive variable rv, so you will have to run the Code in a reactive Content. So wrap it in observe().
2) Create a list of Outputs:
If I am not mistaken you used some of the Code in this answer: dynamically add plots to web page using shiny.
It is a good starting Point. For the part of the taglist it might be easier to simplify to:
output$test <- renderUI({
lapply(unique(rv$df$grp), plotOutput)
})
You can also add tagList(), but it is not necessary here,...
3) Correcting the sample data:
You might want to update the df variable:
data.table(cat = rep('Z', 20), grp = rep(LETTERS[4:6], each=10),
x = rep(1:10, times=2), y = rnorm(20) )
Here your have three letters, so you might change it to LETTERS[5:6] or update the other numbers.
Full reproducible example:
library(shiny)
library(data.table)
df <- rbind(
data.table( cat = rep('X', 40), grp = rep(LETTERS[1:4], each=10), x = rep(1:10, times=4), y = rnorm(40) ),
data.table( cat = rep('Y', 30), grp = rep(LETTERS[1:3], each=10), x = rep(1:10, times=3), y = rnorm(30) ),
data.table( cat = rep('Z', 30), grp = rep(LETTERS[4:6], each=10), x = rep(1:10, times=3), y = rnorm(30) )
)
server <- function(input, output) {
rv <- reactiveValues(
i = NULL,
df = NULL
)
observe({ rv$i <- input$i })
observe({ rv$df <- df[cat == rv$i] })
observe({
for(letter in unique(rv$df$grp)){
local({
let <- letter
output[[let]] <- renderPlot({
if( let %in% c('A','D','E')) {
with(rv$df[grp == let], plot(x, y, type='l'))
} else {
with(rv$df[grp == let], plot(x,y))
}
})
})
}
})
output$test <- renderUI({
lapply(unique(rv$df$grp), plotOutput)
})
}
ui <- fluidPage(
titlePanel('Title'),
sidebarLayout(
sidebarPanel(
helpText('Select the Category you would like to view.'),
selectInput('i', 'Category', c('X','Y','Z'), selectize=TRUE)
),
mainPanel(
uiOutput('test')
)
)
)
shinyApp(ui, server)

Resources