r-tmap / tmap

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

Standardisation of symbols #706

Closed mtennekes closed 6 months ago

mtennekes commented 1 year ago

Symbols in view mode are working (see below) thanks to @tomroh , but we need to standardize the symbols between plotting modes:

Currently, tmap uses the pch numbering:

image

Note that black corresponds to the visual variable col and blue to fill. In order to make fill usable, tmap (by default) uses symbols 21:25.

leaflegend which I use for leaflet legends, natively supports these symbols:

Screenshot 2023-03-02 at 16 49 35 Screenshot 2023-03-01 at 20 58 14

What are your thoughts and ideas (@Nowosad @Robinlovelace @tim-salabim)?

tomroh commented 1 year ago

I think the core of the symbols are there in leaflegend. There are internals but modify triangle in makeSymbol as an example

'inverseTriangle' = htmltools::tags$polygon(
      id = 'triangle',
      points = sprintf('%s,%s %s,%s %s,%s',
                       strokewidth,
                       height + strokewidth,
                       width + strokewidth,
                       height + strokewidth,
                       width / 2  + strokewidth,
                       strokewidth),
      stroke = color,
      fill = fillColor,
      'stroke-opacity' = opacity,
      'fill-opacity' = fillOpacity,
      transform = sprintf('translate(%f, %f) rotate(45 0 0 )', height/2, width/2),
      ...
    )

It's an svg so you can rotate it around the center point.

For pch, maybe a function generator to pick by both name an position

pch_symbol <- function(i, width, height, offset = 0) {
    c('rect' = drawRect(width, height, offset = 0) 
        'circle' = drawCircle(width, height, offset = 0))[i](width, height, offset = 0)
}

I'm happy to add this into leaflegend if it helps. I'm not sure on the naming conventions for each symbol.

Robinlovelace commented 1 year ago

I like the symbols to have userfriendly names, like leaflegend: circle is much easer to understand than 21 (you don't wanna know how often I google 'R pch' :-).

Fully agree. Most important thing for users is ease-of-use and compatibility between plot and view modes for the most commonly needed shapes, and all of pch options are not needed IMO. Leaflegend looks amazing!

mtennekes commented 1 year ago

For pch, maybe a function generator to pick by both name an position

pch_symbol <- function(i, width, height, offset = 0) {
  c('rect' = drawRect(width, height, offset = 0) 
      'circle' = drawCircle(width, height, offset = 0))[i](width, height, offset = 0)
}

I'm happy to add this into leaflegend if it helps. I'm not sure on the naming conventions for each symbol.

That would be awesome, @tomroh! This is especially great for backwards compatibility: probably each of the pch symbols is used in the wild.

mtennekes commented 1 year ago

image

My proposal (feel free to improve):

0 open-rect 1 open-circle 2 open-triangle 3 simple-plus 4 simple-cross 5 open-diamond 6 open-down-triangle 7 cross-rect 8 simple-star 9 plus-diamond 10 plus-circle 11 hexagram 12 plus-rect 13 cross-circle 14 triangle-rect 15 solid-rect 16 solid-circle 17 solid-triangle 18 solid-diamond 19 solid-circle 20 solid-dot 21 circle 22 rect 23 diamond 24 triangle 25 down-triangle

The normal (filled) symbols will the defaults in tmap. Probably I can mimic your plus, cross, and star in plot mode with layering two pch 3/4/8 symbols, one with a large lwd in black ("col") and on top a small lwd symbol in blue ("fill").

tomroh commented 1 year ago

I still need to tweak some of the symbol aesthetics but the implementation will work like this:

pchNames <- stats::setNames(seq(0L, 25L, 1L),
  c('open-rect', 'open-circle', 'open-triangle', 'simple-plus', 
  'simple-cross', 'open-diamond', 'open-down-triangle', 'cross-rect', 
  'simple-star', 'plus-diamond', 'plus-circle', 'hexagram', 'plus-rect', 
  'cross-circle', 'triangle-rect', 'solid-rect', 'solid-circle-md', 
  'solid-triangle', 'solid-diamond', 'solid-circle-bg', 'solid-circle-sm', 'circle', 
  'rect', 'diamond', 'triangle', 'down-triangle'
  ))
defaultSize <- 20
i <- 1:26
pchSvg <- lapply(names(pchNames)[i], makePch, width = defaultSize, 
  color = 'black', `stroke-width` = 2, fillOpacity = .5)
pchSvgI <- lapply(i-1, makePch, width = defaultSize, 
  color = 'black', `stroke-width` = 2, fillOpacity = .5)
leaflet::leaflet(options = leaflet::leafletOptions(zoomControl = FALSE)) |> 
  addLegendImage(images = pchSvg, labels =names(pchNames), 
    width = defaultSize, height = defaultSize, position = 'topright') |> 
  addLegendImage(images = pchSvgI, labels = i-1, 
    width = defaultSize, height = defaultSize, position = 'topleft')
image
tomroh commented 1 year ago

tomroh/leaflegend@a75fd0f700991fa39f79e3614ea9e87b8680a250

It's in the development version on github now.

mtennekes commented 1 year ago

Wow, amazing! Thanks!

I've tried

pchSvg <- lapply(names(pchNames)[i], makePch, width = defaultSize, 
                 color = 'black', `stroke-width` = 2, fillColor = "blue", fillOpacity = .5)
pchSvgI <- lapply(i-1, makePch, width = defaultSize, 
                  color = 'black', `stroke-width` = 2, fillColor = "blue", fillOpacity = .5)
leaflet::leaflet(options = leaflet::leafletOptions(zoomControl = FALSE)) |> 
    addLegendImage(images = pchSvg, labels =names(pchNames), 
                   width = defaultSize, height = defaultSize, position = 'topright') |> 
    addLegendImage(images = pchSvgI, labels = i-1, 
                   width = defaultSize, height = defaultSize, position = 'topleft')
Screenshot 2023-03-06 at 21 58 46

Almost what I expected. The only thing is that 21:25 don't have a black border color anymore. Is that correct, or do I miss something.

tomroh commented 1 year ago

What browser? Although I don't think it should matter. Do you have the output of:

cat(URLdecode(pchSvg[[26]]))

I copied that same code you put up there and:

image
mtennekes commented 1 year ago

My mistake: I was sleeping :-)) My screenshot only showed 1-18, the last ones didn't fit my laptop screen. I've checked again and it looks great! Thanks again!

I'll leave this issue open until I've implemented this in tmap (as reminder).

mtennekes commented 1 year ago

Hi @tomroh I'm about to implement the new symbols. However, I get this:

leaflegend::makeSymbolIcons(shape = "open-rect", width = 20, height = 20, color = "#000000", opacity = 1)
#> Error in (function (shape, width, height = width, color, fillColor = color, : Invalid shape argument.

Created on 2023-04-25 with reprex v2.0.2

I have forgotten why, but currently tmap uses makeSymbolIcons for the map and makeSymbols for the legend.

Do you have any clue?

tomroh commented 1 year ago

pch symbols are in a different function. Use makePch for 'open-rect' and the like. makeSymbolIcons is used for the map because the svg data uris (images) need to be wrapped in leaflet::icon. I added makePchIcons so that you have the same functionality.

mtennekes commented 1 year ago
> leaflegend::makePchIcons
function (shape, color, fillColor = color, opacity, fillOpacity = opacity, 
    strokeWidth = 1, width, height = width, ...) 
{
    symbols <- Map(makeSymbol, shape = shape, width = width, 
        height = height, color = color, fillColor = fillColor, 
        opacity = opacity, fillOpacity = fillOpacity, `stroke-width` = strokeWidth, 
        ...)
    leaflet::icons(iconUrl = unname(symbols), iconAnchorX = width/2, 
        iconAnchorY = height/2)
}
<bytecode: 0x10e0e5548>
<environment: namespace:leaflegend>

Shouldn't it be Map(makePch, ... ) instead of Map(makeSymbol, ...) @tomroh ?

tomroh commented 1 year ago

I rushed that... I refactored the code. You can now use makeSymbol and makeSymbolIcons. Usage:

pchNames <- stats::setNames(seq(0L, 25L, 1L),
  c('open-rect', 'open-circle', 'open-triangle', 'simple-plus',
    'simple-cross', 'open-diamond', 'open-down-triangle', 'cross-rect',
    'simple-star', 'plus-diamond', 'plus-circle', 'hexagram', 'plus-rect',
    'cross-circle', 'triangle-rect', 'solid-rect', 'solid-circle-md',
    'solid-triangle', 'solid-diamond', 'solid-circle-bg', 'solid-circle-sm', 'circle',
    'rect', 'diamond', 'triangle', 'down-triangle'
  ))
i <- 1:26
pchSvg <- lapply(names(pchNames)[i], makeSymbol, width = defaultSize,
  color = 'black', `stroke-width` = 2, fillOpacity = .5)
leaflet::leaflet(options = leaflet::leafletOptions(zoomControl = FALSE)) |>
  addSymbols(lng = i, lat = i, shape = availableShapes()[['pch']],
    color = 'black', values = i, `strokeWidth` = 2, fillOpacity = .5) |> 
  addLegendImage(images = pchSvg, labels =names(pchNames),
    width = defaultSize, height = defaultSize, position = 'topright')

Hopefully that makes it easier to work with. There is a name conflict with original symbols and pch names but they return the same svg specs for the symbol.

tomroh commented 1 year ago

leaflegend version 1.1 is on its way to CRAN. In addition to the pch symbols, you can show NA encodings in the legend when NAs are present in the values, and you can also change the labels for addLegendNumeric.

mtennekes commented 1 year ago

Excellent @tomroh thanks! Very handy that you standardised makeSymbol(Icons)

I just noticed one discrepancy with the base implementation of pch symbols: In base R the color of symbols 15 to (and including) 20 come from color rather than fill:

Conceptually, both interpretations are explainable. Using fill for symbols 15-20 is more intuitive for most users I think, but on the other hand, there may be users who are familiar with using color.

What do you think? Also glad to hear more opinions (eg @Nowosad @Robinlovelace @xiaofanliang @tim-salabim )

Robinlovelace commented 1 year ago

I think fill may be slightly more intuitive. There are few people in the world of ggplot2 who use base R as a daily driver for graphics so I think whatever is most intuitive, without worrying about compatibility issues, is a reasonable approach here.

tim-salabim commented 1 year ago

Personally, I like fill better as it's more intuitive. Though, I tend to stick to the upstream leaflet terminology where it would be fillColor I guess

tomroh commented 1 year ago

I think fillColor is more consistent with the use of leaflegend and more intuitive (granted I'm not a base R plot user in general). You don't have to context switch and treat 6 symbols differently than the others. The only issue I could see is if someone is developing something that can switch between base R static and leaflet interactive graphics. Then it would be a lot smoother to have the color specs consistent. Although, fillColor != fill and col != color anyway, except with partial matching of arguments.

mtennekes commented 1 year ago

Thanks for your input!

tmap4 has a few upstream paths, e.g. leaflet for view mode and grid for plot mode (in the future perhaps also rayshader). So therefore the aim is to have a consistent set of visual variable and values, which is often a trade-off. At the start of tmap4 (already 2 years ago 🙈), we had a short discussion about the variable names: https://github.com/r-tmap/tmap/issues/579

The only issue I could see is if someone is developing something that can switch between base R static and leaflet interactive graphics.

Yes, that is exactly one of the main aims of tmap.

I will settle with the most intuitive, and that is using fill. It could mean that backwards compatibility is not guaranteed, but perhaps I can catch those cases.

tomroh commented 1 year ago

I could make it so you could specify fillColor or the color argument for pch 15-20 if that would help?

tomroh commented 1 year ago

solid pch symbols now use color if fillColor is missing in >=v1.1.1

mtennekes commented 6 months ago

I've added this example to tm_symbols:

# create grid of 25 points in the Athlantic
library(sf)
x = st_as_sf(cbind(expand.grid(x = -51:-47, y = 20:24), id = seq_len(25)), 
    coords = c("x", "y"), crs = 4326)

tm_shape(x, bbox = bb(x, ext = 1.2)) +
tm_symbols(shape = "id",
    size = 2,
    lwd = 2,
    fill = "orange",
    col = "black",
    shape.scale = tm_scale_asis()) +
tm_text("id", ymod = -2)

Plot mode:

image

View mode:

Screenshot 2024-04-25 at 11 17 33

Thx @tomroh for making this possible!