mapdeck icon indicating copy to clipboard operation
mapdeck copied to clipboard

Layers fail with no data

Open mitchelloharawild opened this issue 4 years ago • 12 comments

Describe the bug An error occurs when adding a layer without data. Having no data to display can be common in interactive environments.

To Reproduce

library(sf)
#> Linking to GEOS 3.6.2, GDAL 2.2.3, PROJ 5.1.0
library(mapdeck)
sf <- sf::st_as_sf( capitals, coords = c("lon", "lat") )
mapdeck() %>% 
    add_scatterplot(
      data = head(sf,0)
      , radius = 1000000
      , fill_colour = "country"
      , layer_id = "scatter_layer"
      , tooltip = "capital"
    )
#> Error in rcpp_point_geojson(data, l, geometry_column, digits, "scatterplot"): 'getCharCE' must be called on a CHARSXP

Created on 2019-12-09 by the reprex package (v0.2.1)

Expected behaviour The layer to be added to the plot, without any data to display.

Versions Current dev version


TODO

  • [x] arc
  • [x] column
  • [x] geojson
  • [x] greatcircle
  • [x] grid
  • [x] heatmap
  • [x] hexagon
  • [x] line
  • [x] mesh
  • [x] path
  • [x] pointcloud
  • [x] polygon
  • [x] scatterplot
  • [x] screengrid
  • [x] text
  • [x] trips

mitchelloharawild avatar Dec 09 '19 02:12 mitchelloharawild

yeah at the moment I always check for nrow() > 0, but it's not an ideal solution.

I've made this issue in spatialwidget as that's where the fix will need to be.

I'll leave this one open though until it's solved.

SymbolixAU avatar Dec 09 '19 22:12 SymbolixAU

Great. That's what I'm using at the moment too, along with clearing the layer when nrow() == 0. The minor drawback to clearing the layer to remove points is that it also removes the legend (as it should for clearing a layer, not so much for no data).

mitchelloharawild avatar Dec 10 '19 00:12 mitchelloharawild

I've been exploring solutions to this in both the C++ (spatialwidget) side and the javascript (deck.gl) side.

Deck.gl will error if it has invalid coordinates (i.e., empty data), so it may not be possible to simply pass it a {} object. The solution may actually be to check the data in R and clear the layer if the data.frame / sf object is empty (as we're currently doing).

SymbolixAU avatar Dec 13 '19 04:12 SymbolixAU

I think the soultion is to have a check at the top of each function

if( nrow( data ) == 0 ) {
  return( clear_path( map, layer_id ) )
}

(apart from geojson, where it has to be placed after any conversions to a data.frame)

dcooley avatar Jan 27 '20 17:01 dcooley

I don't think this will preserve legends. It's been a little while since I've looked at the deck.gl side, but if the legend is coupled with the layers, then it may be worth raising an issue with the,.

As for passing {}, does it work if you pass a dataset with just the structure (but no values)?

mitchelloharawild avatar Jan 27 '20 17:01 mitchelloharawild

What should the legend show, if there isn't any data on the map?

If you really do want a legend without any data, you'll have to create the legend manually. I don't see another way around this as the legend is intimately tied to the data on the map. And to me it doesn't make sense to have a legend, but no data being shown.


As for passing {}, does it work if you pass a dataset with just the structure (but no values)?

No, I don't think so.

mapdeck() %>%
	add_path(
		data = roads[0,]
	)

# Error in rcpp_path_geojson(data, l, geometry_column, digits, "path") : 
# 	Error creating data layer 

mapdeck() %>%
	add_scatterplot(
		data = data.frame(lon = numeric(), lat = numeric())
		, lon = "lon", lat = "lat"
	)

# Error in rcpp_point_geojson_df(data, l, geometry_column, digits, "scatterplot") : 
# 	Error creating data layer
# In addition: Warning messages:
# 	1: In min(lon) : no non-missing arguments to min; returning Inf
# 2: In max(lon) : no non-missing arguments to max; returning -Inf
# 3: In min(lat) : no non-missing arguments to min; returning Inf
# 4: In max(lat) : no non-missing arguments to max; returning -Inf

dcooley avatar Jan 27 '20 17:01 dcooley

If you're animating data on the plot, and use a manually created legend to keep the values constant, I would expect the legend to remain static regardless of plotted values. AFAIK, clearing the layer will remove the legend associated with it.

mitchelloharawild avatar Jan 27 '20 18:01 mitchelloharawild

how about if the various clear_() functions gain the arguments

  • update_view = TRUE
  • clear_legend = TRUE

so then if they're false the associated calls in the layer_clear function get by-passed.

dcooley avatar Jan 27 '20 18:01 dcooley

That would work. :+1:

mitchelloharawild avatar Jan 27 '20 18:01 mitchelloharawild

yeah might be the best solution. The remaining questions is how should those arguments propogate into the clear_() functions if nrow( data ) == 0, because you don't want to use the update_view argument from the add_() function...

add_arc <- function(
  map,
  data = get_map_data(map),
  ...
  update_view = TRUE,
  focus_layer = FALSE
) {

    if( nrow( data ) == 0 ) {
      return( clear_arc( map, layer_id, ... ) )
    }
}

dcooley avatar Jan 27 '20 18:01 dcooley

ok, the clear_() functions get the signature

clear_path <- function( map, layer_id = NULL, clear_legend = TRUE, clear_view = TRUE) {}

and the add_() functions get ... so you can pass these arguments in

add_path <- function(map, data, etc, ... ) {
  if( nrow( data ) == 0 ) {
    return( clear_path( map, layer_id, ... ) )
  }
}

Which for the most part works

library(shiny)
library(shinydashboard)

ui <- dashboardPage(
	dashboardHeader()
	, dashboardSidebar(
		actionButton(
			inputId = "clear"
			, label = "clear"
		)
	)
	, dashboardBody(
		mapdeck::mapdeckOutput(
			outputId = "map"
		)
	)
)

server <- function(input, output) {
	
	set_token( read.dcf("~/Documents/.googleAPI", fields = "MAPBOX") )
	
	l1 <- legend_element(
		variables = c("Begins with A", "Doesn't begin with A")
		, colours = c("#00FF00FF", "#FF0000FF")
		, colour_type = "fill"
		, variable_type = "category"
	)
	js <- mapdeck_legend(l1)
	
	output$map <- mapdeck::renderMapdeck({
		mapdeck() %>%
			add_path(
				data = roads
				, stroke_colour = "ROAD_NAME"
				, legend = T
			)
	})
	
	observeEvent(input$clear, {
		mapdeck::mapdeck_update(map_id = "map") %>%
			add_path(
				data = roads[0, ]
				, clear_legend = FALSE
				, clear_view = FALSE
			)
	})
}

shinyApp(ui, server)

(although there is a minor bug in deck.gl where you have to have interacted with the map for it to know the current 'view state', to then know not to change the view. I'm watching this as part of #239 )

dcooley avatar Jan 27 '20 20:01 dcooley

@mitchelloharawild could you try devtools::install_github("SymbolixAU/mapdeck", ref = "issue252") for me and let me know if it works for you?

dcooley avatar Jan 27 '20 20:01 dcooley

updated example:

library(shiny)
library(shinydashboard)

ui <- dashboardPage(
	dashboardHeader()
	, dashboardSidebar(
		actionButton(
			inputId = "clear"
			, label = "clear"
		)
	)
	, dashboardBody(
		mapdeck::mapdeckOutput(
			outputId = "map"
		)
	)
)

server <- function(input, output) {
	
	set_token(secret::get_secret("MAPBOX"))

	output$map <- mapdeck::renderMapdeck({
		mapdeck() %>%
			add_path(
				data = roads
				, stroke_colour = "ROAD_NAME"
				, legend = T
			)
	})
	
	observeEvent(input$clear, {
		mapdeck::mapdeck_update(map_id = "map") %>%
			add_path(
				data = roads[0, ]
				, clear_legend = FALSE
				, update_view = FALSE
			)
	})
}

shinyApp(ui, server)

dcooley avatar Jan 03 '24 21:01 dcooley

now in master branch

dcooley avatar Jan 03 '24 22:01 dcooley