tmap icon indicating copy to clipboard operation
tmap copied to clipboard

tm_inset / tm_minimap

Open mtennekes opened this issue 10 months ago • 9 comments

Struggling myself with inset maps. It is possible with the vp argument of print.tmap, but requires a lot of trial and error:

Image

  1. tm_minimap with shows an inset of a world map, either a projected 'flat' one, or a globe, like mapsf does. This is already implemented for the view mode, but not for plot mode.
  2. a more general tm_inset, which takes any (grid-based) plot, and places it in a map component. In that way it is processed in exactly the same way as other components, like legends and scalebars. The user still has to provide a height and width, and specify the position. Use cases: inset maps (also like this https://github.com/r-tmap/tmap/issues/1046) and ggplot2 plots.

What do you think @Nowosad and @olivroy ? Any additional ideas?

mtennekes avatar Feb 24 '25 16:02 mtennekes

@mtennekes

  1. Should tm_minimap() work in the same way as tm_basemap, i.e., using the base map of the provider in a specific zoom level?
  2. tm_inset(), on the other hand, would be a way to compose inset maps based on the provided data. One suggestion -- I think that tm_inset could try to "stick" to the map frame. For example, when the inset is set to be inside the map frame, then it (by default) would be in one of the corners, while when it is set to be outside of the frame, then it would be aligned to one of the sides of the map.

Nowosad avatar Feb 25 '25 09:02 Nowosad

Good suggestions @Nowosad

  1. Yes, good idea. We could just draw a bbox rectangle on top of this basemap? And what if no basemap is provided? Use a default basemap (e.g. "OpenStreetMap"), or use World, or ...?
  2. tm_inset() is indeed supposed to be. used inside the map frame. So the defaults of tm_pos should be tm_pos_in.

As an enhancement of the 'sticking' process I had an idea on my walk back after work: in addition to sticking to one of the corners, we could also use a component-grid-layout, where components (whatever they may be: legends, insets, credits, titles), can be arranged in a regular grid.

As for the implementation: the easiest way is to use the pos.h and pos.v arguments of tm_pos. These can now be set to left-center-right, top-center-bottom or numbers, but we could add another option.

So it could be something like this:

tm_shape(NLD_muni) +
tm_polygons() +
tm_basemap() +
tm_minimap(position = tm_pos_in_cell(row = 2, col = 1:2)) +
tm_component_grid(nrow = 3, ncol = 3, heights = c(.1, .1, .8))
  • Intuitive? Instead of tm_pos_in_cell, we could also just use tm_pos_in, and the pos.h and pos.v arguments. However, we cannot (yet) use pos.h = 1:2, pos.v = 2, because this conflicts the 'free' numeric placement option. Or pos.h = tm_row(1:2), pos.v = tm_col(2). Or we could use I() function for this: pos.h = I(1:2), pos.v = I(2)`. Perhaps this is the best option (easy to implement and hopefully intuitive to use. What do you think?
  • Alternatives for tm_component_grid? E.g. tm_set_component_grid_layout, tm_grid_layout, etc...

Just an example with the current implementation:

tm_shape(World) + 
tm_polygons(fill = "HPI", 
			col = "economy", 
			lwd = "pop_est",
			lwd.legend= tm_legend(position = tm_pos_in(), width = 13), 
			col.legend= tm_legend(position = tm_pos_in(), width = 13), 
			fill.legend = tm_legend(position = tm_pos_in(), width = 13,
				resize_as_group = F,
				group.frame = F)) + 
tm_options(
	component.offset = c(inside = 0.3, INSIDE = 0, outside = 0, OUTSIDE = 0), 
	component.stack_margin = .3)

Image

The devil is in many details:

  • resize_as_group and group.frame are options for all components that are drawn on the same spot (e.g. top left corner). These determine whether they be stacked as separate components or put into one component frame. Because there can be several comments, I had decided to simply use the first specifications. However, the order of components is not necessarily the order in which the components are processed. In the example above, I had to set resize_as_group to fill.legend because this is processed before the other legends. So this should be improved....
  • component.offset is the offset to the map frame, and component.stack_margin between components. These are still only global options, but ideally, they should be set more locally (e.g. inside the new tm_component_grid function.
  • tm_options(component.offset = .2)fails because it expects this vector. To be improved...

mtennekes avatar Feb 25 '25 10:02 mtennekes

Open question from a users perspective: if you see the map above, what would the ideal tmap code be?

Also think about/consider these kind of shortcut functions, which haven't had much attention, but should be pretty useful: tm_place_legends_left.

mtennekes avatar Feb 25 '25 10:02 mtennekes

We could just draw a bbox rectangle on top of this basemap? And what if no basemap is provided? Use a default basemap (e.g. "OpenStreetMap"), or use World, or ...?

If no basemap is provided -- I would suggest to use the default one.

tm_component_grid

I am unsure about this whole idea (including the syntax there). I.e., I like the possibility to customize the map, but -- is this actually needed for most of the tmap users? Maybe it would be better to start with working tm_inset, and then consider future steps when/if people will ask for it?

Also think about/consider these kind of shortcut functions, which haven't had much attention

My thinking is that we should be careful about adding shortcut functions, because then the package will be fun of these, and it would make it hard to find the actual "base" functions...

Nowosad avatar Feb 26 '25 12:02 Nowosad

@Nowosad @mtennekes

ls this related to my previous issue, #1046 ?

Insert maps and combine separate map look different. Is it possible to implement some basic base methods?

  • Define the boundaries of the frame that combines the graphics, possibly the top, bottom, left, and right edges.
  • Define the distance between the boundaries of the frames when combining the graphics.
  • Em.....it might not be related, when combining map plots, can add things like legend.position, title and tags.

How exactly to implement this, I may have some unclear technical detail. Once a solution approach is discussed and decided upon, I would be glad to contribute to this effort as a newcomer by working on some of the less complex tasks.

Xiezhibin avatar Feb 26 '25 18:02 Xiezhibin

@Nowosad @Xiezhibin thanks for your inputs. Agree in general.

First question: how shall we call the 'inset' components?

Currently, I can think of these ones, where I mention the input:

  1. Images (e.g. png), currently tm_logo
  2. grob objects, currently tm_component
  3. ggplot2 objects (via ggplotGrob)
  4. custom tmap objects (via print and vp)
  5. bounding boxes , currently tm_inset
  6. no input. Currently tm_minimap
  7. ... Any other inputs that I forgot?

Can we harmonize these names, e.g.

  1. tm_inset_image
  2. tm_inset_ggplot2
  3. tm_inset_grob
  4. tm_inset_tmap
  5. tm_inset_minimap
  6. also tm_inset_minimap, but without input?

Not sure about the name 'minimap'. Aren't all minimaps inset maps? The two things that distinguish a 'minimap' in its current use are: it's global, and it has a locator where the main map is. Perhaps tm_inset_locator?

You can add all current components to this list as well, like tm_scalebar and tm_title. However, tm_inset_title is less intuitive. But where to draw the border?

Also happy to hear your opinions @tim-salabim @Nowosad @olivroy , ...

Second: placement. Indeed perhaps tm_component_grid not needed and too complex.

What I need as user: distance from the map frame, distance between the components, option to align map frames.

For the pos.h and pos.v values, I think of three options now:

  • "right", "center", "left" / "top", "center", "bottom"
  • two numbers, where (this is new!) components are also stacked, just like as in the first option
  • I() function, e.g. I(0.5) and , 0.5), which is the same as the current numbers, but because I()stands forAsIs`, the numbers are as they are, so now stacking or whatsoever.

mtennekes avatar Feb 27 '25 10:02 mtennekes

Spoke with @Nowosad in person before lunch. He had a good idea:

Just one tm_inset which functions as one of the above, based on the input.

mtennekes avatar Feb 27 '25 11:02 mtennekes

The tm_inset function is not listed in version 4.0. I get error - No documentation for ‘tm_inset’ in specified packages and libraries

nitimkc avatar Apr 09 '25 09:04 nitimkc

@nitimkc it's only in the development version, which will become 4.1, probably published in CRAN in one month. Happy to hear your feedback :-)

mtennekes avatar Apr 09 '25 11:04 mtennekes

FYI: minimap implemented for plot mode:

tm_shape(NLD_muni) +
    tm_polygons("income_high") +
    tm_minimap()
#> [tip] Consider a suitable map projection, e.g. by adding `+ tm_crs("auto")`.
#> This message is displayed once per session.

tm_shape(World[World$continent == "Africa",]) +
    tm_polygons("HPI") +
    tm_minimap()

library(spData)
tm_shape(nz) +
    tm_polygons() +
    tm_minimap()
#> Warning in lnsY_emp[lnsY_proj_res$tmapID] <- lnsY_proj_emp: number of items to
#> replace is not a multiple of replacement length

Created on 2025-07-07 with reprex v2.1.1

Will write a vignette soon. The globe settings (e.g. colors) are not yet configurable.

mtennekes avatar Jul 07 '25 10:07 mtennekes

Two small issues I found:

  1. There is a warning in the plot mode
  2. Minimap is hidden in the view mode
library(tmap)
library(spData)

tmap_mode("plot")
#> ℹ tmap mode set to "plot".
tm_shape(nz) +
  tm_polygons() +
  tm_minimap()
#> [tip] Consider a suitable map projection, e.g. by adding `+ tm_crs("auto")`.
#> This message is displayed once per session.
#> Warning in lnsY_emp[lnsY_proj_res$tmapID] <- lnsY_proj_emp: number of items to
#> replace is not a multiple of replacement length


tmap_mode("view")
#> ℹ tmap mode set to "view".
tm_shape(nz) +
  tm_polygons() +
  tm_minimap()

Created on 2025-07-07 with reprex v2.1.1

Nowosad avatar Jul 07 '25 13:07 Nowosad