【问题标题】:Multiple, dependent-level sunburst/doughnut chart using ggplot2使用 ggplot2 的多个相关级别的旭日形/甜甜圈图
【发布时间】:2018-10-04 20:31:44
【问题描述】:

我正在尝试创建一个两级旭日形/甜甜圈图(用于打印),其中第二级是第一级的详细视图。我已阅读并理解this tutorial,但我是 R 和 ggplot2 新手,无法制作第二级。在前面的文章中,根级别只有一个元素(有点多余),而我的根有很多元素;其中,二级至少有1个,最多10个元素。

假设我的数据有三列:nametypevalue;其中nametype 分别定义了根元素和二级元素。每个name 恰好有一个typeall,这是values 跨越types 的总和(其中,至少有一个,并且跨越names 的集合@ 987654336@ 可能相交或互斥)。例如:

name  type    value
----- ------- ------
foo   all     444
foo   type1   123
foo   type2   321
bar   all     111
bar   type3   111
baz   all     999
baz   type1   456
baz   type3   543

我可以使用以下方法创建根级堆栈(在转换为极坐标之前):

data.all <- data[data$type == "all",]
ggplot(data.all, aes(x=1, y=data.all$value, fill=data.all$name)) + geom_bar(stat="identity")

第二级堆栈需要type 值与name 值对齐,与它们的值成比例:

 +-----+  +-------+
 |     |  | type3 |
 | baz |  +-------+
 |     |  | type1 |
 +-----+  +-------+
 |     |  |       |
 | bar |  | type3 |
 |     |  |       |
 +-----+  +-------+
 |     |  | type2 |
 | foo |  +-------+
 |     |  | type1 |
-+-----+--+-------+-

(n.b.,这显然不是按比例计算的!)

我还需要对 type 值进行一致的着色(例如,type1 块的颜色对于 foobaz 应该是相同的,等等)

我想我可以通过将 nametype 列合并到一个新列中然后以此着色来做到这一点:

data.other <- data[data$type != "other",]
data.other$comb <- paste(data.other$name, data.other$type, sep=":")
ggplot(data.other, aes(x=2, y=data.other$value, fill=data.other$comb)) + geom_bar(stat="identity")

然而,这破坏了颜色的一致性——显然,事后看来——而且,有趣的是,我完全不相信对齐会是正确的。

我的 R/ggplot2 诞生可能很明显(对不起!);我怎样才能实现我正在寻找的东西?


编辑我也遇到了this question and answer,但是我的数据看起来与他们的不同。如果我的数据可以被整理成相同的形状——我不知道该怎么做——那么我的问题就变成了他们的一个特例。

【问题讨论】:

  • 如果你想避免从头开始,有一个包ggsunburst
  • @camille 只是在玩ggsunburst,它似乎只支持具有非加权节点的树结构。 sunburstR 看起来像是生成交互式、基于 Web 的输出,而不是用于打印的静态输出(例如 PDF)
  • 好的。我想您不想将数据更改为树? (如果您不这样做,那就太好了。)您发布的数据样本是否高于您正在使用的所有内容?如果没有,你能dput你的数据吗?
  • 我的问题中的数据是显示结构的说明性示例;我的真实数据要复杂得多,而且要大得多(并且是从外部源生成的),因此不适合喷入 SO。突出部分与我的示例同构:nametype 表示级别(为方便起见,使用特殊类型all)和value 加权节点(其中all 类型的值是每个 name 的其他类型的总和)。

标签: r ggplot2 pie-chart donut-chart sunburst-diagram


【解决方案1】:

根据您推荐的网页,尝试以下操作:

library(ggplot2) 
library(dplyr) 
library(scales) 

toRead <- "name  type    value
foo   all     444
foo   type1   123
foo   type2   321
bar   all     111
bar   type3   111
baz   all     999
baz   type1   456
baz   type3   543"

data <- read.table(textConnection(toRead), header = TRUE)
closeAllConnections()



sum_total_value = sum(data$value)

firstLevel = data %>% summarize(total_value=sum(value))

sunburst_0 = ggplot(firstLevel) # Just a foundation
sunburst_1 = 
  sunburst_0 + 
  geom_bar(data=firstLevel, aes(x=1, y=total_value), fill='darkgrey', stat='identity') +
  geom_text(aes(x=1, y=sum_total_value/2, label=paste('Sum of all VALUE had', comma(total_value))), color='white')

sunburst_1
sunburst_1 + coord_polar('y')


sum_val = data %>% group_by(type) %>%
  summarize(total_value=sum(value)) %>%
  arrange(desc(total_value))


sunburst_2 <- sunburst_1 +
  geom_bar(data=sum_val,
           aes(x=2, y=total_value, fill=total_value),
           color='white', position='stack', stat='identity', size=0.6) + 
  geom_text(data=sum_val, aes(label=paste(type, total_value), x=2, y=total_value), position='stack')

sunburst_2

这给出了以下情节:

如果你想在极坐标上这样,你可以添加以下内容:

sunburst_2 + coord_polar('y')

这给了你:

【讨论】:

  • 这会将所有名称中的所有值相加,作为根级别(即只有一个),然后第二级由type 拆分(包括all 的特殊值,根据定义,它恰好占据了一半的面积)。我的问题不是要按nametype 完全拆分数据,而是同时拆分数据。文章只有一个根级元素,在这种图表中是多余的。一个更好的例子是在question and answer mentioned in my edit 中,但是他们的数据与我的数据形式不同。
  • 当然。乐意效劳。我仍然不确定我是否理解您想要的输出。是不是:A.) 只是在坐标图中添加了第二个外环?或 B.) 根据 nametype 值的组合值创建单级坐标图或 C.) 其他...
  • 重点是外圈依赖于内圈。正如我所说,如果内环只有一个扇区 - 如上述文章和您的答案 - 这样做没有意义。在我的示例数据中,内环有 3 个扇区,外环有 1 个扇区用于bar,2 个扇区用于foobaz,均与它们的值成正比。
【解决方案2】:

这可能只是其中的一部分,它可能无法很好地扩展到更复杂的数据集。我对如何做到这一点非常好奇,并且有一个类似的更大的数据集,我正试图为工作进行可视化,所以这实际上也有助于我的工作:)

基本上我所做的是将数据集拆分为三个级别的数据框:父级别基本上是虚拟数据,级别 1 df 包含每个名称下所有类型的总和(我想我可以将您的数据过滤为 @ 987654323@--我的工作数据没有类似的列),以及所有外部节点的级别 2。将它们绑定在一起,制作一个堆叠条形图,给它极坐标。

我为工作做的那个有很多标签,而且它们很长,所以我用ggrepel::geom_text_repel 作为标签。它们很快变得笨重和丑陋。

显然这里的美学有一些不足之处,但我认为它可以根据你的喜好进行美化。

library(tidyverse)

df <- "name  type    value
foo   all     444
foo   type1   123
foo   type2   321
bar   all     111
bar   type3   111
baz   all     999
baz   type1   456
baz   type3   543" %>% read_table2() %>%
    filter(type != "all") %>%
    mutate(name = as.factor(name) %>% fct_reorder(value, sum)) %>%
    arrange(name, value) %>%
    mutate(type = as.factor(type) %>% fct_reorder2(name, value))

lvl0 <- tibble(name = "Parent", value = 0, level = 0, fill = NA)

lvl1 <- df %>%
    group_by(name) %>%
    summarise(value = sum(value)) %>%
    ungroup() %>%
    mutate(level = 1) %>%
    mutate(fill = name)

lvl2 <- df %>%
    select(name = type, value, fill = name) %>%
    mutate(level = 2)


bind_rows(lvl0, lvl1, lvl2) %>%
    mutate(name = as.factor(name) %>% fct_reorder2(fill, value)) %>%
    arrange(fill, name) %>%
    mutate(level = as.factor(level)) %>%
    ggplot(aes(x = level, y = value, fill = fill, alpha = level)) +
        geom_col(width = 1, color = "gray90", size = 0.25, position = position_stack()) +
        geom_text(aes(label = name), size = 2.5, position = position_stack(vjust = 0.5)) +
        coord_polar(theta = "y") +
        scale_alpha_manual(values = c("0" = 0, "1" = 1, "2" = 0.7), guide = F) +
        scale_x_discrete(breaks = NULL) +
        scale_y_continuous(breaks = NULL) +
        scale_fill_brewer(palette = "Dark2", na.translate = F) +
        labs(x = NULL, y = NULL) +
        theme_minimal()

reprex package (v0.2.0) 于 2018 年 4 月 24 日创建。

【讨论】:

  • 这真的很接近我正在寻找的东西,它适用于我的数据;谢谢你这么多麻烦:) 你的技术似乎只有在外带的颜色与内带的颜色匹配时才有效,但是我可能可以解决这个问题
  • 酷,我想你可以调整颜色的设置方式。我这样做是因为我通常会看到带有基于父组颜色的旭日形首饰,然后当您从中心向外走时会更亮或更不透明,但不需要只这样做。
  • 我一直在寻找的是分别为内圈和外圈使用不同的调色板。我不确定如何做到这一点,或者即使有可能。我的外环扇区可能非常小,因此标签会变得凌乱。通过使用 alpha 通道作为辅助调色板,我有点——不是——真的得到了解决方案;它不够独特,但足以给人留下深刻印象。
  • 我认为这是可能的。您可能只需要弄乱手动调色板。我很难以正确的顺序将这些因素拼凑在一起,以便让小组保持在一起,但要改变颜色
  • 同样!似乎每次您尝试使用辅助级别作为填充因子时,它都会重新排序所有内容并且不尊重任何后续的arrange 调用。
【解决方案3】:

可以使用 ggsunburst 来完成(正如 camille 建议的那样)。 ggsunburst 读取 newick 和 csv(或任何分隔符分隔的)文件。 您需要安装最新版本 0.0.9 才能使此示例正常运行

# first row with header is mandatory
# remove lines with type "all" from your data
# add colour as additional column
df <- read.table(header=T, text =
"parent node  size  colour
foo   type1   123 type1
foo   type2   321 type2
bar   type3   111 type3
baz   type1   456 type1
baz   type3   543 type3")

# write data.frame into csv file
write.table(df, file = 'df.csv', row.names = F, sep = ",")

# install ggsunburst 0.0.9
if (!require("ggplot2")) install.packages("ggplot2")
if (!require("rPython")) install.packages("rPython")
install.packages("http://genome.crg.es/~didac/ggsunburst/ggsunburst_0.0.9.tar.gz", repos=NULL, type="source")


library(ggsunburst)

sb <- sunburst_data('df.csv', type = "node_parent", sep = ',', node_attributes = 'colour')
sunburst(sb, rects.fill.aes = "colour", node_labels = T, node_labels.min = 25)

see your sunburst here

【讨论】:

    【解决方案4】:

    我一直在寻找一种方法来使用 ggplot 进行这种类型的绘图。 @camille 的回答真的很有帮助!我最终使用this answer here too 为这个问题创建了一个稍微修改的答案。

    已经快一年了,但也许其他人还在寻找这种答案!也许其他答案中提到的其他包更有用,但对于我们这些想要留在 ggplot 中的人来说,希望这能有所帮助。

    我认为我可以按照 OP 的要求去做(始终如一地为第二层着色),尽管我不确定这是最佳的方式。

    我没有使用geom_col,而是使用了geom_rect。这为我们提供了更大的灵活性,并且还可以更好地控制每个矩形的绘制位置(堆叠的条形图总是存在堆叠条形图的问题)。此外,奇怪的是,在极坐标 geom_col 中,最终绘制了从 0 到 x 的所有饼图。所以@camille 不得不使用填充的透明度来获得想要的结果。在geom_rect 中,我们可以设置xminxmax 以获得我们想要的确切形状。

    但我们需要做一些数据处理以使数据帧成形。

    另外,我试图制作的情节有一些第二层是空的。因此,我稍微更改了数据集,以包含一个额外的一级类,而没有二级类。

    这是我的解决方案:

    library(tidyverse)
    library(ggplot2)
    library(RColorBrewer)
    
    df <- "name  type    value
    foo   all     444
    foo   type1   123
    foo   type2   321
    bar   all     111
    bar   type3   111
    baz   all     999
    baz   type1   456
    baz   type3   543
    boz   -       222" %>% read_table2() %>% filter(type != 'all') %>% 
    mutate(type=ifelse(type=='-', NA, type)) %>% arrange(name, value)
    
    # here I create the columns xmin, xmax, ymin, ymax using cumsum function
    # (be VERY careful with ordering of rows!)
    
    # I also created a column 'colour' which I map to the asthetic 'colour' (colour of line of each rectangle)
    # it is a boolean saying if a line should or should not be drawn.
    # for empty second levels i want to draw an empty space (no fill and no line)
    
    # define a padding space between the levels of the pie chart 
    padding <- 0.05
    
    # create df for level 0
    lvl0 <- tibble(name = "Parent", value = 0, level = 0, fill = NA) %>%
      mutate(xmin=0, xmax=1, ymin=0, ymax=value) %>%
      mutate(x.avg=0, y.avg=0, colour=FALSE)
    
    print(lvl0)
    
    # create df for level 1
    lvl1 <- df %>%
      group_by(name) %>%
      summarise(value = sum(value)) %>%
      ungroup() %>%
      mutate(level = 1) %>%
      mutate(fill = name) %>%
      mutate(xmin=1+padding, xmax=2, ymin=0, ymax=cumsum(value)) %>%
      mutate(ymin=lag(ymax, default=0),
             x.avg=(xmin+xmax)/2,
             y.avg=(ymin+ymax)/2,
             colour=TRUE)
    
    print(lvl1)
    
    # create df for level 2
    lvl2 <- df %>%
      select(name = type, value, fill = name) %>%
      mutate(level = 2) %>%
      mutate(fill=paste0(fill, '_', name)) %>%
      mutate(xmin=2+padding, xmax=3, ymin=0, ymax=cumsum(value)) %>%
      mutate(ymin=lag(ymax, default=0),
             x.avg=(xmin+xmax)/2,
             y.avg=(ymin+ymax)/2,
             colour=ifelse(grepl('_NA', fill), FALSE, TRUE))
    
    print(lvl2)
    
    # this is my dirty workaround for defining the colours of levels 1 one 2 independently. Probably not the best way and 
    # maybe it will not scale very well... But for this small data set it seemed to work...
    
    # number of classes in each level (don't include NA)
    n.classes.1 <- 4
    n.classes.2 <- 3
    n.classes.total <- n.classes.1 + n.classes.2
    
    # get colour pallete for level 1
    col.lvl1 <- brewer.pal(n.classes.total,"Dark2")[1:n.classes.1]
    names(col.lvl1) <- as.character(unique(lvl1$name))
    
    # get colour pallete for level 2 (don't include NA)
    col.lvl2 <- brewer.pal(n.classes.total,"Dark2")[(n.classes.1+1):n.classes.total]
    names(col.lvl2) <- as.character(unique(lvl2$name)[!is.na(unique(lvl2$name))])
    
    # compile complete color pallete
    fill.pallete <- c(col.lvl1)
    
    for (l1 in as.character(unique(lvl1$name))) {
      for (l2 in as.character(unique(lvl2$name))) {
        if (!is.na(l2)) {
            name.type <- paste0(l1, '_', l2)
            aux <- col.lvl2[l2]
            names(aux) <- name.type
            fill.pallete <- c(fill.pallete, aux)        
        } else {
            # if level2 is NA, then assign transparent colour
            name.type <- paste0(l1, '_NA')
            aux <- NA
            names(aux) <- name.type
            fill.pallete <- c(fill.pallete, aux)        
        }
      }
    }
    print(fill.pallete)
    
    
    # put all data frames together for ggplot
    
    df.total <- bind_rows(lvl0, lvl1, lvl2) %>%
      mutate(name = as.factor(name) %>% fct_reorder2(fill, value)) %>%
      arrange(fill, name) %>%
      mutate(level = as.factor(level))
    
    print(df.total)
    
    # create plot (it helped me to look at the rectangular coordinates first before changing to polar!)
    
    g <- ggplot(data=df.total, aes(fill = fill)) +
      geom_rect(aes(ymax=ymax, ymin=ymin, xmax=xmax, xmin=xmin, colour=colour), size = 0.1) +
      scale_fill_manual(values = fill.pallete, , guide = F, na.translate = FALSE) +
      scale_color_manual(values = c('TRUE'='gray20', 'FALSE'='#FFFFFF00'), 
                         guide = F, na.translate = FALSE) +
      geom_text(aes(x = x.avg, y = y.avg, label = name), size = rel(2.5)) +
      scale_x_discrete(breaks = NULL) +
      scale_y_continuous(breaks = NULL) +
      labs(x = NULL, y = NULL) +
      theme_minimal() +
      theme(panel.grid=element_blank()) + 
      coord_polar(theta = "y", start = 0, direction = -1)
    
    print(g)
    

    This is the resulting plot.

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-05-07
      • 1970-01-01
      • 1970-01-01
      • 2013-04-11
      相关资源
      最近更新 更多