1

I would like to create a polar heatmap like the heatmap from the Lancet paper "Height and body-mass index trajectories of school-aged children and adolescents from 1985 to 2019 in 200 countries and territories: a pooled analysis of 2181 population-based studies with 65 million participants": enter image description here

I appreciate the idea of annotating the age each layer of ring represents (age 5 to 19 years) by creating a fan-shaped opening of the polar heatmap (manually circled in red). I refer to 5-19 as the Y-AXIS LABELS hereafter.

Below is the code from @Cyrus Mohammadian describing how to arrange the positions of Y-AXIS LABELS of polar heatmaps. I replicate Cyrus Mohammadian's code below:

library(grid)
library(gtable)
library(reshape)
library(ggplot2)
library(plyr)

nba <- read.csv("http://datasets.flowingdata.com/ppg2008.csv")

nba$Name <- with(nba, reorder(Name, PTS))
nba.m <- melt(nba)

nba.m <- ddply(nba.m, .(variable), transform, value = scale(value))

# Convert the factor levels (variables) to numeric + quanity to determine    size   of hole.
nba.m$var2 = as.numeric(nba.m$variable) + 15

# Labels and breaks need to be added with scale_y_discrete.
y_labels = levels(nba.m$variable)
y_breaks = seq_along(y_labels) + 15


nba.labs <- subset(nba.m, variable==levels(nba.m$variable)    [nlevels(nba.m$variable)])

nba.labs <- nba.labs[order(nba.labs$Name),]
nba.labs$ang <- seq(from=(360/nrow(nba.labs))/1.5, to=(1.5* (360/nrow(nba.labs)))-360, length.out=nrow(nba.labs))+80
nba.labs$hjust <- 0
nba.labs$hjust[which(nba.labs$ang < -90)] <- 1
nba.labs$ang[which(nba.labs$ang < -90)] <- (180+nba.labs$ang)[which(nba.labs$ang < -90)]

p<-ggplot(nba.m, aes(x=Name, y=var2, fill=value)) +
  geom_tile(colour="white") +
  geom_text(data=nba.labs, aes(x=Name, y=var2+1.5,
                           label=Name, angle=ang, hjust=hjust), size=2.5) +
  scale_fill_gradient(low = "white", high = "steelblue") +
  ylim(c(0, 50)) +
  coord_polar(theta="x") +
  theme(panel.background=element_blank(),
    axis.title=element_blank(),
    panel.grid=element_blank(),
    axis.text.x=element_blank(),
    axis.ticks=element_blank(),
    axis.text.y=element_text(size=5))+ theme(axis.title.y=element_blank(),
        axis.text.y=element_blank(),
        axis.ticks.y=element_blank())
lab = textGrob((paste("G  MIN  PTS  FGM  FGA  FGP  FTM  FTA  FTP  X3PM X3PA X3PP ORB DRB  TRB  AST  STL  BLK  TO  PF")),
   x = unit(.1, "npc"), just = c("left"), 
   gp = gpar(fontsize = 7))

gp = ggplotGrob(p)
gp = gtable_add_rows(gp, unit(10, "grobheight", lab), -1)
gp = gtable_add_grob(gp, lab, t = -2, l = gp$layout[gp$layout$name == "panel",]$l)

grid.newpage()
grid.draw(gp)

This is the resultant figure: enter image description here

Y-AXIS LABELS are placed at the bottom of the heatmap instead of being positioned immediately next to each layer of ring like the Lancet paper. I therefore ask if it is possible to modify Cyrus Mohammadian's plot so that Y-AXIS LABELS are positioned next to each layer of the ring instead of being presented outside of the heatmap? In addition, it is preferrable that we can control the size of the fan-shaped opening so that we can customize according to length of the Y-AXIS LABEL texts.

A second request is to place the color legend in the center of the heatmap and make it curved. An example is illustrated in the figure below, which is from Fig 3 of the paper "Infectious diseases in children and adolescents in China: analysis of national surveillance data from 2008 to 2017":

enter image description here

Note that the color legend is centrally located and curved. How this could be done?

Thank you.

Patrick
  • 1,057
  • 9
  • 23
  • 1
    might need some modification for your data. https://stackoverflow.com/questions/62556246/how-to-plot-the-variant-circular-bar-chart-in-r-with-ggplot/62557769#62557769 – StupidWolf Nov 06 '20 at 10:25
  • 2
    Does adding the following help? `+ geom_text(aes(x= -Inf, label = variable), size = 2.5, data = nba.m[!duplicated(nba.m$variable),]) + scale_x_discrete(expand = c(0.05, 0))`. Might recalcibrate text angles of the outer labels though. – teunbrand Nov 06 '20 at 13:46
  • Thank you @teunbrand, your suggestion worked. Playing around with the number 0.05 allows for customization of the width of the fan-shaped blank. Besides, in light of the link from StupidWolf, I edited the question to require color legend to be placed in the center of the "donut" and make the lenged circular. – Patrick Nov 06 '20 at 15:17
  • Best I can come up with for the legend is to make a seperate plot containing the manually created legend with `coord_polar() + theme_void()`, grabbing the panel grob and then placing that at the center of the main plot with `annotation_custom()`. – teunbrand Nov 06 '20 at 16:00
  • I seem to realize something from your suggestion, but not yet clear. theme(legend.position = c(0.5, 0.5)) would position the legend centrally. Still not sure how to bend the legend. How about writing a more complete code snippet? Thank you. – Patrick Nov 06 '20 at 18:04

1 Answers1

1

Here is some example code for how you can shape something like a legend and add it to your plot. Due to some restrictions on annotation_custom() in relation to polar coordinates, I decided to use the devel version of patchwork from github to use the new inset_element() function (devtools::install_github("thomasp85/patchwork")).


library(ggplot2)
library(patchwork)

df <- reshape2::melt(volcano[1:20, 1:20])
breaks <- scales::extended_breaks()(df$value)
breaks <- scales::discard(breaks, range(df$value))

main <- ggplot(df, aes(Var1, Var2, fill = value)) +
  geom_tile() +
  scale_y_continuous(limits = c(-20, NA)) +
  guides(fill = "none") +
  coord_polar()


legend <- ggplot() +
  geom_tile(
    aes( 
    x = seq(min(df$value), max(df$value), length.out = 255),
    y = 1, fill = after_stat(x)
    )
  ) +
  annotate(
    "text", x = breaks, y = -0.1, label = breaks, size = 3
  ) +
  annotate(
    "segment", x = breaks, xend = breaks, y = 0.5, yend = 0.7, 
    colour = "white", size = 1
  ) +
  annotate(
    "segment", x = breaks, xend = breaks, y = 1.5, yend = 1.3, 
    colour = "white", size = 1
  ) +
  guides(fill = "none") +
  scale_y_continuous(limits = c(-2, 2)) +
  scale_x_continuous(expand = c(0.1, 0)) +
  coord_polar() +
  theme_void()

legend <- ggplotGrob(legend)

main + inset_element(legend, 0.3, 0.3, 0.7, 0.7) &
  theme(plot.background = element_blank())

Created on 2020-11-06 by the reprex package (v0.3.0)

teunbrand
  • 33,645
  • 4
  • 37
  • 63
  • My question perfectly solved. Thank you. But I am wondering if I could be more picky by adding inner and outer circle and corresponding text annotation to indicate different group membership, like category 1, 2 in the figure above. – Patrick Nov 07 '20 at 14:12
  • Yes you could, you could add `geom_segment()` that span groups in the main plot. If the rotated texts are going to be on the inside of these segments, you might get weird scaling issues though. – teunbrand Nov 07 '20 at 16:38
  • In Cyrus Mohammadian's code, `nba.labs$ang <- seq(from=(360/nrow(nba.labs))/1.5, to=(1.5* (360/nrow(nba.labs)))-360, length.out=nrow(nba.labs))+80` is used to make x-axis label perpendicular to the plot. I would like to ask if it is possible to do so based on your code, regardless of the size of openning defined by `expand = c(0.05, 0)`? I may work out the appropriate angle for each label if I could translate the value 0.05 to angle of openning. – Patrick Dec 02 '20 at 11:06
  • I think the translation would go roughly like this: ```expand <- c(0.05, 0); range <- c(0, 1); exprange <- ggplot2:::expand_range4(range, expand); scales::rescale(range, to = c(0, 360), from = exprange)``` – teunbrand Dec 02 '20 at 11:31
  • I tried to follow your code and obtained two values, 16.36364 and 343.63636. I do not particularly understand how they could be used. Could you please kindly update the above code your wrote previously so that I may have a better understanding. Thank you and sorry indeed for bothering. – Patrick Dec 02 '20 at 12:50
  • These are the start and end angles for the curved legend. Or perhaps I'm mistunderstanding what kind of value you're aiming for? – teunbrand Dec 02 '20 at 13:44
  • Sorry that I did not make myself clear. What I want is to rotate the names of NBA players so that the direction of name lables are aligned with the radius and all name lables point to the center point of the cricular heatmap. I am not talking about legend, but rather x-axis labels, i.e., the NBA players' names. – Patrick Dec 02 '20 at 14:08
  • Ah right, sorry I'm not very good with trigonometry and I don't understand the thoughts behind Cyrus Mohammadian's code well enough to adapt it to your use case, I'm afraid. – teunbrand Dec 02 '20 at 15:13