Select or highlight data on map by click on legend - r

Is there any way to select or highlight data on a leaflet map in by clicking on the legend in Rshiny?
example code:
library(shiny)
library(leaflet)
library(RColorBrewer)
library(leafletGeocoderRshiny)
ui <- fluidPage(
leafletOutput("map"),
p(),
actionButton("recalc", "New points")
)
server <- function(input, output, session) {
df = data.frame(x = rnorm(100), y = rexp(100, 2), z = runif(100))
pal = colorBin('PuOr', df$z, bins = c(0, .1, .4, .9, 1))
output$map <- renderLeaflet({ leaflet(df) %>%
addCircleMarkers(~x, ~y, color = ~pal(z)) %>%
addLegend(pal = pal, values = ~z)
})
}
shinyApp(ui, server)

The recent updates (0.2) to the mapedit package may help: http://r-spatial.org/r/2017/06/09/mapedit_0-2-0.html

I got close, but ran out of time now. But i decided to share anyway, maybe someone else sees a solution to the last step.
So far it works for the first click on any of the rectangles in the legend. It doesnt work for any following clicks as the map is redrawn and with it the onclick listeners are deleted. I didnt find a way to add them again so far,...
Its a hacky aprroach: I add onclick listener to the boxes and decided to update the colors via R as i didnt see a good way in JS.
library(shiny)
library(leaflet)
library(RColorBrewer)
library(leafletGeocoderRshiny)
library(shinyjs)
colors <- c("#000000", "#222222", "#888888", "#FFFFFF")
ui <- fluidPage(
useShinyjs(),
leafletOutput("map"),
p(),
actionButton("recalc", "New points")
)
server <- function(input, output, session) {
global <- reactiveValues(colors = colors,
bins = c(0, .1, .4, .9, 1))
observe({
print(input$interval)
isolate({
if(!is.null(input$interval)){
lowerBound <- as.numeric(unlist(input$interval))
global$colors <- colors
global$colors[which(global$bins == lowerBound)] <- "#FF0000"
}
})
})
session$onFlushed(function() {
runjs("
var legendButton = document.getElementsByTagName('i')
var elem; var interval;
for (nr = 0; nr < legendButton.length; nr++) {
elem = legendButton[nr]
elem.onclick = function(e){
console.log(e.target)
interval = e.target.nextSibling.nodeValue.split(' ');
Shiny.onInputChange('interval', interval[1]);
}
}
")
})
df = data.frame(x = rnorm(100), y = rexp(100, 2), z = runif(100))
pal = reactive({
colorBin(global$colors, df$z, bins = global$bins)
})
output$map <- renderLeaflet({ leaflet(df) %>%
addCircleMarkers(~x, ~y, color = ~pal()(z)) %>%
addLegend(pal = pal(), values = ~z)
})
}
runApp(shinyApp(ui, server), launch.browser = T)

Related

Update plotly data (chloropleth) in R shiny without re-rendering entire map

I am trying to use shiny controls to modify the data underlying a plotly chloropleth map.
Whenever I change the data the entire plot re-renders, which is quite slow. I'm guessing the bottleneck is redrawing the geojson polygons. Because the geojson never changes, I'm wondering if there is a way to keep the rendered widget intact but modify the z values only.
It looks like using plotlyProxy and plotlyProxyInvoke might be the right direction, but I can only see examples of an entire trace (which includes the geojson data) being replaced.
Sorry if I'm missing something or have been unclear - I have not used plotly very much, and even less so the js side of things.
See below for example code:
library(shiny)
library(dplyr)
library(plotly)
library(readr)
library(rjson)
zip_geojson <- fromJSON(file="https://raw.githubusercontent.com/hms1/testData/main/zip3_2.json")
plot_data <- read_csv(file="https://raw.githubusercontent.com/hms1/testData/main/plot_data.csv")
mapboxToken <- "pk.eyJ1IjoiaG1vcmdhbnN0ZXdhcnQiLCJhIjoiY2tmaTg5NDljMDBwbDMwcDd2OHV6cnd5dCJ9.8eLR4FtlO079Gq0NeSNoeg" #burner token
ui <- fluidPage(
sidebarLayout(
sidebarPanel(
sliderInput("multip",
"n:",
min = 1,
max = 10,
value = 1)
),
mainPanel(
plotlyOutput("cPlot")
)
)
)
server <- function(input, output) {
output$cPlot <- renderPlotly({
plot_data_i <- plot_data%>%
mutate(log_count = case_when(log_count <= input$multip ~ log_count * input$multip,
TRUE ~ log_count))
plot_ly() %>%
add_trace(
type = "choroplethmapbox",
geojson = zip_geojson,
locations = plot_data_i$zip,
z = plot_data_i$log_count
) %>%
layout(
mapbox = list(
style = "light",
zoom = 3,
center = list(lon = -95.7129, lat = 37.0902)
)
) %>%
config(mapboxAccessToken = mapboxToken)
})
}
shinyApp(ui = ui, server = server)
For anyone else who comes across this post later, I found a solution.
It turns out that you can change data using the restyle method in plotlyProxyInvoke, as shown below.
library(shiny)
library(dplyr)
library(plotly)
library(readr)
library(rjson)
zip_geojson <- fromJSON(file="https://raw.githubusercontent.com/hms1/testData/main/zip3_2.json")
plot_data <- read_csv(file="https://raw.githubusercontent.com/hms1/testData/main/plot_data.csv")
mapboxToken <- "pk.eyJ1IjoiaG1vcmdhbnN0ZXdhcnQiLCJhIjoiY2tmaTg5NDljMDBwbDMwcDd2OHV6cnd5dCJ9.8eLR4FtlO079Gq0NeSNoeg"
ui <- fluidPage(
sidebarLayout(
sidebarPanel(
sliderInput("multip",
"n:",
min = 1,
max = 10,
value = 1),
actionButton("Remove", "Remove Trace")
),
mainPanel(
plotlyOutput("cPlot")
)
)
)
server <- function(input, output, session) {
output$cPlot <- renderPlotly({
plot_ly(type = "choroplethmapbox", geojson = zip_geojson) %>%
layout(
mapbox = list(
style = "light",
zoom = 3,
center = list(lon = -95.7129, lat = 37.0902)
)
) %>%
config(mapboxAccessToken = mapboxToken)
})
plotproxy <- plotlyProxy("cPlot", session, deferUntilFlush = FALSE)
observeEvent(input$multip, {
plot_data_i <- plot_data %>%
mutate(log_count = case_when(log_count <= input$multip ~ log_count * input$multip,
TRUE ~ log_count))
plotproxy %>%
plotlyProxyInvoke("restyle", list(z = list(plot_data_i$log_count),
locations = list(plot_data_i$zip)))
})
}
shinyApp(ui = ui, server = server)

Reactive Shiny Application

I am trying to create an interactive shiny application that displays a leaflet plot based on a user's date and plot type specification. Ideally, I would like the user to specify whether they would like to view a state-wide or a county-wide plot. Then, based on their answers, I would like them to decide whether to use the regular data or the standardized data. After this, they would hit a submit button and the plot would render. I don't want the plot to render until the user presses the "Submit" action button. This is my idea so far, but it fails whenever I try to implement.
library(ggplot2)
library(shapefiles)
library(sp)
library(CARBayes)
library(leaflet)
library(rgdal)
library(leaflet)
library(shiny)
## County Data
dta <- read.csv()
## County Data (percentage)
perc <-read.csv()
## Date Specification Function
selectdates <- function(data, start, end){
keep <- data[, 1:5]
data <- data[, -c(1:5)]
tmp1 <- as.Date(names(data))
tmp2 <- which(tmp1 >= as.Date(start) & tmp1 <= as.Date(end))
tmp <- data[, tmp2]
Sum <- rowSums(tmp)
tmp <- cbind(keep, Sum)
return(tmp)
}
# Define UI for application that draws a histogram
ui <- fluidPage(
# Application title
titlePanel("Mapping"),
tags$em(""),
tags$hr(),
# Sidebar with a slider input for number of bins
sidebarLayout(
sidebarPanel(
dateRangeInput("daterange", "Date Range:",
start = as.character(Sys.Date() - 6),
end = as.character(Sys.Date())),
selectInput("ptChoice", "Type of Plot:", choices = c("", "County-Wise", "State-Wise")),
selectInput("typeChoice", "Data Type:", choices = c("", "Raw", "Percentage")),
actionButton("submitButton", "Submit", class = "btn btn-primary")
),
# Display leaflet plot of cases
mainPanel(
leafletOutput("countyPlot"),
leafletOutput("statePlot")
)
)
)
# Define server logic required to draw a histogram
server <- function(input, output) {
observeEvent(input$ptChoice, {
req(input$ptchoice)
if(input$ptChoice == "County-Wide"){
hide("statePlot")
show("countyPlot")
}
else{
hide("countyPlot")
show("statePlot")
}
})
fdta <- eventReactive(input$typeChoice, {
if (input$typeChoice == "Raw"){
df <- selectdates(data = tmp, start = input$daterange[1], end = input$daterange[2])
row.names(df) <- df$FIPS
}else if (input$typeChoice == "Percentage"){
df <- selectdates(data = perc, start = input$daterange[1], end = input$daterange[2])
}else {return(NULL)}
df
})
observeEvent(input$submitButton, {
output$statePlot <- renderLeaflet({
## INSERT STATE PLOT CODE HERE
})
output$countyPlot <- renderLeaflet({
## Loads SHP and DBF File
shp <- read.shp()
dbf <- read.dbf()
sp <- combine.data.shapefile(data = fdta, shp = shp, dbf = dbf)
proj4string(sp) <- CRS("+proj=longlat +datum=WGS84 +no_defs")
sp <- spTransform(sp, CRS("+proj=longlat +datum=WGS84 +no_defs"))
colours <- colorNumeric(palette = "YlOrRd", domain = sp#data$Sum)
leaflet(sp) %>%
addTiles() %>%
addPolygons(
fillColor = ~ colours(Sum),
weight = 1,
opacity = 0.7,
color = "white",
dashArray = '3',
fillOpacity = 0.7,
highlight = highlightOptions(
weight = 5,
color = "#666",
dashArray = "",
fillOpacity = 0.7,
bringToFront = TRUE
)
) %>%
addLegend(
pal = colours,
values = sp#data$Sum,
opacity = 1,
title = "Count"
) %>%
addScaleBar(position = "bottomleft")
})
})
}
# Run the application
shinyApp(ui = ui, server = server)
You can put the two plots inside an observeEvent, if you want it only after someone clicks on submit button. To use the appropriate dataframe, create a reactive dataframe and then use it as dfa() to generate the appropriate plot. Try this
server = function(input, output) {
observeEvent(input$ptChoice,{
req(input$ptChoice)
if(input$ptChoice == "County-Wide"){
hide("statePlot")
show("countyPlot")
}else{
hide("countyPlot")
show("statePlot")
}
})
dfa <- eventReactive(input$typechoice, {
if (input$typechoice == "Regular") {
df <- dta
}else if (input$typechoice == "Standardized") {
df <- dta2
}else {return(NULL)}
df
})
observeEvent(input$submitButton,{
output$stateplot <- renderLeaflet({
state <- CODE FOR STATE PLOT
})
output$countyPlot <- renderLeaflet({
county <- CODE FOR COUNTY PLOT
})
})
}
You might want to have your leaflet plot be stored in reactiveValues (rv) - then, you can have one output for your plot, and show what is stored in rv.
To change the plot when the submit button is pressed, be sure to reference the input$submitButton with your observeEvent.
Here is a working example that can be adapted. You could use an additional function to generate the plots based on your input values.
library(ggplot2)
library(leaflet)
library(shiny)
ui = fluidPage(
titlePanel("Leaflet Plot"),
tags$em(""),
tags$hr(),
sidebarLayout(
sidebarPanel(
selectInput("plotChoice", "Type of Plot:", choices = c("", "Boston", "Chicago")),
actionButton("submitButton", "Submit", class = "btn btn-primary")
),
# Display leaflet plot of cases
mainPanel(
leafletOutput("leafletPlot")
)
)
)
server = function(input, output) {
rv <- reactiveValues(plot = NULL)
output$leafletPlot <- renderLeaflet({
rv$plot
})
observeEvent(input$submitButton, {
if (input$plotChoice == "Boston") {
rv$plot <- leaflet() %>% setView(lng = -71.0589, lat = 42.3601, zoom = 12) %>% addTiles()
} else {
rv$plot <- leaflet() %>% setView(lng = -87.6298, lat = 41.8781, zoom = 12) %>% addTiles()
}
})
}
shinyApp(ui = ui, server = server)

Bind input$plotBrush and textInput together

I would like to bind input$plotBrush to textInput and vice versa so that when I draw my x brush on my plot it set the same boundaries ([brush]$xmin and [brush]$xmax) to the related text input and when I enter my values in my textInput, it draws me a brush on my plot whith the same boundaries entered.
I wasn't able to find any solution on accessing the [brush]$xmin and [brush]$xmax variable
(problem is : updateBrushInput does'nt exist)
Here is a reproductible example :
(the text inputs are set and returns [brush]$xmin and [brush]$xmax from the brushPlot, but the filter only works in one way)
library(shiny)
library(leaflet)
library(leaflet.extras)
library(tidyverse)
library(sf)
#Create T0New data
lat <- c(49.823, 49.823, 58.478, 57.478, 45.823)
lng <- c(-10.854,-10.854,-10.854,2.021,2.02)
date_start_min <- c(123,125,135,168,149)
T0New <- data.frame(lat,lng)
ui <- fluidPage(
leafletOutput("map", height = "50vh"),
textInput("input1","Date start (from 123 to 149)",value = ""),
textInput("input2","Date end (from 123 to 149)",value = ""),
plotOutput("distribPlot", height = "47vh",
brush = brushOpts(id = "distribPlot_brush", direction = "x", resetOnNew = FALSE))
)
server <- function(input, output, session) {
observeEvent(input$distribPlot_brush, {
brush <- input$distribPlot_brush
if (!is.null(brush)) {
updateTextInput(session, "input1", value=brush$xmin)
updateTextInput(session, "input2", value=brush$xmax)
}
})
#filter data from plot sel
filteredGraphData <- reactive({
currentlyFiltered <- T0New
if(!is.null(input$distribPlot_brush)){
thisSel <- input$distribPlot_brush
currentlyFiltered <- currentlyFiltered %>%
filter(date_start_min >= thisSel$xmin, date_start_min <= thisSel$xmax)
}
return(currentlyFiltered)
})
#Output map
output$map <- renderLeaflet({
leaflet()%>%
addProviderTiles(providers$OpenTopoMap)
})
observe({
mapData <- filteredGraphData()
mapProxy <- leafletProxy("map", session = session, data = mapData)
mapProxy %>%
clearGroup('A') %>%
addCircleMarkers(
data = mapData,
lat = mapData$lat,
lng = mapData$lng,
radius = 5,
color = 'red',
stroke = F,
fillOpacity = 1,
group = 'A'
)
})
#outputPlot
output$distribPlot <- renderPlot({
distribPlot <- ggplot(T0New,aes(date_start_min)) +
geom_density(col = "#053144", fill = "#43a2ca", alpha = 0.3, adjust = 0.75)
return(distribPlot)
})
}
# Create Shiny app ----
shinyApp(ui = ui, server = server)

Linking Leaflet's icons to plotly line plot in Shiny

I would like the icons on a leaflet map to be linked to the correspondent trace on a plotly line plot in a shiny app. Once I click on an icon, only the line with the same id should be displayed in plotly. Is this possible? I have been trying with crosstalk but I must be missing something.
library(shiny)
library(leaflet)
library(plotly)
library(crosstalk)
tmp1 <- data.frame(Date = seq(as.POSIXct("2016-06-18 10:00"),
length.out = 10, by = "mins"),
Temp = rnorm(n = 10, mean = 20, sd = 5),
lat=51.504162,
long=-0.130472,
id="first")
tmp2 <- data.frame(Date = seq(as.POSIXct("2016-06-18 10:00"),
length.out = 10, by = "mins"),
Temp = rnorm(n = 10, mean = 20, sd = 5),
lat=51.502858,
long= -0.116722,
id="second")
uktemp<-rbind(tmp1,tmp2)
#=========================================
ui <- fluidPage(
fluidRow(
column(6, leafletOutput("map")),
column(6, plotlyOutput("graph"))
)
)
server <- function(input, output, session) {
crossuktemp<- SharedData$new(uktemp)
output$map <- renderLeaflet({
leaflet(options = leafletOptions(minZoom = 15,maxZoom =18 ))%>%
addTiles()%>%
addCircles(data=crossuktemp,
lng= ~ long,
lat= ~ lat,
label=~id)
})
output$graph <- renderPlotly({
plot_ly(crossuktemp,x=~Date,y=~Temp, color =~id, mode="lines")%>%
layout(title = "",yaxis = list(title = "C°"),
xaxis = list(title = "Time")) %>%
highlight(off = "plotly_deselect")
})
}
shinyApp(ui, server)
I've hacked together a solution, making use of leaflets events it creates on the click.
ui <- fluidPage(
# add a reset button to undo click event
fluidRow(actionButton("reset", "Reset")),
fluidRow(
column(6, leafletOutput("map")),
column(6, plotlyOutput("graph"))
),
fluidRow()
)
server <- function(input, output, session) {
# create reactive data set based on map click
filteredData <- reactive({
event <- input$map_shape_click
if (!is.null(event)){
uktemp[uktemp$lat == event$lat & uktemp$long == event$lng,]
}
})
output$map <- renderLeaflet({
leaflet(options = leafletOptions(minZoom = 15,maxZoom =18 ))%>%
addTiles()%>%
addCircles(data=uktemp,
lng= ~ long,
lat= ~ lat,
label=~id)
})
# default graph
output$graph <- renderPlotly({
plot_ly(uktemp,x=~Date,y=~Temp, color =~id, mode="lines")%>%
layout(title = "",yaxis = list(title = "C°"),
xaxis = list(title = "Time")) %>%
highlight(off = "plotly_deselect")
})
# if clicked on map, use filtered data
observeEvent(input$map_click,
output$graph <- renderPlotly({
plot_ly(filteredData(),x=~Date,y=~Temp, color =~id, mode="lines")%>%
layout(title = "",yaxis = list(title = "C°"),
xaxis = list(title = "Time")) %>%
highlight(off = "plotly_deselect")
})
)
# if reset, then go back to main data
observeEvent(input$reset,
output$graph <- renderPlotly({
plot_ly(uktemp,x=~Date,y=~Temp, color =~id, mode="lines")%>%
layout(title = "",yaxis = list(title = "C°"),
xaxis = list(title = "Time")) %>%
highlight(off = "plotly_deselect")
})
)
}
To do so, have a read of these links
see the section: Inputs/Events
https://rstudio.github.io/leaflet/shiny.html
some SO questions
Click event on Leaflet tile map in Shiny
R shiny: reset plot to default state
To do undo the click event, I had to add a reset button in. Maybe there is a way of undoing a click in a more elegant way. I expect there are cleaner ways to build this if you read around it some more :)
Cheers,
Jonny

R Shiny: relayout plotly annotations

I want a plotly plot to change an annotation if the user clicks a button in a shiny app.
I have no idea why this does not work:
library(shiny)
library(plotly)
d <- data.frame(x = c(1,2,3), y = c(9,99,999))
ui <- fluidPage(
plotlyOutput("plot"),
actionButton("button", "toggle visibility"))
server <- function(input, output) {
output$plot <- renderPlotly({
plot_ly(d)%>%
add_lines(y=d$y, x= d$x)%>%
layout(annotations = list(x = 2, y= 99 , text = "hi"))})
observeEvent(input$button, {
plotlyProxy("plot", session= shiny::getDefaultReactiveDomain()) %>%
plotlyProxyInvoke("relayout", list(annotations= list(x = 2, y= 99 ,
text = "ho")))})}
shinyApp(ui, server)
That is not the way to use relayout in plotly. See below for your example using relayout.
I prefer using native shiny buttons for this purpose because of the greater flexibility it offers. Here is how one might go about achieving the hi-ho toggle.
shiny way
library(shiny)
library(plotly)
d <- data.frame(x = c(1,2,3), y = c(9,99,999))
ui <- fluidPage(
plotlyOutput("plot"),
actionButton("button", "toggle visibility"))
server <- function(input, output) {
output$plot <- renderPlotly({
p <- plot_ly(d)%>%
add_lines(y=d$y, x= d$x)
if (is.null(input$button) | (input$button%%2 == 0)) {
p <- p %>% layout(annotations = list(x = 2, y= 99 , text = "hi"))
} else {
p <- p %>% layout(annotations = list(x = 2, y= 99 , text = "ho"))
}
p
})
}
shinyApp(ui, server)
In this case though, it is simple enough to make the relayout feature work, although it does require an extra button.
plotly relayout way
library(shiny)
library(plotly)
d <- data.frame(x = c(1,2,3), y = c(9,99,999))
ui <- fluidPage(
plotlyOutput("plot")
)
server <- function(input, output) {
output$plot <- renderPlotly({
updatemenus <- list(
list(
active = -1,
type = 'buttons',
buttons = list(
list(
label = "hi",
method = "relayout",
args = list(list(annotations = list(list(x = 2, y= 99 , text = "hi"))))),
list(
label = "ho",
method = "relayout",
args = list(list(annotations = list(list(x = 2, y= 99 , text = "ho")))))
)
)
)
p <- plot_ly(d) %>%
add_lines(y=d$y, x= d$x) %>%
layout(updatemenus = updatemenus)
p
})
}
shinyApp(ui, server)
I believe all that needs to change in your code in order to get this to work is wrapping another list around the defined annotation list in your plotly proxy relayout code. I recently discovered that this recursive list structure is all that's needed in order to manipulate annotations using relayout - you can check out my answer to another SO question related to the same issue, but with slightly different context: https://stackoverflow.com/a/70610374/17852464
library(shiny)
library(plotly)
d <- data.frame(x = c(1,2,3), y = c(9,99,999))
ui <- fluidPage(
plotlyOutput("plot"),
actionButton("button", "toggle visibility"))
server <- function(input, output) {
output$plot <- renderPlotly({
plot_ly(d)%>%
add_lines(y=d$y, x= d$x)%>%
layout(annotations = list(x = 2, y= 99 , text = "hi"))
})
observeEvent(input$button, {
plotlyProxy("plot", session= shiny::getDefaultReactiveDomain()) %>%
plotlyProxyInvoke("relayout", list(annotations= list(list(x = 2, y= 99 ,
text = "ho"))))})}
}
shinyApp(ui, server)

Resources