web-dev-qa-db-fra.com

ggplot2: Diviser la légende en deux colonnes, chacune avec son propre titre

J'ai ces facteurs

require(ggplot2)
names(table(diamonds$cut))
# [1] "Fair"      "Good"      "Very Good" "Premium"   "Ideal" 

que je souhaite diviser visuellement en deux groupes dans la légende (en indiquant également le nom du groupe):

"Premier groupe" -> "Passable", "Bon"

et

"Deuxième groupe" -> "Très bien", "Premium", "Idéal"

Commençant par ce complot

ggplot(diamonds, aes(color, fill=cut)) + geom_bar() + 
  guides(fill=guide_legend(ncol=2)) +
  theme(legend.position="bottom")

Je veux obtenir

enter image description here

(notez que "Très bien" a glissé dans la deuxième colonne/groupe)

24
CptNemo

Vous pouvez déplacer la catégorie "Très bien" dans la deuxième colonne de la légende en ajoutant un niveau de facteur fictif et en définissant sa couleur sur blanc dans la légende, afin qu'elle ne soit pas visible. Dans le code ci-dessous, nous ajoutons un niveau de facteur vide entre "Bon" et "Très bon", nous avons donc maintenant six niveaux. Ensuite, nous utilisons scale_fill_manual pour définir la couleur de ce niveau de blanc sur "blanc". drop=FALSE force ggplot à conserver le niveau vide dans la légende. Il pourrait y avoir un moyen plus élégant de contrôler où ggplot place les valeurs de légende, mais au moins cela fera le travail.

diamonds$cut = factor(diamonds$cut, levels=c("Fair","Good"," ","Very Good",
                                             "Premium","Ideal"))

ggplot(diamonds, aes(color, fill=cut)) + geom_bar() + 
  scale_fill_manual(values=c(hcl(seq(15,325,length.out=5), 100, 65)[1:2], 
                             "white",
                             hcl(seq(15,325,length.out=5), 100, 65)[3:5]),
                    drop=FALSE) +
  guides(fill=guide_legend(ncol=2)) +
  theme(legend.position="bottom")

enter image description here

MISE À JOUR: J'espère qu'il y a une meilleure façon d'ajouter des titres à chaque groupe dans la légende, mais la seule option que je peux proposer pour l'instant est recourir à des grobs, ce qui me donne toujours mal à la tête. Le code ci-dessous est adapté de la réponse à this SO question . Il ajoute deux grobs de texte, un pour chaque étiquette, mais les étiquettes doivent être positionnées à la main, ce qui est une énorme douleur. Le code de l'intrigue doit également être modifié pour créer plus de place pour la légende. peut positionner les étiquettes en dehors de la zone découpée, mais alors elles sont trop loin de la légende. J'espère que quelqu'un qui vraiment sait comment travailler avec les grobs pourra résoudre ce problème et plus généralement améliorer sur le code ci-dessous (@baptiste, êtes-vous là-bas?).

library(gtable)

p = ggplot(diamonds, aes(color, fill=cut)) + geom_bar() + 
  scale_fill_manual(values=c(hcl(seq(15,325,length.out=5), 100, 65)[1:2], 
                             "white",
                             hcl(seq(15,325,length.out=5), 100, 65)[3:5]),
                    drop=FALSE) +
  guides(fill=guide_legend(ncol=2)) +
  theme(legend.position=c(0.5,-0.26),  
        plot.margin=unit(c(1,1,7,1),"lines")) +
  labs(fill="") 

# Add two text grobs
p = p + annotation_custom(
    grob = textGrob(label = "First\nGroup", 
                    hjust = 0.5, gp = gpar(cex = 0.7)),
    ymin = -2200, ymax = -2200, xmin = 3.45, xmax = 3.45) +
  annotation_custom(
    grob = textGrob(label = "Second\nGroup",
                    hjust = 0.5, gp = gpar(cex = 0.7)),
    ymin = -2200, ymax = -2200, xmin = 4.2, xmax = 4.2)

# Override clipping
gt <- ggplot_gtable(ggplot_build(p))
gt$layout$clip <- "off"
grid.draw(gt)

Et voici le résultat:

enter image description here

26
eipi10

En utilisant cowplot, il vous suffit de construire les légendes séparément puis de recoudre les choses ensemble. Cela nécessite l'utilisation de scale_fill_manual Pour s'assurer que les couleurs correspondent à travers les tracés, et il y a beaucoup de place pour jouer avec le positionnement de la légende, etc.

Enregistrez les couleurs à utiliser (ici, en utilisant RColorBrewer)

cut_colors <-
  setNames(brewer.pal(5, "Set1")
           , levels(diamonds$cut))

Faites de l'intrigue de base - sans une légende:

full_plot <-
  ggplot(diamonds, aes(color, fill=cut)) + geom_bar() + 
  scale_fill_manual(values = cut_colors) +
  theme(legend.position="none")

Faites deux tracés distincts, limités aux coupes dans le groupe que nous voulons. Nous ne prévoyons pas de tracer ces éléments; nous allons simplement utiliser les légendes qu'ils génèrent. Notez que j'utilise dplyr pour faciliter le filtrage, mais ce n'est pas strictement nécessaire. Si vous effectuez cette opération pour plus de deux groupes, il peut être utile d'utiliser split et lapply pour générer une liste des tracés au lieu de faire chacun manuellement.

for_first_legend <-
  diamonds %>%
  filter(cut %in% c("Fair", "Good")) %>%
  ggplot(aes(color, fill=cut)) + geom_bar() + 
  scale_fill_manual(values = cut_colors
                    , name = "First Group")


for_second_legend <-
  diamonds %>%
  filter(cut %in% c("Very Good", "Premium", "Ideal")) %>%
  ggplot(aes(color, fill=cut)) + geom_bar() + 
  scale_fill_manual(values = cut_colors
                    , name = "Second Group")

Enfin, cousez l'intrigue et les légendes ensemble en utilisant plot_grid. Notez que j'ai utilisé theme_set(theme_minimal()) avant d'exécuter l'intrigue pour obtenir le thème que j'aime personnellement.

plot_grid(
  full_plot
  , plot_grid(
    get_legend(for_first_legend)
    , get_legend(for_second_legend)
    , nrow = 1
  )
  , nrow = 2
  , rel_heights = c(8,2)
)

enter image description here

8
Mark Peterson

Suivant l'idée de @ eipi10, vous pouvez ajouter le nom des titres sous forme d'étiquettes, avec des valeurs white:

diamonds$cut = factor(diamonds$cut, levels=c("Title 1           ","Fair","Good"," ","Title 2","Very Good",
                                         "Premium","Ideal"))

ggplot(diamonds, aes(color, fill=cut)) + geom_bar() + 
   scale_fill_manual(values=c("white",hcl(seq(15,325,length.out=5), 100, 65)[1:2], 
                              "white","white",
                              hcl(seq(15,325,length.out=5), 100, 65)[3:5]),
                     drop=FALSE) +
   guides(fill=guide_legend(ncol=2)) +
   theme(legend.position="bottom", 
         legend.key = element_rect(fill=NA),
         legend.title=element_blank())

enter image description here

J'introduis des espaces blancs après "Title 1 " Pour séparer les colonnes et améliorer la conception, mais il pourrait y avoir une option pour augmenter l'espace.

Le seul problème est que je ne sais pas comment changer le format des étiquettes "title" (j'ai essayé bquote ou expression mais ça n'a pas marché).

_____________________________________________________________

Selon le graphique que vous essayez , un alignement à droite de la légende pourrait être une meilleure alternative, et cette astuce semble meilleure (IMHO). Il sépare la légende en deux et utilise mieux l'espace. Tout ce que vous avez à faire est de remplacer ncol par 1 Et "bottom" (legend.position) Par "right":

diamonds$cut = factor(diamonds$cut, levels=c("Title 1","Fair","Good"," ","Title 2","Very Good","Premium","Ideal"))


ggplot(diamonds, aes(color, fill=cut)) + geom_bar() + 
   scale_fill_manual(values=c("white",hcl(seq(15,325,length.out=5), 100, 65)[1:2], 
                              "white","white",
                              hcl(seq(15,325,length.out=5), 100, 65)[3:5]),
                     drop=FALSE) +
   guides(fill=guide_legend(ncol=1)) +
   theme(legend.position="bottom", 
         legend.key = element_rect(fill=NA),
         legend.title=element_blank())

enter image description here

Dans ce cas, il pourrait être judicieux de laisser le titre dans cette version, en supprimant legend.title=element_blank()

8
toto_tico

Cela ajoute les titres au gtable de la légende. Il utilise la technique de @ eipi10 pour déplacer la catégorie "très bien" dans la deuxième colonne de la légende (merci).

La méthode extrait la légende du tracé. Le gtable de la légende peut être manipulé. Ici, une ligne supplémentaire est ajoutée à la table gtable et les titres sont ajoutés à la nouvelle ligne. La légende (après quelques ajustements) est ensuite replacée dans l'intrigue.

library(ggplot2)
library(gtable)
library(grid)

diamonds$cut = factor(diamonds$cut, levels=c("Fair","Good"," ","Very Good",
                                             "Premium","Ideal"))

p = ggplot(diamonds, aes(color, fill = cut)) + 
       geom_bar() + 
       scale_fill_manual(values = 
              c(hcl(seq(15, 325, length.out = 5), 100, 65)[1:2], 
              "white",
              hcl(seq(15, 325, length.out = 5), 100, 65)[3:5]),
              drop = FALSE) +
  guides(fill = guide_legend(ncol = 2, title.position = "top")) +
  theme(legend.position = "bottom", 
        legend.key = element_rect(fill = "white"))

# Get the ggplot grob
g = ggplotGrob(p)

# Get the legend
leg = g$grobs[[which(g$layout$name == "guide-box")]]$grobs[[1]]

# Set up the two sub-titles as text grobs
st = lapply(c("First group", "Second group"), function(x) {
   textGrob(x, x = 0, just = "left", gp = gpar(cex = 0.8)) } )

# Add a row to the legend gtable to take the legend sub-titles
leg = gtable_add_rows(leg, unit(1, "grobheight", st[[1]]) + unit(0.2, "cm"), pos =  3)

# Add the sub-titles to the new row
leg = gtable_add_grob(leg, st, 
            t = 4, l = c(2, 6), r = c(4, 8), clip = "off")

# Add a little more space between the two columns
leg$widths[[5]] = unit(.6, "cm")

# Move the legend to the right
 leg$vp = viewport(x = unit(.95, "npc"), width = sum(leg$widths), just = "right")

# Put the legend back into the plot
g$grobs[[which(g$layout$name == "guide-box")]] = leg

# Draw the plot
grid.newpage()
grid.draw(g)

enter image description here

7
Sandy Muspratt

Cette question date de quelques années, mais de nouveaux packages sont apparus depuis que cette question a été posée et peuvent vous aider ici.

1) ggnewscale Ce package CRAN fournit new_scale_fill de telle sorte que tout geom de remplissage après son apparition obtient une échelle distincte.

library(ggplot2)
library(dplyr)
library(ggnewscale)

cut.levs <- levels(diamonds$cut)
cut.values <- setNames(Rainbow(length(cut.levs)), cut.levs)

ggplot(diamonds, aes(color)) +
  geom_bar(aes(fill = cut)) + 
  scale_fill_manual(aesthetics = "fill", values = cut.values,
                    breaks = cut.levs[1:2], name = "First Grouop:") +
  new_scale_fill() +
  geom_bar(aes(fill2 = cut)) %>% rename_geom_aes(new_aes = c(fill = "fill2")) +
  scale_fill_manual(aesthetics = "fill2", values = cut.values,
                    breaks = cut.levs[-(1:2)], name = "Second Group:") +
  guides(fill=guide_legend(order = 1)) +
  theme(legend.position="bottom")

2) relayer Le paquet relayer (sur github) permet de définir une nouvelle esthétique alors nous dessinons les barres deux fois, une fois avec un fill esthétique et une fois avec un fill2 esthétique, générant une légende distincte pour chacun en utilisant scale_fill_manual.

library(ggplot2)
library(dplyr)
library(relayer)

cut.levs <- levels(diamonds$cut)
cut.values <- setNames(Rainbow(length(cut.levs)), cut.levs)

ggplot(diamonds, aes(color)) +
  geom_bar(aes(fill = cut)) + 
  geom_bar(aes(fill2 = cut)) %>% rename_geom_aes(new_aes = c(fill = "fill2")) +
  guides(fill=guide_legend(order = 1)) +  ##
  theme(legend.position="bottom") +
  scale_fill_manual(aesthetics = "fill", values = cut.values,
                    breaks = cut.levs[1:2], name = "First Grouop:") +
  scale_fill_manual(aesthetics = "fill2", values = cut.values,
                    breaks = cut.levs[-(1:2)], name = "Second Group:")

screenshot

Je pense que la légende horizontale semble un peu mieux ici car elle ne prend pas autant de place, mais si vous voulez deux légendes verticales côte à côte, utilisez cette ligne à la place de la ligne guides marquée ##:

guides(fill = guide_legend(order = 1, ncol = 1),
  fill2 = guide_legend(ncol = 1)) +
3
G. Grothendieck