I agree, this isn't the best approach. The original thinking was to provide more user-level control over when to hide and show an element, but it is interfering with the rendering of the leaflet function. I'm thinking it is better to attach these functions via htmlwidgets using onRender
function rather than the previous approach. This might be a better method as you do not have to explicitly hide and show the loading element.
Using the onRender
function, it is possible to use the leaflet events whenReady
and .on("layeradd",...)
. I created a new event listener that runs when input$plotbutton
is clicked. Inside, I wrote a Promise that shows the loading screen at the start, and then hides it when the layeradd
event is finished. Add the onRender
function in the renderLeafet
. You do not need to call it anywhere else.
# within renderLeaflet
leaflet() %>%
addTiles() %>%
setView(-93.65, 42.0285, zoom = 17) %>%
addPopups(
lng = -93.65,
lat = 42.0285,
popup = "Here is the <b>Department of Statistics</b>, ISU"
) %>%
onRender(., "function(el, x, data){
// do something here
}")
The javascript code looks like this.
// defined in r onRender(., "...")
// requires: el, x, data
function(el, x, data) {
// select map and busy ui
var m = this;
const elem = document.getElementById('leafletBusy');
// when map is rendered, display loading
// adjust delay as needed (time is in milliseconds)
m.whenReady(function () {
elem.style.display = 'flex';
setTimeout(function () {
elem.style.display = 'none';
}, 3000)
});
// set on click event
const b = document.getElementById('plotbutton');
plotbutton.addEventListener('click', function (event) {
// show loading element
elem.style.display = 'flex';
(new Promise(function (resolve, reject) {
// leaflet event: layeradd
m.addEventListener('layeradd', function (event) {
console.log(event.type)
// resolve after a few seconds to ensure all
// elements rendered (adjust as needed)
// time is in milliseconds
setTimeout(function () {
resolve('done');
}, 500)
})
})).then(function (response) {
// resolve: hide loading screen
console.log('done');
elem.style.display = 'none';
}).catch(function (error) {
// throw errors
console.error(error)
})
});
}
It might be easier to wrap the onRender js using an R function so you can pass other element ids, adjust delays, display properties, etc. In the cloud project, I created a new file leaflet-on-render-app.R
. I'm also copying the full code here as I'm not seeing any of the saved changes from earlier edits (there's something odd with the project).
# pkgs
library(shiny)
library(leaflet)
library(htmlwidgets)
# ui
ui <- tagList(
tags$head(
tags$style(
".map-container {
height: 100%;
width: 100%;
position: relative;
}",
".map-loading {
position: absolute;
display: none;
justify-content: center;
align-items: center;
top: 0;
left: 0;
width: 100%;
height: 400px;
background-color: #bdbdbd;
text-align: center;
z-index: 9999;
}"
)
),
tags$main(
tags$h2("Map Output"),
actionButton("plotbutton", label = "Add Markers"),
tags$div(
class = "map-container",
tags$div(
id = "leafletBusy",
class = "map-loading",
tags$p("Loading...")
),
leafletOutput("map")
)
)
)
# server
server <- function(input, output, session) {
output$map <- renderLeaflet({
leaflet() %>%
addTiles() %>%
setView(-93.65, 42.0285, zoom = 17) %>%
addPopups(
lng = -93.65,
lat = 42.0285,
popup = "Here is the <b>Department of Statistics</b>, ISU"
) %>%
onRender(., "
function(el, x, data) {
// select map and busy ui
var m = this;
const elem = document.getElementById('leafletBusy');
// when map is rendered, display loading
// adjust delay as needed
m.whenReady(function() {
elem.style.display = 'flex';
setTimeout(function() {
elem.style.display = 'none';
}, 3000)
});
const b = document.getElementById('plotbutton');
plotbutton.addEventListener('click', function(event) {
// show loading element
elem.style.display = 'flex';
(new Promise(function(resolve, reject) {
// leaflet event: layeradd
m.addEventListener('layeradd', function(event) {
console.log(event.type)
// resolve after a some time to ensure all
// elements rendered (adjust as needed)
// time is in milliseconds
setTimeout(function() {
resolve('done');
}, 500)
})
})).then(function(response) {
// resolve: hide loading screen
console.log('done');
elem.style.display = 'none';
}).catch(function(error) {
// throw errors
console.error(error)
})
});
}")
})
# add points on render
observeEvent(input$plotbutton, {
dlat <- 1 / 111000 * 100 # degrees per metre
n <- 5000
leafletProxy("map") %>%
addMarkers(
lng = -93.65 + (runif(n) * 2 - 1) * dlat * 3,
lat = 42.0285 + (runif(n) * 2 - 1) * dlat
)
})
}
# app
shinyApp(ui = ui, server = server)