tmap icon indicating copy to clipboard operation
tmap copied to clipboard

Multi-variable / multivariate specification functions

Open marine-ecologist opened this issue 1 year ago • 14 comments

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

marine-ecologist avatar Jan 02 '24 21:01 marine-ecologist

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 avatar Jan 12 '24 08:01 mtennekes

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 avatar Jan 12 '24 09:01 mtennekes

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

  • tm_mv that uses attribute/layer names
  • tm_mv_shape_vars that uses attribute/layer indices
  • tm_mv_dim that uses a (stars) dimension

Let me now how useful this is.

mtennekes avatar Jul 21 '24 15:07 mtennekes

@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?

Nowosad avatar Jul 22 '24 18:07 Nowosad

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.

mtennekes avatar Jul 23 '24 10:07 mtennekes

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

  • tm_mv_dim that uses a (stars) dimension
  • tm_mv_lyr that uses attribute/layer indices

?

Nowosad avatar Aug 05 '24 15:08 Nowosad

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 avatar Aug 12 '24 15:08 mtennekes

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:

  • x which can be either a vector variable names (previously the ...), or variable indices (previously ids), or a single dimension name
  • values only used in case x is a dimension
  • n the first n variables in case x is not specified (and in case x is a dimension and values are not specified, the first n unique (sorted) values of this dimension.
  • multivariate. FALSE means multiple variables (facets), TRUE means multivariate (e.g. bivariate choropleth)

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")))

mtennekes avatar Aug 16 '24 17:08 mtennekes

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

Nowosad avatar Aug 19 '24 09:08 Nowosad

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)

mtennekes avatar Aug 19 '24 14:08 mtennekes

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"

Nowosad avatar Aug 21 '24 12:08 Nowosad