slowkow / ggrepel

:round_pushpin: Repel overlapping text labels away from each other in your ggplot2 figures.
https://ggrepel.slowkow.com
GNU General Public License v3.0
1.22k stars 96 forks source link

Feature request: geom_sf_label_repel(), geom_sf_text_repel() #111

Open yutannihilation opened 6 years ago

yutannihilation commented 6 years ago

Summary

ggplot2 will (hopefully) get geom_sf_label() and geom_sf_text() for labeling sf objects. (c.f. https://github.com/tidyverse/ggplot2/issues/2742#issuecomment-403315573) Are you interested in implementing the _repel version of these? Labelling sf objects is a very common task so I'm sure many people will find geom_sf_label_repel() useful.

If you feel ggrepel is not the right place for them, they will probably be implemented in ggforce.

Minimal code example

Proof of Concept is here: https://yutannihilation.github.io/ggsflabel/#geom_label_repel-for-sf

Suggestions

Note that this can be addressed after geom_sf_label() and geom_sf_text() are implemented in ggplot2. So, please stay tuned on tidyverse/ggplot2#2742! (I will do my best to finish it as soon as possible)

Version information

N/A

slowkow commented 6 years ago

Yes, I'd be interested to include those functions in ggrepel if you think it makes sense, and we're all in agreement across ggplot2, ggrepel, and ggforce. Looks like you have a great start already! Thanks for opening the issue!

I think I need to study the *_sf_* functions a bit to figure out exactly what is going on. I would have assumed that geom_text_repel() should be sufficient without an extra function for spatial data, but clearly this is not correct.

yutannihilation commented 6 years ago

Thanks for the quick reply. Glad to hear that!

I would have assumed that geom_text_repel() should be sufficient without an extra function for spatial data

I think you are right in some sense, but, you need to extract the coordinates by yourself (c.f. https://github.com/tidyverse/ggplot2/issues/2111) so we naturally want some shortcut for this. That is, *_sf_* functions, in my understanding :)

yutannihilation commented 6 years ago

Now geom_sf_text_repel() and geom_sf_label_repel() are basically possible with stat_sf_coordinates(). Example:

# github version of ggplot2
library(ggplot2)

nc <- sf::st_read(system.file("shape/nc.shp", package="sf"))
#> Reading layer `nc' from data source `/Library/Frameworks/R.framework/Versions/3.5/Resources/library/sf/shape/nc.shp' using driver `ESRI Shapefile'
#> Simple feature collection with 100 features and 14 fields
#> geometry type:  MULTIPOLYGON
#> dimension:      XY
#> bbox:           xmin: -84.32385 ymin: 33.88199 xmax: -75.45698 ymax: 36.58965
#> epsg (SRID):    4267
#> proj4string:    +proj=longlat +datum=NAD27 +no_defs
#> Reading layer `nc' from data source `/path/to/sf/shape/nc.shp' using driver `ESRI Shapefile'
#> Simple feature collection with 100 features and 14 fields
#> geometry type:  MULTIPOLYGON
#> dimension:      XY
#> bbox:           xmin: -84.32385 ymin: 33.88199 xmax: -75.45698 ymax: 36.58965
#> epsg (SRID):    4267
#> proj4string:    +proj=longlat +datum=NAD27 +no_defs

ggplot(nc) +
  geom_sf() +
  ggrepel::geom_label_repel(
    data = head(nc),
    aes(label = NAME, geometry = geometry),
    stat = "sf_coordinates",
    min.segment.length = 0
  )
#> Warning in st_point_on_surface.sfc(sf::st_zm(x)): st_point_on_surface may
#> not give correct results for longitude/latitude data

Created on 2018-09-04 by the reprex package (v0.2.0).

slowkow commented 6 years ago

@yutannihilation Do you think we should create two new functions?

I suppose you could say they are not needed anymore, since you've shown that we can use stat = "sf_coordinates" successfully for many types of figures. On the other hand, it would be convenient to have a wrapper.

What do you think? I think it makes sense to add them.

Whether or not we add two more functions, I think the vignette for ggrepel needs an example showing how to put repulsive labels on a sf plot.

If you're interested, I'd be very happy to review your pull request and merge it in. If you prefer, I can try to do it myself, but I'd appreciate your comments to make sure I'm writing the functions the way you'd expect.


As an aside, you're also making me wonder if I should think about a stat like stat_repel() to enable repulsive positioning for other types of elements besides text boxes... I think I need to spend more time studying the ggplot2 code before I'm ready for this.

yutannihilation commented 6 years ago

Yes, I think these functions are worth adding, of course! I want to implement them if you are ok (Sorry for not yet starting on this...).

While it seems easy to implement them as simple wrappers, I'm wondering if I can add an option to prevent labels to overwrap the geometry. Maybe is this the same thing as your concept of stat_repel()? I too have to study the ggplot2 code. 🤔

slowkow commented 6 years ago

Ok, feel free to get started whenever you have time. Thanks for your help!

I like the idea of avoiding collision with the shape of the geometry. This might be a challenge to implement, but I have an inkling that there might be library we can use to do that.

yutannihilation commented 6 years ago

Thanks, I will try :)

kendonB commented 5 years ago

I am facing problems trying to get geom_label_repel with a geom_sf. One neat feature would be to also have it repel from sf features themselves. In my case, I'm trying to map NZ regions with the labels falling in the ocean.

bjsmith commented 4 years ago

Working on the same problem as @kendonB .

CeresBarros commented 3 years ago

Any news on this front? have geom_sf_label/text_repel() been implemented? Thanks :)

slowkow commented 3 years ago

@CeresBarros Did you try the code example by @yutannihilation ?

Did it meet your needs?

CeresBarros commented 3 years ago

I did not - I wasn't sure if that was the current "solution". I'll try it :)

CeresBarros commented 3 years ago

Just tried it, works great, thanks!

francisbarton commented 2 years ago

Just come across this issue - getting "Error: geom_label_repel requires the following missing aesthetics: x and y" message when using geom_label_repel with a geom_sf plot. I was really confused because there's nothing in the ggrepel docs saying anything about geom_label_repel requiring x and y aesthetics! Searching the web for that error message didn't get me any useful info, so I came here to search.

@yutannihilation 's solution works great, but I do think this should be covered in the ggrepel examples vignette. So that future users don't have to hunt around for the fix. Would you welcome a PR to your vignette to include an example of this?

slowkow commented 2 years ago

@francisbarton Contributions to the documentation are welcome! Please feel free to make a PR with your suggested changes or additions.

fariadamasceno commented 1 year ago

Hi, just to let you know that I would be one more who would appreciate the wrapper functions. =) The code from @yutannihilation worked nicely, thanks!

slowkow commented 1 year ago

I'd be happy to review and merge a pull request that resolves this issue.

Steve-Koller commented 1 year ago

Not sure if this is the right place to bring this up, but I'm wondering if it's possible to selectively apply geom_sf_label_repel() to certain features in an sf object and geom_sf_label to other features in the same object? I'm currently trying to do this but unable. Can share photos or a reprex if that would help.

Below is sample code, where "geom_repel_states" is a vector with a few states' FIPS codes. These are the states whose labels I'd like to repel, since their labels overlap when including in geom_sf_label().

Thank you!

example_plot = ggplot(example_sf) + 
    geom_sf(color = "black", aes(fill = example_variable_name)) +
    geom_sf_label_repel(aes(label = ifelse(example_sf$state_fips %in% geom_repel_states, example_variable_name, ""))) + 
    geom_sf_label(aes(label=ifelse(!(example_sf$state_fips %in% geom_repel_states), example_variable_name, ""))) 

  example_plot
slowkow commented 1 year ago

@Steve-Koller This isn't really the best place to ask (better would be a new issue, best would be a new question on Stackoverflow), but since you asked...

Each geom_*() function takes a data argument. You can use geom_text_repel(data = ...) to plot a subset of the original data. The ... might be another dataframe like my_subdata or it might also be a function like function(d) d %>% filter(my_condition).

slowkow commented 1 year ago

We added an example to the ggrepel documentation. Feel free to comment below with your thoughts, and let me know if you believe we need to keep this issue open.

https://ggrepel.slowkow.com/articles/examples.html#label-sf-objects

# thanks to Hiroaki Yutani 
# https://github.com/slowkow/ggrepel/issues/111#issuecomment-416853013

library(ggplot2)
library(sf)

nc <- sf::st_read(system.file("shape/nc.shp", package="sf"), quiet = TRUE)

ggplot(nc) +
  geom_sf() +
  ggrepel::geom_label_repel(
    data = head(nc),
    aes(label = NAME, geometry = geometry),
    stat = "sf_coordinates",
    min.segment.length = 0
  )