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: rich text in labels (like ggtext) #169

Open dmoul opened 4 years ago

dmoul commented 4 years ago

Summary

As requested by @cbrueffer and discussed in #145, ggtext is now on CRAN. It would be wonderful to be able to plot "repelled" labels that are formatted with markup, for example with something like geom_richtext_repel() and geom_textbox_repel().

I thought of this today as I'm plotting nounphrases in which I replaced spaces with "\<br>" so the words wrap on the plot.

slowkow commented 4 years ago

Thanks for opening the issue! This is also on my wishlist. I would be very happy to review a pull request if anyone wants to give it a shot. Otherwise, I might eventually find some time to try this some day.

bwiernik commented 4 years ago

@slowkow Per the discussion here, you could directly support ggtext by making the text grob drawing function configurable. Something like, grob = "shadowtext" as the default with grob = "richtext" indicating to use gridtext::richtext_grob().

https://github.com/wilkelab/ggtext/issues/47#issuecomment-679383037

aphalo commented 4 years ago

@slowkow What about factoring out the code and exporting the repulsion code so that it would be easy to build geoms for different kinds of grobs. (Just dreaming... but adding geom_plot_repel(), geom_table_repel() and geom_grob_repel() to 'ggpmisc' for use on maps and time lines would be very useful...) I need to have a look at the code in 'ggrepel', but you know it better... Does this seem feasible? a repulsion function that takes a list or tree of grobs as input and returns a list or tree of grobs at the new postions?

cbrueffer commented 4 years ago

@aphalo there's some initial work on that in https://github.com/slowkow/ggrepel/pull/157

slowkow commented 4 years ago

@cbrueffer I think that issue is focused on trying to get some repel functionality for plots created with base grid graphics (without ggplot2).

@aphalo I like the idea, but I need to think about whether it is feasible. My first thought about your comment is copying the functionality of annotate() to annotate_repel() or something like that, so we can pass geoms like "rect" or "text" or other stuff to it. I haven't thought about this very much and I haven't studied the code inside annotate(), so I might be missing some obvious roadblocks.

My current understanding is that we probably need to make a new geom_*_repel() function for each geom, like we have two separate functions right now for geom_text_repel() and geom_label_repel(). I think the most straightforward approach is to copy the whole file (i.e. cp geom-text-repel.R geom-point-repel.R) and rewrite the code to suit other needs, e.g., geom_point_repel().

I'm not sure what would go in geom_table_repel() or geom_grob_repel()... do you have any example I might study to get some sense of what these grobs would be? I don't think I would know where to start.

By the way, sometimes it may be easier to copy internal ggrepel code and make a fresh new package instead of trying to figure out how to make it a dependency. This will vary from case to case, but it is worth considering as an option.

aphalo commented 4 years ago

@slowkow Thanks for the detailed answer! Yes, I fully agree that we need to write these geoms individually. I would not use annotate() because it would not work with facets. What I meant was whether one could define a worker function that would take the grobs in a list as input and return the same grobs but repositioned so as to avoid the overlaps. Kind of worker function that could be inserted into the code of various existing geoms to create repulsive versions. This would require doing the repulsion working with grobs and using grid dimensions, etc., so it could be a major undertake. I guess what I am suggesting is an extension to 'grid' rather than an enhancement to 'ggrepel'... I had opportunity to ask from Paul Murrel some years ago at a UseR conference whether it would be feasible to search for empty space for adding a label to a plot within 'grid', his answer was yes. Unless performance is slow, this would be the most general and elegant approach. I do not think I am up to this task and I do not have time for such a project at the moment, as I would need to study 'grid' in some depth to start with. I write this idea here in case someone is willing to take this as a challenge and work on it. If not, I will study the internal code from 'ggrepel' and work from there. Maybe something for my next summer or Christmas holidays.

krassowski commented 4 years ago

Related discussion: https://twitter.com/ClausWilke/status/1245783222283374593. Also see this snippet from a direct reply to the above tweet (though I would prefer that we specify string as in annotate()).

From the user perspective I would like to have consistent experience when specifying grob handling the text when using different ggplot extensions. Would it be worth consulting Claus Wilke and Thomas Lin Pedersen on the preferred API?

py9mrg commented 4 years ago

@aphalo @slowkow This idea of separating out the repulsion seems like a very good idea.

Given gridtext::richtext_grob is nominally a drop in replacement for textGrob, I've just had a quick go with converting all the textGrob calls in geom_text_repel, makeContent.textrepeltree, and shadowtextGrob into gridtext::richtext_grob calls (plus adjusting the arguments as appropriate) but then it fails the tests. Specifically the test about putting segment grobs before text grobs - manually running the code in test-grob-order.R returns NULL for grobnames. No labels are plotted when using the function. I presume this problem arises due to some juggling of the grob order that has to happen.

Perhaps there's some trivial other modification I've missed (the inner workings of grid/ggplot2 are not very familiar to me). But, it seems to me, if the repulsion (and grob juggling) was abstracted away from the main geom, so any geom could call it, then that would make the modification to gridtext::richtext_grob much simpler (and any other future modifications that might arise).

Plus, it would then make @clauswilke idea of allowing functions to have a grob = style argument to switch between textGrob and richtext_grob a simple switch for the user as well, rather than having to have multiple geoms.

blogeman commented 1 year ago

Checking in to see if anyone has progressed this idea and to further express interest in being able to use rich text in combination with ggrepel.

slowkow commented 1 year ago

Pull requests are welcome! I would be happy to review a pull request that implements this feature.