tmap icon indicating copy to clipboard operation
tmap copied to clipboard

Bivariate Map with TMAP 4

Open gcamaro opened this issue 7 months ago • 16 comments

Dear friends,

I would like to have some reference documentation on how to create a bivariate map with tmap.

gcamaro avatar May 14 '25 15:05 gcamaro

Good one. Here is an example:

https://github.com/r-tmap/tmap/issues/1068

We should write a vignette about this... (Surprised that there was none)

mtennekes avatar May 14 '25 15:05 mtennekes

Could we also have an example with a raster? As for now, there is only a shapefile-based version: https://r-tmap.github.io/tmap/articles/basics_charts.html?q=biva#bivariate-charts

Rapsodia86 avatar Jun 11 '25 14:06 Rapsodia86

It would be perfect. But a function would be needed that would put the rasters on the same scale and intervals and allow defining the breaks for each one to combine the information...

gcamaro avatar Jun 12 '25 16:06 gcamaro

I used to work with biscale package: https://cran.r-project.org/web/packages/biscale/vignettes/biscale.html

Rapsodia86 avatar Jun 12 '25 16:06 Rapsodia86

Thanks. I'll take a look and see if I can do something similar using tmap.

gcamaro avatar Jun 12 '25 16:06 gcamaro

Is this any useful @gcamaro and @Rapsodia86 ?

tm_shape(land) +
  tm_raster(
    col = tm_vars(c("trees", "elevation"), multivariate = TRUE),
    col.scale = tm_scale_bivariate(values = "pu_gn_bivs",
    	scale1 = tm_scale_intervals(breaks = c(0, 33, 66, 100)),
    	scale2 = tm_scale_intervals(breaks = c(-500, 1500, 4000, 6500), labels = c("L", "M", "H"))))

Image

Let me know hat you would like to achieve else.

There are a couple of bivariate color palettes in cols4all. However, one thing to improve is changing the order of the colors. For any univariate palette, a "-" can be put in front to reserve it. However, for bivariate palettes we have two dimensions. Via cols4all::c4a two arguments should be added: mirror and flip.

mtennekes avatar Jun 12 '25 20:06 mtennekes

Wow! Very intuitive. Thank you!! I'll try it with my SDM/ENM maps and let you know about the results ASAP.

gcamaro avatar Jun 12 '25 20:06 gcamaro

I tried your code and it didn't work, even after I did a tmap update from the repository. The error message was:

Error: tm_scale_bivariate cannot be used for layer raster, aesthetic col

Any clues?

gcamaro avatar Jun 12 '25 22:06 gcamaro

Thx for testing @gcamaro

That was an old bug that is already fixed. As a double-check, I've just cloned the current repo, and it works for me. Could you test it again? Does it also work for you @Rapsodia86 ?

mtennekes avatar Jun 13 '25 10:06 mtennekes

I reinstalled the entire repository and your code worked. Thank you! Now I'm trying to put this into my workflow and I noticed that there was a change with the legends with histograms, using groups, which requires some adjustments to the code I was using. But as soon as I finish testing, I'll come back.

gcamaro avatar Jun 13 '25 11:06 gcamaro

Yeah! Works very nicely! One thing: To change the title of each scale, I needed to add col.legend(), and xlab.size() & ylab.size() within. To change labels size, I needed to use legend.text.size() in the tm_layout(), which works for both at once. However, tm_scale_intervals() has label.format() parameter, although I could not make it work to change the labels size.

p1 <- tm_shape(ph_ttp) + 
  tm_raster(
    col=tm_vars(c("PH", "TTP"), multivariate = TRUE),
    col.scale = tm_scale_bivariate(values = "bu_br_bivs",
    scale1 = tm_scale_intervals(n=4,style = "quantile", labels = c("Q1", "Q2", "Q3","Q4")),
    scale2 = tm_scale_intervals(n=4,style = "quantile",labels = c("Q1", "Q2", "Q3","Q4"))),
    col.legend = tm_legend_bivariate(xlab="TTPx",ylab="PHx",xlab.size=0.3,ylab.size=0.3))+
  tm_layout(bg.color = "white",legend.text.size = 0.2)

Image

Rapsodia86 avatar Jun 13 '25 14:06 Rapsodia86

Image

It worked like a charm!

Sorry, but I couldn't find a way to rotate the variable names in the legend or to not abbreviate the values; or to be able to write the class names more completely. Maybe that's the intention, but I would like to have a way to specify the values ​​in class intervals, also for the second variable. But it's not a problem. Easily solved in the figure's caption.

env.map <-
  # Ocean
  tm_shape(oceans) +
  tm_polygons(fill = "lightblue",
              fill_alpha = 0.7,
              col = NULL) +
  # Raster of environmental variables
  # Bio01 = Annual Mean Temperature
  # Bio12 = Annual Precipitation
  tm_shape(predictors, is.main = TRUE) +
  tm_raster(
    col = tm_vars(c("Bio01", "Bio12"), multivariate = TRUE),
    col.scale = tm_scale_bivariate(values = "pu_gn_bivs",
                                   scale1 = tm_scale_intervals(
                                     style = "fixed",
                                     n = 5,
                                     midpoint = NA,
                                     breaks = c(-Inf, -15, 0, 15, 30, Inf)
                                   ),
                                   scale2 = tm_scale_intervals(
                                     style = "fixed",
                                     n = 4,
                                     midpoint = NA,
                                     breaks = c(-Inf, 1000, 2000, 4000, Inf)
                                   )),
    col.legend = tm_legend_bivariate(
      title = NA,
      title.size = 0.6,
      title.align = "left",
      title.fontface = "bold",
      xlab = "Temp.",
      ylab = "Prec.",
      xlab.size = 0.6,
      ylab.size = 0.6,
      text.size = 0.6,
      show = TRUE,
      orientation = "portrait",
      position = tm_pos_in("LEFT", "BOTTOM"),
      bg.color = "grey80",
      bg.alpha = 0.6,
      height = 13,
      frame = TRUE)
  ) +
  # Countries of the world
  tm_shape(world) +
  tm_polygons(col = "black",
              fill = NULL,
              lwd = 0.6) +
  # Species occurrences
  tm_shape(points.map) +
  tm_symbols(size = 0.2,
             lwd = 0.1,
             col = "black",
             fill = "sp",
             fill_alpha = 1,
             fill.scale = tm_scale_categorical(
               n = 1,
               values = "black",
               labels = my.species
             ),
             fill.legend = tm_legend(
               title = "Occurrences",
               title.size = 0.6,
               title.align = "left",
               title.fontface = "bold",
               text.size = 0.6,
               show = TRUE,
               orientation = "portrait",
               position = tm_pos_in("LEFT", "BOTTOM"),
               bg.color = "grey80",
               bg.alpha = 0.6,
               frame = TRUE
             )
  ) +
  # Map add-ons 
  tm_graticules(alpha = 0.5, labels.size = 1) +
  tm_compass(
    type = "4star",
    size = 2,
    position = tm_pos_in(pos.h = "RIGHT", pos.v = "BOTTOM", align.h = "center")
  ) +
  tm_scalebar(
    position = tm_pos_in(pos.h = "RIGHT", pos.v = "BOTTOM", align.h = "center"),
    width = 15
  ) +
  tm_layout(
    scale = 1
  ) +
  tm_crs(4326)

gcamaro avatar Jun 13 '25 14:06 gcamaro

@gcamaro you can add labels in tm_scale_intervals() to control class names. See my example above.

Rapsodia86 avatar Jun 13 '25 15:06 Rapsodia86

I did that. Thanks. But I was thinking of a way to leave the numerical ranges calculated. In the first variable, the labels are placed and in the second, due to the restricted space to maintain the format of the legend, they are not. Perhaps, rotating the name of the first variable 90º and the labels of the second, also 90º, this could be done. But I adjusted it in another way, which works elegantly.

Image

gcamaro avatar Jun 13 '25 15:06 gcamaro

I agree! Adding the rotation option and extending space (to at least three characters) per label would be beneficial! @mtennekes, what do you think about this? Would that be possible?

Rapsodia86 avatar Jun 13 '25 16:06 Rapsodia86

Of course @Rapsodia86

I already added the rotation arguments xlab.rot and ylab.rot. The rotation itself was trivial to implement, but it takes some time to fine-tune the alignment and placement of the labels, and the resulting resizing of the whole legend.

tm_shape(land) +
	tm_raster(
		col = tm_vars(c("trees", "elevation"), multivariate = TRUE),
		col.scale = tm_scale_bivariate(values = "pu_gn_bivs",
            scale1 = tm_scale_intervals(breaks = c(0, 33, 66, 100)),
            scale2 = tm_scale_intervals(breaks = c(-500, 1500, 4000, 6500), labels = c("L", "M", "H"))),
        col.legend = tm_legend_bivariate(xlab = "Scale 1",
  		 ylab = "Scale 2",
  		 xlab.size = 2,
  		 ylab.size = 3,
  		 item.r = 0,
  		 ylab.rot = 15,
  		 item.width = 3,
  		 item.height = 3,
  		 text.size = 1.5))

Image

Some other things you may notice:

  • Arguments of tm_legend are passed on. I deliberately didn't name them explicitly, but instead used the ... (to have the argument list not too long).
  • However, some arguments are handy for bivariate legends, in this example: text.size, item.width, item.height, and item.r
  • Probably I would make sense to pass on the text properties not just as text. but also as xtext. and ytext..

mtennekes avatar Jun 16 '25 19:06 mtennekes

For some other examples of bivariate maps with tmap see https://tmap.geocompx.org/scales#sec-bivariate-scales

Nowosad avatar Jun 22 '25 15:06 Nowosad

Wrote a vignette about bivariate choropleths: https://r-tmap.github.io/tmap/articles/examples_biv_choro Let me know your thoughts @gcamaro @Rapsodia86 @Nowosad It requires the dev version of cols4all.

mtennekes avatar Jul 21 '25 13:07 mtennekes

It turned out really well! It will also be a great help in R mapping courses. The package documentation is excellent, and the example texts are crucial, given the various options and ways to obtain different results. Thank you!

gcamaro avatar Jul 21 '25 13:07 gcamaro

Thanks, looking really good. Is it worth mentioning in the page that you need the dev cols4all? That could easily trip some users up.

nickbearman avatar Jul 21 '25 14:07 nickbearman

Looks good! Maybe, it would be useful to add upfront that it also works for tm_raster? But that is a really minor thing! Thank you very much @mtennekes!

Rapsodia86 avatar Jul 21 '25 18:07 Rapsodia86