r-tmap / tmap

R package for thematic maps
https://r-tmap.github.io/tmap
GNU General Public License v3.0
858 stars 120 forks source link

Multi-variable / multivariate specification functions #819

Open marine-ecologist opened 9 months ago

marine-ecologist commented 9 months ago

Example using the stars raster:

L7file = system.file("tif/L7_ETMs.tif", package = "stars")
L7 = read_stars(L7file)

### working but gives warning
tm_shape(L7) + 
   tm_raster(3, 2, 1)

# Warning message:
# In value[[3L]](cond) : could not rename the data.table

### not working gives error
tm_shape(L7) + 
   tm_rgb(3, 2, 1)

# Error: palette should be a character value
# In addition: Warning message:
# In value[[3L]](cond) : could not rename the data.table
mtennekes commented 8 months ago

Thx. It's already some time ago that I've worked on stars objects with bands and attributes. So I have to recall which function calls should be supported and that the 'correct' approach is.

At least, tmap4 does the 'multivariate' stuff via tm_mv and tm_mv_dim.

Since L7 is a stars object with a dimension called band this is the correct (but tedious looking) code:

tm_shape(L7) + 
    tm_raster(col = tm_mv_dim("band", c(3, 2, 1)), col.scale = tm_scale_rgb())

Or, shorter:

tm_shape(L7) + 
    tm_rgb(tm_mv_dim("band", c(3, 2, 1)))

image

Have to make sure your code works as well (backwards compatibility) or at least throws some useful errors.

Do you know if this code worked in earlier tmap4 development versions?

mtennekes commented 8 months ago

FYI: This works as well

L7split = split(L7)
tm_shape(L7split) + 
    tm_rgb(tm_mv("X3", "X2", "X1"))

but tm_rgb() does not because the default is tm_mv(3, 2, 1). So I need to make sure numbers are translated to attribute indices...

mtennekes commented 2 months ago

See the newly added examples of tm_rgb. Now there are three functions to specify multivariate visual variables (such as rgb color channels:

Let me now how useful this is.

Nowosad commented 2 months ago

@mtennekes would it be possible to merge tm_mv and tm_mv_shape_vars into just one function that would react to either provided names or indices?

mtennekes commented 2 months ago

That's not so easy, because in general, the value specified for a visual variable (say col) is either a data variable name (e.g. "life_exp") or a visual value (e.g. "#ABCDEF").

tm_mv also follows this procedure, but for multivariate data. Therefore, tm_mv(1, 2, 3), could be processed as a multivariate visual value, so rgb(1,2,3, maxColorValue = 255). (Note: this does not work yet, because col requires a color name/hex code; to make this work, I need to make use of the tm_scale_rgb in order to obtain maxColorValue).

For tm_raster, I needed a default specification to show all attributes, for example for tm_shape(land) + tm_raster(). That lead to the function tm_shape_vars. For tm_rgb is was relatively easy to extend this function for multivariante data. Hence tm_mv_shape_vars.

That is the history behind these functions and their names. Suggestion to make this more intuitive (while still possible to implement) are welcome.

Nowosad commented 1 month ago

@mtennekes, what do you think about updating the names:

?

mtennekes commented 1 month ago

I like those names @Nowosad before we need to make sure to cover all use cases. Not just raster objects, but also vector objects. For instance, the function tm_mv_shape_vars also applies to polygons, so tm_mv_lyr would be a less intuitive name here.

Currently this family of functions is:

1 tm_mv(...) 2 tm_mv_dim(x, values) 3 tm_shape_vars(ids, n) 4 tm_mv_shape_vars(ids, n)

Examples of vector shapes:

tm_shape(World) +
    tm_polygons(tm_mv("HPI", "well_being"))

tm_shape(World) +
    tm_polygons(tm_shape_vars(n = 3))

tm_shape(World) +
    tm_polygons(tm_mv_shape_vars(ids = c(4, 9)))

So what would be the most intuitive names? We could also combine tm_mv and tm_mv_shape_vars and call it tm_mv_vars. For the multiple-variables (but not multivariate) case, so fill = c("HPI", "well_being") and fill = tm_shape_vars(n = 3), we could introduce tm_vars. So then we have:

1 tm_vars(..., ids, n) 2 tm_mv_vars(..., ids, n) 3 tm_mv_dim(x, values)

Then, tm_mv_vars is the same as your suggestion tm_mv_lyr (variable = layer = attribute) => not sure which of these names is the most intuitive.

mtennekes commented 1 month ago

Ignore my previous post. I've managed to make it into one function: tm_vars https://github.com/r-tmap/tmap/commit/4e7f35d084881497455e3e2dec7a24a93751fb45

@Nowosad @marine-ecologist Please check this script https://github.com/r-tmap/tmap/blob/master/sandbox/vv2.R, which we should convert to vignette(s).

tm_vars will have the arguments:

This has breaking changes w.r.t. the earlier v4 implementation of multivariate visual variables. E.g.

tm_shape(World) +
    tm_polygons(tm_mv("HPI", "well_being"))

will become

tm_shape(World) +
    tm_polygons(tm_vars(c("HPI", "well_being")))
Nowosad commented 1 month ago

Awesome! I think that this is much more intuitive.

I checked the script, and have a few comments (reprex attached below):

  1. The "complex stars" section returns a few identical outputs based on different tmap codes. I think this is not as expected (?).
  2. The terra section gives a warning.
  3. The glyphs section returns an error.
library(tmap)
file = system.file("tif/L7_ETMs.tif", package = "stars")

# 1 complex stars ???

# 2 -- spurious warning
L7_terra = terra::rast(file)

tm_shape(L7_terra) +
tm_rgb(tm_vars(dimvalues = 1:3, multivariate = TRUE))
#> Warning in apply_scale(s, l, crt, val, unm, nm__ord, "legnr", "crtnr", sortRev,
#> : Too many variables defined


# 3 error

library(tmap.glyphs)

tm_shape(NLD_prov) + 
tm_polygons() +
tm_donuts(parts = tm_vars(c("origin_native", "origin_west", "origin_non_west"), multivariate = TRUE),
  size = "population",
  size.scale = tm_scale_continuous(values.scale = 1),
  fill.scale = tm_scale_categorical(values = "brewer.dark2"))
#> Error in get(fun, mode = "function", envir = envir): object 'tmapScaleComposition' of mode 'function' was not found
mtennekes commented 1 month ago

Thx @Nowosad

  • The "complex stars" section returns a few identical outputs based on different tmap codes. I think this is not as expected (?).

Actually, it is as expected, just to show the multiple ways to Rome. Maybe it would be better to ignore the output, and show multiple tmap code chunks that produce the same map.

  • The terra section gives a warning.

Fixed. The problem was in the tmap code: terra doesn't use dimensions, so dimvalues are ignored. By default all (in this case 6) variables are shown. Perhaps the warning could be a little more helpful.

  • The glyphs section returns an error.

Fixed! (please reinstall tmap.glyphs with the latest gh version)

Nowosad commented 1 month ago

Hi @mtennekes -- just to let you know, I am still getting warnings/errors after updating the packages from GitHub, see:

   # remotes::install_github("r-tmap/tmap")
# remotes::install_github("r-tmap/tmap.glyphs")
library(tmap)
file = system.file("tif/L7_ETMs.tif", package = "stars")

# 1 -- spurious warning
L7_terra = terra::rast(file)

tm_shape(L7_terra) +
tm_rgb(tm_vars(dimvalues = 1:3, multivariate = TRUE))
#> Warning in apply_scale(s, l, crt, val, unm, nm__ord, "legnr", "crtnr", sortRev,
#> : Too many variables defined


# 2 -- glyphs
library(tmap.glyphs)
tm_shape(NLD_prov) + 
tm_polygons() +
tm_donuts(parts = tm_vars(c("origin_native", "origin_west", "origin_non_west"), multivariate = TRUE),
  size = "population",
  size.scale = tm_scale_continuous(values.scale = 1),
  fill.scale = tm_scale_categorical(values = "brewer.dark2"))
#> Error in get_scale_defaults(scale, o, aes, layer, cls, ct): could not find function "get_scale_defaults"
mtennekes commented 1 month ago

Should be fixed. Please check if the error message for (1) is clear and if (2) works @Nowosad

Nowosad commented 1 month ago

Thanks, @mtennekes -- the first example is fixed, but the second one still have some issues:

# remotes::install_github("r-tmap/tmap")
# remotes::install_github("r-tmap/tmap.glyphs")
library(tmap)
file = system.file("tif/L7_ETMs.tif", package = "stars")

# glyphs
library(tmap.glyphs)
tm_shape(NLD_prov) + 
  tm_polygons() +
  tm_donuts(parts = tm_vars(c("origin_native", "origin_west", "origin_non_west"), multivariate = TRUE),
            size = "population",
            size.scale = tm_scale_continuous(values.scale = 1),
            fill.scale = tm_scale_categorical(values = "brewer.dark2"))
#> Error in tmapValuesCVV_num(x = c(0, 1), value.na = NA, n = 3L, range = c(0, : could not find function "tmapValuesCVV_num"
mtennekes commented 1 month ago

Thanks, @mtennekes -- the first example is fixed, but the second one still have some issues:

# remotes::install_github("r-tmap/tmap")
# remotes::install_github("r-tmap/tmap.glyphs")
library(tmap)
file = system.file("tif/L7_ETMs.tif", package = "stars")

# glyphs
library(tmap.glyphs)
tm_shape(NLD_prov) + 
  tm_polygons() +
  tm_donuts(parts = tm_vars(c("origin_native", "origin_west", "origin_non_west"), multivariate = TRUE),
            size = "population",
            size.scale = tm_scale_continuous(values.scale = 1),
            fill.scale = tm_scale_categorical(values = "brewer.dark2"))
#> Error in tmapValuesCVV_num(x = c(0, 1), value.na = NA, n = 3L, range = c(0, : could not find function "tmapValuesCVV_num"

Ok, the second one should be working. I had to export a s***tload of internal functions to make it work. They are hidden from the documentation index. However, they mess up the autocompletion when typing tmap::. Perhaps we should rename them with the prefix ".", so .tmapGridLines instead of tmapGridLines etc.