r-tmap / tmap

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

tm_scalebar width calculation #919

Open rhurlin opened 2 months ago

rhurlin commented 2 months ago

In the current v3.99.9002, the examples for arranging tm_scalebar no longer work as expected for me. E.g. in the first example from #747:

tm_scalebar(c(0, 200, 400), position = c(“right”, “bottom”)) positions bottom left instead of bottom right.

The positioning obviously only works if no other parameters such as breaks= or width= are used.

In addition, width does not seem to have any influence on the scaling, e.g. with breaks = seq(0,1000,200) the length of the scale cannot be changed with width = 600 or width = 1200.

mtennekes commented 2 months ago

Thanks, should be fixed now. Please check if it works as expected, and reopen if necessary.

rhurlin commented 2 months ago

@mtennekes, it works much better now. Many thanks!

I would like to express two wishes for the long-term improvement of tm_scalebar:

For presentations and reports, I like to work with tmap-generated visualizations. These are very appealing to many viewers. That's why I'm already looking forward to an official release of version 4 :D

mtennekes commented 2 months ago

Thx!

rhurlin commented 2 months ago

The distance of the scalebar to the right is now better, but rather too small. This becomes clear, for example, when tm_scalebar is used within a legend and the scalebar is wider than the legend text. In the example, the unit “km” already touches the right edge of the legend box:

library(tmap)
library(sf)
#> Linking to GEOS 3.11.1, GDAL 3.6.4, PROJ 9.1.1; sf_use_s2() is TRUE

f = system.file("shapes/world.gpkg", package = "spData")
tanzania = read_sf(f, query = 'SELECT * FROM world WHERE name_long = "Tanzania"')
tanzania_buf = st_buffer(tanzania, 50000)
tanzania_buf_geom = st_geometry(tanzania_buf)
tanzania_buf_wkt = st_as_text(tanzania_buf_geom)
tanzania_neigh = read_sf(f, wkt_filter = tanzania_buf_wkt)

# works fine
tm_shape(tanzania_neigh) +
  tm_polygons() +
  tm_text(text = "name_long",
          text.scale = tm_scale(auto.placement = FALSE, remove.overlap = FALSE), 
          size = "AREA", size.legend = tm_legend_hide()) +
  tm_shape(tanzania_buf) +
  tm_polygons(col = "red", fill = "red", fill_alpha = 0.05) +
  tm_add_legend(type = "polygons", labels = "50km buffer around Tanzania",
                col = "red", fill_alpha = 0.1, fill = "red", position = c("left","bottom"))  +
  tm_scalebar(seq(0, 1000, 200), position = c("left", "bottom"), text.size = 0.6)

Other distances are also not yet ideal, here for example the distance between legend entries and the scalebar. But you can already work with it ;)

The text.size of the scalebar works as desired in the example shown here. Apparently my other code is more complex, so it doesn't scale there for other reasons? I'll look into this more thoroughly and come back to it if necessary.

Thanks again for the very quick and competent help and the patches :D

mtennekes commented 2 months ago

You're welcome. Thx for the example. Three update in latest commit:

  1. Label width/margin computation further improved. Looks perfect on my machine now. Let me know if it doesn't on your system.
  2. Another thing that I changed, is the use of the "width" argument. Now, if breaks are specified, width can still be used for finetuning.
  3. If the last labels doen't fit, but the scale bar does, this last label is removed, but the unit is printed after the second-last label. This only applies when breaks are not specified.
rhurlin commented 2 months ago
  1. Label width/margin computation further improved. Looks perfect on my machine now. Let me know if it doesn't on your system.

  2. Another thing that I changed, is the use of the "width" argument. Now, if breaks are specified, width can still be used for finetuning.

I'm afraid I don't understand the use of width properly now. I have always specified the maximum value that the scalebar should use. If I do this now, it has unpredictable effects for me. If I use the scalebar separately from the legend, width changes the position of the scalebar. If width is used within the legend, the legend is set to maximum width.

Here are some examples, derived from the example above:

# works fine
tm_shape(tanzania_neigh) +
  tm_polygons() +
  tm_text(text = "name_long",
          text.scale = tm_scale(auto.placement = FALSE, remove.overlap = FALSE), 
          size = "AREA", size.legend = tm_legend_hide()) +
  tm_shape(tanzania_buf) +
  tm_polygons(col = "red", fill = "red", fill_alpha = 0.05) +
  tm_add_legend(type = "polygons", labels = "50km buffer around Tanzania",
                col = "red", fill_alpha = 0.1, fill = "red", position = c("left","bottom"))  +
  tm_scalebar(seq(0, 1000, 200), position = c("left", "bottom"), text.size = 0.6)
# box of the legend too wide
tm_shape(tanzania_neigh) +
  tm_polygons() +
  tm_text(text = "name_long",
          text.scale = tm_scale(auto.placement = FALSE, remove.overlap = FALSE), 
          size = "AREA", size.legend = tm_legend_hide()) +
  tm_shape(tanzania_buf) +
  tm_polygons(col = "red", fill = "red", fill_alpha = 0.05) +
  tm_add_legend(type = "polygons", labels = "50km buffer around Tanzania",
                col = "red", fill_alpha = 0.1, fill = "red", position = c("left","bottom"))  +
  tm_scalebar(seq(0, 1000, 200), width = 1000, position = c("left", "bottom"), text.size = 0.6)
# For 'tm_scalebar()', 'breaks' and 'width' are not supposed to be used together; normally, setting the exact width is not needed when breaks have been specified.FALSE
# scalebar outside the legend is set arbitrarily
tm_shape(tanzania_neigh) +
  tm_polygons() +
  tm_text(text = "name_long",
          text.scale = tm_scale(auto.placement = FALSE, remove.overlap = FALSE), 
          size = "AREA", size.legend = tm_legend_hide()) +
  tm_shape(tanzania_buf) +
  tm_polygons(col = "red", fill = "red", fill_alpha = 0.05) +
  tm_add_legend(type = "polygons", labels = "50km buffer around Tanzania",
                col = "red", fill_alpha = 0.1, fill = "red", position = c("left","bottom"))  +
  tm_scalebar(seq(0, 1000, 200), width = 1000, position = c("right", "bottom"), text.size = 0.6)
# For 'tm_scalebar()', 'breaks' and 'width' are not supposed to be used together; normally, setting the exact width is not needed when breaks have been specified.FALSE
# [plot mode] fit legend/component: Some legend items or map compoments do not fit well, and are therefore rescaled. Set the tmap option 'component.autoscale' to FALSE to disable rescaling.
# Scale bar set for latitude km and will be different at the top and bottom of the map.

Overall, I currently prefer to work without width, as the results are more reliable. The overall calculation for setting the legend and scalebar is actually very complex and tricky. Thank you for your efforts to further improve this algorithm.

  1. If the last label doesn't fit, but the scale bar does, this last label is removed, but the unit is printed after the second-last label. This only applies when breaks are not specified.

I really like this automated shortening. It significantly improves the aesthetics.

mtennekes commented 2 months ago

One important piece of information you miss (apparently haven't share properly): width has nothing to do with coordinates, but the unit is 'line heights'. So width = 1000 means the maximal width (which is the width of the map minus margins).

For the last example: if the position is different, then the scale bar doesn't take the other components (in this case add_legend) into account.

You can see the margins a bit better when you enable tmap_design_mode.

I still see some room for improvement (apart from the documentation), so I'll leave this open.

rhurlin commented 2 months ago

One important piece of information you miss (apparently haven't share properly): width has nothing to do with coordinates, but the unit is 'line heights'. So width = 1000 means the maximal width (which is the width of the map minus margins).

Oh, I'm really sorry. I must have misunderstood that completely. I hadn't found any explanation in the tm_scalebar help, so I had assumed that 'width' referred to the width of the scalebar (in terms of units, e.g. 1000 km in my example). Thanks for the clarification.

For the last example: if the position is different, then the scale bar doesn't take the other components (in this case add_legend) into account.

I understand that very well, but with my understanding of width at the time, I had expected the scalebar to be oriented to the right edge of the graph in the last example. Instead, it is more to the left of the center of the graphic.

You can see the margins a bit better when you enable tmap_design_mode.

With design_mode active, you can see that the scalebar in the last example is much wider than I expected due to width=1000. Thanks for this tip too!

I still see some room for improvement (apart from the documentation), so I'll leave this open.

For me, the design possibilities with tmap are already quite good, so I can continue with my projects. Thanks again for the spontaneous help, the quick patches and the patience. Very appreciated!